129 Commits

Author SHA1 Message Date
d1e5581eea Merge pull request #449 from tobychui/v3.1.6
- Exposed log file, sys.uuid and static web server path to start flag
- Optimized connection close implementation
- Added toggle for uptime monitor
- Added optional copy HTTP custom headers to websocket connection
2024-12-31 21:49:41 +08:00
be5797c8a5 Updated geodb and minor instructions 2024-12-31 21:47:19 +08:00
ebd316a7f1 Exposed log and db filepath setting 2024-12-31 21:14:37 +08:00
84aec4387a Added CF and Fastly IP in access list
Added CF and Fastly Client IP passthrough header for access control ip resolver
2024-12-31 20:30:36 +08:00
30dfb9cb65 Added new UI feature
- Added toggle for uptime monitor
- Added toggle for enable custom header passthrough to websocket
2024-12-30 21:41:15 +08:00
0b1768ab5b Added manual toggle for websocket header copy
- Added setting for toggling websocket header copy
- Added close connection in TLS mode
2024-12-30 21:07:29 +08:00
ad4721820b Added websocket header test and benchmark tool 2024-12-30 21:01:45 +08:00
1d4c275db3 Fixed nil pointer exception in new setups 2024-12-29 16:11:00 +08:00
b3ad97743c Fixed #444
- Restored legacy behavior if proxmox cookie is detected in request
2024-12-29 15:09:24 +08:00
1a6a87e79b Merge pull request #443 from Morethanevil/main
Update CHANGELOG.md
2024-12-28 15:19:43 +08:00
749fd4b7af Update CHANGELOG.md 2024-12-28 05:25:00 +01:00
85422c0a74 Merge pull request #439 from tobychui/v3.1.5
Fixed hostname case sensitive bug
Fixed ACME table too wide css bug
Fixed HSTS toggle button bug
Fixed slow GeoIP resolve mode concurrent r/w bug
Added close connection as default site option
Added experimental authelia support
Added custom header support to websocket
Added levelDB as database implementation (not currently used)
Added external GeoIP db loading support
Restructured a lot of modules
2024-12-27 22:12:55 +08:00
73999c1ae9 Merge pull request #440 from PassiveLemon/docker-3.1.5
Add 2 new flags to Docker container and image build instructions
2024-12-27 21:26:18 +08:00
0ad84b3415 Add 2 new flags 2024-12-26 16:17:02 -05:00
64b6769695 Added external geoip db option
- Added support for loading geoip db from external file
- Added -update_geoip flag for automatically update the geoip
2024-12-24 21:12:26 +08:00
e72b2f9e09 Updated geoip database 2024-12-24 20:34:10 +08:00
992dd231f2 Fixed #435 2024-12-22 13:25:16 +08:00
49555c1191 Fixed #430
+ Added no response and I'm a Teapot (config file editing only) to default site options
2024-12-17 22:08:32 +08:00
2fca458bd0 Image building instructions and README touch-ups 2024-12-16 18:14:02 -05:00
2423d0fb3a Added experimental authelia support
- Integrated #33 code snippet
- Added UI for setting Authelia server address
- Updated authentication provider implementation
2024-12-15 15:52:59 +08:00
bb0f55018c System arch optimization
- Optimized types and definitions
- Moved shutdown seq to start.go file
- Moved authelia to auth/sso module
- Added different auth types support (wip)
- Updated proxy config structure
- Added v3.1.4 to v3.1.5 auto upgrade utilities
- Fixed #426
- Optimized status page UI
- Added options to disable uptime montior in config
2024-12-12 20:49:53 +08:00
9e95d84627 Fixed #422
- Added scroll to acme domain table
2024-12-10 21:13:26 +08:00
e73841786b Merge pull request #421 from 7brend7/authelia-integration
Add authelia-verify support
2024-12-10 21:02:58 +08:00
d5449c947a Add authelia-verify suppport 2024-12-09 15:19:07 +02:00
8ff51044bb Fixed #414
- Added sticky menu
- Optimized terminate routine for nil check
- Added test case for statistic module
2024-12-08 12:54:50 +08:00
cc08c704de Database update
- Removed read-only mode
- Added LevelDB for big data storage

TODO: Update backup utilities to support new db structure
2024-12-06 23:34:21 +08:00
2f1a6b5ba4 Merge pull request #416 from tobychui/main
Sync update from main branch
2024-12-06 19:46:51 +08:00
4d163fe80f Merge pull request #406 from Sickjuicy/main
Domain Name Server Option
2024-12-06 19:29:29 +08:00
24371ed22e Fixed #415
- Fixed UI issue on the HSTS toggle
- Added error message on save error for HSTS
2024-12-06 19:06:59 +08:00
12358d3522 added downward compality and spaces are cut from the json 2024-12-01 16:01:23 +01:00
c39af1ff8e Update def.go
Updated version number
2024-12-01 21:22:43 +08:00
6bf944e13c Fixed #401
- Fixed high concurrency panic on slow geoIP resolve mode
- Added test case for concurrent geodb access
2024-12-01 21:21:53 +08:00
b653b805b8 Update autorenew.go 2024-12-01 04:29:29 +01:00
eb91865b70 Added to read json for the renew cert and fixed bug where on creation of a new cert the old NameServer ware used 2024-12-01 04:25:01 +01:00
57e72a8a90 added some commands back 2024-11-30 04:38:29 +01:00
4dbf110edc more Cleanup 2024-11-30 04:20:39 +01:00
1eefa99b72 Cleanup 2024-11-30 03:35:30 +01:00
e6b2d458f7 Added Custom Name Server Option 2024-11-26 23:30:24 +01:00
4a4483e09d Merge pull request #400 from Morethanevil/main
Update CHANGELOG.md
2024-11-24 20:05:02 +08:00
4485d1f811 Update CHANGELOG.md
Updated changelog

Great Work as always, dark mode looks cool. If you want a suggestion about colors, I recommend [Catppuccin](https://github.com/catppuccin)
2024-11-24 11:52:03 +01:00
0eb0696670 Merge pull request #399 from tobychui/v3.1.4
V3.1.4
2024-11-24 14:47:53 +08:00
9fca2354c6 Update darktheme.css
Fixed docker container list text theme color
2024-11-24 14:41:01 +08:00
e56b045689 Added dark theme to docker container list 2024-11-24 13:58:46 +08:00
763ccb4d60 Remove deprecated ZeroTier config directory from Docker readme 2024-11-24 00:44:52 -05:00
4d4492069d Merge branch 'main' into v3.1.4 2024-11-24 12:37:58 +08:00
f3591aa171 Update dockerContainersList.html
Merged PR into dark theme branch
2024-11-24 12:35:26 +08:00
2dcf578cbe Update README.md 2024-11-24 11:52:57 +08:00
23a5c6ceb0 Updated geoIP database 2024-11-24 11:46:49 +08:00
015889851a Optimized UX and code structure
+ Added automatic self-sign certificate sniffing
+ Moved all constant into def.go
+ Added auto restart on port change when proxy server is running
+ Optimized slow search geoIP resolver by introducing new cache mechanism
+ Updated default incoming port to HTTPS instead of HTTP
2024-11-24 11:38:01 +08:00
093ed9c212 Merge pull request #395 from eyerrock/container-searchbar
search bar for Docker container list
2024-11-21 21:47:38 +08:00
0af8c67346 Updated API register function
- Seperated different register for APIs
2024-11-19 21:13:02 +08:00
c5170bcb94 Refactorized main entry function
- Moved constants to def.go
- Added acme close function (not used for now)
- Added robots.txt to prevent webmin panel being scanned by search engine
2024-11-19 20:30:36 +08:00
cd48388c02 refactored docker container list 2024-11-18 21:01:54 +01:00
373845f8fd added searchbar to docker container list 2024-11-18 18:16:07 +01:00
293a527ffc Completed dark theme 2024-11-18 21:04:25 +08:00
e4facbc7b6 Added more dark themes
- Added wrappers for snippet dark theme
- Optimized color pallets
2024-11-17 17:41:22 +08:00
1c79fa4e96 Fixed #394 2024-11-17 08:38:13 +08:00
6515eb99e3 Fixed #393
Updated version code manually
2024-11-15 06:48:35 +08:00
ec5c24b9b8 Added more darktheme
- Added more dark theme css
- Merged main branch fixes and new features
- Added todo tag for custom timeout
2024-11-14 21:18:05 +08:00
df88084375 Merge pull request #391 from eyerrock/list-containers-with-unexposed-ports
list containers with unexposed ports
2024-11-14 20:06:31 +08:00
74017baecf Merge pull request #392 from PassiveLemon/zoraxy-volume
Symlink ZeroTier var to Zoraxy config
2024-11-13 18:42:35 +08:00
294d504ee6 Symlink ZeroTier var to Zoraxy config 2024-11-12 12:40:08 -05:00
477429900e list containers with unexposed ports 2024-11-11 21:07:07 +01:00
2e9bc77a5d Merge commit from fork
Fixed web ssh security bug
2024-11-10 13:57:01 +08:00
ed178d857a Fixed web ssh security bug 2024-11-10 13:22:32 +08:00
4cf5d29692 Added more dark theme 2024-11-09 16:12:41 +08:00
634e9c9855 v3.1.3 init commit
- Fixed #378
- Added wip dark theme
- Fixed in code typo
- Fixed int conversion bug in some DNS challenge supplier
2024-11-08 22:24:07 +08:00
e79a70b7ac Merge pull request #376 from PassiveLemon/actions-cache
Add layer caching to Docker action
2024-11-06 06:58:52 +08:00
779115d06b Add layer caching to Docker action 2024-11-04 20:39:47 -05:00
9cb315ea67 Merge pull request #373 from Morethanevil/main
Update CHANGELOG.md
2024-11-03 17:41:49 +08:00
43ba00ec8d Update CHANGELOG.md
Thanks for your work :)
2024-11-03 10:11:20 +01:00
4577fb1f2f Merge pull request #368 from tobychui/v3.1.2
v3.1.2
2024-11-03 10:57:06 +08:00
f877bf9eda Update reverseproxy.go
Fixed typo
2024-11-03 09:31:24 +08:00
363b9b6d94 Merge branch 'main' into v3.1.2 2024-11-02 15:26:41 +08:00
c5ca68868b Optimized ACME logic
- Added automatic port 80 listener enable for those who don't read our wiki
- Reduced default interval for polling and propagation timeout
2024-10-28 21:40:58 +08:00
f927bb539a Updated geodb
- Updated geoip table
- Fixed bug in streamproxy delete in dev version
- Commented SSO related features (WIP) for release
2024-10-27 16:35:59 +08:00
5f64b622b5 Fixed #353 and #327
- Added user defined polling and propagation timeout option in ACME
- Updated lego and added a few new DNS challenge providers
- Updated code gen to support new parameters
2024-10-27 16:17:44 +08:00
9a371f5bcb Updated code generator for lego
- Removed windows 7 support
2024-10-27 15:40:53 +08:00
172c5afa60 Added support for custom header variables
- Added support for using nginx-like variables in custom headers
- Supported variables includes: $host, $remote_addr, $request_uri, $request_method, $content_length, $content_type, $uri, $args, $scheme, $query_string, $http_user_agent and $http_referer
- Added test case for custom header variable rewriter
2024-10-27 14:47:01 +08:00
f98e04a9fc Fixed #318
- Added support for automatic X-Remote-User header when basic auth is enabled
- Moved header logic to rewrite module (new module)
- Added default site automatic fix for URL missing http:// or https:// prefix
2024-10-26 22:21:49 +08:00
99295cad86 Fixed #342
- Added port scanner
- Moved handlers for IP scanner into ipscan module
-Minor code optimization
2024-10-26 19:41:43 +08:00
95d0a98576 Merge pull request #358 from eltociear/patch-1
Fixed typo in reverseproxy.go
2024-10-26 18:46:51 +08:00
00bfa262cb docs: update reverseproxy.go
Defination -> Definition
2024-10-26 18:46:42 +09:00
528be69fe0 Optimized stream proxy codebase
- Moved stream proxy config from database to file based conf
- Optimized implementation for detecting proxy rule running
- Fixed #320 (hopefully)
2024-10-25 23:30:44 +08:00
6923f0d200 Fixed #328
- Fixed register enter not working
- Updated all link to new project domain (aroz.org)
2024-10-23 21:31:06 +08:00
7255b62e31 Merge pull request #344 from tobychui/main
Update development branch to match new project URL and docker config
2024-10-23 21:11:09 +08:00
cf14d12c31 Update index.html
Updated all links to aroz.org
2024-10-20 17:36:51 +08:00
90cf26306a Update CNAME 2024-10-20 17:25:28 +08:00
cab2f4e63a Fixed #316
Fixed early renew day not passed into auto renewer config bug
2024-09-26 22:57:49 +08:00
75d773887c Merge pull request #308 from PassiveLemon/Fix_307
Fix #307
2024-09-17 10:58:27 +08:00
a944c3ff36 Fix #307 2024-09-16 13:09:37 -04:00
465f332dfc Merge pull request #305 from PassiveLemon/ZeroTierFix
Fix: Build older version of ZeroTier
2024-09-15 23:43:49 +08:00
dfda3fe94b Fix: Build older version of ZeroTier
Anything from 1.12.0+ just doesn't work on Zoraxy
2024-09-14 01:23:17 -04:00
5c56da1180 Added basic oauth module structure (wip)
- Added struct for oauth
- Added interception handler for Zoraxy SSO
- Added user structure for SSO
2024-09-12 10:55:01 +08:00
3392013a5c Fixed #297
- Added UI to showcase ZeroSSL do not support DNS challenge
- Added test case for origin picker
- Updated zerotier struct info (wip)
2024-09-09 21:12:12 +08:00
8b4c601d50 Merge pull request #298 from Morethanevil/main
Update CHANGELOG.md
2024-09-05 08:18:09 +08:00
3a2eaf8766 Update CHANGELOG.md 2024-09-04 17:44:52 +02:00
a45092a449 Patched #274 2024-09-04 22:05:54 +08:00
d5315e5b8e Merge pull request #289 from tobychui/v3.1.1
v3.1.1 update
2024-09-04 21:35:21 +08:00
31cc1a69a1 Merge pull request #295 from PassiveLemon/zerotier
Add ZeroTier to Docker container
2024-09-01 23:01:20 +08:00
d348cbf48b Update Docker README 2024-08-30 09:47:02 -04:00
f6339868ac Refactor Dockerfile and bundle ZeroTier 2024-08-30 09:47:02 -04:00
af10f2a644 Fix typos and inconsistencies in README 2024-08-28 18:27:49 -04:00
3b247c31da Fixed typo in README 2024-08-27 10:18:08 +08:00
d74e8badb9 Fixed #287
- Removed unusded tab switch in quicksetup.js
- Changed Macedonia to North Macedonia
2024-08-25 13:12:07 +08:00
b40131d212 Updated geodb and merged PR from main 2024-08-23 17:52:36 +08:00
563a12c860 Merge pull request #286 from ahmadsyamim/patch-1
Fix typo remvoeClass to removeClass
2024-08-23 17:37:52 +08:00
8b2c3b7e03 Fix typo remvoeClass to removeClass 2024-08-23 09:51:34 +08:00
608cc0c523 Optimized upstream & loadbalancer
- Test and optimized load balancer origin picker
- Fixed no active origin cannot load proxy rule bug
- Implemented logger design in websocket proxy module
- Added more quickstart tours
- Fixed #270 (I guess)
- Fixed #90 (I guess)
2024-08-19 16:10:35 +08:00
b558bcbfcf Merge pull request #258 from bouroo/perf/upstreams-sortfunc
weighted random upstream
2024-08-19 15:39:22 +08:00
9ea3fa2542 Added tour for setup https 2024-08-16 22:28:21 +08:00
01f68c5ef5 Added tour for basic operations
- added static website setup tour
- added subdomain setup tour
2024-08-15 22:35:43 +08:00
a7f89086d4 Restructured log format in acme module
- Replaced all log.Println in acme module to system wide logger
- Fixed file manager path escape bug #274
2024-08-13 21:56:23 +08:00
a5ef6456c6 v3.1.1 init
- Fixed path traverse bug in web server file manager
- Merged docker container list from main
- Updated version code
- Merged network status fix from PR
- Removed unused comments in dpcore
-
2024-08-07 13:53:43 +08:00
87659b43bd Merge pull request #278 from JokerQyou/fix/network-io-chart-not-rendering
Fix network I/O chart not rendering.
2024-08-07 13:49:02 +08:00
ddbecf7b68 Merge pull request #280 from 7brend7/fix-added-containers-list
Fix existings containers list in docker popup
2024-08-07 13:40:24 +08:00
1b3a9de378 Fix existings containers list in docker popup 2024-08-04 00:25:13 +03:00
6dd62f509d Update network data instead of assigning new variables. 2024-08-02 22:00:51 +08:00
d5cc6a6859 Fix network I/O chart not rendering.
Close #200.
2024-08-02 00:07:12 +08:00
1d965da7d0 Merge pull request #277 from Morethanevil/main
Update CHANGELOG.md
2024-08-01 08:43:46 +08:00
3567c70bab Update CHANGELOG.md 2024-07-31 19:52:31 +02:00
8a8ec1cb0b 📝 randIndex for fallbackUpstreams random 2024-07-24 14:59:48 +07:00
e53c3cf3c4 ️ fallbackUpstreams with preserve index 2024-07-24 14:47:33 +07:00
d17de5c200 weighted random upstream 2024-07-23 08:50:10 +07:00
97ff48ee70 🔥 origins already checked before getRandomUpstreamByWeight 2024-07-23 08:31:59 +07:00
d64b1174af keep compatible with go 1.20 2024-07-23 08:31:59 +07:00
bec363abab ️ immediate return if single upstream 2024-07-23 08:31:59 +07:00
0dddd1f9e3 📝 discribe for upstream sort func 2024-07-23 08:31:59 +07:00
6bfcb2e1f5 ️ slices.SortFunc for upstreams 2024-07-23 08:31:59 +07:00
167 changed files with 205099 additions and 48950 deletions

