163 Commits
v.0.46 ... dev

Author SHA1 Message Date
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
Ara Sadoyan
02de5f1c21 Merge remote-tracking branch 'origin/main' 2025-10-16 19:05:15 +02:00
Ara Sadoyan
9519280026 Path filter, and rate limiter for Consul 2025-10-16 19:04:46 +02:00
Ara Sadoyan
e87c60cf4f unifying kubernetes and file provider configs 2025-10-15 19:13:33 +02:00
Ara Sadoyan
25693a7058 Path filtering and rate limit for kubernetes 2025-10-15 13:42:05 +02:00
Ara Sadoyan
3b0b385ec7 Create FUNDING.yml 2025-10-03 11:02:21 +02:00
Ara Sadoyan
5359c2e8e9 Create LICENSE 2025-10-02 11:14:40 +02:00
Ara Sadoyan
2b62d1e6de configs update 2025-10-02 10:56:55 +02:00
Ara Sadoyan
8a290e5084 Kubernetes path based routing 2025-10-01 20:18:36 +02:00
Ara Sadoyan
3541b20c80 intermediate minor optimization 2025-10-01 13:47:30 +02:00
Ara Sadoyan
bd5fed9be0 Fix drop privileges, check root 2025-09-28 12:23:53 +02:00
Ara Sadoyan
b916b152ea Changed config file parser at startup, to keep initially dead nodes in list. 2025-09-25 18:32:46 +02:00
Ara Sadoyan
5d4915d6b9 Fixed drop root privileges on ports below 1024 2025-09-19 12:46:17 +02:00
Ara Sadoyan
3ea3996e27 upgrade to pingora 0.6 2025-09-18 14:15:50 +02:00
Ara Sadoyan
dd069b8532 minor fix 2025-09-17 16:51:57 +02:00
Ara Sadoyan
c78245e695 disable HC for upstream. 2025-09-16 12:54:23 +02:00
Ara Sadoyan
66b1a1c399 upstreams pathconfig fix 2025-09-15 15:22:21 +02:00
Ara Sadoyan
bba6dd8514 minor cleanup 2025-09-09 14:51:37 +02:00
Ara Sadoyan
79485ac69d minor cleanup 2025-09-04 18:16:09 +02:00
Ara Sadoyan
61c5625016 A coffee :-) 2025-09-02 14:57:47 +02:00
Ara Sadoyan
57bdc71acd A coffee :-) 2025-09-02 14:56:36 +02:00
Ara Sadoyan
9e09b829a6 README update 2025-09-01 17:02:57 +02:00
Ara Sadoyan
d3602fa578 Added Kubernetes API support, fo ingress controller. 2025-09-01 16:32:30 +02:00
Ara Sadoyan
e304482667 Optimized healthchecks and config file loading 2025-08-20 14:03:09 +02:00
Ara Sadoyan
f8118f9596 TLS grades change 2025-08-05 19:08:58 +02:00
Ara Sadoyan
f654312466 SSL cipher management 2025-07-29 21:25:27 +02:00
Ara Sadoyan
b44f7069a0 Configurable TLS ciphers 2025-07-27 11:15:49 +02:00
Ara Sadoyan
a44979ec82 Configurable TLS ciphers 2025-07-27 11:13:39 +02:00
Ara Sadoyan
ece4fa20af README 2025-07-24 13:50:15 +02:00
Ara Sadoyan
2ad3a059ab Per path rate limiter 2025-07-24 13:34:15 +02:00
Ara Sadoyan
6f012cee69 Code cleanup 2025-07-22 17:40:58 +02:00
Ara Sadoyan
51c88c8f7c Some structural changes and improvements 2025-07-12 16:17:45 +02:00
Ara Sadoyan
f91bc41103 benchmark image 2025-07-10 17:46:05 +02:00
Ara Sadoyan
21e1276ff5 Readme update 2025-07-09 15:22:38 +02:00
Ara Sadoyan
8463cdabbc Added configurable rate limiter 2025-07-09 15:01:20 +02:00
Ara Sadoyan
d0e4b52ce6 Enable/Disable config API from config 2025-07-04 15:06:05 +02:00
Ara Sadoyan
b552d24497 README 2025-07-02 19:00:05 +02:00
Ara Sadoyan
2e33d692bb Added optional minimal file server 2025-07-02 18:29:14 +02:00
Ara Sadoyan
e586967830 Code cleanup, nothing special 2025-06-30 18:24:25 +02:00
Ara Sadoyan
8d4e434d6a Dynamic load of SSL certificates from disk. 2025-06-19 18:32:44 +02:00
Ara Sadoyan
60b7b3aa7a README 2025-06-16 13:42:30 +02:00
Ara Sadoyan
569db8e18d Project rename. Load multiple certificates from folder. 2025-06-16 13:32:05 +02:00
Ara Sadoyan
4126249bcd Project rename. Load multiple certificates from folder. 2025-06-16 13:29:13 +02:00
Ara Sadoyan
0779f97277 README Update 2025-06-09 18:12:25 +02:00
Ara Sadoyan
b047331e6a README Update 2025-06-09 18:11:44 +02:00
Ara Sadoyan
a341fa30db Add TLS to API server 2025-06-09 18:06:16 +02:00
Ara Sadoyan
9d604d62e7 METRICS.md update 2025-06-07 15:51:23 +02:00
Ara Sadoyan
4a21700552 README update 2025-06-07 11:47:29 +02:00
Ara Sadoyan
f0157b6e8f README update 2025-06-07 11:38:07 +02:00
Ara Sadoyan
1370396ae8 README update 2025-06-07 10:56:31 +02:00
Ara Sadoyan
64ef4e14af README update 2025-06-07 10:11:39 +02:00
Ara Sadoyan
ffc2bab79f API server changes, improvements 2025-06-06 19:30:51 +02:00
40 changed files with 5948 additions and 2273 deletions

13
.cargo/config.toml Normal file
View File

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

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
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
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
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
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

6
.gitignore vendored
View File

@@ -5,9 +5,14 @@
*.dll *.dll
*.exe *.exe
*.sh *.sh
/docs/
/docs
etc/
etc
/target/ /target/
*.iml *.iml
.idea/ .idea/
.etc/
*.ipr *.ipr
*.iws *.iws
/out/ /out/
@@ -18,3 +23,4 @@ crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
/target /target
/z_shpo /z_shpo
Makefile

