Compare commits
77 Commits
Author | SHA1 | Date | |
---|---|---|---|
fe48a9a0c3 | |||
ec973eb3bc | |||
7b69b5fa63 | |||
ce4f46cb50 | |||
3454a9b975 | |||
55bc939a37 | |||
1d63b679dc | |||
3df96350a3 | |||
34fab7b3d0 | |||
46817d0664 | |||
1db2ca61fa | |||
0b601406de | |||
b4c771cdee | |||
a486d42351 | |||
90c2199a1b | |||
161c61fac7 | |||
5ffacb1d06 | |||
75ebd0ffbe | |||
dc069f3c57 | |||
e1b512f78f | |||
8854a38f49 | |||
7583a4628c | |||
73c0ea0896 | |||
7dad7c7305 | |||
faa95b4e21 | |||
cb0e13976d | |||
ccd8dcff56 | |||
750656fd7f | |||
d9f515fdba | |||
176249a7d9 | |||
e2a449a7bc | |||
a9695e969e | |||
7ba997dfc2 | |||
d00117e878 | |||
43a84a3f1c | |||
e24f31bdef | |||
fc9240fbac | |||
e0f5431215 | |||
de658a3c6c | |||
73276b1918 | |||
abdb7d4d75 | |||
72299ace15 | |||
4d6c79f51b | |||
2c045f4f40 | |||
b8cf046ca6 | |||
026dd6b89d | |||
5805fe6ed2 | |||
3c78211800 | |||
8e648a8e1f | |||
a000893dd1 | |||
db88bfb752 | |||
05297d854b | |||
0d7bce4d30 | |||
8db95dddc6 | |||
05daeded37 | |||
8ce6471be5 | |||
e242c9288f | |||
c55a29e7cf | |||
6af047430c | |||
200c924acd | |||
9b2168466c | |||
7ae48bf370 | |||
ee3d76fb96 | |||
40d192524b | |||
c659e05005 | |||
676a45c222 | |||
1da0761b13 | |||
32939874f2 | |||
43a4bf389a | |||
33c7c5fa00 | |||
216b53f224 | |||
059b0a2e1c | |||
3ab952f168 | |||
4f676d6770 | |||
e980bc847b | |||
174efc9080 | |||
3228789375 |
4
.github/workflows/main.yml
vendored
@ -22,7 +22,6 @@ jobs:
|
||||
- name: Login to Docker & GHCR
|
||||
run: |
|
||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
#echo "${{ secrets.GHCR_PASSWORD }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Setup building file structure
|
||||
run: |
|
||||
@ -39,7 +38,4 @@ jobs:
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
|
||||
--tag zoraxydocker/zoraxy:latest \
|
||||
# Since this is still undetermined, I will leave it commented
|
||||
#--tag ghcr.io/zoraxydocker/zoraxy:${{ steps.get_latest_release_tag.outputs.latest_tag }} \
|
||||
#--tag ghcr.io/zoraxydocker/zoraxy:latest \
|
||||
.
|
||||
|
4
.gitignore
vendored
@ -31,4 +31,6 @@ src/rules/*
|
||||
src/README.md
|
||||
docker/ContainerTester.sh
|
||||
docker/ImagePublisher.sh
|
||||
src/mod/acme/test/stackoverflow.pem
|
||||
src/mod/acme/test/stackoverflow.pem
|
||||
/tools/dns_challenge_update/code-gen/acmedns
|
||||
/tools/dns_challenge_update/code-gen/lego
|
||||
|
64
CHANGELOG.md
@ -1,3 +1,67 @@
|
||||
# v3.0.3 Apr 30 2024
|
||||
## Breaking Change
|
||||
|
||||
For users using SMTP with older versions, you might need to update the settings by moving the domains (the part after @ in the username and domain setup field) into the username field.
|
||||
|
||||
+ Updated SMTP UI for non email login username [#129](https://github.com/tobychui/zoraxy/issues/129)
|
||||
+ Fixed ACME cert store reload after cert request [#126](https://github.com/tobychui/zoraxy/issues/126)
|
||||
+ Fixed default rule not applying to default site when default site is set to proxy target [#130](https://github.com/tobychui/zoraxy/issues/130)
|
||||
+ Fixed blacklist-ip not working with CIDR bug
|
||||
+ Fixed minor vdir bug in tailing slash detection and redirect logic
|
||||
+ Added custom mdns name support (-mdnsname flag)
|
||||
+ Added LAN tag in statistic [#131](https://github.com/tobychui/zoraxy/issues/131)
|
||||
|
||||
|
||||
# v3.0.2 Apr 24 2024
|
||||
|
||||
+ Added alias for HTTP proxy host names [#76](https://github.com/tobychui/zoraxy/issues/76)
|
||||
+ Added separator support for create new proxy rules (use "," to add alias when creating new proxy rule)
|
||||
+ Added HTTP proxy host based access rules [#69](https://github.com/tobychui/zoraxy/issues/69)
|
||||
+ Added EAD Configuration for ACME (by [yeungalan](https://github.com/yeungalan)) [#45](https://github.com/tobychui/zoraxy/issues/45)
|
||||
+ Fixed bug for bypassGlobalTLS endpoint do not support basic-auth
|
||||
+ Fixed panic due to empty domain field in json config [#120](https://github.com/tobychui/zoraxy/issues/120)
|
||||
+ Removed dependencies on management panel css for online font files
|
||||
|
||||
# v3.0.1 Apr 04 2024
|
||||
|
||||
## Bugfixupdate for big release of V3, read update notes from V3 if you are still on V2
|
||||
|
||||
+ Added regex support for redirect (slow, don't use it unless you really needs it) [#42](https://github.com/tobychui/zoraxy/issues/42)
|
||||
+ Added new dpcore implementations for faster proxy speed
|
||||
+ Added support for CF-Connecting-IP to X-Real-IP auto rewrite [#114](https://github.com/tobychui/zoraxy/issues/114)
|
||||
+ Added enable / disable of HTTP proxy rules in runtime via slider [#108](https://github.com/tobychui/zoraxy/issues/108)
|
||||
+ Added better 404 page
|
||||
+ Added option to bypass websocket origin check [#107](https://github.com/tobychui/zoraxy/issues/107)
|
||||
+ Updated project homepage design
|
||||
+ Fixed recursive port detection logic
|
||||
+ Fixed UserAgent in resp bug
|
||||
+ Updated minimum required Go version to v1.22 (Notes: Windows 7 support is dropped) [#112](https://github.com/tobychui/zoraxy/issues/112)
|
||||
|
||||
|
||||
# v3.0.0 Feb 18 2024
|
||||
|
||||
## IMPORTANT: V3 is a big rewrite and it is incompatible with V2! There is NO migration, if you want to stay on V2, please use V2 branch!
|
||||
|
||||
+ Added comments for whitelist [#97](https://github.com/tobychui/zoraxy/issues/97)
|
||||
+ Added force-renew for certificates [#92](https://github.com/tobychui/zoraxy/issues/92)
|
||||
+ Added automatic cert pick for multi-host certs (SNI)
|
||||
+ Renamed .crt to .pem for cert store
|
||||
+ Added best-fit selection for wildcard matching rules
|
||||
+ Added x-proxy-by header / Added X-real-Ip header [#93](https://github.com/tobychui/zoraxy/issues/93)
|
||||
+ Added Development Mode (Cache-Control: no-store)
|
||||
+ Updated utm timeout to 10 seconds instead of 90
|
||||
+ Added "Add controller as member" feature to Global Area Network editor
|
||||
+ Added custom header
|
||||
+ Deprecated aroz subservice support
|
||||
+ Updated visuals, improving logical structure, less depressing colors [#95](https://github.com/tobychui/zoraxy/issues/95)
|
||||
+ Added virtual directory into host routing object (each host now got its own sets of virtual directories)
|
||||
+ Added support for wildcard host names (e.g. *.example.com)
|
||||
+ Added best-fit selection for wildcard matching rules (e.g. *.a.example.com > *.example.com in routing)
|
||||
+ Generalized root and hosts routing struct (no more conversion between runtime & save record object
|
||||
+ Added "Default Site" to replace "Proxy Root" interface
|
||||
+ Added Redirect & 404 page for "Default Site"
|
||||
|
||||
|
||||
# v2.6.8 Nov 25 2023
|
||||
|
||||
+ Added opt-out for subdomains for global TLS settings: See [release notes](https://github.com/tobychui/zoraxy/releases/tag/2.6.8)
|
||||
|
83
README.md
@ -2,18 +2,26 @@
|
||||
|
||||
# Zoraxy
|
||||
|
||||
General purpose request (reverse) proxy and forwarding tool for low power devices. Now written in Go!
|
||||
General purpose request (reverse) proxy and forwarding tool for networking noobs. Now written in Go!
|
||||
|
||||
*Zoraxy v3 HTTP proxy config is not compatible with the older v2. If you are looking for the legacy version of Zoraxy, take a look at the [v2 branch](https://github.com/tobychui/zoraxy/tree/v2)*
|
||||
|
||||
### Features
|
||||
|
||||
- Simple to use interface with detail in-system instructions
|
||||
- Reverse Proxy
|
||||
- Subdomain Reverse Proxy
|
||||
- Virtual Directory Reverse Proxy
|
||||
- Reverse Proxy (HTTP/2)
|
||||
- Virtual Directory
|
||||
- WebSocket Proxy (automatic, no set-up needed)
|
||||
- Basic Auth
|
||||
- Alias Hostnames
|
||||
- Custom Headers
|
||||
- Redirection Rules
|
||||
- TLS / SSL setup and deploy
|
||||
- Blacklist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- ACME features like auto-renew to serve your sites in http**s**
|
||||
- SNI support (and SAN certs)
|
||||
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- Global Area Network Controller Web UI (ZeroTier not included)
|
||||
- TCP Tunneling / Proxy
|
||||
- Integrated Up-time Monitor
|
||||
- Web-SSH Terminal
|
||||
- Utilities
|
||||
@ -25,8 +33,17 @@ General purpose request (reverse) proxy and forwarding tool for low power device
|
||||
- External permission management system for easy system integration
|
||||
- SMTP config for password reset
|
||||
|
||||
## Downloads
|
||||
|
||||
[Windows](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_windows_amd64.exe)
|
||||
/[Linux (amd64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64)
|
||||
/[Linux (arm64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64)
|
||||
|
||||
For other systems or architectures, please see [Release](https://github.com/tobychui/zoraxy/releases/latest/)
|
||||
|
||||
## Build from Source
|
||||
Requires Go 1.20 or higher
|
||||
|
||||
Requires Go 1.22 or higher
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tobychui/zoraxy
|
||||
@ -64,27 +81,27 @@ The installation method is same as Linux. If you are using a Raspberry Pi 4 or n
|
||||
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
|
||||
|
||||
#### Docker
|
||||
|
||||
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details.
|
||||
|
||||
### Start Paramters
|
||||
|
||||
```
|
||||
Usage of zoraxy:
|
||||
-autorenew int
|
||||
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
|
||||
-fastgeoip
|
||||
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
|
||||
-info
|
||||
Show information about this program in JSON
|
||||
-log
|
||||
Log terminal output to file (default true)
|
||||
-mdns
|
||||
Enable mDNS scanner and transponder (default true)
|
||||
-mdnsname string
|
||||
mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)
|
||||
-noauth
|
||||
Disable authentication for management interface
|
||||
-port string
|
||||
Management web interface listening port (default ":8000")
|
||||
-rpt string
|
||||
Reserved
|
||||
-sshlb
|
||||
Allow loopback web ssh connection (DANGER)
|
||||
-version
|
||||
@ -109,45 +126,12 @@ If you already have an upstream reverse proxy server in place with permission ma
|
||||
|
||||
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
|
||||
|
||||
#### Use with ArozOS
|
||||
|
||||
The [ArozOS](https://arozos.com) subservice is a built-in, permission-managed, reverse proxy server. To use Zoraxy with ArozOS, connect to your ArozOS host via SSH and use the following command to install Zoraxy:
|
||||
|
||||
```bash
|
||||
# cd into your ArozOS subservice folder. Sometimes it is under ~/arozos/src/subservice.
|
||||
cd ~/arozos/subservices
|
||||
mkdir zoraxy
|
||||
cd ./zoraxy
|
||||
|
||||
# Download the release binary from Github release.
|
||||
wget {binary executable link from release page}
|
||||
|
||||
# Set permission. Change this if required.
|
||||
sudo chmod 775 -R ./
|
||||
|
||||
# Start zoraxy to see if the downloaded arch is correct.
|
||||
./zoraxy
|
||||
|
||||
# After unzipping, press Ctrl + C to kill it.
|
||||
# Rename it to validate the ArozOS subservice binary format.
|
||||
mv ./zoraxy zoraxy_linux_amd64
|
||||
|
||||
# If you are using SBCs with a different CPU arch, use the following names:
|
||||
# mv ./zoraxy zoraxy_linux_arm
|
||||
# mv ./zoraxy zoraxy_linux_arm64
|
||||
|
||||
# Restart ArozOS
|
||||
sudo systemctl restart arozos
|
||||
```
|
||||
|
||||
To start the module, go to System Settings > Modules > Subservice and enable it in the menu. You should be able to see a new module named "Zoraxy" pop up in the start menu.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)!
|
||||
|
||||
## FAQ
|
||||
@ -173,12 +157,13 @@ This allows you to have an infinite number of network members in your Global Are
|
||||
## Web SSH
|
||||
|
||||
Web SSH currently only supports Linux based OSes. The following platforms are supported:
|
||||
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/armv6 (experimental)
|
||||
- linux/386 (experimental)
|
||||
|
||||
### Loopback Connection
|
||||
### Loopback Connection
|
||||
|
||||
Loopback web SSH connection, by default, is disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
|
||||
|
||||
@ -187,12 +172,14 @@ Loopback web SSH connection, by default, is disabled. This means that if you are
|
||||
```
|
||||
|
||||
## Sponsor This Project
|
||||
|
||||
If you like the project and want to support us, please consider a donation. You can use the links below
|
||||
|
||||
- [tobychui (Primary author)](https://paypal.me/tobychui)
|
||||
- PassiveLemon (Docker compatibility maintainer)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This project is open-sourced under AGPL. I open-sourced this project so everyone can check for security issues and benefit all users. **If you plan to use this project in a commercial environment (which violate the AGPL terms), please contact toby@imuslab.com for an alternative commercial license.**
|
||||
This project is open-sourced under AGPL. I open-sourced this project so everyone can check for security issues and benefit all users. **This software is intended to be free of charge. If you have acquired this software from a third-party seller, the authors of this repository bears no responsibility for any technical difficulties assistance or support.**
|
||||
|
||||
|
||||
|
@ -3,17 +3,12 @@ FROM docker.io/golang:alpine
|
||||
ARG VERSION
|
||||
|
||||
RUN apk add --no-cache bash netcat-openbsd sudo
|
||||
# Alternatives for security
|
||||
RUN apk add --no-cache openssl=3.1.4-r1
|
||||
|
||||
RUN mkdir -p /opt/zoraxy/source/ &&\
|
||||
mkdir -p /opt/zoraxy/config/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
|
||||
COPY entrypoint.sh /opt/zoraxy/
|
||||
|
||||
RUN chmod -R 755 /opt/zoraxy/ &&\
|
||||
chmod +x /opt/zoraxy/entrypoint.sh
|
||||
RUN chmod -R 770 /opt/zoraxy/
|
||||
|
||||
VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
@ -26,15 +21,15 @@ RUN go mod tidy &&\
|
||||
go build -o /usr/local/bin/zoraxy &&\
|
||||
rm -r /opt/zoraxy/source/
|
||||
|
||||
RUN chmod +x /usr/local/bin/zoraxy
|
||||
RUN chmod 755 /usr/local/bin/zoraxy &&\
|
||||
chmod +x /usr/local/bin/zoraxy
|
||||
|
||||
WORKDIR /opt/zoraxy/config/
|
||||
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
ENV ARGS="-noauth=false"
|
||||
|
||||
ENTRYPOINT ["/opt/zoraxy/entrypoint.sh"]
|
||||
ENTRYPOINT "zoraxy" "-port=:8000" "${ARGS}"
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=5s --retries=2 CMD nc -vz 127.0.0.1 8000 || exit 1
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
echo "Zoraxy version $VERSION"
|
||||
|
||||
zoraxy -port=:8000 ${ARGS}
|
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">
|
||||
|
||||
<!-- HTML Meta Tags -->
|
||||
<title>Cluster Proxy Gateway | Zoraxy</title>
|
||||
<title>Reverse Proxy Server | Zoraxy</title>
|
||||
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
@ -74,21 +74,16 @@
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<!-- Hero Banner Section -->
|
||||
<div class="dot-container">
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
<div class="headbanner"></div>
|
||||
<div id="home" class="herotext">
|
||||
<div class="ui basic segment">
|
||||
<div class="bannerHeaderWrapper">
|
||||
<h1 class="bannerHeader">Zoraxy</h1>
|
||||
<div class="ui divider"></div><br>
|
||||
<p class="bannerSubheader">All in one homelab network routing solution</p>
|
||||
</div>
|
||||
<br><br>
|
||||
<a class="ui black big button" href="#features">Learn More</a>
|
||||
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
|
||||
<br><br>
|
||||
<table class="ui very basic collapsing unstackable celled table">
|
||||
<thead>
|
||||
@ -126,6 +121,22 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="wavesWrapper">
|
||||
<!-- CSS waves-->
|
||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
|
||||
<defs>
|
||||
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
|
||||
</defs>
|
||||
<g class="parallax">
|
||||
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
@ -240,34 +251,34 @@
|
||||
|
||||
<div class="ui three column stackable grid">
|
||||
<div class="column">
|
||||
<a href="img/screenshots/1.webp" target="_blank"><img src="img/screenshots/1.webp" class="ui fluid image screenshot"></a>
|
||||
<a href="img/screenshots/1.png" target="_blank"><img src="img/screenshots/1.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/2.webp" target="_blank"><img src="img/screenshots/2.webp" class="ui fluid image screenshot"></a>
|
||||
<a href="img/screenshots/2.png" target="_blank"><img src="img/screenshots/2.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/3.webp" target="_blank"><img src="img/screenshots/3.webp" class="ui fluid image screenshot"></a>
|
||||
<a href="img/screenshots/3.png" target="_blank"><img src="img/screenshots/3.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/4.webp" target="_blank"><img src="img/screenshots/4.webp" class="ui fluid image screenshot"></a>
|
||||
<a href="img/screenshots/4.png" target="_blank"><img src="img/screenshots/4.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/5.webp" target="_blank"><img src="img/screenshots/5.webp" class="ui fluid image screenshot"></a>
|
||||
<a href="img/screenshots/5.png" target="_blank"><img src="img/screenshots/5.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/6.webp" target="_blank"><img src="img/screenshots/6.webp" class="ui fluid image screenshot"></a>
|
||||
<a href="img/screenshots/6.png" target="_blank"><img src="img/screenshots/6.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/7.webp" target="_blank"><img src="img/screenshots/7.webp" class="ui fluid image screenshot"></a>
|
||||
<a href="img/screenshots/7.png" target="_blank"><img src="img/screenshots/7.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/8.webp" target="_blank"><img src="img/screenshots/8.webp" class="ui fluid image screenshot"></a>
|
||||
<a href="img/screenshots/8.png" target="_blank"><img src="img/screenshots/8.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/9.webp" target="_blank"><img src="img/screenshots/9.webp" class="ui fluid image screenshot"></a>
|
||||
<a href="img/screenshots/9.png" target="_blank"><img src="img/screenshots/9.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
<div class="column">
|
||||
<a href="img/screenshots/10.webp" target="_blank"><img src="img/screenshots/10.webp" class="ui fluid image screenshot"></a>
|
||||
<a href="img/screenshots/10.png" target="_blank"><img src="img/screenshots/10.png" class="ui fluid image screenshot"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
135
docs/style.css
@ -1,5 +1,5 @@
|
||||
body{
|
||||
background: #f6f6f6 !important;
|
||||
background: #ffffff !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: hidden;
|
||||
@ -18,7 +18,7 @@ body{
|
||||
.left-menu {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
background-color: #ffffff;
|
||||
background-color: #fcfcfc;
|
||||
min-height: 100vh;
|
||||
padding-top: 1.5em;
|
||||
}
|
||||
@ -48,17 +48,19 @@ body{
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
width: 100%;
|
||||
border-right: 0.4em solid var(--themeTextColor);
|
||||
transition: border-left ease-in-out 0.1s, background-color ease-in-out 0.1s;
|
||||
}
|
||||
|
||||
.menu-item.active{
|
||||
border-right: 0.4em solid var(--themeSkyblueColorDecondary);
|
||||
background-color: #f0f8ff;
|
||||
background: linear-gradient(60deg, rgba(84, 58, 183, 0.3) 0%, rgba(0, 172, 193, 0.3) 100%);
|
||||
}
|
||||
|
||||
.menu-item .item-icon{
|
||||
fill: #fcfcfc;
|
||||
}
|
||||
|
||||
.menu-item:hover{
|
||||
border-right: 0.4em solid var(--themeSkyblueColorDecondary);
|
||||
background: rgba(35,35,35,0.1);
|
||||
}
|
||||
|
||||
.menu-item img{
|
||||
@ -69,18 +71,6 @@ body{
|
||||
|
||||
|
||||
/* Head banner */
|
||||
.headbanner{
|
||||
background-image: url('img/bg.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right center;
|
||||
background-size: auto 100%;
|
||||
position:absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
z-index: -100;
|
||||
}
|
||||
|
||||
.herotext{
|
||||
padding-top: 15em;
|
||||
@ -91,11 +81,13 @@ body{
|
||||
.bannerHeader{
|
||||
font-size: 8em;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bannerSubheader{
|
||||
font-weight: 400;
|
||||
font-size: 1.2em;
|
||||
color: #ebebeb;
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
@ -104,6 +96,21 @@ body{
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#home{
|
||||
background: linear-gradient(60deg, rgba(84,58,183,1) 0%, rgba(0,172,193,1) 100%);
|
||||
}
|
||||
|
||||
#home .table th, #home .table h4{
|
||||
color: white;
|
||||
}
|
||||
|
||||
#home .table h4 .content, #home .table h4 .sub.header{
|
||||
color: white;
|
||||
}
|
||||
#home .table td a{
|
||||
color: #d6ddff;
|
||||
}
|
||||
|
||||
/* features */
|
||||
#features{
|
||||
padding-top: 4em;
|
||||
@ -173,56 +180,58 @@ body{
|
||||
}
|
||||
}
|
||||
|
||||
/* Decorative Animation */
|
||||
.dot-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
top: 2em;
|
||||
left: 2em;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: #d9d9d9;
|
||||
margin-right: 6px;
|
||||
animation-name: dot-animation;
|
||||
animation-duration: 4s;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-iteration-count: infinite;
|
||||
/*
|
||||
Waves CSS
|
||||
*/
|
||||
|
||||
#wavesWrapper{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
.waves {
|
||||
position:relative;
|
||||
width: 100%;
|
||||
height:15vh;
|
||||
margin-bottom:-7px; /*Fix for safari gap*/
|
||||
min-height:100px;
|
||||
max-height:150px;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.dot:nth-child(3) {
|
||||
animation-delay: 2s;
|
||||
.parallax > use {
|
||||
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
|
||||
}
|
||||
|
||||
.dot:nth-child(4) {
|
||||
animation-delay: 3s;
|
||||
.parallax > use:nth-child(1) {
|
||||
animation-delay: -8s;
|
||||
animation-duration: 28s;
|
||||
}
|
||||
|
||||
@keyframes dot-animation {
|
||||
0% {
|
||||
background-color: #d9d9d9;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
background-color: #a9d1f3;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
100% {
|
||||
background-color: #d9d9d9;
|
||||
transform: scale(1);
|
||||
}
|
||||
.parallax > use:nth-child(2) {
|
||||
animation-delay: -12s;
|
||||
animation-duration: 40s;
|
||||
}
|
||||
.parallax > use:nth-child(3) {
|
||||
animation-delay: -16s;
|
||||
animation-duration: 52s;
|
||||
}
|
||||
.parallax > use:nth-child(4) {
|
||||
animation-delay: -20s;
|
||||
animation-duration: 80s;
|
||||
}
|
||||
@keyframes move-forever {
|
||||
0% {
|
||||
transform: translate3d(-90px,0,0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(85px,0,0);
|
||||
}
|
||||
}
|
||||
/*Shrinking for mobile*/
|
||||
@media (max-width: 768px) {
|
||||
.waves {
|
||||
height:40px;
|
||||
min-height:40px;
|
||||
}
|
||||
}
|
18
example/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Example www Folder
|
||||
|
||||
This is an example www folder that contains two sub-folders.
|
||||
|
||||
- `html/`
|
||||
- `templates/`
|
||||
|
||||
The html file contain static resources that will be served by Zoraxy build-in static web server. You can use it as a generic web server with a static site generator like [Hugo](https://gohugo.io/) or use it as a small CDN for serving your scripts / image that commonly use across many of your sites.
|
||||
|
||||
The templates folder contains the template for overriding the build in error or access denied pages. The following templates are supported
|
||||
|
||||
- notfound.html (Default site Not-Found error page)
|
||||
- whitelist.html (Error page when client being blocked by whitelist rule)
|
||||
- blacklist.html (Error page when client being blocked by blacklist rule)
|
||||
|
||||
To use the template, copy and paste the `wwww` folder to the same directory as zoraxy executable (aka the src/ file if you `go build` with the current folder tree).
|
||||
|
||||
It is worth mentioning that the uwu icons for not-found and access-denied are created by @SAWARATSUKI
|
229
example/www/html/index.html
Normal file
@ -0,0 +1,229 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Zoraxy Firework!</title>
|
||||
<style>
|
||||
body{
|
||||
margin: 0 !important;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.2/anime.min.js" integrity="sha512-aNMyYYxdIxIaot0Y1/PLuEu3eipGCmsEUBrUq+7aVyPGMFH8z0eTP0tkqAvv34fzN6z+201d3T8HPb1svWSKHQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="c"></canvas>
|
||||
<script>
|
||||
var c = document.getElementById("c");
|
||||
var ctx = c.getContext("2d");
|
||||
var cH;
|
||||
var cW;
|
||||
var bgColor = "#FF6138";
|
||||
var animations = [];
|
||||
var circles = [];
|
||||
|
||||
var colorPicker = (function() {
|
||||
var colors = ["#FF6138", "#FFBE53", "#2980B9", "#FCFCFC", "#282741"];
|
||||
var index = 0;
|
||||
function next() {
|
||||
index = index++ < colors.length-1 ? index : 0;
|
||||
return colors[index];
|
||||
}
|
||||
function current() {
|
||||
return colors[index]
|
||||
}
|
||||
return {
|
||||
next: next,
|
||||
current: current
|
||||
}
|
||||
})();
|
||||
|
||||
function removeAnimation(animation) {
|
||||
var index = animations.indexOf(animation);
|
||||
if (index > -1) animations.splice(index, 1);
|
||||
}
|
||||
|
||||
function calcPageFillRadius(x, y) {
|
||||
var l = Math.max(x - 0, cW - x);
|
||||
var h = Math.max(y - 0, cH - y);
|
||||
return Math.sqrt(Math.pow(l, 2) + Math.pow(h, 2));
|
||||
}
|
||||
|
||||
function addClickListeners() {
|
||||
document.addEventListener("touchstart", handleEvent);
|
||||
document.addEventListener("mousedown", handleEvent);
|
||||
};
|
||||
|
||||
function handleEvent(e) {
|
||||
if (e.touches) {
|
||||
e.preventDefault();
|
||||
e = e.touches[0];
|
||||
}
|
||||
var currentColor = colorPicker.current();
|
||||
var nextColor = colorPicker.next();
|
||||
var targetR = calcPageFillRadius(e.pageX, e.pageY);
|
||||
var rippleSize = Math.min(200, (cW * .4));
|
||||
var minCoverDuration = 750;
|
||||
|
||||
var pageFill = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
r: 0,
|
||||
fill: nextColor
|
||||
});
|
||||
var fillAnimation = anime({
|
||||
targets: pageFill,
|
||||
r: targetR,
|
||||
duration: Math.max(targetR / 2 , minCoverDuration ),
|
||||
easing: "easeOutQuart",
|
||||
complete: function(){
|
||||
bgColor = pageFill.fill;
|
||||
removeAnimation(fillAnimation);
|
||||
}
|
||||
});
|
||||
|
||||
var ripple = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
r: 0,
|
||||
fill: currentColor,
|
||||
stroke: {
|
||||
width: 3,
|
||||
color: currentColor
|
||||
},
|
||||
opacity: 1
|
||||
});
|
||||
var rippleAnimation = anime({
|
||||
targets: ripple,
|
||||
r: rippleSize,
|
||||
opacity: 0,
|
||||
easing: "easeOutExpo",
|
||||
duration: 900,
|
||||
complete: removeAnimation
|
||||
});
|
||||
|
||||
var particles = [];
|
||||
for (var i=0; i<32; i++) {
|
||||
var particle = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
fill: currentColor,
|
||||
r: anime.random(24, 48)
|
||||
})
|
||||
particles.push(particle);
|
||||
}
|
||||
var particlesAnimation = anime({
|
||||
targets: particles,
|
||||
x: function(particle){
|
||||
return particle.x + anime.random(rippleSize, -rippleSize);
|
||||
},
|
||||
y: function(particle){
|
||||
return particle.y + anime.random(rippleSize * 1.15, -rippleSize * 1.15);
|
||||
},
|
||||
r: 0,
|
||||
easing: "easeOutExpo",
|
||||
duration: anime.random(1000,1300),
|
||||
complete: removeAnimation
|
||||
});
|
||||
animations.push(fillAnimation, rippleAnimation, particlesAnimation);
|
||||
}
|
||||
|
||||
function extend(a, b){
|
||||
for(var key in b) {
|
||||
if(b.hasOwnProperty(key)) {
|
||||
a[key] = b[key];
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
var Circle = function(opts) {
|
||||
extend(this, opts);
|
||||
}
|
||||
|
||||
Circle.prototype.draw = function() {
|
||||
ctx.globalAlpha = this.opacity || 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
|
||||
if (this.stroke) {
|
||||
ctx.strokeStyle = this.stroke.color;
|
||||
ctx.lineWidth = this.stroke.width;
|
||||
ctx.stroke();
|
||||
}
|
||||
if (this.fill) {
|
||||
ctx.fillStyle = this.fill;
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
var animate = anime({
|
||||
duration: Infinity,
|
||||
update: function() {
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fillRect(0, 0, cW, cH);
|
||||
animations.forEach(function(anim) {
|
||||
anim.animatables.forEach(function(animatable) {
|
||||
animatable.target.draw();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var resizeCanvas = function() {
|
||||
cW = window.innerWidth;
|
||||
cH = window.innerHeight;
|
||||
c.width = cW * devicePixelRatio;
|
||||
c.height = cH * devicePixelRatio;
|
||||
ctx.scale(devicePixelRatio, devicePixelRatio);
|
||||
};
|
||||
|
||||
(function init() {
|
||||
resizeCanvas();
|
||||
if (window.CP) {
|
||||
// CodePen's loop detection was causin' problems
|
||||
// and I have no idea why, so...
|
||||
window.CP.PenTimer.MAX_TIME_IN_LOOP_WO_EXIT = 6000;
|
||||
}
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
addClickListeners();
|
||||
if (!!window.location.pathname.match(/fullcpgrid/)) {
|
||||
startFauxClicking();
|
||||
}
|
||||
handleInactiveUser();
|
||||
})();
|
||||
|
||||
function handleInactiveUser() {
|
||||
var inactive = setTimeout(function(){
|
||||
fauxClick(cW/2, cH/2);
|
||||
}, 2000);
|
||||
|
||||
function clearInactiveTimeout() {
|
||||
clearTimeout(inactive);
|
||||
document.removeEventListener("mousedown", clearInactiveTimeout);
|
||||
document.removeEventListener("touchstart", clearInactiveTimeout);
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", clearInactiveTimeout);
|
||||
document.addEventListener("touchstart", clearInactiveTimeout);
|
||||
}
|
||||
|
||||
function startFauxClicking() {
|
||||
setTimeout(function(){
|
||||
fauxClick(anime.random( cW * .2, cW * .8), anime.random(cH * .2, cH * .8));
|
||||
startFauxClicking();
|
||||
}, anime.random(200, 900));
|
||||
}
|
||||
|
||||
function fauxClick(x, y) {
|
||||
var fauxClick = new Event("mousedown");
|
||||
fauxClick.pageX = x;
|
||||
fauxClick.pageY = y;
|
||||
document.dispatchEvent(fauxClick);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
52
example/www/templates/blacklist.html
Normal file
42
example/www/templates/notfound.html
Normal file
52
example/www/templates/whitelist.html
Normal file
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 161 KiB |
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 74 KiB |
@ -3,7 +3,12 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -15,6 +20,157 @@ import (
|
||||
banning / whitelist a specific IP address or country code
|
||||
*/
|
||||
|
||||
/*
|
||||
General Function
|
||||
*/
|
||||
|
||||
func handleListAccessRules(w http.ResponseWriter, r *http.Request) {
|
||||
allAccessRules := accessController.ListAllAccessRules()
|
||||
js, _ := json.Marshal(allAccessRules)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func handleAttachRuleToHost(w http.ResponseWriter, r *http.Request) {
|
||||
ruleid, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule name")
|
||||
return
|
||||
}
|
||||
|
||||
host, err := utils.PostPara(r, "host")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule name")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if access rule and proxy rule exists
|
||||
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(host)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid host given")
|
||||
return
|
||||
}
|
||||
if !accessController.AccessRuleExists(ruleid) {
|
||||
utils.SendErrorResponse(w, "access rule not exists")
|
||||
return
|
||||
}
|
||||
|
||||
//Update the proxy host acess rule id
|
||||
targetProxyEndpoint.AccessFilterUUID = ruleid
|
||||
targetProxyEndpoint.UpdateToRuntime()
|
||||
err = SaveReverseProxyConfig(targetProxyEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Create a new access rule, require name and desc only
|
||||
func handleCreateAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
ruleName, err := utils.PostPara(r, "name")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule name")
|
||||
return
|
||||
}
|
||||
ruleDesc, _ := utils.PostPara(r, "desc")
|
||||
|
||||
//Filter out injection if any
|
||||
p := bluemonday.StripTagsPolicy()
|
||||
ruleName = p.Sanitize(ruleName)
|
||||
ruleDesc = p.Sanitize(ruleDesc)
|
||||
|
||||
ruleUUID := uuid.New().String()
|
||||
newAccessRule := access.AccessRule{
|
||||
ID: ruleUUID,
|
||||
Name: ruleName,
|
||||
Desc: ruleDesc,
|
||||
BlacklistEnabled: false,
|
||||
WhitelistEnabled: false,
|
||||
}
|
||||
|
||||
//Add it to runtime
|
||||
err = accessController.AddNewAccessRule(&newAccessRule)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Handle removing an access rule. All proxy endpoint using this rule will be
|
||||
// set to use the default rule
|
||||
func handleRemoveAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule id given")
|
||||
return
|
||||
}
|
||||
|
||||
if ruleID == "default" {
|
||||
utils.SendErrorResponse(w, "default access rule cannot be removed")
|
||||
return
|
||||
}
|
||||
|
||||
ruleID = strings.TrimSpace(ruleID)
|
||||
|
||||
//Set all proxy hosts that use this access rule back to using "default"
|
||||
allProxyEndpoints := dynamicProxyRouter.GetProxyEndpointsAsMap()
|
||||
for _, proxyEndpoint := range allProxyEndpoints {
|
||||
if strings.EqualFold(proxyEndpoint.AccessFilterUUID, ruleID) {
|
||||
//This proxy endpoint is using the current access filter.
|
||||
//set it to default
|
||||
proxyEndpoint.AccessFilterUUID = "default"
|
||||
proxyEndpoint.UpdateToRuntime()
|
||||
err = SaveReverseProxyConfig(proxyEndpoint)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Access", "Unable to save updated proxy endpoint "+proxyEndpoint.RootOrMatchingDomain, err)
|
||||
} else {
|
||||
SystemWideLogger.PrintAndLog("Access", "Updated "+proxyEndpoint.RootOrMatchingDomain+" access filter to \"default\"", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Remove the access rule by ID
|
||||
err = accessController.RemoveAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SystemWideLogger.PrintAndLog("Access", "Access Rule "+ruleID+" removed", nil)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Only the name and desc, for other properties use blacklist / whitelist api
|
||||
func handleUpadateAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule id")
|
||||
return
|
||||
}
|
||||
ruleName, err := utils.PostPara(r, "name")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule name")
|
||||
return
|
||||
}
|
||||
ruleDesc, _ := utils.PostPara(r, "desc")
|
||||
|
||||
//Filter anything weird
|
||||
p := bluemonday.StrictPolicy()
|
||||
ruleName = p.Sanitize(ruleName)
|
||||
ruleDesc = p.Sanitize(ruleDesc)
|
||||
|
||||
err = accessController.UpdateAccessRule(ruleID, ruleName, ruleDesc)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
/*
|
||||
Blacklist Related
|
||||
*/
|
||||
@ -26,11 +182,24 @@ func handleListBlacklisted(w http.ResponseWriter, r *http.Request) {
|
||||
bltype = "country"
|
||||
}
|
||||
|
||||
ruleID, err := utils.GetPara(r, "id")
|
||||
if err != nil {
|
||||
//Use default if not set
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
//Load the target rule from access controller
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resulst := []string{}
|
||||
if bltype == "country" {
|
||||
resulst = geodbStore.GetAllBlacklistedCountryCode()
|
||||
resulst = rule.GetAllBlacklistedCountryCode()
|
||||
} else if bltype == "ip" {
|
||||
resulst = geodbStore.GetAllBlacklistedIp()
|
||||
resulst = rule.GetAllBlacklistedIp()
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(resulst)
|
||||
@ -45,7 +214,23 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddCountryCodeToBlackList(countryCode)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
comment, _ := utils.PostPara(r, "comment")
|
||||
p := bluemonday.StripTagsPolicy()
|
||||
comment = p.Sanitize(comment)
|
||||
|
||||
//Load the target rule from access controller
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rule.AddCountryCodeToBlackList(countryCode, comment)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -57,7 +242,19 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveCountryCodeFromBlackList(countryCode)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
//Load the target rule from access controller
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rule.RemoveCountryCodeFromBlackList(countryCode)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -69,7 +266,24 @@ func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddIPToBlackList(ipAddr)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
//Load the target rule from access controller
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
comment, _ := utils.GetPara(r, "comment")
|
||||
p := bluemonday.StripTagsPolicy()
|
||||
comment = p.Sanitize(comment)
|
||||
|
||||
rule.AddIPToBlackList(ipAddr, comment)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
@ -79,23 +293,46 @@ func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveIPFromBlackList(ipAddr)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
//Load the target rule from access controller
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rule.RemoveIPFromBlackList(ipAddr)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) {
|
||||
enable, err := utils.PostPara(r, "enable")
|
||||
enable, _ := utils.PostPara(r, "enable")
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
//Return the current enabled state
|
||||
currentEnabled := geodbStore.BlacklistEnabled
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if enable == "" {
|
||||
//enable paramter not set
|
||||
currentEnabled := rule.BlacklistEnabled
|
||||
js, _ := json.Marshal(currentEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enable == "true" {
|
||||
geodbStore.ToggleBlacklist(true)
|
||||
rule.ToggleBlacklist(true)
|
||||
} else if enable == "false" {
|
||||
geodbStore.ToggleBlacklist(false)
|
||||
rule.ToggleBlacklist(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
||||
return
|
||||
@ -115,11 +352,22 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
|
||||
bltype = "country"
|
||||
}
|
||||
|
||||
resulst := []string{}
|
||||
ruleID, err := utils.GetPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resulst := []*access.WhitelistEntry{}
|
||||
if bltype == "country" {
|
||||
resulst = geodbStore.GetAllWhitelistedCountryCode()
|
||||
resulst = rule.GetAllWhitelistedCountryCode()
|
||||
} else if bltype == "ip" {
|
||||
resulst = geodbStore.GetAllWhitelistedIp()
|
||||
resulst = rule.GetAllWhitelistedIp()
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(resulst)
|
||||
@ -134,7 +382,22 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddCountryCodeToWhitelist(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
|
||||
}
|
||||
|
||||
comment, _ := utils.PostPara(r, "comment")
|
||||
p := bluemonday.StrictPolicy()
|
||||
comment = p.Sanitize(comment)
|
||||
|
||||
rule.AddCountryCodeToWhitelist(countryCode, comment)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -146,7 +409,18 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveCountryCodeFromWhitelist(countryCode)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rule.RemoveCountryCodeFromWhitelist(countryCode)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -158,7 +432,23 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddIPToWhiteList(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
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -168,23 +458,45 @@ func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveIPFromWhiteList(ipAddr)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rule.RemoveIPFromWhiteList(ipAddr)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
|
||||
enable, err := utils.PostPara(r, "enable")
|
||||
enable, _ := utils.PostPara(r, "enable")
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if enable == "" {
|
||||
//Return the current enabled state
|
||||
currentEnabled := geodbStore.WhitelistEnabled
|
||||
currentEnabled := rule.WhitelistEnabled
|
||||
js, _ := json.Marshal(currentEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enable == "true" {
|
||||
geodbStore.ToggleWhitelist(true)
|
||||
rule.ToggleWhitelist(true)
|
||||
} else if enable == "false" {
|
||||
geodbStore.ToggleWhitelist(false)
|
||||
rule.ToggleWhitelist(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
||||
return
|
||||
|
43
src/acme.go
@ -38,7 +38,7 @@ func initACME() *acme.ACMEHandler {
|
||||
port = getRandomPort(30000)
|
||||
}
|
||||
|
||||
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
|
||||
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb)
|
||||
}
|
||||
|
||||
// create the special routing rule for ACME
|
||||
@ -85,28 +85,39 @@ func acmeRegisterSpecialRoutingRule() {
|
||||
// This function check if the renew setup is satisfied. If not, toggle them automatically
|
||||
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
isForceHttpsRedirectEnabledOriginally := false
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
//Enable port 80 to 443 redirect
|
||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||
SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
|
||||
dnsPara, _ := utils.PostBool(r, "dns")
|
||||
if !dnsPara {
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
//Enable port 80 to 443 redirect
|
||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||
SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
|
||||
} else {
|
||||
//Set this to true, so after renew, do not turn it off
|
||||
isForceHttpsRedirectEnabledOriginally = true
|
||||
}
|
||||
|
||||
} else if dynamicProxyRouter.Option.Port == 80 {
|
||||
//Go ahead
|
||||
|
||||
} else {
|
||||
//Set this to true, so after renew, do not turn it off
|
||||
isForceHttpsRedirectEnabledOriginally = true
|
||||
//This port do not support ACME
|
||||
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
|
||||
return
|
||||
}
|
||||
|
||||
} else if dynamicProxyRouter.Option.Port == 80 {
|
||||
//Go ahead
|
||||
|
||||
} else {
|
||||
//This port do not support ACME
|
||||
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
|
||||
}
|
||||
|
||||
//Add a 3 second delay to make sure everything is settle down
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// Pass over to the acmeHandler to deal with the communication
|
||||
acmeHandler.HandleRenewCertificate(w, r)
|
||||
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
//Update the TLS cert store buffer
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
|
||||
//Restore original settings
|
||||
if dynamicProxyRouter.Option.Port == 443 && !dnsPara {
|
||||
if !isForceHttpsRedirectEnabledOriginally {
|
||||
//Default is off. Turn the redirection off
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
|
||||
|
34
src/api.go
@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
"imuslab.com/zoraxy/mod/acme/acmewizard"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
@ -47,8 +48,11 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
|
||||
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
||||
authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet)
|
||||
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
|
||||
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
|
||||
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
|
||||
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
||||
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
|
||||
@ -56,9 +60,16 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
||||
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
|
||||
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
||||
//Reverse proxy root related APIs
|
||||
authRouter.HandleFunc("/api/proxy/root/listOptions", HandleRootRouteOptionList)
|
||||
authRouter.HandleFunc("/api/proxy/root/updateOptions", HandleRootRouteOptionsUpdate)
|
||||
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
|
||||
//Reverse proxy virtual directory APIs
|
||||
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
|
||||
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
|
||||
//Reverse proxy user define header apis
|
||||
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
|
||||
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
|
||||
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
|
||||
//Reverse proxy auth related APIs
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
|
||||
@ -77,7 +88,14 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
|
||||
|
||||
//Access Rules API
|
||||
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
|
||||
authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
|
||||
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
|
||||
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
|
||||
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
|
||||
//Blacklist APIs
|
||||
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
||||
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
|
||||
@ -85,7 +103,6 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
|
||||
|
||||
//Whitelist APIs
|
||||
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
|
||||
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
|
||||
@ -115,6 +132,8 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
|
||||
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
|
||||
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
|
||||
authRouter.HandleFunc("/api/gan/network/join", ganManager.HandleServerJoinNetwork)
|
||||
authRouter.HandleFunc("/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
|
||||
authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
|
||||
authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP)
|
||||
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
|
||||
@ -154,6 +173,8 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/tools/smtp/set", HandleSMTPSet)
|
||||
authRouter.HandleFunc("/api/tools/smtp/admin", HandleAdminEmailGet)
|
||||
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
|
||||
authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle)
|
||||
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
|
||||
|
||||
//Account Reset
|
||||
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
||||
@ -166,16 +187,19 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HanldeSetDNS)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
|
||||
authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson)
|
||||
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
|
||||
|
||||
//Static Web Server
|
||||
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
|
||||
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
|
||||
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
|
||||
authRouter.HandleFunc("/api/webserv/setPort", staticWebServer.HandlePortChange)
|
||||
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
|
||||
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
||||
if *allowWebFileManager {
|
||||
//Web Directory Manager file operation functions
|
||||
|
16
src/cert.go
@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -46,12 +47,13 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
LastModifiedDate string
|
||||
ExpireDate string
|
||||
RemainingDays int
|
||||
UseDNS bool
|
||||
}
|
||||
|
||||
results := []*CertInfo{}
|
||||
|
||||
for _, filename := range filenames {
|
||||
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".crt")
|
||||
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
|
||||
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
|
||||
fileInfo, err := os.Stat(certFilepath)
|
||||
if err != nil {
|
||||
@ -81,12 +83,19 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
certInfoFilename := filepath.Join(tlsCertManager.CertStore, filename+".json")
|
||||
useDNSValidation := false //Default to false for HTTP TLS certificates
|
||||
certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json
|
||||
if err == nil {
|
||||
useDNSValidation = certInfo.UseDNS
|
||||
}
|
||||
|
||||
thisCertInfo := CertInfo{
|
||||
Domain: filename,
|
||||
LastModifiedDate: modifiedTime,
|
||||
ExpireDate: certExpireTime,
|
||||
RemainingDays: expiredIn,
|
||||
UseDNS: useDNSValidation,
|
||||
}
|
||||
|
||||
results = append(results, &thisCertInfo)
|
||||
@ -248,7 +257,7 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if keytype == "pub" {
|
||||
overWriteFilename = domain + ".crt"
|
||||
overWriteFilename = domain + ".pem"
|
||||
} else if keytype == "pri" {
|
||||
overWriteFilename = domain + ".key"
|
||||
} else {
|
||||
@ -287,6 +296,9 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Update cert list
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
|
||||
// send response
|
||||
fmt.Fprintln(w, "File upload successful!")
|
||||
}
|
||||
|
176
src/config.go
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -35,97 +36,118 @@ type Record struct {
|
||||
BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule
|
||||
}
|
||||
|
||||
// Save a reverse proxy config record to file
|
||||
func SaveReverseProxyConfigToFile(proxyConfigRecord *Record) error {
|
||||
//TODO: Make this accept new def types
|
||||
os.MkdirAll("./conf/proxy/", 0775)
|
||||
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
|
||||
|
||||
//Generate record
|
||||
thisRecord := proxyConfigRecord
|
||||
|
||||
//Write to file
|
||||
js, _ := json.MarshalIndent(thisRecord, "", " ")
|
||||
return os.WriteFile(filepath.Join("./conf/proxy/", filename), js, 0775)
|
||||
}
|
||||
|
||||
// Save a running reverse proxy endpoint to file (with automatic endpoint to record conversion)
|
||||
func SaveReverseProxyEndpointToFile(proxyEndpoint *dynamicproxy.ProxyEndpoint) error {
|
||||
recordToSave, err := ConvertProxyEndpointToRecord(proxyEndpoint)
|
||||
/*
|
||||
Load Reverse Proxy Config from file and append it to current runtime proxy router
|
||||
*/
|
||||
func LoadReverseProxyConfig(configFilepath string) error {
|
||||
//Load the config file from disk
|
||||
endpointConfig, err := os.ReadFile(configFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return SaveReverseProxyConfigToFile(recordToSave)
|
||||
}
|
||||
|
||||
func RemoveReverseProxyConfigFile(rootname string) error {
|
||||
filename := getFilenameFromRootName(rootname)
|
||||
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
|
||||
SystemWideLogger.Println("Config Removed: ", removePendingFile)
|
||||
if utils.FileExists(removePendingFile) {
|
||||
err := os.Remove(removePendingFile)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unabel to remove config file", err)
|
||||
return err
|
||||
}
|
||||
//Parse it into dynamic proxy endpoint
|
||||
thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
|
||||
err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//File already gone
|
||||
//Matching domain not set. Assume root
|
||||
if thisConfigEndpoint.RootOrMatchingDomain == "" {
|
||||
thisConfigEndpoint.RootOrMatchingDomain = "/"
|
||||
}
|
||||
|
||||
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Root {
|
||||
//This is a root config file
|
||||
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
|
||||
|
||||
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Host {
|
||||
//This is a host config file
|
||||
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint)
|
||||
} else {
|
||||
return errors.New("not supported proxy type")
|
||||
}
|
||||
|
||||
SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+thisConfigEndpoint.Domain+" routing rule loaded", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return ptype, rootname and proxyTarget, error if any
|
||||
func LoadReverseProxyConfig(filename string) (*Record, error) {
|
||||
thisRecord := Record{
|
||||
ProxyType: "",
|
||||
Rootname: "",
|
||||
ProxyTarget: "",
|
||||
UseTLS: false,
|
||||
func filterProxyConfigFilename(filename string) string {
|
||||
//Filter out wildcard characters
|
||||
filename = strings.ReplaceAll(filename, "*", "(ST)")
|
||||
filename = strings.ReplaceAll(filename, "?", "(QM)")
|
||||
filename = strings.ReplaceAll(filename, "[", "(OB)")
|
||||
filename = strings.ReplaceAll(filename, "]", "(CB)")
|
||||
filename = strings.ReplaceAll(filename, "#", "(HT)")
|
||||
return filepath.ToSlash(filename)
|
||||
}
|
||||
|
||||
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
|
||||
//Get filename for saving
|
||||
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
|
||||
if endpoint.ProxyType == dynamicproxy.ProxyType_Root {
|
||||
filename = "./conf/proxy/root.config"
|
||||
}
|
||||
|
||||
filename = filterProxyConfigFilename(filename)
|
||||
|
||||
//Save config to file
|
||||
js, err := json.MarshalIndent(endpoint, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, js, 0775)
|
||||
}
|
||||
|
||||
func RemoveReverseProxyConfig(endpoint string) error {
|
||||
filename := filepath.Join("./conf/proxy/", endpoint+".config")
|
||||
if endpoint == "/" {
|
||||
filename = "./conf/proxy/root.config"
|
||||
}
|
||||
|
||||
filename = filterProxyConfigFilename(filename)
|
||||
|
||||
if !utils.FileExists(filename) {
|
||||
return errors.New("target endpoint not exists")
|
||||
}
|
||||
return os.Remove(filename)
|
||||
}
|
||||
|
||||
// Get the default root config that point to the internal static web server
|
||||
// this will be used if root config is not found (new deployment / missing root.config file)
|
||||
func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
|
||||
//Default settings
|
||||
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
|
||||
ProxyType: dynamicproxy.ProxyType_Root,
|
||||
RootOrMatchingDomain: "/",
|
||||
Domain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
|
||||
RequireTLS: false,
|
||||
BypassGlobalTLS: false,
|
||||
SkipTlsValidation: false,
|
||||
SkipCertValidations: false,
|
||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||
RequireBasicAuth: false,
|
||||
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
|
||||
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
|
||||
}
|
||||
|
||||
configContent, err := os.ReadFile(filename)
|
||||
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
|
||||
DefaultSiteValue: "",
|
||||
})
|
||||
if err != nil {
|
||||
return &thisRecord, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Unmarshal the content into config
|
||||
err = json.Unmarshal(configContent, &thisRecord)
|
||||
if err != nil {
|
||||
return &thisRecord, err
|
||||
}
|
||||
|
||||
//Return it
|
||||
return &thisRecord, nil
|
||||
}
|
||||
|
||||
// Convert a running proxy endpoint object into a save-able record struct
|
||||
func ConvertProxyEndpointToRecord(targetProxyEndpoint *dynamicproxy.ProxyEndpoint) (*Record, error) {
|
||||
thisProxyConfigRecord := Record{
|
||||
ProxyType: targetProxyEndpoint.GetProxyTypeString(),
|
||||
Rootname: targetProxyEndpoint.RootOrMatchingDomain,
|
||||
ProxyTarget: targetProxyEndpoint.Domain,
|
||||
UseTLS: targetProxyEndpoint.RequireTLS,
|
||||
BypassGlobalTLS: targetProxyEndpoint.BypassGlobalTLS,
|
||||
SkipTlsValidation: targetProxyEndpoint.SkipCertValidations,
|
||||
RequireBasicAuth: targetProxyEndpoint.RequireBasicAuth,
|
||||
BasicAuthCredentials: targetProxyEndpoint.BasicAuthCredentials,
|
||||
BasicAuthExceptionRules: targetProxyEndpoint.BasicAuthExceptionRules,
|
||||
}
|
||||
|
||||
return &thisProxyConfigRecord, nil
|
||||
}
|
||||
|
||||
func getFilenameFromRootName(rootname string) string {
|
||||
//Generate a filename for this rootname
|
||||
filename := strings.ReplaceAll(rootname, ".", "_")
|
||||
filename = strings.ReplaceAll(filename, "/", "-")
|
||||
filename = filename + ".config"
|
||||
return filename
|
||||
return rootProxyEndpoint, nil
|
||||
}
|
||||
|
||||
/*
|
||||
@ -133,7 +155,7 @@ func getFilenameFromRootName(rootname string) string {
|
||||
*/
|
||||
|
||||
func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
includeSysDBRaw, err := utils.GetPara(r, "includeDB")
|
||||
includeSysDBRaw, _ := utils.GetPara(r, "includeDB")
|
||||
includeSysDB := false
|
||||
if includeSysDBRaw == "true" {
|
||||
//Include the system database in backup snapshot
|
||||
@ -155,7 +177,7 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Walk through the folder and add files to the zip
|
||||
err = filepath.Walk(folderPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
err := filepath.Walk(folderPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -25,12 +25,6 @@ func HandleSMTPSet(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "domain cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
portString, err := utils.PostPara(r, "port")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "port must be a valid integer")
|
||||
@ -76,7 +70,6 @@ func HandleSMTPSet(w http.ResponseWriter, r *http.Request) {
|
||||
//Set the email sender properties
|
||||
thisEmailSender := email.Sender{
|
||||
Hostname: strings.TrimSpace(hostname),
|
||||
Domain: strings.TrimSpace(domain),
|
||||
Port: port,
|
||||
Username: strings.TrimSpace(username),
|
||||
Password: strings.TrimSpace(password),
|
||||
@ -206,7 +199,7 @@ var (
|
||||
)
|
||||
|
||||
func HandleAdminAccountResetEmail(w http.ResponseWriter, r *http.Request) {
|
||||
if EmailSender.Username == "" || EmailSender.Domain == "" {
|
||||
if EmailSender.Username == "" {
|
||||
//Reset account not setup
|
||||
utils.SendErrorResponse(w, "Reset account not setup.")
|
||||
return
|
||||
|
174
src/go.mod
@ -1,18 +1,174 @@
|
||||
module imuslab.com/zoraxy
|
||||
|
||||
go 1.16
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/go-acme/lego/v4 v4.14.0
|
||||
github.com/go-acme/lego/v4 v4.16.1
|
||||
github.com/go-ping/ping v1.1.0
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/likexian/whois v1.15.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
golang.org/x/net v0.14.0
|
||||
golang.org/x/sys v0.11.0
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
golang.org/x/net v0.23.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/text v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.20.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/civo/civogo v0.3.11 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.86.0 // indirect
|
||||
github.com/cpu/goacmedns v0.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.9.1 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/dnsimple/dnsimple-go v1.2.0 // indirect
|
||||
github.com/exoscale/egoscale v0.102.3 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/go-resty/resty/v2 v2.11.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.4 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.11.0 // indirect
|
||||
github.com/gophercloud/gophercloud v1.0.0 // indirect
|
||||
github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||
github.com/linode/linodego v1.28.0 // indirect
|
||||
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
|
||||
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.58 // indirect
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect
|
||||
github.com/nrdcg/desec v0.7.0 // indirect
|
||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||
github.com/nrdcg/freemyip v0.2.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.10.0 // indirect
|
||||
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||
github.com/nrdcg/porkbun v0.3.0 // indirect
|
||||
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||
github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect
|
||||
github.com/ovh/go-ovh v1.4.3 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pquerna/otp v1.4.0 // indirect
|
||||
github.com/sacloud/api-client-go v0.2.8 // indirect
|
||||
github.com/sacloud/go-http v0.1.6 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.11.1 // indirect
|
||||
github.com/sacloud/packages-go v0.0.9 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||
github.com/softlayer/softlayer-go v1.1.3 // indirect
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/stretchr/objx v0.5.1 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect
|
||||
github.com/transip/gotransip/v6 v6.23.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||
github.com/vultr/govultr/v2 v2.17.2 // indirect
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.uber.org/ratelimit v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/oauth2 v0.16.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
google.golang.org/api v0.126.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/grpc v1.55.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.7.13 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
1347
src/go.sum
34
src/main.go
@ -12,12 +12,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/aroz"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/email"
|
||||
"imuslab.com/zoraxy/mod/forwardproxy"
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
@ -35,10 +36,12 @@ import (
|
||||
)
|
||||
|
||||
// General flags
|
||||
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
|
||||
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
||||
var showver = flag.Bool("version", false, "Show version of this server")
|
||||
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
||||
var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
|
||||
var mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
|
||||
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
|
||||
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
|
||||
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
|
||||
@ -49,7 +52,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
|
||||
|
||||
var (
|
||||
name = "Zoraxy"
|
||||
version = "2.6.8"
|
||||
version = "3.0.4"
|
||||
nodeUUID = "generic"
|
||||
development = false //Set this to false to use embedded web fs
|
||||
bootTime = time.Now().Unix()
|
||||
@ -63,13 +66,13 @@ var (
|
||||
/*
|
||||
Handler Modules
|
||||
*/
|
||||
handler *aroz.ArozHandler //Handle arozos managed permission system
|
||||
sysdb *database.Database //System database
|
||||
authAgent *auth.AuthAgent //Authentication agent
|
||||
tlsCertManager *tlscert.Manager //TLS / SSL management
|
||||
redirectTable *redirection.RuleTable //Handle special redirection rule sets
|
||||
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
|
||||
geodbStore *geodb.Store //GeoIP database, also handle black list and whitelist features
|
||||
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
|
||||
accessController *access.Controller //Access controller, handle black list and white list
|
||||
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
|
||||
statisticCollector *statistic.Collector //Collecting statistic from visitors
|
||||
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
|
||||
@ -80,6 +83,7 @@ var (
|
||||
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
|
||||
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
|
||||
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
||||
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
|
||||
|
||||
//Helper modules
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
@ -128,20 +132,8 @@ func ShutdownSeq() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
|
||||
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
|
||||
Name: name,
|
||||
Desc: "Dynamic Reverse Proxy Server",
|
||||
Group: "Network",
|
||||
IconPath: "zoraxy/img/small_icon.png",
|
||||
Version: version,
|
||||
StartDir: "zoraxy/index.html",
|
||||
SupportFW: true,
|
||||
LaunchFWDir: "zoraxy/index.html",
|
||||
SupportEmb: false,
|
||||
InitFWSize: []int{1080, 580},
|
||||
})
|
||||
|
||||
//Parse startup flags
|
||||
flag.Parse()
|
||||
if *showver {
|
||||
fmt.Println(name + " - Version " + version)
|
||||
os.Exit(0)
|
||||
@ -166,7 +158,7 @@ func main() {
|
||||
startupSequence()
|
||||
|
||||
//Initiate management interface APIs
|
||||
requireAuth = !(*noauth || handler.IsUsingExternalPermissionManager())
|
||||
requireAuth = !(*noauth)
|
||||
initAPIs()
|
||||
|
||||
//Start the reverse proxy server in go routine
|
||||
@ -179,8 +171,8 @@ func main() {
|
||||
//Start the finalize sequences
|
||||
finalSequence()
|
||||
|
||||
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
|
||||
err = http.ListenAndServe(handler.Port, nil)
|
||||
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
|
||||
err = http.ListenAndServe(*webUIPort, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
221
src/mod/access/access.go
Normal file
@ -0,0 +1,221 @@
|
||||
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,
|
||||
}
|
||||
|
||||
//Assign default access rule parent
|
||||
thisController.DefaultAccessRule.parent = &thisController
|
||||
|
||||
//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
|
||||
}
|
94
src/mod/access/blacklist.go
Normal file
@ -0,0 +1,94 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
)
|
||||
|
||||
/*
|
||||
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]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
|
||||
//Check for CIDR
|
||||
for ipOrCIDR, _ := range IPBlacklist {
|
||||
wildcardMatch := netutils.MatchIpWildcard(ipAddr, ipOrCIDR)
|
||||
if wildcardMatch {
|
||||
return true
|
||||
}
|
||||
|
||||
cidrMatch := netutils.MatchIpCIDR(ipAddr, ipOrCIDR)
|
||||
if cidrMatch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
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"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@ -24,6 +25,7 @@ import (
|
||||
"github.com/go-acme/lego/v4/challenge/http01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -31,6 +33,7 @@ type CertificateInfoJSON struct {
|
||||
AcmeName string `json:"acme_name"`
|
||||
AcmeUrl string `json:"acme_url"`
|
||||
SkipTLS bool `json:"skip_tls"`
|
||||
UseDNS bool `json:"dns"`
|
||||
}
|
||||
|
||||
// ACMEUser represents a user in the ACME system.
|
||||
@ -40,6 +43,11 @@ type ACMEUser struct {
|
||||
key crypto.PrivateKey
|
||||
}
|
||||
|
||||
type EABConfig struct {
|
||||
Kid string `json:"kid"`
|
||||
HmacKey string `json:"HmacKey"`
|
||||
}
|
||||
|
||||
// GetEmail returns the email of the ACMEUser.
|
||||
func (u *ACMEUser) GetEmail() string {
|
||||
return u.Email
|
||||
@ -59,18 +67,20 @@ func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
|
||||
type ACMEHandler struct {
|
||||
DefaultAcmeServer string
|
||||
Port string
|
||||
Database *database.Database
|
||||
}
|
||||
|
||||
// NewACME creates a new ACMEHandler instance.
|
||||
func NewACME(acmeServer string, port string) *ACMEHandler {
|
||||
func NewACME(acmeServer string, port string, database *database.Database) *ACMEHandler {
|
||||
return &ACMEHandler{
|
||||
DefaultAcmeServer: acmeServer,
|
||||
Port: port,
|
||||
Database: database,
|
||||
}
|
||||
}
|
||||
|
||||
// ObtainCert obtains a certificate for the specified domains.
|
||||
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool) (bool, error) {
|
||||
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool) (bool, error) {
|
||||
log.Println("[ACME] Obtaining certificate...")
|
||||
|
||||
// generate private key
|
||||
@ -107,6 +117,11 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
}
|
||||
}
|
||||
|
||||
//Fallback to Let's Encrypt if it is not set
|
||||
if caName == "" {
|
||||
caName = "Let's Encrypt"
|
||||
}
|
||||
|
||||
// setup the custom ACME url endpoint.
|
||||
if caUrl != "" {
|
||||
config.CADirURL = caUrl
|
||||
@ -136,17 +151,107 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
}
|
||||
|
||||
// setup how to receive challenge
|
||||
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
if useDNS {
|
||||
if !a.Database.TableExists("acme") {
|
||||
a.Database.NewTable("acme")
|
||||
return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -1)")
|
||||
}
|
||||
|
||||
if !a.Database.KeyExists("acme", certificateName+"_dns_provider") || !a.Database.KeyExists("acme", certificateName+"_dns_credentials") {
|
||||
return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -2)")
|
||||
}
|
||||
|
||||
var dnsCredentials string
|
||||
err := a.Database.Read("acme", certificateName+"_dns_credentials", &dnsCredentials)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
var dnsProvider string
|
||||
err = a.Database.Read("acme", certificateName+"_dns_provider", &dnsProvider)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = client.Challenge.SetDNS01Provider(provider)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// New users will need to register
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
/*
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
*/
|
||||
var reg *registration.Resource
|
||||
// New users will need to register
|
||||
if client.GetExternalAccountRequired() {
|
||||
log.Println("External Account Required for this ACME Provider.")
|
||||
// IF KID and HmacEncoded is overidden
|
||||
|
||||
if !a.Database.TableExists("acme") {
|
||||
a.Database.NewTable("acme")
|
||||
return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -1)")
|
||||
}
|
||||
|
||||
if !a.Database.KeyExists("acme", config.CADirURL+"_kid") || !a.Database.KeyExists("acme", config.CADirURL+"_hmacEncoded") {
|
||||
return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -2)")
|
||||
}
|
||||
|
||||
var kid string
|
||||
var hmacEncoded string
|
||||
err := a.Database.Read("acme", config.CADirURL+"_kid", &kid)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
log.Println("EAB Credential retrieved.", kid, hmacEncoded)
|
||||
if kid != "" && hmacEncoded != "" {
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: kid,
|
||||
HmacEncoded: hmacEncoded,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
//return false, errors.New("External Account Required for this ACME Provider.")
|
||||
} else {
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
adminUser.Registration = reg
|
||||
|
||||
@ -163,7 +268,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
|
||||
// Each certificate comes back with the cert bytes, the bytes of the client's
|
||||
// private key, and a certificate URL.
|
||||
err = os.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
|
||||
err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
@ -179,6 +284,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
AcmeName: caName,
|
||||
AcmeUrl: caUrl,
|
||||
SkipTLS: skipTLS,
|
||||
UseDNS: useDNS,
|
||||
}
|
||||
|
||||
certInfoBytes, err := json.Marshal(certInfo)
|
||||
@ -291,6 +397,8 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
}
|
||||
//Make sure the wildcard * do not goes into the filename
|
||||
filename = strings.ReplaceAll(filename, "*", "_")
|
||||
|
||||
email, err := utils.PostPara(r, "email")
|
||||
if err != nil {
|
||||
@ -329,8 +437,18 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
skipTLS = true
|
||||
}
|
||||
|
||||
var dns bool
|
||||
|
||||
if dnsString, err := utils.PostPara(r, "dns"); err != nil {
|
||||
dns = false
|
||||
} else if dnsString != "true" {
|
||||
dns = false
|
||||
} else {
|
||||
dns = true
|
||||
}
|
||||
|
||||
domains := strings.Split(domainPara, ",")
|
||||
result, err := a.ObtainCert(domains, filename, email, ca, caUrl, skipTLS)
|
||||
result, err := a.ObtainCert(domains, filename, email, ca, caUrl, skipTLS, dns)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
@ -362,7 +480,7 @@ func IsPortInUse(port int) bool {
|
||||
}
|
||||
|
||||
// Load cert information from json file
|
||||
func loadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
|
||||
func LoadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
|
||||
certInfoBytes, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
72
src/mod/acme/acme_dns.go
Normal file
@ -0,0 +1,72 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
)
|
||||
|
||||
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string) (challenge.Provider, error) {
|
||||
|
||||
//Original Implementation
|
||||
/*credentials, err := extractDnsCredentials(dnsCredentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setCredentialsIntoEnvironmentVariables(credentials)
|
||||
|
||||
provider, err := dns.NewDNSChallengeProviderByName(dnsProvider)
|
||||
*/
|
||||
|
||||
//New implementation using acmedns CICD pipeline generated datatype
|
||||
return acmedns.GetDNSProviderByJsonConfig(dnsProvider, dnsCredentials)
|
||||
}
|
||||
|
||||
/*
|
||||
Original implementation of DNS ACME using OS.Env as payload
|
||||
*/
|
||||
|
||||
func setCredentialsIntoEnvironmentVariables(credentials map[string]string) {
|
||||
for key, value := range credentials {
|
||||
err := os.Setenv(key, value)
|
||||
if err != nil {
|
||||
log.Println("[ERR] Failed to set environment variable %s: %v", key, err)
|
||||
} else {
|
||||
log.Println("[INFO] Environment variable %s set successfully", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractDnsCredentials(input string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
// Split the input string by newline character
|
||||
lines := strings.Split(input, "\n")
|
||||
|
||||
// Iterate over each line
|
||||
for _, line := range lines {
|
||||
// Split the line by "=" character
|
||||
//use SpliyN to make sure not to split the value if the value is base64
|
||||
parts := strings.SplitN(line, "=", 1)
|
||||
|
||||
// Check if the line is in the correct format
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
// Add the key-value pair to the map
|
||||
result[key] = value
|
||||
|
||||
if value == "" || key == "" {
|
||||
//invalid config
|
||||
return result, errors.New("DNS credential extract failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
982
src/mod/acme/acmedns/acmedns.go
Normal file
@ -0,0 +1,982 @@
|
||||
package acmedns
|
||||
/*
|
||||
THIS MODULE IS GENERATED AUTOMATICALLY
|
||||
DO NOT EDIT THIS FILE
|
||||
*/
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/allinkl"
|
||||
"github.com/go-acme/lego/v4/providers/dns/arvancloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/auroradns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/autodns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/azure"
|
||||
"github.com/go-acme/lego/v4/providers/dns/azuredns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/bindman"
|
||||
"github.com/go-acme/lego/v4/providers/dns/bluecat"
|
||||
"github.com/go-acme/lego/v4/providers/dns/brandit"
|
||||
"github.com/go-acme/lego/v4/providers/dns/bunny"
|
||||
"github.com/go-acme/lego/v4/providers/dns/checkdomain"
|
||||
"github.com/go-acme/lego/v4/providers/dns/civo"
|
||||
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudru"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudxns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/conoha"
|
||||
"github.com/go-acme/lego/v4/providers/dns/constellix"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cpanel"
|
||||
"github.com/go-acme/lego/v4/providers/dns/derak"
|
||||
"github.com/go-acme/lego/v4/providers/dns/desec"
|
||||
"github.com/go-acme/lego/v4/providers/dns/digitalocean"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnshomede"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnsimple"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnspod"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dode"
|
||||
"github.com/go-acme/lego/v4/providers/dns/domeneshop"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dreamhost"
|
||||
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dyn"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dynu"
|
||||
"github.com/go-acme/lego/v4/providers/dns/easydns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/efficientip"
|
||||
"github.com/go-acme/lego/v4/providers/dns/epik"
|
||||
"github.com/go-acme/lego/v4/providers/dns/exoscale"
|
||||
"github.com/go-acme/lego/v4/providers/dns/freemyip"
|
||||
"github.com/go-acme/lego/v4/providers/dns/gandi"
|
||||
"github.com/go-acme/lego/v4/providers/dns/gandiv5"
|
||||
"github.com/go-acme/lego/v4/providers/dns/gcore"
|
||||
"github.com/go-acme/lego/v4/providers/dns/glesys"
|
||||
"github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||
"github.com/go-acme/lego/v4/providers/dns/googledomains"
|
||||
"github.com/go-acme/lego/v4/providers/dns/hetzner"
|
||||
"github.com/go-acme/lego/v4/providers/dns/hostingde"
|
||||
"github.com/go-acme/lego/v4/providers/dns/hosttech"
|
||||
"github.com/go-acme/lego/v4/providers/dns/httpnet"
|
||||
"github.com/go-acme/lego/v4/providers/dns/hyperone"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ibmcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/iij"
|
||||
"github.com/go-acme/lego/v4/providers/dns/iijdpf"
|
||||
"github.com/go-acme/lego/v4/providers/dns/infoblox"
|
||||
"github.com/go-acme/lego/v4/providers/dns/infomaniak"
|
||||
"github.com/go-acme/lego/v4/providers/dns/internetbs"
|
||||
"github.com/go-acme/lego/v4/providers/dns/inwx"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ionos"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ipv64"
|
||||
"github.com/go-acme/lego/v4/providers/dns/iwantmyname"
|
||||
"github.com/go-acme/lego/v4/providers/dns/joker"
|
||||
"github.com/go-acme/lego/v4/providers/dns/liara"
|
||||
"github.com/go-acme/lego/v4/providers/dns/lightsail"
|
||||
"github.com/go-acme/lego/v4/providers/dns/linode"
|
||||
"github.com/go-acme/lego/v4/providers/dns/liquidweb"
|
||||
"github.com/go-acme/lego/v4/providers/dns/loopia"
|
||||
"github.com/go-acme/lego/v4/providers/dns/luadns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/mailinabox"
|
||||
"github.com/go-acme/lego/v4/providers/dns/metaname"
|
||||
"github.com/go-acme/lego/v4/providers/dns/mydnsjp"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namecheap"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namedotcom"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech"
|
||||
"github.com/go-acme/lego/v4/providers/dns/netcup"
|
||||
"github.com/go-acme/lego/v4/providers/dns/netlify"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nicmanager"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nifcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/njalla"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nodion"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ns1"
|
||||
"github.com/go-acme/lego/v4/providers/dns/otc"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||
"github.com/go-acme/lego/v4/providers/dns/pdns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/plesk"
|
||||
"github.com/go-acme/lego/v4/providers/dns/porkbun"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rackspace"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rcodezero"
|
||||
"github.com/go-acme/lego/v4/providers/dns/regru"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rfc2136"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rimuhosting"
|
||||
"github.com/go-acme/lego/v4/providers/dns/route53"
|
||||
"github.com/go-acme/lego/v4/providers/dns/safedns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/sakuracloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/scaleway"
|
||||
"github.com/go-acme/lego/v4/providers/dns/selectel"
|
||||
"github.com/go-acme/lego/v4/providers/dns/servercow"
|
||||
"github.com/go-acme/lego/v4/providers/dns/shellrent"
|
||||
"github.com/go-acme/lego/v4/providers/dns/simply"
|
||||
"github.com/go-acme/lego/v4/providers/dns/sonic"
|
||||
"github.com/go-acme/lego/v4/providers/dns/stackpath"
|
||||
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/transip"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ultradns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/variomedia"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vegadns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vercel"
|
||||
"github.com/go-acme/lego/v4/providers/dns/versio"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vinyldns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vkcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vscale"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vultr"
|
||||
"github.com/go-acme/lego/v4/providers/dns/webnames"
|
||||
"github.com/go-acme/lego/v4/providers/dns/websupport"
|
||||
"github.com/go-acme/lego/v4/providers/dns/wedos"
|
||||
"github.com/go-acme/lego/v4/providers/dns/yandex"
|
||||
"github.com/go-acme/lego/v4/providers/dns/yandex360"
|
||||
"github.com/go-acme/lego/v4/providers/dns/yandexcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/zoneee"
|
||||
"github.com/go-acme/lego/v4/providers/dns/zonomi"
|
||||
|
||||
)
|
||||
|
||||
//name is the DNS provider name, e.g. cloudflare or gandi
|
||||
//JSON (js) must be in key-value string that match ConfigableFields Title in providers.json, e.g. {"Username":"far","Password":"boo"}
|
||||
func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, error){
|
||||
switch name {
|
||||
|
||||
case "alidns":
|
||||
cfg := alidns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return alidns.NewDNSProviderConfig(cfg)
|
||||
case "allinkl":
|
||||
cfg := allinkl.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return allinkl.NewDNSProviderConfig(cfg)
|
||||
case "arvancloud":
|
||||
cfg := arvancloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return arvancloud.NewDNSProviderConfig(cfg)
|
||||
case "auroradns":
|
||||
cfg := auroradns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return auroradns.NewDNSProviderConfig(cfg)
|
||||
case "autodns":
|
||||
cfg := autodns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return autodns.NewDNSProviderConfig(cfg)
|
||||
case "azure":
|
||||
cfg := azure.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return azure.NewDNSProviderConfig(cfg)
|
||||
case "azuredns":
|
||||
cfg := azuredns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return azuredns.NewDNSProviderConfig(cfg)
|
||||
case "bindman":
|
||||
cfg := bindman.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bindman.NewDNSProviderConfig(cfg)
|
||||
case "bluecat":
|
||||
cfg := bluecat.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bluecat.NewDNSProviderConfig(cfg)
|
||||
case "brandit":
|
||||
cfg := brandit.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return brandit.NewDNSProviderConfig(cfg)
|
||||
case "bunny":
|
||||
cfg := bunny.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bunny.NewDNSProviderConfig(cfg)
|
||||
case "checkdomain":
|
||||
cfg := checkdomain.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return checkdomain.NewDNSProviderConfig(cfg)
|
||||
case "civo":
|
||||
cfg := civo.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return civo.NewDNSProviderConfig(cfg)
|
||||
case "clouddns":
|
||||
cfg := clouddns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clouddns.NewDNSProviderConfig(cfg)
|
||||
case "cloudflare":
|
||||
cfg := cloudflare.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloudflare.NewDNSProviderConfig(cfg)
|
||||
case "cloudns":
|
||||
cfg := cloudns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloudns.NewDNSProviderConfig(cfg)
|
||||
case "cloudru":
|
||||
cfg := cloudru.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloudru.NewDNSProviderConfig(cfg)
|
||||
case "cloudxns":
|
||||
cfg := cloudxns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloudxns.NewDNSProviderConfig(cfg)
|
||||
case "conoha":
|
||||
cfg := conoha.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conoha.NewDNSProviderConfig(cfg)
|
||||
case "constellix":
|
||||
cfg := constellix.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return constellix.NewDNSProviderConfig(cfg)
|
||||
case "cpanel":
|
||||
cfg := cpanel.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cpanel.NewDNSProviderConfig(cfg)
|
||||
case "derak":
|
||||
cfg := derak.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return derak.NewDNSProviderConfig(cfg)
|
||||
case "desec":
|
||||
cfg := desec.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desec.NewDNSProviderConfig(cfg)
|
||||
case "digitalocean":
|
||||
cfg := digitalocean.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return digitalocean.NewDNSProviderConfig(cfg)
|
||||
case "dnshomede":
|
||||
cfg := dnshomede.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dnshomede.NewDNSProviderConfig(cfg)
|
||||
case "dnsimple":
|
||||
cfg := dnsimple.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dnsimple.NewDNSProviderConfig(cfg)
|
||||
case "dnsmadeeasy":
|
||||
cfg := dnsmadeeasy.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dnsmadeeasy.NewDNSProviderConfig(cfg)
|
||||
case "dnspod":
|
||||
cfg := dnspod.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dnspod.NewDNSProviderConfig(cfg)
|
||||
case "dode":
|
||||
cfg := dode.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dode.NewDNSProviderConfig(cfg)
|
||||
case "domeneshop":
|
||||
cfg := domeneshop.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return domeneshop.NewDNSProviderConfig(cfg)
|
||||
case "dreamhost":
|
||||
cfg := dreamhost.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dreamhost.NewDNSProviderConfig(cfg)
|
||||
case "duckdns":
|
||||
cfg := duckdns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return duckdns.NewDNSProviderConfig(cfg)
|
||||
case "dyn":
|
||||
cfg := dyn.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dyn.NewDNSProviderConfig(cfg)
|
||||
case "dynu":
|
||||
cfg := dynu.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dynu.NewDNSProviderConfig(cfg)
|
||||
case "easydns":
|
||||
cfg := easydns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return easydns.NewDNSProviderConfig(cfg)
|
||||
case "efficientip":
|
||||
cfg := efficientip.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return efficientip.NewDNSProviderConfig(cfg)
|
||||
case "epik":
|
||||
cfg := epik.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return epik.NewDNSProviderConfig(cfg)
|
||||
case "exoscale":
|
||||
cfg := exoscale.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return exoscale.NewDNSProviderConfig(cfg)
|
||||
case "freemyip":
|
||||
cfg := freemyip.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return freemyip.NewDNSProviderConfig(cfg)
|
||||
case "gandi":
|
||||
cfg := gandi.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gandi.NewDNSProviderConfig(cfg)
|
||||
case "gandiv5":
|
||||
cfg := gandiv5.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gandiv5.NewDNSProviderConfig(cfg)
|
||||
case "gcore":
|
||||
cfg := gcore.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gcore.NewDNSProviderConfig(cfg)
|
||||
case "glesys":
|
||||
cfg := glesys.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return glesys.NewDNSProviderConfig(cfg)
|
||||
case "godaddy":
|
||||
cfg := godaddy.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return godaddy.NewDNSProviderConfig(cfg)
|
||||
case "googledomains":
|
||||
cfg := googledomains.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return googledomains.NewDNSProviderConfig(cfg)
|
||||
case "hetzner":
|
||||
cfg := hetzner.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hetzner.NewDNSProviderConfig(cfg)
|
||||
case "hostingde":
|
||||
cfg := hostingde.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hostingde.NewDNSProviderConfig(cfg)
|
||||
case "hosttech":
|
||||
cfg := hosttech.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hosttech.NewDNSProviderConfig(cfg)
|
||||
case "httpnet":
|
||||
cfg := httpnet.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return httpnet.NewDNSProviderConfig(cfg)
|
||||
case "hyperone":
|
||||
cfg := hyperone.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hyperone.NewDNSProviderConfig(cfg)
|
||||
case "ibmcloud":
|
||||
cfg := ibmcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ibmcloud.NewDNSProviderConfig(cfg)
|
||||
case "iij":
|
||||
cfg := iij.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iij.NewDNSProviderConfig(cfg)
|
||||
case "iijdpf":
|
||||
cfg := iijdpf.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iijdpf.NewDNSProviderConfig(cfg)
|
||||
case "infoblox":
|
||||
cfg := infoblox.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infoblox.NewDNSProviderConfig(cfg)
|
||||
case "infomaniak":
|
||||
cfg := infomaniak.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infomaniak.NewDNSProviderConfig(cfg)
|
||||
case "internetbs":
|
||||
cfg := internetbs.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return internetbs.NewDNSProviderConfig(cfg)
|
||||
case "inwx":
|
||||
cfg := inwx.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inwx.NewDNSProviderConfig(cfg)
|
||||
case "ionos":
|
||||
cfg := ionos.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ionos.NewDNSProviderConfig(cfg)
|
||||
case "ipv64":
|
||||
cfg := ipv64.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ipv64.NewDNSProviderConfig(cfg)
|
||||
case "iwantmyname":
|
||||
cfg := iwantmyname.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iwantmyname.NewDNSProviderConfig(cfg)
|
||||
case "joker":
|
||||
cfg := joker.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return joker.NewDNSProviderConfig(cfg)
|
||||
case "liara":
|
||||
cfg := liara.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return liara.NewDNSProviderConfig(cfg)
|
||||
case "lightsail":
|
||||
cfg := lightsail.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lightsail.NewDNSProviderConfig(cfg)
|
||||
case "linode":
|
||||
cfg := linode.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return linode.NewDNSProviderConfig(cfg)
|
||||
case "liquidweb":
|
||||
cfg := liquidweb.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return liquidweb.NewDNSProviderConfig(cfg)
|
||||
case "loopia":
|
||||
cfg := loopia.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loopia.NewDNSProviderConfig(cfg)
|
||||
case "luadns":
|
||||
cfg := luadns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return luadns.NewDNSProviderConfig(cfg)
|
||||
case "mailinabox":
|
||||
cfg := mailinabox.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mailinabox.NewDNSProviderConfig(cfg)
|
||||
case "metaname":
|
||||
cfg := metaname.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return metaname.NewDNSProviderConfig(cfg)
|
||||
case "mydnsjp":
|
||||
cfg := mydnsjp.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mydnsjp.NewDNSProviderConfig(cfg)
|
||||
case "namecheap":
|
||||
cfg := namecheap.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return namecheap.NewDNSProviderConfig(cfg)
|
||||
case "namedotcom":
|
||||
cfg := namedotcom.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return namedotcom.NewDNSProviderConfig(cfg)
|
||||
case "namesilo":
|
||||
cfg := namesilo.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return namesilo.NewDNSProviderConfig(cfg)
|
||||
case "nearlyfreespeech":
|
||||
cfg := nearlyfreespeech.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nearlyfreespeech.NewDNSProviderConfig(cfg)
|
||||
case "netcup":
|
||||
cfg := netcup.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return netcup.NewDNSProviderConfig(cfg)
|
||||
case "netlify":
|
||||
cfg := netlify.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return netlify.NewDNSProviderConfig(cfg)
|
||||
case "nicmanager":
|
||||
cfg := nicmanager.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nicmanager.NewDNSProviderConfig(cfg)
|
||||
case "nifcloud":
|
||||
cfg := nifcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nifcloud.NewDNSProviderConfig(cfg)
|
||||
case "njalla":
|
||||
cfg := njalla.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return njalla.NewDNSProviderConfig(cfg)
|
||||
case "nodion":
|
||||
cfg := nodion.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nodion.NewDNSProviderConfig(cfg)
|
||||
case "ns1":
|
||||
cfg := ns1.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ns1.NewDNSProviderConfig(cfg)
|
||||
case "otc":
|
||||
cfg := otc.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return otc.NewDNSProviderConfig(cfg)
|
||||
case "ovh":
|
||||
cfg := ovh.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ovh.NewDNSProviderConfig(cfg)
|
||||
case "pdns":
|
||||
cfg := pdns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pdns.NewDNSProviderConfig(cfg)
|
||||
case "plesk":
|
||||
cfg := plesk.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plesk.NewDNSProviderConfig(cfg)
|
||||
case "porkbun":
|
||||
cfg := porkbun.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return porkbun.NewDNSProviderConfig(cfg)
|
||||
case "rackspace":
|
||||
cfg := rackspace.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rackspace.NewDNSProviderConfig(cfg)
|
||||
case "rcodezero":
|
||||
cfg := rcodezero.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rcodezero.NewDNSProviderConfig(cfg)
|
||||
case "regru":
|
||||
cfg := regru.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return regru.NewDNSProviderConfig(cfg)
|
||||
case "rfc2136":
|
||||
cfg := rfc2136.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rfc2136.NewDNSProviderConfig(cfg)
|
||||
case "rimuhosting":
|
||||
cfg := rimuhosting.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rimuhosting.NewDNSProviderConfig(cfg)
|
||||
case "route53":
|
||||
cfg := route53.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return route53.NewDNSProviderConfig(cfg)
|
||||
case "safedns":
|
||||
cfg := safedns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return safedns.NewDNSProviderConfig(cfg)
|
||||
case "sakuracloud":
|
||||
cfg := sakuracloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sakuracloud.NewDNSProviderConfig(cfg)
|
||||
case "scaleway":
|
||||
cfg := scaleway.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return scaleway.NewDNSProviderConfig(cfg)
|
||||
case "selectel":
|
||||
cfg := selectel.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return selectel.NewDNSProviderConfig(cfg)
|
||||
case "servercow":
|
||||
cfg := servercow.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return servercow.NewDNSProviderConfig(cfg)
|
||||
case "shellrent":
|
||||
cfg := shellrent.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return shellrent.NewDNSProviderConfig(cfg)
|
||||
case "simply":
|
||||
cfg := simply.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return simply.NewDNSProviderConfig(cfg)
|
||||
case "sonic":
|
||||
cfg := sonic.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sonic.NewDNSProviderConfig(cfg)
|
||||
case "stackpath":
|
||||
cfg := stackpath.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stackpath.NewDNSProviderConfig(cfg)
|
||||
case "tencentcloud":
|
||||
cfg := tencentcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tencentcloud.NewDNSProviderConfig(cfg)
|
||||
case "transip":
|
||||
cfg := transip.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transip.NewDNSProviderConfig(cfg)
|
||||
case "ultradns":
|
||||
cfg := ultradns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ultradns.NewDNSProviderConfig(cfg)
|
||||
case "variomedia":
|
||||
cfg := variomedia.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return variomedia.NewDNSProviderConfig(cfg)
|
||||
case "vegadns":
|
||||
cfg := vegadns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vegadns.NewDNSProviderConfig(cfg)
|
||||
case "vercel":
|
||||
cfg := vercel.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vercel.NewDNSProviderConfig(cfg)
|
||||
case "versio":
|
||||
cfg := versio.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return versio.NewDNSProviderConfig(cfg)
|
||||
case "vinyldns":
|
||||
cfg := vinyldns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vinyldns.NewDNSProviderConfig(cfg)
|
||||
case "vkcloud":
|
||||
cfg := vkcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vkcloud.NewDNSProviderConfig(cfg)
|
||||
case "vscale":
|
||||
cfg := vscale.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vscale.NewDNSProviderConfig(cfg)
|
||||
case "vultr":
|
||||
cfg := vultr.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vultr.NewDNSProviderConfig(cfg)
|
||||
case "webnames":
|
||||
cfg := webnames.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return webnames.NewDNSProviderConfig(cfg)
|
||||
case "websupport":
|
||||
cfg := websupport.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return websupport.NewDNSProviderConfig(cfg)
|
||||
case "wedos":
|
||||
cfg := wedos.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wedos.NewDNSProviderConfig(cfg)
|
||||
case "yandex":
|
||||
cfg := yandex.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return yandex.NewDNSProviderConfig(cfg)
|
||||
case "yandex360":
|
||||
cfg := yandex360.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return yandex360.NewDNSProviderConfig(cfg)
|
||||
case "yandexcloud":
|
||||
cfg := yandexcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return yandexcloud.NewDNSProviderConfig(cfg)
|
||||
case "zoneee":
|
||||
cfg := zoneee.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zoneee.NewDNSProviderConfig(cfg)
|
||||
case "zonomi":
|
||||
cfg := zonomi.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zonomi.NewDNSProviderConfig(cfg)
|
||||
default:
|
||||
return nil, fmt.Errorf("unrecognized DNS provider: %s", name)
|
||||
}
|
||||
}
|
27
src/mod/acme/acmedns/acmedns_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
package acmedns_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
)
|
||||
|
||||
// Test if the structure of ACME DNS config can be reflected from lego source code definations
|
||||
func TestACMEDNSConfigStructureReflector(t *testing.T) {
|
||||
providers := []string{
|
||||
"gandi",
|
||||
"cloudflare",
|
||||
"azure",
|
||||
}
|
||||
|
||||
for _, provider := range providers {
|
||||
strcture, err := acmedns.GetProviderConfigStructure(provider)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(strcture)
|
||||
}
|
||||
|
||||
}
|
3493
src/mod/acme/acmedns/providers.json
Normal file
80
src/mod/acme/acmedns/providerutils.go
Normal file
@ -0,0 +1,80 @@
|
||||
package acmedns
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
//go:embed providers.json
|
||||
var providers []byte //A list of providers generated by acmedns code-generator
|
||||
|
||||
type ConfigTemplate struct {
|
||||
Name string `json:"Name"`
|
||||
ConfigableFields []struct {
|
||||
Title string `json:"Title"`
|
||||
Datatype string `json:"Datatype"`
|
||||
} `json:"ConfigableFields"`
|
||||
HiddenFields []struct {
|
||||
Title string `json:"Title"`
|
||||
Datatype string `json:"Datatype"`
|
||||
} `json:"HiddenFields"`
|
||||
}
|
||||
|
||||
// Return a map of string => datatype
|
||||
func GetProviderConfigStructure(providerName string) (map[string]string, error) {
|
||||
//Load the target config template from embedded providers.json
|
||||
configTemplateMap := map[string]ConfigTemplate{}
|
||||
err := json.Unmarshal(providers, &configTemplateMap)
|
||||
if err != nil {
|
||||
return map[string]string{}, err
|
||||
}
|
||||
|
||||
targetConfigTemplate, ok := configTemplateMap[providerName]
|
||||
if !ok {
|
||||
return map[string]string{}, errors.New("provider not supported")
|
||||
}
|
||||
|
||||
results := map[string]string{}
|
||||
for _, field := range targetConfigTemplate.ConfigableFields {
|
||||
results[field.Title] = field.Datatype
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// HandleServeProvidersJson return the list of supported providers as json
|
||||
func HandleServeProvidersJson(w http.ResponseWriter, r *http.Request) {
|
||||
providerName, _ := utils.GetPara(r, "name")
|
||||
if providerName == "" {
|
||||
//Send the current list of providers
|
||||
configTemplateMap := map[string]ConfigTemplate{}
|
||||
err := json.Unmarshal(providers, &configTemplateMap)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to load DNS provider")
|
||||
return
|
||||
}
|
||||
|
||||
//Parse the provider names into an array
|
||||
providers := []string{}
|
||||
for providerName, _ := range configTemplateMap {
|
||||
providers = append(providers, providerName)
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(providers)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
}
|
||||
//Get the config for that provider
|
||||
confTemplate, err := GetProviderConfigStructure(providerName)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(confTemplate)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
@ -344,7 +344,7 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
|
||||
|
||||
// Load certificate info for ACME detail
|
||||
certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
|
||||
certInfo, err := loadCertInfoJSON(certInfoFilename)
|
||||
certInfo, err := LoadCertInfoJSON(certInfoFilename)
|
||||
if err != nil {
|
||||
log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, trying org section as ca", certName, err)
|
||||
|
||||
@ -356,7 +356,7 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
|
||||
}
|
||||
}
|
||||
|
||||
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS)
|
||||
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS)
|
||||
if err != nil {
|
||||
log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
|
||||
} else {
|
||||
@ -373,3 +373,65 @@ func (a *AutoRenewer) saveRenewConfigToFile() error {
|
||||
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
|
||||
return os.WriteFile(a.ConfigFilePath, js, 0775)
|
||||
}
|
||||
|
||||
// Handle update auto renew EAD configuration
|
||||
func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
|
||||
kid, err := utils.GetPara(r, "kid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "kid not set")
|
||||
return
|
||||
}
|
||||
|
||||
hmacEncoded, err := utils.GetPara(r, "hmacEncoded")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "hmacEncoded not set")
|
||||
return
|
||||
}
|
||||
|
||||
acmeDirectoryURL, err := utils.GetPara(r, "acmeDirectoryURL")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "acmeDirectoryURL not set")
|
||||
return
|
||||
}
|
||||
|
||||
if !a.AcmeHandler.Database.TableExists("acme") {
|
||||
a.AcmeHandler.Database.NewTable("acme")
|
||||
}
|
||||
|
||||
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_kid", kid)
|
||||
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_hmacEncoded", hmacEncoded)
|
||||
|
||||
utils.SendOK(w)
|
||||
|
||||
}
|
||||
|
||||
// Handle update auto renew DNS configuration
|
||||
func (a *AutoRenewer) HanldeSetDNS(w http.ResponseWriter, r *http.Request) {
|
||||
dnsProvider, err := utils.PostPara(r, "dnsProvider")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "dnsProvider not set")
|
||||
return
|
||||
}
|
||||
|
||||
dnsCredentials, err := utils.PostPara(r, "dnsCredentials")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "dnsCredentials not set")
|
||||
return
|
||||
}
|
||||
|
||||
filename, err := utils.PostPara(r, "filename")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "filename not set")
|
||||
return
|
||||
}
|
||||
|
||||
if !a.AcmeHandler.Database.TableExists("acme") {
|
||||
a.AcmeHandler.Database.NewTable("acme")
|
||||
}
|
||||
|
||||
a.AcmeHandler.Database.Write("acme", filename+"_dns_provider", dnsProvider)
|
||||
a.AcmeHandler.Database.Write("acme", filename+"_dns_credentials", dnsCredentials)
|
||||
|
||||
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
|
||||
}
|
@ -1,16 +1,11 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -26,41 +21,21 @@ import (
|
||||
- Vitrual Directory Routing
|
||||
*/
|
||||
|
||||
var (
|
||||
//go:embed tld.json
|
||||
rawTldMap []byte
|
||||
)
|
||||
|
||||
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
/*
|
||||
Special Routing Rules, bypass most of the limitations
|
||||
*/
|
||||
|
||||
//Check if there are external routing rule matches.
|
||||
//If yes, route them via external rr
|
||||
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
|
||||
if matchedRoutingRule != nil {
|
||||
//Matching routing rule found. Let the sub-router handle it
|
||||
if matchedRoutingRule.UseSystemAccessControl {
|
||||
//This matching rule request system access control.
|
||||
//check access logic
|
||||
respWritten := h.handleAccessRouting(w, r)
|
||||
if respWritten {
|
||||
return
|
||||
}
|
||||
}
|
||||
matchedRoutingRule.Route(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
General Access Check
|
||||
*/
|
||||
|
||||
respWritten := h.handleAccessRouting(w, r)
|
||||
if respWritten {
|
||||
return
|
||||
}
|
||||
//Inject headers
|
||||
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
|
||||
|
||||
/*
|
||||
Redirection Routing
|
||||
@ -72,46 +47,71 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Extract request host to see if it is virtual directory or subdomain
|
||||
/*
|
||||
Host Routing
|
||||
*/
|
||||
//Extract request host to see if any proxy rule is matched
|
||||
domainOnly := r.Host
|
||||
if strings.Contains(r.Host, ":") {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
|
||||
/*
|
||||
Subdomain Routing
|
||||
*/
|
||||
if strings.Contains(r.Host, ".") {
|
||||
//This might be a subdomain. See if there are any subdomain proxy router for this
|
||||
sep := h.Parent.getSubdomainProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil {
|
||||
if sep.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, sep)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
h.subdomainRequest(w, r, sep)
|
||||
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && !sep.Disabled {
|
||||
//Matching proxy rule found
|
||||
//Access Check (blacklist / whitelist)
|
||||
ruleID := sep.AccessFilterUUID
|
||||
if sep.AccessFilterUUID == "" {
|
||||
//Use default rule
|
||||
ruleID = "default"
|
||||
}
|
||||
if h.handleAccessRouting(ruleID, w, r) {
|
||||
//Request handled by subroute
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Virtual Directory Routing
|
||||
*/
|
||||
//Clean up the request URI
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath)
|
||||
if targetProxyEndpoint != nil {
|
||||
if targetProxyEndpoint.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, targetProxyEndpoint)
|
||||
//Validate basic auth
|
||||
if sep.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, sep)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
h.proxyRequest(w, r, targetProxyEndpoint)
|
||||
} else if !strings.HasSuffix(proxyingPath, "/") {
|
||||
|
||||
//Check if any virtual directory rules matches
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||
if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
//Virtual directory routing rule found. Route via vdir mode
|
||||
h.vdirRequest(w, r, targetProxyEndpoint)
|
||||
return
|
||||
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root {
|
||||
potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
|
||||
if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Fallback to handle by the host proxy forwarder
|
||||
h.hostRequest(w, r, sep)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Root Router Handling
|
||||
*/
|
||||
|
||||
//Root access control based on default rule
|
||||
blocked := h.handleAccessRouting("default", w, r)
|
||||
if blocked {
|
||||
return
|
||||
}
|
||||
|
||||
//Clean up the request URI
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
if !strings.HasSuffix(proxyingPath, "/") {
|
||||
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
|
||||
if potentialProxtEndpoint != nil {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
@ -142,121 +142,63 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
|
||||
if h.Parent.RootRoutingOptions.EnableRedirectForUnsetRules {
|
||||
//Route to custom domain
|
||||
if h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget == "" {
|
||||
//Not set. Redirect to first level of domain redirectable
|
||||
fld, err := h.getTopLevelRedirectableDomain(domainOnly)
|
||||
if err != nil {
|
||||
//Redirect to proxy root
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
} else {
|
||||
log.Println("[Router] Redirecting request from " + domainOnly + " to " + fld)
|
||||
h.logRequest(r, false, 307, "root-redirect", domainOnly)
|
||||
http.Redirect(w, r, fld, http.StatusTemporaryRedirect)
|
||||
}
|
||||
//Get the proxy root config
|
||||
proot := h.Parent.Root
|
||||
switch proot.DefaultSiteOption {
|
||||
case DefaultSite_InternalStaticWebServer:
|
||||
fallthrough
|
||||
case DefaultSite_ReverseProxy:
|
||||
//They both share the same behavior
|
||||
|
||||
//Check if any virtual directory rules matches
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||
if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
//Virtual directory routing rule found. Route via vdir mode
|
||||
h.vdirRequest(w, r, targetProxyEndpoint)
|
||||
return
|
||||
} else if h.isTopLevelRedirectableDomain(domainOnly) {
|
||||
//This is requesting a top level private domain that should be serving root
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
} else {
|
||||
//Validate the redirection target URL
|
||||
parsedURL, err := url.Parse(h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget)
|
||||
if err != nil {
|
||||
//Error when parsing target. Send to root
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root {
|
||||
potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
|
||||
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
hostname := parsedURL.Hostname()
|
||||
if domainOnly != hostname {
|
||||
//Redirect to target
|
||||
h.logRequest(r, false, 307, "root-redirect", domainOnly)
|
||||
http.Redirect(w, r, h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget, http.StatusTemporaryRedirect)
|
||||
return
|
||||
} else {
|
||||
//Loopback request due to bad settings (Shd leave it empty)
|
||||
//Forward it to root proxy
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//Route to root
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.WriteHeader(http.StatusForbidden)
|
||||
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/blacklist.html"))
|
||||
//No vdir match. Route via root router
|
||||
h.hostRequest(w, r, h.Parent.Root)
|
||||
case DefaultSite_Redirect:
|
||||
redirectTarget := strings.TrimSpace(proot.DefaultSiteValue)
|
||||
if redirectTarget == "" {
|
||||
redirectTarget = "about:blank"
|
||||
}
|
||||
|
||||
//Check if it is an infinite loopback redirect
|
||||
parsedURL, err := url.Parse(proot.DefaultSiteValue)
|
||||
if err != nil {
|
||||
w.Write(page_forbidden)
|
||||
//Error when parsing target. Send to root
|
||||
h.hostRequest(w, r, h.Parent.Root)
|
||||
return
|
||||
}
|
||||
hostname := parsedURL.Hostname()
|
||||
if hostname == domainOnly {
|
||||
h.logRequest(r, false, 500, "root-redirect", domainOnly)
|
||||
http.Error(w, "Loopback redirects due to invalid settings", 500)
|
||||
return
|
||||
}
|
||||
|
||||
h.logRequest(r, false, 307, "root-redirect", domainOnly)
|
||||
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
|
||||
case DefaultSite_NotFoundPage:
|
||||
//Serve the not found page, use template if exists
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
|
||||
if err != nil {
|
||||
w.Write(page_hosterror)
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
h.logRequest(r, false, 403, "blacklist", "")
|
||||
return true
|
||||
}
|
||||
|
||||
//Check if this ip is in whitelist
|
||||
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/whitelist.html"))
|
||||
if err != nil {
|
||||
w.Write(page_forbidden)
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
h.logRequest(r, false, 403, "whitelist", "")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Return if the given host is already topped (e.g. example.com or example.co.uk) instead of
|
||||
// a host with subdomain (e.g. test.example.com)
|
||||
func (h *ProxyHandler) isTopLevelRedirectableDomain(requestHost string) bool {
|
||||
parts := strings.Split(requestHost, ".")
|
||||
if len(parts) > 2 {
|
||||
//Cases where strange tld is used like .co.uk or .com.hk
|
||||
_, ok := h.Parent.tldMap[strings.Join(parts[1:], ".")]
|
||||
if ok {
|
||||
//Already topped
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
//Already topped
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetTopLevelRedirectableDomain returns the toppest level of domain
|
||||
// that is redirectable. E.g. a.b.c.example.co.uk will return example.co.uk
|
||||
func (h *ProxyHandler) getTopLevelRedirectableDomain(unsetSubdomainHost string) (string, error) {
|
||||
parts := strings.Split(unsetSubdomainHost, ".")
|
||||
if h.isTopLevelRedirectableDomain(unsetSubdomainHost) {
|
||||
//Already topped
|
||||
return "", errors.New("already at top level domain")
|
||||
}
|
||||
|
||||
for i := 0; i < len(parts); i++ {
|
||||
possibleTld := parts[i:]
|
||||
_, ok := h.Parent.tldMap[strings.Join(possibleTld, ".")]
|
||||
if ok {
|
||||
//This is tld length
|
||||
tld := strings.Join(parts[i-1:], ".")
|
||||
return "//" + tld, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("unsupported top level domain given")
|
||||
}
|
||||
|
65
src/mod/dynamicproxy/access.go
Normal file
@ -0,0 +1,65 @@
|
||||
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 {
|
||||
err := handleBasicAuth(w, r, pe)
|
||||
if err != nil {
|
||||
h.logRequest(r, false, 401, "host", pe.Domain)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle basic auth logic
|
||||
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
|
||||
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
|
||||
if len(pe.BasicAuthExceptionRules) > 0 {
|
||||
//Check if the current path matches the exception rules
|
||||
for _, exceptionRule := range pe.BasicAuthExceptionRules {
|
||||
@ -26,10 +36,6 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
}
|
||||
|
||||
proxyType := "vdir-auth"
|
||||
if pe.ProxyType == ProxyType_Subdomain {
|
||||
proxyType = "subd-auth"
|
||||
}
|
||||
u, p, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
@ -48,7 +54,6 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if !matchingFound {
|
||||
h.logRequest(r, false, 401, proxyType, pe.Domain)
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
|
@ -1,6 +1,7 @@
|
||||
package dpcore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
@ -8,12 +9,9 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var onExitFlushLoop func()
|
||||
|
||||
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
||||
// sends it to another server, proxying the response back to the
|
||||
// client, support http, also support https tunnel using http.hijacker
|
||||
@ -60,6 +58,7 @@ type ResponseRewriteRuleSet struct {
|
||||
ProxyDomain string
|
||||
OriginalHost string
|
||||
UseTLS bool
|
||||
NoCache bool
|
||||
PathPrefix string //Vdir prefix for root, / will be rewrite to this
|
||||
}
|
||||
|
||||
@ -67,7 +66,12 @@ type requestCanceler interface {
|
||||
CancelRequest(req *http.Request)
|
||||
}
|
||||
|
||||
func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerification bool) *ReverseProxy {
|
||||
type DpcoreOptions struct {
|
||||
IgnoreTLSVerification bool
|
||||
FlushInterval time.Duration
|
||||
}
|
||||
|
||||
func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOptions) *ReverseProxy {
|
||||
targetQuery := target.RawQuery
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
@ -79,10 +83,6 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
req.Header.Set("User-Agent", "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Hack the default transporter to handle more connections
|
||||
@ -94,16 +94,17 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
|
||||
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
|
||||
thisTransporter.(*http.Transport).DisableCompression = true
|
||||
|
||||
if ignoreTLSVerification {
|
||||
if dpcOptions.IgnoreTLSVerification {
|
||||
//Ignore TLS certificate validation error
|
||||
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
|
||||
return &ReverseProxy{
|
||||
Director: director,
|
||||
Prepender: prepender,
|
||||
Verbal: false,
|
||||
Transport: thisTransporter,
|
||||
Director: director,
|
||||
Prepender: prepender,
|
||||
FlushInterval: dpcOptions.FlushInterval,
|
||||
Verbal: false,
|
||||
Transport: thisTransporter,
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,64 +178,66 @@ var hopHeaders = []string{
|
||||
//"Upgrade",
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
|
||||
if p.FlushInterval != 0 {
|
||||
if wf, ok := dst.(writeFlusher); ok {
|
||||
mlw := &maxLatencyWriter{
|
||||
dst: wf,
|
||||
latency: p.FlushInterval,
|
||||
done: make(chan bool),
|
||||
}
|
||||
|
||||
go mlw.flushLoop()
|
||||
defer mlw.stop()
|
||||
dst = mlw
|
||||
// Copy response from src to dst with given flush interval, reference from httputil.ReverseProxy
|
||||
func (p *ReverseProxy) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error {
|
||||
var w io.Writer = dst
|
||||
if flushInterval != 0 {
|
||||
mlw := &maxLatencyWriter{
|
||||
dst: dst,
|
||||
flush: http.NewResponseController(dst).Flush,
|
||||
latency: flushInterval,
|
||||
}
|
||||
|
||||
defer mlw.stop()
|
||||
// set up initial timer so headers get flushed even if body writes are delayed
|
||||
mlw.flushPending = true
|
||||
mlw.t = time.AfterFunc(flushInterval, mlw.delayedFlush)
|
||||
w = mlw
|
||||
}
|
||||
|
||||
io.Copy(dst, src)
|
||||
var buf []byte
|
||||
_, err := p.copyBuffer(w, src, buf)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
type writeFlusher interface {
|
||||
io.Writer
|
||||
http.Flusher
|
||||
}
|
||||
// Copy with given buffer size. Default to 64k
|
||||
func (p *ReverseProxy) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
|
||||
if len(buf) == 0 {
|
||||
buf = make([]byte, 64*1024)
|
||||
}
|
||||
|
||||
type maxLatencyWriter struct {
|
||||
dst writeFlusher
|
||||
latency time.Duration
|
||||
mu sync.Mutex
|
||||
done chan bool
|
||||
}
|
||||
|
||||
func (m *maxLatencyWriter) Write(b []byte) (int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.dst.Write(b)
|
||||
}
|
||||
|
||||
func (m *maxLatencyWriter) flushLoop() {
|
||||
t := time.NewTicker(m.latency)
|
||||
defer t.Stop()
|
||||
var written int64
|
||||
for {
|
||||
select {
|
||||
case <-m.done:
|
||||
if onExitFlushLoop != nil {
|
||||
onExitFlushLoop()
|
||||
nr, rerr := src.Read(buf)
|
||||
if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
|
||||
p.logf("dpcore read error during body copy: %v", rerr)
|
||||
}
|
||||
|
||||
if nr > 0 {
|
||||
nw, werr := dst.Write(buf[:nr])
|
||||
if nw > 0 {
|
||||
written += int64(nw)
|
||||
}
|
||||
return
|
||||
case <-t.C:
|
||||
m.mu.Lock()
|
||||
m.dst.Flush()
|
||||
m.mu.Unlock()
|
||||
|
||||
if werr != nil {
|
||||
return written, werr
|
||||
}
|
||||
|
||||
if nr != nw {
|
||||
return written, io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
|
||||
if rerr != nil {
|
||||
if rerr == io.EOF {
|
||||
rerr = nil
|
||||
}
|
||||
return written, rerr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *maxLatencyWriter) stop() {
|
||||
m.done <- true
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) logf(format string, args ...interface{}) {
|
||||
if p.ErrorLog != nil {
|
||||
p.ErrorLog.Printf(format, args...)
|
||||
@ -243,7 +246,7 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func removeHeaders(header http.Header) {
|
||||
func removeHeaders(header http.Header, noCache bool) {
|
||||
// Remove hop-by-hop headers listed in the "Connection" header.
|
||||
if c := header.Get("Connection"); c != "" {
|
||||
for _, f := range strings.Split(c, ",") {
|
||||
@ -260,10 +263,25 @@ func removeHeaders(header http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
if header.Get("A-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("A-Upgrade"))
|
||||
header.Del("A-Upgrade")
|
||||
//Restore the Upgrade header if any
|
||||
if header.Get("Zr-Origin-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
|
||||
header.Del("Zr-Origin-Upgrade")
|
||||
}
|
||||
|
||||
//Disable cache if nocache is set
|
||||
if noCache {
|
||||
header.Del("Cache-Control")
|
||||
header.Set("Cache-Control", "no-store")
|
||||
}
|
||||
|
||||
//Hide Go-HTTP-Client UA if the client didnt sent us one
|
||||
if _, ok := header["User-Agent"]; !ok {
|
||||
// If the outbound request doesn't have a User-Agent header set,
|
||||
// don't send the default Go HTTP client User-Agent.
|
||||
header.Set("User-Agent", "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func addXForwardedForHeader(req *http.Request) {
|
||||
@ -281,6 +299,22 @@ func addXForwardedForHeader(req *http.Request) {
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
}
|
||||
|
||||
if req.Header.Get("X-Real-Ip") == "" {
|
||||
//Check if CF-Connecting-IP header exists
|
||||
CF_Connecting_IP := req.Header.Get("CF-Connecting-IP")
|
||||
if CF_Connecting_IP != "" {
|
||||
//Use CF Connecting IP
|
||||
req.Header.Set("X-Real-Ip", CF_Connecting_IP)
|
||||
} else {
|
||||
// Not exists. Fill it in with first entry in X-Forwarded-For
|
||||
ips := strings.Split(clientIP, ",")
|
||||
if len(ips) > 0 {
|
||||
req.Header.Set("X-Real-Ip", strings.TrimSpace(ips[0]))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,7 +357,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
copyHeader(outreq.Header, req.Header)
|
||||
|
||||
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
|
||||
removeHeaders(outreq.Header)
|
||||
removeHeaders(outreq.Header, rrr.NoCache)
|
||||
|
||||
// Add X-Forwarded-For Header.
|
||||
addXForwardedForHeader(outreq)
|
||||
@ -339,7 +373,13 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
}
|
||||
|
||||
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
|
||||
removeHeaders(res.Header)
|
||||
removeHeaders(res.Header, rrr.NoCache)
|
||||
|
||||
//Remove the User-Agent header if exists
|
||||
if _, ok := res.Header["User-Agent"]; ok {
|
||||
//Server to client request should not contains a User-Agent header
|
||||
res.Header.Del("User-Agent")
|
||||
}
|
||||
|
||||
if p.ModifyResponse != nil {
|
||||
if err := p.ModifyResponse(res); err != nil {
|
||||
@ -352,6 +392,12 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
}
|
||||
}
|
||||
|
||||
//if res.StatusCode == 501 || res.StatusCode == 500 {
|
||||
// fmt.Println(outreq.Proto, outreq.RemoteAddr, outreq.RequestURI)
|
||||
// fmt.Println(">>>", outreq.Method, res.Header, res.ContentLength, res.StatusCode)
|
||||
// fmt.Println(outreq.Header, req.Host)
|
||||
//}
|
||||
|
||||
//Custom header rewriter functions
|
||||
if res.Header.Get("Location") != "" {
|
||||
locationRewrite := res.Header.Get("Location")
|
||||
@ -400,7 +446,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
}
|
||||
}
|
||||
|
||||
p.copyResponse(rw, res.Body)
|
||||
//Get flush interval in real time and start copying the request
|
||||
flushInterval := p.getFlushInterval(req, res)
|
||||
p.copyResponse(rw, res.Body, flushInterval)
|
||||
|
||||
// close now, instead of defer, to populate res.Trailer
|
||||
res.Body.Close()
|
||||
copyHeader(rw.Header(), res.Trailer)
|
||||
|
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()
|
||||
}
|
||||
}
|
@ -22,27 +22,19 @@ import (
|
||||
|
||||
func NewDynamicProxy(option RouterOption) (*Router, error) {
|
||||
proxyMap := sync.Map{}
|
||||
domainMap := sync.Map{}
|
||||
thisRouter := Router{
|
||||
Option: &option,
|
||||
ProxyEndpoints: &proxyMap,
|
||||
SubdomainEndpoint: &domainMap,
|
||||
Running: false,
|
||||
server: nil,
|
||||
routingRules: []*RoutingRule{},
|
||||
tldMap: map[string]int{},
|
||||
Option: &option,
|
||||
ProxyEndpoints: &proxyMap,
|
||||
Running: false,
|
||||
server: nil,
|
||||
routingRules: []*RoutingRule{},
|
||||
tldMap: map[string]int{},
|
||||
}
|
||||
|
||||
thisRouter.mux = &ProxyHandler{
|
||||
Parent: &thisRouter,
|
||||
}
|
||||
|
||||
//Prase the tld map for tld redirection in main router
|
||||
//See Server.go declarations
|
||||
if len(rawTldMap) > 0 {
|
||||
json.Unmarshal(rawTldMap, &thisRouter.tldMap)
|
||||
}
|
||||
|
||||
return &thisRouter, nil
|
||||
}
|
||||
|
||||
@ -76,21 +68,14 @@ func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
|
||||
func (router *Router) StartProxyService() error {
|
||||
//Create a new server object
|
||||
if router.server != nil {
|
||||
return errors.New("Reverse proxy server already running")
|
||||
return errors.New("reverse proxy server already running")
|
||||
}
|
||||
|
||||
//Check if root route is set
|
||||
if router.Root == nil {
|
||||
return errors.New("Reverse proxy router root not set")
|
||||
return errors.New("reverse proxy router root not set")
|
||||
}
|
||||
|
||||
//Load root options from file
|
||||
loadedRootOption, err := loadRootRoutingOptionsFromFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
router.RootRoutingOptions = loadedRootOption
|
||||
|
||||
minVersion := tls.VersionTLS10
|
||||
if router.Option.ForceTLSLatest {
|
||||
minVersion = tls.VersionTLS12
|
||||
@ -101,16 +86,6 @@ func (router *Router) StartProxyService() error {
|
||||
}
|
||||
|
||||
if router.Option.UseTls {
|
||||
/*
|
||||
//Serve with TLS mode
|
||||
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
router.Running = false
|
||||
return err
|
||||
}
|
||||
router.tlsListener = ln
|
||||
*/
|
||||
router.server = &http.Server{
|
||||
Addr: ":" + strconv.Itoa(router.Option.Port),
|
||||
Handler: router.mux,
|
||||
@ -129,7 +104,7 @@ func (router *Router) StartProxyService() error {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
sep := router.getSubdomainProxyEndpointFromHostname(domainOnly)
|
||||
sep := router.getProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && sep.BypassGlobalTLS {
|
||||
//Allow routing via non-TLS handler
|
||||
originalHostHeader := r.Host
|
||||
@ -140,7 +115,29 @@ func (router *Router) StartProxyService() error {
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
sep.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
//Access Check (blacklist / whitelist)
|
||||
ruleID := sep.AccessFilterUUID
|
||||
if sep.AccessFilterUUID == "" {
|
||||
//Use default rule
|
||||
ruleID = "default"
|
||||
}
|
||||
accessRule, err := router.Option.AccessController.GetAccessRuleByID(ruleID)
|
||||
if err == nil {
|
||||
isBlocked, _ := accessRequestBlocked(accessRule, router.Option.WebDirectory, w, r)
|
||||
if isBlocked {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Validate basic auth
|
||||
if sep.RequireBasicAuth {
|
||||
err := handleBasicAuth(w, r, sep)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: sep.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: sep.RequireTLS,
|
||||
@ -225,7 +222,7 @@ func (router *Router) StartProxyService() error {
|
||||
|
||||
func (router *Router) StopProxyService() error {
|
||||
if router.server == nil {
|
||||
return errors.New("Reverse proxy server already stopped")
|
||||
return errors.New("reverse proxy server already stopped")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@ -253,13 +250,13 @@ func (router *Router) StopProxyService() error {
|
||||
// Restart the current router if it is running.
|
||||
func (router *Router) Restart() error {
|
||||
//Stop the router if it is already running
|
||||
var err error = nil
|
||||
if router.Running {
|
||||
err := router.StopProxyService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
// Start the server
|
||||
err = router.StartProxyService()
|
||||
if err != nil {
|
||||
@ -267,7 +264,7 @@ func (router *Router) Restart() error {
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
@ -280,128 +277,17 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
|
||||
hostname = r.Host
|
||||
}
|
||||
hostname = strings.Split(hostname, ":")[0]
|
||||
subdEndpoint := router.getSubdomainProxyEndpointFromHostname(hostname)
|
||||
subdEndpoint := router.getProxyEndpointFromHostname(hostname)
|
||||
return subdEndpoint != nil
|
||||
}
|
||||
|
||||
/*
|
||||
Add an URL into a custom proxy services
|
||||
*/
|
||||
func (router *Router) AddVirtualDirectoryProxyService(options *VdirOptions) error {
|
||||
domain := options.Domain
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
/*
|
||||
if rootname[len(rootname)-1:] == "/" {
|
||||
rootname = rootname[:len(rootname)-1]
|
||||
}
|
||||
*/
|
||||
|
||||
webProxyEndpoint := domain
|
||||
if options.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, options.RootName, options.SkipCertValidations)
|
||||
|
||||
endpointObject := ProxyEndpoint{
|
||||
ProxyType: ProxyType_Vdir,
|
||||
RootOrMatchingDomain: options.RootName,
|
||||
Domain: domain,
|
||||
RequireTLS: options.RequireTLS,
|
||||
SkipCertValidations: options.SkipCertValidations,
|
||||
RequireBasicAuth: options.RequireBasicAuth,
|
||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
||||
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
|
||||
Proxy: proxy,
|
||||
}
|
||||
|
||||
router.ProxyEndpoints.Store(options.RootName, &endpointObject)
|
||||
|
||||
log.Println("Registered Proxy Rule: ", options.RootName+" to "+domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Load routing from RP
|
||||
*/
|
||||
func (router *Router) LoadProxy(ptype string, key string) (*ProxyEndpoint, error) {
|
||||
if ptype == "vdir" {
|
||||
proxy, ok := router.ProxyEndpoints.Load(key)
|
||||
if !ok {
|
||||
return nil, errors.New("target proxy not found")
|
||||
}
|
||||
|
||||
targetProxy := proxy.(*ProxyEndpoint)
|
||||
targetProxy.parent = router
|
||||
return targetProxy, nil
|
||||
} else if ptype == "subd" {
|
||||
proxy, ok := router.SubdomainEndpoint.Load(key)
|
||||
if !ok {
|
||||
return nil, errors.New("target proxy not found")
|
||||
}
|
||||
|
||||
targetProxy := proxy.(*ProxyEndpoint)
|
||||
targetProxy.parent = router
|
||||
return targetProxy, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported ptype")
|
||||
}
|
||||
|
||||
/*
|
||||
Add an default router for the proxy server
|
||||
*/
|
||||
func (router *Router) SetRootProxy(options *RootOptions) error {
|
||||
proxyLocation := options.ProxyLocation
|
||||
if proxyLocation[len(proxyLocation)-1:] == "/" {
|
||||
proxyLocation = proxyLocation[:len(proxyLocation)-1]
|
||||
}
|
||||
|
||||
webProxyEndpoint := proxyLocation
|
||||
if options.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
|
||||
|
||||
rootEndpoint := ProxyEndpoint{
|
||||
ProxyType: ProxyType_Vdir,
|
||||
RootOrMatchingDomain: "/",
|
||||
Domain: proxyLocation,
|
||||
RequireTLS: options.RequireTLS,
|
||||
SkipCertValidations: options.SkipCertValidations,
|
||||
RequireBasicAuth: options.RequireBasicAuth,
|
||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
||||
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
|
||||
Proxy: proxy,
|
||||
}
|
||||
|
||||
router.Root = &rootEndpoint
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helpers to export the syncmap for easier processing
|
||||
func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
m := make(map[string]*ProxyEndpoint)
|
||||
r.SubdomainEndpoint.Range(func(key, value interface{}) bool {
|
||||
k, ok := key.(string)
|
||||
func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
|
||||
var targetProxyEndpoint *ProxyEndpoint
|
||||
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
key, ok := key.(string)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
@ -409,13 +295,32 @@ func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
m[k] = v
|
||||
|
||||
if key == matchingDomain {
|
||||
targetProxyEndpoint = v
|
||||
}
|
||||
return true
|
||||
})
|
||||
return m
|
||||
|
||||
if targetProxyEndpoint == nil {
|
||||
return nil, errors.New("target routing rule not found")
|
||||
}
|
||||
|
||||
return targetProxyEndpoint, nil
|
||||
}
|
||||
|
||||
func (r *Router) GetVDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
// Deep copy a proxy endpoint, excluding runtime paramters
|
||||
func CopyEndpoint(endpoint *ProxyEndpoint) *ProxyEndpoint {
|
||||
js, _ := json.Marshal(endpoint)
|
||||
newProxyEndpoint := ProxyEndpoint{}
|
||||
err := json.Unmarshal(js, &newProxyEndpoint)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &newProxyEndpoint
|
||||
}
|
||||
|
||||
func (r *Router) GetProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
m := make(map[string]*ProxyEndpoint)
|
||||
r.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
k, ok := key.(string)
|
||||
|
160
src/mod/dynamicproxy/endpoints.go
Normal file
@ -0,0 +1,160 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
/*
|
||||
endpoint.go
|
||||
author: tobychui
|
||||
|
||||
This script handle the proxy endpoint object actions
|
||||
so proxyEndpoint can be handled like a proper oop object
|
||||
|
||||
Most of the functions are implemented in dynamicproxy.go
|
||||
*/
|
||||
|
||||
/*
|
||||
User Defined Header Functions
|
||||
*/
|
||||
|
||||
// Check if a user define header exists in this endpoint, ignore case
|
||||
func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
|
||||
for _, header := range ep.UserDefinedHeaders {
|
||||
if strings.EqualFold(header.Key, key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Remvoe a user defined header from the list
|
||||
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
|
||||
newHeaderList := []*UserDefinedHeader{}
|
||||
for _, header := range ep.UserDefinedHeaders {
|
||||
if !strings.EqualFold(header.Key, key) {
|
||||
newHeaderList = append(newHeaderList, header)
|
||||
}
|
||||
}
|
||||
|
||||
ep.UserDefinedHeaders = newHeaderList
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add a user defined header to the list, duplicates will be automatically removed
|
||||
func (ep *ProxyEndpoint) AddUserDefinedHeader(key string, value string) error {
|
||||
if ep.UserDefinedHeaderExists(key) {
|
||||
ep.RemoveUserDefinedHeader(key)
|
||||
}
|
||||
|
||||
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, &UserDefinedHeader{
|
||||
Key: cases.Title(language.Und, cases.NoLower).String(key), //e.g. x-proxy-by -> X-Proxy-By
|
||||
Value: value,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Virtual Directory Functions
|
||||
*/
|
||||
|
||||
// Get virtual directory handler from given URI
|
||||
func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI string) *VirtualDirectoryEndpoint {
|
||||
for _, vdir := range ep.VirtualDirectories {
|
||||
if strings.HasPrefix(requestURI, vdir.MatchingPath) {
|
||||
thisVdir := vdir
|
||||
return thisVdir
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get virtual directory handler by matching path (exact match required)
|
||||
func (ep *ProxyEndpoint) GetVirtualDirectoryRuleByMatchingPath(matchingPath string) *VirtualDirectoryEndpoint {
|
||||
for _, vdir := range ep.VirtualDirectories {
|
||||
if vdir.MatchingPath == matchingPath {
|
||||
thisVdir := vdir
|
||||
return thisVdir
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete a vdir rule by its matching path
|
||||
func (ep *ProxyEndpoint) RemoveVirtualDirectoryRuleByMatchingPath(matchingPath string) error {
|
||||
entryFound := false
|
||||
newVirtualDirectoryList := []*VirtualDirectoryEndpoint{}
|
||||
for _, vdir := range ep.VirtualDirectories {
|
||||
if vdir.MatchingPath == matchingPath {
|
||||
entryFound = true
|
||||
} else {
|
||||
newVirtualDirectoryList = append(newVirtualDirectoryList, vdir)
|
||||
}
|
||||
}
|
||||
|
||||
if entryFound {
|
||||
//Update the list of vdirs
|
||||
ep.VirtualDirectories = newVirtualDirectoryList
|
||||
return nil
|
||||
}
|
||||
return errors.New("target virtual directory routing rule not found")
|
||||
}
|
||||
|
||||
// Delete a vdir rule by its matching path
|
||||
func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint) (*ProxyEndpoint, error) {
|
||||
//Check for matching path duplicate
|
||||
if ep.GetVirtualDirectoryRuleByMatchingPath(vdir.MatchingPath) != nil {
|
||||
return nil, errors.New("rule with same matching path already exists")
|
||||
}
|
||||
|
||||
//Append it to the list of virtual directory
|
||||
ep.VirtualDirectories = append(ep.VirtualDirectories, vdir)
|
||||
|
||||
//Prepare to replace the current routing rule
|
||||
parentRouter := ep.parent
|
||||
readyRoutingRule, err := parentRouter.PrepareProxyRoute(ep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ep.ProxyType == ProxyType_Root {
|
||||
parentRouter.Root = readyRoutingRule
|
||||
} else if ep.ProxyType == ProxyType_Host {
|
||||
ep.Remove()
|
||||
parentRouter.AddProxyRouteToRuntime(readyRoutingRule)
|
||||
} else {
|
||||
return nil, errors.New("unsupported proxy type")
|
||||
}
|
||||
|
||||
return readyRoutingRule, nil
|
||||
}
|
||||
|
||||
// Create a deep clone object of the proxy endpoint
|
||||
// Note the returned object is not activated. Call to prepare function before pushing into runtime
|
||||
func (ep *ProxyEndpoint) Clone() *ProxyEndpoint {
|
||||
clonedProxyEndpoint := ProxyEndpoint{}
|
||||
js, _ := json.Marshal(ep)
|
||||
json.Unmarshal(js, &clonedProxyEndpoint)
|
||||
return &clonedProxyEndpoint
|
||||
}
|
||||
|
||||
// Remove this proxy endpoint from running proxy endpoint list
|
||||
func (ep *ProxyEndpoint) Remove() error {
|
||||
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write changes to runtime without respawning the proxy handler
|
||||
// use prepare -> remove -> add if you change anything in the endpoint
|
||||
// that effects the proxy routing src / dest
|
||||
func (ep *ProxyEndpoint) UpdateToRuntime() {
|
||||
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
package dynamicproxy
|
||||
|
||||
import "errors"
|
||||
|
||||
/*
|
||||
ProxyEndpoint.go
|
||||
author: tobychui
|
||||
|
||||
This script handle the proxy endpoint object actions
|
||||
so proxyEndpoint can be handled like a proper oop object
|
||||
|
||||
Most of the functions are implemented in dynamicproxy.go
|
||||
*/
|
||||
|
||||
//Get the string version of proxy type
|
||||
func (ep *ProxyEndpoint) GetProxyTypeString() string {
|
||||
if ep.ProxyType == ProxyType_Subdomain {
|
||||
return "subd"
|
||||
} else if ep.ProxyType == ProxyType_Vdir {
|
||||
return "vdir"
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
//Update change in the current running proxy endpoint config
|
||||
func (ep *ProxyEndpoint) UpdateToRuntime() {
|
||||
if ep.IsVdir() {
|
||||
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
|
||||
|
||||
} else if ep.IsSubDomain() {
|
||||
ep.parent.SubdomainEndpoint.Store(ep.RootOrMatchingDomain, ep)
|
||||
}
|
||||
}
|
||||
|
||||
//Return true if the endpoint type is virtual directory
|
||||
func (ep *ProxyEndpoint) IsVdir() bool {
|
||||
return ep.ProxyType == ProxyType_Vdir
|
||||
}
|
||||
|
||||
//Return true if the endpoint type is subdomain
|
||||
func (ep *ProxyEndpoint) IsSubDomain() bool {
|
||||
return ep.ProxyType == ProxyType_Subdomain
|
||||
}
|
||||
|
||||
//Remove this proxy endpoint from running proxy endpoint list
|
||||
func (ep *ProxyEndpoint) Remove() error {
|
||||
//fmt.Println(ptype, key)
|
||||
if ep.IsVdir() {
|
||||
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
|
||||
return nil
|
||||
} else if ep.IsSubDomain() {
|
||||
ep.parent.SubdomainEndpoint.Delete(ep.RootOrMatchingDomain)
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid or unsupported type")
|
||||
|
||||
}
|
||||
|
||||
//ProxyEndpoint remove provide global access by key
|
||||
func (router *Router) RemoveProxyEndpointByRootname(proxyType string, rootnameOrMatchingDomain string) error {
|
||||
targetEpt, err := router.LoadProxy(proxyType, rootnameOrMatchingDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return targetEpt.Remove()
|
||||
}
|
@ -6,10 +6,12 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/websocketproxy"
|
||||
)
|
||||
@ -28,11 +30,61 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
|
||||
return targetProxyEndpoint
|
||||
}
|
||||
|
||||
func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
||||
ep, ok := router.SubdomainEndpoint.Load(hostname)
|
||||
ep, ok := router.ProxyEndpoints.Load(hostname)
|
||||
if ok {
|
||||
//Exact hit
|
||||
targetSubdomainEndpoint = ep.(*ProxyEndpoint)
|
||||
if !targetSubdomainEndpoint.Disabled {
|
||||
return targetSubdomainEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
//No hit. Try with wildcard and alias
|
||||
matchProxyEndpoints := []*ProxyEndpoint{}
|
||||
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
|
||||
ep := v.(*ProxyEndpoint)
|
||||
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
|
||||
if err != nil {
|
||||
//Bad pattern. Skip this rule
|
||||
return true
|
||||
}
|
||||
|
||||
if match {
|
||||
//Wildcard matches. Skip checking alias
|
||||
matchProxyEndpoints = append(matchProxyEndpoints, ep)
|
||||
return true
|
||||
}
|
||||
|
||||
//Wildcard not match. Check for alias
|
||||
if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 {
|
||||
for _, aliasDomain := range ep.MatchingDomainAlias {
|
||||
match, err := filepath.Match(aliasDomain, hostname)
|
||||
if err != nil {
|
||||
//Bad pattern. Skip this alias
|
||||
continue
|
||||
}
|
||||
|
||||
if match {
|
||||
//This alias match
|
||||
matchProxyEndpoints = append(matchProxyEndpoints, ep)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(matchProxyEndpoints) == 1 {
|
||||
//Only 1 match
|
||||
return matchProxyEndpoints[0]
|
||||
} else if len(matchProxyEndpoints) > 1 {
|
||||
//More than one match. Get the best match one
|
||||
sort.Slice(matchProxyEndpoints, func(i, j int) bool {
|
||||
return matchProxyEndpoints[i].RootOrMatchingDomain < matchProxyEndpoints[j].RootOrMatchingDomain
|
||||
})
|
||||
return matchProxyEndpoints[0]
|
||||
}
|
||||
|
||||
return targetSubdomainEndpoint
|
||||
@ -54,14 +106,22 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
|
||||
return rewrittenURL
|
||||
}
|
||||
|
||||
// Handle subdomain request
|
||||
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
// Handle host request
|
||||
func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
|
||||
//Inject custom headers
|
||||
if len(target.UserDefinedHeaders) > 0 {
|
||||
for _, customHeader := range target.UserDefinedHeaders {
|
||||
r.Header.Set(customHeader.Key, customHeader.Value)
|
||||
}
|
||||
}
|
||||
|
||||
requestURL := r.URL.String()
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||
wsRedirectionEndpoint := target.Domain
|
||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||
//Append / to the end of the redirection endpoint if not exists
|
||||
@ -76,7 +136,10 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
|
||||
}
|
||||
h.logRequest(r, true, 101, "subdomain-websocket", target.Domain)
|
||||
wspHandler := websocketproxy.NewProxy(u, target.SkipCertValidations)
|
||||
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
|
||||
SkipTLSValidation: target.SkipCertValidations,
|
||||
SkipOriginCheck: target.SkipWebSocketOriginCheck,
|
||||
})
|
||||
wspHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@ -89,10 +152,11 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
NoCache: h.Parent.Option.NoCache,
|
||||
PathPrefix: "",
|
||||
})
|
||||
|
||||
@ -113,15 +177,23 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
// Handle vdir type request
|
||||
func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
rewriteURL := h.Parent.rewriteURL(target.RootOrMatchingDomain, r.RequestURI)
|
||||
func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, target *VirtualDirectoryEndpoint) {
|
||||
rewriteURL := h.Parent.rewriteURL(target.MatchingPath, r.RequestURI)
|
||||
r.URL, _ = url.Parse(rewriteURL)
|
||||
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
|
||||
//Inject custom headers
|
||||
if len(target.parent.UserDefinedHeaders) > 0 {
|
||||
for _, customHeader := range target.parent.UserDefinedHeaders {
|
||||
r.Header.Set(customHeader.Key, customHeader.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||
wsRedirectionEndpoint := target.Domain
|
||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
|
||||
@ -131,7 +203,10 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
|
||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
|
||||
}
|
||||
h.logRequest(r, true, 101, "vdir-websocket", target.Domain)
|
||||
wspHandler := websocketproxy.NewProxy(u, target.SkipCertValidations)
|
||||
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
|
||||
SkipTLSValidation: target.SkipCertValidations,
|
||||
SkipOriginCheck: target.parent.SkipWebSocketOriginCheck,
|
||||
})
|
||||
wspHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@ -144,11 +219,11 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
PathPrefix: target.RootOrMatchingDomain,
|
||||
PathPrefix: target.MatchingPath,
|
||||
})
|
||||
|
||||
var dnsError *net.DNSError
|
||||
@ -171,7 +246,7 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
|
||||
if h.Parent.Option.StatisticCollector != nil {
|
||||
go func() {
|
||||
requestInfo := statistic.RequestInfo{
|
||||
IpAddr: geodb.GetRequesterIP(r),
|
||||
IpAddr: netutils.GetRequesterIP(r),
|
||||
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
|
||||
Succ: succ,
|
||||
StatusCode: statusCode,
|
||||
|
@ -2,19 +2,25 @@ package redirection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type RuleTable struct {
|
||||
AllowRegex bool //Allow regular expression to be used in rule matching. Require up to O(n^m) time complexity
|
||||
Logger *logger.Logger
|
||||
configPath string //The location where the redirection rules is stored
|
||||
rules sync.Map //Store the redirection rules for this reverse proxy instance
|
||||
|
||||
}
|
||||
|
||||
type RedirectRules struct {
|
||||
@ -24,10 +30,11 @@ type RedirectRules struct {
|
||||
StatusCode int //Status Code for redirection
|
||||
}
|
||||
|
||||
func NewRuleTable(configPath string) (*RuleTable, error) {
|
||||
func NewRuleTable(configPath string, allowRegex bool) (*RuleTable, error) {
|
||||
thisRuleTable := RuleTable{
|
||||
rules: sync.Map{},
|
||||
configPath: configPath,
|
||||
AllowRegex: allowRegex,
|
||||
}
|
||||
//Load all the rules from the config path
|
||||
if !utils.FileExists(configPath) {
|
||||
@ -77,7 +84,7 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP
|
||||
}
|
||||
|
||||
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
||||
filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json"
|
||||
filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json"
|
||||
|
||||
// Create the full file path by joining the t.configPath with the filename
|
||||
filepath := path.Join(t.configPath, filename)
|
||||
@ -105,11 +112,12 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP
|
||||
|
||||
func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
|
||||
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
||||
filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json"
|
||||
filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json"
|
||||
|
||||
// Create the full file path by joining the t.configPath with the filename
|
||||
filepath := path.Join(t.configPath, filename)
|
||||
|
||||
fmt.Println(redirectURL, filename, filepath)
|
||||
// Check if the file exists
|
||||
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||
return nil // File doesn't exist, nothing to delete
|
||||
@ -145,18 +153,47 @@ func (t *RuleTable) MatchRedirectRule(requestedURL string) *RedirectRules {
|
||||
// Iterate through all the keys in the rules map
|
||||
var targetRedirectionRule *RedirectRules = nil
|
||||
var maxMatch int = 0
|
||||
|
||||
t.rules.Range(func(key interface{}, value interface{}) bool {
|
||||
// Check if the requested URL starts with the key as a prefix
|
||||
if strings.HasPrefix(requestedURL, key.(string)) {
|
||||
// This request URL matched the domain
|
||||
if len(key.(string)) > maxMatch {
|
||||
if t.AllowRegex {
|
||||
//Regexp matching rule
|
||||
matched, err := regexp.MatchString(key.(string), requestedURL)
|
||||
if err != nil {
|
||||
//Something wrong with the regex?
|
||||
t.log("Unable to match regex", err)
|
||||
return true
|
||||
}
|
||||
if matched {
|
||||
maxMatch = len(key.(string))
|
||||
targetRedirectionRule = value.(*RedirectRules)
|
||||
}
|
||||
|
||||
} else {
|
||||
//Default: prefix matching redirect
|
||||
if strings.HasPrefix(requestedURL, key.(string)) {
|
||||
// This request URL matched the domain
|
||||
if len(key.(string)) > maxMatch {
|
||||
maxMatch = len(key.(string))
|
||||
targetRedirectionRule = value.(*RedirectRules)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return targetRedirectionRule
|
||||
}
|
||||
|
||||
// Log the message to log file, use STDOUT if logger not set
|
||||
func (t *RuleTable) log(message string, err error) {
|
||||
if t.Logger == nil {
|
||||
if err == nil {
|
||||
log.Println("[Redirect] " + message)
|
||||
} else {
|
||||
log.Println("[Redirect] " + message + ": " + err.Error())
|
||||
}
|
||||
} else {
|
||||
t.Logger.PrintAndLog("Redirect", message, err)
|
||||
}
|
||||
}
|
||||
|
@ -1,51 +0,0 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
rootRoute.go
|
||||
|
||||
This script handle special case in routing where the root proxy
|
||||
entity is involved. This also include its setting object
|
||||
RootRoutingOptions
|
||||
*/
|
||||
|
||||
var rootConfigFilepath string = "conf/root_config.json"
|
||||
|
||||
func loadRootRoutingOptionsFromFile() (*RootRoutingOptions, error) {
|
||||
if !utils.FileExists(rootConfigFilepath) {
|
||||
//Not found. Create a root option
|
||||
js, _ := json.MarshalIndent(RootRoutingOptions{}, "", " ")
|
||||
err := os.WriteFile(rootConfigFilepath, js, 0775)
|
||||
if err != nil {
|
||||
return nil, errors.New("Unable to write root config to file: " + err.Error())
|
||||
}
|
||||
}
|
||||
newRootOption := RootRoutingOptions{}
|
||||
rootOptionsBytes, err := os.ReadFile(rootConfigFilepath)
|
||||
if err != nil {
|
||||
log.Println("[Error] Unable to read root config file at " + rootConfigFilepath + ": " + err.Error())
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(rootOptionsBytes, &newRootOption)
|
||||
if err != nil {
|
||||
log.Println("[Error] Unable to parse root config file: " + err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &newRootOption, nil
|
||||
}
|
||||
|
||||
// Save the new config to file. Note that this will not overwrite the runtime one
|
||||
func (opt *RootRoutingOptions) SaveToFile() error {
|
||||
js, _ := json.MarshalIndent(opt, "", " ")
|
||||
err := os.WriteFile(rootConfigFilepath, js, 0775)
|
||||
return err
|
||||
}
|
121
src/mod/dynamicproxy/router.go
Normal file
@ -0,0 +1,121 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
)
|
||||
|
||||
/*
|
||||
Dynamic Proxy Router Functions
|
||||
|
||||
This script handle the proxy rules router spawning
|
||||
and preparation
|
||||
*/
|
||||
|
||||
// Prepare proxy route generate a proxy handler service object for your endpoint
|
||||
func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
|
||||
//Filter the tailing slash if any
|
||||
domain := endpoint.Domain
|
||||
if len(domain) == 0 {
|
||||
return nil, errors.New("invalid endpoint config")
|
||||
}
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
endpoint.Domain = domain
|
||||
|
||||
//Parse the web proxy endpoint
|
||||
webProxyEndpoint := domain
|
||||
if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
|
||||
//TLS is not hardcoded in proxy target domain
|
||||
if endpoint.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Create the proxy routing handler
|
||||
proxy := dpcore.NewDynamicProxyCore(path, "", &dpcore.DpcoreOptions{
|
||||
IgnoreTLSVerification: endpoint.SkipCertValidations,
|
||||
})
|
||||
endpoint.proxy = proxy
|
||||
endpoint.parent = router
|
||||
|
||||
//Prepare proxy routing hjandler for each of the virtual directories
|
||||
for _, vdir := range endpoint.VirtualDirectories {
|
||||
domain := vdir.Domain
|
||||
if len(domain) == 0 {
|
||||
//invalid vdir
|
||||
continue
|
||||
}
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
//Parse the web proxy endpoint
|
||||
webProxyEndpoint = domain
|
||||
if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
|
||||
//TLS is not hardcoded in proxy target domain
|
||||
if vdir.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, vdir.MatchingPath, &dpcore.DpcoreOptions{
|
||||
IgnoreTLSVerification: vdir.SkipCertValidations,
|
||||
})
|
||||
vdir.proxy = proxy
|
||||
vdir.parent = endpoint
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
// Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime
|
||||
func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
|
||||
if endpoint.proxy == nil {
|
||||
//This endpoint is not prepared
|
||||
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
|
||||
}
|
||||
// Push record into running subdomain endpoints
|
||||
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set given Proxy Route as Root. Call to PrepareProxyRoute before adding to runtime
|
||||
func (router *Router) SetProxyRouteAsRoot(endpoint *ProxyEndpoint) error {
|
||||
if endpoint.proxy == nil {
|
||||
//This endpoint is not prepared
|
||||
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
|
||||
}
|
||||
// Push record into running root endpoints
|
||||
router.Root = endpoint
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProxyEndpoint remove provide global access by key
|
||||
func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain string) error {
|
||||
targetEpt, err := router.LoadProxy(rootnameOrMatchingDomain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return targetEpt.Remove()
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
)
|
||||
|
||||
/*
|
||||
Add an URL intoa custom subdomain service
|
||||
|
||||
*/
|
||||
|
||||
func (router *Router) AddSubdomainRoutingService(options *SubdOptions) error {
|
||||
domain := options.Domain
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
webProxyEndpoint := domain
|
||||
if options.RequireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
|
||||
|
||||
router.SubdomainEndpoint.Store(options.MatchingDomain, &ProxyEndpoint{
|
||||
RootOrMatchingDomain: options.MatchingDomain,
|
||||
Domain: domain,
|
||||
RequireTLS: options.RequireTLS,
|
||||
Proxy: proxy,
|
||||
BypassGlobalTLS: options.BypassGlobalTLS,
|
||||
SkipCertValidations: options.SkipCertValidations,
|
||||
RequireBasicAuth: options.RequireBasicAuth,
|
||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
||||
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
|
||||
})
|
||||
|
||||
log.Println("Adding Subdomain Rule: ", options.MatchingDomain+" to "+domain)
|
||||
return nil
|
||||
}
|
@ -39,7 +39,7 @@
|
||||
<h3 style="margin-top: 1em;">403 - Forbidden</h3>
|
||||
<div class="ui divider"></div>
|
||||
<p>You do not have permission to view this directory or page. <br>
|
||||
This might cause by the region limit setting of this site.</p>
|
||||
This might be caused by the region limit setting of this site.</p>
|
||||
<div class="ui divider"></div>
|
||||
<div style="text-align: left;">
|
||||
<small>Request time: <span id="reqtime"></span></small><br>
|
||||
|
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>
|