43
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: Build and push Docker image
on:
release:
types: [ published ]
jobs:
setup-build-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Setup building file structure
run: |
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: ./docker
push: true
platforms: linux/amd64,linux/arm64
tags: |
zoraxydocker/zoraxy:latest
zoraxydocker/zoraxy:${{ github.event.release.tag_name }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@ -1,40 +0,0 @@
name: Image Publisher
on:
release:
types: [ published ]
jobs:
setup-build-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker & GHCR
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Setup building file structure
run: |
cp -r $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
- name: Build the image
run: |
cd $GITHUB_WORKSPACE/docker/
docker buildx create --name mainbuilder --driver docker-container --platform linux/amd64,linux/arm64 --use
docker buildx build --push \
--provenance=false \
--platform linux/amd64,linux/arm64 \
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
--tag zoraxydocker/zoraxy:latest \
.

2
.gitignore vendored
View File

@ -30,7 +30,7 @@ src/certs/*
src/rules/* src/rules/*
src/README.md src/README.md
docker/ContainerTester.sh docker/ContainerTester.sh
docker/ImagePublisher.sh docker/docker-compose.yaml
src/mod/acme/test/stackoverflow.pem src/mod/acme/test/stackoverflow.pem
/tools/dns_challenge_update/code-gen/acmedns /tools/dns_challenge_update/code-gen/acmedns
/tools/dns_challenge_update/code-gen/lego /tools/dns_challenge_update/code-gen/lego

View File

@ -1,3 +1,78 @@
# v3.1.5 28 Dec 2024
+ Fixed hostname case sensitive bug [#435](https://github.com/tobychui/zoraxy/issues/435)
+ Fixed ACME table too wide css bug [#422](https://github.com/tobychui/zoraxy/issues/422)
+ Fixed HSTS toggle button bug [#415](https://github.com/tobychui/zoraxy/issues/415)
+ Fixed slow GeoIP resolve mode concurrent r/w bug [#401](https://github.com/tobychui/zoraxy/issues/401)
+ Added close connection as default site option [#430](https://github.com/tobychui/zoraxy/issues/430)
+ Added experimental authelia support [#384](https://github.com/tobychui/zoraxy/issues/384)
+ Added custom header support to websocket [#426](https://github.com/tobychui/zoraxy/issues/426)
+ Added levelDB as database implementation (not currently used)
+ Added external GeoIP db loading support
+ Restructured a lot of modules
# v3.1.4 24 Nov 2024
+ **Added Dark Theme Mode** [#390](https://github.com/tobychui/zoraxy/issues/390) [#82](https://github.com/tobychui/zoraxy/issues/82)
+ Added an auto sniffer for self-signed certificates
+ Added robots.txt to the project
+ Introduced an EU wrapper in the front-end for automatic registration of 26 countries [#378](https://github.com/tobychui/zoraxy/issues/378)
+ Moved all hard-coded values to a dedicated def.go file
+ Fixed a panic issue occurring on unsupported platform exits
+ Integrated fixes for SSH proxy and Docker snippet updates [#330](https://github.com/tobychui/zoraxy/issues/330) [#348](https://github.com/tobychui/zoraxy/issues/348)
+ **Changed the default listening port to 443 and enable TLS by default**
+ Optimized GeoIP database slow-search mode CPU usage
# v3.1.3 12 Nov 2024
+ Fixed a critical security bug [CVE-2024-52010](https://github.com/advisories/GHSA-7hpf-g48v-hw3j)
# v3.1.2 03 Nov 2024
+ Added auto start port 80 listener on acme certificate generator
+ Added polling interval and propagation timeout option in ACME module [#300](https://github.com/tobychui/zoraxy/issues/300)
+ Added support for custom header variables [#318](https://github.com/tobychui/zoraxy/issues/318)
+ Added support for X-Remote-User
+ Added port scanner [#342](https://github.com/tobychui/zoraxy/issues/342)
+ Optimized code base for stream proxy and config file storage [#320](https://github.com/tobychui/zoraxy/issues/320)
+ Removed sorting on cert list
+ Fixed request certificate button bug
+ Fixed cert auto renew logic [#316](https://github.com/tobychui/zoraxy/issues/316)
+ Fixed unable to remove new stream proxy bug
+ Fixed many other minor bugs [#328](https://github.com/tobychui/zoraxy/issues/328) [#297](https://github.com/tobychui/zoraxy/issues/297)
+ Added more code to SSO system (disabled in release)
# v3.1.1. 09 Sep 2024
+ Updated country name in access list [#287](https://github.com/tobychui/zoraxy/issues/287)
+ Added tour for basic operations
+ Updated acme log to system wide logger implementation
+ Fixed path traversal in file manager [#274](https://github.com/tobychui/zoraxy/issues/274)
+ Removed Proxmox debug code
+ Fixed trie tree implementations
**Thanks to all contributors**
+ Fix existing containers list in docker popup [7brend7](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3A7brend7)
+ Fix network I/O chart not rendering [JokerQyou](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3AJokerQyou)
+ Fix typo remvoeClass to removeClass [Aahmadsyamim](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3Aahmadsyamim)
+ Updated weighted random upstream implementation [bouroo](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3Abouroo)
# v3.1.0 31 Jul 2024
+ Updated log viewer with filter and auto refresh [#243](https://github.com/tobychui/zoraxy/issues/243)
+ Fixed csrf vulnerability [#267](https://github.com/tobychui/zoraxy/issues/267)
+ Fixed promox issue
+ Fixed status code bug in upstream log [#254](https://github.com/tobychui/zoraxy/issues/254)
+ Added host overwrite and hop-by-hop header remover
+ Added early renew days settings [#256](https://github.com/tobychui/zoraxy/issues/256)
+ Updated make file to force no CGO in cicd process
+ Fixed bug in updater
+ Fixed wildcard certificate renew bug [#249](https://github.com/tobychui/zoraxy/issues/249)
+ Added certificate download function [#227](https://github.com/tobychui/zoraxy/issues/227)
# v3.0.9 16 Jul 2024 # v3.0.9 16 Jul 2024
+ Added certificate download [#227](https://github.com/tobychui/zoraxy/issues/227) + Added certificate download [#227](https://github.com/tobychui/zoraxy/issues/227)

View File

@ -4,7 +4,6 @@
A general purpose HTTP reverse proxy and forwarding tool. Now written in Go! A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
### Features ### Features
- Simple to use interface with detail in-system instructions - Simple to use interface with detail in-system instructions
@ -34,24 +33,26 @@ A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
- Basic single-admin management mode - Basic single-admin management mode
- External permission management system for easy system integration - External permission management system for easy system integration
- SMTP config for password reset - SMTP config for password reset
- Dark Theme Mode
## Downloads ## Downloads
[Windows](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_windows_amd64.exe) [Windows](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_windows_amd64.exe)
/[Linux (amd64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64) / [Linux (amd64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64)
/[Linux (arm64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64) / [Linux (arm64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64)
For other systems or architectures, please see [Release](https://github.com/tobychui/zoraxy/releases/latest/) For other systems or architectures, please see [Releases](https://github.com/tobychui/zoraxy/releases/latest/)
## Getting Started ## Getting Started
[Installing Zoraxy Reverse Proxy: Your Gateway to Efficient Web Routing](https://geekscircuit.com/installing-zoraxy-reverse-proxy-your-gateway-to-efficient-web-routing/) [Installing Zoraxy Reverse Proxy: Your Gateway to Efficient Web Routing](https://geekscircuit.com/installing-zoraxy-reverse-proxy-your-gateway-to-efficient-web-routing/)
Thank you for the well written and easy to follow tutorial by Reddit users [itsvmn](https://www.reddit.com/user/itsvmn/)! Thank you for the well written and easy to follow tutorial by Reddit user [itsvmn](https://www.reddit.com/user/itsvmn/)!
If you have no background in setting up reverse proxy or web routing, you should check this out before you start setting up your Zoraxy. If you have no background in setting up reverse proxy or web routing, you should check this out before you start setting up your Zoraxy.
## Build from Source ## Build from Source
Requires Go 1.22 or higher Requires Go 1.23 or higher
```bash ```bash
git clone https://github.com/tobychui/zoraxy git clone https://github.com/tobychui/zoraxy
@ -64,7 +65,7 @@ sudo ./zoraxy -port=:8000
## Usage ## Usage
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructionss below for your desired deployment platform. Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructions below for your desired deployment platform.
### Standalone Mode ### Standalone Mode
@ -92,7 +93,7 @@ The installation method is same as Linux. For other ARM SBCs, please refer to yo
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details. See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details.
### Start Paramters ### Start Parameters
``` ```
Usage of zoraxy: Usage of zoraxy:
@ -102,6 +103,8 @@ Usage of zoraxy:
Enable auto config upgrade if breaking change is detected (default true) Enable auto config upgrade if breaking change is detected (default true)
-docker -docker
Run Zoraxy in docker compatibility mode Run Zoraxy in docker compatibility mode
-earlyrenew int
Number of days to early renew a soon expiring certificate (days) (default 30)
-fastgeoip -fastgeoip
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices) Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
-mdns -mdns
@ -134,7 +137,8 @@ If you already have an upstream reverse proxy server in place with permission ma
./zoraxy -noauth=true ./zoraxy -noauth=true
``` ```
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.* > [!WARNING]
> For security reasons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
## Screenshots ## Screenshots
@ -154,7 +158,7 @@ This project also compatible with [ZeroTier](https://www.zerotier.com/). However
To use Zoraxy with ZeroTier, assuming you already have a valid license, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken in the correct location on your host. To use Zoraxy with ZeroTier, assuming you already have a valid license, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken in the correct location on your host.
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags:: If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags:
```bash ```bash
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993 ./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
@ -175,7 +179,7 @@ Web SSH currently only supports Linux based OSes. The following platforms are su
### Loopback Connection ### Loopback Connection
Loopback web SSH connection, by default, is disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking: Loopback web SSH connections, by default, are disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
```bash ```bash
./zoraxy -sshlb=true ./zoraxy -sshlb=true

View File

@ -1,11 +1,8 @@
FROM docker.io/golang:alpine AS build FROM docker.io/golang:alpine AS build-zoraxy
RUN mkdir -p /opt/zoraxy/source/ &&\ RUN mkdir -p /opt/zoraxy/source/ &&\
mkdir -p /opt/zoraxy/config/ &&\
mkdir -p /usr/local/bin/ mkdir -p /usr/local/bin/
RUN chmod -R 770 /opt/zoraxy/
# If you build it yourself, you will need to add the src directory into the docker directory. # If you build it yourself, you will need to add the src directory into the docker directory.
COPY ./src/ /opt/zoraxy/source/ COPY ./src/ /opt/zoraxy/source/
@ -13,23 +10,42 @@ WORKDIR /opt/zoraxy/source/
RUN go mod tidy &&\ RUN go mod tidy &&\
go build -o /usr/local/bin/zoraxy &&\ go build -o /usr/local/bin/zoraxy &&\
rm -r /opt/zoraxy/source/ chmod 755 /usr/local/bin/zoraxy
RUN chmod 755 /usr/local/bin/zoraxy &&\ FROM docker.io/ubuntu:latest AS build-zerotier
chmod +x /usr/local/bin/zoraxy
FROM docker.io/alpine:latest RUN mkdir -p /opt/zerotier/source/ &&\
mkdir -p /usr/local/bin/
RUN apk add --no-cache bash netcat-openbsd sudo WORKDIR /opt/zerotier/source/
COPY --from=build /usr/local/bin/zoraxy /usr/local/bin/zoraxy RUN apt-get update -y &&\
COPY --from=build /opt/zoraxy/config/ /opt/zoraxy/config apt-get install -y curl jq build-essential pkg-config clang cargo libssl-dev
VOLUME [ "/opt/zoraxy/config/" ] RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne/tar.gz/refs/tags/1.10.6 &&\
tar -xzvf ZeroTierOne.tar.gz &&\
cd ZeroTierOne-* &&\
make &&\
mv ./zerotier-one /usr/local/bin/zerotier-one &&\
chmod 755 /usr/local/bin/zerotier-one
FROM docker.io/ubuntu:latest
RUN apt-get update -y &&\
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
WORKDIR /opt/zoraxy/config/ WORKDIR /opt/zoraxy/config/
ENV ZEROTIER="false"
ENV AUTORENEW="86400" ENV AUTORENEW="86400"
ENV CFGUPGRADE="true"
ENV DB="auto"
ENV DOCKER="true"
ENV EARLYRENEW="30" ENV EARLYRENEW="30"
ENV FASTGEOIP="false" ENV FASTGEOIP="false"
ENV MDNS="true" ENV MDNS="true"
@ -37,12 +53,16 @@ ENV MDNSNAME="''"
ENV NOAUTH="false" ENV NOAUTH="false"
ENV PORT="8000" ENV PORT="8000"
ENV SSHLB="false" ENV SSHLB="false"
ENV UPDATE_GEOIP="false"
ENV VERSION="false" ENV VERSION="false"
ENV WEBFM="true" ENV WEBFM="true"
ENV WEBROOT="./www" ENV WEBROOT="./www"
ENV ZTAUTH="''" ENV ZTAUTH=""
ENV ZTPORT="9993" ENV ZTPORT="9993"
ENTRYPOINT "zoraxy" "-docker=true" "-autorenew=${AUTORENEW}" "-earlyrenew=${EARLYRENEW}" "-fastgeoip=${FASTGEOIP}" "-mdns=${MDNS}" "-mdnsname=${MDNSNAME}" "-noauth=${NOAUTH}" "-port=:${PORT}" "-sshlb=${SSHLB}" "-version=${VERSION}" "-webfm=${WEBFM}" "-webroot=${WEBROOT}" "-ztauth=${ZTAUTH}" "-ztport=${ZTPORT}" VOLUME [ "/opt/zoraxy/config/" ]
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1 HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1

View File

@ -1,73 +1,104 @@
# [zoraxy](https://github.com/tobychui/zoraxy/) </br> # Zoraxy Docker
[![Repo](https://img.shields.io/badge/Docker-Repo-007EC6?labelColor-555555&color-007EC6&logo=docker&logoColor=fff&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy) [![Repo](https://img.shields.io/badge/Docker-Repo-007EC6?labelColor-555555&color-007EC6&logo=docker&logoColor=fff&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Version](https://img.shields.io/docker/v/zoraxydocker/zoraxy/latest?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy) [![Version](https://img.shields.io/docker/v/zoraxydocker/zoraxy/latest?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Size](https://img.shields.io/docker/image-size/zoraxydocker/zoraxy/latest?sort=semver&labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy) [![Size](https://img.shields.io/docker/image-size/zoraxydocker/zoraxy/latest?sort=semver&labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Pulls](https://img.shields.io/docker/pulls/zoraxydocker/zoraxy?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy) [![Pulls](https://img.shields.io/docker/pulls/zoraxydocker/zoraxy?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
## Setup: </br> ## Usage
Although not required, it is recommended to give Zoraxy a dedicated location on the host to mount the container. That way, the host/user can access them whenever needed. A volume will be created automatically within Docker if a location is not specified. </br>
You may also need to portforward your 80/443 to allow http and https traffic. If you are accessing the interface from outside of the local network, you may also need to forward your management port. If you know how to do this, great! If not, find the manufacturer of your router and search on how to do that. There are too many to be listed here. </br> If you are attempting to access your service from outside your network, make sure to forward ports 80 and 443 to the Zoraxy host to allow web traffic. If you know how to do this, great! If not, find the manufacturer of your router and search on how to do that. There are too many to be listed here. Read more about it from [whatismyip](https://www.whatismyip.com/port-forwarding/).
The examples below are not exactly how it should be set up, rather they give a general idea of usage. In the examples below, make sure to update `/path/to/zoraxy/config/`. If a path is not provided, a Docker volume will be created at the location but it is recommended to store the data at a defined host location or a named Docker volume.
Once setup, access the webui at `http://<host-ip>:8000` to configure Zoraxy. Change the port in the URL if you changed the management port.
### Docker Run
### Using Docker run </br>
``` ```
docker run -d --name (container name) -p 80:80 -p 443:443 -p (management external):(management internal) -v (path to storage directory):/opt/zoraxy/data/ -e (flag)="(value)" zoraxydocker/zoraxy:latest docker run -d \
--name zoraxy \
--restart unless-stopped \
-p 80:80 \
-p 443:443 \
-p 8000:8000 \
-v /path/to/zoraxy/config/:/opt/zoraxy/config/ \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /etc/localtime:/etc/localtime \
-e FASTGEOIP="true" \
zoraxydocker/zoraxy:latest
``` ```
### Using Docker Compose </br> ### Docker Compose
```yml ```yml
services: services:
zoraxy-docker: zoraxy:
image: zoraxydocker/zoraxy:latest
container_name: (container name)
ports:
- 80:80
- 443:443
- (management external):(management internal)
volumes:
- (path to storage directory):/opt/zoraxy/config/
environment:
(flag): "(value)"
```
| Operator | Need | Details |
|:-|:-|:-|
| `-d` | Yes | will run the container in the background. |
| `--name (container name)` | No | Sets the name of the container to the following word. You can change this to whatever you want. |
| `-p (ports)` | Yes | Depending on how your network is setup, you may need to portforward 80, 443, and the management port. |
| `-v (path to storage directory):/opt/zoraxy/config/` | Recommend | Sets the folder that holds your files. This should be the place you just chose. By default, it will create a Docker volume for the files for persistency but they will not be accessible. |
| `-v /var/run/docker.sock:/var/run/docker.sock` | No | Used for autodiscovery. |
| `-v /etc/localtime:/etc/localtime` | No | Uses the hosts time in the container. |
| `-e (flag)="(value)"` | No | Arguments to run Zoraxy with. They are simply just capitalized Zoraxy flags. `-docker=true` is always set by default. See examples below. |
| `zoraxydocker/zoraxy:latest` | Yes | The repository on Docker hub. By default, it is the latest version that is published. |
> [!IMPORTANT]
> Docker usage of the port flag should not include the colon. Ex: `-e PORT="8000"` for Docker run and `PORT: "8000"` for Docker compose.
## Examples: </br>
### Docker Run </br>
```
docker run -d --name zoraxy -p 80:80 -p 443:443 -p 8005:8005 -v /home/docker/Containers/Zoraxy:/opt/zoraxy/config/ -v /var/run/docker.sock:/var/run/docker.sock -v /etc/localtime:/etc/localtime -e PORT="8005" -e FASTGEOIP="true" zoraxydocker/zoraxy:latest
```
### Docker Compose </br>
```yml
services:
zoraxy-docker:
image: zoraxydocker/zoraxy:latest image: zoraxydocker/zoraxy:latest
container_name: zoraxy container_name: zoraxy
restart: unless-stopped
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443
- 8005:8005 - 8000:8000
volumes: volumes:
- /home/docker/Containers/Zoraxy:/opt/zoraxy/config/ - /path/to/zoraxy/config/:/opt/zoraxy/config/
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- /etc/localtime:/etc/localtime - /etc/localtime:/etc/localtime
environment: environment:
PORT: "8005"
FASTGEOIP: "true" FASTGEOIP: "true"
``` ```
### Ports
| Port | Details |
|:-|:-|
| `80` | HTTP traffic. |
| `443` | HTTPS traffic. |
| `8000` | Management interface. Can be changed with the `PORT` env. |
### Volumes
| Volume | Details |
|:-|:-|
| `/opt/zoraxy/config/` | Zoraxy configuration. |
| `/var/run/docker.sock` | Docker socket. Used for additional functionality with Zoraxy. |
| `/etc/localtime` | Localtime. Set to ensure the host and container are synchronized. |
### Environment
Variables are the same as those in [Start Parameters](https://github.com/tobychui/zoraxy?tab=readme-ov-file#start-paramters).
| Variable | Default | Details |
|:-|:-|:-|
| `AUTORENEW` | `86400` (Integer) | ACME auto TLS/SSL certificate renew check interval. |
| `CFGUPGRADE` | `true` (Boolean) | Enable auto config upgrade if breaking change is detected. |
| `DB` | `auto` (String) | Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV (default "auto"). |
| `DOCKER` | `true` (Boolean) | Run Zoraxy in docker compatibility mode. |
| `EARLYRENEW` | `30` (Integer) | Number of days to early renew a soon expiring certificate. |
| `FASTGEOIP` | `false` (Boolean) | Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices). |
| `MDNS` | `true` (Boolean) | Enable mDNS scanner and transponder. |
| `MDNSNAME` | `''` (String) | mDNS name, leave empty to use default (zoraxy_{node-uuid}.local). |
| `NOAUTH` | `false` (Boolean) | Disable authentication for management interface. |
| `PORT` | `8000` (Integer) | Management web interface listening port |
| `SSHLB` | `false` (Boolean) | Allow loopback web ssh connection (DANGER). |
| `UPDATE_GEOIP` | `false` (Boolean) | Download the latest GeoIP data and exit. |
| `VERSION` | `false` (Boolean) | Show version of this server. |
| `WEBFM` | `true` (Boolean) | Enable web file manager for static web server root folder. |
| `WEBROOT` | `./www` (String) | Static web server root folder. Only allow change in start parameters. |
| `ZEROTIER` | `false` (Boolean) | Enable ZeroTier functionality for GAN. |
| `ZTAUTH` | `""` (String) | ZeroTier authtoken for the local node. |
| `ZTPORT` | `9993` (Integer) | ZeroTier controller API port. |
> [!IMPORTANT]
> Contrary to the Zoraxy README, Docker usage of the port flag should NOT include the colon. Ex: `-e PORT="8000"` for Docker run and `PORT: "8000"` for Docker compose.
### Building
To build the Docker image:
- Check out the repository/branch.
- Copy the Zoraxy `src/` directory into the `docker/` (here) directory.
- Run the build command with `docker build -t zoraxy_build .`
- You can now use the image `zoraxy_build`
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.

37
docker/entrypoint.sh Normal file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
update-ca-certificates
echo "CA certificates updated."
zoraxy -update_geoip=true
echo "Updated GeoIP data."
if [ "$ZEROTIER" = "true" ]; then
if [ ! -d "/opt/zoraxy/config/zerotier/" ]; then
mkdir -p /opt/zoraxy/config/zerotier/
fi
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
zerotier-one -d
echo "ZeroTier daemon started."
fi
echo "Starting Zoraxy..."
exec zoraxy \
-autorenew="$AUTORENEW" \
-cfgupgrade="$CFGUPGRADE" \
-db="$DB" \
-docker="$DOCKER" \
-earlyrenew="$EARLYRENEW" \
-fastgeoip="$FASTGEOIP" \
-mdns="$MDNS" \
-mdnsname="$MDNSNAME" \
-noauth="$NOAUTH" \
-port=:"$PORT" \
-sshlb="$SSHLB" \
-update_geoip="$UPDATE_GEOIP" \
-version="$VERSION" \
-webfm="$WEBFM" \
-webroot="$WEBROOT" \
-ztauth="$ZTAUTH" \
-ztport="$ZTPORT"

View File

@ -1 +1 @@
zoraxy.arozos.com zoraxy.aroz.org

View File

@ -12,19 +12,19 @@
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs"> <meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
<!-- Facebook Meta Tags --> <!-- Facebook Meta Tags -->
<meta property="og:url" content="https://zoraxy.arozos.com/"> <meta property="og:url" content="https://zoraxy.aroz.org/">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy"> <meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs"> <meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
<meta property="og:image" content="https://zoraxy.arozos.com/img/og.png"> <meta property="og:image" content="https://zoraxy.aroz.org/img/og.png">
<!-- Twitter Meta Tags --> <!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="arozos.com"> <meta property="twitter:domain" content="aroz.org">
<meta property="twitter:url" content="https://zoraxy.arozos.com/"> <meta property="twitter:url" content="https://zoraxy.aroz.org/">
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy"> <meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs"> <meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
<meta name="twitter:image" content="https://zoraxy.arozos.com/img/og.png"> <meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
<!-- Favicons --> <!-- Favicons -->
<link href="favicon.png" rel="icon"> <link href="favicon.png" rel="icon">

View File

@ -230,7 +230,17 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
rule.AddCountryCodeToBlackList(countryCode, comment) //Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.AddCountryCodeToBlackList(code, comment)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.AddCountryCodeToBlackList(countryCode, comment)
}
utils.SendOK(w) utils.SendOK(w)
} }
@ -254,7 +264,17 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
return return
} }
rule.RemoveCountryCodeFromBlackList(countryCode) //Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.RemoveCountryCodeFromBlackList(code)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.RemoveCountryCodeFromBlackList(countryCode)
}
utils.SendOK(w) utils.SendOK(w)
} }
@ -397,7 +417,17 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
p := bluemonday.StrictPolicy() p := bluemonday.StrictPolicy()
comment = p.Sanitize(comment) comment = p.Sanitize(comment)
rule.AddCountryCodeToWhitelist(countryCode, comment) //Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.AddCountryCodeToWhitelist(code, comment)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.AddCountryCodeToWhitelist(countryCode, comment)
}
utils.SendOK(w) utils.SendOK(w)
} }
@ -420,7 +450,17 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
return return
} }
rule.RemoveCountryCodeFromWhitelist(countryCode) //Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.RemoveCountryCodeFromWhitelist(code)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.RemoveCountryCodeFromWhitelist(countryCode)
}
utils.SendOK(w) utils.SendOK(w)
} }

View File

@ -38,7 +38,21 @@ func initACME() *acme.ACMEHandler {
port = getRandomPort(30000) port = getRandomPort(30000)
} }
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb) return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb, SystemWideLogger)
}
// Restart ACME handler and auto renewer
func restartACMEHandler() {
SystemWideLogger.Println("Restarting ACME handler")
//Clos the current handler and auto renewer
acmeHandler.Close()
acmeAutoRenewer.Close()
acmeDeregisterSpecialRoutingRule()
//Reinit the handler with a new random port
acmeHandler = initACME()
acmeRegisterSpecialRoutingRule()
} }
// create the special routing rule for ACME // create the special routing rule for ACME
@ -82,12 +96,29 @@ func acmeRegisterSpecialRoutingRule() {
} }
} }
// remove the special routing rule for ACME
func acmeDeregisterSpecialRoutingRule() {
SystemWideLogger.Println("Removing ACME routing rule")
dynamicProxyRouter.RemoveRoutingRule("acme-autorenew")
}
// This function check if the renew setup is satisfied. If not, toggle them automatically // This function check if the renew setup is satisfied. If not, toggle them automatically
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) { func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
isForceHttpsRedirectEnabledOriginally := false isForceHttpsRedirectEnabledOriginally := false
requireRestorePort80 := false
dnsPara, _ := utils.PostBool(r, "dns") dnsPara, _ := utils.PostBool(r, "dns")
if !dnsPara { if !dnsPara {
if dynamicProxyRouter.Option.Port == 443 { if dynamicProxyRouter.Option.Port == 443 {
//Check if port 80 is enabled
if !dynamicProxyRouter.Option.ListenOnPort80 {
//Enable port 80 temporarily
SystemWideLogger.PrintAndLog("ACME", "Temporarily enabling port 80 listener to handle ACME request ", nil)
dynamicProxyRouter.UpdatePort80ListenerState(true)
requireRestorePort80 = true
time.Sleep(2 * time.Second)
}
//Enable port 80 to 443 redirect //Enable port 80 to 443 redirect
if !dynamicProxyRouter.Option.ForceHttpsRedirect { if !dynamicProxyRouter.Option.ForceHttpsRedirect {
SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests") SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
@ -107,8 +138,8 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
} }
} }
//Add a 3 second delay to make sure everything is settle down //Add a 2 second delay to make sure everything is settle down
time.Sleep(3 * time.Second) time.Sleep(2 * time.Second)
// Pass over to the acmeHandler to deal with the communication // Pass over to the acmeHandler to deal with the communication
acmeHandler.HandleRenewCertificate(w, r) acmeHandler.HandleRenewCertificate(w, r)
@ -117,13 +148,17 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
tlsCertManager.UpdateLoadedCertList() tlsCertManager.UpdateLoadedCertList()
//Restore original settings //Restore original settings
if dynamicProxyRouter.Option.Port == 443 && !dnsPara { if requireRestorePort80 {
if !isForceHttpsRedirectEnabledOriginally { //Restore port 80 listener
//Default is off. Turn the redirection off SystemWideLogger.PrintAndLog("ACME", "Restoring previous port 80 listener settings", nil)
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil) dynamicProxyRouter.UpdatePort80ListenerState(false)
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
}
} }
if !isForceHttpsRedirectEnabledOriginally {
//Default is off. Turn the redirection off
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
}
} }
// HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation // HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation

View File

@ -8,6 +8,8 @@ import (
"imuslab.com/zoraxy/mod/acme/acmedns" "imuslab.com/zoraxy/mod/acme/acmedns"
"imuslab.com/zoraxy/mod/acme/acmewizard" "imuslab.com/zoraxy/mod/acme/acmewizard"
"imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
"imuslab.com/zoraxy/mod/ipscan"
"imuslab.com/zoraxy/mod/netstat" "imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/netutils" "imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
@ -17,34 +19,11 @@ import (
API.go API.go
This file contains all the API called by the web management interface This file contains all the API called by the web management interface
*/ */
var requireAuth = true // Register the APIs for HTTP proxy management functions
func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
func initAPIs(targetMux *http.ServeMux) { /* Reverse Proxy Settings & Status */
authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{
AuthAgent: authAgent,
RequireAuth: requireAuth,
TargetMux: targetMux,
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
},
})
//Register the standard web services urls
fs := http.FileServer(http.FS(webres))
if development {
fs = http.FileServer(http.Dir("web/"))
}
//Add a layer of middleware for advance control
advHandler := FSHandler(fs)
targetMux.Handle("/", advHandler)
//Authentication APIs
registerAuthAPIs(requireAuth, targetMux)
//Reverse proxy
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff) authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint) authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus) authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
@ -55,24 +34,24 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias) authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint) authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials) authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS) authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS)
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet) authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect) authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener) authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck) authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange) authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
//Reverse proxy upstream (load balance) APIs /* Reverse proxy upstream (load balance) */
authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList) authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList)
authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd) authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd)
authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority) authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority)
authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate) authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate)
authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete) authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete)
//Reverse proxy virtual directory APIs /* Reverse proxy virtual directory */
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir) authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir) authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir) authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir) authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
//Reverse proxy user define header apis /* Reverse proxy user-defined header */
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList) authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd) authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove) authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
@ -80,12 +59,15 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop) authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop)
authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite) authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite)
authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy) authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy)
//Reverse proxy auth related APIs authRouter.HandleFunc("/api/proxy/header/handleWsHeaderBehavior", HandleWsHeaderBehavior)
/* Reverse proxy auth related */
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths) authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths) authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths) authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths)
}
//TLS / SSL config // Register the APIs for TLS / SSL certificate management functions
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy) authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest) authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
authRouter.HandleFunc("/api/cert/upload", handleCertUpload) authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
@ -94,48 +76,72 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains) authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck) authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
authRouter.HandleFunc("/api/cert/delete", handleCertRemove) authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
}
//Redirection config // Register the APIs for Authentication handlers like Authelia and OAUTH2
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS)
}
// Register the APIs for redirection rules management functions
func RegisterRedirectionAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules) authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule) authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule) authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport) authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
}
//Access Rules API // Register the APIs for access rules management functions
func RegisterAccessRuleAPIs(authRouter *auth.RouterDef) {
/* Access Rules Settings & Status */
authRouter.HandleFunc("/api/access/list", handleListAccessRules) authRouter.HandleFunc("/api/access/list", handleListAccessRules)
authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost) authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule) authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule) authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule) authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
//Blacklist APIs /* Blacklist */
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted) authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd) authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove) authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove)
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd) authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove) authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable) authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
//Whitelist APIs /* Whitelist */
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted) authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd) authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove) authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd) authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd)
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove) authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable) authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
}
//Path Blocker APIs // Register the APIs for path blocking rules management functions, WIP
func RegisterPathRuleAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath) authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath)
authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath) authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath)
authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath) authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath)
}
//Statistic & uptime monitoring API // Register the APIs statistic anlysis and uptime monitoring functions
func RegisterStatisticalAPIs(authRouter *auth.RouterDef) {
/* Traffic Summary */
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad) authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary) authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats) authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats) authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces) authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
/* Zoraxy Analytic */
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
/* UpTime Monitor */
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing) authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
}
//Global Area Network APIs // Register the APIs for Global Area Network management functions, Will be moving to plugin soon
func RegisterGANAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID) authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID)
authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork) authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork)
authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork) authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork)
@ -150,8 +156,10 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming) authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization) authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete) authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
}
//Stream (TCP / UDP) Proxy // Register the APIs for Stream (TCP / UDP) Proxy management functions
func RegisterStreamProxyAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig) authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs) authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs) authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
@ -159,20 +167,59 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy) authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy)
authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy) authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy)
authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus) authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
}
//mDNS APIs // Register the APIs for mDNS service management functions
func RegisterMDNSAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing) authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)
authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning) authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning)
}
//Zoraxy Analytic // Register the APIs for ACME and Auto Renewer management functions
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList) func RegisterACMEAndAutoRenewerAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary) /* ACME Core */
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary) authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport) authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset) /* Auto Renewer */
authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable)
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HandleSetDNS)
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson)
/* ACME Wizard */
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck)
}
//Network utilities // Register the APIs for Static Web Server management functions
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan) func RegisterStaticWebServerAPIs(authRouter *auth.RouterDef) {
/* Static Web Server Controls */
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
/* File Manager */
if *allowWebFileManager {
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
}
}
// Register the APIs for Network Utilities functions
func RegisterNetworkUtilsAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/tools/ipscan", ipscan.HandleIpScan)
authRouter.HandleFunc("/api/tools/portscan", ipscan.HandleScanPort)
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute) authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing) authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois) authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
@ -185,66 +232,10 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend) authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle) authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle)
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort) authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
//Account Reset
targetMux.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup)
//ACME & Auto Renewer
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable)
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HanldeSetDNS)
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson)
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
//Static Web Server
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
if *allowWebFileManager {
//Web Directory Manager file operation functions
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
}
//Docker UX Optimizations
authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable)
authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList)
//Others
targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo)
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
//Debug
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
//If you got APIs to add, append them here
} }
// Function to renders Auth related APIs // Register the APIs for Auth functions, due to scoping issue some functions are defined here
func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) { func RegisterAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
//Auth APIs
targetMux.HandleFunc("/api/auth/login", authAgent.HandleLogin) targetMux.HandleFunc("/api/auth/login", authAgent.HandleLogin)
targetMux.HandleFunc("/api/auth/logout", authAgent.HandleLogout) targetMux.HandleFunc("/api/auth/logout", authAgent.HandleLogout)
targetMux.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) { targetMux.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) {
@ -260,21 +251,17 @@ func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return return
} }
js, _ := json.Marshal(username) js, _ := json.Marshal(username)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
}) })
targetMux.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) { targetMux.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
uc := authAgent.GetUserCounts() js, _ := json.Marshal(authAgent.GetUserCounts())
js, _ := json.Marshal(uc)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
}) })
targetMux.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) { targetMux.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
if authAgent.GetUserCounts() == 0 { if authAgent.GetUserCounts() == 0 {
//Allow register root admin //Allow register root admin
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) { authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {})
})
} else { } else {
//This function is disabled //This function is disabled
utils.SendErrorResponse(w, "Root management account already exists") utils.SendErrorResponse(w, "Root management account already exists")
@ -315,5 +302,60 @@ func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
authAgent.UnregisterUser(username) authAgent.UnregisterUser(username)
authAgent.CreateUserAccount(username, newPassword, "") authAgent.CreateUserAccount(username, newPassword, "")
}) })
}
/* Register all the APIs */
func initAPIs(targetMux *http.ServeMux) {
authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{
AuthAgent: authAgent,
RequireAuth: requireAuth,
TargetMux: targetMux,
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
},
})
//Register the standard web services urls
fs := http.FileServer(http.FS(webres))
if DEVELOPMENT_BUILD {
fs = http.FileServer(http.Dir("web/"))
}
//Add a layer of middleware for advance control
advHandler := FSHandler(fs)
targetMux.Handle("/", advHandler)
//Register the APIs
RegisterAuthAPIs(requireAuth, targetMux)
RegisterHTTPProxyAPIs(authRouter)
RegisterTLSAPIs(authRouter)
RegisterAuthenticationHandlerAPIs(authRouter)
RegisterRedirectionAPIs(authRouter)
RegisterAccessRuleAPIs(authRouter)
RegisterPathRuleAPIs(authRouter)
RegisterStatisticalAPIs(authRouter)
RegisterGANAPIs(authRouter)
RegisterStreamProxyAPIs(authRouter)
RegisterMDNSAPIs(authRouter)
RegisterNetworkUtilsAPIs(authRouter)
RegisterACMEAndAutoRenewerAPIs(authRouter)
RegisterStaticWebServerAPIs(authRouter)
//Account Reset
targetMux.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup)
//Docker UX Optimizations
authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable)
authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList)
//Others
targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo)
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
//Debug
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
} }

View File

@ -177,7 +177,10 @@ func handleListDomains(w http.ResponseWriter, r *http.Request) {
// Handle front-end toggling TLS mode // Handle front-end toggling TLS mode
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) { func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
currentTlsSetting := false currentTlsSetting := true //Default to true
if dynamicProxyRouter.Option != nil {
currentTlsSetting = dynamicProxyRouter.Option.UseTls
}
if sysdb.KeyExists("settings", "usetls") { if sysdb.KeyExists("settings", "usetls") {
sysdb.Read("settings", "usetls", &currentTlsSetting) sysdb.Read("settings", "usetls", &currentTlsSetting)
} }

View File

@ -48,7 +48,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
} }
//Parse it into dynamic proxy endpoint //Parse it into dynamic proxy endpoint
thisConfigEndpoint := dynamicproxy.ProxyEndpoint{} thisConfigEndpoint := dynamicproxy.GetDefaultProxyEndpoint()
err = json.Unmarshal(endpointConfig, &thisConfigEndpoint) err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
if err != nil { if err != nil {
return err return err
@ -59,7 +59,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
thisConfigEndpoint.RootOrMatchingDomain = "/" thisConfigEndpoint.RootOrMatchingDomain = "/"
} }
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Root { if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
//This is a root config file //This is a root config file
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint) rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil { if err != nil {
@ -68,7 +68,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint) dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Host { } else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeHost {
//This is a host config file //This is a host config file
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint) readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil { if err != nil {
@ -97,7 +97,7 @@ func filterProxyConfigFilename(filename string) string {
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error { func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
//Get filename for saving //Get filename for saving
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config") filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
if endpoint.ProxyType == dynamicproxy.ProxyType_Root { if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
filename = "./conf/proxy/root.config" filename = "./conf/proxy/root.config"
} }
@ -129,27 +129,23 @@ func RemoveReverseProxyConfig(endpoint string) error {
// Get the default root config that point to the internal static web server // Get the default root config that point to the internal static web server
// this will be used if root config is not found (new deployment / missing root.config file) // this will be used if root config is not found (new deployment / missing root.config file)
func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) { func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
//Default settings //Get the default proxy endpoint
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{ rootProxyEndpointConfig := dynamicproxy.GetDefaultProxyEndpoint()
ProxyType: dynamicproxy.ProxyType_Root, rootProxyEndpointConfig.ProxyType = dynamicproxy.ProxyTypeRoot
RootOrMatchingDomain: "/", rootProxyEndpointConfig.RootOrMatchingDomain = "/"
ActiveOrigins: []*loadbalance.Upstream{ rootProxyEndpointConfig.ActiveOrigins = []*loadbalance.Upstream{
{ {
OriginIpOrDomain: "127.0.0.1:" + staticWebServer.GetListeningPort(), OriginIpOrDomain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
RequireTLS: false, RequireTLS: false,
SkipCertValidations: false, SkipCertValidations: false,
Weight: 0, Weight: 0,
},
}, },
InactiveOrigins: []*loadbalance.Upstream{}, }
BypassGlobalTLS: false, rootProxyEndpointConfig.DefaultSiteOption = dynamicproxy.DefaultSite_InternalStaticWebServer
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{}, rootProxyEndpointConfig.DefaultSiteValue = ""
RequireBasicAuth: false,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{}, //Default settings
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{}, rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&rootProxyEndpointConfig)
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -167,12 +163,15 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
if includeSysDBRaw == "true" { if includeSysDBRaw == "true" {
//Include the system database in backup snapshot //Include the system database in backup snapshot
//Temporary set it to read only //Temporary set it to read only
sysdb.ReadOnly = true
includeSysDB = true includeSysDB = true
} }
// Specify the folder path to be zipped // Specify the folder path to be zipped
folderPath := "./conf/" if !utils.FileExists("./conf") {
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
return
}
folderPath := "./conf"
// Set the Content-Type header to indicate it's a zip file // Set the Content-Type header to indicate it's a zip file
w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Type", "application/zip")
@ -227,7 +226,7 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
} }
// Open the file on disk // Open the file on disk
file, err := os.Open("sys.db") file, err := os.Open("./sys.db")
if err != nil { if err != nil {
SystemWideLogger.PrintAndLog("Backup", "Unable to open sysdb", err) SystemWideLogger.PrintAndLog("Backup", "Unable to open sysdb", err)
return return
@ -241,8 +240,6 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
return return
} }
//Restore sysdb state
sysdb.ReadOnly = false
} }
if err != nil { if err != nil {
@ -278,6 +275,8 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
targetDir := "./conf" targetDir := "./conf"
if utils.FileExists(targetDir) { if utils.FileExists(targetDir) {
//Backup the old config to old //Backup the old config to old
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
//os.Rename(*path_conf, backupPath)
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix()))) os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
} }

148
src/def.go Normal file
View File

@ -0,0 +1,148 @@
package main
/*
Type and flag definations
This file contains all the type and flag definations
Author: tobychui
*/
import (
"embed"
"flag"
"net/http"
"time"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dockerux"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/email"
"imuslab.com/zoraxy/mod/forwardproxy"
"imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/info/logviewer"
"imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/pathrule"
"imuslab.com/zoraxy/mod/sshprox"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/statistic/analytic"
"imuslab.com/zoraxy/mod/streamproxy"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/webserv"
)
const (
/* Build Constants */
SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.1.6"
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
/* System Constants */
TMP_FOLDER = "./tmp"
WEBSERV_DEFAULT_PORT = 5487
MDNS_HOSTNAME_PREFIX = "zoraxy_" /* Follow by node UUID */
MDNS_IDENTIFY_DEVICE_TYPE = "Network Gateway"
MDNS_IDENTIFY_DOMAIN = "zoraxy.aroz.org"
MDNS_IDENTIFY_VENDOR = "imuslab.com"
MDNS_SCAN_TIMEOUT = 30 /* Seconds */
MDNS_SCAN_UPDATE_INTERVAL = 15 /* Minutes */
GEODB_CACHE_CLEAR_INTERVAL = 15 /* Minutes */
ACME_AUTORENEW_CONFIG_PATH = "./conf/acme_conf.json"
CSRF_COOKIENAME = "zoraxy_csrf"
LOG_PREFIX = "zr"
LOG_EXTENSION = ".log"
/* Configuration Folder Storage Path Constants */
CONF_HTTP_PROXY = "./conf/proxy"
CONF_STREAM_PROXY = "./conf/streamproxy"
CONF_CERT_STORE = "./conf/certs"
CONF_REDIRECTION = "./conf/redirect"
CONF_ACCESS_RULE = "./conf/access"
CONF_PATH_RULE = "./conf/rules/pathrules"
)
/* System Startup Flags */
var (
webUIPort = flag.String("port", ":8000", "Management web interface listening port")
databaseBackend = flag.String("db", "auto", "Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV")
noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
showver = flag.Bool("version", false, "Show version of this server")
allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode")
acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)")
enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
/* Path Configuration Flags */
//path_database = flag.String("dbpath", "./sys.db", "Database path")
//path_conf = flag.String("conf", "./conf", "Configuration folder path")
path_uuid = flag.String("uuid", "./sys.uuid", "sys.uuid file path")
path_logFile = flag.String("log", "./log", "Log folder path")
path_webserver = flag.String("webroot", "./www", "Static web server root folder. Only allow change in start paramters")
/* Maintaince Function Flags */
geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit")
)
/* Global Variables and Handlers */
var (
nodeUUID = "generic" //System uuid in uuidv4 format, load from database on startup
bootTime = time.Now().Unix()
requireAuth = true //Require authentication for webmin panel, override from flag
/*
Binary Embedding File System
*/
//go:embed web/*
webres embed.FS
/*
Handler Modules
*/
sysdb *database.Database //System database
authAgent *auth.AuthAgent //Authentication agent
tlsCertManager *tlscert.Manager //TLS / SSL management
redirectTable *redirection.RuleTable //Handle special redirection rule sets
webminPanelMux *http.ServeMux //Server mux for handling webmin panel APIs
csrfMiddleware func(http.Handler) http.Handler //CSRF protection middleware
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
accessController *access.Controller //Access controller, handle black list and white list
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
statisticCollector *statistic.Collector //Collecting statistic from visitors
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
mdnsScanner *mdns.MDNSHost //mDNS discovery services
ganManager *ganserv.NetworkManager //Global Area Network Manager
webSshManager *sshprox.Manager //Web SSH connection service
streamProxyManager *streamproxy.Manager //Stream Proxy Manager for TCP / UDP forwarding
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
//Authentication Provider
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only
SystemWideLogger *logger.Logger //Logger for Zoraxy
LogViewer *logviewer.Viewer //Log viewer HTTP handlers
)

View File

