Compare commits

...
166 Commits
Author SHA1 Message Date
Ara Sadoyan 430f867792 Merge branch 'main' of github.com:sadoyan/aralez 2026-07-20 13:47:54 +02:00
Ara Sadoyan f6c092adc4 version fix 2026-07-20 13:47:05 +02:00
Ara Sadoyan 19ad8fdbf4 yaml update 2026-07-20 13:40:18 +02:00
Ara Sadoyan b55ff2d392 minor fix on memory response cache 2026-07-20 13:37:10 +02:00
Ara Sadoyan b73ccc527f Implementing in memory response cache 2026-07-20 12:25:27 +02:00
Ara Sadoyan c89fe48e44 Cargo.toml version update 2026-07-13 11:35:52 +02:00
Ara Sadoyan d21fd8cb4b config update 2026-07-13 11:32:36 +02:00
Ara Sadoyan 491442bb92 Added log file rotation and compression. Updated to Rust 1.97.0 2026-07-13 11:29:31 +02:00
Ara Sadoyan e07791e1f0 Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 13:34:47 +02:00
Ara Sadoyan 7b952b467c Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 13:34:08 +02:00
Ara Sadoyan 9a1e8fe149 Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 12:27:49 +02:00
Ara Sadoyan 874ce4fddb Implementing full async access log inspired by #45 2026-06-29 21:35:10 +02:00
Ara Sadoyan 99c7a811da Header parsing broken with rsplit_once in populate_headers_and_auth and build_headers
closed #48
2026-06-27 21:34:13 +02:00
Ara Sadoyan edadda803c version change 2026-06-27 14:20:06 +02:00
Ara Sadoyan c5d0fe60a0 resolved possible divide by zero issue from #46 2026-06-27 13:38:59 +02:00
Ara Sadoyan d7fe476b24 Merge pull request #47 from sadoyan/dev
Dev
2026-06-27 12:33:32 +02:00
Ara Sadoyan aa22749393 Merge branch 'main' into dev 2026-06-27 12:32:41 +02:00
Ara Sadoyan 4b6a558b4b Merge branch 'main' into dev 2026-06-27 12:31:49 +02:00
Ara Sadoyan 6c9c7acf65 Merge pull request #46 from Taqman-probe/fix/sticky-session-404
Fix: Resolve 404/405 errors with sticky session cookie handling in /api routes
2026-06-27 12:31:29 +02:00
Taqman-probe 2845dc5923 fix 404 error on multipath and cleanup fn: clone_idmap_into 2026-06-27 13:51:12 +09:00
Ara Sadoyan f2e07a8d70 resolved #44 2026-06-26 16:32:58 +02:00
Ara Sadoyan 5815da6576 Merge pull request #43 from sadoyan/dev
Update main.yaml
2026-06-22 13:27:17 +02:00
Ara Sadoyan 1ab0916026 Merge branch 'dev' 2026-06-22 13:24:42 +02:00
Ara Sadoyan 707f725b88 Update main.yaml 2026-06-22 13:22:45 +02:00
Ara Sadoyan eb4e73ece0 Basic access/error logging. Upgrade to Pingra 8.0.1 2026-06-22 13:20:56 +02:00
Ara Sadoyan 53e7dcfd33 Pid file follow symlink problem. 2026-06-19 11:14:40 +02:00
Ara Sadoyan f3b346a28d Merge pull request #42 from sadoyan/dev
migrate from serde_yml to noyalib
2026-06-12 16:05:30 +02:00
Ara Sadoyan c011800e1e migrate from serde_yml to noyalib 2026-06-12 16:03:49 +02:00
Ara Sadoyan 265ff6b774 cleanup 2026-06-12 13:33:21 +02:00
Ara Sadoyan 1c3d9a263f example configs 2026-06-12 13:12:39 +02:00
Ara Sadoyan 162c5060c9 cleanup, minor fix 2026-06-10 11:46:30 +02:00
Ara Sadoyan 132cf45dfe cleanup, minor fix 2026-06-10 11:29:35 +02:00
Ara Sadoyan 77dcafbb4e tcp_keepalive options for kernel timer 2026-06-09 17:53:04 +02:00
Ara Sadoyan 649bd979f7 socker address #41 2026-06-07 12:07:06 +02:00
Ara Sadoyan bd315106b9 Merge pull request #41 from Taqman-probe/fix/TcpListener-bind
Fix TcpListener binding and port availability check
2026-06-07 11:57:06 +02:00
Ara Sadoyan 3ba2ed33ae cargo.toml 2026-06-07 11:38:50 +02:00
Taqman-probe c09efab9fd prevent panic when parsing invalid address without port 2026-06-07 00:47:43 +09:00
Taqman-probe 735d605f6d prevent panic when parsing invalid address without port 2026-06-06 22:32:32 +09:00
Ara Sadoyan 6773d0f502 Cargo 2026-06-04 19:12:50 +02:00
Ara Sadoyan a8cb727da5 Cargo Licence 2026-06-04 18:39:02 +02:00
Ara Sadoyan f81194aee7 Cargo Licence 2026-06-04 18:38:47 +02:00
Ara Sadoyan 0f09a2e02b Roll back to MiMalloc 2026-06-04 18:14:22 +02:00
Ara Sadoyan 27aca0a3a5 Roll back to MiMalloc 2026-06-04 17:03:19 +02:00
Ara Sadoyan 3c99ed0c44 Fixed #40 2026-06-03 16:37:56 +02:00
Ara Sadoyan 4a6e1d817b OS signals , PID file 2026-06-02 19:56:08 +02:00
Ara Sadoyan f6a2df44d0 README 2026-06-02 12:20:18 +02:00
Ara Sadoyan e8d976f5f1 A minor issue with file loader 2026-06-01 15:35:49 +02:00
Ara Sadoyan a1341e90f7 Merge branch 'Taqman-probe-fix/mismatch-file-watch' into dev 2026-06-01 14:39:44 +02:00
Ara Sadoyan ddcacc509c Merge branch 'fix/mismatch-file-watch' of github.com:Taqman-probe/aralez into Taqman-probe-fix/mismatch-file-watch 2026-06-01 14:39:06 +02:00
Ara Sadoyan 2b385cc630 gitignore 2026-06-01 14:29:50 +02:00
Ara Sadoyan 1b5223986d README, signals 2026-06-01 14:25:36 +02:00
Ara Sadoyan ac2bd8113d README update 2026-06-01 11:48:54 +02:00
Ara Sadoyan 4d9a2ecfe3 Honoring OS signals 2026-05-30 18:53:36 +02:00
Taqman-probe aa70d818b2 Fix config file change detection logic in file watcher 2026-05-30 00:34:35 +09:00
Ara Sadoyan 15d356f087 Removed Dockerfile, put content to README 2026-05-27 19:23:32 +02:00
Ara Sadoyan b17962e6d5 Merge pull request #31 from Taqman-probe/fix/rate-limit-setting-message
fix: Fix global rate limit and 4xx limit fallback in upstream config log
2026-05-27 16:28:33 +02:00
Ara Sadoyan ea779182f2 Include request path in rate limit and 4xx limit warning logs
#32
2026-05-27 16:15:55 +02:00
Ara Sadoyan 9339a142a6 Merge pull request #33 from Taqman-probe/fix/skip-tls-detection-when-hc-disabled
Skip TLS detection when healthcheck: false
2026-05-27 15:09:12 +02:00
Ara Sadoyan cfc9bd319d Merge branch 'dev' 2026-05-27 14:58:56 +02:00
Ara Sadoyan a5ced59e5c Merge branch 'main' of github.com:sadoyan/aralez 2026-05-27 14:58:33 +02:00
Ara Sadoyan c3df7bc131 gitignore 2026-05-27 14:57:34 +02:00
Ara Sadoyan 0248d73836 Merge pull request #24 from gzsombor/forwarded-for-header
X-Forwarded-For should only contain the IP address
2026-05-27 14:55:51 +02:00
Ara Sadoyan 96a5aef7d0 warning log on retry parce 2026-05-27 14:54:21 +02:00
Ara Sadoyan 5c3b72b7a3 parceyaml 2026-05-27 14:26:05 +02:00
Ara Sadoyan 3cf0fc493f Merge pull request #34 from Taqman-probe/fix/failed-hot-reload-config
Add retry mechanism for configuration parsing failures
2026-05-27 14:18:28 +02:00
Ara Sadoyan 207ee481fb update 2026-05-27 14:12:10 +02:00
Ara Sadoyan 1c2f7327aa gitignore 2026-05-27 14:03:55 +02:00
Ara Sadoyan 310a554a25 gitignore 2026-05-27 14:03:22 +02:00
Ara Sadoyan 69a5167346 Merge pull request #35 from sadoyan/dev
Dev
2026-05-27 13:56:10 +02:00
Ara Sadoyan 4734ccab2f Minor fixed #26 & #28 2026-05-27 13:40:22 +02:00
Taqman-probe 61d65f6e4e Failed hot reloading of config file 2026-05-27 18:36:00 +09:00
Taqman-probe 20ac39067d Skip TLS detection when healthcheck: false 2026-05-27 16:44:17 +09:00
Taqman-probe 7afa76de8f fix: Fix global rate limit fallback in upstream config log 2026-05-27 14:22:59 +09:00
Ara Sadoyan e29161965f Added more monitoring metrics 2026-05-26 19:34:10 +02:00
Zsombor Gegesy 9216710dda X-Forwarded-For should only contain the IP address
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For
2026-05-23 07:11:55 +02:00
Ara Sadoyan faf840d102 Merge pull request #23 from sadoyan/dev
New features, 4xx counter
2026-05-22 16:57:07 +02:00
Ara Sadoyan d74883e16e New features, 4xx counter 2026-05-22 16:56:33 +02:00
Ara Sadoyan d95bbfcd1a Merge pull request #22 from sadoyan/dev
- Changed sticky session value from bool to u32, corresponding to `Max-Age=` of cookie
- Added counter and rate limiter for 4xx requests 
- Changed config for JWT authorization. The `data`  property is removed.
2026-05-22 16:52:27 +02:00
Ara Sadoyan d301f7225f New features, 4xx counter 2026-05-22 16:47:40 +02:00
Ara Sadoyan df02e523e4 cleanups 2026-05-21 18:34:46 +02:00
Ara Sadoyan 2f5def5c3c Changed sticky session from bool to Option<u64> 2026-05-20 21:09:23 +02:00
Ara Sadoyan 1727a2b5e7 deleted some comments 2026-05-20 17:17:04 +02:00
Ara Sadoyan 4bbedee27b JWT auth read and caches KEY from system env. 2026-05-19 15:26:05 +02:00
Ara Sadoyan 37ef118861 Dockerfile #21 2026-05-19 11:48:12 +02:00
Ara Sadoyan 00062b00da Removed authentication from API server, JWT master key as environment variable 2026-05-18 20:38:30 +02:00
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
Ara Sadoyan a0b6de9759 updated . config example 2026-04-30 18:13:37 +02:00
Ara Sadoyan a70eb53bc1 Let's Encrypt auto certificate HTTP-01 challenge #16 2026-04-30 18:04:25 +02:00
Ara Sadoyan bee307793c restructurisation grades 2026-04-27 15:28:54 +02:00
Ara Sadoyan 6e83775127 restructurisation 2026-04-27 15:22:31 +02:00
Ara Sadoyan baded40e6e Cache for JWT tokens, to minimize crypto. BRAKING: Claims key "valid" renamed to "exp" 2026-04-17 17:53:31 +02:00
Ara Sadoyan c0a419f6f7 completed implementation of #17 2026-04-15 18:23:57 +02:00
Ara Sadoyan 8aff2fa875 Standardizing implementation of #17 2026-04-14 16:11:24 +02:00
Ara Sadoyan 9b4ee26a2b Working on #17 2026-04-13 20:06:57 +02:00
Ara Sadoyan f135106a44 Changes in authentication 2026-04-08 19:05:19 +02:00
Ara Sadoyan 389c12119a code cleanup and improvements. 2026-04-08 17:00:06 +02:00
Ara Sadoyan 93a8661281 Cargo cleanup, dependency merge 2026-04-08 15:14:46 +02:00
Ara Sadoyan 0505ce2849 split upstreams.yaml file 2026-03-30 19:04:32 +02:00
Ara Sadoyan 72ed870538 split upstreams.yaml file 2026-03-27 19:24:30 +01:00
Ara Sadoyan 68140d0cf0 tye changes, optimization 2026-03-26 17:40:22 +01:00
Ara Sadoyan 7b9b206c13 optimization & cleanup 2026-03-26 16:58:53 +01:00
Ara Sadoyan 4706b281bc cleanup 2026-03-26 14:17:59 +01:00
Ara Sadoyan 1f8efc6af7 FUNDING.yml 2026-03-25 15:16:47 +01:00
Ara Sadoyan 9f595b2709 example config file update 2026-03-25 11:15:55 +01:00
Ara Sadoyan ed44516015 added redirect_to directive for upstreams 2026-03-24 16:08:14 +01:00
Ara Sadoyan 17da7862e3 upstreams ID hashing update 2026-03-18 20:06:50 +01:00
Ara Sadoyan 24d00da855 performance improvement, sticky session minor bug fix 2026-03-17 19:21:05 +01:00
Ara Sadoyan c9422759aa Minor performance improvement 2026-03-17 13:54:42 +01:00
Ara Sadoyan 94b1f77734 Type changes, auth override policy 2026-03-04 12:35:45 +01:00
Ara Sadoyan 9d986f9a28 Path level authentication 2026-03-03 19:35:16 +01:00
Ara Sadoyan 3afa2f209f pingora 0.8.0 upgrade 2026-03-03 13:54:53 +01:00
Ara Sadoyan c151fdf58b moving to boringssl 2026-02-19 18:11:54 +01:00
Ara Sadoyan 438426153f removed unwrap 2026-02-18 12:00:33 +01:00
Ara Sadoyan 9bb01fd1b0 minor improvements 2026-02-17 18:22:46 +01:00
Ara Sadoyan abb5fef1d6 minor improvements 2026-02-17 17:03:52 +01:00
Ara Sadoyan 3618687ad5 Memory allocation improvements for proxyhttp, fix issue with sticky session . 2026-02-10 19:07:43 +01:00
Ara Sadoyan a893b3c301 Memory allocation improvements for metrics collector . 2026-02-05 13:57:39 +01:00
Ara Sadoyan 3ff262c7f4 Merge pull request #13 from yerke/patch-1
Fix grammar and formatting in README.md
2026-02-04 14:41:50 +01:00
Yerkebulan Tulibergenov 062f02259f Fix grammar and formatting in README.md 2026-01-30 23:59:10 -08:00
Ara Sadoyan 1a4c9b7d55 Performance optimization in headers 2026-01-28 16:07:45 +01:00
Ara Sadoyan 6ef7f23823 Performance optimization v2 2026-01-28 13:20:31 +01:00
Ara Sadoyan 2b437c65fb Performance improvement. String removal from hot paths. 2026-01-27 16:19:51 +01:00
Ara Sadoyan 38055ae94e added new metric aralez_requests_by_upstream 2026-01-25 18:08:15 +01:00
Ara Sadoyan 703de9e909 updates on API server https://sadoyan.github.io/aralez-docs/assets/api/ 2026-01-22 16:50:51 +01:00
Ara Sadoyan 2c8b01295c Minor subfunction removal 2026-01-21 20:01:16 +01:00
Ara Sadoyan baebe1c00f Async apply of config via API 2026-01-20 19:16:27 +01:00
Ara Sadoyan 6c1d3c5ef8 Error handling on API server 2026-01-09 18:44:36 +01:00
Ara Sadoyan 2d1a827007 Removed unneeded loop 2025-12-14 12:09:11 +01:00
Ara Sadoyan a2a5250711 Performance improvements on data types . 2025-12-11 15:21:34 +01:00
Ara Sadoyan 985e923342 to https redirect bug fix 2025-12-11 13:37:40 +01:00
Ara Sadoyan 0fc79c022f perf: optimize header handling and concurrent access patterns 2025-12-10 19:09:04 +01:00
Ara Sadoyan a43bccdfb8 minor, performance improvements 2025-11-28 13:13:15 +01:00
Ara Sadoyan 5b87391fbb some more type changes, performance improvements 2025-11-27 18:47:04 +01:00
Ara Sadoyan c68a4ad83d Type changes, performance improvements 2025-11-27 18:03:34 +01:00
Ara Sadoyan 8ba8d32df1 Performance improvements, type changes 2025-11-26 12:12:41 +01:00
Ara Sadoyan 7a839065e6 update on kubernetes web client 2025-11-24 17:57:44 +01:00
Ara Sadoyan 74821654f3 Added support to send custom headers to upstream servers. 2025-11-22 23:18:06 +01:00
Ara Sadoyan 78c83b802f Merge Consul & Kubernetes discovery 2025-10-26 15:26:09 +01:00
Ara Sadoyan 012505b77e Cleaning up the code 2025-10-24 15:27:15 +02:00
Ara Sadoyan 21c4cb0901 Update README.md 2025-10-18 11:49:51 +02:00
Ara Sadoyan 86dd3d3402 README update 2025-10-18 11:48:48 +02:00
Ara Sadoyan d6b345202b README update 2025-10-17 17:03:45 +02:00
Ara Sadoyan 5209d787e4 README update 2025-10-17 16:44:57 +02:00
43 changed files with 4793 additions and 2767 deletions
+13
View File
@@ -0,0 +1,13 @@
[target.aarch64-unknown-linux-musl]
rustflags = [
"-C", "link-arg=-Wl,--defsym=fopen64=fopen",
"-C", "link-arg=-Wl,--defsym=fseeko64=fseeko",
"-C", "link-arg=-Wl,--defsym=ftello64=ftello"
]
[target.x86_64-unknown-linux-musl]
rustflags = [
"-C", "link-arg=-Wl,--defsym=fopen64=fopen",
"-C", "link-arg=-Wl,--defsym=fseeko64=fseeko",
"-C", "link-arg=-Wl,--defsym=ftello64=ftello"
]
+2 -2
View File
@@ -1,6 +1,6 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: sadoyan
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
@@ -10,6 +10,6 @@ liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: sadoyan
buy_me_a_coffee: sadoyan
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+5 -2
View File
@@ -5,12 +5,13 @@
*.dll
*.exe
*.sh
/docs/
*.yaml
/docs
assets/
assets
/target/
*.iml
.idea/
.etc/
*.ipr
*.iws
/out/
@@ -21,3 +22,5 @@ crashlytics.properties
crashlytics-build.properties
/target
/z_shpo
/configs
Makefile
Generated
+1968 -1166
View File
File diff suppressed because it is too large Load Diff
+38 -36
View File
@@ -1,7 +1,11 @@
[package]
name = "aralez"
version = "0.9.2"
version = "0.92.15"
edition = "2021"
license = "Apache-2.0"
description = "Reverse proxy built on top of Cloudflare's Pingora"
exclude = ["configs/*"]
repository = "https://github.com/sadoyan/aralez"
[profile.release]
opt-level = 3
@@ -11,45 +15,43 @@ panic = "abort"
strip = true
[dependencies]
tokio = { version = "1.45.1", features = ["full"] }
pingora = { version = "0.6.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl
serde = { version = "1.0.219", features = ["derive"] }
tokio = { version = "1.52.3", features = ["full"] }
pingora = { version = "0.8.1", features = ["lb", "openssl"] } # openssl, rustls, boringssl
pingora-core = "0.8.1"
pingora-proxy = "0.8.1"
pingora-http = "0.8.1"
pingora-limits = "0.8.1"
pingora-cache = "0.8.1"
dashmap = "7.0.0-rc2"
pingora-core = "0.6.0"
pingora-proxy = "0.6.0"
pingora-http = "0.6.0"
pingora-limits = "0.6.0"
async-trait = "0.1.89"
env_logger = "0.11.8"
log = "0.4.28"
futures = "0.3.31"
notify = "8.2.0"
axum = { version = "0.8.4" }
axum-server = { version = "0.7.2", features = ["tls-openssl"] }
reqwest = { version = "0.12.23", features = ["json", "native-tls-alpn"] }
#reqwest = { version = "0.12.15", features = ["json", "rustls-tls"] }
#reqwest = { version = "0.12.15", default-features = false, features = ["rustls-tls", "json"] }
serde_yaml = "0.9.34-deprecated"
rand = "0.9.2"
log = "0.4.33"
notify = "9.0.0-rc.4"
axum = { version = "0.8.9" }
reqwest = { version = "0.13.4", features = ["json", "stream", "blocking"] }
serde = { version = "1.0.228", features = ["derive"] }
noyalib = { version = "0.0.15", features = ["compat-serde-yaml"] }
rand = "0.10.2"
base64 = "0.22.1"
jsonwebtoken = "9.3.1"
tonic = "0.14.2"
sha2 = { version = "0.11.0-rc.2", default-features = false }
base16ct = { version = "0.3.0", features = ["alloc"] }
jsonwebtoken = { version = "10.4.0", default-features = false, features = ["use_pem", "rust_crypto"] }
tonic = "0.14.6"
sha2 = { version = "0.11.0-rc.5", default-features = false }
base16ct = { version = "1.0.0", features = ["alloc"] }
urlencoding = "2.1.3"
arc-swap = "1.7.1"
mimalloc = { version = "0.1.48", default-features = false }
arc-swap = "1.9.2"
prometheus = "0.14.0"
lazy_static = "1.5.0"
x509-parser = "0.18.0"
x509-parser = "0.18.1"
rustls-pemfile = "2.2.0"
tower-http = { version = "0.6.6", features = ["fs"] }
once_cell = "1.21.3"
tower-http = { version = "0.7.0", features = ["fs"] }
privdrop = "0.5.6"
ctrlc = "3.5.0"
port_check = "0.3.0"
#moka = { version = "0.12.10", features = ["sync"] }
#rustls = { version = "0.23.27", features = ["ring"] }
#hickory-client = { version = "0.25.2" }
#openssl = "0.10.73"
serde_json = "1.0.150"
subtle = "2.6.1"
moka = { version = "0.12.15", features = ["sync"] }
ahash = "0.8.12"
instant-acme = "0.8.5"
rcgen = "0.14.8"
log4rs = { version = "1.4.0", features = ["rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller", "gzip"] }
mimalloc = { version = "0.1.52", default-features = false }
signal-hook = "0.4.4"
sd-notify = "0.5.0"
libc = "0.2.186"
+240 -186
View File
@@ -2,204 +2,215 @@
---
# Aralez (Արալեզ),
# Aralez (Արալեզ)
### **Reverse proxy and service mesh built on top of Cloudflare's Pingora**
### **Reverse proxy built on top of Cloudflare's Pingora**
Aralez is a high-performance Rust reverse proxy with zero-configuration automatic protocol handling, TLS, and upstream management,
featuring Consul and Kubernetes integration for dynamic pod discovery and health-checked routing, acting as a lightweight ingress-style proxy.
---
What Aralez means ?
**Aralez = Արալեզ** <ins>.Named after the legendary Armenian guardian spirit, winged dog-like creature, that descend upon fallen heroes to lick their wounds and resurrect them.</ins>.
**Aralez = Արալեզ** <ins>Named after the legendary Armenian guardian spirit, winged dog-like creature, that descend upon fallen heroes to lick their wounds and resurrect them</ins>.
Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers world-class performance, security and scalability — right out of the box.
[![Buy Me A Coffee](https://img.shields.io/badge/☕-Buy%20me%20a%20coffee-orange)](https://www.buymeacoffee.com/sadoyan)
---
## 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
- [**Crates**](https://crates.io/crates/aralez) : The Rust crate registry
- [**DockerHUB**](https://hub.docker.com/r/sadoyan/aralez) : DockerHUB official repository
- [**GitHUB Packages**](https://github.com/sadoyan/aralez/pkgs/container/aralez) : GitHUB ghcr.io images
---
## 🔧 Key Features
## Key Features
- **Dynamic Config Reloads** — Upstreams can be updated live via API, no restart required.
- **TLS Termination** — Built-in OpenSSL support.
- **Automatic load of certificates** — Automatically reads and loads certificates from a folder, without a restart.
- **Autoload of certificates** — Automatically loads new/changed certificates from a folder, without a restart.
- **Let’s 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.
- **Built in rate limiter** — Limit requests to server, by setting up upper limit for requests per seconds, per virtualhost.
- **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.
- **Built in rate limiter** — Globar or route limit requests to upstreams.
- **Authentication** — Supports Basic Auth, API tokens, and JWT verification.
- **Basic Auth**
- **API Key** via `x-api-key` header
- **JWT Auth**, with tokens issued by Aralez itself via `/jwt` API
- ⬇️ See below for examples and implementation details.
- **Load Balancing Strategies**
- Round-robin
- Failover with health checks
- Sticky sessions via cookies
- **Unified Port** — Serve HTTP and WebSocket traffic over the same connection.
- **Forward Auth**, Sends requests to an authentication server.
- **Load Balancing** Round-robin, health checks, optional sticky sessions.
- **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.
- **High Performance** — Built with [Pingora](https://github.com/cloudflare/pingora) and tokio for async I/O.
## 🌍 Highlights
- ⚙️ **Upstream Providers:**
- **Upstream Providers:**
- `file` Upstreams are declared in config file.
- `consul` Upstreams are dynamically updated from Hashicorp Consul.
- 🔁 **Hot Reloading:** Modify upstreams on the fly via `upstreams.yaml` — no restart needed.
- 🔮 **Automatic WebSocket Support:** Zero config — connection upgrades are handled seamlessly.
- 🔮 **Automatic GRPC Support:** Zero config, Requires `ssl` to proxy, gRPC handled seamlessly.
- 🔮 **Upstreams Session Stickiness:** Enable/Disable Sticky sessions globally.
- 🔐 **TLS Termination:** Fully supports TLS for upstreams and downstreams.
- 🛡️ **Built-in Authentication** Basic Auth, JWT, API key.
- 🧠 **Header Injection:** Global and per-route header configuration.
- 🧪 **Health Checks:** Pluggable health check methods for upstreams.
- 🛰️ **Remote Config Push:** Lightweight HTTP API to update configs from CI/CD or other systems.
- `kubernetes` Upstreams are dynamically updated from kubernetes api server.
- **Auto WebSocket Support:** WS connection upgrades are handled automatically.
- **Auto gRPC Support:** gRPC detected and handled automatically.
- **Header Injection:** Global and per-route server/client headers injection.
- **Remote Config Push:** Lightweight HTTP API to update configs from CI/CD or other systems.
- **Memory Safe** — 100% Rust.
- **High Performance** — Built with [Pingora](https://github.com/cloudflare/pingora) and tokio for async I/O.
---
## 📁 File Structure
## Configuration Overview
```
.
├── main.yaml # Main configuration loaded at startup
├── upstreams.yaml # Watched config with upstream mappings
├── etc/
│ ├── server.crt # TLS certificate (required if using TLS)
│ └── key.pem # TLS private key
```
### `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 launch as root) |
| **rungroup** | aralez | Optional. Group for running aralez after dropping root privileges (requires 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 |
| **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. `high` matches Qualys SSL Labs A+ (defaults to `medium`) |
| **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/aralez/ | Direcotry containing configuration files, must be writeable by user `aralez` |
| **upstreams_conf** | /etc/aralez/upstreams.yaml | Location of the upstreams file |
| **access_log** | access | Configure access logging. Values: `access, error` |
| **log_level** | info | Log level: `info`, `warn`, `error`, `debug`, `trace`, `off` |
| **log_file** | /tmp/aralez.log, 20, 5, compress | Optional, the location of log file, file size in megabytes, number of log files to keep, compress files .valueas are separated by comma. Defaults to None, 100, 5, No |
| **hc_method** | HEAD | Healthcheck method: HEAD, GET, POST (UPPERCASE) |
| **hc_interval** | 2 | Interval for health checks in seconds |
| **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 |
| **config_api_enabled** | true | Enable/disable remote config push capability |
| **cache_size_mb** | 50 | Optional. Enable internal cache with size in megabytes of value. Cache is entirely disabled when this line is commented out. |
| **cache_ttl** | 10 | Optional. Set Default TTL in seconds for cached items, works if cache control headers are not set via upstream. Defaults to 60 if cache_size_mb is set. |
---
## 🛠 Configuration Overview
### 🔧 `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
## Installation
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.
File names:
| File Name | Description |
|---------------------------|--------------------------------------------------------------------------|
| `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-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency |
| `aralez-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies |
| `sadoyan/aralez` | Docker image on Debian 13 slim (https://hub.docker.com/r/sadoyan/aralez) |
| File Name | Description |
|---------------------------------|----------------------------------------------------------------------------|
| `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-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-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency |
| `aralez-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies |
| `sadoyan/aralez` | Docker image on Debian 13 slim (<https://hub.docker.com/r/sadoyan/aralez>) |
## About binaries
**glibc** builds are in general faster, but have few, basic, Glibc dependencies:
**musl** builds are 100% portable, static compiled binaries and have zero system dependencies.
In general musl builds have a little less performance.
The most intensive tests shows 107k-110k requests per second on **Glibc** binaries against 97k-100k **Musl** ones.
For running **Aralez** on very old hardware, CPUs prior Haswell, (launched before 2013) use `aralez-x86_64-compat-*.gz`
For getting the best performance on newer hardware use `aralez-x86_64-*.gz`.
**Via docker**
```shell
docker run -d \
-v /local/path/to/config:/etc/aralez:ro \
-p 80:80 \
-p 443:443 \
sadoyan/aralez
docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 sadoyan/aralez
docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 sadoyan/aralez:compat
docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 ghcr.io/sadoyan/aralez:latest
docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 ghcr.io/sadoyan/aralez:compat
```
## 💡 Note
**Dockerfile :**
In general **glibc** builds are working faster, but have few, basic, system dependencies for example :
```dockerfile
FROM debian:trixie-slim
```
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)
RUN apt-get update && apt-get install -y ca-certificates curl net-tools iputils-ping
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
COPY aralez /usr/local/bin/aralez
RUN chmod +x /usr/local/bin/aralez
RUN mkdir -p /etc/aralez/certs/upstreams
WORKDIR /etc/aralez
ENTRYPOINT ["/usr/local/bin/aralez", "-c", "/etc/aralez/main.yaml"]
```
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
## Running the Proxy
```bash
./aralez -c path/to/main.yaml
```
## 🔌 Systemd integration
## Systemd integration
Assuming Aralez in installed in `/opt/aralez` folder
```bash
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]
Type=forking
PIDFile=/run/aralez.pid
ExecStart=/bin/aralez -d -c /etc/aralez.conf
ExecReload=kill -QUIT $MAINPID
ExecReload=/bin/aralez -u -d -c /etc/aralez.conf
WorkingDirectory = /opt/aralez/
ExecReload=/bin/kill -HUP
ExecStart=/opt/aralez/aralez -c /opt/aralez/proxyconfigs/main.yaml
KillMode=process
KillSignal=SIGINT
LimitNOFILE=infinity
LimitNPROC=infinity
Restart=on-failure
RestartSec=2
StartLimitBurst=3
StartLimitIntervalSec=10
TasksMax=infinity
[Install]
WantedBy=multi-user.target
EOF
```
```bash
systemctl daemon-reload
systemctl enable aralez.service.
systemctl restart aralez.service.
```
## 💡 Example
A sample `upstreams.yaml` entry:
## Example upstreams config
```yaml
provider: "file"
sticky_sessions: false
sticky_sessions: 8600
to_https: false
rate_limit: 10
headers:
rate_limit: 20
x4xx_limit: 20
server_headers:
- "X-Forwarded-Proto:https"
- "X-Forwarded-Port:443"
client_headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
authorization:
type: "jwt"
creds: "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
myhost.mydomain.com:
paths:
"/":
rate_limit: 20
rate_limit: 10
x4xx_limit: 10
to_https: false
headers:
server_headers:
- "X-Something-Else:Foobar"
- "X-Another-Header:Hohohohoho"
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Hopaaaaaaaaaaaar"
servers:
@@ -207,7 +218,10 @@ myhost.mydomain.com:
- "127.0.0.2:8000"
"/foo":
to_https: true
headers:
authorization:
type: "jwt"
data: "266463d1-210a-4787-9a81-4aacb37a8723"
client_headers:
- "X-Another-Header:Hohohohoho"
servers:
- "127.0.0.4:8443"
@@ -216,19 +230,32 @@ myhost.mydomain.com:
healthcheck: false
servers:
- "127.0.0.1:8001"
DEFAULT:
paths:
"/":
servers:
- "127.0.0.1:3000"
```
**This means:**
- Sticky sessions are disabled globally. This setting applies to all upstreams. If enabled all requests will be 301 redirected to HTTPS.
- Sticky sessions are enabled globally. This setting applies to all upstreams. If enabled the value will be set for `Max-Age=` cookie.
- HTTP to HTTPS redirect disabled globally, but can be overridden by `to_https` setting per upstream.
- Requests to each hosted domains will be limited to 10 requests per second per virtualhost.
- All upstreams will receive custom headers : `X-Forwarded-Proto:https` and `X-Forwarded-Port:443`
- Additionally, myhost.mydomain.com with path `/` will receive custom headers : `X-Another-Header:Hohohohoho` and `X-Something-Else:Foobar`
- Requests with response 4xx to each hosted domains will be limited to 20 requests per second per virtualhost.
- Requests limits are calculated per requester ip plus requested virtualhost.
- If the requester exceeds the limit it will receive `429 Too Many Requests` error.
- Optional. Rate limiter will be disabled if the parameter is entirely removed from config.
- Requests to each hosted domains will be limited to 20 requests per second per virtualhost.
- Requests limits are calculated per requester ip plus requested virtualhost.
- If the requester exceeds the limit it will receive `429 Too Many Requests` error.
- Optional. Rate limiter will be disabled if the parameter is entirely removed from config.
- Requests to `myhost.mydomain.com/` will be limited to 20 requests per second.
- Requests with 4xx responses to `myhost.mydomain.com/` will be limited to 10 requests per second.
- 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.
- `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/.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.
@@ -237,30 +264,40 @@ myhost.mydomain.com:
- Global headers (CORS for this case) will be injected to all upstreams.
- Additional headers will be injected into the request for `myhost.mydomain.com`.
- You can choose any path, deep nested paths are supported, the best match chosen.
- 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`
- Second is the secret key for validating `jwt` tokens
- `DEFAULT` catch up everything else and proxy to `127.0.0.1:3000`
- This is a special upstream and in order to do the catch-up jub it must be **DEFAULT** all capitals
---
## 🔄 Hot Reload
## Hot Reload
- Changes to `upstreams.yaml` are applied immediately.
- No need to restart the proxy — just save the file.
- If `consul` provider is chosen, upstreams will be periodically update from Consul's API.
- Changes to `upstreams.yaml` are applied immediately on save without restart .
- If `consul` or `kubernetes` provider is chosen, upstreams will be periodically update from 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`
2. Provide `tls_certificate` and `tls_key_file`
- Set `proxy_address_tls` in `main.yaml`
- Provide at least one `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
```shell
mkdir -p /etc/aralez/certificates
chown -R aralez:aralez /etc/aralez
cd /etc/aralez/certificates
openssl req -x509 -newkey rsa:4096 \
-keyout dummy.key -out dummy.crt -sha256 -days 3650 -nodes \
-subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"
```
---
## 📡 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.
URL parameter. `key=MASTERKEY` is required. `MASTERKEY` is the value of `master_key` in the `main.yaml`
@@ -271,7 +308,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.
- Only one method can be active at a time.
@@ -325,13 +362,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)
---
## 🧠 Notes
## Notes
- Uses Pingora under the hood for efficiency and flexibility.
- Designed for edge proxying, internal routing, or hybrid cloud scenarios.
@@ -340,24 +377,42 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
- Sticky session support.
- HTTP2 ready.
📊 Why Choose Aralez? – Feature Comparison
### Summary Table: Feature Comparison
| Feature | **Aralez** | **Nginx** | **HAProxy** | **Traefik** |
|----------------------------|----------------------------------------------------------------------|--------------------------|-------------------------|-----------------|
| **Hot Reload** | ✅ Yes (live, API/file) | ⚠️ Reloads config | ⚠️ Reloads config | ✅ Yes (dynamic) |
| **JWT Auth** | ✅ Built-in | ❌ External scripts | ❌ External Lua or agent | ⚠️ With plugins |
| **WebSocket Support** | ✅ Automatic | ⚠️ Manual config | ✅ Yes | ✅ Yes |
| **gRPC Support** | ✅ Automatic (no config) | ⚠️ Manual + HTTP/2 + TLS | ⚠️ Complex setup | ✅ Native |
| **TLS Termination** | ✅ Built-in (OpenSSL) | ✅ Yes | ✅ Yes | ✅ Yes |
| **TLS Upstream Detection** | ✅ Automatic | ❌ | ❌ | ❌ |
| **HTTP/2 Support** | ✅ Automatic | ⚠️ Requires extra config | ⚠️ Requires build flags | ✅ Native |
| **Sticky Sessions** | ✅ Cookie-based | ⚠️ In plus version only | ✅ | ✅ |
| **Prometheus Metrics** | ✅ [Built in](https://github.com/sadoyan/aralez/blob/main/METRICS.md) | ⚠️ With Lua or exporter | ⚠️ With external script | ✅ Native |
| **Built With** | 🦀 Rust | C | C | Go |
| Feature / Proxy | **Aralez** | **Nginx** | **HAProxy** | **Traefik** | **Caddy** | **Envoy** |
|--------------------|:----------:|:-----------:|:-----------:|:-----------:|:----------:|:---------:|
| **Reload** | ✅ Hot | ⚙️ Manual | ⚙️ Manual | ✅ Hot | ✅ Hot | ✅ Hot |
| **Cert load** | ✅ Hot | ❌ Reload | ❌ Reload | ✅ Yes | ✅ Yes | ⚙️ No ? |
| **Authentication** | ✅ Yes | ⚙️ Limited | ⚙️ Limited | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP2** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **TLS Grades** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ⚙️ Manual |
| **gRPC** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual |
| **SSL Proxy** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP/2** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **WebSocket** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Sticky Session** | ✅ Yes | ❌ No | ⚙️ Yes | ✅ Yes | ⚙️ Limited | ✅ Manual |
| **Prometheus** | ✅ Yes | ⚙️ External | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Consul** | ✅ Yes | ❌ No | ⚙️DNS API | ✅ Yes | ❌ No | ✅ Yes |
| **Kubernetes** | ✅ Yes | ⚙️ Ingress | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes |
| **Limiter** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Static Files** | ✅ Yes | ✅ Yes | ⚙️ Lua ? | ✅ Yes | ✅ Yes | ❌ No |
| **Health Checks** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Built With** | Rust | C | C | Go | Go | C++ |
## 💡 Simple benchmark by [Oha](https://github.com/hatoo/oha)
---
⚠️ These benchmarks use :
✅ **Auto** – Automatically detected and loaded
✅ **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
## Simple benchmark by [Oha](https://github.com/hatoo/oha)
**These benchmarks use :**
- 3 async Rust echo servers on a local network with 1Gbit as upstreams.
- A dedicated server for running **Aralez**
@@ -386,7 +441,7 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
- "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
- 300 : simultaneous connections
@@ -395,16 +450,16 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
```
Summary:
Success rate: 100.00%
Total: 600.0027 secs
Slowest: 0.2138 secs
Fastest: 0.0002 secs
Average: 0.0023 secs
Requests/sec: 129777.3838
Success rate: 100.00%
Total: 600.0027 secs
Slowest: 0.2138 secs
Fastest: 0.0002 secs
Average: 0.0023 secs
Requests/sec: 129777.3838
Total data: 0 B
Size/request: 0 B
Size/sec: 0 B
Total data: 0 B
Size/request: 0 B
Size/sec: 0 B
Response time histogram:
0.000 [1] |
@@ -432,8 +487,8 @@ Response time distribution:
Details (average, fastest, slowest):
DNS+dialup: 0.0161 secs, 0.0002 secs, 0.0316 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
DNS+dialup: 0.0161 secs, 0.0002 secs, 0.0316 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
Status code distribution:
[200] 77866624 responses
@@ -451,16 +506,16 @@ Error distribution:
```
Summary:
Success rate: 100.00%
Total: 600.0021 secs
Slowest: 0.2182 secs
Fastest: 0.0002 secs
Average: 0.0024 secs
Requests/sec: 123870.5820
Success rate: 100.00%
Total: 600.0021 secs
Slowest: 0.2182 secs
Fastest: 0.0002 secs
Average: 0.0024 secs
Requests/sec: 123870.5820
Total data: 0 B
Size/request: 0 B
Size/sec: 0 B
Total data: 0 B
Size/request: 0 B
Size/sec: 0 B
Response time histogram:
0.000 [1] |
@@ -488,8 +543,8 @@ Response time distribution:
Details (average, fastest, slowest):
DNS+dialup: 0.0066 secs, 0.0002 secs, 0.0210 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
DNS+dialup: 0.0066 secs, 0.0002 secs, 0.0210 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
Status code distribution:
[200] 74322377 responses
@@ -500,7 +555,7 @@ Error distribution:
![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.
@@ -515,4 +570,3 @@ The results show requests per second performed by Load balancer. You can see 3 b
1. Requests via http1.1 to plain text endpoint.
2. Requests to via http2 to SSL endpoint.
3. Mixed workload with plain http1.1 and htt2 SSL.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+18 -16
View File
@@ -1,25 +1,27 @@
# Main configuration file, applied on startup
threads: 12 # Number of daemon threads default setting
#runuser: pastor # Username for running aralez after dropping root privileges, requires program to start as root
#rungroup: pastor # Group for running aralez after dropping root privileges, requires program to start as root
daemon: false # Run in background
runuser: aralez # Username for running aralez after dropping root privileges, requires program to start as root
rungroup: aralez # Group for running aralez after dropping root privileges, requires program to start as root
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
upgrade_sock: /tmp/aralez.sock # Path to socket file
config_api_enabled: true # Boolean to enable/disable remote config push capability.
config_address: 0.0.0.0:3000 # HTTP API address for pushing upstreams.yaml from remote location
config_tls_address: 0.0.0.0:3001 # HTTP TLS API address for pushing upstreams.yaml from remote location
config_tls_certificate: /etc/server.crt # Mandatory if config_tls_address is set
config_tls_key_file: /etc/key.pem # Mandatory if config_tls_address is set
proxy_address_http: 0.0.0.0:6193 # Proxy HTTP bind address
proxy_address_tls: 0.0.0.0:6194 # Optional, Proxy TLS bind address
proxy_certificates: /etc/certs # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key.
proxy_tls_grade: a+ # Grade of TLS suite for proxy (a+, a, b, c, unsafe), matching grades of Qualys SSL Labs
upstreams_conf: /etc/upstreams.yaml # the location of upstreams file
file_server_folder: /opt/storage # Optional, local folder to serve
file_server_address: 127.0.0.1:3002 # Optional, Local address for file server. Can set as upstream for public access.
config_address: 127.0.0.1:3000 # HTTP API address for pushing upstreams.yaml from remote location
proxy_address_http: 0.0.0.0:80 # Proxy HTTP bind address
proxy_address_tls: 0.0.0.0:443 # Optional, Proxy TLS bind address
proxy_configs: /opt/aralez/asyncweb/etc # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key.
proxy_tls_grade: high # Grade of TLS suite for proxy (high, medium, unsafe), matching grades of Qualys SSL Labs
upstreams_conf: /opt/aralez/etc/upstreams.yaml # the location of upstreams file
file_server_folder: /opt/aralez/public # Optional, local folder to serve
file_server_address: 0.0.0.0:3002 # Optional, Local address for file server. Can set as upstream for public access.
log_level: info # info, warn, error, debug, trace, off
log_file: /tmp/aralez.log, 20, 5, compress # Optional, the location of log file, file size in megabytes, number of log files to keep, compress files .valueas are separated by comma.
access_log: error # all, error, (Off if commented)
hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE
hc_interval: 2 #Interval for health checks in seconds
master_key: 910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774 # Mater key for working with API server and JWT Secret
tcp_keepalive_idle: 60 # Seconds of inactivity before the kernel starts sending keepalive probes to a downstream client
tcp_keepalive_interval: 10 # Seconds between individual keepalive probes if the client does not respond
tcp_keepalive_count: 5 # Number of unanswered probes before the kernel declares the connection dead and closes it
cache_size_mb: 50 # Optional. Enable internal cache with size in megabytes of value. Cache is entirely disabled when this line is commented out
cache_ttl: 120 # Optional. Set Default TTL in seconds for cached items, works if cache control headers are not set via upstream. Defaults to 60 if cache_size_mb is set
-19
View File
@@ -1,19 +0,0 @@
provider: "file"
globals:
headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "X-Custom-Header:Something Special"
upstreams:
myip.netangels.net:
paths:
"/":
ssl: false
headers:
- "X-Proxy-From:Aralez"
servers:
- "192.168.221.213:8000"
- "192.168.221.214:8000"
- "192.168.221.210:8000"
- "192.168.221.212:8000"
+92 -65
View File
@@ -1,110 +1,137 @@
# The file under watch and hot reload, changes are applied immediately, no need to restart or reload.
provider: "file" # "file" "consul" "kubernetes"
sticky_sessions: false
sticky_sessions: 172000
to_https: false
rate_limit: 100
headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
rate_limit: 500000
x4xx_limit: 100000
server_headers:
- "Y-Global-Something: Something For Servers"
client_headers:
- "X-Global-Something: Something For Clients"
#authorization:
# type: "jwt"
# creds: "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
# type: "basic"
# creds: "username:Pa$$w0rd"
# type: "apikey"
# creds: "5ecbf799-1343-4e94-a9b5-e278af5cd313-56b45249-1839-4008-a450-a60dc76d2bae"
# data: "root:toor"
# type: "jwt"
# data: "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
# type: "apikey"
# data: "5ecbf799-1343-4e94-a9b5-e278af5cd313-56b45249-1839-4008-a450-a60dc76d2bae"
consul:
servers:
- "http://192.168.1.199:8500"
- "http://192.168.1.200:8500"
- "http://192.168.1.201:8500"
- "http://consul1:8500"
services: # hostname: The hostname to access the proxy server, upstream : The real service name in Consul database.
- hostname: "vt-webapi-service"
upstream: "vt-webapi-service-health"
- hostname: "nconsul"
upstream: "nginx-consul-NginX-health"
path: "/one"
headers:
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez"
rate_limit: 1
to_https: false
- hostname: "vt-webapi-service"
upstream: "vt-webapi-service-health"
- hostname: "nconsul"
upstream: "nginx-consul-NginX-health"
path: "/"
token: "8e2db809-845b-45e1-8b47-2c8356a09da0-a4370955-18c2-4d6e-a8f8-ffcc0b47be81" # Consul server access token, If Consul auth is enabled
kubernetes:
servers:
- "192.168.1.55:443" #For testing only, overrides with KUBERNETES_SERVICE_HOST : KUBERNETES_SERVICE_PORT_HTTPS env variables.
- "172.16.0.11:5443" # Gets KUBERNETES_SERVICE_HOST : KUBERNETES_SERVICE_PORT_HTTPS env variables.
services:
- hostname: "vt-webapi-service"
- hostname: "api-service"
upstream: "api-service"
path: "/"
upstream: "vt-webapi-service"
- hostname: "vt-webapi-service"
upstream: "vt-console-service"
- hostname: "api-service"
upstream: "console-service"
path: "/one"
headers:
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez"
rate_limit: 100
to_https: false
- hostname: "vt-webapi-service"
upstream: "vt-rambulik-service"
- hostname: "api-service"
upstream: "feed-fanout-service"
path: "/two"
- hostname: "vt-websocket-service"
upstream: "vt-websocket-service"
- hostname: "websocket-service"
upstream: "websocket-service"
path: "/"
tokenpath: "/path/to/kubetoken.txt" #If not set, will default to /var/run/secrets/kubernetes.io/serviceaccount/token
tokenpath: "/opt/Rust/Projects/asyncweb/etc/kubetoken.txt" # Defaults to /var/run/secrets/kubernetes.io/serviceaccount/token
upstreams:
myip.mydomain.com:
www.example.com:
paths:
"/":
rate_limit: 200
rate_limit: 50
x4xx_limit: 100
to_https: false
headers:
- "X-Proxy-From:Aralez"
authorization:
type: "basic"
data: "root:toor"
server_headers:
- "Y-Proxy-Server-Some:Yaaaaaaaaaaaaaaa"
- "Y-Proxy-Server-From:Aralez"
- "Y-Proxy-Server-Vers:Aralez v-xxx"
client_headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
servers:
- "127.0.0.1:8000"
- "127.0.0.2:8000"
- "127.0.0.3:8000"
- "127.0.0.4:8000"
- "127.0.0.5:8000"
"/ping":
to_https: false
headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez"
to_https: true
client_headers:
- "X-Some-Thing:Something Else"
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
servers:
- "127.0.0.1:8000"
"/secret":
authorization:
type: "forward"
data: "http://127.0.0.1:8899/admin/login"
servers:
- "127.0.0.10:8000"
example.com:
paths:
"/":
redirect_to: "https://www.example.com:443"
servers:
- "127.0.0.1:80"
h2.example.com:
paths:
"/":
server_headers:
- "Y-Global-Something: Yes this is something"
client_headers:
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
rate_limit: 60
x4xx_limit: 30
authorization:
type: "jwt"
servers:
- "127.0.0.1:8000"
- "127.0.0.2:8000"
"/draw":
servers:
- "192.168.1.1:8000"
polo.mydomain.com:
paths:
"/":
to_https: false
headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
servers:
- "192.168.1.1:8000"
- "192.168.1.10:8000"
- "127.0.0.1:8000"
- "127.0.0.2:8000"
- "127.0.0.3:8000"
- "127.0.0.4:8000"
apt.mydomain.com:
paths:
"/":
servers:
- "192.168.1.10:443"
"/.well-known/acme-challenge":
healthcheck: false
servers:
- "127.0.0.1:8001"
localpost:
"/400":
rate_limit: 4
x4xx_limit: 2
servers:
- "127.0.0.1:8899"
"/500":
healthcheck: false
servers:
- "127.0.0.1:8899"
DEFAULT:
paths:
"/":
to_https: false
healthcheck: false
servers:
- "127.0.0.1:9000"
- "127.0.0.1:3000"
"/.well-known/acme-challenge":
healthcheck: false
servers:
- "127.0.0.1:3000"
+7
View File
@@ -1,9 +1,16 @@
mod tls;
mod utils;
mod web;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
// pub static A: CountingAllocator = CountingAllocator;
fn main() {
if std::env::args().any(|a| a == "--version" || a == "-v") {
println!("aralez {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
web::start::run();
}
+3
View File
@@ -0,0 +1,3 @@
pub mod acme;
pub mod grades;
pub mod load;
+2
View File
@@ -0,0 +1,2 @@
pub mod account;
pub mod order;
+58
View File
@@ -0,0 +1,58 @@
use instant_acme::{Account, AccountCredentials, LetsEncrypt, NewAccount};
use log::info;
use std::fs;
use std::path::Path;
use std::sync::OnceLock;
static ACCOUNT: OnceLock<Account> = OnceLock::new();
pub async fn get_account(file: &str) -> Result<&'static Account, Box<dyn std::error::Error>> {
if let Some(account) = ACCOUNT.get() {
return Ok(account);
}
if let Some(credentials) = load_credentials(file) {
let acc_builder = Account::builder()?;
let account = acc_builder.from_credentials(credentials).await?;
let _ = ACCOUNT.set(account);
info!("Loaded existing ACME account");
} else {
info!("No existing credentials found, creating new account");
create_account(file).await?;
}
ACCOUNT.get().ok_or("Failed to initialize account".into())
}
async fn create_account(file: &str) -> Result<(), Box<dyn std::error::Error>> {
let new_account = NewAccount {
contact: &[],
terms_of_service_agreed: true,
only_return_existing: false,
};
let acc_builder = Account::builder()?;
let (account, credentials) = acc_builder.create(&new_account, LetsEncrypt::Production.url().to_string(), None).await?;
// let (account, credentials) = acc_builder.create(&new_account, LetsEncrypt::Staging.url().to_string(), None).await?;
info!("Account created: {:?}", account.id());
save_credentials(&credentials, file)?;
let _ = ACCOUNT.set(account);
Ok(())
}
pub async fn load_or_create(file: &str) -> Result<String, Box<dyn std::error::Error>> {
let account = get_account(file).await?;
Ok(account.id().to_string() + "\n")
}
fn save_credentials(credentials: &AccountCredentials, file: &str) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(credentials)?;
fs::write(file, json)?;
info!("ACME credentials saved to {}", file);
Ok(())
}
fn load_credentials(file: &str) -> Option<AccountCredentials> {
if !Path::new(file).exists() {
return None;
}
let json = fs::read_to_string(file).ok()?;
serde_json::from_str(&json).ok()
}
+91
View File
@@ -0,0 +1,91 @@
use crate::tls::acme::account::get_account;
use crate::utils::parceyaml::DOMAINS;
use instant_acme::{ChallengeType, Identifier, NewOrder, RetryPolicy};
use log::{error, info};
use pingora::prelude::sleep;
use rcgen::{CertificateParams, DistinguishedName, KeyPair};
use std::collections::HashMap;
use std::fs;
use std::sync::{LazyLock, RwLock};
use std::time::Duration;
use x509_parser::prelude::*;
pub static CHALLENGES: LazyLock<RwLock<HashMap<String, String>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
pub async fn refresh_order(certs_dir: String, autoconf_dir: String) {
let credsfile = autoconf_dir + "/acme_credentials.json";
loop {
for item in DOMAINS.iter() {
let _what = order(item.key(), credsfile.as_str(), certs_dir.clone()).await;
}
sleep(Duration::from_secs(12 * 3600)).await;
}
}
pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<String, Box<dyn std::error::Error>> {
let crt = certs_dir.clone() + "/" + domain + ".crt";
let key = certs_dir.clone() + "/" + domain + ".key";
if DOMAINS.get(domain).is_none() {
DOMAINS.insert(domain.to_string(), true);
let mut newlist: Vec<String> = Vec::new();
for item in DOMAINS.iter() {
newlist.push(item.key().to_string());
}
if let Ok(json_content) = serde_json::to_string_pretty(&newlist) {
let autocfg_file = credsfile.replace("/acme_credentials.json", "/domains.json");
if let Err(err) = std::fs::write(&autocfg_file, json_content) {
error!("Error Updating domains for certificates: {} : {}", domain, err);
return Err(Box::from(err));
}
}
}
if let Ok(expiry) = cert_expiry(crt.as_str()) {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
if expiry > now + 30 * 24 * 3600 {
// println!("Fresh certificate exists. Not renewing !");
return Ok("Fresh certificate exists. Not renewing ! \n".to_string());
}
};
let account = get_account(credsfile).await?;
let mut order = account.new_order(&NewOrder::new(&[Identifier::Dns(domain.to_string())])).await?;
let mut authorizations = order.authorizations();
while let Some(auth) = authorizations.next().await {
let mut auth = auth?;
let mut challenge_handle = auth.challenge(ChallengeType::Http01).ok_or("no http01 challenge found")?;
let key_auth = challenge_handle.key_authorization();
let key_auth_str = key_auth.as_str().to_string();
let token = key_auth_str.split('.').next().ok_or("invalid key authorization")?.to_string();
CHALLENGES.write().unwrap().insert(token, key_auth_str);
challenge_handle.set_ready().await?;
}
let status = order.poll_ready(&RetryPolicy::default()).await?;
info!("ACME poll_ready status: {:?}", status);
let mut params = CertificateParams::new(vec![domain.to_owned()])?;
params.distinguished_name = DistinguishedName::new();
let private_key = KeyPair::generate()?;
let signing_request = params.serialize_request(&private_key)?;
let csr_der = signing_request.der();
order.finalize_csr(csr_der).await?;
// poll for certificate
let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?;
CHALLENGES.write().unwrap().clear();
let private_key_pem = private_key.serialize_pem();
fs::write(crt, cert_chain_pem)?;
fs::write(key, private_key_pem)?;
Ok("Certificate is successfully generated \n".to_string())
}
fn cert_expiry(path: &str) -> Result<u64, Box<dyn std::error::Error>> {
let pem = fs::read(path)?;
let (_, pem) = parse_x509_pem(&pem)?;
let (_, cert) = parse_x509_certificate(&pem.contents)?;
let expiry = cert.validity().not_after.timestamp() as u64;
Ok(expiry)
}
+75
View File
@@ -0,0 +1,75 @@
use log::{info, warn};
use pingora::tls::ssl::{select_next_proto, AlpnError, SslRef, SslVersion};
use pingora_core::listeners::tls::TlsSettings;
#[derive(Debug)]
pub struct CipherSuite {
pub high: &'static str,
pub medium: &'static str,
pub legacy: &'static str,
}
const CIPHERS: CipherSuite = CipherSuite {
high: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305",
medium: "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:AES128-GCM-SHA256",
legacy: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
};
#[derive(Debug)]
pub enum TlsGrade {
High,
Medium,
Legacy,
}
impl TlsGrade {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::High),
"medium" => Some(TlsGrade::Medium),
"unsafe" => Some(TlsGrade::Legacy),
_ => None,
}
}
}
pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> {
match select_next_proto("\x02h2\x08http/1.1".as_bytes(), alpn_in) {
Some(p) => Ok(p),
_ => Err(AlpnError::NOACK),
}
}
pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
let config_grade = TlsGrade::from_str(grade);
match config_grade {
Some(TlsGrade::High) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2));
// let _ = tls_settings.set_max_proto_version(Some(SslVersion::TLS1_3));
let _ = tls_settings.set_cipher_list(CIPHERS.high);
// let _ = tls_settings.set_ciphersuites(CIPHERS.high);
let _ = tls_settings.set_cipher_list(CIPHERS.high);
info!("TLS grade: {:?}, => High", tls_settings.options());
}
Some(TlsGrade::Medium) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
info!("TLS grade: {:?}, => Medium", tls_settings.options());
}
Some(TlsGrade::Legacy) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::SSL3));
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
// let _ = tls_settings.set_ciphersuites(CIPHERS.legacy);
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
warn!("TLS grade: {:?}, => UNSAFE", tls_settings.options());
}
None => {
// Defaults to Medium
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
warn!("TLS grade is not detected defaulting top Medium");
}
}
}
+13 -82
View File
@@ -1,7 +1,7 @@
use crate::tls::grades;
use dashmap::DashMap;
use log::{error, info, warn};
use pingora::tls::ssl::{select_next_proto, AlpnError, NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef, SslVersion};
use pingora_core::listeners::tls::TlsSettings;
use log::error;
use pingora::tls::ssl::{NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef};
use rustls_pemfile::{read_one, Item};
use serde::Deserialize;
use std::collections::HashSet;
@@ -10,7 +10,6 @@ use std::io::BufReader;
use x509_parser::extensions::GeneralName;
use x509_parser::nom::Err as NomErr;
use x509_parser::prelude::*;
#[derive(Clone, Deserialize, Debug)]
pub struct CertificateConfig {
pub cert_path: String,
@@ -18,14 +17,14 @@ pub struct CertificateConfig {
}
#[derive(Debug)]
struct CertificateInfo {
common_names: Vec<String>,
alt_names: Vec<String>,
ssl_context: SslContext,
pub struct CertificateInfo {
pub common_names: Vec<String>,
pub alt_names: Vec<String>,
pub ssl_context: SslContext,
#[allow(dead_code)]
cert_path: String, // Only used for logging
pub cert_path: String, // Only used for logging
#[allow(dead_code)]
key_path: String, // Only used for logging
pub key_path: String, // Only used for logging
}
#[derive(Debug)]
@@ -61,7 +60,7 @@ impl Certificates {
}
}
Some(Self {
name_map: name_map,
name_map,
configs: cert_infos,
default_cert_path: default_cert.cert_path.clone(),
default_key_path: default_cert.key_path.clone(),
@@ -94,7 +93,7 @@ impl Certificates {
if let Some(name) = server_name {
match self.find_ssl_context(name) {
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 => {
log::debug!("No matching server name found");
@@ -106,7 +105,7 @@ impl Certificates {
}
}
fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option<CertificateInfo> {
pub fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option<CertificateInfo> {
let mut common_names = HashSet::new();
let mut alt_names = HashSet::new();
@@ -180,75 +179,7 @@ fn create_ssl_context(cert_path: &str, key_path: &str) -> Result<SslContext, Box
let mut ctx = SslContext::builder(SslMethod::tls())?;
ctx.set_certificate_chain_file(cert_path)?;
ctx.set_private_key_file(key_path, SslFiletype::PEM)?;
ctx.set_alpn_select_callback(prefer_h2);
ctx.set_alpn_select_callback(grades::prefer_h2);
let built = ctx.build();
Ok(built)
}
#[derive(Debug)]
pub struct CipherSuite {
pub high: &'static str,
pub medium: &'static str,
pub legacy: &'static str,
}
const CIPHERS: CipherSuite = CipherSuite {
high: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305",
medium: "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:AES128-GCM-SHA256",
legacy: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
};
#[derive(Debug)]
pub enum TlsGrade {
HIGH,
MEDIUM,
LEGACY,
}
impl TlsGrade {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::HIGH),
"medium" => Some(TlsGrade::MEDIUM),
"unsafe" => Some(TlsGrade::LEGACY),
_ => None,
}
}
}
pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> {
match select_next_proto("\x02h2\x08http/1.1".as_bytes(), alpn_in) {
Some(p) => Ok(p),
_ => Err(AlpnError::NOACK),
}
}
pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
let config_grade = TlsGrade::from_str(grade);
match config_grade {
Some(TlsGrade::HIGH) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2));
// let _ = tls_settings.set_max_proto_version(Some(SslVersion::TLS1_3));
let _ = tls_settings.set_cipher_list(CIPHERS.high);
let _ = tls_settings.set_ciphersuites(CIPHERS.high);
info!("TLS grade: {:?}, => HIGH", tls_settings.options());
}
Some(TlsGrade::MEDIUM) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
info!("TLS grade: {:?}, => MEDIUM", tls_settings.options());
}
Some(TlsGrade::LEGACY) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::SSL3));
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
let _ = tls_settings.set_ciphersuites(CIPHERS.legacy);
warn!("TLS grade: {:?}, => UNSAFE", tls_settings.options());
}
None => {
// Defaults to MEDIUM
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
warn!("TLS grade is not detected defaulting top MEDIUM");
}
}
}
+4 -4
View File
@@ -1,15 +1,15 @@
pub mod auth;
pub mod consul;
pub mod discovery;
pub mod dnsclient;
mod filewatch;
pub mod fordebug;
pub mod healthcheck;
pub mod httpclient;
pub mod jwt;
pub mod kuber;
pub mod kuberconsul;
pub mod lazylock;
pub mod metrics;
pub mod parceyaml;
pub mod state;
pub mod structs;
pub mod tls;
pub mod tools;
// pub mod watchksecret;
+201 -60
View File
@@ -1,60 +1,160 @@
use crate::utils::jwt::check_jwt;
use crate::utils::jwt::{check_jwt, JWT_TOKEN};
use crate::utils::structs::InnerAuth;
use axum::http::StatusCode;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use pingora::http::RequestHeader;
use pingora_core::connectors::http::Connector;
use pingora_core::upstreams::peer::HttpPeer;
use pingora_http::ResponseHeader;
use pingora_proxy::Session;
use std::collections::HashMap;
use std::sync::LazyLock;
use subtle::ConstantTimeEq;
use urlencoding::decode;
#[async_trait::async_trait]
trait AuthValidator {
fn validate(&self, session: &Session) -> bool;
async fn validate(&self, session: &mut Session) -> bool;
}
struct BasicAuth<'a>(&'a str);
struct ApiKeyAuth<'a>(&'a str);
struct JwtAuth<'a>(&'a str);
struct JwtAuth();
struct ForwardAuth<'a>(&'a str);
impl AuthValidator for BasicAuth<'_> {
fn validate(&self, session: &Session) -> bool {
if let Some(header) = session.get_header("authorization") {
if let Some((_, val)) = header.to_str().ok().unwrap().split_once(' ') {
let decoded = STANDARD.decode(val).ok().unwrap();
let decoded_str = String::from_utf8(decoded).ok().unwrap();
return decoded_str == self.0;
pub static AUTH_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
#[async_trait::async_trait]
impl AuthValidator for ForwardAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
let method = match session.req_header().method.as_str() {
"HEAD" => "HEAD",
_ => "GET",
};
let auth_url = self.0;
let (plain, tls) = if let Some(p) = auth_url.strip_prefix("http://") {
(p, false)
} else if let Some(p) = auth_url.strip_prefix("https://") {
(p, true)
} else {
return false;
};
let (addr, uri) = if let Some(pos) = plain.find('/') {
(&plain[..pos], &plain[pos..])
} else {
(plain, "/")
};
let hp = match split_host_port(addr, tls) {
Some(hp) => hp,
None => return false,
};
let peer = HttpPeer::new((hp.0, hp.1), tls, hp.0.to_string());
let (mut http_session, _) = match AUTH_CONNECTOR.get_http_session(&peer).await {
Ok(s) => s,
Err(e) => {
log::warn!("ForwardAuth: connect failed: {}", e);
return false;
}
};
let mut auth_req = match RequestHeader::build(method, uri.as_bytes(), None) {
Ok(r) => r,
Err(e) => {
log::warn!("ForwardAuth: failed to build request: {}", e);
return false;
}
};
// auth_req.headers = session.req_header().headers.clone();
auth_req.insert_header("Host", addr).ok();
auth_req.insert_header("X-Forwarded-Uri", uri).ok();
auth_req.insert_header("X-Forwarded-Method", session.req_header().method.as_str()).ok();
if let Some(auth) = session.req_header().headers.get("authorization") {
auth_req.insert_header("Authorization", auth.clone()).ok();
}
if let Some(cookie) = session.req_header().headers.get("cookie") {
auth_req.insert_header("Cookie", cookie.clone()).ok();
}
if tls {
auth_req.insert_header("X-Forwarded-Proto", "https").ok();
} else {
auth_req.insert_header("X-Forwarded-Proto", "http").ok();
}
if let Err(e) = http_session.write_request_header(Box::new(auth_req)).await {
log::warn!("ForwardAuth: write failed: {}", e);
return false;
}
let status = match http_session.read_response_header().await {
Ok(_) => http_session.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
log::warn!("ForwardAuth: read failed: {}", e);
return false;
}
};
let auth_headers_to_forward: Vec<(String, String)> = if let Some(resp_header) = http_session.response_header() {
resp_header
.headers
.iter()
.filter_map(|(name, value)| {
let name_str = name.as_str();
if name_str.starts_with("x-") || name_str.starts_with("remote-") || name_str.starts_with("locat") {
value.to_str().ok().map(|v| (name_str.to_string(), v.to_string()))
} else {
None
}
})
.collect()
} else {
Vec::new()
};
AUTH_CONNECTOR.release_http_session(http_session, &peer, None).await;
if (200..300).contains(&status) {
for (name, value) in auth_headers_to_forward {
session.req_header_mut().insert_header(name, value).ok();
}
true
} else if status == 302 || status == 301 {
let resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None);
match resp {
Ok(mut r) => {
for (name, value) in auth_headers_to_forward {
r.insert_header(name, value).ok();
}
let _ = r.insert_header("Content-Length", "0");
let _ = session.write_response_header(Box::new(r), true).await;
true
}
Err(_) => return false,
}
} else {
false
}
false
}
}
impl AuthValidator for ApiKeyAuth<'_> {
fn validate(&self, session: &Session) -> bool {
if let Some(header) = session.get_header("x-api-key") {
return header.to_str().ok().unwrap() == self.0;
}
false
}
}
impl AuthValidator for JwtAuth<'_> {
fn validate(&self, session: &Session) -> bool {
let jwtsecret = self.0;
if let Some(tok) = get_query_param(session, "araleztoken") {
return check_jwt(tok.as_str(), jwtsecret);
}
// if let Some(header) = session.get_header("authorization") {
// let h = header.to_str().ok().unwrap().split(" ").collect::<Vec<_>>();
// match h.len() {
// n => {
// return check_jwt(h[n - 1], jwtsecret);
// }
// }
// }
if let Some(auth_header) = session.get_header("authorization") {
if let Ok(header_str) = auth_header.to_str() {
if let Some((scheme, token)) = header_str.split_once(' ') {
if scheme.eq_ignore_ascii_case("bearer") {
return check_jwt(token, jwtsecret);
#[async_trait::async_trait]
impl AuthValidator for BasicAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("authorization") {
if let Ok(h) = header.to_str() {
if let Some((_, val)) = h.split_once(' ') {
if let Ok(decoded) = STANDARD.decode(val) {
if decoded.as_slice().ct_eq(self.0.as_bytes()).into() {
return true;
}
}
}
}
@@ -62,32 +162,54 @@ impl AuthValidator for JwtAuth<'_> {
false
}
}
fn validate(auth: &dyn AuthValidator, session: &Session) -> bool {
auth.validate(session)
#[async_trait::async_trait]
impl AuthValidator for ApiKeyAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("x-api-key") {
if let Ok(h) = header.to_str() {
return h.as_bytes().ct_eq(self.0.as_bytes()).into();
}
}
false
}
}
pub fn authenticate(c: &[String], session: &Session) -> bool {
match c[0].as_str() {
"basic" => {
let auth = BasicAuth(c[1].as_str().into());
validate(&auth, session)
}
"apikey" => {
let auth = ApiKeyAuth(c[1].as_str().into());
validate(&auth, session)
}
"jwt" => {
let auth = JwtAuth(c[1].as_str().into());
validate(&auth, session)
#[async_trait::async_trait]
impl AuthValidator for JwtAuth {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(jwtsecret) = JWT_TOKEN.clone() {
if let Some(tok) = get_query_param(session, "araleztoken") {
return check_jwt(tok.as_str(), jwtsecret.as_ref());
}
if let Some(auth_header) = session.get_header("authorization") {
if let Ok(header_str) = auth_header.to_str() {
if let Some((scheme, token)) = header_str.split_once(' ') {
if scheme.eq_ignore_ascii_case("bearer") {
return check_jwt(token, jwtsecret.as_ref());
}
}
}
}
}
false
}
}
pub async fn authenticate(auth: &InnerAuth, session: &mut Session) -> bool {
match &*auth.auth_type {
"basic" => BasicAuth(&*auth.auth_cred).validate(session).await,
"apikey" => ApiKeyAuth(&*auth.auth_cred).validate(session).await,
"jwt" => JwtAuth().validate(session).await,
"forward" => ForwardAuth(&*auth.auth_cred).validate(session).await,
_ => {
println!("Unsupported authentication mechanism : {}", c[0]);
log::warn!("Unsupported authentication mechanism : {}", &*auth.auth_type);
false
}
}
}
pub fn get_query_param(session: &Session, key: &str) -> Option<String> {
pub fn get_query_param(session: &mut Session, key: &str) -> Option<String> {
let query = session.req_header().uri.query()?;
let params: HashMap<_, _> = query
@@ -99,6 +221,25 @@ pub fn get_query_param(session: &Session, key: &str) -> Option<String> {
Some((k, v))
})
.collect();
params.get(key).map(|v| decode(v).ok()).flatten().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)> {
match addr.split_once(':') {
Some((h, p)) => match p.parse::<u16>() {
Ok(port) => return Some((h, port, tls, h)),
Err(_) => {
log::warn!("ForwardAuth: invalid port in {}", addr);
return None;
}
},
None => {
if tls {
return Some((addr, 443u16, tls, addr));
} else {
return Some((addr, 80u16, tls, addr));
}
}
};
}
-119
View File
@@ -1,119 +0,0 @@
use crate::utils::kuberconsul::{list_to_upstreams, match_path};
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, InnerMap, ServiceMapping, UpstreamsDashMap};
use crate::utils::tools::{clone_dashmap_into, compare_dashmaps, print_upstreams};
use dashmap::DashMap;
use futures::channel::mpsc::Sender;
use futures::SinkExt;
use pingora::prelude::sleep;
use rand::Rng;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::Client;
use serde::Deserialize;
use std::collections::HashMap;
use std::env;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Deserialize)]
struct Service {
#[serde(rename = "ServiceTaggedAddresses")]
tagged_addresses: HashMap<String, TaggedAddress>,
}
#[derive(Debug, Deserialize)]
struct TaggedAddress {
#[serde(rename = "Address")]
address: String,
#[serde(rename = "Port")]
port: u16,
}
pub async fn start(mut toreturn: Sender<Configuration>, config: Arc<Configuration>) {
let prev_upstreams = UpstreamsDashMap::new();
loop {
if let Some(consul) = config.consul.clone() {
let servers = consul.servers.unwrap_or(vec![format!(
"{}:{}",
env::var("CONSUL_SERVICE_HOST").unwrap_or("0.0.0.0".to_string()),
env::var("CONSUL_SERVICE_PORT").unwrap_or("0".to_string())
)]);
let end = servers.len() - 1;
let upstreams = UpstreamsDashMap::new();
let mut num = 0;
if end > 0 {
num = rand::rng().random_range(0..end);
}
let consul_data = servers.get(num).unwrap().to_string();
let ss = consul_data + "/v1/catalog/service/";
if let Some(ref svc) = consul.services {
for i in svc {
let header_list = DashMap::new();
let mut hl = Vec::new();
build_headers(&i.headers, config.as_ref(), &mut hl);
if hl.len() > 0 {
header_list.insert(i.path.clone().unwrap_or("/".to_string()), hl);
config.headers.insert(i.hostname.clone(), header_list);
}
let pref: String = ss.clone() + &i.upstream;
let list = get_by_http(pref, consul.token.clone(), &i).await;
list_to_upstreams(list, &upstreams, &i);
}
}
if !compare_dashmaps(&upstreams, &prev_upstreams) {
let mut tosend: Configuration = Configuration {
upstreams: Default::default(),
headers: Default::default(),
consul: None,
kubernetes: None,
typecfg: "".to_string(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(&upstreams, &prev_upstreams);
clone_dashmap_into(&upstreams, &tosend.upstreams);
tosend.headers = config.headers.clone();
tosend.extraparams.authentication = config.extraparams.authentication.clone();
tosend.typecfg = config.typecfg.clone();
tosend.consul = config.consul.clone();
print_upstreams(&tosend.upstreams);
toreturn.send(tosend).await.unwrap();
}
}
sleep(Duration::from_secs(5)).await;
}
}
async fn get_by_http(url: String, token: Option<String>, conf: &ServiceMapping) -> Option<DashMap<String, (Vec<InnerMap>, AtomicUsize)>> {
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().ok()?;
let mut headers = HeaderMap::new();
if let Some(token) = token {
headers.insert("X-Consul-Token", HeaderValue::from_str(&token).unwrap());
}
let to = Duration::from_secs(1);
let resp = client.get(url).timeout(to).send().await.ok()?;
if !resp.status().is_success() {
eprintln!("Consul API returned status: {}", resp.status());
return None;
}
let mut inner_vec = Vec::new();
let upstreams: DashMap<String, (Vec<InnerMap>, AtomicUsize)> = DashMap::new();
let endpoints: Vec<Service> = resp.json().await.ok()?;
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 to_add = InnerMap {
address: addr,
port: prt,
is_ssl: false,
is_http2: false,
to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit,
healthcheck: None,
};
inner_vec.push(to_add);
}
match_path(&conf, &upstreams, inner_vec.clone());
Some(upstreams)
}
+19 -17
View File
@@ -1,27 +1,22 @@
use crate::utils::filewatch;
use crate::utils::structs::Configuration;
use crate::utils::{consul, kuber};
use crate::utils::kuberconsul::{ConsulDiscovery, KubernetesDiscovery, ServiceDiscovery};
use crate::utils::structs::{Configuration, UpstreamsDashMap};
use crate::web::webserver;
use async_trait::async_trait;
use futures::channel::mpsc::Sender;
use std::sync::Arc;
use tokio::sync::mpsc::Sender;
pub struct APIUpstreamProvider {
pub config_api_enabled: bool,
pub address: String,
pub masterkey: String,
pub tls_address: Option<String>,
pub tls_certificate: Option<String>,
pub tls_key_file: Option<String>,
pub masterkey: Option<String>,
pub certs_dir: String,
pub config_dir: String,
pub upstreams_file: String,
pub file_server_address: Option<String>,
pub file_server_folder: Option<String>,
}
#[async_trait]
impl Discovery for APIUpstreamProvider {
async fn start(&self, toreturn: Sender<Configuration>) {
webserver::run_server(self, toreturn).await;
}
pub current_upstreams: Arc<UpstreamsDashMap>,
pub full_upstreams: Arc<UpstreamsDashMap>,
}
pub struct FromFileProvider {
@@ -41,23 +36,30 @@ pub trait Discovery {
async fn start(&self, tx: Sender<Configuration>);
}
#[async_trait]
impl Discovery for APIUpstreamProvider {
async fn start(&self, toreturn: Sender<Configuration>) {
webserver::run_server(self, toreturn, self.current_upstreams.clone(), self.full_upstreams.clone()).await;
}
}
#[async_trait]
impl Discovery for FromFileProvider {
async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(filewatch::start(self.path.clone(), tx.clone()));
tokio::spawn(filewatch::start(self.path.clone(), tx));
}
}
#[async_trait]
impl Discovery for ConsulProvider {
async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(consul::start(tx.clone(), self.config.clone()));
tokio::spawn(ConsulDiscovery.fetch_upstreams(self.config.clone(), tx));
}
}
#[async_trait]
impl Discovery for KubernetesProvider {
async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(kuber::start(tx.clone(), self.config.clone()));
tokio::spawn(KubernetesDiscovery.fetch_upstreams(self.config.clone(), tx));
}
}
-158
View File
@@ -1,158 +0,0 @@
/*
use crate::utils::structs::InnerMap;
use dashmap::DashMap;
use hickory_client::client::{Client, ClientHandle};
use hickory_client::proto::rr::{DNSClass, Name, RecordType};
use hickory_client::proto::runtime::TokioRuntimeProvider;
use hickory_client::proto::udp::UdpClientStream;
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::atomic::AtomicUsize;
use std::time::Duration;
use tokio::sync::Mutex;
type DnsError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub struct DnsClientPool {
clients: Vec<Mutex<DnsClient>>,
}
struct DnsClient {
client: Client,
}
pub async fn start2(mut toreturn: Sender<Configuration>, config: Arc<Configuration>) {
let k8s = config.kubernetes.clone();
match k8s {
Some(k8s) => {
let dnserver = k8s.servers.unwrap_or(vec!["127.0.0.1:53".to_string()]);
let headers = DashMap::new();
let end = dnserver.len() - 1;
let mut num = 0;
if end > 0 {
num = rand::rng().random_range(0..end);
}
let srv = dnserver.get(num).unwrap().to_string();
let pool = DnsClientPool::new(5, srv.clone()).await;
let u = UpstreamsDashMap::new();
if let Some(whitelist) = k8s.services {
loop {
let upstreams = UpstreamsDashMap::new();
for service in whitelist.iter() {
let ret = pool.query_srv(service.real.as_str(), srv.clone()).await;
match ret {
Ok(r) => {
upstreams.insert(service.proxy.clone(), r);
}
Err(e) => eprintln!("DNS query failed for {:?}: {:?}", service, e),
}
}
if !compare_dashmaps(&u, &upstreams) {
headers.clear();
for (k, v) in config.headers.clone() {
headers.insert(k.to_string(), v);
}
let mut tosend: Configuration = Configuration {
upstreams: Default::default(),
headers: Default::default(),
consul: None,
kubernetes: None,
typecfg: "".to_string(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(&upstreams, &u);
clone_dashmap_into(&upstreams, &tosend.upstreams);
tosend.headers = headers.clone();
tosend.extraparams.authentication = config.extraparams.authentication.clone();
tosend.typecfg = config.typecfg.clone();
tosend.consul = config.consul.clone();
print_upstreams(&tosend.upstreams);
toreturn.send(tosend).await.unwrap();
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
None => {}
}
}
impl DnsClient {
pub async fn new(server: String) -> Result<Self, DnsError> {
let server_details = server;
let server: SocketAddr = server_details.parse().expect("Unable to parse socket address");
let conn = UdpClientStream::builder(server, TokioRuntimeProvider::default()).build();
let (client, bg) = Client::connect(conn).await.unwrap();
tokio::spawn(bg);
Ok(Self { client })
}
pub async fn query_srv(&mut self, name: &str) -> Result<DashMap<String, (Vec<InnerMap>, AtomicUsize)>, DnsError> {
let upstreams: DashMap<String, (Vec<InnerMap>, AtomicUsize)> = DashMap::new();
let mut values = Vec::new();
match tokio::time::timeout(Duration::from_secs(5), self.client.query(Name::from_str(name)?, DNSClass::IN, RecordType::SRV)).await {
Ok(Ok(response)) => {
for answer in response.answers() {
if let hickory_client::proto::rr::RData::SRV(srv) = answer.data() {
let to_add = InnerMap {
address: srv.target().to_string(),
port: srv.port(),
is_ssl: false,
is_http2: false,
to_https: false,
rate_limit: None,
};
values.push(to_add);
}
}
upstreams.insert("/".to_string(), (values, AtomicUsize::new(0)));
Ok(upstreams)
}
Ok(Err(e)) => Err(Box::new(e)),
Err(_) => Err("DNS query timed out".into()),
}
}
}
impl DnsClientPool {
pub async fn new(pool_size: usize, server: String) -> Self {
let mut clients = Vec::with_capacity(pool_size);
for _ in 0..pool_size {
if let Ok(client) = DnsClient::new(server.clone()).await {
clients.push(Mutex::new(client));
}
}
Self { clients }
}
pub async fn query_srv(&self, name: &str, server: String) -> Result<DashMap<String, (Vec<InnerMap>, AtomicUsize)>, DnsError> {
// Try to get an available client
for client_mutex in &self.clients {
if let Ok(mut client) = client_mutex.try_lock() {
let vay = client.query_srv(name).await;
match vay {
Ok(_) => return vay,
Err(_) => {
// If query fails, drop this client and create a new one
*client = match DnsClient::new(server).await {
Ok(c) => c,
Err(e) => return Err(e),
};
// Retry with the new client
return client.query_srv(name).await;
}
}
}
}
// If all clients are busy, wait for the first one with a timeout
match tokio::time::timeout(Duration::from_secs(2), self.clients[0].lock()).await {
Ok(mut client) => client.query_srv(name).await,
Err(_) => Err("All DNS clients are busy and timeout reached".into()),
}
}
}
*/
+7 -14
View File
@@ -1,16 +1,15 @@
use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration;
use futures::channel::mpsc::Sender;
use futures::SinkExt;
use log::error;
use notify::event::ModifyKind;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use pingora::prelude::sleep;
use std::path::Path;
use std::time::{Duration, Instant};
use tokio::sync::mpsc::Sender;
use tokio::task;
pub async fn start(fp: String, mut toreturn: Sender<Configuration>) {
pub async fn start(fp: String, toreturn: Sender<Configuration>) {
sleep(Duration::from_millis(50)).await; // For having nice logs :-)
let file_path = fp.as_str();
let parent_dir = Path::new(file_path).parent().unwrap();
@@ -37,17 +36,11 @@ pub async fn start(fp: String, mut toreturn: Sender<Configuration>) {
match event {
Ok(e) => match e.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(..) | EventKind::Remove(..) => {
if e.paths[0].to_str().unwrap().ends_with("yaml") {
if start.elapsed() > Duration::from_secs(2) {
start = Instant::now();
// info!("Config File changed :=> {:?}", e);
let snd = load_configuration(file_path, "filepath").await;
match snd {
Some(snd) => {
toreturn.send(snd).await.unwrap();
}
None => {}
}
if start.elapsed() > Duration::from_secs(2) {
start = Instant::now();
let snd = load_configuration(file_path, "filepath").await.0;
if let Some(snd) = snd {
toreturn.send(snd).await.unwrap();
}
}
}
+31
View File
@@ -0,0 +1,31 @@
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
pub struct CountingAllocator;
pub static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
pub static DEALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
pub static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);
#[allow(dead_code)]
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
DEALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
System.dealloc(ptr, layout)
}
}
// Uncomment following lines and comment allocator in main.rs
// #[global_allocator]
// pub static A: CountingAllocator = CountingAllocator;
#[allow(dead_code)]
fn for_example() {
let before = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
let after = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
println!("Allocations : {}", after - before);
}
+38 -45
View File
@@ -1,3 +1,4 @@
use crate::utils::lazylock::REVERSE_STORE;
use crate::utils::structs::{InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::utils::tools::*;
use dashmap::DashMap;
@@ -15,12 +16,19 @@ pub async fn hc2(upslist: Arc<UpstreamsDashMap>, fullist: Arc<UpstreamsDashMap>,
loop {
tokio::select! {
_ = period.tick() => {
populate_upstreams(&upslist, &fullist, &idlist, params, &client).await;
// populate_upstreams(&upslist, &fullist, &idlist, params, &client).await;
let totest = build_upstreams(&fullist, params.0, &client).await;
if !compare_dashmaps(&totest, &upslist) {
clone_dashmap_into(&totest, &upslist);
clone_idmap_into(&totest, &idlist);
REVERSE_STORE.clear();
}
}
}
}
}
/*
pub async fn populate_upstreams(upslist: &Arc<UpstreamsDashMap>, fullist: &Arc<UpstreamsDashMap>, idlist: &Arc<UpstreamsIdMap>, params: (&str, u64), client: &Client) {
let totest = build_upstreams(fullist, params.0, client).await;
if !compare_dashmaps(&totest, upslist) {
@@ -28,6 +36,7 @@ pub async fn populate_upstreams(upslist: &Arc<UpstreamsDashMap>, fullist: &Arc<U
clone_idmap_into(&totest, idlist);
}
}
*/
pub async fn initiate_upstreams(fullist: UpstreamsDashMap) -> UpstreamsDashMap {
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().unwrap();
@@ -45,16 +54,15 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
let path = path_entry.key();
let mut innervec = Vec::new();
for (_, upstream) in path_entry.value().0.iter().enumerate() {
let tls = detect_tls(upstream.address.as_str(), &upstream.port, &client).await;
let is_h2 = matches!(tls.1, Some(Version::HTTP_2));
let link = if tls.0 {
format!("https://{}:{}{}", upstream.address, upstream.port, path)
for upstream in path_entry.value().0.iter() {
let tls = if upstream.healthcheck.unwrap_or(true) {
detect_tls(upstream.address.as_ref(), &upstream.port, client).await
} else {
format!("http://{}:{}{}", upstream.address, upstream.port, path)
(false, None)
};
let is_h2 = matches!(tls.1, Some(Version::HTTP_2));
let mut scheme = InnerMap {
address: upstream.address.clone(),
port: upstream.port,
@@ -62,32 +70,31 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
is_http2: is_h2,
to_https: upstream.to_https,
rate_limit: upstream.rate_limit,
x4xx_limit: upstream.x4xx_limit,
healthcheck: upstream.healthcheck,
redirect_to: upstream.redirect_to.clone(),
authorization: upstream.authorization.clone(),
};
if scheme.healthcheck.unwrap_or(true) {
let resp = http_request(&link, method, "", &client).await;
let link = if tls.0 {
format!("https://{}:{}{}", upstream.address, upstream.port, path)
} else {
format!("http://{}:{}{}", upstream.address, upstream.port, path)
};
let resp = http_request(&link, method, "", client).await;
if resp.0 {
if resp.1 {
scheme.is_http2 = is_h2; // could be adjusted further
}
innervec.push(scheme);
innervec.push(Arc::from(scheme));
} else {
warn!("Dead Upstream : {}", link);
}
} else {
innervec.push(scheme);
innervec.push(Arc::from(scheme));
}
// let resp = http_request(&link, method, "", &client).await;
// if resp.0 {
// if resp.1 {
// scheme.is_http2 = is_h2; // could be adjusted further
// }
// innervec.push(scheme);
// } else {
// warn!("Dead Upstream : {}", link);
// }
}
inner.insert(path.clone(), (innervec, AtomicUsize::new(0)));
}
@@ -110,45 +117,31 @@ 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) => {
let status = response.status().as_u16();
((99..499).contains(&status), false)
}
None => (ping_grpc(&url).await, true),
None => (ping_grpc(url).await, true),
}
}
pub async fn ping_grpc(addr: &str) -> bool {
let endpoint_result = Endpoint::from_shared(addr.to_owned());
if let Ok(endpoint) = endpoint_result {
let endpoint = endpoint.timeout(Duration::from_secs(2));
match tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await {
Ok(Ok(_channel)) => true,
_ => false,
}
} else {
false
}
let endpoint = match Endpoint::from_shared(addr.to_owned()) {
Ok(e) => e.timeout(Duration::from_secs(2)),
Err(_) => return false,
};
tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await.ok().and_then(Result::ok).is_some()
}
async fn detect_tls(ip: &str, port: &u16, client: &Client) -> (bool, Option<Version>) {
let https_url = format!("https://{}:{}", ip, port);
match client.get(&https_url).send().await {
Ok(response) => {
// println!("{} => {:?} (HTTPS)", https_url, response.version());
return (true, Some(response.version()));
}
_ => {}
if let Ok(response) = client.get(&https_url).send().await {
return (true, Some(response.version()));
}
let http_url = format!("http://{}:{}", ip, port);
match client.get(&http_url).send().await {
Ok(response) => {
// println!("{} => {:?} (HTTP)", http_url, response.version());
(false, Some(response.version()))
}
Ok(response) => (false, Some(response.version())),
Err(_) => {
if ping_grpc(&http_url).await {
(false, Some(Version::HTTP_2))
+85
View File
@@ -0,0 +1,85 @@
use crate::utils::kuberconsul::{match_path, ConsulService, KubeEndpoints};
use crate::utils::structs::{GlobalServiceMapping, InnerMap};
use axum::http::{HeaderMap, HeaderValue};
use dashmap::DashMap;
use reqwest::Client;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::time::Duration;
pub async fn for_consul(url: String, token: Option<String>, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().ok()?;
let mut headers = HeaderMap::new();
if let Some(token) = token {
headers.insert("X-Consul-Token", HeaderValue::from_str(&token).unwrap());
}
let to = Duration::from_secs(1);
let resp = client.get(url).timeout(to).send().await.ok()?;
if !resp.status().is_success() {
eprintln!("Consul API returned status: {}", resp.status());
return None;
}
let mut inner_vec = Vec::new();
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
let endpoints: Vec<ConsulService> = resp.json().await.ok()?;
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;
let to_add = Arc::from(InnerMap {
address: Arc::from(&*addr),
port: prt,
is_ssl: false,
is_http2: false,
to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit,
x4xx_limit: conf.x4xx_limit,
redirect_to: None,
healthcheck: None,
authorization: None,
});
inner_vec.push(to_add);
}
match_path(conf, &upstreams, inner_vec);
Some(upstreams)
}
pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
let to = Duration::from_secs(10);
let client = Client::builder().timeout(Duration::from_secs(10)).danger_accept_invalid_certs(true).build().ok()?;
let resp = client.get(url).timeout(to).bearer_auth(token).send().await.ok()?;
if !resp.status().is_success() {
eprintln!("Kubernetes API returned status: {}", resp.status());
return None;
}
let endpoints: KubeEndpoints = resp.json().await.ok()?;
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
if let Some(subsets) = endpoints.subsets {
for subset in subsets {
if let (Some(addresses), Some(ports)) = (subset.addresses, subset.ports) {
let mut inner_vec = Vec::new();
for addr in addresses {
for port in &ports {
// let redirect_link = conf.redirect_to.as_ref().map(|www| Arc::from(www.as_str()));
let to_add = Arc::from(InnerMap {
address: Arc::from(addr.ip.clone()),
port: port.port,
is_ssl: false,
is_http2: false,
to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit,
x4xx_limit: conf.x4xx_limit,
healthcheck: None,
redirect_to: None,
authorization: None,
});
inner_vec.push(to_add);
}
}
match_path(conf, &upstreams, inner_vec.clone());
}
}
}
Some(upstreams)
}
+85 -8
View File
@@ -1,16 +1,93 @@
use ahash::AHasher;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use moka::sync::Cache;
use moka::Expiry;
use serde::{Deserialize, Serialize};
use std::env;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant, SystemTime};
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Claims {
pub(crate) user: String,
pub(crate) exp: u64,
pub struct Claims {
pub master_key: String,
pub owner: String,
pub exp: u64,
pub random: Option<String>,
}
pub fn check_jwt(input: &str, secret: &str) -> bool {
let validation = Validation::new(Algorithm::HS256);
let token_data = decode::<Claims>(&input, &DecodingKey::from_secret(secret.as_ref()), &validation);
match token_data {
Ok(_) => true,
#[derive(Debug, Deserialize)]
struct Expired {
exp: Option<u64>,
}
static JWT_VALIDATION: LazyLock<Validation> = LazyLock::new(|| Validation::new(Algorithm::HS256));
pub static JWT_TOKEN: LazyLock<Option<Arc<str>>> = LazyLock::new(|| match env::var("JWT_KEY") {
Ok(key) if !key.is_empty() => Some(Arc::from(key.as_str())),
_ => None,
});
static JWT_CACHE: LazyLock<Cache<u64, u64>> = LazyLock::new(|| Cache::builder().max_capacity(100_000).expire_after(JwtExpiry).build());
struct JwtExpiry;
impl Expiry<u64, u64> for JwtExpiry {
fn expire_after_create(&self, _key: &u64, value: &u64, _current_time: Instant) -> Option<Duration> {
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if *value > now {
Some(Duration::from_secs(value - now))
} else {
Some(Duration::ZERO)
}
}
}
pub fn check_jwt(token: &str, secret: &str) -> bool {
let key = hash_token(token, secret);
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if let Some(exp) = JWT_CACHE.get(&key) {
if exp < now {
return false;
}
return true;
}
match is_expired(token, now) {
Ok(true) => return false,
Ok(false) => {}
Err(_) => return false,
}
match decode::<Claims>(token, &DecodingKey::from_secret(secret.as_ref()), &JWT_VALIDATION) {
Ok(data) => {
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if data.claims.exp > now {
JWT_CACHE.insert(key, data.claims.exp);
true
} else {
false
}
}
Err(_) => false,
}
}
fn is_expired(token: &str, now: u64) -> Result<bool, Box<dyn std::error::Error>> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err("Invalid JWT format".into());
}
let decoded = URL_SAFE_NO_PAD.decode(parts[1])?;
let claims: Expired = serde_json::from_slice(&decoded)?;
if let Some(exp) = claims.exp {
Ok(exp < now)
} else {
Ok(true)
}
}
fn hash_token(token: &str, secret: &str) -> u64 {
let mut hasher = AHasher::default();
token.hash(&mut hasher);
secret.hash(&mut hasher);
hasher.finish()
}
-133
View File
@@ -1,133 +0,0 @@
use crate::utils::kuberconsul::{list_to_upstreams, match_path};
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, InnerMap, ServiceMapping, UpstreamsDashMap};
use crate::utils::tools::{clone_dashmap_into, compare_dashmaps, print_upstreams};
use dashmap::DashMap;
use futures::channel::mpsc::Sender;
use futures::SinkExt;
use pingora::prelude::sleep;
use rand::Rng;
use reqwest::Client;
use std::env;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::time::Duration;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
#[derive(Debug, serde::Deserialize)]
struct Endpoints {
subsets: Option<Vec<Subset>>,
}
#[derive(Debug, serde::Deserialize)]
struct Subset {
addresses: Option<Vec<Address>>,
ports: Option<Vec<Port>>,
}
#[derive(Debug, serde::Deserialize)]
struct Address {
ip: String,
}
#[derive(Debug, serde::Deserialize)]
struct Port {
port: u16,
}
pub async fn start(mut toreturn: Sender<Configuration>, config: Arc<Configuration>) {
let prev_upstreams = UpstreamsDashMap::new();
loop {
if let Some(kuber) = config.kubernetes.clone() {
let upstreams = UpstreamsDashMap::new();
let path = kuber.tokenpath.unwrap_or("/var/run/secrets/kubernetes.io/serviceaccount/token".to_string());
let token = read_token(path.as_str()).await;
let servers = kuber.servers.unwrap_or(vec![format!(
"{}:{}",
env::var("KUBERNETES_SERVICE_HOST").unwrap_or("0.0.0.0".to_string()),
env::var("KUBERNETES_SERVICE_PORT_HTTPS").unwrap_or("0".to_string())
)]);
let end = servers.len() - 1;
let mut num = 0;
if end > 0 {
num = rand::rng().random_range(0..end);
}
let server = servers.get(num).unwrap().to_string();
if let Some(svc) = kuber.services {
for i in svc {
let header_list = DashMap::new();
let mut hl = Vec::new();
build_headers(&i.headers, config.as_ref(), &mut hl);
if hl.len() > 0 {
header_list.insert(i.path.clone().unwrap_or("/".to_string()), hl);
config.headers.insert(i.hostname.clone(), header_list);
}
let url = format!("https://{}/api/v1/namespaces/staging/endpoints/{}", server, i.hostname);
let list = get_by_http(&*url, &*token, &i).await;
list_to_upstreams(list, &upstreams, &i);
}
}
if !compare_dashmaps(&upstreams, &prev_upstreams) {
let tosend: Configuration = Configuration {
upstreams: Default::default(),
headers: config.headers.clone(),
consul: config.consul.clone(),
kubernetes: config.kubernetes.clone(),
typecfg: config.typecfg.clone(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(&upstreams, &prev_upstreams);
clone_dashmap_into(&upstreams, &tosend.upstreams);
print_upstreams(&tosend.upstreams);
toreturn.send(tosend).await.unwrap();
}
}
sleep(Duration::from_secs(5)).await;
}
}
pub async fn get_by_http(url: &str, token: &str, conf: &ServiceMapping) -> Option<DashMap<String, (Vec<InnerMap>, AtomicUsize)>> {
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().ok()?;
let to = Duration::from_secs(1);
let resp = client.get(url).timeout(to).bearer_auth(token).send().await.ok()?;
if !resp.status().is_success() {
eprintln!("Kubernetes API returned status: {}", resp.status());
return None;
}
let endpoints: Endpoints = resp.json().await.ok()?;
let upstreams: DashMap<String, (Vec<InnerMap>, AtomicUsize)> = DashMap::new();
if let Some(subsets) = endpoints.subsets {
for subset in subsets {
if let (Some(addresses), Some(ports)) = (subset.addresses, subset.ports) {
let mut inner_vec = Vec::new();
for addr in addresses {
for port in &ports {
let to_add = InnerMap {
address: addr.ip.clone(),
port: port.port.clone(),
is_ssl: false,
is_http2: false,
to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit,
healthcheck: None,
};
inner_vec.push(to_add);
}
}
match_path(&conf, &upstreams, inner_vec.clone());
}
}
}
Some(upstreams)
}
async fn read_token(path: &str) -> String {
let mut file = File::open(path).await.unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).await.unwrap();
contents.trim().to_string()
}
+207 -8
View File
@@ -1,29 +1,228 @@
use crate::utils::structs::{InnerMap, ServiceMapping, UpstreamsDashMap};
use crate::utils::httpclient;
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, GlobalServiceMapping, InnerMap, UpstreamsDashMap};
use crate::utils::tools::{clone_dashmap_into, compare_dashmaps, print_upstreams};
use async_trait::async_trait;
use dashmap::DashMap;
use pingora::prelude::sleep;
use rand::RngExt;
use serde::Deserialize;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::time::Duration;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender;
pub fn list_to_upstreams(lt: Option<DashMap<String, (Vec<InnerMap>, AtomicUsize)>>, upstreams: &UpstreamsDashMap, i: &ServiceMapping) {
#[derive(Debug, serde::Deserialize)]
pub struct KubeEndpoints {
pub subsets: Option<Vec<KubeSubset>>,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubeSubset {
pub addresses: Option<Vec<KubeAddress>>,
pub ports: Option<Vec<KubePort>>,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubeAddress {
pub ip: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubePort {
pub port: u16,
}
#[derive(Debug, Deserialize)]
pub struct ConsulService {
#[serde(rename = "ServiceTaggedAddresses")]
pub tagged_addresses: HashMap<String, ConsulTaggedAddress>,
}
#[derive(Debug, Deserialize)]
pub struct ConsulTaggedAddress {
#[serde(rename = "Address")]
pub address: String,
#[serde(rename = "Port")]
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) {
if let Some(list) = lt {
match upstreams.get(&i.hostname.clone()) {
match upstreams.get(&*i.hostname.clone()) {
Some(upstr) => {
for (k, v) in list {
upstr.value().insert(k, v);
upstr.value().insert(k.to_owned(), v);
}
}
None => {
upstreams.insert(i.hostname.clone(), list);
upstreams.insert(Arc::from(i.hostname.clone()), list);
}
};
}
}
pub fn match_path(conf: &ServiceMapping, upstreams: &DashMap<String, (Vec<InnerMap>, AtomicUsize)>, values: Vec<InnerMap>) {
pub fn match_path(conf: &GlobalServiceMapping, upstreams: &DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>, values: Vec<Arc<InnerMap>>) {
match conf.path {
Some(ref p) => {
upstreams.insert(p.to_string(), (values, AtomicUsize::new(0)));
upstreams.insert(Arc::from(p.clone()), (values, AtomicUsize::new(0)));
}
None => {
upstreams.insert("/".to_string(), (values, AtomicUsize::new(0)));
upstreams.insert(Arc::from("/"), (values, AtomicUsize::new(0)));
}
}
}
async fn read_token(path: &str) -> String {
let mut file = File::open(path).await.unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).await.unwrap();
contents.trim().to_string()
}
#[async_trait]
pub trait ServiceDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>);
}
pub struct KubernetesDiscovery;
pub struct ConsulDiscovery;
#[async_trait]
impl ServiceDiscovery for KubernetesDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
let prev_upstreams = UpstreamsDashMap::new();
if let Some(kuber) = config.kubernetes.clone() {
let servers = kuber.servers.unwrap_or(vec![format!(
"{}:{}",
env::var("KUBERNETES_SERVICE_HOST").unwrap_or("0.0.0.0".to_string()),
env::var("KUBERNETES_SERVICE_PORT_HTTPS").unwrap_or("0".to_string())
)]);
let end = servers.len().saturating_sub(1);
let num = if end > 0 { rand::rng().random_range(0..end) } else { 0 };
let server = servers.get(num).unwrap().to_string();
let path = kuber.tokenpath.unwrap_or("/var/run/secrets/kubernetes.io/serviceaccount/token".to_string());
let namespace = get_current_namespace().unwrap_or_else(|| "default".to_string());
let token = read_token(path.as_str()).await;
loop {
let upstreams = UpstreamsDashMap::new();
if let Some(kuber) = config.kubernetes.clone() {
if let Some(svc) = kuber.services {
for service in svc {
let header_list: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut hl = Vec::new();
build_headers(&service.client_headers, config.as_ref(), &mut hl);
if !hl.is_empty() {
match service.path.clone() {
Some(path) => {
header_list.insert(Arc::from(path.as_str()), hl);
}
None => {
header_list.insert(Arc::from("/"), hl);
}
}
// header_list.insert(Arc::from(path.as_str()), hl);
// header_list.insert(Arc::from(i.path).unwrap_or(Arc::from("/")).as_str(), hl);
config.client_headers.insert(Arc::from(service.hostname.clone()), header_list);
}
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 list = httpclient::for_kuber(&url, &token, &service).await;
// println!("{:?}", list);
list_to_upstreams(list, &upstreams, &service);
}
}
if let Some(lt) = clone_compare(&upstreams, &prev_upstreams, &config).await {
toreturn.send(lt).await.unwrap();
}
}
sleep(Duration::from_secs(5)).await;
}
}
}
}
fn get_current_namespace() -> Option<String> {
let ns_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
if Path::new(ns_path).exists() {
if let Ok(contents) = fs::read_to_string(ns_path) {
return Some(contents.trim().to_string());
}
}
std::env::var("POD_NAMESPACE").ok()
}
#[async_trait]
impl ServiceDiscovery for ConsulDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
let prev_upstreams = UpstreamsDashMap::new();
loop {
let upstreams = UpstreamsDashMap::new();
if let Some(consul) = config.consul.clone() {
let servers = consul.servers.unwrap_or(vec![format!(
"{}:{}",
env::var("CONSUL_SERVICE_HOST").unwrap_or("0.0.0.0".to_string()),
env::var("CONSUL_SERVICE_PORT").unwrap_or("0".to_string())
)]);
let end = servers.len().saturating_sub(1);
let num = if end > 0 { rand::rng().random_range(0..end) } else { 0 };
let consul_data = servers.get(num).unwrap().to_string();
let ss = consul_data + "/v1/catalog/service/";
if let Some(svc) = consul.services {
for i in svc {
let header_list = DashMap::new();
let mut hl = Vec::new();
build_headers(&i.client_headers, config.as_ref(), &mut hl);
if !hl.is_empty() {
match i.path.clone() {
Some(path) => {
header_list.insert(Arc::from(path.as_str()), hl);
}
None => {
header_list.insert(Arc::from("/"), hl);
}
}
// header_list.insert(i.path.clone().unwrap_or("/".to_string()), hl);
config.client_headers.insert(Arc::from(i.hostname.clone()), header_list);
}
let pref = ss.clone() + &i.upstream;
let list = httpclient::for_consul(pref, consul.token.clone(), &i).await;
list_to_upstreams(list, &upstreams, &i);
}
}
}
if let Some(lt) = clone_compare(&upstreams, &prev_upstreams, &config).await {
toreturn.send(lt).await.unwrap();
}
sleep(Duration::from_secs(5)).await;
}
}
}
async fn clone_compare(upstreams: &UpstreamsDashMap, prev_upstreams: &UpstreamsDashMap, config: &Arc<Configuration>) -> Option<Configuration> {
if !compare_dashmaps(upstreams, prev_upstreams) {
let tosend: Configuration = Configuration {
upstreams: Default::default(),
client_headers: config.client_headers.clone(),
server_headers: config.server_headers.clone(),
consul: config.consul.clone(),
kubernetes: config.kubernetes.clone(),
typecfg: config.typecfg.clone(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(upstreams, prev_upstreams);
clone_dashmap_into(upstreams, &tosend.upstreams);
print_upstreams(&tosend.upstreams, &tosend.extraparams);
return Some(tosend);
};
None
}
+19
View File
@@ -0,0 +1,19 @@
use dashmap::DashMap;
use moka::sync::Cache;
use pingora_cache::eviction::simple_lru::Manager;
use pingora_cache::lock::CacheLock;
use pingora_cache::MemCache;
use pingora_limits::rate::Rate;
use std::net::IpAddr;
use std::sync::OnceLock;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
pub static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(DashMap::new);
pub static RATE_LIMITER: LazyLock<Rate> = LazyLock::new(|| Rate::new(Duration::from_secs(1)));
pub static REQUESTS_4XX: LazyLock<Cache<IpAddr, u32>> = LazyLock::new(|| Cache::builder().time_to_live(Duration::from_secs(1)).build());
pub static LOCALHOST: LazyLock<Arc<str>> = LazyLock::new(|| Arc::from("localhost"));
pub static MEM_CACHE: LazyLock<MemCache> = LazyLock::new(MemCache::new);
pub static CACHE_LOCK: LazyLock<CacheLock> = LazyLock::new(|| CacheLock::new(Duration::from_secs(30)));
pub static EVICTION: OnceLock<Manager> = OnceLock::new();
pub static CACHE_TTL: OnceLock<u64> = OnceLock::new();
+73 -47
View File
@@ -1,63 +1,89 @@
use crate::utils::lazylock::EVICTION;
use pingora_cache::eviction::EvictionManager;
use pingora_http::Method;
use pingora_http::StatusCode;
use pingora_http::Version;
use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, Histogram, IntCounter, IntCounterVec};
use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, register_int_gauge, Histogram, IntCounter, IntCounterVec, IntGauge};
use std::sync::Arc;
use std::sync::LazyLock;
use std::time::Duration;
pub struct MetricTypes {
pub method: String,
pub code: String,
pub method: Method,
pub upstream: Arc<str>,
pub code: Option<StatusCode>,
pub latency: Duration,
pub version: Version,
}
lazy_static::lazy_static! {
pub static ref REQUEST_COUNT: IntCounter = register_int_counter!(
"aralez_requests_total",
"Total number of requests handled by Aralez"
).unwrap();
pub static ref RESPONSE_CODES: IntCounterVec = register_int_counter_vec!(
"aralez_responses_total",
"Responses grouped by status code",
&["status"]
).unwrap();
pub static ref REQUEST_LATENCY: Histogram = register_histogram!(
"aralez_request_latency_seconds",
"Request latency in seconds",
vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
).unwrap();
pub static ref RESPONSE_LATENCY: Histogram = register_histogram!(
pub static OPEN_FILES: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_open_files", "Number of open file descriptors").unwrap());
pub static LOGGING_ERRORS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_logging_errors", "Number of log errors").unwrap());
pub static MEMORY_USAGE: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_memory_bytes", "Total memory allocated in bytes").unwrap());
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 CACHE_SIZE_BYTES: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_size_bytes", "Current cache size in bytes").unwrap());
pub static CACHE_ITEMS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_items", "Current number of cached objects").unwrap());
pub static CACHE_EVICTED_BYTES: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_evicted_bytes_total", "Total bytes evicted from cache").unwrap());
pub static CACHE_EVICTED_ITEMS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_evicted_items_total", "Total cache items evicted").unwrap());
pub static RESPONSE_CODES: LazyLock<IntCounterVec> =
LazyLock::new(|| register_int_counter_vec!("aralez_responses_total", "Responses grouped by status code", &["status"]).unwrap());
pub static RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
register_histogram!(
"aralez_response_latency_seconds",
"Response latency in seconds",
vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0]
).unwrap();
pub static ref REQUESTS_BY_METHOD: IntCounterVec = register_int_counter_vec!(
"aralez_requests_by_method_total",
"Number of requests by HTTP method",
&["method"]
).unwrap();
pub static ref REQUESTS_BY_VERSION: IntCounterVec = register_int_counter_vec!(
"aralez_requests_by_version_total",
"Number of requests by HTTP versions",
&["version"]
).unwrap();
pub static ref ERROR_COUNT: IntCounter = register_int_counter!(
"aralez_errors_total",
"Total number of errors"
).unwrap();
}
vec![0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0]
)
.unwrap()
});
pub static REQUESTS_BY_METHOD: LazyLock<IntCounterVec> =
LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_method_total", "Number of requests by HTTP method", &["method"]).unwrap());
pub static REQUESTS_BY_UPSTREAM: LazyLock<IntCounterVec> =
LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_upstream", "Number of requests by UPSTREAM server", &["upstream"]).unwrap());
pub static REQUESTS_BY_VERSION: LazyLock<IntCounterVec> =
LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_version_total", "Number of requests by HTTP versions", &["version"]).unwrap());
pub fn calc_metrics(metric_types: &MetricTypes) {
REQUEST_COUNT.inc();
let timer = REQUEST_LATENCY.start_timer();
timer.observe_duration();
let version_str = match &metric_types.version {
&Version::HTTP_11 => "HTTP/1.1",
&Version::HTTP_2 => "HTTP/2.0",
&Version::HTTP_3 => "HTTP/3.0",
&Version::HTTP_10 => "HTTP/1.0",
let version_str = match metric_types.version {
Version::HTTP_11 => "HTTP/1.1",
Version::HTTP_2 => "HTTP/2.0",
Version::HTTP_3 => "HTTP/3.0",
Version::HTTP_10 => "HTTP/1.0",
_ => "Unknown",
};
REQUESTS_BY_VERSION.with_label_values(&[&version_str]).inc();
RESPONSE_CODES.with_label_values(&[&metric_types.code.to_string()]).inc();
REQUESTS_BY_METHOD.with_label_values(&[&metric_types.method]).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();
REQUESTS_BY_METHOD.with_label_values(&[metric_types.method.as_str()]).inc();
REQUESTS_BY_UPSTREAM.with_label_values(&[metric_types.upstream.as_ref()]).inc();
RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64());
if let Some(eviction) = EVICTION.get() {
CACHE_SIZE_BYTES.set(eviction.total_size() as i64);
CACHE_ITEMS.set(eviction.total_items() as i64);
CACHE_EVICTED_BYTES.set(eviction.evicted_size() as i64);
CACHE_EVICTED_ITEMS.set(eviction.evicted_items() as i64);
}
}
pub(crate) fn get_memory_usage() -> usize {
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|s| {
s.lines()
.find(|l| l.starts_with("VmRSS:"))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|v| v.parse::<usize>().ok())
})
.unwrap_or(0)
* 1024
}
pub fn get_open_files() -> usize {
std::fs::read_dir("/proc/self/fd").map(|dir| dir.count()).unwrap_or(0)
}
+177 -89
View File
@@ -1,99 +1,193 @@
use crate::utils::healthcheck;
use crate::utils::lazylock::REVERSE_STORE;
use crate::utils::state::{is_first_run, mark_not_first_run};
use crate::utils::structs::*;
use crate::utils::tools::{clone_dashmap, clone_dashmap_into, print_upstreams};
use crate::web::logging::log_builder;
use dashmap::DashMap;
use log::{error, info, warn};
use std::collections::HashMap;
use std::path::Path;
use std::sync::atomic::AtomicUsize;
// use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::{Arc, LazyLock};
use std::{env, fs};
// use tokio::sync::oneshot::{Receiver, Sender};
pub async fn load_configuration(d: &str, kind: &str) -> Option<Configuration> {
pub static DOMAINS: LazyLock<DashMap<String, bool>> = LazyLock::new(DashMap::new);
pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>, String) {
let mut conf_files = Vec::new();
let yaml_data = match kind {
"filepath" => match fs::read_to_string(d) {
Ok(data) => {
info!("Reading upstreams from {}", d);
data
"filepath" => {
let mut data = String::new();
let mut last_error = None;
for _ in 0..5 {
match fs::read_to_string(d) {
Ok(content) => {
if !content.trim().is_empty() {
data = content;
break;
}
}
Err(e) => {
error!("Config read failed, retrying...");
last_error = Some(e);
}
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
Err(e) => {
error!("Reading: {}: {:?}", d, e);
if data.is_empty() {
let err_msg = match last_error {
Some(e) => {
error!("Reading: {}: {:?}", d, e);
e.to_string()
}
None => {
error!("Reading: {}: File is empty after retries", d);
"File is empty".to_string()
}
};
warn!("Running with empty upstreams list, update it via API");
return None;
return (None, err_msg);
}
},
let mut confdir = Path::new(d).parent().unwrap().to_path_buf();
let mut autocfg = Path::new(d).parent().unwrap().to_path_buf();
autocfg.push("autoconfigs");
if fs::metadata(autocfg.clone()).is_err() {
fs::create_dir_all(autocfg.clone()).ok();
}
autocfg.push("domains.json");
if autocfg.exists() {
let json: Option<Vec<String>> = fs::read_to_string(autocfg).ok().and_then(|s| serde_json::from_str(&s).ok());
if let Some(domains) = json {
for domain in domains {
DOMAINS.insert(domain, true);
}
}
}
confdir.push("conf.d");
if let Ok(entries) = fs::read_dir(&confdir) {
let mut paths: Vec<_> = entries
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("yaml"))
.collect();
paths.sort();
for path in paths {
let content = fs::read_to_string(&path);
match content {
Ok(content) => {
conf_files.push(content);
}
Err(e) => {
error!("Reading: {}: {:?}", path.display(), e)
}
};
}
}
info!("Reading upstreams from {}", d);
data // [2606:4700:2ff9::1]:443
}
"content" => {
info!("Reading upstreams from API post body");
d.to_string()
}
_ => {
error!("Mismatched parameter, only filepath|content is allowed");
return None;
return (None, "Mismatched parameter, only filepath|content is allowed".to_string());
}
};
let parsed: Config = match serde_yaml::from_str(&yaml_data) {
let mut parsed: Config = match noyalib::from_str(&yaml_data) {
Ok(cfg) => cfg,
Err(e) => {
error!("Failed to parse upstreams file: {}", e);
return None;
return (None, e.to_string());
}
};
let mut toreturn = Configuration::default();
if let Some(ref mut upstreams) = parsed.upstreams {
for uconf in conf_files {
let p: HashMap<String, HostConfig> = match noyalib::from_str(&uconf) {
Ok(ucfg) => ucfg,
Err(e) => {
error!("Failed to parse upstreams file: {}", e);
return (None, e.to_string());
}
};
upstreams.extend(p);
}
}
let mut toreturn = Configuration::default();
populate_headers_and_auth(&mut toreturn, &parsed).await;
toreturn.typecfg = parsed.provider.clone();
match parsed.provider.as_str() {
"file" => {
populate_file_upstreams(&mut toreturn, &parsed).await;
Some(toreturn)
(Some(toreturn), "Ok".to_string())
}
"consul" => {
toreturn.consul = parsed.consul;
toreturn.consul.is_some().then_some(toreturn)
(toreturn.consul.is_some().then_some(toreturn), "Ok".to_string())
}
"kubernetes" => {
toreturn.kubernetes = parsed.kubernetes;
toreturn.kubernetes.is_some().then_some(toreturn)
(toreturn.kubernetes.is_some().then_some(toreturn), "Ok".to_string())
}
_ => {
warn!("Unknown provider {}", parsed.provider);
None
(None, "Unknown provider".to_string())
}
}
}
async fn populate_headers_and_auth(config: &mut Configuration, parsed: &Config) {
if let Some(headers) = &parsed.headers {
let mut hl = Vec::new();
let mut ch: Vec<(String, Arc<str>)> = Vec::new();
if let Some(headers) = &parsed.client_headers {
for header in headers {
if let Some((key, val)) = header.split_once(':') {
hl.push((key.trim().to_string(), val.trim().to_string()));
ch.push((key.to_string(), Arc::from(val)));
}
}
let global_headers = DashMap::new();
global_headers.insert("/".to_string(), hl);
config.headers.insert("GLOBAL_HEADERS".to_string(), global_headers);
}
config.extraparams.sticky_sessions = parsed.sticky_sessions;
let global_headers: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
global_headers.insert(Arc::from("/"), ch);
config.client_headers.insert(Arc::from("GLOBAL_CLIENT_HEADERS"), global_headers);
let mut sh: Vec<(String, Arc<str>)> = Vec::new();
if let Some(headers) = &parsed.server_headers {
for header in headers {
if let Some((key, val)) = header.split_once(':') {
sh.push((key.to_string(), Arc::from(val.trim())));
}
}
}
let server_global_headers: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
server_global_headers.insert(Arc::from("/"), sh);
config.server_headers.insert(Arc::from("GLOBAL_SERVER_HEADERS"), server_global_headers);
config.extraparams.to_https = parsed.to_https;
config.extraparams.sticky_sessions = parsed.sticky_sessions;
config.extraparams.rate_limit = parsed.rate_limit;
config.extraparams.x4xx_limit = parsed.x4xx_limit;
if let Some(rate) = &parsed.rate_limit {
info!("Applied Global Rate Limit : {} request per second", rate);
}
if let Some(auth) = &parsed.authorization {
let name = auth.get("type").unwrap_or(&"".to_string()).to_string();
let creds = auth.get("creds").unwrap_or(&"".to_string()).to_string();
config.extraparams.authentication.insert("authorization".to_string(), vec![name, creds]);
} else {
config.extraparams.authentication = DashMap::new();
if let Some(pa) = &parsed.authorization {
let y: InnerAuth = InnerAuth {
auth_type: Arc::from(pa.auth_type.clone()),
auth_cred: Arc::from(pa.auth_cred.clone().unwrap_or_default()),
};
config.extraparams.authentication = Some(Arc::from(y));
}
}
@@ -102,36 +196,53 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
if let Some(upstreams) = &parsed.upstreams {
for (hostname, host_config) in upstreams {
let path_map = DashMap::new();
let header_list = DashMap::new();
let client_header_list = DashMap::new();
let server_header_list = DashMap::new();
for (path, path_config) in &host_config.paths {
if let Some(rate) = &path_config.rate_limit {
info!("Applied Rate Limit for {} : {} request per second", hostname, rate);
}
let mut hl: Vec<(String, String)> = Vec::new();
build_headers(&path_config.headers, config, &mut hl);
header_list.insert(path.clone(), hl);
let mut hl: Vec<(String, Arc<str>)> = Vec::new();
let mut sl: Vec<(String, Arc<str>)> = Vec::new();
build_headers(&path_config.client_headers, config, &mut hl);
build_headers(&path_config.server_headers, config, &mut sl);
client_header_list.insert(Arc::from(path.as_str()), hl);
server_header_list.insert(Arc::from(path.as_str()), sl);
let mut server_list = Vec::new();
for server in &path_config.servers {
if let Some((ip, port_str)) = server.split_once(':') {
let mut path_auth: Option<Arc<InnerAuth>> = None;
if let Some(pa) = &path_config.authorization {
let y: InnerAuth = InnerAuth {
auth_type: Arc::from(pa.auth_type.clone()),
auth_cred: Arc::from(pa.auth_cred.clone().unwrap_or_default()),
};
path_auth = Some(Arc::from(y));
}
let redirect_link = path_config.redirect_to.as_ref().map(|www| Arc::from(www.as_str()));
if let Some((ip, port_str)) = server.rsplit_once(':') {
if let Ok(port) = port_str.parse::<u16>() {
server_list.push(InnerMap {
address: ip.trim().to_string(),
server_list.push(Arc::from(InnerMap {
address: Arc::from(ip),
port,
is_ssl: true,
is_ssl: false,
is_http2: false,
to_https: path_config.to_https.unwrap_or(false),
rate_limit: path_config.rate_limit,
x4xx_limit: path_config.x4xx_limit,
healthcheck: path_config.healthcheck,
});
redirect_to: redirect_link,
authorization: path_auth,
}));
}
}
}
path_map.insert(path.clone(), (server_list, AtomicUsize::new(0)));
path_map.insert(Arc::from(path.clone()), (server_list, AtomicUsize::new(0)));
}
config.headers.insert(hostname.clone(), header_list);
imtdashmap.insert(hostname.clone(), path_map);
config.client_headers.insert(Arc::from(hostname.clone()), client_header_list);
config.server_headers.insert(Arc::from(hostname.clone()), server_header_list);
imtdashmap.insert(Arc::from(hostname.clone()), path_map);
}
if is_first_run() {
@@ -143,31 +254,34 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
clone_dashmap_into(&r, &config.upstreams);
}
info!("Upstream Config:");
print_upstreams(&config.upstreams);
REVERSE_STORE.clear();
print_upstreams(&config.upstreams, &config.extraparams);
}
}
pub fn parce_main_config(path: &str) -> AppConfig {
let data = fs::read_to_string(path).unwrap();
let reply = DashMap::new();
let cfg: HashMap<String, String> = serde_yaml::from_str(&*data).expect("Failed to parse main config file");
let mut cfo: AppConfig = serde_yaml::from_str(&*data).expect("Failed to parse main config file");
log_builder(&cfo);
let mut cfo: AppConfig = noyalib::from_str(&data).expect("Failed to parse main config file");
if let Ok(jwt_key) = env::var("JWT_KEY") {
cfo.master_key = Some(jwt_key);
};
log_builder(&cfo, &cfo.log_file);
cfo.hc_method = cfo.hc_method.to_uppercase();
for (k, v) in cfg {
reply.insert(k.to_string(), v.to_string());
}
if let Some((ip, port_str)) = cfo.config_address.split_once(':') {
if let Some((ip, port_str)) = cfo.config_address.rsplit_once(':') {
if let Ok(port) = port_str.parse::<u16>() {
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((_, port_str)) = tlsport_cfg.rsplit_once(':') {
cfo.proxy_port_tls = Some(port_str.to_string());
}
};
if let Some((_, port_str)) = cfo.proxy_address_http.rsplit_once(':') {
cfo.proxy_port = Some(port_str.to_string());
}
cfo.proxy_tls_grade = parce_tls_grades(cfo.proxy_tls_grade.clone());
cfo
}
@@ -194,42 +308,16 @@ fn parce_tls_grades(what: Option<String>) -> Option<String> {
},
None => {
warn!("TLS grade not set, defaulting to: medium");
Some("b".to_string())
Some("medium".to_string())
}
}
}
fn log_builder(conf: &AppConfig) {
let log_level = conf.log_level.clone();
unsafe {
match log_level.as_str() {
"info" => env::set_var("RUST_LOG", "info"),
"error" => env::set_var("RUST_LOG", "error"),
"warn" => env::set_var("RUST_LOG", "warn"),
"debug" => env::set_var("RUST_LOG", "debug"),
"trace" => env::set_var("RUST_LOG", "trace"),
"off" => env::set_var("RUST_LOG", "off"),
_ => {
println!("Error reading log level, defaulting to: INFO");
env::set_var("RUST_LOG", "info")
}
}
}
env_logger::builder().init();
}
pub fn build_headers(path_config: &Option<Vec<String>>, config: &Configuration, hl: &mut Vec<(String, String)>) {
pub fn build_headers(path_config: &Option<Vec<String>>, _config: &Configuration, hl: &mut Vec<(String, Arc<str>)>) {
if let Some(headers) = &path_config {
for header in headers {
if let Some((key, val)) = header.split_once(':') {
hl.push((key.trim().to_string(), val.trim().to_string()));
}
}
if let Some(push) = config.headers.get("GLOBAL_HEADERS") {
for k in push.iter() {
for x in k.value() {
hl.push(x.to_owned());
}
hl.push((key.trim().to_string(), Arc::from(val.trim())));
}
}
}
+2 -3
View File
@@ -1,12 +1,11 @@
use once_cell::sync::Lazy;
use std::sync::RwLock;
use std::sync::{LazyLock, RwLock};
#[derive(Debug)]
pub struct SharedState {
pub first_run: bool,
}
pub static GLOBAL_STATE: Lazy<RwLock<SharedState>> = Lazy::new(|| RwLock::new(SharedState { first_run: true }));
pub static GLOBAL_STATE: LazyLock<RwLock<SharedState>> = LazyLock::new(|| RwLock::new(SharedState { first_run: true }));
pub fn mark_not_first_run() {
let mut state = GLOBAL_STATE.write().unwrap();
+84 -27
View File
@@ -2,83 +2,102 @@ use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
pub type UpstreamsDashMap = DashMap<String, DashMap<String, (Vec<InnerMap>, AtomicUsize)>>;
pub type UpstreamsDashMap = DashMap<Arc<str>, DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>;
pub type UpstreamsIdMap = DashMap<String, InnerMap>;
pub type Headers = DashMap<String, DashMap<String, Vec<(String, String)>>>;
pub type UpstreamsIdMap = DashMap<String, Arc<InnerMap>>;
pub type Headers = DashMap<Arc<str>, DashMap<Arc<str>, Vec<(String, Arc<str>)>>>;
#[derive(Clone, Debug, Default)]
pub struct Extraparams {
pub to_https: Option<bool>,
pub sticky_sessions: Option<u64>,
pub authentication: Option<Arc<InnerAuth>>,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ServiceMapping {
pub struct GlobalServiceMapping {
pub upstream: String,
pub hostname: String,
pub path: Option<String>,
pub to_https: Option<bool>,
pub sticky_sessions: Option<u64>,
pub rate_limit: Option<isize>,
pub headers: Option<Vec<String>>,
pub x4xx_limit: Option<u32>,
pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>,
}
// pub type Services = DashMap<String, Vec<(String, Option<String>)>>;
#[derive(Clone, Debug, Default)]
pub struct Extraparams {
pub sticky_sessions: bool,
pub to_https: Option<bool>,
pub authentication: DashMap<String, Vec<String>>,
pub rate_limit: Option<isize>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Kubernetes {
pub servers: Option<Vec<String>>,
pub services: Option<Vec<ServiceMapping>>,
pub services: Option<Vec<GlobalServiceMapping>>,
pub tokenpath: Option<String>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Consul {
pub servers: Option<Vec<String>>,
pub services: Option<Vec<ServiceMapping>>,
pub services: Option<Vec<GlobalServiceMapping>>,
pub token: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Config {
pub provider: String,
pub sticky_sessions: bool,
pub to_https: Option<bool>,
pub sticky_sessions: Option<u64>,
#[serde(default)]
pub upstreams: Option<HashMap<String, HostConfig>>,
#[serde(default)]
pub globals: Option<HashMap<String, Vec<String>>>,
#[serde(default)]
pub headers: Option<Vec<String>>,
pub client_headers: Option<Vec<String>>,
#[serde(default)]
pub authorization: Option<HashMap<String, String>>,
pub server_headers: Option<Vec<String>>,
#[serde(default)]
pub authorization: Option<Auth>,
#[serde(default)]
pub consul: Option<Consul>,
#[serde(default)]
pub kubernetes: Option<Kubernetes>,
#[serde(default)]
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct HostConfig {
pub paths: HashMap<String, PathConfig>,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Auth {
#[serde(rename = "type")]
pub auth_type: String,
#[serde(rename = "data")]
pub auth_cred: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct PathConfig {
pub servers: Vec<String>,
pub to_https: Option<bool>,
pub headers: Option<Vec<String>>,
pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub healthcheck: Option<bool>,
pub redirect_to: Option<String>,
pub authorization: Option<Auth>,
}
#[derive(Debug, Default)]
pub struct Configuration {
pub upstreams: UpstreamsDashMap,
pub headers: Headers,
pub client_headers: Headers,
pub server_headers: Headers,
pub consul: Option<Consul>,
pub kubernetes: Option<Kubernetes>,
pub typecfg: String,
@@ -91,7 +110,11 @@ pub struct AppConfig {
pub hc_method: String,
pub upstreams_conf: String,
pub log_level: String,
pub master_key: String,
pub cache_size_mb: Option<usize>,
pub cache_ttl: Option<u64>,
pub access_log: Option<String>,
pub pid_file: Option<String>,
pub master_key: Option<String>,
pub config_address: String,
pub proxy_address_http: String,
pub config_api_enabled: bool,
@@ -99,38 +122,72 @@ pub struct AppConfig {
pub config_tls_certificate: Option<String>,
pub config_tls_key_file: Option<String>,
pub proxy_address_tls: Option<String>,
pub proxy_port_tls: Option<u16>,
pub proxy_port_tls: Option<String>,
pub proxy_port: Option<String>,
pub local_server: Option<(String, u16)>,
pub proxy_certificates: Option<String>,
pub proxy_configs: Option<String>,
pub proxy_tls_grade: Option<String>,
pub file_server_address: Option<String>,
pub file_server_folder: Option<String>,
pub runuser: Option<String>,
pub rungroup: Option<String>,
pub log_file: Option<String>,
pub tcp_keepalive_idle: Option<u64>,
pub tcp_keepalive_interval: Option<u64>,
pub tcp_keepalive_count: Option<usize>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct InnerAuth {
pub auth_type: Arc<str>,
pub auth_cred: Arc<str>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InnerMap {
pub address: String,
pub address: Arc<str>,
pub port: u16,
pub is_ssl: bool,
pub is_http2: bool,
pub to_https: bool,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub healthcheck: Option<bool>,
pub redirect_to: Option<Arc<str>>,
pub authorization: Option<Arc<InnerAuth>>,
}
#[allow(dead_code)]
impl InnerMap {
pub fn new() -> Self {
Self {
address: Default::default(),
address: Arc::from("127.0.0.1"),
port: Default::default(),
is_ssl: Default::default(),
is_http2: Default::default(),
to_https: Default::default(),
rate_limit: Default::default(),
x4xx_limit: Default::default(),
healthcheck: Default::default(),
redirect_to: Default::default(),
authorization: Default::default(),
}
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct InnerMapForJson {
pub address: String,
pub port: u16,
pub is_ssl: bool,
pub is_http2: bool,
pub to_https: bool,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub healthcheck: Option<bool>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct UpstreamSnapshotForJson {
pub backends: Vec<InnerMapForJson>,
pub requests: usize,
}
+177 -68
View File
@@ -1,47 +1,52 @@
use crate::utils::structs::{InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::utils::tls;
use crate::utils::tls::CertificateConfig;
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::structs::{Extraparams, InnerMapForJson, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap};
use dashmap::DashMap;
use log::{error, info};
use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use port_check::is_port_reachable;
use privdrop::PrivDrop;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::any::type_name;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::fs::OpenOptions;
use std::io::Write as IoWrite;
use std::net::SocketAddr;
use std::net::TcpListener;
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::OpenOptionsExt;
use std::str::FromStr;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{channel, Sender};
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{fs, process, thread, time};
#[allow(dead_code)]
pub fn print_upstreams(upstreams: &UpstreamsDashMap) {
pub fn print_upstreams(upstreams: &UpstreamsDashMap, extraparams: &Extraparams) {
let mut out = String::new();
for host_entry in upstreams.iter() {
let hostname = host_entry.key();
println!("Hostname: {}", hostname);
writeln!(out, "Hostname: {}", host_entry.key()).unwrap();
for path_entry in host_entry.value().iter() {
let path = path_entry.key();
println!(" Path: {}", path);
writeln!(out, " Path: {}", path_entry.key()).unwrap();
for f in path_entry.value().0.clone() {
println!(
" IP: {}, Port: {}, SSL: {}, H2: {}, To HTTPS: {}, Rate Limit: {}",
writeln!(
out,
" IP: {}, Port: {}, SSL: {}, H2: {}, To HTTPS: {}, Rate Limit: {}, 4xx Limit: {}",
f.address,
f.port,
f.is_ssl,
f.is_http2,
f.to_https,
f.rate_limit.unwrap_or(0)
);
f.rate_limit.unwrap_or(extraparams.rate_limit.unwrap_or(0)),
f.x4xx_limit.unwrap_or(extraparams.x4xx_limit.unwrap_or(0))
)
.unwrap();
}
}
}
info!("\n{}", out.trim_end());
}
#[allow(dead_code)]
pub fn typeoff<T>(_: T) {
let to = type_name::<T>();
@@ -99,29 +104,26 @@ pub fn clone_dashmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsDashMap
}
pub fn compare_dashmaps(map1: &UpstreamsDashMap, map2: &UpstreamsDashMap) -> bool {
let keys1: HashSet<_> = map1.iter().map(|entry| entry.key().clone()).collect();
let keys2: HashSet<_> = map2.iter().map(|entry| entry.key().clone()).collect();
if keys1 != keys2 {
if map1.len() != map2.len() {
return false;
}
for entry1 in map1.iter() {
let hostname = entry1.key();
let inner_map1 = entry1.value();
let Some(inner_map2) = map2.get(hostname) else {
let Some(inner_map2) = map2.get(entry1.key()) else {
return false;
};
let inner_keys1: HashSet<_> = inner_map1.iter().map(|e| e.key().clone()).collect();
let inner_keys2: HashSet<_> = inner_map2.iter().map(|e| e.key().clone()).collect();
if inner_keys1 != inner_keys2 {
let inner_map1 = entry1.value();
if inner_map1.len() != inner_map2.len() {
return false;
}
for path_entry in inner_map1.iter() {
let path = path_entry.key();
let (vec1, _counter1) = path_entry.value();
let Some(entry2) = inner_map2.get(path) else {
return false; // Path exists in map1 but not in map2
let Some(entry2) = inner_map2.get(path_entry.key()) else {
return false;
};
let (vec2, _counter2) = entry2.value();
let (vec1, _) = path_entry.value();
let (vec2, _) = entry2.value();
if vec1.len() != vec2.len() {
return false;
}
let set1: HashSet<_> = vec1.iter().collect();
let set2: HashSet<_> = vec2.iter().collect();
if set1 != set2 {
@@ -132,11 +134,11 @@ pub fn compare_dashmaps(map1: &UpstreamsDashMap, map2: &UpstreamsDashMap) -> boo
true
}
pub fn merge_headers(target: &DashMap<String, Vec<(String, String)>>, source: &DashMap<String, Vec<(String, String)>>) {
pub fn merge_headers(target: &DashMap<Arc<str>, Vec<(String, Arc<str>)>>, source: &DashMap<Arc<str>, Vec<(String, Arc<str>)>>) {
for entry in source.iter() {
let global_key = entry.key().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);
}
}
@@ -145,39 +147,41 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) {
cloned.clear();
for outer_entry in original.iter() {
let inner_map = outer_entry.value();
let new_inner_map = DashMap::new();
for inner_entry in inner_map.iter() {
let path = inner_entry.key();
let (vec, _) = inner_entry.value();
let new_vec = vec.clone();
for x in vec.iter() {
let mut id = String::new();
write!(&mut id, "{}:{}:{}", x.address, x.port, x.is_ssl).unwrap();
write!(
&mut id,
"{}:{}:{}:{}:{}:{}:{}:{}:{:?}",
outer_entry.key(),
x.address,
x.port,
x.is_http2,
x.to_https,
x.x4xx_limit.unwrap_or_default(),
x.rate_limit.unwrap_or_default(),
x.healthcheck.unwrap_or_default(),
x.authorization
)
.unwrap_or(());
let mut hasher = Sha256::new();
// address: "127.0.0.3", port: 8000, is_ssl: false, is_http2: false, to_https: false, rate_limit: Some(200), healthcheck: None, authorization: None } }
hasher.update(id.clone().into_bytes());
let hash = hasher.finalize();
let hex_hash = base16ct::lower::encode_string(&hash);
let hh = hex_hash[0..50].to_string();
let to_add = InnerMap {
address: hh.clone(),
port: 0,
is_ssl: false,
is_http2: false,
to_https: false,
rate_limit: None,
healthcheck: None,
};
cloned.insert(id, to_add);
cloned.insert(hh, x.to_owned());
}
new_inner_map.insert(path.clone(), new_vec);
}
}
info!("Upstreams are fully populated. Ready to server requests");
}
pub fn listdir(dir: String) -> Vec<tls::CertificateConfig> {
pub fn listdir(dir: String) -> Vec<load::CertificateConfig> {
let mut f = HashMap::new();
let mut certificate_configs: Vec<tls::CertificateConfig> = vec![];
let mut certificate_configs: Vec<load::CertificateConfig> = vec![];
let paths = fs::read_dir(dir).unwrap();
for path in paths {
let path_str = path.unwrap().path().to_str().unwrap().to_owned();
@@ -195,13 +199,13 @@ pub fn listdir(dir: String) -> Vec<tls::CertificateConfig> {
certificate_configs.push(y);
}
}
for (_, v) in f.iter() {
let y = CertificateConfig {
cert_path: v[0].clone(),
key_path: v[1].clone(),
};
certificate_configs.push(y);
}
// for (_, v) in f.iter() {
// let y = CertificateConfig {
// cert_path: v[0].clone(),
// key_path: v[1].clone(),
// };
// certificate_configs.push(y);
// }
certificate_configs
}
@@ -236,14 +240,14 @@ pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Optio
thread::sleep(time::Duration::from_millis(10));
loop {
thread::sleep(time::Duration::from_millis(10));
if is_port_reachable(http_addr.clone()) {
if TcpListener::bind(&http_addr).is_err() {
break;
}
}
if let Some(tls_addr) = tls_addr {
loop {
thread::sleep(time::Duration::from_millis(10));
if is_port_reachable(tls_addr.clone()) {
if TcpListener::bind(&tls_addr).is_err() {
break;
}
}
@@ -256,15 +260,120 @@ pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Optio
}
pub fn check_priv(addr: &str) {
let port = SocketAddr::from_str(addr).map(|sa| sa.port()).unwrap();
match port < 1024 {
true => {
let meta = std::fs::metadata("/proc/self").map(|m| m.uid()).unwrap();
if meta != 0 {
error!("Running on privileged port requires to start as ROOT");
process::exit(1)
}
let port = SocketAddr::from_str(addr).map(|sa| sa.port()).expect("Failed to parse address port ");
if port < 1024 {
let meta = std::fs::metadata("/proc/self").map(|m| m.uid()).unwrap();
if meta != 0 {
error!("Running on privileged port requires to start as ROOT");
process::exit(1)
}
false => {}
}
}
#[allow(dead_code)]
pub fn upstreams_to_json(upstreams: &UpstreamsDashMap) -> serde_json::Result<String> {
let mut outer = HashMap::new();
for outer_entry in upstreams.iter() {
let mut inner_map = HashMap::new();
for inner_entry in outer_entry.value().iter() {
let (backends, counter) = inner_entry.value();
inner_map.insert(
inner_entry.key().to_string(),
UpstreamSnapshotForJson {
backends: backends
.iter()
.map(|a| InnerMapForJson {
address: a.address.to_string(),
port: a.port,
is_ssl: a.is_ssl,
is_http2: a.is_http2,
to_https: a.to_https,
rate_limit: a.rate_limit,
x4xx_limit: a.x4xx_limit,
healthcheck: a.healthcheck,
})
.collect(),
requests: counter.load(Ordering::Relaxed),
},
);
}
outer.insert(outer_entry.key().to_string(), inner_map);
}
// serde_json::to_string_pretty(&outer)
serde_json::to_string(&outer)
}
pub fn upstreams_liveness_json(configured: &UpstreamsDashMap, current: &UpstreamsDashMap) -> Value {
let mut result = serde_json::Map::new();
for host_entry in configured.iter() {
let hostname = host_entry.key().to_string();
let configured_paths = host_entry.value();
let mut paths_json = serde_json::Map::new();
for path_entry in configured_paths.iter() {
let path = path_entry.key().clone();
let (configured_backends, _) = path_entry.value();
let backends_json: Vec<Value> = configured_backends
.iter()
.map(|backend| {
let alive = if let Some(host_map) = current.get(&*hostname) {
if let Some(path_entry) = host_map.get(&*path) {
let list = &path_entry.value().0; // Vec<Arc<InnerMap>>
list.iter().any(|b| b.address == backend.address && b.port == backend.port)
} else {
false
}
} else {
false
};
json!({
"address": &*backend.address,
"port": backend.port,
"alive": alive
})
})
.collect();
paths_json.insert(
path.to_string(),
json!({
"backends": backends_json
}),
);
}
result.insert(hostname, Value::Object(paths_json));
}
Value::Object(result)
}
#[allow(dead_code)]
pub fn prepend(prefix: &str, val: &Option<Arc<str>>, uri: &str, port: &str) -> Option<String> {
val.as_ref().map(|s| {
let mut buf = String::with_capacity(32);
buf.push_str(prefix);
buf.push_str(s);
buf.push(':');
buf.push_str(port);
buf.push_str(uri);
buf
})
}
pub fn write_pid_file(path: &str) -> std::io::Result<()> {
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.custom_flags(libc::O_NOFOLLOW) // refuse to follow symlinks
.open(path)?;
file.write_all(process::id().to_string().as_bytes())?;
Ok(())
}
+2
View File
@@ -1,5 +1,7 @@
pub mod acme;
pub mod bgservice;
pub mod gethosts;
pub mod logging;
pub mod proxyhttp;
pub mod start;
pub mod webserver;
+64
View File
@@ -0,0 +1,64 @@
use crate::tls::acme::order::CHALLENGES;
use crate::tls::acme::{account, order};
use axum::body::Body;
use axum::extract::State;
use axum::http::{Response, StatusCode};
use axum::response::IntoResponse;
#[allow(clippy::needless_return)]
pub async fn acme_create(State(state): State<crate::web::webserver::AppState>) -> impl IntoResponse {
match account::load_or_create(state.cert_creds.as_str()).await {
Ok(txt) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap()
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to create account: {}", e)))
.unwrap()
}
};
}
#[allow(clippy::needless_return)]
pub async fn acme_order(State(state): State<crate::web::webserver::AppState>, axum::extract::Path(domain): axum::extract::Path<String>) -> impl IntoResponse {
let domain_clean = domain.trim_matches('/');
match order::order(domain_clean, state.cert_creds.as_str(), state.certs_dir).await {
Ok(txt) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap()
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to order a certificate: {}", e)))
.unwrap()
}
};
}
pub async fn http01_challenge(axum::extract::Path(token): axum::extract::Path<String>) -> impl IntoResponse {
if let Ok(challenges) = CHALLENGES.read() {
// for k in challenges.iter() {
// println!(" ==> {} : {}", k.0, k.1);
// }
if let Some(key_authorization) = challenges.get(&token) {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(key_authorization.clone()))
.unwrap();
}
}
Response::builder()
.status(StatusCode::NOT_FOUND)
.header("Content-Type", "text/plain")
.body(Body::from("Not found"))
.unwrap()
}
+66 -46
View File
@@ -1,26 +1,28 @@
use crate::tls::acme::order::refresh_order;
use crate::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider};
use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration;
use crate::utils::tools::*;
use crate::utils::*;
use crate::web::logging::init_logging;
use crate::web::proxyhttp::LB;
use async_trait::async_trait;
use dashmap::DashMap;
use futures::channel::mpsc;
use futures::{SinkExt, StreamExt};
use log::{error, info};
use pingora_core::server::ShutdownWatch;
use pingora_core::services::background::BackgroundService;
use std::sync::Arc;
use tokio::sync::mpsc;
#[async_trait]
impl BackgroundService for LB {
async fn start(&self, mut shutdown: ShutdownWatch) {
info!("Starting background service"); // tx: Sender<Configuration>
let (mut tx, mut rx) = mpsc::channel::<Configuration>(1);
let (tx, mut rx) = mpsc::channel::<Configuration>(1);
let tx_api = tx.clone();
let config = load_configuration(self.config.upstreams_conf.clone().as_str(), "filepath")
.await
.0
.expect("Failed to load configuration");
match config.typecfg.as_str() {
@@ -30,85 +32,103 @@ impl BackgroundService for LB {
let file_load = FromFileProvider {
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" => {
info!("Running Kubernetes discovery, requested type is: {}", config.typecfg);
let cf = Arc::from(config);
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" => {
info!("Running Consul discovery, requested type is: {}", config.typecfg);
let cf = Arc::from(config);
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);
}
}
let confdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/autoconfigs";
let certdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/certificates";
let api_load = APIUpstreamProvider {
address: self.config.config_address.clone(),
masterkey: self.config.master_key.clone(),
config_api_enabled: self.config.config_api_enabled.clone(),
tls_address: self.config.config_tls_address.clone(),
tls_certificate: self.config.config_tls_certificate.clone(),
tls_key_file: self.config.config_tls_key_file.clone(),
config_api_enabled: self.config.config_api_enabled,
upstreams_file: self.config.upstreams_conf.clone(),
config_dir: confdir.clone(),
certs_dir: certdir.clone(),
file_server_address: self.config.file_server_address.clone(),
file_server_folder: self.config.file_server_folder.clone(),
current_upstreams: self.ump_upst.clone(),
full_upstreams: self.ump_full.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 ff = self.ump_full.clone();
let im = self.ump_byid.clone();
let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval);
let _ = tokio::spawn(async move { healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await });
drop(tokio::spawn(async move {
healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await
}));
drop(tokio::spawn(async move { refresh_order(certdir, confdir).await }));
init_logging(self.config.access_log.clone());
loop {
tokio::select! {
_ = shutdown.changed() => {
break;
}
val = rx.next() => {
match val {
Some(ss) => {
clone_dashmap_into(&ss.upstreams, &self.ump_full);
clone_dashmap_into(&ss.upstreams, &self.ump_upst);
let current = self.extraparams.load_full();
let mut new = (*current).clone();
new.sticky_sessions = ss.extraparams.sticky_sessions;
new.to_https = ss.extraparams.to_https;
new.authentication = ss.extraparams.authentication.clone();
new.rate_limit = ss.extraparams.rate_limit;
self.extraparams.store(Arc::new(new));
self.headers.clear();
val = rx.recv() => {
if let Some(ss) = val {
clone_dashmap_into(&ss.upstreams, &self.ump_full);
clone_dashmap_into(&ss.upstreams, &self.ump_upst);
clone_idmap_into(&ss.upstreams, &self.ump_byid);
let current = self.extraparams.load_full();
let mut new = (*current).clone();
new.to_https = ss.extraparams.to_https;
new.sticky_sessions = ss.extraparams.sticky_sessions;
new.authentication = ss.extraparams.authentication.clone();
new.rate_limit = ss.extraparams.rate_limit;
new.x4xx_limit = ss.extraparams.x4xx_limit;
self.extraparams.store(Arc::new(new));
self.client_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 global_key = entry.key().clone();
let global_values = DashMap::new();
let mut target_entry = ss.headers.entry(global_key).or_insert_with(DashMap::new);
target_entry.extend(global_values);
self.headers.insert(target_entry.key().to_owned(), target_entry.value().to_owned());
}
for path in ss.headers.iter() {
let path_key = path.key().clone();
let path_headers = path.value().clone();
self.headers.insert(path_key.clone(), path_headers);
if let Some(global_headers) = ss.headers.get("GLOBAL_HEADERS") {
if let Some(existing_headers) = self.headers.get_mut(&path_key) {
merge_headers(&existing_headers, &global_headers);
}
let mut client_target_entry = ss.client_headers.entry(global_key.clone()).or_insert_with(DashMap::new);
client_target_entry.extend(client_global_values);
let mut server_target_entry = ss.server_headers.entry(global_key).or_insert_with(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());
}
for path in ss.client_headers.iter() {
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") {
if let Some(existing_headers) = self.client_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 => {}
}
}
}
+105 -58
View File
@@ -1,77 +1,124 @@
use crate::utils::structs::InnerMap;
use crate::web::proxyhttp::LB;
use async_trait::async_trait;
use std::sync::atomic::Ordering;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct GetHostsReturHeaders {
pub client_headers: Option<Vec<(String, Arc<str>)>>,
pub server_headers: Option<Vec<(String, Arc<str>)>>,
}
#[async_trait]
pub trait GetHost {
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<InnerMap>;
fn get_header(&self, peer: &str, path: &str) -> Option<Vec<(String, String)>>;
fn find_sticky_backend(&self, servers: &[Arc<InnerMap>], backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn pick_backend(&self, servers: &[Arc<InnerMap>], index: &AtomicUsize, backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders>;
}
#[async_trait]
impl GetHost for LB {
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<InnerMap> {
if let Some(b) = backend_id {
if let Some(bb) = self.ump_byid.get(b) {
// println!("BIB :===> {:?}", Some(bb.value()));
return Some(bb.value().clone());
}
}
let host_entry = self.ump_upst.get(peer)?;
let mut current_path = path.to_string();
let mut best_match: Option<InnerMap> = None;
loop {
if let Some(entry) = host_entry.get(&current_path) {
let (servers, index) = entry.value();
if !servers.is_empty() {
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len();
best_match = Some(servers[idx].clone());
break;
}
}
if let Some(pos) = current_path.rfind('/') {
current_path.truncate(pos);
} else {
break;
}
}
if best_match.is_none() {
if let Some(entry) = host_entry.get("/") {
let (servers, index) = entry.value();
if !servers.is_empty() {
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len();
best_match = Some(servers[idx].clone());
}
}
}
// println!("Best Match :===> {:?}", best_match);
best_match
fn find_sticky_backend(&self, servers: &[Arc<InnerMap>], backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
let b = backend_id?;
let bb = self.ump_byid.get(b)?;
let target = bb.value();
servers.iter().any(|s| s.address == target.address && s.port == target.port).then(|| target.clone())
}
fn get_header(&self, peer: &str, path: &str) -> Option<Vec<(String, String)>> {
let host_entry = self.headers.get(peer)?;
let mut current_path = path.to_string();
let mut best_match: Option<Vec<(String, String)>> = None;
fn pick_backend(&self, servers: &[Arc<InnerMap>], index: &AtomicUsize, backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
if servers.is_empty() {
return None;
}
if let Some(target) = self.find_sticky_backend(servers, backend_id) {
return Some(target);
}
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len();
Some(servers[idx].clone())
}
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
let host_entry = self.ump_upst.get(peer)?;
let mut end = path.len();
loop {
if let Some(entry) = host_entry.get(&current_path) {
if !entry.value().is_empty() {
best_match = Some(entry.value().clone());
break;
let slice = &path[..end];
if let Some(entry) = host_entry.get(slice) {
let (servers, index) = entry.value();
if let Some(backend) = self.pick_backend(servers, index, backend_id) {
return Some(backend);
}
}
if let Some(pos) = current_path.rfind('/') {
current_path.truncate(pos);
if let Some(pos) = slice.rfind('/') {
end = pos;
} else {
break;
}
}
if best_match.is_none() {
if let Some(entry) = host_entry.get("/") {
if !entry.value().is_empty() {
best_match = Some(entry.value().clone());
if let Some(entry) = host_entry.get("/") {
let (servers, index) = entry.value();
if let Some(backend) = self.pick_backend(servers, index, backend_id) {
return Some(backend);
}
}
None
}
fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders> {
let client_entry = self.client_headers.get(peer);
let server_entry = self.server_headers.get(peer);
if client_entry.is_none() && server_entry.is_none() {
return None;
}
let mut current_path = path;
let mut clnt_match = None;
if let Some(client_entry) = client_entry {
loop {
if let Some(entry) = client_entry.get(current_path) {
if !entry.value().is_empty() {
clnt_match = Some(entry.value().clone());
break;
}
}
if current_path == "/" {
break;
}
if let Some(pos) = current_path.rfind('/') {
current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else {
break;
}
}
}
best_match
current_path = path;
let mut serv_match = None;
if let Some(server_entry) = server_entry {
loop {
if let Some(entry) = server_entry.get(current_path) {
if !entry.value().is_empty() {
serv_match = Some(entry.value().clone());
break;
}
}
if current_path == "/" {
if let Some(entry) = server_entry.get("/") {
if !entry.value().is_empty() {
serv_match = Some(entry.value().clone());
break;
}
}
break;
}
if let Some(pos) = current_path.rfind('/') {
current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else {
break;
}
}
}
let result = GetHostsReturHeaders {
client_headers: clnt_match,
server_headers: serv_match,
};
if result.client_headers.is_some() || result.server_headers.is_some() {
Some(result)
} else {
None
}
}
}
+176
View File
@@ -0,0 +1,176 @@
use crate::utils::metrics::LOGGING_ERRORS;
use crate::utils::structs::AppConfig;
use log::{info, LevelFilter};
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::{
append::console::ConsoleAppender,
config::{Appender, Config as Log4rsConfig, Root},
encode::pattern::PatternEncoder,
};
use pingora_cache::CachePhase;
use pingora_http::Version;
use pingora_proxy::Session;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::OnceLock;
use tokio::sync::mpsc;
#[derive(Debug)]
pub struct LogMessage {
pub response_code: u16,
pub summary: String,
pub client_ip: IpAddr,
pub version: Version,
pub user_agent: String,
pub cache_status: CachePhase,
}
static LOG_SENDER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
static ACCESS_LOG: OnceLock<LogLevel> = OnceLock::new();
const LOG_BUFFER: usize = 16384;
pub 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 parts: Vec<&str> = location.splitn(4, ',').map(|s| s.trim()).collect();
let path = parts.get(0).expect("Syntax error, could not get path for log files");
let compress = parts.get(3).unwrap_or(&"No");
let size_mb: u64 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(100);
let keep: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(5);
let pattern_str = match compress {
&"compress" => format!("{}.{{}}.gz", path),
_ => format!("{}.{{}}", path),
};
let roller = FixedWindowRoller::builder().build(pattern_str.as_str(), keep).unwrap();
let trigger = SizeTrigger::new(size_mb * 1024 * 1024);
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
let file = RollingFileAppender::builder()
.encoder(Box::new(PatternEncoder::new(pattern)))
.build(path, Box::new(policy))
.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();
info!("Logging to: {}, Max file size: {}mb, Files to keep: {}, compression: {} ", path, size_mb, keep, compress);
} 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();
info!("No files are configured, logging to stdout");
}
}
pub fn init_access_log(level_str: &str) {
let level = LogLevel::from_str(level_str);
let _ = ACCESS_LOG.set(level);
}
#[derive(Debug)]
pub enum LogLevel {
Access,
Error,
None,
}
impl LogLevel {
pub fn from_str(s: &str) -> Self {
match s {
"all" => LogLevel::Access,
"error" => LogLevel::Error,
_ => LogLevel::None,
}
}
}
pub fn access_log(response_code: u16, summary: &str, session: &Session) {
let level = ACCESS_LOG.get().unwrap_or(&LogLevel::None);
let should_log = match level {
LogLevel::Access => true,
LogLevel::None => false,
LogLevel::Error => !(100..=399).contains(&response_code),
};
if !should_log {
return;
}
let ip = session
.client_addr()
.and_then(|addr| addr.as_inet())
.map(|addr| addr.ip())
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
let user_agent = session.req_header().headers.get("user-agent").and_then(|v| v.to_str().ok()).unwrap_or("-");
// let cache_status = match session.cache.phase() {
// CachePhase::Hit => "Hit",
// CachePhase::Miss => "Miss",
// CachePhase::Expired => "Expired",
// _ => "Disabled",
// };
let log = LogMessage {
response_code,
summary: summary.to_owned(),
client_ip: ip,
version: session.req_header().version,
user_agent: user_agent.to_owned(),
cache_status: session.cache.phase(),
};
if let Some(sender) = LOG_SENDER.get() {
let sender = sender;
if let Err(_) = sender.try_send(log) {
LOGGING_ERRORS.inc();
}
}
}
pub fn init_logging(enabled: Option<String>) {
if let Some(_) = enabled {
LOGGING_ERRORS.set(0);
info!("Enabling {:?} log, with buffer of {} messages", ACCESS_LOG.get().unwrap_or(&LogLevel::None), LOG_BUFFER);
let (ltx, lrx) = mpsc::channel(LOG_BUFFER);
LOG_SENDER.set(ltx).unwrap();
std::thread::spawn(move || log_receiver(lrx));
}
}
pub fn log_receiver(mut receiver: mpsc::Receiver<LogMessage>) {
while let Some(msg) = receiver.blocking_recv() {
info!(
"{}, {}, {}, client: {}, version: {:?}, useragent: {}",
msg.response_code,
msg.cache_status.as_str(),
msg.summary,
msg.client_ip,
msg.version,
msg.user_agent,
);
}
}
+245 -156
View File
@@ -1,43 +1,49 @@
use crate::utils::auth::authenticate;
use crate::utils::lazylock::{CACHE_LOCK, CACHE_TTL, EVICTION, LOCALHOST, MEM_CACHE, RATE_LIMITER, REQUESTS_4XX, REVERSE_STORE};
use crate::utils::metrics::*;
use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::web::gethosts::GetHost;
use crate::web::gethosts::{GetHost, GetHostsReturHeaders};
use crate::web::logging::access_log;
use arc_swap::ArcSwap;
use async_trait::async_trait;
use axum::body::Bytes;
use log::{debug, error, warn};
use once_cell::sync::Lazy;
use pingora::http::{RequestHeader, ResponseHeader, StatusCode};
use log::error;
use pingora::http::{Method, RequestHeader, ResponseHeader, StatusCode};
use pingora::prelude::*;
use pingora::ErrorSource::Upstream;
use pingora_cache::cache_control::CacheControl;
use pingora_cache::{CacheKey, CacheMetaDefaults, RespCacheable};
use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer;
use pingora_limits::rate::Rate;
use pingora_proxy::{ProxyHttp, Session};
use sha2::{Digest, Sha256};
use std::cell::RefCell;
use std::fmt::Write;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::Instant;
static RATE_LIMITER: Lazy<Rate> = Lazy::new(|| Rate::new(Duration::from_secs(1)));
thread_local! {static IP_BUFFER: RefCell<String> = RefCell::new(String::with_capacity(50));}
#[derive(Clone)]
pub struct LB {
pub ump_upst: Arc<UpstreamsDashMap>,
pub ump_full: Arc<UpstreamsDashMap>,
pub ump_byid: Arc<UpstreamsIdMap>,
pub headers: Arc<Headers>,
pub client_headers: Arc<Headers>,
pub server_headers: Arc<Headers>,
pub config: Arc<AppConfig>,
pub extraparams: Arc<ArcSwap<Extraparams>>,
pub cache_enabled: bool,
}
pub struct Context {
backend_id: String,
to_https: bool,
redirect_to: String,
backend_id: Option<String>,
start_time: Instant,
hostname: Option<String>,
upstream_peer: Option<InnerMap>,
hostname: Option<Arc<str>>,
upstream_peer: Option<Arc<InnerMap>>,
extraparams: arc_swap::Guard<Arc<Extraparams>>,
client_headers: Option<Vec<(String, Arc<str>)>>,
x4xx_limit: Option<u32>,
}
#[async_trait]
@@ -45,69 +51,133 @@ impl ProxyHttp for LB {
type CTX = Context;
fn new_ctx(&self) -> Self::CTX {
Context {
backend_id: String::new(),
to_https: false,
redirect_to: String::new(),
backend_id: None,
start_time: Instant::now(),
hostname: None,
upstream_peer: None,
extraparams: self.extraparams.load(),
client_headers: None,
x4xx_limit: None,
}
}
async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> {
let ep = _ctx.extraparams.clone();
if let Some(auth) = ep.authentication.get("authorization") {
let authenticated = authenticate(&auth.value(), &session);
if !authenticated {
let _ = session.respond_error(401).await;
warn!("Forbidden: {:?}, {}", session.client_addr(), session.req_header().uri.path());
return Ok(true);
fn cache_key_callback(&self, session: &Session, _ctx: &mut Self::CTX) -> Result<CacheKey> {
Ok(CacheKey::new(_ctx.hostname.clone().unwrap_or_default().as_ref(), session.req_header().uri.to_string(), ""))
}
fn request_cache_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<()> {
if self.cache_enabled && session.req_header().method == Method::GET {
if let Some(eviction) = EVICTION.get() {
session.cache.enable(&*MEM_CACHE, Some(eviction), None, Some(&*CACHE_LOCK), None);
}
};
}
Ok(())
}
fn response_cache_filter(&self, _session: &Session, resp: &ResponseHeader, _ctx: &mut Self::CTX) -> Result<RespCacheable> {
let cc = CacheControl::from_resp_headers(resp);
let defaults = CacheMetaDefaults::new(
|status| {
if status == StatusCode::OK {
Some(Duration::from_secs(*CACHE_TTL.get().unwrap_or(&60)))
} else {
None
}
},
0,
0,
);
Ok(pingora_cache::filters::resp_cacheable(cc.as_ref(), resp.clone(), false, &defaults))
}
let hostname = return_header_host(&session);
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);
_ctx.hostname = hostname;
let mut backend_id = None;
if ep.sticky_sessions {
if let Some(_) = _ctx.extraparams.sticky_sessions {
if let Some(cookies) = session.req_header().headers.get("cookie") {
if let Ok(cookie_str) = cookies.to_str() {
for cookie in cookie_str.split(';') {
let trimmed = cookie.trim();
if let Some(value) = trimmed.strip_prefix("backend_id=") {
backend_id = Some(value);
break;
}
if let Some(pos) = cookie_str.find("backend_id=") {
let value = &cookie_str[pos + "backend_id=".len()..];
let end = value.find(';').unwrap_or(value.len());
backend_id = Some(&value[..end]);
}
}
}
}
match _ctx.hostname.as_ref() {
None => return Ok(false),
Some(host) => {
// let optioninnermap = self.get_host(host.as_str(), host.as_str(), backend_id);
let optioninnermap = self.get_host(host.as_str(), session.req_header().uri.path(), backend_id);
let optioninnermap = self.get_host(host, session.req_header().uri.path(), backend_id);
match optioninnermap {
None => return Ok(false),
Some(ref innermap) => {
if let Some(rate) = innermap.rate_limit.or(ep.rate_limit) {
// let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip().to_string()).unwrap_or_else(|| host.to_string());
if let Some(auth) = _ctx.extraparams.authentication.as_ref().or(innermap.authorization.as_ref()) {
if !authenticate(&auth, session).await {
let _ = session.respond_error(401).await;
return Ok(true);
}
}
if let Some(rate) = innermap.x4xx_limit.or(_ctx.extraparams.x4xx_limit) {
_ctx.x4xx_limit = innermap.x4xx_limit;
let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip());
if let Some(rk) = rate_key {
let count = REQUESTS_4XX.get(&rk).unwrap_or(0);
if count > rate {
let header = ResponseHeader::build(429, None)?;
session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?;
return Ok(true);
}
}
}
if let Some(rate) = innermap.rate_limit.or(_ctx.extraparams.rate_limit) {
let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip());
let curr_window_requests = RATE_LIMITER.observe(&rate_key, 1);
if curr_window_requests > rate {
let mut header = ResponseHeader::build(429, None).unwrap();
header.insert_header("X-Rate-Limit-Limit", rate.to_string()).unwrap();
header.insert_header("X-Rate-Limit-Remaining", "0").unwrap();
header.insert_header("X-Rate-Limit-Reset", "1").unwrap();
let header = ResponseHeader::build(429, None)?;
session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?;
debug!("Rate limited: {:?}, {}", rate_key, rate);
return Ok(true);
}
}
if let Some(redirect_to) = &innermap.redirect_to {
let uri = session.req_header().uri.path();
let capacity = redirect_to.len() + uri.len();
let mut s = String::with_capacity(capacity);
s.push_str(redirect_to);
s.push_str(uri);
let mut resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
resp.insert_header("Location", s)?;
resp.insert_header("Content-Length", "0")?;
session.write_response_header(Box::new(resp), true).await?;
return Ok(true);
}
if _ctx.extraparams.to_https.unwrap_or(false) || innermap.to_https {
if let Some(stream) = session.stream() {
if stream.get_ssl().is_none() {
if let Some(host) = _ctx.hostname.as_ref() {
let port = self.config.proxy_port_tls.as_deref().unwrap_or("443");
let uri = session.req_header().uri.path();
let capacity = host.len() + uri.len() + 8;
let mut s = String::with_capacity(capacity);
s.push_str("https://");
s.push_str(host);
if port != "443" {
s.push(':');
s.push_str(port);
}
s.push_str(uri);
let mut resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
resp.insert_header("Location", s)?;
resp.insert_header("Content-Length", "0")?;
session.write_response_header(Box::new(resp), true).await?;
return Ok(true);
}
}
}
}
}
}
_ctx.upstream_peer = optioninnermap;
@@ -116,56 +186,65 @@ impl ProxyHttp for LB {
Ok(false)
}
async fn upstream_peer(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<Box<HttpPeer>> {
// let host_name = return_header_host(&session);
match ctx.hostname.as_ref() {
Some(hostname) => {
match ctx.upstream_peer.as_ref() {
// Some((address, port, ssl, is_h2, to_https)) => {
Some(innermap) => {
let mut peer = Box::new(HttpPeer::new((innermap.address.clone(), innermap.port.clone()), innermap.is_ssl, String::new()));
// if session.is_http2() {
if innermap.is_http2 {
peer.options.alpn = ALPN::H2;
}
if innermap.is_ssl {
peer.sni = hostname.clone();
peer.options.verify_cert = false;
peer.options.verify_hostname = false;
}
if ctx.to_https || innermap.to_https {
if let Some(stream) = session.stream() {
if stream.get_ssl().is_none() {
if let Some(addr) = session.server_addr() {
if let Some((host, _)) = addr.to_string().split_once(':') {
let uri = session.req_header().uri.path_and_query().map_or("/", |pq| pq.as_str());
let port = self.config.proxy_port_tls.unwrap_or(403);
ctx.to_https = true;
ctx.redirect_to = format!("https://{}:{}{}", host, port, uri);
}
}
}
}
}
Some(hostname) => match ctx.upstream_peer.as_ref() {
Some(innermap) => {
let mut peer = Box::new(HttpPeer::new((&*innermap.address, innermap.port), innermap.is_ssl, hostname.to_string()));
if innermap.is_http2 {
peer.options.alpn = ALPN::H2;
}
if innermap.is_ssl {
peer.options.verify_cert = 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),
});
ctx.backend_id = format!("{}:{}:{}", innermap.address.clone(), innermap.port.clone(), innermap.is_ssl);
Ok(peer)
}
None => {
if let Err(e) = session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await {
error!("Failed to send error response: {:?}", e);
}
Err(Box::new(Error {
etype: HTTPStatus(502),
esource: Upstream,
retry: RetryType::Decided(false),
cause: None,
context: Option::from(ImmutStr::Static("Upstream not found")),
}))
peer.options.idle_timeout = Some(Duration::from_secs(300));
peer.options.tcp_recv_buf = Some(128 * 1024);
End of experimental options
*/
if let Some(_) = ctx.extraparams.sticky_sessions {
let mut s = String::with_capacity(64);
write!(
&mut s,
"{}:{}:{}:{}:{}:{}:{}:{}:{:?}",
hostname,
innermap.address,
innermap.port,
innermap.is_http2,
innermap.to_https,
innermap.x4xx_limit.unwrap_or_default(),
innermap.rate_limit.unwrap_or_default(),
innermap.healthcheck.unwrap_or_default(),
innermap.authorization
)
.unwrap_or(());
ctx.backend_id = Some(s);
}
Ok(peer)
}
}
None => {
if let Err(e) = session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await {
error!("Failed to send error response: {:?}", e);
}
Err(Box::new(Error {
etype: HTTPStatus(502),
esource: Upstream,
retry: RetryType::Decided(false),
cause: None,
context: Option::from(ImmutStr::Static("Upstream not found")),
}))
}
},
None => {
// session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await.expect("Failed to send error");
if let Err(e) = session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await {
error!("Failed to send error response: {:?}", e);
}
@@ -180,93 +259,103 @@ impl ProxyHttp for LB {
}
}
async fn upstream_request_filter(&self, _session: &mut Session, upstream_request: &mut RequestHeader, ctx: &mut Self::CTX) -> Result<()> {
if let Some(hostname) = ctx.hostname.as_ref() {
upstream_request.insert_header("Host", hostname)?;
async fn upstream_request_filter(&self, session: &mut Session, upstream_request: &mut RequestHeader, ctx: &mut Self::CTX) -> Result<()> {
if let Some(ip) = session.client_addr().and_then(|a| a.as_inet()).map(|i| i.ip()) {
IP_BUFFER.with(|buffer| {
let mut buf = buffer.borrow_mut();
buf.clear();
write!(buf, "{}", ip).unwrap_or(());
upstream_request.append_header("X-Forwarded-For", buf.as_str()).unwrap_or(false);
});
}
if let Some(peer) = ctx.upstream_peer.as_ref() {
upstream_request.insert_header("X-Forwarded-For", peer.address.as_str())?;
let hostname = ctx.hostname.as_deref().unwrap_or("localhost");
let path = session.req_header().uri.path();
let GetHostsReturHeaders { server_headers, client_headers } = match self.get_header(hostname, path) {
Some(h) => h,
None => return Ok(()),
};
if let Some(sh) = server_headers {
for (k, v) in sh {
upstream_request.insert_header(k, v.as_ref())?;
}
}
if let Some(ch) = client_headers {
ctx.client_headers = Some(ch);
}
Ok(())
}
// async fn request_body_filter(&self, _session: &mut Session, _body: &mut Option<Bytes>, _end_of_stream: bool, _ctx: &mut Self::CTX) -> Result<()>
// where
// Self::CTX: Send + Sync,
// {
// Ok(())
// }
async fn response_filter(&self, session: &mut Session, _upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> {
// _upstream_response.insert_header("X-Proxied-From", "Fooooooooooooooo").unwrap();
if ctx.extraparams.sticky_sessions {
let backend_id = ctx.backend_id.clone();
if let Some(bid) = self.ump_byid.get(&backend_id) {
let _ = _upstream_response.insert_header("set-cookie", format!("backend_id={}; Path=/; Max-Age=600; HttpOnly; SameSite=Lax", bid.address));
async fn response_filter(&self, _session: &mut Session, _upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> {
if let Some(val) = ctx.extraparams.sticky_sessions {
if let Some(bid) = &ctx.backend_id {
let tt = if let Some(existing) = REVERSE_STORE.get(bid) {
existing.value().clone()
} else {
let mut hasher = Sha256::new();
hasher.update(bid.as_bytes());
let hash = hasher.finalize();
let hex_hash = base16ct::lower::encode_string(&hash);
let hh = hex_hash[0..50].to_string();
REVERSE_STORE.insert(bid.clone(), hh.clone());
REVERSE_STORE.insert(hh.clone(), bid.clone());
hh
};
let mut buf = String::with_capacity(80);
buf.push_str("backend_id=");
buf.push_str(&tt);
buf.push_str("; Path=/; Max-Age=");
buf.push_str(&val.to_string());
buf.push_str("; HttpOnly; SameSite=Lax");
let _ = _upstream_response.append_header("set-cookie", buf.as_str());
}
}
if ctx.to_https {
let mut redirect_response = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
redirect_response.insert_header("Location", ctx.redirect_to.clone())?;
redirect_response.insert_header("Content-Length", "0")?;
session.write_response_header(Box::new(redirect_response), false).await?;
}
match ctx.hostname.as_ref() {
Some(host) => {
let path = session.req_header().uri.path();
let host_header = host;
let split_header = host_header.split_once(':');
match split_header {
Some(sh) => {
let yoyo = self.get_header(sh.0, path);
for k in yoyo.iter() {
for t in k.iter() {
_upstream_response.insert_header(t.0.clone(), t.1.clone()).unwrap();
}
}
}
None => {
let yoyo = self.get_header(host_header, path);
for k in yoyo.iter() {
for t in k.iter() {
_upstream_response.insert_header(t.0.clone(), t.1.clone()).unwrap();
}
}
}
}
if let Some(client_headers) = &ctx.client_headers {
for (k, v) in client_headers.iter() {
_upstream_response.append_header(k.clone(), v.as_ref())?;
}
None => {}
}
session.set_keepalive(Some(300));
Ok(())
}
async fn logging(&self, session: &mut Session, _e: Option<&pingora::Error>, ctx: &mut Self::CTX) {
let response_code = session.response_written().map_or(0, |resp| resp.status.as_u16());
debug!("{}, response code: {response_code}", self.request_summary(session, ctx));
let m = &MetricTypes {
method: session.req_header().method.to_string(),
code: session.response_written().map(|resp| resp.status.as_str().to_owned()).unwrap_or("0".to_string()),
method: session.req_header().method.clone(),
code: session.response_written().map(|resp| resp.status),
latency: ctx.start_time.elapsed(),
version: session.req_header().version,
upstream: ctx.hostname.take().unwrap_or_else(|| LOCALHOST.clone()),
};
calc_metrics(m);
ACTIVE_SESSIONS.dec();
if let Some(_) = ctx.x4xx_limit.or(ctx.extraparams.x4xx_limit) {
if (400..=499).contains(&response_code) {
if let Some(ip) = session.client_addr().and_then(|a| a.as_inet()).map(|i| i.ip()) {
let current = REQUESTS_4XX.get(&ip).unwrap_or(0);
REQUESTS_4XX.insert(ip, current + 1);
}
}
}
// match session.cache.phase() {
// CachePhase::Hit => info!("Cache Status: HIT (Served from Memory)"),
// CachePhase::Miss => info!("Cache Status: MISS (Fetched from Upstream)"),
// CachePhase::Expired => info!("Cache Status: EXPIRED (Revalidating)"),
// _ => {}
// }
access_log(response_code, &self.request_summary(session, ctx), session);
}
}
fn return_header_host(session: &Session) -> Option<String> {
if session.is_http2() {
match session.req_header().uri.host() {
Some(host) => Option::from(host.to_string()),
None => None,
}
fn return_header_host_from_upstream(session: &Session, ump_upst: &UpstreamsDashMap) -> Option<Arc<str>> {
let host_str = if session.is_http2() {
session.req_header().uri.host()?
} else {
match session.req_header().headers.get("host") {
Some(host) => {
let header_host = host.to_str().unwrap().splitn(2, ':').collect::<Vec<&str>>();
Option::from(header_host[0].to_string())
}
None => None,
}
}
let h = session.req_header().headers.get("host")?.to_str().ok()?;
h.split_once(':').map_or(h, |(host, _)| host)
};
ump_upst.get(host_str).or_else(|| ump_upst.get("DEFAULT")).map(|entry| entry.key().clone())
}
+120 -56
View File
@@ -1,39 +1,61 @@
// use rustls::crypto::ring::default_provider;
use crate::tls::grades;
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::lazylock::{CACHE_TTL, EVICTION};
use crate::utils::structs::Extraparams;
use crate::utils::tls;
use crate::utils::tls::CertificateConfig;
use crate::utils::tools::*;
use crate::web::logging::init_access_log;
use crate::web::proxyhttp::LB;
use arc_swap::ArcSwap;
use ctrlc;
use dashmap::DashMap;
use log::info;
use pingora::tls::ssl::{SslAlert, SslRef};
use pingora_cache::eviction::simple_lru::Manager;
use pingora_core::listeners::tls::TlsSettings;
use pingora_core::listeners::TcpSocketOptions;
use pingora_core::prelude::{background_service, Opt};
use pingora_core::protocols::TcpKeepalive;
use pingora_core::server::Server;
use privdrop::reexports::libc::SIGQUIT;
use sd_notify::NotifyState;
use signal_hook::{
consts::{SIGINT, SIGTERM},
iterator::Signals,
};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use std::{fs, thread};
pub fn run() {
// 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 maincfg = crate::utils::parceyaml::parce_main_config(file.as_str());
let cache_enabled = maincfg.cache_size_mb.is_some();
CACHE_TTL.set(maincfg.cache_ttl.unwrap_or(60)).unwrap();
if let Some(size) = maincfg.cache_size_mb {
let cache_size = size * 1024 * 1024;
drop(EVICTION.set(Manager::new(cache_size)));
}
let mut server = Server::new(parameters).unwrap();
server.bootstrap();
let uf_config = Arc::new(DashMap::new());
let ff_config = Arc::new(DashMap::new());
let im_config = Arc::new(DashMap::new());
let hh_config = Arc::new(DashMap::new());
let ch_config = Arc::new(DashMap::new());
let sh_config = Arc::new(DashMap::new());
let ec_config = Arc::new(ArcSwap::from_pointee(Extraparams {
sticky_sessions: false,
to_https: None,
authentication: DashMap::new(),
sticky_sessions: None,
authentication: None,
rate_limit: None,
x4xx_limit: None,
}));
let cfg = Arc::new(maincfg);
@@ -43,74 +65,116 @@ pub fn run() {
ump_full: ff_config,
ump_byid: im_config,
config: cfg.clone(),
headers: hh_config,
client_headers: ch_config,
server_headers: sh_config,
extraparams: ec_config,
cache_enabled: cache_enabled,
};
let al = cfg.access_log.clone().unwrap_or("none".to_string());
init_access_log(al.as_str());
let grade = cfg.proxy_tls_grade.clone().unwrap_or("medium".to_string());
info!("TLS grade set to: [ {} ]", grade);
let bg_srvc = background_service("bgsrvc", lb.clone());
let mut proxy = pingora_proxy::http_proxy_service(&server.configuration, lb.clone());
let bind_address_http = cfg.proxy_address_http.clone();
let bind_address_tls = cfg.proxy_address_tls.clone();
let mut proxy = pingora_proxy::http_proxy_service(&server.configuration, lb.clone());
check_priv(bind_address_http.as_str());
match bind_address_tls {
Some(bind_address_tls) => {
check_priv(bind_address_tls.as_str());
let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel();
let certs_path = cfg.proxy_certificates.clone().unwrap();
thread::spawn(move || {
watch_folder(certs_path, tx).unwrap();
});
let certificate_configs = rx.recv().unwrap();
let first_set = tls::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info"));
let certificates = Arc::new(ArcSwap::from_pointee(first_set));
let certs_for_callback = certificates.clone();
// let mut tcp_options: Option<TcpSocketOptions> = Some(TcpSocketOptions::default());
// let mut tcp_options = TcpSocketOptions::default();
let certs_for_watcher = certificates.clone();
let new_certs = tls::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");
tls::set_tsl_grade(&mut tls_settings, grade.as_str());
tls_settings.set_servername_callback(move |ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert| certs_for_callback.load().server_name_callback(ssl_ref, ssl_alert));
tls_settings.set_alpn_select_callback(tls::prefer_h2);
proxy.add_tls_with_settings(&bind_address_tls, None, tls_settings);
let certs_for_watcher = certificates.clone();
thread::spawn(move || {
while let Ok(new_configs) = rx.recv() {
let new_certs = tls::Certificates::new(&new_configs, grade.as_str());
match new_certs {
Some(new_certs) => {
certs_for_watcher.store(Arc::new(new_certs));
}
None => {}
};
}
});
}
None => {}
let mut tcp_options: Option<TcpSocketOptions> = None;
if let Some(idle) = cfg.tcp_keepalive_idle {
let mut to = TcpSocketOptions::default();
to.tcp_keepalive = Some(TcpKeepalive {
idle: Duration::from_secs(idle),
interval: Duration::from_secs(cfg.tcp_keepalive_interval.unwrap_or(10)),
user_timeout: Default::default(),
count: cfg.tcp_keepalive_count.unwrap_or(5usize),
});
tcp_options = Some(to);
info!(
"Applying kernel tcp_keepalive parameters: idle {}, interval {}, count {}",
idle,
cfg.tcp_keepalive_interval.unwrap_or(60),
cfg.tcp_keepalive_count.unwrap_or(5),
);
}
info!("Running HTTP listener on :{}", bind_address_http.as_str());
proxy.add_tcp(bind_address_http.as_str());
if let Some(bind_address_tls) = bind_address_tls {
check_priv(bind_address_tls.as_str());
let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel();
let certs_path = cfg.proxy_configs.clone().unwrap() + "/certificates";
if fs::metadata(certs_path.clone()).is_err() {
fs::create_dir_all(certs_path.clone()).unwrap();
}
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, tcp_options.clone(), 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);
if let Some(tc) = tcp_options {
proxy.add_tcp_with_settings(&bind_address_http, tc);
} else {
proxy.add_tcp(&bind_address_http)
}
server.add_service(proxy);
server.add_service(bg_srvc);
thread::spawn(move || server.run_forever());
if let (Some(user), Some(group)) = (cfg.rungroup.clone(), cfg.runuser.clone()) {
drop_priv(user, group, cfg.proxy_address_http.clone(), cfg.proxy_address_tls.clone());
}
let _ = sd_notify::notify(&[NotifyState::Ready]);
let (tx, rx) = channel();
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel.")).expect("Error setting Ctrl-C handler");
rx.recv().expect("Could not receive from channel.");
info!("Signal received ! Exiting...");
let pf = cfg.pid_file.clone().unwrap_or("/tmp/aralez.pid".to_string());
if let Err(e) = write_pid_file(pf.as_str()) {
panic!("Failed to write PID file: {} : {}", pf, e);
}
let mut signals = Signals::new(&[SIGINT, SIGTERM, SIGQUIT]).unwrap();
for sig in signals.forever() {
match sig {
SIGINT => info!("SIGINT received! Exiting..."),
SIGTERM => info!("SIGTERM received! Exiting..."),
SIGQUIT => {
thread::sleep(Duration::from_secs(300));
info!("SIGQUIT received! Exiting...")
}
_ => unreachable!(),
}
break;
}
}
+181 -77
View File
@@ -1,138 +1,191 @@
use crate::utils::discovery::APIUpstreamProvider;
use crate::utils::structs::Configuration;
use crate::utils::jwt::Claims;
use crate::utils::metrics::{get_memory_usage, get_open_files, MEMORY_USAGE, OPEN_FILES};
use crate::utils::structs::{Config, Configuration, UpstreamsDashMap};
use crate::utils::tools::{upstreams_liveness_json, upstreams_to_json};
use crate::web::acme::{acme_create, acme_order, http01_challenge};
use axum::body::Body;
use axum::extract::{Query, State};
use axum::http::{Response, StatusCode};
use axum::response::IntoResponse;
use axum::routing::{get, post};
use axum::routing::{any, get, post};
use axum::{Json, Router};
use axum_server::tls_openssl::OpenSSLConfig;
use futures::channel::mpsc::Sender;
use futures::SinkExt;
use jsonwebtoken::{encode, EncodingKey, Header};
use log::{error, info, warn};
use log::{debug, error, info, warn};
use prometheus::{gather, Encoder, TextEncoder};
use serde::{Deserialize, Serialize};
use serde::Serialize;
use signal_hook::{consts::SIGQUIT, iterator::Signals};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use tower_http::services::ServeDir;
#[derive(Deserialize)]
struct InputKey {
master_key: String,
owner: String,
valid: u64,
}
#[derive(Serialize, Debug)]
struct OutToken {
token: String,
}
#[derive(Clone)]
struct AppState {
master_key: String,
config_sender: Sender<Configuration>,
pub(crate) struct AppState {
master_key: Option<String>,
pub(crate) cert_creds: String,
pub(crate) certs_dir: String,
upstreams_file: String,
config_sender: mpsc::Sender<Configuration>,
config_api_enabled: bool,
current_upstreams: Arc<UpstreamsDashMap>,
full_upstreams: Arc<UpstreamsDashMap>,
}
#[allow(unused_mut)]
pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Configuration>) {
pub async fn run_server(config: &APIUpstreamProvider, mut to_return: mpsc::Sender<Configuration>, upstreams_curr: Arc<UpstreamsDashMap>, upstreams_full: Arc<UpstreamsDashMap>) {
let credsfile = config.config_dir.clone() + "/acme_credentials.json";
let app_state = AppState {
master_key: config.masterkey.clone(),
cert_creds: credsfile,
certs_dir: config.certs_dir.clone(),
upstreams_file: config.upstreams_file.clone(),
config_sender: to_return.clone(),
config_api_enabled: config.config_api_enabled.clone(),
config_api_enabled: config.config_api_enabled,
current_upstreams: upstreams_curr,
full_upstreams: upstreams_full,
};
let app = Router::new()
// .route("/{*wildcard}", get(senderror))
// .route("/{*wildcard}", post(senderror))
// .route("/{*wildcard}", put(senderror))
// .route("/{*wildcard}", head(senderror))
// .route("/{*wildcard}", delete(senderror))
// .nest_service("/static", static_files)
.route("/jwt", post(jwt_gen))
.route("/acme_create", any(acme_create))
.route("/acme_order/{*domain}", any(acme_order))
.route("/.well-known/acme-challenge/{*token}", any(http01_challenge))
.route("/conf", post(conf))
.route("/metrics", get(metrics))
.route("/status", get(status))
.with_state(app_state);
if let Some(value) = &config.tls_address {
let cf = OpenSSLConfig::from_pem_file(config.tls_certificate.clone().unwrap(), config.tls_key_file.clone().unwrap()).unwrap();
let addr: SocketAddr = value.parse().expect("Unable to parse socket address");
let tls_app = app.clone();
tokio::spawn(async move {
if let Err(e) = axum_server::bind_openssl(addr, cf).serve(tls_app.into_make_service()).await {
eprintln!("TLS server failed: {}", e);
}
});
info!("Starting the TLS API server on: {}", value);
}
let mut static_handle: Option<tokio::task::JoinHandle<()>> = None;
if let (Some(address), Some(folder)) = (&config.file_server_address, &config.file_server_folder) {
let static_listen = port_is_available("File Server", &address).await;
let static_files = ServeDir::new(folder);
let static_serve: Router = Router::new().fallback_service(static_files);
let static_listen = TcpListener::bind(address).await.unwrap();
let _ = tokio::spawn(async move { axum::serve(static_listen, static_serve).await.unwrap() });
info!("Starting file server on: {}", address);
static_handle = Some(tokio::spawn(async move {
match axum::serve(static_listen, static_serve).await {
Ok(o) => info!("File server exited normally: {:?}", o),
Err(e) => error!("File server exited: {}", e),
}
}))
// let a = config.file_server_address.to_owned();
// let f = config.file_server_folder.to_owned();
// drop(tokio::spawn(async move { run_file_server(a, f).await }));
}
let listener = TcpListener::bind(config.address.clone()).await.unwrap();
let listener = port_is_available("Config API", &config.address).await;
info!("Starting the API server on: {}", config.address);
axum::serve(listener, app).await.unwrap();
let api_server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
let (tx, mut rx) = mpsc::channel(1);
std::thread::spawn(move || {
let mut signals = Signals::new(&[SIGQUIT]).unwrap();
for sig in signals.forever() {
tx.blocking_send(sig).unwrap();
break;
}
});
rx.recv().await;
api_server.abort();
if let Some(handle) = static_handle {
handle.abort();
}
info!("Exiting...");
}
async fn conf(State(mut st): State<AppState>, Query(params): Query<HashMap<String, String>>, content: String) -> impl IntoResponse {
async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>, content: String) -> impl IntoResponse {
if !st.config_api_enabled {
return Response::builder()
.status(StatusCode::FORBIDDEN)
.body(Body::from("Config remote API is disabled !\n"))
.unwrap();
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config API is disabled !\n")).unwrap();
}
if let Some(s) = params.get("key") {
if s.to_owned() == st.master_key {
if let Some(serverlist) = crate::utils::parceyaml::load_configuration(content.as_str(), "content").await {
st.config_sender.send(serverlist).await.unwrap();
return Response::builder().status(StatusCode::OK).body(Body::from("Config, conf file, updated !\n")).unwrap();
let strcontent = content.as_str();
let parsed = noyalib::from_str::<Config>(strcontent);
match parsed {
Ok(_) => {
if let Some(_) = params.get("save") {
drop(tokio::spawn(async move { apply_config(content.as_str(), st, true).await }));
} else {
return Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("Failed to parse config!\n")).unwrap();
};
drop(tokio::spawn(async move { apply_config(content.as_str(), st, false).await }));
}
Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap()
}
Err(err) => {
error!("Failed to parse upstreams file: {}", err);
Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from(format!("Failed: {}\n", err))).unwrap()
}
}
Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap()
}
async fn jwt_gen(State(state): State<AppState>, Json(payload): Json<InputKey>) -> (StatusCode, Json<OutToken>) {
if payload.master_key == state.master_key {
let now = SystemTime::now() + Duration::from_secs(payload.valid * 60);
let a = now.duration_since(UNIX_EPOCH).unwrap().as_secs();
let claim = crate::utils::jwt::Claims { user: payload.owner, exp: a };
match encode(&Header::default(), &claim, &EncodingKey::from_secret(payload.master_key.as_ref())) {
Ok(t) => {
let tok = OutToken { token: t };
info!("Generating token: {:?}", tok);
(StatusCode::CREATED, Json(tok))
async fn apply_config(content: &str, st: AppState, save: bool) {
let sl = crate::utils::parceyaml::load_configuration(content, "content").await;
if let Some(serverlist) = sl.0 {
if save {
info!("Saving new upstreams to: {}", st.upstreams_file);
if let Err(err) = std::fs::write(&st.upstreams_file, content) {
error!("Error saving to: {} : {}", st.upstreams_file, err);
}
Err(e) => {
let tok = OutToken { token: "ERROR".to_string() };
error!("Failed to generate token: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(tok))
}
let _ = st.config_sender.send(serverlist).await;
}
}
async fn jwt_gen(State(state): State<AppState>, Json(payload): Json<Claims>) -> (StatusCode, Json<OutToken>) {
if let Some(master_key) = &state.master_key {
if &payload.master_key == master_key {
let now = SystemTime::now() + Duration::from_secs(payload.exp * 60);
let expire = now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
let claim = Claims {
master_key: String::new(),
owner: payload.owner,
exp: expire,
random: payload.random,
};
match encode(&Header::default(), &claim, &EncodingKey::from_secret(payload.master_key.as_ref())) {
Ok(t) => {
let tok = OutToken { token: t };
debug!("Generating token: {:?}", tok.token);
(StatusCode::CREATED, Json(tok))
}
Err(e) => {
let tok = OutToken { token: "ERROR".to_string() };
error!("Failed to generate token: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(tok))
}
}
} else {
let tok = OutToken {
token: "Unauthorised".to_string(),
};
warn!("Unauthorised JWT generate request: {:?}", tok);
(StatusCode::FORBIDDEN, Json(tok))
}
} else {
let tok = OutToken {
token: "Unauthorised".to_string(),
token: "ERROR Getting JWT_KEY environment variable".to_string(),
};
warn!("Unauthorised JWT generate request: {:?}", tok);
(StatusCode::FORBIDDEN, Json(tok))
error!("ERROR Getting JWT_KEY environment variable");
(StatusCode::INTERNAL_SERVER_ERROR, Json(tok))
}
}
async fn metrics() -> impl IntoResponse {
MEMORY_USAGE.set(get_memory_usage() as i64);
OPEN_FILES.set(get_open_files() as i64);
let metric_families = gather();
let encoder = TextEncoder::new();
let mut buffer = Vec::new();
if let Err(e) = encoder.encode(&metric_families, &mut buffer) {
// encoding error fallback
@@ -141,7 +194,6 @@ async fn metrics() -> impl IntoResponse {
.body(Body::from(format!("Failed to encode metrics: {}", e)))
.unwrap();
}
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", encoder.format_type())
@@ -149,7 +201,59 @@ async fn metrics() -> impl IntoResponse {
.unwrap()
}
// #[allow(dead_code)]
// async fn senderror() -> impl IntoResponse {
// Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("No live upstream found!\n")).unwrap()
// }
#[allow(clippy::needless_return)]
async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>) -> impl IntoResponse {
if params.contains_key("live") {
let r = upstreams_liveness_json(&st.full_upstreams, &st.current_upstreams);
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(format!("{}", r)))
.unwrap();
}
if params.contains_key("all") {
let resp = upstreams_to_json(&st.current_upstreams);
match resp {
Ok(j) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(j))
.unwrap()
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to get status: {}", e)))
.unwrap();
}
}
}
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from("Parameter mismatch"))
.unwrap()
}
pub async fn port_is_available(name: &str, address: &str) -> TcpListener {
let addr = SocketAddr::from_str(address).unwrap_or_else(|e| panic!("{}: Invalid address format: {:?}", name, e));
let t = Duration::from_secs(2);
//if addr.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED) {
// addr.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST));
//}
let p = addr.port();
loop {
match TcpListener::bind(addr).await {
Ok(listener) => {
return listener;
}
Err(_) => {
warn!("{} port is not available: {} will try again in {:?}", name, p, t);
tokio::time::sleep(t).await;
}
}
}
}
// -- ⚝ by Dave -- in NeoVim ⚝ --