23 Commits
3.0.0 ... 3.0.2

Author SHA1 Message Date
4d6c79f51b Update README.md
Added Alias support in Features
2024-04-24 16:17:47 +08:00
2c045f4f40 Merge pull request #124 from tobychui/v3.0.2
V3.0.2 Updates

Pre-checks on git.hkwtc is working and approved

- Added alias for HTTP proxy host names
- Added separator support for create new proxy rules (use "," to add alias when creating new proxy rule)
- Added HTTP proxy host based access rules
- Added EAD Configuration for ACME (by @yeungalan )
- Fixed bug for bypassGlobalTLS endpoint do not support basic-auth
- Removed dependencies on management panel css for online font files
2024-04-24 16:15:53 +08:00
b8cf046ca6 Fixed offline font bug
- Fixed offline font bug
- Set to pre-release embedded webui
2024-04-24 11:34:00 +08:00
026dd6b89d Update README.md
Added more info
2024-04-19 09:57:34 +08:00
5805fe6ed2 Update README.md
Added more info
2024-04-19 09:56:58 +08:00
3c78211800 Added alias support
+ Added alias support (use , when adding a new proxy target to automatically add alias hostnames)
+ Fixed some UI issues
2024-04-16 23:33:24 +08:00
8e648a8e1f v3.0.2 init commit
+ Fixed zeroSSL bug (said by @yeungalan ) #45
+ Fixed manual renew button bug
+ Seperated geodb module with access controller
+ Added per hosts access control (experimental) #69
+ Fixed basic auth not working on TLS bypass mode bug
+ Fixed empty domain crash bug #120
2024-04-14 19:37:01 +08:00
a000893dd1 Merge pull request #118 from Morethanevil/main
Update CHANGELOG.md
2024-04-04 18:44:09 +08:00
db88bfb752 Update CHANGELOG.md
Thanks again for your hard work
2024-04-04 11:54:38 +02:00
05297d854b Merge pull request #117 from tobychui/v3.0.1
V3.0.1 Updates
- Added regex support for redirect (slow, don't use it unless you really needs it)
- Added new dpcore implementations for faster proxy speed
- Added support for CF-Connecting-IP to X-Real-IP auto rewrite
- Added better 404 page
- Added option to bypass websocket origin check
- Updated project homepage design
- Fixed recursive port detection logic
- Fixed UserAgent in resp bug
- Updated minimum required Go version to v1.22 (Notes: Windows 7 support is dropped)
2024-04-04 15:02:10 +08:00
0d7bce4d30 Updated to go 1.22 2024-04-04 14:48:17 +08:00
8db95dddc6 Added regexp redirect support 2024-04-04 14:24:38 +08:00
05daeded37 Updated dpcore
- Added immediate flush for buffer (i.e. no chunked encoding)
- Added support for stream mode proxy sniffing logic
2024-04-01 21:36:11 +08:00
8ce6471be5 Updated homepage design 2024-03-30 16:26:03 +08:00
e242c9288f Fixed recursive port detection logic
+ Fixed recursive port detection logic
+ Added support for CF-Connecting-IP #114
2024-03-30 15:52:10 +08:00
c55a29e7cf Better 404 page and rules disable toggle 2024-03-16 20:45:05 +08:00
6af047430c Added UI for WebSocket Origin Check bypass 2024-03-12 14:03:31 +08:00
200c924acd 3.0.1 init commit
- Removed Go HTTP client UA
- Added optional bypass of websocket origin check #107
- Added basic forward proxy for debug
- Fixed UI error in network utils tab
2024-03-10 14:49:18 +08:00
9b2168466c Update access.html
Fixed issue #103
2024-02-24 14:04:33 +08:00
7ae48bf370 Update README.md
Added quick download links for Github noobs
2024-02-20 22:00:58 +08:00
ee3d76fb96 Fix: Failure to build upon release
If we need these back, we can just re-add them.
2024-02-18 14:20:34 -05:00
40d192524b Merge pull request #102 from Morethanevil/main
Update CHANGELOG.md
2024-02-18 21:08:50 +08:00
c659e05005 Update CHANGELOG.md
Updated Changelog
2024-02-18 13:36:35 +01:00
109 changed files with 4878 additions and 3260 deletions

View File

@ -22,7 +22,6 @@ jobs:
- name: Login to Docker & GHCR
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
#echo "${{ secrets.GHCR_PASSWORD }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin
- name: Setup building file structure
run: |
@ -39,7 +38,4 @@ jobs:
--platform linux/amd64,linux/arm64 \
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
--tag zoraxydocker/zoraxy:latest \
# Since this is still undetermined, I will leave it commented
#--tag ghcr.io/zoraxydocker/zoraxy:${{ steps.get_latest_release_tag.outputs.latest_tag }} \
#--tag ghcr.io/zoraxydocker/zoraxy:latest \
.

View File

@ -1,3 +1,43 @@
# v3.0.1 Apr 04 2024
## Bugfixupdate for big release of V3, read update notes from V3 if you are still on V2
+ Added regex support for redirect (slow, don't use it unless you really needs it) [#42](https://github.com/tobychui/zoraxy/issues/42)
+ Added new dpcore implementations for faster proxy speed
+ Added support for CF-Connecting-IP to X-Real-IP auto rewrite [#114](https://github.com/tobychui/zoraxy/issues/114)
+ Added enable / disable of HTTP proxy rules in runtime via slider [#108](https://github.com/tobychui/zoraxy/issues/108)
+ Added better 404 page
+ Added option to bypass websocket origin check [#107](https://github.com/tobychui/zoraxy/issues/107)
+ Updated project homepage design
+ Fixed recursive port detection logic
+ Fixed UserAgent in resp bug
+ Updated minimum required Go version to v1.22 (Notes: Windows 7 support is dropped) [#112](https://github.com/tobychui/zoraxy/issues/112)
# v3.0.0 Feb 18 2024
## IMPORTANT: V3 is a big rewrite and it is incompatible with V2! There is NO migration, if you want to stay on V2, please use V2 branch!
+ Added comments for whitelist [#97](https://github.com/tobychui/zoraxy/issues/97)
+ Added force-renew for certificates [#92](https://github.com/tobychui/zoraxy/issues/92)
+ Added automatic cert pick for multi-host certs (SNI)
+ Renamed .crt to .pem for cert store
+ Added best-fit selection for wildcard matching rules
+ Added x-proxy-by header / Added X-real-Ip header [#93](https://github.com/tobychui/zoraxy/issues/93)
+ Added Development Mode (Cache-Control: no-store)
+ Updated utm timeout to 10 seconds instead of 90
+ Added "Add controller as member" feature to Global Area Network editor
+ Added custom header
+ Deprecated aroz subservice support
+ Updated visuals, improving logical structure, less depressing colors [#95](https://github.com/tobychui/zoraxy/issues/95)
+ Added virtual directory into host routing object (each host now got its own sets of virtual directories)
+ Added support for wildcard host names (e.g. *.example.com)
+ Added best-fit selection for wildcard matching rules (e.g. *.a.example.com > *.example.com in routing)
+ Generalized root and hosts routing struct (no more conversion between runtime & save record object
+ Added "Default Site" to replace "Proxy Root" interface
+ Added Redirect & 404 page for "Default Site"
# v2.6.8 Nov 25 2023
+ Added opt-out for subdomains for global TLS settings: See [release notes](https://github.com/tobychui/zoraxy/releases/tag/2.6.8)

View File

@ -9,15 +9,16 @@ General purpose request (reverse) proxy and forwarding tool for networking noobs
### Features
- Simple to use interface with detail in-system instructions
- Reverse Proxy
- Reverse Proxy (HTTP/2)
- Virtual Directory
- WebSocket Proxy (automatic, no set-up needed)
- Basic Auth
- Alias Hostnames
- Custom Headers
- Redirection Rules
- TLS / SSL setup and deploy
- ACME features like auto-renew to serve your sites in http**s**
- SNI support (one certificate contains multiple host names)
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
- Global Area Network Controller Web UI (ZeroTier not included)
- TCP Tunneling / Proxy
@ -32,8 +33,15 @@ General purpose request (reverse) proxy and forwarding tool for networking noobs
- External permission management system for easy system integration
- SMTP config for password reset
## Downloads
[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 (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/)
## Build from Source
Requires Go 1.20 or higher
Requires Go 1.22 or higher
```bash
git clone https://github.com/tobychui/zoraxy

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 MiB

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="m772-635-43-100-104-46 104-45 43-95 43 95 104 45-104 46-43 100Zm0 595-43-96-104-45 104-45 43-101 43 101 104 45-104 45-43 96ZM333-194l-92-197-201-90 201-90 92-196 93 196 200 90-200 90-93 197Zm0-148 48-96 98-43-98-43-48-96-47 96-99 43 99 43 47 96Zm0-139Z"/></svg>
<svg class="item-icon" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="m772-635-43-100-104-46 104-45 43-95 43 95 104 45-104 46-43 100Zm0 595-43-96-104-45 104-45 43-101 43 101 104 45-104 45-43 96ZM333-194l-92-197-201-90 201-90 92-196 93 196 200 90-200 90-93 197Zm0-148 48-96 98-43-98-43-48-96-47 96-99 43 99 43 47 96Zm0-139Z"/></svg>

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 377 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M280-453h400v-60H280v60ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z"/></svg>
<svg fill="#ff7a7a" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M280-453h400v-60H280v60ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z"/></svg>

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 448 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M320-242 80-482l242-242 43 43-199 199 197 197-43 43Zm318 2-43-43 199-199-197-197 43-43 240 240-242 242Z"/></svg>
<svg class="item-icon" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M320-242 80-482l242-242 43 43-199 199 197 197-43 43Zm318 2-43-43 199-199-197-197 43-43 240 240-242 242Z"/></svg>

Before

Width:  |  Height:  |  Size: 209 B

After

Width:  |  Height:  |  Size: 227 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M120-80v-270h120v-160h210v-100H330v-270h300v270H510v100h210v160h120v270H540v-270h120v-100H300v100h120v270H120Zm270-590h180v-150H390v150ZM180-140h180v-150H180v150Zm420 0h180v-150H600v150ZM480-670ZM360-290Zm240 0Z"/></svg>
<svg fill="#919191" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M120-80v-270h120v-160h210v-100H330v-270h300v270H510v100h210v160h120v270H540v-270h120v-100H300v100h120v270H120Zm270-590h180v-150H390v150ZM180-140h180v-150H180v150Zm420 0h180v-150H600v150ZM480-670ZM360-290Zm240 0Z"/></svg>

Before

Width:  |  Height:  |  Size: 317 B

After

Width:  |  Height:  |  Size: 332 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M220-180h150v-250h220v250h150v-390L480-765 220-570v390Zm-60 60v-480l320-240 320 240v480H530v-250H430v250H160Zm320-353Z"/></svg>
<svg class="item-icon" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M220-180h150v-250h220v250h150v-390L480-765 220-570v390Zm-60 60v-480l320-240 320 240v480H530v-250H430v250H160Zm320-353Z"/></svg>

Before

Width:  |  Height:  |  Size: 224 B

After

Width:  |  Height:  |  Size: 242 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M356-120H180q-24 0-42-18t-18-42v-176q44-5 75.5-34.5T227-463q0-43-31.5-72.5T120-570v-176q0-24 18-42t42-18h177q11-40 39.5-67t68.5-27q40 0 68.5 27t39.5 67h173q24 0 42 18t18 42v173q40 11 65.5 41.5T897-461q0 40-25.5 67T806-356v176q0 24-18 42t-42 18H570q-5-48-35.5-77.5T463-227q-41 0-71.5 29.5T356-120Zm-176-60h130q25-61 69.888-84 44.888-23 83-23T546-264q45 23 70 84h130v-235h45q20 0 33-13t13-33q0-20-13-33t-33-13h-45v-239H511v-48q0-20-13-33t-33-13q-20 0-33 13t-13 33v48H180v130q48.15 17.817 77.575 59.686Q287-514.445 287-462.777 287-412 257.5-370T180-310v130Zm329-330Z"/></svg>
<svg class="item-icon" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M356-120H180q-24 0-42-18t-18-42v-176q44-5 75.5-34.5T227-463q0-43-31.5-72.5T120-570v-176q0-24 18-42t42-18h177q11-40 39.5-67t68.5-27q40 0 68.5 27t39.5 67h173q24 0 42 18t18 42v173q40 11 65.5 41.5T897-461q0 40-25.5 67T806-356v176q0 24-18 42t-42 18H570q-5-48-35.5-77.5T463-227q-41 0-71.5 29.5T356-120Zm-176-60h130q25-61 69.888-84 44.888-23 83-23T546-264q45 23 70 84h130v-235h45q20 0 33-13t13-33q0-20-13-33t-33-13h-45v-239H511v-48q0-20-13-33t-33-13q-20 0-33 13t-13 33v48H180v130q48.15 17.817 77.575 59.686Q287-514.445 287-462.777 287-412 257.5-370T180-310v130Zm329-330Z"/></svg>

Before

Width:  |  Height:  |  Size: 669 B

After

Width:  |  Height:  |  Size: 688 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M273-160 80-353l193-193 42 42-121 121h316v60H194l121 121-42 42Zm414-254-42-42 121-121H450v-60h316L645-758l42-42 193 193-193 193Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#fcba03"><path d="M273-160 80-353l193-193 42 42-121 121h316v60H194l121 121-42 42Zm414-254-42-42 121-121H450v-60h316L645-758l42-42 193 193-193 193Z"/></svg>

Before

Width:  |  Height:  |  Size: 234 B

After

Width:  |  Height:  |  Size: 249 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M700-160v-410H275l153 153-42 43-226-226 226-226 42 42-153 154h485v470h-60Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="#0388fc" height="48" viewBox="0 -960 960 960" width="48"><path d="M700-160v-410H275l153 153-42 43-226-226 226-226 42 42-153 154h485v470h-60Z"/></svg>

Before

Width:  |  Height:  |  Size: 180 B

After

Width:  |  Height:  |  Size: 195 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M197-197q-54-54-85.5-126.5T80-480q0-84 31.5-156.5T197-763l43 43q-46 46-73 107.5T140-480q0 71 26.5 132T240-240l-43 43Zm113-113q-32-32-51-75.5T240-480q0-51 19-94.5t51-75.5l43 43q-24 24-38.5 56.5T300-480q0 38 14 70t39 57l-43 43Zm170-90q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm170 90-43-43q24-24 38.5-56.5T660-480q0-38-14-70t-39-57l43-43q32 32 51 75.5t19 94.5q0 50-19 93.5T650-310Zm113 113-43-43q46-46 73-107.5T820-480q0-71-26.5-132T720-720l43-43q54 55 85.5 127.5T880-480q0 83-31.5 155.5T763-197Z"/></svg>
<svg fill="#83f2c4" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M197-197q-54-54-85.5-126.5T80-480q0-84 31.5-156.5T197-763l43 43q-46 46-73 107.5T140-480q0 71 26.5 132T240-240l-43 43Zm113-113q-32-32-51-75.5T240-480q0-51 19-94.5t51-75.5l43 43q-24 24-38.5 56.5T300-480q0 38 14 70t39 57l-43 43Zm170-90q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm170 90-43-43q24-24 38.5-56.5T660-480q0-38-14-70t-39-57l43-43q32 32 51 75.5t19 94.5q0 50-19 93.5T650-310Zm113 113-43-43q46-46 73-107.5T820-480q0-71-26.5-132T720-720l43-43q54 55 85.5 127.5T880-480q0 83-31.5 155.5T763-197Z"/></svg>

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 667 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M345-377h391L609-548 506-413l-68-87-93 123Zm-85 177q-24 0-42-18t-18-42v-560q0-24 18-42t42-18h560q24 0 42 18t18 42v560q0 24-18 42t-42 18H260Zm0-60h560v-560H260v560ZM140-80q-24 0-42-18t-18-42v-620h60v620h620v60H140Zm120-740v560-560Z"/></svg>
<svg class="item-icon" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M345-377h391L609-548 506-413l-68-87-93 123Zm-85 177q-24 0-42-18t-18-42v-560q0-24 18-42t42-18h560q24 0 42 18t18 42v560q0 24-18 42t-42 18H260Zm0-60h560v-560H260v560ZM140-80q-24 0-42-18t-18-42v-620h60v620h620v60H140Zm120-740v560-560Z"/></svg>

Before

Width:  |  Height:  |  Size: 336 B

After

Width:  |  Height:  |  Size: 355 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M109.912-150Q81-150 60.5-170.589 40-191.177 40-220.089 40-249 60.494-269.5t49.273-20.5q5.233 0 10.233.5 5 .5 13 2.5l200-200q-2-8-2.5-13t-.5-10.233q0-28.779 20.589-49.273Q371.177-580 400.089-580 429-580 449.5-559.366t20.5 49.61Q470-508 467-487l110 110q8-2 13-2.5t10-.5q5 0 10 .5t13 2.5l160-160q-2-8-2.5-13t-.5-10.233q0-28.779 20.589-49.273Q821.177-630 850.089-630 879-630 899.5-609.411q20.5 20.588 20.5 49.5Q920-531 899.506-510.5T850.233-490Q845-490 840-490.5q-5-.5-13-2.5L667-333q2 8 2.5 13t.5 10.233q0 28.779-20.589 49.273Q628.823-240 599.911-240 571-240 550.5-260.494T530-309.767q0-5.233.5-10.233.5-5 2.5-13L423-443q-8 2-13 2.5t-10.25.5q-1.75 0-22.75-3L177-243q2 8 2.5 13t.5 10.233q0 28.779-20.589 49.273Q138.823-150 109.912-150ZM160-592l-20.253-43.747L96-656l43.747-20.253L160-720l20.253 43.747L224-656l-43.747 20.253L160-592Zm440-51-30.717-66.283L503-740l66.283-30.717L600-837l30.717 66.283L697-740l-66.283 30.717L600-643Z"/></svg>
<svg fill="#edf230" xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M109.912-150Q81-150 60.5-170.589 40-191.177 40-220.089 40-249 60.494-269.5t49.273-20.5q5.233 0 10.233.5 5 .5 13 2.5l200-200q-2-8-2.5-13t-.5-10.233q0-28.779 20.589-49.273Q371.177-580 400.089-580 429-580 449.5-559.366t20.5 49.61Q470-508 467-487l110 110q8-2 13-2.5t10-.5q5 0 10 .5t13 2.5l160-160q-2-8-2.5-13t-.5-10.233q0-28.779 20.589-49.273Q821.177-630 850.089-630 879-630 899.5-609.411q20.5 20.588 20.5 49.5Q920-531 899.506-510.5T850.233-490Q845-490 840-490.5q-5-.5-13-2.5L667-333q2 8 2.5 13t.5 10.233q0 28.779-20.589 49.273Q628.823-240 599.911-240 571-240 550.5-260.494T530-309.767q0-5.233.5-10.233.5-5 2.5-13L423-443q-8 2-13 2.5t-10.25.5q-1.75 0-22.75-3L177-243q2 8 2.5 13t.5 10.233q0 28.779-20.589 49.273Q138.823-150 109.912-150ZM160-592l-20.253-43.747L96-656l43.747-20.253L160-720l20.253 43.747L224-656l-43.747 20.253L160-592Zm440-51-30.717-66.283L503-740l66.283-30.717L600-837l30.717 66.283L697-740l-66.283 30.717L600-643Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
docs/img/screenshots/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

BIN
docs/img/screenshots/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

BIN
docs/img/screenshots/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

BIN
docs/img/screenshots/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/img/screenshots/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

BIN
docs/img/screenshots/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

BIN
docs/img/screenshots/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/img/screenshots/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

BIN
docs/img/screenshots/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

BIN
docs/img/screenshots/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

View File

@ -8,7 +8,7 @@
<meta name="author" content="tobychui">
<!-- HTML Meta Tags -->
<title>Cluster Proxy Gateway | Zoraxy</title>
<title>Reverse Proxy Server | Zoraxy</title>
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
<!-- Facebook Meta Tags -->
@ -74,21 +74,16 @@
</div>
<div class="right-content">
<!-- Hero Banner Section -->
<div class="dot-container">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<div class="headbanner"></div>
<div id="home" class="herotext">
<div class="ui basic segment">
<div class="bannerHeaderWrapper">
<h1 class="bannerHeader">Zoraxy</h1>
<div class="ui divider"></div><br>
<p class="bannerSubheader">All in one homelab network routing solution</p>
</div>
<br><br>
<a class="ui black big button" href="#features">Learn More</a>
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
<br><br>
<table class="ui very basic collapsing unstackable celled table">
<thead>
@ -126,6 +121,22 @@
</tr>
</table>
</div>
<div id="wavesWrapper">
<!-- CSS waves-->
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
</g>
</svg>
</div>
</div>
<!-- Features -->
@ -240,34 +251,34 @@
<div class="ui three column stackable grid">
<div class="column">
<a href="img/screenshots/1.webp" target="_blank"><img src="img/screenshots/1.webp" class="ui fluid image screenshot"></a>
<a href="img/screenshots/1.png" target="_blank"><img src="img/screenshots/1.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/2.webp" target="_blank"><img src="img/screenshots/2.webp" class="ui fluid image screenshot"></a>
<a href="img/screenshots/2.png" target="_blank"><img src="img/screenshots/2.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/3.webp" target="_blank"><img src="img/screenshots/3.webp" class="ui fluid image screenshot"></a>
<a href="img/screenshots/3.png" target="_blank"><img src="img/screenshots/3.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/4.webp" target="_blank"><img src="img/screenshots/4.webp" class="ui fluid image screenshot"></a>
<a href="img/screenshots/4.png" target="_blank"><img src="img/screenshots/4.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/5.webp" target="_blank"><img src="img/screenshots/5.webp" class="ui fluid image screenshot"></a>
<a href="img/screenshots/5.png" target="_blank"><img src="img/screenshots/5.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/6.webp" target="_blank"><img src="img/screenshots/6.webp" class="ui fluid image screenshot"></a>
<a href="img/screenshots/6.png" target="_blank"><img src="img/screenshots/6.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/7.webp" target="_blank"><img src="img/screenshots/7.webp" class="ui fluid image screenshot"></a>
<a href="img/screenshots/7.png" target="_blank"><img src="img/screenshots/7.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/8.webp" target="_blank"><img src="img/screenshots/8.webp" class="ui fluid image screenshot"></a>
<a href="img/screenshots/8.png" target="_blank"><img src="img/screenshots/8.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/9.webp" target="_blank"><img src="img/screenshots/9.webp" class="ui fluid image screenshot"></a>
<a href="img/screenshots/9.png" target="_blank"><img src="img/screenshots/9.png" class="ui fluid image screenshot"></a>
</div>
<div class="column">
<a href="img/screenshots/10.webp" target="_blank"><img src="img/screenshots/10.webp" class="ui fluid image screenshot"></a>
<a href="img/screenshots/10.png" target="_blank"><img src="img/screenshots/10.png" class="ui fluid image screenshot"></a>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
body{
background: #f6f6f6 !important;
background: #ffffff !important;
margin: 0;
padding: 0;
overflow-y: hidden;
@ -18,7 +18,7 @@ body{
.left-menu {
width: 80px;
min-width: 80px;
background-color: #ffffff;
background-color: #fcfcfc;
min-height: 100vh;
padding-top: 1.5em;
}
@ -48,17 +48,19 @@ body{
text-align: center;
border-bottom: 1px solid #f6f6f6;
width: 100%;
border-right: 0.4em solid var(--themeTextColor);
transition: border-left ease-in-out 0.1s, background-color ease-in-out 0.1s;
}
.menu-item.active{
border-right: 0.4em solid var(--themeSkyblueColorDecondary);
background-color: #f0f8ff;
background: linear-gradient(60deg, rgba(84, 58, 183, 0.3) 0%, rgba(0, 172, 193, 0.3) 100%);
}
.menu-item .item-icon{
fill: #fcfcfc;
}
.menu-item:hover{
border-right: 0.4em solid var(--themeSkyblueColorDecondary);
background: rgba(35,35,35,0.1);
}
.menu-item img{
@ -69,18 +71,6 @@ body{
/* Head banner */
.headbanner{
background-image: url('img/bg.png');
background-repeat: no-repeat;
background-position: right center;
background-size: auto 100%;
position:absolute;
right: 0;
top: 0;
height: 100vh;
width: 100%;
z-index: -100;
}
.herotext{
padding-top: 15em;
@ -91,11 +81,13 @@ body{
.bannerHeader{
font-size: 8em;
font-weight: 600;
color: white;
}
.bannerSubheader{
font-weight: 400;
font-size: 1.2em;
color: #ebebeb;
margin-top: -20px;
}
@ -104,6 +96,21 @@ body{
display: inline-block;
}
#home{
background: linear-gradient(60deg, rgba(84,58,183,1) 0%, rgba(0,172,193,1) 100%);
}
#home .table th, #home .table h4{
color: white;
}
#home .table h4 .content, #home .table h4 .sub.header{
color: white;
}
#home .table td a{
color: #d6ddff;
}
/* features */
#features{
padding-top: 4em;
@ -173,56 +180,58 @@ body{
}
}
/* Decorative Animation */
.dot-container {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
position: absolute;
top: 2em;
left: 2em;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #d9d9d9;
margin-right: 6px;
animation-name: dot-animation;
animation-duration: 4s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
/*
Waves CSS
*/
#wavesWrapper{
position: absolute;
bottom: 0;
width: 100%;
left: 0;
}
.dot:nth-child(1) {
animation-delay: 0s;
.waves {
position:relative;
width: 100%;
height:15vh;
margin-bottom:-7px; /*Fix for safari gap*/
min-height:100px;
max-height:150px;
}
.dot:nth-child(2) {
animation-delay: 1s;
}
.dot:nth-child(3) {
animation-delay: 2s;
.parallax > use {
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
}
.dot:nth-child(4) {
animation-delay: 3s;
.parallax > use:nth-child(1) {
animation-delay: -8s;
animation-duration: 28s;
}
@keyframes dot-animation {
0% {
background-color: #d9d9d9;
transform: scale(1);
}
50% {
background-color: #a9d1f3;
transform: scale(1.5);
}
100% {
background-color: #d9d9d9;
transform: scale(1);
}
.parallax > use:nth-child(2) {
animation-delay: -12s;
animation-duration: 40s;
}
.parallax > use:nth-child(3) {
animation-delay: -16s;
animation-duration: 52s;
}
.parallax > use:nth-child(4) {
animation-delay: -20s;
animation-duration: 80s;
}
@keyframes move-forever {
0% {
transform: translate3d(-90px,0,0);
}
100% {
transform: translate3d(85px,0,0);
}
}
/*Shrinking for mobile*/
@media (max-width: 768px) {
.waves {
height:40px;
min-height:40px;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

View File

@ -3,9 +3,12 @@ package main
import (
"encoding/json"
"net/http"
"strings"
strip "github.com/grokify/html-strip-tags-go"
"imuslab.com/zoraxy/mod/geodb"
"github.com/google/uuid"
"github.com/microcosm-cc/bluemonday"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/utils"
)
@ -17,6 +20,157 @@ import (
banning / whitelist a specific IP address or country code
*/
/*
General Function
*/
func handleListAccessRules(w http.ResponseWriter, r *http.Request) {
allAccessRules := accessController.ListAllAccessRules()
js, _ := json.Marshal(allAccessRules)
utils.SendJSONResponse(w, string(js))
}
func handleAttachRuleToHost(w http.ResponseWriter, r *http.Request) {
ruleid, err := utils.PostPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
host, err := utils.PostPara(r, "host")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
//Check if access rule and proxy rule exists
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(host)
if err != nil {
utils.SendErrorResponse(w, "invalid host given")
return
}
if !accessController.AccessRuleExists(ruleid) {
utils.SendErrorResponse(w, "access rule not exists")
return
}
//Update the proxy host acess rule id
targetProxyEndpoint.AccessFilterUUID = ruleid
targetProxyEndpoint.UpdateToRuntime()
err = SaveReverseProxyConfig(targetProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Create a new access rule, require name and desc only
func handleCreateAccessRule(w http.ResponseWriter, r *http.Request) {
ruleName, err := utils.PostPara(r, "name")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
ruleDesc, _ := utils.PostPara(r, "desc")
//Filter out injection if any
p := bluemonday.StripTagsPolicy()
ruleName = p.Sanitize(ruleName)
ruleDesc = p.Sanitize(ruleDesc)
ruleUUID := uuid.New().String()
newAccessRule := access.AccessRule{
ID: ruleUUID,
Name: ruleName,
Desc: ruleDesc,
BlacklistEnabled: false,
WhitelistEnabled: false,
}
//Add it to runtime
err = accessController.AddNewAccessRule(&newAccessRule)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle removing an access rule. All proxy endpoint using this rule will be
// set to use the default rule
func handleRemoveAccessRule(w http.ResponseWriter, r *http.Request) {
ruleID, err := utils.PostPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "invalid rule id given")
return
}
if ruleID == "default" {
utils.SendErrorResponse(w, "default access rule cannot be removed")
return
}
ruleID = strings.TrimSpace(ruleID)
//Set all proxy hosts that use this access rule back to using "default"
allProxyEndpoints := dynamicProxyRouter.GetProxyEndpointsAsMap()
for _, proxyEndpoint := range allProxyEndpoints {
if strings.EqualFold(proxyEndpoint.AccessFilterUUID, ruleID) {
//This proxy endpoint is using the current access filter.
//set it to default
proxyEndpoint.AccessFilterUUID = "default"
proxyEndpoint.UpdateToRuntime()
err = SaveReverseProxyConfig(proxyEndpoint)
if err != nil {
SystemWideLogger.PrintAndLog("Access", "Unable to save updated proxy endpoint "+proxyEndpoint.RootOrMatchingDomain, err)
} else {
SystemWideLogger.PrintAndLog("Access", "Updated "+proxyEndpoint.RootOrMatchingDomain+" access filter to \"default\"", nil)
}
}
}
//Remove the access rule by ID
err = accessController.RemoveAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
SystemWideLogger.PrintAndLog("Access", "Access Rule "+ruleID+" removed", nil)
utils.SendOK(w)
}
// Only the name and desc, for other properties use blacklist / whitelist api
func handleUpadateAccessRule(w http.ResponseWriter, r *http.Request) {
ruleID, err := utils.PostPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "invalid rule id")
return
}
ruleName, err := utils.PostPara(r, "name")
if err != nil {
utils.SendErrorResponse(w, "invalid rule name")
return
}
ruleDesc, _ := utils.PostPara(r, "desc")
//Filter anything weird
p := bluemonday.StrictPolicy()
ruleName = p.Sanitize(ruleName)
ruleDesc = p.Sanitize(ruleDesc)
err = accessController.UpdateAccessRule(ruleID, ruleName, ruleDesc)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
/*
Blacklist Related
*/
@ -28,11 +182,24 @@ func handleListBlacklisted(w http.ResponseWriter, r *http.Request) {
bltype = "country"
}
ruleID, err := utils.GetPara(r, "id")
if err != nil {
//Use default if not set
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
resulst := []string{}
if bltype == "country" {
resulst = geodbStore.GetAllBlacklistedCountryCode()
resulst = rule.GetAllBlacklistedCountryCode()
} else if bltype == "ip" {
resulst = geodbStore.GetAllBlacklistedIp()
resulst = rule.GetAllBlacklistedIp()
}
js, _ := json.Marshal(resulst)
@ -47,7 +214,23 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
return
}
geodbStore.AddCountryCodeToBlackList(countryCode)
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
comment, _ := utils.PostPara(r, "comment")
p := bluemonday.StripTagsPolicy()
comment = p.Sanitize(comment)
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.AddCountryCodeToBlackList(countryCode, comment)
utils.SendOK(w)
}
@ -59,7 +242,19 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
return
}
geodbStore.RemoveCountryCodeFromBlackList(countryCode)
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveCountryCodeFromBlackList(countryCode)
utils.SendOK(w)
}
@ -71,7 +266,24 @@ func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) {
return
}
geodbStore.AddIPToBlackList(ipAddr)
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
comment, _ := utils.GetPara(r, "comment")
p := bluemonday.StripTagsPolicy()
comment = p.Sanitize(comment)
rule.AddIPToBlackList(ipAddr, comment)
utils.SendOK(w)
}
func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
@ -81,23 +293,46 @@ func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
return
}
geodbStore.RemoveIPFromBlackList(ipAddr)
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
//Load the target rule from access controller
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveIPFromBlackList(ipAddr)
utils.SendOK(w)
}
func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) {
enable, err := utils.PostPara(r, "enable")
enable, _ := utils.PostPara(r, "enable")
ruleID, err := utils.PostPara(r, "id")
if err != nil {
//Return the current enabled state
currentEnabled := geodbStore.BlacklistEnabled
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if enable == "" {
//enable paramter not set
currentEnabled := rule.BlacklistEnabled
js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js))
} else {
if enable == "true" {
geodbStore.ToggleBlacklist(true)
rule.ToggleBlacklist(true)
} else if enable == "false" {
geodbStore.ToggleBlacklist(false)
rule.ToggleBlacklist(false)
} else {
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
return
@ -117,11 +352,22 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
bltype = "country"
}
resulst := []*geodb.WhitelistEntry{}
ruleID, err := utils.GetPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
resulst := []*access.WhitelistEntry{}
if bltype == "country" {
resulst = geodbStore.GetAllWhitelistedCountryCode()
resulst = rule.GetAllWhitelistedCountryCode()
} else if bltype == "ip" {
resulst = geodbStore.GetAllWhitelistedIp()
resulst = rule.GetAllWhitelistedIp()
}
js, _ := json.Marshal(resulst)
@ -136,10 +382,22 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return
}
comment, _ := utils.PostPara(r, "comment")
comment = strip.StripTags(comment)
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
geodbStore.AddCountryCodeToWhitelist(countryCode, comment)
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
comment, _ := utils.PostPara(r, "comment")
p := bluemonday.StrictPolicy()
comment = p.Sanitize(comment)
rule.AddCountryCodeToWhitelist(countryCode, comment)
utils.SendOK(w)
}
@ -151,7 +409,18 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
return
}
geodbStore.RemoveCountryCodeFromWhitelist(countryCode)
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveCountryCodeFromWhitelist(countryCode)
utils.SendOK(w)
}
@ -163,10 +432,23 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return
}
comment, _ := utils.PostPara(r, "comment")
comment = strip.StripTags(comment)
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
geodbStore.AddIPToWhiteList(ipAddr, comment)
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
comment, _ := utils.PostPara(r, "comment")
p := bluemonday.StrictPolicy()
comment = p.Sanitize(comment)
rule.AddIPToWhiteList(ipAddr, comment)
utils.SendOK(w)
}
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
@ -176,23 +458,45 @@ func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
return
}
geodbStore.RemoveIPFromWhiteList(ipAddr)
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
rule.RemoveIPFromWhiteList(ipAddr)
utils.SendOK(w)
}
func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
enable, err := utils.PostPara(r, "enable")
enable, _ := utils.PostPara(r, "enable")
ruleID, err := utils.PostPara(r, "id")
if err != nil {
ruleID = "default"
}
rule, err := accessController.GetAccessRuleByID(ruleID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if enable == "" {
//Return the current enabled state
currentEnabled := geodbStore.WhitelistEnabled
currentEnabled := rule.WhitelistEnabled
js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js))
} else {
if enable == "true" {
geodbStore.ToggleWhitelist(true)
rule.ToggleWhitelist(true)
} else if enable == "false" {
geodbStore.ToggleWhitelist(false)
rule.ToggleWhitelist(false)
} else {
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
return

View File

@ -38,7 +38,7 @@ func initACME() *acme.ACMEHandler {
port = getRandomPort(30000)
}
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb)
}
// create the special routing rule for ACME

View File

@ -47,8 +47,11 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet)
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
@ -84,7 +87,14 @@ func initAPIs() {
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
//Access Rules API
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
//Blacklist APIs
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
@ -92,7 +102,6 @@ func initAPIs() {
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
//Whitelist APIs
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
@ -163,6 +172,8 @@ func initAPIs() {
authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet)
authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet)
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle)
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
//Account Reset
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
@ -175,6 +186,7 @@ func initAPIs() {
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/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)

View File

@ -1,20 +1,34 @@
module imuslab.com/zoraxy
go 1.16
go 1.21
toolchain go1.22.2
require (
github.com/boltdb/bolt v1.3.1
github.com/go-acme/lego/v4 v4.14.0
github.com/go-acme/lego/v4 v4.16.1
github.com/go-ping/ping v1.1.0
github.com/google/uuid v1.3.1
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.5.0
github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.2
github.com/gorilla/websocket v1.5.1
github.com/grandcat/zeroconf v1.0.0
github.com/grokify/html-strip-tags-go v0.1.0
github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.25
golang.org/x/net v0.14.0
golang.org/x/sys v0.11.0
golang.org/x/text v0.12.0
golang.org/x/tools v0.12.0 // indirect
github.com/microcosm-cc/bluemonday v1.0.26
golang.org/x/net v0.23.0
golang.org/x/sys v0.18.0
golang.org/x/text v0.14.0
)
require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/miekg/dns v1.1.58 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/tools v0.19.0 // indirect
)

1807
src/go.sum

File diff suppressed because it is too large Load Diff

View File

@ -12,11 +12,13 @@ import (
"time"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/database"
"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"
@ -49,7 +51,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
var (
name = "Zoraxy"
version = "3.0.0"
version = "3.0.2"
nodeUUID = "generic"
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()
@ -68,7 +70,8 @@ var (
tlsCertManager *tlscert.Manager //TLS / SSL management
redirectTable *redirection.RuleTable //Handle special redirection rule sets
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, also handle black list and whitelist features
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
@ -79,6 +82,7 @@ var (
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
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending

217
src/mod/access/access.go Normal file
View File

@ -0,0 +1,217 @@
package access
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"sync"
"imuslab.com/zoraxy/mod/utils"
)
/*
Access.go
This module is the new version of access control system
where now the blacklist / whitelist are seperated from
geodb module
*/
// Create a new access controller to handle blacklist / whitelist
func NewAccessController(options *Options) (*Controller, error) {
sysdb := options.Database
if sysdb == nil {
return nil, errors.New("missing database access")
}
//Create the config folder if not exists
confFolder := options.ConfigFolder
if !utils.FileExists(confFolder) {
err := os.MkdirAll(confFolder, 0775)
if err != nil {
return nil, err
}
}
// Create the global access rule if not exists
var defaultAccessRule = AccessRule{
ID: "default",
Name: "Default",
Desc: "Default access rule for all HTTP proxy hosts",
BlacklistEnabled: false,
WhitelistEnabled: false,
WhiteListCountryCode: &map[string]string{},
WhiteListIP: &map[string]string{},
BlackListContryCode: &map[string]string{},
BlackListIP: &map[string]string{},
}
defaultRuleSettingFile := filepath.Join(confFolder, "default.json")
if utils.FileExists(defaultRuleSettingFile) {
//Load from file
defaultRuleBytes, err := os.ReadFile(defaultRuleSettingFile)
if err == nil {
err = json.Unmarshal(defaultRuleBytes, &defaultAccessRule)
if err != nil {
options.Logger.PrintAndLog("Access", "Unable to parse default routing rule config file. Using default", err)
}
}
} else {
//Create one
js, _ := json.MarshalIndent(defaultAccessRule, "", " ")
os.WriteFile(defaultRuleSettingFile, js, 0775)
}
//Generate a controller object
thisController := Controller{
DefaultAccessRule: &defaultAccessRule,
ProxyAccessRule: &sync.Map{},
Options: options,
}
//Load all acccess rules from file
configFiles, err := filepath.Glob(options.ConfigFolder + "/*.json")
if err != nil {
return nil, err
}
ProxyAccessRules := sync.Map{}
for _, configFile := range configFiles {
if filepath.Base(configFile) == "default.json" {
//Skip this, as this was already loaded as default
continue
}
configContent, err := os.ReadFile(configFile)
if err != nil {
options.Logger.PrintAndLog("Access", "Unable to load config "+filepath.Base(configFile), err)
continue
}
//Parse the config file into AccessRule
thisAccessRule := AccessRule{}
err = json.Unmarshal(configContent, &thisAccessRule)
if err != nil {
options.Logger.PrintAndLog("Access", "Unable to parse config "+filepath.Base(configFile), err)
continue
}
thisAccessRule.parent = &thisController
ProxyAccessRules.Store(thisAccessRule.ID, &thisAccessRule)
}
thisController.ProxyAccessRule = &ProxyAccessRules
return &thisController, nil
}
// Get the global access rule
func (c *Controller) GetGlobalAccessRule() (*AccessRule, error) {
if c.DefaultAccessRule == nil {
return nil, errors.New("global access rule is not set")
}
return c.DefaultAccessRule, nil
}
// Load access rules to runtime, require rule ID
func (c *Controller) GetAccessRuleByID(accessRuleID string) (*AccessRule, error) {
if accessRuleID == "default" || accessRuleID == "" {
return c.DefaultAccessRule, nil
}
//Load from sync.Map, should be O(1)
targetRule, ok := c.ProxyAccessRule.Load(accessRuleID)
if !ok {
return nil, errors.New("target access rule not exists")
}
ar, ok := targetRule.(*AccessRule)
if !ok {
return nil, errors.New("assertion of access rule failed, version too old?")
}
return ar, nil
}
// Return all the access rules currently in runtime, including default
func (c *Controller) ListAllAccessRules() []*AccessRule {
results := []*AccessRule{c.DefaultAccessRule}
c.ProxyAccessRule.Range(func(key, value interface{}) bool {
results = append(results, value.(*AccessRule))
return true
})
return results
}
// Check if an access rule exists given the rule id
func (c *Controller) AccessRuleExists(ruleID string) bool {
r, _ := c.GetAccessRuleByID(ruleID)
if r != nil {
//An access rule with identical ID exists
return true
}
return false
}
// Add a new access rule to runtime and save it to file
func (c *Controller) AddNewAccessRule(newRule *AccessRule) error {
r, _ := c.GetAccessRuleByID(newRule.ID)
if r != nil {
//An access rule with identical ID exists
return errors.New("access rule already exists")
}
//Check if the blacklist and whitelist are populated with empty map
if newRule.BlackListContryCode == nil {
newRule.BlackListContryCode = &map[string]string{}
}
if newRule.BlackListIP == nil {
newRule.BlackListIP = &map[string]string{}
}
if newRule.WhiteListCountryCode == nil {
newRule.WhiteListCountryCode = &map[string]string{}
}
if newRule.WhiteListIP == nil {
newRule.WhiteListIP = &map[string]string{}
}
//Add access rule to runtime
newRule.parent = c
c.ProxyAccessRule.Store(newRule.ID, newRule)
//Save rule to file
newRule.SaveChanges()
return nil
}
// Update the access rule meta info.
func (c *Controller) UpdateAccessRule(ruleID string, name string, desc string) error {
targetAccessRule, err := c.GetAccessRuleByID(ruleID)
if err != nil {
return err
}
///Update the name and desc
targetAccessRule.Name = name
targetAccessRule.Desc = desc
//Overwrite the rule currently in sync map
if ruleID == "default" {
c.DefaultAccessRule = targetAccessRule
} else {
c.ProxyAccessRule.Store(ruleID, targetAccessRule)
}
return targetAccessRule.SaveChanges()
}
// Remove the access rule by its id
func (c *Controller) RemoveAccessRuleByID(ruleID string) error {
if !c.AccessRuleExists(ruleID) {
return errors.New("access rule not exists")
}
//Default cannot be removed
if ruleID == "default" {
return errors.New("default access rule cannot be removed")
}
//Remove it
return c.DeleteAccessRuleByID(ruleID)
}

View File

@ -0,0 +1,153 @@
package access
import (
"encoding/json"
"errors"
"net"
"os"
"path/filepath"
)
// Check both blacklist and whitelist for access for both geoIP and ip / CIDR ranges
func (s *AccessRule) AllowIpAccess(ipaddr string) bool {
if s.IsBlacklisted(ipaddr) {
return false
}
return s.IsWhitelisted(ipaddr)
}
// Check both blacklist and whitelist for access using net.Conn
func (s *AccessRule) AllowConnectionAccess(conn net.Conn) bool {
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
return s.AllowIpAccess(addr.IP.String())
}
return true
}
// Toggle black list
func (s *AccessRule) ToggleBlacklist(enabled bool) {
s.BlacklistEnabled = enabled
s.SaveChanges()
}
// Toggel white list
func (s *AccessRule) ToggleWhitelist(enabled bool) {
s.WhitelistEnabled = enabled
s.SaveChanges()
}
/*
Check if a IP address is blacklisted, in either country or IP blacklist
IsBlacklisted default return is false (allow access)
*/
func (s *AccessRule) IsBlacklisted(ipAddr string) bool {
if !s.BlacklistEnabled {
//Blacklist not enabled. Always return false
return false
}
if ipAddr == "" {
//Unable to get the target IP address
return false
}
countryCode, err := s.parent.Options.GeoDB.ResolveCountryCodeFromIP(ipAddr)
if err != nil {
return false
}
if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) {
return true
}
if s.IsIPBlacklisted(ipAddr) {
return true
}
return false
}
/*
IsWhitelisted check if a given IP address is in the current
server's white list.
Note that the Whitelist default result is true even
when encountered error
*/
func (s *AccessRule) IsWhitelisted(ipAddr string) bool {
if !s.WhitelistEnabled {
//Whitelist not enabled. Always return true (allow access)
return true
}
if ipAddr == "" {
//Unable to get the target IP address, assume ok
return true
}
countryCode, err := s.parent.Options.GeoDB.ResolveCountryCodeFromIP(ipAddr)
if err != nil {
return true
}
if s.IsCountryCodeWhitelisted(countryCode.CountryIsoCode) {
return true
}
if s.IsIPWhitelisted(ipAddr) {
return true
}
return false
}
/* Utilities function */
// Update the current access rule to json file
func (s *AccessRule) SaveChanges() error {
if s.parent == nil {
return errors.New("save failed: access rule detached from controller")
}
saveTarget := filepath.Join(s.parent.Options.ConfigFolder, s.ID+".json")
js, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
err = os.WriteFile(saveTarget, js, 0775)
return err
}
// Delete this access rule, this will only delete the config file.
// for runtime delete, use DeleteAccessRuleByID from parent Controller
func (s *AccessRule) DeleteConfigFile() error {
saveTarget := filepath.Join(s.parent.Options.ConfigFolder, s.ID+".json")
return os.Remove(saveTarget)
}
// Delete the access rule by given ID
func (c *Controller) DeleteAccessRuleByID(accessRuleID string) error {
targetAccessRule, err := c.GetAccessRuleByID(accessRuleID)
if err != nil {
return err
}
//Delete config file associated with this access rule
err = targetAccessRule.DeleteConfigFile()
if err != nil {
return err
}
//Delete the access rule in runtime
c.ProxyAccessRule.Delete(accessRuleID)
return nil
}
// Create a deep copy object of the access rule list
func deepCopy(valueList map[string]string) map[string]string {
result := map[string]string{}
js, _ := json.Marshal(valueList)
json.Unmarshal(js, &result)
return result
}

View File

@ -0,0 +1,75 @@
package access
import (
"strings"
)
/*
Blacklist.go
This script store the blacklist related functions
*/
// Geo Blacklist
func (s *AccessRule) AddCountryCodeToBlackList(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode)
newBlacklistCountryCode := deepCopy(*s.BlackListContryCode)
newBlacklistCountryCode[countryCode] = comment
s.BlackListContryCode = &newBlacklistCountryCode
s.SaveChanges()
}
func (s *AccessRule) RemoveCountryCodeFromBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
newBlacklistCountryCode := deepCopy(*s.BlackListContryCode)
delete(newBlacklistCountryCode, countryCode)
s.BlackListContryCode = &newBlacklistCountryCode
s.SaveChanges()
}
func (s *AccessRule) IsCountryCodeBlacklisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
blacklistMap := *s.BlackListContryCode
_, ok := blacklistMap[countryCode]
return ok
}
func (s *AccessRule) GetAllBlacklistedCountryCode() []string {
bannedCountryCodes := []string{}
blacklistMap := *s.BlackListContryCode
for cc, _ := range blacklistMap {
bannedCountryCodes = append(bannedCountryCodes, cc)
}
return bannedCountryCodes
}
// IP Blacklsits
func (s *AccessRule) AddIPToBlackList(ipAddr string, comment string) {
newBlackListIP := deepCopy(*s.BlackListIP)
newBlackListIP[ipAddr] = comment
s.BlackListIP = &newBlackListIP
s.SaveChanges()
}
func (s *AccessRule) RemoveIPFromBlackList(ipAddr string) {
newBlackListIP := deepCopy(*s.BlackListIP)
delete(newBlackListIP, ipAddr)
s.BlackListIP = &newBlackListIP
s.SaveChanges()
}
func (s *AccessRule) GetAllBlacklistedIp() []string {
bannedIps := []string{}
blacklistMap := *s.BlackListIP
for ip, _ := range blacklistMap {
bannedIps = append(bannedIps, ip)
}
return bannedIps
}
func (s *AccessRule) IsIPBlacklisted(ipAddr string) bool {
IPBlacklist := *s.BlackListIP
_, ok := IPBlacklist[ipAddr]
return ok
}

38
src/mod/access/typedef.go Normal file
View File

@ -0,0 +1,38 @@
package access
import (
"sync"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
)
type Options struct {
Logger logger.Logger
ConfigFolder string //Path for storing config files
GeoDB *geodb.Store //For resolving country code
Database *database.Database //System key-value database
}
type AccessRule struct {
ID string
Name string
Desc string
BlacklistEnabled bool
WhitelistEnabled bool
/* Whitelist Blacklist Table, value is comment if supported */
WhiteListCountryCode *map[string]string
WhiteListIP *map[string]string
BlackListContryCode *map[string]string
BlackListIP *map[string]string
parent *Controller
}
type Controller struct {
DefaultAccessRule *AccessRule
ProxyAccessRule *sync.Map
Options *Options
}

112
src/mod/access/whitelist.go Normal file
View File

@ -0,0 +1,112 @@
package access
import (
"strings"
"imuslab.com/zoraxy/mod/netutils"
)
/*
Whitelist.go
This script handles whitelist related functions
*/
const (
EntryType_CountryCode int = 0
EntryType_IP int = 1
)
type WhitelistEntry struct {
EntryType int //Entry type of whitelist, Country Code or IP
CC string //ISO Country Code
IP string //IP address or range
Comment string //Comment for this entry
}
//Geo Whitelist
func (s *AccessRule) AddCountryCodeToWhitelist(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode)
newWhitelistCC := deepCopy(*s.WhiteListCountryCode)
newWhitelistCC[countryCode] = comment
s.WhiteListCountryCode = &newWhitelistCC
s.SaveChanges()
}
func (s *AccessRule) RemoveCountryCodeFromWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
newWhitelistCC := deepCopy(*s.WhiteListCountryCode)
delete(newWhitelistCC, countryCode)
s.WhiteListCountryCode = &newWhitelistCC
s.SaveChanges()
}
func (s *AccessRule) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
whitelistCC := *s.WhiteListCountryCode
_, ok := whitelistCC[countryCode]
return ok
}
func (s *AccessRule) GetAllWhitelistedCountryCode() []*WhitelistEntry {
whitelistedCountryCode := []*WhitelistEntry{}
whitelistCC := *s.WhiteListCountryCode
for cc, comment := range whitelistCC {
whitelistedCountryCode = append(whitelistedCountryCode, &WhitelistEntry{
EntryType: EntryType_CountryCode,
CC: cc,
Comment: comment,
})
}
return whitelistedCountryCode
}
//IP Whitelist
func (s *AccessRule) AddIPToWhiteList(ipAddr string, comment string) {
newWhitelistIP := deepCopy(*s.WhiteListIP)
newWhitelistIP[ipAddr] = comment
s.WhiteListIP = &newWhitelistIP
s.SaveChanges()
}
func (s *AccessRule) RemoveIPFromWhiteList(ipAddr string) {
newWhitelistIP := deepCopy(*s.WhiteListIP)
delete(newWhitelistIP, ipAddr)
s.WhiteListIP = &newWhitelistIP
s.SaveChanges()
}
func (s *AccessRule) IsIPWhitelisted(ipAddr string) bool {
//Check for IP wildcard and CIRD rules
WhitelistedIP := *s.WhiteListIP
for ipOrCIDR, _ := range WhitelistedIP {
wildcardMatch := netutils.MatchIpWildcard(ipAddr, ipOrCIDR)
if wildcardMatch {
return true
}
cidrMatch := netutils.MatchIpCIDR(ipAddr, ipOrCIDR)
if cidrMatch {
return true
}
}
return false
}
func (s *AccessRule) GetAllWhitelistedIp() []*WhitelistEntry {
whitelistedIp := []*WhitelistEntry{}
currentWhitelistedIP := *s.WhiteListIP
for ipOrCIDR, comment := range currentWhitelistedIP {
thisEntry := WhitelistEntry{
EntryType: EntryType_IP,
IP: ipOrCIDR,
Comment: comment,
}
whitelistedIp = append(whitelistedIp, &thisEntry)
}
return whitelistedIp
}

View File

@ -9,6 +9,7 @@ import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"log"
"net"
@ -24,6 +25,7 @@ import (
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/utils"
)
@ -40,6 +42,11 @@ type ACMEUser struct {
key crypto.PrivateKey
}
type EABConfig struct {
Kid string `json:"kid"`
HmacKey string `json:"HmacKey"`
}
// GetEmail returns the email of the ACMEUser.
func (u *ACMEUser) GetEmail() string {
return u.Email
@ -59,13 +66,15 @@ func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
type ACMEHandler struct {
DefaultAcmeServer string
Port string
Database *database.Database
}
// NewACME creates a new ACMEHandler instance.
func NewACME(acmeServer string, port string) *ACMEHandler {
func NewACME(acmeServer string, port string, database *database.Database) *ACMEHandler {
return &ACMEHandler{
DefaultAcmeServer: acmeServer,
Port: port,
Database: database,
}
}
@ -143,10 +152,63 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
}
// New users will need to register
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Println(err)
return false, err
/*
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Println(err)
return false, err
}
*/
var reg *registration.Resource
// New users will need to register
if client.GetExternalAccountRequired() {
log.Println("External Account Required for this ACME Provider.")
// IF KID and HmacEncoded is overidden
if !a.Database.TableExists("acme") {
a.Database.NewTable("acme")
return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -1)")
}
if !a.Database.KeyExists("acme", config.CADirURL+"_kid") || !a.Database.KeyExists("acme", config.CADirURL+"_hmacEncoded") {
return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -2)")
}
var kid string
var hmacEncoded string
err := a.Database.Read("acme", config.CADirURL+"_kid", &kid)
if err != nil {
log.Println(err)
return false, err
}
err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded)
if err != nil {
log.Println(err)
return false, err
}
log.Println("EAB Credential retrieved.", kid, hmacEncoded)
if kid != "" && hmacEncoded != "" {
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
Kid: kid,
HmacEncoded: hmacEncoded,
})
}
if err != nil {
log.Println(err)
return false, err
}
//return false, errors.New("External Account Required for this ACME Provider.")
} else {
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Println(err)
return false, err
}
}
adminUser.Registration = reg

View File

@ -373,3 +373,34 @@ func (a *AutoRenewer) saveRenewConfigToFile() error {
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
return os.WriteFile(a.ConfigFilePath, js, 0775)
}
// Handle update auto renew EAD configuration
func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
kid, err := utils.GetPara(r, "kid")
if err != nil {
utils.SendErrorResponse(w, "kid not set")
return
}
hmacEncoded, err := utils.GetPara(r, "hmacEncoded")
if err != nil {
utils.SendErrorResponse(w, "hmacEncoded not set")
return
}
acmeDirectoryURL, err := utils.GetPara(r, "acmeDirectoryURL")
if err != nil {
utils.SendErrorResponse(w, "acmeDirectoryURL not set")
return
}
if !a.AcmeHandler.Database.TableExists("acme") {
a.AcmeHandler.Database.NewTable("acme")
}
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_kid", kid)
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_hmacEncoded", hmacEncoded)
utils.SendOK(w)
}

View File

@ -1,76 +0,0 @@
package aroz
import (
"encoding/json"
"flag"
"fmt"
"net/http"
"net/url"
"os"
)
//To be used with arozos system
type ArozHandler struct {
Port string
restfulEndpoint string
}
//Information required for registering this subservice to arozos
type ServiceInfo struct {
Name string //Name of this module. e.g. "Audio"
Desc string //Description for this module
Group string //Group of the module, e.g. "system" / "media" etc
IconPath string //Module icon image path e.g. "Audio/img/function_icon.png"
Version string //Version of the module. Format: [0-9]*.[0-9][0-9].[0-9]
StartDir string //Default starting dir, e.g. "Audio/index.html"
SupportFW bool //Support floatWindow. If yes, floatWindow dir will be loaded
LaunchFWDir string //This link will be launched instead of 'StartDir' if fw mode
SupportEmb bool //Support embedded mode
LaunchEmb string //This link will be launched instead of StartDir / Fw if a file is opened with this module
InitFWSize []int //Floatwindow init size. [0] => Width, [1] => Height
InitEmbSize []int //Embedded mode init size. [0] => Width, [1] => Height
SupportedExt []string //Supported File Extensions. e.g. ".mp3", ".flac", ".wav"
}
//This function will request the required flag from the startup paramters and parse it to the need of the arozos.
func HandleFlagParse(info ServiceInfo) *ArozHandler {
var infoRequestMode = flag.Bool("info", false, "Show information about this program in JSON")
var port = flag.String("port", ":8000", "Management web interface listening port")
var restful = flag.String("rpt", "", "Reserved")
//Parse the flags
flag.Parse()
if *infoRequestMode {
//Information request mode
jsonString, _ := json.MarshalIndent(info, "", " ")
fmt.Println(string(jsonString))
os.Exit(0)
}
return &ArozHandler{
Port: *port,
restfulEndpoint: *restful,
}
}
//Get the username and resources access token from the request, return username, token
func (a *ArozHandler) GetUserInfoFromRequest(w http.ResponseWriter, r *http.Request) (string, string) {
username := r.Header.Get("aouser")
token := r.Header.Get("aotoken")
return username, token
}
func (a *ArozHandler) IsUsingExternalPermissionManager() bool {
return !(a.restfulEndpoint == "")
}
//Request gateway interface for advance permission sandbox control
func (a *ArozHandler) RequestGatewayInterface(token string, script string) (*http.Response, error) {
resp, err := http.PostForm(a.restfulEndpoint,
url.Values{"token": {token}, "script": {script}})
if err != nil {
// handle error
return nil, err
}
return resp, nil
}

Binary file not shown.

View File

@ -6,8 +6,6 @@ import (
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/geodb"
)
/*
@ -27,20 +25,11 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
Special Routing Rules, bypass most of the limitations
*/
//Check if there are external routing rule matches.
//If yes, route them via external rr
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil {
//Matching routing rule found. Let the sub-router handle it
if matchedRoutingRule.UseSystemAccessControl {
//This matching rule request system access control.
//check access logic
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
}
matchedRoutingRule.Route(w, r)
return
}
@ -48,14 +37,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//Inject headers
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
/*
General Access Check
*/
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
/*
Redirection Routing
*/
@ -66,19 +47,30 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
//Extract request host to see if it is virtual directory or subdomain
/*
Host Routing
*/
//Extract request host to see if any proxy rule is matched
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
/*
Host Routing
*/
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
if sep != nil && !sep.Disabled {
//Matching proxy rule found
//Access Check (blacklist / whitelist)
ruleID := sep.AccessFilterUUID
if sep.AccessFilterUUID == "" {
//Use default rule
ruleID = "default"
}
if h.handleAccessRouting(ruleID, w, r) {
//Request handled by subroute
return
}
//Validate basic auth
if sep.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
@ -137,7 +129,6 @@ Once entered this routing segment, the root routing options will take over
for the routing logic.
*/
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
@ -193,41 +184,14 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
case DefaultSite_NotFoundPage:
http.NotFound(w, r)
}
}
// Handle access routing logic. Return true if the request is handled or blocked by the access control logic
// if the return value is false, you can continue process the response writer
func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Request) bool {
//Check if this ip is in blacklist
clientIpAddr := geodb.GetRequesterIP(r)
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
//Serve the not found page, use template if exists
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/blacklist.html"))
w.WriteHeader(http.StatusNotFound)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
if err != nil {
w.Write(page_forbidden)
w.Write(page_hosterror)
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "blacklist", "")
return true
}
//Check if this ip is in whitelist
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/whitelist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "whitelist", "")
return true
}
return false
}

View File

@ -0,0 +1,64 @@
package dynamicproxy
import (
"log"
"net/http"
"os"
"path/filepath"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/netutils"
)
// Handle access check (blacklist / whitelist), return true if request is handled (aka blocked)
// if the return value is false, you can continue process the response writer
func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter, r *http.Request) bool {
accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID)
if err != nil {
//Unable to load access rule. Target rule not found?
log.Println("[Proxy] Unable to load access rule: " + ruleID)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 - Internal Server Error"))
return true
}
isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r)
if isBlocked {
h.logRequest(r, false, 403, blockedReason, "")
}
return isBlocked
}
// Return boolean, return true if access is blocked
// For string, it will return the blocked reason (if any)
func accessRequestBlocked(accessRule *access.AccessRule, templateDirectory string, w http.ResponseWriter, r *http.Request) (bool, string) {
//Check if this ip is in blacklist
clientIpAddr := netutils.GetRequesterIP(r)
if accessRule.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/blacklist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
return true, "blacklist"
}
//Check if this ip is in whitelist
if !accessRule.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/whitelist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
return true, "whitelist"
}
//Not blocked.
return false, ""
}

View File

@ -16,6 +16,16 @@ import (
*/
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.logRequest(r, false, 401, "host", pe.Domain)
}
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 {
@ -44,7 +54,6 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
}
if !matchingFound {
h.logRequest(r, false, 401, "host", pe.Domain)
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")

View File

@ -1,6 +1,7 @@
package dpcore
import (
"context"
"errors"
"io"
"log"
@ -8,12 +9,9 @@ import (
"net/http"
"net/url"
"strings"
"sync"
"time"
)
var onExitFlushLoop func()
// ReverseProxy is an HTTP Handler that takes an incoming request and
// sends it to another server, proxying the response back to the
// client, support http, also support https tunnel using http.hijacker
@ -68,7 +66,12 @@ type requestCanceler interface {
CancelRequest(req *http.Request)
}
func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerification bool) *ReverseProxy {
type DpcoreOptions struct {
IgnoreTLSVerification bool
FlushInterval time.Duration
}
func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOptions) *ReverseProxy {
targetQuery := target.RawQuery
director := func(req *http.Request) {
req.URL.Scheme = target.Scheme
@ -80,10 +83,6 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", "")
}
}
//Hack the default transporter to handle more connections
@ -95,16 +94,17 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).DisableCompression = true
if ignoreTLSVerification {
if dpcOptions.IgnoreTLSVerification {
//Ignore TLS certificate validation error
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
}
return &ReverseProxy{
Director: director,
Prepender: prepender,
Verbal: false,
Transport: thisTransporter,
Director: director,
Prepender: prepender,
FlushInterval: dpcOptions.FlushInterval,
Verbal: false,
Transport: thisTransporter,
}
}
@ -178,64 +178,66 @@ var hopHeaders = []string{
//"Upgrade",
}
func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
if p.FlushInterval != 0 {
if wf, ok := dst.(writeFlusher); ok {
mlw := &maxLatencyWriter{
dst: wf,
latency: p.FlushInterval,
done: make(chan bool),
}
go mlw.flushLoop()
defer mlw.stop()
dst = mlw
// Copy response from src to dst with given flush interval, reference from httputil.ReverseProxy
func (p *ReverseProxy) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error {
var w io.Writer = dst
if flushInterval != 0 {
mlw := &maxLatencyWriter{
dst: dst,
flush: http.NewResponseController(dst).Flush,
latency: flushInterval,
}
defer mlw.stop()
// set up initial timer so headers get flushed even if body writes are delayed
mlw.flushPending = true
mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
w = mlw
}
io.Copy(dst, src)
var buf []byte
_, err := p.copyBuffer(w, src, buf)
return err
}
type writeFlusher interface {
io.Writer
http.Flusher
}
// Copy with given buffer size. Default to 64k
func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
if len(buf) == 0 {
buf = make([]byte, 64*1024)
}
type maxLatencyWriter struct {
dst writeFlusher
latency time.Duration
mu sync.Mutex
done chan bool
}
func (m *maxLatencyWriter) Write(b []byte) (int, error) {
m.mu.Lock()
defer m.mu.Unlock()
return m.dst.Write(b)
}
func (m *maxLatencyWriter) flushLoop() {
t := time.NewTicker(m.latency)
defer t.Stop()
var written int64
for {
select {
case <-m.done:
if onExitFlushLoop != nil {
onExitFlushLoop()
nr, rerr := src.Read(buf)
if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
p.logf("dpcore read error during body copy: %v", rerr)
}
if nr > 0 {
nw, werr := dst.Write(buf[:nr])
if nw > 0 {
written += int64(nw)
}
return
case <-t.C:
m.mu.Lock()
m.dst.Flush()
m.mu.Unlock()
if werr != nil {
return written, werr
}
if nr != nw {
return written, io.ErrShortWrite
}
}
if rerr != nil {
if rerr == io.EOF {
rerr = nil
}
return written, rerr
}
}
}
func (m *maxLatencyWriter) stop() {
m.done <- true
}
func (p *ReverseProxy) logf(format string, args ...interface{}) {
if p.ErrorLog != nil {
p.ErrorLog.Printf(format, args...)
@ -272,6 +274,14 @@ func removeHeaders(header http.Header, noCache bool) {
header.Del("Cache-Control")
header.Set("Cache-Control", "no-store")
}
//Hide Go-HTTP-Client UA if the client didnt sent us one
if _, ok := header["User-Agent"]; !ok {
// If the outbound request doesn't have a User-Agent header set,
// don't send the default Go HTTP client User-Agent.
header.Set("User-Agent", "")
}
}
func addXForwardedForHeader(req *http.Request) {
@ -290,8 +300,19 @@ func addXForwardedForHeader(req *http.Request) {
}
if req.Header.Get("X-Real-Ip") == "" {
//Not exists. Fill it in with client IP
req.Header.Set("X-Real-Ip", clientIP)
//Check if CF-Connecting-IP header exists
CF_Connecting_IP := req.Header.Get("CF-Connecting-IP")
if CF_Connecting_IP != "" {
//Use CF Connecting IP
req.Header.Set("X-Real-Ip", CF_Connecting_IP)
} else {
// Not exists. Fill it in with first entry in X-Forwarded-For
ips := strings.Split(clientIP, ",")
if len(ips) > 0 {
req.Header.Set("X-Real-Ip", strings.TrimSpace(ips[0]))
}
}
}
}
@ -354,6 +375,12 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
removeHeaders(res.Header, rrr.NoCache)
//Remove the User-Agent header if exists
if _, ok := res.Header["User-Agent"]; ok {
//Server to client request should not contains a User-Agent header
res.Header.Del("User-Agent")
}
if p.ModifyResponse != nil {
if err := p.ModifyResponse(res); err != nil {
if p.Verbal {
@ -365,6 +392,12 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
}
}
//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)
//}
//Custom header rewriter functions
if res.Header.Get("Location") != "" {
locationRewrite := res.Header.Get("Location")
@ -413,7 +446,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
}
}
p.copyResponse(rw, res.Body)
//Get flush interval in real time and start copying the request
flushInterval := p.getFlushInterval(req, res)
p.copyResponse(rw, res.Body, flushInterval)
// close now, instead of defer, to populate res.Trailer
res.Body.Close()
copyHeader(rw.Header(), res.Trailer)

View File

@ -0,0 +1,38 @@
package dpcore
import (
"mime"
"net/http"
"time"
)
// Auto sniff of flush interval from header
func (p *ReverseProxy) getFlushInterval(req *http.Request, res *http.Response) time.Duration {
contentType := req.Header.Get("Content-Type")
if actualContentType, _, _ := mime.ParseMediaType(contentType); actualContentType == "text/event-stream" {
return -1
}
if req.ContentLength == -1 || p.isBidirectionalStream(req, res) {
return -1
}
//Cannot sniff anything. Use default value
return p.FlushInterval
}
// Check for bidirectional stream, copy from Caddy :D
func (p *ReverseProxy) isBidirectionalStream(req *http.Request, res *http.Response) bool {
// We have to check the encoding here; only flush headers with identity encoding.
// Non-identity encoding might combine with "encode" directive, and in that case,
// if body size larger than enc.MinLength, upper level encode handle might have
// Content-Encoding header to write.
// (see https://github.com/caddyserver/caddy/issues/3606 for use case)
ae := req.Header.Get("Accept-Encoding")
return req.ProtoMajor == 2 &&
res.ProtoMajor == 2 &&
res.ContentLength == -1 &&
(ae == "identity" || ae == "")
}

View File

@ -0,0 +1,73 @@
package dpcore
/*
Max Latency Writer
This script implements a io writer with periodic flushing base on a ticker
Mostly based on httputil.ReverseProxy
*/
import (
"io"
"sync"
"time"
)
type maxLatencyWriter struct {
dst io.Writer
flush func() error
latency time.Duration // non-zero; negative means to flush immediately
mu sync.Mutex // protects t, flushPending, and dst.Flush
t *time.Timer
flushPending bool
}
func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
m.mu.Lock()
defer m.mu.Unlock()
n, err = m.dst.Write(p)
if m.latency < 0 {
//Flush immediately
m.flush()
return
}
if m.flushPending {
//Flush in next tick cycle
return
}
if m.t == nil {
m.t = time.AfterFunc(m.latency, m.delayedFlush)
} else {
m.t.Reset(m.latency)
}
m.flushPending = true
return
}
func (m *maxLatencyWriter) delayedFlush() {
m.mu.Lock()
defer m.mu.Unlock()
if !m.flushPending {
// if stop was called but AfterFunc already started this goroutine
return
}
m.flush()
m.flushPending = false
}
func (m *maxLatencyWriter) stop() {
m.mu.Lock()
defer m.mu.Unlock()
m.flushPending = false
if m.t != nil {
m.t.Stop()
}
}

View File

@ -115,6 +115,28 @@ func (router *Router) StartProxyService() error {
r.URL, _ = url.Parse(originalHostHeader)
}
//Access Check (blacklist / whitelist)
ruleID := sep.AccessFilterUUID
if sep.AccessFilterUUID == "" {
//Use default rule
ruleID = "default"
}
accessRule, err := router.Option.AccessController.GetAccessRuleByID(ruleID)
if err == nil {
isBlocked, _ := accessRequestBlocked(accessRule, router.Option.WebDirectory, w, r)
if isBlocked {
return
}
}
//Validate basic auth
if sep.RequireBasicAuth {
err := handleBasicAuth(w, r, sep)
if err != nil {
return
}
}
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: sep.Domain,
OriginalHost: originalHostHeader,

View File

@ -11,7 +11,7 @@ import (
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/websocketproxy"
)
@ -34,23 +34,45 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
var targetSubdomainEndpoint *ProxyEndpoint = nil
ep, ok := router.ProxyEndpoints.Load(hostname)
if ok {
//Exact hit
targetSubdomainEndpoint = ep.(*ProxyEndpoint)
if !targetSubdomainEndpoint.Disabled {
return targetSubdomainEndpoint
}
}
//No hit. Try with wildcard
//No hit. Try with wildcard and alias
matchProxyEndpoints := []*ProxyEndpoint{}
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
ep := v.(*ProxyEndpoint)
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
if err != nil {
//Continue
//Bad pattern. Skip this rule
return true
}
if match {
//targetSubdomainEndpoint = ep
//Wildcard matches. Skip checking alias
matchProxyEndpoints = append(matchProxyEndpoints, ep)
return true
}
//Wildcard not match. Check for alias
if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 {
for _, aliasDomain := range ep.MatchingDomainAlias {
match, err := filepath.Match(aliasDomain, hostname)
if err != nil {
//Bad pattern. Skip this alias
continue
}
if match {
//This alias match
matchProxyEndpoints = append(matchProxyEndpoints, ep)
return true
}
}
}
return true
})
@ -114,7 +136,10 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
}
h.logRequest(r, true, 101, "subdomain-websocket", target.Domain)
wspHandler := websocketproxy.NewProxy(u, target.SkipCertValidations)
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: target.SkipWebSocketOriginCheck,
})
wspHandler.ServeHTTP(w, r)
return
}
@ -178,7 +203,10 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
}
h.logRequest(r, true, 101, "vdir-websocket", target.Domain)
wspHandler := websocketproxy.NewProxy(u, target.SkipCertValidations)
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: target.parent.SkipWebSocketOriginCheck,
})
wspHandler.ServeHTTP(w, r)
return
}
@ -218,7 +246,7 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
if h.Parent.Option.StatisticCollector != nil {
go func() {
requestInfo := statistic.RequestInfo{
IpAddr: geodb.GetRequesterIP(r),
IpAddr: netutils.GetRequesterIP(r),
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
Succ: succ,
StatusCode: statusCode,

View File

@ -2,19 +2,25 @@ package redirection
import (
"encoding/json"
"fmt"
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type RuleTable struct {
AllowRegex bool //Allow regular expression to be used in rule matching. Require up to O(n^m) time complexity
Logger *logger.Logger
configPath string //The location where the redirection rules is stored
rules sync.Map //Store the redirection rules for this reverse proxy instance
}
type RedirectRules struct {
@ -24,10 +30,11 @@ type RedirectRules struct {
StatusCode int //Status Code for redirection
}
func NewRuleTable(configPath string) (*RuleTable, error) {
func NewRuleTable(configPath string, allowRegex bool) (*RuleTable, error) {
thisRuleTable := RuleTable{
rules: sync.Map{},
configPath: configPath,
AllowRegex: allowRegex,
}
//Load all the rules from the config path
if !utils.FileExists(configPath) {
@ -77,7 +84,7 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP
}
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json"
filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json"
// Create the full file path by joining the t.configPath with the filename
filepath := path.Join(t.configPath, filename)
@ -105,11 +112,12 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP
func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json"
filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json"
// Create the full file path by joining the t.configPath with the filename
filepath := path.Join(t.configPath, filename)
fmt.Println(redirectURL, filename, filepath)
// Check if the file exists
if _, err := os.Stat(filepath); os.IsNotExist(err) {
return nil // File doesn't exist, nothing to delete
@ -145,18 +153,47 @@ func (t *RuleTable) MatchRedirectRule(requestedURL string) *RedirectRules {
// Iterate through all the keys in the rules map
var targetRedirectionRule *RedirectRules = nil
var maxMatch int = 0
t.rules.Range(func(key interface{}, value interface{}) bool {
// Check if the requested URL starts with the key as a prefix
if strings.HasPrefix(requestedURL, key.(string)) {
// This request URL matched the domain
if len(key.(string)) > maxMatch {
if t.AllowRegex {
//Regexp matching rule
matched, err := regexp.MatchString(key.(string), requestedURL)
if err != nil {
//Something wrong with the regex?
t.log("Unable to match regex", err)
return true
}
if matched {
maxMatch = len(key.(string))
targetRedirectionRule = value.(*RedirectRules)
}
} else {
//Default: prefix matching redirect
if strings.HasPrefix(requestedURL, key.(string)) {
// This request URL matched the domain
if len(key.(string)) > maxMatch {
maxMatch = len(key.(string))
targetRedirectionRule = value.(*RedirectRules)
}
}
}
return true
})
return targetRedirectionRule
}
// Log the message to log file, use STDOUT if logger not set
func (t *RuleTable) log(message string, err error) {
if t.Logger == nil {
if err == nil {
log.Println("[Redirect] " + message)
} else {
log.Println("[Redirect] " + message + ": " + err.Error())
}
} else {
t.Logger.PrintAndLog("Redirect", message, err)
}
}

View File

@ -19,6 +19,9 @@ import (
func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
//Filter the tailing slash if any
domain := endpoint.Domain
if len(domain) == 0 {
return nil, errors.New("invalid endpoint config")
}
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
@ -42,13 +45,19 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
}
//Create the proxy routing handler
proxy := dpcore.NewDynamicProxyCore(path, "", endpoint.SkipCertValidations)
proxy := dpcore.NewDynamicProxyCore(path, "", &dpcore.DpcoreOptions{
IgnoreTLSVerification: endpoint.SkipCertValidations,
})
endpoint.proxy = proxy
endpoint.parent = router
//Prepare proxy routing hjandler for each of the virtual directories
for _, vdir := range endpoint.VirtualDirectories {
domain := vdir.Domain
if len(domain) == 0 {
//invalid vdir
continue
}
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
@ -69,7 +78,9 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
return nil, err
}
proxy := dpcore.NewDynamicProxyCore(path, vdir.MatchingPath, vdir.SkipCertValidations)
proxy := dpcore.NewDynamicProxyCore(path, vdir.MatchingPath, &dpcore.DpcoreOptions{
IgnoreTLSVerification: vdir.SkipCertValidations,
})
vdir.proxy = proxy
vdir.parent = endpoint
}

View File

@ -0,0 +1,157 @@
<!DOCTYPE html>
<html>
<head>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
<meta charset="UTF-8">
<meta name="theme-color" content="#4b75ff">
<link rel="icon" type="image/png" href="img/small_icon.png"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700;900&display=swap" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script>
<title>404 - Host Not Found</title>
<style>
h1, h2, h3, h4, h5, p, a, span{
font-family: 'Noto Sans TC', sans-serif;
font-weight: 300;
color: rgb(88, 88, 88)
}
.diagram{
background-color: #ebebeb;
box-shadow:
inset 0px 11px 8px -10px #CCC,
inset 0px -11px 8px -10px #CCC;
padding-bottom: 2em;
}
.diagramHeader{
margin-top: 0.2em;
}
@media (max-width:512px) {
.widescreenOnly{
display: none !important;
}
.four.wide.column:not(.widescreenOnly){
width: 50% !important;
}
.ui.grid{
justify-content: center !important;
}
}
</style>
</head>
<body>
<div>
<br><br>
<div class="ui container">
<h1 style="font-size: 4rem;">Error 404</h1>
<p style="font-size: 2rem; margin-bottom: 0.4em;">Target Host Not Found</p>
<small id="timestamp"></small>
</div>
<br><br>
</div>
<div class="diagram">
<div class="ui text container">
<div class="ui grid">
<div class="four wide column widescreenOnly" align="center">
<svg version="1.1" id="client_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
<path fill="#C9CACA" d="M184.795,143.037c0,9.941-8.059,18-18,18H33.494c-9.941,0-18-8.059-18-18V44.952c0-9.941,8.059-18,18-18
h133.301c9.941,0,18,8.059,18,18V143.037z"/>
<circle fill="#FFFFFF" cx="37.39" cy="50.88" r="6.998"/>
<circle fill="#FFFFFF" cx="54.115" cy="50.88" r="6.998"/>
<path fill="#FFFFFF" d="M167.188,50.88c0,3.865-3.133,6.998-6.998,6.998H72.379c-3.865,0-6.998-3.133-6.998-6.998l0,0
c0-3.865,3.133-6.998,6.998-6.998h87.811C164.055,43.882,167.188,47.015,167.188,50.88L167.188,50.88z"/>
<rect x="31.296" y="66.907" fill="#FFFFFF" width="132.279" height="77.878"/>
<circle fill="#9BCA3E" cx="96.754" cy="144.785" r="37.574"/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="108.497,133.047 93.373,153.814
82.989,143.204 "/>
</svg>
<small>You</small>
<h2 class="diagramHeader">Browser</h2>
<p style="font-weight: 500; color: #9bca3e;">Working</p>
</div>
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
</div>
<div class="four wide column widescreenOnly" align="center">
<svg version="1.1" id="cloud_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
<ellipse fill="#9FA0A0" cx="46.979" cy="108.234" rx="25.399" ry="25.139"/>
<circle fill="#9FA0A0" cx="109.407" cy="100.066" r="50.314"/>
<circle fill="#9FA0A0" cx="22.733" cy="129.949" r="19.798"/>
<circle fill="#9FA0A0" cx="172.635" cy="125.337" r="24.785"/>
<path fill="#9FA0A0" d="M193.514,133.318c0,9.28-7.522,16.803-16.803,16.803H28.223c-9.281,0-16.803-7.522-16.803-16.803l0,0
c0-9.28,7.522-16.804,16.803-16.804h148.488C185.991,116.515,193.514,124.038,193.514,133.318L193.514,133.318z"/>
<circle fill="#9BCA3D" cx="100" cy="149.572" r="38.267"/>
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="113.408,136.402 95.954,160.369
83.971,148.123 "/>
</svg>
<small>Gateway Node</small>
<h2 class="diagramHeader">Reverse Proxy</h2>
<p style="font-weight: 500; color: #9bca3e;">Working</p>
</div>
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
</div>
<div class="four wide column" align="center">
<svg version="1.1" id="host_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
<path fill="#999999" d="M168.484,113.413c0,9.941,3.317,46.324-6.624,46.324H35.359c-9.941,0-5.873-39.118-5.715-46.324
l17.053-50.909c1.928-9.879,8.059-18,18-18h69.419c9.941,0,15.464,7.746,18,18L168.484,113.413z"/>
<rect x="38.068" y="118.152" fill="#FFFFFF" width="122.573" height="34.312"/>
<circle fill="#BD2426" cx="141.566" cy="135.873" r="8.014"/>
<circle fill="#BD2426" cx="99.354" cy="152.464" r="36.343"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="144.125" x2="107.594" y2="161.946"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="161.946" x2="107.594" y2="144.79"/>
</svg>
<small id="host"></small>
<h2 class="diagramHeader">Host</h2>
<p style="font-weight: 500; color: #bd2426;">Not Found</p>
</div>
</div>
</div>
</div>
<div>
<br>
<div class="ui container">
<div class="ui stackable grid">
<div class="eight wide column">
<h1>What happend?</h1>
<p>The reverse proxy target domain is not found.<br>For more information, see the error message on the reverse proxy terminal.</p>
</div>
<div class="eight wide column">
<h1>What can I do?</h1>
<h5 style="font-weight: 500;">If you are a visitor of this website: </h5>
<p>Please try again in a few minutes</p>
<h5 style="font-weight: 500;">If you are the owner of this website:</h5>
<div class="ui bulleted list">
<div class="item">Check if the proxy rules that match this hostname exists</div>
<div class="item">Visit the Reverse Proxy management interface to correct any setting errors</div>
</div>
</div>
</div>
</div>
<br>
</div>
<div class="ui divider"></div>
<div class="ui container" style="color: grey; font-size: 90%">
<p>Powered by Zoraxy</p>
</div>
<br><br>
<script>
$("#timestamp").text(new Date());
$("#host").text(location.href);
</script>
</body>
</html>

View File

@ -6,6 +6,7 @@ import (
"net/http"
"sync"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/geodb"
@ -34,7 +35,8 @@ type RouterOption struct {
ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager
RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store //GeoIP blacklist and whitelist
GeodbStore *geodb.Store //GeoIP resolver
AccessController *access.Controller //Blacklist / whitelist controller
StatisticCollector *statistic.Collector
WebDirectory string //The static web server directory containing the templates folder
}
@ -90,14 +92,16 @@ type VirtualDirectoryEndpoint struct {
// A proxy endpoint record, a general interface for handling inbound routing
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
Domain string //Domain or IP to proxy to
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
Domain string //Domain or IP to proxy to
//TLS/SSL Related
RequireTLS bool //Target domain require TLS
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
SkipCertValidations bool //Set to true to accept self signed certs
RequireTLS bool //Target domain require TLS
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
SkipCertValidations bool //Set to true to accept self signed certs
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
@ -110,13 +114,17 @@ type ProxyEndpoint struct {
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
//Fallback routing logic
//Access Control
AccessFilterUUID string //Access filter ID
Disabled bool //If the rule is disabled
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
Disabled bool //If the rule is disabled
//Internal Logic Elements
parent *Router
parent *Router `json:"-"`
proxy *dpcore.ReverseProxy `json:"-"`
}
@ -141,4 +149,6 @@ Web Templates
var (
//go:embed templates/forbidden.html
page_forbidden []byte
//go:embed templates/hosterror.html
page_hosterror []byte
)

View File

@ -0,0 +1,25 @@
MIT License
Copyright (c) 2022 Smarty
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
NOTE: Various optional and subordinate components carry their own licensing
requirements and restrictions. Use of those components is subject to the terms
and conditions outlined the respective license of each component.

View File

@ -0,0 +1,109 @@
package cproxy
import (
"net/http"
"time"
)
func New(options ...option) http.Handler {
var this configuration
Options.apply(options...)(&this)
return newHandler(this.Filter, this.ClientConnector, this.ServerConnector, this.Monitor)
}
var Options singleton
type singleton struct{}
type option func(*configuration)
type configuration struct {
DialTimeout time.Duration
Filter Filter
DialAddress string
Dialer Dialer
LogConnections bool
ProxyProtocol bool
Initializer initializer
ClientConnector clientConnector
ServerConnector serverConnector
Monitor monitor
Logger logger
}
func (singleton) DialTimeout(value time.Duration) option {
return func(this *configuration) { this.DialTimeout = value }
}
func (singleton) Filter(value Filter) option {
return func(this *configuration) { this.Filter = value }
}
func (singleton) ClientConnector(value clientConnector) option {
return func(this *configuration) { this.ClientConnector = value }
}
func (singleton) DialAddress(value string) option {
return func(this *configuration) { this.DialAddress = value }
}
func (singleton) Dialer(value Dialer) option {
return func(this *configuration) { this.Dialer = value }
}
func (singleton) LogConnections(value bool) option {
return func(this *configuration) { this.LogConnections = value }
}
func (singleton) ProxyProtocol(value bool) option {
return func(this *configuration) { this.ProxyProtocol = value }
}
func (singleton) Initializer(value initializer) option {
return func(this *configuration) { this.Initializer = value }
}
func (singleton) ServerConnector(value serverConnector) option {
return func(this *configuration) { this.ServerConnector = value }
}
func (singleton) Monitor(value monitor) option {
return func(this *configuration) { this.Monitor = value }
}
func (singleton) Logger(value logger) option {
return func(this *configuration) { this.Logger = value }
}
func (singleton) apply(options ...option) option {
return func(this *configuration) {
for _, item := range Options.defaults(options...) {
item(this)
}
if this.Dialer == nil {
this.Dialer = newDialer(this)
}
this.Dialer = newRoutingDialer(this)
if this.ProxyProtocol {
this.Initializer = newProxyProtocolInitializer()
}
if this.Initializer == nil {
this.Initializer = nop{}
}
this.Initializer = newLoggingInitializer(this)
if this.ServerConnector == nil {
this.ServerConnector = newServerConnector(this.Dialer, this.Initializer)
}
}
}
func (singleton) defaults(options ...option) []option {
return append([]option{
Options.DialTimeout(time.Second * 10),
Options.Filter(newFilter()),
Options.ClientConnector(newClientConnector()),
Options.Initializer(nop{}),
Options.Monitor(nop{}),
Options.Logger(nop{}),
}, options...)
}
type nop struct{}
func (nop) Measure(int) {}
func (nop) Printf(string, ...interface{}) {}
func (nop) Initialize(Socket, Socket) bool { return true }

View File

@ -0,0 +1,19 @@
package cproxy
import "net/http"
type defaultClientConnector struct{}
func newClientConnector() *defaultClientConnector {
return &defaultClientConnector{}
}
func (this *defaultClientConnector) Connect(response http.ResponseWriter) Socket {
if hijacker, ok := response.(http.Hijacker); !ok {
return nil
} else if socket, _, _ := hijacker.Hijack(); socket == nil {
return nil // this 'else if' exists to avoid the pointer nil != interface nil issue
} else {
return socket
}
}

View File

@ -0,0 +1,25 @@
package cproxy
import (
"net"
"time"
)
type defaultDialer struct {
timeout time.Duration
logger logger
}
func newDialer(config *configuration) *defaultDialer {
return &defaultDialer{timeout: config.DialTimeout, logger: config.Logger}
}
func (this *defaultDialer) Dial(address string) Socket {
if socket, err := net.DialTimeout("tcp", address, this.timeout); err == nil {
return socket
} else {
this.logger.Printf("[INFO] Unable to establish connection to [%s]: %s", address, err)
}
return nil
}

View File

@ -0,0 +1,9 @@
package cproxy
import "net/http"
type defaultFilter struct{}
func newFilter() *defaultFilter { return &defaultFilter{} }
func (this *defaultFilter) IsAuthorized(http.ResponseWriter, *http.Request) bool { return true }

View File

@ -0,0 +1,56 @@
package cproxy
import "net/http"
type defaultHandler struct {
filter Filter
clientConnector clientConnector
serverConnector serverConnector
meter monitor
}
func newHandler(filter Filter, clientConnector clientConnector, serverConnector serverConnector, meter monitor) *defaultHandler {
return &defaultHandler{
filter: filter,
clientConnector: clientConnector,
serverConnector: serverConnector,
meter: meter,
}
}
func (this *defaultHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
this.meter.Measure(MeasurementHTTPRequest)
if request.Method != "CONNECT" {
this.meter.Measure(MeasurementBadMethod)
writeResponseStatus(response, http.StatusMethodNotAllowed)
} else if !this.filter.IsAuthorized(response, request) {
this.meter.Measure(MeasurementUnauthorizedRequest)
//writeResponseStatus(response, http.StatusUnauthorized)
} else if client := this.clientConnector.Connect(response); client == nil {
this.meter.Measure(MeasurementClientConnectionFailed)
writeResponseStatus(response, http.StatusNotImplemented)
} else if connection := this.serverConnector.Connect(client, request.URL.Host); connection == nil {
this.meter.Measure(MeasurementServerConnectionFailed)
_, _ = client.Write(statusBadGateway)
_ = client.Close()
} else {
this.meter.Measure(MeasurementProxyReady)
_, _ = client.Write(statusReady)
connection.Proxy()
this.meter.Measure(MeasurementProxyComplete)
}
}
func writeResponseStatus(response http.ResponseWriter, statusCode int) {
http.Error(response, http.StatusText(statusCode), statusCode)
}
var (
statusBadGateway = []byte("HTTP/1.1 502 Bad Gateway\r\n\r\n")
statusReady = []byte("HTTP/1.1 200 OK\r\n\r\n")
)

View File

@ -0,0 +1,54 @@
package cproxy
import (
"io"
"sync"
)
type defaultProxy struct {
client Socket
server Socket
waiter *sync.WaitGroup
}
func newProxy(client, server Socket) *defaultProxy {
waiter := &sync.WaitGroup{}
waiter.Add(2) // wait on both client->server and server->client streams
return &defaultProxy{
waiter: waiter,
client: client,
server: server,
}
}
func (this *defaultProxy) Proxy() {
go this.streamAndClose(this.client, this.server)
go this.streamAndClose(this.server, this.client)
this.closeSockets()
}
func (this *defaultProxy) streamAndClose(reader, writer Socket) {
_, _ = io.Copy(writer, reader)
tryCloseRead(reader)
tryCloseWrite(writer)
this.waiter.Done()
}
func tryCloseRead(socket Socket) {
if tcp, ok := socket.(tcpSocket); ok {
_ = tcp.CloseRead()
}
}
func tryCloseWrite(socket Socket) {
if tcp, ok := socket.(tcpSocket); ok {
_ = tcp.CloseWrite()
}
}
func (this *defaultProxy) closeSockets() {
this.waiter.Wait()
_ = this.client.Close()
_ = this.server.Close()
}

View File

@ -0,0 +1,24 @@
package cproxy
type defaultServerConnector struct {
dialer Dialer
initializer initializer
}
func newServerConnector(dialer Dialer, initializer initializer) *defaultServerConnector {
return &defaultServerConnector{dialer: dialer, initializer: initializer}
}
func (this *defaultServerConnector) Connect(client Socket, serverAddress string) proxy {
server := this.dialer.Dial(serverAddress)
if server == nil {
return nil
}
if !this.initializer.Initialize(client, server) {
_ = server.Close()
return nil
}
return newProxy(client, server)
}

View File

@ -0,0 +1,32 @@
package cproxy
import "net/http"
type hostnameFilter struct {
authorized []string
}
func NewHostnameFilter(authorized []string) Filter {
return &hostnameFilter{authorized: authorized}
}
func (this hostnameFilter) IsAuthorized(_ http.ResponseWriter, request *http.Request) bool {
if len(this.authorized) == 0 {
return true
}
host := request.URL.Host
hostLength := len(host)
for _, authorized := range this.authorized {
if authorized[:2] == "*." {
have, want := hostLength, len(authorized)-1
if have > want && authorized[1:] == host[hostLength-want:] {
return true
}
} else if authorized == host {
return true
}
}
return false
}

View File

@ -0,0 +1,26 @@
package cproxy
import (
"net/http"
"strings"
)
type hostnameSuffixFilter struct {
authorized []string
}
func NewHostnameSuffixFilter(authorized []string) Filter {
return &hostnameSuffixFilter{authorized: authorized}
}
func (this hostnameSuffixFilter) IsAuthorized(_ http.ResponseWriter, request *http.Request) bool {
host := request.URL.Host
for _, authorized := range this.authorized {
if strings.HasSuffix(host, authorized) {
return true
}
}
return false
}

View File

@ -0,0 +1,67 @@
package cproxy
import (
"io"
"net"
"net/http"
)
type (
Filter interface {
IsAuthorized(http.ResponseWriter, *http.Request) bool
}
clientConnector interface {
Connect(http.ResponseWriter) Socket
}
)
type (
Dialer interface {
Dial(string) Socket
}
serverConnector interface {
Connect(Socket, string) proxy
}
initializer interface {
Initialize(Socket, Socket) bool
}
proxy interface {
Proxy()
}
)
type (
Socket interface {
io.ReadWriteCloser
RemoteAddr() net.Addr
}
tcpSocket interface {
Socket
CloseRead() error
CloseWrite() error
}
)
type (
monitor interface {
Measure(int)
}
logger interface {
Printf(string, ...interface{})
}
)
const (
MeasurementHTTPRequest int = iota
MeasurementBadMethod
MeasurementUnauthorizedRequest
MeasurementClientConnectionFailed
MeasurementServerConnectionFailed
MeasurementProxyReady
MeasurementProxyComplete
)

View File

@ -0,0 +1,24 @@
package cproxy
type loggingInitializer struct {
logger logger
inner initializer
}
func newLoggingInitializer(config *configuration) initializer {
if !config.LogConnections {
return config.Initializer
}
return &loggingInitializer{inner: config.Initializer, logger: config.Logger}
}
func (this *loggingInitializer) Initialize(client, server Socket) bool {
result := this.inner.Initialize(client, server)
if !result {
this.logger.Printf("[INFO] Connection failed [%s] -> [%s]", client.RemoteAddr(), server.RemoteAddr())
}
return result
}

View File

@ -0,0 +1,36 @@
package cproxy
import (
"fmt"
"io"
"net"
"strings"
)
type proxyProtocolInitializer struct{}
func newProxyProtocolInitializer() *proxyProtocolInitializer {
return &proxyProtocolInitializer{}
}
func (this *proxyProtocolInitializer) Initialize(client, server Socket) bool {
header := formatHeader(client.RemoteAddr(), server.RemoteAddr())
_, err := io.WriteString(server, header)
return err == nil
}
func formatHeader(client, server net.Addr) string {
clientAddress, clientPort := parseAddress(client.String())
serverAddress, serverPort := parseAddress(server.String())
if strings.Contains(clientAddress, ":") {
return fmt.Sprintf(proxyProtocolIPv6Preamble, clientAddress, serverAddress, clientPort, serverPort)
}
return fmt.Sprintf(proxyProtocolIPv4Preamble, clientAddress, serverAddress, clientPort, serverPort)
}
func parseAddress(address string) (string, string) {
address, port, _ := net.SplitHostPort(address)
return address, port
}
const proxyProtocolIPv4Preamble = "PROXY TCP4 %s %s %s %s\r\n"
const proxyProtocolIPv6Preamble = "PROXY TCP6 %s %s %s %s\r\n"

View File

@ -0,0 +1,18 @@
package cproxy
type routingDialer struct {
inner Dialer
targetAddress string
}
func newRoutingDialer(config *configuration) Dialer {
if len(config.DialAddress) == 0 {
return config.Dialer
}
return &routingDialer{inner: config.Dialer, targetAddress: config.DialAddress}
}
func (this *routingDialer) Dial(string) Socket {
return this.inner.Dial(this.targetAddress)
}

View File

@ -0,0 +1,137 @@
package forwardproxy
import (
"context"
"encoding/json"
"errors"
"log"
"net/http"
"strconv"
"time"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/forwardproxy/cproxy"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type ZrFilter struct {
//To be implemented
}
type Handler struct {
server *http.Server
handler *http.Handler
running bool
db *database.Database
logger *logger.Logger
Port int
}
func NewForwardProxy(sysdb *database.Database, port int, logger *logger.Logger) *Handler {
thisFilter := ZrFilter{}
handler := cproxy.New(cproxy.Options.Filter(thisFilter))
return &Handler{
db: sysdb,
server: nil,
handler: &handler,
running: false,
logger: logger,
Port: port,
}
}
// Start the forward proxy
func (h *Handler) Start() error {
if h.running {
return errors.New("forward proxy already running")
}
server := &http.Server{Addr: ":" + strconv.Itoa(h.Port), Handler: *h.handler}
h.server = server
go func() {
if err := server.ListenAndServe(); err != nil {
if err != nil {
log.Println(err.Error())
}
}
}()
h.running = true
return nil
}
// Stop the forward proxy
func (h *Handler) Stop() error {
if h.running && h.server != nil {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := h.server.Shutdown(ctx); err != nil {
return err
}
h.running = false
h.server = nil
}
return nil
}
// Update the port number of the forward proxy
func (h *Handler) UpdatePort(newPort int) error {
h.Stop()
h.Port = newPort
return h.Start()
}
func (it ZrFilter) IsAuthorized(w http.ResponseWriter, r *http.Request) bool {
return true
}
// Handle port change of the forward proxy
func (h *Handler) HandlePort(w http.ResponseWriter, r *http.Request) {
port, err := utils.PostInt(r, "port")
if err != nil {
js, _ := json.Marshal(h.Port)
utils.SendJSONResponse(w, string(js))
} else {
//Update the port
err = h.UpdatePort(port)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
h.logger.PrintAndLog("Forward Proxy", "HTTP Forward Proxy port updated to :"+strconv.Itoa(h.Port), nil)
h.db.Write("fwdproxy", "port", port)
utils.SendOK(w)
}
}
// Handle power toggle of the forward proxys
func (h *Handler) HandleToogle(w http.ResponseWriter, r *http.Request) {
enabled, err := utils.PostBool(r, "enable")
if err != nil {
//Get the current state of the forward proxy
js, _ := json.Marshal(h.running)
utils.SendJSONResponse(w, string(js))
} else {
if enabled {
err = h.Start()
if err != nil {
h.logger.PrintAndLog("Forward Proxy", "Unable to start forward proxy server", err)
utils.SendErrorResponse(w, err.Error())
return
}
h.logger.PrintAndLog("Forward Proxy", "HTTP Forward Proxy Started, listening on :"+strconv.Itoa(h.Port), nil)
} else {
err = h.Stop()
if err != nil {
h.logger.PrintAndLog("Forward Proxy", "Unable to stop forward proxy server", err)
utils.SendErrorResponse(w, err.Error())
return
}
h.logger.PrintAndLog("Forward Proxy", "HTTP Forward Proxy Stopped", nil)
}
h.db.Write("fwdproxy", "enabled", enabled)
utils.SendOK(w)
}
}

View File

@ -1,91 +0,0 @@
package geodb
import "strings"
/*
Blacklist.go
This script store the blacklist related functions
*/
//Geo Blacklist
func (s *Store) AddCountryCodeToBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("blacklist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("blacklist-cn", countryCode)
}
func (s *Store) IsCountryCodeBlacklisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isBlacklisted bool = false
s.sysdb.Read("blacklist-cn", countryCode, &isBlacklisted)
return isBlacklisted
}
func (s *Store) GetAllBlacklistedCountryCode() []string {
bannedCountryCodes := []string{}
entries, err := s.sysdb.ListTable("blacklist-cn")
if err != nil {
return bannedCountryCodes
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedCountryCodes = append(bannedCountryCodes, ip)
}
return bannedCountryCodes
}
//IP Blacklsits
func (s *Store) AddIPToBlackList(ipAddr string) {
s.sysdb.Write("blacklist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromBlackList(ipAddr string) {
s.sysdb.Delete("blacklist-ip", ipAddr)
}
func (s *Store) GetAllBlacklistedIp() []string {
bannedIps := []string{}
entries, err := s.sysdb.ListTable("blacklist-ip")
if err != nil {
return bannedIps
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedIps = append(bannedIps, ip)
}
return bannedIps
}
func (s *Store) IsIPBlacklisted(ipAddr string) bool {
var isBlacklisted bool = false
s.sysdb.Read("blacklist-ip", ipAddr, &isBlacklisted)
if isBlacklisted {
return true
}
//Check for IP wildcard and CIRD rules
AllBlacklistedIps := s.GetAllBlacklistedIp()
for _, blacklistRule := range AllBlacklistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
if cidrMatch {
return true
}
}
return false
}

View File

@ -2,11 +2,10 @@ package geodb
import (
_ "embed"
"log"
"net"
"net/http"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/netutils"
)
//go:embed geoipv4.csv
@ -16,12 +15,10 @@ var geoipv4 []byte //Geodb dataset for ipv4
var geoipv6 []byte //Geodb dataset for ipv6
type Store struct {
BlacklistEnabled bool
WhitelistEnabled bool
geodb [][]string //Parsed geodb list
geodbIpv6 [][]string //Parsed geodb list for ipv6
geotrie *trie
geotrieIpv6 *trie
geodb [][]string //Parsed geodb list
geodbIpv6 [][]string //Parsed geodb list for ipv6
geotrie *trie
geotrieIpv6 *trie
//geoipCache sync.Map
sysdb *database.Database
option *StoreOptions
@ -48,40 +45,6 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
return nil, err
}
blacklistEnabled := false
whitelistEnabled := false
if sysdb != nil {
err = sysdb.NewTable("blacklist-cn")
if err != nil {
return nil, err
}
err = sysdb.NewTable("blacklist-ip")
if err != nil {
return nil, err
}
err = sysdb.NewTable("whitelist-cn")
if err != nil {
return nil, err
}
err = sysdb.NewTable("whitelist-ip")
if err != nil {
return nil, err
}
err = sysdb.NewTable("blackwhitelist")
if err != nil {
return nil, err
}
sysdb.Read("blackwhitelist", "blacklistEnabled", &blacklistEnabled)
sysdb.Read("blackwhitelist", "whitelistEnabled", &whitelistEnabled)
} else {
log.Println("Database pointer set to nil: Entering debug mode")
}
var ipv4Trie *trie
if !option.AllowSlowIpv4LookUp {
ipv4Trie = constrctTrieTree(parsedGeoData)
@ -93,27 +56,15 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
}
return &Store{
BlacklistEnabled: blacklistEnabled,
WhitelistEnabled: whitelistEnabled,
geodb: parsedGeoData,
geotrie: ipv4Trie,
geodbIpv6: parsedGeoDataIpv6,
geotrieIpv6: ipv6Trie,
sysdb: sysdb,
option: option,
geodb: parsedGeoData,
geotrie: ipv4Trie,
geodbIpv6: parsedGeoDataIpv6,
geotrieIpv6: ipv6Trie,
sysdb: sysdb,
option: option,
}, nil
}
func (s *Store) ToggleBlacklist(enabled bool) {
s.sysdb.Write("blackwhitelist", "blacklistEnabled", enabled)
s.BlacklistEnabled = enabled
}
func (s *Store) ToggleWhitelist(enabled bool) {
s.sysdb.Write("blackwhitelist", "whitelistEnabled", enabled)
s.WhitelistEnabled = enabled
}
func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
cc := s.search(ipstring)
return &CountryInfo{
@ -127,90 +78,8 @@ func (s *Store) Close() {
}
/*
Check if a IP address is blacklisted, in either country or IP blacklist
IsBlacklisted default return is false (allow access)
*/
func (s *Store) IsBlacklisted(ipAddr string) bool {
if !s.BlacklistEnabled {
//Blacklist not enabled. Always return false
return false
}
if ipAddr == "" {
//Unable to get the target IP address
return false
}
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
if err != nil {
return false
}
if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) {
return true
}
if s.IsIPBlacklisted(ipAddr) {
return true
}
return false
}
/*
IsWhitelisted check if a given IP address is in the current
server's white list.
Note that the Whitelist default result is true even
when encountered error
*/
func (s *Store) IsWhitelisted(ipAddr string) bool {
if !s.WhitelistEnabled {
//Whitelist not enabled. Always return true (allow access)
return true
}
if ipAddr == "" {
//Unable to get the target IP address, assume ok
return true
}
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
if err != nil {
return true
}
if s.IsCountryCodeWhitelisted(countryCode.CountryIsoCode) {
return true
}
if s.IsIPWhitelisted(ipAddr) {
return true
}
return false
}
// A helper function that check both blacklist and whitelist for access
// for both geoIP and ip / CIDR ranges
func (s *Store) AllowIpAccess(ipaddr string) bool {
if s.IsBlacklisted(ipaddr) {
return false
}
return s.IsWhitelisted(ipaddr)
}
func (s *Store) AllowConnectionAccess(conn net.Conn) bool {
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
return s.AllowIpAccess(addr.IP.String())
}
return true
}
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
ipAddr := GetRequesterIP(r)
ipAddr := netutils.GetRequesterIP(r)
if ipAddr == "" {
return ""
}

View File

@ -5,6 +5,8 @@ import (
"encoding/csv"
"io"
"strings"
"imuslab.com/zoraxy/mod/netutils"
)
func (s *Store) search(ip string) string {
@ -24,7 +26,7 @@ func (s *Store) search(ip string) string {
//Search in geotrie tree
cc := ""
if IsIPv6(ip) {
if netutils.IsIPv6(ip) {
if s.geotrieIpv6 == nil {
cc = s.slowSearchIpv6(ip)
} else {

View File

@ -1,129 +0,0 @@
package geodb
import (
"encoding/json"
"strings"
)
/*
Whitelist.go
This script handles whitelist related functions
*/
const (
EntryType_CountryCode int = 0
EntryType_IP int = 1
)
type WhitelistEntry struct {
EntryType int //Entry type of whitelist, Country Code or IP
CC string //ISO Country Code
IP string //IP address or range
Comment string //Comment for this entry
}
//Geo Whitelist
func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode)
entry := WhitelistEntry{
EntryType: EntryType_CountryCode,
CC: countryCode,
Comment: comment,
}
s.sysdb.Write("whitelist-cn", countryCode, entry)
}
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("whitelist-cn", countryCode)
}
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
return s.sysdb.KeyExists("whitelist-cn", countryCode)
}
func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry {
whitelistedCountryCode := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-cn")
if err != nil {
return whitelistedCountryCode
}
for _, keypairs := range entries {
thisWhitelistEntry := WhitelistEntry{}
json.Unmarshal(keypairs[1], &thisWhitelistEntry)
whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry)
}
return whitelistedCountryCode
}
//IP Whitelist
func (s *Store) AddIPToWhiteList(ipAddr string, comment string) {
thisIpEntry := WhitelistEntry{
EntryType: EntryType_IP,
IP: ipAddr,
Comment: comment,
}
s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry)
}
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
s.sysdb.Delete("whitelist-ip", ipAddr)
}
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr)
if isWhitelisted {
//single IP whitelist entry
return true
}
//Check for IP wildcard and CIRD rules
AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice()
for _, whitelistRules := range AllWhitelistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, whitelistRules)
if cidrMatch {
return true
}
}
return false
}
func (s *Store) GetAllWhitelistedIp() []*WhitelistEntry {
whitelistedIp := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-ip")
if err != nil {
return whitelistedIp
}
for _, keypairs := range entries {
//ip := string(keypairs[0])
thisEntry := WhitelistEntry{}
json.Unmarshal(keypairs[1], &thisEntry)
whitelistedIp = append(whitelistedIp, &thisEntry)
}
return whitelistedIp
}
func (s *Store) GetAllWhitelistedIpAsStringSlice() []string {
allWhitelistedIPs := []string{}
entries := s.GetAllWhitelistedIp()
for _, entry := range entries {
allWhitelistedIPs = append(allWhitelistedIPs, entry.IP)
}
return allWhitelistedIPs
}

View File

@ -1,4 +1,4 @@
package geodb
package netutils
import (
"net"
@ -6,7 +6,13 @@ import (
"strings"
)
// Utilities function
/*
MatchIP.go
This script contains function for matching IP address, comparing
CIDR and IPv4 / v6 validations
*/
func GetRequesterIP(r *http.Request) string {
ip := r.Header.Get("X-Real-Ip")
if ip == "" {

View File

@ -85,10 +85,6 @@ func NewReverseProxy(target *url.URL) *ReverseProxy {
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", "")
}
}
return &ReverseProxy{Director: director, Verbal: false}

View File

@ -85,7 +85,10 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
r.Header.Set("Zr-Origin-Upgrade", "websocket")
requestURL = strings.TrimPrefix(requestURL, "/")
u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL)
wspHandler := websocketproxy.NewProxy(u, false)
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: false,
SkipOriginCheck: false,
})
wspHandler.ServeHTTP(w, r)
return
}

View File

@ -1,6 +1,9 @@
package utils
import "strconv"
import (
"strconv"
"strings"
)
func StringToInt64(number string) (int64, error) {
i, err := strconv.ParseInt(number, 10, 64)
@ -14,3 +17,36 @@ func Int64ToString(number int64) string {
convedNumber := strconv.FormatInt(number, 10)
return convedNumber
}
func ReplaceSpecialCharacters(filename string) string {
replacements := map[string]string{
"#": "%pound%",
"&": "%amp%",
"{": "%left_cur%",
"}": "%right_cur%",
"\\": "%backslash%",
"<": "%left_ang%",
">": "%right_ang%",
"*": "%aster%",
"?": "%quest%",
" ": "%space%",
"$": "%dollar%",
"!": "%exclan%",
"'": "%sin_q%",
"\"": "%dou_q%",
":": "%colon%",
"@": "%at%",
"+": "%plus%",
"`": "%backtick%",
"|": "%pipe%",
"=": "%equal%",
".": "_",
"/": "-",
}
for char, replacement := range replacements {
filename = strings.ReplaceAll(filename, char, replacement)
}
return filename
}

View File

@ -47,19 +47,26 @@ type WebsocketProxy struct {
// If nil, DefaultDialer is used.
Dialer *websocket.Dialer
Verbal bool
SkipTlsValidation bool
Verbal bool
Options Options
}
// Additional options for websocket proxy runtime
type Options struct {
SkipTLSValidation bool //Skip backend TLS validation
SkipOriginCheck bool //Skip origin check
}
// ProxyHandler returns a new http.Handler interface that reverse proxies the
// request to the given target.
func ProxyHandler(target *url.URL, skipTlsValidation bool) http.Handler {
return NewProxy(target, skipTlsValidation)
func ProxyHandler(target *url.URL, options Options) http.Handler {
return NewProxy(target, options)
}
// NewProxy returns a new Websocket reverse proxy that rewrites the
// URL's to the scheme, host and base path provider in target.
func NewProxy(target *url.URL, skipTlsValidation bool) *WebsocketProxy {
func NewProxy(target *url.URL, options Options) *WebsocketProxy {
backend := func(r *http.Request) *url.URL {
// Shallow copy
u := *target
@ -68,7 +75,7 @@ func NewProxy(target *url.URL, skipTlsValidation bool) *WebsocketProxy {
u.RawQuery = r.URL.RawQuery
return &u
}
return &WebsocketProxy{Backend: backend, Verbal: false, SkipTlsValidation: skipTlsValidation}
return &WebsocketProxy{Backend: backend, Verbal: false, Options: options}
}
// ServeHTTP implements the http.Handler that proxies WebSocket connections.
@ -88,7 +95,7 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
dialer := w.Dialer
if w.Dialer == nil {
if w.SkipTlsValidation {
if w.Options.SkipTLSValidation {
//Disable TLS secure check if target allow skip verification
bypassDialer := websocket.DefaultDialer
bypassDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
@ -171,6 +178,13 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
upgrader = DefaultUpgrader
}
//Fixing issue #107 by bypassing request origin check
if w.Options.SkipOriginCheck {
upgrader.CheckOrigin = func(r *http.Request) bool {
return true
}
}
// Only pass those headers to the upgrader.
upgradeHeader := http.Header{}
if hdr := resp.Header.Get("Sec-Websocket-Protocol"); hdr != "" {

View File

@ -28,7 +28,10 @@ func TestProxy(t *testing.T) {
}
u, _ := url.Parse(backendURL)
proxy := NewProxy(u, false)
proxy := NewProxy(u, Options{
SkipTLSValidation: false,
SkipOriginCheck: false,
})
proxy.Upgrader = upgrader
mux := http.NewServeMux()

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"strconv"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
@ -15,12 +16,14 @@ import (
related to redirection function in the reverse proxy
*/
// Handle request for listing all stored redirection rules
func handleListRedirectionRules(w http.ResponseWriter, r *http.Request) {
rules := redirectTable.GetAllRedirectRules()
js, _ := json.Marshal(rules)
utils.SendJSONResponse(w, string(js))
}
// Handle request for adding new redirection rule
func handleAddRedirectionRule(w http.ResponseWriter, r *http.Request) {
redirectUrl, err := utils.PostPara(r, "redirectUrl")
if err != nil {
@ -58,6 +61,7 @@ func handleAddRedirectionRule(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w)
}
// Handle remove of a given redirection rule
func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) {
redirectUrl, err := utils.PostPara(r, "redirectUrl")
if err != nil {
@ -73,3 +77,30 @@ func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w)
}
// Toggle redirection regex support. Note that this cost another O(n) time complexity to each page load
func handleToggleRedirectRegexpSupport(w http.ResponseWriter, r *http.Request) {
enabled, err := utils.PostPara(r, "enable")
if err != nil {
//Return the current state of the regex support
js, _ := json.Marshal(redirectTable.AllowRegex)
utils.SendJSONResponse(w, string(js))
return
}
//Update the current regex support rule enable state
enableRegexSupport := strings.EqualFold(strings.TrimSpace(enabled), "true")
redirectTable.AllowRegex = enableRegexSupport
err = sysdb.Write("Redirect", "regex", enableRegexSupport)
if enableRegexSupport {
SystemWideLogger.PrintAndLog("redirect", "Regex redirect rule enabled", nil)
} else {
SystemWideLogger.PrintAndLog("redirect", "Regex redirect rule disabled", nil)
}
if err != nil {
utils.SendErrorResponse(w, "unable to save settings")
return
}
utils.SendOK(w)
}

View File

@ -94,6 +94,7 @@ func ReverseProxtInit() {
GeodbStore: geodbStore,
StatisticCollector: statisticCollector,
WebDirectory: *staticWebServerRoot,
AccessController: accessController,
})
if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err)
@ -194,6 +195,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
useTLS := (tls == "true")
//Bypass global TLS value / allow direct access from port 80?
bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS")
if bypassGlobalTLS == "" {
bypassGlobalTLS = "false"
@ -201,6 +203,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
useBypassGlobalTLS := bypassGlobalTLS == "true"
//Enable TLS validation?
stv, _ := utils.PostPara(r, "tlsval")
if stv == "" {
stv = "false"
@ -208,6 +211,17 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
skipTlsValidation := (stv == "true")
//Get access rule ID
accessRuleID, _ := utils.PostPara(r, "access")
if accessRuleID == "" {
accessRuleID = "default"
}
if !accessController.AccessRuleExists(accessRuleID) {
utils.SendErrorResponse(w, "invalid access rule ID selected")
return
}
//Require basic auth?
rba, _ := utils.PostPara(r, "bauth")
if rba == "" {
rba = "false"
@ -215,6 +229,13 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
requireBasicAuth := (rba == "true")
// Bypass WebSocket Origin Check
strbpwsorg, _ := utils.PostPara(r, "bpwsorg")
if strbpwsorg == "" {
strbpwsorg = "false"
}
bypassWebsocketOriginCheck := (strbpwsorg == "true")
//Prase the basic auth to correct structure
cred, _ := utils.PostPara(r, "cred")
basicAuthCredentials := []*dynamicproxy.BasicAuthCredentials{}
@ -247,18 +268,37 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
if eptype == "host" {
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
if err != nil {
utils.SendErrorResponse(w, "subdomain not defined")
utils.SendErrorResponse(w, "hostname not defined")
return
}
rootOrMatchingDomain = strings.TrimSpace(rootOrMatchingDomain)
//Check if it contains ",", if yes, split the remainings as alias
aliasHostnames := []string{}
if strings.Contains(rootOrMatchingDomain, ",") {
matchingDomains := strings.Split(rootOrMatchingDomain, ",")
if len(matchingDomains) > 1 {
rootOrMatchingDomain = matchingDomains[0]
for _, aliasHostname := range matchingDomains[1:] {
//Filter out any space
aliasHostnames = append(aliasHostnames, strings.TrimSpace(aliasHostname))
}
}
}
//Generate a proxy endpoint object
thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
//I/O
ProxyType: dynamicproxy.ProxyType_Host,
RootOrMatchingDomain: rootOrMatchingDomain,
MatchingDomainAlias: aliasHostnames,
Domain: endpoint,
//TLS
RequireTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
RequireTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
SkipWebSocketOriginCheck: bypassWebsocketOriginCheck,
AccessFilterUUID: accessRuleID,
//VDir
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
//Custom headers
@ -305,12 +345,13 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
//Write the root options to file
rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyType_Root,
RootOrMatchingDomain: "/",
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: false,
SkipCertValidations: false,
ProxyType: dynamicproxy.ProxyType_Root,
RootOrMatchingDomain: "/",
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: false,
SkipCertValidations: false,
SkipWebSocketOriginCheck: true,
DefaultSiteOption: defaultSiteOption,
DefaultSiteValue: dsVal,
@ -381,6 +422,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
}
bypassGlobalTLS := (bpgtls == "true")
// Basic Auth
rba, _ := utils.PostPara(r, "bauth")
if rba == "" {
rba = "false"
@ -388,6 +430,13 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
requireBasicAuth := (rba == "true")
// Bypass WebSocket Origin Check
strbpwsorg, _ := utils.PostPara(r, "bpwsorg")
if strbpwsorg == "" {
strbpwsorg = "false"
}
bypassWebsocketOriginCheck := (strbpwsorg == "true")
//Load the previous basic auth credentials from current proxy rules
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
if err != nil {
@ -402,6 +451,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
newProxyEndpoint.SkipCertValidations = skipTlsValidation
newProxyEndpoint.RequireBasicAuth = requireBasicAuth
newProxyEndpoint.SkipWebSocketOriginCheck = bypassWebsocketOriginCheck
//Prepare to replace the current routing rule
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
@ -421,6 +471,62 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w)
}
func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
rootNameOrMatchingDomain, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "Invalid ep given")
return
}
//No need to check for type as root (/) can be set to default route
//and hence, you will not need alias
//Load the previous alias from current proxy rules
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
if err != nil {
utils.SendErrorResponse(w, "Target proxy config not found or could not be loaded")
return
}
newAliasJSON, err := utils.PostPara(r, "alias")
if err != nil {
//No new set of alias given
utils.SendErrorResponse(w, "new alias not given")
return
}
//Write new alias to runtime and file
newAlias := []string{}
err = json.Unmarshal([]byte(newAliasJSON), &newAlias)
if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Unable to parse new alias list", err)
utils.SendErrorResponse(w, "Invalid alias list given")
return
}
//Set the current alias
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
newProxyEndpoint.MatchingDomainAlias = newAlias
// Prepare to replace the current routing rule
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
targetProxyEntry.Remove()
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
// Save it to file
err = SaveReverseProxyConfig(newProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, "Alias update failed")
SystemWideLogger.PrintAndLog("Proxy", "Unable to save alias update", err)
}
utils.SendOK(w)
}
func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
ep, err := utils.GetPara(r, "ep")
if err != nil {
@ -684,11 +790,73 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
utils.SendOK(w)
}
// Report the current status of the reverse proxy server
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(dynamicProxyRouter)
utils.SendJSONResponse(w, string(js))
}
// Toggle a certain rule on and off
func ReverseProxyToggleRuleSet(w http.ResponseWriter, r *http.Request) {
//No need to check for type as root cannot be turned off
ep, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "invalid ep given")
return
}
targetProxyRule, err := dynamicProxyRouter.LoadProxy(ep)
if err != nil {
utils.SendErrorResponse(w, "invalid endpoint given")
return
}
enableStr, err := utils.PostPara(r, "enable")
if err != nil {
enableStr = "true"
}
//Flip the enable and disabled tag state
ruleDisabled := enableStr == "false"
targetProxyRule.Disabled = ruleDisabled
err = SaveReverseProxyConfig(targetProxyRule)
if err != nil {
utils.SendErrorResponse(w, "unable to save updated rule")
return
}
utils.SendOK(w)
}
func ReverseProxyListDetail(w http.ResponseWriter, r *http.Request) {
eptype, err := utils.PostPara(r, "type") //Support root and host
if err != nil {
utils.SendErrorResponse(w, "type not defined")
return
}
if eptype == "host" {
epname, err := utils.PostPara(r, "epname")
if err != nil {
utils.SendErrorResponse(w, "epname not defined")
return
}
endpointRaw, ok := dynamicProxyRouter.ProxyEndpoints.Load(epname)
if !ok {
utils.SendErrorResponse(w, "proxy rule not found")
return
}
targetEndpoint := dynamicproxy.CopyEndpoint(endpointRaw.(*dynamicproxy.ProxyEndpoint))
js, _ := json.Marshal(targetEndpoint)
utils.SendJSONResponse(w, string(js))
} else if eptype == "root" {
js, _ := json.Marshal(dynamicProxyRouter.Root)
utils.SendJSONResponse(w, string(js))
} else {
utils.SendErrorResponse(w, "Invalid type given")
}
}
func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
eptype, err := utils.PostPara(r, "type") //Support root and host
if err != nil {
@ -845,7 +1013,7 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
}
proxyRoot := strings.TrimSuffix(dynamicProxyRouter.Root.Domain, "/")
if strings.HasPrefix(proxyRoot, "localhost:"+strconv.Itoa(newIncomingPortInt)) || strings.HasPrefix(proxyRoot, "127.0.0.1:"+strconv.Itoa(newIncomingPortInt)) {
if strings.EqualFold(proxyRoot, "localhost:"+strconv.Itoa(newIncomingPortInt)) || strings.EqualFold(proxyRoot, "127.0.0.1:"+strconv.Itoa(newIncomingPortInt)) {
//Listening port is same as proxy root
//Not allow recursive settings
utils.SendErrorResponse(w, "Recursive listening port! Check your proxy root settings.")

View File

@ -8,10 +8,12 @@ import (
"strings"
"time"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/forwardproxy"
"imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
@ -72,10 +74,14 @@ func startupSequence() {
}
//Create a redirection rule table
redirectTable, err = redirection.NewRuleTable("./conf/redirect")
db.NewTable("redirect")
redirectAllowRegexp := false
db.Read("redirect", "regex", &redirectAllowRegexp)
redirectTable, err = redirection.NewRuleTable("./conf/redirect", redirectAllowRegexp)
if err != nil {
panic(err)
}
redirectTable.Logger = SystemWideLogger
//Create a geodb store
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
@ -86,6 +92,16 @@ func startupSequence() {
panic(err)
}
//Create the access controller
accessController, err = access.NewAccessController(&access.Options{
Database: sysdb,
GeoDB: geodbStore,
ConfigFolder: "./conf/access",
})
if err != nil {
panic(err)
}
//Create a statistic collector
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
Database: sysdb,
@ -206,7 +222,7 @@ func startupSequence() {
//Create TCP Proxy Manager
tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{
Database: sysdb,
AccessControlHandler: geodbStore.AllowConnectionAccess,
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
})
//Create WoL MAC storage table
@ -219,6 +235,18 @@ func startupSequence() {
//Create an analytic loader
AnalyticLoader = analytic.NewDataLoader(sysdb, statisticCollector)
//Create basic forward proxy
sysdb.NewTable("fwdproxy")
fwdProxyEnabled := false
fwdProxyPort := 5587
sysdb.Read("fwdproxy", "port", &fwdProxyPort)
sysdb.Read("fwdproxy", "enabled", &fwdProxyEnabled)
forwardProxy = forwardproxy.NewForwardProxy(sysdb, fwdProxyPort, SystemWideLogger)
if fwdProxyEnabled {
SystemWideLogger.PrintAndLog("Forward Proxy", "HTTP Forward Proxy Listening on :"+strconv.Itoa(forwardProxy.Port), nil)
forwardProxy.Start()
}
/*
ACME API
@ -241,4 +269,5 @@ func finalSequence() {
//Inject routing rules
registerBuildInRoutingRules()
}

File diff suppressed because it is too large Load Diff

View File

@ -237,7 +237,7 @@
msgbox("Certificate installed successfully");
if (callback != undefined){
callback(false);
callback(true);
}
}
},

View File

@ -3,7 +3,16 @@
<h2>HTTP Proxy</h2>
<p>Proxy HTTP server with HTTP or HTTPS for multiple hosts. If you are only proxying for one host / domain, use Default Site instead.</p>
</div>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<style>
#httpProxyList .ui.toggle.checkbox input:checked ~ label::before{
background-color: #00ca52 !important;
}
.subdEntry td:not(.ignoremw){
min-width: 200px;
}
</style>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em; min-height: 300px;">
<table class="ui celled sortable unstackable compact table">
<thead>
<tr>
@ -11,7 +20,7 @@
<th>Destination</th>
<th>Virtual Directory</th>
<th>Basic Auth</th>
<th class="no-sort" style="min-width:100px;">Actions</th>
<th class="no-sort" style="min-width:150px;">Actions</th>
</tr>
</thead>
<tbody id="httpProxyList">
@ -25,6 +34,8 @@
</div>
<script>
/* List all proxy endpoints */
function listProxyEndpoints(){
$.get("/api/proxy/list?type=host", function(data){
$("#httpProxyList").html(``);
@ -37,6 +48,8 @@
<td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td>
</tr>`);
}else{
//Sort by RootOrMatchingDomain field
data.sort((a,b) => (a.RootOrMatchingDomain > b.RootOrMatchingDomain) ? 1 : ((b.RootOrMatchingDomain > a.RootOrMatchingDomain) ? -1 : 0))
data.forEach(subd => {
let tlsIcon = "";
let subdData = encodeURIComponent(JSON.stringify(subd));
@ -68,21 +81,124 @@
vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;"><i class="check icon"></i> No Virtual Directory</small>`;
}
let enableChecked = "checked";
if (subd.Disabled){
enableChecked = "";
}
let aliasDomains = ``;
if (subd.MatchingDomainAlias != undefined && subd.MatchingDomainAlias.length > 0){
aliasDomains = `<small class="aliasDomains" eptuuid="${subd.RootOrMatchingDomain}" style="color: #636363;">Alias: `;
subd.MatchingDomainAlias.forEach(alias => {
aliasDomains += `<a href="//${alias}" target="_blank">${alias}</a>, `;
});
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
aliasDomains += `</small><br>`;
}
$("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
<td data-label="" editable="true" datatype="inbound"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}</td>
<td data-label="" editable="true" datatype="inbound">
<a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}<br>
${aliasDomains}
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
</td>
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="vdir">${vdList}</td>
<td data-label="" editable="true" datatype="basicauth">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
<td class="center aligned" editable="true" datatype="action" data-label="">
<button class="ui circular mini basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="edit icon"></i></button>
<button class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button>
<td data-label="" editable="true" datatype="basicauth">
${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}
</td>
<td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
<div class="ui toggle tiny fitted checkbox" style="margin-bottom: -0.5em; margin-right: 0.4em;" title="Enable / Disable Rule">
<input type="checkbox" class="enableToggle" name="active" ${enableChecked} eptuuid="${subd.RootOrMatchingDomain}" onchange="handleProxyRuleToggle(this);">
<label></label>
</div>
<button title="Edit Proxy Rule" class="ui circular mini basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="edit icon"></i></button>
<button title="Remove Proxy Rule" class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button>
</td>
</tr>`);
});
}
resolveAccessRuleNameOnHostRPlist();
});
}
//Perform realtime alias update without refreshing the whole page
function updateAliasListForEndpoint(endpointName, newAliasDomainList){
let targetEle = $(`.aliasDomains[eptuuid='${endpointName}']`);
console.log(targetEle);
if (targetEle.length == 0){
return;
}
let aliasDomains = ``;
if (newAliasDomainList != undefined && newAliasDomainList.length > 0){
aliasDomains = `Alias: `;
newAliasDomainList.forEach(alias => {
aliasDomains += `<a href="//${alias}" target="_blank">${alias}</a>, `;
});
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
$(targetEle).html(aliasDomains);
$(targetEle).show();
}else{
$(targetEle).hide();
}
}
//Resolve & Update all rule names on host PR list
function resolveAccessRuleNameOnHostRPlist(){
//Resolve the access filters
$.get("/api/access/list", function(data){
console.log(data);
if (data.error == undefined){
//Build a map base on the data
let accessRuleMap = {};
for (var i = 0; i < data.length; i++){
accessRuleMap[data[i].ID] = data[i];
}
$(".accessRuleNameUnderHost").each(function(){
let thisAccessRuleID = $(this).attr("ruleid");
if (thisAccessRuleID== ""){
thisAccessRuleID = "default"
}
if (thisAccessRuleID == "default"){
//No need to label default access rules
$(this).html("");
return;
}
let rule = accessRuleMap[thisAccessRuleID];
let icon = `<i class="ui grey filter icon"></i>`;
if (rule.ID == "default"){
icon = `<i class="ui yellow star icon"></i>`;
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
//This is a blacklist filter
icon = `<i class="ui red filter icon"></i>`;
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
//This is a whitelist filter
icon = `<i class="ui green filter icon"></i>`;
}else if (rule.WhitelistEnabled && rule.BlacklistEnabled){
//Whitelist and blacklist filter
icon = `<i class="ui yellow filter icon"></i>`;
}
if (rule != undefined){
$(this).html(`${icon} ${rule.Name}`);
}
});
}
})
}
//Update the access rule name on given epuuid, call by hostAccessEditor.html
function updateAccessRuleNameUnderHost(epuuid, newruleUID){
$(`tr[eptuuid='${epuuid}'].subdEntry`).find(".accessRuleNameUnderHost").attr("ruleid", newruleUID);
resolveAccessRuleNameOnHostRPlist();
}
/*
Inline editor for httprp.html
@ -153,9 +269,16 @@
if (requireBasicAuth){
checkstate = "checked";
}
let skipWebSocketOriginCheck = payload.SkipWebSocketOriginCheck;
let wsCheckstate = "";
if (skipWebSocketOriginCheck){
wsCheckstate = "checked";
}
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="RequireBasicAuth" ${checkstate}>
<label>Require Basic Auth</label>
<label>Require Basic Auth</label>
</div>
<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
<div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;">
@ -165,6 +288,12 @@
Advance Configs
</div>
<div class="content">
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="SkipWebSocketOriginCheck" ${wsCheckstate}>
<label>Skip WebSocket Origin Check<br>
<small>Check this to allow cross-origin websocket requests</small></label>
</div>
<br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
<!-- <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editLoadBalanceOptions('${uuid}');"><i class="blue server icon"></i> Load Balance</button> -->
</div>
@ -187,7 +316,12 @@
<label>Allow plain HTTP access<br>
<small>Allow inbound connections without TLS/SSL</small></label>
</div><br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
`);
$(".hostAccessRuleSelector").dropdown();
}else{
//Unknown field. Leave it untouched
}
@ -215,7 +349,7 @@
let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
let bypassWebsocketOrigin = $(row).find(".SkipWebSocketOriginCheck")[0].checked;
console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
$.ajax({
@ -228,6 +362,7 @@
"bpgtls": bypassGlobalTLS,
"tls" :requireTLS,
"tlsval": skipCertValidations,
"bpwsorg" : bypassWebsocketOrigin,
"bauth" :requireBasicAuth,
},
success: function(data){
@ -250,6 +385,22 @@
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
}
function editAccessRule(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/hostAccessEditor.html?t=" + Date.now() + "#" + payload);
}
function editAliasHostnames(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/aliasEditor.html?t=" + Date.now() + "#" + payload);
}
function quickEditVdir(uuid){
openTabById("vdir");
$("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
@ -263,9 +414,32 @@
showSideWrapper("snippet/customHeaders.html?t=" + Date.now() + "#" + payload);
}
function editLoadBalanceOptions(uuid){
alert(uuid);
function handleProxyRuleToggle(object){
let endpointUUID = $(object).attr("eptuuid");
let isChecked = object.checked;
$.ajax({
url: "/api/proxy/toggle",
data: {
"ep": endpointUUID,
"enable": isChecked
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
if (isChecked){
msgbox("Proxy Rule Enabled");
}else{
msgbox("Proxy Rule Disabled");
}
}
}
})
}
/* Access List handling */
//Bind on tab switch events

View File

@ -84,7 +84,23 @@
Paste to Terminal <code style="float: right;">Shift + Insert</code>
</div>
</div>
<div class="ui divider"></div>
<h2>Forward Proxy</h2>
<p>Setup a basic HTTP forward proxy to access web server in another LAN<br>
To enable forward proxy in your domain, add a proxy rule to 127.0.0.1:{selected_port}</p>
<form class="ui form">
<div class="field">
<label>Listening Port</label>
<div class="ui action input">
<input id="forwardProxyPort" type="number" placeholder="5587" step="1", min="1024" max="65535" value="5587">
<button onclick="updateForwardProxyPort(); event.preventDefault();" class="ui basic button"><i class="ui green check icon"></i> Apply</button>
</div>
</div>
<div id="forwardProxyButtons" class="field">
<button onclick="toggleForwadProxy(true); event.preventDefault();" class="ui basic small green button startBtn"><i class="ui green arrow alternate circle up icon"></i> Start</button>
<button onclick="toggleForwadProxy(false); event.preventDefault();" class="ui basic small red button stopBtn"><i class="ui red minus circle icon"></i> Stop</button>
</div>
</form>
<div class="ui divider"></div>
<h2>Wake On LAN</h2>
<p>Wake up a remote server by WOL Magic Packet or an IoT device</p>
@ -558,6 +574,68 @@ function renderWhoisDomainTable(jsonData) {
}
//Forward Proxy
function initForwardProxyInfo(){
$.get("/api/tools/fwdproxy/enable", function(data){
if (data == true){
//Disable the start btn
$("#forwardProxyButtons").find(".startBtn").addClass('disabled');
$("#forwardProxyButtons").find(".stopBtn").removeClass('disabled');
}else{
$("#forwardProxyButtons").find(".startBtn").removeClass('disabled');
$("#forwardProxyButtons").find(".stopBtn").addClass('disabled');
}
});
$.get("/api/tools/fwdproxy/port", function(data){
$("#forwardProxyPort").val(data);
})
}
initForwardProxyInfo();
function toggleForwadProxy(enabled){
$.ajax({
url: "/api/tools/fwdproxy/enable",
method: "POST",
data: {
"enable": enabled
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox(`Forward proxy ${enabled?"enabled":"disabled"}`)
}
initForwardProxyInfo();
}
})
}
function updateForwardProxyPort(){
let newPortNumber = $("#forwardProxyPort").val();
if (newPortNumber < 1024 || newPortNumber > 65535){
$("#newPortNumber").parent().addClass('error');
}else{
$("#newPortNumber").parent().removeClass('error');
}
$.ajax({
url: "/api/tools/fwdproxy/port",
method: "POST",
data: {
"port": newPortNumber
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}
msgbox("Forward proxy port updated");
initForwardProxyInfo();
}
});
}
</script>

View File

@ -4,6 +4,7 @@
<h2>Redirection Rules</h2>
<p>Add exception case for redirecting any matching URLs</p>
</div>
<!-- Current list of redirection rules-->
<div style="width: 100%; overflow-x: auto;">
<table class="ui sortable unstackable celled table" >
<thead>
@ -28,6 +29,27 @@
<div class="ui green message" id="delRuleSucc" style="display:none;">
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
</div>
<!-- Options -->
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<div class="ui basic segment">
<div class="ui toggle checkbox">
<input id="redirectRegex" type="checkbox">
<label>Enable Regular Expression Support<br>
<small>Regular expression redirection check will noticeably slow down page load<br>
Support <a href="https://yourbasic.org/golang/regexp-cheat-sheet/" target="_blank">Go style regex</a>. e.g. <code style="background-color: rgb(44, 44, 44); color: white">.\.redirect\.example\.com</code></small></label>
</div>
</div>
</div>
</div>
</div>
<!-- Add New Redirection Rules -->
<div class="ui divider"></div>
<h4>Add Redirection Rule</h4>
<div class="ui form">
@ -76,12 +98,12 @@
</div>
</div>
<script>
/*
Redirection functions
*/
$(".checkbox").checkbox();
/*
Redirection functions
*/
$(".checkbox").checkbox();
$(".advanceSettings").accordion();
function resetForm() {
document.getElementById("rurl").value = "";
document.getElementsByName("destination-url")[0].value = "";
@ -149,7 +171,7 @@
<td><button onclick="deleteRule(this);" rurl="${encodeURIComponent(JSON.stringify(entry.RedirectURL))}" title="Delete redirection rule" class="ui mini red icon basic button"><i class="trash icon"></i></button></td>
</tr>`);
});
if (data.length == 0){
$("#redirectionRuleList").append(`<tr colspan="4"><td><i class="green check circle icon"></i> No redirection rule</td></tr>`);
}
@ -158,6 +180,34 @@
}
initRedirectionRuleList();
function initRegexpSupportToggle(){
$.get("/api/redirect/regex", function(data){
//Set the checkbox initial state
if (data == true){
$("#redirectRegex").parent().checkbox("set checked");
}else{
$("#redirectRegex").parent().checkbox("set unchecked");
}
//Bind event to the checkbox
$("#redirectRegex").on("change", function(){
$.ajax({
url: "/api/redirect/regex",
data: {"enable": $(this)[0].checked},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Regex redirect setting updated", true);
}
}
});
});
});
}
initRegexpSupportToggle();
$("#rurl").on('change', (event) => {
const value = event.target.value.trim().replace(/^(https?:\/\/)/, '');
event.target.value = value;

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