@ -1,107 +1,129 @@
module imuslab.com/zoraxy module imuslab.com/zoraxy
go 1.21 go 1.22.0
toolchain go1.22.2 toolchain go1.22.2
require ( require (
github.com/boltdb/bolt v1.3.1 github.com/boltdb/bolt v1.3.1
github.com/docker/docker v27.0.0+incompatible github.com/docker/docker v27.0.0+incompatible
github.com/go-acme/lego/v4 v4.16.1 github.com/go-acme/lego/v4 v4.19.2
github.com/go-ping/ping v1.1.0 github.com/go-ping/ping v1.1.0
github.com/go-session/session v3.1.2+incompatible
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.2 github.com/gorilla/sessions v1.2.2
github.com/gorilla/websocket v1.5.1 github.com/gorilla/websocket v1.5.1
github.com/grandcat/zeroconf v1.0.0 github.com/grandcat/zeroconf v1.0.0
github.com/likexian/whois v1.15.1 github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.26 github.com/microcosm-cc/bluemonday v1.0.26
golang.org/x/net v0.25.0 golang.org/x/net v0.29.0
golang.org/x/sys v0.20.0 golang.org/x/sys v0.25.0
golang.org/x/text v0.15.0 golang.org/x/text v0.18.0
) )
require ( require (
cloud.google.com/go/compute v1.25.1 // indirect cloud.google.com/go/auth v0.9.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
github.com/tidwall/buntdb v1.1.2 // indirect
github.com/tidwall/gjson v1.12.1 // indirect
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/vultr/govultr/v3 v3.9.1 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
)
require (
cloud.google.com/go/compute/metadata v0.5.1 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.29 // indirect github.com/Azure/go-autorest/autorest v0.11.29 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/Microsoft/go-winio v0.4.14 // indirect github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 // indirect github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect github.com/aws/smithy-go v1.20.4 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/civo/civogo v0.3.11 // indirect github.com/civo/civogo v0.3.11 // indirect
github.com/cloudflare/cloudflare-go v0.86.0 // indirect github.com/cloudflare/cloudflare-go v0.104.0 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deepmap/oapi-codegen v1.9.1 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/dnsimple/dnsimple-go v1.2.0 // indirect github.com/dnsimple/dnsimple-go v1.7.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/exoscale/egoscale v0.102.3 // indirect
github.com/fatih/structs v1.1.0 // indirect github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect github.com/go-errors/errors v1.0.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-resty/resty/v2 v2.11.0 // indirect github.com/go-oauth2/oauth2/v4 v4.5.2
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/go-resty/resty/v2 v2.13.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/goccy/go-json v0.10.3 // indirect
github.com/gofrs/uuid v4.4.0+incompatible
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.12.2 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gophercloud/gophercloud v1.0.0 // indirect github.com/gophercloud/gophercloud v1.14.0 // indirect
github.com/gorilla/csrf v1.7.2 // indirect github.com/gorilla/csrf v1.7.2
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
@ -111,11 +133,11 @@ require (
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect github.com/labbsr0x/goh v1.0.1 // indirect
github.com/linode/linodego v1.28.0 // indirect github.com/linode/linodego v1.40.0 // indirect
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.58 // indirect github.com/miekg/dns v1.1.62 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -126,66 +148,64 @@ require (
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
github.com/nrdcg/auroradns v1.1.0 // indirect github.com/nrdcg/auroradns v1.1.0 // indirect
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
github.com/nrdcg/desec v0.7.0 // indirect github.com/nrdcg/desec v0.8.0 // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nrdcg/freemyip v0.2.0 // indirect github.com/nrdcg/freemyip v0.2.0 // indirect
github.com/nrdcg/goinwx v0.10.0 // indirect github.com/nrdcg/goinwx v0.10.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/porkbun v0.3.0 // indirect github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nzdjb/go-metaname v1.0.0 // indirect github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/ovh/go-ovh v1.4.3 // indirect github.com/ovh/go-ovh v1.6.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/pquerna/otp v1.4.0 // indirect github.com/pquerna/otp v1.4.0 // indirect
github.com/sacloud/api-client-go v0.2.8 // indirect github.com/sacloud/api-client-go v0.2.10 // indirect
github.com/sacloud/go-http v0.1.6 // indirect github.com/sacloud/go-http v0.1.8 // indirect
github.com/sacloud/iaas-api-go v1.11.1 // indirect github.com/sacloud/iaas-api-go v1.12.0 // indirect
github.com/sacloud/packages-go v0.0.9 // indirect github.com/sacloud/packages-go v0.0.10 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 // indirect github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/softlayer/softlayer-go v1.1.3 // indirect github.com/softlayer/softlayer-go v1.1.5 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect github.com/stretchr/testify v1.9.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
github.com/transip/gotransip/v6 v6.23.0 // indirect github.com/transip/gotransip/v6 v6.26.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/vultr/govultr/v2 v2.17.2 // indirect github.com/xlzd/gotp v0.1.0
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 // indirect
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.27.0 // indirect go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.27.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/sdk v1.27.0 // indirect go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.27.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/ratelimit v0.3.0 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.27.0 // indirect
golang.org/x/mod v0.16.0 // indirect golang.org/x/mod v0.21.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.6.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.6.0 // indirect
golang.org/x/tools v0.19.0 // indirect golang.org/x/tools v0.25.0 // indirect
google.golang.org/api v0.169.0 // indirect google.golang.org/api v0.197.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect google.golang.org/grpc v1.66.1 // indirect
google.golang.org/grpc v1.64.0 // indirect google.golang.org/protobuf v1.34.2 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.7.13 // indirect gopkg.in/ns1/ns1-go.v2 v2.12.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect gotest.tools/v3 v3.5.1 // indirect

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,36 @@
package main package main
/*
______
|___ /
/ / ___ _ __ __ ___ ___ _
/ / / _ \| '__/ _` \ \/ / | | |
/ /_| (_) | | | (_| |> <| |_| |
/_____\___/|_| \__,_/_/\_\\__, |
__/ |
|___/
Zoraxy - A general purpose HTTP reverse proxy and forwarding tool
Author: tobychui
License: AGPLv3
--------------------------------------------
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3 of the License or any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ( import (
"embed"
"flag" "flag"
"fmt" "fmt"
"log" "log"
@ -13,98 +42,12 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dockerux"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/email"
"imuslab.com/zoraxy/mod/forwardproxy"
"imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/info/logviewer"
"imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/pathrule"
"imuslab.com/zoraxy/mod/sshprox"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/statistic/analytic"
"imuslab.com/zoraxy/mod/streamproxy"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/update" "imuslab.com/zoraxy/mod/update"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
"imuslab.com/zoraxy/mod/webserv"
) )
// General flags /* SIGTERM handler, do shutdown sequences before closing */
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
var showver = flag.Bool("version", false, "Show version of this server")
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
var mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
var runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode")
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
var acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)")
var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
var enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
var (
name = "Zoraxy"
version = "3.1.0"
nodeUUID = "generic" //System uuid, in uuidv4 format
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()
/*
Binary Embedding File System
*/
//go:embed web/*
webres embed.FS
/*
Handler Modules
*/
sysdb *database.Database //System database
authAgent *auth.AuthAgent //Authentication agent
tlsCertManager *tlscert.Manager //TLS / SSL management
redirectTable *redirection.RuleTable //Handle special redirection rule sets
webminPanelMux *http.ServeMux //Server mux for handling webmin panel APIs
csrfMiddleware func(http.Handler) http.Handler //CSRF protection middleware
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
accessController *access.Controller //Access controller, handle black list and white list
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
statisticCollector *statistic.Collector //Collecting statistic from visitors
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
mdnsScanner *mdns.MDNSHost //mDNS discovery services
ganManager *ganserv.NetworkManager //Global Area Network Manager
webSshManager *sshprox.Manager //Web SSH connection service
streamProxyManager *streamproxy.Manager //Stream Proxy Manager for TCP / UDP forwarding
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only
SystemWideLogger *logger.Logger //Logger for Zoraxy
LogViewer *logviewer.Viewer
)
// Kill signal handler. Do something before the system the core terminate.
func SetupCloseHandler() { func SetupCloseHandler() {
c := make(chan os.Signal, 2) c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
@ -115,45 +58,21 @@ func SetupCloseHandler() {
}() }()
} }
func ShutdownSeq() {
SystemWideLogger.Println("Shutting down " + name)
SystemWideLogger.Println("Closing GeoDB ")
geodbStore.Close()
SystemWideLogger.Println("Closing Netstats Listener")
netstatBuffers.Close()
SystemWideLogger.Println("Closing Statistic Collector")
statisticCollector.Close()
if mdnsTickerStop != nil {
SystemWideLogger.Println("Stopping mDNS Discoverer (might take a few minutes)")
// Stop the mdns service
mdnsTickerStop <- true
}
mdnsScanner.Close()
SystemWideLogger.Println("Shutting down load balancer")
loadBalancer.Close()
SystemWideLogger.Println("Closing Certificates Auto Renewer")
acmeAutoRenewer.Close()
//Remove the tmp folder
SystemWideLogger.Println("Cleaning up tmp files")
os.RemoveAll("./tmp")
//Close database
SystemWideLogger.Println("Stopping system database")
sysdb.Close()
//Close logger
SystemWideLogger.Println("Closing system wide logger")
SystemWideLogger.Close()
}
func main() { func main() {
//Parse startup flags //Parse startup flags
flag.Parse() flag.Parse()
/* Maintaince Function Modes */
if *showver { if *showver {
fmt.Println(name + " - Version " + version) fmt.Println(SYSTEM_NAME + " - Version " + SYSTEM_VERSION)
os.Exit(0)
}
if *geoDbUpdate {
geodb.DownloadGeoDBUpdate("./conf/geodb")
os.Exit(0) os.Exit(0)
} }
/* Main Zoraxy Routines */
if !utils.ValidateListeningAddress(*webUIPort) { if !utils.ValidateListeningAddress(*webUIPort) {
fmt.Println("Malformed -port (listening address) paramter. Do you mean -port=:" + *webUIPort + "?") fmt.Println("Malformed -port (listening address) paramter. Do you mean -port=:" + *webUIPort + "?")
os.Exit(0) os.Exit(0)
@ -161,13 +80,13 @@ func main() {
if *enableAutoUpdate { if *enableAutoUpdate {
fmt.Println("Checking required config update") fmt.Println("Checking required config update")
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(version)) update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(SYSTEM_VERSION))
} }
SetupCloseHandler() SetupCloseHandler()
//Read or create the system uuid //Read or create the system uuid
uuidRecord := "./sys.uuid" uuidRecord := *path_uuid
if !utils.FileExists(uuidRecord) { if !utils.FileExists(uuidRecord) {
newSystemUUID := uuid.New().String() newSystemUUID := uuid.New().String()
os.WriteFile(uuidRecord, []byte(newSystemUUID), 0775) os.WriteFile(uuidRecord, []byte(newSystemUUID), 0775)
@ -183,13 +102,13 @@ func main() {
webminPanelMux = http.NewServeMux() webminPanelMux = http.NewServeMux()
csrfMiddleware = csrf.Protect( csrfMiddleware = csrf.Protect(
[]byte(nodeUUID), []byte(nodeUUID),
csrf.CookieName("zoraxy-csrf"), csrf.CookieName(CSRF_COOKIENAME),
csrf.Secure(false), csrf.Secure(false),
csrf.Path("/"), csrf.Path("/"),
csrf.SameSite(csrf.SameSiteLaxMode), csrf.SameSite(csrf.SameSiteLaxMode),
) )
//Startup all modules //Startup all modules, see start.go
startupSequence() startupSequence()
//Initiate management interface APIs //Initiate management interface APIs
@ -206,11 +125,10 @@ func main() {
//Start the finalize sequences //Start the finalize sequences
finalSequence() finalSequence()
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort) SystemWideLogger.Println(SYSTEM_NAME + " started. Visit control panel at http://localhost" + *webUIPort)
err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux)) err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }

View File

@ -11,7 +11,6 @@ import (
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"log"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -22,18 +21,29 @@ import (
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/challenge/http01" "github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
var defaultNameservers = []string{
"8.8.8.8:53", // Google DNS
"8.8.4.4:53", // Google DNS
"1.1.1.1:53", // Cloudflare DNS
"1.0.0.1:53", // Cloudflare DNS
}
type CertificateInfoJSON struct { type CertificateInfoJSON struct {
AcmeName string `json:"acme_name"` AcmeName string `json:"acme_name"` //ACME provider name
AcmeUrl string `json:"acme_url"` AcmeUrl string `json:"acme_url"` //Custom ACME URL (if any)
SkipTLS bool `json:"skip_tls"` SkipTLS bool `json:"skip_tls"` //Skip TLS verification of upstream
UseDNS bool `json:"dns"` UseDNS bool `json:"dns"` //Use DNS challenge
PropTimeout int `json:"prop_time"` //Propagation timeout
DNSServers []string `json:"dnsServers"` // DNS servers
} }
// ACMEUser represents a user in the ACME system. // ACMEUser represents a user in the ACME system.
@ -68,25 +78,38 @@ type ACMEHandler struct {
DefaultAcmeServer string DefaultAcmeServer string
Port string Port string
Database *database.Database Database *database.Database
Logger *logger.Logger
} }
// NewACME creates a new ACMEHandler instance. // NewACME creates a new ACMEHandler instance.
func NewACME(acmeServer string, port string, database *database.Database) *ACMEHandler { func NewACME(defaultAcmeServer string, port string, database *database.Database, logger *logger.Logger) *ACMEHandler {
return &ACMEHandler{ return &ACMEHandler{
DefaultAcmeServer: acmeServer, DefaultAcmeServer: defaultAcmeServer,
Port: port, Port: port,
Database: database, Database: database,
Logger: logger,
} }
} }
func (a *ACMEHandler) Logf(message string, err error) {
a.Logger.PrintAndLog("ACME", message, err)
}
// Close closes the ACMEHandler.
// ACME Handler does not need to close anything
// Function defined for future compatibility
func (a *ACMEHandler) Close() error {
return nil
}
// ObtainCert obtains a certificate for the specified domains. // ObtainCert obtains a certificate for the specified domains.
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool) (bool, error) { func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool, propagationTimeout int, dnsServers string) (bool, error) {
log.Println("[ACME] Obtaining certificate...") a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil)
// generate private key // generate private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil { if err != nil {
log.Println(err) a.Logf("Private key generation failed", err)
return false, err return false, err
} }
@ -102,7 +125,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// skip TLS verify if need // skip TLS verify if need
// Ref: https://github.com/go-acme/lego/blob/6af2c756ac73a9cb401621afca722d0f4112b1b8/lego/client_config.go#L74 // Ref: https://github.com/go-acme/lego/blob/6af2c756ac73a9cb401621afca722d0f4112b1b8/lego/client_config.go#L74
if skipTLS { if skipTLS {
log.Println("[INFO] Ignore TLS/SSL Verification Error for ACME Server") a.Logf("Ignoring TLS/SSL Verification Error for ACME Server", nil)
config.HTTPClient.Transport = &http.Transport{ config.HTTPClient.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{ DialContext: (&net.Dialer{
@ -129,16 +152,16 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// if not custom ACME url, load it from ca.json // if not custom ACME url, load it from ca.json
if caName == "custom" { if caName == "custom" {
log.Println("[INFO] Using Custom ACME " + caUrl + " for CA Directory URL") a.Logf("Using Custom ACME "+caUrl+" for CA Directory URL", nil)
} else { } else {
caLinkOverwrite, err := loadCAApiServerFromName(caName) caLinkOverwrite, err := loadCAApiServerFromName(caName)
if err == nil { if err == nil {
config.CADirURL = caLinkOverwrite config.CADirURL = caLinkOverwrite
log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL") a.Logf("Using "+caLinkOverwrite+" for CA Directory URL", nil)
} else { } else {
// (caName == "" || caUrl == "") will use default acme // (caName == "" || caUrl == "") will use default acme
config.CADirURL = a.DefaultAcmeServer config.CADirURL = a.DefaultAcmeServer
log.Println("[INFO] Using Default ACME " + a.DefaultAcmeServer + " for CA Directory URL") a.Logf("Using Default ACME "+a.DefaultAcmeServer+" for CA Directory URL", nil)
} }
} }
@ -146,50 +169,72 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
client, err := lego.NewClient(config) client, err := lego.NewClient(config)
if err != nil { if err != nil {
log.Println(err) a.Logf("Failed to spawn new ACME client from current config", err)
return false, err return false, err
} }
// Load certificate info from JSON file
certInfo, err := LoadCertInfoJSON(fmt.Sprintf("./conf/certs/%s.json", certificateName))
if err == nil {
useDNS = certInfo.UseDNS
if dnsServers == "" && certInfo.DNSServers != nil && len(certInfo.DNSServers) > 0 {
dnsServers = strings.Join(certInfo.DNSServers, ",")
}
propagationTimeout = certInfo.PropTimeout
}
// Clean DNS servers
dnsNameservers := strings.Split(dnsServers, ",")
for i := range dnsNameservers {
dnsNameservers[i] = strings.TrimSpace(dnsNameservers[i])
}
// setup how to receive challenge // setup how to receive challenge
if useDNS { if useDNS {
if !a.Database.TableExists("acme") { if !a.Database.TableExists("acme") {
a.Database.NewTable("acme") a.Database.NewTable("acme")
return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -1)") return false, errors.New("DNS Provider and DNS Credential configuration required for ACME Provider (Error -1)")
} }
if !a.Database.KeyExists("acme", certificateName+"_dns_provider") || !a.Database.KeyExists("acme", certificateName+"_dns_credentials") { if !a.Database.KeyExists("acme", certificateName+"_dns_provider") || !a.Database.KeyExists("acme", certificateName+"_dns_credentials") {
return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -2)") return false, errors.New("DNS Provider and DNS Credential configuration required for ACME Provider (Error -2)")
} }
var dnsCredentials string var dnsCredentials string
err := a.Database.Read("acme", certificateName+"_dns_credentials", &dnsCredentials) err := a.Database.Read("acme", certificateName+"_dns_credentials", &dnsCredentials)
if err != nil { if err != nil {
log.Println(err) a.Logf("Read DNS credential failed", err)
return false, err return false, err
} }
var dnsProvider string var dnsProvider string
err = a.Database.Read("acme", certificateName+"_dns_provider", &dnsProvider) err = a.Database.Read("acme", certificateName+"_dns_provider", &dnsProvider)
if err != nil { if err != nil {
log.Println(err) a.Logf("Read DNS Provider failed", err)
return false, err return false, err
} }
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials) provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials, propagationTimeout)
if err != nil { if err != nil {
log.Println(err) a.Logf("Unable to resolve DNS challenge provider", err)
return false, err return false, err
} }
err = client.Challenge.SetDNS01Provider(provider) if len(dnsNameservers) > 0 && dnsNameservers[0] != "" {
a.Logf("Using DNS servers: "+strings.Join(dnsNameservers, ", "), nil)
err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(dnsNameservers))
} else {
// Use default DNS-01 nameservers if dnsServers is empty
err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(defaultNameservers))
}
if err != nil { if err != nil {
log.Println(err) a.Logf("Failed to resolve DNS01 Provider", err)
return false, err return false, err
} }
} else { } else {
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port)) err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
if err != nil { if err != nil {
log.Println(err) a.Logf("Failed to resolve HTTP01 Provider", err)
return false, err return false, err
} }
} }
@ -205,7 +250,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
var reg *registration.Resource var reg *registration.Resource
// New users will need to register // New users will need to register
if client.GetExternalAccountRequired() { if client.GetExternalAccountRequired() {
log.Println("External Account Required for this ACME Provider.") a.Logf("External Account Required for this ACME Provider", nil)
// IF KID and HmacEncoded is overidden // IF KID and HmacEncoded is overidden
if !a.Database.TableExists("acme") { if !a.Database.TableExists("acme") {
@ -220,20 +265,18 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
var kid string var kid string
var hmacEncoded string var hmacEncoded string
err := a.Database.Read("acme", config.CADirURL+"_kid", &kid) err := a.Database.Read("acme", config.CADirURL+"_kid", &kid)
if err != nil { if err != nil {
log.Println(err) a.Logf("Failed to read kid from database", err)
return false, err return false, err
} }
err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded) err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded)
if err != nil { if err != nil {
log.Println(err) a.Logf("Failed to read HMAC from database", err)
return false, err return false, err
} }
log.Println("EAB Credential retrieved.", kid, hmacEncoded) a.Logf("EAB Credential retrieved: "+kid+" / "+hmacEncoded, nil)
if kid != "" && hmacEncoded != "" { if kid != "" && hmacEncoded != "" {
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true, TermsOfServiceAgreed: true,
@ -242,14 +285,14 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
}) })
} }
if err != nil { if err != nil {
log.Println(err) a.Logf("Register with external account binder failed", err)
return false, err return false, err
} }
//return false, errors.New("External Account Required for this ACME Provider.") //return false, errors.New("External Account Required for this ACME Provider.")
} else { } else {
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil { if err != nil {
log.Println(err) a.Logf("Unable to register client", err)
return false, err return false, err
} }
} }
@ -262,7 +305,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
} }
certificates, err := client.Certificate.Obtain(request) certificates, err := client.Certificate.Obtain(request)
if err != nil { if err != nil {
log.Println(err) a.Logf("Obtain certificate failed", err)
return false, err return false, err
} }
@ -270,32 +313,34 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// private key, and a certificate URL. // private key, and a certificate URL.
err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777) err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
if err != nil { if err != nil {
log.Println(err) a.Logf("Failed to write public key to disk", err)
return false, err return false, err
} }
err = os.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777) err = os.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
if err != nil { if err != nil {
log.Println(err) a.Logf("Failed to write private key to disk", err)
return false, err return false, err
} }
// Save certificate's ACME info for renew usage // Save certificate's ACME info for renew usage
certInfo := &CertificateInfoJSON{ certInfo = &CertificateInfoJSON{
AcmeName: caName, AcmeName: caName,
AcmeUrl: caUrl, AcmeUrl: caUrl,
SkipTLS: skipTLS, SkipTLS: skipTLS,
UseDNS: useDNS, UseDNS: useDNS,
PropTimeout: propagationTimeout,
DNSServers: dnsNameservers,
} }
certInfoBytes, err := json.Marshal(certInfo) certInfoBytes, err := json.Marshal(certInfo)
if err != nil { if err != nil {
log.Println(err) a.Logf("Marshal certificate renew config failed", err)
return false, err return false, err
} }
err = os.WriteFile("./conf/certs/"+certificateName+".json", certInfoBytes, 0777) err = os.WriteFile("./conf/certs/"+certificateName+".json", certInfoBytes, 0777)
if err != nil { if err != nil {
log.Println(err) a.Logf("Failed to write certificate renew config to file", err)
return false, err return false, err
} }
@ -313,7 +358,7 @@ func (a *ACMEHandler) CheckCertificate() []string {
expiredCerts := []string{} expiredCerts := []string{}
if err != nil { if err != nil {
log.Println(err) a.Logf("Failed to load certificate folder", err)
return []string{} return []string{}
} }
@ -410,14 +455,14 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
ca, err := utils.PostPara(r, "ca") ca, err := utils.PostPara(r, "ca")
if err != nil { if err != nil {
log.Println("[INFO] CA not set. Using default") a.Logf("CA not set. Using default", nil)
ca, caUrl = "", "" ca, caUrl = "", ""
} }
if ca == "custom" { if ca == "custom" {
caUrl, err = utils.PostPara(r, "caURL") caUrl, err = utils.PostPara(r, "caURL")
if err != nil { if err != nil {
log.Println("[INFO] Custom CA set but no URL provide, Using default") a.Logf("Custom CA set but no URL provide, Using default", nil)
ca, caUrl = "", "" ca, caUrl = "", ""
} }
} }
@ -448,12 +493,44 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
} }
domains := strings.Split(domainPara, ",") domains := strings.Split(domainPara, ",")
// Default propagation timeout is 300 seconds
propagationTimeout := 300
if dns {
ppgTimeout, err := utils.PostPara(r, "ppgTimeout")
if err == nil {
propagationTimeout, err = strconv.Atoi(ppgTimeout)
if err != nil {
utils.SendErrorResponse(w, "Invalid propagation timeout value")
return
}
if propagationTimeout < 60 {
//Minimum propagation timeout is 60 seconds
propagationTimeout = 60
}
}
}
//Clean spaces in front or behind each domain //Clean spaces in front or behind each domain
cleanedDomains := []string{} cleanedDomains := []string{}
for _, domain := range domains { for _, domain := range domains {
cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain)) cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
} }
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns)
// Extract DNS servers from the request
var dnsServers []string
dnsServersPara, err := utils.PostPara(r, "dnsServers")
if err == nil && dnsServersPara != "" {
dnsServers = strings.Split(dnsServersPara, ",")
for i := range dnsServers {
dnsServers[i] = strings.TrimSpace(dnsServers[i])
}
}
// Convert DNS servers slice to a single string
dnsServersString := strings.Join(dnsServers, ",")
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout, dnsServersString)
if err != nil { if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error())) utils.SendErrorResponse(w, jsonEscape(err.Error()))
return return
@ -465,7 +542,7 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
func jsonEscape(i string) string { func jsonEscape(i string) string {
b, err := json.Marshal(i) b, err := json.Marshal(i)
if err != nil { if err != nil {
log.Println("Unable to escape json data: " + err.Error()) //log.Println("Unable to escape json data: " + err.Error())
return i return i
} }
s := string(b) s := string(b)
@ -496,5 +573,10 @@ func LoadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
return nil, err return nil, err
} }
// Clean DNS servers
for i := range certInfo.DNSServers {
certInfo.DNSServers[i] = strings.TrimSpace(certInfo.DNSServers[i])
}
return certInfo, nil return certInfo, nil
} }

View File

@ -1,70 +1,56 @@
package acme package acme
import ( import (
"encoding/json"
"strconv"
"github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge"
"imuslab.com/zoraxy/mod/acme/acmedns" "imuslab.com/zoraxy/mod/acme/acmedns"
) )
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string) (challenge.Provider, error) { // Preprocessor function to get DNS challenge provider by name
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string, ppgTimeout int) (challenge.Provider, error) {
//Original Implementation //Unpack the dnsCredentials (json string) to map
/*credentials, err := extractDnsCredentials(dnsCredentials) var dnsCredentialsMap map[string]interface{}
err := json.Unmarshal([]byte(dnsCredentials), &dnsCredentialsMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
setCredentialsIntoEnvironmentVariables(credentials)
provider, err := dns.NewDNSChallengeProviderByName(dnsProvider) //Clear the PollingInterval and PropagationTimeout field and conert to int
*/ userDefinedPollingInterval := 2
if dnsCredentialsMap["PollingInterval"] != nil {
//New implementation using acmedns CICD pipeline generated datatype userDefinedPollingIntervalRaw := dnsCredentialsMap["PollingInterval"].(string)
return acmedns.GetDNSProviderByJsonConfig(dnsProvider, dnsCredentials) delete(dnsCredentialsMap, "PollingInterval")
} convertedPollingInterval, err := strconv.Atoi(userDefinedPollingIntervalRaw)
if err == nil {
/* userDefinedPollingInterval = convertedPollingInterval
Original implementation of DNS ACME using OS.Env as payload
*/
/*
func setCredentialsIntoEnvironmentVariables(credentials map[string]string) {
for key, value := range credentials {
err := os.Setenv(key, value)
if err != nil {
log.Println("[ERR] Failed to set environment variable %s: %v", key, err)
} else {
log.Println("[INFO] Environment variable %s set successfully", key)
}
}
}
func extractDnsCredentials(input string) (map[string]string, error) {
result := make(map[string]string)
// Split the input string by newline character
lines := strings.Split(input, "\n")
// Iterate over each line
for _, line := range lines {
// Split the line by "=" character
//use SpliyN to make sure not to split the value if the value is base64
parts := strings.SplitN(line, "=", 1)
// Check if the line is in the correct format
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Add the key-value pair to the map
result[key] = value
if value == "" || key == "" {
//invalid config
return result, errors.New("DNS credential extract failed")
}
} }
} }
return result, nil userDefinedPropagationTimeout := ppgTimeout
} if dnsCredentialsMap["PropagationTimeout"] != nil {
userDefinedPropagationTimeoutRaw := dnsCredentialsMap["PropagationTimeout"].(string)
delete(dnsCredentialsMap, "PropagationTimeout")
convertedPropagationTimeout, err := strconv.Atoi(userDefinedPropagationTimeoutRaw)
if err == nil {
//Overwrite the default propagation timeout if it is requeted from UI
userDefinedPropagationTimeout = convertedPropagationTimeout
}
}
*/ //Restructure dnsCredentials string from map
dnsCredentialsBytes, err := json.Marshal(dnsCredentialsMap)
if err != nil {
return nil, err
}
dnsCredentials = string(dnsCredentialsBytes)
//Using acmedns CICD pipeline generated datatype to optain the DNS provider
return acmedns.GetDNSProviderByJsonConfig(
dnsProvider,
dnsCredentials,
int64(userDefinedPropagationTimeout),
int64(userDefinedPollingInterval),
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -75,6 +75,15 @@ func HandleGuidedStepCheck(w http.ResponseWriter, r *http.Request) {
httpServerReachable := isHTTPServerAvailable(domain) httpServerReachable := isHTTPServerAvailable(domain)
js, _ := json.Marshal(httpServerReachable) js, _ := json.Marshal(httpServerReachable)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else if stepNo == 10 {
//Resolve public Ip address for tour
publicIp, err := getPublicIPAddress()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(publicIp)
utils.SendJSONResponse(w, string(js))
} else { } else {
utils.SendErrorResponse(w, "invalid step number") utils.SendErrorResponse(w, "invalid step number")
} }

View File

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"log"
"net/http" "net/http"
"net/mail" "net/mail"
"os" "os"
@ -12,6 +11,7 @@ import (
"strings" "strings"
"time" "time"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -26,6 +26,7 @@ type AutoRenewConfig struct {
Email string //Email for acme Email string //Email for acme
RenewAll bool //Renew all or selective renew with the slice below RenewAll bool //Renew all or selective renew with the slice below
FilesToRenew []string //If RenewAll is false, renew these certificate files FilesToRenew []string //If RenewAll is false, renew these certificate files
DNSServers string // DNS servers
} }
type AutoRenewer struct { type AutoRenewer struct {
@ -36,6 +37,7 @@ type AutoRenewer struct {
RenewTickInterval int64 RenewTickInterval int64
EarlyRenewDays int //How many days before cert expire to renew certificate EarlyRenewDays int //How many days before cert expire to renew certificate
TickerstopChan chan bool TickerstopChan chan bool
Logger *logger.Logger //System wide logger
} }
type ExpiredCerts struct { type ExpiredCerts struct {
@ -45,7 +47,7 @@ type ExpiredCerts struct {
// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds) // Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
// Set renew check interval to 0 for auto (1 day) // Set renew check interval to 0 for auto (1 day)
func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, earlyRenewDays int, AcmeHandler *ACMEHandler) (*AutoRenewer, error) { func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, earlyRenewDays int, AcmeHandler *ACMEHandler, logger *logger.Logger) (*AutoRenewer, error) {
if renewCheckInterval == 0 { if renewCheckInterval == 0 {
renewCheckInterval = 86400 //1 day renewCheckInterval = 86400 //1 day
} }
@ -87,8 +89,12 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
AcmeHandler: AcmeHandler, AcmeHandler: AcmeHandler,
RenewerConfig: &renewerConfig, RenewerConfig: &renewerConfig,
RenewTickInterval: renewCheckInterval, RenewTickInterval: renewCheckInterval,
EarlyRenewDays: earlyRenewDays,
Logger: logger,
} }
thisRenewer.Logf("ACME early renew set to "+fmt.Sprint(earlyRenewDays)+" days and check interval set to "+fmt.Sprint(renewCheckInterval)+" seconds", nil)
if thisRenewer.RenewerConfig.Enabled { if thisRenewer.RenewerConfig.Enabled {
//Start the renew ticker //Start the renew ticker
thisRenewer.StartAutoRenewTicker() thisRenewer.StartAutoRenewTicker()
@ -100,6 +106,10 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
return &thisRenewer, nil return &thisRenewer, nil
} }
func (a *AutoRenewer) Logf(message string, err error) {
a.Logger.PrintAndLog("cert-renew", message, err)
}
func (a *AutoRenewer) StartAutoRenewTicker() { func (a *AutoRenewer) StartAutoRenewTicker() {
//Stop the previous ticker if still running //Stop the previous ticker if still running
if a.TickerstopChan != nil { if a.TickerstopChan != nil {
@ -118,7 +128,7 @@ func (a *AutoRenewer) StartAutoRenewTicker() {
case <-done: case <-done:
return return
case <-ticker.C: case <-ticker.C:
log.Println("Check and renew certificates in progress") a.Logf("Check and renew certificates in progress", nil)
a.CheckAndRenewCertificates() a.CheckAndRenewCertificates()
} }
} }
@ -233,12 +243,12 @@ func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Reque
} }
a.RenewerConfig.Enabled = true a.RenewerConfig.Enabled = true
a.saveRenewConfigToFile() a.saveRenewConfigToFile()
log.Println("[ACME] ACME auto renew enabled") a.Logf("ACME auto renew enabled", nil)
a.StartAutoRenewTicker() a.StartAutoRenewTicker()
} else { } else {
a.RenewerConfig.Enabled = false a.RenewerConfig.Enabled = false
a.saveRenewConfigToFile() a.saveRenewConfigToFile()
log.Println("[ACME] ACME auto renew disabled") a.Logf("ACME auto renew disabled", nil)
a.StopAutoRenewTicker() a.StopAutoRenewTicker()
} }
} else { } else {
@ -283,7 +293,7 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
certFolder := a.CertFolder certFolder := a.CertFolder
files, err := os.ReadDir(certFolder) files, err := os.ReadDir(certFolder)
if err != nil { if err != nil {
log.Println("Unable to renew certificates: " + err.Error()) a.Logf("Read certificate store failed", err)
return []string{}, err return []string{}, err
} }
@ -299,11 +309,10 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
} }
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) { if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
//This cert is expired //This cert is expired
DNSName, err := ExtractDomains(certBytes) DNSName, err := ExtractDomains(certBytes)
if err != nil { if err != nil {
//Maybe self signed. Ignore this //Maybe self signed. Ignore this
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name()) a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
continue continue
} }
@ -327,11 +336,10 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
} }
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) { if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
//This cert is expired //This cert is expired
DNSName, err := ExtractDomains(certBytes) DNSName, err := ExtractDomains(certBytes)
if err != nil { if err != nil {
//Maybe self signed. Ignore this //Maybe self signed. Ignore this
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name()) a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
continue continue
} }
@ -347,6 +355,7 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
return a.renewExpiredDomains(expiredCertList) return a.renewExpiredDomains(expiredCertList)
} }
// Close the auto renewer
func (a *AutoRenewer) Close() { func (a *AutoRenewer) Close() {
if a.TickerstopChan != nil { if a.TickerstopChan != nil {
a.TickerstopChan <- true a.TickerstopChan <- true
@ -358,7 +367,7 @@ func (a *AutoRenewer) Close() {
func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) { func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
renewedCertFiles := []string{} renewedCertFiles := []string{}
for _, expiredCert := range certs { for _, expiredCert := range certs {
log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)") a.Logf("Renewing "+expiredCert.Filepath+" (Might take a few minutes)", nil)
fileName := filepath.Base(expiredCert.Filepath) fileName := filepath.Base(expiredCert.Filepath)
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))] certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
@ -366,21 +375,33 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName) certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
certInfo, err := LoadCertInfoJSON(certInfoFilename) certInfo, err := LoadCertInfoJSON(certInfoFilename)
if err != nil { if err != nil {
log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, trying org section as ca", certName, err) a.Logf("Renew "+certName+"certificate error, can't get the ACME detail for certificate, trying org section as ca", err)
if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil { if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil {
log.Printf("extract issuer name for cert error: %v, using default ca", extractErr) a.Logf("Extract issuer name for cert error, using default ca", err)
certInfo = &CertificateInfoJSON{} certInfo = &CertificateInfoJSON{}
} else { } else {
certInfo = &CertificateInfoJSON{AcmeName: CAName} certInfo = &CertificateInfoJSON{AcmeName: CAName}
} }
} }
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS) //For upgrading config from older version of Zoraxy which don't have timeout
if certInfo.PropTimeout == 0 {
//Set default timeout
certInfo.PropTimeout = 300
}
// Extract DNS servers from the certificate info if available
var dnsServers string
if len(certInfo.DNSServers) > 0 {
dnsServers = strings.Join(certInfo.DNSServers, ",")
}
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout, dnsServers)
if err != nil { if err != nil {
log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error()) a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
} else { } else {
log.Println("Successfully renewed " + filepath.Base(expiredCert.Filepath)) a.Logf("Successfully renewed "+filepath.Base(expiredCert.Filepath), nil)
renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath)) renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
} }
} }
@ -426,7 +447,7 @@ func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
} }
// Handle update auto renew DNS configuration // Handle update auto renew DNS configuration
func (a *AutoRenewer) HanldeSetDNS(w http.ResponseWriter, r *http.Request) { func (a *AutoRenewer) HandleSetDNS(w http.ResponseWriter, r *http.Request) {
dnsProvider, err := utils.PostPara(r, "dnsProvider") dnsProvider, err := utils.PostPara(r, "dnsProvider")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "dnsProvider not set") utils.SendErrorResponse(w, "dnsProvider not set")
@ -445,12 +466,18 @@ func (a *AutoRenewer) HanldeSetDNS(w http.ResponseWriter, r *http.Request) {
return return
} }
dnsServers, err := utils.PostPara(r, "dnsServers")
if err != nil {
dnsServers = ""
}
if !a.AcmeHandler.Database.TableExists("acme") { if !a.AcmeHandler.Database.TableExists("acme") {
a.AcmeHandler.Database.NewTable("acme") a.AcmeHandler.Database.NewTable("acme")
} }
a.AcmeHandler.Database.Write("acme", filename+"_dns_provider", dnsProvider) a.AcmeHandler.Database.Write("acme", filename+"_dns_provider", dnsProvider)
a.AcmeHandler.Database.Write("acme", filename+"_dns_credentials", dnsCredentials) a.AcmeHandler.Database.Write("acme", filename+"_dns_credentials", dnsCredentials)
a.AcmeHandler.Database.Write("acme", filename+"_dns_servers", dnsServers)
utils.SendOK(w) utils.SendOK(w)

View File

@ -3,7 +3,7 @@ package acme
/* /*
CA.go CA.go
This script load CA defination from embedded ca.json This script load CA definition from embedded ca.json
*/ */
import ( import (
_ "embed" _ "embed"
@ -13,7 +13,7 @@ import (
"strings" "strings"
) )
// CA Defination, load from embeded json when startup // CA definition, load from embeded json when startup
type CaDef struct { type CaDef struct {
Production map[string]string Production map[string]string
Test map[string]string Test map[string]string

View File

@ -5,14 +5,14 @@ import (
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "os"
"time" "time"
) )
// Get the issuer name from pem file // Get the issuer name from pem file
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) { func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
// Read the PEM file // Read the PEM file
pemData, err := ioutil.ReadFile(pemFilePath) pemData, err := os.ReadFile(pemFilePath)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -210,8 +210,8 @@ func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
} }
session.Values["authenticated"] = false session.Values["authenticated"] = false
session.Values["username"] = nil session.Values["username"] = nil
session.Save(r, w) session.Options.MaxAge = -1
return nil return session.Save(r, w)
} }
// Get the current session username from request // Get the current session username from request
@ -339,6 +339,7 @@ func (a *AuthAgent) CheckAuth(r *http.Request) bool {
if err != nil { if err != nil {
return false return false
} }
// Check if user is authenticated // Check if user is authenticated
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth { if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
return false return false

View File

@ -0,0 +1,136 @@
package authelia
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type AutheliaRouterOptions struct {
UseHTTPS bool //If the Authelia server is using HTTPS
AutheliaURL string //The URL of the Authelia server
Logger *logger.Logger
Database *database.Database
}
type AutheliaRouter struct {
options *AutheliaRouterOptions
}
// NewAutheliaRouter creates a new AutheliaRouter object
func NewAutheliaRouter(options *AutheliaRouterOptions) *AutheliaRouter {
options.Database.NewTable("authelia")
//Read settings from database, if exists
options.Database.Read("authelia", "autheliaURL", &options.AutheliaURL)
options.Database.Read("authelia", "useHTTPS", &options.UseHTTPS)
return &AutheliaRouter{
options: options,
}
}
// HandleSetAutheliaURLAndHTTPS is the internal handler for setting the Authelia URL and HTTPS
func (ar *AutheliaRouter) HandleSetAutheliaURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current settings
js, _ := json.Marshal(map[string]interface{}{
"useHTTPS": ar.options.UseHTTPS,
"autheliaURL": ar.options.AutheliaURL,
})
utils.SendJSONResponse(w, string(js))
return
} else if r.Method == http.MethodPost {
//Update the settings
autheliaURL, err := utils.PostPara(r, "autheliaURL")
if err != nil {
utils.SendErrorResponse(w, "autheliaURL not found")
return
}
useHTTPS, err := utils.PostBool(r, "useHTTPS")
if err != nil {
useHTTPS = false
}
//Write changes to runtime
ar.options.AutheliaURL = autheliaURL
ar.options.UseHTTPS = useHTTPS
//Write changes to database
ar.options.Database.Write("authelia", "autheliaURL", autheliaURL)
ar.options.Database.Write("authelia", "useHTTPS", useHTTPS)
utils.SendOK(w)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
// handleAutheliaAuth is the internal handler for Authelia authentication
// Set useHTTPS to true if your authelia server is using HTTPS
// Set autheliaURL to the URL of the Authelia server, e.g. authelia.example.com
func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
client := &http.Client{}
if ar.options.AutheliaURL == "" {
ar.options.Logger.PrintAndLog("Authelia", "Authelia URL not set", nil)
w.WriteHeader(500)
w.Write([]byte("500 - Internal Server Error"))
return errors.New("authelia URL not set")
}
protocol := "http"
if ar.options.UseHTTPS {
protocol = "https"
}
autheliaBaseURL := protocol + "://" + ar.options.AutheliaURL
//Remove tailing slash if any
if autheliaBaseURL[len(autheliaBaseURL)-1] == '/' {
autheliaBaseURL = autheliaBaseURL[:len(autheliaBaseURL)-1]
}
//Make a request to Authelia to verify the request
req, err := http.NewRequest("POST", autheliaBaseURL+"/api/verify", nil)
if err != nil {
ar.options.Logger.PrintAndLog("Authelia", "Unable to create request", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
req.Header.Add("X-Original-URL", fmt.Sprintf("%s://%s", scheme, r.Host))
// Copy cookies from the incoming request
for _, cookie := range r.Cookies() {
req.AddCookie(cookie)
}
// Making the verification request
resp, err := client.Do(req)
if err != nil {
ar.options.Logger.PrintAndLog("Authelia", "Unable to verify", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
if resp.StatusCode != 200 {
redirectURL := autheliaBaseURL + "/?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String()) + "&rm=" + r.Method
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return errors.New("unauthorized")
}
return nil
}

View File

@ -9,17 +9,39 @@ package database
*/ */
import ( import (
"sync" "log"
"runtime"
"imuslab.com/zoraxy/mod/database/dbinc"
) )
type Database struct { type Database struct {
Db interface{} //This will be nil on openwrt and *bolt.DB in the rest of the systems Db interface{} //This will be nil on openwrt, leveldb.DB on x64 platforms or bolt.DB on other platforms
Tables sync.Map BackendType dbinc.BackendType
ReadOnly bool Backend dbinc.Backend
} }
func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) { func NewDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
return newDatabase(dbfile, readOnlyMode) if runtime.GOARCH == "riscv64" {
log.Println("RISCV hardware detected, ignoring the backend type and using FS emulated database")
}
return newDatabase(dbfile, backendType)
}
// Get the recommended backend type for the current system
func GetRecommendedBackendType() dbinc.BackendType {
//Check if the system is running on RISCV hardware
if runtime.GOARCH == "riscv64" {
//RISCV hardware, currently only support FS emulated database
return dbinc.BackendFSOnly
} else if runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "amd64") {
//Powerful hardware
return dbinc.BackendBoltDB
//return dbinc.BackendLevelDB
}
//Default to BoltDB, the safest option
return dbinc.BackendBoltDB
} }
/* /*
@ -29,39 +51,33 @@ func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
err := sysdb.DropTable("MyTable") err := sysdb.DropTable("MyTable")
*/ */
func (d *Database) UpdateReadWriteMode(readOnly bool) { // Create a new table
d.ReadOnly = readOnly
}
//Dump the whole db into a log file
func (d *Database) Dump(filename string) ([]string, error) {
return d.dump(filename)
}
//Create a new table
func (d *Database) NewTable(tableName string) error { func (d *Database) NewTable(tableName string) error {
return d.newTable(tableName) return d.newTable(tableName)
} }
//Check is table exists // Check is table exists
func (d *Database) TableExists(tableName string) bool { func (d *Database) TableExists(tableName string) bool {
return d.tableExists(tableName) return d.tableExists(tableName)
} }
//Drop the given table // Drop the given table
func (d *Database) DropTable(tableName string) error { func (d *Database) DropTable(tableName string) error {
return d.dropTable(tableName) return d.dropTable(tableName)
} }
/* /*
Write to database with given tablename and key. Example Usage: Write to database with given tablename and key. Example Usage:
type demo struct{ type demo struct{
content string content string
} }
thisDemo := demo{ thisDemo := demo{
content: "Hello World", content: "Hello World",
} }
err := sysdb.Write("MyTable", "username/message",thisDemo);
err := sysdb.Write("MyTable", "username/message",thisDemo);
*/ */
func (d *Database) Write(tableName string, key string, value interface{}) error { func (d *Database) Write(tableName string, key string, value interface{}) error {
return d.write(tableName, key, value) return d.write(tableName, key, value)
@ -81,14 +97,21 @@ func (d *Database) Read(tableName string, key string, assignee interface{}) erro
return d.read(tableName, key, assignee) return d.read(tableName, key, assignee)
} }
/*
Check if a key exists in the database table given tablename and key
if sysdb.KeyExists("MyTable", "username/message"){
log.Println("Key exists")
}
*/
func (d *Database) KeyExists(tableName string, key string) bool { func (d *Database) KeyExists(tableName string, key string) bool {
return d.keyExists(tableName, key) return d.keyExists(tableName, key)
} }
/* /*
Delete a value from the database table given tablename and key Delete a value from the database table given tablename and key
err := sysdb.Delete("MyTable", "username/message"); err := sysdb.Delete("MyTable", "username/message");
*/ */
func (d *Database) Delete(tableName string, key string) error { func (d *Database) Delete(tableName string, key string) error {
return d.delete(tableName, key) return d.delete(tableName, key)
@ -115,6 +138,9 @@ func (d *Database) ListTable(tableName string) ([][][]byte, error) {
return d.listTable(tableName) return d.listTable(tableName)
} }
/*
Close the database connection
*/
func (d *Database) Close() { func (d *Database) Close() {
d.close() d.close()
} }

View File

@ -4,183 +4,67 @@
package database package database
import ( import (
"encoding/json"
"errors" "errors"
"log"
"sync"
"github.com/boltdb/bolt" "imuslab.com/zoraxy/mod/database/dbbolt"
"imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/database/dbleveldb"
) )
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) { func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
db, err := bolt.Open(dbfile, 0600, nil) if backendType == dbinc.BackendFSOnly {
if err != nil { return nil, errors.New("Unsupported backend type for this platform")
return nil, err
} }
tableMap := sync.Map{} if backendType == dbinc.BackendLevelDB {
//Build the table list from database db, err := dbleveldb.NewDB(dbfile)
err = db.View(func(tx *bolt.Tx) error { return &Database{
return tx.ForEach(func(name []byte, _ *bolt.Bucket) error { Db: nil,
tableMap.Store(string(name), "") BackendType: backendType,
return nil Backend: db,
}) }, err
}) }
db, err := dbbolt.NewBoltDatabase(dbfile)
return &Database{ return &Database{
Db: db, Db: nil,
Tables: tableMap, BackendType: backendType,
ReadOnly: readOnlyMode, Backend: db,
}, err }, err
} }
//Dump the whole db into a log file
func (d *Database) dump(filename string) ([]string, error) {
results := []string{}
d.Tables.Range(func(tableName, v interface{}) bool {
entries, err := d.ListTable(tableName.(string))
if err != nil {
log.Println("Reading table " + tableName.(string) + " failed: " + err.Error())
return false
}
for _, keypairs := range entries {
results = append(results, string(keypairs[0])+":"+string(keypairs[1])+"\n")
}
return true
})
return results, nil
}
//Create a new table
func (d *Database) newTable(tableName string) error { func (d *Database) newTable(tableName string) error {
if d.ReadOnly == true { return d.Backend.NewTable(tableName)
return errors.New("Operation rejected in ReadOnly mode")
}
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
return nil
})
d.Tables.Store(tableName, "")
return err
} }
//Check is table exists
func (d *Database) tableExists(tableName string) bool { func (d *Database) tableExists(tableName string) bool {
if _, ok := d.Tables.Load(tableName); ok { return d.Backend.TableExists(tableName)
return true
}
return false
} }
//Drop the given table
func (d *Database) dropTable(tableName string) error { func (d *Database) dropTable(tableName string) error {
if d.ReadOnly == true { return d.Backend.DropTable(tableName)
return errors.New("Operation rejected in ReadOnly mode")
}
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
err := tx.DeleteBucket([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
} }
//Write to table
func (d *Database) write(tableName string, key string, value interface{}) error { func (d *Database) write(tableName string, key string, value interface{}) error {
if d.ReadOnly { return d.Backend.Write(tableName, key, value)
return errors.New("Operation rejected in ReadOnly mode")
}
jsonString, err := json.Marshal(value)
if err != nil {
return err
}
err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
b := tx.Bucket([]byte(tableName))
err = b.Put([]byte(key), jsonString)
return err
})
return err
} }
func (d *Database) read(tableName string, key string, assignee interface{}) error { func (d *Database) read(tableName string, key string, assignee interface{}) error {
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error { return d.Backend.Read(tableName, key, assignee)
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
json.Unmarshal(v, &assignee)
return nil
})
return err
} }
func (d *Database) keyExists(tableName string, key string) bool { func (d *Database) keyExists(tableName string, key string) bool {
resultIsNil := false return d.Backend.KeyExists(tableName, key)
if !d.TableExists(tableName) {
//Table not exists. Do not proceed accessing key
log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
return false
}
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
if v == nil {
resultIsNil = true
}
return nil
})
if err != nil {
return false
} else {
if resultIsNil {
return false
} else {
return true
}
}
} }
func (d *Database) delete(tableName string, key string) error { func (d *Database) delete(tableName string, key string) error {
if d.ReadOnly { return d.Backend.Delete(tableName, key)
return errors.New("Operation rejected in ReadOnly mode")
}
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte(tableName)).Delete([]byte(key))
return nil
})
return err
} }
func (d *Database) listTable(tableName string) ([][][]byte, error) { func (d *Database) listTable(tableName string) ([][][]byte, error) {
var results [][][]byte return d.Backend.ListTable(tableName)
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
results = append(results, [][]byte{k, v})
}
return nil
})
return results, err
} }
func (d *Database) close() { func (d *Database) close() {
d.Db.(*bolt.DB).Close() d.Backend.Close()
} }

