88 Commits
3.0.7 ... 3.1.1

Author SHA1 Message Date
d5315e5b8e Merge pull request #289 from tobychui/v3.1.1
v3.1.1 update
2024-09-04 21:35:21 +08:00
31cc1a69a1 Merge pull request #295 from PassiveLemon/zerotier
Add ZeroTier to Docker container
2024-09-01 23:01:20 +08:00
d348cbf48b Update Docker README 2024-08-30 09:47:02 -04:00
f6339868ac Refactor Dockerfile and bundle ZeroTier 2024-08-30 09:47:02 -04:00
af10f2a644 Fix typos and inconsistencies in README 2024-08-28 18:27:49 -04:00
3b247c31da Fixed typo in README 2024-08-27 10:18:08 +08:00
d74e8badb9 Fixed #287
- Removed unusded tab switch in quicksetup.js
- Changed Macedonia to North Macedonia
2024-08-25 13:12:07 +08:00
b40131d212 Updated geodb and merged PR from main 2024-08-23 17:52:36 +08:00
563a12c860 Merge pull request #286 from ahmadsyamim/patch-1
Fix typo remvoeClass to removeClass
2024-08-23 17:37:52 +08:00
8b2c3b7e03 Fix typo remvoeClass to removeClass 2024-08-23 09:51:34 +08:00
608cc0c523 Optimized upstream & loadbalancer
- Test and optimized load balancer origin picker
- Fixed no active origin cannot load proxy rule bug
- Implemented logger design in websocket proxy module
- Added more quickstart tours
- Fixed #270 (I guess)
- Fixed #90 (I guess)
2024-08-19 16:10:35 +08:00
b558bcbfcf Merge pull request #258 from bouroo/perf/upstreams-sortfunc
weighted random upstream
2024-08-19 15:39:22 +08:00
9ea3fa2542 Added tour for setup https 2024-08-16 22:28:21 +08:00
01f68c5ef5 Added tour for basic operations
- added static website setup tour
- added subdomain setup tour
2024-08-15 22:35:43 +08:00
a7f89086d4 Restructured log format in acme module
- Replaced all log.Println in acme module to system wide logger
- Fixed file manager path escape bug #274
2024-08-13 21:56:23 +08:00
a5ef6456c6 v3.1.1 init
- Fixed path traverse bug in web server file manager
- Merged docker container list from main
- Updated version code
- Merged network status fix from PR
- Removed unused comments in dpcore
-
2024-08-07 13:53:43 +08:00
87659b43bd Merge pull request #278 from JokerQyou/fix/network-io-chart-not-rendering
Fix network I/O chart not rendering.
2024-08-07 13:49:02 +08:00
ddbecf7b68 Merge pull request #280 from 7brend7/fix-added-containers-list
Fix existings containers list in docker popup
2024-08-07 13:40:24 +08:00
1b3a9de378 Fix existings containers list in docker popup 2024-08-04 00:25:13 +03:00
6dd62f509d Update network data instead of assigning new variables. 2024-08-02 22:00:51 +08:00
d5cc6a6859 Fix network I/O chart not rendering.
Close #200.
2024-08-02 00:07:12 +08:00
1d965da7d0 Merge pull request #277 from Morethanevil/main
Update CHANGELOG.md
2024-08-01 08:43:46 +08:00
3567c70bab Update CHANGELOG.md 2024-07-31 19:52:31 +02:00
0a734e0bd3 Merge pull request #275 from tobychui/v3.1.0
v3.1.0 Update
2024-07-31 22:39:01 +08:00
f4fa92635c Added example go.mod files for windows 7 2024-07-31 22:35:25 +08:00
7d5151bb00 Add EarlyRenew flag to Dockerfile 2024-07-31 10:21:57 -04:00
54475e4b99 Fixed #271
- Fixed implementation in geoip resolver trie tree
2024-07-31 21:57:59 +08:00
6ac16caf37 Update main.go
- Updated main to internal web fs
2024-07-31 16:15:59 +08:00
97502db607 Update extract.go
- Updated lego config extractor
2024-07-31 16:12:28 +08:00
0747cf4b0f Fixed gandi DNS bug
- Fixed gandi DNS challenge extra input field
- Updated geoip list
2024-07-31 16:11:50 +08:00
94483acc92 Added log viewer filter
+ Added filter to log viewer #243
+ Added auto log refresh
2024-07-31 16:01:49 +08:00
7626857c02 Updated acme dns list
- Updated acme dns configs
- Updated dns propagation timeout from default (2min) to 5 minutes
2024-07-29 12:55:37 +08:00
0f772a715b Update extract.go
Updared extractor to compatible with later version of lego
2024-07-29 12:50:57 +08:00
fd1439f746 Fixed csrf token error in cert upload ui
- Fixed csrf token error in cert upload interface
- Added system wide logger into tls cert manager
2024-07-29 12:28:21 +08:00
ca37bfbfa6 Fixed #106
- Added experimental proxmox fixes
- Fixed upstream error resp code not logging bug
2024-07-27 17:33:41 +08:00
c1e16d55ab Optimized csrf mux
- Forced same site to lax mode for better browser compatibility
- Set zoraxy-csrf as cookie name
2024-07-24 22:47:49 +08:00
f595da92a1 Fixed #267
- Added csrf middleware to management portal mux
- Added csrf token to all html templates
- Added csrf validation to all endpoints
- Optimized some old endpoints implementation
2024-07-24 21:58:44 +08:00
8a8ec1cb0b 📝 randIndex for fallbackUpstreams random 2024-07-24 14:59:48 +07:00
e53c3cf3c4 ️ fallbackUpstreams with preserve index 2024-07-24 14:47:33 +07:00
d17de5c200 weighted random upstream 2024-07-23 08:50:10 +07:00
97ff48ee70 🔥 origins already checked before getRandomUpstreamByWeight 2024-07-23 08:31:59 +07:00
d64b1174af keep compatible with go 1.20 2024-07-23 08:31:59 +07:00
bec363abab ️ immediate return if single upstream 2024-07-23 08:31:59 +07:00
0dddd1f9e3 📝 discribe for upstream sort func 2024-07-23 08:31:59 +07:00
6bfcb2e1f5 ️ slices.SortFunc for upstreams 2024-07-23 08:31:59 +07:00
02ff288280 Doc: Note about PORT usage for Docker run and compose 2024-07-22 14:03:10 -04:00
b1c5bc2963 Fixed #255
- Added host header manual overwrite feature
- Added toggle for automatic hop-by-hop header removing
2024-07-21 17:06:09 +08:00
d3dbbf9052 Merge branch 'v3.1.0' of https://github.com/tobychui/zoraxy into v3.1.0 2024-07-21 15:11:27 +08:00
f4a5c905e7 Fixed #256
- Added startup paramter to change the early renew days of certificates
- Changed the default early renew days of certificates from 14 days to 30 days
- Fixed vdir update not updating uptime monitor bug
2024-07-21 15:11:13 +08:00
245379e91f Fixed #254
- Added uptime cleaning logic to update function
2024-07-19 10:21:26 +08:00
955a2232df Update Makefile
- Fixed bug in CICD pipeline
2024-07-18 18:50:45 +08:00
7eb7ae7ced Merge pull request #251 from PassiveLemon/docker-timezone
Doc: Document on how to use host time in the container
2024-07-16 23:12:14 +08:00
3aa0f2d914 Target latest alpine image 2024-07-16 11:07:47 -04:00
39b0c8c674 Doc: Document on how to use host time in the container 2024-07-16 10:56:12 -04:00
bddeae8365 Fixed manual renew certificate bug
- Fixed manual renew certificate bug in wildcard certs
- Updated version no
2024-07-16 22:08:51 +08:00
8e0e9531e7 Merge pull request #250 from Morethanevil/main
Update CHANGELOG.md
2024-07-16 20:35:04 +08:00
6ff22865e0 Update CHANGELOG.md 2024-07-16 14:26:19 +02:00
0828fd1958 Update update.go
Fixed bug in skip version upgrade
2024-07-16 15:14:49 +08:00
82f84470f7 Merge pull request #246 from tobychui/3.0.9
Update 3.0.9
2024-07-16 13:15:02 +08:00
cf9a05f130 Updated v3.0.9
- Added certificate download
- Updated netcup timeout value
- Updated geoip db
- Removed debug print from log viewer
- Upgraded netstat log printing to new log formatter
- Improved updater implementation
2024-07-16 11:30:12 +08:00
301072db90 Fixed #231
- Added higher propagation timeout for netcup
- Fixed bug in CICD script
2024-07-16 10:37:10 +08:00
cfcd10d64f Update README.md
Updated new start parameters and feature list
2024-07-15 23:00:59 +08:00
c85760c73a Merge pull request #242 from Morethanevil/main
Update CHANGELOG.md
2024-07-15 21:39:01 +08:00
b7bb918aa3 Fix: Container issue due to deprecated flag 2024-07-15 09:21:14 -04:00
962f3e0566 Update CHANGELOG.md 2024-07-15 14:16:46 +02:00
0bcf2b2ae3 Updates v3.0.8
Merge pull request #239 from tobychui/main
2024-07-15 14:49:46 +08:00
6bfeb8cf3d Merge pull request #239 from tobychui/main
Merge change in main branch before v3.0.8 release
2024-07-15 14:43:07 +08:00
33def66386 Merge branch 'v3.0.8' into main 2024-07-15 14:42:19 +08:00
cb469f28d2 Updated geodb 2024-07-15 14:39:04 +08:00
8239f4cb53 Added apache compatible logger
- Rewritten the logger to make it more apache log parser friendly
- Fixed uptime not updating after upstream change bug
- Added SSO page (wip)
- Added log viewer
2024-07-14 22:25:49 +08:00
e410b92e34 Fixed #235
- Added flush sniffing for keep-alive request
- Set default flush interval to 100ms for hostname and 500ms for virtual directory
2024-07-13 23:12:23 +08:00
aca6e44b35 Added load balance origin picker
+ Added load balance picker
+ Added fallback mode for upstream
+ Added stick session
2024-07-12 20:14:31 +08:00
2aa35cbe6d Added load balancer (wip)
+ Added support for multiple upstreams
+ Added load balancer
+ Added upstream abstraction in endpoint
+ Added load balancer structure
+ Added breaking change auto-updater
+ Added uptime monitor proxy type definitions
+ Added upstream editor UI
+ Fixed charset bug in many snippets HTML files
2024-07-01 21:17:20 +08:00
745a54605f Merge pull request #225 from Kegelcizer/image-size-shrink
Update Dockerfile to shrink image
2024-06-29 12:02:27 +08:00
e3b61868a1 Update Dockerfile
Created folders and set permissions at build stage. Install stuff and copy only required folders with permissions already set at build stage.
2024-06-28 11:23:35 +03:00
764b1944be Merge pull request #216 from Kirari04/main
fix: unable to edit proxy if ratelimit is not set or <= 0
2024-06-28 09:58:15 +08:00
100cd727fc format docker file 2024-06-26 19:54:35 +02:00
7e62fef879 3.0.8 init
- Fixed rate limit bug  #216
- Added HSTS auto subdomain include check
2024-06-25 18:10:39 +08:00
1a4a55721f Improve Docker Image Size by 500mb by using 2 stage build 2024-06-24 23:32:38 +02:00
bb9deccff6 fix: unable to edit proxy if ratelimit is not set or <= 0
this fix checks the ratelimit value only if the
requireRateLimit is set to true else it will use
the provided ratelimit value unless it is less or equal to 0
then it will default to 1000 (the same value as set inside the ui)
2024-06-24 22:59:51 +02:00
a18413dd03 Merge pull request #213 from PassiveLemon/docker-env-rework
Docker env rework
2024-06-21 10:38:44 +08:00
2cd1b1de3c Update: Docker README 2024-06-20 10:58:13 -04:00
3a2db63d61 Refactor: Docker environment variables 2024-06-20 10:58:04 -04:00
123d3bcf3f Remove: Deprecated VERSION variable 2024-06-20 10:57:34 -04:00
3ec1d9c888 Update CHANGELOG.md
Example is removed while docker maintainers is discussing how to better support docker parameter
2024-06-20 15:49:48 +08:00
5785261c7e Merge pull request #210 from Morethanevil/main
Update CHANGELOG.md
2024-06-20 15:29:37 +08:00
89e60649e5 Update CHANGELOG.md 2024-06-20 09:07:22 +02:00
5423b82858 Update CHANGELOG.md
Updated changelog, providing an example configuration for docker
2024-06-20 09:01:14 +02:00
130 changed files with 21834 additions and 26101 deletions

View File

@ -33,7 +33,6 @@ jobs:
docker buildx create --name mainbuilder --driver docker-container --platform linux/amd64,linux/arm64 --use
docker buildx build --push \
--build-arg VERSION=${{ github.event.release.tag_name }} \
--provenance=false \
--platform linux/amd64,linux/arm64 \
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \

View File

