Compare commits

...
174 Commits
Author SHA1 Message Date
Ara Sadoyan 0831ffb801 Merge pull request #53 from sergelogvinov/chart
feat: add aralez helm chart
2026-09-09 19:08:06 +02:00
Ara Sadoyan 5d55e2b120 configs 2026-09-09 19:01:38 +02:00
Ara Sadoyan b3c614e592 configs 2026-09-09 18:54:25 +02:00
Ara Sadoyan b22bb2fe16 Config files update, cleanup 2026-09-09 18:52:32 +02:00
Ara Sadoyan e6204a2b1d version change 2026-09-09 18:43:49 +02:00
Ara Sadoyan 6a78acd486 version change 2026-09-09 18:43:28 +02:00
Ara Sadoyan 1a2485b87a Implemented EndpointSlices #55 2026-09-09 18:42:41 +02:00
Ara Sadoyan 3329c34a09 working on #55 2026-09-09 15:39:28 +02:00
Serge 6df63f69cd feat: add aralez helm chart
Add basic Aralez deployment configuration, Helm chart linting workflow,
and Dependabot configuration.
2026-09-06 11:33:16 +02:00
Ara Sadoyan 4a4f3e6187 fixed closed #52 2026-09-04 16:24:49 +02:00
Ara Sadoyan 31f513cc2a hcl examples 2026-09-01 12:53:42 +02:00
Ara Sadoyan 0006d27d02 hcl examples 2026-09-01 12:19:09 +02:00
Ara Sadoyan d2d4100cba Finalized Consul integration,
Cached headers bug fix
2026-08-31 19:18:10 +02:00
Ara Sadoyan f575007044 Rewrite of Consul integration. 2026-08-30 18:38:31 +02:00
Ara Sadoyan bb43df0e4d Full rewrite of Kubernetes integration. 2026-08-25 19:32:42 +02:00
Ara Sadoyan 29c3d6a413 Full rewrite of Kubernetes integration. 2026-08-25 19:31:05 +02:00
Ara Sadoyan c21374f2a2 Full rewrite of Kubernetes integration. 2026-08-25 19:29:53 +02:00
Ara Sadoyan 51eec524ca Cargo 2026-08-10 15:40:50 +02:00
Ara Sadoyan 97ac839001 Removed reqwest dependency, moved all http client reqvetsts to native pingora. 2026-08-06 19:38:03 +02:00
Ara Sadoyan b63d822577 Rewrote and optimized healtchecks 2026-08-05 18:51:07 +02:00
Ara Sadoyan 600cb602e3 Readme update 2026-07-28 14:45:46 +02:00
Ara Sadoyan 29d1f1db78 Solved conflict on H2 upstream and Websocket, better gRPC ping 2026-07-24 12:28:44 +02:00
Ara Sadoyan 3ae54185ad Solved conflict on H2 upstream and Websocket, better gRPC ping 2026-07-24 12:27:12 +02:00
Ara Sadoyan 430f867792 Merge branch 'main' of github.com:sadoyan/aralez 2026-07-20 13:47:54 +02:00
Ara Sadoyan f6c092adc4 version fix 2026-07-20 13:47:05 +02:00
Ara Sadoyan 19ad8fdbf4 yaml update 2026-07-20 13:40:18 +02:00
Ara Sadoyan b55ff2d392 minor fix on memory response cache 2026-07-20 13:37:10 +02:00
Ara Sadoyan b73ccc527f Implementing in memory response cache 2026-07-20 12:25:27 +02:00
Ara Sadoyan c89fe48e44 Cargo.toml version update 2026-07-13 11:35:52 +02:00
Ara Sadoyan d21fd8cb4b config update 2026-07-13 11:32:36 +02:00
Ara Sadoyan 491442bb92 Added log file rotation and compression. Updated to Rust 1.97.0 2026-07-13 11:29:31 +02:00
Ara Sadoyan e07791e1f0 Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 13:34:47 +02:00
Ara Sadoyan 7b952b467c Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 13:34:08 +02:00
Ara Sadoyan 9a1e8fe149 Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 12:27:49 +02:00
Ara Sadoyan 874ce4fddb Implementing full async access log inspired by #45 2026-06-29 21:35:10 +02:00
Ara Sadoyan 99c7a811da Header parsing broken with rsplit_once in populate_headers_and_auth and build_headers
closed #48
2026-06-27 21:34:13 +02:00
Ara Sadoyan edadda803c version change 2026-06-27 14:20:06 +02:00
Ara Sadoyan c5d0fe60a0 resolved possible divide by zero issue from #46 2026-06-27 13:38:59 +02:00
Ara Sadoyan d7fe476b24 Merge pull request #47 from sadoyan/dev
Dev
2026-06-27 12:33:32 +02:00
Ara Sadoyan aa22749393 Merge branch 'main' into dev 2026-06-27 12:32:41 +02:00
Ara Sadoyan 4b6a558b4b Merge branch 'main' into dev 2026-06-27 12:31:49 +02:00
Ara Sadoyan 6c9c7acf65 Merge pull request #46 from Taqman-probe/fix/sticky-session-404
Fix: Resolve 404/405 errors with sticky session cookie handling in /api routes
2026-06-27 12:31:29 +02:00
Taqman-probe 2845dc5923 fix 404 error on multipath and cleanup fn: clone_idmap_into 2026-06-27 13:51:12 +09:00
Ara Sadoyan f2e07a8d70 resolved #44 2026-06-26 16:32:58 +02:00
Ara Sadoyan 5815da6576 Merge pull request #43 from sadoyan/dev
Update main.yaml
2026-06-22 13:27:17 +02:00
Ara Sadoyan 1ab0916026 Merge branch 'dev' 2026-06-22 13:24:42 +02:00
Ara Sadoyan 707f725b88 Update main.yaml 2026-06-22 13:22:45 +02:00
Ara Sadoyan eb4e73ece0 Basic access/error logging. Upgrade to Pingra 8.0.1 2026-06-22 13:20:56 +02:00
Ara Sadoyan 53e7dcfd33 Pid file follow symlink problem. 2026-06-19 11:14:40 +02:00
Ara Sadoyan f3b346a28d Merge pull request #42 from sadoyan/dev
migrate from serde_yml to noyalib
2026-06-12 16:05:30 +02:00
Ara Sadoyan c011800e1e migrate from serde_yml to noyalib 2026-06-12 16:03:49 +02:00
Ara Sadoyan 265ff6b774 cleanup 2026-06-12 13:33:21 +02:00
Ara Sadoyan 1c3d9a263f example configs 2026-06-12 13:12:39 +02:00
Ara Sadoyan 162c5060c9 cleanup, minor fix 2026-06-10 11:46:30 +02:00
Ara Sadoyan 132cf45dfe cleanup, minor fix 2026-06-10 11:29:35 +02:00
Ara Sadoyan 77dcafbb4e tcp_keepalive options for kernel timer 2026-06-09 17:53:04 +02:00
Ara Sadoyan 649bd979f7 socker address #41 2026-06-07 12:07:06 +02:00
Ara Sadoyan bd315106b9 Merge pull request #41 from Taqman-probe/fix/TcpListener-bind
Fix TcpListener binding and port availability check
2026-06-07 11:57:06 +02:00
Ara Sadoyan 3ba2ed33ae cargo.toml 2026-06-07 11:38:50 +02:00
Taqman-probe c09efab9fd prevent panic when parsing invalid address without port 2026-06-07 00:47:43 +09:00
Taqman-probe 735d605f6d prevent panic when parsing invalid address without port 2026-06-06 22:32:32 +09:00
Ara Sadoyan 6773d0f502 Cargo 2026-06-04 19:12:50 +02:00
Ara Sadoyan a8cb727da5 Cargo Licence 2026-06-04 18:39:02 +02:00
Ara Sadoyan f81194aee7 Cargo Licence 2026-06-04 18:38:47 +02:00
Ara Sadoyan 0f09a2e02b Roll back to MiMalloc 2026-06-04 18:14:22 +02:00
Ara Sadoyan 27aca0a3a5 Roll back to MiMalloc 2026-06-04 17:03:19 +02:00
Ara Sadoyan 3c99ed0c44 Fixed #40 2026-06-03 16:37:56 +02:00
Ara Sadoyan 4a6e1d817b OS signals , PID file 2026-06-02 19:56:08 +02:00
Ara Sadoyan f6a2df44d0 README 2026-06-02 12:20:18 +02:00
Ara Sadoyan e8d976f5f1 A minor issue with file loader 2026-06-01 15:35:49 +02:00
Ara Sadoyan a1341e90f7 Merge branch 'Taqman-probe-fix/mismatch-file-watch' into dev 2026-06-01 14:39:44 +02:00
Ara Sadoyan ddcacc509c Merge branch 'fix/mismatch-file-watch' of github.com:Taqman-probe/aralez into Taqman-probe-fix/mismatch-file-watch 2026-06-01 14:39:06 +02:00
Ara Sadoyan 2b385cc630 gitignore 2026-06-01 14:29:50 +02:00
Ara Sadoyan 1b5223986d README, signals 2026-06-01 14:25:36 +02:00
Ara Sadoyan ac2bd8113d README update 2026-06-01 11:48:54 +02:00
Ara Sadoyan 4d9a2ecfe3 Honoring OS signals 2026-05-30 18:53:36 +02:00
Taqman-probe aa70d818b2 Fix config file change detection logic in file watcher 2026-05-30 00:34:35 +09:00
Ara Sadoyan 15d356f087 Removed Dockerfile, put content to README 2026-05-27 19:23:32 +02:00
Ara Sadoyan b17962e6d5 Merge pull request #31 from Taqman-probe/fix/rate-limit-setting-message
fix: Fix global rate limit and 4xx limit fallback in upstream config log
2026-05-27 16:28:33 +02:00
Ara Sadoyan ea779182f2 Include request path in rate limit and 4xx limit warning logs
#32
2026-05-27 16:15:55 +02:00
Ara Sadoyan 9339a142a6 Merge pull request #33 from Taqman-probe/fix/skip-tls-detection-when-hc-disabled
Skip TLS detection when healthcheck: false
2026-05-27 15:09:12 +02:00
Ara Sadoyan cfc9bd319d Merge branch 'dev' 2026-05-27 14:58:56 +02:00
Ara Sadoyan a5ced59e5c Merge branch 'main' of github.com:sadoyan/aralez 2026-05-27 14:58:33 +02:00
Ara Sadoyan c3df7bc131 gitignore 2026-05-27 14:57:34 +02:00
Ara Sadoyan 0248d73836 Merge pull request #24 from gzsombor/forwarded-for-header
X-Forwarded-For should only contain the IP address
2026-05-27 14:55:51 +02:00
Ara Sadoyan 96a5aef7d0 warning log on retry parce 2026-05-27 14:54:21 +02:00
Ara Sadoyan 5c3b72b7a3 parceyaml 2026-05-27 14:26:05 +02:00
Ara Sadoyan 3cf0fc493f Merge pull request #34 from Taqman-probe/fix/failed-hot-reload-config
Add retry mechanism for configuration parsing failures
2026-05-27 14:18:28 +02:00
Ara Sadoyan 207ee481fb update 2026-05-27 14:12:10 +02:00
Ara Sadoyan 1c2f7327aa gitignore 2026-05-27 14:03:55 +02:00
Ara Sadoyan 310a554a25 gitignore 2026-05-27 14:03:22 +02:00
Ara Sadoyan 69a5167346 Merge pull request #35 from sadoyan/dev
Dev
2026-05-27 13:56:10 +02:00
Ara Sadoyan 4734ccab2f Minor fixed #26 & #28 2026-05-27 13:40:22 +02:00
Taqman-probe 61d65f6e4e Failed hot reloading of config file 2026-05-27 18:36:00 +09:00
Taqman-probe 20ac39067d Skip TLS detection when healthcheck: false 2026-05-27 16:44:17 +09:00
Taqman-probe 7afa76de8f fix: Fix global rate limit fallback in upstream config log 2026-05-27 14:22:59 +09:00
Ara Sadoyan e29161965f Added more monitoring metrics 2026-05-26 19:34:10 +02:00
Zsombor Gegesy 9216710dda X-Forwarded-For should only contain the IP address
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For
2026-05-23 07:11:55 +02:00
Ara Sadoyan faf840d102 Merge pull request #23 from sadoyan/dev
New features, 4xx counter
2026-05-22 16:57:07 +02:00
Ara Sadoyan d74883e16e New features, 4xx counter 2026-05-22 16:56:33 +02:00
Ara Sadoyan d95bbfcd1a Merge pull request #22 from sadoyan/dev
- Changed sticky session value from bool to u32, corresponding to `Max-Age=` of cookie
- Added counter and rate limiter for 4xx requests 
- Changed config for JWT authorization. The `data`  property is removed.
2026-05-22 16:52:27 +02:00
Ara Sadoyan d301f7225f New features, 4xx counter 2026-05-22 16:47:40 +02:00
Ara Sadoyan df02e523e4 cleanups 2026-05-21 18:34:46 +02:00
Ara Sadoyan 2f5def5c3c Changed sticky session from bool to Option<u64> 2026-05-20 21:09:23 +02:00
Ara Sadoyan 1727a2b5e7 deleted some comments 2026-05-20 17:17:04 +02:00
Ara Sadoyan 4bbedee27b JWT auth read and caches KEY from system env. 2026-05-19 15:26:05 +02:00
Ara Sadoyan 37ef118861 Dockerfile #21 2026-05-19 11:48:12 +02:00
Ara Sadoyan 00062b00da Removed authentication from API server, JWT master key as environment variable 2026-05-18 20:38:30 +02:00
Ara Sadoyan 2ce290abcf README update 2026-05-15 18:32:58 +02:00
Ara Sadoyan 2380f83d8e Added log to file option. 2026-05-15 16:00:57 +02:00
Ara Sadoyan 3965a1de93 README typo 2026-05-14 10:54:27 +02:00
Ara Sadoyan 7bc8294c22 README typo 2026-05-14 10:53:42 +02:00
Ara Sadoyan 37c2693e22 update cargo 2026-05-13 18:17:43 +02:00
Ara Sadoyan 554fa6648a Added special DEFAULT upstreams for catch up all. 2026-05-13 17:27:13 +02:00
Ara Sadoyan 20329518c1 Fix, add metrics and cleanup. 2026-05-13 16:17:11 +02:00
Ara Sadoyan 136ccc8e44 Persist config from API 2026-05-11 18:34:41 +02:00
Ara Sadoyan 1cbb19ea90 README 2026-05-09 14:53:10 +02:00
Ara Sadoyan c0e9fbc069 README site change 2026-05-09 13:54:26 +02:00
Ara Sadoyan 98318c5fb6 README 2026-05-08 19:36:38 +02:00
Ara Sadoyan fec9d5f1d6 Cleanup. Making clippy happy. 2026-05-08 16:35:20 +02:00
Ara Sadoyan 783ffb27e1 Merge pull request #20 from HrachMD/token-in-logs
Token Logging
2026-05-08 13:53:08 +02:00
Ara Sadoyan 3f6ee1799c Merge branch 'main' into token-in-logs 2026-05-08 13:52:14 +02:00
Ara Sadoyan c549750e9d README update 2026-05-08 13:07:28 +02:00
Ara Sadoyan 788f7fd4ea README update 2026-05-08 13:03:17 +02:00
hrachdev 5e29c077f3 chore: readme tweaks & fmt 2026-05-07 23:10:13 -04:00
hrachdev 22609df4ba Merge branch 'main' of github.com:HrachMD/aralez into token-in-logs 2026-05-07 23:02:58 -04:00
hrachdev c0594aed48 chore: cleanup 2026-05-07 20:02:18 -04:00
hrachdev 4f7f2d21ca fix: typo 2026-05-07 19:56:08 -04:00
hrachdev 4d04b8d7f1 fix: token logging, unneeded copy 2026-05-07 19:51:55 -04:00
Ara Sadoyan c381faabc6 README update 2026-05-07 19:57:14 +02:00
Ara Sadoyan 982feb632e minor fix update 2026-05-06 18:14:08 +02:00
Ara Sadoyan aee71c74f5 README update 2026-05-01 12:32:58 +02:00
Ara Sadoyan a0b6de9759 updated . config example 2026-04-30 18:13:37 +02:00
Ara Sadoyan a70eb53bc1 Let's Encrypt auto certificate HTTP-01 challenge #16 2026-04-30 18:04:25 +02:00
Ara Sadoyan bee307793c restructurisation grades 2026-04-27 15:28:54 +02:00
Ara Sadoyan 6e83775127 restructurisation 2026-04-27 15:22:31 +02:00
Ara Sadoyan baded40e6e Cache for JWT tokens, to minimize crypto. BRAKING: Claims key "valid" renamed to "exp" 2026-04-17 17:53:31 +02:00
Ara Sadoyan c0a419f6f7 completed implementation of #17 2026-04-15 18:23:57 +02:00
Ara Sadoyan 8aff2fa875 Standardizing implementation of #17 2026-04-14 16:11:24 +02:00
Ara Sadoyan 9b4ee26a2b Working on #17 2026-04-13 20:06:57 +02:00
Ara Sadoyan f135106a44 Changes in authentication 2026-04-08 19:05:19 +02:00
Ara Sadoyan 389c12119a code cleanup and improvements. 2026-04-08 17:00:06 +02:00
Ara Sadoyan 93a8661281 Cargo cleanup, dependency merge 2026-04-08 15:14:46 +02:00
Ara Sadoyan 0505ce2849 split upstreams.yaml file 2026-03-30 19:04:32 +02:00
Ara Sadoyan 72ed870538 split upstreams.yaml file 2026-03-27 19:24:30 +01:00
Ara Sadoyan 68140d0cf0 tye changes, optimization 2026-03-26 17:40:22 +01:00
Ara Sadoyan 7b9b206c13 optimization & cleanup 2026-03-26 16:58:53 +01:00
Ara Sadoyan 4706b281bc cleanup 2026-03-26 14:17:59 +01:00
Ara Sadoyan 1f8efc6af7 FUNDING.yml 2026-03-25 15:16:47 +01:00
Ara Sadoyan 9f595b2709 example config file update 2026-03-25 11:15:55 +01:00
Ara Sadoyan ed44516015 added redirect_to directive for upstreams 2026-03-24 16:08:14 +01:00
Ara Sadoyan 17da7862e3 upstreams ID hashing update 2026-03-18 20:06:50 +01:00
Ara Sadoyan 24d00da855 performance improvement, sticky session minor bug fix 2026-03-17 19:21:05 +01:00
Ara Sadoyan c9422759aa Minor performance improvement 2026-03-17 13:54:42 +01:00
Ara Sadoyan 94b1f77734 Type changes, auth override policy 2026-03-04 12:35:45 +01:00
Ara Sadoyan 9d986f9a28 Path level authentication 2026-03-03 19:35:16 +01:00
Ara Sadoyan 3afa2f209f pingora 0.8.0 upgrade 2026-03-03 13:54:53 +01:00
Ara Sadoyan c151fdf58b moving to boringssl 2026-02-19 18:11:54 +01:00
Ara Sadoyan 438426153f removed unwrap 2026-02-18 12:00:33 +01:00
Ara Sadoyan 9bb01fd1b0 minor improvements 2026-02-17 18:22:46 +01:00
Ara Sadoyan abb5fef1d6 minor improvements 2026-02-17 17:03:52 +01:00
Ara Sadoyan 3618687ad5 Memory allocation improvements for proxyhttp, fix issue with sticky session . 2026-02-10 19:07:43 +01:00
Ara Sadoyan a893b3c301 Memory allocation improvements for metrics collector . 2026-02-05 13:57:39 +01:00
Ara Sadoyan 3ff262c7f4 Merge pull request #13 from yerke/patch-1
Fix grammar and formatting in README.md
2026-02-04 14:41:50 +01:00
Yerkebulan Tulibergenov 062f02259f Fix grammar and formatting in README.md 2026-01-30 23:59:10 -08:00
Ara Sadoyan 1a4c9b7d55 Performance optimization in headers 2026-01-28 16:07:45 +01:00
Ara Sadoyan 6ef7f23823 Performance optimization v2 2026-01-28 13:20:31 +01:00
Ara Sadoyan 2b437c65fb Performance improvement. String removal from hot paths. 2026-01-27 16:19:51 +01:00
Ara Sadoyan 38055ae94e added new metric aralez_requests_by_upstream 2026-01-25 18:08:15 +01:00
Ara Sadoyan 703de9e909 updates on API server https://sadoyan.github.io/aralez-docs/assets/api/ 2026-01-22 16:50:51 +01:00
Ara Sadoyan 2c8b01295c Minor subfunction removal 2026-01-21 20:01:16 +01:00
Ara Sadoyan baebe1c00f Async apply of config via API 2026-01-20 19:16:27 +01:00
Ara Sadoyan 6c1d3c5ef8 Error handling on API server 2026-01-09 18:44:36 +01:00
Ara Sadoyan 2d1a827007 Removed unneeded loop 2025-12-14 12:09:11 +01:00
77 changed files with 6951 additions and 2959 deletions
+13
View File
@@ -0,0 +1,13 @@
[target.aarch64-unknown-linux-musl]
rustflags = [
"-C", "link-arg=-Wl,--defsym=fopen64=fopen",
"-C", "link-arg=-Wl,--defsym=fseeko64=fseeko",
"-C", "link-arg=-Wl,--defsym=ftello64=ftello"
]
[target.x86_64-unknown-linux-musl]
rustflags = [
"-C", "link-arg=-Wl,--defsym=fopen64=fopen",
"-C", "link-arg=-Wl,--defsym=fseeko64=fseeko",
"-C", "link-arg=-Wl,--defsym=ftello64=ftello"
]
+1 -1
View File
@@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: sadoyan
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
+9
View File
@@ -0,0 +1,9 @@
helm-extra-args: --timeout 300s
check-version-increment: true
debug: true
chart-dirs:
- charts
validate-maintainers: true
namespace: default
release-label: test
target-branch: main
+23
View File
@@ -0,0 +1,23 @@
---
# See https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
commit-message:
prefix: "chore:"
open-pull-requests-limit: 8
rebase-strategy: disabled
schedule:
interval: "monthly"
day: "monday"
time: "10:00"
timezone: "UTC"
cooldown:
default-days: 7
groups:
actions:
patterns:
- "actions/*"
- "helm/*"
+28
View File
@@ -0,0 +1,28 @@
name: Helm chart check
on:
pull_request:
branches:
- main
paths:
- "charts/**"
jobs:
helm-lint:
name: Helm chart check
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install chart-testing tools
id: lint
uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0
- name: Run helm chart linter
run: ct --config .github/ct.yml lint
- name: Run helm template
run: helm lint charts/aralez
+5 -2
View File
@@ -5,12 +5,13 @@
*.dll *.dll
*.exe *.exe
*.sh *.sh
/docs/ *.yaml
/docs /docs
assets/
assets
/target/ /target/
*.iml *.iml
.idea/ .idea/
.etc/
*.ipr *.ipr
*.iws *.iws
/out/ /out/
@@ -21,3 +22,5 @@ crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
/target /target
/z_shpo /z_shpo
/configs
Makefile
Generated
+2301 -1340
View File
File diff suppressed because it is too large Load Diff
+48 -38
View File
@@ -1,7 +1,11 @@
[package] [package]
name = "aralez" name = "aralez"
version = "0.9.2" version = "0.93.4"
edition = "2021" edition = "2021"
license = "Apache-2.0"
description = "Reverse proxy built on top of Cloudflare's Pingora"
exclude = ["configs/*"]
repository = "https://github.com/sadoyan/aralez"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
@@ -11,45 +15,51 @@ panic = "abort"
strip = true strip = true
[dependencies] [dependencies]
tokio = { version = "1.45.1", features = ["full"] } tokio = { version = "1.53.1", features = ["full"] }
pingora = { version = "0.6.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl pingora = { version = "0.8.1", features = ["lb", "openssl"] } # openssl, rustls, boringssl
serde = { version = "1.0.219", features = ["derive"] } pingora-core = "0.8.1"
pingora-proxy = "0.8.1"
pingora-http = "0.8.1"
pingora-limits = "0.8.1"
pingora-cache = "0.8.1"
dashmap = "7.0.0-rc2" dashmap = "7.0.0-rc2"
pingora-core = "0.6.0" async-trait = "0.1.92"
pingora-proxy = "0.6.0" log = "0.4.34"
pingora-http = "0.6.0" notify = "9.0.0-rc.4"
pingora-limits = "0.6.0" axum = { version = "0.8.9" }
async-trait = "0.1.89" serde = { version = "1.0.229", features = ["derive"] }
env_logger = "0.11.8" noyalib = { version = "0.0.32", features = ["compat-serde-yaml"] }
log = "0.4.28" rand = "0.10.2"
futures = "0.3.31" base64 = "0.23.1"
notify = "8.2.0" jsonwebtoken = { version = "11.0.0", default-features = false, features = ["use_pem", "rust_crypto"] }
axum = { version = "0.8.4" } sha2 = { version = "0.11.0-rc.5", default-features = false }
axum-server = { version = "0.7.2", features = ["tls-openssl"] } base16ct = { version = "1.0.0", features = ["alloc"] }
reqwest = { version = "0.12.23", features = ["json", "native-tls-alpn", "stream"] }
#reqwest = { version = "0.12.15", features = ["json", "rustls-tls"] }
#reqwest = { version = "0.12.15", default-features = false, features = ["rustls-tls", "json"] }
serde_yaml = "0.9.34-deprecated"
rand = "0.9.2"
base64 = "0.22.1"
jsonwebtoken = "9.3.1"
tonic = "0.14.2"
sha2 = { version = "0.11.0-rc.2", default-features = false }
base16ct = { version = "0.3.0", features = ["alloc"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
arc-swap = "1.7.1" arc-swap = "1.9.2"
mimalloc = { version = "0.1.48", default-features = false }
prometheus = "0.14.0" prometheus = "0.14.0"
lazy_static = "1.5.0" x509-parser = "0.18.1"
x509-parser = "0.18.0"
rustls-pemfile = "2.2.0" rustls-pemfile = "2.2.0"
tower-http = { version = "0.6.6", features = ["fs"] } tower-http = { version = "0.7.1", features = ["fs"] }
once_cell = "1.21.3"
privdrop = "0.5.6" privdrop = "0.5.6"
ctrlc = "3.5.0" serde_json = "1.0.151"
port_check = "0.3.0" subtle = "2.6.1"
#moka = { version = "0.12.10", features = ["sync"] } moka = { version = "0.12.16", features = ["sync"] }
#rustls = { version = "0.23.27", features = ["ring"] } ahash = "0.8.12"
#hickory-client = { version = "0.25.2" } instant-acme = "0.8.5"
#openssl = "0.10.73" rcgen = "0.14.10"
log4rs = { version = "1.4.0", features = ["rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller", "gzip"] }
mimalloc = { version = "0.1.52", default-features = false }
signal-hook = "0.4.4"
sd-notify = "0.5.0"
libc = "0.2.186"
tokio-native-tls = "0.3.1"
hyper = "1.11.1"
hyper-util = "0.1.20"
http-body-util = "0.1.5"
bytes = "1.12.1"
rustls = { version = "0.23", default-features = false, features = ["ring"] }
kube = { version = "4.2.0", features = ["runtime", "client", "derive"] }
k8s-openapi = { version = "0.28.0", features = ["v1_32"] }
futures = "0.3.34"
lazy_static = "1.5.0"
+186 -160
View File
@@ -2,195 +2,196 @@
--- ---
# Aralez (Արալեզ), # Aralez (Արալեզ)
### **Reverse proxy built on top of Cloudflare's Pingora** ### **Reverse proxy built on top of Cloudflare's Pingora**
Aralez is a high-performance Rust reverse proxy with zero-configuration automatic protocol handling, TLS, and upstream management, 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
featuring Consul and Kubernetes integration for dynamic pod discovery and health-checked routing, acting as a lightweight ingress-style proxy. lightweight ingress-style proxy.
--- ---
What Aralez means ? What Aralez means ? **Aralez = Արալեզ** <ins>Named after the legendary Armenian guardian spirit, winged dog-like creature, that descend upon fallen heroes to lick their wounds and resurrect them</ins>.
**Aralez = Արալեզ** <ins>.Named after the legendary Armenian guardian spirit, winged dog-like creature, that descend upon fallen heroes to lick their wounds and resurrect them.</ins>.
Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers world-class performance, security and scalability — right out of the box. Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers world-class performance, security and scalability — right out of the box.
[![Buy Me A Coffee](https://img.shields.io/badge/☕-Buy%20me%20a%20coffee-orange)](https://www.buymeacoffee.com/sadoyan) ---
## Links
- [**Documentation**](https://aralez.rs) : The manual you should read
- [**Downloads**](https://github.com/sadoyan/aralez/releases) : Binary downloads
- [**Issues**](https://github.com/sadoyan/aralez/issues) : Issues and requests
- [**Crates**](https://crates.io/crates/aralez) : The Rust crate registry
- [**DockerHUB**](https://hub.docker.com/r/sadoyan/aralez) : DockerHUB official repository
- [**GitHUB Packages**](https://github.com/sadoyan/aralez/pkgs/container/aralez) : GitHUB ghcr.io images
--- ---
## 🔧 Key Features ## Key Features
- **Dynamic Config Reloads** — Upstreams can be updated live via API, no restart required. - **Dynamic Config Reloads** — Upstreams can be updated live via API, no restart required.
- **TLS Termination** — Built-in OpenSSL support. - **Autoload of certificates** — Automatically loads new/changed certificates from a folder, without a restart.
- **Automatic load of certificates** — Automatically reads and loads certificates from a folder, without a restart. - **Let’s Encrypt Certificates** — Ordering and renewal of SSL/TLS certificates via the HTTP-01 challenge.
- **Upstreams TLS detection** — Aralez will automatically detect if upstreams uses secure connection. - **Upstreams TLS detection** — Aralez will automatically detect if upstreams uses secure connection.
- **Built in rate limiter** — Limit requests to server, by setting up upper limit for requests per seconds, per virtualhost. - **Built in rate limiter** — Globar or route limit requests to upstreams.
- **Global rate limiter** — Set rate limit for all virtualhosts. - **Caching Proxy** — In memory cache with configurable TTL, without external dependencies.
- **Per path rate limiter** — Set rate limit for specific paths. Path limits will override global limits.
- **Authentication** — Supports Basic Auth, API tokens, and JWT verification. - **Authentication** — Supports Basic Auth, API tokens, and JWT verification.
- **Basic Auth** - **Basic Auth**
- **API Key** via `x-api-key` header - **API Key** via `x-api-key` header
- **JWT Auth**, with tokens issued by Aralez itself via `/jwt` API - **JWT Auth**, with tokens issued by Aralez itself via `/jwt` API
- ⬇️ See below for examples and implementation details. - **Forward Auth**, Sends requests to an authentication server.
- **Load Balancing Strategies** - **Load Balancing** Round-robin, health checks, optional sticky sessions.
- Round-robin
- Failover with health checks
- Sticky sessions via cookies
- **Unified Port** — Serve HTTP and WebSocket traffic over the same connection.
- **Built in file server** — Build in minimalistic file server for serving static files, should be added as upstreams for public access. - **Built in file server** — Build in minimalistic file server for serving static files, should be added as upstreams for public access.
- **Memory Safe** — Created purely on Rust. - **Upstream Providers:**
- **High Performance** — Built with [Pingora](https://github.com/cloudflare/pingora) and tokio for async I/O.
## 🌍 Highlights
- ⚙️ **Upstream Providers:**
- `file` Upstreams are declared in config file. - `file` Upstreams are declared in config file.
- `consul` Upstreams are dynamically updated from Hashicorp Consul. - `consul` Upstreams are dynamically updated from Hashicorp Consul.
- 🔁 **Hot Reloading:** Modify upstreams on the fly via `upstreams.yaml` — no restart needed. - `kubernetes` Upstreams are dynamically updated from kubernetes api server.
- 🔮 **Automatic WebSocket Support:** Zero config — connection upgrades are handled seamlessly. - **Auto WebSocket Support:** WS connection upgrades are handled automatically.
- 🔮 **Automatic GRPC Support:** Zero config, Requires `ssl` to proxy, gRPC handled seamlessly. - **Auto gRPC Support:** gRPC detected and handled automatically.
- 🔮 **Upstreams Session Stickiness:** Enable/Disable Sticky sessions globally. - **Header Injection:** Global and per-route server/client headers injection.
- 🔐 **TLS Termination:** Fully supports TLS for upstreams and downstreams. - **Remote Config Push:** Lightweight HTTP API to update configs from CI/CD or other systems.
- 🛡️ **Built-in Authentication** Basic Auth, JWT, API key. - **Memory Safe** — 100% Rust.
- 🧠 **Header Injection:** Global and per-route header configuration. - **High Performance** — Built with [Pingora](https://github.com/cloudflare/pingora) and tokio for async I/O.
- 🧪 **Health Checks:** Pluggable health check methods for upstreams.
- 🛰️ **Remote Config Push:** Lightweight HTTP API to update configs from CI/CD or other systems.
--- ---
## 📁 File Structure ## Configuration Overview
``` ### `main.yaml`
.
├── main.yaml # Main configuration loaded at startup
├── upstreams.yaml # Watched config with upstream mappings
├── etc/
│ ├── server.crt # TLS certificate (required if using TLS)
│ └── key.pem # TLS private key
```
---
## 🛠 Configuration Overview
### 🔧 `main.yaml`
| Key | Example Value | Description | | Key | Example Value | Description |
|----------------------------------|--------------------------------------|----------------------------------------------------------------------------------------------------| |----------------------------------|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **threads** | 12 | Number of running daemon threads. Optional, defaults to 1 | | **threads** | 12 | Number of running daemon threads. Optional, defaults to 1 |
| **runuser** | aralez | Optional, Username for running aralez after dropping root privileges, requires to launch as root | | **runuser** | aralez | Optional. Username for running aralez after dropping root privileges (requires launch as root) |
| **rungroup** | aralez | Optional,Group for running aralez after dropping root privileges, requires to launch as root | | **rungroup** | aralez | Optional. Group for running aralez after dropping root privileges (requires launch as root) |
| **daemon** | false | Run in background (boolean) | | **daemon** | false | Run in background (boolean) |
| **upstream_keepalive_pool_size** | 500 | Pool size for upstream keepalive connections | | **upstream_keepalive_pool_size** | 500 | Pool size for upstream keepalive connections |
| **pid_file** | /tmp/aralez.pid | Path to PID file | | **pid_file** | /tmp/aralez.pid | Path to PID file |
| **error_log** | /tmp/aralez_err.log | Path to error log file |
| **upgrade_sock** | /tmp/aralez.sock | Path to live upgrade socket file |
| **config_address** | 0.0.0.0:3000 | HTTP API address for pushing upstreams.yaml from remote location | | **config_address** | 0.0.0.0:3000 | HTTP API address for pushing upstreams.yaml from remote location |
| **config_tls_address** | 0.0.0.0:3001 | HTTPS API address for pushing upstreams.yaml from remote location | | **proxy_tls_grade** | high, medium, unsafe | Grade of TLS ciphers. `high` matches Qualys SSL Labs A+ (defaults to `medium`) |
| **config_tls_certificate** | etc/server.crt | Certificate file path for API. Mandatory if proxy_address_tls is set, else optional |
| **proxy_tls_grade** | (high, medium, unsafe) | Grade of TLS ciphers, for easy configuration. High matches Qualys SSL Labs A+ (defaults to medium) |
| **config_tls_key_file** | etc/key.pem | Private Key file path. Mandatory if proxy_address_tls is set, else optional |
| **proxy_address_http** | 0.0.0.0:6193 | Aralez HTTP bind address | | **proxy_address_http** | 0.0.0.0:6193 | Aralez HTTP bind address |
| **proxy_address_tls** | 0.0.0.0:6194 | Aralez HTTPS bind address (Optional) | | **proxy_address_tls** | 0.0.0.0:6194 | Aralez HTTPS bind address (Optional) |
| **proxy_certificates** | etc/certs/ | The directory containing certificate and key files. In a format {NAME}.crt, {NAME}.key. | | **proxy_configs** | /etc/aralez/ | Direcotry containing configuration files, must be writeable by user `aralez` |
| **upstreams_conf** | etc/upstreams.yaml | The location of upstreams file | | **upstreams_conf** | /etc/aralez/upstreams.yaml | Location of the upstreams file |
| **log_level** | info | Log level , possible values : info, warn, error, debug, trace, off | | **access_log** | access | Configure access logging. Values: `access, error` |
| **hc_method** | HEAD | Healthcheck method (HEAD, GET, POST are supported) UPPERCASE | | **log_level** | info | Log level: `info`, `warn`, `error`, `debug`, `trace`, `off` |
| **log_file** | /tmp/aralez.log, 20, 5, compress | Optional, the location of log file, file size in megabytes, number of log files to keep, compress files .valueas are separated by comma. Defaults to None, 100, 5, No |
| **hc_method** | HEAD | Healthcheck method: HEAD, GET, POST (UPPERCASE) |
| **hc_interval** | 2 | Interval for health checks in seconds | | **hc_interval** | 2 | Interval for health checks in seconds |
| **master_key** | 5aeff7f9-7b94-447c-af60-e8c488544a3e | Master key for working with API server and JWT Secret generation | | **file_server_folder** | /some/local/folder | Optional. Local folder to serve |
| **file_server_folder** | /some/local/folder | Optional, local folder to serve | | **file_server_address** | 127.0.0.1:3002 | Optional. Local address for file server |
| **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 | Enable/disable remote config push capability |
| **config_api_enabled** | true | Boolean to enable/disable remote config push capability | | **cache_size_mb** | 50 | Optional. Enable internal cache with size in megabytes of value. Cache is entirely disabled when this line is commented out. |
| **cache_ttl** | 10 | Optional. Set Default TTL in seconds for cached items, works if cache control headers are not set via upstream. Defaults to 60 if cache_size_mb is set. |
### 🌐 `upstreams.yaml`
- `provider`: `file` or `consul`
- File-based upstreams define:
- Hostnames and routing paths
- Backend servers (load-balanced)
- Optional request headers, specific to this upstream
- Global headers (e.g., CORS) apply to all proxied responses
- Optional authentication (Basic, API Key, JWT)
--- ---
## 🛠 Installation ## Installation
Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/aralez/releases) repo Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/aralez/releases) repo Make the binary executable `chmod 755 ./aralez-VERSION` and run.
Make the binary executable `chmod 755 ./aralez-VERSION` and run.
File names: File names:
| File Name | Description | | File Name | Description |
|---------------------------|--------------------------------------------------------------------------| |---------------------------------|----------------------------------------------------------------------------|
| `aralez-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency | | `aralez-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency |
| `aralez-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies | | `aralez-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies |
| `aralez-x86_64-compat-musl.gz` | Static Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `aralez-x86_64-compat-glibc.gz` | Dynamic Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `aralez-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency | | `aralez-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency |
| `aralez-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies | | `aralez-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies |
| `sadoyan/aralez` | Docker image on Debian 13 slim (https://hub.docker.com/r/sadoyan/aralez) | | `sadoyan/aralez` | Docker image on Debian 13 slim (<https://hub.docker.com/r/sadoyan/aralez>) |
## About binaries
**glibc** builds are in general faster, but have few, basic, Glibc dependencies:
**musl** builds are 100% portable, static compiled binaries and have zero system dependencies. In general musl builds have a little less performance.
The most intensive tests shows 107k-110k requests per second on **Glibc** binaries against 97k-100k **Musl** ones.
For running **Aralez** on very old hardware, CPUs prior Haswell, (launched before 2013) use `aralez-x86_64-compat-*.gz`
For getting the best performance on newer hardware use `aralez-x86_64-*.gz`.
**Via docker** **Via docker**
```shell ```shell
docker run -d \ docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 sadoyan/aralez
-v /local/path/to/config:/etc/aralez:ro \ docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 sadoyan/aralez:compat
-p 80:80 \ docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 ghcr.io/sadoyan/aralez:latest
-p 443:443 \ docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 ghcr.io/sadoyan/aralez:compat
sadoyan/aralez
``` ```
## 💡 Note **Dockerfile :**
In general **glibc** builds are working faster, but have few, basic, system dependencies for example : ```dockerfile
FROM debian:trixie-slim
``` RUN apt-get update && apt-get install -y ca-certificates curl net-tools iputils-ping
linux-vdso.so.1 (0x00007ffeea33b000) RUN apt-get clean && rm -rf /var/lib/apt/lists/*
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f09e7377000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f09e6320000) COPY aralez /usr/local/bin/aralez
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f09e613f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f09e73b1000) RUN chmod +x /usr/local/bin/aralez
RUN mkdir -p /etc/aralez/certs/upstreams
WORKDIR /etc/aralez
ENTRYPOINT ["/usr/local/bin/aralez", "-c", "/etc/aralez/main.yaml"]
``` ```
These are common to any Linux systems, so the binary should work on almost any Linux system. ## Running the Proxy
**musl** builds are 100% portable, static compiled binaries and have zero system depencecies.
In general musl builds have a little less performance.
The most intensive tests shows 107k-110k requests per second on **Glibc** binaries against 97k-100k **Musl** ones.
## 🔌 Running the Proxy
```bash ```bash
./aralez -c path/to/main.yaml ./aralez -c path/to/main.yaml
``` ```
## 🔌 Systemd integration ## Systemd integration
Assuming Aralez in installed in `/opt/aralez` folder
```bash ```bash
cat > /etc/systemd/system/aralez.service <<EOF cat > /etc/systemd/system/aralez.service <<EOF
[Unit]
Description=meilisearch
Documentation=https://github.com/sadoyan/aralez
Wants=network-online.target
After=network-online.target
[Service] [Service]
Type=forking WorkingDirectory = /opt/aralez/
PIDFile=/run/aralez.pid ExecReload=/bin/kill -HUP
ExecStart=/bin/aralez -d -c /etc/aralez.conf ExecStart=/opt/aralez/aralez -c /opt/aralez/proxyconfigs/main.yaml
ExecReload=kill -QUIT $MAINPID KillMode=process
ExecReload=/bin/aralez -u -d -c /etc/aralez.conf KillSignal=SIGINT
LimitNOFILE=infinity
LimitNPROC=infinity
Restart=on-failure
RestartSec=2
StartLimitBurst=3
StartLimitIntervalSec=10
TasksMax=infinity
[Install]
WantedBy=multi-user.target
EOF EOF
``` ```
```bash ```bash
systemctl daemon-reload
systemctl enable aralez.service. systemctl enable aralez.service.
systemctl restart aralez.service. systemctl restart aralez.service.
``` ```
## 💡 Example ## Example upstreams config
A sample `upstreams.yaml` entry:
```yaml ```yaml
provider: "file" provider: "file"
sticky_sessions: false sticky_sessions: 8600
to_https: false to_https: false
rate_limit: 10 rate_limit: 20
x4xx_limit: 20
server_headers: server_headers:
- "X-Forwarded-Proto:https" - "X-Forwarded-Proto:https"
- "X-Forwarded-Port:443" - "X-Forwarded-Port:443"
@@ -198,13 +199,11 @@ 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: 20 rate_limit: 10
x4xx_limit: 10
to_https: false to_https: false
server_headers: server_headers:
- "X-Something-Else:Foobar" - "X-Something-Else:Foobar"
@@ -217,6 +216,9 @@ myhost.mydomain.com:
- "127.0.0.2:8000" - "127.0.0.2:8000"
"/foo": "/foo":
to_https: true to_https: true
authorization:
type: "jwt"
data: "266463d1-210a-4787-9a81-4aacb37a8723"
client_headers: client_headers:
- "X-Another-Header:Hohohohoho" - "X-Another-Header:Hohohohoho"
servers: servers:
@@ -226,21 +228,32 @@ myhost.mydomain.com:
healthcheck: false healthcheck: false
servers: servers:
- "127.0.0.1:8001" - "127.0.0.1:8001"
DEFAULT:
paths:
"/":
servers:
- "127.0.0.1:3000"
``` ```
**This means:** **This means:**
- 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` - 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` - Additionally, myhost.mydomain.com with path `/` will receive custom headers : `X-Another-Header:Hohohohoho` and `X-Something-Else:Foobar`
- Requests to each hosted domains will be limited to 10 requests per second per virtualhost. - 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. - Requests limits are calculated per requester ip plus requested virtualhost.
- If the requester exceeds the limit it will receive `429 Too Many Requests` error. - 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. - 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 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 Aralez. - Plain HTTP to `myhost.mydomain.com/foo` will get 301 redirect to configured TLS port of Aralez.
- `myhost.mydomain.com/foo` will require authentication with JWT token, signed by `266463d1-210a-4787-9a81-4aacb37a8723`.
- Requests to `myhost.mydomain.com/foo` will be proxied to `127.0.0.4` and `127.0.0.5`. - Requests to `myhost.mydomain.com/foo` will be proxied to `127.0.0.4` and `127.0.0.5`.
- Requests to `myhost.mydomain.com/.well-known/acme-challenge` will be proxied to `127.0.0.1:8001`, but healthcheks are disabled. - Requests to `myhost.mydomain.com/.well-known/acme-challenge` will be proxied to `127.0.0.1:8001`, but healthcheks are disabled.
- SSL/TLS for upstreams is detected automatically, no need to set any config parameter. - SSL/TLS for upstreams is detected automatically, no need to set any config parameter.
@@ -249,33 +262,42 @@ myhost.mydomain.com:
- 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 one `tls_certificate/tls_key_file` pair.
- First pair is required to create the TLS listener.
- This pair can be anything, even self-signed with dummy domain.
- After getting normal certificate it can be deleted
```shell
mkdir -p /etc/aralez/certificates
chown -R aralez:aralez /etc/aralez
cd /etc/aralez/certificates
openssl req -x509 -newkey rsa:4096 \
-keyout dummy.key -out dummy.crt -sha256 -days 3650 -nodes \
-subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"
```
--- ---
## 📡 Remote Config API ## Remote Config API
Push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates. Push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates. URL parameter. `key=MASTERKEY` is required. `MASTERKEY` is the value of `master_key` in the `main.yaml`
URL parameter. `key=MASTERKEY` is required. `MASTERKEY` is the value of `master_key` in the `main.yaml`
```bash ```bash
curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf?key=${MASTERKEY} curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf?key=${MASTERKEY}
@@ -283,7 +305,7 @@ curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf?key=${MASTERK
--- ---
## 🔐 Authentication (Optional) ## Authentication (Optional)
- Adds authentication to all requests. - Adds authentication to all requests.
- Only one method can be active at a time. - Only one method can be active at a time.
@@ -337,13 +359,13 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
``` ```
## 📃 License ## License
[Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) [Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
--- ---
## 🧠 Notes ## Notes
- Uses Pingora under the hood for efficiency and flexibility. - Uses Pingora under the hood for efficiency and flexibility.
- Designed for edge proxying, internal routing, or hybrid cloud scenarios. - Designed for edge proxying, internal routing, or hybrid cloud scenarios.
@@ -352,37 +374,42 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
- Sticky session support. - Sticky session support.
- HTTP2 ready. - HTTP2 ready.
### 🧩 Summary Table: Feature Comparison ### Summary Table: Feature Comparison
| Feature / Proxy | **Aralez** | **Nginx** | **HAProxy** | **Traefik** | **Caddy** | **Envoy** | | Feature / Proxy | **Aralez** | **Nginx** | **HAProxy** | **Traefik** | **Caddy** | **Envoy** |
|----------------------------------|:-----------------:|:---------------------------:|:-----------------:|:--------------------------------:|:---------------:|:---------------:| |--------------------|:----------:|:-----------:|:-----------:|:-----------:|:----------:|:---------:|
| **Hot Reload (Zero Downtime)** | ✅ **Automatic** | ⚙️ Manual (graceful reload) | ⚙️ Manual | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **Reload** | ✅ Hot | ⚙️ Manual | ⚙️ Manual | ✅ Hot | ✅ Hot | ✅ Hot |
| **Auto Cert Reload (from disk)** | ✅ **Automatic** | ❌ No | ❌ No | ✅ Automatic (Let's Encrypt only) | ✅ Automatic | ⚙️ Manual | | **Cert load** | ✅ Hot | ❌ Reload | ❌ Reload | ✅ Yes | ✅ Yes | ⚙️ No ? |
| **Auth: Basic / API Key / JWT** | ✅ **Built-in** | ⚙️ Basic only | ⚙️ Basic only | ✅ Config-based | ✅ Config-based | ✅ Config-based | | **Authentication** | ✅ Yes | ⚙️ Limited | ⚙️ Limited | ✅ Yes | ✅ Yes | ✅ Yes |
| **TLS / HTTP2 Termination** | ✅ **Automatic** | ⚙️ Manual config | ⚙️ Manual config | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **HTTP2** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Built-in A+ TLS Grades** | ✅ **Automatic** | ⚙️ Manual tuning | ⚙️ Manual | ⚙️ Manual | ✅ Automatic | ⚙️ Manual | | **TLS Grades** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ⚙️ Manual |
| **gRPC Proxy** | ✅ **Zero-Config** | ⚙️ Manual setup | ⚙️ Manual | ⚙️ Needs config | ⚙️ Needs config | ⚙️ Needs config | | **gRPC** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual |
| **SSL Proxy** | ✅ **Zero-Config** | ⚙️ Manual | ⚙️ Manual | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **SSL Proxy** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP/2 Proxy** | ✅ **Zero-Config** | ⚙️ Manual enable | ⚙️ Manual enable | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **HTTP/2** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **WebSocket Proxy** | ✅ **Zero-Config** | ⚙️ Manual upgrade | ⚙️ Manual upgrade | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **WebSocket** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Sticky Sessions** | ✅ **Built-in** | ⚙️ Config-based | ⚙️ Config-based | ✅ Automatic | ⚙️ Limited | ✅ Config-based | | **Sticky Session** | ✅ Yes | ❌ No | ⚙️ Yes | ✅ Yes | ⚙️ Limited | ✅ Manual |
| **Prometheus Metrics** | ✅ **Built-in** | ⚙️ External exporter | ✅ Built-in | ✅ Built-in | ✅ Built-in | ✅ Built-in | | **Prometheus** | ✅ Yes | ⚙️ External | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Consul Integration** | ✅ **Yes** | ❌ No | ⚙️ Via DNS only | ✅ Yes | ❌ No | ✅ Yes | | **Consul** | ✅ Yes | ❌ No | ⚙️DNS API | ✅ Yes | ❌ No | ✅ Yes |
| **Kubernetes Integration** | ✅ **Yes** | ⚙️ Needs ingress setup | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes | | **Kubernetes** | ✅ Yes | ⚙️ Ingress | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes |
| **Request Limiter** | ✅ **Yes** | ✅ Config-based | ✅ Config-based | ✅ Config-based | ✅ Config-based | ✅ Config-based | | **Limiter** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Serve Static Files** | ✅ **Yes** | ✅ Yes | ⚙️ Basic | ✅ Automatic | ✅ Automatic | ❌ No | | **Static Files** | ✅ Yes | ✅ Yes | ⚙️ Lua ? | ✅ Yes | ✅ Yes | ❌ No |
| **Upstream Health Checks** | ✅ **Automatic** | ⚙️ Manual config | ⚙️ Manual config | ✅ Automatic | ✅ Automatic | ✅ Automatic | | **Health Checks** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Built With** | 🦀 **Rust** | C | C | Go | Go | C++ | | **Built With** | Rust | C | C | Go | Go | C++ |
--- ---
✅ **Automatic / Zero-Config** – Works immediately, no setup required ✅ **Auto** – Automatically detected and loaded
⚙️ **Manual / Config-based** – Requires explicit configuration or modules ✅ **Hot** – Works immediately, no reload/restart is required
✅ **Yes** – Works immediately, no setup required
⚙️ **Manual** – Requires explicit configuration or modules
⚙️ **Reload** – Reload or restart is required
⚙️ **Limited** – Support is limited to certain features
⚙️ **External** – Requires an external module
❌ **No** – Not supported ❌ **No** – Not supported
## 💡 Simple benchmark by [Oha](https://github.com/hatoo/oha) ## Simple benchmark by [Oha](https://github.com/hatoo/oha)
⚠️ These benchmarks use : **These benchmarks use :**
- 3 async Rust echo servers on a local network with 1Gbit as upstreams. - 3 async Rust echo servers on a local network with 1Gbit as upstreams.
- A dedicated server for running **Aralez** - A dedicated server for running **Aralez**
@@ -411,7 +438,7 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
- "192.168.211.212:8000" - "192.168.211.212:8000"
``` ```
## 💡 Results reflect synthetic performance under optimal conditions. ## Results reflect synthetic performance under optimal conditions.
- CPU : Intel (R) Xeon (R) CPU E3-1270 v6 @ 3.80GHz - CPU : Intel (R) Xeon (R) CPU E3-1270 v6 @ 3.80GHz
- 300 : simultaneous connections - 300 : simultaneous connections
@@ -525,7 +552,7 @@ Error distribution:
![Aralez](https://netangels.net/utils/musl10.png) ![Aralez](https://netangels.net/utils/musl10.png)
## 🚀 Aralez, Nginx, Traefik performance benchmark ## Aralez, Nginx, Traefik performance benchmark
This benchmark is done on 4 servers. With CPU Intel (R) Xeon (R) E-2174G CPU @ 3.80GHz, 64 GB RAM. This benchmark is done on 4 servers. With CPU Intel (R) Xeon (R) E-2174G CPU @ 3.80GHz, 64 GB RAM.
@@ -540,4 +567,3 @@ The results show requests per second performed by Load balancer. You can see 3 b
1. Requests via http1.1 to plain text endpoint. 1. Requests via http1.1 to plain text endpoint.
2. Requests to via http2 to SSL endpoint. 2. Requests to via http2 to SSL endpoint.
3. Mixed workload with plain http1.1 and htt2 SSL. 3. Mixed workload with plain http1.1 and htt2 SSL.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+23
View File
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
+18
View File
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://json.schemastore.org/chart.json
apiVersion: v2
name: aralez
description: Ingress controller for Kubernetes
type: application
home: https://github.com/sadoyan/aralez/charts
sources:
- https://github.com/sadoyan/helm-charts/tree/main/charts/aralez
keywords:
- ingress-controller
- aralez
maintainers: []
#
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.1
#
# renovate: datasource=docker depName=ghcr.io/sadoyan/aralez
appVersion: 0.93.3
+17
View File
@@ -0,0 +1,17 @@
{{ template "chart.header" . }}
{{ template "chart.deprecationWarning" . }}
{{ template "chart.badgesSection" . }}
{{ template "chart.description" . }}
{{ template "chart.homepageLine" . }}
{{ template "chart.maintainersSection" . }}
{{ template "chart.sourcesSection" . }}
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}
+18
View File
@@ -0,0 +1,18 @@
ingressClass: aralez
ingressClassResource:
enabled: true
# hostNetwork: true
# hostPort:
# ports:
# http: 8080
# https: 8443
priorityClassName: system-cluster-critical
env: []
args: []
useDaemonSet: true
terminationGracePeriodSeconds: 120
View File
+104
View File
@@ -0,0 +1,104 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "aralez.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "aralez.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "aralez.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "aralez.labels" -}}
helm.sh/chart: {{ include "aralez.chart" . }}
{{ include "aralez.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- with .Values.podLabels }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "aralez.selectorLabels" -}}
app.kubernetes.io/name: {{ include "aralez.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Return a podAffinity/podAntiAffinity definition
*/}}
{{- define "affinities.pods" -}}
{{- if eq .Values.podAntiAffinityPreset "soft" }}
{{- include "affinities.pods.soft" . -}}
{{- else if eq .Values.podAntiAffinityPreset "hard" }}
{{- include "affinities.pods.hard" . -}}
{{- end -}}
{{- end -}}
{{/*
Return a soft podAntiAffinity definition
*/}}
{{- define "affinities.pods.soft" -}}
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels: {{- (include "aralez.selectorLabels" .) | nindent 12 }}
namespaces:
- {{ .Release.Namespace | quote }}
topologyKey: kubernetes.io/hostname
weight: 1
{{- end -}}
{{/*
Return a hard podAntiAffinity definition
*/}}
{{- define "affinities.pods.hard" -}}
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels: {{- (include "aralez.selectorLabels" .) | nindent 10 }}
namespaces:
- {{ .Release.Namespace | quote }}
topologyKey: kubernetes.io/hostname
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "aralez.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "aralez.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
+21
View File
@@ -0,0 +1,21 @@
{{- $ca := genCA .Chart.Name 3650 }}
{{- $server := genSignedCert "server" (list "127.0.0.1") nil 3650 $ca }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "aralez.fullname" . }}-tls
labels:
{{- include "aralez.labels" . | nindent 4 }}
type: kubernetes.io/tls
data:
{{- $secretName := printf "%s-tls" (include "aralez.fullname" .) }}
{{- $secretData := (lookup "v1" "Secret" .Release.Namespace $secretName).data }}
{{- if and $secretData (hasKey $secretData "ca.crt") }}
ca.crt: {{ get $secretData "ca.crt" }}
tls.crt: {{ get $secretData "tls.crt" }}
tls.key: {{ get $secretData "tls.key" }}
{{- else }}
ca.crt: {{ $ca.Cert | b64enc }}
tls.crt: {{ $server.Cert | b64enc }}
tls.key: {{ $server.Key | b64enc }}
{{- end }}
+28
View File
@@ -0,0 +1,28 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
data:
aralez.yaml: |
daemon: false
upstreams_conf: /etc/aralez/upstreams.yaml
proxy_configs: /etc/aralez-proxy
proxy_address_http: "0.0.0.0:{{ .Values.hostPort.ports.http }}"
proxy_address_tls: "0.0.0.0:{{ .Values.hostPort.ports.https }}"
config_api_enabled: false
config_address: "0.0.0.0:3000"
upgrade_sock: /tmp/aralez.sock
pid_file: /tmp/aralez.pid
log_level: debug
hc_method: HEAD
hc_interval: 5
upstreams.yaml: |
provider: "kubernetes"
sticky_sessions: 172000
kubernetes: {}
{{- with .Values.upstreams }}
{{- toYaml . | nindent 4 }}
{{- end }}
+130
View File
@@ -0,0 +1,130 @@
apiVersion: apps/v1
{{- if .Values.useDaemonSet }}
kind: DaemonSet
{{- else }}
kind: Deployment
{{- end }}
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
spec:
{{- if not (or .Values.autoscaling.enabled .Values.useDaemonSet) }}
replicas: {{ .Values.replicaCount }}
{{- end }}
{{- if not .Values.useDaemonSet }}
strategy:
type: {{ .Values.updateStrategy.type }}
{{- else }}
updateStrategy:
type: {{ .Values.updateStrategy.type }}
{{- end }}
selector:
matchLabels:
{{- include "aralez.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "aralez.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
{{- if .Values.upstreams }}
checksum/upstreams: {{ toJson .Values.upstreams | sha256sum }}
{{- end }}
spec:
enableServiceLinks: false
{{- if and .Values.hostNetwork .Values.useDaemonSet }}
hostNetwork: {{ .Values.hostNetwork }}
dnsPolicy: "ClusterFirstWithHostNet"
{{- end }}
{{- if .Values.priorityClassName }}
priorityClassName: {{ .Values.priorityClassName }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "aralez.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- if .Values.terminationGracePeriodSeconds }}
terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["aralez"]
args:
- --conf=/etc/aralez/aralez.yaml
{{- range $value := .Values.args }}
- {{ $value }}
{{- end }}
env:
{{- with .Values.env }}
{{- . | toYaml | nindent 12 }}
{{- end }}
ports:
{{- if .Values.hostPort.ports.http }}
- name: http
containerPort: {{ .Values.hostPort.ports.http }}
protocol: TCP
{{- end }}
{{- if .Values.hostPort.ports.https }}
- name: https
containerPort: {{ .Values.hostPort.ports.https }}
protocol: TCP
{{- end }}
# readinessProbe:
# httpGet:
# path: /status
# port: 3000
# initialDelaySeconds: 30
# timeoutSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config
mountPath: /etc/aralez
- name: tlscerts
mountPath: /etc/aralez-proxy/certificates
{{- with .Values.volumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: config
configMap:
name: {{ include "aralez.fullname" . }}
- name: tlscerts
secret:
secretName: {{ include "aralez.fullname" . }}-tls
defaultMode: 256
items:
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.key
{{- with .Values.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
affinity:
{{- include "affinities.pods" . | nindent 8 }}
{{- with .Values.affinity }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
+24
View File
@@ -0,0 +1,24 @@
{{- if and .Values.autoscaling.enabled (not .Values.useDaemonSet) }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "aralez.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- end }}
+10
View File
@@ -0,0 +1,10 @@
{{- if .Values.ingressClassResource.enabled -}}
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
labels:
{{- include "aralez.labels" . | nindent 4 }}
name: {{ .Values.ingressClass }}
spec:
controller: {{ .Values.ingressClassResource.controllerValue }}
{{- end }}
+41
View File
@@ -0,0 +1,41 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if eq .Values.service.type "NodePort" }}
externalTrafficPolicy: Local
{{- end }}
{{- with .Values.service.ipFamilies }}
ipFamilies:
{{- toYaml . | nindent 4 }}
{{- end }}
ipFamilyPolicy: {{ if eq (len .Values.service.ipFamilies) 2 }}PreferDualStack{{ else }}SingleStack{{ end }}
ports:
{{- if .Values.hostPort.ports.http }}
- port: 80
targetPort: http
protocol: TCP
name: http
{{- if .Values.hostPort.enabled }}
nodePort: {{ .Values.hostPort.ports.http }}
{{- end }}
{{- end }}
{{- if .Values.hostPort.ports.https }}
- port: 443
targetPort: https
protocol: TCP
name: https
{{- if .Values.hostPort.enabled }}
nodePort: {{ .Values.hostPort.ports.https }}
{{- end }}
{{- end }}
selector:
{{- include "aralez.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,52 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "aralez.serviceAccountName" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: true
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["get", "update", "patch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["nodes", "services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "aralez.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "aralez.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
+164
View File
@@ -0,0 +1,164 @@
# Default values for aralez.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: ghcr.io/sadoyan/aralez
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# -- Environment variables
env: []
# -- Aralez arguments
args: []
# -- Aralez upstreams file body
# refs: https://aralez.rs/docs/kubernetes/
upstreams:
rate_limit: 500000
x4xx_limit: 100000
to_https: true
server_headers:
- "X-Forwarded-Proto:https"
- "X-Forwarded-Port:443"
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# -- Annotations for pod.
# ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
podAnnotations: {}
# cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
# -- Extra labels for pod.
# ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
podLabels: {}
# -- Pod Security Context.
# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
podSecurityContext:
runAsNonRoot: true
runAsUser: 9999
runAsGroup: 9999
fsGroup: 9999
fsGroupChangePolicy: "OnRootMismatch"
# -- Container Security Context.
# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
securityContext:
seccompProfile:
type: RuntimeDefault
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
# -- Pod host network
# Use it only for performance reason
hostNetwork: false
# -- Service node port
# refs: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport
hostPort:
enabled: false
ports:
http: 8080
https: 8443
ingressClass: aralez
ingressClassResource:
enabled: true
controllerValue: aralez.proxy/ingress-controller
# -- Bitwarden service parameters
# ref: https://kubernetes.io/docs/user-guide/services/
service:
# -- service type
type: ClusterIP
annotations: {}
# -- IP families for service
# possible values: IPv4, IPv6
# ref: https://kubernetes.io/docs/concepts/services-networking/dual-stack/
ipFamilies: [IPv4]
# -- Resource requests and limits.
# ref: https://kubernetes.io/docs/user-guide/compute-resources/
resources:
limits:
cpu: 1
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
# -- Horizontal and Vertical pod autoscaler.
# ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80
# -- Use a daemonset instead of a deployment
useDaemonSet: false
# -- The time in seconds that is allowed for a Aralez instance to gracefully shutdown
# ref: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks
terminationGracePeriodSeconds: 120
## updateStrategy for PostgreSQL StatefulSet and its slaves StatefulSets
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
# -- Priority Class Name
# ref: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass
priorityClassName: ""
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
# -- Node labels for pod assignment.
# ref: https://kubernetes.io/docs/user-guide/node-selection/
nodeSelector: {}
# -- Tolerations for pod assignment.
# ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
tolerations: []
# -- Pod Anti Affinity
# soft/hard
podAntiAffinityPreset: soft
# -- Affinity for pod assignment.
# ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
affinity: {}
+66
View File
@@ -0,0 +1,66 @@
job "apache" {
meta {
uuid = uuidv4()
}
datacenters = ["eu-ams-1"]
type = "service"
constraint {
attribute = node.unique.name
operator = "regexp"
value = "consul[0-9]"
}
update {
max_parallel = 1
min_healthy_time = "10s"
healthy_deadline = "3m"
progress_deadline = "10m"
auto_revert = false
canary = 0
}
migrate {
max_parallel = 1
health_check = "checks"
min_healthy_time = "10s"
healthy_deadline = "5m"
}
group "Apache" {
network {
port "apache" { to = 80 }
}
constraint {
operator = "distinct_hosts"
value = "true"
}
count = 3
restart {
attempts = 2
interval = "30m"
delay = "15s"
mode = "fail"
}
task "health" {
service {
port = "apache"
tags = [
"aralez.service=yes",
"aralez.host=apache.example.com",
"aralez.path=/",
"aralez.redirect=http://checkip.amazonaws.com"
]
check {
type = "http"
port = "apache"
path = "/"
interval = "5s"
timeout = "2s"
}
}
driver = "docker"
config {
image = "httpd:latest"
force_pull = true
ports = ["apache"]
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
job "echo" {
meta {
uuid = uuidv4()
}
datacenters = ["eu-ams-1"]
type = "service"
constraint {
attribute = node.unique.name
operator = "regexp"
value = "consul[0-9]"
}
update {
max_parallel = 1
min_healthy_time = "10s"
healthy_deadline = "3m"
progress_deadline = "10m"
auto_revert = false
canary = 0
}
migrate {
max_parallel = 1
health_check = "checks"
min_healthy_time = "10s"
healthy_deadline = "5m"
}
group "Echo" {
network {
port "echo" { to = 80 }
}
constraint {
operator = "distinct_hosts"
value = "true"
}
count = 3
restart {
attempts = 2
interval = "30m"
delay = "15s"
mode = "fail"
}
task "server" {
service {
port = "echo"
tags = [
"aralez.service=yes",
"aralez.host=echo.example.com",
"aralez.path=/",
"aralez.rate=20",
"aralez.4xx_rate=10",
"aralez.to_https=true",
"aralez.client_header=X-Client-Header:Some simple header",
"aralez.server_header=X-Server-Header:Some simple header",
]
check {
type = "http"
port = "echo"
path = "/"
interval = "5s"
timeout = "2s"
}
}
driver = "docker"
config {
image = "ealen/echo-server:latest"
force_pull = true
ports = ["echo"]
}
}
}
}
+72
View File
@@ -0,0 +1,72 @@
job "nginx" {
meta {
uuid = uuidv4()
}
datacenters = ["eu-ams-1"]
type = "service"
constraint {
attribute = node.unique.name
operator = "regexp"
value = "consul[0-9]"
}
update {
max_parallel = 1
min_healthy_time = "10s"
healthy_deadline = "3m"
progress_deadline = "10m"
auto_revert = false
canary = 0
}
migrate {
max_parallel = 1
health_check = "checks"
min_healthy_time = "10s"
healthy_deadline = "5m"
}
group "NginX" {
network {
port "nginx" { to = 80 }
}
constraint {
operator = "distinct_hosts"
value = "true"
}
count = 3
restart {
attempts = 2
interval = "30m"
delay = "15s"
mode = "fail"
}
task "server" {
service {
port = "nginx"
tags = [
"aralez.service=yes",
"aralez.host=nginx.example.com",
"aralez.path=/",
"aralez.rate=1",
"aralez.4xx_rate=10",
"aralez.to_https=true",
"aralez.client_header=X-Some:Some Random Header",
"aralez.client_header=X-Emos:Redaeh Modnar Emos",
"aralez.server_header=X-From:Aralez & Consul",
"aralez.server_header=X-Mrof: Lusnoc & Zelara",
]
check {
type = "http"
port = "nginx"
path = "/"
interval = "5s"
timeout = "2s"
}
}
driver = "docker"
config {
image = "nginx:latest"
force_pull = true
ports = ["nginx"]
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
# 1. Define the IngressClass so Kubernetes knows "aralez" is a valid controller
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: aralez
namespace: upstreams
spec:
controller: aralez.proxy/ingress-controller
+76
View File
@@ -0,0 +1,76 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: aralez
namespace: aralez
labels:
app.kubernetes.io/name: aralez
app.kubernetes.io/component: ingress-controller
spec:
replicas: 1
selector:
matchLabels:
app: aralez
template:
metadata:
labels:
app: aralez
spec:
serviceAccountName: aralez-sa
containers:
- name: aralez
#image: sadoyan/aralez:latest
image: nginx:latest
imagePullPolicy: IfNotPresent
env:
- name: ARALEZ_SERVICE_NAME
value: "aralez-service"
- name: ARALEZ_SERVICE_NAMESPACE
value: "aralez"
- name: ARALEZ_EXTERNAL_IP # Only for bare metal deployment, remove this in Cloud.
value: "192.168.10.10" # Only for bare metal deployment, remove this in Cloud.
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
readinessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "1"
memory: "512Mi"
volumeMounts:
- name: main-config
mountPath: /etc/aralez/main.yaml
subPath: main.yaml
readOnly: true
- name: upstreams-config
mountPath: /etc/aralez/upstreams.yml
subPath: upstreams.yml
readOnly: true
- name: tls-certs
mountPath: /etc/aralez/certificates
readOnly: true
volumes:
- name: main-config
configMap:
name: aralez-main-config
- name: upstreams-config
configMap:
name: aralez-upstreams-config
- name: tls-certs
secret:
secretName: aralez-tls
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: aralez-service
namespace: aralez
spec:
type: NodePort
selector:
app: aralez
ports:
- name: http
port: 80
targetPort: 80
- name: https
port: 443
targetPort: 443
+9
View File
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: aralez-tls
namespace: aralez
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUY3ekNDQTllZ0F3SUJBZ0lVUm12YytSU25LS01HNXVaV1JNS3NtS2dwU1drd0RRWUpLb1pJaHZjTkFRRUwKQlFBd2dZWXhDekFKQmdOVkJBWVRBbGhZTVJJd0VBWURWUVFJREFsVGRHRjBaVTVoYldVeEVUQVBCZ05WQkFjTQpDRU5wZEhsT1lXMWxNUlF3RWdZRFZRUUtEQXREYjIxd1lXNTVUbUZ0WlRFYk1Ca0dBMVVFQ3d3U1EyOXRjR0Z1CmVWTmxZM1JwYjI1T1lXMWxNUjB3R3dZRFZRUUREQlJEYjIxdGIyNU9ZVzFsVDNKSWIzTjBibUZ0WlRBZUZ3MHkKTmpBNE1UZ3hOREkwTVRKYUZ3MHpOakE0TVRVeE5ESTBNVEphTUlHR01Rc3dDUVlEVlFRR0V3SllXREVTTUJBRwpBMVVFQ0F3SlUzUmhkR1ZPWVcxbE1SRXdEd1lEVlFRSERBaERhWFI1VG1GdFpURVVNQklHQTFVRUNnd0xRMjl0CmNHRnVlVTVoYldVeEd6QVpCZ05WQkFzTUVrTnZiWEJoYm5sVFpXTjBhVzl1VG1GdFpURWRNQnNHQTFVRUF3d1UKUTI5dGJXOXVUbUZ0WlU5eVNHOXpkRzVoYldVd2dnSWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUNEd0F3Z2dJSwpBb0lDQVFEQklZejBUNDRnVnZWdTBPMlA3NEV3NzE1c3FDekZuWHhTRDE4RnNCRHE2L1NadW1qYmpvTXBjWFZhCkVVQVQwMXpxRW9JZm5waHY3VExNWmtNNmZ6WTFNVGdOcXVqeHVXUGNVMTBES2VQa0Y1K1RPbWdoUUE1ODRCV1gKcmZrMlp5NXpGT012ODZUZXFCOGRyUVh2SWpHNTZhMW5DMFMwSFlnTnJ6R2srVXdrNWpDZTlaYURnc3Q5VlkrcApRYlZyUVdwR2V6RDl3WVE2bU41MkRGRERYbnp6WUVwRWVVekwwNkpNSlE5M2d5ZDVtbWd4aWlpbU1QMS9UTG5BClo2cWlYQ05UVGQvMERiL2tVWW9tK1hkTm11em83Qm1qaDY4ZzVTYUtYWkJXM25BQnhoaUVkZHoyaUViTGx2UFIKcXltcjVzazgyYlZQTWlWUC82alFLaTk0anBxVDc4ZVZZRG81dmhyMDQvczhVZk9xWVBEY0NLTGFzOEpBdTRvUwpoTnRjYm5GbGJjOG1Qcm00bnY2NlhKUUpIV1cwU1Rqc1pZWVRTRTllM3lMV2JqRlpUQTBIb3lQV1dDS1ZXSS82CnExa09IcnhJWitzOStHajB3aUJhdUpsbWk2by9oZE5qWUhsY2sxbnFTM2l6UXZ2WFpuSWtwTWkrU2pQRzhBZ0MKdEFqSWVPNlFOZVFFM3p3d3ZJb09tRnVTMC8zWkJFSkZuaWdxV0xuKzRidnV4eTNReW5JWHNsY250dDRXbjdMTAp0MWZtMTU3a2x6TUNHeHBWVXFqT1FwVnVuVnVmMDdURHdWbGxxek96TXlxZWdYSHlYODJxVlV2czdidCtpZFdOCngrdks1RUtrQUhidU9QV3lGRkJJci91aE93OWZsOEUxR1ZUYzg1OG5pM0NrWWtGeEpRSURBUUFCbzFNd1VUQWQKQmdOVkhRNEVGZ1FVQkFya1htNXZ2dDhmNVRGdkI0YmJNVS9EcEc4d0h3WURWUjBqQkJnd0ZvQVVCQXJrWG01dgp2dDhmNVRGdkI0YmJNVS9EcEc4d0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DCkFnRUFYdGJ3TVpkOElrTkxwWGFHeTRleGR6OVN0dVpjYnBhWjVWeURTdTJBdFZERm1BeXNSM0hHaEhWend5a0cKOGtibGdQQ3B3OEhWeGdMd0xmNHFuTWhvdUNHQ3ZiMVd2UjM5VW45NmVOWFA5cHFDTWhRTzFMU0JCSy80UllsUwp4ajhyRlBub3p5OHdFTzRwR1VaejN3LzJIdXgyTGovVHIxRXY5cklsa0hMZ3plazc1clpHTUIxUHhsRU1zUGNsCmtGY3NacGZ5TE5LM0FydUMzbmVGbldSODh5UHFVU3FwVEJ1OVRubjZUQ002TXVEZHlaRTZXYVlGU01zNzBJOU8KN3c5SkdreDc0UlAraTBQeUZUOFNhbUdncTUxWmtrUFZnK3ZZSUhTbGszdGlxZFN0MEYzMnVNaHNDZXZCcVUzQQp2M3lUOW9UUlpRZE9XKzQySDZjVTJ4Q2FCTVBRdnpaZWRtYno1eTNpS1NjMDBQUy8vMDI4YjIzbHpGQVZjbURHCnlOaUcraUJUVkRydHgwZC9qdGhqdzV4cllqNFBnNXl6emM2R3hkaC9VWGpUN1dqSUpweThnR3V2MHhMWVNQTDYKMmROUkYyVDl1TDQ0Z1J2K09ndTRRaHNacGg3WWxNZVp0aVpoT1IvTC8yUWdnZDMrNEhpeXBNNFpiWkUrMVg2TQoyNEtVdG5wZU53dE0wR1huU01aY2dkUm5LMWg0WDQ2T1lMYW9vSTdERkhSUWJSclJQSWl1eXZ2NXpPQzF5NTFFCm9rY0tvMVREQThRcXVHMlBRay9kMEZ0LzNiOEJoOHJCcTR1V3lKWHYyYTJJUmt1Q05MN0Myck5Rem92ekF0aW4KZnB5ejVKb1NHc1dUSVhSYlNucnpocWxFeUJCRGxQQ3AxTW1zM2xzZkFROThIQmM9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRUUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Nzd2dna25BZ0VBQW9JQ0FRREJJWXowVDQ0Z1Z2VnUKME8yUDc0RXc3MTVzcUN6Rm5YeFNEMThGc0JEcTYvU1p1bWpiam9NcGNYVmFFVUFUMDF6cUVvSWZucGh2N1RMTQpaa002ZnpZMU1UZ05xdWp4dVdQY1UxMERLZVBrRjUrVE9tZ2hRQTU4NEJXWHJmazJaeTV6Rk9Ndjg2VGVxQjhkCnJRWHZJakc1NmExbkMwUzBIWWdOcnpHaytVd2s1akNlOVphRGdzdDlWWStwUWJWclFXcEdlekQ5d1lRNm1ONTIKREZERFhuenpZRXBFZVV6TDA2Sk1KUTkzZ3lkNW1tZ3hpaWltTVAxL1RMbkFaNnFpWENOVFRkLzBEYi9rVVlvbQorWGRObXV6bzdCbWpoNjhnNVNhS1haQlczbkFCeGhpRWRkejJpRWJMbHZQUnF5bXI1c2s4MmJWUE1pVlAvNmpRCktpOTRqcHFUNzhlVllEbzV2aHIwNC9zOFVmT3FZUERjQ0tMYXM4SkF1NG9TaE50Y2JuRmxiYzhtUHJtNG52NjYKWEpRSkhXVzBTVGpzWllZVFNFOWUzeUxXYmpGWlRBMEhveVBXV0NLVldJLzZxMWtPSHJ4SVorczkrR2owd2lCYQp1SmxtaTZvL2hkTmpZSGxjazFucVMzaXpRdnZYWm5Ja3BNaStTalBHOEFnQ3RBakllTzZRTmVRRTN6d3d2SW9PCm1GdVMwLzNaQkVKRm5pZ3FXTG4rNGJ2dXh5M1F5bklYc2xjbnR0NFduN0xMdDFmbTE1N2tsek1DR3hwVlVxak8KUXBWdW5WdWYwN1REd1ZsbHF6T3pNeXFlZ1hIeVg4MnFWVXZzN2J0K2lkV054K3ZLNUVLa0FIYnVPUFd5RkZCSQpyL3VoT3c5Zmw4RTFHVlRjODU4bmkzQ2tZa0Z4SlFJREFRQUJBb0lDQUNjaHhDeXdmZXpCa2VOTlJBVlpFd1UyClN4eWJDczlUdEovdjYySDhwY0RqNUdETnEzZHBMZHhVMStGVEEzZkE2TitvdG1MdndKVlZLMmpOQTZ3em9Ha3cKdXRuU0duYjRDR3JaSFV5SnpWb25DemRLcTFTcFpRWWFaV1doWTVDSllaa0J6M0trM0hBZEFva0J5T1VHT2FzZwoxUGkvQ01HeUJpSTNzRWhzRnVpWEY1enR4L3lIb1BHWlI0MlUrY3hzcWJyME5SNEZldVJnbXhxeUxoOFg0eHR4CkZEdEJsTFRzQWNPSjh0TlZhTWN6NlE2NG5ybmx6VWRnNVhZOXl3NHd1eHVpK05DN2hIa2ZxRzF6T0tBcDF2enUKOGRiM3BCMTNNTkx5V0UreHJHSS9vREllcTVIc1ZPUGxoeXVoa2NUNTh5WjRPa3Q4cnFEQi9aT0lLS3cxd000RQp1S1h3VnhmMEZzZ09pSjhCME1iY1g4OTdEdUJrV29DeHorbVlwVzByTW9EWTlhcEI3ZllZZkN2ZVJXVWt3RmJnCkdhWDZPRndwWkU5cHBOOXR4YUpKYVgrdXowT3ZySWh4c1FPamtSdTA1cXl5NjcvTG04aFpoRkVMMm1PMjBOUGMKVWpVUHpoenF1M3NHaHltR01Kb21aa3VQR1FyYnlYR2lSVjl2dWhWSHlVa3EyWXJ5ZTJpeEpUMWgreDFBZXJHbgo2ZzQxWFA5UzVTNDUzTU8wUVFDcVQ2aG9ZUnBpbUxkYlpmZjJOTmt1ZFFINS9UVjZqTGN0SjZua212ZDVTbGhPCm9YSHdkcW9jVlkrUEVZZHRyVzE5OGxhVG9JVFZQZFBBOHdwL0pmb2pGK0hSM1owQmlBMUFsWjhzL0RsM0xLWVoKb2xHUUVnQ3kwQ2xVazFZa0FiYzFBb0lCQVFEZnNiZWlwOTNrNitjSGJHdTk2eE53b1RHMEdWVnkzWDBzeVhWSAppSERvdnpTNGtzY0c0SEpteHc2S2N0ZDR5eEIvaDRuZHZ2VkV0M3RqTFQ3VGxqOTlhNWJVaTZOS3ZyNll0ZGsxCk50elFTaXJjeG81M3dNUlRSeFVNZnVPQW1Ia3VKWXduZWVFMHcxdnRBZloyR0RnVTBGbGZicThkRFc2TlFZQ0gKS01wNUtWNnJyenE2Y1g1cGNWcWRZckp6Y2REbk5YeXUyUXBDT3FSZFhFQ3RnYmxWdmF6UTh2c0t0K3Awd1NpcgpCdmFqY3c1YW1iZ2x4Zm1uWWhNY2ZkWjRqR3NrVVJmWE5JMGZUcHRwMEdNVlFYZEFkL3BScThjKzljQU1LY3p6Ck9OL0NzVjVpbHpjUDBJZzB0bU50a0tVV3dKTGRKbkp1Z1Vud200SFFtUzJWNUUwL0FvSUJBUURkQmQ4Y0NUbUgKYmJUbmVjcHJnNkd4ZUN3aW95dnVkZVJZS2NCcGdvcW5uUVdEMm9LQmVDWC9HL1lJNVIwcWxLdXZBRlEvWEd3ZAp3N1pwYlZsbUcvQTBBRjNQeW5MMlFHSWhDN1Z6cHNrSFRPOU5VOTJjS0Fhd2Erbk9nZVJSbzBhOEhucGJpcktmCjlEdzkvcjhxTWVmdzhRS3JmMDVwMEVjVVJlTTR2WlJ1cGs5bVZac0x0QmxIMjhEYXE4YnJBYlNXUjNKN1RsZEkKMHZaZE9Cd0xxWkZVTWVOR09rUjN6azAvWDNXOWVIT3RmTkZYQzU3N1Bvby9vTDNobERDcnVXalZodkM0dUttdQpLTU5CMTM2ZnFuanZ6QXcrVTdMbkhHYTlQSHp5aUhFN1B3TVNiSGlBcXpFNXdBaDlLOWJGdEttUE0wN1FudTNSClBWMVlDNk1tMGxTYkFvSUJBQkFRRU9Hck1ESGFCckx2eENUVkZvWkY3TzNEeS9EOW1wbDJ0ZWMzRXZTRGxtT1MKbHB3VnplRTNkZ0hYRVVYcFB2d0pYWXNBeFZmejNQWllMMlMrS3p6MFZ0WllXSmRyYndCRVBwNmxtK0dSRWRiYwpveXBTTzBiSG1lbFZ5enhXdG96cndVNnNYOUdNSGJPOUtsZDhrYnBjNDY0SW9xWnhhbmpXTlVaL1ZWcXg3OHhnClB0VlloUDFKV0hwb3RldTRSb256dTBwSzU0T3Q5bE9GNWxjUE02SEdodnhTRmxqRXNCOUdKNGR3YnBGNkY1UEYKNmdVOFhCcjYzZWh6Mk15N09JNVpERjgxdE5TRXJvQkFxSHpFUUszdWI1a2h2cUwwdnArZytNaVdVdXNHRllMNgozTWRUOEJQbzdPYlhOaU9XSEhXbzV4cXNTZ1R2QmJsVzVvRVc3YjBDZ2dFQWZFOWRHT3VuUzZCTWF4UjQ0TmQ4CkRkREZJZ1ljSTFaelE1TFlBWC9HZEZkQVR0U0xLajVsUUlKOWNxdEtLRFRPQ3ZOb2J1cVFkSDByRDBqTlN6elQKY0t2MEYzYUtDaTVVQXJvUEtDamFVQnpaU3Vpc2I3T2piMzZDVmYrQVp0b3FZb3ZSemNMSWpHWlZxNWluOUJkMQptSEkxU2plajVxZWFFckRzQjNHNUZqSzczWmF3U1hqVjd6Q04yMjlaeGJ1RDBqSUZGWTBPVW9zMHZXRmRoTUd1ClYyaG5wNHdHRGt3ZDdsTEdhZlpoT1daR1oyckYxQkZzdURTMEZ6ZTkyQmZEK3JTV2w0NWxBT3MwUnNmcEUreFIKY0syWEJVY1dEQjVST29YVzBLSWs3V21YVkxsbFhSanI2c3N5K3hPNURDRjhiR296SGVzRlh2WGdsakNWWnJSTgptUUtDQVFCQ044M21FMllMVWtlUTUrWVVoS1kyYktQcS9GVnA2NWNPVXZ2R2RCNGxQUTlDU01xWUc0SHR5VFJUCmdzaEdvdjRrYk5NQ2JsdnlkWkttU1RKZk16bnJhdlYrbDVJekRFZWpEUXQrUzMxdWh5OXdiMVlIaFBRNStXZ2sKbnVMcUhzWE9WKzZuWmdyMzJvUVFxeVdlTjFkUW1XTUVoYWVOVVlwamxpTFNlUm55UjVNazBCbm55clJEZGV2NQpLekpyZmdYZG5CSGdJY3M4VmxoWXZoYlFDQmpNT2FIcXp1dXJ3M21VSVJxNlNWWVR5eHprZElEM2tna0VxbDdrCnEwL3RBbkhrNHZIS1JhSU1nSmdGOVVBRG50MHVpTFdVcTZBbCsyRU5sQW16VGhmQlAxOUVPSlFuNC85L0tnOXEKTXV3ZGxLSTl5MWRpQlBmdk00R3RIcGlYaEZnVQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==
+73
View File
@@ -0,0 +1,73 @@
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-service-v2
namespace: upstreams
annotations:
# Custom annotations parsed by Aralez
aralez.rs/rate_limit: "100"
aralez.rs/x4xx_limit: "20"
aralez.rs/client_headers: '["X-Proxy-From:Aralezzzzzzzzzzz", "X-Hopar-From:Hopaaaaaaaaaaaar"]'
aralez.rs/server_headers: '["X-Proxy-Version:Aralez", "X-Po-From:Kung Fu Panda"]'
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: api-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service-v2
port:
number: 80
---
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wss-service-v2
namespace: upstreams
annotations:
# Custom annotations parsed by Aralez
aralez.rs/rate_limit: "50"
aralez.rs/x4xx_limit: "10"
aralez.rs/client_headers: '["X-Hors-Arev:Naturi", "X-Grand-Master:Oogway"]'
aralez.rs/server_headers: '["X-Mors-Arev:Hastat", "X-Shifu-Master:Shifu"]'
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: wss-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: svc-nginx
port:
number: 80
---
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: joj-service-v2
namespace: upstreams
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: joj-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: svc-nginx
port:
number: 80
+16
View File
@@ -0,0 +1,16 @@
threads: 12
daemon: false
upstream_keepalive_pool_size: 500
pid_file: /tmp/aralez.pid
error_log: /tmp/aralez_err.log
upgrade_sock: /tmp/aralez.sock
config_api_enabled: false
config_address: 127.0.0.1:3000
proxy_address_http: 0.0.0.0:80
proxy_address_tls: 0.0.0.0:443
proxy_configs: /etc/aralez
proxy_tls_grade: high
upstreams_conf: /etc/aralez/upstreams.yml
log_level: info
hc_method: HEAD
hc_interval: 2
+22
View File
@@ -0,0 +1,22 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: svc-nginx
namespace: upstreams
labels:
app: svc-nginx
spec:
selector:
matchLabels:
app: svc-nginx
replicas: 3
template:
metadata:
labels:
app: svc-nginx
spec:
containers:
- name: svc-nginx
image: nginx:latest
ports:
- containerPort: 80
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: svc-nginx
namespace: upstreams
spec:
selector:
app: svc-nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: api-service-v2
namespace: upstreams
spec:
selector:
app: svc-nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
+40
View File
@@ -0,0 +1,40 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: aralez-sa
namespace: aralez
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: aralez-role
rules:
# Watch Ingresses and IngressClasses across all namespaces
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
# Update Ingress status (for setting load balancer IPs)
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["update", "patch"]
# Watch Services and Endpoints across all namespaces
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch"]
# Watch EndpointSlices across all namespaces
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: aralez-ingress
subjects:
- kind: ServiceAccount
name: aralez-sa
namespace: aralez # <--- MUST match where Aralez is deployed
roleRef:
kind: ClusterRole
name: aralez-role
apiGroup: rbac.authorization.k8s.io
+16
View File
@@ -0,0 +1,16 @@
provider: "kubernetes"
to_https: false
sticky_sessions: 86000
rate_limit: 100
x4xx_limit: 100
headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
kubernetes:
services:
- hostname: "svc-nginx"
upstream: "svc-nginx"
path: "/"
tokenpath: "/var/run/secrets/kubernetes.io/serviceaccount/token"
+18 -16
View File
@@ -1,25 +1,27 @@
# Main configuration file, applied on startup # Main configuration file, applied on startup
threads: 12 # Number of daemon threads default setting threads: 12 # Number of daemon threads default setting
#runuser: pastor # Username for running aralez after dropping root privileges, requires program to start as root runuser: aralez # Username for running aralez after dropping root privileges, requires program to start as root
#rungroup: pastor # Group for running aralez after dropping root privileges, requires program to start as root rungroup: aralez # Group for running aralez after dropping root privileges, requires program to start as root
daemon: false # Run in background
upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections
pid_file: /tmp/aralez.pid # Path to PID file pid_file: /tmp/aralez.pid # Path to PID file
error_log: /tmp/aralez_err.log # Path to error log
upgrade_sock: /tmp/aralez.sock # Path to socket file upgrade_sock: /tmp/aralez.sock # Path to socket file
config_api_enabled: true # Boolean to enable/disable remote config push capability. config_api_enabled: true # Boolean to enable/disable remote config push capability.
config_address: 0.0.0.0:3000 # HTTP API address for pushing upstreams.yaml from remote location config_address: 127.0.0.1:3000 # HTTP API address for pushing upstreams.yaml from remote location
config_tls_address: 0.0.0.0:3001 # HTTP TLS API address for pushing upstreams.yaml from remote location proxy_address_http: 0.0.0.0:80 # Proxy HTTP bind address
config_tls_certificate: /etc/server.crt # Mandatory if config_tls_address is set proxy_address_tls: 0.0.0.0:443 # Optional, Proxy TLS bind address
config_tls_key_file: /etc/key.pem # Mandatory if config_tls_address is set proxy_configs: /opt/aralez/asyncweb/etc # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key.
proxy_address_http: 0.0.0.0:6193 # Proxy HTTP bind address proxy_tls_grade: high # Grade of TLS suite for proxy (high, medium, unsafe), matching grades of Qualys SSL Labs
proxy_address_tls: 0.0.0.0:6194 # Optional, Proxy TLS bind address upstreams_conf: /opt/aralez/etc/upstreams.yaml # the location of upstreams file
proxy_certificates: /etc/certs # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key. file_server_folder: /opt/aralez/public # Optional, local folder to serve
proxy_tls_grade: a+ # Grade of TLS suite for proxy (a+, a, b, c, unsafe), matching grades of Qualys SSL Labs file_server_address: 0.0.0.0:3002 # Optional, Local address for file server. Can set as upstream for public access.
upstreams_conf: /etc/upstreams.yaml # the location of upstreams file
file_server_folder: /opt/storage # Optional, local folder to serve
file_server_address: 127.0.0.1:3002 # Optional, Local address for file server. Can set as upstream for public access.
log_level: info # info, warn, error, debug, trace, off log_level: info # info, warn, error, debug, trace, off
log_file: /tmp/aralez.log, 20, 5, compress # Optional, the location of log file, file size in megabytes, number of log files to keep, compress files .valueas are separated by comma.
access_log: error # all, error, (Off if commented)
hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE hc_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 tcp_keepalive_idle: 60 # Seconds of inactivity before the kernel starts sending keepalive probes to a downstream client
tcp_keepalive_interval: 10 # Seconds between individual keepalive probes if the client does not respond
tcp_keepalive_count: 5 # Number of unanswered probes before the kernel declares the connection dead and closes it
cache_size_mb: 50 # Optional. Enable internal cache with size in megabytes of value. Cache is entirely disabled when this line is commented out
cache_ttl: 120 # Optional. Set Default TTL in seconds for cached items, works if cache control headers are not set via upstream. Defaults to 60 if cache_size_mb is set
-19
View File
@@ -1,19 +0,0 @@
provider: "file"
globals:
headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "X-Custom-Header:Something Special"
upstreams:
myip.netangels.net:
paths:
"/":
ssl: false
headers:
- "X-Proxy-From:Aralez"
servers:
- "192.168.221.213:8000"
- "192.168.221.214:8000"
- "192.168.221.210:8000"
- "192.168.221.212:8000"
+84 -62
View File
@@ -1,115 +1,137 @@
# 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" # "file" "consul" "kubernetes" provider: "file" # "file" "consul" "kubernetes"
sticky_sessions: false sticky_sessions: 172000
to_https: false to_https: false
rate_limit: 100 rate_limit: 500000
x4xx_limit: 100000
server_headers: server_headers:
- "X-Forwarded-Proto:https" - "Y-Global-Something: Something For Servers"
- "X-Forwarded-Port:443"
client_headers: client_headers:
- "Access-Control-Allow-Origin:*" - "X-Global-Something: Something For Clients"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
#authorization: #authorization:
# type: "jwt"
# creds: "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
# type: "basic" # type: "basic"
# creds: "username:Pa$$w0rd" # data: "root:toor"
# type: "jwt"
# data: "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
# type: "apikey" # type: "apikey"
# creds: "5ecbf799-1343-4e94-a9b5-e278af5cd313-56b45249-1839-4008-a450-a60dc76d2bae" # data: "5ecbf799-1343-4e94-a9b5-e278af5cd313-56b45249-1839-4008-a450-a60dc76d2bae"
consul: consul:
servers: servers:
- "http://192.168.1.199:8500" - "http://consul1:8500"
- "http://192.168.1.200:8500"
- "http://192.168.1.201:8500"
services: # hostname: The hostname to access the proxy server, upstream : The real service name in Consul database. services: # hostname: The hostname to access the proxy server, upstream : The real service name in Consul database.
- hostname: "webapi-service" - hostname: "nconsul"
upstream: "webapi-service-health" upstream: "nginx-consul-NginX-health"
path: "/one" path: "/one"
client_headers: client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa" - "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez" - "X-Proxy-From:Aralez"
rate_limit: 1 rate_limit: 1
to_https: false to_https: false
- hostname: "webapi-service" - hostname: "nconsul"
upstream: "webapi-service-health" upstream: "nginx-consul-NginX-health"
path: "/" 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: kubernetes:
servers: servers:
- "192.168.1.55:443" #For testing only, overrides with KUBERNETES_SERVICE_HOST : KUBERNETES_SERVICE_PORT_HTTPS env variables. - "172.16.0.11:5443" # Gets KUBERNETES_SERVICE_HOST : KUBERNETES_SERVICE_PORT_HTTPS env variables.
services: services:
- hostname: "webapi-service" - hostname: "api-service"
upstream: "api-service"
path: "/" path: "/"
upstream: "webapi-service" - hostname: "api-service"
- hostname: "webapi-service" upstream: "console-service"
upstream: "vt-console-service"
path: "/one" path: "/one"
client_headers: client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa" - "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez" - "X-Proxy-From:Aralez"
rate_limit: 100 rate_limit: 100
to_https: false to_https: false
- hostname: "webapi-service" - hostname: "api-service"
upstream: "vt-rambulik-service" upstream: "feed-fanout-service"
path: "/two" path: "/two"
- hostname: "websocket-service" - hostname: "websocket-service"
upstream: "websocket-service" upstream: "websocket-service"
path: "/" path: "/"
tokenpath: "/path/to/kubetoken.txt" #If not set, will default to /var/run/secrets/kubernetes.io/serviceaccount/token tokenpath: "/opt/Rust/Projects/asyncweb/etc/kubetoken.txt" # Defaults to /var/run/secrets/kubernetes.io/serviceaccount/token
upstreams: upstreams:
myip.mydomain.com: www.example.com:
paths: paths:
"/": "/":
rate_limit: 200 rate_limit: 50
to_https: false x4xx_limit: 100
client_headers:
- "X-Proxy-From:Aralez"
servers:
- "127.0.0.1:8000"
- "127.0.0.2:8000"
- "127.0.0.3:8000"
- "127.0.0.4:8000"
- "127.0.0.5:8000"
"/ping":
to_https: false to_https: false
authorization:
type: "basic"
data: "root:toor"
server_headers: server_headers:
- "X-Forwarded-Proto:https" - "Y-Proxy-Server-Some:Yaaaaaaaaaaaaaaa"
- "X-Forwarded-Port:443" - "Y-Proxy-Server-From:Aralez"
- "Y-Proxy-Server-Vers:Aralez v-xxx"
client_headers: client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa" - "Access-Control-Allow-Origin:*"
- "X-Proxy-From:Aralez" - "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
servers: servers:
- "127.0.0.1:8000" - "127.0.0.1:8000"
- "127.0.0.2:8000" - "127.0.0.2:8000"
"/draw":
servers:
- "192.168.1.1:8000"
polo.mydomain.com:
paths:
"/":
to_https: false
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
servers:
- "192.168.1.1:8000"
- "192.168.1.10:8000"
- "127.0.0.1:8000"
- "127.0.0.2:8000"
- "127.0.0.3:8000" - "127.0.0.3:8000"
- "127.0.0.4:8000" "/ping":
apt.mydomain.com: to_https: true
client_headers:
- "X-Some-Thing:Something Else"
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
servers:
- "127.0.0.1:8000"
"/secret":
authorization:
type: "forward"
data: "http://127.0.0.1:8899/admin/login"
servers:
- "127.0.0.10:8000"
example.com:
paths: paths:
"/": "/":
redirect_to: "https://www.example.com:443"
servers: servers:
- "192.168.1.10:443" - "127.0.0.1:80"
h2.example.com:
paths:
"/":
server_headers:
- "Y-Global-Something: Yes this is something"
client_headers:
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
rate_limit: 60
x4xx_limit: 30
authorization:
type: "jwt"
servers:
- "127.0.0.1:8000"
- "127.0.0.2:8000"
"/.well-known/acme-challenge": "/.well-known/acme-challenge":
healthcheck: false healthcheck: false
servers: servers:
- "127.0.0.1:8001" - "127.0.0.1:8001"
localpost: "/400":
rate_limit: 4
x4xx_limit: 2
servers:
- "127.0.0.1:8899"
"/500":
healthcheck: false
servers:
- "127.0.0.1:8899"
DEFAULT:
paths: paths:
"/": "/":
to_https: false healthcheck: false
servers: servers:
- "127.0.0.1:9000" - "127.0.0.1:3000"
"/.well-known/acme-challenge":
healthcheck: false
servers:
- "127.0.0.1:3000"
+7
View File
@@ -1,9 +1,16 @@
mod tls;
mod utils; mod utils;
mod web; mod web;
#[global_allocator] #[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
// pub static A: CountingAllocator = CountingAllocator;
fn main() { fn main() {
if std::env::args().any(|a| a == "--version" || a == "-v") {
println!("aralez {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
web::start::run(); web::start::run();
} }
+3
View File
@@ -0,0 +1,3 @@
pub mod acme;
pub mod grades;
pub mod load;
+2
View File
@@ -0,0 +1,2 @@
pub mod account;
pub mod order;
+58
View File
@@ -0,0 +1,58 @@
use instant_acme::{Account, AccountCredentials, LetsEncrypt, NewAccount};
use log::info;
use std::fs;
use std::path::Path;
use std::sync::OnceLock;
static ACCOUNT: OnceLock<Account> = OnceLock::new();
pub async fn get_account(file: &str) -> Result<&'static Account, Box<dyn std::error::Error>> {
if let Some(account) = ACCOUNT.get() {
return Ok(account);
}
if let Some(credentials) = load_credentials(file) {
let acc_builder = Account::builder()?;
let account = acc_builder.from_credentials(credentials).await?;
let _ = ACCOUNT.set(account);
info!("Loaded existing ACME account");
} else {
info!("No existing credentials found, creating new account");
create_account(file).await?;
}
ACCOUNT.get().ok_or("Failed to initialize account".into())
}
async fn create_account(file: &str) -> Result<(), Box<dyn std::error::Error>> {
let new_account = NewAccount {
contact: &[],
terms_of_service_agreed: true,
only_return_existing: false,
};
let acc_builder = Account::builder()?;
let (account, credentials) = acc_builder.create(&new_account, LetsEncrypt::Production.url().to_string(), None).await?;
// let (account, credentials) = acc_builder.create(&new_account, LetsEncrypt::Staging.url().to_string(), None).await?;
info!("Account created: {:?}", account.id());
save_credentials(&credentials, file)?;
let _ = ACCOUNT.set(account);
Ok(())
}
pub async fn load_or_create(file: &str) -> Result<String, Box<dyn std::error::Error>> {
let account = get_account(file).await?;
Ok(account.id().to_string() + "\n")
}
fn save_credentials(credentials: &AccountCredentials, file: &str) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(credentials)?;
fs::write(file, json)?;
info!("ACME credentials saved to {}", file);
Ok(())
}
fn load_credentials(file: &str) -> Option<AccountCredentials> {
if !Path::new(file).exists() {
return None;
}
let json = fs::read_to_string(file).ok()?;
serde_json::from_str(&json).ok()
}
+88
View File
@@ -0,0 +1,88 @@
use crate::tls::acme::account::get_account;
use crate::utils::lazylock::CHALLENGES;
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::fs;
use std::time::Duration;
use x509_parser::prelude::*;
pub async fn refresh_order(certs_dir: String, autoconf_dir: String) {
let credsfile = autoconf_dir + "/acme_credentials.json";
loop {
for item in DOMAINS.iter() {
let _what = order(item.key(), credsfile.as_str(), certs_dir.clone()).await;
}
sleep(Duration::from_secs(12 * 3600)).await;
}
}
pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<String, Box<dyn std::error::Error>> {
let crt = certs_dir.clone() + "/" + domain + ".crt";
let key = certs_dir.clone() + "/" + domain + ".key";
if DOMAINS.get(domain).is_none() {
DOMAINS.insert(domain.to_string(), true);
let mut newlist: Vec<String> = Vec::new();
for item in DOMAINS.iter() {
newlist.push(item.key().to_string());
}
if let Ok(json_content) = serde_json::to_string_pretty(&newlist) {
let autocfg_file = credsfile.replace("/acme_credentials.json", "/domains.json");
if let Err(err) = std::fs::write(&autocfg_file, json_content) {
error!("Error Updating domains for certificates: {} : {}", domain, err);
return Err(Box::from(err));
}
}
}
if let Ok(expiry) = cert_expiry(crt.as_str()) {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
if expiry > now + 30 * 24 * 3600 {
// println!("Fresh certificate exists. Not renewing !");
return Ok("Fresh certificate exists. Not renewing ! \n".to_string());
}
};
let account = get_account(credsfile).await?;
let mut order = account.new_order(&NewOrder::new(&[Identifier::Dns(domain.to_string())])).await?;
let mut authorizations = order.authorizations();
while let Some(auth) = authorizations.next().await {
let mut auth = auth?;
let mut challenge_handle = auth.challenge(ChallengeType::Http01).ok_or("no http01 challenge found")?;
let key_auth = challenge_handle.key_authorization();
let key_auth_str = key_auth.as_str().to_string();
let token = key_auth_str.split('.').next().ok_or("invalid key authorization")?.to_string();
CHALLENGES.write().unwrap().insert(token, key_auth_str);
challenge_handle.set_ready().await?;
}
let status = order.poll_ready(&RetryPolicy::default()).await?;
info!("ACME poll_ready status: {:?}", status);
let mut params = CertificateParams::new(vec![domain.to_owned()])?;
params.distinguished_name = DistinguishedName::new();
let private_key = KeyPair::generate()?;
let signing_request = params.serialize_request(&private_key)?;
let csr_der = signing_request.der();
order.finalize_csr(csr_der).await?;
// poll for certificate
let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?;
CHALLENGES.write().unwrap().clear();
let private_key_pem = private_key.serialize_pem();
fs::write(crt, cert_chain_pem)?;
fs::write(key, private_key_pem)?;
Ok("Certificate is successfully generated \n".to_string())
}
fn cert_expiry(path: &str) -> Result<u64, Box<dyn std::error::Error>> {
let pem = fs::read(path)?;
let (_, pem) = parse_x509_pem(&pem)?;
let (_, cert) = parse_x509_certificate(&pem.contents)?;
let expiry = cert.validity().not_after.timestamp() as u64;
Ok(expiry)
}
+75
View File
@@ -0,0 +1,75 @@
use log::{info, warn};
use pingora::tls::ssl::{select_next_proto, AlpnError, SslRef, SslVersion};
use pingora_core::listeners::tls::TlsSettings;
#[derive(Debug)]
pub struct CipherSuite {
pub high: &'static str,
pub medium: &'static str,
pub legacy: &'static str,
}
const CIPHERS: CipherSuite = CipherSuite {
high: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305",
medium: "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:AES128-GCM-SHA256",
legacy: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
};
#[derive(Debug)]
pub enum TlsGrade {
High,
Medium,
Legacy,
}
impl TlsGrade {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::High),
"medium" => Some(TlsGrade::Medium),
"unsafe" => Some(TlsGrade::Legacy),
_ => None,
}
}
}
pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> {
match select_next_proto("\x02h2\x08http/1.1".as_bytes(), alpn_in) {
Some(p) => Ok(p),
_ => Err(AlpnError::NOACK),
}
}
pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
let config_grade = TlsGrade::from_str(grade);
match config_grade {
Some(TlsGrade::High) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2));
// let _ = tls_settings.set_max_proto_version(Some(SslVersion::TLS1_3));
let _ = tls_settings.set_cipher_list(CIPHERS.high);
// let _ = tls_settings.set_ciphersuites(CIPHERS.high);
let _ = tls_settings.set_cipher_list(CIPHERS.high);
info!("TLS grade: {:?}, => High", tls_settings.options());
}
Some(TlsGrade::Medium) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
info!("TLS grade: {:?}, => Medium", tls_settings.options());
}
Some(TlsGrade::Legacy) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::SSL3));
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
// let _ = tls_settings.set_ciphersuites(CIPHERS.legacy);
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
warn!("TLS grade: {:?}, => UNSAFE", tls_settings.options());
}
None => {
// Defaults to Medium
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
warn!("TLS grade is not detected defaulting top Medium");
}
}
}
+13 -82
View File
@@ -1,7 +1,7 @@
use crate::tls::grades;
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info, warn}; use log::error;
use pingora::tls::ssl::{select_next_proto, AlpnError, NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef, SslVersion}; use pingora::tls::ssl::{NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef};
use pingora_core::listeners::tls::TlsSettings;
use rustls_pemfile::{read_one, Item}; use rustls_pemfile::{read_one, Item};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashSet; use std::collections::HashSet;
@@ -10,7 +10,6 @@ use std::io::BufReader;
use x509_parser::extensions::GeneralName; use x509_parser::extensions::GeneralName;
use x509_parser::nom::Err as NomErr; use x509_parser::nom::Err as NomErr;
use x509_parser::prelude::*; use x509_parser::prelude::*;
#[derive(Clone, Deserialize, Debug)] #[derive(Clone, Deserialize, Debug)]
pub struct CertificateConfig { pub struct CertificateConfig {
pub cert_path: String, pub cert_path: String,
@@ -18,14 +17,14 @@ pub struct CertificateConfig {
} }
#[derive(Debug)] #[derive(Debug)]
struct CertificateInfo { pub struct CertificateInfo {
common_names: Vec<String>, pub common_names: Vec<String>,
alt_names: Vec<String>, pub alt_names: Vec<String>,
ssl_context: SslContext, pub ssl_context: SslContext,
#[allow(dead_code)] #[allow(dead_code)]
cert_path: String, // Only used for logging pub cert_path: String, // Only used for logging
#[allow(dead_code)] #[allow(dead_code)]
key_path: String, // Only used for logging pub key_path: String, // Only used for logging
} }
#[derive(Debug)] #[derive(Debug)]
@@ -61,7 +60,7 @@ impl Certificates {
} }
} }
Some(Self { Some(Self {
name_map: name_map, name_map,
configs: cert_infos, configs: cert_infos,
default_cert_path: default_cert.cert_path.clone(), default_cert_path: default_cert.cert_path.clone(),
default_key_path: default_cert.key_path.clone(), default_key_path: default_cert.key_path.clone(),
@@ -94,7 +93,7 @@ impl Certificates {
if let Some(name) = server_name { if let Some(name) = server_name {
match self.find_ssl_context(name) { match self.find_ssl_context(name) {
Some(ctx) => { Some(ctx) => {
ssl_ref.set_ssl_context(&*ctx).map_err(|_| SniError::ALERT_FATAL)?; ssl_ref.set_ssl_context(&ctx).map_err(|_| SniError::ALERT_FATAL)?;
} }
None => { None => {
log::debug!("No matching server name found"); log::debug!("No matching server name found");
@@ -106,7 +105,7 @@ impl Certificates {
} }
} }
fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option<CertificateInfo> { pub fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option<CertificateInfo> {
let mut common_names = HashSet::new(); let mut common_names = HashSet::new();
let mut alt_names = HashSet::new(); let mut alt_names = HashSet::new();
@@ -180,75 +179,7 @@ fn create_ssl_context(cert_path: &str, key_path: &str) -> Result<SslContext, Box
let mut ctx = SslContext::builder(SslMethod::tls())?; let mut ctx = SslContext::builder(SslMethod::tls())?;
ctx.set_certificate_chain_file(cert_path)?; ctx.set_certificate_chain_file(cert_path)?;
ctx.set_private_key_file(key_path, SslFiletype::PEM)?; ctx.set_private_key_file(key_path, SslFiletype::PEM)?;
ctx.set_alpn_select_callback(prefer_h2); ctx.set_alpn_select_callback(grades::prefer_h2);
let built = ctx.build(); let built = ctx.build();
Ok(built) Ok(built)
} }
#[derive(Debug)]
pub struct CipherSuite {
pub high: &'static str,
pub medium: &'static str,
pub legacy: &'static str,
}
const CIPHERS: CipherSuite = CipherSuite {
high: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305",
medium: "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:AES128-GCM-SHA256",
legacy: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
};
#[derive(Debug)]
pub enum TlsGrade {
HIGH,
MEDIUM,
LEGACY,
}
impl TlsGrade {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::HIGH),
"medium" => Some(TlsGrade::MEDIUM),
"unsafe" => Some(TlsGrade::LEGACY),
_ => None,
}
}
}
pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> {
match select_next_proto("\x02h2\x08http/1.1".as_bytes(), alpn_in) {
Some(p) => Ok(p),
_ => Err(AlpnError::NOACK),
}
}
pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
let config_grade = TlsGrade::from_str(grade);
match config_grade {
Some(TlsGrade::HIGH) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2));
// let _ = tls_settings.set_max_proto_version(Some(SslVersion::TLS1_3));
let _ = tls_settings.set_cipher_list(CIPHERS.high);
let _ = tls_settings.set_ciphersuites(CIPHERS.high);
info!("TLS grade: {:?}, => HIGH", tls_settings.options());
}
Some(TlsGrade::MEDIUM) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
info!("TLS grade: {:?}, => MEDIUM", tls_settings.options());
}
Some(TlsGrade::LEGACY) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::SSL3));
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
let _ = tls_settings.set_ciphersuites(CIPHERS.legacy);
warn!("TLS grade: {:?}, => UNSAFE", tls_settings.options());
}
None => {
// Defaults to MEDIUM
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
warn!("TLS grade is not detected defaulting top MEDIUM");
}
}
}
+7 -3
View File
@@ -1,15 +1,19 @@
pub mod auth; pub mod auth;
pub mod consul;
pub mod discovery; pub mod discovery;
pub mod dnsclient;
mod filewatch; mod filewatch;
pub mod fordebug;
pub mod grpc;
pub mod hcclient;
pub mod healthcheck; pub mod healthcheck;
pub mod httpclient; pub mod httpclient;
pub mod jwt; pub mod jwt;
pub mod kuberconsul; pub mod kuberconsul;
pub mod kubernetes;
pub mod kubewatcher;
pub mod lazylock;
pub mod metrics; pub mod metrics;
pub mod parceyaml; pub mod parceyaml;
pub mod state; pub mod state;
pub mod structs; pub mod structs;
pub mod tls;
pub mod tools; pub mod tools;
// pub mod watchksecret;
+188 -36
View File
@@ -1,50 +1,194 @@
use crate::utils::jwt::check_jwt; use crate::utils::jwt::{check_jwt, JWT_TOKEN};
use crate::utils::structs::InnerAuth;
pub(crate) use crate::utils::tools::split_host_port;
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(jwtsecret) = JWT_TOKEN.clone() {
if let Some(tok) = get_query_param(session, "araleztoken") { if let Some(tok) = get_query_param(session, "araleztoken") {
return check_jwt(tok.as_str(), jwtsecret); return check_jwt(tok.as_str(), jwtsecret.as_ref());
} }
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());
}
} }
} }
} }
@@ -52,32 +196,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
@@ -89,6 +222,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)]
// pub 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));
// }
// }
// };
// }
+91
View File
@@ -0,0 +1,91 @@
use crate::utils::httpclient;
use crate::utils::kuberconsul::{list_to_upstreams, ServiceDiscovery};
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, GlobalServiceMapping, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use pingora::prelude::sleep;
use rand::RngExt;
use serde::Deserialize;
use std::env;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::Sender;
#[derive(Debug, Deserialize)]
pub struct ConsulService {
#[serde(rename = "Address")]
pub address: String,
#[serde(rename = "ServicePort")]
pub port: u16,
}
pub struct ConsulDiscovery;
#[async_trait]
impl ServiceDiscovery for ConsulDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
loop {
let upstreams = UpstreamsDashMap::new();
if let Some(consul) = config.consul.clone() {
let servers = consul.servers.unwrap_or_else(|| {
vec![format!(
"{}:{}",
env::var("CONSUL_SERVICE_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
env::var("CONSUL_SERVICE_PORT").unwrap_or_else(|_| "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 = format!("{}/v1/catalog/service/", consul_data);
let dc = format!("{}/v1/catalog/services?filter=%22aralez.service=yes%22+in+ServiceTags", consul_data);
if let Some(infolist) = httpclient::for_consul_list(dc.as_str(), consul.token.clone()).await {
for (k, v) in infolist {
let client_header_map: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut client_header_list = Vec::new();
build_headers(&v.client_headers, config.as_ref(), &mut client_header_list);
let sever_header_map: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut server_header_list = Vec::new();
build_headers(&v.server_headers, config.as_ref(), &mut server_header_list);
if !client_header_list.is_empty() {
let path_key = v.path.clone();
client_header_map.insert(Arc::from(path_key), client_header_list);
config.client_headers.insert(Arc::from(v.host.clone()), client_header_map);
}
if !server_header_list.is_empty() {
let path_key = v.path.clone();
sever_header_map.insert(Arc::from(path_key), server_header_list);
config.server_headers.insert(Arc::from(v.host.clone()), sever_header_map);
}
let pref = format!("{}{}", ss, k);
let gsm = &GlobalServiceMapping {
upstream: k.clone(),
hostname: v.host.clone(),
path: Some(v.path),
to_https: v.to_https,
redirect_to: v.redirect,
authorization: v.auth,
rate_limit: v.rate,
x4xx_limit: v.xrate,
client_headers: v.client_headers,
server_headers: v.server_headers,
};
let list = httpclient::for_consul(pref.as_str(), consul.token.clone(), gsm).await;
list_to_upstreams(list, &upstreams, gsm);
}
}
}
if let Some(lt) = crate::utils::kuberconsul::clone_compare(&upstreams, &config).await {
let _ = toreturn.send(lt).await;
}
sleep(Duration::from_secs(5)).await;
}
}
}
+19 -15
View File
@@ -1,27 +1,24 @@
use crate::utils::consul::ConsulDiscovery;
use crate::utils::filewatch; use crate::utils::filewatch;
use crate::utils::kuberconsul::{ConsulDiscovery, KubernetesDiscovery, ServiceDiscovery}; use crate::utils::kuberconsul::ServiceDiscovery;
use crate::utils::structs::Configuration; use crate::utils::kubernetes::KubernetesDiscovery;
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 std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc::Sender;
pub struct APIUpstreamProvider { pub struct APIUpstreamProvider {
pub config_api_enabled: bool, pub config_api_enabled: bool,
pub address: String, pub address: String,
pub masterkey: String, pub masterkey: Option<String>,
pub tls_address: Option<String>, pub certs_dir: String,
pub tls_certificate: Option<String>, pub config_dir: String,
pub tls_key_file: Option<String>, pub upstreams_file: String,
pub file_server_address: Option<String>, pub file_server_address: Option<String>,
pub file_server_folder: Option<String>, pub file_server_folder: Option<String>,
} pub current_upstreams: Arc<UpstreamsDashMap>,
pub full_upstreams: Arc<UpstreamsDashMap>,
#[async_trait]
impl Discovery for APIUpstreamProvider {
async fn start(&self, toreturn: Sender<Configuration>) {
webserver::run_server(self, toreturn).await;
}
} }
pub struct FromFileProvider { pub struct FromFileProvider {
@@ -41,10 +38,17 @@ pub trait Discovery {
async fn start(&self, tx: Sender<Configuration>); async fn start(&self, tx: Sender<Configuration>);
} }
#[async_trait]
impl Discovery for APIUpstreamProvider {
async fn start(&self, toreturn: Sender<Configuration>) {
webserver::run_server(self, toreturn, self.current_upstreams.clone(), self.full_upstreams.clone()).await;
}
}
#[async_trait] #[async_trait]
impl Discovery for FromFileProvider { impl Discovery for FromFileProvider {
async fn start(&self, tx: Sender<Configuration>) { async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(filewatch::start(self.path.clone(), tx.clone())); tokio::spawn(filewatch::start(self.path.clone(), tx));
} }
} }
-158
View File
@@ -1,158 +0,0 @@
/*
use crate::utils::structs::InnerMap;
use dashmap::DashMap;
use hickory_client::client::{Client, ClientHandle};
use hickory_client::proto::rr::{DNSClass, Name, RecordType};
use hickory_client::proto::runtime::TokioRuntimeProvider;
use hickory_client::proto::udp::UdpClientStream;
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::atomic::AtomicUsize;
use std::time::Duration;
use tokio::sync::Mutex;
type DnsError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub struct DnsClientPool {
clients: Vec<Mutex<DnsClient>>,
}
struct DnsClient {
client: Client,
}
pub async fn start2(mut toreturn: Sender<Configuration>, config: Arc<Configuration>) {
let k8s = config.kubernetes.clone();
match k8s {
Some(k8s) => {
let dnserver = k8s.servers.unwrap_or(vec!["127.0.0.1:53".to_string()]);
let headers = DashMap::new();
let end = dnserver.len() - 1;
let mut num = 0;
if end > 0 {
num = rand::rng().random_range(0..end);
}
let srv = dnserver.get(num).unwrap().to_string();
let pool = DnsClientPool::new(5, srv.clone()).await;
let u = UpstreamsDashMap::new();
if let Some(whitelist) = k8s.services {
loop {
let upstreams = UpstreamsDashMap::new();
for service in whitelist.iter() {
let ret = pool.query_srv(service.real.as_str(), srv.clone()).await;
match ret {
Ok(r) => {
upstreams.insert(service.proxy.clone(), r);
}
Err(e) => eprintln!("DNS query failed for {:?}: {:?}", service, e),
}
}
if !compare_dashmaps(&u, &upstreams) {
headers.clear();
for (k, v) in config.headers.clone() {
headers.insert(k.to_string(), v);
}
let mut tosend: Configuration = Configuration {
upstreams: Default::default(),
headers: Default::default(),
consul: None,
kubernetes: None,
typecfg: "".to_string(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(&upstreams, &u);
clone_dashmap_into(&upstreams, &tosend.upstreams);
tosend.headers = headers.clone();
tosend.extraparams.authentication = config.extraparams.authentication.clone();
tosend.typecfg = config.typecfg.clone();
tosend.consul = config.consul.clone();
print_upstreams(&tosend.upstreams);
toreturn.send(tosend).await.unwrap();
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
None => {}
}
}
impl DnsClient {
pub async fn new(server: String) -> Result<Self, DnsError> {
let server_details = server;
let server: SocketAddr = server_details.parse().expect("Unable to parse socket address");
let conn = UdpClientStream::builder(server, TokioRuntimeProvider::default()).build();
let (client, bg) = Client::connect(conn).await.unwrap();
tokio::spawn(bg);
Ok(Self { client })
}
pub async fn query_srv(&mut self, name: &str) -> Result<DashMap<String, (Vec<InnerMap>, AtomicUsize)>, DnsError> {
let upstreams: DashMap<String, (Vec<InnerMap>, AtomicUsize)> = DashMap::new();
let mut values = Vec::new();
match tokio::time::timeout(Duration::from_secs(5), self.client.query(Name::from_str(name)?, DNSClass::IN, RecordType::SRV)).await {
Ok(Ok(response)) => {
for answer in response.answers() {
if let hickory_client::proto::rr::RData::SRV(srv) = answer.data() {
let to_add = InnerMap {
address: srv.target().to_string(),
port: srv.port(),
is_ssl: false,
is_http2: false,
to_https: false,
rate_limit: None,
};
values.push(to_add);
}
}
upstreams.insert("/".to_string(), (values, AtomicUsize::new(0)));
Ok(upstreams)
}
Ok(Err(e)) => Err(Box::new(e)),
Err(_) => Err("DNS query timed out".into()),
}
}
}
impl DnsClientPool {
pub async fn new(pool_size: usize, server: String) -> Self {
let mut clients = Vec::with_capacity(pool_size);
for _ in 0..pool_size {
if let Ok(client) = DnsClient::new(server.clone()).await {
clients.push(Mutex::new(client));
}
}
Self { clients }
}
pub async fn query_srv(&self, name: &str, server: String) -> Result<DashMap<String, (Vec<InnerMap>, AtomicUsize)>, DnsError> {
// Try to get an available client
for client_mutex in &self.clients {
if let Ok(mut client) = client_mutex.try_lock() {
let vay = client.query_srv(name).await;
match vay {
Ok(_) => return vay,
Err(_) => {
// If query fails, drop this client and create a new one
*client = match DnsClient::new(server).await {
Ok(c) => c,
Err(e) => return Err(e),
};
// Retry with the new client
return client.query_srv(name).await;
}
}
}
}
// If all clients are busy, wait for the first one with a timeout
match tokio::time::timeout(Duration::from_secs(2), self.clients[0].lock()).await {
Ok(mut client) => client.query_srv(name).await,
Err(_) => Err("All DNS clients are busy and timeout reached".into()),
}
}
}
*/
+4 -11
View File
@@ -1,16 +1,15 @@
use crate::utils::parceyaml::load_configuration; use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration; use crate::utils::structs::Configuration;
use futures::channel::mpsc::Sender;
use futures::SinkExt;
use log::error; 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;
use std::path::Path; use std::path::Path;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::sync::mpsc::Sender;
use tokio::task; use tokio::task;
pub async fn start(fp: String, mut toreturn: Sender<Configuration>) { pub async fn start(fp: String, toreturn: Sender<Configuration>) {
sleep(Duration::from_millis(50)).await; // For having nice logs :-) sleep(Duration::from_millis(50)).await; // For having nice logs :-)
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();
@@ -37,18 +36,12 @@ pub async fn start(fp: String, mut toreturn: Sender<Configuration>) {
match event { match event {
Ok(e) => match e.kind { Ok(e) => match e.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(..) | EventKind::Remove(..) => { EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(..) | EventKind::Remove(..) => {
if e.paths[0].to_str().unwrap().ends_with("yaml") {
if start.elapsed() > Duration::from_secs(2) { if start.elapsed() > Duration::from_secs(2) {
start = Instant::now(); start = Instant::now();
// info!("Config File changed :=> {:?}", e); let snd = load_configuration(file_path, "filepath").await.0;
let snd = load_configuration(file_path, "filepath").await; if let Some(snd) = snd {
match snd {
Some(snd) => {
toreturn.send(snd).await.unwrap(); toreturn.send(snd).await.unwrap();
} }
None => {}
}
}
} }
} }
_ => (), _ => (),
+31
View File
@@ -0,0 +1,31 @@
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
pub struct CountingAllocator;
pub static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
pub static DEALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
pub static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);
#[allow(dead_code)]
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
DEALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
System.dealloc(ptr, layout)
}
}
// Uncomment following lines and comment allocator in main.rs
// #[global_allocator]
// pub static A: CountingAllocator = CountingAllocator;
#[allow(dead_code)]
fn for_example() {
let before = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
let after = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
println!("Allocations : {}", after - before);
}
+23
View File
@@ -0,0 +1,23 @@
use bytes::Bytes;
use http_body_util::Full;
use hyper::Request;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;
pub async fn ping_grpc(addr: &str) -> bool {
let connector = HttpConnector::new();
let client = Client::builder(TokioExecutor::new()).http2_only(true).build(connector);
let uri = format!("{}{}", addr, "/aralez.Probe/Ping");
let body = Full::new(Bytes::from(vec![0, 0, 0, 0, 0]));
let request = Request::post(uri).header("content-type", "application/grpc").header("te", "trailers").body(body).unwrap();
match client.request(request).await {
Ok(resp) => resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.map(|v| v.starts_with("application/grpc"))
.unwrap_or(false),
Err(_) => false,
}
}
+95
View File
@@ -0,0 +1,95 @@
use crate::utils::grpc::ping_grpc;
use bytes::Bytes;
use pingora_core::connectors::http::Connector;
use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer;
use pingora_http::RequestHeader;
use std::sync::LazyLock;
use std::time::Duration;
pub static HC_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
pub async fn httpclient(method: &str, tls: bool, host: &str, path: &str, address: &str, port: u16, url: String) -> (bool, bool) {
let method = match method {
"HEAD" => "HEAD",
"GET" => "GET",
"POST" => "POST",
_ => "GET",
};
let mut peer = HttpPeer::new((address, port), tls, host.to_string());
if tls {
peer.options.verify_cert = false;
peer.options.verify_hostname = false;
}
let mut is_h2 = false;
peer.options.total_connection_timeout = Option::from(Duration::from_secs(5));
peer.options.alpn = ALPN::H2H1;
let (mut http_session, _) = match HC_CONNECTOR.get_http_session(&peer).await {
Ok(s) => s,
Err(e) => {
if let Some(msg) = e.context {
log::warn!("{}, type: {}", msg.as_str(), e.etype.as_str());
} else {
log::warn!("Fail to connect to addr: {}, tls {}, host {}", address, tls, host);
}
return (false, false);
}
};
match http_session.as_http2() {
Some(_) => {
peer.options.alpn = ALPN::H2;
is_h2 = true;
}
None => {
if ping_grpc(url.as_str()).await {
return (true, true);
}
}
}
let mut req = match RequestHeader::build(method, path.as_bytes(), None) {
Ok(r) => r,
Err(e) => {
log::warn!("Failed to build request: {}", e);
return (false, false);
}
};
req.insert_header("Host", host).ok();
if let Err(e) = http_session.write_request_header(Box::new(req)).await {
log::warn!("Write header failed: {}", e);
return (false, false);
}
let status = if is_h2 {
if let Err(e) = http_session.write_request_body(Bytes::new(), true).await {
log::warn!("Write body failed: {}", e);
return (false, false);
}
match http_session.read_response_header().await {
Ok(_) => http_session.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
if ping_grpc(url.as_str()).await {
return (true, true);
}
log::warn!("Health Check H2 read failed: {} - {}", host, e);
return (false, false);
}
}
} else {
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!("Health Check H1 read failed: {} - {}", e, host);
return (false, false);
}
}
};
HC_CONNECTOR.release_http_session(http_session, &peer, None).await;
((200..500).contains(&status), is_h2)
}
+50 -97
View File
@@ -1,40 +1,36 @@
use crate::utils::hcclient::httpclient;
use crate::utils::lazylock::REVERSE_STORE;
use crate::utils::structs::{InnerMap, 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, warn};
use reqwest::{Client, Version};
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::time::interval; use tokio::net::TcpStream;
use tonic::transport::Endpoint; use tokio::time::{interval, timeout};
use tokio_native_tls::native_tls::TlsConnector;
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 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() => {
populate_upstreams(&upslist, &fullist, &idlist, params, &client).await; let totest = build_upstreams(&fullist, params.0).await;
if !compare_dashmaps(&totest, &upslist) {
clone_dashmap_into(&totest, &upslist);
clone_idmap_into(&totest, &idlist);
REVERSE_STORE.clear();
} }
} }
} }
} }
pub async fn populate_upstreams(upslist: &Arc<UpstreamsDashMap>, fullist: &Arc<UpstreamsDashMap>, idlist: &Arc<UpstreamsIdMap>, params: (&str, u64), client: &Client) {
let totest = build_upstreams(fullist, params.0, client).await;
if !compare_dashmaps(&totest, upslist) {
clone_dashmap_into(&totest, upslist);
clone_idmap_into(&totest, idlist);
}
} }
pub async fn initiate_upstreams(fullist: UpstreamsDashMap) -> UpstreamsDashMap { pub async fn initiate_upstreams(fullist: UpstreamsDashMap) -> UpstreamsDashMap {
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().unwrap(); build_upstreams(&fullist, "HEAD").await
build_upstreams(&fullist, "HEAD", &client).await
} }
async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Client) -> UpstreamsDashMap { async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str) -> UpstreamsDashMap {
let totest: UpstreamsDashMap = DashMap::new(); let totest: UpstreamsDashMap = DashMap::new();
let fclone = clone_dashmap(fullist); let fclone = clone_dashmap(fullist);
for val in fclone.iter() { for val in fclone.iter() {
@@ -45,35 +41,37 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
let path = path_entry.key(); let path = path_entry.key();
let mut innervec = Vec::new(); let mut innervec = Vec::new();
for (_, upstream) in path_entry.value().0.iter().enumerate() { for upstream in path_entry.value().0.iter() {
let tls = detect_tls(&upstream.address.to_string(), &upstream.port, &client).await; let tls = if upstream.healthcheck.unwrap_or(true) {
let is_h2 = matches!(tls.1, Some(Version::HTTP_2)); detect_tls(upstream.address.as_ref(), &upstream.port, host).await
} else {
false
};
let mut scheme = InnerMap {
address: upstream.address.clone(),
port: upstream.port,
is_ssl: tls,
is_http2: false,
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(),
};
let link = if tls.0 { if scheme.healthcheck.unwrap_or(true) {
let link = if tls {
format!("https://{}:{}{}", upstream.address, upstream.port, path) format!("https://{}:{}{}", upstream.address, upstream.port, path)
} else { } else {
format!("http://{}:{}{}", upstream.address, upstream.port, path) format!("http://{}:{}{}", upstream.address, upstream.port, path)
}; };
let resp = httpclient(method, tls, host, path, upstream.address.as_ref(), upstream.port, link).await;
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,
healthcheck: upstream.healthcheck,
};
if scheme.healthcheck.unwrap_or(true) {
let resp = http_request(&link, method, "", &client).await;
if resp.0 { if resp.0 {
if resp.1 { if resp.1 {
scheme.is_http2 = is_h2; // could be adjusted further scheme.is_http2 = resp.1;
} }
innervec.push(Arc::from(scheme)); innervec.push(Arc::from(scheme));
} else {
warn!("Dead Upstream : {}", link);
} }
} else { } else {
innervec.push(Arc::from(scheme)); innervec.push(Arc::from(scheme));
@@ -86,65 +84,20 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
totest totest
} }
async fn http_request(url: &str, method: &str, payload: &str, client: &Client) -> (bool, bool) { async fn detect_tls(ip: &str, port: &u16, sni: &str) -> bool {
if !["POST", "GET", "HEAD"].contains(&method) { let addr = format!("{ip}:{port}");
error!("Method {} not supported. Only GET|POST|HEAD are supported ", method); let timeout_duration = Duration::from_secs(2); // THINK !!!
return (false, false); let stream = match timeout(timeout_duration, TcpStream::connect(&addr)).await {
} Ok(Ok(s)) => s,
async fn send_request(client: &Client, method: &str, url: &str, payload: &str) -> Option<reqwest::Response> { Ok(Err(_)) => return false,
match method { Err(_) => return false,
"POST" => client.post(url).body(payload.to_owned()).send().await.ok(), };
"GET" => client.get(url).send().await.ok(), let connector = tokio_native_tls::TlsConnector::from(
"HEAD" => client.head(url).send().await.ok(), TlsConnector::builder()
_ => None, .danger_accept_invalid_certs(true)
} .danger_accept_invalid_hostnames(true)
} .build()
.unwrap(),
match send_request(&client, method, url, payload).await { );
Some(response) => { connector.connect(sni, stream).await.is_ok()
let status = response.status().as_u16();
((99..499).contains(&status), false)
}
None => (ping_grpc(&url).await, true),
}
}
pub async fn ping_grpc(addr: &str) -> bool {
let endpoint_result = Endpoint::from_shared(addr.to_owned());
if let Ok(endpoint) = endpoint_result {
let endpoint = endpoint.timeout(Duration::from_secs(2));
match tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await {
Ok(Ok(_channel)) => true,
_ => false,
}
} else {
false
}
}
async fn detect_tls(ip: &str, port: &u16, client: &Client) -> (bool, Option<Version>) {
let https_url = format!("https://{}:{}", ip, port);
match client.get(&https_url).send().await {
Ok(response) => {
// println!("{} => {:?} (HTTPS)", https_url, response.version());
return (true, Some(response.version()));
}
_ => {}
}
let http_url = format!("http://{}:{}", ip, port);
match client.get(&http_url).send().await {
Ok(response) => {
// println!("{} => {:?} (HTTP)", http_url, response.version());
(false, Some(response.version()))
}
Err(_) => {
if ping_grpc(&http_url).await {
(false, Some(Version::HTTP_2))
} else {
(false, None)
}
}
}
} }
+221 -55
View File
@@ -1,78 +1,244 @@
use crate::utils::kuberconsul::{match_path, ConsulService, KubeEndpoints}; use crate::utils::consul::ConsulService;
use crate::utils::structs::{InnerMap, ServiceMapping}; use crate::utils::kuberconsul::match_path;
use axum::http::{HeaderMap, HeaderValue}; use crate::utils::kubernetes::KubeEndpointSliceList;
use crate::utils::structs::{GlobalServiceMapping, InnerMap};
use ahash::HashMap;
use dashmap::DashMap; use dashmap::DashMap;
use reqwest::Client; use pingora_core::connectors::http::Connector;
use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer;
use pingora_http::RequestHeader;
use serde::{Deserialize, Serialize};
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::{Arc, LazyLock};
use std::time::Duration; use std::time::Duration;
pub async fn for_consul(url: String, token: Option<String>, conf: &ServiceMapping) -> Option<DashMap<String, (Vec<Arc<InnerMap>>, AtomicUsize)>> { pub static CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().ok()?;
let mut headers = HeaderMap::new(); #[derive(Debug, Default, Serialize, Deserialize, Clone)]
if let Some(token) = token { pub struct ConsulServicesInternal {
headers.insert("X-Consul-Token", HeaderValue::from_str(&token).unwrap()); #[serde(rename = "aralez.host")]
} pub host: String,
let to = Duration::from_secs(1); #[serde(rename = "aralez.path")]
let resp = client.get(url).timeout(to).send().await.ok()?; pub path: String,
if !resp.status().is_success() { #[serde(rename = "aralez.auth")]
eprintln!("Consul API returned status: {}", resp.status()); pub auth: Option<String>,
return None; #[serde(rename = "aralez.redirect")]
} pub redirect: Option<String>,
let mut inner_vec = Vec::new(); #[serde(rename = "aralez.rate")]
let upstreams: DashMap<String, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new(); pub rate: Option<isize>,
let endpoints: Vec<ConsulService> = resp.json().await.ok()?; #[serde(rename = "aralez.4xx_rate")]
for subsets in endpoints { pub xrate: Option<u32>,
// let addr = subsets.tagged_addresses.get("lan_ipv4").unwrap().address.clone(); #[serde(rename = "aralez.client_headers")]
// let prt = subsets.tagged_addresses.get("lan_ipv4").unwrap().port.clone(); pub client_headers: Option<Vec<String>>,
let addr = subsets.tagged_addresses.get("lan_ipv4").unwrap().address.clone().parse().unwrap(); #[serde(rename = "aralez.server_headers")]
let prt = subsets.tagged_addresses.get("lan_ipv4").unwrap().port.clone(); pub server_headers: Option<Vec<String>>,
let to_add = Arc::from(InnerMap { #[serde(rename = "aralez.to_https")]
address: addr, pub to_https: Option<bool>,
port: prt,
is_ssl: false,
is_http2: false,
to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit,
healthcheck: None,
});
inner_vec.push(to_add);
}
match_path(&conf, &upstreams, inner_vec.clone());
Some(upstreams)
} }
pub async fn for_kuber(url: &str, token: &str, conf: &ServiceMapping) -> Option<DashMap<String, (Vec<Arc<InnerMap>>, AtomicUsize)>> { pub type ConsulServices = HashMap<String, ConsulServicesInternal>;
let to = Duration::from_secs(10);
let client = Client::builder().timeout(Duration::from_secs(10)).danger_accept_invalid_certs(true).build().ok()?; pub async fn for_consul_list(url: &str, token: Option<String>) -> Option<ConsulServices> {
let resp = client.get(url).timeout(to).bearer_auth(token).send().await.ok()?; if let Some(data) = getfromapi(url, token, "consul").await {
if !resp.status().is_success() { let yo = parse_services(data);
eprintln!("Kubernetes API returned status: {}", resp.status()); if let Ok(y) = yo {
return Some(y);
}
return None; return None;
} }
let endpoints: KubeEndpoints = resp.json().await.ok()?; None
let upstreams: DashMap<String, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new(); }
if let Some(subsets) = endpoints.subsets {
for subset in subsets { fn parse_services(json: Vec<u8>) -> Result<ConsulServices, serde_json::Error> {
if let (Some(addresses), Some(ports)) = (subset.addresses, subset.ports) { let raw: HashMap<String, Vec<String>> = serde_json::from_slice(&json)?;
Ok(raw
.into_iter()
.map(|(service, tags)| {
let mut internal = ConsulServicesInternal::default();
for tag in tags {
if let Some((key, value)) = tag.split_once('=') {
match key {
"aralez.host" => internal.host = value.to_string(),
"aralez.path" => internal.path = value.to_string(),
"aralez.rate" => internal.rate = value.parse::<isize>().ok(),
"aralez.4xx_rate" => internal.xrate = value.parse::<u32>().ok(),
"aralez.to_https" => internal.to_https = value.parse::<bool>().ok(),
"aralez.auth" => internal.auth = Option::from(value.to_string()),
"aralez.redirect" => internal.redirect = Option::from(value.to_string()),
"aralez.client_header" => internal.client_headers.get_or_insert_default().push(value.to_string()),
"aralez.server_header" => internal.server_headers.get_or_insert_default().push(value.to_string()),
_ => {}
}
}
}
(service, internal)
})
.collect())
}
pub async fn for_consul(url: &str, token: Option<String>, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
if let Some(data) = getfromapi(url, token, "consul").await {
let endpoints: Vec<ConsulService> = serde_json::from_slice(&data).ok()?;
let mut inner_vec = Vec::new(); let mut inner_vec = Vec::new();
for addr in addresses { let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
for port in &ports { for subsets in endpoints {
let to_add = Arc::from(InnerMap { let to_add = Arc::from(InnerMap {
address: addr.ip.parse().unwrap(), address: Arc::from(&*subsets.address),
port: port.port.clone(), port: subsets.port,
is_ssl: false, is_ssl: false,
is_http2: false, is_http2: false,
to_https: conf.to_https.unwrap_or(false), to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit, rate_limit: conf.rate_limit,
x4xx_limit: conf.x4xx_limit,
redirect_to: conf.redirect_to.clone().map(Arc::<str>::from),
healthcheck: None, healthcheck: None,
authorization: None,
});
inner_vec.push(to_add);
}
match_path(conf, &upstreams, inner_vec);
return Some(upstreams);
};
None
}
pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
if let Some(data) = getfromapi(url, Some(token.to_string()), "kubernetes").await {
let slice_list: KubeEndpointSliceList = serde_json::from_slice(&data).ok()?;
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
let mut inner_vec = Vec::new();
for slice in slice_list.items {
let ports = match &slice.ports {
Some(p) if !p.is_empty() => p,
_ => continue,
};
for ep in &slice.endpoints {
let is_ready = ep.conditions.as_ref().and_then(|c| c.ready).unwrap_or(true);
if !is_ready {
continue;
}
for addr in &ep.addresses {
for port in ports {
if let Some(port_num) = port.port {
let to_add = Arc::from(InnerMap {
address: Arc::from(addr.as_str()),
port: port_num,
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); inner_vec.push(to_add);
} }
} }
match_path(&conf, &upstreams, inner_vec.clone());
} }
} }
} }
Some(upstreams)
if !inner_vec.is_empty() {
match_path(conf, &upstreams, inner_vec);
return Some(upstreams);
}
}
None
}
pub async fn getfromapi(url: &str, token: Option<String>, provider: &str) -> Option<Vec<u8>> {
let (host, port, path, is_tls) = parse_url(&url).ok()?;
let mut peer = HttpPeer::new((host, port), is_tls, host.to_string());
peer.options.total_connection_timeout = Some(Duration::from_secs(5));
peer.options.read_timeout = Some(Duration::from_secs(5));
if is_tls {
peer.options.verify_cert = false;
peer.options.verify_hostname = false;
peer.options.alpn = ALPN::H2H1;
}
let mut http_session = CONNECTOR.get_http_session(&peer).await.ok()?;
let mut req = RequestHeader::build("GET", path.as_bytes(), None).ok()?;
let host_header = if (is_tls && port == 443) || (!is_tls && port == 80) {
host.to_string()
} else {
format!("{}:{}", host, port)
};
req.insert_header("Host", host_header).ok()?;
req.insert_header("Accept", "application/json").ok()?;
match provider {
"consul" => {
if let Some(token) = token {
req.insert_header("X-Consul-Token", token).ok()?;
}
}
"kubernetes" => {
if let Some(token) = token {
req.insert_header("Authorization", format!("Bearer {}", token)).ok()?;
}
}
_ => {}
}
if http_session.0.write_request_header(Box::new(req)).await.is_err() {
CONNECTOR.release_http_session(http_session.0, &peer, None).await;
return None;
}
let status = match http_session.0.read_response_header().await {
Ok(_) => http_session.0.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
log::warn!("API call failed to read response header for {}: {}", url, e);
500
}
};
let mut body_bytes = Vec::new();
if status == 200 {
while let Ok(Some(chunk)) = http_session.0.read_response_body().await {
body_bytes.extend_from_slice(&chunk);
}
}
CONNECTOR.release_http_session(http_session.0, &peer, None).await;
if status == 200 && !body_bytes.is_empty() {
Some(body_bytes)
} else {
None
}
}
fn parse_url(url: &str) -> Result<(&str, u16, &str, bool), &'static str> {
let is_https = url.starts_with("https://");
let default_port = if is_https { 443 } else { 80 };
let no_scheme = url.split_once("://").map_or(url, |(_, rest)| rest);
let (authority, uri) = no_scheme.find('/').map_or((no_scheme, "/"), |i| (&no_scheme[..i], &no_scheme[i..]));
let (host, port) = match authority.split_once(':') {
Some((h, p)) => {
let port_num = p.parse::<u16>().map_err(|_| "Invalid port number")?;
(h, port_num)
}
None => (authority, default_port),
};
if host.is_empty() {
return Err("Empty host in URL");
}
Ok((host, port, uri, is_https))
} }
+85 -8
View File
@@ -1,16 +1,93 @@
use ahash::AHasher;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use 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>,
}
#[derive(Debug, Deserialize)]
struct Expired {
exp: Option<u64>,
}
static JWT_VALIDATION: LazyLock<Validation> = LazyLock::new(|| Validation::new(Algorithm::HS256));
pub static JWT_TOKEN: LazyLock<Option<Arc<str>>> = LazyLock::new(|| match env::var("JWT_KEY") {
Ok(key) if !key.is_empty() => Some(Arc::from(key.as_str())),
_ => None,
});
static JWT_CACHE: LazyLock<Cache<u64, u64>> = LazyLock::new(|| Cache::builder().max_capacity(100_000).expire_after(JwtExpiry).build());
struct JwtExpiry;
impl Expiry<u64, u64> for JwtExpiry {
fn expire_after_create(&self, _key: &u64, value: &u64, _current_time: Instant) -> Option<Duration> {
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if *value > now {
Some(Duration::from_secs(value - now))
} else {
Some(Duration::ZERO)
}
}
}
pub fn check_jwt(token: &str, secret: &str) -> bool {
let key = hash_token(token, secret);
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if let Some(exp) = JWT_CACHE.get(&key) {
if exp < now {
return false;
}
return true;
}
match is_expired(token, now) {
Ok(true) => return false,
Ok(false) => {}
Err(_) => return false,
}
match decode::<Claims>(token, &DecodingKey::from_secret(secret.as_ref()), &JWT_VALIDATION) {
Ok(data) => {
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if data.claims.exp > now {
JWT_CACHE.insert(key, data.claims.exp);
true
} else {
false
}
} }
pub fn check_jwt(input: &str, secret: &str) -> bool {
let validation = Validation::new(Algorithm::HS256);
let token_data = decode::<Claims>(&input, &DecodingKey::from_secret(secret.as_ref()), &validation);
match token_data {
Ok(_) => true,
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()
}
+20 -183
View File
@@ -1,214 +1,50 @@
use crate::utils::httpclient; use crate::utils::structs::{Configuration, GlobalServiceMapping, InnerMap, UpstreamsDashMap};
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, InnerMap, ServiceMapping, UpstreamsDashMap};
use crate::utils::tools::{clone_dashmap_into, compare_dashmaps, print_upstreams}; use crate::utils::tools::{clone_dashmap_into, compare_dashmaps, print_upstreams};
use async_trait::async_trait; use async_trait::async_trait;
use dashmap::DashMap; use dashmap::DashMap;
use futures::channel::mpsc::Sender;
use futures::SinkExt;
use pingora::prelude::sleep;
use rand::Rng;
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::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use tokio::fs::File; use tokio::fs::File;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender;
#[derive(Debug, serde::Deserialize)] #[allow(clippy::type_complexity)]
pub struct KubeEndpoints { pub fn list_to_upstreams(lt: Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>, upstreams: &UpstreamsDashMap, i: &GlobalServiceMapping) {
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,
}
pub fn list_to_upstreams(lt: Option<DashMap<String, (Vec<Arc<InnerMap>>, AtomicUsize)>>, upstreams: &UpstreamsDashMap, i: &ServiceMapping) {
if let Some(list) = lt { if let Some(list) = lt {
match upstreams.get(&i.hostname.clone()) { let key: Arc<str> = Arc::from(i.hostname.as_str());
match upstreams.get(&key) {
Some(upstr) => { Some(upstr) => {
for (k, v) in list { for (k, v) in list {
upstr.value().insert(k, v); upstr.value().insert(k, v);
} }
} }
None => { None => {
upstreams.insert(i.hostname.clone(), list); upstreams.insert(key, list);
} }
}; };
} }
} }
pub fn match_path(conf: &ServiceMapping, upstreams: &DashMap<String, (Vec<Arc<InnerMap>>, AtomicUsize)>, values: Vec<Arc<InnerMap>>) { pub fn match_path(conf: &GlobalServiceMapping, upstreams: &DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>, values: Vec<Arc<InnerMap>>) {
match conf.path { let path = conf.path.as_deref().unwrap_or("/");
Some(ref p) => { upstreams.insert(Arc::from(path), (values, AtomicUsize::new(0)));
upstreams.insert(p.to_string(), (values, AtomicUsize::new(0)));
}
None => {
upstreams.insert("/".to_string(), (values, AtomicUsize::new(0)));
}
}
} }
async fn read_token(path: &str) -> String { pub async fn read_token(path: &str) -> String {
let mut file = File::open(path).await.unwrap(); let mut file = File::open(path).await.unwrap();
let mut contents = String::new(); let mut contents = String::new();
file.read_to_string(&mut contents).await.unwrap(); file.read_to_string(&mut contents).await.unwrap();
contents.trim().to_string() contents.trim().to_string()
} }
#[async_trait] #[async_trait]
pub trait ServiceDiscovery { pub trait ServiceDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>); async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>);
} }
pub struct KubernetesDiscovery; pub async fn clone_compare(upstreams: &UpstreamsDashMap, config: &Arc<Configuration>) -> Option<Configuration> {
pub struct ConsulDiscovery; if !compare_dashmaps(upstreams, &config.upstreams) {
let tosend = Configuration {
#[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 i in svc {
let header_list: DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>> = 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(Arc::from(path.as_str()), hl);
// header_list.insert(Arc::from(i.path).unwrap_or(Arc::from("/")).as_str(), hl);
config.client_headers.insert(i.hostname.clone(), header_list);
}
let url = format!("https://{}/api/v1/namespaces/{}/endpoints/{}", server, namespace, i.hostname);
let list = httpclient::for_kuber(&*url, &*token, &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;
}
}
}
}
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(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(), upstreams: Default::default(),
client_headers: config.client_headers.clone(), client_headers: config.client_headers.clone(),
server_headers: config.server_headers.clone(), server_headers: config.server_headers.clone(),
@@ -217,10 +53,11 @@ async fn clone_compare(upstreams: &UpstreamsDashMap, prev_upstreams: &UpstreamsD
typecfg: config.typecfg.clone(), typecfg: config.typecfg.clone(),
extraparams: config.extraparams.clone(), extraparams: config.extraparams.clone(),
}; };
clone_dashmap_into(&upstreams, &prev_upstreams); clone_dashmap_into(upstreams, &config.upstreams);
clone_dashmap_into(&upstreams, &tosend.upstreams); clone_dashmap_into(upstreams, &tosend.upstreams);
print_upstreams(&tosend.upstreams);
print_upstreams(&tosend.upstreams, &tosend.extraparams);
return Some(tosend); return Some(tosend);
}; }
None None
} }
+163
View File
@@ -0,0 +1,163 @@
use crate::utils::httpclient;
use crate::utils::kubewatcher::{start_ingress_watcher_with_config, AralezRouteUpdate};
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, GlobalServiceMapping, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use log::error;
use rand::RngExt;
use std::env;
use std::sync::Arc;
use crate::utils::kuberconsul::{clone_compare, list_to_upstreams, ServiceDiscovery};
use rustls::crypto::ring::default_provider;
use rustls::crypto::CryptoProvider;
use tokio::sync::mpsc::Sender;
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeEndpointSliceList {
#[serde(default)]
pub items: Vec<KubeEndpointSlice>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeEndpointSlice {
pub ports: Option<Vec<KubePort>>,
#[serde(default)]
pub endpoints: Vec<KubeEndpoint>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeEndpoint {
#[serde(default)]
pub addresses: Vec<String>,
pub conditions: Option<KubeConditions>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeConditions {
pub ready: Option<bool>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubePort {
pub port: Option<u16>,
}
pub struct KubernetesDiscovery;
#[async_trait]
impl ServiceDiscovery for KubernetesDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
if let Some(kuber) = config.kubernetes.clone() {
let servers = kuber.servers.unwrap_or_else(|| {
vec![format!(
"{}:{}",
env::var("KUBERNETES_SERVICE_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
env::var("KUBERNETES_SERVICE_PORT_HTTPS").unwrap_or_else(|_| "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_else(|| "/var/run/secrets/kubernetes.io/serviceaccount/token".to_string());
let token = crate::utils::kuberconsul::read_token(path.as_str()).await;
if CryptoProvider::get_default().is_none() {
default_provider().install_default().expect("Failed to install rustls crypto provider");
}
let (tx, mut rx) = tokio::sync::mpsc::channel::<AralezRouteUpdate>(100);
let srv = server.clone();
let tok = token.clone();
tokio::spawn(async move {
if let Err(e) = start_ingress_watcher_with_config(srv.as_str(), tok.as_str(), tx, "aralez").await {
error!("Fatal watcher error: {:?}", e);
}
});
let upstreams = UpstreamsDashMap::new();
while let Some(update) = rx.recv().await {
let host_key: Arc<str> = Arc::from(update.host.as_str());
if update.is_deleted {
upstreams.remove(&host_key);
config.client_headers.remove(&host_key);
} else {
let service = GlobalServiceMapping {
upstream: update.service_name.clone(),
hostname: update.host.clone(),
path: Some(update.path.clone()),
to_https: None,
redirect_to: None,
authorization: None,
rate_limit: update.rate_limit,
x4xx_limit: update.x4xx_limit,
client_headers: None,
server_headers: None,
};
let cheader_list: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let sheader_list: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut client_headers_list = Vec::new();
let mut server_headers_list = Vec::new();
let mut chl: Option<Vec<String>> = None;
let mut shl: Option<Vec<String>> = None;
if let Some(hdr) = service.client_headers.as_ref() {
chl.get_or_insert_with(Vec::new).extend(hdr.iter().cloned());
}
if let Some(hdr) = service.server_headers.as_ref() {
shl.get_or_insert_with(Vec::new).extend(hdr.iter().cloned());
}
if let Some(ch) = update.client_headers {
chl.get_or_insert_with(Vec::new).extend(ch);
}
if let Some(hdr) = update.server_headers.as_ref() {
shl.get_or_insert_with(Vec::new).extend(hdr.iter().cloned());
}
build_headers(&chl, config.as_ref(), &mut client_headers_list);
if !client_headers_list.is_empty() {
let path_key = service.path.as_deref().unwrap_or("/");
cheader_list.insert(Arc::from(path_key), client_headers_list);
config.client_headers.insert(host_key.clone(), cheader_list);
}
build_headers(&shl, config.as_ref(), &mut server_headers_list);
if !server_headers_list.is_empty() {
let path_key = service.path.as_deref().unwrap_or("/");
sheader_list.insert(Arc::from(path_key), server_headers_list);
config.server_headers.insert(host_key.clone(), sheader_list);
}
let url = format!(
"https://{}/apis/discovery.k8s.io/v1/namespaces/{}/endpointslices?labelSelector=kubernetes.io%2Fservice-name%3D{}",
server, update.namespace, update.service_name
);
let list = httpclient::for_kuber(&url, &token, &service).await;
if list.is_none() {
error!("Endpoint URL: {} returned empty/invalid response", url);
}
list_to_upstreams(list, &upstreams, &service);
}
if let Some(lt) = clone_compare(&upstreams, &config).await {
let _ = toreturn.send(lt).await;
}
}
}
}
}
+279
View File
@@ -0,0 +1,279 @@
use dashmap::DashMap;
use futures::StreamExt;
use hyper::Uri;
use k8s_openapi::api::core::v1::{Node, Service};
use k8s_openapi::api::networking::v1::Ingress;
use kube::api::{Patch, PatchParams};
use kube::{
api::Api,
client::Client,
config::{AuthInfo, Config},
runtime::watcher::{self, Event},
};
use log::{error, info};
use serde_json::json;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
pub struct AralezRouteUpdate {
pub host: String,
pub path: String,
pub namespace: String,
pub service_name: String,
pub is_deleted: bool,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
struct AralezAnnotation {
rate_limit: Option<isize>,
x4xx_limit: Option<u32>,
namespace: String,
client_headers: Option<Vec<String>>,
server_headers: Option<Vec<String>>,
}
lazy_static::lazy_static! {
static ref PATCHED_INGRESSES: Arc<DashMap<String, String>> = Arc::new(DashMap::new());
}
pub async fn start_ingress_watcher_with_config(
api_server: &str,
token: &str,
tx: mpsc::Sender<AralezRouteUpdate>,
target_class: &'static str,
) -> Result<(), Box<dyn std::error::Error>> {
let client = create_custom_k8s_client(api_server, token, true).await?;
let ingresses: Api<Ingress> = Api::all(client.clone());
let watcher_config = watcher::Config::default().any_semantic();
let mut stream = watcher::watcher(ingresses, watcher_config).boxed();
while let Some(event) = stream.next().await {
match event {
Ok(Event::Apply(ing)) => {
process_ingress_change(&client, &ing, target_class, false, &tx).await;
}
Ok(Event::Delete(ing)) => {
process_ingress_change(&client, &ing, target_class, true, &tx).await;
}
Ok(Event::InitApply(ing)) => {
process_ingress_change(&client, &ing, target_class, false, &tx).await;
}
Ok(Event::InitDone) => {
info!("Ingress watcher initial sync complete");
}
Err(err) => {
error!("Kubernetes Ingress watch error: {:?}", err);
}
_ => {}
}
}
Ok(())
}
async fn process_ingress_change(client: &Client, ing: &Ingress, target_class: &str, is_deleted: bool, tx: &mpsc::Sender<AralezRouteUpdate>) {
let spec = match &ing.spec {
Some(s) => s,
None => return,
};
let class_name = spec.ingress_class_name.as_deref().unwrap_or_default();
if class_name != target_class {
return;
}
if !is_deleted {
if let (Some(name), Some(ns)) = (ing.metadata.name.as_deref(), ing.metadata.namespace.as_deref()) {
let key = format!("{}/{}", ns, name);
let external_ip = resolve_external_ip(client).await;
let status_already_correct = ing
.status
.as_ref()
.and_then(|s| s.load_balancer.as_ref())
.and_then(|lb| lb.ingress.as_ref())
.map(|list| list.iter().any(|i| i.ip.as_deref() == Some(&external_ip)))
.unwrap_or(false);
if status_already_correct {
PATCHED_INGRESSES.insert(key, external_ip);
} else {
PATCHED_INGRESSES.insert(key.clone(), external_ip.clone());
let client_clone = client.clone();
let name_clone = name.to_string();
let ns_clone = ns.to_string();
let key_clone = key.clone();
tokio::spawn(async move {
if let Err(e) = patch_ingress_status_address(&client_clone, &ns_clone, &name_clone, &external_ip).await {
error!("Failed to patch ingress status address {}/{}: {}", &ns_clone, &name_clone, e);
PATCHED_INGRESSES.remove(&key_clone);
}
});
}
}
} else {
if let (Some(name), Some(ns)) = (ing.metadata.name.as_deref(), ing.metadata.namespace.as_deref()) {
PATCHED_INGRESSES.remove(&format!("{}/{}", ns, name));
}
}
if let Some(rules) = &spec.rules {
for rule in rules {
let host = rule.host.clone().unwrap_or_else(|| "*".to_string());
if let Some(http) = &rule.http {
for path_entry in &http.paths {
let path = path_entry.path.clone().unwrap_or_else(|| "/".to_string());
if let Some(service) = &path_entry.backend.service {
let service_name = service.name.clone();
let annot = parse_ingress_config(ing);
let update = AralezRouteUpdate {
host: host.clone(),
path,
service_name,
is_deleted,
rate_limit: annot.rate_limit,
x4xx_limit: annot.x4xx_limit,
client_headers: annot.client_headers,
server_headers: annot.server_headers,
namespace: annot.namespace,
};
if let Err(e) = tx.send(update).await {
error!("Failed to send route update to channel: {:?}", e);
}
}
}
}
}
}
}
pub async fn create_custom_k8s_client(api_server_url: &str, bearer_token: &str, insecure_tls: bool) -> Result<Client, Box<dyn std::error::Error>> {
let full_url = if !api_server_url.starts_with("http://") && !api_server_url.starts_with("https://") {
format!("https://{}", api_server_url)
} else {
api_server_url.to_string()
};
let uri: Uri = full_url.parse()?;
let mut config = Config::new(uri);
config.auth_info = AuthInfo {
token: Some(bearer_token.trim().to_string().into()),
..Default::default()
};
config.connect_timeout = Some(Duration::from_secs(10));
config.read_timeout = None;
if insecure_tls {
config.accept_invalid_certs = true;
}
let client = Client::try_from(config)?;
Ok(client)
}
fn parse_ingress_config(ing: &Ingress) -> AralezAnnotation {
let namespace = ing.metadata.namespace.clone().unwrap_or_else(|| "default".to_string());
let annotations = ing.metadata.annotations.as_ref();
let client_headers = annotations.and_then(|a| a.get("aralez.rs/client_headers")).and_then(|ch| serde_json::from_str(ch).ok());
let server_headers = annotations.and_then(|a| a.get("aralez.rs/server_headers")).and_then(|sh| serde_json::from_str(sh).ok());
AralezAnnotation {
rate_limit: annotations.and_then(|a| a.get("aralez.rs/rate_limit")).and_then(|v| v.parse().ok()),
x4xx_limit: annotations.and_then(|a| a.get("aralez.rs/x4xx_limit")).and_then(|v| v.parse().ok()),
client_headers,
server_headers,
namespace,
}
}
pub async fn patch_ingress_status_address(client: &Client, namespace: &str, name: &str, external_ip: &str) -> Result<(), kube::Error> {
let ingresses: Api<Ingress> = Api::namespaced(client.clone(), namespace);
let patch = json!({
"status": {
"loadBalancer": {
"ingress": [
{
"ip": external_ip
}
]
}
}
});
let params = PatchParams::default();
match ingresses.patch_status(name, &params, &Patch::Strategic(patch)).await {
Ok(_) => {
info!("Successfully patched Ingress {}/{} ADDRESS to {}", namespace, name, external_ip);
Ok(())
}
Err(e) => {
error!("Failed to patch status for Ingress {}/{}: {:?}", namespace, name, e);
Err(e)
}
}
}
pub async fn discover_node_ip(client: &Client) -> Option<String> {
let nodes: Api<Node> = Api::all(client.clone());
if let Ok(node_list) = nodes.list(&Default::default()).await {
for node in node_list {
if let Some(status) = node.status {
if let Some(addresses) = status.addresses {
for addr in &addresses {
if addr.type_ == "ExternalIP" {
return Some(addr.address.clone());
}
}
for addr in &addresses {
if addr.type_ == "InternalIP" {
return Some(addr.address.clone());
}
}
}
}
}
}
None
}
pub async fn discover_controller_ip(client: &Client, namespace: &str, service_name: &str) -> Option<String> {
let services: Api<Service> = Api::namespaced(client.clone(), namespace);
if let Ok(svc) = services.get(service_name).await {
if let Some(status) = svc.status {
if let Some(lb) = status.load_balancer {
if let Some(ingress_list) = lb.ingress {
for ing in ingress_list {
if let Some(ip) = ing.ip {
return Some(ip);
}
if let Some(hostname) = ing.hostname {
return Some(hostname);
}
}
}
}
}
}
None
}
pub async fn resolve_external_ip(client: &Client) -> String {
if let Ok(ip) = std::env::var("ARALEZ_EXTERNAL_IP") {
let trimmed = ip.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
let svc_name = std::env::var("ARALEZ_SERVICE_NAME").unwrap_or_else(|_| "aralez-service".to_string());
let svc_ns = std::env::var("ARALEZ_SERVICE_NAMESPACE").unwrap_or_else(|_| "aralez".to_string());
if let Some(ip) = discover_controller_ip(client, &svc_ns, &svc_name).await {
return ip;
}
if let Some(ip) = discover_node_ip(client).await {
return ip;
}
"127.0.0.1".to_string()
}
+21
View File
@@ -0,0 +1,21 @@
use dashmap::DashMap;
use moka::sync::Cache;
use pingora_cache::eviction::simple_lru::Manager;
use pingora_cache::lock::CacheLock;
use pingora_cache::MemCache;
use pingora_limits::rate::Rate;
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::{Arc, LazyLock};
use std::sync::{OnceLock, RwLock};
use std::time::Duration;
pub static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(DashMap::new);
pub static RATE_LIMITER: LazyLock<Rate> = LazyLock::new(|| Rate::new(Duration::from_secs(1)));
pub static REQUESTS_4XX: LazyLock<Cache<IpAddr, u32>> = LazyLock::new(|| Cache::builder().time_to_live(Duration::from_secs(1)).build());
pub static LOCALHOST: LazyLock<Arc<str>> = LazyLock::new(|| Arc::from("localhost"));
pub static MEM_CACHE: LazyLock<MemCache> = LazyLock::new(MemCache::new);
pub static CACHE_LOCK: LazyLock<CacheLock> = LazyLock::new(|| CacheLock::new(Duration::from_secs(30)));
pub static EVICTION: OnceLock<Manager> = OnceLock::new();
pub static CACHE_TTL: OnceLock<u64> = OnceLock::new();
pub static CHALLENGES: LazyLock<RwLock<HashMap<String, String>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
+142 -49
View File
@@ -1,63 +1,156 @@
use crate::utils::lazylock::EVICTION;
use pingora_cache::eviction::EvictionManager;
use pingora_http::Method;
use pingora_http::StatusCode;
use pingora_http::Version; use pingora_http::Version;
use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, Histogram, IntCounter, IntCounterVec}; use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, register_int_gauge, Histogram, IntCounter, IntCounterVec, IntGauge};
use std::sync::Arc;
use std::sync::LazyLock;
use std::time::Duration; use std::time::Duration;
pub struct MetricTypes { pub struct MetricTypes {
pub method: String, pub method: Method,
pub code: String, pub upstream: Arc<str>,
pub code: Option<StatusCode>,
pub latency: Duration, pub latency: Duration,
pub version: Version, pub version: Version,
} }
lazy_static::lazy_static! {
pub static ref REQUEST_COUNT: IntCounter = register_int_counter!( pub static OPEN_FILES: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_open_files", "Number of open file descriptors").unwrap());
"aralez_requests_total", pub static LOGGING_ERRORS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_logging_errors", "Number of log errors").unwrap());
"Total number of requests handled by Aralez" pub static MEMORY_USAGE: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_memory_bytes", "Total memory allocated in bytes").unwrap());
).unwrap(); pub static ACTIVE_SESSIONS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_active_sessions", "Current number of active sessions").unwrap());
pub static ref RESPONSE_CODES: IntCounterVec = register_int_counter_vec!( pub static REQUEST_COUNT: LazyLock<IntCounter> = LazyLock::new(|| register_int_counter!("aralez_requests_total", "Total number of requests handled by Aralez").unwrap());
"aralez_responses_total",
"Responses grouped by status code", pub static CACHE_SIZE_BYTES: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_size_bytes", "Current cache size in bytes").unwrap());
&["status"] pub static CACHE_ITEMS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_items", "Current number of cached objects").unwrap());
).unwrap(); pub static CACHE_EVICTED_BYTES: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_evicted_bytes_total", "Total bytes evicted from cache").unwrap());
pub static ref REQUEST_LATENCY: Histogram = register_histogram!(
"aralez_request_latency_seconds", pub static CACHE_EVICTED_ITEMS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_evicted_items_total", "Total cache items evicted").unwrap());
"Request latency in seconds", pub static RESPONSE_CODES: LazyLock<IntCounterVec> =
vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0] LazyLock::new(|| register_int_counter_vec!("aralez_responses_total", "Responses grouped by status code", &["status"]).unwrap());
).unwrap();
pub static ref RESPONSE_LATENCY: Histogram = register_histogram!( pub static RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
register_histogram!(
"aralez_response_latency_seconds", "aralez_response_latency_seconds",
"Response latency in seconds", "Response latency in seconds",
vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0] vec![0.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()
"aralez_requests_by_method_total", });
"Number of requests by HTTP method",
&["method"] pub static REQUESTS_BY_METHOD: LazyLock<IntCounterVec> =
).unwrap(); LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_method_total", "Number of requests by HTTP method", &["method"]).unwrap());
pub static ref REQUESTS_BY_VERSION: IntCounterVec = register_int_counter_vec!(
"aralez_requests_by_version_total", pub static REQUESTS_BY_UPSTREAM: LazyLock<IntCounterVec> =
"Number of requests by HTTP versions", LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_upstream", "Number of requests by UPSTREAM server", &["upstream"]).unwrap());
&["version"]
).unwrap(); pub static REQUESTS_BY_VERSION: LazyLock<IntCounterVec> =
pub static ref ERROR_COUNT: IntCounter = register_int_counter!( LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_version_total", "Number of requests by HTTP versions", &["version"]).unwrap());
"aralez_errors_total", pub static HTTP_11_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/1.1"]));
"Total number of errors" pub static HTTP_20_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/2.0"]));
).unwrap(); pub static HTTP_30_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/3.0"]));
} pub static HTTP_10_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/1.0"]));
pub static HTTP_UNK_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["Unknown"]));
pub static METHOD_GET: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["GET"]));
pub static METHOD_POST: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["POST"]));
pub static METHOD_PUT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["PUT"]));
pub static METHOD_DELETE: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["DELETE"]));
pub static METHOD_HEAD: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["HEAD"]));
pub static METHOD_OPTIONS: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["OPTIONS"]));
pub static METHOD_PATCH: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["PATCH"]));
pub static METHOD_OTHER: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["OTHER"]));
pub static STATUS_100: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["100"]));
pub static STATUS_101: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["101"]));
pub static STATUS_200: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["200"]));
pub static STATUS_204: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["204"]));
pub static STATUS_301: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["301"]));
pub static STATUS_302: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["302"]));
pub static STATUS_304: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["304"]));
pub static STATUS_400: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["400"]));
pub static STATUS_403: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["403"]));
pub static STATUS_404: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["404"]));
pub static STATUS_500: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["500"]));
pub static STATUS_501: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["501"]));
pub static STATUS_502: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["502"]));
pub static STATUS_503: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["503"]));
pub fn calc_metrics(metric_types: &MetricTypes) { pub fn calc_metrics(metric_types: &MetricTypes) {
REQUEST_COUNT.inc(); REQUEST_COUNT.inc();
let timer = REQUEST_LATENCY.start_timer(); match metric_types.version {
timer.observe_duration(); Version::HTTP_11 => HTTP_11_COUNT.inc(),
Version::HTTP_2 => HTTP_20_COUNT.inc(),
let version_str = match &metric_types.version { Version::HTTP_3 => HTTP_30_COUNT.inc(),
&Version::HTTP_11 => "HTTP/1.1", Version::HTTP_10 => HTTP_10_COUNT.inc(),
&Version::HTTP_2 => "HTTP/2.0", _ => HTTP_UNK_COUNT.inc(),
&Version::HTTP_3 => "HTTP/3.0",
&Version::HTTP_10 => "HTTP/1.0",
_ => "Unknown",
}; };
REQUESTS_BY_VERSION.with_label_values(&[&version_str]).inc(); match metric_types.method.as_str() {
RESPONSE_CODES.with_label_values(&[&metric_types.code.to_string()]).inc(); "GET" => METHOD_GET.inc(),
REQUESTS_BY_METHOD.with_label_values(&[&metric_types.method]).inc(); "POST" => METHOD_POST.inc(),
RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64()); "PUT" => METHOD_PUT.inc(),
"DELETE" => METHOD_DELETE.inc(),
"HEAD" => METHOD_HEAD.inc(),
"OPTIONS" => METHOD_OPTIONS.inc(),
"PATCH" => METHOD_PATCH.inc(),
_ => METHOD_OTHER.inc(),
}
let code = metric_types.code.unwrap_or(StatusCode::GONE);
match code.as_u16() {
100 => STATUS_100.inc(),
101 => STATUS_101.inc(),
200 => STATUS_200.inc(),
204 => STATUS_204.inc(),
301 => STATUS_301.inc(),
302 => STATUS_302.inc(),
304 => STATUS_304.inc(),
400 => STATUS_400.inc(),
403 => STATUS_403.inc(),
404 => STATUS_404.inc(),
500 => STATUS_500.inc(),
501 => STATUS_501.inc(),
502 => STATUS_502.inc(),
503 => STATUS_503.inc(),
_ => RESPONSE_CODES.with_label_values(&[code.as_str()]).inc(), // Fallback for rare status codes
}
REQUESTS_BY_UPSTREAM.with_label_values(&[metric_types.upstream.as_ref()]).inc();
RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64());
// if let Some(eviction) = EVICTION.get() {
// CACHE_SIZE_BYTES.set(eviction.total_size() as i64);
// CACHE_ITEMS.set(eviction.total_items() as i64);
// CACHE_EVICTED_BYTES.set(eviction.evicted_size() as i64);
// CACHE_EVICTED_ITEMS.set(eviction.evicted_items() as i64);
// }
}
pub(crate) fn get_memory_usage() -> usize {
std::fs::read_to_string("/proc/self/status")
.ok()
.and_then(|s| {
s.lines()
.find(|l| l.starts_with("VmRSS:"))
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|v| v.parse::<usize>().ok())
})
.unwrap_or(0)
* 1024
}
pub fn get_open_files() -> usize {
std::fs::read_dir("/proc/self/fd").map(|dir| dir.count()).unwrap_or(0)
}
pub async fn calc_cache_metrics() {
loop {
if let Some(eviction) = EVICTION.get() {
CACHE_SIZE_BYTES.set(eviction.total_size() as i64);
CACHE_ITEMS.set(eviction.total_items() as i64);
CACHE_EVICTED_BYTES.set(eviction.evicted_size() as i64);
CACHE_EVICTED_ITEMS.set(eviction.evicted_items() as i64);
}
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
} }
+161 -92
View File
@@ -1,113 +1,193 @@
use crate::utils::healthcheck; use crate::utils::healthcheck;
use crate::utils::lazylock::REVERSE_STORE;
use crate::utils::state::{is_first_run, mark_not_first_run}; use crate::utils::state::{is_first_run, mark_not_first_run};
use crate::utils::structs::*; use crate::utils::structs::*;
use crate::utils::tools::{clone_dashmap, clone_dashmap_into, print_upstreams}; use crate::utils::tools::{clone_dashmap, clone_dashmap_into, print_upstreams};
use crate::web::logging::log_builder;
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info, warn}; use log::{error, info, warn};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::{Arc, LazyLock};
use std::{env, fs}; use std::{env, fs};
// use tokio::sync::oneshot::{Receiver, Sender};
pub async fn load_configuration(d: &str, kind: &str) -> Option<Configuration> { pub static DOMAINS: LazyLock<DashMap<String, bool>> = LazyLock::new(DashMap::new);
pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>, String) {
let mut conf_files = Vec::new();
let yaml_data = match kind { let yaml_data = match kind {
"filepath" => match fs::read_to_string(d) { "filepath" => {
Ok(data) => { let mut data = String::new();
info!("Reading upstreams from {}", d); let mut last_error = None;
data for _ in 0..5 {
match fs::read_to_string(d) {
Ok(content) => {
if !content.trim().is_empty() {
data = content;
break;
}
} }
Err(e) => { Err(e) => {
error!("Reading: {}: {:?}", d, e); 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 // [2606:4700:2ff9::1]:443
} }
},
"content" => { "content" => {
info!("Reading upstreams from API post body"); info!("Reading upstreams from API post body");
d.to_string() d.to_string()
} }
_ => { _ => {
error!("Mismatched parameter, only filepath|content is allowed"); error!("Mismatched parameter, only filepath|content is allowed");
return None; return (None, "Mismatched parameter, only filepath|content is allowed".to_string());
} }
}; };
let parsed: Config = match serde_yaml::from_str(&yaml_data) { let mut parsed: Config = match noyalib::from_str(&yaml_data) {
Ok(cfg) => cfg, Ok(cfg) => cfg,
Err(e) => { Err(e) => {
error!("Failed to parse upstreams file: {}", e); error!("Failed to parse upstreams file: {}", e);
return None; return (None, e.to_string());
} }
}; };
let mut toreturn = Configuration::default(); if let Some(ref mut upstreams) = parsed.upstreams {
for uconf in conf_files {
let p: HashMap<String, HostConfig> = match noyalib::from_str(&uconf) {
Ok(ucfg) => ucfg,
Err(e) => {
error!("Failed to parse upstreams file: {}", e);
return (None, e.to_string());
}
};
upstreams.extend(p);
}
}
let mut toreturn = Configuration::default();
populate_headers_and_auth(&mut toreturn, &parsed).await; populate_headers_and_auth(&mut toreturn, &parsed).await;
toreturn.typecfg = parsed.provider.clone(); toreturn.typecfg = parsed.provider.clone();
match parsed.provider.as_str() { match parsed.provider.as_str() {
"file" => { "file" => {
populate_file_upstreams(&mut toreturn, &parsed).await; populate_file_upstreams(&mut toreturn, &parsed).await;
Some(toreturn) (Some(toreturn), "Ok".to_string())
} }
"consul" => { "consul" => {
toreturn.consul = parsed.consul; toreturn.consul = parsed.consul;
toreturn.consul.is_some().then_some(toreturn) (toreturn.consul.is_some().then_some(toreturn), "Ok".to_string())
} }
"kubernetes" => { "kubernetes" => {
toreturn.kubernetes = parsed.kubernetes; toreturn.kubernetes = parsed.kubernetes;
toreturn.kubernetes.is_some().then_some(toreturn) (toreturn.kubernetes.is_some().then_some(toreturn), "Ok".to_string())
} }
_ => { _ => {
warn!("Unknown provider {}", parsed.provider); warn!("Unknown provider {}", parsed.provider);
None (None, "Unknown provider".to_string())
} }
} }
} }
async fn populate_headers_and_auth(config: &mut Configuration, parsed: &Config) { async fn populate_headers_and_auth(config: &mut Configuration, parsed: &Config) {
let mut ch: Vec<(Arc<str>, Arc<str>)> = Vec::new(); let mut ch: Vec<(String, Arc<str>)> = Vec::new();
ch.push((Arc::from("Server"), Arc::from("Aralez")));
// println!("{:?}", &parsed.client_headers);
if let Some(headers) = &parsed.client_headers { if let Some(headers) = &parsed.client_headers {
for header in headers { for header in headers {
if let Some((key, val)) = header.split_once(':') { if let Some((key, val)) = header.split_once(':') {
ch.push((Arc::from(key), Arc::from(val))); ch.push((key.to_string(), Arc::from(val)));
} }
} }
} }
let global_headers: DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>> = DashMap::new();
global_headers.insert(Arc::from("/"), ch);
config.client_headers.insert("GLOBAL_CLIENT_HEADERS".to_string(), global_headers);
let mut sh: Vec<(Arc<str>, Arc<str>)> = Vec::new(); let global_headers: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
sh.push((Arc::from("X-Proxy-Server"), Arc::from("Aralez"))); 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 { if let Some(headers) = &parsed.server_headers {
for header in headers { for header in headers {
if let Some((key, val)) = header.split_once(':') { if let Some((key, val)) = header.split_once(':') {
sh.push((Arc::from(key.trim()), Arc::from(val.trim()))); sh.push((key.to_string(), Arc::from(val.trim())));
} }
} }
} }
let server_global_headers: DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>> = DashMap::new(); let server_global_headers: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
server_global_headers.insert(Arc::from("/"), sh); server_global_headers.insert(Arc::from("/"), sh);
config.server_headers.insert("GLOBAL_SERVER_HEADERS".to_string(), server_global_headers); config.server_headers.insert(Arc::from("GLOBAL_SERVER_HEADERS"), server_global_headers);
config.extraparams.sticky_sessions = parsed.sticky_sessions;
config.extraparams.to_https = parsed.to_https; config.extraparams.to_https = parsed.to_https;
config.extraparams.sticky_sessions = parsed.sticky_sessions;
config.extraparams.rate_limit = parsed.rate_limit; config.extraparams.rate_limit = parsed.rate_limit;
config.extraparams.x4xx_limit = parsed.x4xx_limit;
if let Some(rate) = &parsed.rate_limit { if let Some(rate) = &parsed.rate_limit {
info!("Applied Global Rate Limit : {} request per second", rate); info!("Applied Global Rate Limit : {} request per second", rate);
} }
if let Some(auth) = &parsed.authorization { if let Some(pa) = &parsed.authorization {
let name = auth.get("type").unwrap_or(&"".to_string()).to_string(); let y: InnerAuth = InnerAuth {
let creds = auth.get("creds").unwrap_or(&"".to_string()).to_string(); auth_type: Arc::from(pa.auth_type.clone()),
config.extraparams.authentication.insert("authorization".to_string(), vec![name, creds]); auth_cred: Arc::from(pa.auth_cred.clone().unwrap_or_default()),
} else { };
config.extraparams.authentication = DashMap::new(); config.extraparams.authentication = Some(Arc::from(y));
} }
} }
@@ -122,35 +202,47 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
if let Some(rate) = &path_config.rate_limit { if let Some(rate) = &path_config.rate_limit {
info!("Applied Rate Limit for {} : {} request per second", hostname, rate); info!("Applied Rate Limit for {} : {} request per second", hostname, rate);
} }
let mut hl: Vec<(String, Arc<str>)> = Vec::new();
let mut hl: Vec<(Arc<str>, Arc<str>)> = Vec::new(); let mut sl: Vec<(String, Arc<str>)> = Vec::new();
let mut sl: Vec<(Arc<str>, Arc<str>)> = Vec::new();
build_headers(&path_config.client_headers, config, &mut hl); build_headers(&path_config.client_headers, config, &mut hl);
build_headers(&path_config.server_headers, config, &mut sl); build_headers(&path_config.server_headers, config, &mut sl);
client_header_list.insert(Arc::from(path.as_str()), hl); client_header_list.insert(Arc::from(path.as_str()), hl);
server_header_list.insert(Arc::from(path.as_str()), sl); server_header_list.insert(Arc::from(path.as_str()), sl);
let mut server_list = Vec::new(); let mut server_list = Vec::new();
for server in &path_config.servers { for server in &path_config.servers {
if let Some((ip, port_str)) = server.split_once(':') { let mut path_auth: Option<Arc<InnerAuth>> = None;
if let Some(pa) = &path_config.authorization {
let y: InnerAuth = InnerAuth {
auth_type: Arc::from(pa.auth_type.clone()),
auth_cred: Arc::from(pa.auth_cred.clone().unwrap_or_default()),
};
path_auth = Some(Arc::from(y));
}
let redirect_link = path_config.redirect_to.as_ref().map(|www| Arc::from(www.as_str()));
if let Some((ip, port_str)) = server.rsplit_once(':') {
if let Ok(port) = port_str.parse::<u16>() { if let Ok(port) = port_str.parse::<u16>() {
server_list.push(Arc::from(InnerMap { server_list.push(Arc::from(InnerMap {
address: ip.trim().parse().unwrap(), address: Arc::from(ip),
port, port,
is_ssl: true, is_ssl: false,
is_http2: false, is_http2: false,
to_https: path_config.to_https.unwrap_or(false), to_https: path_config.to_https.unwrap_or(false),
rate_limit: path_config.rate_limit, rate_limit: path_config.rate_limit,
x4xx_limit: path_config.x4xx_limit,
healthcheck: path_config.healthcheck, healthcheck: path_config.healthcheck,
redirect_to: redirect_link,
authorization: path_auth,
})); }));
} }
} }
} }
path_map.insert(path.clone(), (server_list, AtomicUsize::new(0))); path_map.insert(Arc::from(path.clone()), (server_list, AtomicUsize::new(0)));
} }
config.client_headers.insert(hostname.clone(), client_header_list); config.client_headers.insert(Arc::from(hostname.clone()), client_header_list);
config.server_headers.insert(hostname.clone(), server_header_list); config.server_headers.insert(Arc::from(hostname.clone()), server_header_list);
imtdashmap.insert(hostname.clone(), path_map); imtdashmap.insert(Arc::from(hostname.clone()), path_map);
} }
if is_first_run() { if is_first_run() {
@@ -162,31 +254,34 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
clone_dashmap_into(&r, &config.upstreams); clone_dashmap_into(&r, &config.upstreams);
} }
info!("Upstream Config:"); info!("Upstream Config:");
print_upstreams(&config.upstreams); REVERSE_STORE.clear();
print_upstreams(&config.upstreams, &config.extraparams);
} }
} }
pub fn parce_main_config(path: &str) -> AppConfig { pub fn parce_main_config(path: &str) -> AppConfig {
let data = fs::read_to_string(path).unwrap(); let data = fs::read_to_string(path).unwrap();
let reply = DashMap::new(); let mut cfo: AppConfig = noyalib::from_str(&data).expect("Failed to parse main config file");
let cfg: HashMap<String, String> = serde_yaml::from_str(&*data).expect("Failed to parse main config file"); if let Ok(jwt_key) = env::var("JWT_KEY") {
let mut cfo: AppConfig = serde_yaml::from_str(&*data).expect("Failed to parse main config file"); cfo.master_key = Some(jwt_key);
log_builder(&cfo); };
log_builder(&cfo, &cfo.log_file);
cfo.hc_method = cfo.hc_method.to_uppercase(); cfo.hc_method = cfo.hc_method.to_uppercase();
for (k, v) in cfg { if let Some((ip, port_str)) = cfo.config_address.rsplit_once(':') {
reply.insert(k.to_string(), v.to_string());
}
if let Some((ip, port_str)) = cfo.config_address.split_once(':') {
if let Ok(port) = port_str.parse::<u16>() { if let Ok(port) = port_str.parse::<u16>() {
cfo.local_server = Option::from((ip.to_string(), port)); cfo.local_server = Option::from((ip.to_string(), port));
} }
} }
if let Some(tlsport_cfg) = cfo.proxy_address_tls.clone() { if let Some(tlsport_cfg) = cfo.proxy_address_tls.clone() {
if let Some((_, port_str)) = tlsport_cfg.split_once(':') { if let Some((_, port_str)) = tlsport_cfg.rsplit_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.rsplit_once(':') {
cfo.proxy_port = Some(port_str.to_string());
}
cfo.proxy_tls_grade = parce_tls_grades(cfo.proxy_tls_grade.clone()); cfo.proxy_tls_grade = parce_tls_grades(cfo.proxy_tls_grade.clone());
cfo cfo
} }
@@ -213,43 +308,17 @@ fn parce_tls_grades(what: Option<String>) -> Option<String> {
}, },
None => { None => {
warn!("TLS grade not set, defaulting to: medium"); warn!("TLS grade not set, defaulting to: medium");
Some("b".to_string()) Some("medium".to_string())
} }
} }
} }
fn log_builder(conf: &AppConfig) { pub fn build_headers(path_config: &Option<Vec<String>>, _config: &Configuration, hl: &mut Vec<(String, Arc<str>)>) {
let log_level = conf.log_level.clone();
unsafe {
match log_level.as_str() {
"info" => env::set_var("RUST_LOG", "info"),
"error" => env::set_var("RUST_LOG", "error"),
"warn" => env::set_var("RUST_LOG", "warn"),
"debug" => env::set_var("RUST_LOG", "debug"),
"trace" => env::set_var("RUST_LOG", "trace"),
"off" => env::set_var("RUST_LOG", "off"),
_ => {
println!("Error reading log level, defaulting to: INFO");
env::set_var("RUST_LOG", "info")
}
}
}
env_logger::builder().init();
}
pub fn build_headers(path_config: &Option<Vec<String>>, _config: &Configuration, hl: &mut Vec<(Arc<str>, Arc<str>)>) {
if let Some(headers) = &path_config { if let Some(headers) = &path_config {
for header in headers { for header in headers {
if let Some((key, val)) = header.split_once(':') { if let Some((key, val)) = header.split_once(':') {
hl.push((Arc::from(key.trim()), Arc::from(val.trim()))); hl.push((key.trim().to_string(), Arc::from(val.trim())));
} }
} }
// if let Some(push) = config.client_headers.get("GLOBAL_HEADERS") {
// for k in push.iter() {
// for x in k.value() {
// hl.push(x.to_owned());
// }
// }
// }
} }
} }
+2 -14
View File
@@ -1,12 +1,11 @@
use once_cell::sync::Lazy; use std::sync::{LazyLock, RwLock};
use std::sync::RwLock;
#[derive(Debug)] #[derive(Debug)]
pub struct SharedState { pub struct SharedState {
pub first_run: bool, pub first_run: bool,
} }
pub static GLOBAL_STATE: Lazy<RwLock<SharedState>> = Lazy::new(|| RwLock::new(SharedState { first_run: true })); pub static GLOBAL_STATE: LazyLock<RwLock<SharedState>> = LazyLock::new(|| RwLock::new(SharedState { first_run: true }));
pub fn mark_not_first_run() { pub fn mark_not_first_run() {
let mut state = GLOBAL_STATE.write().unwrap(); let mut state = GLOBAL_STATE.write().unwrap();
@@ -17,14 +16,3 @@ pub fn is_first_run() -> bool {
let state = GLOBAL_STATE.read().unwrap(); let state = GLOBAL_STATE.read().unwrap();
state.first_run 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
}
}
*/
+73 -23
View File
@@ -2,51 +2,52 @@ use dashmap::DashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
pub type UpstreamsDashMap = DashMap<String, DashMap<String, (Vec<Arc<InnerMap>>, AtomicUsize)>>;
use std::net::IpAddr;
use std::sync::Arc; use std::sync::Arc;
pub type UpstreamsDashMap = DashMap<Arc<str>, DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>;
pub type UpstreamsIdMap = DashMap<String, Arc<InnerMap>>; pub type UpstreamsIdMap = DashMap<String, Arc<InnerMap>>;
pub type Headers = DashMap<String, DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>>>; pub type Headers = DashMap<Arc<str>, DashMap<Arc<str>, Vec<(String, Arc<str>)>>>;
#[derive(Clone, Debug, Default)]
pub struct Extraparams {
pub to_https: Option<bool>,
pub sticky_sessions: Option<u64>,
pub authentication: Option<Arc<InnerAuth>>,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ServiceMapping { pub struct GlobalServiceMapping {
pub redirect_to: Option<String>,
pub authorization: Option<String>,
pub upstream: String, pub upstream: String,
pub hostname: String, pub hostname: String,
pub path: Option<String>, pub path: Option<String>,
pub to_https: Option<bool>, pub to_https: Option<bool>,
pub rate_limit: Option<isize>, pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub client_headers: Option<Vec<String>>, pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>, pub server_headers: Option<Vec<String>>,
} }
// pub type Services = DashMap<String, Vec<(String, Option<String>)>>;
#[derive(Clone, Debug, Default)]
pub struct Extraparams {
pub sticky_sessions: bool,
pub to_https: Option<bool>,
pub authentication: DashMap<String, Vec<String>>,
pub rate_limit: Option<isize>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)] #[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Kubernetes { pub struct Kubernetes {
pub servers: Option<Vec<String>>, pub servers: Option<Vec<String>>,
pub services: Option<Vec<ServiceMapping>>,
pub tokenpath: Option<String>, pub tokenpath: Option<String>,
} }
#[derive(Clone, Default, Debug, Serialize, Deserialize)] #[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Consul { pub struct Consul {
pub servers: Option<Vec<String>>, pub servers: Option<Vec<String>>,
pub services: Option<Vec<ServiceMapping>>,
pub token: Option<String>, pub token: Option<String>,
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub provider: String, pub provider: String,
pub sticky_sessions: bool,
pub to_https: Option<bool>, pub to_https: Option<bool>,
pub sticky_sessions: Option<u64>,
#[serde(default)] #[serde(default)]
pub upstreams: Option<HashMap<String, HostConfig>>, pub upstreams: Option<HashMap<String, HostConfig>>,
#[serde(default)] #[serde(default)]
@@ -56,21 +57,29 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub server_headers: Option<Vec<String>>, pub server_headers: Option<Vec<String>>,
#[serde(default)] #[serde(default)]
pub authorization: Option<HashMap<String, String>>, pub authorization: Option<Auth>,
#[serde(default)] #[serde(default)]
pub consul: Option<Consul>, pub consul: Option<Consul>,
#[serde(default)] #[serde(default)]
pub kubernetes: Option<Kubernetes>, pub kubernetes: Option<Kubernetes>,
#[serde(default)] #[serde(default)]
pub rate_limit: Option<isize>, pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
} }
#[derive(Debug, Default, 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 rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Auth {
#[serde(rename = "type")]
pub auth_type: String,
#[serde(rename = "data")]
pub auth_cred: Option<String>,
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct PathConfig { pub struct PathConfig {
pub servers: Vec<String>, pub servers: Vec<String>,
@@ -78,7 +87,10 @@ pub struct PathConfig {
pub client_headers: Option<Vec<String>>, pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>, pub server_headers: Option<Vec<String>>,
pub rate_limit: Option<isize>, pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub healthcheck: Option<bool>, pub healthcheck: Option<bool>,
pub redirect_to: Option<String>,
pub authorization: Option<Auth>,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Configuration { pub struct Configuration {
@@ -97,7 +109,11 @@ pub struct AppConfig {
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: String, pub cache_size_mb: Option<usize>,
pub cache_ttl: Option<u64>,
pub access_log: Option<String>,
pub pid_file: Option<String>,
pub master_key: Option<String>,
pub config_address: String, pub config_address: String,
pub proxy_address_http: String, pub proxy_address_http: String,
pub config_api_enabled: bool, pub config_api_enabled: bool,
@@ -105,38 +121,72 @@ pub struct AppConfig {
pub config_tls_certificate: Option<String>, pub config_tls_certificate: Option<String>,
pub config_tls_key_file: Option<String>, pub config_tls_key_file: Option<String>,
pub proxy_address_tls: Option<String>, pub proxy_address_tls: Option<String>,
pub proxy_port_tls: Option<u16>, pub proxy_port_tls: Option<String>,
pub proxy_port: Option<String>,
pub local_server: Option<(String, u16)>, pub local_server: Option<(String, u16)>,
pub proxy_certificates: Option<String>, pub proxy_configs: Option<String>,
pub proxy_tls_grade: Option<String>, pub proxy_tls_grade: Option<String>,
pub file_server_address: Option<String>, pub file_server_address: Option<String>,
pub file_server_folder: Option<String>, pub file_server_folder: Option<String>,
pub runuser: Option<String>, pub runuser: Option<String>,
pub rungroup: Option<String>, pub rungroup: Option<String>,
pub log_file: Option<String>,
pub tcp_keepalive_idle: Option<u64>,
pub tcp_keepalive_interval: Option<u64>,
pub tcp_keepalive_count: Option<usize>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct InnerAuth {
pub auth_type: Arc<str>,
pub auth_cred: Arc<str>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InnerMap { pub struct InnerMap {
pub address: IpAddr, pub address: Arc<str>,
pub port: u16, pub port: u16,
pub is_ssl: bool, pub is_ssl: bool,
pub is_http2: bool, pub is_http2: bool,
pub to_https: bool, pub to_https: bool,
pub rate_limit: Option<isize>, pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub healthcheck: Option<bool>, pub healthcheck: Option<bool>,
pub redirect_to: Option<Arc<str>>,
pub authorization: Option<Arc<InnerAuth>>,
} }
#[allow(dead_code)] #[allow(dead_code)]
impl InnerMap { impl InnerMap {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
address: "127.0.0.1".parse().unwrap(), address: Arc::from("127.0.0.1"),
port: Default::default(), port: Default::default(),
is_ssl: Default::default(), is_ssl: Default::default(),
is_http2: Default::default(), is_http2: Default::default(),
to_https: Default::default(), to_https: Default::default(),
rate_limit: Default::default(), rate_limit: Default::default(),
x4xx_limit: Default::default(),
healthcheck: 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,
}
+193 -65
View File
@@ -1,48 +1,52 @@
use crate::utils::structs::{InnerMap, UpstreamsDashMap, UpstreamsIdMap}; use crate::tls::load;
use crate::utils::tls; use crate::tls::load::CertificateConfig;
use crate::utils::tls::CertificateConfig; use crate::utils::structs::{Extraparams, InnerMapForJson, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap};
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info}; use log::{error, info};
use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use port_check::is_port_reachable;
use privdrop::PrivDrop; use privdrop::PrivDrop;
use serde_json::{json, Value};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::any::type_name; use std::any::type_name;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt::Write; use std::fmt::Write;
use std::fs::OpenOptions;
use std::io::Write as IoWrite;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::net::TcpListener;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::OpenOptionsExt;
use std::str::FromStr; use std::str::FromStr;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{channel, Sender}; use std::sync::mpsc::{channel, Sender};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{fs, process, thread, time}; 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() { for f in path_entry.value().0.clone() {
println!( writeln!(
" IP: {}, Port: {}, SSL: {}, H2: {}, To HTTPS: {}, Rate Limit: {}", out,
" IP: {}, Port: {}, SSL: {}, H2: {}, To HTTPS: {}, Rate Limit: {}, 4xx Limit: {}",
f.address, f.address,
f.port, f.port,
f.is_ssl, f.is_ssl,
f.is_http2, f.is_http2,
f.to_https, f.to_https,
f.rate_limit.unwrap_or(0) f.rate_limit.unwrap_or(extraparams.rate_limit.unwrap_or(0)),
); f.x4xx_limit.unwrap_or(extraparams.x4xx_limit.unwrap_or(0))
)
.unwrap();
} }
} }
} }
info!("\n{}", out.trim_end());
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn typeoff<T>(_: T) { pub fn typeoff<T>(_: T) {
let to = type_name::<T>(); let to = type_name::<T>();
@@ -100,29 +104,26 @@ pub fn clone_dashmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsDashMap
} }
pub fn compare_dashmaps(map1: &UpstreamsDashMap, map2: &UpstreamsDashMap) -> bool { pub fn compare_dashmaps(map1: &UpstreamsDashMap, map2: &UpstreamsDashMap) -> bool {
let keys1: HashSet<_> = map1.iter().map(|entry| entry.key().clone()).collect(); if map1.len() != map2.len() {
let keys2: HashSet<_> = map2.iter().map(|entry| entry.key().clone()).collect();
if keys1 != keys2 {
return false; return false;
} }
for entry1 in map1.iter() { for entry1 in map1.iter() {
let hostname = entry1.key(); let Some(inner_map2) = map2.get(entry1.key()) else {
let inner_map1 = entry1.value();
let Some(inner_map2) = map2.get(hostname) else {
return false; return false;
}; };
let inner_keys1: HashSet<_> = inner_map1.iter().map(|e| e.key().clone()).collect(); let inner_map1 = entry1.value();
let inner_keys2: HashSet<_> = inner_map2.iter().map(|e| e.key().clone()).collect(); if inner_map1.len() != inner_map2.len() {
if inner_keys1 != inner_keys2 {
return false; return false;
} }
for path_entry in inner_map1.iter() { for path_entry in inner_map1.iter() {
let path = path_entry.key(); let Some(entry2) = inner_map2.get(path_entry.key()) else {
let (vec1, _counter1) = path_entry.value(); return false;
let Some(entry2) = inner_map2.get(path) else {
return false; // Path exists in map1 but not in map2
}; };
let (vec2, _counter2) = entry2.value(); let (vec1, _) = path_entry.value();
let (vec2, _) = entry2.value();
if vec1.len() != vec2.len() {
return false;
}
let set1: HashSet<_> = vec1.iter().collect(); let set1: HashSet<_> = vec1.iter().collect();
let set2: HashSet<_> = vec2.iter().collect(); let set2: HashSet<_> = vec2.iter().collect();
if set1 != set2 { if set1 != set2 {
@@ -133,11 +134,11 @@ pub fn compare_dashmaps(map1: &UpstreamsDashMap, map2: &UpstreamsDashMap) -> boo
true true
} }
pub fn merge_headers(target: &DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>>, source: &DashMap<Arc<str>, Vec<(Arc<str>, Arc<str>)>>) { pub fn merge_headers(target: &DashMap<Arc<str>, Vec<(String, Arc<str>)>>, source: &DashMap<Arc<str>, Vec<(String, Arc<str>)>>) {
for entry in source.iter() { for entry in source.iter() {
let global_key = entry.key().clone(); let global_key = entry.key().clone();
let global_values = entry.value().clone(); let global_values = entry.value().clone();
let mut target_entry = target.entry(global_key).or_insert_with(Vec::new); let mut target_entry = target.entry(global_key).or_default();
target_entry.extend(global_values); target_entry.extend(global_values);
} }
} }
@@ -146,39 +147,41 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) {
cloned.clear(); cloned.clear();
for outer_entry in original.iter() { for outer_entry in original.iter() {
let inner_map = outer_entry.value(); let inner_map = outer_entry.value();
let new_inner_map = DashMap::new();
for inner_entry in inner_map.iter() { for inner_entry in inner_map.iter() {
let path = inner_entry.key();
let (vec, _) = inner_entry.value(); let (vec, _) = inner_entry.value();
let new_vec = vec.clone();
for x in vec.iter() { for x in vec.iter() {
let mut id = String::new(); let mut id = String::new();
write!(&mut id, "{}:{}:{}", x.address, x.port, x.is_ssl).unwrap(); write!(
&mut id,
"{}:{}:{}:{}:{}:{}:{}:{}:{:?}",
outer_entry.key(),
x.address,
x.port,
x.is_http2,
x.to_https,
x.x4xx_limit.unwrap_or_default(),
x.rate_limit.unwrap_or_default(),
x.healthcheck.unwrap_or_default(),
x.authorization
)
.unwrap_or(());
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
// address: "127.0.0.3", port: 8000, is_ssl: false, is_http2: false, to_https: false, rate_limit: Some(200), healthcheck: None, authorization: None } }
hasher.update(id.clone().into_bytes()); hasher.update(id.clone().into_bytes());
let hash = hasher.finalize(); let hash = hasher.finalize();
let hex_hash = base16ct::lower::encode_string(&hash); let hex_hash = base16ct::lower::encode_string(&hash);
let hh = hex_hash[0..50].to_string(); let hh = hex_hash[0..50].to_string();
let to_add = InnerMap { cloned.insert(hh, x.to_owned());
address: "127.0.0.1".parse().unwrap(),
port: 0,
is_ssl: false,
is_http2: false,
to_https: false,
rate_limit: None,
healthcheck: None,
};
cloned.insert(id, Arc::from(to_add));
cloned.insert(hh, Arc::from(x.to_owned()));
}
new_inner_map.insert(path.clone(), new_vec);
} }
} }
} }
info!("Upstreams are fully populated. Ready to server requests");
}
pub fn listdir(dir: String) -> Vec<tls::CertificateConfig> { pub fn listdir(dir: String) -> Vec<load::CertificateConfig> {
let mut f = HashMap::new(); let mut f = HashMap::new();
let mut certificate_configs: Vec<tls::CertificateConfig> = vec![]; let mut certificate_configs: Vec<load::CertificateConfig> = vec![];
let paths = fs::read_dir(dir).unwrap(); let paths = fs::read_dir(dir).unwrap();
for path in paths { for path in paths {
let path_str = path.unwrap().path().to_str().unwrap().to_owned(); let path_str = path.unwrap().path().to_str().unwrap().to_owned();
@@ -196,13 +199,13 @@ pub fn listdir(dir: String) -> Vec<tls::CertificateConfig> {
certificate_configs.push(y); certificate_configs.push(y);
} }
} }
for (_, v) in f.iter() { // for (_, v) in f.iter() {
let y = CertificateConfig { // let y = CertificateConfig {
cert_path: v[0].clone(), // cert_path: v[0].clone(),
key_path: v[1].clone(), // key_path: v[1].clone(),
}; // };
certificate_configs.push(y); // certificate_configs.push(y);
} // }
certificate_configs certificate_configs
} }
@@ -237,14 +240,14 @@ pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Optio
thread::sleep(time::Duration::from_millis(10)); thread::sleep(time::Duration::from_millis(10));
loop { loop {
thread::sleep(time::Duration::from_millis(10)); thread::sleep(time::Duration::from_millis(10));
if is_port_reachable(http_addr.clone()) { if TcpListener::bind(&http_addr).is_err() {
break; break;
} }
} }
if let Some(tls_addr) = tls_addr { if let Some(tls_addr) = tls_addr {
loop { loop {
thread::sleep(time::Duration::from_millis(10)); thread::sleep(time::Duration::from_millis(10));
if is_port_reachable(tls_addr.clone()) { if TcpListener::bind(&tls_addr).is_err() {
break; break;
} }
} }
@@ -257,15 +260,140 @@ pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Optio
} }
pub fn check_priv(addr: &str) { pub fn check_priv(addr: &str) {
let port = SocketAddr::from_str(addr).map(|sa| sa.port()).unwrap(); let port = SocketAddr::from_str(addr).map(|sa| sa.port()).expect("Failed to parse address port ");
match port < 1024 { if port < 1024 {
true => {
let meta = std::fs::metadata("/proc/self").map(|m| m.uid()).unwrap(); let meta = std::fs::metadata("/proc/self").map(|m| m.uid()).unwrap();
if meta != 0 { if meta != 0 {
error!("Running on privileged port requires to start as ROOT"); error!("Running on privileged port requires to start as ROOT");
process::exit(1) process::exit(1)
} }
} }
false => {} }
#[allow(dead_code)]
pub fn upstreams_to_json(upstreams: &UpstreamsDashMap) -> serde_json::Result<String> {
let mut outer = HashMap::new();
for outer_entry in upstreams.iter() {
let mut inner_map = HashMap::new();
for inner_entry in outer_entry.value().iter() {
let (backends, counter) = inner_entry.value();
inner_map.insert(
inner_entry.key().to_string(),
UpstreamSnapshotForJson {
backends: backends
.iter()
.map(|a| InnerMapForJson {
address: a.address.to_string(),
port: a.port,
is_ssl: a.is_ssl,
is_http2: a.is_http2,
to_https: a.to_https,
rate_limit: a.rate_limit,
x4xx_limit: a.x4xx_limit,
healthcheck: a.healthcheck,
})
.collect(),
requests: counter.load(Ordering::Relaxed),
},
);
}
outer.insert(outer_entry.key().to_string(), inner_map);
}
// serde_json::to_string_pretty(&outer)
serde_json::to_string(&outer)
}
pub fn upstreams_liveness_json(configured: &UpstreamsDashMap, current: &UpstreamsDashMap) -> Value {
let mut result = serde_json::Map::new();
for host_entry in configured.iter() {
let hostname = host_entry.key().to_string();
let configured_paths = host_entry.value();
let mut paths_json = serde_json::Map::new();
for path_entry in configured_paths.iter() {
let path = path_entry.key().clone();
let (configured_backends, _) = path_entry.value();
let backends_json: Vec<Value> = configured_backends
.iter()
.map(|backend| {
let alive = if let Some(host_map) = current.get(&*hostname) {
if let Some(path_entry) = host_map.get(&*path) {
let list = &path_entry.value().0; // Vec<Arc<InnerMap>>
list.iter().any(|b| b.address == backend.address && b.port == backend.port)
} else {
false
}
} else {
false
};
json!({
"address": &*backend.address,
"port": backend.port,
"alive": alive
})
})
.collect();
paths_json.insert(
path.to_string(),
json!({
"backends": backends_json
}),
);
}
result.insert(hostname, Value::Object(paths_json));
}
Value::Object(result)
}
#[allow(dead_code)]
pub fn prepend(prefix: &str, val: &Option<Arc<str>>, uri: &str, port: &str) -> Option<String> {
val.as_ref().map(|s| {
let mut buf = String::with_capacity(32);
buf.push_str(prefix);
buf.push_str(s);
buf.push(':');
buf.push_str(port);
buf.push_str(uri);
buf
})
}
pub fn write_pid_file(path: &str) -> std::io::Result<()> {
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.custom_flags(libc::O_NOFOLLOW) // refuse to follow symlinks
.open(path)?;
file.write_all(process::id().to_string().as_bytes())?;
Ok(())
}
#[allow(clippy::needless_return)]
pub 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));
} }
} }
};
}
+2
View File
@@ -1,5 +1,7 @@
pub mod acme;
pub mod bgservice; pub mod bgservice;
pub mod gethosts; pub mod gethosts;
pub mod logging;
pub mod proxyhttp; pub mod proxyhttp;
pub mod start; pub mod start;
pub mod webserver; pub mod webserver;
+60
View File
@@ -0,0 +1,60 @@
use crate::tls::acme::{account, order};
use crate::utils::lazylock::CHALLENGES;
use axum::body::Body;
use axum::extract::State;
use axum::http::{Response, StatusCode};
use axum::response::IntoResponse;
#[allow(clippy::needless_return)]
pub async fn acme_create(State(state): State<crate::web::webserver::AppState>) -> impl IntoResponse {
match account::load_or_create(state.cert_creds.as_str()).await {
Ok(txt) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap()
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to create account: {}", e)))
.unwrap()
}
};
}
#[allow(clippy::needless_return)]
pub async fn acme_order(State(state): State<crate::web::webserver::AppState>, axum::extract::Path(domain): axum::extract::Path<String>) -> impl IntoResponse {
let domain_clean = domain.trim_matches('/');
match order::order(domain_clean, state.cert_creds.as_str(), state.certs_dir).await {
Ok(txt) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap()
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to order a certificate: {}", e)))
.unwrap()
}
};
}
pub async fn http01_challenge(axum::extract::Path(token): axum::extract::Path<String>) -> impl IntoResponse {
if let Ok(challenges) = CHALLENGES.read() {
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()
}
+31 -25
View File
@@ -1,26 +1,29 @@
use crate::tls::acme::order::refresh_order;
use crate::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider}; use crate::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider};
use crate::utils::metrics::calc_cache_metrics;
use crate::utils::parceyaml::load_configuration; 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::*;
use crate::web::logging::init_logging;
use crate::web::proxyhttp::LB; 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::{SinkExt, StreamExt};
use log::{error, 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;
use tokio::sync::mpsc;
#[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"); // tx: Sender<Configuration> info!("Starting background service"); // tx: Sender<Configuration>
let (mut tx, mut rx) = mpsc::channel::<Configuration>(1); let (tx, mut rx) = mpsc::channel::<Configuration>(1);
let tx_api = tx.clone(); let tx_api = tx.clone();
let config = load_configuration(self.config.upstreams_conf.clone().as_str(), "filepath") let config = load_configuration(self.config.upstreams_conf.clone().as_str(), "filepath")
.await .await
.0
.expect("Failed to load configuration"); .expect("Failed to load configuration");
match config.typecfg.as_str() { match config.typecfg.as_str() {
@@ -30,64 +33,73 @@ impl BackgroundService for LB {
let file_load = FromFileProvider { let file_load = FromFileProvider {
path: self.config.upstreams_conf.clone(), path: self.config.upstreams_conf.clone(),
}; };
let _ = tokio::spawn(async move { file_load.start(tx).await }); // let _ = tokio::spawn(async move { file_load.start(tx).await });
drop(tokio::spawn(async move { file_load.start(tx).await }));
} }
"kubernetes" => { "kubernetes" => {
info!("Running Kubernetes discovery, requested type is: {}", config.typecfg); info!("Running Kubernetes discovery, requested type is: {}", config.typecfg);
let cf = Arc::from(config); let cf = Arc::from(config);
let kuber_load = KubernetesProvider { config: cf.clone() }; let kuber_load = KubernetesProvider { config: cf.clone() };
let _ = tokio::spawn(async move { kuber_load.start(tx).await }); drop(tokio::spawn(async move { kuber_load.start(tx).await }));
} }
"consul" => { "consul" => {
info!("Running Consul discovery, requested type is: {}", config.typecfg); info!("Running Consul discovery, requested type is: {}", config.typecfg);
let cf = Arc::from(config); let cf = Arc::from(config);
let consul_load = ConsulProvider { config: cf.clone() }; let consul_load = ConsulProvider { config: cf.clone() };
let _ = tokio::spawn(async move { consul_load.start(tx).await }); drop(tokio::spawn(async move { consul_load.start(tx).await }));
} }
_ => { _ => {
error!("Unknown discovery type: {}", config.typecfg); error!("Unknown discovery type: {}", config.typecfg);
} }
} }
let confdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/autoconfigs";
let certdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/certificates";
let api_load = APIUpstreamProvider { let api_load = APIUpstreamProvider {
address: self.config.config_address.clone(), address: self.config.config_address.clone(),
masterkey: self.config.master_key.clone(), masterkey: self.config.master_key.clone(),
config_api_enabled: self.config.config_api_enabled.clone(), config_api_enabled: self.config.config_api_enabled,
tls_address: self.config.config_tls_address.clone(), upstreams_file: self.config.upstreams_conf.clone(),
tls_certificate: self.config.config_tls_certificate.clone(), config_dir: confdir.clone(),
tls_key_file: self.config.config_tls_key_file.clone(), certs_dir: certdir.clone(),
file_server_address: self.config.file_server_address.clone(), file_server_address: self.config.file_server_address.clone(),
file_server_folder: self.config.file_server_folder.clone(), file_server_folder: self.config.file_server_folder.clone(),
current_upstreams: self.ump_upst.clone(),
full_upstreams: self.ump_full.clone(),
}; };
// let tx_api = tx.clone(); drop(tokio::spawn(async move { api_load.start(tx_api).await }));
let _ = 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 { calc_cache_metrics().await }));
drop(tokio::spawn(async move { refresh_order(certdir, confdir).await }));
init_logging(self.config.access_log.clone());
loop { loop {
tokio::select! { tokio::select! {
_ = shutdown.changed() => { _ = shutdown.changed() => {
break; break;
} }
val = rx.next() => { val = rx.recv() => {
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);
clone_idmap_into(&ss.upstreams, &self.ump_byid);
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.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.client_headers.clear(); self.client_headers.clear();
self.server_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 client_global_values = DashMap::new(); let client_global_values = DashMap::new();
@@ -99,7 +111,6 @@ impl BackgroundService for LB {
server_target_entry.extend(server_global_values); server_target_entry.extend(server_global_values);
self.server_headers.insert(server_target_entry.key().to_owned(), server_target_entry.value().to_owned()); self.server_headers.insert(server_target_entry.key().to_owned(), server_target_entry.value().to_owned());
} }
for path in ss.client_headers.iter() { 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();
@@ -110,7 +121,6 @@ impl BackgroundService for LB {
} }
} }
} }
for path in ss.server_headers.iter() { for path in ss.server_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();
@@ -121,10 +131,6 @@ impl BackgroundService for LB {
} }
} }
} }
// info!("Upstreams list is changed, updating to:");
// print_upstreams(&self.ump_full);
}
None => {}
} }
} }
} }
+52 -30
View File
@@ -1,40 +1,46 @@
use crate::utils::structs::InnerMap; use crate::utils::structs::InnerMap;
use crate::web::proxyhttp::LB; use crate::web::proxyhttp::LB;
use async_trait::async_trait; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct GetHostsReturHeaders { pub struct GetHostsReturHeaders {
pub client_headers: Option<Vec<(Arc<str>, Arc<str>)>>, pub client_headers: Option<Vec<(String, Arc<str>)>>,
pub server_headers: Option<Vec<(Arc<str>, Arc<str>)>>, pub server_headers: Option<Vec<(String, Arc<str>)>>,
} }
#[async_trait]
pub trait GetHost { pub trait GetHost {
// fn get_host<'a>(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<&'a InnerMap>; fn find_sticky_backend(&self, servers: &[Arc<InnerMap>], backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn pick_backend(&self, servers: &[Arc<InnerMap>], index: &AtomicUsize, backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>>; fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders>; fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders>;
} }
#[async_trait]
impl GetHost for LB { impl GetHost for LB {
fn find_sticky_backend(&self, servers: &[Arc<InnerMap>], backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
let b = backend_id?;
let bb = self.ump_byid.get(b)?;
let target = bb.value();
servers.iter().any(|s| s.address == target.address && s.port == target.port).then(|| target.clone())
}
fn pick_backend(&self, servers: &[Arc<InnerMap>], index: &AtomicUsize, backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
if servers.is_empty() {
return None;
}
if let Some(target) = self.find_sticky_backend(servers, backend_id) {
return Some(target);
}
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len();
Some(servers[idx].clone())
}
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>> { fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
if let Some(b) = backend_id {
if let Some(bb) = self.ump_byid.get(b) {
return Some(bb.value().clone());
}
}
let host_entry = self.ump_upst.get(peer)?; let host_entry = self.ump_upst.get(peer)?;
let mut end = path.len(); let mut end = path.len();
loop { loop {
let slice = &path[..end]; let slice = &path[..end];
if let Some(entry) = host_entry.get(slice) { if let Some(entry) = host_entry.get(slice) {
let (servers, index) = entry.value(); let (servers, index) = entry.value();
if !servers.is_empty() { if let Some(backend) = self.pick_backend(servers, index, backend_id) {
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len(); return Some(backend);
return Some(servers[idx].clone());
} }
} }
if let Some(pos) = slice.rfind('/') { if let Some(pos) = slice.rfind('/') {
@@ -45,19 +51,22 @@ impl GetHost for LB {
} }
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 let Some(backend) = self.pick_backend(servers, index, backend_id) {
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len(); return Some(backend);
return Some(servers[idx].clone());
} }
} }
None None
} }
fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders> { fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders> {
let client_entry = self.client_headers.get(peer)?; let client_entry = self.client_headers.get(peer);
let server_entry = self.server_headers.get(peer)?; let server_entry = self.server_headers.get(peer);
if client_entry.is_none() && server_entry.is_none() {
return None;
}
let mut current_path = path; let mut current_path = path;
let mut clnt_match = None; let mut clnt_match = None;
if let Some(client_entry) = client_entry {
loop { loop {
if let Some(entry) = client_entry.get(current_path) { if let Some(entry) = client_entry.get(current_path) {
if !entry.value().is_empty() { if !entry.value().is_empty() {
@@ -65,14 +74,19 @@ impl GetHost for LB {
break; break;
} }
} }
if current_path == "/" {
break;
}
if let Some(pos) = current_path.rfind('/') { if let Some(pos) = current_path.rfind('/') {
current_path = if pos == 0 { "/" } else { &current_path[..pos] }; current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else { } else {
break; break;
} }
} }
}
current_path = path; current_path = path;
let mut serv_match = None; let mut serv_match = None;
if let Some(server_entry) = server_entry {
loop { loop {
if let Some(entry) = server_entry.get(current_path) { if let Some(entry) = server_entry.get(current_path) {
if !entry.value().is_empty() { if !entry.value().is_empty() {
@@ -80,23 +94,31 @@ impl GetHost for LB {
break; break;
} }
} }
if let Some(pos) = current_path.rfind('/') { if current_path == "/" {
current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else {
break;
}
if serv_match.is_none() {
if let Some(entry) = server_entry.get("/") { if let Some(entry) = server_entry.get("/") {
if !entry.value().is_empty() { if !entry.value().is_empty() {
serv_match = Some(entry.value().clone()); serv_match = Some(entry.value().clone());
break; break;
} }
} }
break;
}
if let Some(pos) = current_path.rfind('/') {
current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else {
break;
} }
} }
Some(GetHostsReturHeaders { }
let result = GetHostsReturHeaders {
client_headers: clnt_match, client_headers: clnt_match,
server_headers: serv_match, server_headers: serv_match,
}) };
if result.client_headers.is_some() || result.server_headers.is_some() {
Some(result)
} else {
None
}
} }
} }
+176
View File
@@ -0,0 +1,176 @@
use crate::utils::metrics::LOGGING_ERRORS;
use crate::utils::structs::AppConfig;
use log::{info, LevelFilter};
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::{
append::console::ConsoleAppender,
config::{Appender, Config as Log4rsConfig, Root},
encode::pattern::PatternEncoder,
};
use pingora_cache::CachePhase;
use pingora_http::Version;
use pingora_proxy::Session;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::OnceLock;
use tokio::sync::mpsc;
#[derive(Debug)]
pub struct LogMessage {
pub response_code: u16,
pub summary: String,
pub client_ip: IpAddr,
pub version: Version,
pub user_agent: String,
pub cache_status: CachePhase,
}
static LOG_SENDER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
static ACCESS_LOG: OnceLock<LogLevel> = OnceLock::new();
const LOG_BUFFER: usize = 16384;
pub fn log_builder(conf: &AppConfig, location: &Option<String>) {
let log_level = match conf.log_level.as_str() {
"info" => LevelFilter::Info,
"error" => LevelFilter::Error,
"warn" => LevelFilter::Warn,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
"off" => LevelFilter::Off,
_ => {
println!("Error reading log level, defaulting to: INFO");
LevelFilter::Info
}
};
let pattern = "{d(%Y-%m-%d %H:%M:%S)} {l} {t} - {m}\n";
if let Some(location) = location {
let parts: Vec<&str> = location.splitn(4, ',').map(|s| s.trim()).collect();
let path = parts.get(0).expect("Syntax error, could not get path for log files");
let compress = parts.get(3).unwrap_or(&"No");
let size_mb: u64 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(100);
let keep: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(5);
let pattern_str = match compress {
&"compress" => format!("{}.{{}}.gz", path),
_ => format!("{}.{{}}", path),
};
let roller = FixedWindowRoller::builder().build(pattern_str.as_str(), keep).unwrap();
let trigger = SizeTrigger::new(size_mb * 1024 * 1024);
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
let file = RollingFileAppender::builder()
.encoder(Box::new(PatternEncoder::new(pattern)))
.build(path, Box::new(policy))
.unwrap();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("file", Box::new(file)))
.build(Root::builder().appender("file").build(log_level))
.unwrap();
log4rs::init_config(config).unwrap();
info!("Logging to: {}, Max file size: {}mb, Files to keep: {}, compression: {} ", path, size_mb, keep, compress);
} else {
let stdout = ConsoleAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.build(Root::builder().appender("stdout").build(log_level))
.unwrap();
log4rs::init_config(config).unwrap();
info!("No files are configured, logging to stdout");
}
}
pub fn init_access_log(level_str: &str) {
let level = LogLevel::from_str(level_str);
let _ = ACCESS_LOG.set(level);
}
#[derive(Debug)]
pub enum LogLevel {
Access,
Error,
None,
}
impl LogLevel {
pub fn from_str(s: &str) -> Self {
match s {
"all" => LogLevel::Access,
"error" => LogLevel::Error,
_ => LogLevel::None,
}
}
}
pub fn access_log(response_code: u16, summary: &str, session: &Session) {
let level = ACCESS_LOG.get().unwrap_or(&LogLevel::None);
let should_log = match level {
LogLevel::Access => true,
LogLevel::None => false,
LogLevel::Error => !(100..=399).contains(&response_code),
};
if !should_log {
return;
}
let ip = session
.client_addr()
.and_then(|addr| addr.as_inet())
.map(|addr| addr.ip())
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
let user_agent = session.req_header().headers.get("user-agent").and_then(|v| v.to_str().ok()).unwrap_or("-");
// let cache_status = match session.cache.phase() {
// CachePhase::Hit => "Hit",
// CachePhase::Miss => "Miss",
// CachePhase::Expired => "Expired",
// _ => "Disabled",
// };
let log = LogMessage {
response_code,
summary: summary.to_owned(),
client_ip: ip,
version: session.req_header().version,
user_agent: user_agent.to_owned(),
cache_status: session.cache.phase(),
};
if let Some(sender) = LOG_SENDER.get() {
let sender = sender;
if let Err(_) = sender.try_send(log) {
LOGGING_ERRORS.inc();
}
}
}
pub fn init_logging(enabled: Option<String>) {
if let Some(_) = enabled {
LOGGING_ERRORS.set(0);
info!("Enabling {:?} log, with buffer of {} messages", ACCESS_LOG.get().unwrap_or(&LogLevel::None), LOG_BUFFER);
let (ltx, lrx) = mpsc::channel(LOG_BUFFER);
LOG_SENDER.set(ltx).unwrap();
std::thread::spawn(move || log_receiver(lrx));
}
}
pub fn log_receiver(mut receiver: mpsc::Receiver<LogMessage>) {
while let Some(msg) = receiver.blocking_recv() {
info!(
"{}, {}, {}, client: {}, version: {:?}, useragent: {}",
msg.response_code,
msg.cache_status.as_str(),
msg.summary,
msg.client_ip,
msg.version,
msg.user_agent,
);
}
}
+222 -121
View File
@@ -1,26 +1,29 @@
use crate::utils::auth::authenticate; use crate::utils::auth::authenticate;
use crate::utils::lazylock::{CACHE_LOCK, CACHE_TTL, EVICTION, LOCALHOST, MEM_CACHE, RATE_LIMITER, REQUESTS_4XX, REVERSE_STORE};
use crate::utils::metrics::*; use crate::utils::metrics::*;
use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap}; use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::web::gethosts::GetHost; use crate::web::gethosts::{GetHost, GetHostsReturHeaders};
use crate::web::logging::access_log;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use async_trait::async_trait; use async_trait::async_trait;
use axum::body::Bytes; use axum::body::Bytes;
use log::{debug, error, warn}; use log::error;
use once_cell::sync::Lazy; use pingora::http::{Method, RequestHeader, ResponseHeader, StatusCode};
use pingora::http::{RequestHeader, ResponseHeader, StatusCode};
use pingora::prelude::*; use pingora::prelude::*;
use pingora::ErrorSource::Upstream; use pingora::ErrorSource::Upstream;
use pingora_cache::cache_control::CacheControl;
use pingora_cache::{CacheKey, CacheMetaDefaults, RespCacheable};
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::net::{IpAddr, Ipv4Addr}; use sha2::{Digest, Sha256};
use std::cell::RefCell;
use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::time::Instant; use tokio::time::Instant;
static RATE_LIMITER: Lazy<Rate> = Lazy::new(|| Rate::new(Duration::from_secs(1))); thread_local! {static IP_BUFFER: RefCell<String> = RefCell::new(String::with_capacity(50));}
#[derive(Clone)] #[derive(Clone)]
pub struct LB { pub struct LB {
pub ump_upst: Arc<UpstreamsDashMap>, pub ump_upst: Arc<UpstreamsDashMap>,
@@ -30,18 +33,17 @@ pub struct LB {
pub server_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 cache_enabled: bool,
} }
pub struct Context { pub struct Context {
backend_id: Arc<str>, backend_id: Option<String>,
// backend_id: Arc<(IpAddr, u16, bool)>,
to_https: bool,
redirect_to: Arc<str>,
start_time: Instant, start_time: Instant,
hostname: Option<Arc<str>>, hostname: Option<Arc<str>>,
upstream_peer: Option<Arc<InnerMap>>, upstream_peer: Option<Arc<InnerMap>>,
extraparams: arc_swap::Guard<Arc<Extraparams>>, extraparams: arc_swap::Guard<Arc<Extraparams>>,
client_headers: Arc<Vec<(Arc<str>, Arc<str>)>>, client_headers: Option<Vec<(String, Arc<str>)>>,
x4xx_limit: Option<u32>,
} }
#[async_trait] #[async_trait]
@@ -49,71 +51,133 @@ impl ProxyHttp for LB {
type CTX = Context; type CTX = Context;
fn new_ctx(&self) -> Self::CTX { fn new_ctx(&self) -> Self::CTX {
Context { Context {
backend_id: Arc::from(""), backend_id: None,
// backend_id: Arc::new((IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0, false)),
to_https: false,
redirect_to: Arc::from(""),
start_time: Instant::now(), start_time: Instant::now(),
hostname: None, hostname: None,
upstream_peer: None, upstream_peer: None,
extraparams: self.extraparams.load(), extraparams: self.extraparams.load(),
client_headers: Arc::new(Vec::new()), client_headers: None,
x4xx_limit: None,
} }
} }
fn cache_key_callback(&self, session: &Session, _ctx: &mut Self::CTX) -> Result<CacheKey> {
Ok(CacheKey::new(_ctx.hostname.clone().unwrap_or_default().as_ref(), session.req_header().uri.to_string(), ""))
}
fn request_cache_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<()> {
if self.cache_enabled && session.req_header().method == Method::GET {
if let Some(eviction) = EVICTION.get() {
session.cache.enable(&*MEM_CACHE, Some(eviction), None, Some(&*CACHE_LOCK), None);
}
}
Ok(())
}
fn response_cache_filter(&self, _session: &Session, resp: &ResponseHeader, _ctx: &mut Self::CTX) -> Result<RespCacheable> {
let cc = CacheControl::from_resp_headers(resp);
let defaults = CacheMetaDefaults::new(
|status| {
if status == StatusCode::OK {
Some(Duration::from_secs(*CACHE_TTL.get().unwrap_or(&60)))
} else {
None
}
},
0,
0,
);
Ok(pingora_cache::filters::resp_cacheable(cc.as_ref(), resp.clone(), false, &defaults))
}
async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> { async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> {
let ep = _ctx.extraparams.as_ref(); ACTIVE_SESSIONS.inc();
let hostname = return_header_host_from_upstream(session, &self.ump_upst);
if let Some(auth) = ep.authentication.get("authorization") {
let authenticated = authenticate(&auth.value(), &session);
if !authenticated {
let _ = session.respond_error(401).await;
warn!("Forbidden: {:?}, {}", session.client_addr(), session.req_header().uri.path());
return Ok(true);
}
};
let hostname = return_header_host(&session);
_ctx.hostname = hostname; _ctx.hostname = hostname;
let mut backend_id = None; let mut backend_id = None;
if let Some(_) = _ctx.extraparams.sticky_sessions {
if ep.sticky_sessions {
if let Some(cookies) = session.req_header().headers.get("cookie") { if let Some(cookies) = session.req_header().headers.get("cookie") {
if let Ok(cookie_str) = cookies.to_str() { if let Ok(cookie_str) = cookies.to_str() {
for cookie in cookie_str.split(';') { if let Some(pos) = cookie_str.find("backend_id=") {
let trimmed = cookie.trim(); let value = &cookie_str[pos + "backend_id=".len()..];
if let Some(value) = trimmed.strip_prefix("backend_id=") { let end = value.find(';').unwrap_or(value.len());
backend_id = Some(value); backend_id = Some(&value[..end]);
break;
} }
} }
} }
} }
}
match _ctx.hostname.as_ref() { match _ctx.hostname.as_ref() {
None => return Ok(false), None => return Ok(false),
Some(host) => { Some(host) => {
// let optioninnermap = self.get_host(host.as_str(), host.as_str(), backend_id);
let optioninnermap = self.get_host(host, session.req_header().uri.path(), backend_id); let optioninnermap = self.get_host(host, session.req_header().uri.path(), backend_id);
match optioninnermap { match optioninnermap {
None => return Ok(false), None => return Ok(false),
Some(ref innermap) => { Some(ref innermap) => {
if let Some(rate) = innermap.rate_limit.or(ep.rate_limit) { if let Some(auth) = _ctx.extraparams.authentication.as_ref().or(innermap.authorization.as_ref()) {
// let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip().to_string()).unwrap_or_else(|| host.to_string()); if !authenticate(&auth, session).await {
let _ = session.respond_error(401).await;
return Ok(true);
}
}
if let Some(rate) = innermap.x4xx_limit.or(_ctx.extraparams.x4xx_limit) {
_ctx.x4xx_limit = innermap.x4xx_limit;
let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip());
if let Some(rk) = rate_key {
let count = REQUESTS_4XX.get(&rk).unwrap_or(0);
if count > rate {
let header = ResponseHeader::build(429, None)?;
session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?;
return Ok(true);
}
}
}
if let Some(rate) = innermap.rate_limit.or(_ctx.extraparams.rate_limit) {
let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip()); let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip());
let curr_window_requests = RATE_LIMITER.observe(&rate_key, 1); let curr_window_requests = RATE_LIMITER.observe(&rate_key, 1);
if curr_window_requests > rate { if curr_window_requests > rate {
let mut header = ResponseHeader::build(429, None).unwrap(); let header = ResponseHeader::build(429, None)?;
header.insert_header("X-Rate-Limit-Limit", rate.to_string()).unwrap();
header.insert_header("X-Rate-Limit-Remaining", "0").unwrap();
header.insert_header("X-Rate-Limit-Reset", "1").unwrap();
session.set_keepalive(None); session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?; session.write_response_header(Box::new(header), true).await?;
debug!("Rate limited: {:?}, {}", rate_key, rate);
return Ok(true); return Ok(true);
} }
} }
if let Some(redirect_to) = &innermap.redirect_to {
let uri = session.req_header().uri.path();
let capacity = redirect_to.len() + uri.len();
let mut s = String::with_capacity(capacity);
s.push_str(redirect_to);
s.push_str(uri);
let mut resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
resp.insert_header("Location", s)?;
resp.insert_header("Content-Length", "0")?;
session.write_response_header(Box::new(resp), true).await?;
return Ok(true);
}
if _ctx.extraparams.to_https.unwrap_or(false) || innermap.to_https {
if let Some(stream) = session.stream() {
if stream.get_ssl().is_none() {
if let Some(host) = _ctx.hostname.as_ref() {
let port = self.config.proxy_port_tls.as_deref().unwrap_or("443");
let uri = session.req_header().uri.path();
let capacity = host.len() + uri.len() + 8;
let mut s = String::with_capacity(capacity);
s.push_str("https://");
s.push_str(host);
if port != "443" {
s.push(':');
s.push_str(port);
}
s.push_str(uri);
let mut resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
resp.insert_header("Location", s)?;
resp.insert_header("Content-Length", "0")?;
session.write_response_header(Box::new(resp), true).await?;
return Ok(true);
}
}
}
}
} }
} }
_ctx.upstream_peer = optioninnermap; _ctx.upstream_peer = optioninnermap;
@@ -125,36 +189,46 @@ impl ProxyHttp for LB {
match ctx.hostname.as_ref() { match ctx.hostname.as_ref() {
Some(hostname) => match ctx.upstream_peer.as_ref() { Some(hostname) => match ctx.upstream_peer.as_ref() {
Some(innermap) => { Some(innermap) => {
let mut peer = Box::new(HttpPeer::new((innermap.address, innermap.port), innermap.is_ssl, String::new())); let mut peer = Box::new(HttpPeer::new((&*innermap.address, innermap.port), innermap.is_ssl, hostname.to_string()));
if innermap.is_http2 { if innermap.is_http2 && !session.is_upgrade_req() {
peer.options.alpn = ALPN::H2; peer.options.alpn = ALPN::H2;
} }
if innermap.is_ssl { if innermap.is_ssl {
peer.sni = hostname.to_string();
peer.options.verify_cert = false; peer.options.verify_cert = false;
peer.options.verify_hostname = false; peer.options.verify_hostname = false;
} }
if ctx.to_https || innermap.to_https { /*
if let Some(stream) = session.stream() { Experimental optionsv
if stream.get_ssl().is_none() { The following TCP optimizations were tested but caused performance degrade under heavy load:
if let Some(host) = ctx.hostname.as_ref() { peer.options.tcp_keepalive = Some(TcpKeepalive {
let uri = session.req_header().uri.path_and_query().map_or("/", |pq| pq.as_str()); idle: Duration::from_secs(60),
let port = self.config.proxy_port_tls.unwrap_or(403); interval: Duration::from_secs(10),
ctx.to_https = true; count: 5,
ctx.redirect_to = Arc::from(format!("https://{}:{}{}", host, port, uri)); user_timeout: Duration::from_secs(30),
});
peer.options.idle_timeout = Some(Duration::from_secs(300));
peer.options.tcp_recv_buf = Some(128 * 1024);
End of experimental options
*/
if let Some(_) = ctx.extraparams.sticky_sessions {
let mut s = String::with_capacity(64);
write!(
&mut s,
"{}:{}:{}:{}:{}:{}:{}:{}:{:?}",
hostname,
innermap.address,
innermap.port,
innermap.is_http2,
innermap.to_https,
innermap.x4xx_limit.unwrap_or_default(),
innermap.rate_limit.unwrap_or_default(),
innermap.healthcheck.unwrap_or_default(),
innermap.authorization
)
.unwrap_or(());
ctx.backend_id = Some(s);
} }
// if let Some(addr) = session.server_addr() {
// if let Some((host, _)) = addr.to_string().split_once(':') {
// let uri = session.req_header().uri.path_and_query().map_or("/", |pq| pq.as_str());
// let port = self.config.proxy_port_tls.unwrap_or(403);
// ctx.to_https = true;
// ctx.redirect_to = Arc::from(format!("https://{}:{}{}", host, port, uri));
// }
// }
}
}
}
ctx.backend_id = Arc::from(format!("{}:{}:{}", innermap.address, innermap.port, innermap.is_ssl));
Ok(peer) Ok(peer)
} }
None => { None => {
@@ -171,7 +245,6 @@ impl ProxyHttp for LB {
} }
}, },
None => { None => {
// session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await.expect("Failed to send error");
if let Err(e) = session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await { if let Err(e) = session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await {
error!("Failed to send error response: {:?}", e); error!("Failed to send error response: {:?}", e);
} }
@@ -187,75 +260,103 @@ impl ProxyHttp for LB {
} }
async fn upstream_request_filter(&self, session: &mut Session, upstream_request: &mut RequestHeader, ctx: &mut Self::CTX) -> Result<()> { async fn upstream_request_filter(&self, session: &mut Session, upstream_request: &mut RequestHeader, ctx: &mut Self::CTX) -> Result<()> {
if let Some(hostname) = ctx.hostname.as_ref() { if let Some(ip) = session.client_addr().and_then(|a| a.as_inet()).map(|i| i.ip()) {
upstream_request.insert_header("Host", hostname.as_ref())?; IP_BUFFER.with(|buffer| {
} let mut buf = buffer.borrow_mut();
if let Some(peer) = ctx.upstream_peer.as_ref() { buf.clear();
upstream_request.insert_header("X-Forwarded-For", peer.address.to_string())?; write!(buf, "{}", ip).unwrap_or(());
upstream_request.append_header("X-Forwarded-For", buf.as_str()).unwrap_or(false);
});
} }
if let Some(headers) = self.get_header(ctx.hostname.as_ref().unwrap_or(&Arc::from("localhost")), session.req_header().uri.path()) { let hostname = ctx.hostname.as_deref().unwrap_or("localhost");
if let Some(server_headers) = headers.server_headers { let path = session.req_header().uri.path();
for k in server_headers { let GetHostsReturHeaders { server_headers, client_headers } = match self.get_header(hostname, path) {
upstream_request.insert_header(k.0.to_string(), k.1.as_ref())?; Some(h) => h,
} None => return Ok(()),
} };
if let Some(client_headers) = headers.client_headers {
let converted: Vec<(Arc<str>, Arc<str>)> = client_headers.into_iter().map(|(k, v)| (Arc::<str>::from(k), Arc::<str>::from(v))).collect();
ctx.client_headers = Arc::new(converted); if let Some(sh) = server_headers {
for (k, v) in sh {
upstream_request.insert_header(k, v.as_ref())?;
} }
} }
if let Some(ch) = client_headers {
ctx.client_headers = Some(ch);
}
Ok(()) Ok(())
} }
async fn response_filter(&self, session: &mut Session, _upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> { async fn response_filter(&self, _session: &mut Session, _upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> {
if ctx.extraparams.sticky_sessions { if let Some(val) = ctx.extraparams.sticky_sessions {
if let Some(bid) = self.ump_byid.get(ctx.backend_id.as_ref()) { if let Some(bid) = &ctx.backend_id {
let _ = _upstream_response.insert_header("set-cookie", format!("backend_id={}; Path=/; Max-Age=600; HttpOnly; SameSite=Lax", bid.address)); let tt = if let Some(existing) = REVERSE_STORE.get(bid) {
existing.value().clone()
} else {
let mut hasher = Sha256::new();
hasher.update(bid.as_bytes());
let hash = hasher.finalize();
let hex_hash = base16ct::lower::encode_string(&hash);
let hh = hex_hash[0..50].to_string();
REVERSE_STORE.insert(bid.clone(), hh.clone());
REVERSE_STORE.insert(hh.clone(), bid.clone());
hh
};
let mut buf = String::with_capacity(80);
buf.push_str("backend_id=");
buf.push_str(&tt);
buf.push_str("; Path=/; Max-Age=");
buf.push_str(&val.to_string());
buf.push_str("; HttpOnly; SameSite=Lax");
let _ = _upstream_response.append_header("set-cookie", buf.as_str());
}
}
Ok(())
}
async fn upstream_response_filter(&self, _session: &mut Session, upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> {
if let Some(client_headers) = &ctx.client_headers {
for (k, v) in client_headers.iter() {
upstream_response.append_header(k.clone(), v.as_ref())?;
} }
} }
if ctx.to_https {
let mut redirect_response = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
redirect_response.insert_header("Location", ctx.redirect_to.as_ref())?;
redirect_response.insert_header("Content-Length", "0")?;
session.write_response_header(Box::new(redirect_response), false).await?;
}
for (key, value) in ctx.client_headers.iter() {
_upstream_response.insert_header(key.to_string(), value.as_ref()).unwrap();
}
session.set_keepalive(Some(300));
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));
let m = &MetricTypes { let m = &MetricTypes {
method: session.req_header().method.to_string(), method: session.req_header().method.clone(),
code: session.response_written().map(|resp| resp.status.as_str().to_owned()).unwrap_or("0".to_string()), code: session.response_written().map(|resp| resp.status),
latency: ctx.start_time.elapsed(), latency: ctx.start_time.elapsed(),
version: session.req_header().version, version: session.req_header().version,
upstream: ctx.hostname.take().unwrap_or_else(|| LOCALHOST.clone()),
}; };
calc_metrics(m); calc_metrics(m);
ACTIVE_SESSIONS.dec();
if let Some(_) = ctx.x4xx_limit.or(ctx.extraparams.x4xx_limit) {
if (400..=499).contains(&response_code) {
if let Some(ip) = session.client_addr().and_then(|a| a.as_inet()).map(|i| i.ip()) {
let current = REQUESTS_4XX.get(&ip).unwrap_or(0);
REQUESTS_4XX.insert(ip, current + 1);
}
}
}
// match session.cache.phase() {
// CachePhase::Hit => info!("Cache Status: HIT (Served from Memory)"),
// CachePhase::Miss => info!("Cache Status: MISS (Fetched from Upstream)"),
// CachePhase::Expired => info!("Cache Status: EXPIRED (Revalidating)"),
// _ => {}
// }
access_log(response_code, &self.request_summary(session, ctx), session);
} }
} }
fn return_header_host(session: &Session) -> Option<Arc<str>> { fn return_header_host_from_upstream(session: &Session, ump_upst: &UpstreamsDashMap) -> Option<Arc<str>> {
if session.is_http2() { let host_str = if session.is_http2() {
match session.req_header().uri.host() { session.req_header().uri.host()?
Some(host) => Option::from(Arc::from(host)),
None => None,
}
} else { } else {
match session.req_header().headers.get("host") { let h = session.req_header().headers.get("host")?.to_str().ok()?;
Some(host) => { h.split_once(':').map_or(h, |(host, _)| host)
let header_host: &str = host.to_str().unwrap().split_once(':').map_or(host.to_str().unwrap(), |(h, _)| h); };
Option::from(Arc::<str>::from(header_host))
} ump_upst.get(host_str).or_else(|| ump_upst.get("DEFAULT")).map(|entry| entry.key().clone())
None => None,
}
}
} }
+92 -31
View File
@@ -1,26 +1,45 @@
// use rustls::crypto::ring::default_provider; use crate::tls::grades;
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::lazylock::{CACHE_TTL, EVICTION};
use crate::utils::structs::Extraparams; use crate::utils::structs::Extraparams;
use crate::utils::tls;
use crate::utils::tls::CertificateConfig;
use crate::utils::tools::*; use crate::utils::tools::*;
use crate::web::logging::init_access_log;
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::tls::ssl::{SslAlert, SslRef};
use pingora_cache::eviction::simple_lru::Manager;
use pingora_core::listeners::tls::TlsSettings; use pingora_core::listeners::tls::TlsSettings;
use pingora_core::listeners::TcpSocketOptions;
use pingora_core::prelude::{background_service, Opt}; use pingora_core::prelude::{background_service, Opt};
use pingora_core::protocols::TcpKeepalive;
use pingora_core::server::Server; use pingora_core::server::Server;
use privdrop::reexports::libc::SIGQUIT;
use rustls::crypto::ring::default_provider;
use sd_notify::NotifyState;
use signal_hook::{
consts::{SIGINT, SIGTERM},
iterator::Signals,
};
use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc; use std::sync::Arc;
use std::thread; use std::time::Duration;
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());
let cache_enabled = maincfg.cache_size_mb.is_some();
CACHE_TTL.set(maincfg.cache_ttl.unwrap_or(60)).unwrap();
if let Some(size) = maincfg.cache_size_mb {
let cache_size = size * 1024 * 1024;
drop(EVICTION.set(Manager::new(cache_size)));
}
let mut server = Server::new(parameters).unwrap(); let mut server = Server::new(parameters).unwrap();
server.bootstrap(); server.bootstrap();
@@ -31,10 +50,11 @@ pub fn run() {
let sh_config = Arc::new(DashMap::new()); let sh_config = Arc::new(DashMap::new());
let ec_config = Arc::new(ArcSwap::from_pointee(Extraparams { let ec_config = Arc::new(ArcSwap::from_pointee(Extraparams {
sticky_sessions: false,
to_https: None, to_https: None,
authentication: DashMap::new(), sticky_sessions: None,
authentication: None,
rate_limit: None, rate_limit: None,
x4xx_limit: None,
})); }));
let cfg = Arc::new(maincfg); let cfg = Arc::new(maincfg);
@@ -47,72 +67,113 @@ pub fn run() {
client_headers: ch_config, client_headers: ch_config,
server_headers: sh_config, server_headers: sh_config,
extraparams: ec_config, extraparams: ec_config,
cache_enabled: cache_enabled,
}; };
let al = cfg.access_log.clone().unwrap_or("none".to_string());
init_access_log(al.as_str());
let grade = cfg.proxy_tls_grade.clone().unwrap_or("medium".to_string()); let grade = cfg.proxy_tls_grade.clone().unwrap_or("medium".to_string());
info!("TLS grade set to: [ {} ]", grade); info!("TLS grade set to: [ {} ]", grade);
let bg_srvc = background_service("bgsrvc", lb.clone()); let bg_srvc = background_service("bgsrvc", lb.clone());
let mut proxy = pingora_proxy::http_proxy_service(&server.configuration, lb.clone());
let bind_address_http = cfg.proxy_address_http.clone(); let bind_address_http = cfg.proxy_address_http.clone();
let bind_address_tls = cfg.proxy_address_tls.clone(); let bind_address_tls = cfg.proxy_address_tls.clone();
let mut proxy = pingora_proxy::http_proxy_service(&server.configuration, lb.clone());
check_priv(bind_address_http.as_str()); check_priv(bind_address_http.as_str());
match bind_address_tls { // let mut tcp_options: Option<TcpSocketOptions> = Some(TcpSocketOptions::default());
Some(bind_address_tls) => { // let mut tcp_options = TcpSocketOptions::default();
let mut tcp_options: Option<TcpSocketOptions> = None;
if let Some(idle) = cfg.tcp_keepalive_idle {
let mut to = TcpSocketOptions::default();
to.tcp_keepalive = Some(TcpKeepalive {
idle: Duration::from_secs(idle),
interval: Duration::from_secs(cfg.tcp_keepalive_interval.unwrap_or(10)),
user_timeout: Default::default(),
count: cfg.tcp_keepalive_count.unwrap_or(5usize),
});
tcp_options = Some(to);
info!(
"Applying kernel tcp_keepalive parameters: idle {}, interval {}, count {}",
idle,
cfg.tcp_keepalive_interval.unwrap_or(60),
cfg.tcp_keepalive_count.unwrap_or(5),
);
}
if let Some(bind_address_tls) = bind_address_tls {
check_priv(bind_address_tls.as_str()); check_priv(bind_address_tls.as_str());
let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel(); let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel();
let certs_path = cfg.proxy_certificates.clone().unwrap(); let certs_path = cfg.proxy_configs.clone().unwrap() + "/certificates";
if fs::metadata(certs_path.clone()).is_err() {
fs::create_dir_all(certs_path.clone()).unwrap();
}
thread::spawn(move || { thread::spawn(move || {
watch_folder(certs_path, tx).unwrap(); watch_folder(certs_path, tx).unwrap();
}); });
let certificate_configs = rx.recv().unwrap(); let certificate_configs = rx.recv().unwrap();
let first_set = tls::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info")); let 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 certificates = Arc::new(ArcSwap::from_pointee(first_set));
let certs_for_callback = certificates.clone(); let certs_for_callback = certificates.clone();
let certs_for_watcher = certificates.clone(); let certs_for_watcher = certificates.clone();
let new_certs = tls::Certificates::new(&certificate_configs, grade.as_str()); let new_certs = load::Certificates::new(&certificate_configs, grade.as_str());
certs_for_watcher.store(Arc::new(new_certs.unwrap())); certs_for_watcher.store(Arc::new(new_certs.unwrap()));
let mut tls_settings = 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"); TlsSettings::intermediate(&certs_for_callback.load().default_cert_path, &certs_for_callback.load().default_key_path).expect("unable to load or parse cert/key");
tls::set_tsl_grade(&mut tls_settings, grade.as_str()); 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_servername_callback(move |ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert| certs_for_callback.load().server_name_callback(ssl_ref, ssl_alert));
tls_settings.set_alpn_select_callback(tls::prefer_h2); tls_settings.set_alpn_select_callback(grades::prefer_h2);
proxy.add_tls_with_settings(&bind_address_tls, None, tls_settings); proxy.add_tls_with_settings(&bind_address_tls, tcp_options.clone(), tls_settings);
let certs_for_watcher = certificates.clone(); let certs_for_watcher = certificates.clone();
thread::spawn(move || { thread::spawn(move || {
while let Ok(new_configs) = rx.recv() { while let Ok(new_configs) = rx.recv() {
let new_certs = tls::Certificates::new(&new_configs, grade.as_str()); let new_certs = load::Certificates::new(&new_configs, grade.as_str());
match new_certs { if let Some(new_certs) = new_certs {
Some(new_certs) => {
certs_for_watcher.store(Arc::new(new_certs)); certs_for_watcher.store(Arc::new(new_certs));
}
None => {}
}; };
} }
}); });
} }
None => {} info!("Running HTTP listener on :{}", bind_address_http);
if let Some(tc) = tcp_options {
proxy.add_tcp_with_settings(&bind_address_http, tc);
} else {
proxy.add_tcp(&bind_address_http)
} }
info!("Running HTTP listener on :{}", 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);
thread::spawn(move || server.run_forever()); thread::spawn(move || server.run_forever());
if let (Some(user), Some(group)) = (cfg.rungroup.clone(), cfg.runuser.clone()) { 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()); drop_priv(user, group, cfg.proxy_address_http.clone(), cfg.proxy_address_tls.clone());
} }
let _ = sd_notify::notify(&[NotifyState::Ready]);
let (tx, rx) = channel(); let pf = cfg.pid_file.clone().unwrap_or("/tmp/aralez.pid".to_string());
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel.")).expect("Error setting Ctrl-C handler"); if let Err(e) = write_pid_file(pf.as_str()) {
rx.recv().expect("Could not receive from channel."); panic!("Failed to write PID file: {} : {}", pf, e);
info!("Signal received ! Exiting..."); }
let mut signals = Signals::new(&[SIGINT, SIGTERM, SIGQUIT]).unwrap();
for sig in signals.forever() {
match sig {
SIGINT => info!("SIGINT received! Exiting..."),
SIGTERM => info!("SIGTERM received! Exiting..."),
SIGQUIT => {
thread::sleep(Duration::from_secs(300));
info!("SIGQUIT received! Exiting...")
}
_ => unreachable!(),
}
break;
}
} }
+169 -65
View File
@@ -1,117 +1,161 @@
use crate::utils::discovery::APIUpstreamProvider; use crate::utils::discovery::APIUpstreamProvider;
use crate::utils::structs::Configuration; use crate::utils::jwt::Claims;
use crate::utils::metrics::{get_memory_usage, get_open_files, MEMORY_USAGE, OPEN_FILES};
use crate::utils::structs::{Config, Configuration, UpstreamsDashMap};
use crate::utils::tools::{upstreams_liveness_json, upstreams_to_json};
use crate::web::acme::{acme_create, acme_order, http01_challenge};
use axum::body::Body; use axum::body::Body;
use axum::extract::{Query, 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::{get, post}; use axum::routing::{any, get, post};
use axum::{Json, Router}; use axum::{Json, Router};
use axum_server::tls_openssl::OpenSSLConfig;
use futures::channel::mpsc::Sender;
use futures::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 signal_hook::{consts::SIGQUIT, iterator::Signals};
use std::collections::HashMap; use std::collections::HashMap;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::str::FromStr;
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 tokio::sync::mpsc;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
#[derive(Deserialize)]
struct InputKey {
master_key: String,
owner: String,
valid: u64,
}
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
struct OutToken { struct OutToken {
token: String, token: String,
} }
#[derive(Clone)] #[derive(Clone)]
struct AppState { pub(crate) struct AppState {
master_key: String, master_key: Option<String>,
config_sender: Sender<Configuration>, pub(crate) cert_creds: String,
pub(crate) certs_dir: String,
upstreams_file: String,
config_sender: mpsc::Sender<Configuration>,
config_api_enabled: bool, config_api_enabled: bool,
current_upstreams: Arc<UpstreamsDashMap>,
full_upstreams: Arc<UpstreamsDashMap>,
} }
#[allow(unused_mut)] #[allow(unused_mut)]
pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Configuration>) { pub async fn run_server(config: &APIUpstreamProvider, mut to_return: mpsc::Sender<Configuration>, upstreams_curr: Arc<UpstreamsDashMap>, upstreams_full: Arc<UpstreamsDashMap>) {
let credsfile = config.config_dir.clone() + "/acme_credentials.json";
let app_state = AppState { let app_state = AppState {
master_key: config.masterkey.clone(), master_key: config.masterkey.clone(),
cert_creds: credsfile,
certs_dir: config.certs_dir.clone(),
upstreams_file: config.upstreams_file.clone(),
config_sender: to_return.clone(), config_sender: to_return.clone(),
config_api_enabled: config.config_api_enabled.clone(), config_api_enabled: config.config_api_enabled,
current_upstreams: upstreams_curr,
full_upstreams: upstreams_full,
}; };
let app = Router::new() let app = Router::new()
// .route("/{*wildcard}", get(senderror)) // .route("/{*wildcard}", get(senderror))
// .route("/{*wildcard}", post(senderror))
// .route("/{*wildcard}", put(senderror))
// .route("/{*wildcard}", head(senderror))
// .route("/{*wildcard}", delete(senderror))
// .nest_service("/static", static_files)
.route("/jwt", post(jwt_gen)) .route("/jwt", post(jwt_gen))
.route("/acme_create", any(acme_create))
.route("/acme_order/{*domain}", any(acme_order))
.route("/.well-known/acme-challenge/{*token}", any(http01_challenge))
.route("/conf", post(conf)) .route("/conf", post(conf))
.route("/metrics", get(metrics)) .route("/metrics", get(metrics))
.route("/status", get(status))
.with_state(app_state); .with_state(app_state);
if let Some(value) = &config.tls_address { let mut static_handle: Option<tokio::task::JoinHandle<()>> = None;
let cf = OpenSSLConfig::from_pem_file(config.tls_certificate.clone().unwrap(), config.tls_key_file.clone().unwrap()).unwrap();
let addr: SocketAddr = value.parse().expect("Unable to parse socket address");
let tls_app = app.clone();
tokio::spawn(async move {
if let Err(e) = axum_server::bind_openssl(addr, cf).serve(tls_app.into_make_service()).await {
eprintln!("TLS server failed: {}", e);
}
});
info!("Starting the TLS API server on: {}", value);
}
if let (Some(address), Some(folder)) = (&config.file_server_address, &config.file_server_folder) { if let (Some(address), Some(folder)) = (&config.file_server_address, &config.file_server_folder) {
let static_listen = port_is_available("File Server", &address).await;
let static_files = ServeDir::new(folder); let static_files = ServeDir::new(folder);
let static_serve: Router = Router::new().fallback_service(static_files); let static_serve: Router = Router::new().fallback_service(static_files);
let static_listen = TcpListener::bind(address).await.unwrap(); info!("Starting file server on: {}", address);
let _ = tokio::spawn(async move { axum::serve(static_listen, static_serve).await.unwrap() });
static_handle = Some(tokio::spawn(async move {
match axum::serve(static_listen, static_serve).await {
Ok(o) => info!("File server exited normally: {:?}", o),
Err(e) => error!("File server exited: {}", e),
}
}))
// let a = config.file_server_address.to_owned();
// let f = config.file_server_folder.to_owned();
// drop(tokio::spawn(async move { run_file_server(a, f).await }));
} }
let listener = TcpListener::bind(config.address.clone()).await.unwrap(); let listener = port_is_available("Config API", &config.address).await;
info!("Starting the API server on: {}", config.address); info!("Starting the API server on: {}", config.address);
axum::serve(listener, app).await.unwrap(); let api_server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
let (tx, mut rx) = mpsc::channel(1);
std::thread::spawn(move || {
let mut signals = Signals::new(&[SIGQUIT]).unwrap();
for sig in signals.forever() {
tx.blocking_send(sig).unwrap();
break;
}
});
rx.recv().await;
api_server.abort();
if let Some(handle) = static_handle {
handle.abort();
}
info!("Exiting...");
} }
async fn conf(State(mut st): State<AppState>, Query(params): Query<HashMap<String, String>>, content: String) -> impl IntoResponse { async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>, content: String) -> impl IntoResponse {
if !st.config_api_enabled { if !st.config_api_enabled {
return Response::builder() return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config API is disabled !\n")).unwrap();
.status(StatusCode::FORBIDDEN)
.body(Body::from("Config remote API is disabled !\n"))
.unwrap();
} }
if let Some(s) = params.get("key") { let strcontent = content.as_str();
if s.to_owned() == st.master_key { let parsed = noyalib::from_str::<Config>(strcontent);
if let Some(serverlist) = crate::utils::parceyaml::load_configuration(content.as_str(), "content").await { match parsed {
st.config_sender.send(serverlist).await.unwrap(); Ok(_) => {
return Response::builder().status(StatusCode::OK).body(Body::from("Config, conf file, updated !\n")).unwrap(); if let Some(_) = params.get("save") {
drop(tokio::spawn(async move { apply_config(content.as_str(), st, true).await }));
} else { } else {
return Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("Failed to parse config!\n")).unwrap(); drop(tokio::spawn(async move { apply_config(content.as_str(), st, false).await }));
}; }
Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap()
}
Err(err) => {
error!("Failed to parse upstreams file: {}", err);
Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from(format!("Failed: {}\n", err))).unwrap()
} }
} }
Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Access Denied !\n")).unwrap()
} }
async fn jwt_gen(State(state): State<AppState>, Json(payload): Json<InputKey>) -> (StatusCode, Json<OutToken>) { async fn apply_config(content: &str, st: AppState, save: bool) {
if payload.master_key == state.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) => {
@@ -127,12 +171,21 @@ async fn jwt_gen(State(state): State<AppState>, 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
@@ -141,7 +194,6 @@ async fn metrics() -> impl IntoResponse {
.body(Body::from(format!("Failed to encode metrics: {}", e))) .body(Body::from(format!("Failed to encode metrics: {}", e)))
.unwrap(); .unwrap();
} }
Response::builder() Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header("Content-Type", encoder.format_type()) .header("Content-Type", encoder.format_type())
@@ -149,7 +201,59 @@ async fn metrics() -> impl IntoResponse {
.unwrap() .unwrap()
} }
// #[allow(dead_code)] #[allow(clippy::needless_return)]
// async fn senderror() -> impl IntoResponse { async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>) -> impl IntoResponse {
// Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("No live upstream found!\n")).unwrap() if params.contains_key("live") {
let r = upstreams_liveness_json(&st.full_upstreams, &st.current_upstreams);
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(format!("{}", r)))
.unwrap();
}
if params.contains_key("all") {
let resp = upstreams_to_json(&st.current_upstreams);
match resp {
Ok(j) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(j))
.unwrap()
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to get status: {}", e)))
.unwrap();
}
}
}
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from("Parameter mismatch"))
.unwrap()
}
pub async fn port_is_available(name: &str, address: &str) -> TcpListener {
let addr = SocketAddr::from_str(address).unwrap_or_else(|e| panic!("{}: Invalid address format: {:?}", name, e));
let t = Duration::from_secs(2);
//if addr.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED) {
// addr.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST));
//} //}
let p = addr.port();
loop {
match TcpListener::bind(addr).await {
Ok(listener) => {
return listener;
}
Err(_) => {
warn!("{} port is not available: {} will try again in {:?}", name, p, t);
tokio::time::sleep(t).await;
}
}
}
}
// -- ⚝ by Dave -- in NeoVim ⚝ --