View File

@ -10,10 +10,19 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"imuslab.com/zoraxy/mod/database/dbinc"
) )
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) { /*
OpenWRT or RISCV backend
For OpenWRT or RISCV platform, we will use the filesystem as the database backend
as boltdb or leveldb is not supported on these platforms, including boltDB and LevelDB
in conditional compilation will create a build error on these platforms
*/
func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
dbRootPath := filepath.ToSlash(filepath.Clean(dbfile)) dbRootPath := filepath.ToSlash(filepath.Clean(dbfile))
dbRootPath = "fsdb/" + dbRootPath dbRootPath = "fsdb/" + dbRootPath
err := os.MkdirAll(dbRootPath, 0755) err := os.MkdirAll(dbRootPath, 0755)
@ -21,24 +30,11 @@ func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
return nil, err return nil, err
} }
tableMap := sync.Map{}
//build the table list from file system
files, err := filepath.Glob(filepath.Join(dbRootPath, "/*"))
if err != nil {
return nil, err
}
for _, file := range files {
if isDirectory(file) {
tableMap.Store(filepath.Base(file), "")
}
}
log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath) log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath)
return &Database{ return &Database{
Db: dbRootPath, Db: dbRootPath,
Tables: tableMap, BackendType: dbinc.BackendFSOnly,
ReadOnly: readOnlyMode, Backend: nil,
}, nil }, nil
} }
@ -61,9 +57,7 @@ func (d *Database) dump(filename string) ([]string, error) {
} }
func (d *Database) newTable(tableName string) error { func (d *Database) newTable(tableName string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
if !fileExists(tablePath) { if !fileExists(tablePath) {
return os.MkdirAll(tablePath, 0755) return os.MkdirAll(tablePath, 0755)
@ -85,9 +79,7 @@ func (d *Database) tableExists(tableName string) bool {
} }
func (d *Database) dropTable(tableName string) error { func (d *Database) dropTable(tableName string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
if d.tableExists(tableName) { if d.tableExists(tableName) {
return os.RemoveAll(tablePath) return os.RemoveAll(tablePath)
@ -98,9 +90,7 @@ func (d *Database) dropTable(tableName string) error {
} }
func (d *Database) write(tableName string, key string, value interface{}) error { func (d *Database) write(tableName string, key string, value interface{}) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName)) tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
js, err := json.Marshal(value) js, err := json.Marshal(value)
if err != nil { if err != nil {
@ -138,9 +128,7 @@ func (d *Database) keyExists(tableName string, key string) bool {
} }
func (d *Database) delete(tableName string, key string) error { func (d *Database) delete(tableName string, key string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
if !d.keyExists(tableName, key) { if !d.keyExists(tableName, key) {
return errors.New("key not exists") return errors.New("key not exists")
} }

View File

@ -0,0 +1,141 @@
package dbbolt
import (
"encoding/json"
"errors"
"github.com/boltdb/bolt"
)
type Database struct {
Db interface{} //This is the bolt database object
}
func NewBoltDatabase(dbfile string) (*Database, error) {
db, err := bolt.Open(dbfile, 0600, nil)
if err != nil {
return nil, err
}
return &Database{
Db: db,
}, err
}
// Create a new table
func (d *Database) NewTable(tableName string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
}
// Check is table exists
func (d *Database) TableExists(tableName string) bool {
return d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
if b == nil {
return errors.New("table not exists")
}
return nil
}) == nil
}
// Drop the given table
func (d *Database) DropTable(tableName string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
err := tx.DeleteBucket([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
}
// Write to table
func (d *Database) Write(tableName string, key string, value interface{}) error {
jsonString, err := json.Marshal(value)
if err != nil {
return err
}
err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
b := tx.Bucket([]byte(tableName))
err = b.Put([]byte(key), jsonString)
return err
})
return err
}
func (d *Database) Read(tableName string, key string, assignee interface{}) error {
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
json.Unmarshal(v, &assignee)
return nil
})
return err
}
func (d *Database) KeyExists(tableName string, key string) bool {
resultIsNil := false
if !d.TableExists(tableName) {
//Table not exists. Do not proceed accessing key
//log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
return false
}
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
if v == nil {
resultIsNil = true
}
return nil
})
if err != nil {
return false
} else {
if resultIsNil {
return false
} else {
return true
}
}
}
func (d *Database) Delete(tableName string, key string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte(tableName)).Delete([]byte(key))
return nil
})
return err
}
func (d *Database) ListTable(tableName string) ([][][]byte, error) {
var results [][][]byte
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
results = append(results, [][]byte{k, v})
}
return nil
})
return results, err
}
func (d *Database) Close() {
d.Db.(*bolt.DB).Close()
}

View File

@ -0,0 +1,67 @@
package dbbolt_test
import (
"os"
"testing"
"imuslab.com/zoraxy/mod/database/dbbolt"
)
func TestNewBoltDatabase(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
if db.Db == nil {
t.Fatalf("Expected non-nil database object")
}
}
func TestNewTable(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
err = db.NewTable("testTable")
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
}
func TestTableExists(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
tableName := "testTable"
err = db.NewTable(tableName)
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
exists := db.TableExists(tableName)
if !exists {
t.Fatalf("Expected table %s to exist", tableName)
}
nonExistentTable := "nonExistentTable"
exists = db.TableExists(nonExistentTable)
if exists {
t.Fatalf("Expected table %s to not exist", nonExistentTable)
}
}

View File

@ -0,0 +1,39 @@
package dbinc
/*
dbinc is the interface for all database backend
*/
type BackendType int
const (
BackendBoltDB BackendType = iota //Default backend
BackendFSOnly //OpenWRT or RISCV backend
BackendLevelDB //LevelDB backend
BackEndAuto = BackendBoltDB
)
type Backend interface {
NewTable(tableName string) error
TableExists(tableName string) bool
DropTable(tableName string) error
Write(tableName string, key string, value interface{}) error
Read(tableName string, key string, assignee interface{}) error
KeyExists(tableName string, key string) bool
Delete(tableName string, key string) error
ListTable(tableName string) ([][][]byte, error)
Close()
}
func (b BackendType) String() string {
switch b {
case BackendBoltDB:
return "BoltDB"
case BackendFSOnly:
return "File System Emulated Key-Value Store"
case BackendLevelDB:
return "LevelDB"
default:
return "Unknown"
}
}

View File

@ -0,0 +1,152 @@
package dbleveldb
import (
"encoding/json"
"log"
"path/filepath"
"strings"
"sync"
"time"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
"imuslab.com/zoraxy/mod/database/dbinc"
)
// Ensure the DB struct implements the Backend interface
var _ dbinc.Backend = (*DB)(nil)
type DB struct {
db *leveldb.DB
Table sync.Map //For emulating table creation
batch leveldb.Batch //Batch write
writeFlushTicker *time.Ticker //Ticker for flushing data into disk
writeFlushStop chan bool //Stop channel for write flush ticker
}
func NewDB(path string) (*DB, error) {
//If the path is not a directory (e.g. /tmp/dbfile.db), convert the filename to directory
if filepath.Ext(path) != "" {
path = strings.ReplaceAll(path, ".", "_")
}
db, err := leveldb.OpenFile(path, nil)
if err != nil {
return nil, err
}
thisDB := &DB{
db: db,
Table: sync.Map{},
batch: leveldb.Batch{},
}
//Create a ticker to flush data into disk every 1 seconds
writeFlushTicker := time.NewTicker(1 * time.Second)
writeFlushStop := make(chan bool)
go func() {
for {
select {
case <-writeFlushTicker.C:
if thisDB.batch.Len() == 0 {
//No flushing needed
continue
}
err = db.Write(&thisDB.batch, nil)
if err != nil {
log.Println("[LevelDB] Failed to flush data into disk: ", err)
}
thisDB.batch.Reset()
case <-writeFlushStop:
return
}
}
}()
thisDB.writeFlushTicker = writeFlushTicker
thisDB.writeFlushStop = writeFlushStop
return thisDB, nil
}
func (d *DB) NewTable(tableName string) error {
//Create a table entry in the sync.Map
d.Table.Store(tableName, true)
return nil
}
func (d *DB) TableExists(tableName string) bool {
_, ok := d.Table.Load(tableName)
return ok
}
func (d *DB) DropTable(tableName string) error {
d.Table.Delete(tableName)
iter := d.db.NewIterator(nil, nil)
defer iter.Release()
for iter.Next() {
key := iter.Key()
if filepath.Dir(string(key)) == tableName {
err := d.db.Delete(key, nil)
if err != nil {
return err
}
}
}
return nil
}
func (d *DB) Write(tableName string, key string, value interface{}) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
d.batch.Put([]byte(filepath.ToSlash(filepath.Join(tableName, key))), data)
return nil
}
func (d *DB) Read(tableName string, key string, assignee interface{}) error {
data, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
if err != nil {
return err
}
return json.Unmarshal(data, assignee)
}
func (d *DB) KeyExists(tableName string, key string) bool {
_, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
return err == nil
}
func (d *DB) Delete(tableName string, key string) error {
return d.db.Delete([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
}
func (d *DB) ListTable(tableName string) ([][][]byte, error) {
iter := d.db.NewIterator(util.BytesPrefix([]byte(tableName+"/")), nil)
defer iter.Release()
var result [][][]byte
for iter.Next() {
key := iter.Key()
//The key contains the table name as prefix. Trim it before returning
value := iter.Value()
result = append(result, [][]byte{[]byte(strings.TrimPrefix(string(key), tableName+"/")), value})
}
err := iter.Error()
if err != nil {
return nil, err
}
return result, nil
}
func (d *DB) Close() {
//Write the remaining data in batch back into disk
d.writeFlushStop <- true
d.writeFlushTicker.Stop()
d.db.Write(&d.batch, nil)
d.db.Close()
}

View File

@ -0,0 +1,141 @@
package dbleveldb_test
import (
"os"
"testing"
"imuslab.com/zoraxy/mod/database/dbleveldb"
)
func TestNewDB(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
}
func TestNewTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
err = db.NewTable("testTable")
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
}
func TestTableExists(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
if !db.TableExists("testTable") {
t.Fatalf("Table should exist")
}
}
func TestDropTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.DropTable("testTable")
if err != nil {
t.Fatalf("Failed to drop table: %v", err)
}
if db.TableExists("testTable") {
t.Fatalf("Table should not exist")
}
}
func TestWriteAndRead(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.Write("testTable", "testKey", "testValue")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
var value string
err = db.Read("testTable", "testKey", &value)
if err != nil {
t.Fatalf("Failed to read from table: %v", err)
}
if value != "testValue" {
t.Fatalf("Expected 'testValue', got '%v'", value)
}
}
func TestListTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.Write("testTable", "testKey1", "testValue1")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
err = db.Write("testTable", "testKey2", "testValue2")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
result, err := db.ListTable("testTable")
if err != nil {
t.Fatalf("Failed to list table: %v", err)
}
if len(result) != 2 {
t.Fatalf("Expected 2 entries, got %v", len(result))
}
expected := map[string]string{
"testTable/testKey1": "\"testValue1\"",
"testTable/testKey2": "\"testValue2\"",
}
for _, entry := range result {
key := string(entry[0])
value := string(entry[1])
if expected[key] != value {
t.Fatalf("Expected value '%v' for key '%v', got '%v'", expected[key], key, value)
}
}
}

View File

@ -21,6 +21,7 @@ import (
- Blacklist - Blacklist
- Whitelist - Whitelist
- Rate Limitor - Rate Limitor
- SSO Auth
- Basic Auth - Basic Auth
- Vitrual Directory Proxy - Vitrual Directory Proxy
- Subdomain Proxy - Subdomain Proxy
@ -77,18 +78,16 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if sep.RequireRateLimit { if sep.RequireRateLimit {
err := h.handleRateLimitRouting(w, r, sep) err := h.handleRateLimitRouting(w, r, sep)
if err != nil { if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 429) h.Parent.Option.Logger.LogHTTPRequest(r, "host", 307)
return return
} }
} }
//Validate basic auth //Validate basic auth
if sep.RequireBasicAuth { respWritten := handleAuthProviderRouting(sep, w, r, h)
err := h.handleBasicAuthRouting(w, r, sep) if respWritten {
if err != nil { //Request handled by subroute
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401) return
return
}
} }
//Check if any virtual directory rules matches //Check if any virtual directory rules matches
@ -98,7 +97,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//Virtual directory routing rule found. Route via vdir mode //Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint) h.vdirRequest(w, r, targetProxyEndpoint)
return return
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root { } else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyTypeRoot {
potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/") potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled { if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint //Missing tailing slash. Redirect to target proxy endpoint
@ -143,7 +142,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/* /*
handleRootRouting handleRootRouting
This function handle root routing situations where there are no subdomain This function handle root routing (aka default sites) situations where there are no subdomain
, vdir or special routing rule matches the requested URI. , vdir or special routing rule matches the requested URI.
Once entered this routing segment, the root routing options will take over Once entered this routing segment, the root routing options will take over
@ -163,7 +162,6 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
fallthrough fallthrough
case DefaultSite_ReverseProxy: case DefaultSite_ReverseProxy:
//They both share the same behavior //They both share the same behavior
//Check if any virtual directory rules matches //Check if any virtual directory rules matches
proxyingPath := strings.TrimSpace(r.RequestURI) proxyingPath := strings.TrimSpace(r.RequestURI)
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath) targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
@ -171,7 +169,7 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
//Virtual directory routing rule found. Route via vdir mode //Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint) h.vdirRequest(w, r, targetProxyEndpoint)
return return
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root { } else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyTypeRoot {
potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/") potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled { if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint //Missing tailing slash. Redirect to target proxy endpoint
@ -188,8 +186,13 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
redirectTarget = "about:blank" redirectTarget = "about:blank"
} }
//Check if the default site values start with http or https
if !strings.HasPrefix(redirectTarget, "http://") && !strings.HasPrefix(redirectTarget, "https://") {
redirectTarget = "http://" + redirectTarget
}
//Check if it is an infinite loopback redirect //Check if it is an infinite loopback redirect
parsedURL, err := url.Parse(proot.DefaultSiteValue) parsedURL, err := url.Parse(redirectTarget)
if err != nil { if err != nil {
//Error when parsing target. Send to root //Error when parsing target. Send to root
h.hostRequest(w, r, h.Parent.Root) h.hostRequest(w, r, h.Parent.Root)
@ -214,5 +217,27 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
} else { } else {
w.Write(template) w.Write(template)
} }
case DefaultSite_NoResponse:
//No response. Just close the connection
h.Parent.logRequest(r, false, 444, "root-noresponse", domainOnly)
hijacker, ok := w.(http.Hijacker)
if !ok {
w.Header().Set("Connection", "close")
return
}
conn, _, err := hijacker.Hijack()
if err != nil {
w.Header().Set("Connection", "close")
return
}
conn.Close()
case DefaultSite_TeaPot:
//I'm a teapot
h.Parent.logRequest(r, false, 418, "root-teapot", domainOnly)
http.Error(w, "I'm a teapot", http.StatusTeapot)
default:
//Unknown routing option. Send empty response
h.Parent.logRequest(r, false, 544, "root-unknown", domainOnly)
http.Error(w, "544 - No Route Defined", 544)
} }
} }

View File

@ -1,7 +1,6 @@
package dynamicproxy package dynamicproxy
import ( import (
"log"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -16,7 +15,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter,
accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID) accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID)
if err != nil { if err != nil {
//Unable to load access rule. Target rule not found? //Unable to load access rule. Target rule not found?
log.Println("[Proxy] Unable to load access rule: " + ruleID) h.Parent.Option.Logger.PrintAndLog("proxy-access", "Unable to load access rule: "+ruleID, err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 - Internal Server Error")) w.Write([]byte("500 - Internal Server Error"))
return true return true

View File

@ -0,0 +1,108 @@
package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
/*
authProviders.go
This script handle authentication providers
*/
/*
Central Authentication Provider Router
This function will route the request to the correct authentication provider
if the return value is true, do not continue to the next handler
handleAuthProviderRouting takes in 4 parameters:
- sep: the ProxyEndpoint object
- w: the http.ResponseWriter object
- r: the http.Request object
- h: the ProxyHandler object
and return a boolean indicate if the request is written to http.ResponseWriter
- true: the request is handled, do not write to http.ResponseWriter
- false: the request is not handled (usually means auth ok), continue to the next handler
*/
func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool {
if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return true
}
} else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthelia {
err := h.handleAutheliaAuth(w, r)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return true
}
}
//No authentication provider, do not need to handle
return false
}
/* Basic Auth */
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname())
}
return err
}
// Handle basic auth logic
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.AuthenticationProvider.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.AuthenticationProvider.BasicAuthExceptionRules {
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
}
}
}
u, p, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
//Check for the credentials to see if there is one matching
hashedPassword := auth.Hash(p)
matchingFound := false
for _, cred := range pe.AuthenticationProvider.BasicAuthCredentials {
if u == cred.Username && hashedPassword == cred.PasswordHash {
matchingFound = true
//Set the X-Remote-User header
r.Header.Set("X-Remote-User", u)
break
}
}
if !matchingFound {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
return nil
}
/* Authelia */
// Handle authelia auth routing
func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r)
}

View File

@ -1,63 +0,0 @@
package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
/*
BasicAuth.go
This file handles the basic auth on proxy endpoints
if RequireBasicAuth is set to true
*/
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname())
}
return err
}
// Handle basic auth logic
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.BasicAuthExceptionRules {
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
}
}
}
u, p, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
//Check for the credentials to see if there is one matching
hashedPassword := auth.Hash(p)
matchingFound := false
for _, cred := range pe.BasicAuthCredentials {
if u == cred.Username && hashedPassword == cred.PasswordHash {
matchingFound = true
break
}
}
if !matchingFound {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
return nil
}

View File

@ -1,80 +1,12 @@
package dynamicproxy package dynamicproxy
import (
"strconv"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
)
/* /*
CustomHeader.go CustomHeader.go
This script handle parsing and injecting custom headers This script handle parsing and injecting custom headers
into the dpcore routing logic into the dpcore routing logic
Updates: 2024-10-26
Contents from this file has been moved to rewrite/rewrite.go
This file is kept for contributors to understand the structure
*/ */
// SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers
// return upstream header and downstream header key-value pairs
// if the header is expected to be deleted, the value will be set to empty string
func (ept *ProxyEndpoint) SplitInboundOutboundHeaders() ([][]string, [][]string) {
if len(ept.UserDefinedHeaders) == 0 && ept.HSTSMaxAge == 0 && !ept.EnablePermissionPolicyHeader {
//Early return if there are no defined headers
return [][]string{}, [][]string{}
}
//Use pre-allocation for faster performance
//Downstream +2 for Permission Policy and HSTS
upstreamHeaders := make([][]string, len(ept.UserDefinedHeaders))
downstreamHeaders := make([][]string, len(ept.UserDefinedHeaders)+2)
upstreamHeaderCounter := 0
downstreamHeaderCounter := 0
//Sort the headers into upstream or downstream
for _, customHeader := range ept.UserDefinedHeaders {
thisHeaderSet := make([]string, 2)
thisHeaderSet[0] = customHeader.Key
thisHeaderSet[1] = customHeader.Value
if customHeader.IsRemove {
//Prevent invalid config
thisHeaderSet[1] = ""
}
//Assign to slice
if customHeader.Direction == HeaderDirection_ZoraxyToUpstream {
upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet
upstreamHeaderCounter++
} else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream {
downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet
downstreamHeaderCounter++
}
}
//Check if the endpoint require HSTS headers
if ept.HSTSMaxAge > 0 {
if ept.ContainsWildcardName(true) {
//Endpoint listening domain includes wildcards.
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge)) + "; includeSubdomains"}
} else {
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge))}
}
downstreamHeaderCounter++
}
//Check if the endpoint require Permission Policy
if ept.EnablePermissionPolicyHeader {
var usingPermissionPolicy *permissionpolicy.PermissionsPolicy
if ept.PermissionPolicy != nil {
//Custom permission policy
usingPermissionPolicy = ept.PermissionPolicy
} else {
//Permission policy is enabled but not customized. Use default
usingPermissionPolicy = permissionpolicy.GetDefaultPermissionPolicy()
}
downstreamHeaders[downstreamHeaderCounter] = usingPermissionPolicy.ToKeyValueHeader()
downstreamHeaderCounter++
}
return upstreamHeaders, downstreamHeaders
}

View File

@ -0,0 +1,65 @@
package dynamicproxy
import (
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
)
/*
Default Provider
This script provide the default options for all datatype
provided by dynamicproxy module
*/
// GetDefaultAuthenticationProvider return a default authentication provider
func GetDefaultAuthenticationProvider() *AuthenticationProvider {
return &AuthenticationProvider{
AuthMethod: AuthMethodNone,
BasicAuthCredentials: []*BasicAuthCredentials{},
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
BasicAuthGroupIDs: []string{},
AutheliaURL: "",
UseHTTPS: false,
}
}
// GetDefaultHeaderRewriteRules return a default header rewrite rules
func GetDefaultHeaderRewriteRules() *HeaderRewriteRules {
return &HeaderRewriteRules{
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
RequestHostOverwrite: "",
HSTSMaxAge: 0,
EnablePermissionPolicyHeader: false,
PermissionPolicy: nil,
DisableHopByHopHeaderRemoval: false,
}
}
// GetDefaultProxyEndpoint return a default proxy endpoint
func GetDefaultProxyEndpoint() ProxyEndpoint {
randomPrefix := uuid.New().String()
return ProxyEndpoint{
ProxyType: ProxyTypeHost,
RootOrMatchingDomain: randomPrefix + ".internal",
MatchingDomainAlias: []string{},
ActiveOrigins: []*loadbalance.Upstream{},
InactiveOrigins: []*loadbalance.Upstream{},
UseStickySession: false,
UseActiveLoadBalance: false,
Disabled: false,
BypassGlobalTLS: false,
VirtualDirectories: []*VirtualDirectoryEndpoint{},
HeaderRewriteRules: GetDefaultHeaderRewriteRules(),
EnableWebsocketCustomHeaders: false,
AuthenticationProvider: GetDefaultAuthenticationProvider(),
RequireRateLimit: false,
RateLimit: 0,
DisableUptimeMonitor: false,
AccessFilterUUID: "default",
DefaultSiteOption: DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
}
}

View File

@ -1,11 +1,26 @@
package domainsniff package domainsniff
/*
Domainsniff
This package contain codes that perform project / domain specific behavior in Zoraxy
If you want Zoraxy to handle a particular domain or open source project in a special way,
you can add the checking logic here.
*/
import ( import (
"crypto/tls"
"encoding/json"
"fmt"
"net" "net"
"net/http"
"strings"
"time" "time"
"imuslab.com/zoraxy/mod/utils"
) )
//Check if the domain is reachable and return err if not reachable // Check if the domain is reachable and return err if not reachable
func DomainReachableWithError(domain string) error { func DomainReachableWithError(domain string) error {
timeout := 1 * time.Second timeout := 1 * time.Second
conn, err := net.DialTimeout("tcp", domain, timeout) conn, err := net.DialTimeout("tcp", domain, timeout)
@ -17,7 +32,132 @@ func DomainReachableWithError(domain string) error {
return nil return nil
} }
//Check if domain reachable // Check if a domain have TLS but it is self-signed or expired
// Return false if sniff error
func DomainIsSelfSigned(domain string) bool {
//Extract the domain from URl in case the user input the full URL
host, port, err := net.SplitHostPort(domain)
if err != nil {
host = domain
} else {
domain = host + ":" + port
}
if !strings.Contains(domain, ":") {
domain = domain + ":443"
}
//Get the certificate
conn, err := net.Dial("tcp", domain)
if err != nil {
return false
}
defer conn.Close()
//Connect with TLS using secure verify
tlsConn := tls.Client(conn, nil)
err = tlsConn.Handshake()
if err == nil {
//This is a valid certificate
fmt.Println()
return false
}
//Connect with TLS using insecure skip verify
config := &tls.Config{
InsecureSkipVerify: true,
}
tlsConn = tls.Client(conn, config)
err = tlsConn.Handshake()
//If the handshake is successful, this is a self-signed certificate
return err == nil
}
// Check if domain reachable
func DomainReachable(domain string) bool { func DomainReachable(domain string) bool {
return DomainReachableWithError(domain) == nil return DomainReachableWithError(domain) == nil
} }
// Check if domain is served by a web server using HTTPS
func DomainUsesTLS(targetURL string) bool {
//Check if the site support https
httpsUrl := fmt.Sprintf("https://%s", targetURL)
httpUrl := fmt.Sprintf("http://%s", targetURL)
client := http.Client{Timeout: 5 * time.Second}
resp, err := client.Head(httpsUrl)
if err == nil && resp.StatusCode == http.StatusOK {
return true
}
resp, err = client.Head(httpUrl)
if err == nil && resp.StatusCode == http.StatusOK {
return false
}
//If the site is not reachable, return false
return false
}
/*
WebSocket Header Sniff
*/
// Check if the requst is a special case where
// user defined header shall not be passed over
func RequireWebsocketHeaderCopy(r *http.Request) bool {
//Return false for proxmox
if IsProxmox(r) {
return false
}
//Add more edge cases here
return true
}
/*
Request Handlers
*/
//Check if site support TLS
//Pass in ?selfsignchk=true to also check for self-signed certificate
func HandleCheckSiteSupportTLS(w http.ResponseWriter, r *http.Request) {
targetURL, err := utils.PostPara(r, "url")
if err != nil {
utils.SendErrorResponse(w, "invalid url given")
return
}
//If the selfsign flag is set, also chec for self-signed certificate
_, err = utils.PostBool(r, "selfsignchk")
if err == nil {
//Return the https and selfsign status
type result struct {
Protocol string `json:"protocol"`
SelfSign bool `json:"selfsign"`
}
scanResult := result{Protocol: "http", SelfSign: false}
if DomainUsesTLS(targetURL) {
scanResult.Protocol = "https"
if DomainIsSelfSigned(targetURL) {
scanResult.SelfSign = true
}
}
js, _ := json.Marshal(scanResult)
utils.SendJSONResponse(w, string(js))
return
}
if DomainUsesTLS(targetURL) {
js, _ := json.Marshal("https")
utils.SendJSONResponse(w, string(js))
return
} else {
js, _ := json.Marshal("http")
utils.SendJSONResponse(w, string(js))
return
}
}

View File

@ -54,6 +54,9 @@ type ReverseProxy struct {
Prepender string Prepender string
Verbal bool Verbal bool
//Appended by Zoraxy project
} }
type ResponseRewriteRuleSet struct { type ResponseRewriteRuleSet struct {
@ -106,6 +109,8 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2 thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).DisableCompression = true thisTransporter.(*http.Transport).DisableCompression = true
//TODO: Add user adjustable timeout option here
if dpcOptions.IgnoreTLSVerification { if dpcOptions.IgnoreTLSVerification {
//Ignore TLS certificate validation error //Ignore TLS certificate validation error
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
@ -350,13 +355,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
} }
} }
//TODO: Figure out a way to proxy for proxmox
//if res.StatusCode == 501 || res.StatusCode == 500 {
// fmt.Println(outreq.Proto, outreq.RemoteAddr, outreq.RequestURI)
// fmt.Println(">>>", outreq.Method, res.Header, res.ContentLength, res.StatusCode)
// fmt.Println(outreq.Header, req.Host)
//}
//Add debug X-Proxy-By tracker //Add debug X-Proxy-By tracker
res.Header.Set("x-proxy-by", "zoraxy/"+rrr.Version) res.Header.Set("x-proxy-by", "zoraxy/"+rrr.Version)

View File

@ -1,29 +1,67 @@
package dpcore_test package dpcore_test
import ( import (
"net/url"
"testing" "testing"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
) )
func TestReplaceLocationHost(t *testing.T) { func TestReplaceLocationHost(t *testing.T) {
urlString := "http://private.com/test/newtarget/" tests := []struct {
rrr := &dpcore.ResponseRewriteRuleSet{ name string
OriginalHost: "test.example.com", urlString string
ProxyDomain: "private.com/test", rrr *dpcore.ResponseRewriteRuleSet
UseTLS: true, useTLS bool
} expectedResult string
useTLS := true expectError bool
}{
{
name: "Basic HTTP to HTTPS redirection",
urlString: "http://example.com/resource",
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "example.com", OriginalHost: "proxy.example.com", UseTLS: true},
useTLS: true,
expectedResult: "https://proxy.example.com/resource",
expectError: false,
},
expectedResult := "https://test.example.com/newtarget/" {
name: "Basic HTTPS to HTTP redirection",
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS) urlString: "https://proxy.example.com/resource",
if err != nil { rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "proxy.example.com", OriginalHost: "proxy.example.com", UseTLS: false},
t.Errorf("Error occurred: %v", err) useTLS: false,
expectedResult: "http://proxy.example.com/resource",
expectError: false,
},
{
name: "No rewrite on mismatched domain",
urlString: "http://anotherdomain.com/resource",
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "proxy.example.com", OriginalHost: "proxy.example.com", UseTLS: true},
useTLS: true,
expectedResult: "http://anotherdomain.com/resource",
expectError: false,
},
{
name: "Subpath trimming with HTTPS",
urlString: "https://blog.example.com/post?id=1",
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "blog.example.com", OriginalHost: "proxy.example.com/blog", UseTLS: true},
useTLS: true,
expectedResult: "https://proxy.example.com/blog/post?id=1",
expectError: false,
},
} }
if result != expectedResult { for _, tt := range tests {
t.Errorf("Expected: %s, but got: %s", expectedResult, result) t.Run(tt.name, func(t *testing.T) {
result, err := dpcore.ReplaceLocationHost(tt.urlString, tt.rrr, tt.useTLS)
if (err != nil) != tt.expectError {
t.Errorf("Expected error: %v, got: %v", tt.expectError, err)
}
if result != tt.expectedResult {
result, _ = url.QueryUnescape(result)
t.Errorf("Expected result: %s, got: %s", tt.expectedResult, result)
}
})
} }
} }
@ -36,7 +74,7 @@ func TestReplaceLocationHostRelative(t *testing.T) {
} }
useTLS := true useTLS := true
expectedResult := "https://test.example.com/api/" expectedResult := "api/"
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS) result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
if err != nil { if err != nil {

View File

@ -60,7 +60,7 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
return u.String(), nil return u.String(), nil
} }
// Debug functions // Debug functions for replaceLocationHost
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) { func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
return replaceLocationHost(urlString, rrr, useTLS) return replaceLocationHost(urlString, rrr, useTLS)
} }