@ -1,3 +1,53 @@
# v3.1.0 31 Jul 2024
+ Updated log viewer with filter and auto refresh [#243](https://github.com/tobychui/zoraxy/issues/243)
+ Fixed csrf vulnerability [#267](https://github.com/tobychui/zoraxy/issues/267)
+ Fixed promox issue
+ Fixed status code bug in upstream log [#254](https://github.com/tobychui/zoraxy/issues/254)
+ Added host overwrite and hop-by-hop header remover
+ Added early renew days settings [#256](https://github.com/tobychui/zoraxy/issues/256)
+ Updated make file to force no CGO in cicd process
+ Fixed bug in updater
+ Fixed wildcard certificate renew bug [#249](https://github.com/tobychui/zoraxy/issues/249)
+ Added certificate download function [#227](https://github.com/tobychui/zoraxy/issues/227)
# v3.0.9 16 Jul 2024
+ Added certificate download [#227](https://github.com/tobychui/zoraxy/issues/227)
+ Updated netcup timeout value [#231](https://github.com/tobychui/zoraxy/issues/231)
+ Updated geoip db
+ Removed debug print from log viewer
+ Upgraded netstat log printing to new log formatter
+ Improved update module implementation
# v3.0.8 15 Jul 2024
+ Added apache style logging mechanism (and build-in log viewer) [#218](https://github.com/tobychui/zoraxy/issues/218)
+ Fixed keep alive flushing issues [#235](https://github.com/tobychui/zoraxy/issues/235)
+ Added multi-upstream supports [#100](https://github.com/tobychui/zoraxy/issues/100)
+ Added stick session load balancer
+ Added weighted random load balancer
+ Added domain cleaning logic to domain / IP input fields
+ Added HSTS "include subdomain" auto injector
+ Added work-in-progress SSO / Oauth Server UI
+ Fixed uptime monitor not updating on proxy rule change bug
+ Optimized UI for create new proxy rule
+ Removed service expose proxy feature
# v3.0.7 20 Jun 2024
+ Fixed redirection enable bug [#199](https://github.com/tobychui/zoraxy/issues/199)
+ Fixed header tool user agent rewrite sequence
+ Optimized rate limit UI
+ Added HSTS and Permission Policy Editor [#163](https://github.com/tobychui/zoraxy/issues/163)
+ Docker UX optimization start parameter `-docker`
+ Docker container selector implementation for conditional compilations for Windows
From contributors:
+ Add Rate Limits Limits to Zoraxy fixes [185](https://github.com/tobychui/zoraxy/issues/185) by [Kirari04](https://github.com/Kirari04)
+ Add docker containers list to set rule by [7brend7](https://github.com/7brend7) [PR202](https://github.com/tobychui/zoraxy/pull/202)
# v3.0.6 10 Jun 2024
+ Added fastly_client_ip to X-Real-IP auto rewrite
@ -13,6 +63,9 @@
+ Added stream proxy auto start [#169](https://github.com/tobychui/zoraxy/issues/169)
+ Optimized UX for reminding user to click Apply after port change
+ Added version number to footer [#160](https://github.com/tobychui/zoraxy/issues/160)
From contributors:
+ Fixed missing / unnecessary error check [PR187](https://github.com/tobychui/zoraxy/pull/187) by [Kirari04](https://github.com/Kirari04)
# v3.0.5 May 26 2024

View File

@ -4,7 +4,6 @@
A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
### Features
- Simple to use interface with detail in-system instructions
@ -21,12 +20,14 @@ A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
- DNS Challenge for Let's Encrypt and [these DNS providers](https://go-acme.github.io/lego/dns/)
- 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
- Stream Proxy (TCP & UDP)
- Integrated Up-time Monitor
- Web-SSH Terminal
- Utilities
- CIDR IP converters
- mDNS Scanner
- Wake-On-Lan
- Debug Forward Proxy
- IP Scanner
- Others
- Basic single-admin management mode
@ -36,15 +37,16 @@ A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
## 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)
/ [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/)
For other systems or architectures, please see [Releases](https://github.com/tobychui/zoraxy/releases/latest/)
## Getting Started
[Installing Zoraxy Reverse Proxy: Your Gateway to Efficient Web Routing](https://geekscircuit.com/installing-zoraxy-reverse-proxy-your-gateway-to-efficient-web-routing/)
Thank you for the well written and easy to follow tutorial by Reddit users [itsvmn](https://www.reddit.com/user/itsvmn/)!
Thank you for the well written and easy to follow tutorial by Reddit user [itsvmn](https://www.reddit.com/user/itsvmn/)!
If you have no background in setting up reverse proxy or web routing, you should check this out before you start setting up your Zoraxy.
## Build from Source
@ -62,7 +64,7 @@ sudo ./zoraxy -port=:8000
## Usage
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructionss below for your desired deployment platform.
Zoraxy provides basic authentication system for standalone mode. To use it in standalone mode, follow the instructions below for your desired deployment platform.
### Standalone Mode
@ -90,18 +92,18 @@ The installation method is same as Linux. For other ARM SBCs, please refer to yo
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details.
### Start Paramters
### Start Parameters
```
Usage of zoraxy:
-autorenew int
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
-cfgupgrade
Enable auto config upgrade if breaking change is detected (default true)
-docker
Run Zoraxy in docker compatibility mode
-fastgeoip
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
-log
Log terminal output to file (default true)
-mdns
Enable mDNS scanner and transponder (default true)
-mdnsname string
@ -117,7 +119,7 @@ Usage of zoraxy:
-webfm
Enable web file manager for static web server root folder (default true)
-webroot string
Static web server root folder. Only allow chnage in start paramters (default "./www")
Static web server root folder. Only allow change in start parameters (default "./www")
-ztauth string
ZeroTier authtoken for the local node
-ztport int
@ -132,7 +134,7 @@ If you already have an upstream reverse proxy server in place with permission ma
./zoraxy -noauth=true
```
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
*Note: For security reasons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
## Screenshots

View File

@ -1,17 +1,8 @@
FROM docker.io/golang:alpine
# VERSION comes from the main.yml workflow --build-arg
ARG VERSION
RUN apk add --no-cache bash netcat-openbsd sudo
FROM docker.io/golang:alpine AS build
RUN mkdir -p /opt/zoraxy/source/ &&\
mkdir -p /opt/zoraxy/config/ &&\
mkdir -p /usr/local/bin/
RUN chmod -R 770 /opt/zoraxy/
VOLUME [ "/opt/zoraxy/config/" ]
# If you build it yourself, you will need to add the src directory into the docker directory.
COPY ./src/ /opt/zoraxy/source/
@ -19,17 +10,43 @@ WORKDIR /opt/zoraxy/source/
RUN go mod tidy &&\
go build -o /usr/local/bin/zoraxy &&\
chmod 755 /usr/local/bin/zoraxy
FROM docker.io/alpine:latest
WORKDIR /opt/zoraxy/source/
RUN apk add --no-cache bash netcat-openbsd sudo &&\
wget https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/zerotier-one-1.10.2-r0.apk &&\
apk add --no-cache zerotier-one-1.10.2-r0.apk &&\
rm -r /opt/zoraxy/source/
RUN chmod 755 /usr/local/bin/zoraxy &&\
chmod +x /usr/local/bin/zoraxy
COPY --from=build /usr/local/bin/zoraxy /usr/local/bin/zoraxy
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
WORKDIR /opt/zoraxy/config/
ENV VERSION=$VERSION
ENV ARGS="-noauth=false"
ENV ZEROTIER="false"
ENTRYPOINT "zoraxy" "-port=:8000" "-docker=true" "${ARGS}"
ENV AUTORENEW="86400"
ENV CFGUPGRADE="true"
ENV DOCKER="true"
ENV EARLYRENEW="30"
ENV FASTGEOIP="false"
ENV MDNS="true"
ENV MDNSNAME="''"
ENV NOAUTH="false"
ENV PORT="8000"
ENV SSHLB="false"
ENV VERSION="false"
ENV WEBFM="true"
ENV WEBROOT="./www"
ENV ZTAUTH=""
ENV ZTPORT="9993"
HEALTHCHECK --interval=5s --timeout=5s --retries=2 CMD nc -vz 127.0.0.1 8000 || exit 1
VOLUME [ "/opt/zoraxy/config/", "/var/lib/zerotier-one/" ]
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 CMD nc -vz 127.0.0.1 $PORT || exit 1

View File

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

25
docker/entrypoint.sh Normal file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
if [ "$ZEROTIER" = "true" ]; then
echo "Starting ZeroTier daemon..."
zerotier-one -d
fi
echo "Starting Zoraxy..."
exec zoraxy \
-autorenew="$AUTORENEW" \
-cfgupgrade="$CFGUPGRADE" \
-docker="$DOCKER" \
-earlyrenew="$EARLYRENEW" \
-fastgeoip="$FASTGEOIP" \
-mdns="$MDNS" \
-mdnsname="$MDNSNAME" \
-noauth="$NOAUTH" \
-port=:"$PORT" \
-sshlb="$SSHLB" \
-version="$VERSION" \
-webfm="$WEBFM" \
-webroot="$WEBROOT" \
-ztauth="$ZTAUTH" \
-ztport="$ZTPORT"

View File

@ -19,7 +19,7 @@ clean:
$(PLATFORMS):
@echo "Building $(os)/$(arch)"
GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) $(if $(filter linux/arm,$(os)/$(arch)),GOARM=6,) go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) $(if $(filter linux/arm,$(os)/$(arch)),GOARM=6,) CGO_ENABLED="0" go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
# GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) GOARM=6 go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath

View File

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

View File

@ -22,11 +22,11 @@ import (
var requireAuth = true
func initAPIs() {
func initAPIs(targetMux *http.ServeMux) {
authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{
AuthAgent: authAgent,
RequireAuth: requireAuth,
TargetMux: targetMux,
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
},
@ -37,12 +37,12 @@ func initAPIs() {
if development {
fs = http.FileServer(http.Dir("web/"))
}
//Add a layer of middleware for advance control
//Add a layer of middleware for advance control
advHandler := FSHandler(fs)
http.Handle("/", advHandler)
targetMux.Handle("/", advHandler)
//Authentication APIs
registerAuthAPIs(requireAuth)
registerAuthAPIs(requireAuth, targetMux)
//Reverse proxy
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
@ -61,6 +61,12 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
//Reverse proxy upstream (load balance) APIs
authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList)
authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd)
authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority)
authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate)
authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete)
//Reverse proxy virtual directory APIs
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
@ -71,6 +77,8 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
authRouter.HandleFunc("/api/proxy/header/handleHSTS", HandleHSTSState)
authRouter.HandleFunc("/api/proxy/header/handleHopByHop", HandleHopByHop)
authRouter.HandleFunc("/api/proxy/header/handleHostOverwrite", HandleHostOverwrite)
authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy)
//Reverse proxy auth related APIs
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
@ -81,6 +89,7 @@ func initAPIs() {
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
authRouter.HandleFunc("/api/cert/download", handleCertDownload)
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
@ -121,7 +130,7 @@ func initAPIs() {
//Statistic & uptime monitoring API
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
authRouter.HandleFunc("/api/stats/netstat", netstat.HandleGetNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
@ -142,7 +151,7 @@ func initAPIs() {
authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
//TCP Proxy
//Stream (TCP / UDP) Proxy
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
@ -178,8 +187,8 @@ func initAPIs() {
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
//Account Reset
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
http.HandleFunc("/api/account/new", HandleNewPasswordSetup)
targetMux.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup)
//ACME & Auto Renewer
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
@ -219,32 +228,33 @@ func initAPIs() {
authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList)
//Others
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo)
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
//Debug
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
//If you got APIs to add, append them here
// get available docker containers
}
// Function to renders Auth related APIs
func registerAuthAPIs(requireAuth bool) {
func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
//Auth APIs
http.HandleFunc("/api/auth/login", authAgent.HandleLogin)
http.HandleFunc("/api/auth/logout", authAgent.HandleLogout)
http.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) {
targetMux.HandleFunc("/api/auth/login", authAgent.HandleLogin)
targetMux.HandleFunc("/api/auth/logout", authAgent.HandleLogout)
targetMux.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) {
if requireAuth {
authAgent.CheckLogin(w, r)
} else {
utils.SendJSONResponse(w, "true")
}
})
http.HandleFunc("/api/auth/username", func(w http.ResponseWriter, r *http.Request) {
targetMux.HandleFunc("/api/auth/username", func(w http.ResponseWriter, r *http.Request) {
username, err := authAgent.GetUserName(w, r)
if err != nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
@ -254,12 +264,12 @@ func registerAuthAPIs(requireAuth bool) {
js, _ := json.Marshal(username)
utils.SendJSONResponse(w, string(js))
})
http.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
targetMux.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
uc := authAgent.GetUserCounts()
js, _ := json.Marshal(uc)
utils.SendJSONResponse(w, string(js))
})
http.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
targetMux.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
if authAgent.GetUserCounts() == 0 {
//Allow register root admin
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {
@ -270,7 +280,7 @@ func registerAuthAPIs(requireAuth bool) {
utils.SendErrorResponse(w, "Root management account already exists")
}
})
http.HandleFunc("/api/auth/changePassword", func(w http.ResponseWriter, r *http.Request) {
targetMux.HandleFunc("/api/auth/changePassword", func(w http.ResponseWriter, r *http.Request) {
username, err := authAgent.GetUserName(w, r)
if err != nil {
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)

View File

@ -182,27 +182,28 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
sysdb.Read("settings", "usetls", &currentTlsSetting)
}
newState, err := utils.PostPara(r, "set")
if err != nil {
//No setting. Get the current status
if r.Method == http.MethodGet {
//Get the current status
js, _ := json.Marshal(currentTlsSetting)
utils.SendJSONResponse(w, string(js))
} else {
if newState == "true" {
} else if r.Method == http.MethodPost {
newState, err := utils.PostBool(r, "set")
if err != nil {
utils.SendErrorResponse(w, "new state not set or invalid")
return
}
if newState {
sysdb.Write("settings", "usetls", true)
SystemWideLogger.Println("Enabling TLS mode on reverse proxy")
dynamicProxyRouter.UpdateTLSSetting(true)
} else if newState == "false" {
} else {
sysdb.Write("settings", "usetls", false)
SystemWideLogger.Println("Disabling TLS mode on reverse proxy")
dynamicProxyRouter.UpdateTLSSetting(false)
} else {
utils.SendErrorResponse(w, "invalid state given. Only support true or false")
return
}
utils.SendOK(w)
} else {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
}
@ -233,6 +234,51 @@ func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
}
}
// Handle download of the selected certificate
func handleCertDownload(w http.ResponseWriter, r *http.Request) {
// get the certificate name
certname, err := utils.GetPara(r, "certname")
if err != nil {
utils.SendErrorResponse(w, "invalid certname given")
return
}
certname = filepath.Base(certname) //prevent path escape
// check if the cert exists
pubKey := filepath.Join(filepath.Join("./conf/certs"), certname+".key")
priKey := filepath.Join(filepath.Join("./conf/certs"), certname+".pem")
if utils.FileExists(pubKey) && utils.FileExists(priKey) {
//Zip them and serve them via http download
seeking, _ := utils.GetBool(r, "seek")
if seeking {
//This request only check if the key exists. Do not provide download
utils.SendOK(w)
return
}
//Serve both file in zip
zipTmpFolder := "./tmp/download"
os.MkdirAll(zipTmpFolder, 0775)
zipFileName := filepath.Join(zipTmpFolder, certname+".zip")
err := utils.ZipFiles(zipFileName, pubKey, priKey)
if err != nil {
http.Error(w, "Failed to create zip file", http.StatusInternalServerError)
return
}
defer os.Remove(zipFileName) // Clean up the zip file after serving
// Serve the zip file
w.Header().Set("Content-Disposition", "attachment; filename=\""+certname+"_export.zip\"")
w.Header().Set("Content-Type", "application/zip")
http.ServeFile(w, r, zipFileName)
} else {
//Not both key exists
utils.SendErrorResponse(w, "invalid key-pairs: private key or public key not found in key store")
return
}
}
// Handle upload of the certificate
func handleCertUpload(w http.ResponseWriter, r *http.Request) {
// check if request method is POST

View File

@ -14,6 +14,7 @@ import (
"time"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/utils"
)
@ -79,7 +80,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
return errors.New("not supported proxy type")
}
SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+thisConfigEndpoint.Domain+" routing rule loaded", nil)
SystemWideLogger.PrintAndLog("proxy-config", thisConfigEndpoint.RootOrMatchingDomain+" -> "+loadbalance.GetUpstreamsAsString(thisConfigEndpoint.ActiveOrigins)+" routing rule loaded", nil)
return nil
}
@ -130,12 +131,18 @@ func RemoveReverseProxyConfig(endpoint string) error {
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,
ProxyType: dynamicproxy.ProxyType_Root,
RootOrMatchingDomain: "/",
ActiveOrigins: []*loadbalance.Upstream{
{
OriginIpOrDomain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
RequireTLS: false,
SkipCertValidations: false,
Weight: 0,
},
},
InactiveOrigins: []*loadbalance.Upstream{},
BypassGlobalTLS: false,
SkipCertValidations: false,
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
RequireBasicAuth: false,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},

View File

@ -95,6 +95,7 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.2 // indirect
github.com/gophercloud/gophercloud v1.0.0 // indirect
github.com/gorilla/csrf v1.7.2 // 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

View File

@ -317,6 +317,8 @@ github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7
github.com/gophercloud/gophercloud v1.0.0 h1:9nTGx0jizmHxDobe4mck89FyQHVyA3CaXLIUSGJjP9k=
github.com/gophercloud/gophercloud v1.0.0/go.mod h1:Q8fZtyi5zZxPS/j9aj3sSxtvj41AdQMDwyo1myduD5c=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=

View File

@ -12,6 +12,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/gorilla/csrf"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
@ -24,6 +25,7 @@ import (
"imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/info/logviewer"
"imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/pathrule"
@ -32,6 +34,7 @@ import (
"imuslab.com/zoraxy/mod/statistic/analytic"
"imuslab.com/zoraxy/mod/streamproxy"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/update"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
"imuslab.com/zoraxy/mod/webserv"
@ -48,16 +51,17 @@ var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local no
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
var runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode")
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
var acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)")
var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
var enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
var (
name = "Zoraxy"
version = "3.0.7"
nodeUUID = "generic"
development = false //Set this to false to use embedded web fs
version = "3.1.1"
nodeUUID = "generic" //System uuid, in uuidv4 format
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()
/*
@ -69,11 +73,13 @@ var (
/*
Handler Modules
*/
sysdb *database.Database //System database
authAgent *auth.AuthAgent //Authentication agent
tlsCertManager *tlscert.Manager //TLS / SSL management
redirectTable *redirection.RuleTable //Handle special redirection rule sets
loadbalancer *loadbalance.RouteManager //Load balancer manager to get routing targets from proxy rules
sysdb *database.Database //System database
authAgent *auth.AuthAgent //Authentication agent
tlsCertManager *tlscert.Manager //TLS / SSL management
redirectTable *redirection.RuleTable //Handle special redirection rule sets
webminPanelMux *http.ServeMux //Server mux for handling webmin panel APIs
csrfMiddleware func(http.Handler) http.Handler //CSRF protection middleware
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
accessController *access.Controller //Access controller, handle black list and white list
@ -88,12 +94,14 @@ var (
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only
SystemWideLogger *logger.Logger //Logger for Zoraxy
LogViewer *logviewer.Viewer
)
// Kill signal handler. Do something before the system the core terminate.
@ -108,32 +116,34 @@ func SetupCloseHandler() {
}
func ShutdownSeq() {
fmt.Println("- Shutting down " + name)
fmt.Println("- Closing GeoDB ")
geodbStore.Close()
fmt.Println("- Closing Netstats Listener")
SystemWideLogger.Println("Shutting down " + name)
//SystemWideLogger.Println("Closing GeoDB")
//geodbStore.Close()
SystemWideLogger.Println("Closing Netstats Listener")
netstatBuffers.Close()
fmt.Println("- Closing Statistic Collector")
SystemWideLogger.Println("Closing Statistic Collector")
statisticCollector.Close()
if mdnsTickerStop != nil {
fmt.Println("- Stopping mDNS Discoverer (might take a few minutes)")
SystemWideLogger.Println("Stopping mDNS Discoverer (might take a few minutes)")
// Stop the mdns service
mdnsTickerStop <- true
}
mdnsScanner.Close()
fmt.Println("- Closing Certificates Auto Renewer")
SystemWideLogger.Println("Shutting down load balancer")
loadBalancer.Close()
SystemWideLogger.Println("Closing Certificates Auto Renewer")
acmeAutoRenewer.Close()
//Remove the tmp folder
fmt.Println("- Cleaning up tmp files")
SystemWideLogger.Println("Cleaning up tmp files")
os.RemoveAll("./tmp")
fmt.Println("- Closing system wide logger")
SystemWideLogger.Close()
//Close database, final
fmt.Println("- Stopping system database")
//Close database
SystemWideLogger.Println("Stopping system database")
sysdb.Close()
//Close logger
SystemWideLogger.Println("Closing system wide logger")
SystemWideLogger.Close()
}
func main() {
@ -144,6 +154,16 @@ func main() {
os.Exit(0)
}
if !utils.ValidateListeningAddress(*webUIPort) {
fmt.Println("Malformed -port (listening address) paramter. Do you mean -port=:" + *webUIPort + "?")
os.Exit(0)
}
if *enableAutoUpdate {
fmt.Println("Checking required config update")
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(version))
}
SetupCloseHandler()
//Read or create the system uuid
@ -159,12 +179,22 @@ func main() {
}
nodeUUID = string(uuidBytes)
//Create a new webmin mux and csrf middleware layer
webminPanelMux = http.NewServeMux()
csrfMiddleware = csrf.Protect(
[]byte(nodeUUID),
csrf.CookieName("zoraxy-csrf"),
csrf.Secure(false),
csrf.Path("/"),
csrf.SameSite(csrf.SameSiteLaxMode),
)
//Startup all modules
startupSequence()
//Initiate management interface APIs
requireAuth = !(*noauth)
initAPIs()
initAPIs(webminPanelMux)
//Start the reverse proxy server in go routine
go func() {
@ -177,7 +207,7 @@ func main() {
finalSequence()
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
err = http.ListenAndServe(*webUIPort, nil)
err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux))
if err != nil {
log.Fatal(err)

View File

@ -11,7 +11,6 @@ import (
"encoding/pem"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
@ -26,6 +25,7 @@ import (
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
@ -68,25 +68,31 @@ type ACMEHandler struct {
DefaultAcmeServer string
Port string
Database *database.Database
Logger *logger.Logger
}
// NewACME creates a new ACMEHandler instance.
func NewACME(acmeServer string, port string, database *database.Database) *ACMEHandler {
func NewACME(defaultAcmeServer string, port string, database *database.Database, logger *logger.Logger) *ACMEHandler {
return &ACMEHandler{
DefaultAcmeServer: acmeServer,
DefaultAcmeServer: defaultAcmeServer,
Port: port,
Database: database,
Logger: logger,
}
}
func (a *ACMEHandler) Logf(message string, err error) {
a.Logger.PrintAndLog("ACME", message, err)
}
// ObtainCert obtains a certificate for the specified domains.
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool) (bool, error) {
log.Println("[ACME] Obtaining certificate...")
a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil)
// generate private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Println(err)
a.Logf("Private key generation failed", err)
return false, err
}
@ -102,7 +108,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// skip TLS verify if need
// Ref: https://github.com/go-acme/lego/blob/6af2c756ac73a9cb401621afca722d0f4112b1b8/lego/client_config.go#L74
if skipTLS {
log.Println("[INFO] Ignore TLS/SSL Verification Error for ACME Server")
a.Logf("Ignoring TLS/SSL Verification Error for ACME Server", nil)
config.HTTPClient.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
@ -129,16 +135,16 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// if not custom ACME url, load it from ca.json
if caName == "custom" {
log.Println("[INFO] Using Custom ACME " + caUrl + " for CA Directory URL")
a.Logf("Using Custom ACME "+caUrl+" for CA Directory URL", nil)
} else {
caLinkOverwrite, err := loadCAApiServerFromName(caName)
if err == nil {
config.CADirURL = caLinkOverwrite
log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
a.Logf("Using "+caLinkOverwrite+" for CA Directory URL", nil)
} else {
// (caName == "" || caUrl == "") will use default acme
config.CADirURL = a.DefaultAcmeServer
log.Println("[INFO] Using Default ACME " + a.DefaultAcmeServer + " for CA Directory URL")
a.Logf("Using Default ACME "+a.DefaultAcmeServer+" for CA Directory URL", nil)
}
}
@ -146,7 +152,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
client, err := lego.NewClient(config)
if err != nil {
log.Println(err)
a.Logf("Failed to spawn new ACME client from current config", err)
return false, err
}
@ -164,32 +170,32 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
var dnsCredentials string
err := a.Database.Read("acme", certificateName+"_dns_credentials", &dnsCredentials)
if err != nil {
log.Println(err)
a.Logf("Read DNS credential failed", err)
return false, err
}
var dnsProvider string
err = a.Database.Read("acme", certificateName+"_dns_provider", &dnsProvider)
if err != nil {
log.Println(err)
a.Logf("Read DNS Provider failed", err)
return false, err
}
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials)
if err != nil {
log.Println(err)
a.Logf("Unable to resolve DNS challenge provider", err)
return false, err
}
err = client.Challenge.SetDNS01Provider(provider)
if err != nil {
log.Println(err)
a.Logf("Failed to resolve DNS01 Provider", err)
return false, err
}
} else {
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
if err != nil {
log.Println(err)
a.Logf("Failed to resolve HTTP01 Provider", err)
return false, err
}
}
@ -205,7 +211,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
var reg *registration.Resource
// New users will need to register
if client.GetExternalAccountRequired() {
log.Println("External Account Required for this ACME Provider.")
a.Logf("External Account Required for this ACME Provider", nil)
// IF KID and HmacEncoded is overidden
if !a.Database.TableExists("acme") {
@ -220,20 +226,18 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
var kid string
var hmacEncoded string
err := a.Database.Read("acme", config.CADirURL+"_kid", &kid)
if err != nil {
log.Println(err)
a.Logf("Failed to read kid from database", err)
return false, err
}
err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded)
if err != nil {
log.Println(err)
a.Logf("Failed to read HMAC from database", err)
return false, err
}
log.Println("EAB Credential retrieved.", kid, hmacEncoded)
a.Logf("EAB Credential retrieved: "+kid+" / "+hmacEncoded, nil)
if kid != "" && hmacEncoded != "" {
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true,
@ -242,14 +246,14 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
})
}
if err != nil {
log.Println(err)
a.Logf("Register with external account binder failed", 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)
a.Logf("Unable to register client", err)
return false, err
}
}
@ -262,7 +266,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
}
certificates, err := client.Certificate.Obtain(request)
if err != nil {
log.Println(err)
a.Logf("Obtain certificate failed", err)
return false, err
}
@ -270,12 +274,12 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// private key, and a certificate URL.
err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
if err != nil {
log.Println(err)
a.Logf("Failed to write public key to disk", err)
return false, err
}
err = os.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
if err != nil {
log.Println(err)
a.Logf("Failed to write private key to disk", err)
return false, err
}
@ -289,13 +293,13 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
certInfoBytes, err := json.Marshal(certInfo)
if err != nil {
log.Println(err)
a.Logf("Marshal certificate renew config failed", err)
return false, err
}
err = os.WriteFile("./conf/certs/"+certificateName+".json", certInfoBytes, 0777)
if err != nil {
log.Println(err)
a.Logf("Failed to write certificate renew config to file", err)
return false, err
}
@ -313,7 +317,7 @@ func (a *ACMEHandler) CheckCertificate() []string {
expiredCerts := []string{}
if err != nil {
log.Println(err)
a.Logf("Failed to load certificate folder", err)
return []string{}
}
@ -410,14 +414,14 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
ca, err := utils.PostPara(r, "ca")
if err != nil {
log.Println("[INFO] CA not set. Using default")
a.Logf("CA not set. Using default", nil)
ca, caUrl = "", ""
}
if ca == "custom" {
caUrl, err = utils.PostPara(r, "caURL")
if err != nil {
log.Println("[INFO] Custom CA set but no URL provide, Using default")
a.Logf("Custom CA set but no URL provide, Using default", nil)
ca, caUrl = "", ""
}
}
@ -448,7 +452,12 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
}
domains := strings.Split(domainPara, ",")
result, err := a.ObtainCert(domains, filename, email, ca, caUrl, skipTLS, dns)
//Clean spaces in front or behind each domain
cleanedDomains := []string{}
for _, domain := range domains {
cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
}
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns)
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
@ -460,7 +469,7 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
func jsonEscape(i string) string {
b, err := json.Marshal(i)
if err != nil {
log.Println("Unable to escape json data: " + err.Error())
//log.Println("Unable to escape json data: " + err.Error())
return i
}
s := string(b)

View File

@ -1,11 +1,6 @@
package acme
import (
"errors"
"log"
"os"
"strings"
"github.com/go-acme/lego/v4/challenge"
"imuslab.com/zoraxy/mod/acme/acmedns"
)
@ -29,7 +24,7 @@ func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string) (c
/*
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)
@ -41,6 +36,7 @@ func setCredentialsIntoEnvironmentVariables(credentials map[string]string) {
}
}
func extractDnsCredentials(input string) (map[string]string, error) {
result := make(map[string]string)
@ -70,3 +66,5 @@ func extractDnsCredentials(input string) (map[string]string, error) {
return result, nil
}
*/

View File

@ -6,6 +6,7 @@ package acmedns
import (
"encoding/json"
"fmt"
"time"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/providers/dns/alidns"
@ -142,6 +143,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return alidns.NewDNSProviderConfig(cfg)
case "allinkl":
cfg := allinkl.NewDefaultConfig()
@ -149,6 +151,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return allinkl.NewDNSProviderConfig(cfg)
case "arvancloud":
cfg := arvancloud.NewDefaultConfig()
@ -156,6 +159,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return arvancloud.NewDNSProviderConfig(cfg)
case "auroradns":
cfg := auroradns.NewDefaultConfig()
@ -163,6 +167,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return auroradns.NewDNSProviderConfig(cfg)
case "autodns":
cfg := autodns.NewDefaultConfig()
@ -170,6 +175,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return autodns.NewDNSProviderConfig(cfg)
case "azure":
cfg := azure.NewDefaultConfig()
@ -177,6 +183,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return azure.NewDNSProviderConfig(cfg)
case "azuredns":
cfg := azuredns.NewDefaultConfig()
@ -184,6 +191,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return azuredns.NewDNSProviderConfig(cfg)
case "bindman":
cfg := bindman.NewDefaultConfig()
@ -191,6 +199,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return bindman.NewDNSProviderConfig(cfg)
case "bluecat":
cfg := bluecat.NewDefaultConfig()
@ -198,6 +207,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return bluecat.NewDNSProviderConfig(cfg)
case "brandit":
cfg := brandit.NewDefaultConfig()
@ -205,6 +215,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return brandit.NewDNSProviderConfig(cfg)
case "bunny":
cfg := bunny.NewDefaultConfig()
@ -212,6 +223,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return bunny.NewDNSProviderConfig(cfg)
case "checkdomain":
cfg := checkdomain.NewDefaultConfig()
@ -219,6 +231,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return checkdomain.NewDNSProviderConfig(cfg)
case "civo":
cfg := civo.NewDefaultConfig()
@ -226,6 +239,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return civo.NewDNSProviderConfig(cfg)
case "clouddns":
cfg := clouddns.NewDefaultConfig()
@ -233,6 +247,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return clouddns.NewDNSProviderConfig(cfg)
case "cloudflare":
cfg := cloudflare.NewDefaultConfig()
@ -240,6 +255,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return cloudflare.NewDNSProviderConfig(cfg)
case "cloudns":
cfg := cloudns.NewDefaultConfig()
@ -247,6 +263,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return cloudns.NewDNSProviderConfig(cfg)
case "cloudru":
cfg := cloudru.NewDefaultConfig()
@ -254,6 +271,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return cloudru.NewDNSProviderConfig(cfg)
case "cloudxns":
cfg := cloudxns.NewDefaultConfig()
@ -261,6 +279,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return cloudxns.NewDNSProviderConfig(cfg)
case "conoha":
cfg := conoha.NewDefaultConfig()
@ -268,6 +287,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return conoha.NewDNSProviderConfig(cfg)
case "constellix":
cfg := constellix.NewDefaultConfig()
@ -275,6 +295,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return constellix.NewDNSProviderConfig(cfg)
case "cpanel":
cfg := cpanel.NewDefaultConfig()
@ -282,6 +303,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return cpanel.NewDNSProviderConfig(cfg)
case "derak":
cfg := derak.NewDefaultConfig()
@ -289,6 +311,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return derak.NewDNSProviderConfig(cfg)
case "desec":
cfg := desec.NewDefaultConfig()
@ -296,6 +319,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return desec.NewDNSProviderConfig(cfg)
case "digitalocean":
cfg := digitalocean.NewDefaultConfig()
@ -303,6 +327,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return digitalocean.NewDNSProviderConfig(cfg)
case "dnshomede":
cfg := dnshomede.NewDefaultConfig()
@ -310,6 +335,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return dnshomede.NewDNSProviderConfig(cfg)
case "dnsimple":
cfg := dnsimple.NewDefaultConfig()
@ -317,6 +343,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return dnsimple.NewDNSProviderConfig(cfg)
case "dnsmadeeasy":
cfg := dnsmadeeasy.NewDefaultConfig()
@ -324,6 +351,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return dnsmadeeasy.NewDNSProviderConfig(cfg)
case "dnspod":
cfg := dnspod.NewDefaultConfig()
@ -331,6 +359,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return dnspod.NewDNSProviderConfig(cfg)
case "dode":
cfg := dode.NewDefaultConfig()
@ -338,6 +367,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return dode.NewDNSProviderConfig(cfg)
case "domeneshop":
cfg := domeneshop.NewDefaultConfig()
@ -345,6 +375,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return domeneshop.NewDNSProviderConfig(cfg)
case "dreamhost":
cfg := dreamhost.NewDefaultConfig()
@ -352,6 +383,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return dreamhost.NewDNSProviderConfig(cfg)
case "duckdns":
cfg := duckdns.NewDefaultConfig()
@ -359,6 +391,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return duckdns.NewDNSProviderConfig(cfg)
case "dyn":
cfg := dyn.NewDefaultConfig()
@ -366,6 +399,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return dyn.NewDNSProviderConfig(cfg)
case "dynu":
cfg := dynu.NewDefaultConfig()
@ -373,6 +407,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return dynu.NewDNSProviderConfig(cfg)
case "easydns":
cfg := easydns.NewDefaultConfig()
@ -380,6 +415,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return easydns.NewDNSProviderConfig(cfg)
case "efficientip":
cfg := efficientip.NewDefaultConfig()
@ -387,6 +423,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return efficientip.NewDNSProviderConfig(cfg)
case "epik":
cfg := epik.NewDefaultConfig()
@ -394,6 +431,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return epik.NewDNSProviderConfig(cfg)
case "exoscale":
cfg := exoscale.NewDefaultConfig()
@ -401,6 +439,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return exoscale.NewDNSProviderConfig(cfg)
case "freemyip":
cfg := freemyip.NewDefaultConfig()
@ -408,6 +447,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return freemyip.NewDNSProviderConfig(cfg)
case "gandi":
cfg := gandi.NewDefaultConfig()
@ -415,6 +455,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return gandi.NewDNSProviderConfig(cfg)
case "gandiv5":
cfg := gandiv5.NewDefaultConfig()
@ -422,6 +463,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return gandiv5.NewDNSProviderConfig(cfg)
case "gcore":
cfg := gcore.NewDefaultConfig()
@ -429,6 +471,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return gcore.NewDNSProviderConfig(cfg)
case "glesys":
cfg := glesys.NewDefaultConfig()
@ -436,6 +479,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return glesys.NewDNSProviderConfig(cfg)
case "godaddy":
cfg := godaddy.NewDefaultConfig()
@ -443,6 +487,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return godaddy.NewDNSProviderConfig(cfg)
case "googledomains":
cfg := googledomains.NewDefaultConfig()
@ -450,6 +495,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return googledomains.NewDNSProviderConfig(cfg)
case "hetzner":
cfg := hetzner.NewDefaultConfig()
@ -457,6 +503,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return hetzner.NewDNSProviderConfig(cfg)
case "hostingde":
cfg := hostingde.NewDefaultConfig()
@ -464,6 +511,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return hostingde.NewDNSProviderConfig(cfg)
case "hosttech":
cfg := hosttech.NewDefaultConfig()
@ -471,6 +519,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return hosttech.NewDNSProviderConfig(cfg)
case "httpnet":
cfg := httpnet.NewDefaultConfig()
@ -478,6 +527,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return httpnet.NewDNSProviderConfig(cfg)
case "hyperone":
cfg := hyperone.NewDefaultConfig()
@ -485,6 +535,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return hyperone.NewDNSProviderConfig(cfg)
case "ibmcloud":
cfg := ibmcloud.NewDefaultConfig()
@ -492,6 +543,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return ibmcloud.NewDNSProviderConfig(cfg)
case "iij":
cfg := iij.NewDefaultConfig()
@ -499,6 +551,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return iij.NewDNSProviderConfig(cfg)
case "iijdpf":
cfg := iijdpf.NewDefaultConfig()
@ -506,6 +559,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return iijdpf.NewDNSProviderConfig(cfg)
case "infoblox":
cfg := infoblox.NewDefaultConfig()
@ -513,6 +567,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return infoblox.NewDNSProviderConfig(cfg)
case "infomaniak":
cfg := infomaniak.NewDefaultConfig()
@ -520,6 +575,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return infomaniak.NewDNSProviderConfig(cfg)
case "internetbs":
cfg := internetbs.NewDefaultConfig()
@ -527,6 +583,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return internetbs.NewDNSProviderConfig(cfg)
case "inwx":
cfg := inwx.NewDefaultConfig()
@ -534,6 +591,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return inwx.NewDNSProviderConfig(cfg)
case "ionos":
cfg := ionos.NewDefaultConfig()
@ -541,6 +599,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return ionos.NewDNSProviderConfig(cfg)
case "ipv64":
cfg := ipv64.NewDefaultConfig()
@ -548,6 +607,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return ipv64.NewDNSProviderConfig(cfg)
case "iwantmyname":
cfg := iwantmyname.NewDefaultConfig()
@ -555,6 +615,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return iwantmyname.NewDNSProviderConfig(cfg)
case "joker":
cfg := joker.NewDefaultConfig()
@ -562,6 +623,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return joker.NewDNSProviderConfig(cfg)
case "liara":
cfg := liara.NewDefaultConfig()
@ -569,6 +631,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return liara.NewDNSProviderConfig(cfg)
case "lightsail":
cfg := lightsail.NewDefaultConfig()
@ -576,6 +639,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return lightsail.NewDNSProviderConfig(cfg)
case "linode":
cfg := linode.NewDefaultConfig()
@ -583,6 +647,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return linode.NewDNSProviderConfig(cfg)
case "liquidweb":
cfg := liquidweb.NewDefaultConfig()
@ -590,6 +655,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return liquidweb.NewDNSProviderConfig(cfg)
case "loopia":
cfg := loopia.NewDefaultConfig()
@ -597,6 +663,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return loopia.NewDNSProviderConfig(cfg)
case "luadns":
cfg := luadns.NewDefaultConfig()
@ -604,6 +671,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return luadns.NewDNSProviderConfig(cfg)
case "mailinabox":
cfg := mailinabox.NewDefaultConfig()
@ -611,6 +679,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return mailinabox.NewDNSProviderConfig(cfg)
case "metaname":
cfg := metaname.NewDefaultConfig()
@ -618,6 +687,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return metaname.NewDNSProviderConfig(cfg)
case "mydnsjp":
cfg := mydnsjp.NewDefaultConfig()
@ -625,6 +695,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return mydnsjp.NewDNSProviderConfig(cfg)
case "namecheap":
cfg := namecheap.NewDefaultConfig()
@ -632,6 +703,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return namecheap.NewDNSProviderConfig(cfg)
case "namedotcom":
cfg := namedotcom.NewDefaultConfig()
@ -639,6 +711,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return namedotcom.NewDNSProviderConfig(cfg)
case "namesilo":
cfg := namesilo.NewDefaultConfig()
@ -646,6 +719,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return namesilo.NewDNSProviderConfig(cfg)
case "nearlyfreespeech":
cfg := nearlyfreespeech.NewDefaultConfig()
@ -653,20 +727,23 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
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 "netcup":
cfg := netcup.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 20*time.Minute
return netcup.NewDNSProviderConfig(cfg)
case "netlify":
cfg := netlify.NewDefaultConfig()
err := json.Unmarshal([]byte(js), &cfg)
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return netlify.NewDNSProviderConfig(cfg)
case "nicmanager":
cfg := nicmanager.NewDefaultConfig()
@ -674,6 +751,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return nicmanager.NewDNSProviderConfig(cfg)
case "nifcloud":
cfg := nifcloud.NewDefaultConfig()
@ -681,6 +759,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return nifcloud.NewDNSProviderConfig(cfg)
case "njalla":
cfg := njalla.NewDefaultConfig()
@ -688,6 +767,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return njalla.NewDNSProviderConfig(cfg)
case "nodion":
cfg := nodion.NewDefaultConfig()
@ -695,6 +775,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return nodion.NewDNSProviderConfig(cfg)
case "ns1":
cfg := ns1.NewDefaultConfig()
@ -702,6 +783,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return ns1.NewDNSProviderConfig(cfg)
case "otc":
cfg := otc.NewDefaultConfig()
@ -709,6 +791,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return otc.NewDNSProviderConfig(cfg)
case "ovh":
cfg := ovh.NewDefaultConfig()
@ -716,6 +799,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return ovh.NewDNSProviderConfig(cfg)
case "pdns":
cfg := pdns.NewDefaultConfig()
@ -723,6 +807,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return pdns.NewDNSProviderConfig(cfg)
case "plesk":
cfg := plesk.NewDefaultConfig()
@ -730,6 +815,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return plesk.NewDNSProviderConfig(cfg)
case "porkbun":
cfg := porkbun.NewDefaultConfig()
@ -737,6 +823,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return porkbun.NewDNSProviderConfig(cfg)
case "rackspace":
cfg := rackspace.NewDefaultConfig()
@ -744,6 +831,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return rackspace.NewDNSProviderConfig(cfg)
case "rcodezero":
cfg := rcodezero.NewDefaultConfig()
@ -751,6 +839,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return rcodezero.NewDNSProviderConfig(cfg)
case "regru":
cfg := regru.NewDefaultConfig()
@ -758,6 +847,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return regru.NewDNSProviderConfig(cfg)
case "rfc2136":
cfg := rfc2136.NewDefaultConfig()
@ -765,6 +855,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return rfc2136.NewDNSProviderConfig(cfg)
case "rimuhosting":
cfg := rimuhosting.NewDefaultConfig()
@ -772,6 +863,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return rimuhosting.NewDNSProviderConfig(cfg)
case "route53":
cfg := route53.NewDefaultConfig()
@ -779,6 +871,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return route53.NewDNSProviderConfig(cfg)
case "safedns":
cfg := safedns.NewDefaultConfig()
@ -786,6 +879,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return safedns.NewDNSProviderConfig(cfg)
case "sakuracloud":
cfg := sakuracloud.NewDefaultConfig()
@ -793,6 +887,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return sakuracloud.NewDNSProviderConfig(cfg)
case "scaleway":
cfg := scaleway.NewDefaultConfig()
@ -800,6 +895,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return scaleway.NewDNSProviderConfig(cfg)
case "selectel":
cfg := selectel.NewDefaultConfig()
@ -807,6 +903,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return selectel.NewDNSProviderConfig(cfg)
case "servercow":
cfg := servercow.NewDefaultConfig()
@ -814,6 +911,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return servercow.NewDNSProviderConfig(cfg)
case "shellrent":
cfg := shellrent.NewDefaultConfig()
@ -821,6 +919,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return shellrent.NewDNSProviderConfig(cfg)
case "simply":
cfg := simply.NewDefaultConfig()
@ -828,6 +927,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return simply.NewDNSProviderConfig(cfg)
case "sonic":
cfg := sonic.NewDefaultConfig()
@ -835,6 +935,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return sonic.NewDNSProviderConfig(cfg)
case "stackpath":
cfg := stackpath.NewDefaultConfig()
@ -842,6 +943,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return stackpath.NewDNSProviderConfig(cfg)
case "tencentcloud":
cfg := tencentcloud.NewDefaultConfig()
@ -849,6 +951,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return tencentcloud.NewDNSProviderConfig(cfg)
case "transip":
cfg := transip.NewDefaultConfig()
@ -856,6 +959,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return transip.NewDNSProviderConfig(cfg)
case "ultradns":
cfg := ultradns.NewDefaultConfig()
@ -863,6 +967,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return ultradns.NewDNSProviderConfig(cfg)
case "variomedia":
cfg := variomedia.NewDefaultConfig()
@ -870,6 +975,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return variomedia.NewDNSProviderConfig(cfg)
case "vegadns":
cfg := vegadns.NewDefaultConfig()
@ -877,6 +983,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return vegadns.NewDNSProviderConfig(cfg)
case "vercel":
cfg := vercel.NewDefaultConfig()
@ -884,6 +991,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return vercel.NewDNSProviderConfig(cfg)
case "versio":
cfg := versio.NewDefaultConfig()
@ -891,6 +999,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return versio.NewDNSProviderConfig(cfg)
case "vinyldns":
cfg := vinyldns.NewDefaultConfig()
@ -898,6 +1007,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return vinyldns.NewDNSProviderConfig(cfg)
case "vkcloud":
cfg := vkcloud.NewDefaultConfig()
@ -905,6 +1015,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return vkcloud.NewDNSProviderConfig(cfg)
case "vscale":
cfg := vscale.NewDefaultConfig()
@ -912,6 +1023,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return vscale.NewDNSProviderConfig(cfg)
case "vultr":
cfg := vultr.NewDefaultConfig()
@ -919,6 +1031,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return vultr.NewDNSProviderConfig(cfg)
case "webnames":
cfg := webnames.NewDefaultConfig()
@ -926,6 +1039,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return webnames.NewDNSProviderConfig(cfg)
case "websupport":
cfg := websupport.NewDefaultConfig()
@ -933,6 +1047,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return websupport.NewDNSProviderConfig(cfg)
case "wedos":
cfg := wedos.NewDefaultConfig()
@ -940,6 +1055,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return wedos.NewDNSProviderConfig(cfg)
case "yandex":
cfg := yandex.NewDefaultConfig()
@ -947,6 +1063,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return yandex.NewDNSProviderConfig(cfg)
case "yandex360":
cfg := yandex360.NewDefaultConfig()
@ -954,6 +1071,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return yandex360.NewDNSProviderConfig(cfg)
case "yandexcloud":
cfg := yandexcloud.NewDefaultConfig()
@ -961,6 +1079,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return yandexcloud.NewDNSProviderConfig(cfg)
case "zoneee":
cfg := zoneee.NewDefaultConfig()
@ -968,6 +1087,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return zoneee.NewDNSProviderConfig(cfg)
case "zonomi":
cfg := zonomi.NewDefaultConfig()
@ -975,6 +1095,7 @@ func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, erro
if err != nil {
return nil, err
}
cfg.PropagationTimeout = 5*time.Minute
return zonomi.NewDNSProviderConfig(cfg)
default:
return nil, fmt.Errorf("unrecognized DNS provider: %s", name)

View File

@ -153,6 +153,10 @@
"azure": {
"Name": "azure",
"ConfigableFields": [
{
"Title": "ZoneName",
"Datatype": "string"
},
{
"Title": "ClientID",
"Datatype": "string"
@ -208,6 +212,10 @@
"azuredns": {
"Name": "azuredns",
"ConfigableFields": [
{
"Title": "ZoneName",
"Datatype": "string"
},
{
"Title": "SubscriptionID",
"Datatype": "string"
@ -343,6 +351,10 @@
{
"Title": "HTTPClient",
"Datatype": "*http.Client"
},
{
"Title": "SkipDeploy",
"Datatype": "bool"
}
]
},
@ -1214,10 +1226,6 @@
"gandi": {
"Name": "gandi",
"ConfigableFields": [
{
"Title": "BaseURL",
"Datatype": "string"
},
{
"Title": "APIKey",
"Datatype": "string"
@ -1241,10 +1249,6 @@
"gandiv5": {
"Name": "gandiv5",
"ConfigableFields": [
{
"Title": "BaseURL",
"Datatype": "string"
},
{
"Title": "APIKey",
"Datatype": "string"

View File

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

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"net/mail"
"os"
@ -12,6 +11,7 @@ import (
"strings"
"time"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
@ -34,7 +34,9 @@ type AutoRenewer struct {
AcmeHandler *ACMEHandler
RenewerConfig *AutoRenewConfig
RenewTickInterval int64
EarlyRenewDays int //How many days before cert expire to renew certificate
TickerstopChan chan bool
Logger *logger.Logger //System wide logger
}
type ExpiredCerts struct {
@ -44,11 +46,15 @@ type ExpiredCerts struct {
// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
// Set renew check interval to 0 for auto (1 day)
func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, AcmeHandler *ACMEHandler) (*AutoRenewer, error) {
func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, earlyRenewDays int, AcmeHandler *ACMEHandler, logger *logger.Logger) (*AutoRenewer, error) {
if renewCheckInterval == 0 {
renewCheckInterval = 86400 //1 day
}
if earlyRenewDays == 0 {
earlyRenewDays = 30
}
//Load the config file. If not found, create one
if !utils.FileExists(config) {
//Create one
@ -82,6 +88,7 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
AcmeHandler: AcmeHandler,
RenewerConfig: &renewerConfig,
RenewTickInterval: renewCheckInterval,
Logger: logger,
}
if thisRenewer.RenewerConfig.Enabled {
@ -95,6 +102,10 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
return &thisRenewer, nil
}
func (a *AutoRenewer) Logf(message string, err error) {
a.Logger.PrintAndLog("CertRenew", message, err)
}
func (a *AutoRenewer) StartAutoRenewTicker() {
//Stop the previous ticker if still running
if a.TickerstopChan != nil {
@ -113,7 +124,7 @@ func (a *AutoRenewer) StartAutoRenewTicker() {
case <-done:
return
case <-ticker.C:
log.Println("Check and renew certificates in progress")
a.Logf("Check and renew certificates in progress", nil)
a.CheckAndRenewCertificates()
}
}
@ -135,7 +146,7 @@ func (a *AutoRenewer) StopAutoRenewTicker() {
// opr = setSelected -> Enter a list of file names (or matching rules) for auto renew
// opr = setAuto -> Set to use auto detect certificates and renew
func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
opr, err := utils.GetPara(r, "opr")
opr, err := utils.PostPara(r, "opr")
if err != nil {
utils.SendErrorResponse(w, "Operation not set")
return
@ -165,6 +176,8 @@ func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.R
a.RenewerConfig.RenewAll = true
a.saveRenewConfigToFile()
utils.SendOK(w)
} else {
utils.SendErrorResponse(w, "invalid operation given")
}
}
@ -208,42 +221,52 @@ func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
utils.SendJSONResponse(w, string(js))
}
// HandleAutoRenewEnable get and set the auto renew enable state
func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Request) {
val, err := utils.PostPara(r, "enable")
if err != nil {
if r.Method == http.MethodGet {
js, _ := json.Marshal(a.RenewerConfig.Enabled)
utils.SendJSONResponse(w, string(js))
} else {
if val == "true" {
} else if r.Method == http.MethodPost {
val, err := utils.PostBool(r, "enable")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty enable state")
}
if val {
//Check if the email is not empty
if a.RenewerConfig.Email == "" {
utils.SendErrorResponse(w, "Email is not set")
return
}
a.RenewerConfig.Enabled = true
a.saveRenewConfigToFile()
log.Println("[ACME] ACME auto renew enabled")
a.Logf("ACME auto renew enabled", nil)
a.StartAutoRenewTicker()
} else {
a.RenewerConfig.Enabled = false
a.saveRenewConfigToFile()
log.Println("[ACME] ACME auto renew disabled")
a.Logf("ACME auto renew disabled", nil)
a.StopAutoRenewTicker()
}
} else {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
}
func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
email, err := utils.PostPara(r, "set")
if err != nil {
if r.Method == http.MethodGet {
//Return the current email to user
js, _ := json.Marshal(a.RenewerConfig.Email)
utils.SendJSONResponse(w, string(js))
} else {
} else if r.Method == http.MethodPost {
email, err := utils.PostPara(r, "set")
if err != nil {
utils.SendErrorResponse(w, "invalid or empty email given")
return
}
//Check if the email is valid
_, err := mail.ParseAddress(email)
_, err = mail.ParseAddress(email)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
@ -252,8 +275,11 @@ func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
//Set the new config
a.RenewerConfig.Email = email
a.saveRenewConfigToFile()
}
utils.SendOK(w)
} else {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
}
// Check and renew certificates. This check all the certificates in the
@ -263,7 +289,7 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
certFolder := a.CertFolder
files, err := os.ReadDir(certFolder)
if err != nil {
log.Println("Unable to renew certificates: " + err.Error())
a.Logf("Read certificate store failed", err)
return []string{}, err
}
@ -277,13 +303,13 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
if err != nil {
continue
}
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
//This cert is expired
DNSName, err := ExtractDomains(certBytes)
if err != nil {
//Maybe self signed. Ignore this
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
continue
}
@ -305,13 +331,12 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
if err != nil {
continue
}
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
//This cert is expired
DNSName, err := ExtractDomains(certBytes)
if err != nil {
//Maybe self signed. Ignore this
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
a.Logf("Encounted error when trying to resolve DNS name for cert "+file.Name(), err)
continue
}
@ -338,7 +363,7 @@ func (a *AutoRenewer) Close() {
func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
renewedCertFiles := []string{}
for _, expiredCert := range certs {
log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
a.Logf("Renewing "+expiredCert.Filepath+" (Might take a few minutes)", nil)
fileName := filepath.Base(expiredCert.Filepath)
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
@ -346,10 +371,10 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
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)
a.Logf("Renew "+certName+"certificate error, can't get the ACME detail for certificate, trying org section as ca", err)
if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil {
log.Printf("extract issuer name for cert error: %v, using default ca", extractErr)
a.Logf("Extract issuer name for cert error, using default ca", err)
certInfo = &CertificateInfoJSON{}
} else {
certInfo = &CertificateInfoJSON{AcmeName: CAName}
@ -358,9 +383,9 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
_, 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())
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
} else {
log.Println("Successfully renewed " + filepath.Base(expiredCert.Filepath))
a.Logf("Successfully renewed "+filepath.Base(expiredCert.Filepath), nil)
renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
}
}

View File

@ -81,13 +81,14 @@ func CertIsExpired(certBytes []byte) bool {
return false
}
func CertExpireSoon(certBytes []byte) bool {
// CertExpireSoon check if the given cert bytes will expires within the given number of days from now
func CertExpireSoon(certBytes []byte, numberOfDays int) bool {
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
expirationDate := cert.NotAfter
threshold := 14 * 24 * time.Hour // 14 days
threshold := time.Duration(numberOfDays) * 24 * time.Hour
timeRemaining := time.Until(expirationDate)
if timeRemaining <= threshold {

View File

@ -14,10 +14,10 @@ import (
"strings"
"encoding/hex"
"log"
"github.com/gorilla/sessions"
db "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
@ -27,6 +27,7 @@ type AuthAgent struct {
SessionStore *sessions.CookieStore
Database *db.Database
LoginRedirectionHandler func(http.ResponseWriter, *http.Request)
Logger *logger.Logger
}
type AuthEndpoints struct {
@ -37,12 +38,12 @@ type AuthEndpoints struct {
Autologin string
}
//Constructor
func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent {
// Constructor
func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, systemLogger *logger.Logger, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent {
store := sessions.NewCookieStore(key)
err := sysdb.NewTable("auth")
if err != nil {
log.Println("Failed to create auth database. Terminating.")
systemLogger.Println("Failed to create auth database. Terminating.")
panic(err)
}
@ -52,13 +53,14 @@ func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database,
SessionStore: store,
Database: sysdb,
LoginRedirectionHandler: loginRedirectionHandler,
Logger: systemLogger,
}
//Return the authAgent
return &newAuthAgent
}
func GetSessionKey(sysdb *db.Database) (string, error) {
func GetSessionKey(sysdb *db.Database, logger *logger.Logger) (string, error) {
sysdb.NewTable("auth")
sessionKey := ""
if !sysdb.KeyExists("auth", "sessionkey") {
@ -66,9 +68,9 @@ func GetSessionKey(sysdb *db.Database) (string, error) {
rand.Read(key)
sessionKey = string(key)
sysdb.Write("auth", "sessionkey", sessionKey)
log.Println("[Auth] New authentication session key generated")
logger.PrintAndLog("auth", "New authentication session key generated", nil)
} else {
log.Println("[Auth] Authentication session key loaded from database")
logger.PrintAndLog("auth", "Authentication session key loaded from database", nil)
err := sysdb.Read("auth", "sessionkey", &sessionKey)
if err != nil {
return "", errors.New("database read error. Is the database file corrupted?")
@ -77,7 +79,7 @@ func GetSessionKey(sysdb *db.Database) (string, error) {
return sessionKey, nil
}
//This function will handle an http request and redirect to the given login address if not logged in
// This function will handle an http request and redirect to the given login address if not logged in
func (a *AuthAgent) HandleCheckAuth(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
if a.CheckAuth(r) {
//User already logged in
@ -88,14 +90,14 @@ func (a *AuthAgent) HandleCheckAuth(w http.ResponseWriter, r *http.Request, hand
}
}
//Handle login request, require POST username and password
// Handle login request, require POST username and password
func (a *AuthAgent) HandleLogin(w http.ResponseWriter, r *http.Request) {
//Get username from request using POST mode
username, err := utils.PostPara(r, "username")
if err != nil {
//Username not defined
log.Println("[Auth] " + r.RemoteAddr + " trying to login with username: " + username)
a.Logger.PrintAndLog("auth", r.RemoteAddr+" trying to login with username: "+username, nil)
utils.SendErrorResponse(w, "Username not defined or empty.")
return
}
@ -124,11 +126,11 @@ func (a *AuthAgent) HandleLogin(w http.ResponseWriter, r *http.Request) {
a.LoginUserByRequest(w, r, username, rememberme)
//Print the login message to console
log.Println(username + " logged in.")
a.Logger.PrintAndLog("auth", username+" logged in.", nil)
utils.SendOK(w)
} else {
//Password incorrect
log.Println(username + " login request rejected: " + rejectionReason)
a.Logger.PrintAndLog("auth", username+" login request rejected: "+rejectionReason, nil)
utils.SendErrorResponse(w, rejectionReason)
return
@ -140,14 +142,14 @@ func (a *AuthAgent) ValidateUsernameAndPassword(username string, password string
return succ
}
//validate the username and password, return reasons if the auth failed
// validate the username and password, return reasons if the auth failed
func (a *AuthAgent) ValidateUsernameAndPasswordWithReason(username string, password string) (bool, string) {
hashedPassword := Hash(password)
var passwordInDB string
err := a.Database.Read("auth", "passhash/"+username, &passwordInDB)
if err != nil {
//User not found or db exception
log.Println("[Auth] " + username + " login with incorrect password")
a.Logger.PrintAndLog("auth", username+" login with incorrect password", nil)
return false, "Invalid username or password"
}
@ -158,7 +160,7 @@ func (a *AuthAgent) ValidateUsernameAndPasswordWithReason(username string, passw
}
}
//Login the user by creating a valid session for this user
// Login the user by creating a valid session for this user
func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, username string, rememberme bool) {
session, _ := a.SessionStore.Get(r, a.SessionName)
@ -181,11 +183,15 @@ func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, u
session.Save(r, w)
}
//Handle logout, reply OK after logged out. WILL NOT DO REDIRECTION
// Handle logout, reply OK after logged out. WILL NOT DO REDIRECTION
func (a *AuthAgent) HandleLogout(w http.ResponseWriter, r *http.Request) {
username, err := a.GetUserName(w, r)
if err != nil {
utils.SendErrorResponse(w, "user not logged in")
return
}
if username != "" {
log.Println(username + " logged out.")
a.Logger.PrintAndLog("auth", username+" logged out", nil)
}
// Revoke users authentication
err = a.Logout(w, r)
@ -194,7 +200,7 @@ func (a *AuthAgent) HandleLogout(w http.ResponseWriter, r *http.Request) {
return
}
w.Write([]byte("OK"))
utils.SendOK(w)
}
func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
@ -208,7 +214,7 @@ func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
return nil
}
//Get the current session username from request
// Get the current session username from request
func (a *AuthAgent) GetUserName(w http.ResponseWriter, r *http.Request) (string, error) {
if a.CheckAuth(r) {
//This user has logged in.
@ -220,7 +226,7 @@ func (a *AuthAgent) GetUserName(w http.ResponseWriter, r *http.Request) (string,
}
}
//Get the current session user email from request
// Get the current session user email from request
func (a *AuthAgent) GetUserEmail(w http.ResponseWriter, r *http.Request) (string, error) {
if a.CheckAuth(r) {
//This user has logged in.
@ -239,7 +245,7 @@ func (a *AuthAgent) GetUserEmail(w http.ResponseWriter, r *http.Request) (string
}
}
//Check if the user has logged in, return true / false in JSON
// Check if the user has logged in, return true / false in JSON
func (a *AuthAgent) CheckLogin(w http.ResponseWriter, r *http.Request) {
if a.CheckAuth(r) {
utils.SendJSONResponse(w, "true")
@ -248,7 +254,7 @@ func (a *AuthAgent) CheckLogin(w http.ResponseWriter, r *http.Request) {
}
}
//Handle new user register. Require POST username, password, group.
// Handle new user register. Require POST username, password, group.
func (a *AuthAgent) HandleRegister(w http.ResponseWriter, r *http.Request, callback func(string, string)) {
//Get username from request
newusername, err := utils.PostPara(r, "username")
@ -291,10 +297,10 @@ func (a *AuthAgent) HandleRegister(w http.ResponseWriter, r *http.Request, callb
//Return to the client with OK
utils.SendOK(w)
log.Println("[Auth] New user " + newusername + " added to system.")
a.Logger.PrintAndLog("auth", "New user "+newusername+" added to system.", nil)
}
//Handle new user register without confirmation email. Require POST username, password, group.
// Handle new user register without confirmation email. Require POST username, password, group.
func (a *AuthAgent) HandleRegisterWithoutEmail(w http.ResponseWriter, r *http.Request, callback func(string, string)) {
//Get username from request
newusername, err := utils.PostPara(r, "username")
@ -324,10 +330,10 @@ func (a *AuthAgent) HandleRegisterWithoutEmail(w http.ResponseWriter, r *http.Re
//Return to the client with OK
utils.SendOK(w)
log.Println("[Auth] Admin account created: " + newusername)
a.Logger.PrintAndLog("auth", "Admin account created: "+newusername, nil)
}
//Check authentication from request header's session value
// Check authentication from request header's session value
func (a *AuthAgent) CheckAuth(r *http.Request) bool {
session, err := a.SessionStore.Get(r, a.SessionName)
if err != nil {
@ -340,8 +346,8 @@ func (a *AuthAgent) CheckAuth(r *http.Request) bool {
return true
}
//Handle de-register of users. Require POST username.
//THIS FUNCTION WILL NOT CHECK FOR PERMISSION. PLEASE USE WITH PERMISSION HANDLER
// Handle de-register of users. Require POST username.
// THIS FUNCTION WILL NOT CHECK FOR PERMISSION. PLEASE USE WITH PERMISSION HANDLER
func (a *AuthAgent) HandleUnregister(w http.ResponseWriter, r *http.Request) {
//Check if the user is logged in
if !a.CheckAuth(r) {
@ -365,7 +371,7 @@ func (a *AuthAgent) HandleUnregister(w http.ResponseWriter, r *http.Request) {
//Return to the client with OK
utils.SendOK(w)
log.Println("[Auth] User " + username + " has been removed from the system.")
a.Logger.PrintAndLog("auth", "User "+username+" has been removed from the system", nil)
}
func (a *AuthAgent) UnregisterUser(username string) error {
@ -381,7 +387,7 @@ func (a *AuthAgent) UnregisterUser(username string) error {
return nil
}
//Get the number of users in the system
// Get the number of users in the system
func (a *AuthAgent) GetUserCounts() int {
entries, _ := a.Database.ListTable("auth")
usercount := 0
@ -393,12 +399,12 @@ func (a *AuthAgent) GetUserCounts() int {
}
if usercount == 0 {
log.Println("There are no user in the database.")
a.Logger.PrintAndLog("auth", "There are no user in the database", nil)
}
return usercount
}
//List all username within the system
// List all username within the system
func (a *AuthAgent) ListUsers() []string {
entries, _ := a.Database.ListTable("auth")
results := []string{}
@ -411,7 +417,7 @@ func (a *AuthAgent) ListUsers() []string {
return results
}
//Check if the given username exists
// Check if the given username exists
func (a *AuthAgent) UserExists(username string) bool {
userpasswordhash := ""
err := a.Database.Read("auth", "passhash/"+username, &userpasswordhash)
@ -421,7 +427,7 @@ func (a *AuthAgent) UserExists(username string) bool {
return true
}
//Update the session expire time given the request header.
// Update the session expire time given the request header.
func (a *AuthAgent) UpdateSessionExpireTime(w http.ResponseWriter, r *http.Request) bool {
session, _ := a.SessionStore.Get(r, a.SessionName)
if session.Values["authenticated"].(bool) {
@ -446,7 +452,7 @@ func (a *AuthAgent) UpdateSessionExpireTime(w http.ResponseWriter, r *http.Reque
}
}
//Create user account
// Create user account
func (a *AuthAgent) CreateUserAccount(newusername string, password string, email string) error {
//Check user already exists
if a.UserExists(newusername) {
@ -470,7 +476,7 @@ func (a *AuthAgent) CreateUserAccount(newusername string, password string, email
return nil
}
//Hash the given raw string into sha512 hash
// Hash the given raw string into sha512 hash
func Hash(raw string) string {
h := sha512.New()
h.Write([]byte(raw))

View File

@ -2,7 +2,7 @@ package auth
import (
"errors"
"log"
"fmt"
"net/http"
)
@ -10,7 +10,7 @@ type RouterOption struct {
AuthAgent *AuthAgent
RequireAuth bool //This router require authentication
DeniedHandler func(http.ResponseWriter, *http.Request) //Things to do when request is rejected
TargetMux *http.ServeMux
}
type RouterDef struct {
@ -28,24 +28,38 @@ func NewManagedHTTPRouter(option RouterOption) *RouterDef {
func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request)) error {
//Check if the endpoint already registered
if _, exist := router.endpoints[endpoint]; exist {
log.Println("WARNING! Duplicated registering of web endpoint: " + endpoint)
fmt.Println("WARNING! Duplicated registering of web endpoint: " + endpoint)
return errors.New("endpoint register duplicated")
}
authAgent := router.option.AuthAgent
//OK. Register handler
http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
//Check authentication of the user
if router.option.RequireAuth {
authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
if router.option.TargetMux == nil {
http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
//Check authentication of the user
if router.option.RequireAuth {
authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
handler(w, r)
})
} else {
handler(w, r)
})
} else {
handler(w, r)
}
}
})
})
} else {
router.option.TargetMux.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
//Check authentication of the user
if router.option.RequireAuth {
authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
handler(w, r)
})
} else {
handler(w, r)
}
})
}
router.endpoints[endpoint] = handler

View File

@ -3,8 +3,6 @@
package dockerux
/* Windows docker optimizer*/
import (
"context"
"encoding/json"
@ -16,7 +14,6 @@ import (
"imuslab.com/zoraxy/mod/utils"
)
// Windows build not support docker
func (d *UXOptimizer) HandleDockerAvailable(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(d.RunninInDocker)
utils.SendJSONResponse(w, string(js))

View File

@ -20,6 +20,7 @@ import (
- Access Router
- Blacklist
- Whitelist
- Rate Limitor
- Basic Auth
- Vitrual Directory Proxy
- Subdomain Proxy
@ -30,7 +31,7 @@ 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.
//Check if there are external routing rule (rr) matches.
//If yes, route them via external rr
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil {
@ -45,7 +46,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//Check if this is a redirection url
if h.Parent.Option.RedirectRuleTable.IsRedirectable(r) {
statusCode := h.Parent.Option.RedirectRuleTable.HandleRedirect(w, r)
h.logRequest(r, statusCode != 500, statusCode, "redirect", "")
h.Parent.logRequest(r, statusCode != 500, statusCode, "redirect", "")
return
}
@ -76,6 +77,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if sep.RequireRateLimit {
err := h.handleRateLimitRouting(w, r, sep)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 429)
return
}
}
@ -84,6 +86,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if sep.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return
}
}
@ -100,6 +103,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
h.Parent.Option.Logger.LogHTTPRequest(r, "redirect", 307)
return
}
}
@ -193,12 +197,12 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
}
hostname := parsedURL.Hostname()
if hostname == domainOnly {
h.logRequest(r, false, 500, "root-redirect", domainOnly)
h.Parent.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)
h.Parent.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

View File

@ -24,7 +24,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter,
isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r)
if isBlocked {
h.logRequest(r, false, 403, blockedReason, "")
h.Parent.logRequest(r, false, 403, blockedReason, "")
}
return isBlocked
}

View File

@ -18,7 +18,7 @@ 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)
h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname())
}
return err
}

View File

@ -51,7 +51,13 @@ func (ept *ProxyEndpoint) SplitInboundOutboundHeaders() ([][]string, [][]string)
//Check if the endpoint require HSTS headers
if ept.HSTSMaxAge > 0 {
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge))}
if ept.ContainsWildcardName(true) {
//Endpoint listening domain includes wildcards.
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge)) + "; includeSubdomains"}
} else {
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge))}
}
downstreamHeaderCounter++
}

View File

@ -0,0 +1,21 @@
package domainsniff
import "net/http"
/*
Promox API sniffer
This handler sniff proxmox API endpoint and
adjust the request accordingly to fix shits
in the proxmox API server
*/
func IsProxmox(r *http.Request) bool {
// Check if any of the cookies is named PVEAuthCookie
for _, cookie := range r.Cookies() {
if cookie.Name == "PVEAuthCookie" {
return true
}
}
return false
}

View File

@ -11,6 +11,7 @@ import (
"strings"
"time"
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
)
@ -50,13 +51,16 @@ type ReverseProxy struct {
ModifyResponse func(*http.Response) error
//Prepender is an optional prepend text for URL rewrite
//
Prepender string
Verbal bool
//Appended by Zoraxy project
}
type ResponseRewriteRuleSet struct {
/* Basic Rewrite Rulesets */
ProxyDomain string
OriginalHost string
UseTLS bool
@ -64,7 +68,13 @@ type ResponseRewriteRuleSet struct {
PathPrefix string //Vdir prefix for root, / will be rewrite to this
UpstreamHeaders [][]string
DownstreamHeaders [][]string
Version string //Version number of Zoraxy, use for X-Proxy-By
/* Advance Usecase Options */
HostHeaderOverwrite string //Force overwrite of request "Host" header (advanced usecase)
NoRemoveHopByHop bool //Do not remove hop-by-hop headers (advanced usecase)
/* System Information Payload */
Version string //Version number of Zoraxy, use for X-Proxy-By
}
type requestCanceler interface {
@ -72,8 +82,8 @@ type requestCanceler interface {
}
type DpcoreOptions struct {
IgnoreTLSVerification bool
FlushInterval time.Duration
IgnoreTLSVerification bool //Disable all TLS verification when request pass through this proxy router
FlushInterval time.Duration //Duration to flush in normal requests. Stream request or keep-alive request will always flush with interval of -1 (immediately)
}
func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOptions) *ReverseProxy {
@ -180,7 +190,7 @@ var hopHeaders = []string{
"Te", // canonicalized version of "TE"
"Trailer", // not Trailers per URL above; http://www.rfc-editor.org/errata_search.php?eid=4522
"Transfer-Encoding",
//"Upgrade",
//"Upgrade", // handled by websocket proxy in higher layer abstraction
}
// Copy response from src to dst with given flush interval, reference from httputil.ReverseProxy
@ -251,7 +261,7 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) {
}
}
func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error {
func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) (int, error) {
transport := p.Transport
outreq := new(http.Request)
@ -280,7 +290,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
outreq.Close = false
//Only skip origin rewrite iff proxy target require TLS and it is external domain name like github.com
if !(rrr.UseTLS && isExternalDomainName(rrr.ProxyDomain)) {
if rrr.HostHeaderOverwrite != "" {
//Use user defined overwrite header value, see issue #255
outreq.Host = rrr.HostHeaderOverwrite
} else if !(rrr.UseTLS && isExternalDomainName(rrr.ProxyDomain)) {
// Always use the original host, see issue #164
outreq.Host = rrr.OriginalHost
}
@ -290,7 +303,9 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
copyHeader(outreq.Header, req.Header)
// Remove hop-by-hop headers.
removeHeaders(outreq.Header, rrr.NoCache)
if !rrr.NoRemoveHopByHop {
removeHeaders(outreq.Header, rrr.NoCache)
}
// Add X-Forwarded-For Header.
addXForwardedForHeader(outreq)
@ -301,6 +316,11 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
// Rewrite outbound UA, must be after user headers
rewriteUserAgent(outreq.Header, "Zoraxy/"+rrr.Version)
//Fix proxmox transfer encoding bug if detected Proxmox Cookie
if domainsniff.IsProxmox(req) {
outreq.TransferEncoding = []string{"identity"}
}
res, err := transport.RoundTrip(outreq)
if err != nil {
if p.Verbal {
@ -308,11 +328,13 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
}
//rw.WriteHeader(http.StatusBadGateway)
return err
return http.StatusBadGateway, err
}
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
removeHeaders(res.Header, rrr.NoCache)
if !rrr.NoRemoveHopByHop {
removeHeaders(res.Header, rrr.NoCache)
}
//Remove the User-Agent header if exists
if _, ok := res.Header["User-Agent"]; ok {
@ -327,17 +349,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
}
//rw.WriteHeader(http.StatusBadGateway)
return err
return http.StatusBadGateway, err
}
}
//TODO: Figure out a way to proxy for proxmox
//if res.StatusCode == 501 || res.StatusCode == 500 {
// fmt.Println(outreq.Proto, outreq.RemoteAddr, outreq.RequestURI)
// fmt.Println(">>>", outreq.Method, res.Header, res.ContentLength, res.StatusCode)
// fmt.Println(outreq.Header, req.Host)
//}
//Add debug X-Proxy-By tracker
res.Header.Set("x-proxy-by", "zoraxy/"+rrr.Version)
@ -374,7 +389,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
copyHeader(rw.Header(), res.Header)
// inject permission policy headers
//TODO: Load permission policy from rrr
permissionpolicy.InjectPermissionPolicyHeader(rw, nil)
// The "Trailer" header isn't included in the Transport's response, Build it up from Trailer.
@ -404,14 +418,14 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
res.Body.Close()
copyHeader(rw.Header(), res.Trailer)
return nil
return res.StatusCode, nil
}
func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) error {
func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) (int, error) {
hij, ok := rw.(http.Hijacker)
if !ok {
p.logf("http server does not support hijacker")
return errors.New("http server does not support hijacker")
return http.StatusNotImplemented, errors.New("http server does not support hijacker")
}
clientConn, _, err := hij.Hijack()
@ -419,7 +433,7 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err
if p.Verbal {
p.logf("http: proxy error: %v", err)
}
return err
return http.StatusInternalServerError, err
}
proxyConn, err := net.Dial("tcp", req.URL.Host)
@ -428,7 +442,7 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err
p.logf("http: proxy error: %v", err)
}
return err
return http.StatusInternalServerError, err
}
// The returned net.Conn may have read or write deadlines
@ -447,7 +461,7 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err
if p.Verbal {
p.logf("http: proxy error: %v", err)
}
return err
return http.StatusGatewayTimeout, err
}
err = proxyConn.SetDeadline(deadline)
@ -456,7 +470,7 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err
p.logf("http: proxy error: %v", err)
}
return err
return http.StatusGatewayTimeout, err
}
_, err = clientConn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
@ -465,7 +479,7 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err
p.logf("http: proxy error: %v", err)
}
return err
return http.StatusInternalServerError, err
}
go func() {
@ -478,15 +492,13 @@ func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) err
proxyConn.Close()
clientConn.Close()
return nil
return http.StatusOK, nil
}
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error {
func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) (int, error) {
if req.Method == "CONNECT" {
err := p.ProxyHTTPS(rw, req)
return err
return p.ProxyHTTPS(rw, req)
} else {
err := p.ProxyHTTP(rw, req, rrr)
return err
return p.ProxyHTTP(rw, req, rrr)
}
}

View File

@ -3,6 +3,7 @@ package dpcore
import (
"mime"
"net/http"
"strings"
"time"
)
@ -17,6 +18,12 @@ func (p *ReverseProxy) getFlushInterval(req *http.Request, res *http.Response) t
return -1
}
// Fixed issue #235: Added auto detection for ollama / llm output stream
connectionHeader := req.Header["Connection"]
if len(connectionHeader) > 0 && strings.Contains(strings.Join(connectionHeader, ","), "keep-alive") {
return -1
}
//Cannot sniff anything. Use default value
return p.FlushInterval

View File

@ -1,7 +1,10 @@
package dpcore
import (
"bytes"
"io"
"net"
"net/http"
"net/url"
"strings"
)
@ -92,3 +95,63 @@ func isExternalDomainName(hostname string) bool {
return true
}
// DeepCopyRequest returns a deep copy of the given http.Request.
func DeepCopyRequest(req *http.Request) (*http.Request, error) {
// Copy the URL
urlCopy := *req.URL
// Copy the headers
headersCopy := make(http.Header, len(req.Header))
for k, vv := range req.Header {
vvCopy := make([]string, len(vv))
copy(vvCopy, vv)
headersCopy[k] = vvCopy
}
// Copy the cookies
cookiesCopy := make([]*http.Cookie, len(req.Cookies()))
for i, cookie := range req.Cookies() {
cookieCopy := *cookie
cookiesCopy[i] = &cookieCopy
}
// Copy the body, if present
var bodyCopy io.ReadCloser
if req.Body != nil {
var buf bytes.Buffer
if _, err := buf.ReadFrom(req.Body); err != nil {
return nil, err
}
// Reset the request body so it can be read again
if err := req.Body.Close(); err != nil {
return nil, err
}
req.Body = io.NopCloser(&buf)
bodyCopy = io.NopCloser(bytes.NewReader(buf.Bytes()))
}
// Create the new request
reqCopy := &http.Request{
Method: req.Method,
URL: &urlCopy,
Proto: req.Proto,
ProtoMajor: req.ProtoMajor,
ProtoMinor: req.ProtoMinor,
Header: headersCopy,
Body: bodyCopy,
ContentLength: req.ContentLength,
TransferEncoding: append([]string(nil), req.TransferEncoding...),
Close: req.Close,
Host: req.Host,
Form: req.Form,
PostForm: req.PostForm,
MultipartForm: req.MultipartForm,
Trailer: req.Trailer,
RemoteAddr: req.RemoteAddr,
TLS: req.TLS,
// Cancel and Context are not copied as it might cause issues
}
return reqCopy, nil
}

View File

@ -28,6 +28,7 @@ func NewDynamicProxy(option RouterOption) (*Router, error) {
Running: false,
server: nil,
routingRules: []*RoutingRule{},
loadBalancer: option.LoadBalancer,
rateLimitCounter: RequestCountPerIpTable{},
}
@ -150,12 +151,20 @@ func (router *Router) StartProxyService() error {
}
}
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: sep.Domain,
OriginalHost: originalHostHeader,
UseTLS: sep.RequireTLS,
PathPrefix: "",
Version: sep.parent.Option.HostVersion,
selectedUpstream, err := router.loadBalancer.GetRequestUpstreamTarget(w, r, sep.ActiveOrigins, sep.UseStickySession)
if err != nil {
http.ServeFile(w, r, "./web/hosterror.html")
router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err)
router.logRequest(r, false, 404, "vdir-http", r.Host)
}
selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: selectedUpstream.OriginIpOrDomain,
OriginalHost: originalHostHeader,
UseTLS: selectedUpstream.RequireTLS,
HostHeaderOverwrite: sep.RequestHostOverwrite,
NoRemoveHopByHop: sep.DisableHopByHopHeaderRemoval,
PathPrefix: "",
Version: sep.parent.Option.HostVersion,
})
return
}
@ -187,7 +196,7 @@ func (router *Router) StartProxyService() error {
IdleTimeout: 120 * time.Second,
}
log.Println("Starting HTTP-to-HTTPS redirector (port 80)")
router.Option.Logger.PrintAndLog("dprouter", "Starting HTTP-to-HTTPS redirector (port 80)", nil)
//Create a redirection stop channel
stopChan := make(chan bool)
@ -198,7 +207,7 @@ func (router *Router) StartProxyService() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
httpServer.Shutdown(ctx)
log.Println("HTTP to HTTPS redirection listener stopped")
router.Option.Logger.PrintAndLog("dprouter", "HTTP to HTTPS redirection listener stopped", nil)
}()
//Start the http server that listens to port 80 and redirect to 443
@ -213,10 +222,10 @@ func (router *Router) StartProxyService() error {
}
//Start the TLS server
log.Println("Reverse proxy service started in the background (TLS mode)")
router.Option.Logger.PrintAndLog("dprouter", "Reverse proxy service started in the background (TLS mode)", nil)
go func() {
if err := router.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not start proxy server: %v\n", err)
router.Option.Logger.PrintAndLog("dprouter", "Could not start proxy server", err)
}
}()
} else {
@ -224,10 +233,9 @@ func (router *Router) StartProxyService() error {
router.tlsListener = nil
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
router.Running = true
log.Println("Reverse proxy service started in the background (Plain HTTP mode)")
router.Option.Logger.PrintAndLog("dprouter", "Reverse proxy service started in the background (Plain HTTP mode)", nil)
go func() {
router.server.ListenAndServe()
//log.Println("[DynamicProxy] " + err.Error())
}()
}

View File

@ -7,6 +7,7 @@ import (
"golang.org/x/text/cases"
"golang.org/x/text/language"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
)
/*
@ -133,6 +134,116 @@ func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint)
return readyRoutingRule, nil
}
/* Upstream related wrapper functions */
//Check if there already exists another upstream with identical origin
func (ep *ProxyEndpoint) UpstreamOriginExists(originURL string) bool {
for _, origin := range ep.ActiveOrigins {
if origin.OriginIpOrDomain == originURL {
return true
}
}
for _, origin := range ep.InactiveOrigins {
if origin.OriginIpOrDomain == originURL {
return true
}
}
return false
}
// Get a upstream origin from given origin ip or domain
func (ep *ProxyEndpoint) GetUpstreamOriginByMatchingIP(originIpOrDomain string) (*loadbalance.Upstream, error) {
for _, origin := range ep.ActiveOrigins {
if origin.OriginIpOrDomain == originIpOrDomain {
return origin, nil
}
}
for _, origin := range ep.InactiveOrigins {
if origin.OriginIpOrDomain == originIpOrDomain {
return origin, nil
}
}
return nil, errors.New("target upstream origin not found")
}
// Add upstream to endpoint and update it to runtime
func (ep *ProxyEndpoint) AddUpstreamOrigin(newOrigin *loadbalance.Upstream, activate bool) error {
//Check if the upstream already exists
if ep.UpstreamOriginExists(newOrigin.OriginIpOrDomain) {
return errors.New("upstream with same origin already exists")
}
if activate {
//Add it to the active origin list
err := newOrigin.StartProxy()
if err != nil {
return err
}
ep.ActiveOrigins = append(ep.ActiveOrigins, newOrigin)
} else {
//Add to inactive origin list
ep.InactiveOrigins = append(ep.InactiveOrigins, newOrigin)
}
ep.UpdateToRuntime()
return nil
}
// Remove upstream from endpoint and update it to runtime
func (ep *ProxyEndpoint) RemoveUpstreamOrigin(originIpOrDomain string) error {
//Just to make sure there are no spaces
originIpOrDomain = strings.TrimSpace(originIpOrDomain)
//Check if the upstream already been removed
if !ep.UpstreamOriginExists(originIpOrDomain) {
//Not exists in the first place
return nil
}
newActiveOriginList := []*loadbalance.Upstream{}
for _, origin := range ep.ActiveOrigins {
if origin.OriginIpOrDomain != originIpOrDomain {
newActiveOriginList = append(newActiveOriginList, origin)
}
}
newInactiveOriginList := []*loadbalance.Upstream{}
for _, origin := range ep.InactiveOrigins {
if origin.OriginIpOrDomain != originIpOrDomain {
newInactiveOriginList = append(newInactiveOriginList, origin)
}
}
//Ok, set the origin list to the new one
ep.ActiveOrigins = newActiveOriginList
ep.InactiveOrigins = newInactiveOriginList
ep.UpdateToRuntime()
return nil
}
// Check if the proxy endpoint hostname or alias name contains subdomain wildcard
func (ep *ProxyEndpoint) ContainsWildcardName(skipAliasCheck bool) bool {
hostname := ep.RootOrMatchingDomain
aliasHostnames := ep.MatchingDomainAlias
wildcardCheck := func(hostname string) bool {
return len(hostname) > 0 && hostname[0] == '*'
}
if wildcardCheck(hostname) {
return true
}
if !skipAliasCheck {
for _, aliasHostname := range aliasHostnames {
if wildcardCheck(aliasHostname) {
return true
}
}
}
return false
}
// 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 {

View File

@ -1,9 +1,14 @@
package loadbalance
import (
"strings"
"sync"
"github.com/google/uuid"
"github.com/gorilla/sessions"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/uptime"
)
/*
@ -12,49 +17,87 @@ import (
Handleing load balance request for upstream destinations
*/
type BalancePolicy int
const (
BalancePolicy_RoundRobin BalancePolicy = 0 //Round robin, will ignore upstream if down
BalancePolicy_Fallback BalancePolicy = 1 //Fallback only. Will only switch to next node if the first one failed
BalancePolicy_Random BalancePolicy = 2 //Random, randomly pick one from the list that is online
BalancePolicy_GeoRegion BalancePolicy = 3 //Use the one defined for this geo-location, when down, pick the next avaible node
)
type LoadBalanceRule struct {
Upstreams []string //Reverse proxy upstream servers
LoadBalancePolicy BalancePolicy //Policy in deciding which target IP to proxy
UseRegionLock bool //If this is enabled with BalancePolicy_Geo, when the main site failed, it will not pick another node
UseStickySession bool //Use sticky session, if you are serving EU countries, make sure to add the "Do you want cookie" warning
parent *RouteManager
}
type Options struct {
Geodb *geodb.Store //GeoIP resolver for checking incoming request origin country
UptimeMonitor *uptime.Monitor //For checking if the target is online, this might be nil when the module starts
SystemUUID string //Use for the session store
UseActiveHealthCheck bool //Use active health check, default to false
Geodb *geodb.Store //GeoIP resolver for checking incoming request origin country
Logger *logger.Logger
}
type RouteManager struct {
Options Options
Logger *logger.Logger
SessionStore *sessions.CookieStore
LoadBalanceMap sync.Map //Sync map to store the last load balance state of a given node
OnlineStatusMap sync.Map //Sync map to store the online status of a given ip address or domain name
onlineStatusTickerStop chan bool //Stopping channel for the online status pinger
Options Options //Options for the load balancer
}
// Create a new load balance route manager
func NewRouteManager(options *Options, logger *logger.Logger) *RouteManager {
newManager := RouteManager{
Options: *options,
Logger: logger,
/* Upstream or Origin Server */
type Upstream struct {
//Upstream Proxy Configs
OriginIpOrDomain string //Target IP address or domain name with port
RequireTLS bool //Require TLS connection
SkipCertValidations bool //Set to true to accept self signed certs
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
//Load balancing configs
Weight int //Random weight for round robin, 0 for fallback only
MaxConn int //TODO: Maxmium connection to this server, 0 for unlimited
//currentConnectionCounts atomic.Uint64 //Counter for number of client currently connected
proxy *dpcore.ReverseProxy
}
// Create a new load balancer
func NewLoadBalancer(options *Options) *RouteManager {
if options.SystemUUID == "" {
//System UUID not passed in. Use random key
options.SystemUUID = uuid.New().String()
}
//Generate a session store for stickySession
store := sessions.NewCookieStore([]byte(options.SystemUUID))
return &RouteManager{
SessionStore: store,
LoadBalanceMap: sync.Map{},
OnlineStatusMap: sync.Map{},
onlineStatusTickerStop: nil,
Options: *options,
}
logger.PrintAndLog("INFO", "Load Balance Route Manager started", nil)
return &newManager
}
func (b *LoadBalanceRule) GetProxyTargetIP() {
//TODO: Implement get proxy target IP logic here
// UpstreamsReady checks if the group of upstreams contains at least one
// origin server that is ready
func (m *RouteManager) UpstreamsReady(upstreams []*Upstream) bool {
for _, upstream := range upstreams {
if upstream.IsReady() {
return true
}
}
return false
}
// Print debug message
func (m *RouteManager) debugPrint(message string, err error) {
m.Logger.PrintAndLog("LB", message, err)
// String format and convert a list of upstream into a string representations
func GetUpstreamsAsString(upstreams []*Upstream) string {
targets := []string{}
for _, upstream := range upstreams {
targets = append(targets, upstream.String())
}
if len(targets) == 0 {
//No upstream
return "(no upstream config)"
}
return strings.Join(targets, ", ")
}
func (m *RouteManager) Close() {
if m.onlineStatusTickerStop != nil {
m.onlineStatusTickerStop <- true
}
}
// Log Println, replace all log.Println or fmt.Println with this
func (m *RouteManager) println(message string, err error) {
m.Options.Logger.PrintAndLog("LoadBalancer", message, err)
}

View File

@ -0,0 +1,39 @@
package loadbalance
import (
"net/http"
"time"
)
// Return the last ping status to see if the target is online
func (m *RouteManager) IsTargetOnline(matchingDomainOrIp string) bool {
value, ok := m.LoadBalanceMap.Load(matchingDomainOrIp)
if !ok {
return false
}
isOnline, ok := value.(bool)
return ok && isOnline
}
// Ping a target to see if it is online
func PingTarget(targetMatchingDomainOrIp string, requireTLS bool) bool {
client := &http.Client{
Timeout: 10 * time.Second,
}
url := targetMatchingDomainOrIp
if requireTLS {
url = "https://" + url
} else {
url = "http://" + url
}
resp, err := client.Get(url)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode <= 600
}

View File

@ -0,0 +1,177 @@
package loadbalance
import (
"errors"
"math/rand"
"net/http"
)
/*
Origin Picker
This script contains the code to pick the best origin
by this request.
*/
// GetRequestUpstreamTarget return the upstream target where this
// request should be routed
func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.Request, origins []*Upstream, useStickySession bool) (*Upstream, error) {
if len(origins) == 0 {
return nil, errors.New("no upstream is defined for this host")
}
var targetOrigin = origins[0]
if useStickySession {
//Use stick session, check which origins this request previously used
targetOriginId, err := m.getSessionHandler(r, origins)
if err != nil {
//No valid session found. Assign a new upstream
targetOrigin, index, err := getRandomUpstreamByWeight(origins)
if err != nil {
m.println("Unable to get random upstream", err)
targetOrigin = origins[0]
index = 0
}
m.setSessionHandler(w, r, targetOrigin.OriginIpOrDomain, index)
return targetOrigin, nil
}
//Valid session found. Resume the previous session
return origins[targetOriginId], nil
} else {
//Do not use stick session. Get a random one
var err error
targetOrigin, _, err = getRandomUpstreamByWeight(origins)
if err != nil {
m.println("Failed to get next origin", err)
targetOrigin = origins[0]
}
}
//fmt.Println("DEBUG: Picking origin " + targetOrigin.OriginIpOrDomain)
return targetOrigin, nil
}
/* Features related to session access */
//Set a new origin for this connection by session
func (m *RouteManager) setSessionHandler(w http.ResponseWriter, r *http.Request, originIpOrDomain string, index int) error {
session, err := m.SessionStore.Get(r, "STICKYSESSION")
if err != nil {
return err
}
session.Values["zr_sid_origin"] = originIpOrDomain
session.Values["zr_sid_index"] = index
session.Options.MaxAge = 86400 //1 day
session.Options.Path = "/"
err = session.Save(r, w)
if err != nil {
return err
}
return nil
}
// Get the previous connected origin from session
func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream) (int, error) {
// Get existing session
session, err := m.SessionStore.Get(r, "STICKYSESSION")
if err != nil {
return -1, err
}
// Retrieve session values for origin
originDomainRaw := session.Values["zr_sid_origin"]
originIDRaw := session.Values["zr_sid_index"]
if originDomainRaw == nil || originIDRaw == nil {
return -1, errors.New("no session has been set")
}
originDomain := originDomainRaw.(string)
originID := originIDRaw.(int)
//Check if it has been modified
if len(upstreams) < originID || upstreams[originID].OriginIpOrDomain != originDomain {
//Mismatch or upstreams has been updated
return -1, errors.New("upstreams has been changed")
}
return originID, nil
}
/* Functions related to random upstream picking */
// Get a random upstream by the weights defined in Upstream struct, return the upstream, index value and any error
func getRandomUpstreamByWeight(upstreams []*Upstream) (*Upstream, int, error) {
// If there is only one upstream, return it
if len(upstreams) == 1 {
return upstreams[0], 0, nil
}
// Preserve the index with upstreams
type upstreamWithIndex struct {
Upstream *Upstream
Index int
}
// Calculate total weight for upstreams with weight > 0
totalWeight := 0
fallbackUpstreams := make([]upstreamWithIndex, 0, len(upstreams))
for index, upstream := range upstreams {
if upstream.Weight > 0 {
totalWeight += upstream.Weight
} else {
// Collect fallback upstreams
fallbackUpstreams = append(fallbackUpstreams, upstreamWithIndex{upstream, index})
}
}
// If there are no upstreams with weight > 0, return a fallback upstream if available
if totalWeight == 0 {
if len(fallbackUpstreams) > 0 {
// Randomly select one of the fallback upstreams
randIndex := rand.Intn(len(fallbackUpstreams))
return fallbackUpstreams[randIndex].Upstream, fallbackUpstreams[randIndex].Index, nil
}
// No upstreams available at all
return nil, -1, errors.New("no valid upstream servers available")
}
// Random weight between 0 and total weight
randomWeight := rand.Intn(totalWeight)
// Select an upstream based on the random weight
for index, upstream := range upstreams {
if upstream.Weight > 0 { // Only consider upstreams with weight > 0
if randomWeight < upstream.Weight {
// Return the selected upstream and its index
return upstream, index, nil
}
randomWeight -= upstream.Weight
}
}
// If we reach here, it means we should return a fallback upstream if available
if len(fallbackUpstreams) > 0 {
randIndex := rand.Intn(len(fallbackUpstreams))
return fallbackUpstreams[randIndex].Upstream, fallbackUpstreams[randIndex].Index, nil
}
return nil, -1, errors.New("failed to pick an upstream origin server")
}
// IntRange returns a random integer in the range from min to max.
/*
func intRange(min, max int) (int, error) {
var result int
switch {
case min > max:
// Fail with error
return result, errors.New("min is greater than max")
case max == min:
result = max
case max > min:
b := rand.Intn(max-min) + min
result = min + int(b)
}
return result, nil
}
*/

View File

@ -0,0 +1,77 @@
package loadbalance
import (
"encoding/json"
"errors"
"net/http"
"net/url"
"strings"
"time"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
)
// StartProxy create and start a HTTP proxy using dpcore
// Example of webProxyEndpoint: https://example.com:443 or http://192.168.1.100:8080
func (u *Upstream) StartProxy() error {
//Filter the tailing slash if any
domain := u.OriginIpOrDomain
if len(domain) == 0 {
return errors.New("invalid endpoint config")
}
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
//TLS is not hardcoded in proxy target domain
if u.RequireTLS {
domain = "https://" + domain
} else {
domain = "http://" + domain
}
}
//Create a new proxy agent for this upstream
path, err := url.Parse(domain)
if err != nil {
return err
}
proxy := dpcore.NewDynamicProxyCore(path, "", &dpcore.DpcoreOptions{
IgnoreTLSVerification: u.SkipCertValidations,
FlushInterval: 100 * time.Millisecond,
})
u.proxy = proxy
return nil
}
// IsReady return the proxy ready state of the upstream server
// Return false if StartProxy() is not called on this upstream before
func (u *Upstream) IsReady() bool {
return u.proxy != nil
}
// Clone return a new deep copy object of the identical upstream
func (u *Upstream) Clone() *Upstream {
newUpstream := Upstream{}
js, _ := json.Marshal(u)
json.Unmarshal(js, &newUpstream)
return &newUpstream
}
// ServeHTTP uses this upstream proxy router to route the current request, return the status code and error if any
func (u *Upstream) ServeHTTP(w http.ResponseWriter, r *http.Request, rrr *dpcore.ResponseRewriteRuleSet) (int, error) {
//Auto rewrite to upstream origin if not set
if rrr.ProxyDomain == "" {
rrr.ProxyDomain = u.OriginIpOrDomain
}
return u.proxy.ServeHTTP(w, r, rrr)
}
// String return the string representations of endpoints in this upstream
func (u *Upstream) String() string {
return u.OriginIpOrDomain
}

View File

@ -16,6 +16,7 @@ import (
"imuslab.com/zoraxy/mod/websocketproxy"
)
// Check if the request URI matches any of the proxy endpoint
func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *ProxyEndpoint {
var targetProxyEndpoint *ProxyEndpoint = nil
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
@ -30,6 +31,7 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
return targetProxyEndpoint
}
// Get the proxy endpoint from hostname, which might includes checking of wildcard certificates
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
var targetSubdomainEndpoint *ProxyEndpoint = nil
ep, ok := router.ProxyEndpoints.Load(hostname)
@ -111,11 +113,21 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
/* Load balancing */
selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession)
if err != nil {
http.ServeFile(w, r, "./web/rperror.html")
h.Parent.Option.Logger.PrintAndLog("proxy", "Failed to assign an upstream for this request", err)
h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname())
return
}
/* WebSocket automatic proxy */
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("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain
wsRedirectionEndpoint := selectedUpstream.OriginIpOrDomain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
//Append / to the end of the redirection endpoint if not exists
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
@ -125,13 +137,14 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
requestURL = requestURL[1:]
}
u, _ := url.Parse("ws://" + wsRedirectionEndpoint + requestURL)
if target.RequireTLS {
if selectedUpstream.RequireTLS {
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
}
h.logRequest(r, true, 101, "subdomain-websocket", target.Domain)
h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain)
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: target.SkipWebSocketOriginCheck,
SkipTLSValidation: selectedUpstream.SkipCertValidations,
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
Logger: h.Parent.Option.Logger,
})
wspHandler.ServeHTTP(w, r)
return
@ -148,31 +161,32 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
//Build downstream and upstream header rules
upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders()
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
NoCache: h.Parent.Option.NoCache,
PathPrefix: "",
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
Version: target.parent.Option.HostVersion,
statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: selectedUpstream.OriginIpOrDomain,
OriginalHost: originalHostHeader,
UseTLS: selectedUpstream.RequireTLS,
NoCache: h.Parent.Option.NoCache,
PathPrefix: "",
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
HostHeaderOverwrite: target.RequestHostOverwrite,
NoRemoveHopByHop: target.DisableHopByHopHeaderRemoval,
Version: target.parent.Option.HostVersion,
})
var dnsError *net.DNSError
if err != nil {
if errors.As(err, &dnsError) {
http.ServeFile(w, r, "./web/hosterror.html")
log.Println(err.Error())
h.logRequest(r, false, 404, "subdomain-http", target.Domain)
h.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname())
} else {
http.ServeFile(w, r, "./web/rperror.html")
log.Println(err.Error())
h.logRequest(r, false, 521, "subdomain-http", target.Domain)
//TODO: Take this upstream offline automatically
h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname())
}
}
h.logRequest(r, true, 200, "subdomain-http", target.Domain)
h.Parent.logRequest(r, true, statusCode, "host-http", r.URL.Hostname())
}
// Handle vdir type request
@ -194,10 +208,11 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
if target.RequireTLS {
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
}
h.logRequest(r, true, 101, "vdir-websocket", target.Domain)
h.Parent.logRequest(r, true, 101, "vdir-websocket", target.Domain)
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: target.parent.SkipWebSocketOriginCheck,
SkipOriginCheck: true, //You should not use websocket via virtual directory. But keep this to true for compatibility
Logger: h.Parent.Option.Logger,
})
wspHandler.ServeHTTP(w, r)
return
@ -214,14 +229,15 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
//Build downstream and upstream header rules
upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders()
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
PathPrefix: target.MatchingPath,
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
Version: target.parent.parent.Option.HostVersion,
statusCode, err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
PathPrefix: target.MatchingPath,
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
HostHeaderOverwrite: target.parent.RequestHostOverwrite,
Version: target.parent.parent.Option.HostVersion,
})
var dnsError *net.DNSError
@ -229,23 +245,24 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
if errors.As(err, &dnsError) {
http.ServeFile(w, r, "./web/hosterror.html")
log.Println(err.Error())
h.logRequest(r, false, 404, "vdir-http", target.Domain)
h.Parent.logRequest(r, false, 404, "vdir-http", target.Domain)
} else {
http.ServeFile(w, r, "./web/rperror.html")
log.Println(err.Error())
h.logRequest(r, false, 521, "vdir-http", target.Domain)
h.Parent.logRequest(r, false, 521, "vdir-http", target.Domain)
}
}
h.logRequest(r, true, 200, "vdir-http", target.Domain)
h.Parent.logRequest(r, true, statusCode, "vdir-http", target.Domain)
}
func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, target string) {
if h.Parent.Option.StatisticCollector != nil {
// This logger collect data for the statistical analysis. For log to file logger, check the Logger and LogHTTPRequest handler
func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, target string) {
if router.Option.StatisticCollector != nil {
go func() {
requestInfo := statistic.RequestInfo{
IpAddr: netutils.GetRequesterIP(r),
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
RequestOriginalCountryISOCode: router.Option.GeodbStore.GetRequesterCountryISOCode(r),
Succ: succ,
StatusCode: statusCode,
ForwardType: forwardType,
@ -254,7 +271,8 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
RequestURL: r.Host + r.RequestURI,
Target: target,
}
h.Parent.Option.StatisticCollector.RecordRequest(requestInfo)
router.Option.StatisticCollector.RecordRequest(requestInfo)
}()
}
router.Option.Logger.LogHTTPRequest(r, forwardType, statusCode)
}

View File

@ -51,7 +51,7 @@ func (t *RequestCountPerIpTable) Clear() {
func (h *ProxyHandler) handleRateLimitRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := h.Parent.handleRateLimit(w, r, pe)
if err != nil {
h.logRequest(r, false, 429, "ratelimit", pe.Domain)
h.Parent.logRequest(r, false, 429, "ratelimit", r.URL.Hostname())
}
return err
}

View File

@ -195,6 +195,6 @@ func (t *RuleTable) log(message string, err error) {
log.Println("[Redirect] " + message + ": " + err.Error())
}
} else {
t.Logger.PrintAndLog("Redirect", message, err)
t.Logger.PrintAndLog("redirect", message, err)
}
}