3430
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "gazan" name = "aralez"
version = "0.1.0" version = "0.9.2"
edition = "2021" edition = "2021"
[profile.release] [profile.release]
@@ -11,35 +11,42 @@ panic = "abort"
strip = true strip = true
[dependencies] [dependencies]
tokio = { version = "1.45.0", features = ["full"] } tokio = { version = "1.52.3", features = ["full"] }
#pingora = { version = "0.5.0", features = ["lb", "rustls"] } # openssl, rustls, boringssl pingora = { version = "0.8.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl
pingora = { version = "0.5.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl serde = { version = "1.0.228", features = ["derive"] }
serde = { version = "1.0.219", features = ["derive"] }
dashmap = "7.0.0-rc2" dashmap = "7.0.0-rc2"
pingora-core = "0.5.0" pingora-core = "0.8.0"
pingora-proxy = "0.5.0" pingora-proxy = "0.8.0"
pingora-http = "0.5.0" pingora-http = "0.8.0"
async-trait = "0.1.88" pingora-limits = "0.8.0"
env_logger = "0.11.8" async-trait = "0.1.89"
log = "0.4.27" log = "0.4.30"
futures = "0.3.31" futures = "0.3.32"
notify = "8.0.0" notify = "9.0.0-rc.4"
axum = { version = "0.8.4" } axum = { version = "0.8.9" }
reqwest = { version = "0.12.15", features = ["json", "native-tls-alpn"] } reqwest = { version = "0.13.4", features = ["json", "stream", "blocking"] }
#reqwest = { version = "0.12.15", features = ["json", "rustls-tls"] } serde_yml = "0.0.12"
#reqwest = { version = "0.12.15", default-features = false, features = ["rustls-tls", "json"] } rand = "0.10.1"
serde_yaml = "0.9.34-deprecated"
rand = "0.9.0"
base64 = "0.22.1" base64 = "0.22.1"
jsonwebtoken = "9.3.1" jsonwebtoken = { version = "10.4.0", default-features = false, features = ["use_pem", "rust_crypto"] }
tonic = "0.13.0" tonic = "0.14.6"
sha2 = { version = "0.11.0-pre.5", default-features = false } sha2 = { version = "0.11.0-rc.5", default-features = false }
base16ct = { version = "0.2.0", features = ["alloc"] } base16ct = { version = "1.0.0", features = ["alloc"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
arc-swap = "1.7.1" arc-swap = "1.9.1"
#rustls = { version = "0.23.27", features = ["ring"] }
mimalloc = { version = "0.1.46", default-features = false }
prometheus = "0.14.0" prometheus = "0.14.0"
lazy_static = "1.5.0" x509-parser = "0.18.1"
rustls-pemfile = "2.2.0"
tower-http = { version = "0.6.11", features = ["fs"] }
privdrop = "0.5.6"
ctrlc = "3.5.2"
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 = "1.4.0"
#mimalloc = { version = "0.1.52", default-features = false }
tikv-jemallocator = "0.7.0"
tikv-jemalloc-ctl = { version = "0.7.0", features = ["stats"] }

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,116 +0,0 @@
# 📈 Gazan Prometheus Metrics Reference
This document outlines Prometheus metrics for the [Gazan](https://github.com/sadoyan/gazan) reverse proxy.
These metrics can be used for monitoring, alerting and performance analysis.
Exposed to `http://config_address/metrics`
By default `http://127.0.0.1:3000/metrics`
---
## 🛠️ Prometheus Metrics
### 1. `gazan_requests_total`
- **Type**: `Counter`
- **Purpose**: Total amount requests served by Gazan.
**PromQL example:**
```promql
rate(gazan_requests_total[5m])
```
---
### 2. `gazan_errors_total`
- **Type**: `Counter`
- **Purpose**: Count of requests that resulted in an error.
**PromQL example:**
```promql
rate(gazan_errors_total[5m])
```
---
### 3. `gazan_responses_total{status="200"}`
- **Type**: `CounterVec`
- **Purpose**: Count of responses by HTTP status code.
**PromQL example:**
```promql
rate(gazan_responses_total{status=~"5.."}[5m]) > 0
```
> Useful for alerting on 5xx errors.
---
### 4. `gazan_response_latency_seconds`
- **Type**: `Histogram`
- **Purpose**: Tracks the latency of responses in seconds.
**Example bucket output:**
```prometheus
gazan_response_latency_seconds_bucket{le="0.01"} 15
gazan_response_latency_seconds_bucket{le="0.1"} 120
gazan_response_latency_seconds_bucket{le="0.25"} 245
gazan_response_latency_seconds_bucket{le="0.5"} 500
...
gazan_response_latency_seconds_count 1023
gazan_response_latency_seconds_sum 42.6
```
| Metric | Meaning |
|-------------------------|---------------------------------------------------------------|
| `bucket{le="0.1"} 120` | 120 requests were ≤ 100ms |
| `bucket{le="0.25"} 245` | 245 requests were ≤ 250ms |
| `count` | Total number of observations (i.e., total responses measured) |
| `sum` | Total time of all responses, in seconds |
### 🔍 How to interpret:
- `le` means “less than or equal to”.
- `count` is total amount of observations.
- `sum` is the total time (in seconds) of all responses.
**PromQL examples:**
🔹 **95th percentile latency**
```promql
histogram_quantile(0.95, rate(gazan_response_latency_seconds_bucket[5m]))
```
🔹 **Average latency**
```promql
rate(gazan_response_latency_seconds_sum[5m]) / rate(gazan_response_latency_seconds_count[5m])
```
---
## ✅ Notes
- Metrics are registered after the first served request.
---
✅ Summary of key metrics
| Metric Name | Type | What it Tells You |
|---------------------------------------|------------|---------------------------|
| `gazan_requests_total` | Counter | Total requests served |
| `gazan_errors_total` | Counter | Number of failed requests |
| `gazan_responses_total{status="200"}` | CounterVec | Response status breakdown |
| `gazan_response_latency_seconds` | Histogram | How fast responses are |
📘 *Last updated: May 2025*

531
README.md
View File

@@ -1,142 +1,204 @@
![Gazan](https://netangels.net/utils/gazan-white.jpg) ![Aralez](https://netangels.net/utils/aralez-white.jpg)
# Gazan - The beast-mode reverse proxy.
Gazan is a Reverse proxy, service mesh based on Cloudflare's Pingora
**What Gazan means?**
<ins>Gazan = Գազան = beast / wild animal in Armenian / Often used as a synonym to something great.</ins>.
Built on Rust, on top of **Cloudflares Pingora engine**, **Gazan** delivers world-class performance, security and scalability — right out of the box.
--- ---
## 🔧 Key Features # Aralez (Արալեզ)
- **Dynamic Config Reloads** — Upstreams can be updated live via API, no restart required ### **Reverse proxy built on top of Cloudflare's Pingora**
- **TLS Termination** — Built-in OpenSSL support
- **Upstreams TLS detection** — Gazan will automatically detect if upstreams uses secure connection
- **Authentication** — Supports Basic Auth, API tokens, and JWT verification
- **Load Balancing Strategies**
- Round-robin
- Failover with health checks
- Sticky sessions via cookies
- **Unified Port** — Serve HTTP and WebSocket traffic over the same connection
- **Memory Safe** — Created purely on Rust
- **High Performance** — Built with [Pingora](https://github.com/cloudflare/pingora) and tokio for async I/O
## 🌍 Highlights 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.
- ⚙️ **Upstream Providers:** Supports `file`-based static upstreams, dynamic service discovery via `Consul`. ---
- 🔁 **Hot Reloading:** Modify upstreams on the fly via `upstreams.yaml` — no restart needed. What Aralez means ?
- 🔮 **Automatic WebSocket Support:** Zero config — connection upgrades are handled seamlessly. **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>.
- 🔮 **Automatic GRPC Support:** Zero config, Requires `ssl` to proxy, gRPC is handled seamlessly.
- 🔮 **Upstreams Session Stickiness:** Enable/Disable Sticky sessions. Built on Rust, on top of **Cloudflares Pingora engine**, **Aralez** delivers world-class performance, security and scalability — right out of the box.
- 🔐 **TLS Termination:** Fully supports TLS for incoming and upstream traffic.
- 🛡️ **Built-in Authentication** Basic Auth, JWT, API key. [![Buy Me A Coffee](https://img.shields.io/badge/☕-Buy%20me%20a%20coffee-orange)](https://www.buymeacoffee.com/sadoyan)
- 🧠 **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.
--- ---
## 📁 File Structure ## Key Features
``` - **Dynamic Config Reloads** — Upstreams can be updated live via API, no restart required.
. - **Autoload of certificates** — Automatically loads new/changed certificates from a folder, without a restart.
├── main.yaml # Main configuration loaded at startup - **Lets Encrypt Certificates** — Ordering and renewal of SSL/TLS certificates via the HTTP-01 challenge.
├── upstreams.yaml # Watched config with upstream mappings - **Upstreams TLS detection** — Aralez will automatically detect if upstreams uses secure connection.
├── etc/ - **Built in rate limiter** — Globar or route limit requests to upstreams.
│ ├── server.crt # TLS certificate (required if using TLS) - **Authentication** — Supports Basic Auth, API tokens, and JWT verification.
└── key.pem # TLS private key - **Basic Auth**
``` - **API Key** via `x-api-key` header
- **JWT Auth**, with tokens issued by Aralez itself via `/jwt` API
- **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.
- **Upstream Providers:**
- `file` Upstreams are declared in config file.
- `consul` Upstreams are dynamically updated from Hashicorp Consul.
- `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.
--- ---
## 🛠 Configuration Overview ## Configuration Overview
### 🔧 `main.yaml` ### `main.yaml`
- `proxy_address_http`: `0.0.0.0:6193` (HTTP listener) | Key | Example Value | Description |
- `proxy_address_tls`: `0.0.0.0:6194` (TLS listener, optional) |----------------------------------|--------------------------|----------------------------------------------------------------------------------------------------|
- `config_address`: `0.0.0.0:3000` (HTTP API for remote config push) | **threads** | 12 | Number of running daemon threads. Optional, defaults to 1 |
- `upstreams_conf`: `etc/upstreams.yaml` (location of upstreams config) | **runuser** | aralez | Optional, Username for running aralez after dropping root privileges, requires to launch as root |
- `log_level`: `info` (verbosity of logs) | **rungroup** | aralez | Optional,Group for running aralez after dropping root privileges, requires to launch as root |
- `hc_method`: `HEAD`, `hc_interval`: `2s` (upstream health checks) | **daemon** | false | Run in background (boolean) |
- `user` Optional. Drop privileges to regular user. To bind to privileged ports. Requires to start as root. | **upstream_keepalive_pool_size** | 500 | Pool size for upstream keepalive connections |
- `group` Optional. Drop privileges to regular group | **pid_file** | /tmp/aralez.pid | Path to PID file |
- Other defaults: thread count, keep-alive pool size, etc. | **error_log** | /tmp/aralez_err.log | Path to error log file |
| **upgrade_sock** | /tmp/aralez.sock | Path to live upgrade socket file |
### 🌐 `upstreams.yaml` | **config_address** | 0.0.0.0:3000 | HTTP API address for pushing upstreams.yaml from remote location |
| **proxy_tls_grade** | (high, medium, unsafe) | Grade of TLS ciphers, for easy configuration. High matches Qualys SSL Labs A+ (defaults to medium) |
- `provider`: `file` or `consul` | **config_tls_key_file** | etc/key.pem | Private Key file path. Mandatory if proxy_address_tls is set, else optional |
- File-based upstreams define: | **proxy_address_http** | 0.0.0.0:6193 | Aralez HTTP bind address |
- Hostnames and routing paths | **proxy_address_tls** | 0.0.0.0:6194 | Aralez HTTPS bind address (Optional) |
- Backend servers (load-balanced) | **proxy_configs** | etc/ | The top directory of config files |
- Optional request headers, specific to this upstream | **upstreams_conf** | etc/upstreams.yaml | The location of upstreams file |
- Global headers (e.g., CORS) apply to all proxied responses | **log_level** | info | Log level , possible values : info, warn, error, debug, trace, off |
- Optional authentication (Basic, API Key, JWT) | **log_file** | /full/path/to/aralez.log | Optional, the location of log file. If thi entry does not exist logs will be emitted to stdout. |
| **hc_method** | HEAD | Healthcheck method (HEAD, GET, POST are supported) UPPERCASE |
| **hc_interval** | 2 | Interval for health checks in seconds |
| **master_key** | Random long string | Master key for working with API server and JWT Secret generation |
| **file_server_folder** | /some/local/folder | Optional, local folder to serve |
| **file_server_address** | 127.0.0.1:3002 | Optional, Local address for file server. Can set as upstream for public access |
| **config_api_enabled** | true | Boolean to enable/disable remote config push capability |
--- ---
## 🛠 Installation ## Installation
Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/gazan/releases) repo Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/aralez/releases) repo
Make the binary executable `chmod 755 ./gazan-VERSION` and run. Make the binary executable `chmod 755 ./aralez-VERSION` and run.
File names: File names:
| File Name | Description | | File Name | Description |
|--------------------------|---------------------------------------------------------------| |---------------------------------|----------------------------------------------------------------------------|
| `gazan-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency | | `aralez-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency |
| `gazan-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies | | `aralez-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies |
| `gazan-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency | | `aralez-x86_64-compat-musl.gz` | Static Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `gazan-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies | | `aralez-x86_64-compat-glibc.gz` | Dynamic Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `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>) |
## 🔌 Running the Proxy ## About binaries
```bash **glibc** builds are in general faster, but have few, basic, Glibc dependencies:
./gazan -c path/to/main.yaml
**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 /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 sadoyan/aralez
``` ```
## 🔌 Systemd integration **Dockerfile :**
```dockerfile
FROM debian:trixie-slim
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"]
```
## Running the Proxy
```bash ```bash
cat > /etc/systemd/system/gazan.service <<EOF ./aralez -c path/to/main.yaml
```
## 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] [Service]
Type=forking WorkingDirectory = /opt/aralez/
PIDFile=/run/gazan.pid ExecReload=/bin/kill -HUP
ExecStart=/bin/gazan -d -c /etc/gazan.conf ExecStart=/opt/aralez/aralez -c /opt/aralez/proxyconfigs/main.yaml
ExecReload=kill -QUIT $MAINPID KillMode=process
ExecReload=/bin/gazan -u -d -c /etc/gazan.conf KillSignal=SIGINT
LimitNOFILE=infinity
LimitNPROC=infinity
Restart=on-failure
RestartSec=2
StartLimitBurst=3
StartLimitIntervalSec=10
TasksMax=infinity
[Install]
WantedBy=multi-user.target
EOF EOF
``` ```
```bash ```bash
systemctl enable gazan.service. systemctl daemon-reload
systemctl restart gazan.service. systemctl enable aralez.service.
systemctl restart aralez.service.
``` ```
## 💡 Example ## Example upstreams config
A sample `upstreams.yaml` entry:
```yaml ```yaml
provider: "file" provider: "file"
sticky_sessions: false sticky_sessions: 8600
to_https: false to_https: false
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-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS" - "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400" - "Access-Control-Max-Age:86400"
authorization:
type: "jwt"
creds: "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
myhost.mydomain.com: myhost.mydomain.com:
paths: paths:
"/": "/":
rate_limit: 10
x4xx_limit: 10
to_https: false to_https: false
headers: server_headers:
- "X-Something-Else:Foobar"
- "X-Another-Header:Hohohohoho"
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa" - "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Hopaaaaaaaaaaaar" - "X-Proxy-From:Hopaaaaaaaaaaaar"
servers: servers:
@@ -144,77 +206,105 @@ myhost.mydomain.com:
- "127.0.0.2:8000" - "127.0.0.2:8000"
"/foo": "/foo":
to_https: true to_https: true
headers: authorization:
type: "jwt"
data: "266463d1-210a-4787-9a81-4aacb37a8723"
client_headers:
- "X-Another-Header:Hohohohoho" - "X-Another-Header:Hohohohoho"
servers: servers:
- "127.0.0.4:8443" - "127.0.0.4:8443"
- "127.0.0.5:8443" - "127.0.0.5:8443"
"/.well-known/acme-challenge":
healthcheck: false
servers:
- "127.0.0.1:8001"
DEFAULT:
paths:
"/":
servers:
- "127.0.0.1:3000"
``` ```
**This means:** **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. - HTTP to HTTPS redirect disabled globally, but can be overridden by `to_https` setting per upstream.
- 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`. - 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 Gazan. - Plain HTTP to `myhost.mydomain.com/foo` will get 301 redirect to configured TLS port of Aralez.
- `myhost.mydomain.com/foo` will require authentication with JWT token, signed by `266463d1-210a-4787-9a81-4aacb37a8723`.
- Requests to `myhost.mydomain.com/foo` will be proxied to `127.0.0.4` and `127.0.0.5`. - Requests to `myhost.mydomain.com/foo` will be proxied to `127.0.0.4` and `127.0.0.5`.
- Requests to `myhost.mydomain.com/.well-known/acme-challenge` will be proxied to `127.0.0.1:8001`, but healthcheks are disabled.
- SSL/TLS for upstreams is detected automatically, no need to set any config parameter. - SSL/TLS for upstreams is detected automatically, no need to set any config parameter.
- Assuming the `127.0.0.5:8443` is SSL protected. The inner traffic will use TLS. - Assuming the `127.0.0.5:8443` is SSL protected. The inner traffic will use TLS.
- Self signed certificates are silently accepted. - Self-signed certificates are silently accepted.
- Global headers (CORS for this case) will be injected to all upstreams - Global headers (CORS for this case) will be injected to all upstreams.
- Additional headers will be injected into the request for `myhost.mydomain.com`. - 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. - 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), - `DEFAULT` catch up everything else and proxy to `127.0.0.1:3000`
- Firs parameter specifies the mechanism of authorisation `jwt` - This is a special upstream and in order to do the catch-up jub it must be **DEFAULT** all capitals
- Second is the secret key for validating `jwt` tokens
--- ---
## 🔄 Hot Reload ## Hot Reload
- Changes to `upstreams.yaml` are applied immediately. - Changes to `upstreams.yaml` are applied immediately on save without restart .
- No need to restart the proxy — just save the file. - If `consul` or `kubernetes` provider is chosen, upstreams will be periodically update from API.
- If `consul` provider is chosen, upstreams will be periodically update from Consul's API.
--- ---
## 🔐 TLS Support ## TLS Support
To enable TLS for A proxy server: Currently only OpenSSL is supported, working on Boringssl and Rustls To enable TLS for the proxy server.
1. Set `proxy_address_tls` in `main.yaml` - Set `proxy_address_tls` in `main.yaml`
2. Provide `tls_certificate` and `tls_key_file` - Provide at least on `tls_certificate/tls_key_file` pair.
- First pair is required to create the TLS listener.
- This pair can be anything, even self-signed with dummy domain.
- After getting normal certificate it can be deleted
--- ---
## 📡 Remote Config API ## Remote Config API
You can push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates. Push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates.
URL parameter. `key=MASTERKEY` is required. `MASTERKEY` is the value of `master_key` in the `main.yaml`
```bash ```bash
curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf?key=${MASTERKEY}
``` ```
--- ---
## 🔐 Authentication (Optional) ## Authentication (Optional)
- Adds authentication to all requests. - Adds authentication to all requests.
- Only one method can be active at a time. - Only one method can be active at a time.
- `basic` : Standard HTTP Basic Authentication requests. - `basic` : Standard HTTP Basic Authentication requests.
- `apikey` : Authentication via `x-api-key` header, which should match the value in config. - `apikey` : Authentication via `x-api-key` header, which should match the value in config.
- `jwt`: JWT authentication implemented via `gazantoken=` url parameter. `/some/url?gazantoken=TOKEN` - `jwt`: JWT authentication implemented via `araleztoken=` url parameter. `/some/url?araleztoken=TOKEN`
- `jwt`: JWT authentication implemented via `Authorization: Bearer <token>` header. - `jwt`: JWT authentication implemented via `Authorization: Bearer <token>` header.
- To obtain JWT token, you should send **generate** request to built in api server's `/jwt` endpoint. - To obtain JWT a token, you should send **generate** request to built in api server's `/jwt` endpoint.
- `masterkey`: should match configured `masterkey` in `main.yaml` and `upstreams.yaml`. - `master_key`: should match configured `masterkey` in `main.yaml` and `upstreams.yaml`.
- `owner` : Just a placeholder, can be anything. - `owner` : Just a placeholder, can be anything.
- `valid` : Time in minutes during which the generated token will be valid. - `valid` : Time in minutes during which the generated token will be valid.
**Example JWT token generateion request** **Example JWT token generation request**
```bash ```bash
PAYLOAD='{ PAYLOAD='{
"masterkey": "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774", "master_key": "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774",
"owner": "valod", "owner": "valod",
"valid": 10 "valid": 10
}' }'
@@ -234,7 +324,7 @@ curl -H "Authorization: Bearer ${TOK}" -H 'Host: myip.mydomain.com' http://127.0
With URL parameter (Very useful if you want to generate and share temporary links) With URL parameter (Very useful if you want to generate and share temporary links)
```bash ```bash
curl -H 'Host: myip.mydomain.com' "http://127.0.0.1:6193/?gazantoken=${TOK}`" curl -H 'Host: myip.mydomain.com' "http://127.0.0.1:6193/?araleztoken=${TOK}`"
``` ```
**Example Request with API Key** **Example Request with API Key**
@@ -251,13 +341,13 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
``` ```
## 📃 License ## License
[Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) [Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
--- ---
## 🧠 Notes ## Notes
- Uses Pingora under the hood for efficiency and flexibility. - Uses Pingora under the hood for efficiency and flexibility.
- Designed for edge proxying, internal routing, or hybrid cloud scenarios. - Designed for edge proxying, internal routing, or hybrid cloud scenarios.
@@ -265,3 +355,204 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
- Transparent, fully automatic gRPC proxy. - Transparent, fully automatic gRPC proxy.
- Sticky session support. - Sticky session support.
- HTTP2 ready. - HTTP2 ready.
### Summary Table: Feature Comparison
| 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++ |
---
**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**
- A dedicated server for running **Oha**
- The following upstreams configuration.
- 9 test URLs from simple `/` to nested up to 7 subpaths.
```yaml
myhost.mydomain.com:
paths:
"/":
to_https: false
headers:
- "X-Proxy-From:Aralez"
servers:
- "192.168.211.211:8000"
- "192.168.211.212:8000"
- "192.168.211.213:8000"
"/ping":
to_https: false
headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez"
servers:
- "192.168.211.211:8000"
- "192.168.211.212:8000"
```
## Results reflect synthetic performance under optimal conditions.
- CPU : Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz
- 300 : simultaneous connections
- Duration : 10 Minutes
- Binary : aralez-x86_64-glibc
```
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
Total data: 0 B
Size/request: 0 B
Size/sec: 0 B
Response time histogram:
0.000 [1] |
0.022 [77668026] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.043 [190362] |
0.064 [7908] |
0.086 [319] |
0.107 [4] |
0.128 [0] |
0.150 [0] |
0.171 [0] |
0.192 [0] |
0.214 [4] |
Response time distribution:
10.00% in 0.0012 secs
25.00% in 0.0016 secs
50.00% in 0.0020 secs
75.00% in 0.0026 secs
90.00% in 0.0033 secs
95.00% in 0.0040 secs
99.00% in 0.0078 secs
99.90% in 0.0278 secs
99.99% in 0.0434 secs
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
Status code distribution:
[200] 77866624 responses
Error distribution:
[158] aborted due to deadline
```
![Aralez](https://netangels.net/utils/glibc10.png)
- CPU : Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz
- 300 : simultaneous connections
- Duration : 10 Minutes
- Binary : aralez-x86_64-musl
```
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
Total data: 0 B
Size/request: 0 B
Size/sec: 0 B
Response time histogram:
0.000 [1] |
0.022 [74254679] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.044 [61400] |
0.066 [5911] |
0.087 [385] |
0.109 [0] |
0.131 [0] |
0.153 [0] |
0.175 [0] |
0.196 [0] |
0.218 [1] |
Response time distribution:
10.00% in 0.0012 secs
25.00% in 0.0016 secs
50.00% in 0.0021 secs
75.00% in 0.0028 secs
90.00% in 0.0037 secs
95.00% in 0.0045 secs
99.00% in 0.0077 secs
99.90% in 0.0214 secs
99.99% in 0.0424 secs
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
Status code distribution:
[200] 74322377 responses
Error distribution:
[228] aborted due to deadline
```
![Aralez](https://netangels.net/utils/musl10.png)
## 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.
1. Sever runs Aralez, Traefik, Nginx on different ports. Tuned as much as I could .
2. 3x Upstreams servers, running Nginx. Replying with dummy json hardcoded in config file for max performance.
All servers are connected to the same switch with 1GB port in datacenter , not a home lab. The results:
![Aralez](https://raw.githubusercontent.com/sadoyan/aralez/refs/heads/main/assets/bench.png)
The results show requests per second performed by Load balancer. You can see 3 batches with 800 concurrent users.
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.
## Links
- [**Documentation**](https://aralez.rs) : The manual you should read
- [**Downloads**](https://github.com/sadoyan/aralez/releases) : Binary downloads
- [**Issues**](https://github.com/sadoyan/aralez/issues) : Issues and requests

BIN
assets/bench.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

BIN
assets/bench2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -1,19 +1,23 @@
# Main configuration file , applied on startup # Main configuration file, applied on startup
threads: 12 # Nubber of daemon threads default setting threads: 12 # Number of daemon threads default setting
#user: pastor # Username for running gazan after dropping root privileges, requires program to start as root #runuser: pastor # Username for running aralez after dropping root privileges, requires program to start as root
#group: pastor # Group for running gazan 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 daemon: false # Run in background
upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections
pid_file: /tmp/gazan.pid # Path to PID file pid_file: /tmp/aralez.pid # Path to PID file
error_log: /tmp/gazan_err.log # Path to error log error_log: /tmp/aralez_err.log # Path to error log
upgrade_sock: /tmp/gazan.sock # Path to socket file upgrade_sock: /tmp/aralez.sock # Path to socket file
config_api_enabled: true # Boolean to enable/disable remote config push capability.
config_address: 0.0.0.0:3000 # HTTP API address for pushing upstreams.yaml from remote location config_address: 0.0.0.0:3000 # HTTP API address for pushing upstreams.yaml from remote location
proxy_address_http: 0.0.0.0:6193 # Proxy HTTP bind address proxy_address_http: 0.0.0.0:6193 # Proxy HTTP bind address
proxy_address_tls: 0.0.0.0:6194 # Optional, Proxy TLS bind address proxy_address_tls: 0.0.0.0:6194 # Optional, Proxy TLS bind address
tls_certificate: etc/server.crt # Mandatory if proxy_address_tls is set proxy_configs: /opt/Rust/Projects/asyncweb/etc # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key.
tls_key_file: etc/key.pem # Mandatory if proxy_address_tls is set proxy_tls_grade: high # Grade of TLS suite for proxy (high, medium, unsafe), matching grades of Qualys SSL Labs
upstreams_conf: etc/upstreams.yaml # the location of upstreams file upstreams_conf: /opt/Rust/Projects/asyncweb/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.
log_level: info # info, warn, error, debug, trace, off log_level: info # info, warn, error, debug, trace, off
log_file: /tmp/aralez.log # Optional, the location of log file. If this entry does not exist logs will be emitted to stdout.
hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE
hc_interval: 2 #Interval for health checks in seconds hc_interval: 2 #Interval for health checks in seconds
master_key: 910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774 # Mater key for working with API server and JWT Secret #master_key: 910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774 # Mater key for working with API server and JWT Secret

View File

@@ -11,7 +11,7 @@ upstreams:
"/": "/":
ssl: false ssl: false
headers: headers:
- "X-Proxy-From:Gazan" - "X-Proxy-From:Aralez"
servers: servers:
- "192.168.221.213:8000" - "192.168.221.213:8000"
- "192.168.221.214:8000" - "192.168.221.214:8000"

View File

@@ -1,45 +1,89 @@
# The file under watch and hot reload, changes are applied immediately, no need to restart or reload. # The file under watch and hot reload, changes are applied immediately, no need to restart or reload.
provider: "file" # consul provider: "file" # "file" "consul" "kubernetes"
sticky_sessions: false sticky_sessions: 8600
to_ssl: false to_https: false
headers: rate_limit: 300
x4xx_limit: 200
server_headers:
- "X-Forwarded-Proto:https"
- "X-Forwarded-Port:443"
client_headers:
- "Access-Control-Allow-Origin:*" - "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS" - "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400" - "Access-Control-Max-Age:86400"
- "X-Custom-Header:Something Special" #authorization:
authorization: # type: "jwt"
type: "jwt" # creds: "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
creds: "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
# type: "basic" # type: "basic"
# creds: "user:Passw0rd" # creds: "username:Pa$$w0rd"
# type: "apikey" # type: "apikey"
# creds: "5ecbf799-1343-4e94-a9b5-e278af5cd313-56b45249-1839-4008-a450-a60dc76d2bae" # creds: "5ecbf799-1343-4e94-a9b5-e278af5cd313-56b45249-1839-4008-a450-a60dc76d2bae"
consul: # If the provider is consul. Otherwise, ignored. consul:
servers: servers:
- "http://master1:8500" - "http://192.168.1.199:8500"
- "http://192.168.22.1:8500" - "http://192.168.1.200:8500"
- "http://master1.foo.local:8500" - "http://192.168.1.201:8500"
services: # proxy: The hostname to access the proxy server, real : The real service name in Consul database. services: # hostname: The hostname to access the proxy server, upstream : The real service name in Consul database.
- proxy: "proxy-frontend-dev-frontend-srv" - hostname: "webapi-service"
real: "frontend-dev-frontend-srv" upstream: "webapi-service-health"
path: "/one"
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez"
rate_limit: 1
to_https: false
- hostname: "webapi-service"
upstream: "webapi-service-health"
path: "/"
token: "8e2db809-845b-45e1-8b47-2c8356a09da0-a4370955-18c2-4d6e-a8f8-ffcc0b47be81" # Consul server access token, If Consul auth is enabled 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.
services:
- hostname: "webapi-service"
path: "/"
upstream: "webapi-service"
- hostname: "webapi-service"
upstream: "console-service"
path: "/one"
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez"
rate_limit: 100
to_https: false
- hostname: "webapi-service"
upstream: "rambul-service"
path: "/two"
- 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
upstreams: upstreams:
myip.mydomain.com: myip.mydomain.com:
paths: paths:
"/": "/":
rate_limit: 200
x4xx_limit: 100
to_https: false to_https: false
headers: client_headers:
- "X-Proxy-From:Gazan" - "X-Proxy-From:Aralez"
servers: # List of upstreams HOST:PORT servers:
- "127.0.0.1:8000" - "127.0.0.1:8000"
- "127.0.0.2:8000" - "127.0.0.2:8000"
- "127.0.0.3:8000" - "127.0.0.3:8000"
- "127.0.0.4:8000" - "127.0.0.4:8000"
- "127.0.0.5:8000"
"/ping": "/ping":
to_https: true authorization: # Will be ignored if global authentication is enabled.
headers: type: "basic"
creds: "admin:admin"
to_https: false
server_headers:
- "X-Forwarded-Proto:https"
- "X-Forwarded-Port:443"
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa" - "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Gazan" - "X-Proxy-From:Aralez"
servers: servers:
- "127.0.0.1:8000" - "127.0.0.1:8000"
- "127.0.0.2:8000" - "127.0.0.2:8000"
@@ -49,7 +93,8 @@ upstreams:
polo.mydomain.com: polo.mydomain.com:
paths: paths:
"/": "/":
headers: to_https: false
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa" - "X-Some-Thing:Yaaaaaaaaaaaaaaa"
servers: servers:
- "192.168.1.1:8000" - "192.168.1.1:8000"
@@ -58,3 +103,19 @@ upstreams:
- "127.0.0.2:8000" - "127.0.0.2:8000"
- "127.0.0.3:8000" - "127.0.0.3:8000"
- "127.0.0.4: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"
rdr.mydomain.com:
paths:
"/":
redirect_to: "https://som.other.domain:6194"
healthcheck: false
servers:
- "127.0.0.1:8080"

View File

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

3
src/tls.rs Normal file
View File

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

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

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

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

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

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

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

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

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

185
src/tls/load.rs Normal file
View File

@@ -0,0 +1,185 @@
use crate::tls::grades;
use dashmap::DashMap;
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;
use std::fs::File;
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,
pub key_path: String,
}
#[derive(Debug)]
pub struct CertificateInfo {
pub common_names: Vec<String>,
pub alt_names: Vec<String>,
pub ssl_context: SslContext,
#[allow(dead_code)]
pub cert_path: String, // Only used for logging
#[allow(dead_code)]
pub key_path: String, // Only used for logging
}
#[derive(Debug)]
pub struct Certificates {
configs: Vec<CertificateInfo>,
name_map: DashMap<String, SslContext>,
pub default_cert_path: String,
pub default_key_path: String,
}
impl Certificates {
pub fn new(configs: &Vec<CertificateConfig>, _grade: &str) -> Option<Self> {
let default_cert = configs.first().expect("At least one TLS certificate required");
let mut cert_infos = Vec::new();
let name_map: DashMap<String, SslContext> = DashMap::new();
for config in configs {
let cert_info = load_cert_info(&config.cert_path, &config.key_path, _grade);
match cert_info {
Some(cert) => {
for name in &cert.common_names {
name_map.insert(name.clone(), cert.ssl_context.clone());
}
for name in &cert.alt_names {
name_map.insert(name.clone(), cert.ssl_context.clone());
}
cert_infos.push(cert)
}
None => {
error!("Unable to load certificate info | public: {}, private: {}", &config.cert_path, &config.key_path);
return None;
}
}
}
Some(Self {
name_map,
configs: cert_infos,
default_cert_path: default_cert.cert_path.clone(),
default_key_path: default_cert.key_path.clone(),
})
}
fn find_ssl_context(&self, server_name: &str) -> Option<SslContext> {
if let Some(ctx) = self.name_map.get(server_name) {
return Some(ctx.clone());
}
for config in &self.configs {
for name in &config.common_names {
if name.starts_with("*.") && server_name.ends_with(&name[1..]) {
return Some(config.ssl_context.clone());
}
}
for name in &config.alt_names {
if name.starts_with("*.") && server_name.ends_with(&name[1..]) {
return Some(config.ssl_context.clone());
}
}
}
None
}
pub fn server_name_callback(&self, ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert) -> Result<(), SniError> {
let server_name = ssl_ref.servername(NameType::HOST_NAME);
log::debug!("TLS connect: server_name = {:?}, ssl_ref = {:?}, ssl_alert = {:?}", server_name, ssl_ref, ssl_alert);
// let start_time = Instant::now();
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)?;
}
None => {
log::debug!("No matching server name found");
}
}
}
// println!("Context ==> {:?} <==", start_time.elapsed());
Ok(())
}
}
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();
let file = File::open(cert_path);
match file {
Err(e) => {
log::error!("Failed to open certificate file: {:?}", e);
return None;
}
Ok(file) => {
let mut reader = BufReader::new(file);
match read_one(&mut reader) {
Err(e) => {
log::error!("Failed to decode PEM from certificate file: {:?}", e);
return None;
}
Ok(leaf) => match leaf {
Some(Item::X509Certificate(cert)) => match X509Certificate::from_der(&cert) {
Err(NomErr::Error(e)) | Err(NomErr::Failure(e)) => {
log::error!("Failed to parse certificate: {:?}", e);
return None;
}
Err(_) => {
log::error!("Unknown error while parsing certificate");
return None;
}
Ok((_, x509)) => {
let subject = x509.subject();
for attr in subject.iter_common_name() {
if let Ok(cn) = attr.as_str() {
common_names.insert(cn.to_string());
}
}
if let Ok(Some(san)) = x509.subject_alternative_name() {
for name in san.value.general_names.iter() {
if let GeneralName::DNSName(dns) = name {
let dns_string = dns.to_string();
if !common_names.contains(&dns_string) {
alt_names.insert(dns_string);
}
}
}
}
}
},
_ => {
log::error!("Failed to read certificate");
return None;
}
},
}
}
}
if let Ok(ssl_context) = create_ssl_context(cert_path, key_path) {
Some(CertificateInfo {
cert_path: cert_path.to_string(),
key_path: key_path.to_string(),
common_names: common_names.into_iter().collect(),
alt_names: alt_names.into_iter().collect(),
ssl_context,
})
} else {
log::error!("Failed to create SSL context from cert paths");
None
}
}
fn create_ssl_context(cert_path: &str, key_path: &str) -> Result<SslContext, Box<dyn std::error::Error>> {
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(grades::prefer_h2);
let built = ctx.build();
Ok(built)
}

View File

@@ -1,10 +1,14 @@
pub mod auth; pub mod auth;
pub mod consul;
pub mod discovery; pub mod discovery;
mod filewatch; mod filewatch;
pub mod fordebug;
pub mod healthcheck; pub mod healthcheck;
pub mod httpclient;
pub mod jwt; pub mod jwt;
pub mod kuberconsul;
pub mod metrics; pub mod metrics;
pub mod parceyaml; pub mod parceyaml;
pub mod state;
pub mod structs; pub mod structs;
pub mod tools; pub mod tools;
// pub mod watchksecret;

View File

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

View File

@@ -1,141 +0,0 @@
use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::{Configuration, ServiceMapping, UpstreamsDashMap};
use crate::utils::tools::{clone_dashmap_into, compare_dashmaps};
use dashmap::DashMap;
use futures::channel::mpsc::Sender;
use futures::SinkExt;
use log::{info, warn};
use pingora::prelude::sleep;
use rand::Rng;
use reqwest::header::{HeaderMap, HeaderValue};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
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(fp: String, mut toreturn: Sender<Configuration>) {
let config = load_configuration(fp.as_str(), "filepath");
let headers = DashMap::new();
match config {
Some(config) => {
if config.typecfg.to_string() != "consul" {
info!("Not running Consul discovery, requested type is: {}", config.typecfg);
return;
}
info!("Consul Discovery is enabled : {}", config.typecfg);
let consul = config.consul.clone();
let prev_upstreams = UpstreamsDashMap::new();
match consul {
Some(consul) => {
let servers = consul.servers.unwrap();
info!("Consul Servers => {:?}", servers);
let end = servers.len();
loop {
let num = rand::rng().random_range(1..end);
headers.clear();
for (k, v) in config.headers.clone() {
headers.insert(k.to_string(), v);
}
let consul_data = servers.get(num).unwrap().to_string();
let upstreams = consul_request(consul_data, consul.services.clone(), consul.token.clone());
match upstreams.await {
Some(upstreams) => {
if !compare_dashmaps(&upstreams, &prev_upstreams) {
let mut tosend: Configuration = Configuration {
upstreams: Default::default(),
headers: Default::default(),
consul: None,
typecfg: "".to_string(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(&upstreams, &prev_upstreams);
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();
toreturn.send(tosend).await.unwrap();
}
}
None => {}
}
sleep(Duration::from_secs(5)).await;
}
}
None => {}
}
}
None => {}
}
}
async fn consul_request(url: String, whitelist: Option<Vec<ServiceMapping>>, token: Option<String>) -> Option<UpstreamsDashMap> {
let upstreams = UpstreamsDashMap::new();
let ss = url.clone() + "/v1/catalog/service/";
match whitelist {
Some(whitelist) => {
for k in whitelist.iter() {
let pref: String = ss.clone() + &k.real;
let list = get_by_http(pref.clone(), token.clone()).await;
match list {
Some(list) => {
upstreams.insert(k.proxy.clone(), list);
}
None => {
warn!("Whitelist not found for {}", k.proxy);
}
}
}
}
None => {}
}
Some(upstreams)
}
async fn get_by_http(url: String, token: Option<String>) -> Option<DashMap<String, (Vec<(String, u16, bool, bool, bool)>, AtomicUsize)>> {
let client = reqwest::Client::new();
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 u = client.get(url).timeout(to).send();
let mut values = Vec::new();
let upstreams: DashMap<String, (Vec<(String, u16, bool, bool, bool)>, AtomicUsize)> = DashMap::new();
match u.await {
Ok(r) => {
let jason = r.json::<Vec<Service>>().await;
match jason {
Ok(whitelist) => {
for service in whitelist {
let addr = service.tagged_addresses.get("lan_ipv4").unwrap().address.clone();
let prt = service.tagged_addresses.get("lan_ipv4").unwrap().port.clone();
let to_add = (addr, prt, false, false, false);
values.push(to_add);
}
}
Err(_) => return None,
}
}
Err(_) => return None,
}
upstreams.insert("/".to_string(), (values, AtomicUsize::new(0)));
Some(upstreams)
}

View File

@@ -1,20 +1,34 @@
use crate::utils::consul;
use crate::utils::filewatch; use crate::utils::filewatch;
use crate::utils::structs::Configuration; use crate::utils::kuberconsul::{ConsulDiscovery, KubernetesDiscovery, ServiceDiscovery};
use crate::utils::structs::{Configuration, UpstreamsDashMap};
use crate::web::webserver; use crate::web::webserver;
use async_trait::async_trait; use async_trait::async_trait;
use futures::channel::mpsc::Sender; use futures::channel::mpsc::Sender;
use std::sync::Arc;
pub struct APIUpstreamProvider {
pub config_api_enabled: bool,
pub address: 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>,
pub current_upstreams: Arc<UpstreamsDashMap>,
pub full_upstreams: Arc<UpstreamsDashMap>,
}
pub struct FromFileProvider { pub struct FromFileProvider {
pub path: String, pub path: String,
} }
pub struct APIUpstreamProvider {
pub address: String,
pub masterkey: String,
}
pub struct ConsulProvider { pub struct ConsulProvider {
pub path: String, pub config: Arc<Configuration>,
}
pub struct KubernetesProvider {
pub config: Arc<Configuration>,
} }
#[async_trait] #[async_trait]
@@ -25,7 +39,7 @@ pub trait Discovery {
#[async_trait] #[async_trait]
impl Discovery for APIUpstreamProvider { impl Discovery for APIUpstreamProvider {
async fn start(&self, toreturn: Sender<Configuration>) { async fn start(&self, toreturn: Sender<Configuration>) {
webserver::run_server(self.address.clone(), self.masterkey.clone(), toreturn).await; webserver::run_server(self, toreturn, self.current_upstreams.clone(), self.full_upstreams.clone()).await;
} }
} }
@@ -39,6 +53,13 @@ impl Discovery for FromFileProvider {
#[async_trait] #[async_trait]
impl Discovery for ConsulProvider { impl Discovery for ConsulProvider {
async fn start(&self, tx: Sender<Configuration>) { async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(consul::start(self.path.clone(), tx.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(KubernetesDiscovery.fetch_upstreams(self.config.clone(), tx));
} }
} }

View File

@@ -2,7 +2,7 @@ use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration; use crate::utils::structs::Configuration;
use futures::channel::mpsc::Sender; use futures::channel::mpsc::Sender;
use futures::SinkExt; use futures::SinkExt;
use log::{error, info, warn}; use log::error;
use notify::event::ModifyKind; use notify::event::ModifyKind;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use pingora::prelude::sleep; use pingora::prelude::sleep;
@@ -15,19 +15,7 @@ pub async fn start(fp: String, mut toreturn: Sender<Configuration>) {
let file_path = fp.as_str(); let file_path = fp.as_str();
let parent_dir = Path::new(file_path).parent().unwrap(); let parent_dir = Path::new(file_path).parent().unwrap();
let (local_tx, mut local_rx) = tokio::sync::mpsc::channel::<notify::Result<Event>>(1); let (local_tx, mut local_rx) = tokio::sync::mpsc::channel::<notify::Result<Event>>(1);
let snd = load_configuration(file_path, "filepath");
match snd {
Some(snd) => {
if snd.typecfg != "file" {
warn!("Disabling file watcher, requested discovery type is: {}", snd.typecfg);
return;
}
info!("Watching for changes in {:?}", parent_dir);
toreturn.send(snd).await.unwrap();
}
None => {}
}
let _watcher_handle = task::spawn_blocking({ let _watcher_handle = task::spawn_blocking({
let parent_dir = parent_dir.to_path_buf(); // Move directory path into the closure let parent_dir = parent_dir.to_path_buf(); // Move directory path into the closure
move || { move || {
@@ -49,18 +37,13 @@ pub async fn start(fp: String, mut toreturn: Sender<Configuration>) {
match event { match event {
Ok(e) => match e.kind { Ok(e) => match e.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(..) | EventKind::Remove(..) => { EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(..) | EventKind::Remove(..) => {
if e.paths[0].to_str().unwrap().ends_with("yaml") { if e.paths[0].to_str().unwrap().ends_with("yaml") && start.elapsed() > Duration::from_secs(2) {
if start.elapsed() > Duration::from_secs(2) {
start = Instant::now(); start = Instant::now();
// info!("Config File changed :=> {:?}", e); // info!("Config File changed :=> {:?}", e);
let snd = load_configuration(file_path, "filepath"); let snd = load_configuration(file_path, "filepath").await.0;
match snd { if let Some(snd) = snd {
Some(snd) => {
toreturn.send(snd).await.unwrap(); toreturn.send(snd).await.unwrap();
} }
None => {}
}
}
} }
} }
_ => (), _ => (),

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

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

View File

@@ -1,7 +1,7 @@
use crate::utils::structs::{UpstreamsDashMap, UpstreamsIdMap}; use crate::utils::structs::{InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::utils::tools::*; use crate::utils::tools::*;
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info, warn}; use log::{error, warn};
use reqwest::{Client, Version}; use reqwest::{Client, Version};
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::Arc;
@@ -11,143 +11,141 @@ use tonic::transport::Endpoint;
pub async fn hc2(upslist: Arc<UpstreamsDashMap>, fullist: Arc<UpstreamsDashMap>, idlist: Arc<UpstreamsIdMap>, params: (&str, u64)) { pub async fn hc2(upslist: Arc<UpstreamsDashMap>, fullist: Arc<UpstreamsDashMap>, idlist: Arc<UpstreamsIdMap>, params: (&str, u64)) {
let mut period = interval(Duration::from_secs(params.1)); let mut period = interval(Duration::from_secs(params.1));
let mut first_run = 0; let client = Client::builder().timeout(Duration::from_secs(params.1)).danger_accept_invalid_certs(true).build().unwrap();
loop { loop {
tokio::select! { tokio::select! {
_ = period.tick() => { _ = period.tick() => {
let totest : UpstreamsDashMap = DashMap::new(); // populate_upstreams(&upslist, &fullist, &idlist, params, &client).await;
let fclone : UpstreamsDashMap = clone_dashmap(&fullist); let totest = build_upstreams(&fullist, params.0, &client).await;
for val in fclone.iter() { if !compare_dashmaps(&totest, &upslist) {
let host = val.key();
let inner = DashMap::new();
let mut _scheme: (String, u16, bool, bool, bool) = ("".to_string(), 0, false, false, false);
for path_entry in val.value().iter() {
// let inner = DashMap::new();
let path = path_entry.key();
let mut innervec= Vec::new();
for k in path_entry.value().0 .iter().enumerate() {
let (ip, port, _ssl, _version, _redir) = k.1;
let mut _link = String::new();
let tls = detect_tls(ip, port).await;
let mut is_h2 = false;
// if tls.1 == Some(Version::HTTP_11) {
// println!(" V1: ==> {:?}", tls.1)
// }else if tls.1 == Some(Version::HTTP_2) {
// is_h2 = true;
// println!(" V2: ==> {:?}", tls.1)
// }
if tls.1 == Some(Version::HTTP_2) {
is_h2 = true;
// println!(" V2: ==> {} ==> {:?}", tls.0, tls.1)
}
match tls.0 {
true => _link = format!("https://{}:{}{}", ip, port, path),
false => _link = format!("http://{}:{}{}", ip, port, path),
}
// if _pref == "https://" {
// _scheme = (ip.to_string(), *port, true);
// }else {
// _scheme = (ip.to_string(), *port, false);
// }
_scheme = (ip.to_string(), *port, tls.0, is_h2, *_redir);
// let link = format!("{}{}:{}{}", _pref, ip, port, path);
let resp = http_request(_link.as_str(), params.0, "").await;
match resp.0 {
true => {
if resp.1 {
_scheme = (ip.to_string(), *port, tls.0, true, *_redir);
}
innervec.push(_scheme.clone());
}
false => {
warn!("Dead Upstream : {}", _link);
}
}
}
inner.insert(path.clone().to_owned(), (innervec, AtomicUsize::new(0)));
}
totest.insert(host.clone(), inner);
}
if first_run == 1 {
info!("Performing initial hatchecks and upstreams ssl detection");
clone_idmap_into(&totest, &idlist);
info!("Gazan is up and ready to serve requests, the upstreams list is:");
print_upstreams(&totest)
}
first_run+=1;
if ! compare_dashmaps(&totest, &upslist){
clone_dashmap_into(&totest, &upslist); clone_dashmap_into(&totest, &upslist);
clone_idmap_into(&totest, &idlist); clone_idmap_into(&totest, &idlist);
} }
} }
} }
} }
} }
#[allow(dead_code)] /*
async fn http_request(url: &str, method: &str, payload: &str) -> (bool, bool) { pub async fn populate_upstreams(upslist: &Arc<UpstreamsDashMap>, fullist: &Arc<UpstreamsDashMap>, idlist: &Arc<UpstreamsIdMap>, params: (&str, u64), client: &Client) {
let client = Client::builder().danger_accept_invalid_certs(true).build().unwrap(); let totest = build_upstreams(fullist, params.0, client).await;
let timeout = Duration::from_secs(1); if !compare_dashmaps(&totest, upslist) {
clone_dashmap_into(&totest, upslist);
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();
build_upstreams(&fullist, "HEAD", &client).await
}
async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Client) -> UpstreamsDashMap {
let totest: UpstreamsDashMap = DashMap::new();
let fclone = clone_dashmap(fullist);
for val in fclone.iter() {
let host = val.key();
let inner = DashMap::new();
for path_entry in val.value().iter() {
let path = path_entry.key();
let mut innervec = Vec::new();
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 {
(false, None)
};
let is_h2 = matches!(tls.1, Some(Version::HTTP_2));
let mut scheme = InnerMap {
address: upstream.address.clone(),
port: upstream.port,
is_ssl: tls.0,
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 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(Arc::from(scheme));
} else {
warn!("Dead Upstream : {}", link);
}
} else {
innervec.push(Arc::from(scheme));
}
}
inner.insert(path.clone(), (innervec, AtomicUsize::new(0)));
}
totest.insert(host.clone(), inner);
}
totest
}
async fn http_request(url: &str, method: &str, payload: &str, client: &Client) -> (bool, bool) {
if !["POST", "GET", "HEAD"].contains(&method) { if !["POST", "GET", "HEAD"].contains(&method) {
error!("Method {} not supported. Only GET|POST|HEAD are supported ", method); error!("Method {} not supported. Only GET|POST|HEAD are supported ", method);
return (false, false); return (false, false);
} }
async fn send_request(client: &Client, method: &str, url: &str, payload: &str, timeout: Duration) -> Option<reqwest::Response> { async fn send_request(client: &Client, method: &str, url: &str, payload: &str) -> Option<reqwest::Response> {
match method { match method {
"POST" => client.post(url).body(payload.to_owned()).timeout(timeout).send().await.ok(), "POST" => client.post(url).body(payload.to_owned()).send().await.ok(),
"GET" => client.get(url).timeout(timeout).send().await.ok(), "GET" => client.get(url).send().await.ok(),
"HEAD" => client.head(url).timeout(timeout).send().await.ok(), "HEAD" => client.head(url).send().await.ok(),
_ => None, _ => None,
} }
} }
match send_request(&client, method, url, payload, timeout).await { match send_request(client, method, url, payload).await {
Some(response) => { Some(response) => {
let status = response.status().as_u16(); let status = response.status().as_u16();
((99..499).contains(&status), false) ((99..499).contains(&status), false)
} }
None => { None => (ping_grpc(url).await, true),
// let fallback_url = url.replace("https", "http");
// ping_grpc(&fallback_url).await
(ping_grpc(&url).await, true)
}
} }
} }
pub async fn ping_grpc(addr: &str) -> bool { pub async fn ping_grpc(addr: &str) -> bool {
let endpoint_result = Endpoint::from_shared(addr.to_owned()); let endpoint = match Endpoint::from_shared(addr.to_owned()) {
Ok(e) => e.timeout(Duration::from_secs(2)),
if let Ok(endpoint) = endpoint_result { Err(_) => return false,
let endpoint = endpoint.timeout(Duration::from_secs(2)); };
tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await.ok().and_then(Result::ok).is_some()
match tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await {
Ok(Ok(_channel)) => {
// println!("{:?} ==> {:?} ==> {}", endpoint, _channel, addr);
true
}
_ => false,
}
} else {
false
}
} }
async fn detect_tls(ip: &str, port: &u16) -> (bool, Option<Version>) { async fn detect_tls(ip: &str, port: &u16, client: &Client) -> (bool, Option<Version>) {
let url = format!("https://{}:{}", ip, port); let https_url = format!("https://{}:{}", ip, port);
// let url = format!("{}:{}", ip, port); if let Ok(response) = client.get(&https_url).send().await {
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().unwrap(); return (true, Some(response.version()));
match client.get(&url).send().await { }
Ok(response) => (true, Some(response.version())), let http_url = format!("http://{}:{}", ip, port);
Err(e) => { match client.get(&http_url).send().await {
if e.is_builder() || e.is_connect() || e.to_string().contains("tls") { Ok(response) => {
(false, None) // println!("{} => {:?} (HTTP)", http_url, response.version());
(false, Some(response.version()))
}
Err(_) => {
if ping_grpc(&http_url).await {
(false, Some(Version::HTTP_2))
} else { } else {
(false, None) (false, None)
} }

85
src/utils/httpclient.rs Normal file
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)
}

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 jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use moka::sync::Cache;
use moka::Expiry;
use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Claims { pub struct Claims {
pub(crate) user: String, pub master_key: String,
pub(crate) exp: u64, pub owner: String,
pub exp: u64,
pub random: Option<String>,
} }
pub fn check_jwt(input: &str, secret: &str) -> bool {
let validation = Validation::new(Algorithm::HS256); #[derive(Debug, Deserialize)]
let token_data = decode::<Claims>(&input, &DecodingKey::from_secret(secret.as_ref()), &validation); struct Expired {
match token_data { exp: Option<u64>,
Ok(_) => true, }
static JWT_VALIDATION: LazyLock<Validation> = LazyLock::new(|| Validation::new(Algorithm::HS256));
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, 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()
}

229
src/utils/kuberconsul.rs Normal file
View File

@@ -0,0 +1,229 @@
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 futures::channel::mpsc::Sender;
use futures::SinkExt;
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;
#[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()) {
Some(upstr) => {
for (k, v) in list {
upstr.value().insert(k.to_owned(), v);
}
}
None => {
upstreams.insert(Arc::from(i.hostname.clone()), list);
}
};
}
}
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(Arc::from(p.clone()), (values, AtomicUsize::new(0)));
}
None => {
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>, mut 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>, mut 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
}

View File

@@ -1,58 +1,76 @@
use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, Histogram, IntCounter, IntCounterVec}; use pingora_http::Method;
use pingora_http::StatusCode;
use pingora_http::Version;
use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, register_int_gauge, Histogram, IntCounter, IntCounterVec, IntGauge};
use std::sync::Arc;
use std::sync::LazyLock;
use std::time::Duration; use std::time::Duration;
use tikv_jemalloc_ctl::{epoch, stats};
lazy_static::lazy_static! { pub struct MetricTypes {
pub static ref REQUEST_COUNT: IntCounter = register_int_counter!( pub method: Method,
"gazan_requests_total", pub upstream: Arc<str>,
"Total number of requests handled by Gazan" pub code: Option<StatusCode>,
).unwrap(); pub latency: Duration,
pub static ref RESPONSE_CODES: IntCounterVec = register_int_counter_vec!( pub version: Version,
"gazan_responses_total", }
"Responses grouped by status code",
&["status"] pub static OPEN_FILES: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_open_files", "Number of open file descriptors").unwrap());
).unwrap(); pub static MEMORY_USAGE: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_memory_bytes", "Total memory allocated in bytes").unwrap());
pub static ref REQUEST_LATENCY: Histogram = register_histogram!( pub static ACTIVE_SESSIONS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_active_sessions", "Current number of active sessions").unwrap());
"gazan_request_latency_seconds", pub static REQUEST_COUNT: LazyLock<IntCounter> = LazyLock::new(|| register_int_counter!("aralez_requests_total", "Total number of requests handled by Aralez").unwrap());
"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] pub static RESPONSE_CODES: LazyLock<IntCounterVec> =
).unwrap(); LazyLock::new(|| register_int_counter_vec!("aralez_responses_total", "Responses grouped by status code", &["status"]).unwrap());
pub static ref RESPONSE_LATENCY: Histogram = register_histogram!(
"gazan_response_latency_seconds", // 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 RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
register_histogram!(
"aralez_response_latency_seconds",
"Response latency in seconds", "Response latency in seconds",
vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0] vec![0.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 ref REQUESTS_BY_METHOD: IntCounterVec = register_int_counter_vec!( .unwrap()
"gazan_requests_by_method_total",
"Number of requests by HTTP method",
&["method"]
).unwrap();
pub static ref ERROR_COUNT: IntCounter = register_int_counter!(
"gazan_errors_total",
"Total number of errors"
).unwrap();
}
pub fn calc_metrics(method: String, code: u16, latency: Duration) {
REQUEST_COUNT.inc();
let timer = REQUEST_LATENCY.start_timer();
timer.observe_duration();
RESPONSE_CODES.with_label_values(&[&code.to_string()]).inc();
REQUESTS_BY_METHOD.with_label_values(&[&method]).inc();
RESPONSE_LATENCY.observe(latency.as_secs_f64());
}
/*
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
loop {
interval.tick().await;
// read Pingora stats
let stats = pingora.get_stats();
// update Prometheus metrics accordingly
REQUEST_COUNT.set(stats.requests_total);
// ... etc
}
}); });
*/
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 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.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());
}
pub fn get_memory_usage() -> usize {
epoch::mib().unwrap().advance().unwrap(); // refresh stats
stats::allocated::mib().unwrap().read().unwrap() // bytes allocated
}
pub fn get_open_files() -> usize {
std::fs::read_dir("/proc/self/fd").map(|dir| dir.count()).unwrap_or(0)
}

View File

@@ -1,139 +1,277 @@
use crate::utils::healthcheck;
use crate::utils::state::{is_first_run, mark_not_first_run};
use crate::utils::structs::*; use crate::utils::structs::*;
use crate::utils::tools::{clone_dashmap, clone_dashmap_into, print_upstreams};
use dashmap::DashMap; use dashmap::DashMap;
use log::LevelFilter;
use log::{error, info, warn}; use log::{error, info, warn};
use serde_yaml::Error; use log4rs::{
append::{console::ConsoleAppender, file::FileAppender},
config::{Appender, Config as Log4rsConfig, Root},
encode::pattern::PatternEncoder,
};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::path::Path;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, LazyLock};
use std::{env, fs};
pub fn load_configuration(d: &str, kind: &str) -> Option<Configuration> { pub static DOMAINS: LazyLock<DashMap<String, bool>> = LazyLock::new(DashMap::new);
let mut toreturn: Configuration = Configuration {
upstreams: Default::default(),
headers: Default::default(),
consul: None,
typecfg: "".to_string(),
extraparams: Extraparams {
sticky_sessions: false,
to_https: None,
authentication: DashMap::new(),
},
};
toreturn.upstreams = UpstreamsDashMap::new();
toreturn.headers = Headers::new();
let mut yaml_data = d.to_string(); pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>, String) {
match kind { let mut conf_files = Vec::new();
let yaml_data = match kind {
"filepath" => { "filepath" => {
let _ = match fs::read_to_string(d) { let mut data = String::new();
Ok(data) => { let mut last_error = None;
info!("Reading upstreams from {}", d); for _ in 0..5 {
yaml_data = data match fs::read_to_string(d) {
Ok(content) => {
if !content.trim().is_empty() {
data = content;
break;
}
} }
Err(e) => { Err(e) => {
error!("Reading: {}: {:?}", d, e.to_string()); error!("Config read failed, retrying...");
warn!("Running with empty upstreams list, update it via API"); last_error = Some(e);
return None; }
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
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, 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
} }
"content" => { "content" => {
info!("Reading upstreams from API post body"); info!("Reading upstreams from API post body");
d.to_string()
}
_ => {
error!("Mismatched parameter, only filepath|content is allowed");
return (None, "Mismatched parameter, only filepath|content is allowed".to_string());
}
};
let mut parsed: Config = match serde_yml::from_str(&yaml_data) {
Ok(cfg) => cfg,
Err(e) => {
println!("================================================");
error!("Failed to parse upstreams file: {}", e);
return (None, e.to_string());
}
};
if let Some(ref mut upstreams) = parsed.upstreams {
for uconf in conf_files {
let p: HashMap<String, HostConfig> = match serde_yml::from_str(&uconf) {
Ok(ucfg) => ucfg,
Err(e) => {
error!("Failed to parse upstreams file: {}", e);
return (None, e.to_string());
}
};
upstreams.extend(p);
} }
_ => error!("Mismatched parameter, only filepath|content is allowed "),
} }
let p: Result<Config, Error> = serde_yaml::from_str(&yaml_data); let mut toreturn = Configuration::default();
match p { populate_headers_and_auth(&mut toreturn, &parsed).await;
Ok(parsed) => { toreturn.typecfg = parsed.provider.clone();
let global_headers = DashMap::new();
let mut hl = Vec::new();
if let Some(headers) = &parsed.headers {
for header in headers.iter() {
if let Some((key, val)) = header.split_once(':') {
hl.push((key.to_string(), val.to_string()));
}
}
global_headers.insert("/".to_string(), hl);
toreturn.headers.insert("GLOBAL_HEADERS".to_string(), global_headers);
toreturn.extraparams.sticky_sessions = parsed.sticky_sessions;
toreturn.extraparams.to_https = parsed.to_https;
}
if let Some(auth) = &parsed.authorization {
let name = auth.get("type").unwrap().to_string();
let creds = auth.get("creds").unwrap().to_string();
let val: Vec<String> = vec![name, creds];
toreturn.extraparams.authentication.insert("authorization".to_string(), val);
} else {
toreturn.extraparams.authentication = DashMap::new();
}
match parsed.provider.as_str() { match parsed.provider.as_str() {
"file" => { "file" => {
toreturn.typecfg = "file".to_string(); populate_file_upstreams(&mut toreturn, &parsed).await;
if let Some(upstream) = parsed.upstreams { (Some(toreturn), "Ok".to_string())
for (hostname, host_config) in upstream {
let path_map = DashMap::new();
let header_list = DashMap::new();
for (path, path_config) in host_config.paths {
let mut server_list = Vec::new();
let mut hl = Vec::new();
if let Some(headers) = &path_config.headers {
for header in headers.iter().by_ref() {
if let Some((key, val)) = header.split_once(':') {
hl.push((key.to_string(), val.to_string()));
}
}
}
header_list.insert(path.clone(), hl);
for server in path_config.servers {
if let Some((ip, port_str)) = server.split_once(':') {
if let Ok(port) = port_str.parse::<u16>() {
// let to_https = matches!(path_config.to_https, Some(true));
let to_https = path_config.to_https.unwrap_or(false);
server_list.push((ip.to_string(), port, true, false, to_https));
}
}
}
path_map.insert(path, (server_list, AtomicUsize::new(0)));
}
toreturn.headers.insert(hostname.clone(), header_list);
toreturn.upstreams.insert(hostname, path_map);
}
}
Some(toreturn)
} }
"consul" => { "consul" => {
toreturn.typecfg = "consul".to_string(); toreturn.consul = parsed.consul;
let consul = parsed.consul; (toreturn.consul.is_some().then_some(toreturn), "Ok".to_string())
match consul {
Some(consul) => {
toreturn.consul = Some(consul);
Some(toreturn)
} }
None => None, "kubernetes" => {
toreturn.kubernetes = parsed.kubernetes;
(toreturn.kubernetes.is_some().then_some(toreturn), "Ok".to_string())
} }
}
"kubernetes" => None,
_ => { _ => {
warn!("Unknown provider {}", parsed.provider); warn!("Unknown provider {}", parsed.provider);
None (None, "Unknown provider".to_string())
}
}
}
Err(e) => {
error!("Failed to parse upstreams file: {}", e);
None
} }
} }
} }
async fn populate_headers_and_auth(config: &mut Configuration, parsed: &Config) {
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(':') {
ch.push((key.to_string(), Arc::from(val)));
}
}
}
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(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));
}
}
async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
let imtdashmap = UpstreamsDashMap::new();
if let Some(upstreams) = &parsed.upstreams {
for (hostname, host_config) in upstreams {
let path_map = 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, 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 {
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.split_once(':') {
if let Ok(port) = port_str.parse::<u16>() {
server_list.push(Arc::from(InnerMap {
address: Arc::from(ip),
port,
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(Arc::from(path.clone()), (server_list, AtomicUsize::new(0)));
}
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() {
clone_dashmap_into(&imtdashmap, &config.upstreams);
mark_not_first_run();
} else {
let y = clone_dashmap(&imtdashmap);
let r = healthcheck::initiate_upstreams(y).await;
clone_dashmap_into(&r, &config.upstreams);
}
info!("Upstream Config:");
print_upstreams(&config.upstreams, &config.extraparams);
}
}
pub fn parce_main_config(path: &str) -> AppConfig { pub fn parce_main_config(path: &str) -> AppConfig {
info!("Parsing configuration");
let data = fs::read_to_string(path).unwrap(); let data = fs::read_to_string(path).unwrap();
let reply = DashMap::new(); let reply = DashMap::new();
let cfg: HashMap<String, String> = serde_yaml::from_str(&*data).expect("Failed to parse main config file"); let cfg: HashMap<String, String> = serde_yml::from_str(&data).expect("Failed to parse main config file");
let mut cfo: AppConfig = serde_yaml::from_str(&*data).expect("Failed to parse main config file"); let mut cfo: AppConfig = serde_yml::from_str(&data).expect("Failed to parse main config file");
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(); cfo.hc_method = cfo.hc_method.to_uppercase();
for (k, v) in cfg { for (k, v) in cfg {
reply.insert(k.to_string(), v.to_string()); reply.insert(k.to_string(), v.to_string());
@@ -145,10 +283,83 @@ pub fn parce_main_config(path: &str) -> AppConfig {
} }
if let Some(tlsport_cfg) = cfo.proxy_address_tls.clone() { if let Some(tlsport_cfg) = cfo.proxy_address_tls.clone() {
if let Some((_, port_str)) = tlsport_cfg.split_once(':') { if let Some((_, port_str)) = tlsport_cfg.split_once(':') {
if let Ok(port) = port_str.parse::<u16>() { cfo.proxy_port_tls = Some(port_str.to_string());
cfo.proxy_port_tls = Some(port);
}
} }
}; };
if let Some((_, port_str)) = cfo.proxy_address_http.split_once(':') {
cfo.proxy_port = Some(port_str.to_string());
}
cfo.proxy_tls_grade = parce_tls_grades(cfo.proxy_tls_grade.clone());
cfo cfo
} }
fn parce_tls_grades(what: Option<String>) -> Option<String> {
match what {
Some(g) => match g.to_ascii_lowercase().as_str() {
"high" => {
// info!("TLS grade set to: [ HIGH ]");
Some("high".to_string())
}
"medium" => {
// info!("TLS grade set to: [ MEDIUM ]");
Some("medium".to_string())
}
"unsafe" => {
// info!("TLS grade set to: [ UNSAFE ]");
Some("unsafe".to_string())
}
_ => {
warn!("Error parsing TLS grade, defaulting to: `medium`");
Some("medium".to_string())
}
},
None => {
warn!("TLS grade not set, defaulting to: medium");
Some("medium".to_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(), Arc::from(val.trim())));
}
}
}
}
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}";
let pattern = "{d(%Y-%m-%d %H:%M:%S)} {l} {t} - {m}\n";
if let Some(location) = location {
let file = FileAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build(location).unwrap();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("file", Box::new(file)))
.build(Root::builder().appender("file").build(log_level))
.unwrap();
log4rs::init_config(config).unwrap();
} else {
let stdout = ConsoleAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.build(Root::builder().appender("stdout").build(log_level))
.unwrap();
log4rs::init_config(config).unwrap();
}
}

29
src/utils/state.rs Normal file
View File

@@ -0,0 +1,29 @@
use std::sync::{LazyLock, RwLock};
#[derive(Debug)]
pub struct SharedState {
pub first_run: bool,
}
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();
state.first_run = false;
}
pub fn is_first_run() -> bool {
let state = GLOBAL_STATE.read().unwrap();
state.first_run
}
/*
impl SharedState {
pub fn mark_first_run(&mut self) {
self.first_run = false;
}
pub fn is_first_run(&self) -> bool {
self.first_run
}
}
*/

View File

@@ -2,75 +2,187 @@ use dashmap::DashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
pub type InnerMap = (String, u16, bool, bool, bool); pub type UpstreamsDashMap = DashMap<Arc<str>, DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>;
pub type UpstreamsDashMap = DashMap<String, DashMap<String, (Vec<InnerMap>, AtomicUsize)>>;
pub type UpstreamsIdMap = DashMap<String, InnerMap>;
pub type Headers = DashMap<String, DashMap<String, Vec<(String, String)>>>;
#[derive(Debug, Clone, Serialize, Deserialize)] pub type UpstreamsIdMap = DashMap<String, Arc<InnerMap>>;
pub struct ServiceMapping { pub type Headers = DashMap<Arc<str>, DashMap<Arc<str>, Vec<(String, Arc<str>)>>>;
pub proxy: String, // pub type UpstreamsSerDde = Option<HashMap<String, HostConfig>>;
pub real: String, // pub type UpstreamsSerDe = HashMap<String, HostConfig>;
}
#[derive(Clone, Debug)] #[derive(Clone, Debug, Default)]
pub struct Extraparams { pub struct Extraparams {
pub sticky_sessions: bool,
pub to_https: Option<bool>, pub to_https: Option<bool>,
pub authentication: DashMap<String, Vec<String>>, pub sticky_sessions: Option<u64>,
pub authentication: Option<Arc<InnerAuth>>,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
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 x4xx_limit: Option<u32>,
pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Kubernetes {
pub servers: Option<Vec<String>>,
pub services: Option<Vec<GlobalServiceMapping>>,
pub tokenpath: Option<String>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Consul { pub struct Consul {
pub servers: Option<Vec<String>>, pub servers: Option<Vec<String>>,
pub services: Option<Vec<ServiceMapping>>, pub services: Option<Vec<GlobalServiceMapping>>,
pub token: Option<String>, pub token: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub provider: String, pub provider: String,
pub sticky_sessions: bool,
pub to_https: Option<bool>, pub to_https: Option<bool>,
pub sticky_sessions: Option<u64>,
#[serde(default)]
pub upstreams: Option<HashMap<String, HostConfig>>, pub upstreams: Option<HashMap<String, HostConfig>>,
#[serde(default)]
pub globals: Option<HashMap<String, Vec<String>>>, pub globals: Option<HashMap<String, Vec<String>>>,
pub headers: Option<Vec<String>>, #[serde(default)]
pub authorization: Option<HashMap<String, String>>, pub client_headers: Option<Vec<String>>,
#[serde(default)]
pub server_headers: Option<Vec<String>>,
#[serde(default)]
pub authorization: Option<Auth>,
#[serde(default)]
pub consul: Option<Consul>, pub consul: Option<Consul>,
#[serde(default)]
pub kubernetes: Option<Kubernetes>,
#[serde(default)]
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct HostConfig { pub struct HostConfig {
pub paths: HashMap<String, PathConfig>, pub paths: HashMap<String, PathConfig>,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
} }
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize)] 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 struct PathConfig {
pub servers: Vec<String>, pub servers: Vec<String>,
pub to_https: Option<bool>, 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)] #[derive(Debug, Default)]
pub struct Configuration { pub struct Configuration {
pub upstreams: UpstreamsDashMap, pub upstreams: UpstreamsDashMap,
pub headers: Headers, pub client_headers: Headers,
pub server_headers: Headers,
pub consul: Option<Consul>, pub consul: Option<Consul>,
pub kubernetes: Option<Kubernetes>,
pub typecfg: String, pub typecfg: String,
pub extraparams: Extraparams, pub extraparams: Extraparams,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct AppConfig { pub struct AppConfig {
pub hc_interval: u16, pub hc_interval: u16,
pub hc_method: String, pub hc_method: String,
pub upstreams_conf: String, pub upstreams_conf: String,
pub log_level: String, pub log_level: String,
pub master_key: Option<String>,
pub config_address: String, pub config_address: String,
pub proxy_address_http: String, pub proxy_address_http: String,
pub master_key: String, pub config_api_enabled: bool,
pub config_tls_address: Option<String>,
pub config_tls_certificate: Option<String>,
pub config_tls_key_file: Option<String>,
pub proxy_address_tls: Option<String>, pub proxy_address_tls: Option<String>,
pub proxy_port_tls: Option<u16>, pub proxy_port_tls: Option<String>,
pub tls_certificate: Option<String>, pub proxy_port: Option<String>,
pub tls_key_file: Option<String>,
pub local_server: Option<(String, u16)>, pub local_server: Option<(String, u16)>,
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>,
}
#[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: 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: 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,
} }

View File

@@ -1,28 +1,49 @@
use crate::utils::structs::{UpstreamsDashMap, UpstreamsIdMap}; use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::structs::{InnerMap, InnerMapForJson, Extraparams, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap};
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info};
use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use privdrop::PrivDrop;
use serde_json::{json, Value};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::any::type_name; use std::any::type_name;
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
use std::fmt::Write; use std::fmt::Write;
use std::sync::atomic::AtomicUsize; use std::net::SocketAddr;
use std::net::TcpListener;
use std::os::unix::fs::MetadataExt;
use std::str::FromStr;
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, extraparams: &Extraparams) {
pub fn print_upstreams(upstreams: &UpstreamsDashMap) { let mut out = String::new();
for host_entry in upstreams.iter() { for host_entry in upstreams.iter() {
let hostname = host_entry.key(); writeln!(out, "Hostname: {}", host_entry.key()).unwrap();
println!("Hostname: {}", hostname);
for path_entry in host_entry.value().iter() { for path_entry in host_entry.value().iter() {
let path = path_entry.key(); writeln!(out, " Path: {}", path_entry.key()).unwrap();
println!(" Path: {}", path); for f in path_entry.value().0.clone() {
writeln!(
for (ip, port, ssl, vers, to_https) in path_entry.value().0.clone() { out,
println!(" ===> IP: {}, Port: {}, SSL: {}, H2: {}, To HTTPS: {}", ip, port, ssl, vers, to_https); " 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(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)] #[allow(dead_code)]
pub fn typeoff<T>(_: T) { pub fn typeoff<T>(_: T) {
let to = type_name::<T>(); let to = type_name::<T>();
@@ -80,29 +101,26 @@ pub fn clone_dashmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsDashMap
} }
pub fn compare_dashmaps(map1: &UpstreamsDashMap, map2: &UpstreamsDashMap) -> bool { pub fn compare_dashmaps(map1: &UpstreamsDashMap, map2: &UpstreamsDashMap) -> bool {
let keys1: HashSet<_> = map1.iter().map(|entry| entry.key().clone()).collect(); if map1.len() != map2.len() {
let keys2: HashSet<_> = map2.iter().map(|entry| entry.key().clone()).collect();
if keys1 != keys2 {
return false; return false;
} }
for entry1 in map1.iter() { for entry1 in map1.iter() {
let hostname = entry1.key(); let Some(inner_map2) = map2.get(entry1.key()) else {
let inner_map1 = entry1.value();
let Some(inner_map2) = map2.get(hostname) else {
return false; return false;
}; };
let inner_keys1: HashSet<_> = inner_map1.iter().map(|e| e.key().clone()).collect(); let inner_map1 = entry1.value();
let inner_keys2: HashSet<_> = inner_map2.iter().map(|e| e.key().clone()).collect(); if inner_map1.len() != inner_map2.len() {
if inner_keys1 != inner_keys2 {
return false; return false;
} }
for path_entry in inner_map1.iter() { for path_entry in inner_map1.iter() {
let path = path_entry.key(); let Some(entry2) = inner_map2.get(path_entry.key()) else {
let (vec1, _counter1) = path_entry.value(); return false;
let Some(entry2) = inner_map2.get(path) else {
return false; // Path exists in map1 but not in map2
}; };
let (vec2, _counter2) = entry2.value(); let (vec1, _) = path_entry.value();
let (vec2, _) = entry2.value();
if vec1.len() != vec2.len() {
return false;
}
let set1: HashSet<_> = vec1.iter().collect(); let set1: HashSet<_> = vec1.iter().collect();
let set2: HashSet<_> = vec2.iter().collect(); let set2: HashSet<_> = vec2.iter().collect();
if set1 != set2 { if set1 != set2 {
@@ -113,11 +131,11 @@ pub fn compare_dashmaps(map1: &UpstreamsDashMap, map2: &UpstreamsDashMap) -> boo
true 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() { for entry in source.iter() {
let global_key = entry.key().clone(); let global_key = entry.key().clone();
let global_values = entry.value().clone(); let global_values = entry.value().clone();
let mut target_entry = target.entry(global_key).or_insert_with(Vec::new); let mut target_entry = target.entry(global_key).or_default();
target_entry.extend(global_values); target_entry.extend(global_values);
} }
} }
@@ -133,16 +151,232 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) {
let new_vec = vec.clone(); let new_vec = vec.clone();
for x in vec.iter() { for x in vec.iter() {
let mut id = String::new(); let mut id = String::new();
write!(&mut id, "{}:{}:{}", x.0, x.1, x.2).unwrap(); write!(
&mut id,
"{}:{}:{}:{}:{}:{}:{}:{}:{:?}",
outer_entry.key(),
x.address,
x.port,
x.is_http2,
x.to_https,
x.rate_limit.unwrap_or_default(),
x.x4xx_limit.unwrap_or_default(),
x.healthcheck.unwrap_or_default(),
x.authorization
)
.unwrap_or(());
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
// address: "127.0.0.3", port: 8000, is_ssl: false, is_http2: false, to_https: false, rate_limit: Some(200), healthcheck: None, authorization: None } }
hasher.update(id.clone().into_bytes()); hasher.update(id.clone().into_bytes());
let hash = hasher.finalize(); let hash = hasher.finalize();
let hex_hash = base16ct::lower::encode_string(&hash); let hex_hash = base16ct::lower::encode_string(&hash);
let hh = hex_hash[0..50].to_string(); let hh = hex_hash[0..50].to_string();
cloned.insert(id, (hh.clone(), 0000, false, false, false)); let to_add = InnerMap {
address: Arc::from("127.0.0.1"),
port: 0,
is_ssl: false,
is_http2: false,
to_https: false,
rate_limit: None,
x4xx_limit: None,
healthcheck: None,
redirect_to: None,
authorization: None,
};
cloned.insert(id, Arc::from(to_add));
cloned.insert(hh, x.to_owned()); cloned.insert(hh, x.to_owned());
} }
new_inner_map.insert(path.clone(), new_vec); new_inner_map.insert(path.clone(), new_vec);
} }
} }
info!("Upstreams are fully populated. Ready to server requests");
}
pub fn listdir(dir: String) -> Vec<load::CertificateConfig> {
let mut f = HashMap::new();
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();
if path_str.ends_with(".crt") {
let name = path_str.replace(".crt", "");
let mut inner = vec![];
let domain = name.split("/").collect::<Vec<&str>>();
inner.push(name.clone() + ".crt");
inner.push(name.clone() + ".key");
f.insert(domain[domain.len() - 1].to_owned(), inner);
let y = CertificateConfig {
cert_path: name.clone() + ".crt",
key_path: name.clone() + ".key",
};
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
}
pub fn watch_folder(path: String, sender: Sender<Vec<CertificateConfig>>) -> notify::Result<()> {
let (tx, rx) = channel();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
info!("Watching for certificates in : {}", path);
let certificate_configs = listdir(path.clone());
sender.send(certificate_configs)?;
let mut start = Instant::now();
loop {
match rx.recv_timeout(Duration::from_secs(1)) {
Ok(Ok(event)) => match &event.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(_) | EventKind::Remove(_) => {
if start.elapsed() > Duration::from_secs(1) {
start = Instant::now();
let certificate_configs = listdir(path.clone());
sender.send(certificate_configs)?;
info!("Certificate changed: {:?}, {:?}", event.kind, event.paths);
}
}
_ => {}
},
Ok(Err(e)) => error!("Watch error: {:?}", e),
Err(_) => {}
}
}
}
pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Option<String>) {
thread::sleep(time::Duration::from_millis(10));
loop {
thread::sleep(time::Duration::from_millis(10));
if TcpListener::bind(&http_addr).is_err() {
break;
}
}
if let Some(tls_addr) = tls_addr {
loop {
thread::sleep(time::Duration::from_millis(10));
if TcpListener::bind(&tls_addr).is_err() {
break;
}
}
}
info!("Dropping ROOT privileges to: {}:{}", user, group);
if let Err(e) = PrivDrop::default().user(user).group(group).apply() {
error!("Failed to drop privileges: {}", e);
process::exit(1)
}
}
pub fn check_priv(addr: &str) {
let port = SocketAddr::from_str(addr).map(|sa| sa.port()).unwrap();
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)
}
}
}
#[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
})
} }

View File

@@ -1,4 +1,6 @@
use crate::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider}; 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::structs::Configuration;
use crate::utils::tools::*; use crate::utils::tools::*;
use crate::utils::*; use crate::utils::*;
@@ -6,8 +8,8 @@ use crate::web::proxyhttp::LB;
use async_trait::async_trait; use async_trait::async_trait;
use dashmap::DashMap; use dashmap::DashMap;
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::StreamExt; use futures::{SinkExt, StreamExt};
use log::info; use log::{error, info};
use pingora_core::server::ShutdownWatch; use pingora_core::server::ShutdownWatch;
use pingora_core::services::background::BackgroundService; use pingora_core::services::background::BackgroundService;
use std::sync::Arc; use std::sync::Arc;
@@ -15,34 +17,72 @@ use std::sync::Arc;
#[async_trait] #[async_trait]
impl BackgroundService for LB { impl BackgroundService for LB {
async fn start(&self, mut shutdown: ShutdownWatch) { async fn start(&self, mut shutdown: ShutdownWatch) {
info!("Starting background service"); info!("Starting background service"); // tx: Sender<Configuration>
let (tx, mut rx) = mpsc::channel::<Configuration>(0); let (mut tx, mut rx) = mpsc::channel::<Configuration>(1);
let tx_api = tx.clone();
let tx_file = tx.clone(); let config = load_configuration(self.config.upstreams_conf.clone().as_str(), "filepath")
let tx_consul = tx.clone(); .await
.0
.expect("Failed to load configuration");
match config.typecfg.as_str() {
"file" => {
info!("Running File discovery, requested type is: {}", config.typecfg);
tx.send(config).await.unwrap();
let file_load = FromFileProvider { let file_load = FromFileProvider {
path: self.config.upstreams_conf.clone(), path: self.config.upstreams_conf.clone(),
}; };
let consul_load = ConsulProvider { // let _ = tokio::spawn(async move { file_load.start(tx).await });
path: self.config.upstreams_conf.clone(), 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() };
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() };
drop(tokio::spawn(async move { consul_load.start(tx).await }));
}
_ => {
error!("Unknown discovery type: {}", config.typecfg);
}
}
let _ = tokio::spawn(async move { file_load.start(tx_file).await }); let confdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/autoconfigs";
let _ = tokio::spawn(async move { consul_load.start(tx_consul).await }); let certdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/certificates";
let api_load = APIUpstreamProvider { let api_load = APIUpstreamProvider {
address: self.config.config_address.clone(), address: self.config.config_address.clone(),
masterkey: self.config.master_key.clone(), masterkey: self.config.master_key.clone(),
config_api_enabled: self.config.config_api_enabled,
upstreams_file: self.config.upstreams_conf.clone(),
// certs_dir: self.config.proxy_certificates.clone().unwrap_or_else(|| "/tmp".to_string()),
config_dir: confdir.clone(),
certs_dir: certdir.clone(),
// tls_address: self.config.config_tls_address.clone(),
// tls_certificate: self.config.config_tls_certificate.clone(),
// tls_key_file: self.config.config_tls_key_file.clone(),
file_server_address: self.config.file_server_address.clone(),
file_server_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 crtdir = api_load.certs_dir.clone();
let _ = tokio::spawn(async move { api_load.start(tx_api).await }); // let tx_api = tx.clone();
drop(tokio::spawn(async move { api_load.start(tx_api).await }));
let uu = self.ump_upst.clone(); let uu = self.ump_upst.clone();
let ff = self.ump_full.clone(); let ff = self.ump_full.clone();
let im = self.ump_byid.clone(); let im = self.ump_byid.clone();
let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval); let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval);
let _ = tokio::spawn(async move { healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await }); drop(tokio::spawn(async move {
healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await
}));
drop(tokio::spawn(async move { refresh_order(certdir, confdir).await }));
loop { loop {
tokio::select! { tokio::select! {
@@ -50,40 +90,50 @@ impl BackgroundService for LB {
break; break;
} }
val = rx.next() => { val = rx.next() => {
match val { if let Some(ss) = val {
Some(ss) => {
clone_dashmap_into(&ss.upstreams, &self.ump_full); clone_dashmap_into(&ss.upstreams, &self.ump_full);
clone_dashmap_into(&ss.upstreams, &self.ump_upst); clone_dashmap_into(&ss.upstreams, &self.ump_upst);
let current = self.extraparams.load_full(); let current = self.extraparams.load_full();
let mut new = (*current).clone(); let mut new = (*current).clone();
new.sticky_sessions = ss.extraparams.sticky_sessions;
new.to_https = ss.extraparams.to_https; new.to_https = ss.extraparams.to_https;
new.sticky_sessions = ss.extraparams.sticky_sessions;
new.authentication = ss.extraparams.authentication.clone(); new.authentication = ss.extraparams.authentication.clone();
new.rate_limit = ss.extraparams.rate_limit;
new.x4xx_limit = ss.extraparams.x4xx_limit;
self.extraparams.store(Arc::new(new)); self.extraparams.store(Arc::new(new));
self.headers.clear(); self.client_headers.clear();
self.server_headers.clear();
for entry in ss.upstreams.iter() { for entry in ss.upstreams.iter() {
let global_key = entry.key().clone(); let global_key = entry.key().clone();
let global_values = DashMap::new(); let client_global_values = DashMap::new();
let mut target_entry = ss.headers.entry(global_key).or_insert_with(DashMap::new); let server_global_values = 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 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_key = path.key().clone();
let path_headers = path.value().clone(); let path_headers = path.value().clone();
self.headers.insert(path_key.clone(), path_headers); self.client_headers.insert(path_key.clone(), path_headers);
if let Some(global_headers) = ss.headers.get("GLOBAL_HEADERS") { if let Some(global_headers) = ss.client_headers.get("GLOBAL_CLIENT_HEADERS") {
if let Some(existing_headers) = self.headers.get_mut(&path_key) { if let Some(existing_headers) = self.client_headers.get_mut(&path_key) {
merge_headers(&existing_headers, &global_headers); merge_headers(&existing_headers, &global_headers);
} }
} }
} }
// info!("Upstreams list is changed, updating to:"); for path in ss.server_headers.iter() {
// print_upstreams(&self.ump_full); 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);
}
}
} }
None => {}
} }
} }
} }

View File

@@ -2,77 +2,117 @@ use crate::utils::structs::InnerMap;
use crate::web::proxyhttp::LB; use crate::web::proxyhttp::LB;
use async_trait::async_trait; use async_trait::async_trait;
use std::sync::atomic::Ordering; use std::sync::atomic::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] #[async_trait]
pub trait GetHost { pub trait GetHost {
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<InnerMap>; fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn get_header(&self, peer: &str, path: &str) -> Option<Vec<(String, String)>>;
fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders>;
// fn get_upstreams(&self) -> Arc<UpstreamsDashMap>;
} }
#[async_trait] #[async_trait]
impl GetHost for LB { impl GetHost for LB {
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<InnerMap> { fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
if let Some(b) = backend_id { if let Some(b) = backend_id {
if let Some(bb) = self.ump_byid.get(b) { if let Some(bb) = self.ump_byid.get(b) {
// println!("BIB :===> {:?}", Some(bb.value()));
return Some(bb.value().clone()); return Some(bb.value().clone());
} }
} }
let host_entry = self.ump_upst.get(peer)?; let host_entry = self.ump_upst.get(peer)?;
let mut current_path = path.to_string(); let mut end = path.len();
let mut best_match: Option<InnerMap> = None;
loop { loop {
if let Some(entry) = host_entry.get(&current_path) { let slice = &path[..end];
if let Some(entry) = host_entry.get(slice) {
let (servers, index) = entry.value(); let (servers, index) = entry.value();
if !servers.is_empty() { if !servers.is_empty() {
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len(); let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len();
best_match = Some(servers[idx].clone()); return Some(servers[idx].clone());
break;
} }
} }
if let Some(pos) = current_path.rfind('/') { if let Some(pos) = slice.rfind('/') {
current_path.truncate(pos); end = pos;
} else { } else {
break; break;
} }
} }
if best_match.is_none() {
if let Some(entry) = host_entry.get("/") { if let Some(entry) = host_entry.get("/") {
let (servers, index) = entry.value(); let (servers, index) = entry.value();
if !servers.is_empty() { if !servers.is_empty() {
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len(); let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len();
best_match = Some(servers[idx].clone()); return Some(servers[idx].clone());
} }
} }
None
} }
// println!("BMT :===> {:?}", best_match);
best_match
}
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 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 { loop {
if let Some(entry) = host_entry.get(&current_path) { if let Some(entry) = client_entry.get(current_path) {
if !entry.value().is_empty() { if !entry.value().is_empty() {
best_match = Some(entry.value().clone()); clnt_match = Some(entry.value().clone());
break; break;
} }
} }
if current_path == "/" {
break;
}
if let Some(pos) = current_path.rfind('/') { if let Some(pos) = current_path.rfind('/') {
current_path.truncate(pos); current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else { } else {
break; break;
} }
} }
if best_match.is_none() { }
if let Some(entry) = host_entry.get("/") { 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() { if !entry.value().is_empty() {
best_match = Some(entry.value().clone()); 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;
} }
} }
} }
best_match 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
}
} }
} }

View File

@@ -1,219 +1,326 @@
use crate::utils::auth::authenticate; use crate::utils::auth::authenticate;
use crate::utils::metrics::*; use crate::utils::metrics::*;
use crate::utils::structs::{AppConfig, Extraparams, Headers, UpstreamsDashMap, UpstreamsIdMap}; use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::web::gethosts::GetHost; use crate::web::gethosts::{GetHost, GetHostsReturHeaders};
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use async_trait::async_trait; use async_trait::async_trait;
use log::{debug, warn}; use axum::body::Bytes;
use dashmap::DashMap;
use log::{debug, error, warn};
use moka::sync::Cache;
use pingora::http::{RequestHeader, ResponseHeader, StatusCode}; use pingora::http::{RequestHeader, ResponseHeader, StatusCode};
use pingora::prelude::*; use pingora::prelude::*;
use pingora::ErrorSource::Upstream;
use pingora_core::listeners::ALPN; use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer; use pingora_core::prelude::HttpPeer;
use pingora_limits::rate::Rate;
use pingora_proxy::{ProxyHttp, Session}; use pingora_proxy::{ProxyHttp, Session};
use std::sync::Arc; use sha2::{Digest, Sha256};
use std::cell::RefCell;
use std::fmt::Write;
use std::net::IpAddr;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use tokio::time::Instant; use tokio::time::Instant;
static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(DashMap::new);
thread_local! {static IP_BUFFER: RefCell<String> = RefCell::new(String::with_capacity(50));}
pub static RATE_LIMITER: LazyLock<Rate> = LazyLock::new(|| Rate::new(Duration::from_secs(1)));
pub static 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"));
#[derive(Clone)]
pub struct LB { pub struct LB {
pub ump_upst: Arc<UpstreamsDashMap>, pub ump_upst: Arc<UpstreamsDashMap>,
pub ump_full: Arc<UpstreamsDashMap>, pub ump_full: Arc<UpstreamsDashMap>,
pub ump_byid: Arc<UpstreamsIdMap>, pub ump_byid: Arc<UpstreamsIdMap>,
pub headers: Arc<Headers>, pub client_headers: Arc<Headers>,
pub server_headers: Arc<Headers>,
pub config: Arc<AppConfig>, pub config: Arc<AppConfig>,
pub extraparams: Arc<ArcSwap<Extraparams>>, pub extraparams: Arc<ArcSwap<Extraparams>>,
} }
pub struct Context { pub struct Context {
backend_id: String, backend_id: Option<String>,
to_https: bool,
redirect_to: String,
start_time: Instant, start_time: Instant,
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] #[async_trait]
impl ProxyHttp for LB { impl ProxyHttp for LB {
// type CTX = ();
// fn new_ctx(&self) -> Self::CTX {}
type CTX = Context; type CTX = Context;
fn new_ctx(&self) -> Self::CTX { fn new_ctx(&self) -> Self::CTX {
Context { Context {
backend_id: String::new(), backend_id: None,
to_https: false,
redirect_to: String::new(),
start_time: Instant::now(), 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> { async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> {
if let Some(auth) = self.extraparams.load().authentication.get("authorization") { ACTIVE_SESSIONS.inc();
let authenticated = authenticate(&auth.value(), &session); let hostname = return_header_host_from_upstream(session, &self.ump_upst);
if !authenticated { _ctx.hostname = hostname;
let mut backend_id = None;
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() {
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, session.req_header().uri.path(), backend_id);
match optioninnermap {
None => return Ok(false),
Some(ref innermap) => {
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; let _ = session.respond_error(401).await;
warn!("Forbidden: {:?}, {}", session.client_addr(), session.req_header().uri.path().to_string()); warn!("Forbidden: {:?}, {}", session.client_addr(), session.req_header().uri.path());
return Ok(true); 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?;
if let (Some(oi), Some(oa)) = (&_ctx.hostname, rate_key) {
warn!("Limit 4XX: {}-rps exceed on {} from {} path {}", rate, oi, oa, session.req_header().uri.path());
}
return Ok(true);
}
}
}
if let Some(rate) = innermap.rate_limit.or(_ctx.extraparams.rate_limit) {
let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip());
let curr_window_requests = RATE_LIMITER.observe(&rate_key, 1);
if curr_window_requests > rate {
let header = ResponseHeader::build(429, None)?;
session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?;
if let (Some(oi), Some(oa)) = (&_ctx.hostname, rate_key) {
warn!("Limit: {}-rps exceed on {} from {}", rate, oi, oa);
}
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;
}
}
Ok(false) Ok(false)
} }
async fn upstream_peer(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<Box<HttpPeer>> { 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() {
match host_name { Some(hostname) => match ctx.upstream_peer.as_ref() {
Some(hostname) => { Some(innermap) => {
// session.req_header_mut().headers.insert("X-Host-Name", host.to_string().parse().unwrap()); let mut peer = Box::new(HttpPeer::new((&*innermap.address, innermap.port), innermap.is_ssl, hostname.to_string()));
let mut backend_id = None;
if self.extraparams.load().sticky_sessions { if innermap.is_http2 {
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;
}
}
}
}
}
let ddr = self.get_host(hostname, hostname, backend_id);
match ddr {
Some((address, port, ssl, is_h2, to_https)) => {
let mut peer = Box::new(HttpPeer::new((address.clone(), port.clone()), ssl, String::new()));
// if session.is_http2() {
if is_h2 {
peer.options.alpn = ALPN::H2; peer.options.alpn = ALPN::H2;
} }
if ssl { if innermap.is_ssl {
peer.sni = hostname.to_string();
peer.options.verify_cert = false; peer.options.verify_cert = false;
peer.options.verify_hostname = false; peer.options.verify_hostname = false;
} }
// println!("{}, {}, alpn {}, h2 {:?}, to_https {}", hostname, address.as_str(), peer.options.alpn, is_h2, _to_https); if let Some(_) = ctx.extraparams.sticky_sessions {
if self.extraparams.load().to_https.unwrap_or(false) || to_https { let mut s = String::with_capacity(64);
if let Some(stream) = session.stream() { write!(
if stream.get_ssl().is_none() { &mut s,
if let Some(addr) = session.server_addr() { "{}:{}:{}:{}:{}:{}:{}:{:?}",
if let Some((host, _)) = addr.to_string().split_once(':') { hostname,
let uri = session.req_header().uri.path_and_query().map_or("/", |pq| pq.as_str()); innermap.address,
let port = self.config.proxy_port_tls.unwrap_or(443); innermap.port,
ctx.to_https = true; innermap.is_http2,
ctx.redirect_to = format!("https://{}:{}{}", host, port, uri); innermap.to_https,
innermap.rate_limit.unwrap_or_default(),
innermap.healthcheck.unwrap_or_default(),
innermap.authorization
)
.unwrap_or(());
ctx.backend_id = Some(s);
} }
}
}
}
}
ctx.backend_id = format!("{}:{}:{}", address.clone(), port.clone(), ssl);
Ok(peer) Ok(peer)
} }
None => { None => {
warn!("Upstream not found. Host: {:?}, Path: {}", hostname, session.req_header().uri); if let Err(e) = session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await {
Ok(return_no_host(&self.config.local_server)) 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 => { None => {
warn!("Upstream not found. Host: {:?}, Path: {}", host_name, session.req_header().uri); if let Err(e) = session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await {
Ok(return_no_host(&self.config.local_server)) error!("Failed to send error response: {:?}", e);
}
Err(Box::new(Error {
etype: HTTPStatus(502),
esource: Upstream,
retry: RetryType::Decided(false),
cause: None,
context: None,
}))
} }
} }
} }
async fn upstream_request_filter(&self, session: &mut Session, _upstream_request: &mut RequestHeader, _ctx: &mut Self::CTX) -> Result<()> { async fn upstream_request_filter(&self, session: &mut Session, upstream_request: &mut RequestHeader, ctx: &mut Self::CTX) -> Result<()> {
match session.client_addr() { if let Some(ip) = session.client_addr().and_then(|a| a.as_inet()).map(|i| i.ip()) {
Some(ip) => { IP_BUFFER.with(|buffer| {
let inet = ip.as_inet(); let mut buf = buffer.borrow_mut();
match inet { buf.clear();
Some(addr) => { write!(buf, "{}", ip).unwrap_or(());
_upstream_request upstream_request.append_header("X-Forwarded-For", buf.as_str()).unwrap_or(false);
.insert_header("X-Forwarded-For", addr.to_string().split(':').collect::<Vec<&str>>()[0]) });
.unwrap();
} }
None => warn!("Malformed Client IP: {:?}", inet),
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())?;
} }
} }
None => { if let Some(ch) = client_headers {
warn!("Cannot detect client IP"); ctx.client_headers = Some(ch);
}
} }
Ok(()) Ok(())
} }
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 _ = _upstream_response.insert_header("set-cookie", format!("backend_id={}; Path=/; Max-Age=600; HttpOnly; SameSite=Lax", tt));
let mut buf = String::with_capacity(80);
buf.push_str("backend_id=");
buf.push_str(&tt);
buf.push_str("; Path=/; Max-Age=");
buf.push_str(&val.to_string());
buf.push_str("; HttpOnly; SameSite=Lax");
// buf.push_str("; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax");
// println!("{}", buf);
let _ = _upstream_response.insert_header("set-cookie", buf.as_str());
}
}
async fn response_filter(&self, session: &mut Session, _upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> { if let Some(client_headers) = &ctx.client_headers {
// _upstream_response.insert_header("X-Proxied-From", "Fooooooooooooooo").unwrap(); for (k, v) in client_headers.iter() {
if self.extraparams.load().sticky_sessions { _upstream_response.append_header(k.clone(), v.as_ref())?;
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.0));
} }
} }
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 return_header_host(&session) {
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();
}
}
}
}
}
None => {}
}
Ok(()) Ok(())
} }
async fn logging(&self, session: &mut Session, _e: Option<&pingora::Error>, ctx: &mut Self::CTX) { 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()); let response_code = session.response_written().map_or(0, |resp| resp.status.as_u16());
debug!("{}, response code: {response_code}", self.request_summary(session, ctx)); debug!("{}, response code: {response_code}", self.request_summary(session, ctx));
let m = &MetricTypes {
let method = session.req_header().method.to_string(); method: session.req_header().method.clone(),
let status = session.response_written().map(|resp| resp.status.as_u16()).unwrap_or(0); code: session.response_written().map(|resp| resp.status),
let latency = ctx.start_time.elapsed(); latency: ctx.start_time.elapsed(),
calc_metrics(method, status, latency); 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 <= response_code && response_code <= 499 {
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);
}
}
}
} }
} }
fn return_header_host(session: &Session) -> Option<&str> { fn return_header_host_from_upstream(session: &Session, ump_upst: &UpstreamsDashMap) -> Option<Arc<str>> {
if session.is_http2() { let host_str = if session.is_http2() {
match session.req_header().uri.host() { session.req_header().uri.host()?
Some(host) => Option::from(host),
None => None,
}
} else { } else {
match session.req_header().headers.get("host") { let h = session.req_header().headers.get("host")?.to_str().ok()?;
Some(host) => { h.split_once(':').map_or(h, |(host, _)| host)
let header_host = host.to_str().unwrap().splitn(2, ':').collect::<Vec<&str>>(); };
Option::from(header_host[0])
}
None => None,
}
}
}
fn return_no_host(inp: &Option<(String, u16)>) -> Box<HttpPeer> { ump_upst.get(host_str).or_else(|| ump_upst.get("DEFAULT")).map(|entry| entry.key().clone())
match inp {
Some(t) => Box::new(HttpPeer::new(t, false, String::new())),
None => Box::new(HttpPeer::new(("0.0.0.0", 0), false, String::new())),
}
} }

View File

@@ -1,17 +1,24 @@
// use rustls::crypto::ring::default_provider; // use rustls::crypto::ring::default_provider;
use crate::tls::grades;
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::structs::Extraparams; use crate::utils::structs::Extraparams;
use crate::utils::tools::*;
use crate::web::proxyhttp::LB; use crate::web::proxyhttp::LB;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use ctrlc;
use dashmap::DashMap; use dashmap::DashMap;
use log::info; use log::info;
use pingora::tls::ssl::{SslAlert, SslRef};
use pingora_core::listeners::tls::TlsSettings;
use pingora_core::prelude::{background_service, Opt}; use pingora_core::prelude::{background_service, Opt};
use pingora_core::server::Server; use pingora_core::server::Server;
use std::env; use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc; use std::sync::Arc;
use std::{fs, thread};
pub fn run() { pub fn run() {
// default_provider().install_default().expect("Failed to install rustls crypto provider"); // default_provider().install_default().expect("Failed to install rustls crypto provider");
let parameters = Some(Opt::parse_args()).unwrap(); let parameters = Opt::parse_args();
let file = parameters.conf.clone().unwrap(); let file = parameters.conf.clone().unwrap();
let maincfg = crate::utils::parceyaml::parce_main_config(file.as_str()); let maincfg = crate::utils::parceyaml::parce_main_config(file.as_str());
@@ -21,76 +28,91 @@ pub fn run() {
let uf_config = Arc::new(DashMap::new()); let uf_config = Arc::new(DashMap::new());
let ff_config = Arc::new(DashMap::new()); let ff_config = Arc::new(DashMap::new());
let im_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 { let ec_config = Arc::new(ArcSwap::from_pointee(Extraparams {
sticky_sessions: false,
to_https: None, to_https: None,
authentication: DashMap::new(), sticky_sessions: None,
authentication: None,
rate_limit: None,
x4xx_limit: None,
})); }));
let cfg = Arc::new(maincfg); let cfg = Arc::new(maincfg);
let lb = LB { let lb = LB {
ump_upst: uf_config.clone(), ump_upst: uf_config,
ump_full: ff_config.clone(), ump_full: ff_config,
ump_byid: im_config.clone(), ump_byid: im_config,
config: cfg.clone(), config: cfg.clone(),
headers: hh_config.clone(), client_headers: ch_config,
extraparams: ec_config.clone(), server_headers: sh_config,
}; extraparams: ec_config,
let bg = LB {
ump_upst: uf_config.clone(),
ump_full: ff_config.clone(),
ump_byid: im_config.clone(),
config: cfg.clone(),
headers: hh_config.clone(),
extraparams: ec_config.clone(),
}; };
// env_logger::Env::new(); let grade = cfg.proxy_tls_grade.clone().unwrap_or("medium".to_string());
// env_logger::init(); info!("TLS grade set to: [ {} ]", grade);
let log_level = cfg.log_level.clone(); let bg_srvc = background_service("bgsrvc", lb.clone());
unsafe { let mut proxy = pingora_proxy::http_proxy_service(&server.configuration, lb.clone());
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()
// .format_timestamp(None)
// .format_module_path(false)
// .format_source_path(false)
// .format_target(false)
.init();
let bg_srvc = background_service("bgsrvc", bg);
let mut proxy = pingora_proxy::http_proxy_service(&server.configuration, lb);
let bind_address_http = cfg.proxy_address_http.clone(); let bind_address_http = cfg.proxy_address_http.clone();
let bind_address_tls = cfg.proxy_address_tls.clone(); let bind_address_tls = cfg.proxy_address_tls.clone();
match bind_address_tls {
Some(bind_address_tls) => { check_priv(bind_address_http.as_str());
info!("Running TLS listener on :{}", bind_address_tls);
let cert_path = cfg.tls_certificate.clone().unwrap(); if let Some(bind_address_tls) = bind_address_tls {
let key_path = cfg.tls_key_file.clone().unwrap(); check_priv(bind_address_tls.as_str());
let mut tls_settings = pingora_core::listeners::tls::TlsSettings::intermediate(&cert_path, &key_path).unwrap(); let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel();
tls_settings.enable_h2(); let certs_path = cfg.proxy_configs.clone().unwrap() + "/certificates";
proxy.add_tls_with_settings(&bind_address_tls, None, tls_settings);
if fs::metadata(certs_path.clone()).is_err() {
fs::create_dir_all(certs_path.clone()).unwrap();
} }
None => {} thread::spawn(move || {
watch_folder(certs_path, tx).unwrap();
});
let certificate_configs = rx.recv().unwrap();
let first_set = load::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info"));
let certificates = Arc::new(ArcSwap::from_pointee(first_set));
let certs_for_callback = certificates.clone();
let certs_for_watcher = certificates.clone();
let new_certs = load::Certificates::new(&certificate_configs, grade.as_str());
certs_for_watcher.store(Arc::new(new_certs.unwrap()));
let mut tls_settings =
TlsSettings::intermediate(&certs_for_callback.load().default_cert_path, &certs_for_callback.load().default_key_path).expect("unable to load or parse cert/key");
grades::set_tsl_grade(&mut tls_settings, grade.as_str());
tls_settings.set_servername_callback(move |ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert| certs_for_callback.load().server_name_callback(ssl_ref, ssl_alert));
tls_settings.set_alpn_select_callback(grades::prefer_h2);
proxy.add_tls_with_settings(&bind_address_tls, None, tls_settings);
let certs_for_watcher = certificates.clone();
thread::spawn(move || {
while let Ok(new_configs) = rx.recv() {
let new_certs = load::Certificates::new(&new_configs, grade.as_str());
if let Some(new_certs) = new_certs {
certs_for_watcher.store(Arc::new(new_certs));
};
}
});
} }
info!("Running HTTP listener on :{}", bind_address_http.as_str()); info!("Running HTTP listener on :{}", bind_address_http.as_str());
proxy.add_tcp(bind_address_http.as_str()); proxy.add_tcp(bind_address_http.as_str());
server.add_service(proxy); server.add_service(proxy);
server.add_service(bg_srvc); server.add_service(bg_srvc);
server.run_forever();
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 (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...");
} }

View File

@@ -1,80 +1,132 @@
use crate::utils::structs::Configuration; use crate::tls::acme::order::CHALLENGES;
use crate::tls::acme::{account, order};
use crate::utils::discovery::APIUpstreamProvider;
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 axum::body::Body; use axum::body::Body;
use axum::extract::State; use axum::extract::{Query, State};
use axum::http::{Response, StatusCode}; use axum::http::{Response, StatusCode};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::routing::{delete, get, head, post, put}; use axum::routing::{any, get, post};
use axum::{Json, Router}; use axum::{Json, Router};
use futures::channel::mpsc::Sender; use futures::channel::mpsc::Sender;
use futures::SinkExt; use futures::SinkExt;
use jsonwebtoken::{encode, EncodingKey, Header}; use jsonwebtoken::{encode, EncodingKey, Header};
use log::{error, info, warn}; use log::{debug, error, info, warn};
use prometheus::{gather, Encoder, TextEncoder}; use prometheus::{gather, Encoder, TextEncoder};
use serde::{Deserialize, Serialize}; use serde::Serialize;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_http::services::ServeDir;
#[derive(Deserialize)]
struct InputKey {
master_key: String,
owner: String,
valid: u64,
}
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
struct OutToken { struct OutToken {
token: String, token: String,
} }
#[allow(unused_mut)] #[derive(Clone)]
pub async fn run_server(bindaddress: String, master_key: String, mut to_return: Sender<Configuration>) { struct AppState {
let mut tr = to_return.clone(); master_key: Option<String>,
let app = Router::new() cert_creds: String,
.route("/{*wildcard}", get(senderror)) certs_dir: String,
.route("/{*wildcard}", post(senderror)) upstreams_file: String,
.route("/{*wildcard}", put(senderror)) config_sender: Sender<Configuration>,
.route("/{*wildcard}", head(senderror)) config_api_enabled: bool,
.route("/{*wildcard}", delete(senderror)) current_upstreams: Arc<UpstreamsDashMap>,
.route("/jwt", post(jwt_gen)) full_upstreams: Arc<UpstreamsDashMap>,
.route("/metrics", get(metrics)) }
.with_state(master_key.clone())
.route(
"/conf",
post(|up: String| async move {
let serverlist = crate::utils::parceyaml::load_configuration(up.as_str(), "content");
match serverlist { #[allow(unused_mut)]
Some(serverlist) => { pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Configuration>, upstreams_curr: Arc<UpstreamsDashMap>, upstreams_full: Arc<UpstreamsDashMap>) {
let _ = tr.send(serverlist).await.unwrap(); let credsfile = config.config_dir.clone() + "/acme_credentials.json";
Response::builder().status(StatusCode::CREATED).body(Body::from("Config, conf file, updated!\n")).unwrap() 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,
current_upstreams: upstreams_curr,
full_upstreams: upstreams_full,
};
let app = Router::new()
// .route("/{*wildcard}", get(senderror))
.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(address), Some(folder)) = (&config.file_server_address, &config.file_server_folder) {
let static_files = ServeDir::new(folder);
let static_serve: Router = Router::new().fallback_service(static_files);
let static_listen = TcpListener::bind(address).await.unwrap();
drop(tokio::spawn(async move { axum::serve(static_listen, static_serve).await.unwrap() }));
} }
None => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR) let listener = TcpListener::bind(config.address.clone()).await.unwrap();
.body(Body::from("Failed to parce config file!\n")) info!("Starting the API server on: {}", config.address);
.unwrap(),
}
})
.with_state("state"),
);
let listener = TcpListener::bind(bindaddress.clone()).await.unwrap();
info!("Starting the API server on: {}", bindaddress);
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
} }
#[allow(dead_code)] async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>, content: String) -> impl IntoResponse {
async fn senderror() -> impl IntoResponse { if !st.config_api_enabled {
Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("No live upstream found!\n")).unwrap() return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config API is disabled !\n")).unwrap();
}
let strcontent = content.as_str();
let parsed = serde_yml::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 {
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()
}
}
} }
async fn jwt_gen(State(master_key): State<String>, Json(payload): Json<InputKey>) -> (StatusCode, Json<OutToken>) { async fn apply_config(content: &str, mut st: AppState, save: bool) {
if payload.master_key == master_key { let sl = crate::utils::parceyaml::load_configuration(content, "content").await;
let now = SystemTime::now() + Duration::from_secs(payload.valid * 60); if let Some(serverlist) = sl.0 {
let a = now.duration_since(UNIX_EPOCH).unwrap().as_secs(); if save {
let claim = crate::utils::jwt::Claims { user: payload.owner, exp: a }; info!("Saving new upstreams to: {}", st.upstreams_file);
if let Err(err) = std::fs::write(&st.upstreams_file, content) {
error!("Error saving to: {} : {}", st.upstreams_file, err);
}
}
let _ = st.config_sender.send(serverlist).await;
}
}
async fn jwt_gen(State(state): State<AppState>, Json(payload): Json<Claims>) -> (StatusCode, Json<OutToken>) {
if 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())) { match encode(&Header::default(), &claim, &EncodingKey::from_secret(payload.master_key.as_ref())) {
Ok(t) => { Ok(t) => {
let tok = OutToken { token: t }; let tok = OutToken { token: t };
info!("Generating token: {:?}", tok); debug!("Generating token: {:?}", tok.token);
(StatusCode::CREATED, Json(tok)) (StatusCode::CREATED, Json(tok))
} }
Err(e) => { Err(e) => {
@@ -90,12 +142,21 @@ async fn jwt_gen(State(master_key): State<String>, Json(payload): Json<InputKey>
warn!("Unauthorised JWT generate request: {:?}", tok); warn!("Unauthorised JWT generate request: {:?}", tok);
(StatusCode::FORBIDDEN, Json(tok)) (StatusCode::FORBIDDEN, Json(tok))
} }
} else {
let tok = OutToken {
token: "ERROR Getting JWT_KEY environment variable".to_string(),
};
error!("ERROR Getting JWT_KEY environment variable");
(StatusCode::INTERNAL_SERVER_ERROR, Json(tok))
}
} }
async fn metrics() -> impl IntoResponse { 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 metric_families = gather();
let encoder = TextEncoder::new(); let encoder = TextEncoder::new();
let mut buffer = Vec::new(); let mut buffer = Vec::new();
if let Err(e) = encoder.encode(&metric_families, &mut buffer) { if let Err(e) = encoder.encode(&metric_families, &mut buffer) {
// encoding error fallback // encoding error fallback
@@ -104,10 +165,104 @@ async fn metrics() -> impl IntoResponse {
.body(Body::from(format!("Failed to encode metrics: {}", e))) .body(Body::from(format!("Failed to encode metrics: {}", e)))
.unwrap(); .unwrap();
} }
Response::builder() Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header("Content-Type", encoder.format_type()) .header("Content-Type", encoder.format_type())
.body(Body::from(buffer)) .body(Body::from(buffer))
.unwrap() .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()
}
#[allow(clippy::needless_return)]
async fn acme_create(State(state): State<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)]
async fn acme_order(State(state): State<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()
}
// -- ⚝ by Dave -- in NeoVim ⚝ --