View File

@ -144,7 +144,7 @@ func (router *Router) StartProxyService() error {
} }
//Validate basic auth //Validate basic auth
if sep.RequireBasicAuth { if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
err := handleBasicAuth(w, r, sep) err := handleBasicAuth(w, r, sep)
if err != nil { if err != nil {
return return
@ -157,12 +157,18 @@ func (router *Router) StartProxyService() error {
router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err) router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err)
router.logRequest(r, false, 404, "vdir-http", r.Host) router.logRequest(r, false, 404, "vdir-http", r.Host)
} }
endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
if sep.HeaderRewriteRules != nil {
endpointProxyRewriteRules = sep.HeaderRewriteRules
}
selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: selectedUpstream.OriginIpOrDomain, ProxyDomain: selectedUpstream.OriginIpOrDomain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
UseTLS: selectedUpstream.RequireTLS, UseTLS: selectedUpstream.RequireTLS,
HostHeaderOverwrite: sep.RequestHostOverwrite, HostHeaderOverwrite: endpointProxyRewriteRules.RequestHostOverwrite,
NoRemoveHopByHop: sep.DisableHopByHopHeaderRemoval, NoRemoveHopByHop: endpointProxyRewriteRules.DisableHopByHopHeaderRemoval,
PathPrefix: "", PathPrefix: "",
Version: sep.parent.Option.HostVersion, Version: sep.parent.Option.HostVersion,
}) })
@ -291,7 +297,7 @@ func (router *Router) Restart() error {
return err return err
} }
time.Sleep(300 * time.Millisecond) time.Sleep(800 * time.Millisecond)
// Start the server // Start the server
err = router.StartProxyService() err = router.StartProxyService()
if err != nil { if err != nil {

View File

@ -8,6 +8,7 @@ import (
"golang.org/x/text/cases" "golang.org/x/text/cases"
"golang.org/x/text/language" "golang.org/x/text/language"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
) )
/* /*
@ -26,7 +27,12 @@ import (
// Check if a user define header exists in this endpoint, ignore case // Check if a user define header exists in this endpoint, ignore case
func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool { func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
for _, header := range ep.UserDefinedHeaders { endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
if ep.HeaderRewriteRules != nil {
endpointProxyRewriteRules = ep.HeaderRewriteRules
}
for _, header := range endpointProxyRewriteRules.UserDefinedHeaders {
if strings.EqualFold(header.Key, key) { if strings.EqualFold(header.Key, key) {
return true return true
} }
@ -36,26 +42,32 @@ func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
// Remvoe a user defined header from the list // Remvoe a user defined header from the list
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error { func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
newHeaderList := []*UserDefinedHeader{} newHeaderList := []*rewrite.UserDefinedHeader{}
for _, header := range ep.UserDefinedHeaders { if ep.HeaderRewriteRules == nil {
ep.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
for _, header := range ep.HeaderRewriteRules.UserDefinedHeaders {
if !strings.EqualFold(header.Key, key) { if !strings.EqualFold(header.Key, key) {
newHeaderList = append(newHeaderList, header) newHeaderList = append(newHeaderList, header)
} }
} }
ep.UserDefinedHeaders = newHeaderList ep.HeaderRewriteRules.UserDefinedHeaders = newHeaderList
return nil return nil
} }
// Add a user defined header to the list, duplicates will be automatically removed // Add a user defined header to the list, duplicates will be automatically removed
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *UserDefinedHeader) error { func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *rewrite.UserDefinedHeader) error {
if ep.UserDefinedHeaderExists(newHeaderRule.Key) { if ep.UserDefinedHeaderExists(newHeaderRule.Key) {
ep.RemoveUserDefinedHeader(newHeaderRule.Key) ep.RemoveUserDefinedHeader(newHeaderRule.Key)
} }
if ep.HeaderRewriteRules == nil {
ep.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
newHeaderRule.Key = cases.Title(language.Und, cases.NoLower).String(newHeaderRule.Key) newHeaderRule.Key = cases.Title(language.Und, cases.NoLower).String(newHeaderRule.Key)
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, newHeaderRule) ep.HeaderRewriteRules.UserDefinedHeaders = append(ep.HeaderRewriteRules.UserDefinedHeaders, newHeaderRule)
return nil return nil
} }
@ -105,7 +117,7 @@ func (ep *ProxyEndpoint) RemoveVirtualDirectoryRuleByMatchingPath(matchingPath s
return errors.New("target virtual directory routing rule not found") return errors.New("target virtual directory routing rule not found")
} }
// Delete a vdir rule by its matching path // Add a vdir rule by its matching path
func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint) (*ProxyEndpoint, error) { func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint) (*ProxyEndpoint, error) {
//Check for matching path duplicate //Check for matching path duplicate
if ep.GetVirtualDirectoryRuleByMatchingPath(vdir.MatchingPath) != nil { if ep.GetVirtualDirectoryRuleByMatchingPath(vdir.MatchingPath) != nil {
@ -122,9 +134,9 @@ func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint)
return nil, err return nil, err
} }
if ep.ProxyType == ProxyType_Root { if ep.ProxyType == ProxyTypeRoot {
parentRouter.Root = readyRoutingRule parentRouter.Root = readyRoutingRule
} else if ep.ProxyType == ProxyType_Host { } else if ep.ProxyType == ProxyTypeHost {
ep.Remove() ep.Remove()
parentRouter.AddProxyRouteToRuntime(readyRoutingRule) parentRouter.AddProxyRouteToRuntime(readyRoutingRule)
} else { } else {
@ -263,5 +275,6 @@ func (ep *ProxyEndpoint) Remove() error {
// use prepare -> remove -> add if you change anything in the endpoint // use prepare -> remove -> add if you change anything in the endpoint
// that effects the proxy routing src / dest // that effects the proxy routing src / dest
func (ep *ProxyEndpoint) UpdateToRuntime() { func (ep *ProxyEndpoint) UpdateToRuntime() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep) lookupHostname := strings.ToLower(ep.RootOrMatchingDomain)
ep.parent.ProxyEndpoints.Store(lookupHostname, ep)
} }

View File

@ -83,6 +83,10 @@ func GetUpstreamsAsString(upstreams []*Upstream) string {
for _, upstream := range upstreams { for _, upstream := range upstreams {
targets = append(targets, upstream.String()) targets = append(targets, upstream.String())
} }
if len(targets) == 0 {
//No upstream
return "(no upstream config)"
}
return strings.Join(targets, ", ") return strings.Join(targets, ", ")
} }
@ -93,7 +97,7 @@ func (m *RouteManager) Close() {
} }
// Print debug message // Log Println, replace all log.Println or fmt.Println with this
func (m *RouteManager) debugPrint(message string, err error) { func (m *RouteManager) println(message string, err error) {
m.Options.Logger.PrintAndLog("LoadBalancer", message, err) m.Options.Logger.PrintAndLog("LoadBalancer", message, err)
} }

View File

@ -2,8 +2,6 @@ package loadbalance
import ( import (
"errors" "errors"
"fmt"
"log"
"math/rand" "math/rand"
"net/http" "net/http"
) )
@ -29,7 +27,7 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
//No valid session found. Assign a new upstream //No valid session found. Assign a new upstream
targetOrigin, index, err := getRandomUpstreamByWeight(origins) targetOrigin, index, err := getRandomUpstreamByWeight(origins)
if err != nil { if err != nil {
fmt.Println("Oops. Unable to get random upstream") m.println("Unable to get random upstream", err)
targetOrigin = origins[0] targetOrigin = origins[0]
index = 0 index = 0
} }
@ -44,7 +42,7 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
var err error var err error
targetOrigin, _, err = getRandomUpstreamByWeight(origins) targetOrigin, _, err = getRandomUpstreamByWeight(origins)
if err != nil { if err != nil {
log.Println(err) m.println("Failed to get next origin", err)
targetOrigin = origins[0] targetOrigin = origins[0]
} }
@ -102,42 +100,66 @@ func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream)
/* Functions related to random upstream picking */ /* Functions related to random upstream picking */
// Get a random upstream by the weights defined in Upstream struct, return the upstream, index value and any error // Get a random upstream by the weights defined in Upstream struct, return the upstream, index value and any error
func getRandomUpstreamByWeight(upstreams []*Upstream) (*Upstream, int, error) { func getRandomUpstreamByWeight(upstreams []*Upstream) (*Upstream, int, error) {
var ret *Upstream // If there is only one upstream, return it
sum := 0 if len(upstreams) == 1 {
for _, c := range upstreams { return upstreams[0], 0, nil
sum += c.Weight
}
r, err := intRange(0, sum)
if err != nil {
return ret, -1, err
}
counter := 0
for _, c := range upstreams {
r -= c.Weight
if r < 0 {
return c, counter, nil
}
counter++
} }
if ret == nil { // Preserve the index with upstreams
//All fallback type upstreamWithIndex struct {
//use the first one that is with weight = 0 Upstream *Upstream
fallbackUpstreams := []*Upstream{} Index int
fallbackUpstreamsOriginalID := []int{}
for ix, upstream := range upstreams {
if upstream.Weight == 0 {
fallbackUpstreams = append(fallbackUpstreams, upstream)
fallbackUpstreamsOriginalID = append(fallbackUpstreamsOriginalID, ix)
}
}
upstreamID := rand.Intn(len(fallbackUpstreams))
return fallbackUpstreams[upstreamID], fallbackUpstreamsOriginalID[upstreamID], nil
} }
return ret, -1, errors.New("failed to pick an upstream origin server")
// Calculate total weight for upstreams with weight > 0
totalWeight := 0
fallbackUpstreams := make([]upstreamWithIndex, 0, len(upstreams))
for index, upstream := range upstreams {
if upstream.Weight > 0 {
totalWeight += upstream.Weight
} else {
// Collect fallback upstreams
fallbackUpstreams = append(fallbackUpstreams, upstreamWithIndex{upstream, index})
}
}
// If there are no upstreams with weight > 0, return a fallback upstream if available
if totalWeight == 0 {
if len(fallbackUpstreams) > 0 {
// Randomly select one of the fallback upstreams
randIndex := rand.Intn(len(fallbackUpstreams))
return fallbackUpstreams[randIndex].Upstream, fallbackUpstreams[randIndex].Index, nil
}
// No upstreams available at all
return nil, -1, errors.New("no valid upstream servers available")
}
// Random weight between 0 and total weight
randomWeight := rand.Intn(totalWeight)
// Select an upstream based on the random weight
for index, upstream := range upstreams {
if upstream.Weight > 0 { // Only consider upstreams with weight > 0
if randomWeight < upstream.Weight {
// Return the selected upstream and its index
return upstream, index, nil
}
randomWeight -= upstream.Weight
}
}
// If we reach here, it means we should return a fallback upstream if available
if len(fallbackUpstreams) > 0 {
randIndex := rand.Intn(len(fallbackUpstreams))
return fallbackUpstreams[randIndex].Upstream, fallbackUpstreams[randIndex].Index, nil
}
return nil, -1, errors.New("failed to pick an upstream origin server")
} }
// IntRange returns a random integer in the range from min to max. // IntRange returns a random integer in the range from min to max.
/*
func intRange(min, max int) (int, error) { func intRange(min, max int) (int, error) {
var result int var result int
switch { switch {
@ -152,3 +174,4 @@ func intRange(min, max int) (int, error) {
} }
return result, nil return result, nil
} }
*/

View File

@ -0,0 +1,100 @@
package loadbalance
import (
"fmt"
"math"
"math/rand"
"testing"
"time"
)
// func getRandomUpstreamByWeight(upstreams []*Upstream) (*Upstream, int, error) { ... }
func TestRandomUpstreamSelection(t *testing.T) {
rand.Seed(time.Now().UnixNano()) // Seed for randomness
// Define some test upstreams
upstreams := []*Upstream{
{
OriginIpOrDomain: "192.168.1.1:8080",
RequireTLS: false,
SkipCertValidations: false,
SkipWebSocketOriginCheck: false,
Weight: 1,
MaxConn: 0, // No connection limit for now
},
{
OriginIpOrDomain: "192.168.1.2:8080",
RequireTLS: false,
SkipCertValidations: false,
SkipWebSocketOriginCheck: false,
Weight: 1,
MaxConn: 0,
},
{
OriginIpOrDomain: "192.168.1.3:8080",
RequireTLS: true,
SkipCertValidations: true,
SkipWebSocketOriginCheck: true,
Weight: 1,
MaxConn: 0,
},
{
OriginIpOrDomain: "192.168.1.4:8080",
RequireTLS: true,
SkipCertValidations: true,
SkipWebSocketOriginCheck: true,
Weight: 1,
MaxConn: 0,
},
}
// Track how many times each upstream is selected
selectionCount := make(map[string]int)
totalPicks := 10000 // Number of times to call getRandomUpstreamByWeight
//expectedPickCount := totalPicks / len(upstreams) // Ideal count for each upstream
// Pick upstreams and record their selection count
for i := 0; i < totalPicks; i++ {
upstream, _, err := getRandomUpstreamByWeight(upstreams)
if err != nil {
t.Fatalf("Error getting random upstream: %v", err)
}
selectionCount[upstream.OriginIpOrDomain]++
}
// Condition 1: Ensure every upstream has been picked at least once
for _, upstream := range upstreams {
if selectionCount[upstream.OriginIpOrDomain] == 0 {
t.Errorf("Upstream %s was never selected", upstream.OriginIpOrDomain)
}
}
// Condition 2: Check that the distribution is within 1-2 standard deviations
counts := make([]float64, len(upstreams))
for i, upstream := range upstreams {
counts[i] = float64(selectionCount[upstream.OriginIpOrDomain])
}
mean := float64(totalPicks) / float64(len(upstreams))
stddev := calculateStdDev(counts, mean)
tolerance := 2 * stddev // Allowing up to 2 standard deviations
for i, count := range counts {
if math.Abs(count-mean) > tolerance {
t.Errorf("Selection of upstream %s is outside acceptable range: %v picks (mean: %v, stddev: %v)", upstreams[i].OriginIpOrDomain, count, mean, stddev)
}
}
fmt.Println("Selection count:", selectionCount)
fmt.Printf("Mean: %.2f, StdDev: %.2f\n", mean, stddev)
}
// Helper function to calculate standard deviation
func calculateStdDev(data []float64, mean float64) float64 {
var sumOfSquares float64
for _, value := range data {
sumOfSquares += (value - mean) * (value - mean)
}
variance := sumOfSquares / float64(len(data))
return math.Sqrt(variance)
}

View File

@ -10,7 +10,9 @@ import (
"sort" "sort"
"strings" "strings"
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/netutils" "imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/websocketproxy" "imuslab.com/zoraxy/mod/websocketproxy"
@ -34,6 +36,7 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
// Get the proxy endpoint from hostname, which might includes checking of wildcard certificates // Get the proxy endpoint from hostname, which might includes checking of wildcard certificates
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint { func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
var targetSubdomainEndpoint *ProxyEndpoint = nil var targetSubdomainEndpoint *ProxyEndpoint = nil
hostname = strings.ToLower(hostname)
ep, ok := router.ProxyEndpoints.Load(hostname) ep, ok := router.ProxyEndpoints.Load(hostname)
if ok { if ok {
//Exact hit //Exact hit
@ -117,7 +120,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession) selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession)
if err != nil { if err != nil {
http.ServeFile(w, r, "./web/rperror.html") http.ServeFile(w, r, "./web/rperror.html")
log.Println(err.Error()) h.Parent.Option.Logger.PrintAndLog("proxy", "Failed to assign an upstream for this request", err)
h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname()) h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname())
return return
} }
@ -141,9 +144,17 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL) u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
} }
h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain) h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain)
if target.HeaderRewriteRules == nil {
target.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: selectedUpstream.SkipCertValidations, SkipTLSValidation: selectedUpstream.SkipCertValidations,
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck, SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
CopyAllHeaders: target.EnableWebsocketCustomHeaders,
UserDefinedHeaders: target.HeaderRewriteRules.UserDefinedHeaders,
Logger: h.Parent.Option.Logger,
}) })
wspHandler.ServeHTTP(w, r) wspHandler.ServeHTTP(w, r)
return return
@ -157,9 +168,23 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
r.URL, _ = url.Parse(originalHostHeader) r.URL, _ = url.Parse(originalHostHeader)
} }
//Build downstream and upstream header rules //Populate the user-defined headers with the values from the request
upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders() headerRewriteOptions := GetDefaultHeaderRewriteRules()
if target.HeaderRewriteRules != nil {
headerRewriteOptions = target.HeaderRewriteRules
}
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, headerRewriteOptions.UserDefinedHeaders)
//Build downstream and upstream header rules
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
HSTSMaxAge: headerRewriteOptions.HSTSMaxAge,
HSTSIncludeSubdomains: target.ContainsWildcardName(true),
EnablePermissionPolicyHeader: headerRewriteOptions.EnablePermissionPolicyHeader,
PermissionPolicy: headerRewriteOptions.PermissionPolicy,
})
//Handle the request reverse proxy
statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: selectedUpstream.OriginIpOrDomain, ProxyDomain: selectedUpstream.OriginIpOrDomain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
@ -168,8 +193,8 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
PathPrefix: "", PathPrefix: "",
UpstreamHeaders: upstreamHeaders, UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders, DownstreamHeaders: downstreamHeaders,
HostHeaderOverwrite: target.RequestHostOverwrite, HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
NoRemoveHopByHop: target.DisableHopByHopHeaderRemoval, NoRemoveHopByHop: headerRewriteOptions.DisableHopByHopHeaderRemoval,
Version: target.parent.Option.HostVersion, Version: target.parent.Option.HostVersion,
}) })
@ -177,11 +202,10 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
if err != nil { if err != nil {
if errors.As(err, &dnsError) { if errors.As(err, &dnsError) {
http.ServeFile(w, r, "./web/hosterror.html") http.ServeFile(w, r, "./web/hosterror.html")
log.Println(err.Error())
h.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname()) h.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname())
} else { } else {
http.ServeFile(w, r, "./web/rperror.html") http.ServeFile(w, r, "./web/rperror.html")
log.Println(err.Error()) //TODO: Take this upstream offline automatically
h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname()) h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname())
} }
} }
@ -208,10 +232,18 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
if target.RequireTLS { if target.RequireTLS {
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String()) u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
} }
if target.parent.HeaderRewriteRules != nil {
target.parent.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
h.Parent.logRequest(r, true, 101, "vdir-websocket", target.Domain) h.Parent.logRequest(r, true, 101, "vdir-websocket", target.Domain)
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations, SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: true, //You should not use websocket via virtual directory. But keep this to true for compatibility SkipOriginCheck: target.parent.EnableWebsocketCustomHeaders, //You should not use websocket via virtual directory. But keep this to true for compatibility
CopyAllHeaders: domainsniff.RequireWebsocketHeaderCopy(r), //Left this as default to prevent nginx user setting / as vdir
UserDefinedHeaders: target.parent.HeaderRewriteRules.UserDefinedHeaders,
Logger: h.Parent.Option.Logger,
}) })
wspHandler.ServeHTTP(w, r) wspHandler.ServeHTTP(w, r)
return return
@ -225,9 +257,24 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
r.URL, _ = url.Parse(originalHostHeader) r.URL, _ = url.Parse(originalHostHeader)
} }
//Build downstream and upstream header rules //Populate the user-defined headers with the values from the request
upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders() headerRewriteOptions := GetDefaultHeaderRewriteRules()
if target.parent.HeaderRewriteRules != nil {
headerRewriteOptions = target.parent.HeaderRewriteRules
}
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, headerRewriteOptions.UserDefinedHeaders)
//Build downstream and upstream header rules, use the parent (subdomain) endpoint's headers
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
HSTSMaxAge: headerRewriteOptions.HSTSMaxAge,
HSTSIncludeSubdomains: target.parent.ContainsWildcardName(true),
EnablePermissionPolicyHeader: headerRewriteOptions.EnablePermissionPolicyHeader,
PermissionPolicy: headerRewriteOptions.PermissionPolicy,
})
//Handle the virtual directory reverse proxy request
statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain, ProxyDomain: target.Domain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
@ -235,7 +282,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
PathPrefix: target.MatchingPath, PathPrefix: target.MatchingPath,
UpstreamHeaders: upstreamHeaders, UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders, DownstreamHeaders: downstreamHeaders,
HostHeaderOverwrite: target.parent.RequestHostOverwrite, HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
Version: target.parent.parent.Option.HostVersion, Version: target.parent.parent.Option.HostVersion,
}) })

View File

@ -0,0 +1,63 @@
package rewrite
import (
"fmt"
"net/http"
"strings"
)
// GetHeaderVariableValuesFromRequest returns a map of header variables and their values
// note that variables behavior is not exactly identical to nginx variables
func GetHeaderVariableValuesFromRequest(r *http.Request) map[string]string {
vars := make(map[string]string)
// Request-specific variables
vars["$host"] = r.Host
vars["$remote_addr"] = r.RemoteAddr
vars["$request_uri"] = r.RequestURI
vars["$request_method"] = r.Method
vars["$content_length"] = fmt.Sprintf("%d", r.ContentLength)
vars["$content_type"] = r.Header.Get("Content-Type")
// Parsed URI elements
vars["$uri"] = r.URL.Path
vars["$args"] = r.URL.RawQuery
vars["$scheme"] = r.URL.Scheme
vars["$query_string"] = r.URL.RawQuery
// User agent and referer
vars["$http_user_agent"] = r.UserAgent()
vars["$http_referer"] = r.Referer()
return vars
}
// CustomHeadersIncludeDynamicVariables checks if the user-defined headers contain dynamic variables
// use for early exit when processing the headers
func CustomHeadersIncludeDynamicVariables(userDefinedHeaders []*UserDefinedHeader) bool {
for _, header := range userDefinedHeaders {
if strings.Contains(header.Value, "$") {
return true
}
}
return false
}
// PopulateRequestHeaderVariables populates the user-defined headers with the values from the request
func PopulateRequestHeaderVariables(r *http.Request, userDefinedHeaders []*UserDefinedHeader) []*UserDefinedHeader {
if !CustomHeadersIncludeDynamicVariables(userDefinedHeaders) {
// Early exit if there are no dynamic variables
return userDefinedHeaders
}
vars := GetHeaderVariableValuesFromRequest(r)
populatedHeaders := []*UserDefinedHeader{}
// Populate the user-defined headers with the values from the request
for _, header := range userDefinedHeaders {
thisHeader := header.Copy()
for key, value := range vars {
thisHeader.Value = strings.ReplaceAll(thisHeader.Value, key, value)
}
populatedHeaders = append(populatedHeaders, thisHeader)
}
return populatedHeaders
}

View File

@ -0,0 +1,172 @@
package rewrite
import (
"net/http/httptest"
"testing"
)
func TestGetHeaderVariableValuesFromRequest(t *testing.T) {
// Create a sample request
req := httptest.NewRequest("GET", "https://example.com/test?foo=bar", nil)
req.Host = "example.com"
req.RemoteAddr = "192.168.1.1:12345"
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "TestAgent")
req.Header.Set("Referer", "https://referer.com")
// Call the function
vars := GetHeaderVariableValuesFromRequest(req)
// Expected results
expected := map[string]string{
"$host": "example.com",
"$remote_addr": "192.168.1.1:12345",
"$request_uri": "https://example.com/test?foo=bar",
"$request_method": "GET",
"$content_length": "0", // ContentLength is 0 because there's no body in the request
"$content_type": "application/json",
"$uri": "/test",
"$args": "foo=bar",
"$scheme": "https",
"$query_string": "foo=bar",
"$http_user_agent": "TestAgent",
"$http_referer": "https://referer.com",
}
// Check each expected variable
for key, expectedValue := range expected {
if vars[key] != expectedValue {
t.Errorf("Expected %s to be %s, but got %s", key, expectedValue, vars[key])
}
}
}
func TestCustomHeadersIncludeDynamicVariables(t *testing.T) {
tests := []struct {
name string
headers []*UserDefinedHeader
expectedHasVar bool
}{
{
name: "No headers",
headers: []*UserDefinedHeader{},
expectedHasVar: false,
},
{
name: "Headers without dynamic variables",
headers: []*UserDefinedHeader{
{
Direction: HeaderDirection_ZoraxyToUpstream,
Key: "X-Custom-Header",
Value: "staticValue",
IsRemove: false,
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Another-Header",
Value: "staticValue",
IsRemove: false,
},
},
expectedHasVar: false,
},
{
name: "Headers with one dynamic variable",
headers: []*UserDefinedHeader{
{
Direction: HeaderDirection_ZoraxyToUpstream,
Key: "X-Custom-Header",
Value: "$dynamicValue",
IsRemove: false,
},
},
expectedHasVar: true,
},
{
name: "Headers with multiple dynamic variables",
headers: []*UserDefinedHeader{
{
Direction: HeaderDirection_ZoraxyToUpstream,
Key: "X-Custom-Header",
Value: "$dynamicValue1",
IsRemove: false,
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Another-Header",
Value: "$dynamicValue2",
IsRemove: false,
},
},
expectedHasVar: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hasVar := CustomHeadersIncludeDynamicVariables(tt.headers)
if hasVar != tt.expectedHasVar {
t.Errorf("Expected %v, but got %v", tt.expectedHasVar, hasVar)
}
})
}
}
func TestPopulateRequestHeaderVariables(t *testing.T) {
// Create a sample request with specific values
req := httptest.NewRequest("GET", "https://example.com/test?foo=bar", nil)
req.Host = "example.com"
req.RemoteAddr = "192.168.1.1:12345"
req.Header.Set("User-Agent", "TestAgent")
req.Header.Set("Referer", "https://referer.com")
// Define user-defined headers with dynamic variables
userDefinedHeaders := []*UserDefinedHeader{
{
Direction: HeaderDirection_ZoraxyToUpstream,
Key: "X-Forwarded-Host",
Value: "$host",
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Client-IP",
Value: "$remote_addr",
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Custom-Header",
Value: "$request_uri",
},
}
// Call the function with the test data
resultHeaders := PopulateRequestHeaderVariables(req, userDefinedHeaders)
// Expected results after variable substitution
expectedHeaders := []*UserDefinedHeader{
{
Direction: HeaderDirection_ZoraxyToUpstream,
Key: "X-Forwarded-Host",
Value: "example.com",
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Client-IP",
Value: "192.168.1.1:12345",
},
{
Direction: HeaderDirection_ZoraxyToDownstream,
Key: "X-Custom-Header",
Value: "https://example.com/test?foo=bar",
},
}
// Validate results
for i, expected := range expectedHeaders {
if resultHeaders[i].Direction != expected.Direction ||
resultHeaders[i].Key != expected.Key ||
resultHeaders[i].Value != expected.Value {
t.Errorf("Expected header %v, but got %v", expected, resultHeaders[i])
}
}
}

View File

@ -0,0 +1,79 @@
package rewrite
/*
rewrite.go
This script handle the rewrite logic for custom headers
*/
import (
"strconv"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
)
// SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers
// return upstream header and downstream header key-value pairs
// if the header is expected to be deleted, the value will be set to empty string
func SplitUpDownStreamHeaders(rewriteOptions *HeaderRewriteOptions) ([][]string, [][]string) {
if len(rewriteOptions.UserDefinedHeaders) == 0 && rewriteOptions.HSTSMaxAge == 0 && !rewriteOptions.EnablePermissionPolicyHeader {
//Early return if there are no defined headers
return [][]string{}, [][]string{}
}
//Use pre-allocation for faster performance
//Downstream +2 for Permission Policy and HSTS
upstreamHeaders := make([][]string, len(rewriteOptions.UserDefinedHeaders))
downstreamHeaders := make([][]string, len(rewriteOptions.UserDefinedHeaders)+2)
upstreamHeaderCounter := 0
downstreamHeaderCounter := 0
//Sort the headers into upstream or downstream
for _, customHeader := range rewriteOptions.UserDefinedHeaders {
thisHeaderSet := make([]string, 2)
thisHeaderSet[0] = customHeader.Key
thisHeaderSet[1] = customHeader.Value
if customHeader.IsRemove {
//Prevent invalid config
thisHeaderSet[1] = ""
}
//Assign to slice
if customHeader.Direction == HeaderDirection_ZoraxyToUpstream {
upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet
upstreamHeaderCounter++
} else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream {
downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet
downstreamHeaderCounter++
}
}
//Check if the endpoint require HSTS headers
if rewriteOptions.HSTSMaxAge > 0 {
if rewriteOptions.HSTSIncludeSubdomains {
//Endpoint listening domain includes wildcards.
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(rewriteOptions.HSTSMaxAge)) + "; includeSubdomains"}
} else {
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(rewriteOptions.HSTSMaxAge))}
}
downstreamHeaderCounter++
}
//Check if the endpoint require Permission Policy
if rewriteOptions.EnablePermissionPolicyHeader {
var usingPermissionPolicy *permissionpolicy.PermissionsPolicy
if rewriteOptions.PermissionPolicy != nil {
//Custom permission policy
usingPermissionPolicy = rewriteOptions.PermissionPolicy
} else {
//Permission policy is enabled but not customized. Use default
usingPermissionPolicy = permissionpolicy.GetDefaultPermissionPolicy()
}
downstreamHeaders[downstreamHeaderCounter] = usingPermissionPolicy.ToKeyValueHeader()
downstreamHeaderCounter++
}
return upstreamHeaders, downstreamHeaders
}

View File

@ -0,0 +1,51 @@
package rewrite
import (
"encoding/json"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
)
/*
typdef.go
This script handle the type definition for custom headers
*/
/* Custom Header Related Data structure */
// Header injection direction type
type HeaderDirection int
const (
HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
)
// User defined headers to add into a proxy endpoint
type UserDefinedHeader struct {
Direction HeaderDirection
Key string
Value string
IsRemove bool //Instead of set, remove this key instead
}
type HeaderRewriteOptions struct {
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
HSTSIncludeSubdomains bool //Include subdomains in HSTS header
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
}
// Utilities for header rewrite
func (h *UserDefinedHeader) GetDirection() HeaderDirection {
return h.Direction
}
// Copy eturns a deep copy of the UserDefinedHeader
func (h *UserDefinedHeader) Copy() *UserDefinedHeader {
result := UserDefinedHeader{}
js, _ := json.Marshal(h)
json.Unmarshal(js, &result)
return &result
}

View File

@ -70,12 +70,18 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
// Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime // Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime
func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error { func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
lookupHostname := strings.ToLower(endpoint.RootOrMatchingDomain)
if len(endpoint.ActiveOrigins) == 0 {
//There are no active origins. No need to check for ready
router.ProxyEndpoints.Store(lookupHostname, endpoint)
return nil
}
if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) { if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) {
//This endpoint is not prepared //This endpoint is not prepared
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime") return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
} }
// Push record into running subdomain endpoints // Push record into running subdomain endpoints
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint) router.ProxyEndpoints.Store(lookupHostname, endpoint)
return nil return nil
} }

View File