View File

@ -2,8 +2,10 @@ package dynamicproxy
import (
"errors"
"log"
"net/url"
"strings"
"time"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
)
@ -17,41 +19,18 @@ import (
// 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
for _, thisOrigin := range endpoint.ActiveOrigins {
//Create the proxy routing handler
err := thisOrigin.StartProxy()
if err != nil {
log.Println("Unable to setup upstream " + thisOrigin.OriginIpOrDomain + ": " + err.Error())
continue
}
}
//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
//Prepare proxy routing handler for each of the virtual directories
for _, vdir := range endpoint.VirtualDirectories {
domain := vdir.Domain
if len(domain) == 0 {
@ -63,7 +42,7 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
}
//Parse the web proxy endpoint
webProxyEndpoint = domain
webProxyEndpoint := domain
if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
//TLS is not hardcoded in proxy target domain
if vdir.RequireTLS {
@ -80,6 +59,7 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
proxy := dpcore.NewDynamicProxyCore(path, vdir.MatchingPath, &dpcore.DpcoreOptions{
IgnoreTLSVerification: vdir.SkipCertValidations,
FlushInterval: 500 * time.Millisecond,
})
vdir.proxy = proxy
vdir.parent = endpoint
@ -90,7 +70,12 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
// Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime
func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
if endpoint.proxy == nil {
if len(endpoint.ActiveOrigins) == 0 {
//There are no active origins. No need to check for ready
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint)
return nil
}
if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) {
//This endpoint is not prepared
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
}
@ -101,7 +86,7 @@ func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
// Set given Proxy Route as Root. Call to PrepareProxyRoute before adding to runtime
func (router *Router) SetProxyRouteAsRoot(endpoint *ProxyEndpoint) error {
if endpoint.proxy == nil {
if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) {
//This endpoint is not prepared
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
}

View File

@ -8,9 +8,11 @@ import (
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/tlscert"
)
@ -25,23 +27,27 @@ type ProxyHandler struct {
Parent *Router
}
/* Router Object Options */
type RouterOption struct {
HostUUID string //The UUID of Zoraxy, use for heading mod
HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above
NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager
RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store //GeoIP resolver
AccessController *access.Controller //Blacklist / whitelist controller
StatisticCollector *statistic.Collector
WebDirectory string //The static web server directory containing the templates folder
HostUUID string //The UUID of Zoraxy, use for heading mod
HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above
NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager //TLS manager for serving SAN certificates
RedirectRuleTable *redirection.RuleTable //Redirection rules handler and table
GeodbStore *geodb.Store //GeoIP resolver
AccessController *access.Controller //Blacklist / whitelist controller
StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors
WebDirectory string //The static web server directory containing the templates folder
LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target
Logger *logger.Logger //Logger for reverse proxy requets
}
/* Router Object */
type Router struct {
Option *RouterOption
ProxyEndpoints *sync.Map
@ -50,6 +56,7 @@ type Router struct {
mux http.Handler
server *http.Server
tlsListener net.Listener
loadBalancer *loadbalance.RouteManager //Load balancer routing manager
routingRules []*RoutingRule
tlsRedirectStop chan bool //Stop channel for tls redirection server
@ -57,6 +64,7 @@ type Router struct {
rateLimitCounter RequestCountPerIpTable //Request counter for rate limter
}
/* Basic Auth Related Data structure*/
// Auth credential for basic auth on certain endpoints
type BasicAuthCredentials struct {
Username string
@ -74,6 +82,7 @@ type BasicAuthExceptionRule struct {
PathPrefix string
}
/* Custom Header Related Data structure */
// Header injection direction type
type HeaderDirection int
@ -90,6 +99,8 @@ type UserDefinedHeader struct {
IsRemove bool //Instead of set, remove this key instead
}
/* Routing Rule Data Structures */
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
// program structure than directly using ProxyEndpoint
type VirtualDirectoryEndpoint struct {
@ -104,25 +115,28 @@ type VirtualDirectoryEndpoint struct {
// A proxy endpoint record, a general interface for handling inbound routing
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
Domain string //Domain or IP to proxy to
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
InactiveOrigins []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
UseStickySession bool //Use stick session for load balancing
UseActiveLoadBalance bool //Use active loadbalancing, default passive
Disabled bool //If the rule is disabled
//TLS/SSL Related
RequireTLS bool //Target domain require TLS
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
SkipCertValidations bool //Set to true to accept self signed certs
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
//Inbound TLS/SSL Related
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
//Authentication
RequireBasicAuth bool //Set to true to request basic auth before proxy
@ -136,15 +150,12 @@ type ProxyEndpoint struct {
//Access Control
AccessFilterUUID string //Access filter ID
Disabled bool //If the rule is disabled
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
//Internal Logic Elements
parent *Router `json:"-"`
proxy *dpcore.ReverseProxy `json:"-"`
parent *Router `json:"-"`
}
/*

View File

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

View File

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

View File

@ -19,9 +19,8 @@ type Store struct {
geodbIpv6 [][]string //Parsed geodb list for ipv6
geotrie *trie
geotrieIpv6 *trie
//geoipCache sync.Map
sysdb *database.Database
option *StoreOptions
sysdb *database.Database
option *StoreOptions
}
type StoreOptions struct {

View File

@ -43,7 +43,7 @@ func TestResolveCountryCodeFromIP(t *testing.T) {
// Create a new store
store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{
false,
false,
true,
})
if err != nil {
t.Errorf("error creating store: %v", err)
@ -56,6 +56,7 @@ func TestResolveCountryCodeFromIP(t *testing.T) {
{"176.113.115.113", "RU"},
{"65.21.233.213", "FI"},
{"94.23.207.193", "FR"},
{"77.131.21.232", "FR"},
}
for _, testcase := range knownIpCountryMap {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -16,13 +16,6 @@ func (s *Store) search(ip string) string {
ip = strings.Split(ip, ",")[0]
ip = strings.TrimSpace(ip)
}
//See if there are cached country code for this ip
/*
ccc, ok := s.geoipCache.Load(ip)
if ok {
return ccc.(string)
}
*/
//Search in geotrie tree
cc := ""

View File

@ -1,7 +1,6 @@
package geodb
import (
"math"
"net"
)
@ -41,14 +40,10 @@ func (t *trie) insert(ipAddr string, cc string) {
ipBytes := ipToBytes(ipAddr)
current := t.root
for _, b := range ipBytes {
//For each byte in the ip address
//For each byte in the ip address (4 / 16 bytes)
//each byte is 8 bit
for j := 0; j < 8; j++ {
bitwise := (b&uint8(math.Pow(float64(2), float64(j))) > 0)
bit := 0b0000
if bitwise {
bit = 0b0001
}
for j := 7; j >= 0; j-- {
bit := int(b >> j & 1)
if current.childrens[bit] == nil {
current.childrens[bit] = &trie_Node{
childrens: [2]*trie_Node{},
@ -58,21 +53,9 @@ func (t *trie) insert(ipAddr string, cc string) {
current = current.childrens[bit]
}
}
/*
for i := 63; i >= 0; i-- {
bit := (ipInt64 >> uint(i)) & 1
if current.childrens[bit] == nil {
current.childrens[bit] = &trie_Node{
childrens: [2]*trie_Node{},
cc: cc,
}
}
current = current.childrens[bit]
}
*/
}
// isReservedIP check if the given ip address is NOT a public ip address
func isReservedIP(ip string) bool {
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
@ -86,12 +69,10 @@ func isReservedIP(ip string) bool {
if parsedIP.IsLinkLocalUnicast() || parsedIP.IsLinkLocalMulticast() {
return true
}
//Check if the IP is in the reserved private range
if parsedIP.IsPrivate() {
return true
}
// If the IP address is not a reserved address, return false
return false
}
@ -106,27 +87,15 @@ func (t *trie) search(ipAddr string) string {
for _, b := range ipBytes {
//For each byte in the ip address
//each byte is 8 bit
for j := 0; j < 8; j++ {
bitwise := (b&uint8(math.Pow(float64(2), float64(j))) > 0)
bit := 0b0000
if bitwise {
bit = 0b0001
}
for j := 7; j >= 0; j-- {
bit := int(b >> j & 1)
if current.childrens[bit] == nil {
return current.cc
}
current = current.childrens[bit]
}
}
/*
for i := 63; i >= 0; i-- {
bit := (ipInt64 >> uint(i)) & 1
if current.childrens[bit] == nil {
return current.cc
}
current = current.childrens[bit]
}
*/
if len(current.childrens) == 0 {
return current.cc
}

View File

@ -13,29 +13,31 @@ import (
Zoraxy Logger
This script is designed to make a managed log for the Zoraxy
and replace the ton of log.Println in the system core
and replace the ton of log.Println in the system core.
The core logger is based in golang's build-in log package
*/
type Logger struct {
LogToFile bool //Set enable write to file
Prefix string //Prefix for log files
LogFolder string //Folder to store the log file
CurrentLogFile string //Current writing filename
logger *log.Logger
file *os.File
}
func NewLogger(logFilePrefix string, logFolder string, logToFile bool) (*Logger, error) {
// Create a new logger that log to files
func NewLogger(logFilePrefix string, logFolder string) (*Logger, error) {
err := os.MkdirAll(logFolder, 0775)
if err != nil {
return nil, err
}
thisLogger := Logger{
LogToFile: logToFile,
Prefix: logFilePrefix,
LogFolder: logFolder,
}
//Create the log file if not exists
logFilePath := thisLogger.getLogFilepath()
f, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
if err != nil {
@ -43,9 +45,26 @@ func NewLogger(logFilePrefix string, logFolder string, logToFile bool) (*Logger,
}
thisLogger.CurrentLogFile = logFilePath
thisLogger.file = f
//Start the logger
logger := log.New(f, "", log.Flags()&^(log.Ldate|log.Ltime))
logger.SetFlags(0)
logger.SetOutput(f)
thisLogger.logger = logger
return &thisLogger, nil
}
// Create a fmt logger that only log to STDOUT
func NewFmtLogger() (*Logger, error) {
return &Logger{
Prefix: "",
LogFolder: "",
CurrentLogFile: "",
logger: nil,
file: nil,
}, nil
}
func (l *Logger) getLogFilepath() string {
year, month, _ := time.Now().Date()
return filepath.Join(l.LogFolder, l.Prefix+"_"+strconv.Itoa(year)+"-"+strconv.Itoa(int(month))+".log")
@ -54,9 +73,8 @@ func (l *Logger) getLogFilepath() string {
// PrintAndLog will log the message to file and print the log to STDOUT
func (l *Logger) PrintAndLog(title string, message string, originalError error) {
go func() {
l.Log(title, message, originalError)
l.Log(title, message, originalError, true)
}()
log.Println("[" + title + "] " + message)
}
// Println is a fast snap-in replacement for log.Println
@ -64,18 +82,26 @@ func (l *Logger) Println(v ...interface{}) {
//Convert the array of interfaces into string
message := fmt.Sprint(v...)
go func() {
l.Log("info", string(message), nil)
l.Log("internal", string(message), nil, true)
}()
log.Println("[INFO] " + string(message))
}
func (l *Logger) Log(title string, errorMessage string, originalError error) {
func (l *Logger) Log(title string, errorMessage string, originalError error, copyToSTDOUT bool) {
l.ValidateAndUpdateLogFilepath()
if l.LogToFile {
if l.logger == nil || copyToSTDOUT {
//Use STDOUT instead of logger
if originalError == nil {
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [INFO]" + errorMessage + "\n")
fmt.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [" + title + "] [system:info] " + errorMessage)
} else {
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [ERROR]" + errorMessage + " " + originalError.Error() + "\n")
fmt.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [" + title + "] [system:error] " + errorMessage + ": " + originalError.Error())
}
}
if l.logger != nil {
if originalError == nil {
l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [" + title + "] [system:info] " + errorMessage)
} else {
l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [" + title + "] [system:error] " + errorMessage + ": " + originalError.Error())
}
}
@ -83,18 +109,28 @@ func (l *Logger) Log(title string, errorMessage string, originalError error) {
// Validate if the logging target is still valid (detect any months change)
func (l *Logger) ValidateAndUpdateLogFilepath() {
if l.file == nil {
return
}
expectedCurrentLogFilepath := l.getLogFilepath()
if l.CurrentLogFile != expectedCurrentLogFilepath {
//Change of month. Update to a new log file
l.file.Close()
l.file = nil
//Create a new log file
f, err := os.OpenFile(expectedCurrentLogFilepath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
if err != nil {
log.Println("[Logger] Unable to create new log. Logging to file disabled.")
l.LogToFile = false
log.Println("Unable to create new log. Logging is disabled: ", err.Error())
l.logger = nil
return
}
l.CurrentLogFile = expectedCurrentLogFilepath
l.file = f
//Start a new logger
logger := log.New(f, "", log.Default().Flags())
l.logger = logger
}
}

View File

@ -0,0 +1,32 @@
package logger
/*
Traffic Log
This script log the traffic of HTTP requests
*/
import (
"net/http"
"strconv"
"time"
"imuslab.com/zoraxy/mod/netutils"
)
// Log HTTP request. Note that this must run in go routine to prevent any blocking
// in reverse proxy router
func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int) {
go func() {
l.ValidateAndUpdateLogFilepath()
if l.logger == nil || l.file == nil {
//logger is not initiated. Do not log http request
return
}
clientIP := netutils.GetRequesterIP(r)
requestURI := r.RequestURI
statusCodeString := strconv.Itoa(statusCode)
//fmt.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [client " + clientIP + "] " + r.Method + " " + requestURI + " " + statusCodeString)
l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [origin:" + r.URL.Hostname() + "] [client " + clientIP + "] " + r.Method + " " + requestURI + " " + statusCodeString)
}()
}

View File

@ -51,13 +51,7 @@ func (v *Viewer) HandleReadLog(w http.ResponseWriter, r *http.Request) {
return
}
catergory, err := utils.GetPara(r, "catergory")
if err != nil {
utils.SendErrorResponse(w, "invalid catergory given")
return
}
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(catergory)), strings.TrimSpace(filepath.Base(filename)))
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(filename)))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
@ -106,8 +100,10 @@ func (v *Viewer) ListLogFiles(showFullpath bool) map[string][]*LogFile {
return result
}
func (v *Viewer) LoadLogFile(catergory string, filename string) (string, error) {
logFilepath := filepath.Join(v.option.RootFolder, catergory, filename)
func (v *Viewer) LoadLogFile(filename string) (string, error) {
filename = filepath.ToSlash(filename)
filename = strings.ReplaceAll(filename, "../", "")
logFilepath := filepath.Join(v.option.RootFolder, filename)
if utils.FileExists(logFilepath) {
//Load it
content, err := os.ReadFile(logFilepath)

View File

@ -3,8 +3,6 @@ package netstat
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/exec"
@ -14,6 +12,7 @@ import (
"strings"
"time"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
@ -35,11 +34,11 @@ type NetStatBuffers struct {
Stats []*FlowStat //Statistic of the flow
StopChan chan bool //Channel to stop the ticker
EventTicker *time.Ticker //Ticker for event logging
logger *logger.Logger
}
// Get a new network statistic buffers
func NewNetStatBuffer(recordCount int) (*NetStatBuffers, error) {
func NewNetStatBuffer(recordCount int, systemWideLogger *logger.Logger) (*NetStatBuffers, error) {
//Flood fill the stats with 0
initialStats := []*FlowStat{}
for i := 0; i < recordCount; i++ {
@ -66,21 +65,22 @@ func NewNetStatBuffer(recordCount int) (*NetStatBuffers, error) {
Stats: initialStats,
StopChan: stopCh,
EventTicker: ticker,
logger: systemWideLogger,
}
//Get the initial measurements of netstats
rx, tx, err := GetNetworkInterfaceStats()
rx, tx, err := thisNetBuffer.GetNetworkInterfaceStats()
if err != nil {
log.Println("Unable to get NIC stats: ", err.Error())
systemWideLogger.PrintAndLog("netstat", "Unable to get NIC stats: ", err)
}
retryCount := 0
for rx == 0 && tx == 0 && retryCount < 10 {
//Strange. Retry
log.Println("NIC stats return all 0. Retrying...")
rx, tx, err = GetNetworkInterfaceStats()
systemWideLogger.PrintAndLog("netstat", "NIC stats return all 0. Retrying...", nil)
rx, tx, err = thisNetBuffer.GetNetworkInterfaceStats()
if err != nil {
log.Println("Unable to get NIC stats: ", err.Error())
systemWideLogger.PrintAndLog("netstat", "Unable to get NIC stats: ", err)
}
retryCount++
}
@ -95,20 +95,20 @@ func NewNetStatBuffer(recordCount int) (*NetStatBuffers, error) {
for {
select {
case <-n.StopChan:
fmt.Println("- Netstats listener stopped")
systemWideLogger.PrintAndLog("netstat", "Netstats listener stopped", nil)
return
case <-ticker.C:
if n.PreviousStat.RX == 0 && n.PreviousStat.TX == 0 {
//Initiation state is still not done. Ignore request
log.Println("No initial states. Waiting")
systemWideLogger.PrintAndLog("netstat", "No initial states. Waiting", nil)
return
}
// Get the latest network interface stats
rx, tx, err := GetNetworkInterfaceStats()
rx, tx, err := thisNetBuffer.GetNetworkInterfaceStats()
if err != nil {
// Log the error, but don't stop the buffer
log.Printf("Failed to get network interface stats: %v", err)
systemWideLogger.PrintAndLog("netstat", "Failed to get network interface stats", err)
continue
}
@ -174,8 +174,8 @@ func (n *NetStatBuffers) Close() {
n.EventTicker.Stop()
}
func HandleGetNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) {
rx, tx, err := GetNetworkInterfaceStats()
func (n *NetStatBuffers) HandleGetNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) {
rx, tx, err := n.GetNetworkInterfaceStats()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
@ -194,7 +194,7 @@ func HandleGetNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) {
}
// Get network interface stats, return accumulated rx bits, tx bits and error if any
func GetNetworkInterfaceStats() (int64, int64, error) {
func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
if runtime.GOOS == "windows" {
//Windows wmic sometime freeze and not respond.
//The safer way is to make a bypass mechanism
@ -263,7 +263,7 @@ func GetNetworkInterfaceStats() (int64, int64, error) {
result = <-callbackChan
cmd = nil
if result.Err != nil {
log.Println("Unable to extract NIC info from wmic: " + result.Err.Error())
n.logger.PrintAndLog("netstat", "Unable to extract NIC info from wmic", result.Err)
}
return result.RX, result.TX, result.Err
} else if runtime.GOOS == "linux" {

View File

@ -88,6 +88,7 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: false,
SkipOriginCheck: false,
Logger: nil,
})
wspHandler.ServeHTTP(w, r)
return

View File

@ -6,11 +6,11 @@ import (
"embed"
"encoding/pem"
"io"
"log"
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
@ -21,15 +21,16 @@ type CertCache struct {
}
type Manager struct {
CertStore string //Path where all the certs are stored
LoadedCerts []*CertCache //A list of loaded certs
CertStore string //Path where all the certs are stored
LoadedCerts []*CertCache //A list of loaded certs
Logger *logger.Logger //System wide logger for debug mesage
verbal bool
}
//go:embed localhost.pem localhost.key
var buildinCertStore embed.FS
func NewManager(certStore string, verbal bool) (*Manager, error) {
func NewManager(certStore string, verbal bool, logger *logger.Logger) (*Manager, error) {
if !utils.FileExists(certStore) {
os.MkdirAll(certStore, 0775)
}
@ -52,6 +53,7 @@ func NewManager(certStore string, verbal bool) (*Manager, error) {
CertStore: certStore,
LoadedCerts: []*CertCache{},
verbal: verbal,
Logger: logger,
}
err := thisManager.UpdateLoadedCertList()
@ -78,7 +80,7 @@ func (m *Manager) UpdateLoadedCertList() error {
priKey := filepath.Join(m.CertStore, certname+".key")
certificate, err := tls.LoadX509KeyPair(pubKey, priKey)
if err != nil {
log.Println("Certificate loaded failed: " + certname)
m.Logger.PrintAndLog("tls-router", "Certificate load failed: "+certname, err)
continue
}
@ -86,6 +88,7 @@ func (m *Manager) UpdateLoadedCertList() error {
loadedCert, err := x509.ParseCertificate(thisCert)
if err != nil {
//Error pasring cert, skip this byte segment
m.Logger.PrintAndLog("tls-router", "Certificate parse failed: "+certname, err)
continue
}
@ -171,44 +174,16 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName)
} else {
//Fallback to legacy method of matching certificates
/*
domainCerts, _ := m.ListCertDomains()
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
if cloestDomainCert != "" {
//There is a matching parent domain for this subdomain. Use this instead.
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".pem")
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
} else if m.DefaultCertExists() {
//Use default.pem and default.key
pubKey = filepath.Join(m.CertStore, "default.pem")
priKey = filepath.Join(m.CertStore, "default.key")
if m.verbal {
log.Println("No matching certificate found. Serving with default")
}
} else {
if m.verbal {
log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
}
}*/
if m.DefaultCertExists() {
//Use default.pem and default.key
pubKey = filepath.Join(m.CertStore, "default.pem")
priKey = filepath.Join(m.CertStore, "default.key")
//if m.verbal {
// log.Println("No matching certificate found. Serving with default")
//}
} else {
//if m.verbal {
// log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
//}
}
}
//Load the cert and serve it
cer, err := tls.LoadX509KeyPair(pubKey, priKey)
if err != nil {
log.Println(err)
return nil, nil
}

107
src/mod/update/update.go Normal file
View File

@ -0,0 +1,107 @@
package update
/*
Update.go
This module handle cross version updates that contains breaking changes
update command should always exit after the update is completed
*/
import (
"fmt"
"os"
"strconv"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
// Run config update. Version numbers are int. For example
// to update 3.0.7 to 3.0.8, use RunConfigUpdate(307, 308)
// This function support cross versions updates (e.g. 307 -> 310)
func RunConfigUpdate(fromVersion int, toVersion int) {
versionFile := "./conf/version"
isFirstTimeInit, _ := isFirstTimeInitialize("./conf/proxy/")
if isFirstTimeInit {
//Create version file and exit
os.MkdirAll("./conf/", 0775)
os.WriteFile(versionFile, []byte(strconv.Itoa(toVersion)), 0775)
return
}
if fromVersion == 0 {
//Run auto previous version detection
fromVersion = 307
if utils.FileExists(versionFile) {
//Read the version file
previousVersionText, err := os.ReadFile(versionFile)
if err != nil {
panic("Unable to read version file at " + versionFile)
}
//Convert the version to int
versionInt, err := strconv.Atoi(strings.TrimSpace(string(previousVersionText)))
if err != nil {
panic("Unable to read version file at " + versionFile)
}
fromVersion = versionInt
}
if fromVersion == toVersion {
//No need to update
return
}
}
//Do iterate update
for i := fromVersion; i < toVersion; i++ {
oldVersion := i
newVersion := i + 1
fmt.Println("Updating from v", oldVersion, " to v", newVersion)
runUpdateRoutineWithVersion(oldVersion, newVersion)
//Write the updated version to file
os.WriteFile(versionFile, []byte(strconv.Itoa(newVersion)), 0775)
}
fmt.Println("Update completed")
}
func GetVersionIntFromVersionNumber(version string) int {
versionNumberOnly := strings.ReplaceAll(version, ".", "")
versionInt, _ := strconv.Atoi(versionNumberOnly)
return versionInt
}
// Check if the folder "./conf/proxy/" exists and contains files
func isFirstTimeInitialize(path string) (bool, error) {
// Check if the folder exists
info, err := os.Stat(path)
if os.IsNotExist(err) {
// The folder does not exist
return true, nil
}
if err != nil {
// Some other error occurred
return false, err
}
// Check if it is a directory
if !info.IsDir() {
// The path is not a directory
return false, fmt.Errorf("%s is not a directory", path)
}
// Read the directory contents
files, err := os.ReadDir(path)
if err != nil {
return false, err
}
// Check if the directory is empty
if len(files) == 0 {
// The folder exists but is empty
return true, nil
}
// The folder exists and contains files
return false, nil
}

View File

@ -0,0 +1,16 @@
package update
import v308 "imuslab.com/zoraxy/mod/update/v308"
// Updater Core logic
func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
if fromVersion == 307 && toVersion == 308 {
//Updating from v3.0.7 to v3.0.8
err := v308.UpdateFrom307To308()
if err != nil {
panic(err)
}
}
//ADD MORE VERSIONS HERE
}

View File

@ -0,0 +1,138 @@
package v308
/*
v307 type definations
This file wrap up the self-contained data structure
for v3.0.7 structure and allow automatic updates
for future releases if required
*/
type v307PermissionsPolicy struct {
Accelerometer []string `json:"accelerometer"`
AmbientLightSensor []string `json:"ambient_light_sensor"`
Autoplay []string `json:"autoplay"`
Battery []string `json:"battery"`
Camera []string `json:"camera"`
CrossOriginIsolated []string `json:"cross_origin_isolated"`
DisplayCapture []string `json:"display_capture"`
DocumentDomain []string `json:"document_domain"`
EncryptedMedia []string `json:"encrypted_media"`
ExecutionWhileNotRendered []string `json:"execution_while_not_rendered"`
ExecutionWhileOutOfView []string `json:"execution_while_out_of_viewport"`
Fullscreen []string `json:"fullscreen"`
Geolocation []string `json:"geolocation"`
Gyroscope []string `json:"gyroscope"`
KeyboardMap []string `json:"keyboard_map"`
Magnetometer []string `json:"magnetometer"`
Microphone []string `json:"microphone"`
Midi []string `json:"midi"`
NavigationOverride []string `json:"navigation_override"`
Payment []string `json:"payment"`
PictureInPicture []string `json:"picture_in_picture"`
PublicKeyCredentialsGet []string `json:"publickey_credentials_get"`
ScreenWakeLock []string `json:"screen_wake_lock"`
SyncXHR []string `json:"sync_xhr"`
USB []string `json:"usb"`
WebShare []string `json:"web_share"`
XRSpatialTracking []string `json:"xr_spatial_tracking"`
ClipboardRead []string `json:"clipboard_read"`
ClipboardWrite []string `json:"clipboard_write"`
Gamepad []string `json:"gamepad"`
SpeakerSelection []string `json:"speaker_selection"`
ConversionMeasurement []string `json:"conversion_measurement"`
FocusWithoutUserActivation []string `json:"focus_without_user_activation"`
HID []string `json:"hid"`
IdleDetection []string `json:"idle_detection"`
InterestCohort []string `json:"interest_cohort"`
Serial []string `json:"serial"`
SyncScript []string `json:"sync_script"`
TrustTokenRedemption []string `json:"trust_token_redemption"`
Unload []string `json:"unload"`
WindowPlacement []string `json:"window_placement"`
VerticalScroll []string `json:"vertical_scroll"`
}
// Auth credential for basic auth on certain endpoints
type v307BasicAuthCredentials struct {
Username string
PasswordHash string
}
// Auth credential for basic auth on certain endpoints
type v307BasicAuthUnhashedCredentials struct {
Username string
Password string
}
// Paths to exclude in basic auth enabled proxy handler
type v307BasicAuthExceptionRule struct {
PathPrefix string
}
// Header injection direction type
type v307HeaderDirection int
const (
HeaderDirection_ZoraxyToUpstream v307HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
HeaderDirection_ZoraxyToDownstream v307HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
)
// User defined headers to add into a proxy endpoint
type v307UserDefinedHeader struct {
Direction v307HeaderDirection
Key string
Value string
IsRemove bool //Instead of set, remove this key instead
}
// The original proxy endpoint structure from v3.0.7
type v307ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
Domain string //Domain or IP to proxy to
//TLS/SSL Related
RequireTLS bool //Target domain require TLS
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
SkipCertValidations bool //Set to true to accept self signed certs
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
//Virtual Directories
VirtualDirectories []*v307VirtualDirectoryEndpoint
//Custom Headers
UserDefinedHeaders []*v307UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *v307PermissionsPolicy //Permission policy header
//Authentication
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*v307BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*v307BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
// Rate Limiting
RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second
//Access Control
AccessFilterUUID string //Access filter ID
Disabled bool //If the rule is disabled
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
}
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
// program structure than directly using ProxyEndpoint
type v307VirtualDirectoryEndpoint struct {
MatchingPath string //Matching prefix of the request path, also act as key
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
Disabled bool //If the rule is enabled
}

View File

@ -0,0 +1,63 @@
package v308
/*
v308 type definations
This file wrap up the self-contained data structure
for v3.0.8 structure and allow automatic updates
for future releases if required
Some struct are identical as v307 and hence it is not redefined here
*/
/* Upstream or Origin Server */
type v308Upstream struct {
//Upstream Proxy Configs
OriginIpOrDomain string //Target IP address or domain name with port
RequireTLS bool //Require TLS connection
SkipCertValidations bool //Set to true to accept self signed certs
SkipWebSocketOriginCheck bool //Skip origin check on websocket upgrade connections
//Load balancing configs
Weight int //Prirotiy of fallback, set all to 0 for round robin
MaxConn int //Maxmium connection to this server
}
// A proxy endpoint record, a general interface for handling inbound routing
type v308ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*v308Upstream //Activated Upstream or origin servers IP or domain to proxy to
InactiveOrigins []*v308Upstream //Disabled Upstream or origin servers IP or domain to proxy to
UseStickySession bool //Use stick session for load balancing
Disabled bool //If the rule is disabled
//Inbound TLS/SSL Related
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
//Virtual Directories
VirtualDirectories []*v307VirtualDirectoryEndpoint
//Custom Headers
UserDefinedHeaders []*v307UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *v307PermissionsPolicy //Permission policy header
//Authentication
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*v307BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*v307BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
// Rate Limiting
RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second
//Access Control
AccessFilterUUID string //Access filter ID
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
}

132
src/mod/update/v308/v308.go Normal file
View File

@ -0,0 +1,132 @@
package v308
import (
"encoding/json"
"io"
"log"
"os"
"path/filepath"
)
/*
v3.0.7 update to v3.0.8
This update function
*/
// Update proxy config files from v3.0.7 to v3.0.8
func UpdateFrom307To308() error {
//Load the configs
oldConfigFiles, err := filepath.Glob("./conf/proxy/*.config")
if err != nil {
return err
}
//Backup all the files
err = os.MkdirAll("./conf/proxy.old/", 0775)
if err != nil {
return err
}
for _, oldConfigFile := range oldConfigFiles {
// Extract the file name from the path
fileName := filepath.Base(oldConfigFile)
// Construct the backup file path
backupFile := filepath.Join("./conf/proxy.old/", fileName)
// Copy the file to the backup directory
err := copyFile(oldConfigFile, backupFile)
if err != nil {
return err
}
}
//read the config into the old struct
for _, oldConfigFile := range oldConfigFiles {
configContent, err := os.ReadFile(oldConfigFile)
if err != nil {
log.Println("Unable to read config file "+filepath.Base(oldConfigFile), err.Error())
continue
}
thisOldConfigStruct := v307ProxyEndpoint{}
err = json.Unmarshal(configContent, &thisOldConfigStruct)
if err != nil {
log.Println("Unable to parse file "+filepath.Base(oldConfigFile), err.Error())
continue
}
//Convert the old config to new config
newProxyStructure := convertV307ToV308(thisOldConfigStruct)
js, _ := json.MarshalIndent(newProxyStructure, "", " ")
err = os.WriteFile(oldConfigFile, js, 0775)
if err != nil {
log.Println(err.Error())
continue
}
}
return nil
}
func convertV307ToV308(old v307ProxyEndpoint) v308ProxyEndpoint {
// Create a new v308ProxyEndpoint instance
matchingDomainsSlice := old.MatchingDomainAlias
if matchingDomainsSlice == nil {
matchingDomainsSlice = []string{}
}
newEndpoint := v308ProxyEndpoint{
ProxyType: old.ProxyType,
RootOrMatchingDomain: old.RootOrMatchingDomain,
MatchingDomainAlias: matchingDomainsSlice,
ActiveOrigins: []*v308Upstream{{ // Mapping Domain field to v308Upstream struct
OriginIpOrDomain: old.Domain,
RequireTLS: old.RequireTLS,
SkipCertValidations: old.SkipCertValidations,
SkipWebSocketOriginCheck: old.SkipWebSocketOriginCheck,
Weight: 1,
MaxConn: 0,
}},
InactiveOrigins: []*v308Upstream{},
UseStickySession: false,
Disabled: old.Disabled,
BypassGlobalTLS: old.BypassGlobalTLS,
VirtualDirectories: old.VirtualDirectories,
UserDefinedHeaders: old.UserDefinedHeaders,
HSTSMaxAge: old.HSTSMaxAge,
EnablePermissionPolicyHeader: old.EnablePermissionPolicyHeader,
PermissionPolicy: old.PermissionPolicy,
RequireBasicAuth: old.RequireBasicAuth,
BasicAuthCredentials: old.BasicAuthCredentials,
BasicAuthExceptionRules: old.BasicAuthExceptionRules,
RequireRateLimit: old.RequireRateLimit,
RateLimit: old.RateLimit,
AccessFilterUUID: old.AccessFilterUUID,
DefaultSiteOption: old.DefaultSiteOption,
DefaultSiteValue: old.DefaultSiteValue,
}
return newEndpoint
}
// Helper function to copy files
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destinationFile, err := os.Create(dst)
if err != nil {
return err
}
defer destinationFile.Close()
_, err = io.Copy(destinationFile, sourceFile)
return err
}

View File

@ -2,16 +2,22 @@ package uptime
import (
"encoding/json"
"log"
"errors"
"net/http"
"net/http/cookiejar"
"strconv"
"strings"
"time"
"golang.org/x/net/publicsuffix"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
const (
logModuleName = "uptime-monitor"
)
type Record struct {
Timestamp int64
ID string
@ -23,17 +29,26 @@ type Record struct {
Latency int64
}
type ProxyType string
const (
ProxyType_Host ProxyType = "Origin Server"
ProxyType_Vdir ProxyType = "Virtual Directory"
)
type Target struct {
ID string
Name string
URL string
Protocol string
ID string
Name string
URL string
Protocol string
ProxyType ProxyType
}
type Config struct {
Targets []*Target
Interval int
MaxRecordsStore int
Logger *logger.Logger
}
type Monitor struct {
@ -56,6 +71,12 @@ func NewUptimeMonitor(config *Config) (*Monitor, error) {
Config: config,
OnlineStatusLog: map[string][]*Record{},
}
if config.Logger == nil {
//Use default fmt to log if logger is not provided
config.Logger, _ = logger.NewFmtLogger()
}
//Start the endpoint listener
ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
done := make(chan bool)
@ -69,7 +90,7 @@ func NewUptimeMonitor(config *Config) (*Monitor, error) {
case <-done:
return
case t := <-ticker.C:
log.Println("Uptime updated - ", t.Unix())
thisMonitor.Config.Logger.PrintAndLog(logModuleName, "Uptime updated - "+strconv.Itoa(int(t.Unix())), nil)
thisMonitor.ExecuteUptimeCheck()
}
}
@ -83,7 +104,7 @@ func (m *Monitor) ExecuteUptimeCheck() {
//For each target to check online, do the following
var thisRecord Record
if target.Protocol == "http" || target.Protocol == "https" {
online, laterncy, statusCode := getWebsiteStatusWithLatency(target.URL)
online, laterncy, statusCode := m.getWebsiteStatusWithLatency(target.URL)
thisRecord = Record{
Timestamp: time.Now().Unix(),
ID: target.ID,
@ -96,7 +117,7 @@ func (m *Monitor) ExecuteUptimeCheck() {
}
} else {
log.Println("Unknown protocol: " + target.Protocol + ". Skipping")
m.Config.Logger.PrintAndLog(logModuleName, "Unknown protocol: "+target.Protocol, errors.New("unsupported protocol"))
continue
}
@ -116,8 +137,6 @@ func (m *Monitor) ExecuteUptimeCheck() {
m.OnlineStatusLog[target.ID] = thisRecords
}
}
//TODO: Write results to db
}
func (m *Monitor) AddTargetToMonitor(target *Target) {
@ -193,12 +212,12 @@ func (m *Monitor) HandleUptimeLogRead(w http.ResponseWriter, r *http.Request) {
*/
// Get website stauts with latency given URL, return is conn succ and its latency and status code
func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
func (m *Monitor) getWebsiteStatusWithLatency(url string) (bool, int64, int) {
start := time.Now().UnixNano() / int64(time.Millisecond)
statusCode, err := getWebsiteStatus(url)
end := time.Now().UnixNano() / int64(time.Millisecond)
if err != nil {
log.Println(err.Error())
m.Config.Logger.PrintAndLog(logModuleName, "Ping upstream timeout. Assume offline", err)
return false, 0, 0
} else {
diff := end - start
@ -222,7 +241,7 @@ func getWebsiteStatus(url string) (int, error) {
// Create a one-time use cookie jar to store cookies
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
log.Fatal(err)
return 0, err
}
client := http.Client{

View File

@ -1,6 +1,10 @@
package utils
import (
"archive/zip"
"io"
"os"
"path/filepath"
"strconv"
"strings"
)
@ -50,3 +54,52 @@ func ReplaceSpecialCharacters(filename string) string {
return filename
}
/* Zip File Handler */
// zipFiles compresses multiple files into a single zip archive file
func ZipFiles(filename string, files ...string) error {
newZipFile, err := os.Create(filename)
if err != nil {
return err
}
defer newZipFile.Close()
zipWriter := zip.NewWriter(newZipFile)
defer zipWriter.Close()
for _, file := range files {
if err := addFileToZip(zipWriter, file); err != nil {
return err
}
}
return nil
}
// addFileToZip adds an individual file to a zip archive
func addFileToZip(zipWriter *zip.Writer, filename string) error {
fileToZip, err := os.Open(filename)
if err != nil {
return err
}
defer fileToZip.Close()
info, err := fileToZip.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = filepath.Base(filename)
header.Method = zip.Deflate
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
_, err = io.Copy(writer, fileToZip)
return err
}

View File

@ -3,6 +3,7 @@ package utils
import (
"errors"
"log"
"net"
"net/http"
"os"
"strconv"
@ -40,23 +41,44 @@ func SendOK(w http.ResponseWriter) {
// Get GET parameter
func GetPara(r *http.Request, key string) (string, error) {
keys, ok := r.URL.Query()[key]
if !ok || len(keys[0]) < 1 {
// Get first value from the URL query
value := r.URL.Query().Get(key)
if len(value) == 0 {
return "", errors.New("invalid " + key + " given")
} else {
return keys[0], nil
}
return value, nil
}
// Get POST paramter
func PostPara(r *http.Request, key string) (string, error) {
r.ParseForm()
x := r.Form.Get(key)
if x == "" {
return "", errors.New("invalid " + key + " given")
} else {
return x, nil
// Get GET paramter as boolean, accept 1 or true
func GetBool(r *http.Request, key string) (bool, error) {
x, err := GetPara(r, key)
if err != nil {
return false, err
}
// Convert to lowercase and trim spaces just once to compare
switch strings.ToLower(strings.TrimSpace(x)) {
case "1", "true", "on":
return true, nil
case "0", "false", "off":
return false, nil
}
return false, errors.New("invalid boolean given")
}
// Get POST parameter
func PostPara(r *http.Request, key string) (string, error) {
// Try to parse the form
if err := r.ParseForm(); err != nil {
return "", err
}
// Get first value from the form
x := r.Form.Get(key)
if len(x) == 0 {
return "", errors.New("invalid " + key + " given")
}
return x, nil
}
// Get POST paramter as boolean, accept 1 or true
@ -66,11 +88,11 @@ func PostBool(r *http.Request, key string) (bool, error) {
return false, err
}
x = strings.TrimSpace(x)
if x == "1" || strings.ToLower(x) == "true" || strings.ToLower(x) == "on" {
// Convert to lowercase and trim spaces just once to compare
switch strings.ToLower(strings.TrimSpace(x)) {
case "1", "true", "on":
return true, nil
} else if x == "0" || strings.ToLower(x) == "false" || strings.ToLower(x) == "off" {
case "0", "false", "off":
return false, nil
}
@ -95,14 +117,19 @@ func PostInt(r *http.Request, key string) (int, error) {
func FileExists(filename string) bool {
_, err := os.Stat(filename)
if os.IsNotExist(err) {
if err == nil {
// File exists
return true
} else if errors.Is(err, os.ErrNotExist) {
// File does not exist
return false
}
return true
// Some other error
return false
}
func IsDir(path string) bool {
if FileExists(path) == false {
if !FileExists(path) {
return false
}
fi, err := os.Stat(path)
@ -141,3 +168,35 @@ func StringInArrayIgnoreCase(arr []string, str string) bool {
return StringInArray(smallArray, strings.ToLower(str))
}
// Validate if the listening address is correct
func ValidateListeningAddress(address string) bool {
// Check if the address starts with a colon, indicating it's just a port
if strings.HasPrefix(address, ":") {
return true
}
// Split the address into host and port parts
host, port, err := net.SplitHostPort(address)
if err != nil {
// Try to parse it as just a port
if _, err := strconv.Atoi(address); err == nil {
return false // It's just a port number
}
return false // It's an invalid address
}
// Check if the port part is a valid number
if _, err := strconv.Atoi(port); err != nil {
return false
}
// Check if the host part is a valid IP address or empty (indicating any IP)
if host != "" {
if net.ParseIP(host) == nil {
return false
}
}
return true
}

View File

@ -42,6 +42,12 @@ func (fm *FileManager) HandleList(w http.ResponseWriter, r *http.Request) {
// Construct the absolute path to the target directory
targetDir := filepath.Join(fm.Directory, directory)
// Clean path to prevent path escape #274
targetDir = filepath.ToSlash(filepath.Clean(targetDir))
for strings.Contains(targetDir, "../") {
targetDir = strings.ReplaceAll(targetDir, "../", "")
}
// Open the target directory
dirEntries, err := os.ReadDir(targetDir)
if err != nil {
@ -173,7 +179,7 @@ func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) {
// HandleNewFolder creates a new folder in the specified directory
func (fm *FileManager) HandleNewFolder(w http.ResponseWriter, r *http.Request) {
// Parse the directory name from the request
dirName, err := utils.GetPara(r, "path")
dirName, err := utils.PostPara(r, "path")
if err != nil {
utils.SendErrorResponse(w, "invalid directory name")
return
@ -268,13 +274,13 @@ func (fm *FileManager) HandleFileCopy(w http.ResponseWriter, r *http.Request) {
func (fm *FileManager) HandleFileMove(w http.ResponseWriter, r *http.Request) {
// Parse the source and destination paths from the request
srcPath, err := utils.GetPara(r, "srcpath")
srcPath, err := utils.PostPara(r, "srcpath")
if err != nil {
utils.SendErrorResponse(w, "invalid source path")
return
}
destPath, err := utils.GetPara(r, "destpath")
destPath, err := utils.PostPara(r, "destpath")
if err != nil {
utils.SendErrorResponse(w, "invalid destination path")
return

View File

@ -5,13 +5,13 @@ import (
_ "embed"
"errors"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
"imuslab.com/zoraxy/mod/webserv/filemanager"
)
@ -30,6 +30,7 @@ type WebServerOptions struct {
EnableDirectoryListing bool //Enable listing of directory
WebRoot string //Folder for stroing the static web folders
EnableWebDirManager bool //Enable web file manager to handle files in web directory
Logger *logger.Logger //System logger
Sysdb *database.Database //Database for storing configs
}
@ -45,13 +46,16 @@ type WebServer struct {
// NewWebServer creates a new WebServer instance. One instance only
func NewWebServer(options *WebServerOptions) *WebServer {
if options.Logger == nil {
options.Logger, _ = logger.NewFmtLogger()
}
if !utils.FileExists(options.WebRoot) {
//Web root folder not exists. Create one with default templates
os.MkdirAll(filepath.Join(options.WebRoot, "html"), 0775)
os.MkdirAll(filepath.Join(options.WebRoot, "templates"), 0775)
indexTemplate, err := templates.ReadFile("templates/index.html")
if err != nil {
log.Println("Failed to read static wev server template file: ", err.Error())
options.Logger.PrintAndLog("static-webserv", "Failed to read static wev server template file: ", err)
} else {
os.WriteFile(filepath.Join(options.WebRoot, "html", "index.html"), indexTemplate, 0775)
}
@ -102,7 +106,7 @@ func (ws *WebServer) RestorePreviousState() {
// ChangePort changes the server's port.
func (ws *WebServer) ChangePort(port string) error {
if IsPortInUse(port) {
return errors.New("Selected port is used by another process")
return errors.New("selected port is used by another process")
}
if ws.isRunning {
@ -119,6 +123,7 @@ func (ws *WebServer) ChangePort(port string) error {
return err
}
ws.option.Logger.PrintAndLog("static-webserv", "Listening port updated to "+port, nil)
ws.option.Sysdb.Write("webserv", "port", port)
return nil
@ -141,7 +146,7 @@ func (ws *WebServer) Start() error {
//Check if the port is usable
if IsPortInUse(ws.option.Port) {
return errors.New("Port already in use or access denied by host OS")
return errors.New("port already in use or access denied by host OS")
}
//Dispose the old mux and create a new one
@ -159,12 +164,12 @@ func (ws *WebServer) Start() error {
go func() {
if err := ws.server.ListenAndServe(); err != nil {
if err != http.ErrServerClosed {
fmt.Printf("Web server error: %v\n", err)
ws.option.Logger.PrintAndLog("static-webserv", "Web server failed to start", err)
}
}
}()
log.Println("Static Web Server started. Listeing on :" + ws.option.Port)
ws.option.Logger.PrintAndLog("static-webserv", "Static Web Server started. Listeing on :"+ws.option.Port, nil)
ws.isRunning = true
ws.option.Sysdb.Write("webserv", "enabled", true)
return nil
@ -182,7 +187,7 @@ func (ws *WebServer) Stop() error {
if err := ws.server.Close(); err != nil {
return err
}
ws.option.Logger.PrintAndLog("static-webserv", "Static Web Server stopped", nil)
ws.isRunning = false
ws.option.Sysdb.Write("webserv", "enabled", false)
return nil

View File

@ -3,6 +3,7 @@ package websocketproxy
import (
"crypto/tls"
"errors"
"fmt"
"io"
"log"
@ -12,6 +13,7 @@ import (
"strings"
"github.com/gorilla/websocket"
"imuslab.com/zoraxy/mod/info/logger"
)
var (
@ -54,8 +56,9 @@ type WebsocketProxy struct {
// Additional options for websocket proxy runtime
type Options struct {
SkipTLSValidation bool //Skip backend TLS validation
SkipOriginCheck bool //Skip origin check
SkipTLSValidation bool //Skip backend TLS validation
SkipOriginCheck bool //Skip origin check
Logger *logger.Logger //Logger, can be nil
}
// ProxyHandler returns a new http.Handler interface that reverse proxies the
@ -78,17 +81,26 @@ func NewProxy(target *url.URL, options Options) *WebsocketProxy {
return &WebsocketProxy{Backend: backend, Verbal: false, Options: options}
}
// Utilities function for log printing
func (w *WebsocketProxy) Println(messsage string, err error) {
if w.Options.Logger != nil {
w.Options.Logger.PrintAndLog("websocket", messsage, err)
return
}
log.Println("[websocketproxy] [system:info]"+messsage, err)
}
// ServeHTTP implements the http.Handler that proxies WebSocket connections.
func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if w.Backend == nil {
log.Println("websocketproxy: backend function is not defined")
w.Println("Invalid websocket backend configuration", errors.New("backend function not found"))
http.Error(rw, "internal server error (code: 1)", http.StatusInternalServerError)
return
}
backendURL := w.Backend(req)
if backendURL == nil {
log.Println("websocketproxy: backend URL is nil")
w.Println("Invalid websocket backend configuration", errors.New("backend URL is nil"))
http.Error(rw, "internal server error (code: 2)", http.StatusInternalServerError)
return
}
@ -158,13 +170,13 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01
connBackend, resp, err := dialer.Dial(backendURL.String(), requestHeader)
if err != nil {
log.Printf("websocketproxy: couldn't dial to remote backend url %s", err)
w.Println("Couldn't dial to remote backend url "+backendURL.String(), err)
if resp != nil {
// If the WebSocket handshake fails, ErrBadHandshake is returned
// along with a non-nil *http.Response so that callers can handle
// redirects, authentication, etcetera.
if err := copyResponse(rw, resp); err != nil {
log.Printf("websocketproxy: couldn't write response after failed remote backend handshake: %s", err)
w.Println("Couldn't write response after failed remote backend handshake to "+backendURL.String(), err)
}
} else {
http.Error(rw, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
@ -198,7 +210,7 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Also pass the header that we gathered from the Dial handshake.
connPub, err := upgrader.Upgrade(rw, req, upgradeHeader)
if err != nil {
log.Printf("websocketproxy: couldn't upgrade %s", err)
w.Println("Couldn't upgrade incoming request", err)
return
}
defer connPub.Close()

View File

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

View File

@ -11,6 +11,7 @@ import (
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
@ -96,9 +97,11 @@ func ReverseProxtInit() {
StatisticCollector: statisticCollector,
WebDirectory: *staticWebServerRoot,
AccessController: accessController,
LoadBalancer: loadBalancer,
Logger: SystemWideLogger,
})
if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err)
SystemWideLogger.PrintAndLog("proxy-config", "Unable to create dynamic proxy router", err)
return
}
@ -113,7 +116,7 @@ func ReverseProxtInit() {
for _, conf := range confs {
err := LoadReverseProxyConfig(conf)
if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Failed to load config file: "+filepath.Base(conf), err)
SystemWideLogger.PrintAndLog("proxy-config", "Failed to load config file: "+filepath.Base(conf), err)
return
}
}
@ -122,7 +125,7 @@ func ReverseProxtInit() {
//Root config not set (new deployment?), use internal static web server as root
defaultRootRouter, err := GetDefaultRootConfig()
if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Failed to generate default root routing", err)
SystemWideLogger.PrintAndLog("proxy-config", "Failed to generate default root routing", err)
return
}
dynamicProxyRouter.SetProxyRouteAsRoot(defaultRootRouter)
@ -141,13 +144,11 @@ func ReverseProxtInit() {
//This must be done in go routine to prevent blocking on system startup
uptimeMonitor, _ = uptime.NewUptimeMonitor(&uptime.Config{
Targets: GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter),
Interval: 300, //5 minutes
MaxRecordsStore: 288, //1 day
Interval: 300, //5 minutes
MaxRecordsStore: 288, //1 day
Logger: SystemWideLogger, //Logger
})
//Pass the pointer of this uptime monitor into the load balancer
loadbalancer.Options.UptimeMonitor = uptimeMonitor
SystemWideLogger.Println("Uptime Monitor background service started")
}()
}
@ -208,12 +209,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
useBypassGlobalTLS := bypassGlobalTLS == "true"
//Enable TLS validation?
stv, _ := utils.PostPara(r, "tlsval")
if stv == "" {
stv = "false"
}
skipTlsValidation := (stv == "true")
skipTlsValidation, _ := utils.PostBool(r, "tlsval")
//Get access rule ID
accessRuleID, _ := utils.PostPara(r, "access")
@ -226,12 +222,10 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
}
// Require basic auth?
rba, _ := utils.PostPara(r, "bauth")
if rba == "" {
rba = "false"
}
requireBasicAuth, _ := utils.PostBool(r, "bauth")
requireBasicAuth := (rba == "true")
//Use sticky session?
useStickySession, _ := utils.PostBool(r, "stickysess")
// Require Rate Limiting?
requireRateLimit := false
@ -319,13 +313,21 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
ProxyType: dynamicproxy.ProxyType_Host,
RootOrMatchingDomain: rootOrMatchingDomain,
MatchingDomainAlias: aliasHostnames,
Domain: endpoint,
ActiveOrigins: []*loadbalance.Upstream{
{
OriginIpOrDomain: endpoint,
RequireTLS: useTLS,
SkipCertValidations: skipTlsValidation,
SkipWebSocketOriginCheck: bypassWebsocketOriginCheck,
Weight: 1,
},
},
InactiveOrigins: []*loadbalance.Upstream{},
UseStickySession: useStickySession,
//TLS
RequireTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
SkipWebSocketOriginCheck: bypassWebsocketOriginCheck,
AccessFilterUUID: accessRuleID,
BypassGlobalTLS: useBypassGlobalTLS,
AccessFilterUUID: accessRuleID,
//VDir
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
//Custom headers
@ -375,14 +377,19 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
//Write the root options to file
rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyType_Root,
RootOrMatchingDomain: "/",
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: false,
SkipCertValidations: false,
SkipWebSocketOriginCheck: true,
ProxyType: dynamicproxy.ProxyType_Root,
RootOrMatchingDomain: "/",
ActiveOrigins: []*loadbalance.Upstream{
{
OriginIpOrDomain: endpoint,
RequireTLS: useTLS,
SkipCertValidations: true,
SkipWebSocketOriginCheck: true,
Weight: 1,
},
},
InactiveOrigins: []*loadbalance.Upstream{},
BypassGlobalTLS: false,
DefaultSiteOption: defaultSiteOption,
DefaultSiteValue: dsVal,
}
@ -392,7 +399,11 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
dynamicProxyRouter.SetProxyRouteAsRoot(preparedRootProxyRoute)
err = dynamicProxyRouter.SetProxyRouteAsRoot(preparedRootProxyRoute)
if err != nil {
utils.SendErrorResponse(w, "unable to update default site: "+err.Error())
return
}
proxyEndpointCreated = &rootRoutingEndpoint
} else {
//Invalid eptype
@ -403,7 +414,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
//Save the config to file
err = SaveReverseProxyConfig(proxyEndpointCreated)
if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Unable to save new proxy rule to file", err)
SystemWideLogger.PrintAndLog("proxy-config", "Unable to save new proxy rule to file", err)
return
}
@ -426,24 +437,12 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
endpoint, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "endpoint not defined")
return
}
tls, _ := utils.PostPara(r, "tls")
if tls == "" {
tls = "false"
}
useTLS := (tls == "true")
stv, _ := utils.PostPara(r, "tlsval")
if stv == "" {
stv = "false"
}
skipTlsValidation := (stv == "true")
useStickySession, _ := utils.PostBool(r, "ss")
//Load bypass TLS option
bpgtls, _ := utils.PostPara(r, "bpgtls")
@ -475,18 +474,14 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, "invalid rate limit number")
return
}
if proxyRateLimit <= 0 {
if requireRateLimit && proxyRateLimit <= 0 {
utils.SendErrorResponse(w, "rate limit number must be greater than 0")
return
} else if proxyRateLimit < 0 {
proxyRateLimit = 1000
}
// Bypass WebSocket Origin Check
strbpwsorg, _ := utils.PostPara(r, "bpwsorg")
if strbpwsorg == "" {
strbpwsorg = "false"
}
bypassWebsocketOriginCheck := (strbpwsorg == "true")
//Load the previous basic auth credentials from current proxy rules
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
if err != nil {
@ -496,14 +491,11 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
//Generate a new proxyEndpoint from the new config
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
newProxyEndpoint.Domain = endpoint
newProxyEndpoint.RequireTLS = useTLS
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
newProxyEndpoint.SkipCertValidations = skipTlsValidation
newProxyEndpoint.RequireBasicAuth = requireBasicAuth
newProxyEndpoint.RequireRateLimit = requireRateLimit
newProxyEndpoint.RateLimit = proxyRateLimit
newProxyEndpoint.SkipWebSocketOriginCheck = bypassWebsocketOriginCheck
newProxyEndpoint.UseStickySession = useStickySession
//Prepare to replace the current routing rule
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
@ -517,7 +509,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
//Save it to file
SaveReverseProxyConfig(newProxyEndpoint)
//Update uptime monitor
//Update uptime monitor targets
UpdateUptimeMonitorTargets()
utils.SendOK(w)
@ -551,7 +543,7 @@ func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
newAlias := []string{}
err = json.Unmarshal([]byte(newAliasJSON), &newAlias)
if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Unable to parse new alias list", err)
SystemWideLogger.PrintAndLog("proxy-config", "Unable to parse new alias list", err)
utils.SendErrorResponse(w, "Invalid alias list given")
return
}
@ -573,14 +565,14 @@ func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
err = SaveReverseProxyConfig(newProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, "Alias update failed")
SystemWideLogger.PrintAndLog("Proxy", "Unable to save alias update", err)
SystemWideLogger.PrintAndLog("proxy-config", "Unable to save alias update", err)
}
utils.SendOK(w)
}
func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
ep, err := utils.GetPara(r, "ep")
ep, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "Invalid ep given")
return
@ -600,12 +592,6 @@ func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
//Update utm if exists
if uptimeMonitor != nil {
uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter)
uptimeMonitor.CleanRecords()
}
//Update uptime monitor
UpdateUptimeMonitorTargets()
@ -877,6 +863,10 @@ func ReverseProxyToggleRuleSet(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, "unable to save updated rule")
return
}
//Update uptime monitor
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}
@ -920,7 +910,6 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
results := []*dynamicproxy.ProxyEndpoint{}
dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
thisEndpoint := dynamicproxy.CopyEndpoint(value.(*dynamicproxy.ProxyEndpoint))
//Clear the auth passwords before showing to front-end
cleanedCredentials := []*dynamicproxy.BasicAuthCredentials{}
for _, user := range thisEndpoint.BasicAuthCredentials {
@ -929,14 +918,13 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
PasswordHash: "",
})
}
thisEndpoint.BasicAuthCredentials = cleanedCredentials
results = append(results, thisEndpoint)
return true
})
sort.Slice(results, func(i, j int) bool {
return results[i].Domain < results[j].Domain
return results[i].RootOrMatchingDomain < results[j].RootOrMatchingDomain
})
js, _ := json.Marshal(results)
@ -951,18 +939,22 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
// Handle port 80 incoming traffics
func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
enabled, err := utils.GetPara(r, "enable")
if err != nil {
if r.Method == http.MethodGet {
//Load the current status
currentEnabled := false
err = sysdb.Read("settings", "listenP80", &currentEnabled)
err := sysdb.Read("settings", "listenP80", &currentEnabled)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js))
} else {
} else if r.Method == http.MethodPost {
enabled, err := utils.PostPara(r, "enable")
if err != nil {
utils.SendErrorResponse(w, "enable state not set")
return
}
if enabled == "true" {
sysdb.Write("settings", "listenP80", true)
SystemWideLogger.Println("Enabling port 80 listener")
@ -975,38 +967,48 @@ func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, "invalid mode given: "+enabled)
}
utils.SendOK(w)
} else {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
}
// Handle https redirect
func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) {
useRedirect, err := utils.GetPara(r, "set")
if err != nil {
if r.Method == http.MethodGet {
currentRedirectToHttps := false
//Load the current status
err = sysdb.Read("settings", "redirect", &currentRedirectToHttps)
err := sysdb.Read("settings", "redirect", &currentRedirectToHttps)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(currentRedirectToHttps)
utils.SendJSONResponse(w, string(js))
} else {
} else if r.Method == http.MethodPost {
useRedirect, err := utils.PostBool(r, "set")
if err != nil {
utils.SendErrorResponse(w, "status not set")
return
}
if dynamicProxyRouter.Option.Port == 80 {
utils.SendErrorResponse(w, "This option is not available when listening on port 80")
return
}
if useRedirect == "true" {
if useRedirect {
sysdb.Write("settings", "redirect", true)
SystemWideLogger.Println("Updating force HTTPS redirection to true")
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
} else if useRedirect == "false" {
} else {
sysdb.Write("settings", "redirect", false)
SystemWideLogger.Println("Updating force HTTPS redirection to false")
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
}
utils.SendOK(w)
} else {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
}
@ -1056,15 +1058,20 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
return
}
rootProxyTargetOrigin := ""
if len(dynamicProxyRouter.Root.ActiveOrigins) > 0 {
rootProxyTargetOrigin = dynamicProxyRouter.Root.ActiveOrigins[0].OriginIpOrDomain
}
//Check if it is identical as proxy root (recursion!)
if dynamicProxyRouter.Root == nil || dynamicProxyRouter.Root.Domain == "" {
if dynamicProxyRouter.Root == nil || rootProxyTargetOrigin == "" {
//Check if proxy root is set before checking recursive listen
//Fixing issue #43
utils.SendErrorResponse(w, "Set Proxy Root before changing inbound port")
return
}
proxyRoot := strings.TrimSuffix(dynamicProxyRouter.Root.Domain, "/")
proxyRoot := strings.TrimSuffix(rootProxyTargetOrigin, "/")
if strings.EqualFold(proxyRoot, "localhost:"+strconv.Itoa(newIncomingPortInt)) || strings.EqualFold(proxyRoot, "127.0.0.1:"+strconv.Itoa(newIncomingPortInt)) {
//Listening port is same as proxy root
//Not allow recursive settings
@ -1091,13 +1098,13 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
//List all the custom header defined in this proxy rule
func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
epType, err := utils.PostPara(r, "type")
epType, err := utils.GetPara(r, "type")
if err != nil {
utils.SendErrorResponse(w, "endpoint type not defined")
return
}
domain, err := utils.PostPara(r, "domain")
domain, err := utils.GetPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "domain or matching rule not defined")
return
@ -1240,6 +1247,150 @@ func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) {
}
func HandleHostOverwrite(w http.ResponseWriter, r *http.Request) {
domain, err := utils.PostPara(r, "domain")
if err != nil {
domain, err = utils.GetPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "domain or matching rule not defined")
return
}
}
//Get the proxy endpoint object dedicated to this domain
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
if r.Method == http.MethodGet {
//Get the current host header
js, _ := json.Marshal(targetProxyEndpoint.RequestHostOverwrite)
utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost {
//Set the new host header
newHostname, _ := utils.PostPara(r, "hostname")
//As this will require change in the proxy instance we are running
//we need to clone and respawn this proxy endpoint
newProxyEndpoint := targetProxyEndpoint.Clone()
newProxyEndpoint.RequestHostOverwrite = newHostname
//Save proxy endpoint
err = SaveReverseProxyConfig(newProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Spawn a new endpoint with updated dpcore
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Remove the old endpoint
err = targetProxyEndpoint.Remove()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Add the newly prepared endpoint to runtime
err = dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Print log message
if newHostname != "" {
SystemWideLogger.Println("Updated " + domain + " hostname overwrite to: " + newHostname)
} else {
SystemWideLogger.Println("Removed " + domain + " hostname overwrite")
}
utils.SendOK(w)
} else {
//Invalid method
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
}
// HandleHopByHop get and set the hop by hop remover state
// note that it shows the DISABLE STATE of hop-by-hop remover, not the enable state
func HandleHopByHop(w http.ResponseWriter, r *http.Request) {
domain, err := utils.PostPara(r, "domain")
if err != nil {
domain, err = utils.GetPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "domain or matching rule not defined")
return
}
}
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
if r.Method == http.MethodGet {
//Get the current hop by hop header state
js, _ := json.Marshal(!targetProxyEndpoint.DisableHopByHopHeaderRemoval)
utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost {
//Set the hop by hop header state
enableHopByHopRemover, _ := utils.PostBool(r, "removeHopByHop")
//As this will require change in the proxy instance we are running
//we need to clone and respawn this proxy endpoint
newProxyEndpoint := targetProxyEndpoint.Clone()
//Storage file use false as default, so disable removal = not enable remover
newProxyEndpoint.DisableHopByHopHeaderRemoval = !enableHopByHopRemover
//Save proxy endpoint
err = SaveReverseProxyConfig(newProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Spawn a new endpoint with updated dpcore
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Remove the old endpoint
err = targetProxyEndpoint.Remove()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Add the newly prepared endpoint to runtime
err = dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Print log message
if enableHopByHopRemover {
SystemWideLogger.Println("Enabled hop-by-hop headers removal on " + domain)
} else {
SystemWideLogger.Println("Disabled hop-by-hop headers removal on " + domain)
}
utils.SendOK(w)
} else {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
}
// Handle view or edit HSTS states
func HandleHSTSState(w http.ResponseWriter, r *http.Request) {
domain, err := utils.PostPara(r, "domain")

View File

@ -4,9 +4,11 @@ import (
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/gorilla/csrf"
"imuslab.com/zoraxy/mod/sshprox"
)
@ -42,11 +44,15 @@ func FSHandler(handler http.Handler) http.Handler {
// Allow access to /script/*, /img/pubic/* and /login.html without authentication
if strings.HasPrefix(r.URL.Path, ppf("/script/")) || strings.HasPrefix(r.URL.Path, ppf("/img/public/")) || r.URL.Path == ppf("/login.html") || r.URL.Path == ppf("/reset.html") || r.URL.Path == ppf("/favicon.png") {
if isHTMLFilePath(r.URL.Path) {
handleInjectHTML(w, r, r.URL.Path)
return
}
handler.ServeHTTP(w, r)
return
}
// check authentication
// Check authentication
if !authAgent.CheckAuth(r) && requireAuth {
http.Redirect(w, r, ppf("/login.html"), http.StatusTemporaryRedirect)
return
@ -77,6 +83,10 @@ func FSHandler(handler http.Handler) http.Handler {
}
//Authenticated
if isHTMLFilePath(r.URL.Path) {
handleInjectHTML(w, r, r.URL.Path)
return
}
handler.ServeHTTP(w, r)
})
}
@ -88,3 +98,53 @@ func ppf(relativeFilepath string) string {
}
return relativeFilepath
}
func isHTMLFilePath(requestURI string) bool {
return strings.HasSuffix(requestURI, ".html") || strings.HasSuffix(requestURI, "/")
}
// Serve the html file with template token injected
func handleInjectHTML(w http.ResponseWriter, r *http.Request, relativeFilepath string) {
// Read the HTML file
var content []byte
var err error
if len(relativeFilepath) > 0 && relativeFilepath[len(relativeFilepath)-1:] == "/" {
relativeFilepath = relativeFilepath + "index.html"
}
if development {
//Load from disk
targetFilePath := strings.ReplaceAll(filepath.Join("web/", relativeFilepath), "\\", "/")
content, err = os.ReadFile(targetFilePath)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
} else {
//Load from embedded fs, require trimming off the prefix slash for relative path
relativeFilepath = strings.TrimPrefix(relativeFilepath, "/")
content, err = webres.ReadFile(relativeFilepath)
if err != nil {
SystemWideLogger.Println("load embedded web file failed: ", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}
// Convert the file content to a string
htmlContent := string(content)
//Defeine the system template for this request
templateStrings := map[string]string{
".csrfToken": csrf.Token(r),
}
// Replace template tokens in the HTML content
for key, value := range templateStrings {
placeholder := "{{" + key + "}}"
htmlContent = strings.ReplaceAll(htmlContent, placeholder, value)
}
// Write the modified HTML content to the response
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlContent))
}

View File

@ -20,6 +20,7 @@ import (
"imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/info/logviewer"
"imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/pathrule"
@ -47,6 +48,18 @@ var (
)
func startupSequence() {
//Start a system wide logger and log viewer
l, err := logger.NewLogger("zr", "./log")
if err == nil {
SystemWideLogger = l
} else {
panic(err)
}
LogViewer = logviewer.NewLogViewer(&logviewer.ViewerOption{
RootFolder: "./log",
Extension: ".log",
})
//Create database
db, err := database.NewDatabase("sys.db", false)
if err != nil {
@ -61,29 +74,21 @@ func startupSequence() {
os.MkdirAll("./conf/proxy/", 0775)
//Create an auth agent
sessionKey, err := auth.GetSessionKey(sysdb)
sessionKey, err := auth.GetSessionKey(sysdb, SystemWideLogger)
if err != nil {
log.Fatal(err)
}
authAgent = auth.NewAuthenticationAgent(name, []byte(sessionKey), sysdb, true, func(w http.ResponseWriter, r *http.Request) {
authAgent = auth.NewAuthenticationAgent(name, []byte(sessionKey), sysdb, true, SystemWideLogger, func(w http.ResponseWriter, r *http.Request) {
//Not logged in. Redirecting to login page
http.Redirect(w, r, ppf("/login.html"), http.StatusTemporaryRedirect)
})
//Create a TLS certificate manager
tlsCertManager, err = tlscert.NewManager("./conf/certs", development)
tlsCertManager, err = tlscert.NewManager("./conf/certs", development, SystemWideLogger)
if err != nil {
panic(err)
}
//Create a system wide logger
l, err := logger.NewLogger("zr", "./log", *logOutputToFile)
if err == nil {
SystemWideLogger = l
} else {
panic(err)
}
//Create a redirection rule table
db.NewTable("redirect")
redirectAllowRegexp := false
@ -102,10 +107,12 @@ func startupSequence() {
panic(err)
}
//Create a load balance route manager
loadbalancer = loadbalance.NewRouteManager(&loadbalance.Options{
Geodb: geodbStore,
}, SystemWideLogger)
//Create a load balancer
loadBalancer = loadbalance.NewLoadBalancer(&loadbalance.Options{
SystemUUID: nodeUUID,
Geodb: geodbStore,
Logger: SystemWideLogger,
})
//Create the access controller
accessController, err = access.NewAccessController(&access.Options{
@ -132,12 +139,13 @@ func startupSequence() {
WebRoot: *staticWebServerRoot,
EnableDirectoryListing: true,
EnableWebDirManager: *allowWebFileManager,
Logger: SystemWideLogger,
})
//Restore the web server to previous shutdown state
staticWebServer.RestorePreviousState()
//Create a netstat buffer
netstatBuffers, err = netstat.NewNetStatBuffer(300)
netstatBuffers, err = netstat.NewNetStatBuffer(300, SystemWideLogger)
if err != nil {
SystemWideLogger.PrintAndLog("Network", "Failed to load network statistic info", err)
panic(err)
@ -271,14 +279,21 @@ func startupSequence() {
//Create a table just to store acme related preferences
sysdb.NewTable("acmepref")
acmeHandler = initACME()
acmeAutoRenewer, err = acme.NewAutoRenewer("./conf/acme_conf.json", "./conf/certs/", int64(*acmeAutoRenewInterval), acmeHandler)
acmeAutoRenewer, err = acme.NewAutoRenewer(
"./conf/acme_conf.json",
"./conf/certs/",
int64(*acmeAutoRenewInterval),
*acmeCertAutoRenewDays,
acmeHandler,
SystemWideLogger,
)
if err != nil {
log.Fatal(err)
}
/* Docker UX Optimizer */
if runtime.GOOS == "windows" && *runningInDocker {
SystemWideLogger.PrintAndLog("WARNING", "Invalid start flag combination: docker=true && runtime.GOOS == windows. Running in docker UX development mode.", nil)
SystemWideLogger.PrintAndLog("warning", "Invalid start flag combination: docker=true && runtime.GOOS == windows. Running in docker UX development mode.", nil)
}
DockerUXOptimizer = dockerux.NewDockerOptimizer(*runningInDocker, SystemWideLogger)
@ -291,5 +306,4 @@ func finalSequence() {
//Inject routing rules
registerBuildInRoutingRules()
}

283
src/upstreams.go Normal file
View File

@ -0,0 +1,283 @@
package main
import (
"encoding/json"
"net/http"
"sort"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/utils"
)
/*
Upstreams.go
This script handle upstream and load balancer
related API
*/
// List upstreams from a endpoint
func ReverseProxyUpstreamList(w http.ResponseWriter, r *http.Request) {
endpoint, err := utils.GetPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "endpoint not defined")
return
}
targetEndpoint, err := dynamicProxyRouter.LoadProxy(endpoint)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not found")
return
}
activeUpstreams := targetEndpoint.ActiveOrigins
inactiveUpstreams := targetEndpoint.InactiveOrigins
// Sort the upstreams slice by weight, then by origin domain alphabetically
sort.Slice(activeUpstreams, func(i, j int) bool {
if activeUpstreams[i].Weight != activeUpstreams[j].Weight {
return activeUpstreams[i].Weight > activeUpstreams[j].Weight
}
return activeUpstreams[i].OriginIpOrDomain < activeUpstreams[j].OriginIpOrDomain
})
sort.Slice(inactiveUpstreams, func(i, j int) bool {
if inactiveUpstreams[i].Weight != inactiveUpstreams[j].Weight {
return inactiveUpstreams[i].Weight > inactiveUpstreams[j].Weight
}
return inactiveUpstreams[i].OriginIpOrDomain < inactiveUpstreams[j].OriginIpOrDomain
})
type UpstreamCombinedList struct {
ActiveOrigins []*loadbalance.Upstream
InactiveOrigins []*loadbalance.Upstream
}
js, _ := json.Marshal(UpstreamCombinedList{
ActiveOrigins: activeUpstreams,
InactiveOrigins: inactiveUpstreams,
})
utils.SendJSONResponse(w, string(js))
}
// Add an upstream to a given proxy upstream endpoint
func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
endpoint, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "endpoint not defined")
return
}
targetEndpoint, err := dynamicProxyRouter.LoadProxy(endpoint)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not found")
return
}
upstreamOrigin, err := utils.PostPara(r, "origin")
if err != nil {
utils.SendErrorResponse(w, "upstream origin not set")
return
}
requireTLS, _ := utils.PostBool(r, "tls")
skipTlsValidation, _ := utils.PostBool(r, "tlsval")
bpwsorg, _ := utils.PostBool(r, "bpwsorg")
preactivate, _ := utils.PostBool(r, "active")
//Create a new upstream object
newUpstream := loadbalance.Upstream{
OriginIpOrDomain: upstreamOrigin,
RequireTLS: requireTLS,
SkipCertValidations: skipTlsValidation,
SkipWebSocketOriginCheck: bpwsorg,
Weight: 1,
MaxConn: 0,
}
//Add the new upstream to endpoint
err = targetEndpoint.AddUpstreamOrigin(&newUpstream, preactivate)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Save changes to configs
err = SaveReverseProxyConfig(targetEndpoint)
if err != nil {
SystemWideLogger.PrintAndLog("INFO", "Unable to save new upstream to proxy config", err)
utils.SendErrorResponse(w, "Failed to save new upstream config")
return
}
//Update Uptime Monitor
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}
// Update the connection configuration of this origin
// pass in the whole new upstream origin json via "payload" POST variable
// for missing fields, original value will be used instead
func ReverseProxyUpstreamUpdate(w http.ResponseWriter, r *http.Request) {
endpoint, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "endpoint not defined")
return
}
targetEndpoint, err := dynamicProxyRouter.LoadProxy(endpoint)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not found")
return
}
//Editing upstream origin IP
originIP, err := utils.PostPara(r, "origin")
if err != nil {
utils.SendErrorResponse(w, "origin ip or matching address not set")
return
}
originIP = strings.TrimSpace(originIP)
//Update content payload
payload, err := utils.PostPara(r, "payload")
if err != nil {
utils.SendErrorResponse(w, "update payload not set")
return
}
isActive, _ := utils.PostBool(r, "active")
targetUpstream, err := targetEndpoint.GetUpstreamOriginByMatchingIP(originIP)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Deep copy the upstream so other request handling goroutine won't be effected
newUpstream := targetUpstream.Clone()
//Overwrite the new value into the old upstream
err = json.Unmarshal([]byte(payload), &newUpstream)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Replace the old upstream with the new one
err = targetEndpoint.RemoveUpstreamOrigin(originIP)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
err = targetEndpoint.AddUpstreamOrigin(newUpstream, isActive)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Save changes to configs
err = SaveReverseProxyConfig(targetEndpoint)
if err != nil {
SystemWideLogger.PrintAndLog("INFO", "Unable to save upstream update to proxy config", err)
utils.SendErrorResponse(w, "Failed to save updated upstream config")
return
}
//Update Uptime Monitor
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}
func ReverseProxyUpstreamSetPriority(w http.ResponseWriter, r *http.Request) {
endpoint, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "endpoint not defined")
return
}
targetEndpoint, err := dynamicProxyRouter.LoadProxy(endpoint)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not found")
return
}
weight, err := utils.PostInt(r, "weight")
if err != nil {
utils.SendErrorResponse(w, "priority not defined")
return
}
if weight < 0 {
utils.SendErrorResponse(w, "invalid weight given")
return
}
//Editing upstream origin IP
originIP, err := utils.PostPara(r, "origin")
if err != nil {
utils.SendErrorResponse(w, "origin ip or matching address not set")
return
}
originIP = strings.TrimSpace(originIP)
editingUpstream, err := targetEndpoint.GetUpstreamOriginByMatchingIP(originIP)
editingUpstream.Weight = weight
// The editing upstream is a pointer to the runtime object
// and the change of weight do not requre a respawn of the proxy object
// so no need to remove & re-prepare the upstream on weight changes
err = SaveReverseProxyConfig(targetEndpoint)
if err != nil {
SystemWideLogger.PrintAndLog("INFO", "Unable to update upstream weight", err)
utils.SendErrorResponse(w, "Failed to update upstream weight")
return
}
utils.SendOK(w)
}
func ReverseProxyUpstreamDelete(w http.ResponseWriter, r *http.Request) {
endpoint, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "endpoint not defined")
return
}
targetEndpoint, err := dynamicProxyRouter.LoadProxy(endpoint)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not found")
return
}
//Editing upstream origin IP
originIP, err := utils.PostPara(r, "origin")
if err != nil {
utils.SendErrorResponse(w, "origin ip or matching address not set")
return
}
originIP = strings.TrimSpace(originIP)
if !targetEndpoint.UpstreamOriginExists(originIP) {
utils.SendErrorResponse(w, "target upstream not found")
return
}
err = targetEndpoint.RemoveUpstreamOrigin(originIP)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Save changes to configs
err = SaveReverseProxyConfig(targetEndpoint)
if err != nil {
SystemWideLogger.PrintAndLog("INFO", "Unable to remove upstream", err)
utils.SendErrorResponse(w, "Failed to remove upstream from proxy rule")
return
}
//Update uptime monitor
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}

View File

@ -28,7 +28,7 @@ func ReverseProxyListVdir(w http.ResponseWriter, r *http.Request) {
var targetEndpoint *dynamicproxy.ProxyEndpoint
if eptype == "host" {
endpoint, err := utils.PostPara(r, "ep") //Support root and host
endpoint, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "endpoint not defined")
return
@ -197,6 +197,8 @@ func ReverseProxyDeleteVdir(w http.ResponseWriter, r *http.Request) {
return
}
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}

View File

@ -197,7 +197,7 @@
<div class="item" data-value="lt"><i class="lt flag"></i>Lithuania</div>
<div class="item" data-value="lu"><i class="lu flag"></i>Luxembourg</div>
<div class="item" data-value="mo"><i class="mo flag"></i>Macau</div>
<div class="item" data-value="mk"><i class="mk flag"></i>Macedonia</div>
<div class="item" data-value="mk"><i class="mk flag"></i>North Macedonia</div>
<div class="item" data-value="mg"><i class="mg flag"></i>Madagascar</div>
<div class="item" data-value="mw"><i class="mw flag"></i>Malawi</div>
<div class="item" data-value="my"><i class="my flag"></i>Malaysia</div>
@ -514,7 +514,7 @@
<div class="item" data-value="lt"><i class="lt flag"></i>Lithuania</div>
<div class="item" data-value="lu"><i class="lu flag"></i>Luxembourg</div>
<div class="item" data-value="mo"><i class="mo flag"></i>Macau</div>
<div class="item" data-value="mk"><i class="mk flag"></i>Macedonia</div>
<div class="item" data-value="mk"><i class="mk flag"></i>North Macedonia</div>
<div class="item" data-value="mg"><i class="mg flag"></i>Madagascar</div>
<div class="item" data-value="mw"><i class="mw flag"></i>Malawi</div>
<div class="item" data-value="my"><i class="my flag"></i>Malaysia</div>
@ -1000,7 +1000,7 @@
*/
function enableBlacklist() {
var isChecked = $('#enableBlacklist').is(':checked');
$.ajax({
$.cjax({
type: 'POST',
url: '/api/blacklist/enable',
data: { enable: isChecked, id: currentEditingAccessRule},
@ -1028,9 +1028,10 @@
let counter = 0;
for(var i = 0; i < ccs.length; i++){
let thisCountryCode = ccs[i];
$.ajax({
$.cjax({
type: "POST",
url: "/api/blacklist/country/add",
method: "POST",
data: { cc: thisCountryCode, id: currentEditingAccessRule},
success: function(response) {
if (response.error != undefined){
@ -1066,7 +1067,7 @@
function removeFromBannedList(countryCode){
countryCode = countryCode.toLowerCase();
let countryName = getCountryName(countryCode);
$.ajax({
$.cjax({
url: "/api/blacklist/country/remove",
method: "POST",
data: { cc: countryCode, id: currentEditingAccessRule},
@ -1097,7 +1098,7 @@
}
}
$.ajax({
$.cjax({
url: "/api/blacklist/ip/add",
type: "POST",
data: {ip: targetIp.toLowerCase(), id: currentEditingAccessRule},
@ -1109,7 +1110,7 @@
}
$("#ipAddressInput").val("");
$("#ipAddressInput").parent().remvoeClass("error");
$("#ipAddressInput").parent().removeClass("error");
},
error: function() {
alert("Failed to add IP address to blacklist");
@ -1119,7 +1120,7 @@
function removeIpBlacklist(ipaddr){
if (confirm("Confirm remove blacklist for " + ipaddr + " ?")){
$.ajax({
$.cjax({
url: "/api/blacklist/ip/remove",
type: "POST",
data: {ip: ipaddr.toLowerCase(), id: currentEditingAccessRule},
@ -1143,7 +1144,7 @@
*/
function enableWhitelist() {
var isChecked = $('#enableWhitelist').is(':checked');
$.ajax({
$.cjax({
type: 'POST',
url: '/api/whitelist/enable',
data: { enable: isChecked , id: currentEditingAccessRule},
@ -1165,7 +1166,7 @@
let counter = 0;
for(var i = 0; i < ccs.length; i++){
let thisCountryCode = ccs[i];
$.ajax({
$.cjax({
type: "POST",
url: "/api/whitelist/country/add",
data: { cc: thisCountryCode , id: currentEditingAccessRule},
@ -1199,7 +1200,7 @@
function removeFromWhiteList(countryCode){
if (confirm("Confirm removing " + getCountryName(countryCode) + " from whitelist?")){
countryCode = countryCode.toLowerCase();
$.ajax({
$.cjax({
url: "/api/whitelist/country/remove",
method: "POST",
data: { cc: countryCode , id: currentEditingAccessRule},
@ -1230,7 +1231,7 @@
}
}
$.ajax({
$.cjax({
url: "/api/whitelist/ip/add",
type: "POST",
data: {ip: targetIp.toLowerCase(), "comment": remarks, id: currentEditingAccessRule},
@ -1243,7 +1244,7 @@
$("#ipAddressInputWhitelist").val("");
$("#ipAddressCommentsWhitelist").val("");
$("#ipAddressInputWhitelist").parent().remvoeClass("error");
$("#ipAddressInputWhitelist").parent().removeClass("error");
},
error: function() {
alert("Failed to add IP address to whitelist");
@ -1253,7 +1254,7 @@
function removeIpWhitelist(ipaddr){
if (confirm("Confirm remove whitelist for " + ipaddr + " ?")){
$.ajax({
$.cjax({
url: "/api/whitelist/ip/remove",
type: "POST",
data: {ip: ipaddr.toLowerCase(), id: currentEditingAccessRule},

View File

@ -59,7 +59,7 @@
</div>
</div>
<p>Current list of loaded certificates</p>
<div>
<div tourstep="certTable">
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui sortable unstackable basic celled table">
<thead>
@ -79,7 +79,8 @@
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
</div>
<div class="ui divider"></div>
<h3>Fallback Certificate</h3>
<div tourstep="defaultCertificate">
<h3>Fallback Certificate</h3>
<p>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</p>
<table class="ui very basic unstackable celled table">
<thead>
@ -102,43 +103,46 @@
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
</div>
</div>
<div class="ui divider"></div>
<h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
<p>Management features regarding CA and ACME</p>
<h4>Prefered Certificate Authority</h4>
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
<div class="ui fluid form">
<div class="field">
<label>Preferred CA</label>
<div class="ui selection dropdown" id="defaultCA">
<input type="hidden" name="defaultCA">
<i class="dropdown icon"></i>
<div class="default text">Let's Encrypt</div>
<div class="menu">
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
<div tourstep="acmeSettings">
<h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
<p>Management features regarding CA and ACME</p>
<h4>Prefered Certificate Authority</h4>
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
<div class="ui fluid form">
<div class="field">
<label>Preferred CA</label>
<div class="ui selection dropdown" id="defaultCA">
<input type="hidden" name="defaultCA">
<i class="dropdown icon"></i>
<div class="default text">Let's Encrypt</div>
<div class="menu">
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
</div>
</div>
</div>
</div>
<div class="field">
<label>ACME Email</label>
<input id="prefACMEEmail" type="text" placeholder="ACME Email">
</div>
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
</div><br>
<h5>Certificate Renew / Generation (ACME) Settings</h5>
<div class="ui basic segment acmeRenewStateWrapper">
<h4 class="ui header" id="acmeAutoRenewer">
<i class="white remove icon"></i>
<div class="content">
<span id="acmeAutoRenewerStatus">Disabled</span>
<div class="sub header">ACME Auto-Renewer</div>
<div class="field">
<label>ACME Email</label>
<input id="prefACMEEmail" type="text" placeholder="ACME Email">
</div>
</h4>
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
</div><br>
<h5>Certificate Renew / Generation (ACME) Settings</h5>
<div class="ui basic segment acmeRenewStateWrapper">
<h4 class="ui header" id="acmeAutoRenewer">
<i class="white remove icon"></i>
<div class="content">
<span id="acmeAutoRenewerStatus">Disabled</span>
<div class="sub header">ACME Auto-Renewer</div>
</div>
</h4>
</div>
<p>This tool provide you a graphical interface to setup auto certificate renew on your (sub)domains. You can also manually generate a certificate if one of your domain do not have certificate.</p>
<button class="ui basic button" tourstep="openACMEManager" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
</div>
<p>This tool provide you a graphical interface to setup auto certificate renew on your (sub)domains. You can also manually generate a certificate if one of your domain do not have certificate.</p>
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
</div>
<script>
var uploadPendingPublicKey = undefined;
@ -161,6 +165,7 @@
$(btn).addClass('disabled');
$(btn).html(`<i class="ui loading spinner icon"></i>`);
}
obtainCertificate(domain, dns, defaultCA.trim(), function(succ){
if (btn != undefined){
$(btn).removeClass('disabled');
@ -256,7 +261,7 @@
//Delete the certificate by its domain
function deleteCertificate(domain){
if (confirm("Confirm delete certificate for " + domain + " ?")){
$.ajax({
$.cjax({
url: "/api/cert/delete",
method: "POST",
data: {domain: domain},
@ -315,7 +320,7 @@
return;
}
$.ajax({
$.cjax({
url: "/api/acme/autoRenew/email",
method: "POST",
data: {"set": newDefaultEmail},
@ -329,7 +334,7 @@
}
});
$.ajax({
$.cjax({
url: "/api/acme/autoRenew/ca",
data: {"set": newDefaultCA},
method: "POST",
@ -356,13 +361,16 @@
});
data.forEach(entry => {
let isExpired = entry.RemainingDays <= 0;
let entryDomainRenewKey = entry.Domain;
if (entryDomainRenewKey.includes("_.")){
entryDomainRenewKey = entryDomainRenewKey.replace("_.","*.");
}
$("#certifiedDomainList").append(`<tr>
<td>${entry.Domain}</td>
<td><a style="cursor: pointer;" title="Download certificate" onclick="handleCertDownload('${entry.Domain}');">${entry.Domain}</a></td>
<td>${entry.LastModifiedDate}</td>
<td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
<td><i class="${entry.UseDNS?"green check": "red times"} circle outline icon"></i></td>
<td><button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entry.Domain}', '${entry.UseDNS}', this);"><i class="ui green refresh icon"></i></button></td>
<td><i class="${entry.UseDNS?"green check": "red times"} icon"></i></td>
<td><button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entryDomainRenewKey}', '${entry.UseDNS}', this);"><i class="ui green refresh icon"></i></button></td>
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
</tr>`);
});
@ -397,6 +405,19 @@
initManagedDomainCertificateList();
});
}
function handleCertDownload(certName){
$.get("/api/cert/download?seek=true&certname=" + certName, function(data){
if (data.error != undefined){
//Error resolving certificate
msgbox(data.error, false);
}else{
//Continue to download
window.open("/api/cert/download?certname=" + certName);
}
});
}
//Handle domain keys upload
function handleDomainKeysUpload(callback=undefined){
let domain = $("#certdomain").val();
@ -406,6 +427,8 @@
}
if (uploadPendingPublicKey && uploadPendingPrivateKey && typeof uploadPendingPublicKey === 'object' && typeof uploadPendingPrivateKey === 'object') {
const publicKeyForm = new FormData();
const csrfToken = document.querySelector('meta[name="zoraxy.csrf.Token"]').getAttribute("content");
publicKeyForm.append('file', uploadPendingPublicKey, 'publicKey');
const privateKeyForm = new FormData();
@ -413,6 +436,7 @@
const publicKeyRequest = new XMLHttpRequest();
publicKeyRequest.open('POST', '/api/cert/upload?ktype=pub&domain=' + domain);
publicKeyRequest.setRequestHeader('X-CSRF-Token', csrfToken);
publicKeyRequest.onreadystatechange = function() {
if (publicKeyRequest.readyState === XMLHttpRequest.DONE) {
if (publicKeyRequest.status !== 200) {
@ -429,6 +453,7 @@
const privateKeyRequest = new XMLHttpRequest();
privateKeyRequest.open('POST', '/api/cert/upload?ktype=pri&domain=' + domain);
privateKeyRequest.setRequestHeader('X-CSRF-Token', csrfToken);
privateKeyRequest.onreadystatechange = function() {
if (privateKeyRequest.readyState === XMLHttpRequest.DONE) {
if (privateKeyRequest.status !== 200) {
@ -449,15 +474,11 @@
//ktype = {"pub" / "pri"}
function handleFileSelect(event, ktype="pub") {
const file = event.target.files[0];
//const fileNameInput = document.getElementById('selected-file-name');
if (ktype == "pub"){
uploadPendingPublicKey = file;
}else if (ktype == "pri"){
uploadPendingPrivateKey = file;
}
//fileNameInput.value = file.name;
}
//Check if the default keypairs exists
@ -480,14 +501,18 @@
input.addEventListener('change', () => {
// create form data object
const formData = new FormData();
const csrfToken = document.querySelector('meta[name="zoraxy.csrf.Token"]').getAttribute("content");
// add selected file to form data
formData.append('file', input.files[0]);
// send form data to server
fetch('/api/cert/upload?ktype=pri', {
method: 'POST',
body: formData
body: formData,
headers: {
'X-CSRF-Token': csrfToken
}
})
.then(response => {
initDefaultKeypairCheck();
@ -514,6 +539,7 @@
function uploadPublicKey() {
// create file input element
const input = document.createElement('input');
const csrfToken = document.querySelector('meta[name="zoraxy.csrf.Token"]').getAttribute("content");
input.type = 'file';
// add change listener to file input
@ -527,7 +553,10 @@
// send form data to server
fetch('/api/cert/upload?ktype=pub', {
method: 'POST',
body: formData
body: formData,
headers: {
'X-CSRF-Token': csrfToken
}
})
.then(response => {
if (response.ok) {

View File

@ -87,7 +87,7 @@
}
function addGANet() {
$.ajax({
$.cjax({
url: "/api/gan/network/add",
type: "POST",
dataType: "json",
@ -191,7 +191,7 @@
//Remove the given GANet
function removeGANet(netid){
if (confirm("Confirm remove Network " + netid + " PERMANENTLY ?"))
$.ajax({
$.cjax({
url: "/api/gan/network/remove",
type: "POST",
dataType: "json",

View File

@ -214,7 +214,7 @@
//Get CIDR from selected range group
var cidr = $(".iprange.active").attr("cidr");
$.ajax({
$.cjax({
url: "/api/gan/network/setRange",
metohd: "POST",
data:{
@ -240,7 +240,7 @@
if (object != undefined){
$(object).addClass("loading");
}
$.ajax({
$.cjax({
url: "/api/gan/network/name",
method: "POST",
data: {
@ -287,7 +287,7 @@
//Handle delete IP from memeber
function deleteIpFromMemeber(memberid, ip){
$.ajax({
$.cjax({
url: "/api/gan/members/ip",
metohd: "POST",
data: {
@ -334,7 +334,7 @@
return
}
$.ajax({
$.cjax({
url: "/api/gan/members/ip",
metohd: "POST",
data: {
@ -461,7 +461,7 @@
$(".memberName").each(function(){
let addr = $(this).attr("addr");
let targetDOM = $(this);
$.ajax({
$.cjax({
url: "/api/gan/members/name",
method: "POST",
data: {
@ -487,7 +487,7 @@
let newname = prompt("Enter a easy manageable name for " + targetMemberAddr, "");
if (newname != null && newname.trim() != "") {
$.ajax({
$.cjax({
url: "/api/gan/members/name",
method: "POST",
data: {
@ -553,7 +553,7 @@
function handleMemberAuth(object){
let targetMemberAddr = $(object).attr("addr");
let isAuthed = object.checked;
$.ajax({
$.cjax({
url: "/api/gan/members/authorize",
method: "POST",
data: {
@ -580,7 +580,7 @@
function handleMemberDelete(addr){
if (confirm("Confirm delete member " + addr + " ?")){
$.ajax({
$.cjax({
url: "/api/gan/members/delete",
method: "POST",
data: {
@ -605,7 +605,7 @@
$(".addControllerToNetworkBtn").addClass("disabled");
$(".addControllerToNetworkBtn").addClass("loading");
$.ajax({
$.cjax({
url: "/api/gan/network/join",
method: "POST",
data: {
@ -630,7 +630,7 @@
$(".removeControllerFromNetworkBtn").addClass("disabled");
$(".removeControllerFromNetworkBtn").addClass("loading");
$.ajax({
$.cjax({
url: "/api/gan/network/leave",
method: "POST",
data: {

View File

@ -51,13 +51,29 @@
//Sort by RootOrMatchingDomain field
data.sort((a,b) => (a.RootOrMatchingDomain > b.RootOrMatchingDomain) ? 1 : ((b.RootOrMatchingDomain > a.RootOrMatchingDomain) ? -1 : 0))
data.forEach(subd => {
let tlsIcon = "";
let subdData = encodeURIComponent(JSON.stringify(subd));
if (subd.RequireTLS){
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
if (subd.SkipCertValidations){
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
}
//Build the upstream list
let upstreams = "";
if (subd.ActiveOrigins.length == 0){
//Invalid config
upstreams = `<i class="ui yellow exclamation triangle icon"></i> No Active Upstream Origin<br>`;
}else{
subd.ActiveOrigins.forEach(upstream => {
console.log(upstream);
//Check if the upstreams require TLS connections
let tlsIcon = "";
if (upstream.RequireTLS){
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
if (upstream.SkipCertValidations){
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
}
}
let upstreamLink = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`;
upstreams += `<a href="${upstreamLink}" target="_blank">${upstream.OriginIpOrDomain} ${tlsIcon}</a><br>`;
})
}
let inboundTlsIcon = "";
@ -102,7 +118,11 @@
${aliasDomains}
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
</td>
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="domain">
<div class="upstreamList">
${upstreams}
</div>
</td>
<td data-label="" editable="true" datatype="vdir">${vdList}</td>
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
${subd.RequireBasicAuth?`<i class="ui green check icon"></i> Basic Auth`:``}
@ -214,7 +234,6 @@
var payload = $(row).attr("payload");
payload = JSON.parse(decodeURIComponent(payload));
console.log(payload);
//console.log(payload);
columns.each(function(index) {
var column = $(this);
var oldValue = column.text().trim();
@ -228,39 +247,21 @@
var input;
var datatype = $(this).attr("datatype");
if (datatype == "domain"){
let domain = payload.Domain;
//Target require TLS for proxying
let tls = payload.RequireTLS;
if (tls){
tls = "checked";
}else{
tls = "";
let useStickySessionChecked = "";
if (payload.UseStickySession){
useStickySessionChecked = "checked";
}
input = `<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 1em;" onclick="editUpstreams('${uuid}');"><i class="grey server icon"></i> Edit Upstreams</button>
<div class="ui divider"></div>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="UseStickySession" ${useStickySessionChecked}>
<label>Use Sticky Session<br>
<small>Enable stick session on load balancing</small></label>
</div>
//Require TLS validation
let skipTLSValidation = payload.SkipCertValidations;
let checkstate = "";
if (skipTLSValidation){
checkstate = "checked";
}
input = `
<div class="ui mini fluid input">
<input type="text" class="Domain" onchange="cleanProxyTargetValue(this)" value="${domain}">
</div>
<div class="ui checkbox" style="margin-top: 0.6em;">
<input type="checkbox" class="RequireTLS" ${tls}>
<label>Require TLS<br>
<small>Proxy target require HTTPS connection</small></label>
</div><br>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
<label>Skip Verification<br>
<small>Check this if proxy target is using self signed certificates</small></label>
</div><br>
<!-- <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editLoadBalanceOptions('${uuid}');"><i class="purple server icon"></i> Load Balance</button> -->
`;
column.empty().append(input);
column.append(input);
$(column).find(".upstreamList").addClass("editing");
}else if (datatype == "vdir"){
//Append a quick access button for vdir page
column.append(`<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="quickEditVdir('${uuid}');">
@ -311,12 +312,6 @@
Security Options
</div>
<div class="content">
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="SkipWebSocketOriginCheck" ${wsCheckstate}>
<label>Skip WebSocket Origin Check<br>
<small>Check this to allow cross-origin websocket requests</small></label>
</div>
<br>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" onchange="handleToggleRateLimitInput();" class="RequireRateLimit" ${rateLimitCheckState}>
<label>Require Rate Limit<br>
@ -353,6 +348,20 @@
`);
}else if (datatype == "inbound"){
let originalContent = $(column).html();
//Check if this host is covered within one of the certificates. If not, show the icon
let domainIsCovered = true;
let domains = [payload.RootOrMatchingDomain]; //Domain for getting certificate if needed
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
let thisAliasName = payload.MatchingDomainAlias[i];
domains.push(thisAliasName);
}
if (true){
domainIsCovered = false;
}
//encode the domain to DOM
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
column.empty().append(`${originalContent}
<div class="ui divider"></div>
<div class="ui checkbox" style="margin-top: 0.4em;">
@ -360,10 +369,11 @@
<label>Allow plain HTTP access<br>
<small>Allow inbound connections without TLS/SSL</small></label>
</div><br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
<button class="ui basic compact tiny ${domainIsCovered?"disabled":""} button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="requestCertificateForExistingHost('${uuid}', '${certificateDomains}');"><i class="green lock icon"></i> Get Certificate</button>
`);
$(".hostAccessRuleSelector").dropdown();
}else{
@ -399,27 +409,20 @@
}
var epttype = "host";
let newDomain = $(row).find(".Domain").val();
let requireTLS = $(row).find(".RequireTLS")[0].checked;
let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
let useStickySession = $(row).find(".UseStickySession")[0].checked;
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked;
let rateLimit = $(row).find(".RateLimit").val();
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
let bypassWebsocketOrigin = $(row).find(".SkipWebSocketOriginCheck")[0].checked;
console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
$.ajax({
$.cjax({
url: "/api/proxy/edit",
method: "POST",
data: {
"type": epttype,
"rootname": uuid,
"ep":newDomain,
"ss":useStickySession,
"bpgtls": bypassGlobalTLS,
"tls" :requireTLS,
"tlsval": skipCertValidations,
"bpwsorg" : bypassWebsocketOrigin,
"bauth" :requireBasicAuth,
"rate" :requireRateLimit,
"ratenum" :rateLimit,
@ -435,20 +438,27 @@
})
}
//Clearn the proxy target value, make sure user do not enter http:// or https://
//and auto select TLS checkbox if https:// exists
function cleanProxyTargetValue(input){
let targetDomain = $(input).val().trim();
if (targetDomain.startsWith("http://")){
targetDomain = targetDomain.substr(7);
$(input).val(targetDomain);
$("#httpProxyList input.RequireTLS").parent().checkbox("set unchecked");
}else if (targetDomain.startsWith("https://")){
targetDomain = targetDomain.substr(8);
$(input).val(targetDomain);
$("#httpProxyList input.RequireTLS").parent().checkbox("set checked");
//Generic functions for delete rp endpoints
function deleteEndpoint(epoint){
epoint = decodeURIComponent(epoint).hexDecode();
if (confirm("Confirm remove proxy for :" + epoint + "?")){
$.cjax({
url: "/api/proxy/del",
method: "POST",
data: {ep: epoint},
success: function(data){
if (data.error == undefined){
listProxyEndpoints();
msgbox("Proxy Rule Deleted", true);
reloadUptimeList();
}else{
msgbox(data.error, false);
}
}
})
}
}
/* button events */
function editBasicAuthCredentials(uuid){
@ -490,18 +500,18 @@
}
//Open the load balance option
function editLoadBalanceOptions(uuid){
function editUpstreams(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/loadBalancer.html?t=" + Date.now() + "#" + payload);
showSideWrapper("snippet/upstreams.html?t=" + Date.now() + "#" + payload);
}
function handleProxyRuleToggle(object){
let endpointUUID = $(object).attr("eptuuid");
let isChecked = object.checked;
$.ajax({
$.cjax({
url: "/api/proxy/toggle",
data: {
"ep": endpointUUID,
@ -522,6 +532,15 @@
})
}
/*
Certificate Shortcut
*/
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains){
RootAndAliasDomains = JSON.parse(decodeURIComponent(RootAndAliasDomains))
alert(RootAndAliasDomains.join(", "))
}
//Bind on tab switch events
tabSwitchEventBind["httprp"] = function(){
listProxyEndpoints();

View File

@ -339,7 +339,7 @@ function setWoLAddress() {
$("#wol_mac").parent().removeClass("error");
}
$.ajax({
$.cjax({
url: wake_on_lan_API,
type: "POST",
data: {
@ -363,7 +363,7 @@ function setWoLAddress() {
function delWoLAddr(mac, name) {
if (confirm(`Confirm remove WoL record for ${name} (${mac}) ?`)){
$.ajax({
$.cjax({
url: wake_on_lan_API,
type: "POST",
data: {
@ -385,7 +385,7 @@ function wakeWoL(mac, object=undefined) {
if (object != undefined){
$(object).addClass("loading").addClass("disabled");
}
$.ajax({
$.cjax({
url: wake_on_lan_API,
type: "POST",
data: {
@ -594,7 +594,7 @@ function initForwardProxyInfo(){
initForwardProxyInfo();
function toggleForwadProxy(enabled){
$.ajax({
$.cjax({
url: "/api/tools/fwdproxy/enable",
method: "POST",
data: {
@ -620,7 +620,7 @@ function updateForwardProxyPort(){
$("#newPortNumber").parent().removeClass('error');
}
$.ajax({
$.cjax({
url: "/api/tools/fwdproxy/port",
method: "POST",
data: {

View File

@ -0,0 +1,77 @@
<div id="quickstart" class="standardContainer">
<div class="ui container">
<h1 class="ui header">
<img src="img/res/1F44B.png">
<div class="content" style="font-weight: lighter;">
Welcome to Zoraxy!
<div class="sub header">What services are you planning to setup today?</div>
</div>
</h1>
<br>
<div class="ui stackable equal width grid">
<div class="column">
<div class="serviceOption homepage" name="homepage">
<div class="titleWrapper">
<p>Basic Homepage</p>
</div>
<div class="ui divider"></div>
<p>Host a static homepage with Zoraxy and point your domain name to your web server.</p>
<img class="themebackground ui small image" src="img/res/1F310.png">
<div class="activeOption">
<i class="ui white huge circle check icon"></i>
</div>
</div>
</div>
<div class="column">
<div class="serviceOption subdomain" name="subdomain">
<div class="titleWrapper">
<p>Sub-domains Routing</p>
</div>
<div class="ui divider"></div>
<p>Add and handle traffic from your subdomains and point them to a dedicated web services somewhere else.</p>
<img class="themebackground ui small image" src="img/res/1F500.png">
<div class="activeOption">
<i class="ui white huge circle check icon"></i>
</div>
</div>
</div>
<div class="column">
<div class="serviceOption tls" name="tls">
<div class="titleWrapper">
<p>HTTPS Green Lock(s)</p>
</div>
<div class="ui divider"></div>
<p>Turn your unsafe HTTP website into HTTPS using free certificate from public certificate authorities organizations.</p>
<img class="themebackground ui small image" src="img/res/1F512.png">
<div class="activeOption">
<i class="ui white huge circle check icon"></i>
</div>
</div>
</div>
</div>
<div class="ui divider"></div>
<div style="width: 100%;" align="center">
<button onclick="startQuickStartTour();" class="ui finished button quickstartControlButton">
Start Walkthrough
</button>
</div>
</div>
</div>
<script>
var currentQuickSetupClass = "";
var currentQuickSetupTourStep = 0;
//For tour logic, see quicksetup.js
//Bind selecting events to serviceOption
$("#quickstart .serviceOption").on("click", function(data){
$(".serviceOption.active").removeClass("active");
$(this).addClass("active");
let tourType = $(this).attr("name");
currentQuickSetupClass = tourType;
});
</script>
<script src="script/quicksetup.js"></script>

View File

@ -116,7 +116,7 @@
let forwardChildpath = document.querySelector('input[name="forward-childpath"]').checked;
let redirectType = document.querySelector('input[name="redirect-type"]:checked').value;
$.ajax({
$.cjax({
url: "/api/redirect/add",
method: "POST",
data: {
@ -141,7 +141,7 @@
let targetURL = $(obj).attr("rurl");
targetURL = JSON.parse(decodeURIComponent(targetURL));
if (confirm("Confirm remove redirection from " + targetURL + " ?")){
$.ajax({
$.cjax({
url: "/api/redirect/delete",
method: "POST",
data: {
@ -191,8 +191,9 @@
//Bind event to the checkbox
$("#redirectRegex").on("change", function(){
$.ajax({
$.cjax({
url: "/api/redirect/regex",
method: "POST",
data: {"enable": $(this)[0].checked},
success: function(data){
if (data.error != undefined){

View File

@ -122,7 +122,7 @@
function initRootInfo(callback=undefined){
$.get("/api/proxy/list?type=root", function(data){
if (data == null){
msgbox("Default site load failed", false);
}else{
var $radios = $('input:radio[name=defaultsiteOption]');
let proxyType = data.DefaultSiteOption;
@ -140,8 +140,8 @@
}
updateAvaibleDefaultSiteOptions();
$("#proxyRoot").val(data.Domain);
checkRootRequireTLS(data.Domain);
$("#proxyRoot").val(data.ActiveOrigins[0].OriginIpOrDomain);
checkRootRequireTLS(data.ActiveOrigins[0].OriginIpOrDomain);
}
if (callback != undefined){
@ -181,8 +181,9 @@
targetDomain = targetDomain.substring(8);
$("#proxyRoot").val(targetDomain);
}
$.ajax({
$.cjax({
url: "/api/proxy/tlscheck",
method: "POST",
data: {url: targetDomain},
success: function(data){
if (data.error != undefined){
@ -232,7 +233,7 @@
}
//Create the endpoint by calling add
$.ajax({
$.cjax({
url: "/api/proxy/add",
data: {
"type": "root",
@ -247,7 +248,9 @@
msgbox(data.error, false, 5000);
}else{
//OK
initRootInfo(function(){
//Check if WebServ is enabled
isUsingStaticWebServerAsRoot(function(isUsingWebServ){
if (isUsingWebServ){
@ -256,11 +259,7 @@
setWebServerRunningState(true);
}
setTimeout(function(){
//Update the checkbox
msgbox("Default Site Updated");
}, 100);
msgbox("Default Site Updated");
})
});
@ -269,6 +268,9 @@
if (btn != undefined){
$(btn).removeClass("disabled");
}
},
error: function(){
msgbox("Unknown error occured", false);
}
});

View File

@ -11,6 +11,17 @@
border-radius: 0.6em;
padding: 1em;
}
.descheader{
display:none !important;
}
@media (min-width: 1367px) {
.descheader{
display:auto !important;
}
}
</style>
<div class="standardContainer">
<div class="ui stackable grid">
@ -19,20 +30,20 @@
<h2>New Proxy Rule</h2>
<p>You can add more proxy rules to support more site via domain / subdomains</p>
<div class="ui form">
<div class="field">
<div class="field" tourstep="matchingkeyword">
<label>Matching Keyword / Domain</label>
<input type="text" id="rootname" placeholder="mydomain.com">
<small>Support subdomain and wildcard, e.g. s1.mydomain.com or *.test.mydomain.com. Use comma (,) for alias hostnames. </small>
</div>
<div class="field">
<div class="field" tourstep="targetdomain">
<label>Target IP Address or Domain Name with port</label>
<input type="text" id="proxyDomain" onchange="autoCheckTls(this.value);">
<small>E.g. 192.168.0.101:8000 or example.com</small>
<input type="text" id="proxyDomain" onchange="autoFillTargetTLS(this);">
<small>e.g. 192.168.0.101:8000 or example.com</small>
</div>
<div class="field dockerOptimizations" style="display:none;">
<button style="margin-top: -2em;" class="ui basic small button" onclick="openDockerContainersList();"><i class="blue docker icon"></i> Pick from Docker Containers</button>
</div>
<div class="field">
<div class="field" tourstep="requireTLS">
<div class="ui checkbox">
<input type="checkbox" id="reqTls">
<label>Proxy Target require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
@ -47,18 +58,16 @@
</div>
<div class="content">
<div class="field">
<label>Access Rule</label>
<div class="ui selection dropdown">
<input type="hidden" id="newProxyRuleAccessFilter" value="default">
<i class="dropdown icon"></i>
<div class="default text">Default</div>
<div class="menu" id="newProxyRuleAccessList">
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
</div>
<div class="ui checkbox">
<input type="checkbox" id="useStickySessionLB">
<label>Sticky Session<br><small>Enable stick session on upstream load balancing</small></label>
</div>
<small>Allow regional access control using blacklist or whitelist. Use "default" for "allow all".</small>
</div>
<div class="field">
<div class="ui horizontal divider">
<i class="ui green lock icon"></i>
Security
</div>
<div class="field" tourstep="skipTLSValidation">
<div class="ui checkbox">
<input type="checkbox" id="skipTLSValidation">
<label>Ignore TLS/SSL Verification Error<br><small>For targets that is using self-signed, expired certificate (Not Recommended)</small></label>
@ -76,21 +85,21 @@
<label>Allow plain HTTP access<br><small>Allow this subdomain to be connected without TLS (Require HTTP server enabled on port 80)</small></label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="requireRateLimit">
<label>Require Rate Limit<br><small>This proxy endpoint will be rate limited.</small></label>
</div>
<div class="ui horizontal divider">
<i class="ui red ban icon"></i>
Access Control
</div>
<div class="field">
<label>Rate Limit</label>
<div class="ui fluid right labeled input">
<input type="number" id="proxyRateLimit" placeholder="100" min="1" max="1000" value="100">
<div class="ui basic label">
req / sec / IP
<label>Access Rule</label>
<div class="ui selection dropdown">
<input type="hidden" id="newProxyRuleAccessFilter" value="default">
<i class="dropdown icon"></i>
<div class="default text">Default</div>
<div class="menu" id="newProxyRuleAccessList">
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
</div>
</div>
<small>Return a 429 error code if request rate exceed the rate limit.</small>
<small>Allow regional access control using blacklist or whitelist. Use "default" for "allow all".</small>
</div>
<div class="field">
<div class="ui checkbox">
@ -125,11 +134,29 @@
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="requireRateLimit">
<label>Require Rate Limit<br><small>This proxy endpoint will be rate limited.</small></label>
</div>
</div>
<div class="field">
<label>Rate Limit</label>
<div class="ui fluid right labeled input">
<input type="number" id="proxyRateLimit" placeholder="100" min="1" max="1000" value="100">
<div class="ui basic label">
req / sec / IP
</div>
</div>
<small>Return a 429 error code if request rate exceed the rate limit.</small>
</div>
</div>
</div>
</div>
<br>
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="green add icon"></i> Create Endpoint</button>
<div tourstep="newProxyRule" style="display: inline-block;">
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="green add icon"></i> Create Endpoint</button>
</div>
<br><br>
</div>
</div>
@ -160,17 +187,18 @@
//New Proxy Endpoint
function newProxyEndpoint(){
var rootname = $("#rootname").val();
var proxyDomain = $("#proxyDomain").val();
var useTLS = $("#reqTls")[0].checked;
var skipTLSValidation = $("#skipTLSValidation")[0].checked;
var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
var requireBasicAuth = $("#requireBasicAuth")[0].checked;
var proxyRateLimit = $("#proxyRateLimit").val();
var requireRateLimit = $("#requireRateLimit")[0].checked;
var skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked;
var accessRuleToUse = $("#newProxyRuleAccessFilter").val();
let rootname = $("#rootname").val();
let proxyDomain = $("#proxyDomain").val();
let useTLS = $("#reqTls")[0].checked;
let skipTLSValidation = $("#skipTLSValidation")[0].checked;
let bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
let requireBasicAuth = $("#requireBasicAuth")[0].checked;
let proxyRateLimit = $("#proxyRateLimit").val();
let requireRateLimit = $("#requireRateLimit")[0].checked;
let skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked;
let accessRuleToUse = $("#newProxyRuleAccessFilter").val();
let useStickySessionLB = $("#useStickySessionLB")[0].checked;
if (rootname.trim() == ""){
$("#rootname").parent().addClass("error");
return
@ -186,8 +214,9 @@
}
//Create the endpoint by calling add
$.ajax({
$.cjax({
url: "/api/proxy/add",
method: "POST",
data: {
type: "host",
rootname: rootname,
@ -201,6 +230,7 @@
ratenum: proxyRateLimit,
cred: JSON.stringify(credentials),
access: accessRuleToUse,
stickysess: useStickySessionLB,
},
success: function(data){
if (data.error != undefined){
@ -243,30 +273,33 @@
}
//Generic functions for delete rp endpoints
function deleteEndpoint(epoint){
epoint = decodeURIComponent(epoint).hexDecode();
if (confirm("Confirm remove proxy for :" + epoint + "?")){
$.ajax({
url: "/api/proxy/del",
data: {ep: epoint, },
success: function(){
listProxyEndpoints();
msgbox("Proxy Rule Deleted", true);
reloadUptimeList();
}
})
//Clearn the proxy target value, make sure user do not enter http:// or https://
//and auto select TLS checkbox if https:// exists
function autoFillTargetTLS(input){
let targetDomain = $(input).val().trim();
if (targetDomain.startsWith("http://")){
targetDomain = targetDomain.substr(7);
$(input).val(targetDomain);
$("#reqTls").parent().checkbox("set unchecked");
}else if (targetDomain.startsWith("https://")){
targetDomain = targetDomain.substr(8);
$(input).val(targetDomain);
$("#reqTls").parent().checkbox("set checked");
}else{
//No http or https was given. Sniff it
autoCheckTls(targetDomain);
}
}
//Automatic check if the site require TLS and check the checkbox if needed
function autoCheckTls(targetDomain){
$.ajax({
$.cjax({
url: "/api/proxy/tlscheck",
data: {url: targetDomain},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else if (data == "https"){
$("#reqTls").parent().checkbox("set checked");
}else if (data == "http"){
@ -453,7 +486,25 @@
}
/* UI Element Initialization */
$("#advanceProxyRules").accordion();
$("#newProxyRuleAccessFilter").parent().dropdown();
function initAdvanceSettingsAccordion(){
function hasClickEvent(element) {
var events = $._data(element, "events");
return events && events.click && events.click.length > 0;
}
if (!hasClickEvent($("#advanceProxyRules"))){
// Not sure why sometime the accordion events are not binding
// to the DOM element. This makes sure the element is binded
// correctly by checking it again after 300ms
$("#advanceProxyRules").accordion();
$("#newProxyRuleAccessFilter").parent().dropdown();
setTimeout(function(){
initAdvanceSettingsAccordion();
}, 300);
}
}
initAdvanceSettingsAccordion();
</script>

View File

@ -1,7 +1,7 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Service Expose Proxy</h2>
<p>Expose your local test-site on the internet with single command</p>
<h2>Single-Sign-On</h2>
<p>Create and manage accounts with Zoraxy!</p>
</div>
<div class="ui message">
<h4>Work In Progress</h4>

View File

@ -53,8 +53,10 @@
</div>
<div class="standardContainer" style="padding-bottom: 0 !important;">
<!-- Power Buttons-->
<button id="startbtn" class="ui basic button" onclick="startService();"><i class="ui green arrow alternate circle up icon"></i> Start Service</button>
<button id="stopbtn" class="ui basic notloopbackOnly disabled button" onclick="stopService();"><i class="ui red minus circle icon"></i> Stop Service</button>
<div class="poweroptions" style="display:inline-block;">
<button id="startbtn" class="ui basic button" onclick="startService();"><i class="ui green arrow alternate circle up icon"></i> Start Service</button>
<button id="stopbtn" class="ui basic notloopbackOnly disabled button" onclick="stopService();"><i class="ui red minus circle icon"></i> Stop Service</button>
</div>
<div class="ui divider"></div>
<h4>Network Status</h4>
<p>Overall Network I/O in Current Host Server</p>
@ -69,7 +71,7 @@
<div class="ui divider"></div>
<h4>Global Settings</h4>
<p>Inbound Port (Reverse Proxy Listening Port)</p>
<div class="ui action fluid notloopbackOnly input">
<div class="ui action fluid notloopbackOnly input" tourstep="incomingPort">
<small id="applyButtonReminder">Click "Apply" button to confirm listening port changes</small>
<input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
<button class="ui green notloopbackOnly button" style="background: linear-gradient(60deg, #27e7ff, #00ca52);" onclick="handlePortChange();"><i class="ui checkmark icon"></i> Apply</button>
@ -86,9 +88,11 @@
<small>(Only apply when TLS enabled and not using port 80)</small></label>
</div>
<br>
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em; padding-left: 2em;">
<input type="checkbox">
<label>Force redirect HTTP request to HTTPS</label>
<div tourstep="forceHttpsRedirect" style="display: inline-block;">
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em; padding-left: 2em;">
<input type="checkbox">
<label>Force redirect HTTP request to HTTPS</label>
</div>
</div>
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui accordion advanceSettings">
@ -315,26 +319,39 @@
//Start and stop service button
function startService(){
$.post("/api/proxy/enable", {enable: true}, function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
$.cjax({
url: "/api/proxy/enable",
method: "POST",
data: {enable: true},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}
initRPStaste();
}
initRPStaste();
});
}
function stopService(){
$.post("/api/proxy/enable", {enable: false}, function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
$.cjax({
url: "/api/proxy/enable",
method: "POST",
data: {enable: false},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}
initRPStaste();
}
initRPStaste();
});
}
function handleP80ListenerStateChange(enabled){
$.ajax({
$.cjax({
url: "/api/proxy/listenPort80",
method: "POST",
data: {"enable": enabled},
success: function(data){
if (data.error != undefined){
@ -361,16 +378,21 @@
return;
}
$.post("/api/proxy/setIncoming", {incoming: newPortValue}, function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
return;
}
msgbox("Listening Port Updated");
initRPStaste();
$.cjax({
url: "/api/proxy/setIncoming",
method: "POST",
data: {incoming: newPortValue},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
return;
}
msgbox("Listening Port Updated");
initRPStaste();
//Hide the reminder text
$("#applyButtonReminder").hide();
//Hide the reminder text
$("#applyButtonReminder").hide();
}
});
}
@ -402,8 +424,9 @@
//Initiate the input listener on the checkbox
$("#redirect").find("input").on("change", function(){
let thisValue = $("#redirect").checkbox("is checked");
$.ajax({
$.cjax({
url: "/api/proxy/useHttpsRedirect",
method: "POST",
data: {set: thisValue},
success: function(data){
if (data.error != undefined){
@ -440,9 +463,10 @@
//Bind events to the checkbox
$("#tlsMinVer").find("input").on("change", function(){
let thisValue = $("#tlsMinVer").checkbox("is checked");
$.ajax({
$.cjax({
url: "/api/cert/tlsRequireLatest",
data: {"set": thisValue},
method: "POST",
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
@ -498,15 +522,15 @@
}else{
$(".tlsEnabledOnly").addClass('disabled');
}
$.ajax({
$.cjax({
url: "/api/cert/tls",
method: "POST",
data: {set: thisValue},
success: function(data){
if (data.error != undefined){
alert(data.error);
msgbox(data.error, false);
}else{
//Updated
//Check for case if the port is invalid default ports
if ($("#incomingPort").val() == "80" && thisValue == true){
confirmBox("Change listen port to :443?", function(choice){
@ -563,14 +587,14 @@
url: '/api/stats/netstatgraph?array=true',
success: function(data){
if (rxValues.length == 0){
rxValues = JSON.parse(JSON.stringify(data.Rx));
rxValues.push(...data.Rx);
}else{
rxValues.push(data.Rx[dataCount-1]);
rxValues.shift();
}
if (txValues.length == 0){
txValues = JSON.parse(JSON.stringify(data.Tx));
txValues.push(...data.Tx);
}else{
txValues.push(data.Tx[dataCount-1]);
txValues.shift();

View File

@ -100,7 +100,7 @@
}
// Send the AJAX POST request
$.ajax({
$.cjax({
type: 'POST',
url: '/api/streamprox/config/add',
data: form.serialize(),
@ -285,7 +285,7 @@
}
// Send the AJAX POST request
$.ajax({
$.cjax({
type: 'POST',
url: '/api/streamprox/config/edit',
method: "POST",
@ -316,7 +316,7 @@
}
function deleteTCPProxyConfig(configUUID){
$.ajax({
$.cjax({
url: "/api/streamprox/config/delete",
method: "POST",
data: {uuid: configUUID},
@ -333,7 +333,7 @@
//Start a TCP proxy by their config UUID
function startStreamProx(configUUID){
$.ajax({
$.cjax({
url: "/api/streamprox/config/start",
method: "POST",
data: {uuid: configUUID},
@ -351,7 +351,7 @@
//Stop a TCP proxy by their config UUID
function stopStreamProx(configUUID){
$.ajax({
$.cjax({
url: "/api/streamprox/config/stop",
method: "POST",
data: {uuid: configUUID},

View File

@ -120,7 +120,12 @@
<!-- Config Tools -->
<h3>System Backup & Restore</h3>
<p>Options related to system backup, migrate and restore.</p>
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');"><i class="ui green undo icon icon"></i> Open Config Tools</button>
<div class="ui divider"></div>
<!-- Log Viewer -->
<h3>System Log Viewer</h3>
<p>View and download Zoraxy log</p>
<button class="ui basic button" onclick="launchToolWithSize('snippet/logview.html', 1024, 768);"><i class="ui blue file icon"></i> Open Log Viewer</button>
<div class="ui divider"></div>
<!-- System Information -->
<div id="zoraxyinfo">
@ -228,7 +233,7 @@
const newPassword = document.getElementsByName('newPassword')[0].value;
const confirmNewPassword = document.getElementsByName('confirmNewPassword')[0].value;
$.ajax({
$.cjax({
type: "POST",
url: "/api/auth/changePassword",
data: {
@ -274,7 +279,7 @@
return;
}
$.ajax({
$.cjax({
type: "POST",
url: "/api/tools/smtp/set",
data: data,

View File

@ -190,7 +190,7 @@
function updateVDTargetTLSState(){
var targetDomain = $("#virtualDirectoryDomain").val().trim();
if (targetDomain != ""){
$.ajax({
$.cjax({
url: "/api/proxy/tlscheck",
data: {url: targetDomain},
success: function(data){
@ -252,7 +252,7 @@
}
//Create a virtual directory endpoint
$.ajax({
$.cjax({
url: "/api/proxy/vdir/add",
method: "POST",
data: {
@ -295,7 +295,7 @@
epType = "root";
path = "";
}
$.ajax({
$.cjax({
url: "/api/proxy/vdir/del",
method: "POST",
data: {
@ -384,7 +384,7 @@
//console.log(mathingPath, newDomain, requireTLS, skipValidation);
$.ajax({
$.cjax({
url: "/api/proxy/vdir/edit",
method: "POST",
data: {

View File

@ -13,34 +13,35 @@
</div>
</h4>
</div>
<h3>Web Server Settings</h3>
<div class="ui form">
<div class="inline field">
<div class="ui toggle checkbox webservRootDisabled">
<input id="webserv_enable" type="checkbox" class="hidden">
<label>Enable Static Web Server</label>
<div>
<h3>Web Server Settings</h3>
<div class="ui form">
<div class="inline field">
<div class="ui toggle checkbox webservRootDisabled">
<input id="webserv_enable" type="checkbox" class="hidden">
<label>Enable Static Web Server</label>
</div>
</div>
</div>
<div class="inline field">
<div class="ui toggle checkbox">
<input id="webserv_enableDirList" type="checkbox" class="hidden">
<label>Enable Directory Listing</label>
<small>If this folder do not contains any index files, list the directory of this folder.</small>
<div class="inline field">
<div class="ui toggle checkbox">
<input id="webserv_enableDirList" type="checkbox" class="hidden">
<label>Enable Directory Listing</label>
<small>If this folder do not contains any index files, list the directory of this folder.</small>
</div>
</div>
<div class="field">
<label>Document Root Folder</label>
<input id="webserv_docRoot" type="text" readonly="true">
<small>
The web server root folder can only be changed via startup flags of zoraxy for security reasons.
See the -webserv flag for more details.
</small>
</div>
<div class="field webservRootDisabled">
<label>Port Number</label>
<input id="webserv_listenPort" type="number" step="1" min="0" max="65535" value="8081" onchange="updateWebServLinkExample(this.value);">
<small>Use <code>http://127.0.0.1:<span class="webserv_port">8081</span></code> in proxy rules to access the web server</small>
</div>
</div>
<div class="field">
<label>Document Root Folder</label>
<input id="webserv_docRoot" type="text" readonly="true">
<small>
The web server root folder can only be changed via startup flags of zoraxy for security reasons.
See the -webserv flag for more details.
</small>
</div>
<div class="field webservRootDisabled">
<label>Port Number</label>
<input id="webserv_listenPort" type="number" step="1" min="0" max="65535" value="8081" onchange="updateWebServLinkExample(this.value);">
<small>Use <code>http://127.0.0.1:<span class="webserv_port">8081</span></code> in proxy rules to access the web server</small>
</div>
</div>
<small><i class="ui blue save icon"></i> Changes are saved automatically</small>
@ -164,7 +165,7 @@
$("#webserv_enableDirList").off("change").on("change", function(){
let enable = $(this)[0].checked;
$.ajax({
$.cjax({
url: "/api/webserv/setDirList",
method: "POST",
data: {"enable": enable},
@ -186,7 +187,7 @@
confirmBox("This setting might cause port conflict. Continue Anyway?", function(choice){
if (choice == true){
//Continue anyway
$.ajax({
$.cjax({
url: "/api/webserv/setPort",
method: "POST",
data: {"port": newPort},
@ -206,7 +207,7 @@
}
});
}else{
$.ajax({
$.cjax({
url: "/api/webserv/setPort",
method: "POST",
data: {"port": newPort},

BIN
src/web/img/res/1F310.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/web/img/res/1F387.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/web/img/res/1F38A.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
src/web/img/res/1F44B.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/web/img/res/1F500.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
src/web/img/res/1F512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/web/img/res/1F914.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/web/img/res/2728.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
src/web/img/res/2753.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
src/web/img/res/E25E.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

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