Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
4d6c79f51b | |||
2c045f4f40 | |||
b8cf046ca6 | |||
026dd6b89d | |||
5805fe6ed2 | |||
3c78211800 | |||
8e648a8e1f | |||
a000893dd1 | |||
db88bfb752 | |||
05297d854b | |||
0d7bce4d30 | |||
8db95dddc6 | |||
05daeded37 | |||
8ce6471be5 | |||
e242c9288f | |||
c55a29e7cf | |||
6af047430c | |||
200c924acd | |||
9b2168466c | |||
7ae48bf370 | |||
ee3d76fb96 | |||
40d192524b | |||
c659e05005 |
4
.github/workflows/main.yml
vendored
@ -22,7 +22,6 @@ jobs:
|
|||||||
- name: Login to Docker & GHCR
|
- name: Login to Docker & GHCR
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
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
|
- name: Setup building file structure
|
||||||
run: |
|
run: |
|
||||||
@ -39,7 +38,4 @@ jobs:
|
|||||||
--platform linux/amd64,linux/arm64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
|
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
|
||||||
--tag zoraxydocker/zoraxy:latest \
|
--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 \
|
|
||||||
.
|
.
|
||||||
|
40
CHANGELOG.md
@ -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
|
# 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)
|
+ Added opt-out for subdomains for global TLS settings: See [release notes](https://github.com/tobychui/zoraxy/releases/tag/2.6.8)
|
||||||
|
14
README.md
@ -9,15 +9,16 @@ General purpose request (reverse) proxy and forwarding tool for networking noobs
|
|||||||
### Features
|
### Features
|
||||||
|
|
||||||
- Simple to use interface with detail in-system instructions
|
- Simple to use interface with detail in-system instructions
|
||||||
- Reverse Proxy
|
- Reverse Proxy (HTTP/2)
|
||||||
- Virtual Directory
|
- Virtual Directory
|
||||||
|
- WebSocket Proxy (automatic, no set-up needed)
|
||||||
- Basic Auth
|
- Basic Auth
|
||||||
|
- Alias Hostnames
|
||||||
- Custom Headers
|
- Custom Headers
|
||||||
- Redirection Rules
|
- Redirection Rules
|
||||||
- TLS / SSL setup and deploy
|
- TLS / SSL setup and deploy
|
||||||
- ACME features like auto-renew to serve your sites in http**s**
|
- ACME features like auto-renew to serve your sites in http**s**
|
||||||
- SNI support (one certificate contains multiple host names)
|
- SNI support (one certificate contains multiple host names)
|
||||||
|
|
||||||
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||||
- Global Area Network Controller Web UI (ZeroTier not included)
|
- Global Area Network Controller Web UI (ZeroTier not included)
|
||||||
- TCP Tunneling / Proxy
|
- 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
|
- External permission management system for easy system integration
|
||||||
- SMTP config for password reset
|
- 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
|
## Build from Source
|
||||||
Requires Go 1.20 or higher
|
Requires Go 1.22 or higher
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/tobychui/zoraxy
|
git clone https://github.com/tobychui/zoraxy
|
||||||
|
BIN
docs/img/bg.png
Before Width: | Height: | Size: 4.5 MiB |
BIN
docs/img/bg2.png
Before Width: | Height: | Size: 9.4 MiB |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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
After Width: | Height: | Size: 202 KiB |
Before Width: | Height: | Size: 42 KiB |
BIN
docs/img/screenshots/10.png
Normal file
After Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 62 KiB |
BIN
docs/img/screenshots/2.png
Normal file
After Width: | Height: | Size: 146 KiB |
Before Width: | Height: | Size: 32 KiB |
BIN
docs/img/screenshots/3.png
Normal file
After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 28 KiB |
BIN
docs/img/screenshots/4.png
Normal file
After Width: | Height: | Size: 203 KiB |
Before Width: | Height: | Size: 41 KiB |
BIN
docs/img/screenshots/5.png
Normal file
After Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 46 KiB |
BIN
docs/img/screenshots/6.png
Normal file
After Width: | Height: | Size: 194 KiB |
Before Width: | Height: | Size: 48 KiB |
BIN
docs/img/screenshots/7.png
Normal file
After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 55 KiB |
BIN
docs/img/screenshots/8.png
Normal file
After Width: | Height: | Size: 185 KiB |
Before Width: | Height: | Size: 68 KiB |
BIN
docs/img/screenshots/9.png
Normal file
After Width: | Height: | Size: 867 KiB |
Before Width: | Height: | Size: 153 KiB |
@ -8,7 +8,7 @@
|
|||||||
<meta name="author" content="tobychui">
|
<meta name="author" content="tobychui">
|
||||||
|
|
||||||
<!-- HTML Meta Tags -->
|
<!-- 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">
|
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||||
|
|
||||||
<!-- Facebook Meta Tags -->
|
<!-- Facebook Meta Tags -->
|
||||||
@ -74,21 +74,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="right-content">
|
<div class="right-content">
|
||||||
<!-- Hero Banner Section -->
|
<!-- 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 class="headbanner"></div>
|
||||||
<div id="home" class="herotext">
|
<div id="home" class="herotext">
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
<div class="bannerHeaderWrapper">
|
<div class="bannerHeaderWrapper">
|
||||||
<h1 class="bannerHeader">Zoraxy</h1>
|
<h1 class="bannerHeader">Zoraxy</h1>
|
||||||
|
<div class="ui divider"></div><br>
|
||||||
<p class="bannerSubheader">All in one homelab network routing solution</p>
|
<p class="bannerSubheader">All in one homelab network routing solution</p>
|
||||||
</div>
|
</div>
|
||||||
<br><br>
|
<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>
|
<br><br>
|
||||||
<table class="ui very basic collapsing unstackable celled table">
|
<table class="ui very basic collapsing unstackable celled table">
|
||||||
<thead>
|
<thead>
|
||||||
@ -126,6 +121,22 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Features -->
|
<!-- Features -->
|
||||||
@ -240,34 +251,34 @@
|
|||||||
|
|
||||||
<div class="ui three column stackable grid">
|
<div class="ui three column stackable grid">
|
||||||
<div class="column">
|
<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>
|
||||||
<div class="column">
|
<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>
|
||||||
<div class="column">
|
<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>
|
||||||
<div class="column">
|
<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>
|
||||||
<div class="column">
|
<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>
|
||||||
<div class="column">
|
<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>
|
||||||
<div class="column">
|
<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>
|
||||||
<div class="column">
|
<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>
|
||||||
<div class="column">
|
<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>
|
||||||
<div class="column">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
135
docs/style.css
@ -1,5 +1,5 @@
|
|||||||
body{
|
body{
|
||||||
background: #f6f6f6 !important;
|
background: #ffffff !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
@ -18,7 +18,7 @@ body{
|
|||||||
.left-menu {
|
.left-menu {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
background-color: #ffffff;
|
background-color: #fcfcfc;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding-top: 1.5em;
|
padding-top: 1.5em;
|
||||||
}
|
}
|
||||||
@ -48,17 +48,19 @@ body{
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
border-bottom: 1px solid #f6f6f6;
|
border-bottom: 1px solid #f6f6f6;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-right: 0.4em solid var(--themeTextColor);
|
|
||||||
transition: border-left ease-in-out 0.1s, background-color ease-in-out 0.1s;
|
transition: border-left ease-in-out 0.1s, background-color ease-in-out 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item.active{
|
.menu-item.active{
|
||||||
border-right: 0.4em solid var(--themeSkyblueColorDecondary);
|
background: linear-gradient(60deg, rgba(84, 58, 183, 0.3) 0%, rgba(0, 172, 193, 0.3) 100%);
|
||||||
background-color: #f0f8ff;
|
}
|
||||||
|
|
||||||
|
.menu-item .item-icon{
|
||||||
|
fill: #fcfcfc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item:hover{
|
.menu-item:hover{
|
||||||
border-right: 0.4em solid var(--themeSkyblueColorDecondary);
|
background: rgba(35,35,35,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item img{
|
.menu-item img{
|
||||||
@ -69,18 +71,6 @@ body{
|
|||||||
|
|
||||||
|
|
||||||
/* Head banner */
|
/* 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{
|
.herotext{
|
||||||
padding-top: 15em;
|
padding-top: 15em;
|
||||||
@ -91,11 +81,13 @@ body{
|
|||||||
.bannerHeader{
|
.bannerHeader{
|
||||||
font-size: 8em;
|
font-size: 8em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bannerSubheader{
|
.bannerSubheader{
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
|
color: #ebebeb;
|
||||||
margin-top: -20px;
|
margin-top: -20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +96,21 @@ body{
|
|||||||
display: inline-block;
|
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 */
|
||||||
#features{
|
#features{
|
||||||
padding-top: 4em;
|
padding-top: 4em;
|
||||||
@ -173,56 +180,58 @@ body{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Decorative Animation */
|
/*
|
||||||
.dot-container {
|
Waves CSS
|
||||||
display: flex;
|
*/
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
#wavesWrapper{
|
||||||
height: 40px;
|
position: absolute;
|
||||||
position: absolute;
|
bottom: 0;
|
||||||
top: 2em;
|
width: 100%;
|
||||||
left: 2em;
|
left: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot:nth-child(1) {
|
.waves {
|
||||||
animation-delay: 0s;
|
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) {
|
.parallax > use {
|
||||||
animation-delay: 2s;
|
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
|
||||||
}
|
}
|
||||||
|
.parallax > use:nth-child(1) {
|
||||||
.dot:nth-child(4) {
|
animation-delay: -8s;
|
||||||
animation-delay: 3s;
|
animation-duration: 28s;
|
||||||
}
|
}
|
||||||
|
.parallax > use:nth-child(2) {
|
||||||
@keyframes dot-animation {
|
animation-delay: -12s;
|
||||||
0% {
|
animation-duration: 40s;
|
||||||
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(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;
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 74 KiB |
@ -3,9 +3,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
strip "github.com/grokify/html-strip-tags-go"
|
"github.com/google/uuid"
|
||||||
"imuslab.com/zoraxy/mod/geodb"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/access"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,6 +20,157 @@ import (
|
|||||||
banning / whitelist a specific IP address or country code
|
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
|
Blacklist Related
|
||||||
*/
|
*/
|
||||||
@ -28,11 +182,24 @@ func handleListBlacklisted(w http.ResponseWriter, r *http.Request) {
|
|||||||
bltype = "country"
|
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{}
|
resulst := []string{}
|
||||||
if bltype == "country" {
|
if bltype == "country" {
|
||||||
resulst = geodbStore.GetAllBlacklistedCountryCode()
|
resulst = rule.GetAllBlacklistedCountryCode()
|
||||||
} else if bltype == "ip" {
|
} else if bltype == "ip" {
|
||||||
resulst = geodbStore.GetAllBlacklistedIp()
|
resulst = rule.GetAllBlacklistedIp()
|
||||||
}
|
}
|
||||||
|
|
||||||
js, _ := json.Marshal(resulst)
|
js, _ := json.Marshal(resulst)
|
||||||
@ -47,7 +214,23 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
@ -59,7 +242,19 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
@ -71,7 +266,24 @@ func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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) {
|
func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -81,23 +293,46 @@ func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) {
|
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 {
|
if err != nil {
|
||||||
//Return the current enabled state
|
ruleID = "default"
|
||||||
currentEnabled := geodbStore.BlacklistEnabled
|
}
|
||||||
|
|
||||||
|
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)
|
js, _ := json.Marshal(currentEnabled)
|
||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
} else {
|
} else {
|
||||||
if enable == "true" {
|
if enable == "true" {
|
||||||
geodbStore.ToggleBlacklist(true)
|
rule.ToggleBlacklist(true)
|
||||||
} else if enable == "false" {
|
} else if enable == "false" {
|
||||||
geodbStore.ToggleBlacklist(false)
|
rule.ToggleBlacklist(false)
|
||||||
} else {
|
} else {
|
||||||
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
||||||
return
|
return
|
||||||
@ -117,11 +352,22 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
|
|||||||
bltype = "country"
|
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" {
|
if bltype == "country" {
|
||||||
resulst = geodbStore.GetAllWhitelistedCountryCode()
|
resulst = rule.GetAllWhitelistedCountryCode()
|
||||||
} else if bltype == "ip" {
|
} else if bltype == "ip" {
|
||||||
resulst = geodbStore.GetAllWhitelistedIp()
|
resulst = rule.GetAllWhitelistedIp()
|
||||||
}
|
}
|
||||||
|
|
||||||
js, _ := json.Marshal(resulst)
|
js, _ := json.Marshal(resulst)
|
||||||
@ -136,10 +382,22 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
comment, _ := utils.PostPara(r, "comment")
|
ruleID, err := utils.PostPara(r, "id")
|
||||||
comment = strip.StripTags(comment)
|
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)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
@ -151,7 +409,18 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
@ -163,10 +432,23 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
comment, _ := utils.PostPara(r, "comment")
|
ruleID, err := utils.PostPara(r, "id")
|
||||||
comment = strip.StripTags(comment)
|
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) {
|
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -176,23 +458,45 @@ func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
|
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 {
|
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
|
//Return the current enabled state
|
||||||
currentEnabled := geodbStore.WhitelistEnabled
|
currentEnabled := rule.WhitelistEnabled
|
||||||
js, _ := json.Marshal(currentEnabled)
|
js, _ := json.Marshal(currentEnabled)
|
||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
} else {
|
} else {
|
||||||
if enable == "true" {
|
if enable == "true" {
|
||||||
geodbStore.ToggleWhitelist(true)
|
rule.ToggleWhitelist(true)
|
||||||
} else if enable == "false" {
|
} else if enable == "false" {
|
||||||
geodbStore.ToggleWhitelist(false)
|
rule.ToggleWhitelist(false)
|
||||||
} else {
|
} else {
|
||||||
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
||||||
return
|
return
|
||||||
|
@ -38,7 +38,7 @@ func initACME() *acme.ACMEHandler {
|
|||||||
port = getRandomPort(30000)
|
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
|
// create the special routing rule for ACME
|
||||||
|
14
src/api.go
@ -47,8 +47,11 @@ func initAPIs() {
|
|||||||
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
|
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
|
||||||
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
|
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
|
||||||
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
||||||
|
authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet)
|
||||||
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
|
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
|
||||||
|
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
|
||||||
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
|
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
|
||||||
|
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
|
||||||
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
||||||
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
||||||
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
|
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
|
||||||
@ -84,7 +87,14 @@ func initAPIs() {
|
|||||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||||
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
||||||
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
||||||
|
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
|
||||||
|
|
||||||
|
//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
|
//Blacklist APIs
|
||||||
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
||||||
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
|
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
|
||||||
@ -92,7 +102,6 @@ func initAPIs() {
|
|||||||
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
|
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
|
||||||
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
|
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
|
||||||
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
|
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
|
||||||
|
|
||||||
//Whitelist APIs
|
//Whitelist APIs
|
||||||
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
|
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
|
||||||
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
|
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
|
||||||
@ -163,6 +172,8 @@ func initAPIs() {
|
|||||||
authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet)
|
authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet)
|
||||||
authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet)
|
authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet)
|
||||||
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
|
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
|
||||||
|
authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle)
|
||||||
|
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
|
||||||
|
|
||||||
//Account Reset
|
//Account Reset
|
||||||
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
||||||
@ -175,6 +186,7 @@ func initAPIs() {
|
|||||||
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
|
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
|
||||||
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
|
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
|
||||||
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
|
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/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
|
||||||
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
|
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
|
||||||
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
|
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
|
||||||
|
36
src/go.mod
@ -1,20 +1,34 @@
|
|||||||
module imuslab.com/zoraxy
|
module imuslab.com/zoraxy
|
||||||
|
|
||||||
go 1.16
|
go 1.21
|
||||||
|
|
||||||
|
toolchain go1.22.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/boltdb/bolt v1.3.1
|
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/go-ping/ping v1.1.0
|
||||||
github.com/google/uuid v1.3.1
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/sessions v1.2.1
|
github.com/gorilla/sessions v1.2.2
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.1
|
||||||
github.com/grandcat/zeroconf v1.0.0
|
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/likexian/whois v1.15.1
|
||||||
github.com/microcosm-cc/bluemonday v1.0.25
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
golang.org/x/net v0.14.0
|
golang.org/x/net v0.23.0
|
||||||
golang.org/x/sys v0.11.0
|
golang.org/x/sys v0.18.0
|
||||||
golang.org/x/text v0.12.0
|
golang.org/x/text v0.14.0
|
||||||
golang.org/x/tools v0.12.0 // indirect
|
)
|
||||||
|
|
||||||
|
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
@ -12,11 +12,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"imuslab.com/zoraxy/mod/access"
|
||||||
"imuslab.com/zoraxy/mod/acme"
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
"imuslab.com/zoraxy/mod/auth"
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||||
"imuslab.com/zoraxy/mod/email"
|
"imuslab.com/zoraxy/mod/email"
|
||||||
|
"imuslab.com/zoraxy/mod/forwardproxy"
|
||||||
"imuslab.com/zoraxy/mod/ganserv"
|
"imuslab.com/zoraxy/mod/ganserv"
|
||||||
"imuslab.com/zoraxy/mod/geodb"
|
"imuslab.com/zoraxy/mod/geodb"
|
||||||
"imuslab.com/zoraxy/mod/info/logger"
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
@ -49,7 +51,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
name = "Zoraxy"
|
name = "Zoraxy"
|
||||||
version = "3.0.0"
|
version = "3.0.2"
|
||||||
nodeUUID = "generic"
|
nodeUUID = "generic"
|
||||||
development = false //Set this to false to use embedded web fs
|
development = false //Set this to false to use embedded web fs
|
||||||
bootTime = time.Now().Unix()
|
bootTime = time.Now().Unix()
|
||||||
@ -68,7 +70,8 @@ var (
|
|||||||
tlsCertManager *tlscert.Manager //TLS / SSL management
|
tlsCertManager *tlscert.Manager //TLS / SSL management
|
||||||
redirectTable *redirection.RuleTable //Handle special redirection rule sets
|
redirectTable *redirection.RuleTable //Handle special redirection rule sets
|
||||||
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
|
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
|
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
|
||||||
statisticCollector *statistic.Collector //Collecting statistic from visitors
|
statisticCollector *statistic.Collector //Collecting statistic from visitors
|
||||||
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
|
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
|
||||||
@ -79,6 +82,7 @@ var (
|
|||||||
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
|
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
|
||||||
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
|
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
|
||||||
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
||||||
|
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
|
||||||
|
|
||||||
//Helper modules
|
//Helper modules
|
||||||
EmailSender *email.Sender //Email sender that handle email sending
|
EmailSender *email.Sender //Email sender that handle email sending
|
||||||
|
217
src/mod/access/access.go
Normal 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)
|
||||||
|
}
|
153
src/mod/access/accessRule.go
Normal 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
|
||||||
|
}
|
75
src/mod/access/blacklist.go
Normal 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
@ -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
@ -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
|
||||||
|
}
|
@ -9,6 +9,7 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@ -24,6 +25,7 @@ import (
|
|||||||
"github.com/go-acme/lego/v4/challenge/http01"
|
"github.com/go-acme/lego/v4/challenge/http01"
|
||||||
"github.com/go-acme/lego/v4/lego"
|
"github.com/go-acme/lego/v4/lego"
|
||||||
"github.com/go-acme/lego/v4/registration"
|
"github.com/go-acme/lego/v4/registration"
|
||||||
|
"imuslab.com/zoraxy/mod/database"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,6 +42,11 @@ type ACMEUser struct {
|
|||||||
key crypto.PrivateKey
|
key crypto.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EABConfig struct {
|
||||||
|
Kid string `json:"kid"`
|
||||||
|
HmacKey string `json:"HmacKey"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetEmail returns the email of the ACMEUser.
|
// GetEmail returns the email of the ACMEUser.
|
||||||
func (u *ACMEUser) GetEmail() string {
|
func (u *ACMEUser) GetEmail() string {
|
||||||
return u.Email
|
return u.Email
|
||||||
@ -59,13 +66,15 @@ func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
|
|||||||
type ACMEHandler struct {
|
type ACMEHandler struct {
|
||||||
DefaultAcmeServer string
|
DefaultAcmeServer string
|
||||||
Port string
|
Port string
|
||||||
|
Database *database.Database
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewACME creates a new ACMEHandler instance.
|
// 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{
|
return &ACMEHandler{
|
||||||
DefaultAcmeServer: acmeServer,
|
DefaultAcmeServer: acmeServer,
|
||||||
Port: port,
|
Port: port,
|
||||||
|
Database: database,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,10 +152,63 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New users will need to register
|
// New users will need to register
|
||||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
/*
|
||||||
if err != nil {
|
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||||
log.Println(err)
|
if err != nil {
|
||||||
return false, err
|
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
|
adminUser.Registration = reg
|
||||||
|
|
||||||
|
@ -373,3 +373,34 @@ func (a *AutoRenewer) saveRenewConfigToFile() error {
|
|||||||
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
|
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
|
||||||
return os.WriteFile(a.ConfigFilePath, js, 0775)
|
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)
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -6,8 +6,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"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
|
Special Routing Rules, bypass most of the limitations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//Check if there are external routing rule matches.
|
//Check if there are external routing rule matches.
|
||||||
//If yes, route them via external rr
|
//If yes, route them via external rr
|
||||||
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
|
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
|
||||||
if matchedRoutingRule != nil {
|
if matchedRoutingRule != nil {
|
||||||
//Matching routing rule found. Let the sub-router handle it
|
//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)
|
matchedRoutingRule.Route(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -48,14 +37,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
//Inject headers
|
//Inject headers
|
||||||
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
|
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
|
||||||
|
|
||||||
/*
|
|
||||||
General Access Check
|
|
||||||
*/
|
|
||||||
respWritten := h.handleAccessRouting(w, r)
|
|
||||||
if respWritten {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Redirection Routing
|
Redirection Routing
|
||||||
*/
|
*/
|
||||||
@ -66,19 +47,30 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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
|
domainOnly := r.Host
|
||||||
if strings.Contains(r.Host, ":") {
|
if strings.Contains(r.Host, ":") {
|
||||||
hostPath := strings.Split(r.Host, ":")
|
hostPath := strings.Split(r.Host, ":")
|
||||||
domainOnly = hostPath[0]
|
domainOnly = hostPath[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Host Routing
|
|
||||||
*/
|
|
||||||
|
|
||||||
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
|
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
|
||||||
if sep != nil && !sep.Disabled {
|
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 {
|
if sep.RequireBasicAuth {
|
||||||
err := h.handleBasicAuthRouting(w, r, sep)
|
err := h.handleBasicAuthRouting(w, r, sep)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -137,7 +129,6 @@ Once entered this routing segment, the root routing options will take over
|
|||||||
for the routing logic.
|
for the routing logic.
|
||||||
*/
|
*/
|
||||||
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
|
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
domainOnly := r.Host
|
domainOnly := r.Host
|
||||||
if strings.Contains(r.Host, ":") {
|
if strings.Contains(r.Host, ":") {
|
||||||
hostPath := strings.Split(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)
|
h.logRequest(r, false, 307, "root-redirect", domainOnly)
|
||||||
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
|
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
|
||||||
case DefaultSite_NotFoundPage:
|
case DefaultSite_NotFoundPage:
|
||||||
http.NotFound(w, r)
|
//Serve the not found page, use template if exists
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/blacklist.html"))
|
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.Write(page_forbidden)
|
w.Write(page_hosterror)
|
||||||
} else {
|
} else {
|
||||||
w.Write(template)
|
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
|
|
||||||
}
|
}
|
||||||
|
64
src/mod/dynamicproxy/access.go
Normal 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, ""
|
||||||
|
}
|
@ -16,6 +16,16 @@ import (
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
|
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 {
|
if len(pe.BasicAuthExceptionRules) > 0 {
|
||||||
//Check if the current path matches the exception rules
|
//Check if the current path matches the exception rules
|
||||||
for _, exceptionRule := range pe.BasicAuthExceptionRules {
|
for _, exceptionRule := range pe.BasicAuthExceptionRules {
|
||||||
@ -44,7 +54,6 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !matchingFound {
|
if !matchingFound {
|
||||||
h.logRequest(r, false, 401, "host", pe.Domain)
|
|
||||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||||
w.WriteHeader(401)
|
w.WriteHeader(401)
|
||||||
return errors.New("unauthorized")
|
return errors.New("unauthorized")
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package dpcore
|
package dpcore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@ -8,12 +9,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var onExitFlushLoop func()
|
|
||||||
|
|
||||||
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
||||||
// sends it to another server, proxying the response back to the
|
// sends it to another server, proxying the response back to the
|
||||||
// client, support http, also support https tunnel using http.hijacker
|
// client, support http, also support https tunnel using http.hijacker
|
||||||
@ -68,7 +66,12 @@ type requestCanceler interface {
|
|||||||
CancelRequest(req *http.Request)
|
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
|
targetQuery := target.RawQuery
|
||||||
director := func(req *http.Request) {
|
director := func(req *http.Request) {
|
||||||
req.URL.Scheme = target.Scheme
|
req.URL.Scheme = target.Scheme
|
||||||
@ -80,10 +83,6 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
|
|||||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
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
|
//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).MaxConnsPerHost = optimalConcurrentConnection * 2
|
||||||
thisTransporter.(*http.Transport).DisableCompression = true
|
thisTransporter.(*http.Transport).DisableCompression = true
|
||||||
|
|
||||||
if ignoreTLSVerification {
|
if dpcOptions.IgnoreTLSVerification {
|
||||||
//Ignore TLS certificate validation error
|
//Ignore TLS certificate validation error
|
||||||
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ReverseProxy{
|
return &ReverseProxy{
|
||||||
Director: director,
|
Director: director,
|
||||||
Prepender: prepender,
|
Prepender: prepender,
|
||||||
Verbal: false,
|
FlushInterval: dpcOptions.FlushInterval,
|
||||||
Transport: thisTransporter,
|
Verbal: false,
|
||||||
|
Transport: thisTransporter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,64 +178,66 @@ var hopHeaders = []string{
|
|||||||
//"Upgrade",
|
//"Upgrade",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
|
// Copy response from src to dst with given flush interval, reference from httputil.ReverseProxy
|
||||||
if p.FlushInterval != 0 {
|
func (p *ReverseProxy) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error {
|
||||||
if wf, ok := dst.(writeFlusher); ok {
|
var w io.Writer = dst
|
||||||
mlw := &maxLatencyWriter{
|
if flushInterval != 0 {
|
||||||
dst: wf,
|
mlw := &maxLatencyWriter{
|
||||||
latency: p.FlushInterval,
|
dst: dst,
|
||||||
done: make(chan bool),
|
flush: http.NewResponseController(dst).Flush,
|
||||||
}
|
latency: flushInterval,
|
||||||
|
|
||||||
go mlw.flushLoop()
|
|
||||||
defer mlw.stop()
|
|
||||||
dst = mlw
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
// Copy with given buffer size. Default to 64k
|
||||||
io.Writer
|
func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
|
||||||
http.Flusher
|
if len(buf) == 0 {
|
||||||
}
|
buf = make([]byte, 64*1024)
|
||||||
|
}
|
||||||
|
|
||||||
type maxLatencyWriter struct {
|
var written int64
|
||||||
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()
|
|
||||||
for {
|
for {
|
||||||
select {
|
nr, rerr := src.Read(buf)
|
||||||
case <-m.done:
|
if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
|
||||||
if onExitFlushLoop != nil {
|
p.logf("dpcore read error during body copy: %v", rerr)
|
||||||
onExitFlushLoop()
|
}
|
||||||
|
|
||||||
|
if nr > 0 {
|
||||||
|
nw, werr := dst.Write(buf[:nr])
|
||||||
|
if nw > 0 {
|
||||||
|
written += int64(nw)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
case <-t.C:
|
if werr != nil {
|
||||||
m.mu.Lock()
|
return written, werr
|
||||||
m.dst.Flush()
|
}
|
||||||
m.mu.Unlock()
|
|
||||||
|
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{}) {
|
func (p *ReverseProxy) logf(format string, args ...interface{}) {
|
||||||
if p.ErrorLog != nil {
|
if p.ErrorLog != nil {
|
||||||
p.ErrorLog.Printf(format, args...)
|
p.ErrorLog.Printf(format, args...)
|
||||||
@ -272,6 +274,14 @@ func removeHeaders(header http.Header, noCache bool) {
|
|||||||
header.Del("Cache-Control")
|
header.Del("Cache-Control")
|
||||||
header.Set("Cache-Control", "no-store")
|
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) {
|
func addXForwardedForHeader(req *http.Request) {
|
||||||
@ -290,8 +300,19 @@ func addXForwardedForHeader(req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Header.Get("X-Real-Ip") == "" {
|
if req.Header.Get("X-Real-Ip") == "" {
|
||||||
//Not exists. Fill it in with client IP
|
//Check if CF-Connecting-IP header exists
|
||||||
req.Header.Set("X-Real-Ip", clientIP)
|
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.
|
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
|
||||||
removeHeaders(res.Header, rrr.NoCache)
|
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 p.ModifyResponse != nil {
|
||||||
if err := p.ModifyResponse(res); err != nil {
|
if err := p.ModifyResponse(res); err != nil {
|
||||||
if p.Verbal {
|
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
|
//Custom header rewriter functions
|
||||||
if res.Header.Get("Location") != "" {
|
if res.Header.Get("Location") != "" {
|
||||||
locationRewrite := 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
|
// close now, instead of defer, to populate res.Trailer
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
copyHeader(rw.Header(), res.Trailer)
|
copyHeader(rw.Header(), res.Trailer)
|
||||||
|
38
src/mod/dynamicproxy/dpcore/flush.go
Normal 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 == "")
|
||||||
|
}
|
73
src/mod/dynamicproxy/dpcore/maxLatencyWriter.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -115,6 +115,28 @@ func (router *Router) StartProxyService() error {
|
|||||||
r.URL, _ = url.Parse(originalHostHeader)
|
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{
|
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||||
ProxyDomain: sep.Domain,
|
ProxyDomain: sep.Domain,
|
||||||
OriginalHost: originalHostHeader,
|
OriginalHost: originalHostHeader,
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"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/statistic"
|
||||||
"imuslab.com/zoraxy/mod/websocketproxy"
|
"imuslab.com/zoraxy/mod/websocketproxy"
|
||||||
)
|
)
|
||||||
@ -34,23 +34,45 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
|
|||||||
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
||||||
ep, ok := router.ProxyEndpoints.Load(hostname)
|
ep, ok := router.ProxyEndpoints.Load(hostname)
|
||||||
if ok {
|
if ok {
|
||||||
|
//Exact hit
|
||||||
targetSubdomainEndpoint = ep.(*ProxyEndpoint)
|
targetSubdomainEndpoint = ep.(*ProxyEndpoint)
|
||||||
|
if !targetSubdomainEndpoint.Disabled {
|
||||||
|
return targetSubdomainEndpoint
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//No hit. Try with wildcard
|
//No hit. Try with wildcard and alias
|
||||||
matchProxyEndpoints := []*ProxyEndpoint{}
|
matchProxyEndpoints := []*ProxyEndpoint{}
|
||||||
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
|
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
|
||||||
ep := v.(*ProxyEndpoint)
|
ep := v.(*ProxyEndpoint)
|
||||||
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
|
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//Continue
|
//Bad pattern. Skip this rule
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if match {
|
if match {
|
||||||
//targetSubdomainEndpoint = ep
|
//Wildcard matches. Skip checking alias
|
||||||
matchProxyEndpoints = append(matchProxyEndpoints, ep)
|
matchProxyEndpoints = append(matchProxyEndpoints, ep)
|
||||||
return true
|
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
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -114,7 +136,10 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
|||||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
|
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
|
||||||
}
|
}
|
||||||
h.logRequest(r, true, 101, "subdomain-websocket", target.Domain)
|
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)
|
wspHandler.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -178,7 +203,10 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
|||||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
|
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
|
||||||
}
|
}
|
||||||
h.logRequest(r, true, 101, "vdir-websocket", target.Domain)
|
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)
|
wspHandler.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -218,7 +246,7 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
|
|||||||
if h.Parent.Option.StatisticCollector != nil {
|
if h.Parent.Option.StatisticCollector != nil {
|
||||||
go func() {
|
go func() {
|
||||||
requestInfo := statistic.RequestInfo{
|
requestInfo := statistic.RequestInfo{
|
||||||
IpAddr: geodb.GetRequesterIP(r),
|
IpAddr: netutils.GetRequesterIP(r),
|
||||||
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
|
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
|
||||||
Succ: succ,
|
Succ: succ,
|
||||||
StatusCode: statusCode,
|
StatusCode: statusCode,
|
||||||
|
@ -2,19 +2,25 @@ package redirection
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RuleTable struct {
|
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
|
configPath string //The location where the redirection rules is stored
|
||||||
rules sync.Map //Store the redirection rules for this reverse proxy instance
|
rules sync.Map //Store the redirection rules for this reverse proxy instance
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RedirectRules struct {
|
type RedirectRules struct {
|
||||||
@ -24,10 +30,11 @@ type RedirectRules struct {
|
|||||||
StatusCode int //Status Code for redirection
|
StatusCode int //Status Code for redirection
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRuleTable(configPath string) (*RuleTable, error) {
|
func NewRuleTable(configPath string, allowRegex bool) (*RuleTable, error) {
|
||||||
thisRuleTable := RuleTable{
|
thisRuleTable := RuleTable{
|
||||||
rules: sync.Map{},
|
rules: sync.Map{},
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
|
AllowRegex: allowRegex,
|
||||||
}
|
}
|
||||||
//Load all the rules from the config path
|
//Load all the rules from the config path
|
||||||
if !utils.FileExists(configPath) {
|
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 "_"
|
// 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
|
// Create the full file path by joining the t.configPath with the filename
|
||||||
filepath := path.Join(t.configPath, 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 {
|
func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
|
||||||
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
// 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
|
// Create the full file path by joining the t.configPath with the filename
|
||||||
filepath := path.Join(t.configPath, filename)
|
filepath := path.Join(t.configPath, filename)
|
||||||
|
|
||||||
|
fmt.Println(redirectURL, filename, filepath)
|
||||||
// Check if the file exists
|
// Check if the file exists
|
||||||
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||||
return nil // File doesn't exist, nothing to delete
|
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
|
// Iterate through all the keys in the rules map
|
||||||
var targetRedirectionRule *RedirectRules = nil
|
var targetRedirectionRule *RedirectRules = nil
|
||||||
var maxMatch int = 0
|
var maxMatch int = 0
|
||||||
|
|
||||||
t.rules.Range(func(key interface{}, value interface{}) bool {
|
t.rules.Range(func(key interface{}, value interface{}) bool {
|
||||||
// Check if the requested URL starts with the key as a prefix
|
// Check if the requested URL starts with the key as a prefix
|
||||||
if strings.HasPrefix(requestedURL, key.(string)) {
|
if t.AllowRegex {
|
||||||
// This request URL matched the domain
|
//Regexp matching rule
|
||||||
if len(key.(string)) > maxMatch {
|
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))
|
maxMatch = len(key.(string))
|
||||||
targetRedirectionRule = value.(*RedirectRules)
|
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 true
|
||||||
})
|
})
|
||||||
|
|
||||||
return targetRedirectionRule
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -19,6 +19,9 @@ import (
|
|||||||
func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
|
func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
|
||||||
//Filter the tailing slash if any
|
//Filter the tailing slash if any
|
||||||
domain := endpoint.Domain
|
domain := endpoint.Domain
|
||||||
|
if len(domain) == 0 {
|
||||||
|
return nil, errors.New("invalid endpoint config")
|
||||||
|
}
|
||||||
if domain[len(domain)-1:] == "/" {
|
if domain[len(domain)-1:] == "/" {
|
||||||
domain = 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
|
//Create the proxy routing handler
|
||||||
proxy := dpcore.NewDynamicProxyCore(path, "", endpoint.SkipCertValidations)
|
proxy := dpcore.NewDynamicProxyCore(path, "", &dpcore.DpcoreOptions{
|
||||||
|
IgnoreTLSVerification: endpoint.SkipCertValidations,
|
||||||
|
})
|
||||||
endpoint.proxy = proxy
|
endpoint.proxy = proxy
|
||||||
endpoint.parent = router
|
endpoint.parent = router
|
||||||
|
|
||||||
//Prepare proxy routing hjandler for each of the virtual directories
|
//Prepare proxy routing hjandler for each of the virtual directories
|
||||||
for _, vdir := range endpoint.VirtualDirectories {
|
for _, vdir := range endpoint.VirtualDirectories {
|
||||||
domain := vdir.Domain
|
domain := vdir.Domain
|
||||||
|
if len(domain) == 0 {
|
||||||
|
//invalid vdir
|
||||||
|
continue
|
||||||
|
}
|
||||||
if domain[len(domain)-1:] == "/" {
|
if domain[len(domain)-1:] == "/" {
|
||||||
domain = domain[:len(domain)-1]
|
domain = domain[:len(domain)-1]
|
||||||
}
|
}
|
||||||
@ -69,7 +78,9 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
|
|||||||
return nil, err
|
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.proxy = proxy
|
||||||
vdir.parent = endpoint
|
vdir.parent = endpoint
|
||||||
}
|
}
|
||||||
|
157
src/mod/dynamicproxy/templates/hosterror.html
Normal 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>
|
@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/access"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||||
"imuslab.com/zoraxy/mod/geodb"
|
"imuslab.com/zoraxy/mod/geodb"
|
||||||
@ -34,7 +35,8 @@ type RouterOption struct {
|
|||||||
ForceHttpsRedirect bool //Force redirection of http to https endpoint
|
ForceHttpsRedirect bool //Force redirection of http to https endpoint
|
||||||
TlsManager *tlscert.Manager
|
TlsManager *tlscert.Manager
|
||||||
RedirectRuleTable *redirection.RuleTable
|
RedirectRuleTable *redirection.RuleTable
|
||||||
GeodbStore *geodb.Store //GeoIP blacklist and whitelist
|
GeodbStore *geodb.Store //GeoIP resolver
|
||||||
|
AccessController *access.Controller //Blacklist / whitelist controller
|
||||||
StatisticCollector *statistic.Collector
|
StatisticCollector *statistic.Collector
|
||||||
WebDirectory string //The static web server directory containing the templates folder
|
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
|
// A proxy endpoint record, a general interface for handling inbound routing
|
||||||
type ProxyEndpoint struct {
|
type ProxyEndpoint struct {
|
||||||
ProxyType int //The type of this proxy, see const def
|
ProxyType int //The type of this proxy, see const def
|
||||||
RootOrMatchingDomain string //Matching domain for host, also act as key
|
RootOrMatchingDomain string //Matching domain for host, also act as key
|
||||||
Domain string //Domain or IP to proxy to
|
MatchingDomainAlias []string //A list of domains that alias to this rule
|
||||||
|
Domain string //Domain or IP to proxy to
|
||||||
|
|
||||||
//TLS/SSL Related
|
//TLS/SSL Related
|
||||||
RequireTLS bool //Target domain require TLS
|
RequireTLS bool //Target domain require TLS
|
||||||
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
|
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
|
||||||
SkipCertValidations bool //Set to true to accept self signed certs
|
SkipCertValidations bool //Set to true to accept self signed certs
|
||||||
|
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
|
||||||
|
|
||||||
//Virtual Directories
|
//Virtual Directories
|
||||||
VirtualDirectories []*VirtualDirectoryEndpoint
|
VirtualDirectories []*VirtualDirectoryEndpoint
|
||||||
@ -110,13 +114,17 @@ type ProxyEndpoint struct {
|
|||||||
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
||||||
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
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
|
DefaultSiteOption int //Fallback routing logic options
|
||||||
DefaultSiteValue string //Fallback routing target, optional
|
DefaultSiteValue string //Fallback routing target, optional
|
||||||
|
|
||||||
Disabled bool //If the rule is disabled
|
|
||||||
//Internal Logic Elements
|
//Internal Logic Elements
|
||||||
parent *Router
|
parent *Router `json:"-"`
|
||||||
proxy *dpcore.ReverseProxy `json:"-"`
|
proxy *dpcore.ReverseProxy `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,4 +149,6 @@ Web Templates
|
|||||||
var (
|
var (
|
||||||
//go:embed templates/forbidden.html
|
//go:embed templates/forbidden.html
|
||||||
page_forbidden []byte
|
page_forbidden []byte
|
||||||
|
//go:embed templates/hosterror.html
|
||||||
|
page_hosterror []byte
|
||||||
)
|
)
|
||||||
|
25
src/mod/forwardproxy/cproxy/LICENSE.md
Normal 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.
|
109
src/mod/forwardproxy/cproxy/config.go
Normal 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 }
|
19
src/mod/forwardproxy/cproxy/default_client_connector.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
25
src/mod/forwardproxy/cproxy/default_dialer.go
Normal 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
|
||||||
|
}
|
9
src/mod/forwardproxy/cproxy/default_filter.go
Normal 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 }
|
56
src/mod/forwardproxy/cproxy/default_handler.go
Normal 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")
|
||||||
|
)
|
54
src/mod/forwardproxy/cproxy/default_proxy.go
Normal 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()
|
||||||
|
}
|
24
src/mod/forwardproxy/cproxy/default_server_connector.go
Normal 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)
|
||||||
|
}
|
32
src/mod/forwardproxy/cproxy/hostname_filter.go
Normal 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
|
||||||
|
}
|
26
src/mod/forwardproxy/cproxy/hostname_suffix_filter.go
Normal 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
|
||||||
|
}
|
67
src/mod/forwardproxy/cproxy/interfaces.go
Normal 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
|
||||||
|
)
|
24
src/mod/forwardproxy/cproxy/logging_initializer.go
Normal 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
|
||||||
|
}
|
36
src/mod/forwardproxy/cproxy/proxy_protocol_initializer.go
Normal 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"
|
18
src/mod/forwardproxy/cproxy/routing_dialer.go
Normal 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)
|
||||||
|
}
|
137
src/mod/forwardproxy/forwardproxy.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
@ -2,11 +2,10 @@ package geodb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
|
"imuslab.com/zoraxy/mod/netutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed geoipv4.csv
|
//go:embed geoipv4.csv
|
||||||
@ -16,12 +15,10 @@ var geoipv4 []byte //Geodb dataset for ipv4
|
|||||||
var geoipv6 []byte //Geodb dataset for ipv6
|
var geoipv6 []byte //Geodb dataset for ipv6
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
BlacklistEnabled bool
|
geodb [][]string //Parsed geodb list
|
||||||
WhitelistEnabled bool
|
geodbIpv6 [][]string //Parsed geodb list for ipv6
|
||||||
geodb [][]string //Parsed geodb list
|
geotrie *trie
|
||||||
geodbIpv6 [][]string //Parsed geodb list for ipv6
|
geotrieIpv6 *trie
|
||||||
geotrie *trie
|
|
||||||
geotrieIpv6 *trie
|
|
||||||
//geoipCache sync.Map
|
//geoipCache sync.Map
|
||||||
sysdb *database.Database
|
sysdb *database.Database
|
||||||
option *StoreOptions
|
option *StoreOptions
|
||||||
@ -48,40 +45,6 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
|
|||||||
return nil, err
|
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
|
var ipv4Trie *trie
|
||||||
if !option.AllowSlowIpv4LookUp {
|
if !option.AllowSlowIpv4LookUp {
|
||||||
ipv4Trie = constrctTrieTree(parsedGeoData)
|
ipv4Trie = constrctTrieTree(parsedGeoData)
|
||||||
@ -93,27 +56,15 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Store{
|
return &Store{
|
||||||
BlacklistEnabled: blacklistEnabled,
|
geodb: parsedGeoData,
|
||||||
WhitelistEnabled: whitelistEnabled,
|
geotrie: ipv4Trie,
|
||||||
geodb: parsedGeoData,
|
geodbIpv6: parsedGeoDataIpv6,
|
||||||
geotrie: ipv4Trie,
|
geotrieIpv6: ipv6Trie,
|
||||||
geodbIpv6: parsedGeoDataIpv6,
|
sysdb: sysdb,
|
||||||
geotrieIpv6: ipv6Trie,
|
option: option,
|
||||||
sysdb: sysdb,
|
|
||||||
option: option,
|
|
||||||
}, nil
|
}, 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) {
|
func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
|
||||||
cc := s.search(ipstring)
|
cc := s.search(ipstring)
|
||||||
return &CountryInfo{
|
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 {
|
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
|
||||||
ipAddr := GetRequesterIP(r)
|
ipAddr := netutils.GetRequesterIP(r)
|
||||||
if ipAddr == "" {
|
if ipAddr == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import (
|
|||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/netutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Store) search(ip string) string {
|
func (s *Store) search(ip string) string {
|
||||||
@ -24,7 +26,7 @@ func (s *Store) search(ip string) string {
|
|||||||
|
|
||||||
//Search in geotrie tree
|
//Search in geotrie tree
|
||||||
cc := ""
|
cc := ""
|
||||||
if IsIPv6(ip) {
|
if netutils.IsIPv6(ip) {
|
||||||
if s.geotrieIpv6 == nil {
|
if s.geotrieIpv6 == nil {
|
||||||
cc = s.slowSearchIpv6(ip)
|
cc = s.slowSearchIpv6(ip)
|
||||||
} else {
|
} else {
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
package geodb
|
package netutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
@ -6,7 +6,13 @@ import (
|
|||||||
"strings"
|
"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 {
|
func GetRequesterIP(r *http.Request) string {
|
||||||
ip := r.Header.Get("X-Real-Ip")
|
ip := r.Header.Get("X-Real-Ip")
|
||||||
if ip == "" {
|
if ip == "" {
|
@ -85,10 +85,6 @@ func NewReverseProxy(target *url.URL) *ReverseProxy {
|
|||||||
} else {
|
} else {
|
||||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
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}
|
return &ReverseProxy{Director: director, Verbal: false}
|
||||||
|
@ -85,7 +85,10 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
|
|||||||
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||||
requestURL = strings.TrimPrefix(requestURL, "/")
|
requestURL = strings.TrimPrefix(requestURL, "/")
|
||||||
u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + 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)
|
wspHandler.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import "strconv"
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
func StringToInt64(number string) (int64, error) {
|
func StringToInt64(number string) (int64, error) {
|
||||||
i, err := strconv.ParseInt(number, 10, 64)
|
i, err := strconv.ParseInt(number, 10, 64)
|
||||||
@ -14,3 +17,36 @@ func Int64ToString(number int64) string {
|
|||||||
convedNumber := strconv.FormatInt(number, 10)
|
convedNumber := strconv.FormatInt(number, 10)
|
||||||
return convedNumber
|
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
|
||||||
|
}
|
||||||
|
@ -47,19 +47,26 @@ type WebsocketProxy struct {
|
|||||||
// If nil, DefaultDialer is used.
|
// If nil, DefaultDialer is used.
|
||||||
Dialer *websocket.Dialer
|
Dialer *websocket.Dialer
|
||||||
|
|
||||||
Verbal bool
|
Verbal bool
|
||||||
SkipTlsValidation 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
|
// ProxyHandler returns a new http.Handler interface that reverse proxies the
|
||||||
// request to the given target.
|
// request to the given target.
|
||||||
func ProxyHandler(target *url.URL, skipTlsValidation bool) http.Handler {
|
func ProxyHandler(target *url.URL, options Options) http.Handler {
|
||||||
return NewProxy(target, skipTlsValidation)
|
return NewProxy(target, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProxy returns a new Websocket reverse proxy that rewrites the
|
// NewProxy returns a new Websocket reverse proxy that rewrites the
|
||||||
// URL's to the scheme, host and base path provider in target.
|
// 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 {
|
backend := func(r *http.Request) *url.URL {
|
||||||
// Shallow copy
|
// Shallow copy
|
||||||
u := *target
|
u := *target
|
||||||
@ -68,7 +75,7 @@ func NewProxy(target *url.URL, skipTlsValidation bool) *WebsocketProxy {
|
|||||||
u.RawQuery = r.URL.RawQuery
|
u.RawQuery = r.URL.RawQuery
|
||||||
return &u
|
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.
|
// 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
|
dialer := w.Dialer
|
||||||
if w.Dialer == nil {
|
if w.Dialer == nil {
|
||||||
if w.SkipTlsValidation {
|
if w.Options.SkipTLSValidation {
|
||||||
//Disable TLS secure check if target allow skip verification
|
//Disable TLS secure check if target allow skip verification
|
||||||
bypassDialer := websocket.DefaultDialer
|
bypassDialer := websocket.DefaultDialer
|
||||||
bypassDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
bypassDialer.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||||
@ -171,6 +178,13 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|||||||
upgrader = DefaultUpgrader
|
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.
|
// Only pass those headers to the upgrader.
|
||||||
upgradeHeader := http.Header{}
|
upgradeHeader := http.Header{}
|
||||||
if hdr := resp.Header.Get("Sec-Websocket-Protocol"); hdr != "" {
|
if hdr := resp.Header.Get("Sec-Websocket-Protocol"); hdr != "" {
|
||||||
|
@ -28,7 +28,10 @@ func TestProxy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
u, _ := url.Parse(backendURL)
|
u, _ := url.Parse(backendURL)
|
||||||
proxy := NewProxy(u, false)
|
proxy := NewProxy(u, Options{
|
||||||
|
SkipTLSValidation: false,
|
||||||
|
SkipOriginCheck: false,
|
||||||
|
})
|
||||||
proxy.Upgrader = upgrader
|
proxy.Upgrader = upgrader
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
@ -15,12 +16,14 @@ import (
|
|||||||
related to redirection function in the reverse proxy
|
related to redirection function in the reverse proxy
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Handle request for listing all stored redirection rules
|
||||||
func handleListRedirectionRules(w http.ResponseWriter, r *http.Request) {
|
func handleListRedirectionRules(w http.ResponseWriter, r *http.Request) {
|
||||||
rules := redirectTable.GetAllRedirectRules()
|
rules := redirectTable.GetAllRedirectRules()
|
||||||
js, _ := json.Marshal(rules)
|
js, _ := json.Marshal(rules)
|
||||||
utils.SendJSONResponse(w, string(js))
|
utils.SendJSONResponse(w, string(js))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle request for adding new redirection rule
|
||||||
func handleAddRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
func handleAddRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
||||||
redirectUrl, err := utils.PostPara(r, "redirectUrl")
|
redirectUrl, err := utils.PostPara(r, "redirectUrl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -58,6 +61,7 @@ func handleAddRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle remove of a given redirection rule
|
||||||
func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
||||||
redirectUrl, err := utils.PostPara(r, "redirectUrl")
|
redirectUrl, err := utils.PostPara(r, "redirectUrl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -73,3 +77,30 @@ func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
utils.SendOK(w)
|
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)
|
||||||
|
}
|
||||||
|
@ -94,6 +94,7 @@ func ReverseProxtInit() {
|
|||||||
GeodbStore: geodbStore,
|
GeodbStore: geodbStore,
|
||||||
StatisticCollector: statisticCollector,
|
StatisticCollector: statisticCollector,
|
||||||
WebDirectory: *staticWebServerRoot,
|
WebDirectory: *staticWebServerRoot,
|
||||||
|
AccessController: accessController,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err)
|
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")
|
useTLS := (tls == "true")
|
||||||
|
|
||||||
|
//Bypass global TLS value / allow direct access from port 80?
|
||||||
bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS")
|
bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS")
|
||||||
if bypassGlobalTLS == "" {
|
if bypassGlobalTLS == "" {
|
||||||
bypassGlobalTLS = "false"
|
bypassGlobalTLS = "false"
|
||||||
@ -201,6 +203,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
useBypassGlobalTLS := bypassGlobalTLS == "true"
|
useBypassGlobalTLS := bypassGlobalTLS == "true"
|
||||||
|
|
||||||
|
//Enable TLS validation?
|
||||||
stv, _ := utils.PostPara(r, "tlsval")
|
stv, _ := utils.PostPara(r, "tlsval")
|
||||||
if stv == "" {
|
if stv == "" {
|
||||||
stv = "false"
|
stv = "false"
|
||||||
@ -208,6 +211,17 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
skipTlsValidation := (stv == "true")
|
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")
|
rba, _ := utils.PostPara(r, "bauth")
|
||||||
if rba == "" {
|
if rba == "" {
|
||||||
rba = "false"
|
rba = "false"
|
||||||
@ -215,6 +229,13 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
requireBasicAuth := (rba == "true")
|
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
|
//Prase the basic auth to correct structure
|
||||||
cred, _ := utils.PostPara(r, "cred")
|
cred, _ := utils.PostPara(r, "cred")
|
||||||
basicAuthCredentials := []*dynamicproxy.BasicAuthCredentials{}
|
basicAuthCredentials := []*dynamicproxy.BasicAuthCredentials{}
|
||||||
@ -247,18 +268,37 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
if eptype == "host" {
|
if eptype == "host" {
|
||||||
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.SendErrorResponse(w, "subdomain not defined")
|
utils.SendErrorResponse(w, "hostname not defined")
|
||||||
return
|
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{
|
thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
|
||||||
//I/O
|
//I/O
|
||||||
ProxyType: dynamicproxy.ProxyType_Host,
|
ProxyType: dynamicproxy.ProxyType_Host,
|
||||||
RootOrMatchingDomain: rootOrMatchingDomain,
|
RootOrMatchingDomain: rootOrMatchingDomain,
|
||||||
|
MatchingDomainAlias: aliasHostnames,
|
||||||
Domain: endpoint,
|
Domain: endpoint,
|
||||||
//TLS
|
//TLS
|
||||||
RequireTLS: useTLS,
|
RequireTLS: useTLS,
|
||||||
BypassGlobalTLS: useBypassGlobalTLS,
|
BypassGlobalTLS: useBypassGlobalTLS,
|
||||||
SkipCertValidations: skipTlsValidation,
|
SkipCertValidations: skipTlsValidation,
|
||||||
|
SkipWebSocketOriginCheck: bypassWebsocketOriginCheck,
|
||||||
|
AccessFilterUUID: accessRuleID,
|
||||||
//VDir
|
//VDir
|
||||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||||
//Custom headers
|
//Custom headers
|
||||||
@ -305,12 +345,13 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
//Write the root options to file
|
//Write the root options to file
|
||||||
rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{
|
rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{
|
||||||
ProxyType: dynamicproxy.ProxyType_Root,
|
ProxyType: dynamicproxy.ProxyType_Root,
|
||||||
RootOrMatchingDomain: "/",
|
RootOrMatchingDomain: "/",
|
||||||
Domain: endpoint,
|
Domain: endpoint,
|
||||||
RequireTLS: useTLS,
|
RequireTLS: useTLS,
|
||||||
BypassGlobalTLS: false,
|
BypassGlobalTLS: false,
|
||||||
SkipCertValidations: false,
|
SkipCertValidations: false,
|
||||||
|
SkipWebSocketOriginCheck: true,
|
||||||
|
|
||||||
DefaultSiteOption: defaultSiteOption,
|
DefaultSiteOption: defaultSiteOption,
|
||||||
DefaultSiteValue: dsVal,
|
DefaultSiteValue: dsVal,
|
||||||
@ -381,6 +422,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
bypassGlobalTLS := (bpgtls == "true")
|
bypassGlobalTLS := (bpgtls == "true")
|
||||||
|
|
||||||
|
// Basic Auth
|
||||||
rba, _ := utils.PostPara(r, "bauth")
|
rba, _ := utils.PostPara(r, "bauth")
|
||||||
if rba == "" {
|
if rba == "" {
|
||||||
rba = "false"
|
rba = "false"
|
||||||
@ -388,6 +430,13 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
requireBasicAuth := (rba == "true")
|
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
|
//Load the previous basic auth credentials from current proxy rules
|
||||||
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
|
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -402,6 +451,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
|
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
|
||||||
newProxyEndpoint.SkipCertValidations = skipTlsValidation
|
newProxyEndpoint.SkipCertValidations = skipTlsValidation
|
||||||
newProxyEndpoint.RequireBasicAuth = requireBasicAuth
|
newProxyEndpoint.RequireBasicAuth = requireBasicAuth
|
||||||
|
newProxyEndpoint.SkipWebSocketOriginCheck = bypassWebsocketOriginCheck
|
||||||
|
|
||||||
//Prepare to replace the current routing rule
|
//Prepare to replace the current routing rule
|
||||||
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
|
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
|
||||||
@ -421,6 +471,62 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendOK(w)
|
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) {
|
func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
ep, err := utils.GetPara(r, "ep")
|
ep, err := utils.GetPara(r, "ep")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -684,11 +790,73 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
|
|||||||
utils.SendOK(w)
|
utils.SendOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Report the current status of the reverse proxy server
|
||||||
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
|
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
js, _ := json.Marshal(dynamicProxyRouter)
|
js, _ := json.Marshal(dynamicProxyRouter)
|
||||||
utils.SendJSONResponse(w, string(js))
|
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) {
|
func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
||||||
eptype, err := utils.PostPara(r, "type") //Support root and host
|
eptype, err := utils.PostPara(r, "type") //Support root and host
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -845,7 +1013,7 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
proxyRoot := strings.TrimSuffix(dynamicProxyRouter.Root.Domain, "/")
|
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
|
//Listening port is same as proxy root
|
||||||
//Not allow recursive settings
|
//Not allow recursive settings
|
||||||
utils.SendErrorResponse(w, "Recursive listening port! Check your proxy root settings.")
|
utils.SendErrorResponse(w, "Recursive listening port! Check your proxy root settings.")
|
||||||
|
33
src/start.go
@ -8,10 +8,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/access"
|
||||||
"imuslab.com/zoraxy/mod/acme"
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
"imuslab.com/zoraxy/mod/auth"
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||||
|
"imuslab.com/zoraxy/mod/forwardproxy"
|
||||||
"imuslab.com/zoraxy/mod/ganserv"
|
"imuslab.com/zoraxy/mod/ganserv"
|
||||||
"imuslab.com/zoraxy/mod/geodb"
|
"imuslab.com/zoraxy/mod/geodb"
|
||||||
"imuslab.com/zoraxy/mod/info/logger"
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
@ -72,10 +74,14 @@ func startupSequence() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Create a redirection rule table
|
//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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
redirectTable.Logger = SystemWideLogger
|
||||||
|
|
||||||
//Create a geodb store
|
//Create a geodb store
|
||||||
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
|
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
|
||||||
@ -86,6 +92,16 @@ func startupSequence() {
|
|||||||
panic(err)
|
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
|
//Create a statistic collector
|
||||||
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
||||||
Database: sysdb,
|
Database: sysdb,
|
||||||
@ -206,7 +222,7 @@ func startupSequence() {
|
|||||||
//Create TCP Proxy Manager
|
//Create TCP Proxy Manager
|
||||||
tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{
|
tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{
|
||||||
Database: sysdb,
|
Database: sysdb,
|
||||||
AccessControlHandler: geodbStore.AllowConnectionAccess,
|
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
|
||||||
})
|
})
|
||||||
|
|
||||||
//Create WoL MAC storage table
|
//Create WoL MAC storage table
|
||||||
@ -219,6 +235,18 @@ func startupSequence() {
|
|||||||
//Create an analytic loader
|
//Create an analytic loader
|
||||||
AnalyticLoader = analytic.NewDataLoader(sysdb, statisticCollector)
|
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
|
ACME API
|
||||||
|
|
||||||
@ -241,4 +269,5 @@ func finalSequence() {
|
|||||||
|
|
||||||
//Inject routing rules
|
//Inject routing rules
|
||||||
registerBuildInRoutingRules()
|
registerBuildInRoutingRules()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -237,7 +237,7 @@
|
|||||||
msgbox("Certificate installed successfully");
|
msgbox("Certificate installed successfully");
|
||||||
|
|
||||||
if (callback != undefined){
|
if (callback != undefined){
|
||||||
callback(false);
|
callback(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3,7 +3,16 @@
|
|||||||
<h2>HTTP Proxy</h2>
|
<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>
|
<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>
|
||||||
<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">
|
<table class="ui celled sortable unstackable compact table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -11,7 +20,7 @@
|
|||||||
<th>Destination</th>
|
<th>Destination</th>
|
||||||
<th>Virtual Directory</th>
|
<th>Virtual Directory</th>
|
||||||
<th>Basic Auth</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="httpProxyList">
|
<tbody id="httpProxyList">
|
||||||
@ -25,6 +34,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
/* List all proxy endpoints */
|
||||||
function listProxyEndpoints(){
|
function listProxyEndpoints(){
|
||||||
$.get("/api/proxy/list?type=host", function(data){
|
$.get("/api/proxy/list?type=host", function(data){
|
||||||
$("#httpProxyList").html(``);
|
$("#httpProxyList").html(``);
|
||||||
@ -37,6 +48,8 @@
|
|||||||
<td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td>
|
<td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td>
|
||||||
</tr>`);
|
</tr>`);
|
||||||
}else{
|
}else{
|
||||||
|
//Sort by RootOrMatchingDomain field
|
||||||
|
data.sort((a,b) => (a.RootOrMatchingDomain > b.RootOrMatchingDomain) ? 1 : ((b.RootOrMatchingDomain > a.RootOrMatchingDomain) ? -1 : 0))
|
||||||
data.forEach(subd => {
|
data.forEach(subd => {
|
||||||
let tlsIcon = "";
|
let tlsIcon = "";
|
||||||
let subdData = encodeURIComponent(JSON.stringify(subd));
|
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>`;
|
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">
|
$("#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="domain">${subd.Domain} ${tlsIcon}</td>
|
||||||
<td data-label="" editable="true" datatype="vdir">${vdList}</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 data-label="" editable="true" datatype="basicauth">
|
||||||
<td class="center aligned" editable="true" datatype="action" data-label="">
|
${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}
|
||||||
<button class="ui circular mini basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="edit icon"></i></button>
|
</td>
|
||||||
<button class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button>
|
<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>
|
</td>
|
||||||
</tr>`);
|
</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
|
Inline editor for httprp.html
|
||||||
@ -153,9 +269,16 @@
|
|||||||
if (requireBasicAuth){
|
if (requireBasicAuth){
|
||||||
checkstate = "checked";
|
checkstate = "checked";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let skipWebSocketOriginCheck = payload.SkipWebSocketOriginCheck;
|
||||||
|
let wsCheckstate = "";
|
||||||
|
if (skipWebSocketOriginCheck){
|
||||||
|
wsCheckstate = "checked";
|
||||||
|
}
|
||||||
|
|
||||||
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
|
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
<input type="checkbox" class="RequireBasicAuth" ${checkstate}>
|
<input type="checkbox" class="RequireBasicAuth" ${checkstate}>
|
||||||
<label>Require Basic Auth</label>
|
<label>Require Basic Auth</label>
|
||||||
</div>
|
</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>
|
<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;">
|
<div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;">
|
||||||
@ -165,6 +288,12 @@
|
|||||||
Advance Configs
|
Advance Configs
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<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="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> -->
|
<!-- <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>
|
</div>
|
||||||
@ -187,7 +316,12 @@
|
|||||||
<label>Allow plain HTTP access<br>
|
<label>Allow plain HTTP access<br>
|
||||||
<small>Allow inbound connections without TLS/SSL</small></label>
|
<small>Allow inbound connections without TLS/SSL</small></label>
|
||||||
</div><br>
|
</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{
|
}else{
|
||||||
//Unknown field. Leave it untouched
|
//Unknown field. Leave it untouched
|
||||||
}
|
}
|
||||||
@ -215,7 +349,7 @@
|
|||||||
let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
|
let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
|
||||||
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
|
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
|
||||||
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
|
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
|
||||||
|
let bypassWebsocketOrigin = $(row).find(".SkipWebSocketOriginCheck")[0].checked;
|
||||||
console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
|
console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@ -228,6 +362,7 @@
|
|||||||
"bpgtls": bypassGlobalTLS,
|
"bpgtls": bypassGlobalTLS,
|
||||||
"tls" :requireTLS,
|
"tls" :requireTLS,
|
||||||
"tlsval": skipCertValidations,
|
"tlsval": skipCertValidations,
|
||||||
|
"bpwsorg" : bypassWebsocketOrigin,
|
||||||
"bauth" :requireBasicAuth,
|
"bauth" :requireBasicAuth,
|
||||||
},
|
},
|
||||||
success: function(data){
|
success: function(data){
|
||||||
@ -250,6 +385,22 @@
|
|||||||
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
|
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){
|
function quickEditVdir(uuid){
|
||||||
openTabById("vdir");
|
openTabById("vdir");
|
||||||
$("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
|
$("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
|
||||||
@ -263,9 +414,32 @@
|
|||||||
showSideWrapper("snippet/customHeaders.html?t=" + Date.now() + "#" + payload);
|
showSideWrapper("snippet/customHeaders.html?t=" + Date.now() + "#" + payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
function editLoadBalanceOptions(uuid){
|
function handleProxyRuleToggle(object){
|
||||||
alert(uuid);
|
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
|
//Bind on tab switch events
|
||||||
|
@ -84,7 +84,23 @@
|
|||||||
Paste to Terminal <code style="float: right;">Shift + Insert</code>
|
Paste to Terminal <code style="float: right;">Shift + Insert</code>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<div class="ui divider"></div>
|
||||||
<h2>Wake On LAN</h2>
|
<h2>Wake On LAN</h2>
|
||||||
<p>Wake up a remote server by WOL Magic Packet or an IoT device</p>
|
<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>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
<h2>Redirection Rules</h2>
|
<h2>Redirection Rules</h2>
|
||||||
<p>Add exception case for redirecting any matching URLs</p>
|
<p>Add exception case for redirecting any matching URLs</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Current list of redirection rules-->
|
||||||
<div style="width: 100%; overflow-x: auto;">
|
<div style="width: 100%; overflow-x: auto;">
|
||||||
<table class="ui sortable unstackable celled table" >
|
<table class="ui sortable unstackable celled table" >
|
||||||
<thead>
|
<thead>
|
||||||
@ -28,6 +29,27 @@
|
|||||||
<div class="ui green message" id="delRuleSucc" style="display:none;">
|
<div class="ui green message" id="delRuleSucc" style="display:none;">
|
||||||
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
|
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
|
||||||
</div>
|
</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>
|
<div class="ui divider"></div>
|
||||||
<h4>Add Redirection Rule</h4>
|
<h4>Add Redirection Rule</h4>
|
||||||
<div class="ui form">
|
<div class="ui form">
|
||||||
@ -76,12 +98,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
/*
|
||||||
/*
|
Redirection functions
|
||||||
Redirection functions
|
*/
|
||||||
*/
|
|
||||||
$(".checkbox").checkbox();
|
|
||||||
|
|
||||||
|
$(".checkbox").checkbox();
|
||||||
|
$(".advanceSettings").accordion();
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
document.getElementById("rurl").value = "";
|
document.getElementById("rurl").value = "";
|
||||||
document.getElementsByName("destination-url")[0].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>
|
<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>`);
|
</tr>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.length == 0){
|
if (data.length == 0){
|
||||||
$("#redirectionRuleList").append(`<tr colspan="4"><td><i class="green check circle icon"></i> No redirection rule</td></tr>`);
|
$("#redirectionRuleList").append(`<tr colspan="4"><td><i class="green check circle icon"></i> No redirection rule</td></tr>`);
|
||||||
}
|
}
|
||||||
@ -158,6 +180,34 @@
|
|||||||
}
|
}
|
||||||
initRedirectionRuleList();
|
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) => {
|
$("#rurl").on('change', (event) => {
|
||||||
const value = event.target.value.trim().replace(/^(https?:\/\/)/, '');
|
const value = event.target.value.trim().replace(/^(https?:\/\/)/, '');
|
||||||
event.target.value = value;
|
event.target.value = value;
|
||||||
|