@ -1,5 +1,12 @@
package dynamicproxy package dynamicproxy
/*
typdef.go
This script handle the type definition for dynamic proxy and endpoints
If you are looking for the default object initailization, please refer to default.go
*/
import ( import (
_ "embed" _ "embed"
"net" "net"
@ -7,20 +14,24 @@ import (
"sync" "sync"
"imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection" "imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger" "imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/statistic" "imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/tlscert" "imuslab.com/zoraxy/mod/tlscert"
) )
type ProxyType int
const ( const (
ProxyType_Root = 0 ProxyTypeRoot ProxyType = iota //Root Proxy, everything not matching will be routed here
ProxyType_Host = 1 ProxyTypeHost //Host Proxy, match by host (domain) name
ProxyType_Vdir = 2 ProxyTypeVdir //Virtual Directory Proxy, match by path prefix
) )
type ProxyHandler struct { type ProxyHandler struct {
@ -29,14 +40,17 @@ type ProxyHandler struct {
/* Router Object Options */ /* Router Object Options */
type RouterOption struct { type RouterOption struct {
HostUUID string //The UUID of Zoraxy, use for heading mod /* Basic Settings */
HostVersion string //The version of Zoraxy, use for heading mod HostUUID string //The UUID of Zoraxy, use for heading mod
Port int //Incoming port HostVersion string //The version of Zoraxy, use for heading mod
UseTls bool //Use TLS to serve incoming requsts Port int //Incoming port
ForceTLSLatest bool //Force TLS1.2 or above UseTls bool //Use TLS to serve incoming requsts
NoCache bool //Force set Cache-Control: no-store ForceTLSLatest bool //Force TLS1.2 or above
ListenOnPort80 bool //Enable port 80 http listener NoCache bool //Force set Cache-Control: no-store
ForceHttpsRedirect bool //Force redirection of http to https endpoint ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint
/* Routing Service Managers */
TlsManager *tlscert.Manager //TLS manager for serving SAN certificates TlsManager *tlscert.Manager //TLS manager for serving SAN certificates
RedirectRuleTable *redirection.RuleTable //Redirection rules handler and table RedirectRuleTable *redirection.RuleTable //Redirection rules handler and table
GeodbStore *geodb.Store //GeoIP resolver GeodbStore *geodb.Store //GeoIP resolver
@ -44,20 +58,25 @@ type RouterOption struct {
StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors
WebDirectory string //The static web server directory containing the templates folder WebDirectory string //The static web server directory containing the templates folder
LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target
Logger *logger.Logger //Logger for reverse proxy requets
/* Authentication Providers */
AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
/* Utilities */
Logger *logger.Logger //Logger for reverse proxy requets
} }
/* Router Object */ /* Router Object */
type Router struct { type Router struct {
Option *RouterOption Option *RouterOption
ProxyEndpoints *sync.Map ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests
Running bool Running bool //If the router is running
Root *ProxyEndpoint Root *ProxyEndpoint //Root proxy endpoint, default site
mux http.Handler mux http.Handler //HTTP handler
server *http.Server server *http.Server //HTTP server
tlsListener net.Listener tlsListener net.Listener //TLS listener, handle SNI routing
loadBalancer *loadbalance.RouteManager //Load balancer routing manager loadBalancer *loadbalance.RouteManager //Load balancer routing manager
routingRules []*RoutingRule routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling
tlsRedirectStop chan bool //Stop channel for tls redirection server tlsRedirectStop chan bool //Stop channel for tls redirection server
rateLimterStop chan bool //Stop channel for rate limiter rateLimterStop chan bool //Stop channel for rate limiter
@ -82,23 +101,6 @@ type BasicAuthExceptionRule struct {
PathPrefix string PathPrefix string
} }
/* Custom Header Related Data structure */
// Header injection direction type
type HeaderDirection int
const (
HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
)
// User defined headers to add into a proxy endpoint
type UserDefinedHeader struct {
Direction HeaderDirection
Key string
Value string
IsRemove bool //Instead of set, remove this key instead
}
/* Routing Rule Data Structures */ /* Routing Rule Data Structures */
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better // A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
@ -113,9 +115,48 @@ type VirtualDirectoryEndpoint struct {
parent *ProxyEndpoint `json:"-"` parent *ProxyEndpoint `json:"-"`
} }
// Rules and settings for header rewriting
type HeaderRewriteRules struct {
UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
}
/*
Authentication Provider
TODO: Move these into a dedicated module
*/
type AuthMethod int
const (
AuthMethodNone AuthMethod = iota //No authentication required
AuthMethodBasic //Basic Auth
AuthMethodAuthelia //Authelia
AuthMethodOauth2 //Oauth2
)
type AuthenticationProvider struct {
AuthMethod AuthMethod //The authentication method to use
/* Basic Auth Settings */
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint
/* Authelia Settings */
AutheliaURL string //URL of the Authelia server, leave empty to use global settings e.g. authelia.example.com
UseHTTPS bool //Whether to use HTTPS for the Authelia server
}
// A proxy endpoint record, a general interface for handling inbound routing // A proxy endpoint record, a general interface for handling inbound routing
type ProxyEndpoint struct { type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def ProxyType ProxyType //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
@ -131,22 +172,19 @@ type ProxyEndpoint struct {
VirtualDirectories []*VirtualDirectoryEndpoint VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers //Custom Headers
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint HeaderRewriteRules *HeaderRewriteRules
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header EnableWebsocketCustomHeaders bool //Enable custom headers for websocket connections as well (default only http reqiests)
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
//Authentication //Authentication
RequireBasicAuth bool //Set to true to request basic auth before proxy AuthenticationProvider *AuthenticationProvider
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
// Rate Limiting // Rate Limiting
RequireRateLimit bool RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second RateLimit int64 // Rate limit in requests per second
//Uptime Monitor
DisableUptimeMonitor bool //Disable uptime monitor for this endpoint
//Access Control //Access Control
AccessFilterUUID string //Access filter ID AccessFilterUUID string //Access filter ID
@ -171,6 +209,9 @@ const (
DefaultSite_ReverseProxy = 1 DefaultSite_ReverseProxy = 1
DefaultSite_Redirect = 2 DefaultSite_Redirect = 2
DefaultSite_NotFoundPage = 3 DefaultSite_NotFoundPage = 3
DefaultSite_NoResponse = 4
DefaultSite_TeaPot = 418 //I'm a teapot
) )
/* /*

View File

@ -16,7 +16,7 @@ type Sender struct {
Port int //E.g. 587 Port int //E.g. 587
Username string //Username of the email account Username string //Username of the email account
Password string //Password of the email account Password string //Password of the email account
SenderAddr string //e.g. admin@arozos.com SenderAddr string //e.g. admin@aroz.org
} }
// Create a new email sender object // Create a new email sender object

View File

@ -18,7 +18,7 @@ func (this *defaultDialer) Dial(address string) Socket {
if socket, err := net.DialTimeout("tcp", address, this.timeout); err == nil { if socket, err := net.DialTimeout("tcp", address, this.timeout); err == nil {
return socket return socket
} else { } else {
this.logger.Printf("[INFO] Unable to establish connection to [%s]: %s", address, err) this.logger.Printf("Unable to establish connection to [%s]: %s", address, err)
} }
return nil return nil

View File

@ -17,7 +17,7 @@ func (this *loggingInitializer) Initialize(client, server Socket) bool {
result := this.inner.Initialize(client, server) result := this.inner.Initialize(client, server)
if !result { if !result {
this.logger.Printf("[INFO] Connection failed [%s] -> [%s]", client.RemoteAddr(), server.RemoteAddr()) this.logger.Printf("Connection failed [%s] -> [%s]", client.RemoteAddr(), server.RemoteAddr())
} }
return result return result

View File

@ -1,6 +1,7 @@
package ganserv package ganserv
import ( import (
"log"
"net" "net"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
@ -85,6 +86,7 @@ func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
//Get controller info //Get controller info
instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort) instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
if err != nil { if err != nil {
log.Println("ZeroTier connection failed: ", err.Error())
return &NetworkManager{ return &NetworkManager{
authToken: option.AuthToken, authToken: option.AuthToken,
apiPort: option.ApiPort, apiPort: option.ApiPort,

View File

@ -28,11 +28,17 @@ type NodeInfo struct {
Clock int64 `json:"clock"` Clock int64 `json:"clock"`
Config struct { Config struct {
Settings struct { Settings struct {
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay"` AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,omitempty"`
PortMappingEnabled bool `json:"portMappingEnabled"` ForceTCPRelay bool `json:"forceTcpRelay,omitempty"`
PrimaryPort int `json:"primaryPort"` HomeDir string `json:"homeDir,omitempty"`
SoftwareUpdate string `json:"softwareUpdate"` ListeningOn []string `json:"listeningOn,omitempty"`
SoftwareUpdateChannel string `json:"softwareUpdateChannel"` PortMappingEnabled bool `json:"portMappingEnabled,omitempty"`
PrimaryPort int `json:"primaryPort,omitempty"`
SecondaryPort int `json:"secondaryPort,omitempty"`
SoftwareUpdate string `json:"softwareUpdate,omitempty"`
SoftwareUpdateChannel string `json:"softwareUpdateChannel,omitempty"`
SurfaceAddresses []string `json:"surfaceAddresses,omitempty"`
TertiaryPort int `json:"tertiaryPort,omitempty"`
} `json:"settings"` } `json:"settings"`
} `json:"config"` } `json:"config"`
Online bool `json:"online"` Online bool `json:"online"`
@ -46,7 +52,6 @@ type NodeInfo struct {
VersionMinor int `json:"versionMinor"` VersionMinor int `json:"versionMinor"`
VersionRev int `json:"versionRev"` VersionRev int `json:"versionRev"`
} }
type ErrResp struct { type ErrResp struct {
Message string `json:"message"` Message string `json:"message"`
} }

View File

@ -3,9 +3,14 @@ package geodb
import ( import (
_ "embed" _ "embed"
"net/http" "net/http"
"os"
"sync"
"time"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/netutils" "imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/utils"
) )
//go:embed geoipv4.csv //go:embed geoipv4.csv
@ -15,17 +20,23 @@ var geoipv4 []byte //Geodb dataset for ipv4
var geoipv6 []byte //Geodb dataset for ipv6 var geoipv6 []byte //Geodb dataset for ipv6
type Store struct { type Store struct {
geodb [][]string //Parsed geodb list geodb [][]string //Parsed geodb list
geodbIpv6 [][]string //Parsed geodb list for ipv6 geodbIpv6 [][]string //Parsed geodb list for ipv6
geotrie *trie geotrie *trie
geotrieIpv6 *trie geotrieIpv6 *trie
sysdb *database.Database sysdb *database.Database
option *StoreOptions slowLookupCacheIpv4 sync.Map //Cache for slow lookup, ip -> cc
slowLookupCacheIpv6 sync.Map //Cache for slow lookup ipv6, ip -> cc
cacheClearTicker *time.Ticker //Ticker for clearing cache
cacheClearTickerStopChan chan bool //Stop channel for cache clear ticker
option *StoreOptions
} }
type StoreOptions struct { type StoreOptions struct {
AllowSlowIpv4LookUp bool AllowSlowIpv4LookUp bool
AllowSloeIpv6Lookup bool AllowSlowIpv6Lookup bool
Logger *logger.Logger
SlowLookupCacheClearInterval time.Duration //Clear slow lookup cache interval
} }
type CountryInfo struct { type CountryInfo struct {
@ -34,6 +45,23 @@ type CountryInfo struct {
} }
func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) { func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
//Check if external geoDB data is available
if utils.FileExists("./conf/geodb/geoipv4.csv") {
externalV4Db, err := os.ReadFile("./conf/geodb/geoipv4.csv")
if err == nil {
option.Logger.PrintAndLog("GeoDB", "External GeoDB data found, using external IPv4 GeoIP data", nil)
geoipv4 = externalV4Db
}
}
if utils.FileExists("./conf/geodb/geoipv6.csv") {
externalV6Db, err := os.ReadFile("./conf/geodb/geoipv6.csv")
if err == nil {
option.Logger.PrintAndLog("GeoDB", "External GeoDB data found, using external IPv6 GeoIP data", nil)
geoipv6 = externalV6Db
}
}
parsedGeoData, err := parseCSV(geoipv4) parsedGeoData, err := parseCSV(geoipv4)
if err != nil { if err != nil {
return nil, err return nil, err
@ -50,18 +78,44 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
} }
var ipv6Trie *trie var ipv6Trie *trie
if !option.AllowSloeIpv6Lookup { if !option.AllowSlowIpv6Lookup {
ipv6Trie = constrctTrieTree(parsedGeoDataIpv6) ipv6Trie = constrctTrieTree(parsedGeoDataIpv6)
} }
return &Store{ if option.SlowLookupCacheClearInterval == 0 {
geodb: parsedGeoData, option.SlowLookupCacheClearInterval = 30 * time.Minute
geotrie: ipv4Trie, }
geodbIpv6: parsedGeoDataIpv6,
geotrieIpv6: ipv6Trie, //Create a new store
sysdb: sysdb, thisGeoDBStore := &Store{
option: option, geodb: parsedGeoData,
}, nil geotrie: ipv4Trie,
geodbIpv6: parsedGeoDataIpv6,
geotrieIpv6: ipv6Trie,
sysdb: sysdb,
slowLookupCacheIpv4: sync.Map{},
slowLookupCacheIpv6: sync.Map{},
cacheClearTicker: time.NewTicker(option.SlowLookupCacheClearInterval),
cacheClearTickerStopChan: make(chan bool),
option: option,
}
//Start cache clear ticker
if option.AllowSlowIpv4LookUp || option.AllowSlowIpv6Lookup {
go func(store *Store) {
for {
select {
case <-store.cacheClearTickerStopChan:
return
case <-thisGeoDBStore.cacheClearTicker.C:
thisGeoDBStore.slowLookupCacheIpv4 = sync.Map{}
thisGeoDBStore.slowLookupCacheIpv6 = sync.Map{}
}
}
}(thisGeoDBStore)
}
return thisGeoDBStore, nil
} }
func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) { func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
@ -73,8 +127,12 @@ func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error)
} }
// Close the store
func (s *Store) Close() { func (s *Store) Close() {
if s.option.AllowSlowIpv4LookUp || s.option.AllowSlowIpv6Lookup {
//Stop cache clear ticker
s.cacheClearTickerStopChan <- true
}
} }
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string { func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {

View File

@ -4,6 +4,7 @@ import (
"testing" "testing"
"imuslab.com/zoraxy/mod/geodb" "imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
) )
/* /*
@ -42,8 +43,10 @@ func TestTrieConstruct(t *testing.T) {
func TestResolveCountryCodeFromIP(t *testing.T) { func TestResolveCountryCodeFromIP(t *testing.T) {
// Create a new store // Create a new store
store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{ store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{
false,
true, true,
true,
&logger.Logger{},
0,
}) })
if err != nil { if err != nil {
t.Errorf("error creating store: %v", err) t.Errorf("error creating store: %v", err)
@ -83,4 +86,24 @@ func TestResolveCountryCodeFromIP(t *testing.T) {
if info.CountryIsoCode != expected { if info.CountryIsoCode != expected {
t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip) t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip)
} }
// Test for issue #401
// Create 100 concurrent goroutines to resolve country code for random IP addresses in the test cases above
for i := 0; i < 100; i++ {
go func() {
for _, testcase := range knownIpCountryMap {
ip := testcase[0]
expected := testcase[1]
info, err := store.ResolveCountryCodeFromIP(ip)
if err != nil {
t.Errorf("error resolving country code for IP %s: %v", ip, err)
return
}
if info.CountryIsoCode != expected {
t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip)
}
}
}()
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

67
src/mod/geodb/locale.go Normal file
View File

@ -0,0 +1,67 @@
package geodb
import "net/http"
// GetRequesterCountryISOCode get the locale of the requester
func (s *Store) GetLocaleFromRequest(r *http.Request) (string, error) {
cc := s.GetRequesterCountryISOCode(r)
return GetLocaleFromCountryCode(cc), nil
}
// GetLocaleFromCountryCode get the locale given the country code
func GetLocaleFromCountryCode(cc string) string {
//If you find your country is not in the list, please add it here
mapCountryToLocale := map[string]string{
"aa": "ar_AA",
"by": "be_BY",
"bg": "bg_BG",
"es": "ca_ES",
"cz": "cs_CZ",
"dk": "da_DK",
"ch": "de_CH",
"de": "de_DE",
"gr": "el_GR",
"au": "en_AU",
"be": "en_BE",
"gb": "en_GB",
"jp": "en_JP",
"us": "en_US",
"za": "en_ZA",
"fi": "fi_FI",
"ca": "fr_CA",
"fr": "fr_FR",
"hr": "hr_HR",
"hu": "hu_HU",
"is": "is_IS",
"it": "it_IT",
"il": "iw_IL",
"kr": "ko_KR",
"lt": "lt_LT",
"lv": "lv_LV",
"mk": "mk_MK",
"nl": "nl_NL",
"no": "no_NO",
"pl": "pl_PL",
"br": "pt_BR",
"pt": "pt_PT",
"ro": "ro_RO",
"ru": "ru_RU",
"sp": "sh_SP",
"sk": "sk_SK",
"sl": "sl_SL",
"al": "sq_AL",
"se": "sv_SE",
"th": "th_TH",
"tr": "tr_TR",
"ua": "uk_UA",
"cn": "zh_CN",
"tw": "zh_TW",
"hk": "zh_HK",
}
locale, ok := mapCountryToLocale[cc]
if !ok {
return "en-US"
}
return locale
}

View File

@ -56,6 +56,13 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
if isReservedIP(ipAddr) { if isReservedIP(ipAddr) {
return "" return ""
} }
//Check if already in cache
cc := s.GetSlowSearchCachedIpv4(ipAddr)
if cc != "" {
return cc
}
for _, ipRange := range s.geodb { for _, ipRange := range s.geodb {
startIp := ipRange[0] startIp := ipRange[0]
endIp := ipRange[1] endIp := ipRange[1]
@ -63,6 +70,8 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
inRange, _ := isIPv4InRange(startIp, endIp, ipAddr) inRange, _ := isIPv4InRange(startIp, endIp, ipAddr)
if inRange { if inRange {
//Add to cache
s.slowLookupCacheIpv4.Store(ipAddr, cc)
return cc return cc
} }
} }
@ -73,6 +82,13 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
if isReservedIP(ipAddr) { if isReservedIP(ipAddr) {
return "" return ""
} }
//Check if already in cache
cc := s.GetSlowSearchCachedIpv6(ipAddr)
if cc != "" {
return cc
}
for _, ipRange := range s.geodbIpv6 { for _, ipRange := range s.geodbIpv6 {
startIp := ipRange[0] startIp := ipRange[0]
endIp := ipRange[1] endIp := ipRange[1]
@ -80,8 +96,28 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
inRange, _ := isIPv6InRange(startIp, endIp, ipAddr) inRange, _ := isIPv6InRange(startIp, endIp, ipAddr)
if inRange { if inRange {
//Add to cache
s.slowLookupCacheIpv6.Store(ipAddr, cc)
return cc return cc
} }
} }
return "" return ""
} }
// GetSlowSearchCachedIpv4 return the country code for the given ipv4 address, return empty string if not found
func (s *Store) GetSlowSearchCachedIpv4(ipAddr string) string {
cc, ok := s.slowLookupCacheIpv4.Load(ipAddr)
if ok {
return cc.(string)
}
return ""
}
// GetSlowSearchCachedIpv6 return the country code for the given ipv6 address, return empty string if not found
func (s *Store) GetSlowSearchCachedIpv6(ipAddr string) string {
cc, ok := s.slowLookupCacheIpv6.Load(ipAddr)
if ok {
return cc.(string)
}
return ""
}

56
src/mod/geodb/updater.go Normal file
View File

@ -0,0 +1,56 @@
package geodb
import (
"io"
"log"
"net/http"
"os"
"imuslab.com/zoraxy/mod/utils"
)
const (
ipv4UpdateSource = "https://cdn.jsdelivr.net/npm/@ip-location-db/geo-whois-asn-country/geo-whois-asn-country-ipv4.csv"
ipv6UpdateSource = "https://cdn.jsdelivr.net/npm/@ip-location-db/geo-whois-asn-country/geo-whois-asn-country-ipv6.csv"
)
// DownloadGeoDBUpdate download the latest geodb update
func DownloadGeoDBUpdate(externalGeoDBStoragePath string) {
//Create the storage path if not exist
if !utils.FileExists(externalGeoDBStoragePath) {
os.MkdirAll(externalGeoDBStoragePath, 0755)
}
//Download the update
log.Println("Downloading IPv4 database update...")
err := downloadFile(ipv4UpdateSource, externalGeoDBStoragePath+"/geoipv4.csv")
if err != nil {
log.Println(err)
return
}
log.Println("Downloading IPv6 database update...")
err = downloadFile(ipv6UpdateSource, externalGeoDBStoragePath+"/geoipv6.csv")
if err != nil {
log.Println(err)
return
}
log.Println("GeoDB update stored at: " + externalGeoDBStoragePath)
log.Println("Exiting...")
}
// Utility functions
func downloadFile(url string, savepath string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
fileContent, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return os.WriteFile(savepath, fileContent, 0644)
}

View File

@ -0,0 +1,79 @@
package ipscan
/*
ipscan http handlers
This script provide http handlers for ipscan module
*/
import (
"encoding/json"
"net"
"net/http"
"imuslab.com/zoraxy/mod/utils"
)
// HandleScanPort is the HTTP handler for scanning opened ports on a given IP address
func HandleScanPort(w http.ResponseWriter, r *http.Request) {
targetIp, err := utils.GetPara(r, "ip")
if err != nil {
utils.SendErrorResponse(w, "target IP address not given")
return
}
// Check if the IP is a valid IP address
ip := net.ParseIP(targetIp)
if ip == nil {
utils.SendErrorResponse(w, "invalid IP address")
return
}
// Scan the ports
openPorts := ScanPorts(targetIp)
jsonData, err := json.Marshal(openPorts)
if err != nil {
utils.SendErrorResponse(w, "failed to marshal JSON")
return
}
utils.SendJSONResponse(w, string(jsonData))
}
// HandleIpScan is the HTTP handler for scanning IP addresses in a given range or CIDR
func HandleIpScan(w http.ResponseWriter, r *http.Request) {
cidr, err := utils.PostPara(r, "cidr")
if err != nil {
//Ip range mode
start, err := utils.PostPara(r, "start")
if err != nil {
utils.SendErrorResponse(w, "missing start ip")
return
}
end, err := utils.PostPara(r, "end")
if err != nil {
utils.SendErrorResponse(w, "missing end ip")
return
}
discoveredHosts, err := ScanIpRange(start, end)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(discoveredHosts)
utils.SendJSONResponse(w, string(js))
} else {
//CIDR mode
discoveredHosts, err := ScanCIDRRange(cidr)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(discoveredHosts)
utils.SendJSONResponse(w, string(js))
}
}

View File

@ -27,7 +27,7 @@ type DiscoveredHost struct {
HttpsPortDetected bool HttpsPortDetected bool
} }
//Scan an IP range given the start and ending ip address // Scan an IP range given the start and ending ip address
func ScanIpRange(start, end string) ([]*DiscoveredHost, error) { func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
ipStart := net.ParseIP(start) ipStart := net.ParseIP(start)
ipEnd := net.ParseIP(end) ipEnd := net.ParseIP(end)
@ -57,7 +57,6 @@ func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
host.CheckHostname() host.CheckHostname()
host.CheckPort("http", 80, &host.HttpPortDetected) host.CheckPort("http", 80, &host.HttpPortDetected)
host.CheckPort("https", 443, &host.HttpsPortDetected) host.CheckPort("https", 443, &host.HttpsPortDetected)
fmt.Println("OK", host)
hosts = append(hosts, host) hosts = append(hosts, host)
}(thisIp) }(thisIp)
@ -118,7 +117,7 @@ func (host *DiscoveredHost) CheckPing() error {
func (host *DiscoveredHost) CheckHostname() { func (host *DiscoveredHost) CheckHostname() {
// lookup the hostname for the IP address // lookup the hostname for the IP address
names, err := net.LookupAddr(host.IP) names, err := net.LookupAddr(host.IP)
fmt.Println(names, err) //fmt.Println(names, err)
if err == nil && len(names) > 0 { if err == nil && len(names) > 0 {
host.Hostname = names[0] host.Hostname = names[0]
} }

View File

@ -0,0 +1,48 @@
package ipscan
/*
Port Scanner
This module scan the given IP address and scan all the opened port
*/
import (
"fmt"
"net"
"sync"
"time"
)
// OpenedPort holds information about an open port and its service type
type OpenedPort struct {
Port int
IsTCP bool
}
// ScanPorts scans all the opened ports on a given host IP (both IPv4 and IPv6)
func ScanPorts(host string) []*OpenedPort {
var openPorts []*OpenedPort
var wg sync.WaitGroup
var mu sync.Mutex
for port := 1; port <= 65535; port++ {
wg.Add(1)
go func(port int) {
defer wg.Done()
address := fmt.Sprintf("%s:%d", host, port)
// Check TCP
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
if err == nil {
mu.Lock()
openPorts = append(openPorts, &OpenedPort{Port: port, IsTCP: true})
mu.Unlock()
conn.Close()
}
}(port)
}
wg.Wait()
return openPorts
}

View File

@ -169,9 +169,16 @@ func (n *NetStatBuffers) HandleGetBufferedNetworkInterfaceStats(w http.ResponseW
} }
func (n *NetStatBuffers) Close() { func (n *NetStatBuffers) Close() {
n.StopChan <- true //Fixed issue #394 for stopping netstat listener on platforms not supported platforms
time.Sleep(300 * time.Millisecond) if n.StopChan != nil {
n.EventTicker.Stop() n.StopChan <- true
time.Sleep(300 * time.Millisecond)
}
if n.EventTicker != nil {
n.EventTicker.Stop()
}
} }
func (n *NetStatBuffers) HandleGetNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) { func (n *NetStatBuffers) HandleGetNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) {
@ -270,11 +277,11 @@ func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
allIfaceRxByteFiles, err := filepath.Glob("/sys/class/net/*/statistics/rx_bytes") allIfaceRxByteFiles, err := filepath.Glob("/sys/class/net/*/statistics/rx_bytes")
if err != nil { if err != nil {
//Permission denied //Permission denied
return 0, 0, errors.New("Access denied") return 0, 0, errors.New("access denied")
} }
if len(allIfaceRxByteFiles) == 0 { if len(allIfaceRxByteFiles) == 0 {
return 0, 0, errors.New("No valid iface found") return 0, 0, errors.New("no valid iface found")
} }
rxSum := int64(0) rxSum := int64(0)
@ -334,5 +341,5 @@ func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
return 0, 0, nil //no ethernet adapters with en*/<Link#*> return 0, 0, nil //no ethernet adapters with en*/<Link#*>
} }
return 0, 0, errors.New("Platform not supported") return 0, 0, errors.New("platform not supported")
} }

View File

@ -16,6 +16,15 @@ import (
func GetRequesterIP(r *http.Request) string { func GetRequesterIP(r *http.Request) string {
ip := r.Header.Get("X-Real-Ip") ip := r.Header.Get("X-Real-Ip")
if ip == "" { if ip == "" {
CF_Connecting_IP := r.Header.Get("CF-Connecting-IP")
Fastly_Client_IP := r.Header.Get("Fastly-Client-IP")
if CF_Connecting_IP != "" {
//Use CF Connecting IP
return CF_Connecting_IP
} else if Fastly_Client_IP != "" {
//Use Fastly Client IP
return Fastly_Client_IP
}
ip = r.Header.Get("X-Forwarded-For") ip = r.Header.Get("X-Forwarded-For")
} }
if ip == "" { if ip == "" {

View File

@ -50,21 +50,6 @@ func NewSSHProxyManager() *Manager {
} }
} }
// Get the next free port in the list
func (m *Manager) GetNextPort() int {
nextPort := m.StartingPort
occupiedPort := make(map[int]bool)
for _, instance := range m.Instances {
occupiedPort[instance.AssignedPort] = true
}
for {
if !occupiedPort[nextPort] {
return nextPort
}
nextPort++
}
}
func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWriter, r *http.Request) { func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWriter, r *http.Request) {
targetInstance, err := m.GetInstanceById(instanceId) targetInstance, err := m.GetInstanceById(instanceId)
if err != nil { if err != nil {
@ -88,6 +73,7 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{ wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: false, SkipTLSValidation: false,
SkipOriginCheck: false, SkipOriginCheck: false,
Logger: nil,
}) })
wspHandler.ServeHTTP(w, r) wspHandler.ServeHTTP(w, r)
return return
@ -167,6 +153,17 @@ func (i *Instance) CreateNewConnection(listenPort int, username string, remoteIp
if username != "" { if username != "" {
connAddr = username + "@" + remoteIpAddr connAddr = username + "@" + remoteIpAddr
} }
//Trim the space in the username and remote address
username = strings.TrimSpace(username)
remoteIpAddr = strings.TrimSpace(remoteIpAddr)
//Validate the username and remote address
err := ValidateUsernameAndRemoteAddr(username, remoteIpAddr)
if err != nil {
return err
}
configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty") configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty")
title := username + "@" + remoteIpAddr title := username + "@" + remoteIpAddr
if remotePort != 22 { if remotePort != 22 {

View File

@ -0,0 +1,66 @@
package sshprox
import (
"testing"
)
func TestInstance_Destroy(t *testing.T) {
manager := NewSSHProxyManager()
instance, err := manager.NewSSHProxy("/tmp")
if err != nil {
t.Fatalf("Failed to create new SSH proxy: %v", err)
}
instance.Destroy()
if len(manager.Instances) != 0 {
t.Errorf("Expected Instances to be empty, got %d", len(manager.Instances))
}
}
func TestInstance_ValidateUsernameAndRemoteAddr(t *testing.T) {
tests := []struct {
username string
remoteAddr string
expectError bool
}{
{"validuser", "127.0.0.1", false},
{"valid.user", "example.com", false},
{"; bash ;", "example.com", true},
{"valid-user", "example.com", false},
{"invalid user", "127.0.0.1", true},
{"validuser", "invalid address", true},
{"invalid@user", "127.0.0.1", true},
{"validuser", "invalid@address", true},
{"injection; rm -rf /", "127.0.0.1", true},
{"validuser", "127.0.0.1; rm -rf /", true},
{"$(reboot)", "127.0.0.1", true},
{"validuser", "$(reboot)", true},
{"validuser", "127.0.0.1; $(reboot)", true},
{"validuser", "127.0.0.1 | ls", true},
{"validuser", "127.0.0.1 & ls", true},
{"validuser", "127.0.0.1 && ls", true},
{"validuser", "127.0.0.1 |& ls", true},
{"validuser", "127.0.0.1 ; ls", true},
{"validuser", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", false},
{"validuser", "2001:db8::ff00:42:8329", false},
{"validuser", "2001:db8:0:1234:0:567:8:1", false},
{"validuser", "2001:db8::1234:0:567:8:1", false},
{"validuser", "2001:db8:0:0:0:0:2:1", false},
{"validuser", "2001:db8::2:1", false},
{"validuser", "2001:db8:0:0:8:800:200c:417a", false},
{"validuser", "2001:db8::8:800:200c:417a", false},
{"validuser", "2001:db8:0:0:8:800:200c:417a; rm -rf /", true},
{"validuser", "2001:db8::8:800:200c:417a; rm -rf /", true},
}
for _, test := range tests {
err := ValidateUsernameAndRemoteAddr(test.username, test.remoteAddr)
if test.expectError && err == nil {
t.Errorf("Expected error for username %s and remoteAddr %s, but got none", test.username, test.remoteAddr)
}
if !test.expectError && err != nil {
t.Errorf("Did not expect error for username %s and remoteAddr %s, but got %v", test.username, test.remoteAddr, err)
}
}
}

View File

@ -1,9 +1,11 @@
package sshprox package sshprox
import ( import (
"errors"
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
"regexp"
"runtime" "runtime"
"strings" "strings"
"time" "time"
@ -34,6 +36,21 @@ func IsWebSSHSupported() bool {
return true return true
} }
// Get the next free port in the list
func (m *Manager) GetNextPort() int {
nextPort := m.StartingPort
occupiedPort := make(map[int]bool)
for _, instance := range m.Instances {
occupiedPort[instance.AssignedPort] = true
}
for {
if !occupiedPort[nextPort] {
return nextPort
}
nextPort++
}
}
// Check if a given domain and port is a valid ssh server // Check if a given domain and port is a valid ssh server
func IsSSHConnectable(ipOrDomain string, port int) bool { func IsSSHConnectable(ipOrDomain string, port int) bool {
timeout := time.Second * 3 timeout := time.Second * 3
@ -60,13 +77,47 @@ func IsSSHConnectable(ipOrDomain string, port int) bool {
return string(buf[:7]) == "SSH-2.0" return string(buf[:7]) == "SSH-2.0"
} }
// Check if the port is used by other process or application // Validate the username and remote address to prevent injection
func isPortInUse(port int) bool { func ValidateUsernameAndRemoteAddr(username string, remoteIpAddr string) error {
address := fmt.Sprintf(":%d", port) // Validate and sanitize the username to prevent ssh injection
listener, err := net.Listen("tcp", address) validUsername := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
if err != nil { if !validUsername.MatchString(username) {
return errors.New("invalid username, only alphanumeric characters, dots, underscores and dashes are allowed")
}
//Check if the remoteIpAddr is a valid ipv4 or ipv6 address
if net.ParseIP(remoteIpAddr) != nil {
//A valid IP address do not need further validation
return nil
}
// Validate and sanitize the remote domain to prevent injection
validRemoteAddr := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
if !validRemoteAddr.MatchString(remoteIpAddr) {
return errors.New("invalid remote address, only alphanumeric characters, dots, underscores and dashes are allowed")
}
return nil
}
// Check if the given ip or domain is a loopback address
// or resolves to a loopback address
func IsLoopbackIPOrDomain(ipOrDomain string) bool {
if strings.EqualFold(strings.TrimSpace(ipOrDomain), "localhost") || strings.TrimSpace(ipOrDomain) == "127.0.0.1" {
return true return true
} }
listener.Close()
//Check if the ipOrDomain resolves to a loopback address
ips, err := net.LookupIP(ipOrDomain)
if err != nil {
return false
}
for _, ip := range ips {
if ip.IsLoopback() {
return true
}
}
return false return false
} }

View File

@ -33,15 +33,15 @@ type DailySummary struct {
} }
type RequestInfo struct { type RequestInfo struct {
IpAddr string IpAddr string //IP address of the downstream request
RequestOriginalCountryISOCode string RequestOriginalCountryISOCode string //ISO code of the country where the request originated
Succ bool Succ bool //If the request is successful and resp generated by upstream instead of Zoraxy (except static web server)
StatusCode int StatusCode int //HTTP status code of the request
ForwardType string ForwardType string //Forward type of the request, usually the proxy type (e.g. host-http, subdomain-websocket or vdir-http or any of the combination)
Referer string Referer string //Referer of the downstream request
UserAgent string UserAgent string //UserAgent of the downstream request
RequestURL string RequestURL string //Request URL
Target string Target string //Target domain or hostname
} }
type CollectorOption struct { type CollectorOption struct {
@ -59,7 +59,7 @@ func NewStatisticCollector(option CollectorOption) (*Collector, error) {
//Create the collector object //Create the collector object
thisCollector := Collector{ thisCollector := Collector{
DailySummary: newDailySummary(), DailySummary: NewDailySummary(),
Option: &option, Option: &option,
} }
@ -87,6 +87,11 @@ func (c *Collector) SaveSummaryOfDay() {
c.Option.Database.Write("stats", summaryKey, saveData) c.Option.Database.Write("stats", summaryKey, saveData)
} }
// Get the daily summary up until now
func (c *Collector) GetCurrentDailySummary() *DailySummary {
return c.DailySummary
}
// Load the summary of a day given // Load the summary of a day given
func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *DailySummary { func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *DailySummary {
date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local) date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
@ -99,7 +104,7 @@ func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *Daily
// Reset today summary, for debug or restoring injections // Reset today summary, for debug or restoring injections
func (c *Collector) ResetSummaryOfDay() { func (c *Collector) ResetSummaryOfDay() {
c.DailySummary = newDailySummary() c.DailySummary = NewDailySummary()
} }
// This function gives the current slot in the 288- 5 minutes interval of the day // This function gives the current slot in the 288- 5 minutes interval of the day
@ -185,8 +190,6 @@ func (c *Collector) RecordRequest(ri RequestInfo) {
c.DailySummary.UserAgent.Store(ri.UserAgent, ua.(int)+1) c.DailySummary.UserAgent.Store(ri.UserAgent, ua.(int)+1)
} }
//ADD MORE HERE IF NEEDED
//Record request URL, if it is a page //Record request URL, if it is a page
ext := filepath.Ext(ri.RequestURL) ext := filepath.Ext(ri.RequestURL)
@ -201,6 +204,8 @@ func (c *Collector) RecordRequest(ri RequestInfo) {
c.DailySummary.RequestURL.Store(ri.RequestURL, ru.(int)+1) c.DailySummary.RequestURL.Store(ri.RequestURL, ru.(int)+1)
} }
}() }()
//ADD MORE HERE IF NEEDED
} }
// nightly task // nightly task
@ -223,7 +228,7 @@ func (c *Collector) ScheduleResetRealtimeStats() chan bool {
case <-time.After(duration): case <-time.After(duration):
// store daily summary to database and reset summary // store daily summary to database and reset summary
c.SaveSummaryOfDay() c.SaveSummaryOfDay()
c.DailySummary = newDailySummary() c.DailySummary = NewDailySummary()
case <-doneCh: case <-doneCh:
// stop the routine // stop the routine
return return
@ -234,7 +239,7 @@ func (c *Collector) ScheduleResetRealtimeStats() chan bool {
return doneCh return doneCh
} }
func newDailySummary() *DailySummary { func NewDailySummary() *DailySummary {
return &DailySummary{ return &DailySummary{
TotalRequest: 0, TotalRequest: 0,
ErrorRequest: 0, ErrorRequest: 0,
@ -247,3 +252,30 @@ func newDailySummary() *DailySummary {
RequestURL: &sync.Map{}, RequestURL: &sync.Map{},
} }
} }
func PrintDailySummary(summary *DailySummary) {
summary.ForwardTypes.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.RequestOrigin.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.RequestClientIp.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.Referer.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.UserAgent.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.RequestURL.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
}

View File

@ -0,0 +1,215 @@
package statistic_test
import (
"net"
"os"
"testing"
"time"
"math/rand"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/statistic"
)
const test_db_path = "test_db"
func getNewDatabase() *database.Database {
db, err := database.NewDatabase(test_db_path, dbinc.BackendLevelDB)
if err != nil {
panic(err)
}
db.NewTable("stats")
return db
}
func clearDatabase(db *database.Database) {
db.Close()
os.RemoveAll(test_db_path)
}
func TestNewStatisticCollector(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, err := statistic.NewStatisticCollector(option)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if collector == nil {
t.Fatalf("Expected collector, got nil")
}
}
func TestSaveSummaryOfDay(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
collector.SaveSummaryOfDay()
// Add assertions to check if data is saved correctly
}
func TestLoadSummaryOfDay(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
year, month, day := time.Now().Date()
summary := collector.LoadSummaryOfDay(year, month, day)
if summary == nil {
t.Fatalf("Expected summary, got nil")
}
}
func TestResetSummaryOfDay(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
collector.ResetSummaryOfDay()
if collector.DailySummary.TotalRequest != 0 {
t.Fatalf("Expected TotalRequest to be 0, got %v", collector.DailySummary.TotalRequest)
}
}
func TestGetCurrentRealtimeStatIntervalId(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
intervalId := collector.GetCurrentRealtimeStatIntervalId()
if intervalId < 0 || intervalId > 287 {
t.Fatalf("Expected intervalId to be between 0 and 287, got %v", intervalId)
}
}
func TestRecordRequest(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
requestInfo := statistic.RequestInfo{
IpAddr: "127.0.0.1",
RequestOriginalCountryISOCode: "US",
Succ: true,
StatusCode: 200,
ForwardType: "type1",
Referer: "http://example.com",
UserAgent: "Mozilla/5.0",
RequestURL: "/test",
Target: "target1",
}
collector.RecordRequest(requestInfo)
time.Sleep(1 * time.Second) // Wait for the goroutine to finish
if collector.DailySummary.TotalRequest != 1 {
t.Fatalf("Expected TotalRequest to be 1, got %v", collector.DailySummary.TotalRequest)
}
}
func TestScheduleResetRealtimeStats(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
stopChan := collector.ScheduleResetRealtimeStats()
if stopChan == nil {
t.Fatalf("Expected stopChan, got nil")
}
collector.Close()
}
func TestNewDailySummary(t *testing.T) {
summary := statistic.NewDailySummary()
if summary.TotalRequest != 0 {
t.Fatalf("Expected TotalRequest to be 0, got %v", summary.TotalRequest)
}
if summary.ForwardTypes == nil {
t.Fatalf("Expected ForwardTypes to be initialized, got nil")
}
if summary.RequestOrigin == nil {
t.Fatalf("Expected RequestOrigin to be initialized, got nil")
}
if summary.RequestClientIp == nil {
t.Fatalf("Expected RequestClientIp to be initialized, got nil")
}
if summary.Referer == nil {
t.Fatalf("Expected Referer to be initialized, got nil")
}
if summary.UserAgent == nil {
t.Fatalf("Expected UserAgent to be initialized, got nil")
}
if summary.RequestURL == nil {
t.Fatalf("Expected RequestURL to be initialized, got nil")
}
}
func generateTestRequestInfo(db *database.Database) statistic.RequestInfo {
//Generate a random IPv4 address
randomIpAddr := ""
for {
ip := net.IPv4(byte(rand.Intn(256)), byte(rand.Intn(256)), byte(rand.Intn(256)), byte(rand.Intn(256)))
if !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsMulticast() && !ip.IsUnspecified() {
randomIpAddr = ip.String()
break
}
}
//Resolve the country code for this IP
ipLocation := "unknown"
geoIpResolver, err := geodb.NewGeoDb(db, &geodb.StoreOptions{
AllowSlowIpv4LookUp: false,
AllowSlowIpv6Lookup: true, //Just to save some RAM
})
if err == nil {
ipInfo, _ := geoIpResolver.ResolveCountryCodeFromIP(randomIpAddr)
ipLocation = ipInfo.CountryIsoCode
}
forwardType := "host-http"
//Generate a random forward type between "subdomain-http" and "host-https"
if rand.Intn(2) == 1 {
forwardType = "subdomain-http"
}
//Generate 5 random refers URL and pick from there
referers := []string{"https://example.com", "https://example.org", "https://example.net", "https://example.io", "https://example.co"}
referer := referers[rand.Intn(5)]
return statistic.RequestInfo{
IpAddr: randomIpAddr,
RequestOriginalCountryISOCode: ipLocation,
Succ: true,
StatusCode: 200,
ForwardType: forwardType,
Referer: referer,
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
RequestURL: "/benchmark",
Target: "test.imuslab.internal",
}
}
func BenchmarkRecordRequest(b *testing.B) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
var requestInfo statistic.RequestInfo = generateTestRequestInfo(db)
b.ResetTimer()
for i := 0; i < b.N; i++ {
collector.RecordRequest(requestInfo)
collector.SaveSummaryOfDay()
}
//Write the current in-memory summary to database file
b.StopTimer()
//Print the generated summary
//testSummary := collector.GetCurrentDailySummary()
//statistic.PrintDailySummary(testSummary)
}

View File

@ -1,15 +1,18 @@
package streamproxy package streamproxy
import ( import (
"encoding/json"
"errors" "errors"
"log"
"net" "net"
"os"
"path/filepath"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
) )
/* /*
@ -48,9 +51,10 @@ type ProxyRelayConfig struct {
} }
type Options struct { type Options struct {
Database *database.Database
DefaultTimeout int DefaultTimeout int
AccessControlHandler func(net.Conn) bool AccessControlHandler func(net.Conn) bool
ConfigStore string //Folder to store the config files, will be created if not exists
Logger *logger.Logger //Logger for the stream proxy
} }
type Manager struct { type Manager struct {
@ -63,13 +67,37 @@ type Manager struct {
} }
func NewStreamProxy(options *Options) *Manager { func NewStreamProxy(options *Options) (*Manager, error) {
options.Database.NewTable("tcprox") if !utils.FileExists(options.ConfigStore) {
err := os.MkdirAll(options.ConfigStore, 0775)
if err != nil {
return nil, err
}
}
//Load relay configs from db //Load relay configs from db
previousRules := []*ProxyRelayConfig{} previousRules := []*ProxyRelayConfig{}
if options.Database.KeyExists("tcprox", "rules") { streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config")
options.Database.Read("tcprox", "rules", &previousRules) if err != nil {
return nil, err
}
for _, configFile := range streamProxyConfigFiles {
//Read file into bytes
configBytes, err := os.ReadFile(configFile)
if err != nil {
options.Logger.PrintAndLog("stream-prox", "Read stream proxy config failed", err)
continue
}
thisRelayConfig := &ProxyRelayConfig{}
err = json.Unmarshal(configBytes, thisRelayConfig)
if err != nil {
options.Logger.PrintAndLog("stream-prox", "Unmarshal stream proxy config failed", err)
continue
}
//Append the config to the list
previousRules = append(previousRules, thisRelayConfig)
} }
//Check if the AccessControlHandler is empty. If yes, set it to always allow access //Check if the AccessControlHandler is empty. If yes, set it to always allow access
@ -91,14 +119,27 @@ func NewStreamProxy(options *Options) *Manager {
rule.parent = &thisManager rule.parent = &thisManager
if rule.Running { if rule.Running {
//This was previously running. Start it again //This was previously running. Start it again
log.Println("[Stream Proxy] Resuming stream proxy rule " + rule.Name) thisManager.logf("Resuming stream proxy rule "+rule.Name, nil)
rule.Start() rule.Start()
} }
} }
thisManager.Configs = previousRules thisManager.Configs = previousRules
return &thisManager return &thisManager, nil
}
// Wrapper function to log error
func (m *Manager) logf(message string, originalError error) {
if m.Options.Logger == nil {
//Print to fmt
if originalError != nil {
message += ": " + originalError.Error()
}
println(message)
return
}
m.Options.Logger.PrintAndLog("stream-prox", message, originalError)
} }
func (m *Manager) NewConfig(config *ProxyRelayOptions) string { func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
@ -179,6 +220,11 @@ func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr
} }
func (m *Manager) RemoveConfig(configUUID string) error { func (m *Manager) RemoveConfig(configUUID string) error {
//Remove the config from file
err := os.Remove(filepath.Join(m.Options.ConfigStore, configUUID+".config"))
if err != nil {
return err
}
// Find and remove the config with the specified UUID // Find and remove the config with the specified UUID
for i, config := range m.Configs { for i, config := range m.Configs {
if config.UUID == configUUID { if config.UUID == configUUID {
@ -190,8 +236,19 @@ func (m *Manager) RemoveConfig(configUUID string) error {
return errors.New("config not found") return errors.New("config not found")
} }
// Save all configs to ConfigStore folder
func (m *Manager) SaveConfigToDatabase() { func (m *Manager) SaveConfigToDatabase() {
m.Options.Database.Write("tcprox", "rules", m.Configs) for _, config := range m.Configs {
configBytes, err := json.Marshal(config)
if err != nil {
m.logf("Failed to marshal stream proxy config", err)
continue
}
err = os.WriteFile(m.Options.ConfigStore+"/"+config.UUID+".config", configBytes, 0775)
if err != nil {
m.logf("Failed to save stream proxy config", err)
}
}
} }
/* /*
@ -217,9 +274,10 @@ func (c *ProxyRelayConfig) Start() error {
if err != nil { if err != nil {
if !c.UseTCP { if !c.UseTCP {
c.Running = false c.Running = false
c.udpStopChan = nil
c.parent.SaveConfigToDatabase() c.parent.SaveConfigToDatabase()
} }
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error()) c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
} }
}() }()
} }
@ -231,8 +289,9 @@ func (c *ProxyRelayConfig) Start() error {
err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan) err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
if err != nil { if err != nil {
c.Running = false c.Running = false
c.tcpStopChan = nil
c.parent.SaveConfigToDatabase() c.parent.SaveConfigToDatabase()
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error()) c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
} }
}() }()
} }
@ -253,27 +312,27 @@ func (c *ProxyRelayConfig) Restart() {
if c.IsRunning() { if c.IsRunning() {
c.Stop() c.Stop()
} }
time.Sleep(300 * time.Millisecond) time.Sleep(3000 * time.Millisecond)
c.Start() c.Start()
} }
// Stop a running proxy if running // Stop a running proxy if running
func (c *ProxyRelayConfig) Stop() { func (c *ProxyRelayConfig) Stop() {
log.Println("[STREAM PROXY] Stopping Stream Proxy " + c.Name) c.parent.logf("Stopping Stream Proxy "+c.Name, nil)
if c.udpStopChan != nil { if c.udpStopChan != nil {
log.Println("[STREAM PROXY] Stopping UDP for " + c.Name) c.parent.logf("Stopping UDP for "+c.Name, nil)
c.udpStopChan <- true c.udpStopChan <- true
c.udpStopChan = nil c.udpStopChan = nil
} }
if c.tcpStopChan != nil { if c.tcpStopChan != nil {
log.Println("[STREAM PROXY] Stopping TCP for " + c.Name) c.parent.logf("Stopping TCP for "+c.Name, nil)
c.tcpStopChan <- true c.tcpStopChan <- true
c.tcpStopChan = nil c.tcpStopChan = nil
} }
log.Println("[STREAM PROXY] Stopped Stream Proxy " + c.Name) c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
c.Running = false c.Running = false
//Update the running status //Update the running status

View File

@ -6,7 +6,6 @@ import (
"embed" "embed"
"encoding/pem" "encoding/pem"
"io" "io"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -185,7 +184,6 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
//Load the cert and serve it //Load the cert and serve it
cer, err := tls.LoadX509KeyPair(pubKey, priKey) cer, err := tls.LoadX509KeyPair(pubKey, priKey)
if err != nil { if err != nil {
log.Println(err)
return nil, nil return nil, nil
} }

View File

@ -1,6 +1,9 @@
package update package update
import v308 "imuslab.com/zoraxy/mod/update/v308" import (
v308 "imuslab.com/zoraxy/mod/update/v308"
v315 "imuslab.com/zoraxy/mod/update/v315"
)
// Updater Core logic // Updater Core logic
func runUpdateRoutineWithVersion(fromVersion int, toVersion int) { func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
@ -10,6 +13,12 @@ func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
} else if fromVersion == 314 && toVersion == 315 {
//Updating from v3.1.4 to v3.1.5
err := v315.UpdateFrom314To315()
if err != nil {
panic(err)
}
} }
//ADD MORE VERSIONS HERE //ADD MORE VERSIONS HERE

View File

@ -0,0 +1,24 @@
package updateutil
import (
"io"
"os"
)
// Helper function to copy files
func CopyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destinationFile, err := os.Create(dst)
if err != nil {
return err
}
defer destinationFile.Close()
_, err = io.Copy(destinationFile, sourceFile)
return err
}

View File

@ -1,7 +1,7 @@
package v308 package v308
/* /*
v307 type definations v307 type definitions
This file wrap up the self-contained data structure This file wrap up the self-contained data structure
for v3.0.7 structure and allow automatic updates for v3.0.7 structure and allow automatic updates

View File

@ -1,7 +1,7 @@
package v308 package v308
/* /*
v308 type definations v308 type definition
This file wrap up the self-contained data structure This file wrap up the self-contained data structure
for v3.0.8 structure and allow automatic updates for v3.0.8 structure and allow automatic updates

View File

@ -0,0 +1,50 @@
package v315
import (
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
)
// A proxy endpoint record, a general interface for handling inbound routing
type v314ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
InactiveOrigins []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
UseStickySession bool //Use stick session for load balancing
UseActiveLoadBalance bool //Use active loadbalancing, default passive
Disabled bool //If the rule is disabled
//Inbound TLS/SSL Related
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
//Authentication
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
UseSSOIntercept bool //Allow SSO to intercept this endpoint and provide authentication via Oauth2 credentials
// Rate Limiting
RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second
//Access Control
AccessFilterUUID string //Access filter ID
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
}

View File

@ -0,0 +1,106 @@
package v315
import (
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
)
type ProxyType int
const (
ProxyTypeRoot ProxyType = iota //Root Proxy, everything not matching will be routed here
ProxyTypeHost //Host Proxy, match by host (domain) name
ProxyTypeVdir //Virtual Directory Proxy, match by path prefix
)
/* Basic Auth Related Data structure*/
// Auth credential for basic auth on certain endpoints
type BasicAuthCredentials struct {
Username string
PasswordHash string
}
// Auth credential for basic auth on certain endpoints
type BasicAuthUnhashedCredentials struct {
Username string
Password string
}
// Paths to exclude in basic auth enabled proxy handler
type BasicAuthExceptionRule struct {
PathPrefix string
}
/* Routing Rule Data Structures */
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
// program structure than directly using ProxyEndpoint
type VirtualDirectoryEndpoint struct {
MatchingPath string //Matching prefix of the request path, also act as key
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
Disabled bool //If the rule is enabled
}
// Rules and settings for header rewriting
type HeaderRewriteRules struct {
UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
}
type AuthProvider int
const (
AuthProviderNone AuthProvider = iota
AuthProviderBasicAuth
AuthProviderAuthelia
AuthProviderOauth2
)
type AuthenticationProvider struct {
AuthProvider AuthProvider //The type of authentication provider
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
}
// A proxy endpoint record, a general interface for handling inbound routing
type v315ProxyEndpoint struct {
ProxyType ProxyType //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
InactiveOrigins []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
UseStickySession bool //Use stick session for load balancing
UseActiveLoadBalance bool //Use active loadbalancing, default passive
Disabled bool //If the rule is disabled
//Inbound TLS/SSL Related
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
HeaderRewriteRules *HeaderRewriteRules
//Authentication
AuthenticationProvider *AuthenticationProvider
// Rate Limiting
RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second
//Access Control
AccessFilterUUID string //Access filter ID
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
}

124
src/mod/update/v315/v315.go Normal file
View File

@ -0,0 +1,124 @@
package v315
import (
"encoding/json"
"log"
"os"
"path/filepath"
"imuslab.com/zoraxy/mod/update/updateutil"
)
func UpdateFrom314To315() error {
//Load the configs
oldConfigFiles, err := filepath.Glob("./conf/proxy/*.config")
if err != nil {
return err
}
//Backup all the files
err = os.MkdirAll("./conf/proxy-314.old/", 0775)
if err != nil {
return err
}
for _, oldConfigFile := range oldConfigFiles {
// Extract the file name from the path
fileName := filepath.Base(oldConfigFile)
// Construct the backup file path
backupFile := filepath.Join("./conf/proxy-314.old/", fileName)
// Copy the file to the backup directory
err := updateutil.CopyFile(oldConfigFile, backupFile)
if err != nil {
return err
}
}
//read the config into the old struct
for _, oldConfigFile := range oldConfigFiles {
configContent, err := os.ReadFile(oldConfigFile)
if err != nil {
log.Println("Unable to read config file "+filepath.Base(oldConfigFile), err.Error())
continue
}
thisOldConfigStruct := v314ProxyEndpoint{}
err = json.Unmarshal(configContent, &thisOldConfigStruct)
if err != nil {
log.Println("Unable to parse file "+filepath.Base(oldConfigFile), err.Error())
continue
}
//Convert the old struct to the new struct
thisNewConfigStruct := convertV314ToV315(thisOldConfigStruct)
//Write the new config to file
newConfigContent, err := json.MarshalIndent(thisNewConfigStruct, "", " ")
if err != nil {
log.Println("Unable to marshal new config "+filepath.Base(oldConfigFile), err.Error())
continue
}
err = os.WriteFile(oldConfigFile, newConfigContent, 0664)
if err != nil {
log.Println("Unable to write new config "+filepath.Base(oldConfigFile), err.Error())
continue
}
}
return nil
}
func convertV314ToV315(thisOldConfigStruct v314ProxyEndpoint) v315ProxyEndpoint {
//Move old header and auth configs into struct
newHeaderRewriteRules := HeaderRewriteRules{
UserDefinedHeaders: thisOldConfigStruct.UserDefinedHeaders,
RequestHostOverwrite: thisOldConfigStruct.RequestHostOverwrite,
HSTSMaxAge: thisOldConfigStruct.HSTSMaxAge,
EnablePermissionPolicyHeader: thisOldConfigStruct.EnablePermissionPolicyHeader,
PermissionPolicy: thisOldConfigStruct.PermissionPolicy,
DisableHopByHopHeaderRemoval: thisOldConfigStruct.DisableHopByHopHeaderRemoval,
}
newAuthenticationProvider := AuthenticationProvider{
RequireBasicAuth: thisOldConfigStruct.RequireBasicAuth,
BasicAuthCredentials: thisOldConfigStruct.BasicAuthCredentials,
BasicAuthExceptionRules: thisOldConfigStruct.BasicAuthExceptionRules,
}
//Convert proxy type int to enum
var newConfigProxyType ProxyType
if thisOldConfigStruct.ProxyType == 0 {
newConfigProxyType = ProxyTypeRoot
} else if thisOldConfigStruct.ProxyType == 1 {
newConfigProxyType = ProxyTypeHost
} else if thisOldConfigStruct.ProxyType == 2 {
newConfigProxyType = ProxyTypeVdir
}
//Update the config struct
thisNewConfigStruct := v315ProxyEndpoint{
ProxyType: newConfigProxyType,
RootOrMatchingDomain: thisOldConfigStruct.RootOrMatchingDomain,
MatchingDomainAlias: thisOldConfigStruct.MatchingDomainAlias,
ActiveOrigins: thisOldConfigStruct.ActiveOrigins,
InactiveOrigins: thisOldConfigStruct.InactiveOrigins,
UseStickySession: thisOldConfigStruct.UseStickySession,
UseActiveLoadBalance: thisOldConfigStruct.UseActiveLoadBalance,
Disabled: thisOldConfigStruct.Disabled,
BypassGlobalTLS: thisOldConfigStruct.BypassGlobalTLS,
VirtualDirectories: thisOldConfigStruct.VirtualDirectories,
RequireRateLimit: thisOldConfigStruct.RequireRateLimit,
RateLimit: thisOldConfigStruct.RateLimit,
AccessFilterUUID: thisOldConfigStruct.AccessFilterUUID,
DefaultSiteOption: thisOldConfigStruct.DefaultSiteOption,
DefaultSiteValue: thisOldConfigStruct.DefaultSiteValue,
//Append the new struct into the new config
HeaderRewriteRules: &newHeaderRewriteRules,
AuthenticationProvider: &newAuthenticationProvider,
}
return thisNewConfigStruct
}

View File

@ -3,7 +3,6 @@ package uptime
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"log"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"strconv" "strconv"
@ -242,7 +241,7 @@ func getWebsiteStatus(url string) (int, error) {
// Create a one-time use cookie jar to store cookies // Create a one-time use cookie jar to store cookies
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil { if err != nil {
log.Fatal(err) return 0, err
} }
client := http.Client{ client := http.Client{

View File

@ -41,12 +41,12 @@ func SendOK(w http.ResponseWriter) {
// Get GET parameter // Get GET parameter
func GetPara(r *http.Request, key string) (string, error) { func GetPara(r *http.Request, key string) (string, error) {
keys, ok := r.URL.Query()[key] // Get first value from the URL query
if !ok || len(keys[0]) < 1 { value := r.URL.Query().Get(key)
if len(value) == 0 {
return "", errors.New("invalid " + key + " given") return "", errors.New("invalid " + key + " given")
} else {
return keys[0], nil
} }
return value, nil
} }
// Get GET paramter as boolean, accept 1 or true // Get GET paramter as boolean, accept 1 or true
@ -56,26 +56,29 @@ func GetBool(r *http.Request, key string) (bool, error) {
return false, err return false, err
} }
x = strings.TrimSpace(x) // Convert to lowercase and trim spaces just once to compare
switch strings.ToLower(strings.TrimSpace(x)) {
if x == "1" || strings.ToLower(x) == "true" || strings.ToLower(x) == "on" { case "1", "true", "on":
return true, nil return true, nil
} else if x == "0" || strings.ToLower(x) == "false" || strings.ToLower(x) == "off" { case "0", "false", "off":
return false, nil return false, nil
} }
return false, errors.New("invalid boolean given") return false, errors.New("invalid boolean given")
} }
// Get POST paramter // Get POST parameter
func PostPara(r *http.Request, key string) (string, error) { func PostPara(r *http.Request, key string) (string, error) {
r.ParseForm() // Try to parse the form
x := r.Form.Get(key) if err := r.ParseForm(); err != nil {
if x == "" { return "", err
return "", errors.New("invalid " + key + " given")
} else {
return x, nil
} }
// Get first value from the form
x := r.Form.Get(key)
if len(x) == 0 {
return "", errors.New("invalid " + key + " given")
}
return x, nil
} }
// Get POST paramter as boolean, accept 1 or true // Get POST paramter as boolean, accept 1 or true
@ -85,11 +88,11 @@ func PostBool(r *http.Request, key string) (bool, error) {
return false, err return false, err
} }
x = strings.TrimSpace(x) // Convert to lowercase and trim spaces just once to compare
switch strings.ToLower(strings.TrimSpace(x)) {
if x == "1" || strings.ToLower(x) == "true" || strings.ToLower(x) == "on" { case "1", "true", "on":
return true, nil return true, nil
} else if x == "0" || strings.ToLower(x) == "false" || strings.ToLower(x) == "off" { case "0", "false", "off":
return false, nil return false, nil
} }
@ -114,14 +117,19 @@ func PostInt(r *http.Request, key string) (int, error) {
func FileExists(filename string) bool { func FileExists(filename string) bool {
_, err := os.Stat(filename) _, err := os.Stat(filename)
if os.IsNotExist(err) { if err == nil {
// File exists
return true
} else if errors.Is(err, os.ErrNotExist) {
// File does not exist
return false return false
} }
return true // Some other error
return false
} }
func IsDir(path string) bool { func IsDir(path string) bool {
if FileExists(path) == false { if !FileExists(path) {
return false return false
} }
fi, err := os.Stat(path) fi, err := os.Stat(path)
@ -191,4 +199,4 @@ func ValidateListeningAddress(address string) bool {
} }
return true return true
} }

View File

@ -42,6 +42,13 @@ func (fm *FileManager) HandleList(w http.ResponseWriter, r *http.Request) {
// Construct the absolute path to the target directory // Construct the absolute path to the target directory
targetDir := filepath.Join(fm.Directory, directory) targetDir := filepath.Join(fm.Directory, directory)
// Clean path to prevent path escape #274
isValidRequest := validatePathEscape(targetDir, fm.Directory)
if !isValidRequest {
http.Error(w, "403 - Forbidden", http.StatusForbidden)
return
}
// Open the target directory // Open the target directory
dirEntries, err := os.ReadDir(targetDir) dirEntries, err := os.ReadDir(targetDir)
if err != nil { if err != nil {
@ -118,6 +125,14 @@ func (fm *FileManager) HandleUpload(w http.ResponseWriter, r *http.Request) {
// Specify the directory where you want to save the uploaded file // Specify the directory where you want to save the uploaded file
uploadDir := filepath.Join(fm.Directory, dir) uploadDir := filepath.Join(fm.Directory, dir)
// Clean path to prevent path escape #274
isValidRequest := validatePathEscape(uploadDir, fm.Directory)
if !isValidRequest {
http.Error(w, "403 - Forbidden", http.StatusForbidden)
return
}
if !utils.FileExists(uploadDir) { if !utils.FileExists(uploadDir) {
utils.SendErrorResponse(w, "upload target directory not exists") utils.SendErrorResponse(w, "upload target directory not exists")
return return
@ -157,14 +172,20 @@ func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) {
return return
} }
filePath := filepath.Join(fm.Directory, filename)
// Clean path to prevent path escape #274
isValidRequest := validatePathEscape(filePath, fm.Directory)
if !isValidRequest {
http.Error(w, "403 - Forbidden", http.StatusForbidden)
return
}
previewMode, _ := utils.GetPara(r, "preview") previewMode, _ := utils.GetPara(r, "preview")
if previewMode == "true" { if previewMode == "true" {
// Serve the file using http.ServeFile // Serve the file using http.ServeFile
filePath := filepath.Join(fm.Directory, filename)
http.ServeFile(w, r, filePath) http.ServeFile(w, r, filePath)
} else { } else {
// Trigger a download with content disposition headers // Trigger a download with content disposition headers
filePath := filepath.Join(fm.Directory, filename)
w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename)) w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename))
http.ServeFile(w, r, filePath) http.ServeFile(w, r, filePath)
} }
@ -185,6 +206,11 @@ func (fm *FileManager) HandleNewFolder(w http.ResponseWriter, r *http.Request) {
// Specify the directory where you want to create the new folder // Specify the directory where you want to create the new folder
newFolderPath := filepath.Join(fm.Directory, dirName) newFolderPath := filepath.Join(fm.Directory, dirName)
isValidRequest := validatePathEscape(newFolderPath, fm.Directory)
if !isValidRequest {
http.Error(w, "403 - Forbidden", http.StatusForbidden)
return
}
// Check if the folder already exists // Check if the folder already exists
if _, err := os.Stat(newFolderPath); os.IsNotExist(err) { if _, err := os.Stat(newFolderPath); os.IsNotExist(err) {
@ -226,6 +252,18 @@ func (fm *FileManager) HandleFileCopy(w http.ResponseWriter, r *http.Request) {
absSrcPath := filepath.Join(fm.Directory, srcPath) absSrcPath := filepath.Join(fm.Directory, srcPath)
absDestPath := filepath.Join(fm.Directory, destPath) absDestPath := filepath.Join(fm.Directory, destPath)
//Make sure the copy source and dest are within web directory folder
isValidRequest := validatePathEscape(absSrcPath, fm.Directory)
if !isValidRequest {
http.Error(w, "403 - Forbidden", http.StatusForbidden)
return
}
isValidRequest = validatePathEscape(absDestPath, fm.Directory)
if !isValidRequest {
http.Error(w, "403 - Forbidden", http.StatusForbidden)
return
}
// Check if the source path exists // Check if the source path exists
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) { if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
utils.SendErrorResponse(w, "source path does not exist") utils.SendErrorResponse(w, "source path does not exist")
@ -288,6 +326,18 @@ func (fm *FileManager) HandleFileMove(w http.ResponseWriter, r *http.Request) {
absSrcPath := filepath.Join(fm.Directory, srcPath) absSrcPath := filepath.Join(fm.Directory, srcPath)
absDestPath := filepath.Join(fm.Directory, destPath) absDestPath := filepath.Join(fm.Directory, destPath)
//Make sure move source and target are within web server directory
isValidRequest := validatePathEscape(absSrcPath, fm.Directory)
if !isValidRequest {
http.Error(w, "403 - Forbidden", http.StatusForbidden)
return
}
isValidRequest = validatePathEscape(absDestPath, fm.Directory)
if !isValidRequest {
http.Error(w, "403 - Forbidden", http.StatusForbidden)
return
}
// Check if the source path exists // Check if the source path exists
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) { if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
utils.SendErrorResponse(w, "source path does not exist") utils.SendErrorResponse(w, "source path does not exist")
@ -319,6 +369,11 @@ func (fm *FileManager) HandleFileProperties(w http.ResponseWriter, r *http.Reque
// Construct the absolute path to the target file or directory // Construct the absolute path to the target file or directory
absPath := filepath.Join(fm.Directory, filePath) absPath := filepath.Join(fm.Directory, filePath)
isValidRequest := validatePathEscape(absPath, fm.Directory)
if !isValidRequest {
http.Error(w, "403 - Forbidden", http.StatusForbidden)
return
}
// Check if the target path exists // Check if the target path exists
_, err = os.Stat(absPath) _, err = os.Stat(absPath)
@ -386,6 +441,11 @@ func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request)
// Construct the absolute path to the target file or directory // Construct the absolute path to the target file or directory
absPath := filepath.Join(fm.Directory, filePath) absPath := filepath.Join(fm.Directory, filePath)
isValidRequest := validatePathEscape(absPath, fm.Directory)
if !isValidRequest {
http.Error(w, "403 - Forbidden", http.StatusForbidden)
return
}
// Check if the target path exists // Check if the target path exists
_, err = os.Stat(absPath) _, err = os.Stat(absPath)
@ -404,3 +464,25 @@ func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request)
// Respond with a success message or appropriate response // Respond with a success message or appropriate response
utils.SendOK(w) utils.SendOK(w)
} }
// Return true if the path is within the root path
func validatePathEscape(reqestPath string, rootPath string) bool {
reqestPath = filepath.ToSlash(filepath.Clean(reqestPath))
rootPath = filepath.ToSlash(filepath.Clean(rootPath))
requestPathAbs, err := filepath.Abs(reqestPath)
if err != nil {
return false
}
rootPathAbs, err := filepath.Abs(rootPath)
if err != nil {
return false
}
if strings.HasPrefix(requestPathAbs, rootPathAbs) {
return true
}
return false
}

View File

@ -83,7 +83,11 @@ func (ws *WebServer) SetEnableDirectoryListing(w http.ResponseWriter, r *http.Re
utils.SendErrorResponse(w, "invalid setting given") utils.SendErrorResponse(w, "invalid setting given")
return return
} }
err = ws.option.Sysdb.Write("webserv", "dirlist", enableList)
if err != nil {
utils.SendErrorResponse(w, "unable to save setting")
return
}
ws.option.EnableDirectoryListing = enableList ws.option.EnableDirectoryListing = enableList
utils.SendOK(w) utils.SendOK(w)
} }

View File

@ -51,7 +51,7 @@
You can upload your html files to your web directory via the <b>Web Directory Manager</b>. You can upload your html files to your web directory via the <b>Web Directory Manager</b>.
</p> </p>
<p> <p>
For online documentation, please refer to <a href="//zoraxy.arozos.com">zoraxy.arozos.com</a> or the <a href="https://github.com/tobychui/zoraxy/wiki">project wiki</a>.<br> For online documentation, please refer to <a href="//zoraxy.aroz.org">zoraxy.aroz.org</a> or the <a href="https://github.com/tobychui/zoraxy/wiki">project wiki</a>.<br>
Thank you for using Zoraxy! Thank you for using Zoraxy!
</p> </p>
</div> </div>

View File

@ -3,6 +3,7 @@ package websocketproxy
import ( import (
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -12,6 +13,8 @@ import (
"strings" "strings"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/info/logger"
) )
var ( var (
@ -54,8 +57,11 @@ type WebsocketProxy struct {
// Additional options for websocket proxy runtime // Additional options for websocket proxy runtime
type Options struct { type Options struct {
SkipTLSValidation bool //Skip backend TLS validation SkipTLSValidation bool //Skip backend TLS validation
SkipOriginCheck bool //Skip origin check SkipOriginCheck bool //Skip origin check
CopyAllHeaders bool //Copy all headers from incoming request to backend request
UserDefinedHeaders []*rewrite.UserDefinedHeader //User defined headers
Logger *logger.Logger //Logger, can be nil
} }
// ProxyHandler returns a new http.Handler interface that reverse proxies the // ProxyHandler returns a new http.Handler interface that reverse proxies the
@ -75,20 +81,65 @@ func NewProxy(target *url.URL, options Options) *WebsocketProxy {
u.RawQuery = r.URL.RawQuery u.RawQuery = r.URL.RawQuery
return &u return &u
} }
return &WebsocketProxy{Backend: backend, Verbal: false, Options: options}
// Create a new websocket proxy
wsprox := &WebsocketProxy{Backend: backend, Verbal: false, Options: options}
if options.CopyAllHeaders {
wsprox.Director = DefaultDirector
}
return wsprox
}
// Utilities function for log printing
func (w *WebsocketProxy) Println(messsage string, err error) {
if w.Options.Logger != nil {
w.Options.Logger.PrintAndLog("websocket", messsage, err)
return
}
log.Println("[websocketproxy] [system:info]"+messsage, err)
}
// DefaultDirector is the default implementation of Director, which copies
// all headers from the incoming request to the outgoing request.
func DefaultDirector(r *http.Request, h http.Header) {
//Copy all header values from request to target header
for k, vv := range r.Header {
for _, v := range vv {
h.Set(k, v)
}
}
// Remove hop-by-hop headers
for _, removePendingHeader := range []string{
"Connection",
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Authorization",
"Te",
"Trailers",
"Transfer-Encoding",
"Sec-WebSocket-Extensions",
"Sec-WebSocket-Key",
"Sec-WebSocket-Protocol",
"Sec-WebSocket-Version",
"Upgrade",
} {
h.Del(removePendingHeader)
}
} }
// ServeHTTP implements the http.Handler that proxies WebSocket connections. // ServeHTTP implements the http.Handler that proxies WebSocket connections.
func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if w.Backend == nil { if w.Backend == nil {
log.Println("websocketproxy: backend function is not defined") w.Println("Invalid websocket backend configuration", errors.New("backend function not found"))
http.Error(rw, "internal server error (code: 1)", http.StatusInternalServerError) http.Error(rw, "internal server error (code: 1)", http.StatusInternalServerError)
return return
} }
backendURL := w.Backend(req) backendURL := w.Backend(req)
if backendURL == nil { if backendURL == nil {
log.Println("websocketproxy: backend URL is nil") w.Println("Invalid websocket backend configuration", errors.New("backend URL is nil"))
http.Error(rw, "internal server error (code: 2)", http.StatusInternalServerError) http.Error(rw, "internal server error (code: 2)", http.StatusInternalServerError)
return return
} }
@ -121,6 +172,11 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if req.Host != "" { if req.Host != "" {
requestHeader.Set("Host", req.Host) requestHeader.Set("Host", req.Host)
} }
if userAgent := req.Header.Get("User-Agent"); userAgent != "" {
requestHeader.Set("User-Agent", userAgent)
} else {
requestHeader.Set("User-Agent", "zoraxy-wsproxy/1.1")
}
// Pass X-Forwarded-For headers too, code below is a part of // Pass X-Forwarded-For headers too, code below is a part of
// httputil.ReverseProxy. See http://en.wikipedia.org/wiki/X-Forwarded-For // httputil.ReverseProxy. See http://en.wikipedia.org/wiki/X-Forwarded-For
@ -144,10 +200,29 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
requestHeader.Set("X-Forwarded-Proto", "https") requestHeader.Set("X-Forwarded-Proto", "https")
} }
// Enable the director to copy any additional headers it desires for // Replace header variables and copy user-defined headers
// forwarding to the remote server. if w.Options.CopyAllHeaders {
if w.Director != nil { // Rewrite the user defined headers
w.Director(req, requestHeader) // This is reported to be not compatible with Proxmox and Home Assistant
// but required by some other projects like MeshCentral
// we will make this optional
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(req, w.Options.UserDefinedHeaders)
upstreamHeaders, _ := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
})
for _, headerValuePair := range upstreamHeaders {
//Do not copy Upgrade and Connection headers, it will be handled by the upgrader
if strings.EqualFold(headerValuePair[0], "Upgrade") || strings.EqualFold(headerValuePair[0], "Connection") {
continue
}
requestHeader.Set(headerValuePair[0], headerValuePair[1])
}
// Enable the director to copy any additional headers it desires for
// forwarding to the remote server.
if w.Director != nil {
w.Director(req, requestHeader)
}
} }
// Connect to the backend URL, also pass the headers we get from the requst // Connect to the backend URL, also pass the headers we get from the requst
@ -158,13 +233,13 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01 // http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01
connBackend, resp, err := dialer.Dial(backendURL.String(), requestHeader) connBackend, resp, err := dialer.Dial(backendURL.String(), requestHeader)
if err != nil { if err != nil {
log.Printf("websocketproxy: couldn't dial to remote backend url %s", err) w.Println("Couldn't dial to remote backend url "+backendURL.String(), err)
if resp != nil { if resp != nil {
// If the WebSocket handshake fails, ErrBadHandshake is returned // If the WebSocket handshake fails, ErrBadHandshake is returned
// along with a non-nil *http.Response so that callers can handle // along with a non-nil *http.Response so that callers can handle
// redirects, authentication, etcetera. // redirects, authentication, etcetera.
if err := copyResponse(rw, resp); err != nil { if err := copyResponse(rw, resp); err != nil {
log.Printf("websocketproxy: couldn't write response after failed remote backend handshake: %s", err) w.Println("Couldn't write response after failed remote backend handshake to "+backendURL.String(), err)
} }
} else { } else {
http.Error(rw, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) http.Error(rw, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
@ -198,7 +273,7 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Also pass the header that we gathered from the Dial handshake. // Also pass the header that we gathered from the Dial handshake.
connPub, err := upgrader.Upgrade(rw, req, upgradeHeader) connPub, err := upgrader.Upgrade(rw, req, upgradeHeader)
if err != nil { if err != nil {
log.Printf("websocketproxy: couldn't upgrade %s", err) w.Println("Couldn't upgrade incoming request", err)
return return
} }
defer connPub.Close() defer connPub.Close()

View File

@ -31,6 +31,7 @@ func TestProxy(t *testing.T) {
proxy := NewProxy(u, Options{ proxy := NewProxy(u, Options{
SkipTLSValidation: false, SkipTLSValidation: false,
SkipOriginCheck: false, SkipOriginCheck: false,
Logger: nil,
}) })
proxy.Upgrader = upgrader proxy.Upgrader = upgrader

View File

@ -13,6 +13,7 @@ import (
"imuslab.com/zoraxy/mod/dynamicproxy" "imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/uptime" "imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -26,18 +27,18 @@ func ReverseProxtInit() {
/* /*
Load Reverse Proxy Global Settings Load Reverse Proxy Global Settings
*/ */
inboundPort := 80 inboundPort := 443
if sysdb.KeyExists("settings", "inbound") { if sysdb.KeyExists("settings", "inbound") {
sysdb.Read("settings", "inbound", &inboundPort) sysdb.Read("settings", "inbound", &inboundPort)
SystemWideLogger.Println("Serving inbound port ", inboundPort) SystemWideLogger.Println("Serving inbound port ", inboundPort)
} else { } else {
SystemWideLogger.Println("Inbound port not set. Using default (80)") SystemWideLogger.Println("Inbound port not set. Using default (443)")
} }
useTls := false useTls := true
sysdb.Read("settings", "usetls", &useTls) sysdb.Read("settings", "usetls", &useTls)
if useTls { if useTls {
SystemWideLogger.Println("TLS mode enabled. Serving proxxy request with TLS") SystemWideLogger.Println("TLS mode enabled. Serving proxy request with TLS")
} else { } else {
SystemWideLogger.Println("TLS mode disabled. Serving proxy request with plain http") SystemWideLogger.Println("TLS mode disabled. Serving proxy request with plain http")
} }
@ -58,7 +59,7 @@ func ReverseProxtInit() {
SystemWideLogger.Println("Development mode disabled. Proxying with default Cache Control policy") SystemWideLogger.Println("Development mode disabled. Proxying with default Cache Control policy")
} }
listenOnPort80 := false listenOnPort80 := true
sysdb.Read("settings", "listenP80", &listenOnPort80) sysdb.Read("settings", "listenP80", &listenOnPort80)
if listenOnPort80 { if listenOnPort80 {
SystemWideLogger.Println("Port 80 listener enabled") SystemWideLogger.Println("Port 80 listener enabled")
@ -66,7 +67,7 @@ func ReverseProxtInit() {
SystemWideLogger.Println("Port 80 listener disabled") SystemWideLogger.Println("Port 80 listener disabled")
} }
forceHttpsRedirect := false forceHttpsRedirect := true
sysdb.Read("settings", "redirect", &forceHttpsRedirect) sysdb.Read("settings", "redirect", &forceHttpsRedirect)
if forceHttpsRedirect { if forceHttpsRedirect {
SystemWideLogger.Println("Force HTTPS mode enabled") SystemWideLogger.Println("Force HTTPS mode enabled")
@ -84,7 +85,7 @@ func ReverseProxtInit() {
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{ dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
HostUUID: nodeUUID, HostUUID: nodeUUID,
HostVersion: version, HostVersion: SYSTEM_VERSION,
Port: inboundPort, Port: inboundPort,
UseTls: useTls, UseTls: useTls,
ForceTLSLatest: forceLatestTLSVersion, ForceTLSLatest: forceLatestTLSVersion,
@ -95,8 +96,9 @@ func ReverseProxtInit() {
RedirectRuleTable: redirectTable, RedirectRuleTable: redirectTable,
GeodbStore: geodbStore, GeodbStore: geodbStore,
StatisticCollector: statisticCollector, StatisticCollector: statisticCollector,
WebDirectory: *staticWebServerRoot, WebDirectory: *path_webserver,
AccessController: accessController, AccessController: accessController,
AutheliaRouter: autheliaRouter,
LoadBalancer: loadBalancer, LoadBalancer: loadBalancer,
Logger: SystemWideLogger, Logger: SystemWideLogger,
}) })
@ -307,10 +309,21 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
} }
} }
//Generate a default authenticaion provider
authMethod := dynamicproxy.AuthMethodNone
if requireBasicAuth {
authMethod = dynamicproxy.AuthMethodBasic
}
thisAuthenticationProvider := dynamicproxy.AuthenticationProvider{
AuthMethod: authMethod,
BasicAuthCredentials: basicAuthCredentials,
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
//Generate a proxy endpoint object //Generate a proxy endpoint object
thisProxyEndpoint := dynamicproxy.ProxyEndpoint{ thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
//I/O //I/O
ProxyType: dynamicproxy.ProxyType_Host, ProxyType: dynamicproxy.ProxyTypeHost,
RootOrMatchingDomain: rootOrMatchingDomain, RootOrMatchingDomain: rootOrMatchingDomain,
MatchingDomainAlias: aliasHostnames, MatchingDomainAlias: aliasHostnames,
ActiveOrigins: []*loadbalance.Upstream{ ActiveOrigins: []*loadbalance.Upstream{
@ -331,13 +344,16 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
//VDir //VDir
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{}, VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
//Custom headers //Custom headers
UserDefinedHeaders: []*dynamicproxy.UserDefinedHeader{},
//Auth //Auth
RequireBasicAuth: requireBasicAuth, AuthenticationProvider: &thisAuthenticationProvider,
BasicAuthCredentials: basicAuthCredentials,
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{}, //Header Rewrite
DefaultSiteOption: 0, HeaderRewriteRules: dynamicproxy.GetDefaultHeaderRewriteRules(),
DefaultSiteValue: "",
//Default Site
DefaultSiteOption: 0,
DefaultSiteValue: "",
// Rate Limit // Rate Limit
RequireRateLimit: requireRateLimit, RequireRateLimit: requireRateLimit,
RateLimit: int64(proxyRateLimit), RateLimit: int64(proxyRateLimit),
@ -377,7 +393,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
//Write the root options to file //Write the root options to file
rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{ rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyType_Root, ProxyType: dynamicproxy.ProxyTypeRoot,
RootOrMatchingDomain: "/", RootOrMatchingDomain: "/",
ActiveOrigins: []*loadbalance.Upstream{ ActiveOrigins: []*loadbalance.Upstream{
{ {
@ -451,13 +467,23 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
} }
bypassGlobalTLS := (bpgtls == "true") bypassGlobalTLS := (bpgtls == "true")
// Basic Auth //Disable uptime monitor
rba, _ := utils.PostPara(r, "bauth") disbleUtm, err := utils.PostBool(r, "dutm")
if rba == "" { if err != nil {
rba = "false" disbleUtm = false
} }
requireBasicAuth := (rba == "true") // Auth Provider
authProviderTypeStr, _ := utils.PostPara(r, "authprovider")
if authProviderTypeStr == "" {
authProviderTypeStr = "0"
}
authProviderType, err := strconv.Atoi(authProviderTypeStr)
if err != nil {
utils.SendErrorResponse(w, "Invalid auth provider type")
return
}
// Rate Limiting? // Rate Limiting?
rl, _ := utils.PostPara(r, "rate") rl, _ := utils.PostPara(r, "rate")
@ -492,10 +518,27 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
//Generate a new proxyEndpoint from the new config //Generate a new proxyEndpoint from the new config
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry) newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
newProxyEndpoint.RequireBasicAuth = requireBasicAuth if newProxyEndpoint.AuthenticationProvider == nil {
newProxyEndpoint.AuthenticationProvider = &dynamicproxy.AuthenticationProvider{
AuthMethod: dynamicproxy.AuthMethodNone,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
}
if authProviderType == 1 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic
} else if authProviderType == 2 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia
} else if authProviderType == 3 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2
} else {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone
}
newProxyEndpoint.RequireRateLimit = requireRateLimit newProxyEndpoint.RequireRateLimit = requireRateLimit
newProxyEndpoint.RateLimit = proxyRateLimit newProxyEndpoint.RateLimit = proxyRateLimit
newProxyEndpoint.UseStickySession = useStickySession newProxyEndpoint.UseStickySession = useStickySession
newProxyEndpoint.DisableUptimeMonitor = disbleUtm
//Prepare to replace the current routing rule //Prepare to replace the current routing rule
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint) readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
@ -622,7 +665,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
} }
usernames := []string{} usernames := []string{}
for _, cred := range targetProxy.BasicAuthCredentials { for _, cred := range targetProxy.AuthenticationProvider.BasicAuthCredentials {
usernames = append(usernames, cred.Username) usernames = append(usernames, cred.Username)
} }
@ -666,7 +709,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
if credential.Password == "" { if credential.Password == "" {
//Check if exists in the old credential files //Check if exists in the old credential files
keepUnchange := false keepUnchange := false
for _, oldCredEntry := range targetProxy.BasicAuthCredentials { for _, oldCredEntry := range targetProxy.AuthenticationProvider.BasicAuthCredentials {
if oldCredEntry.Username == credential.Username { if oldCredEntry.Username == credential.Username {
//Exists! Reuse the old hash //Exists! Reuse the old hash
mergedCredentials = append(mergedCredentials, &dynamicproxy.BasicAuthCredentials{ mergedCredentials = append(mergedCredentials, &dynamicproxy.BasicAuthCredentials{
@ -691,7 +734,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
} }
} }
targetProxy.BasicAuthCredentials = mergedCredentials targetProxy.AuthenticationProvider.BasicAuthCredentials = mergedCredentials
//Save it to file //Save it to file
SaveReverseProxyConfig(targetProxy) SaveReverseProxyConfig(targetProxy)
@ -725,7 +768,7 @@ func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
} }
//List all the exception paths for this proxy //List all the exception paths for this proxy
results := targetProxy.BasicAuthExceptionRules results := targetProxy.AuthenticationProvider.BasicAuthExceptionRules
if results == nil { if results == nil {
//It is a config from a really old version of zoraxy. Overwrite it with empty array //It is a config from a really old version of zoraxy. Overwrite it with empty array
results = []*dynamicproxy.BasicAuthExceptionRule{} results = []*dynamicproxy.BasicAuthExceptionRule{}
@ -762,7 +805,7 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
//Add a new exception rule if it is not already exists //Add a new exception rule if it is not already exists
alreadyExists := false alreadyExists := false
for _, thisExceptionRule := range targetProxy.BasicAuthExceptionRules { for _, thisExceptionRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules {
if thisExceptionRule.PathPrefix == matchingPrefix { if thisExceptionRule.PathPrefix == matchingPrefix {
alreadyExists = true alreadyExists = true
break break
@ -772,7 +815,7 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, "This matching path already exists") utils.SendErrorResponse(w, "This matching path already exists")
return return
} }
targetProxy.BasicAuthExceptionRules = append(targetProxy.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{ targetProxy.AuthenticationProvider.BasicAuthExceptionRules = append(targetProxy.AuthenticationProvider.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{
PathPrefix: strings.TrimSpace(matchingPrefix), PathPrefix: strings.TrimSpace(matchingPrefix),
}) })
@ -806,7 +849,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
newExceptionRuleList := []*dynamicproxy.BasicAuthExceptionRule{} newExceptionRuleList := []*dynamicproxy.BasicAuthExceptionRule{}
matchingExists := false matchingExists := false
for _, thisExceptionalRule := range targetProxy.BasicAuthExceptionRules { for _, thisExceptionalRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules {
if thisExceptionalRule.PathPrefix != matchingPrefix { if thisExceptionalRule.PathPrefix != matchingPrefix {
newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule) newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule)
} else { } else {
@ -819,7 +862,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
return return
} }
targetProxy.BasicAuthExceptionRules = newExceptionRuleList targetProxy.AuthenticationProvider.BasicAuthExceptionRules = newExceptionRuleList
// Save configs to runtime and file // Save configs to runtime and file
targetProxy.UpdateToRuntime() targetProxy.UpdateToRuntime()
@ -883,6 +926,7 @@ func ReverseProxyListDetail(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, "epname not defined") utils.SendErrorResponse(w, "epname not defined")
return return
} }
epname = strings.ToLower(strings.TrimSpace(epname))
endpointRaw, ok := dynamicProxyRouter.ProxyEndpoints.Load(epname) endpointRaw, ok := dynamicProxyRouter.ProxyEndpoints.Load(epname)
if !ok { if !ok {
utils.SendErrorResponse(w, "proxy rule not found") utils.SendErrorResponse(w, "proxy rule not found")
@ -910,17 +954,15 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
results := []*dynamicproxy.ProxyEndpoint{} results := []*dynamicproxy.ProxyEndpoint{}
dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool { dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
thisEndpoint := dynamicproxy.CopyEndpoint(value.(*dynamicproxy.ProxyEndpoint)) thisEndpoint := dynamicproxy.CopyEndpoint(value.(*dynamicproxy.ProxyEndpoint))
//Clear the auth passwords before showing to front-end //Clear the auth passwords before showing to front-end
cleanedCredentials := []*dynamicproxy.BasicAuthCredentials{} cleanedCredentials := []*dynamicproxy.BasicAuthCredentials{}
for _, user := range thisEndpoint.BasicAuthCredentials { for _, user := range thisEndpoint.AuthenticationProvider.BasicAuthCredentials {
cleanedCredentials = append(cleanedCredentials, &dynamicproxy.BasicAuthCredentials{ cleanedCredentials = append(cleanedCredentials, &dynamicproxy.BasicAuthCredentials{
Username: user.Username, Username: user.Username,
PasswordHash: "", PasswordHash: "",
}) })
} }
thisEndpoint.AuthenticationProvider.BasicAuthCredentials = cleanedCredentials
thisEndpoint.BasicAuthCredentials = cleanedCredentials
results = append(results, thisEndpoint) results = append(results, thisEndpoint)
return true return true
}) })
@ -1085,6 +1127,7 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
if dynamicProxyRouter.Running { if dynamicProxyRouter.Running {
dynamicProxyRouter.StopProxyService() dynamicProxyRouter.StopProxyService()
dynamicProxyRouter.Option.Port = newIncomingPortInt dynamicProxyRouter.Option.Port = newIncomingPortInt
time.Sleep(1 * time.Second) //Fixed start fail issue
dynamicProxyRouter.StartProxyService() dynamicProxyRouter.StartProxyService()
} else { } else {
//Only change setting but not starting the proxy service //Only change setting but not starting the proxy service
@ -1126,9 +1169,9 @@ func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
} }
//List all custom headers //List all custom headers
customHeaderList := targetProxyEndpoint.UserDefinedHeaders customHeaderList := targetProxyEndpoint.HeaderRewriteRules.UserDefinedHeaders
if customHeaderList == nil { if customHeaderList == nil {
customHeaderList = []*dynamicproxy.UserDefinedHeader{} customHeaderList = []*rewrite.UserDefinedHeader{}
} }
js, _ := json.Marshal(customHeaderList) js, _ := json.Marshal(customHeaderList)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
@ -1173,12 +1216,12 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
//Create a Custom Header Defination type //Create a Custom Header Definition type
var rewriteDirection dynamicproxy.HeaderDirection var rewriteDirection rewrite.HeaderDirection
if direction == "toOrigin" { if direction == "toOrigin" {
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToUpstream rewriteDirection = rewrite.HeaderDirection_ZoraxyToUpstream
} else if direction == "toClient" { } else if direction == "toClient" {
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToDownstream rewriteDirection = rewrite.HeaderDirection_ZoraxyToDownstream
} else { } else {
//Unknown direction //Unknown direction
utils.SendErrorResponse(w, "header rewrite direction not supported") utils.SendErrorResponse(w, "header rewrite direction not supported")
@ -1189,7 +1232,8 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
if rewriteType == "remove" { if rewriteType == "remove" {
isRemove = true isRemove = true
} }
headerRewriteDefination := dynamicproxy.UserDefinedHeader{
headerRewriteDefinition := rewrite.UserDefinedHeader{
Key: name, Key: name,
Value: value, Value: value,
Direction: rewriteDirection, Direction: rewriteDirection,
@ -1197,7 +1241,7 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
} }
//Create a new custom header object //Create a new custom header object
err = targetProxyEndpoint.AddUserDefinedHeader(&headerRewriteDefination) err = targetProxyEndpoint.AddUserDefinedHeader(&headerRewriteDefinition)
if err != nil { if err != nil {
utils.SendErrorResponse(w, "unable to add header rewrite rule: "+err.Error()) utils.SendErrorResponse(w, "unable to add header rewrite rule: "+err.Error())
return return
@ -1267,7 +1311,7 @@ func HandleHostOverwrite(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
//Get the current host header //Get the current host header
js, _ := json.Marshal(targetProxyEndpoint.RequestHostOverwrite) js, _ := json.Marshal(targetProxyEndpoint.HeaderRewriteRules.RequestHostOverwrite)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost { } else if r.Method == http.MethodPost {
//Set the new host header //Set the new host header
@ -1276,7 +1320,7 @@ func HandleHostOverwrite(w http.ResponseWriter, r *http.Request) {
//As this will require change in the proxy instance we are running //As this will require change in the proxy instance we are running
//we need to clone and respawn this proxy endpoint //we need to clone and respawn this proxy endpoint
newProxyEndpoint := targetProxyEndpoint.Clone() newProxyEndpoint := targetProxyEndpoint.Clone()
newProxyEndpoint.RequestHostOverwrite = newHostname newProxyEndpoint.HeaderRewriteRules.RequestHostOverwrite = newHostname
//Save proxy endpoint //Save proxy endpoint
err = SaveReverseProxyConfig(newProxyEndpoint) err = SaveReverseProxyConfig(newProxyEndpoint)
if err != nil { if err != nil {
@ -1339,7 +1383,7 @@ func HandleHopByHop(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
//Get the current hop by hop header state //Get the current hop by hop header state
js, _ := json.Marshal(!targetProxyEndpoint.DisableHopByHopHeaderRemoval) js, _ := json.Marshal(!targetProxyEndpoint.HeaderRewriteRules.DisableHopByHopHeaderRemoval)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost { } else if r.Method == http.MethodPost {
//Set the hop by hop header state //Set the hop by hop header state
@ -1349,7 +1393,7 @@ func HandleHopByHop(w http.ResponseWriter, r *http.Request) {
//we need to clone and respawn this proxy endpoint //we need to clone and respawn this proxy endpoint
newProxyEndpoint := targetProxyEndpoint.Clone() newProxyEndpoint := targetProxyEndpoint.Clone()
//Storage file use false as default, so disable removal = not enable remover //Storage file use false as default, so disable removal = not enable remover
newProxyEndpoint.DisableHopByHopHeaderRemoval = !enableHopByHopRemover newProxyEndpoint.HeaderRewriteRules.DisableHopByHopHeaderRemoval = !enableHopByHopRemover
//Save proxy endpoint //Save proxy endpoint
err = SaveReverseProxyConfig(newProxyEndpoint) err = SaveReverseProxyConfig(newProxyEndpoint)
@ -1412,7 +1456,7 @@ func HandleHSTSState(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
//Return current HSTS enable state //Return current HSTS enable state
hstsAge := targetProxyEndpoint.HSTSMaxAge hstsAge := targetProxyEndpoint.HeaderRewriteRules.HSTSMaxAge
js, _ := json.Marshal(hstsAge) js, _ := json.Marshal(hstsAge)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
return return
@ -1424,8 +1468,12 @@ func HandleHSTSState(w http.ResponseWriter, r *http.Request) {
} }
if newMaxAge == 0 || newMaxAge >= 31536000 { if newMaxAge == 0 || newMaxAge >= 31536000 {
targetProxyEndpoint.HSTSMaxAge = int64(newMaxAge) targetProxyEndpoint.HeaderRewriteRules.HSTSMaxAge = int64(newMaxAge)
SaveReverseProxyConfig(targetProxyEndpoint) err = SaveReverseProxyConfig(targetProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, "save HSTS state failed: "+err.Error())
return
}
targetProxyEndpoint.UpdateToRuntime() targetProxyEndpoint.UpdateToRuntime()
} else { } else {
utils.SendErrorResponse(w, "invalid max age given") utils.SendErrorResponse(w, "invalid max age given")
@ -1462,11 +1510,11 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
} }
currentPolicy := permissionpolicy.GetDefaultPermissionPolicy() currentPolicy := permissionpolicy.GetDefaultPermissionPolicy()
if targetProxyEndpoint.PermissionPolicy != nil { if targetProxyEndpoint.HeaderRewriteRules.PermissionPolicy != nil {
currentPolicy = targetProxyEndpoint.PermissionPolicy currentPolicy = targetProxyEndpoint.HeaderRewriteRules.PermissionPolicy
} }
result := CurrentPolicyState{ result := CurrentPolicyState{
PPEnabled: targetProxyEndpoint.EnablePermissionPolicyHeader, PPEnabled: targetProxyEndpoint.HeaderRewriteRules.EnablePermissionPolicyHeader,
CurrentPolicy: currentPolicy, CurrentPolicy: currentPolicy,
} }
@ -1481,7 +1529,7 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
return return
} }
targetProxyEndpoint.EnablePermissionPolicyHeader = enableState targetProxyEndpoint.HeaderRewriteRules.EnablePermissionPolicyHeader = enableState
SaveReverseProxyConfig(targetProxyEndpoint) SaveReverseProxyConfig(targetProxyEndpoint)
targetProxyEndpoint.UpdateToRuntime() targetProxyEndpoint.UpdateToRuntime()
utils.SendOK(w) utils.SendOK(w)
@ -1503,7 +1551,7 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
} }
//Save it to file //Save it to file
targetProxyEndpoint.PermissionPolicy = newPermissionPolicy targetProxyEndpoint.HeaderRewriteRules.PermissionPolicy = newPermissionPolicy
SaveReverseProxyConfig(targetProxyEndpoint) SaveReverseProxyConfig(targetProxyEndpoint)
targetProxyEndpoint.UpdateToRuntime() targetProxyEndpoint.UpdateToRuntime()
utils.SendOK(w) utils.SendOK(w)
@ -1512,3 +1560,39 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
} }
func HandleWsHeaderBehavior(w http.ResponseWriter, r *http.Request) {
domain, err := utils.PostPara(r, "domain")
if err != nil {
domain, err = utils.GetPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "domain or matching rule not defined")
return
}
}
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
if r.Method == http.MethodGet {
js, _ := json.Marshal(targetProxyEndpoint.EnableWebsocketCustomHeaders)
utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost {
enableWsHeader, err := utils.PostBool(r, "enable")
if err != nil {
utils.SendErrorResponse(w, "invalid enable state given")
return
}
targetProxyEndpoint.EnableWebsocketCustomHeaders = enableWsHeader
SaveReverseProxyConfig(targetProxyEndpoint)
targetProxyEndpoint.UpdateToRuntime()
utils.SendOK(w)
} else {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
}

View File

@ -27,7 +27,7 @@ func FSHandler(handler http.Handler) http.Handler {
Development Mode Override Development Mode Override
=> Web root is located in / => Web root is located in /
*/ */
if development && strings.HasPrefix(r.URL.Path, "/web/") { if DEVELOPMENT_BUILD && strings.HasPrefix(r.URL.Path, "/web/") {
u, _ := url.Parse(strings.TrimPrefix(r.URL.Path, "/web")) u, _ := url.Parse(strings.TrimPrefix(r.URL.Path, "/web"))
r.URL = u r.URL = u
} }
@ -36,7 +36,7 @@ func FSHandler(handler http.Handler) http.Handler {
Production Mode Override Production Mode Override
=> Web root is located in /web => Web root is located in /web
*/ */
if !development && r.URL.Path == "/" { if !DEVELOPMENT_BUILD && r.URL.Path == "/" {
//Redirect to web UI //Redirect to web UI
http.Redirect(w, r, "/web/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/web/", http.StatusTemporaryRedirect)
return return
@ -93,7 +93,7 @@ func FSHandler(handler http.Handler) http.Handler {
// Production path fix wrapper. Fix the path on production or development environment // Production path fix wrapper. Fix the path on production or development environment
func ppf(relativeFilepath string) string { func ppf(relativeFilepath string) string {
if !development { if !DEVELOPMENT_BUILD {
return strings.ReplaceAll(filepath.Join("/web/", relativeFilepath), "\\", "/") return strings.ReplaceAll(filepath.Join("/web/", relativeFilepath), "\\", "/")
} }
return relativeFilepath return relativeFilepath
@ -111,7 +111,7 @@ func handleInjectHTML(w http.ResponseWriter, r *http.Request, relativeFilepath s
if len(relativeFilepath) > 0 && relativeFilepath[len(relativeFilepath)-1:] == "/" { if len(relativeFilepath) > 0 && relativeFilepath[len(relativeFilepath)-1:] == "/" {
relativeFilepath = relativeFilepath + "index.html" relativeFilepath = relativeFilepath + "index.html"
} }
if development { if DEVELOPMENT_BUILD {
//Load from disk //Load from disk
targetFilePath := strings.ReplaceAll(filepath.Join("web/", relativeFilepath), "\\", "/") targetFilePath := strings.ReplaceAll(filepath.Join("web/", relativeFilepath), "\\", "/")
content, err = os.ReadFile(targetFilePath) content, err = os.ReadFile(targetFilePath)

View File

@ -12,7 +12,9 @@ import (
"imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/dockerux" "imuslab.com/zoraxy/mod/dockerux"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection" "imuslab.com/zoraxy/mod/dynamicproxy/redirection"
@ -36,7 +38,10 @@ import (
Startup Sequence Startup Sequence
This function starts the startup sequence of all This function starts the startup sequence of all
required modules required modules. Their startup sequences are inter-dependent
and must be started in a specific order.
Don't touch this function unless you know what you are doing
*/ */
var ( var (
@ -49,19 +54,26 @@ var (
func startupSequence() { func startupSequence() {
//Start a system wide logger and log viewer //Start a system wide logger and log viewer
l, err := logger.NewLogger("zr", "./log") l, err := logger.NewLogger(LOG_PREFIX, *path_logFile)
if err == nil { if err == nil {
SystemWideLogger = l SystemWideLogger = l
} else { } else {
panic(err) panic(err)
} }
LogViewer = logviewer.NewLogViewer(&logviewer.ViewerOption{ LogViewer = logviewer.NewLogViewer(&logviewer.ViewerOption{
RootFolder: "./log", RootFolder: *path_logFile,
Extension: ".log", Extension: LOG_EXTENSION,
}) })
//Create database //Create database
db, err := database.NewDatabase("sys.db", false) backendType := database.GetRecommendedBackendType()
if *databaseBackend == "leveldb" {
backendType = dbinc.BackendLevelDB
} else if *databaseBackend == "boltdb" {
backendType = dbinc.BackendBoltDB
}
l.PrintAndLog("database", "Using "+backendType.String()+" as the database backend", nil)
db, err := database.NewDatabase("./sys.db", backendType)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -70,21 +82,21 @@ func startupSequence() {
sysdb.NewTable("settings") sysdb.NewTable("settings")
//Create tmp folder and conf folder //Create tmp folder and conf folder
os.MkdirAll("./tmp", 0775) os.MkdirAll(TMP_FOLDER, 0775)
os.MkdirAll("./conf/proxy/", 0775) os.MkdirAll(CONF_HTTP_PROXY, 0775)
//Create an auth agent //Create an auth agent
sessionKey, err := auth.GetSessionKey(sysdb, SystemWideLogger) sessionKey, err := auth.GetSessionKey(sysdb, SystemWideLogger)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
authAgent = auth.NewAuthenticationAgent(name, []byte(sessionKey), sysdb, true, SystemWideLogger, func(w http.ResponseWriter, r *http.Request) { authAgent = auth.NewAuthenticationAgent(SYSTEM_NAME, []byte(sessionKey), sysdb, true, SystemWideLogger, func(w http.ResponseWriter, r *http.Request) {
//Not logged in. Redirecting to login page //Not logged in. Redirecting to login page
http.Redirect(w, r, ppf("/login.html"), http.StatusTemporaryRedirect) http.Redirect(w, r, ppf("/login.html"), http.StatusTemporaryRedirect)
}) })
//Create a TLS certificate manager //Create a TLS certificate manager
tlsCertManager, err = tlscert.NewManager("./conf/certs", development, SystemWideLogger) tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, DEVELOPMENT_BUILD, SystemWideLogger)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -93,15 +105,17 @@ func startupSequence() {
db.NewTable("redirect") db.NewTable("redirect")
redirectAllowRegexp := false redirectAllowRegexp := false
db.Read("redirect", "regex", &redirectAllowRegexp) db.Read("redirect", "regex", &redirectAllowRegexp)
redirectTable, err = redirection.NewRuleTable("./conf/redirect", redirectAllowRegexp, SystemWideLogger) redirectTable, err = redirection.NewRuleTable(CONF_REDIRECTION, redirectAllowRegexp, SystemWideLogger)
if err != nil { if err != nil {
panic(err) panic(err)
} }
//Create a geodb store //Create a geodb store
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{ geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup, AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup,
AllowSloeIpv6Lookup: !*enableHighSpeedGeoIPLookup, AllowSlowIpv6Lookup: !*enableHighSpeedGeoIPLookup,
Logger: SystemWideLogger,
SlowLookupCacheClearInterval: GEODB_CACHE_CLEAR_INTERVAL * time.Minute,
}) })
if err != nil { if err != nil {
panic(err) panic(err)
@ -118,12 +132,20 @@ func startupSequence() {
accessController, err = access.NewAccessController(&access.Options{ accessController, err = access.NewAccessController(&access.Options{
Database: sysdb, Database: sysdb,
GeoDB: geodbStore, GeoDB: geodbStore,
ConfigFolder: "./conf/access", ConfigFolder: CONF_ACCESS_RULE,
}) })
if err != nil { if err != nil {
panic(err) panic(err)
} }
//Create authentication providers
autheliaRouter = authelia.NewAutheliaRouter(&authelia.AutheliaRouterOptions{
UseHTTPS: false, // Automatic populate in router initiation
AutheliaURL: "", // Automatic populate in router initiation
Logger: SystemWideLogger,
Database: sysdb,
})
//Create a statistic collector //Create a statistic collector
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{ statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
Database: sysdb, Database: sysdb,
@ -135,8 +157,8 @@ func startupSequence() {
//Start the static web server //Start the static web server
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{ staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
Sysdb: sysdb, Sysdb: sysdb,
Port: "5487", //Default Port Port: strconv.Itoa(WEBSERV_DEFAULT_PORT), //Default Port
WebRoot: *staticWebServerRoot, WebRoot: *path_webserver,
EnableDirectoryListing: true, EnableDirectoryListing: true,
EnableWebDirManager: *allowWebFileManager, EnableWebDirManager: *allowWebFileManager,
Logger: SystemWideLogger, Logger: SystemWideLogger,
@ -160,7 +182,7 @@ func startupSequence() {
pathRuleHandler = pathrule.NewPathRuleHandler(&pathrule.Options{ pathRuleHandler = pathrule.NewPathRuleHandler(&pathrule.Options{
Enabled: false, Enabled: false,
ConfigFolder: "./conf/rules/pathrules", ConfigFolder: CONF_PATH_RULE,
}) })
/* /*
@ -178,7 +200,7 @@ func startupSequence() {
hostName := *mdnsName hostName := *mdnsName
if hostName == "" { if hostName == "" {
hostName = "zoraxy_" + nodeUUID hostName = MDNS_HOSTNAME_PREFIX + nodeUUID
} else { } else {
//Trim off the suffix //Trim off the suffix
hostName = strings.TrimSuffix(hostName, ".local") hostName = strings.TrimSuffix(hostName, ".local")
@ -187,24 +209,24 @@ func startupSequence() {
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{ mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
HostName: hostName, HostName: hostName,
Port: portInt, Port: portInt,
Domain: "zoraxy.arozos.com", Domain: MDNS_IDENTIFY_DOMAIN,
Model: "Network Gateway", Model: MDNS_IDENTIFY_DEVICE_TYPE,
UUID: nodeUUID, UUID: nodeUUID,
Vendor: "imuslab.com", Vendor: MDNS_IDENTIFY_VENDOR,
BuildVersion: version, BuildVersion: SYSTEM_VERSION,
}, "") }, "")
if err != nil { if err != nil {
SystemWideLogger.Println("Unable to startup mDNS service. Disabling mDNS services") SystemWideLogger.Println("Unable to startup mDNS service. Disabling mDNS services")
} else { } else {
//Start initial scanning //Start initial scanning
go func() { go func() {
hosts := mdnsScanner.Scan(30, "") hosts := mdnsScanner.Scan(MDNS_SCAN_TIMEOUT, "")
previousmdnsScanResults = hosts previousmdnsScanResults = hosts
SystemWideLogger.Println("mDNS Startup scan completed") SystemWideLogger.Println("mDNS Startup scan completed")
}() }()
//Create a ticker to update mDNS results every 5 minutes //Create a ticker to update mDNS results every 5 minutes
ticker := time.NewTicker(15 * time.Minute) ticker := time.NewTicker(MDNS_SCAN_UPDATE_INTERVAL * time.Minute)
stopChan := make(chan bool) stopChan := make(chan bool)
go func() { go func() {
for { for {
@ -212,7 +234,7 @@ func startupSequence() {
case <-stopChan: case <-stopChan:
ticker.Stop() ticker.Stop()
case <-ticker.C: case <-ticker.C:
hosts := mdnsScanner.Scan(30, "") hosts := mdnsScanner.Scan(MDNS_SCAN_TIMEOUT, "")
previousmdnsScanResults = hosts previousmdnsScanResults = hosts
SystemWideLogger.Println("mDNS scan result updated") SystemWideLogger.Println("mDNS scan result updated")
} }
@ -244,10 +266,14 @@ func startupSequence() {
webSshManager = sshprox.NewSSHProxyManager() webSshManager = sshprox.NewSSHProxyManager()
//Create TCP Proxy Manager //Create TCP Proxy Manager
streamProxyManager = streamproxy.NewStreamProxy(&streamproxy.Options{ streamProxyManager, err = streamproxy.NewStreamProxy(&streamproxy.Options{
Database: sysdb,
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess, AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
ConfigStore: CONF_STREAM_PROXY,
Logger: SystemWideLogger,
}) })
if err != nil {
panic(err)
}
//Create WoL MAC storage table //Create WoL MAC storage table
sysdb.NewTable("wolmac") sysdb.NewTable("wolmac")
@ -280,11 +306,12 @@ func startupSequence() {
sysdb.NewTable("acmepref") sysdb.NewTable("acmepref")
acmeHandler = initACME() acmeHandler = initACME()
acmeAutoRenewer, err = acme.NewAutoRenewer( acmeAutoRenewer, err = acme.NewAutoRenewer(
"./conf/acme_conf.json", ACME_AUTORENEW_CONFIG_PATH,
"./conf/certs/", CONF_CERT_STORE,
int64(*acmeAutoRenewInterval), int64(*acmeAutoRenewInterval),
*acmeCertAutoRenewDays, *acmeCertAutoRenewDays,
acmeHandler, acmeHandler,
SystemWideLogger,
) )
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -298,6 +325,7 @@ func startupSequence() {
} }
/* Finalize Startup Sequence */
// This sequence start after everything is initialized // This sequence start after everything is initialized
func finalSequence() { func finalSequence() {
//Start ACME renew agent //Start ACME renew agent
@ -306,3 +334,45 @@ func finalSequence() {
//Inject routing rules //Inject routing rules
registerBuildInRoutingRules() registerBuildInRoutingRules()
} }
/* Shutdown Sequence */
func ShutdownSeq() {
SystemWideLogger.Println("Shutting down " + SYSTEM_NAME)
SystemWideLogger.Println("Closing Netstats Listener")
if netstatBuffers != nil {
netstatBuffers.Close()
}
SystemWideLogger.Println("Closing Statistic Collector")
if statisticCollector != nil {
statisticCollector.Close()
}
if mdnsTickerStop != nil {
SystemWideLogger.Println("Stopping mDNS Discoverer (might take a few minutes)")
// Stop the mdns service
mdnsTickerStop <- true
}
if mdnsScanner != nil {
mdnsScanner.Close()
}
SystemWideLogger.Println("Shutting down load balancer")
if loadBalancer != nil {
loadBalancer.Close()
}
SystemWideLogger.Println("Closing Certificates Auto Renewer")
if acmeAutoRenewer != nil {
acmeAutoRenewer.Close()
}
//Remove the tmp folder
SystemWideLogger.Println("Cleaning up tmp files")
os.RemoveAll("./tmp")
//Close database
SystemWideLogger.Println("Stopping system database")
sysdb.Close()
//Close logger
SystemWideLogger.Println("Closing system wide logger")
SystemWideLogger.Close()
}

Some files were not shown because too many files have changed in this diff Show More