mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-29 10:51:45 +02:00
Compare commits
98 Commits
Author | SHA1 | Date | |
---|---|---|---|
2e9bc77a5d | |||
ed178d857a | |||
e79a70b7ac | |||
779115d06b | |||
9cb315ea67 | |||
43ba00ec8d | |||
4577fb1f2f | |||
f877bf9eda | |||
363b9b6d94 | |||
c5ca68868b | |||
f927bb539a | |||
5f64b622b5 | |||
9a371f5bcb | |||
172c5afa60 | |||
f98e04a9fc | |||
99295cad86 | |||
95d0a98576 | |||
00bfa262cb | |||
528be69fe0 | |||
6923f0d200 | |||
7255b62e31 | |||
cf14d12c31 | |||
90cf26306a | |||
cab2f4e63a | |||
75d773887c | |||
a944c3ff36 | |||
465f332dfc | |||
dfda3fe94b | |||
5c56da1180 | |||
3392013a5c | |||
8b4c601d50 | |||
3a2eaf8766 | |||
a45092a449 | |||
d5315e5b8e | |||
31cc1a69a1 | |||
d348cbf48b | |||
f6339868ac | |||
af10f2a644 | |||
3b247c31da | |||
d74e8badb9 | |||
b40131d212 | |||
563a12c860 | |||
8b2c3b7e03 | |||
608cc0c523 | |||
b558bcbfcf | |||
9ea3fa2542 | |||
01f68c5ef5 | |||
a7f89086d4 | |||
a5ef6456c6 | |||
87659b43bd | |||
ddbecf7b68 | |||
1b3a9de378 | |||
6dd62f509d | |||
d5cc6a6859 | |||
1d965da7d0 | |||
3567c70bab | |||
0a734e0bd3 | |||
f4fa92635c | |||
7d5151bb00 | |||
54475e4b99 | |||
6ac16caf37 | |||
97502db607 | |||
0747cf4b0f | |||
94483acc92 | |||
7626857c02 | |||
0f772a715b | |||
fd1439f746 | |||
ca37bfbfa6 | |||
c1e16d55ab | |||
f595da92a1 | |||
8a8ec1cb0b | |||
e53c3cf3c4 | |||
d17de5c200 | |||
97ff48ee70 | |||
d64b1174af | |||
bec363abab | |||
0dddd1f9e3 | |||
6bfcb2e1f5 | |||
02ff288280 | |||
b1c5bc2963 | |||
d3dbbf9052 | |||
f4a5c905e7 | |||
245379e91f | |||
955a2232df | |||
7eb7ae7ced | |||
3aa0f2d914 | |||
39b0c8c674 | |||
bddeae8365 | |||
8e0e9531e7 | |||
6ff22865e0 | |||
0828fd1958 | |||
82f84470f7 | |||
cf9a05f130 | |||
301072db90 | |||
cfcd10d64f | |||
c85760c73a | |||
b7bb918aa3 | |||
962f3e0566 |
43
.github/workflows/docker.yml
vendored
Normal file
43
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
name: Build and push Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
setup-build-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Setup building file structure
|
||||
run: |
|
||||
cp -lr $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./docker
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
zoraxydocker/zoraxy:latest
|
||||
zoraxydocker/zoraxy:${{ github.event.release.tag_name }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
40
.github/workflows/main.yml
vendored
40
.github/workflows/main.yml
vendored
@ -1,40 +0,0 @@
|
||||
name: Image Publisher
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
setup-build-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker & GHCR
|
||||
run: |
|
||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Setup building file structure
|
||||
run: |
|
||||
cp -r $GITHUB_WORKSPACE/src/ $GITHUB_WORKSPACE/docker/
|
||||
|
||||
- name: Build the image
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/docker/
|
||||
docker buildx create --name mainbuilder --driver docker-container --platform linux/amd64,linux/arm64 --use
|
||||
|
||||
docker buildx build --push \
|
||||
--provenance=false \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
|
||||
--tag zoraxydocker/zoraxy:latest \
|
||||
.
|
68
CHANGELOG.md
68
CHANGELOG.md
@ -1,3 +1,71 @@
|
||||
# v3.1.2 03 Nov 2024
|
||||
|
||||
+ Added auto start port 80 listener on acme certificate generator
|
||||
+ Added polling interval and propagation timeout option in ACME module [#300](https://github.com/tobychui/zoraxy/issues/300)
|
||||
+ Added support for custom header variables [#318](https://github.com/tobychui/zoraxy/issues/318)
|
||||
+ Added support for X-Remote-User
|
||||
+ Added port scanner [#342](https://github.com/tobychui/zoraxy/issues/342)
|
||||
+ Optimized code base for stream proxy and config file storage [#320](https://github.com/tobychui/zoraxy/issues/320)
|
||||
+ Removed sorting on cert list
|
||||
+ Fixed request certificate button bug
|
||||
+ Fixed cert auto renew logic [#316](https://github.com/tobychui/zoraxy/issues/316)
|
||||
+ Fixed unable to remove new stream proxy bug
|
||||
+ Fixed many other minor bugs [#328](https://github.com/tobychui/zoraxy/issues/328) [#297](https://github.com/tobychui/zoraxy/issues/297)
|
||||
+ Added more code to SSO system (disabled in release)
|
||||
|
||||
|
||||
# v3.1.1. 09 Sep 2024
|
||||
|
||||
+ Updated country name in access list [#287](https://github.com/tobychui/zoraxy/issues/287)
|
||||
+ Added tour for basic operations
|
||||
+ Updated acme log to system wide logger implementation
|
||||
+ Fixed path traversal in file manager [#274](https://github.com/tobychui/zoraxy/issues/274)
|
||||
+ Removed Proxmox debug code
|
||||
+ Fixed trie tree implementations
|
||||
|
||||
**Thanks to all contributors**
|
||||
|
||||
+ Fix existing containers list in docker popup [7brend7](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3A7brend7)
|
||||
+ Fix network I/O chart not rendering [JokerQyou](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3AJokerQyou)
|
||||
+ Fix typo remvoeClass to removeClass [Aahmadsyamim](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3Aahmadsyamim)
|
||||
+ Updated weighted random upstream implementation [bouroo](https://github.com/tobychui/zoraxy/issues?q=is%3Apr+author%3Abouroo)
|
||||
|
||||
# v3.1.0 31 Jul 2024
|
||||
|
||||
+ Updated log viewer with filter and auto refresh [#243](https://github.com/tobychui/zoraxy/issues/243)
|
||||
+ Fixed csrf vulnerability [#267](https://github.com/tobychui/zoraxy/issues/267)
|
||||
+ Fixed promox issue
|
||||
+ Fixed status code bug in upstream log [#254](https://github.com/tobychui/zoraxy/issues/254)
|
||||
+ Added host overwrite and hop-by-hop header remover
|
||||
+ Added early renew days settings [#256](https://github.com/tobychui/zoraxy/issues/256)
|
||||
+ Updated make file to force no CGO in cicd process
|
||||
+ Fixed bug in updater
|
||||
+ Fixed wildcard certificate renew bug [#249](https://github.com/tobychui/zoraxy/issues/249)
|
||||
+ Added certificate download function [#227](https://github.com/tobychui/zoraxy/issues/227)
|
||||
|
||||
# v3.0.9 16 Jul 2024
|
||||
|
||||
+ 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)
|
||||
|
28
README.md
28
README.md
@ -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,20 +37,21 @@ 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
|
||||
|
||||
Requires Go 1.22 or higher
|
||||
Requires Go 1.23 or higher
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tobychui/zoraxy
|
||||
@ -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
|
||||
|
||||
|
@ -1,11 +1,8 @@
|
||||
FROM docker.io/golang:alpine AS build
|
||||
FROM docker.io/golang:alpine AS build-zoraxy
|
||||
|
||||
RUN mkdir -p /opt/zoraxy/source/ &&\
|
||||
mkdir -p /opt/zoraxy/config/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
|
||||
RUN chmod -R 770 /opt/zoraxy/
|
||||
|
||||
# If you build it yourself, you will need to add the src directory into the docker directory.
|
||||
COPY ./src/ /opt/zoraxy/source/
|
||||
|
||||
@ -13,25 +10,43 @@ WORKDIR /opt/zoraxy/source/
|
||||
|
||||
RUN go mod tidy &&\
|
||||
go build -o /usr/local/bin/zoraxy &&\
|
||||
rm -r /opt/zoraxy/source/
|
||||
chmod 755 /usr/local/bin/zoraxy
|
||||
|
||||
RUN chmod 755 /usr/local/bin/zoraxy &&\
|
||||
chmod +x /usr/local/bin/zoraxy
|
||||
FROM docker.io/ubuntu:latest AS build-zerotier
|
||||
|
||||
FROM docker.io/alpine:3.20
|
||||
RUN mkdir -p /opt/zerotier/source/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
|
||||
RUN apk add --no-cache bash netcat-openbsd sudo
|
||||
WORKDIR /opt/zerotier/source/
|
||||
|
||||
COPY --from=build /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||
COPY --from=build /opt/zoraxy/config/ /opt/zoraxy/config
|
||||
RUN apt-get update -y &&\
|
||||
apt-get install -y curl jq build-essential pkg-config clang cargo libssl-dev
|
||||
|
||||
VOLUME [ "/opt/zoraxy/config/" ]
|
||||
RUN curl -Lo ZeroTierOne.tar.gz https://codeload.github.com/zerotier/ZeroTierOne/tar.gz/refs/tags/1.10.6 &&\
|
||||
tar -xzvf ZeroTierOne.tar.gz &&\
|
||||
cd ZeroTierOne-* &&\
|
||||
make &&\
|
||||
mv ./zerotier-one /usr/local/bin/zerotier-one &&\
|
||||
chmod 755 /usr/local/bin/zerotier-one
|
||||
|
||||
FROM docker.io/ubuntu:latest
|
||||
|
||||
RUN apt-get update -y &&\
|
||||
apt-get install -y bash sudo netcat-openbsd libssl-dev ca-certificates
|
||||
|
||||
COPY --chmod=700 ./entrypoint.sh /opt/zoraxy/
|
||||
COPY --from=build-zoraxy /usr/local/bin/zoraxy /usr/local/bin/zoraxy
|
||||
COPY --from=build-zerotier /usr/local/bin/zerotier-one /usr/local/bin/zerotier-one
|
||||
|
||||
WORKDIR /opt/zoraxy/config/
|
||||
|
||||
ENV ZEROTIER="false"
|
||||
|
||||
ENV AUTORENEW="86400"
|
||||
ENV CFGUPGRADE="true"
|
||||
ENV DOCKER="true"
|
||||
ENV EARLYRENEW="30"
|
||||
ENV FASTGEOIP="false"
|
||||
ENV LOG="true"
|
||||
ENV MDNS="true"
|
||||
ENV MDNSNAME="''"
|
||||
ENV NOAUTH="false"
|
||||
@ -40,9 +55,12 @@ ENV SSHLB="false"
|
||||
ENV VERSION="false"
|
||||
ENV WEBFM="true"
|
||||
ENV WEBROOT="./www"
|
||||
ENV ZTAUTH="''"
|
||||
ENV ZTAUTH=""
|
||||
ENV ZTPORT="9993"
|
||||
|
||||
ENTRYPOINT "zoraxy" "-docker=true" "-autorenew=${AUTORENEW}" "-fastgeoip=${FASTGEOIP}" "-log=${LOG}" "-mdns=${MDNS}" "-mdnsname=${MDNSNAME}" "-noauth=${NOAUTH}" "-port=:${PORT}" "-sshlb=${SSHLB}" "-version=${VERSION}" "-webfm=${WEBFM}" "-webroot=${WEBROOT}" "-ztauth=${ZTAUTH}" "-ztport=${ZTPORT}"
|
||||
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
|
||||
|
||||
|
121
docker/README.md
121
docker/README.md
@ -1,71 +1,98 @@
|
||||
# [zoraxy](https://github.com/tobychui/zoraxy/) </br>
|
||||
# Zoraxy Docker
|
||||
|
||||
[](https://hub.docker.com/r/zoraxydocker/zoraxy)
|
||||
[](https://hub.docker.com/r/zoraxydocker/zoraxy)
|
||||
[](https://hub.docker.com/r/zoraxydocker/zoraxy)
|
||||
[](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/).
|
||||
|
||||
The examples below are not exactly how it should be set up, rather they give a general idea of usage.
|
||||
In the examples below, make sure to update `/path/to/zoraxy/config/` 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 80:80 -p 443:443 -p (management external):(management internal) -v (path to storage directory):/opt/zoraxy/data/ -e (flag)="(value)" zoraxydocker/zoraxy:latest
|
||||
docker run -d \
|
||||
--name zoraxy \
|
||||
--restart unless-stopped \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-p 8000:8000 \
|
||||
-v /path/to/zoraxy/config/:/opt/zoraxy/config/ \
|
||||
-v /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
|
||||
services:
|
||||
zoraxy-docker:
|
||||
image: zoraxydocker/zoraxy:latest
|
||||
container_name: (container name)
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- (management external):(management internal)
|
||||
volumes:
|
||||
- (path to storage directory):/opt/zoraxy/config/
|
||||
environment:
|
||||
(flag): "(value)"
|
||||
```
|
||||
|
||||
| Operator | Need | Details |
|
||||
|:-|:-|:-|
|
||||
| `-d` | Yes | will run the container in the background. |
|
||||
| `--name (container name)` | No | Sets the name of the container to the following word. You can change this to whatever you want. |
|
||||
| `-p (ports)` | Yes | Depending on how your network is setup, you may need to portforward 80, 443, and the management port. |
|
||||
| `-v (path to storage directory):/opt/zoraxy/config/` | Recommend | Sets the folder that holds your files. This should be the place you just chose. By default, it will create a Docker volume for the files for persistency but they will not be accessible. |
|
||||
| `-v /var/run/docker.sock:/var/run/docker.sock` | No | Used for autodiscovery. |
|
||||
| `-e (flag)="(value)"` | No | Arguments to run Zoraxy with. They are simply just capitalized Zoraxy flags. `-docker=true` is always set by default. See examples below. |
|
||||
| `zoraxydocker/zoraxy:latest` | Yes | The repository on Docker hub. By default, it is the latest version that is published. |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Docker usage of the port flag should not include the colon. Ex: PORT="8000"
|
||||
|
||||
## Examples: </br>
|
||||
### Docker Run </br>
|
||||
```
|
||||
docker run -d --name zoraxy -p 80:80 -p 443:443 -p 8005:8005 -v /home/docker/Containers/Zoraxy:/opt/zoraxy/config/ -v /var/run/docker.sock:/var/run/docker.sock -e PORT="8005" -e FASTGEOIP="true" zoraxydocker/zoraxy:latest
|
||||
```
|
||||
|
||||
### Docker Compose </br>
|
||||
```yml
|
||||
services:
|
||||
zoraxy-docker:
|
||||
zoraxy:
|
||||
image: zoraxydocker/zoraxy:latest
|
||||
container_name: zoraxy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- 8005:8005
|
||||
- 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:
|
||||
PORT: "8005"
|
||||
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.
|
||||
|
||||
|
28
docker/entrypoint.sh
Normal file
28
docker/entrypoint.sh
Normal file
@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
update-ca-certificates
|
||||
echo "CA certificates updated"
|
||||
|
||||
if [ "$ZEROTIER" = "true" ]; then
|
||||
zerotier-one -d
|
||||
echo "ZeroTier daemon started"
|
||||
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"
|
||||
|
@ -1 +1 @@
|
||||
zoraxy.arozos.com
|
||||
zoraxy.aroz.org
|
@ -12,19 +12,19 @@
|
||||
<meta name="description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:url" content="https://zoraxy.arozos.com/">
|
||||
<meta property="og:url" content="https://zoraxy.aroz.org/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta property="og:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta property="og:image" content="https://zoraxy.arozos.com/img/og.png">
|
||||
<meta property="og:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="arozos.com">
|
||||
<meta property="twitter:url" content="https://zoraxy.arozos.com/">
|
||||
<meta property="twitter:domain" content="aroz.org">
|
||||
<meta property="twitter:url" content="https://zoraxy.aroz.org/">
|
||||
<meta name="twitter:title" content="Cluster Proxy Gateway | Zoraxy">
|
||||
<meta name="twitter:description" content="A reverse proxy server and cluster network gateway for noobs">
|
||||
<meta name="twitter:image" content="https://zoraxy.arozos.com/img/og.png">
|
||||
<meta name="twitter:image" content="https://zoraxy.aroz.org/img/og.png">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link href="favicon.png" rel="icon">
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
33
src/acme.go
33
src/acme.go
@ -38,7 +38,7 @@ func initACME() *acme.ACMEHandler {
|
||||
port = getRandomPort(30000)
|
||||
}
|
||||
|
||||
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb)
|
||||
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb, SystemWideLogger)
|
||||
}
|
||||
|
||||
// create the special routing rule for ACME
|
||||
@ -85,9 +85,20 @@ func acmeRegisterSpecialRoutingRule() {
|
||||
// This function check if the renew setup is satisfied. If not, toggle them automatically
|
||||
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
isForceHttpsRedirectEnabledOriginally := false
|
||||
requireRestorePort80 := false
|
||||
dnsPara, _ := utils.PostBool(r, "dns")
|
||||
if !dnsPara {
|
||||
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
//Check if port 80 is enabled
|
||||
if !dynamicProxyRouter.Option.ListenOnPort80 {
|
||||
//Enable port 80 temporarily
|
||||
SystemWideLogger.PrintAndLog("ACME", "Temporarily enabling port 80 listener to handle ACME request ", nil)
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(true)
|
||||
requireRestorePort80 = true
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
//Enable port 80 to 443 redirect
|
||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||
SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
|
||||
@ -107,8 +118,8 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
//Add a 3 second delay to make sure everything is settle down
|
||||
time.Sleep(3 * time.Second)
|
||||
//Add a 2 second delay to make sure everything is settle down
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Pass over to the acmeHandler to deal with the communication
|
||||
acmeHandler.HandleRenewCertificate(w, r)
|
||||
@ -117,13 +128,17 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
|
||||
//Restore original settings
|
||||
if dynamicProxyRouter.Option.Port == 443 && !dnsPara {
|
||||
if !isForceHttpsRedirectEnabledOriginally {
|
||||
//Default is off. Turn the redirection off
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
if requireRestorePort80 {
|
||||
//Restore port 80 listener
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring previous port 80 listener settings", nil)
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(false)
|
||||
}
|
||||
if !isForceHttpsRedirectEnabledOriginally {
|
||||
//Default is off. Turn the redirection off
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation
|
||||
|
56
src/api.go
56
src/api.go
@ -8,6 +8,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
"imuslab.com/zoraxy/mod/acme/acmewizard"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/ipscan"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
@ -22,11 +23,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 +38,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)
|
||||
@ -77,6 +78,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)
|
||||
@ -87,11 +90,27 @@ 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)
|
||||
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
||||
|
||||
//SSO and Oauth
|
||||
authRouter.HandleFunc("/api/sso/status", ssoHandler.HandleSSOStatus)
|
||||
authRouter.HandleFunc("/api/sso/enable", ssoHandler.HandleSSOEnable)
|
||||
authRouter.HandleFunc("/api/sso/setPort", ssoHandler.HandlePortChange)
|
||||
authRouter.HandleFunc("/api/sso/setAuthURL", ssoHandler.HandleSetAuthURL)
|
||||
|
||||
authRouter.HandleFunc("/api/sso/app/register", ssoHandler.HandleRegisterApp)
|
||||
//authRouter.HandleFunc("/api/sso/app/list", ssoHandler.HandleListApp)
|
||||
//authRouter.HandleFunc("/api/sso/app/remove", ssoHandler.HandleRemoveApp)
|
||||
|
||||
authRouter.HandleFunc("/api/sso/user/list", ssoHandler.HandleListUser)
|
||||
authRouter.HandleFunc("/api/sso/user/add", ssoHandler.HandleAddUser)
|
||||
authRouter.HandleFunc("/api/sso/user/edit", ssoHandler.HandleEditUser)
|
||||
authRouter.HandleFunc("/api/sso/user/remove", ssoHandler.HandleRemoveUser)
|
||||
|
||||
//Redirection config
|
||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
||||
@ -127,7 +146,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)
|
||||
@ -169,7 +188,8 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
|
||||
|
||||
//Network utilities
|
||||
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
|
||||
authRouter.HandleFunc("/api/tools/ipscan", ipscan.HandleIpScan)
|
||||
authRouter.HandleFunc("/api/tools/portscan", ipscan.HandleScanPort)
|
||||
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
|
||||
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
|
||||
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
|
||||
@ -184,8 +204,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)
|
||||
@ -225,7 +245,7 @@ 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)
|
||||
@ -240,18 +260,18 @@ func initAPIs() {
|
||||
}
|
||||
|
||||
// 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)
|
||||
@ -261,12 +281,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) {
|
||||
@ -277,7 +297,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)
|
||||
|
68
src/cert.go
68
src/cert.go
@ -182,27 +182,28 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
||||
}
|
||||
|
||||
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
|
||||
|
203
src/go.mod
203
src/go.mod
@ -1,106 +1,127 @@
|
||||
module imuslab.com/zoraxy
|
||||
|
||||
go 1.21
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/docker/docker v27.0.0+incompatible
|
||||
github.com/go-acme/lego/v4 v4.16.1
|
||||
github.com/go-acme/lego/v4 v4.19.2
|
||||
github.com/go-ping/ping v1.1.0
|
||||
github.com/go-session/session v3.1.2+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/likexian/whois v1.15.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/text v0.15.0
|
||||
golang.org/x/net v0.29.0
|
||||
golang.org/x/sys v0.25.0
|
||||
golang.org/x/text v0.18.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.25.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/auth v0.9.3 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
|
||||
github.com/benbjohnson/clock v1.3.0 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.114 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect
|
||||
github.com/tidwall/buntdb v1.1.2 // indirect
|
||||
github.com/tidwall/gjson v1.12.1 // indirect
|
||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
|
||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/vultr/govultr/v3 v3.9.1 // indirect
|
||||
go.mongodb.org/mongo-driver v1.12.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.5.1 // indirect
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.2.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.13 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.40.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.7 // indirect
|
||||
github.com/aws/smithy-go v1.20.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/civo/civogo v0.3.11 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.86.0 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.104.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.9.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dnsimple/dnsimple-go v1.2.0 // indirect
|
||||
github.com/dnsimple/dnsimple-go v1.7.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/exoscale/egoscale v0.102.3 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-resty/resty/v2 v2.11.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2
|
||||
github.com/go-resty/resty/v2 v2.13.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
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/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/gophercloud/gophercloud v1.14.0 // indirect
|
||||
github.com/gorilla/csrf v1.7.2
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
@ -110,11 +131,11 @@ require (
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||
github.com/linode/linodego v1.28.0 // indirect
|
||||
github.com/linode/linodego v1.40.0 // indirect
|
||||
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
|
||||
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.58 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@ -125,66 +146,64 @@ require (
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect
|
||||
github.com/nrdcg/desec v0.7.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
|
||||
github.com/nrdcg/desec v0.8.0 // indirect
|
||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||
github.com/nrdcg/freemyip v0.2.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.10.0 // indirect
|
||||
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||
github.com/nrdcg/porkbun v0.3.0 // indirect
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/ovh/go-ovh v1.4.3 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/ovh/go-ovh v1.6.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/pquerna/otp v1.4.0 // indirect
|
||||
github.com/sacloud/api-client-go v0.2.8 // indirect
|
||||
github.com/sacloud/go-http v0.1.6 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.11.1 // indirect
|
||||
github.com/sacloud/packages-go v0.0.9 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 // indirect
|
||||
github.com/sacloud/api-client-go v0.2.10 // indirect
|
||||
github.com/sacloud/go-http v0.1.8 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.12.0 // indirect
|
||||
github.com/sacloud/packages-go v0.0.10 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||
github.com/softlayer/softlayer-go v1.1.3 // indirect
|
||||
github.com/softlayer/softlayer-go v1.1.5 // indirect
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect
|
||||
github.com/transip/gotransip/v6 v6.23.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 // indirect
|
||||
github.com/transip/gotransip/v6 v6.26.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.7.0-20240913052650-970ca9a // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||
github.com/vultr/govultr/v2 v2.17.2 // indirect
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20240911120709-1fa0cb6f47c2 // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20240911121212-e4e74d0d02f5 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
|
||||
go.opentelemetry.io/otel v1.27.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
|
||||
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.27.0 // indirect
|
||||
go.uber.org/ratelimit v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/oauth2 v0.18.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
google.golang.org/api v0.169.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||
go.uber.org/ratelimit v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/tools v0.25.0 // indirect
|
||||
google.golang.org/api v0.197.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/grpc v1.66.1 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.7.13 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.12.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
|
599
src/go.sum
599
src/go.sum
File diff suppressed because it is too large
Load Diff
34
src/main.go
34
src/main.go
@ -12,9 +12,11 @@ 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"
|
||||
"imuslab.com/zoraxy/mod/auth/sso"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dockerux"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
@ -50,6 +52,7 @@ 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")
|
||||
@ -57,7 +60,7 @@ var enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade
|
||||
|
||||
var (
|
||||
name = "Zoraxy"
|
||||
version = "3.0.8"
|
||||
version = "3.1.2"
|
||||
nodeUUID = "generic" //System uuid, in uuidv4 format
|
||||
development = false //Set this to false to use embedded web fs
|
||||
bootTime = time.Now().Unix()
|
||||
@ -71,10 +74,12 @@ 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
|
||||
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
|
||||
@ -91,6 +96,7 @@ var (
|
||||
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
|
||||
ssoHandler *sso.SSOHandler //Single Sign On handler
|
||||
|
||||
//Helper modules
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
@ -113,8 +119,8 @@ func SetupCloseHandler() {
|
||||
|
||||
func ShutdownSeq() {
|
||||
SystemWideLogger.Println("Shutting down " + name)
|
||||
SystemWideLogger.Println("Closing GeoDB ")
|
||||
geodbStore.Close()
|
||||
//SystemWideLogger.Println("Closing GeoDB")
|
||||
//geodbStore.Close()
|
||||
SystemWideLogger.Println("Closing Netstats Listener")
|
||||
netstatBuffers.Close()
|
||||
SystemWideLogger.Println("Closing Statistic Collector")
|
||||
@ -175,12 +181,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() {
|
||||
@ -193,7 +209,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)
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -26,14 +25,16 @@ 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"
|
||||
)
|
||||
|
||||
type CertificateInfoJSON struct {
|
||||
AcmeName string `json:"acme_name"`
|
||||
AcmeUrl string `json:"acme_url"`
|
||||
SkipTLS bool `json:"skip_tls"`
|
||||
UseDNS bool `json:"dns"`
|
||||
AcmeName string `json:"acme_name"` //ACME provider name
|
||||
AcmeUrl string `json:"acme_url"` //Custom ACME URL (if any)
|
||||
SkipTLS bool `json:"skip_tls"` //Skip TLS verification of upstream
|
||||
UseDNS bool `json:"dns"` //Use DNS challenge
|
||||
PropTimeout int `json:"prop_time"` //Propagation timeout
|
||||
}
|
||||
|
||||
// ACMEUser represents a user in the ACME system.
|
||||
@ -68,25 +69,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...")
|
||||
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool, propagationTimeout int) (bool, error) {
|
||||
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 +109,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 +136,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 +153,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 +171,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)
|
||||
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials, propagationTimeout)
|
||||
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 +212,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 +227,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 +247,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 +267,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,32 +275,33 @@ 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
|
||||
}
|
||||
|
||||
// Save certificate's ACME info for renew usage
|
||||
certInfo := &CertificateInfoJSON{
|
||||
AcmeName: caName,
|
||||
AcmeUrl: caUrl,
|
||||
SkipTLS: skipTLS,
|
||||
UseDNS: useDNS,
|
||||
AcmeName: caName,
|
||||
AcmeUrl: caUrl,
|
||||
SkipTLS: skipTLS,
|
||||
UseDNS: useDNS,
|
||||
PropTimeout: propagationTimeout,
|
||||
}
|
||||
|
||||
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 +319,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 +416,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,12 +454,30 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
domains := strings.Split(domainPara, ",")
|
||||
|
||||
// Default propagation timeout is 300 seconds
|
||||
propagationTimeout := 300
|
||||
if dns {
|
||||
ppgTimeout, err := utils.PostPara(r, "ppgTimeout")
|
||||
if err == nil {
|
||||
propagationTimeout, err = strconv.Atoi(ppgTimeout)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid propagation timeout value")
|
||||
return
|
||||
}
|
||||
if propagationTimeout < 60 {
|
||||
//Minimum propagation timeout is 60 seconds
|
||||
propagationTimeout = 60
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Clean spaces in front or behind each domain
|
||||
cleanedDomains := []string{}
|
||||
for _, domain := range domains {
|
||||
cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
|
||||
}
|
||||
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns)
|
||||
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
@ -465,7 +489,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)
|
||||
|
@ -1,72 +1,56 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
)
|
||||
|
||||
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string) (challenge.Provider, error) {
|
||||
|
||||
//Original Implementation
|
||||
/*credentials, err := extractDnsCredentials(dnsCredentials)
|
||||
// Preprocessor function to get DNS challenge provider by name
|
||||
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string, ppgTimeout int) (challenge.Provider, error) {
|
||||
//Unpack the dnsCredentials (json string) to map
|
||||
var dnsCredentialsMap map[string]interface{}
|
||||
err := json.Unmarshal([]byte(dnsCredentials), &dnsCredentialsMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setCredentialsIntoEnvironmentVariables(credentials)
|
||||
|
||||
provider, err := dns.NewDNSChallengeProviderByName(dnsProvider)
|
||||
*/
|
||||
|
||||
//New implementation using acmedns CICD pipeline generated datatype
|
||||
return acmedns.GetDNSProviderByJsonConfig(dnsProvider, dnsCredentials)
|
||||
}
|
||||
|
||||
/*
|
||||
Original implementation of DNS ACME using OS.Env as payload
|
||||
*/
|
||||
|
||||
func setCredentialsIntoEnvironmentVariables(credentials map[string]string) {
|
||||
for key, value := range credentials {
|
||||
err := os.Setenv(key, value)
|
||||
if err != nil {
|
||||
log.Println("[ERR] Failed to set environment variable %s: %v", key, err)
|
||||
} else {
|
||||
log.Println("[INFO] Environment variable %s set successfully", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractDnsCredentials(input string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
// Split the input string by newline character
|
||||
lines := strings.Split(input, "\n")
|
||||
|
||||
// Iterate over each line
|
||||
for _, line := range lines {
|
||||
// Split the line by "=" character
|
||||
//use SpliyN to make sure not to split the value if the value is base64
|
||||
parts := strings.SplitN(line, "=", 1)
|
||||
|
||||
// Check if the line is in the correct format
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
// Add the key-value pair to the map
|
||||
result[key] = value
|
||||
|
||||
if value == "" || key == "" {
|
||||
//invalid config
|
||||
return result, errors.New("DNS credential extract failed")
|
||||
}
|
||||
//Clear the PollingInterval and PropagationTimeout field and conert to int
|
||||
userDefinedPollingInterval := 2
|
||||
if dnsCredentialsMap["PollingInterval"] != nil {
|
||||
userDefinedPollingIntervalRaw := dnsCredentialsMap["PollingInterval"].(string)
|
||||
delete(dnsCredentialsMap, "PollingInterval")
|
||||
convertedPollingInterval, err := strconv.Atoi(userDefinedPollingIntervalRaw)
|
||||
if err == nil {
|
||||
userDefinedPollingInterval = convertedPollingInterval
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
userDefinedPropagationTimeout := ppgTimeout
|
||||
if dnsCredentialsMap["PropagationTimeout"] != nil {
|
||||
userDefinedPropagationTimeoutRaw := dnsCredentialsMap["PropagationTimeout"].(string)
|
||||
delete(dnsCredentialsMap, "PropagationTimeout")
|
||||
convertedPropagationTimeout, err := strconv.Atoi(userDefinedPropagationTimeoutRaw)
|
||||
if err == nil {
|
||||
//Overwrite the default propagation timeout if it is requeted from UI
|
||||
userDefinedPropagationTimeout = convertedPropagationTimeout
|
||||
}
|
||||
}
|
||||
|
||||
//Restructure dnsCredentials string from map
|
||||
dnsCredentialsBytes, err := json.Marshal(dnsCredentialsMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dnsCredentials = string(dnsCredentialsBytes)
|
||||
|
||||
//Using acmedns CICD pipeline generated datatype to optain the DNS provider
|
||||
return acmedns.GetDNSProviderByJsonConfig(
|
||||
dnsProvider,
|
||||
dnsCredentials,
|
||||
int64(userDefinedPropagationTimeout),
|
||||
int64(userDefinedPollingInterval),
|
||||
)
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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")
|
||||
}
|
||||
|
@ -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,8 +88,12 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
|
||||
AcmeHandler: AcmeHandler,
|
||||
RenewerConfig: &renewerConfig,
|
||||
RenewTickInterval: renewCheckInterval,
|
||||
EarlyRenewDays: earlyRenewDays,
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
thisRenewer.Logf("ACME early renew set to "+fmt.Sprint(earlyRenewDays)+" days and check interval set to "+fmt.Sprint(renewCheckInterval)+" seconds", nil)
|
||||
|
||||
if thisRenewer.RenewerConfig.Enabled {
|
||||
//Start the renew ticker
|
||||
thisRenewer.StartAutoRenewTicker()
|
||||
@ -95,6 +105,10 @@ func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64,
|
||||
return &thisRenewer, nil
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) Logf(message string, err error) {
|
||||
a.Logger.PrintAndLog("cert-renew", message, err)
|
||||
}
|
||||
|
||||
func (a *AutoRenewer) StartAutoRenewTicker() {
|
||||
//Stop the previous ticker if still running
|
||||
if a.TickerstopChan != nil {
|
||||
@ -113,7 +127,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 +149,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 +179,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 +224,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 +278,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 +292,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 +306,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 +334,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 +366,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,21 +374,27 @@ 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}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS)
|
||||
//For upgrading config from older version of Zoraxy which don't have timeout
|
||||
if certInfo.PropTimeout == 0 {
|
||||
//Set default timeout
|
||||
certInfo.PropTimeout = 300
|
||||
}
|
||||
|
||||
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,14 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Get the issuer name from pem file
|
||||
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
|
||||
// Read the PEM file
|
||||
pemData, err := ioutil.ReadFile(pemFilePath)
|
||||
pemData, err := os.ReadFile(pemFilePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -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 {
|
||||
|
@ -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 {
|
||||
@ -35,17 +35,31 @@ func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseW
|
||||
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
|
||||
|
||||
|
34
src/mod/auth/sso/app.go
Normal file
34
src/mod/auth/sso/app.go
Normal file
@ -0,0 +1,34 @@
|
||||
package sso
|
||||
|
||||
/*
|
||||
app.go
|
||||
|
||||
This file contains the app structure and app management
|
||||
functions for the SSO module.
|
||||
|
||||
*/
|
||||
|
||||
// RegisteredUpstreamApp is a structure that contains the information of an
|
||||
// upstream app that is registered with the SSO server
|
||||
type RegisteredUpstreamApp struct {
|
||||
ID string
|
||||
Secret string
|
||||
Domain []string
|
||||
Scopes []string
|
||||
SessionDuration int //in seconds, default to 1 hour
|
||||
}
|
||||
|
||||
// RegisterUpstreamApp registers an upstream app with the SSO server
|
||||
func (s *SSOHandler) ListRegisteredApps() []*RegisteredUpstreamApp {
|
||||
apps := make([]*RegisteredUpstreamApp, 0)
|
||||
for _, app := range s.Apps {
|
||||
apps = append(apps, &app)
|
||||
}
|
||||
return apps
|
||||
}
|
||||
|
||||
// RegisterUpstreamApp registers an upstream app with the SSO server
|
||||
func (s *SSOHandler) GetAppByID(appID string) (*RegisteredUpstreamApp, bool) {
|
||||
app, ok := s.Apps[appID]
|
||||
return &app, ok
|
||||
}
|
271
src/mod/auth/sso/handlers.go
Normal file
271
src/mod/auth/sso/handlers.go
Normal file
@ -0,0 +1,271 @@
|
||||
package sso
|
||||
|
||||
/*
|
||||
handlers.go
|
||||
|
||||
This file contains the handlers for the SSO module.
|
||||
If you are looking for handlers for SSO user management,
|
||||
please refer to userHandlers.go.
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// HandleSSOStatus handle the request to get the status of the SSO portal server
|
||||
func (s *SSOHandler) HandleSSOStatus(w http.ResponseWriter, r *http.Request) {
|
||||
type SSOStatus struct {
|
||||
Enabled bool
|
||||
SSOInterceptEnabled bool
|
||||
ListeningPort int
|
||||
AuthURL string
|
||||
}
|
||||
|
||||
status := SSOStatus{
|
||||
Enabled: s.ssoPortalServer != nil,
|
||||
//SSOInterceptEnabled: s.ssoInterceptEnabled,
|
||||
ListeningPort: s.Config.PortalServerPort,
|
||||
AuthURL: s.Config.AuthURL,
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(status)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Wrapper for starting and stopping the SSO portal server
|
||||
// require POST request with key "enable" and value "true" or "false"
|
||||
func (s *SSOHandler) HandleSSOEnable(w http.ResponseWriter, r *http.Request) {
|
||||
enable, err := utils.PostBool(r, "enable")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid enable value")
|
||||
return
|
||||
}
|
||||
|
||||
if enable {
|
||||
s.HandleStartSSOPortal(w, r)
|
||||
} else {
|
||||
s.HandleStopSSOPortal(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleStartSSOPortal handle the request to start the SSO portal server
|
||||
func (s *SSOHandler) HandleStartSSOPortal(w http.ResponseWriter, r *http.Request) {
|
||||
if s.ssoPortalServer != nil {
|
||||
//Already enabled. Do restart instead.
|
||||
err := s.RestartSSOServer()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to start SSO server")
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the authURL is set correctly. If not, return error
|
||||
if s.Config.AuthURL == "" {
|
||||
utils.SendErrorResponse(w, "auth URL not set")
|
||||
return
|
||||
}
|
||||
|
||||
//Start the SSO portal server in go routine
|
||||
go s.StartSSOPortal()
|
||||
|
||||
//Write current state to database
|
||||
err := s.Config.Database.Write("sso_conf", "enabled", true)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to update SSO state")
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleStopSSOPortal handle the request to stop the SSO portal server
|
||||
func (s *SSOHandler) HandleStopSSOPortal(w http.ResponseWriter, r *http.Request) {
|
||||
if s.ssoPortalServer == nil {
|
||||
//Already disabled
|
||||
utils.SendOK(w)
|
||||
return
|
||||
}
|
||||
|
||||
err := s.ssoPortalServer.Close()
|
||||
if err != nil {
|
||||
s.Log("Failed to stop SSO portal server", err)
|
||||
utils.SendErrorResponse(w, "failed to stop SSO portal server")
|
||||
return
|
||||
}
|
||||
s.ssoPortalServer = nil
|
||||
|
||||
//Write current state to database
|
||||
err = s.Config.Database.Write("sso_conf", "enabled", false)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to update SSO state")
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandlePortChange handle the request to change the SSO portal server port
|
||||
func (s *SSOHandler) HandlePortChange(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
//Return the current port
|
||||
js, _ := json.Marshal(s.Config.PortalServerPort)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
}
|
||||
|
||||
port, err := utils.PostInt(r, "port")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid port given")
|
||||
return
|
||||
}
|
||||
|
||||
s.Config.PortalServerPort = port
|
||||
|
||||
//Write to the database
|
||||
err = s.Config.Database.Write("sso_conf", "port", port)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to update port")
|
||||
return
|
||||
}
|
||||
|
||||
if s.IsRunning() {
|
||||
//Restart the server if it is running
|
||||
err = s.RestartSSOServer()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to restart SSO server")
|
||||
return
|
||||
}
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleSetAuthURL handle the request to change the SSO auth URL
|
||||
// This is the URL that the SSO portal server will redirect to for authentication
|
||||
// e.g. auth.yourdomain.com
|
||||
func (s *SSOHandler) HandleSetAuthURL(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
//Return the current auth URL
|
||||
js, _ := json.Marshal(s.Config.AuthURL)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
}
|
||||
|
||||
//Get the auth URL
|
||||
authURL, err := utils.PostPara(r, "auth_url")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid auth URL given")
|
||||
return
|
||||
}
|
||||
|
||||
s.Config.AuthURL = authURL
|
||||
|
||||
//Write to the database
|
||||
err = s.Config.Database.Write("sso_conf", "authurl", authURL)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to update auth URL")
|
||||
return
|
||||
}
|
||||
|
||||
//Clear the cookie store and restart the server
|
||||
err = s.RestartSSOServer()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to restart SSO server")
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleRegisterApp handle the request to register a new app to the SSO portal
|
||||
func (s *SSOHandler) HandleRegisterApp(w http.ResponseWriter, r *http.Request) {
|
||||
appName, err := utils.PostPara(r, "app_name")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid app name given")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := utils.PostPara(r, "app_id")
|
||||
if err != nil {
|
||||
//If id is not given, use the app name with a random UUID
|
||||
newID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to generate new app ID")
|
||||
return
|
||||
}
|
||||
id = strings.ReplaceAll(appName, " ", "") + "-" + newID.String()
|
||||
}
|
||||
|
||||
//Check if the given appid is already in use
|
||||
if _, ok := s.Apps[id]; ok {
|
||||
utils.SendErrorResponse(w, "app ID already in use")
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Process the app domain
|
||||
An app can have multiple domains, separated by commas
|
||||
Usually the app domain is the proxy rule that points to the app
|
||||
For example, if the app is hosted at app.yourdomain.com, the app domain is app.yourdomain.com
|
||||
*/
|
||||
appDomain, err := utils.PostPara(r, "app_domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid app URL given")
|
||||
return
|
||||
}
|
||||
|
||||
appURLs := strings.Split(appDomain, ",")
|
||||
//Remove padding and trailing spaces in each URL
|
||||
for i := range appURLs {
|
||||
appURLs[i] = strings.TrimSpace(appURLs[i])
|
||||
}
|
||||
|
||||
//Create a new app entry
|
||||
thisAppEntry := RegisteredUpstreamApp{
|
||||
ID: id,
|
||||
Secret: "",
|
||||
Domain: appURLs,
|
||||
Scopes: []string{},
|
||||
SessionDuration: 3600,
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(thisAppEntry)
|
||||
|
||||
//Create a new app in the database
|
||||
err = s.Config.Database.Write("sso_apps", appName, string(js))
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to create new app")
|
||||
return
|
||||
}
|
||||
|
||||
//Also add the app to runtime config
|
||||
s.Apps[appName] = thisAppEntry
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleAppRemove handle the request to remove an app from the SSO portal
|
||||
func (s *SSOHandler) HandleAppRemove(w http.ResponseWriter, r *http.Request) {
|
||||
appID, err := utils.PostPara(r, "app_id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid app ID given")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the app actually exists
|
||||
if _, ok := s.Apps[appID]; !ok {
|
||||
utils.SendErrorResponse(w, "app not found")
|
||||
return
|
||||
}
|
||||
delete(s.Apps, appID)
|
||||
|
||||
//Also remove it from the database
|
||||
err = s.Config.Database.Delete("sso_apps", appID)
|
||||
if err != nil {
|
||||
s.Log("Failed to remove app from database", err)
|
||||
}
|
||||
|
||||
}
|
295
src/mod/auth/sso/oauth2.go
Normal file
295
src/mod/auth/sso/oauth2.go
Normal file
@ -0,0 +1,295 @@
|
||||
package sso
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-oauth2/oauth2/v4/errors"
|
||||
"github.com/go-oauth2/oauth2/v4/generates"
|
||||
"github.com/go-oauth2/oauth2/v4/manage"
|
||||
"github.com/go-oauth2/oauth2/v4/models"
|
||||
"github.com/go-oauth2/oauth2/v4/server"
|
||||
"github.com/go-oauth2/oauth2/v4/store"
|
||||
"github.com/go-session/session"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
SSO_SESSION_NAME = "ZoraxySSO"
|
||||
)
|
||||
|
||||
type OAuth2Server struct {
|
||||
srv *server.Server //oAuth server instance
|
||||
config *SSOConfig
|
||||
parent *SSOHandler
|
||||
}
|
||||
|
||||
//go:embed static/auth.html
|
||||
var authHtml []byte
|
||||
|
||||
//go:embed static/login.html
|
||||
var loginHtml []byte
|
||||
|
||||
// NewOAuth2Server creates a new OAuth2 server instance
|
||||
func NewOAuth2Server(config *SSOConfig, parent *SSOHandler) (*OAuth2Server, error) {
|
||||
manager := manage.NewDefaultManager()
|
||||
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
|
||||
// token store
|
||||
manager.MustTokenStorage(store.NewFileTokenStore("./conf/sso.db"))
|
||||
// generate jwt access token
|
||||
manager.MapAccessGenerate(generates.NewAccessGenerate())
|
||||
|
||||
//Load the information of registered app within the OAuth2 server
|
||||
clientStore := store.NewClientStore()
|
||||
clientStore.Set("myapp", &models.Client{
|
||||
ID: "myapp",
|
||||
Secret: "verysecurepassword",
|
||||
Domain: "localhost:9094",
|
||||
})
|
||||
//TODO: LOAD THIS DYNAMICALLY FROM DATABASE
|
||||
manager.MapClientStorage(clientStore)
|
||||
|
||||
thisServer := OAuth2Server{
|
||||
config: config,
|
||||
parent: parent,
|
||||
}
|
||||
|
||||
//Create a new oauth server
|
||||
srv := server.NewServer(server.NewConfig(), manager)
|
||||
srv.SetPasswordAuthorizationHandler(thisServer.PasswordAuthorizationHandler)
|
||||
srv.SetUserAuthorizationHandler(thisServer.UserAuthorizeHandler)
|
||||
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
|
||||
log.Println("Internal Error:", err.Error())
|
||||
return
|
||||
})
|
||||
srv.SetResponseErrorHandler(func(re *errors.Response) {
|
||||
log.Println("Response Error:", re.Error.Error())
|
||||
})
|
||||
|
||||
//Set the access scope handler
|
||||
srv.SetAuthorizeScopeHandler(thisServer.AuthorizationScopeHandler)
|
||||
//Set the access token expiration handler based on requesting domain / hostname
|
||||
srv.SetAccessTokenExpHandler(thisServer.ExpireHandler)
|
||||
thisServer.srv = srv
|
||||
return &thisServer, nil
|
||||
}
|
||||
|
||||
// Password handler, validate if the given username and password are correct
|
||||
func (oas *OAuth2Server) PasswordAuthorizationHandler(ctx context.Context, clientID, username, password string) (userID string, err error) {
|
||||
//TODO: LOAD THIS DYNAMICALLY FROM DATABASE
|
||||
if username == "test" && password == "test" {
|
||||
userID = "test"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// User Authorization Handler, handle auth request from user
|
||||
func (oas *OAuth2Server) UserAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
|
||||
store, err := session.Start(r.Context(), w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
uid, ok := store.Get(SSO_SESSION_NAME)
|
||||
if !ok {
|
||||
if r.Form == nil {
|
||||
r.ParseForm()
|
||||
}
|
||||
|
||||
store.Set("ReturnUri", r.Form)
|
||||
store.Save()
|
||||
|
||||
w.Header().Set("Location", "/oauth2/login")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
userID = uid.(string)
|
||||
store.Delete(SSO_SESSION_NAME)
|
||||
store.Save()
|
||||
return
|
||||
}
|
||||
|
||||
// AccessTokenExpHandler, set the SSO session length default value
|
||||
func (oas *OAuth2Server) ExpireHandler(w http.ResponseWriter, r *http.Request) (exp time.Duration, err error) {
|
||||
requestHostname := r.Host
|
||||
if requestHostname == "" {
|
||||
//Use default value
|
||||
return time.Hour, nil
|
||||
}
|
||||
|
||||
//Get the Registered App Config from parent
|
||||
appConfig, ok := oas.parent.Apps[requestHostname]
|
||||
if !ok {
|
||||
//Use default value
|
||||
return time.Hour, nil
|
||||
}
|
||||
|
||||
//Use the app's session length
|
||||
return time.Second * time.Duration(appConfig.SessionDuration), nil
|
||||
}
|
||||
|
||||
// AuthorizationScopeHandler, handle the scope of the request
|
||||
func (oas *OAuth2Server) AuthorizationScopeHandler(w http.ResponseWriter, r *http.Request) (scope string, err error) {
|
||||
//Get the scope from post or GEt request
|
||||
if r.Form == nil {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return "none", err
|
||||
}
|
||||
}
|
||||
|
||||
//Get the hostname of the request
|
||||
requestHostname := r.Host
|
||||
if requestHostname == "" {
|
||||
//No rule set. Use default
|
||||
return "none", nil
|
||||
}
|
||||
|
||||
//Get the Registered App Config from parent
|
||||
appConfig, ok := oas.parent.Apps[requestHostname]
|
||||
if !ok {
|
||||
//No rule set. Use default
|
||||
return "none", nil
|
||||
}
|
||||
|
||||
//Check if the scope is set in the request
|
||||
if v, ok := r.Form["scope"]; ok {
|
||||
//Check if the requested scope is in the appConfig scope
|
||||
if utils.StringInArray(appConfig.Scopes, v[0]) {
|
||||
return v[0], nil
|
||||
}
|
||||
return "none", nil
|
||||
}
|
||||
|
||||
return "none", nil
|
||||
}
|
||||
|
||||
/* SSO Web Server Toggle Functions */
|
||||
func (oas *OAuth2Server) RegisterOauthEndpoints(primaryMux *http.ServeMux) {
|
||||
primaryMux.HandleFunc("/oauth2/login", oas.loginHandler)
|
||||
primaryMux.HandleFunc("/oauth2/auth", oas.authHandler)
|
||||
|
||||
primaryMux.HandleFunc("/oauth2/authorize", func(w http.ResponseWriter, r *http.Request) {
|
||||
store, err := session.Start(r.Context(), w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var form url.Values
|
||||
if v, ok := store.Get("ReturnUri"); ok {
|
||||
form = v.(url.Values)
|
||||
}
|
||||
r.Form = form
|
||||
|
||||
store.Delete("ReturnUri")
|
||||
store.Save()
|
||||
|
||||
err = oas.srv.HandleAuthorizeRequest(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
})
|
||||
|
||||
primaryMux.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
|
||||
err := oas.srv.HandleTokenRequest(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
|
||||
primaryMux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := oas.srv.ValidationBearerToken(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"expires_in": int64(time.Until(token.GetAccessCreateAt().Add(token.GetAccessExpiresIn())).Seconds()),
|
||||
"client_id": token.GetClientID(),
|
||||
"user_id": token.GetUserID(),
|
||||
}
|
||||
e := json.NewEncoder(w)
|
||||
e.SetIndent("", " ")
|
||||
e.Encode(data)
|
||||
})
|
||||
}
|
||||
|
||||
func (oas *OAuth2Server) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
store, err := session.Start(r.Context(), w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
if r.Form == nil {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Load username and password from form post
|
||||
username, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
w.Write([]byte("invalid username or password"))
|
||||
return
|
||||
}
|
||||
|
||||
password, err := utils.PostPara(r, "password")
|
||||
if err != nil {
|
||||
w.Write([]byte("invalid username or password"))
|
||||
return
|
||||
}
|
||||
|
||||
//Validate the user
|
||||
if !oas.parent.ValidateUsernameAndPassword(username, password) {
|
||||
//Wrong password
|
||||
w.Write([]byte("invalid username or password"))
|
||||
return
|
||||
}
|
||||
|
||||
store.Set(SSO_SESSION_NAME, r.Form.Get("username"))
|
||||
store.Save()
|
||||
|
||||
w.Header().Set("Location", "/oauth2/auth")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
return
|
||||
} else if r.Method == "GET" {
|
||||
//Check if the user is logged in
|
||||
if _, ok := store.Get(SSO_SESSION_NAME); ok {
|
||||
w.Header().Set("Location", "/oauth2/auth")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
//User not logged in. Show login page
|
||||
w.Write(loginHtml)
|
||||
}
|
||||
|
||||
func (oas *OAuth2Server) authHandler(w http.ResponseWriter, r *http.Request) {
|
||||
store, err := session.Start(context.TODO(), w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := store.Get(SSO_SESSION_NAME); !ok {
|
||||
w.Header().Set("Location", "/oauth2/login")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
return
|
||||
}
|
||||
//User logged in. Check if this user have previously authorized the app
|
||||
|
||||
//TODO: Check if the user have previously authorized the app
|
||||
|
||||
//User have not authorized the app. Show the authorization page
|
||||
w.Write(authHtml)
|
||||
}
|
1
src/mod/auth/sso/oauth_test.go
Normal file
1
src/mod/auth/sso/oauth_test.go
Normal file
@ -0,0 +1 @@
|
||||
package sso
|
58
src/mod/auth/sso/openid.go
Normal file
58
src/mod/auth/sso/openid.go
Normal file
@ -0,0 +1,58 @@
|
||||
package sso
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OpenIDConfiguration struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
JwksUri string `json:"jwks_uri"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||
SubjectTypesSupported []string `json:"subject_types_supported"`
|
||||
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
|
||||
ClaimsSupported []string `json:"claims_supported"`
|
||||
}
|
||||
|
||||
func (h *SSOHandler) HandleDiscoveryRequest(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
//Prepend https:// if not present
|
||||
authBaseURL := h.Config.AuthURL
|
||||
if !strings.HasPrefix(authBaseURL, "http://") && !strings.HasPrefix(authBaseURL, "https://") {
|
||||
authBaseURL = "https://" + authBaseURL
|
||||
}
|
||||
|
||||
//Handle the discovery request
|
||||
discovery := OpenIDConfiguration{
|
||||
Issuer: authBaseURL,
|
||||
AuthorizationEndpoint: authBaseURL + "/oauth2/authorize",
|
||||
TokenEndpoint: authBaseURL + "/oauth2/token",
|
||||
JwksUri: authBaseURL + "/jwks.json",
|
||||
ResponseTypesSupported: []string{"code", "token"},
|
||||
SubjectTypesSupported: []string{"public"},
|
||||
IDTokenSigningAlgValuesSupported: []string{
|
||||
"RS256",
|
||||
},
|
||||
ClaimsSupported: []string{
|
||||
"sub", //Subject, usually the user ID
|
||||
"iss", //Issuer, usually the server URL
|
||||
"aud", //Audience, usually the client ID
|
||||
"exp", //Expiration Time
|
||||
"iat", //Issued At
|
||||
"email", //Email
|
||||
"locale", //Locale
|
||||
"name", //Full Name
|
||||
"nickname", //Nickname
|
||||
"preferred_username", //Preferred Username
|
||||
"website", //Website
|
||||
},
|
||||
}
|
||||
|
||||
//Write the response
|
||||
js, _ := json.Marshal(discovery)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(js)
|
||||
}
|
132
src/mod/auth/sso/server.go
Normal file
132
src/mod/auth/sso/server.go
Normal file
@ -0,0 +1,132 @@
|
||||
package sso
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-oauth2/oauth2/v4/errors"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
server.go
|
||||
|
||||
This is the web server for the SSO portal. It contains the
|
||||
HTTP server and the handlers for the SSO portal.
|
||||
|
||||
If you are looking for handlers that changes the settings
|
||||
of the SSO portale or user management, please refer to
|
||||
handlers.go.
|
||||
|
||||
*/
|
||||
|
||||
func (h *SSOHandler) InitSSOPortal(portalServerPort int) {
|
||||
//Create a new web server for the SSO portal
|
||||
pmux := http.NewServeMux()
|
||||
fs := http.FileServer(http.FS(staticFiles))
|
||||
pmux.Handle("/", fs)
|
||||
|
||||
//Register API endpoint for the SSO portal
|
||||
pmux.HandleFunc("/sso/login", h.HandleLogin)
|
||||
|
||||
//Register API endpoint for autodiscovery
|
||||
pmux.HandleFunc("/.well-known/openid-configuration", h.HandleDiscoveryRequest)
|
||||
|
||||
//Register OAuth2 endpoints
|
||||
h.Oauth2Server.RegisterOauthEndpoints(pmux)
|
||||
h.ssoPortalMux = pmux
|
||||
}
|
||||
|
||||
// StartSSOPortal start the SSO portal server
|
||||
// This function will block the main thread, call it in a goroutine
|
||||
func (h *SSOHandler) StartSSOPortal() error {
|
||||
if h.ssoPortalServer != nil {
|
||||
return errors.New("SSO portal server already running")
|
||||
}
|
||||
h.ssoPortalServer = &http.Server{
|
||||
Addr: ":" + strconv.Itoa(h.Config.PortalServerPort),
|
||||
Handler: h.ssoPortalMux,
|
||||
}
|
||||
err := h.ssoPortalServer.ListenAndServe()
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
h.Log("Failed to start SSO portal server", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// StopSSOPortal stop the SSO portal server
|
||||
func (h *SSOHandler) StopSSOPortal() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
err := h.ssoPortalServer.Shutdown(ctx)
|
||||
if err != nil {
|
||||
h.Log("Failed to stop SSO portal server", err)
|
||||
return err
|
||||
}
|
||||
h.ssoPortalServer = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartSSOPortal start the SSO portal server
|
||||
func (h *SSOHandler) RestartSSOServer() error {
|
||||
if h.ssoPortalServer != nil {
|
||||
err := h.StopSSOPortal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
go h.StartSSOPortal()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SSOHandler) IsRunning() bool {
|
||||
return h.ssoPortalServer != nil
|
||||
}
|
||||
|
||||
// HandleLogin handle the login request
|
||||
func (h *SSOHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
//Handle the login request
|
||||
username, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid username or password")
|
||||
return
|
||||
}
|
||||
|
||||
password, err := utils.PostPara(r, "password")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid username or password")
|
||||
return
|
||||
}
|
||||
|
||||
rememberMe, err := utils.PostBool(r, "remember_me")
|
||||
if err != nil {
|
||||
rememberMe = false
|
||||
}
|
||||
|
||||
//Check if the user exists
|
||||
userEntry, err := h.GetSSOUser(username)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the password is correct
|
||||
if !userEntry.VerifyPassword(password) {
|
||||
utils.SendErrorResponse(w, "incorrect password")
|
||||
return
|
||||
}
|
||||
|
||||
//Create a new session for the user
|
||||
session, _ := h.cookieStore.Get(r, "Zoraxy-SSO")
|
||||
session.Values["username"] = username
|
||||
if rememberMe {
|
||||
session.Options.MaxAge = 86400 * 15 //15 days
|
||||
} else {
|
||||
session.Options.MaxAge = 3600 //1 hour
|
||||
}
|
||||
session.Save(r, w) //Save the session
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
158
src/mod/auth/sso/sso.go
Normal file
158
src/mod/auth/sso/sso.go
Normal file
@ -0,0 +1,158 @@
|
||||
package sso
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
)
|
||||
|
||||
/*
|
||||
sso.go
|
||||
|
||||
This file contains the main SSO handler and the SSO configuration
|
||||
structure. It also contains the main SSO handler functions.
|
||||
|
||||
SSO web interface are stored in the static folder, which is embedded
|
||||
into the binary.
|
||||
*/
|
||||
|
||||
//go:embed static/*
|
||||
var staticFiles embed.FS //Static files for the SSO portal
|
||||
|
||||
type SSOConfig struct {
|
||||
SystemUUID string //System UUID, should be passed in from main scope
|
||||
AuthURL string //Authentication subdomain URL, e.g. auth.example.com
|
||||
PortalServerPort int //SSO portal server port
|
||||
Database *database.Database //System master key-value database
|
||||
Logger *logger.Logger
|
||||
}
|
||||
|
||||
// SSOHandler is the main SSO handler structure
|
||||
type SSOHandler struct {
|
||||
cookieStore *sessions.CookieStore
|
||||
ssoPortalServer *http.Server
|
||||
ssoPortalMux *http.ServeMux
|
||||
Oauth2Server *OAuth2Server
|
||||
Config *SSOConfig
|
||||
Apps map[string]RegisteredUpstreamApp
|
||||
}
|
||||
|
||||
// Create a new Zoraxy SSO handler
|
||||
func NewSSOHandler(config *SSOConfig) (*SSOHandler, error) {
|
||||
//Create a cookie store for the SSO handler
|
||||
cookieStore := sessions.NewCookieStore([]byte(config.SystemUUID))
|
||||
cookieStore.Options = &sessions.Options{
|
||||
Path: "",
|
||||
Domain: "",
|
||||
MaxAge: 0,
|
||||
Secure: false,
|
||||
HttpOnly: false,
|
||||
SameSite: 0,
|
||||
}
|
||||
|
||||
config.Database.NewTable("sso_users") //For storing user information
|
||||
config.Database.NewTable("sso_conf") //For storing SSO configuration
|
||||
config.Database.NewTable("sso_apps") //For storing registered apps
|
||||
|
||||
//Create the SSO Handler
|
||||
thisHandler := SSOHandler{
|
||||
cookieStore: cookieStore,
|
||||
Config: config,
|
||||
}
|
||||
|
||||
//Read the app info from database
|
||||
thisHandler.Apps = make(map[string]RegisteredUpstreamApp)
|
||||
|
||||
//Create an oauth2 server
|
||||
oauth2Server, err := NewOAuth2Server(config, &thisHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Register endpoints
|
||||
thisHandler.Oauth2Server = oauth2Server
|
||||
thisHandler.InitSSOPortal(config.PortalServerPort)
|
||||
|
||||
return &thisHandler, nil
|
||||
}
|
||||
|
||||
func (h *SSOHandler) RestorePreviousRunningState() {
|
||||
//Load the previous SSO state
|
||||
ssoEnabled := false
|
||||
ssoPort := 5488
|
||||
ssoAuthURL := ""
|
||||
h.Config.Database.Read("sso_conf", "enabled", &ssoEnabled)
|
||||
h.Config.Database.Read("sso_conf", "port", &ssoPort)
|
||||
h.Config.Database.Read("sso_conf", "authurl", &ssoAuthURL)
|
||||
|
||||
if ssoAuthURL == "" {
|
||||
//Cannot enable SSO without auth URL
|
||||
ssoEnabled = false
|
||||
}
|
||||
|
||||
h.Config.PortalServerPort = ssoPort
|
||||
h.Config.AuthURL = ssoAuthURL
|
||||
|
||||
if ssoEnabled {
|
||||
go h.StartSSOPortal()
|
||||
}
|
||||
}
|
||||
|
||||
// ServeForwardAuth handle the SSO request in interception mode
|
||||
// Suppose to be called in dynamicproxy.
|
||||
// Return true if the request is allowed to pass, false if the request is blocked and shall not be further processed
|
||||
func (h *SSOHandler) ServeForwardAuth(w http.ResponseWriter, r *http.Request) bool {
|
||||
//Get the current uri for appending to the auth subdomain
|
||||
originalRequestURL := r.RequestURI
|
||||
|
||||
redirectAuthURL := h.Config.AuthURL
|
||||
if redirectAuthURL == "" || !h.IsRunning() {
|
||||
//Redirect not set or auth server is offlined
|
||||
w.Write([]byte("SSO auth URL not set or SSO server offline."))
|
||||
//TODO: Use better looking template if exists
|
||||
return false
|
||||
}
|
||||
|
||||
//Check if the user have the cookie "Zoraxy-SSO" set
|
||||
session, err := h.cookieStore.Get(r, "Zoraxy-SSO")
|
||||
if err != nil {
|
||||
//Redirect to auth subdomain
|
||||
http.Redirect(w, r, redirectAuthURL+"/sso/login?m=new&t="+originalRequestURL, http.StatusFound)
|
||||
return false
|
||||
}
|
||||
|
||||
//Check if the user is logged in
|
||||
if session.Values["username"] != true {
|
||||
//Redirect to auth subdomain
|
||||
http.Redirect(w, r, redirectAuthURL+"/sso/login?m=expired&t="+originalRequestURL, http.StatusFound)
|
||||
return false
|
||||
}
|
||||
|
||||
//Check if the current request subdomain is allowed
|
||||
userName := session.Values["username"].(string)
|
||||
user, err := h.GetSSOUser(userName)
|
||||
if err != nil {
|
||||
//User might have been removed from SSO. Redirect to auth subdomain
|
||||
http.Redirect(w, r, redirectAuthURL, http.StatusFound)
|
||||
return false
|
||||
}
|
||||
|
||||
//Check if the user have access to the current subdomain
|
||||
if !user.Subdomains[r.Host].AllowAccess {
|
||||
//User is not allowed to access the current subdomain. Sent 403
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
//TODO: Use better looking template if exists
|
||||
return false
|
||||
}
|
||||
|
||||
//User is logged in, continue to the next handler
|
||||
return true
|
||||
}
|
||||
|
||||
// Log a message with the SSO module tag
|
||||
func (h *SSOHandler) Log(message string, err error) {
|
||||
h.Config.Logger.PrintAndLog("SSO", message, err)
|
||||
}
|
33
src/mod/auth/sso/static/auth.html
Normal file
33
src/mod/auth/sso/static/auth.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Auth</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
|
||||
/>
|
||||
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="jumbotron">
|
||||
<form action="/oauth2/authorize" method="POST">
|
||||
<h1>Authorize</h1>
|
||||
<p>The client would like to perform actions on your behalf.</p>
|
||||
<p>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-lg"
|
||||
style="width:200px;"
|
||||
>
|
||||
Allow
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
43
src/mod/auth/sso/static/index.html
Normal file
43
src/mod/auth/sso/static/index.html
Normal file
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login Page</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="ui container">
|
||||
<div class="ui middle aligned center aligned grid">
|
||||
<div class="column">
|
||||
<h2 class="ui teal image header">
|
||||
<div class="content">
|
||||
Log in to your account
|
||||
</div>
|
||||
</h2>
|
||||
<form class="ui large form">
|
||||
<div class="ui stacked segment">
|
||||
<div class="field">
|
||||
<div class="ui left icon input">
|
||||
<i class="user icon"></i>
|
||||
<input type="text" name="username" placeholder="Username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui left icon input">
|
||||
<i class="lock icon"></i>
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui fluid large teal submit button">Login</div>
|
||||
</div>
|
||||
<div class="ui error message"></div>
|
||||
</form>
|
||||
<div class="ui message">
|
||||
New to us? <a href="#">Sign Up</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
|
||||
</body>
|
||||
</html>
|
29
src/mod/auth/sso/static/login.html
Normal file
29
src/mod/auth/sso/static/login.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
|
||||
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Login In</h1>
|
||||
<form action="/oauth2/login" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="username">User Name</label>
|
||||
<input type="text" class="form-control" name="username" required placeholder="Please enter your user name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" name="password" placeholder="Please enter your password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
309
src/mod/auth/sso/userHandlers.go
Normal file
309
src/mod/auth/sso/userHandlers.go
Normal file
@ -0,0 +1,309 @@
|
||||
package sso
|
||||
|
||||
/*
|
||||
userHandlers.go
|
||||
Handlers for SSO user management
|
||||
|
||||
If you are looking for handlers that changes the settings
|
||||
of the SSO portal (e.g. authURL or port), please refer to
|
||||
handlers.go.
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// HandleAddUser handle the request to add a new user to the SSO system
|
||||
func (s *SSOHandler) HandleAddUser(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid username given")
|
||||
return
|
||||
}
|
||||
|
||||
password, err := utils.PostPara(r, "password")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid password given")
|
||||
return
|
||||
}
|
||||
|
||||
newUserId, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to generate new user ID")
|
||||
return
|
||||
}
|
||||
|
||||
//Create a new user entry
|
||||
thisUserEntry := UserEntry{
|
||||
UserID: newUserId.String(),
|
||||
Username: username,
|
||||
PasswordHash: auth.Hash(password),
|
||||
TOTPCode: "",
|
||||
Enable2FA: false,
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(thisUserEntry)
|
||||
|
||||
//Create a new user in the database
|
||||
err = s.Config.Database.Write("sso_users", newUserId.String(), string(js))
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to create new user")
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Edit user information, only accept change of username, password and enabled subdomain filed
|
||||
func (s *SSOHandler) HandleEditUser(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := utils.PostPara(r, "user_id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid user ID given")
|
||||
return
|
||||
}
|
||||
|
||||
if !(s.SSOUserExists(userID)) {
|
||||
utils.SendErrorResponse(w, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
//Load the user entry from database
|
||||
userEntry, err := s.GetSSOUser(userID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to load user entry")
|
||||
return
|
||||
}
|
||||
|
||||
//Update each of the fields if it is provided
|
||||
username, err := utils.PostPara(r, "username")
|
||||
if err == nil {
|
||||
userEntry.Username = username
|
||||
}
|
||||
|
||||
password, err := utils.PostPara(r, "password")
|
||||
if err == nil {
|
||||
userEntry.PasswordHash = auth.Hash(password)
|
||||
}
|
||||
|
||||
//Update the user entry in the database
|
||||
js, _ := json.Marshal(userEntry)
|
||||
err = s.Config.Database.Write("sso_users", userID, string(js))
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to update user entry")
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleRemoveUser remove a user from the SSO system
|
||||
func (s *SSOHandler) HandleRemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
userID, err := utils.PostPara(r, "user_id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid user ID given")
|
||||
return
|
||||
}
|
||||
|
||||
if !(s.SSOUserExists(userID)) {
|
||||
utils.SendErrorResponse(w, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
//Remove the user from the database
|
||||
err = s.Config.Database.Delete("sso_users", userID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to remove user")
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleListUser list all users in the SSO system
|
||||
func (s *SSOHandler) HandleListUser(w http.ResponseWriter, r *http.Request) {
|
||||
ssoUsers, err := s.ListSSOUsers()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to list users")
|
||||
return
|
||||
}
|
||||
js, _ := json.Marshal(ssoUsers)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// HandleAddSubdomain add a subdomain to a user
|
||||
func (s *SSOHandler) HandleAddSubdomain(w http.ResponseWriter, r *http.Request) {
|
||||
userid, err := utils.PostPara(r, "user_id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid user ID given")
|
||||
return
|
||||
}
|
||||
|
||||
if !(s.SSOUserExists(userid)) {
|
||||
utils.SendErrorResponse(w, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
UserEntry, err := s.GetSSOUser(userid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to load user entry")
|
||||
return
|
||||
}
|
||||
|
||||
subdomain, err := utils.PostPara(r, "subdomain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid subdomain given")
|
||||
return
|
||||
}
|
||||
|
||||
allowAccess, err := utils.PostBool(r, "allow_access")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid allow access value given")
|
||||
return
|
||||
}
|
||||
|
||||
UserEntry.Subdomains[subdomain] = &SubdomainAccessRule{
|
||||
Subdomain: subdomain,
|
||||
AllowAccess: allowAccess,
|
||||
}
|
||||
|
||||
err = UserEntry.Update()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to update user entry")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleRemoveSubdomain remove a subdomain from a user
|
||||
func (s *SSOHandler) HandleRemoveSubdomain(w http.ResponseWriter, r *http.Request) {
|
||||
userid, err := utils.PostPara(r, "user_id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid user ID given")
|
||||
return
|
||||
}
|
||||
|
||||
if !(s.SSOUserExists(userid)) {
|
||||
utils.SendErrorResponse(w, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
UserEntry, err := s.GetSSOUser(userid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to load user entry")
|
||||
return
|
||||
}
|
||||
|
||||
subdomain, err := utils.PostPara(r, "subdomain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid subdomain given")
|
||||
return
|
||||
}
|
||||
|
||||
delete(UserEntry.Subdomains, subdomain)
|
||||
|
||||
err = UserEntry.Update()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to update user entry")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleEnable2FA enable 2FA for a user
|
||||
func (s *SSOHandler) HandleEnable2FA(w http.ResponseWriter, r *http.Request) {
|
||||
userid, err := utils.PostPara(r, "user_id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid user ID given")
|
||||
return
|
||||
}
|
||||
|
||||
if !(s.SSOUserExists(userid)) {
|
||||
utils.SendErrorResponse(w, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
UserEntry, err := s.GetSSOUser(userid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to load user entry")
|
||||
return
|
||||
}
|
||||
|
||||
UserEntry.Enable2FA = true
|
||||
provisionUri, err := UserEntry.ResetTotp(UserEntry.UserID, "Zoraxy-SSO")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to reset TOTP")
|
||||
return
|
||||
}
|
||||
//As the ResetTotp function will update the user entry in the database, no need to call Update here
|
||||
|
||||
js, _ := json.Marshal(provisionUri)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Handle Disable 2FA for a user
|
||||
func (s *SSOHandler) HandleDisable2FA(w http.ResponseWriter, r *http.Request) {
|
||||
userid, err := utils.PostPara(r, "user_id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid user ID given")
|
||||
return
|
||||
}
|
||||
|
||||
if !(s.SSOUserExists(userid)) {
|
||||
utils.SendErrorResponse(w, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
UserEntry, err := s.GetSSOUser(userid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to load user entry")
|
||||
return
|
||||
}
|
||||
|
||||
UserEntry.Enable2FA = false
|
||||
UserEntry.TOTPCode = ""
|
||||
|
||||
err = UserEntry.Update()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to update user entry")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// HandleVerify2FA verify the 2FA code for a user
|
||||
func (s *SSOHandler) HandleVerify2FA(w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
userid, err := utils.PostPara(r, "user_id")
|
||||
if err != nil {
|
||||
return false, errors.New("invalid user ID given")
|
||||
}
|
||||
|
||||
if !(s.SSOUserExists(userid)) {
|
||||
utils.SendErrorResponse(w, "user not found")
|
||||
return false, errors.New("user not found")
|
||||
}
|
||||
|
||||
UserEntry, err := s.GetSSOUser(userid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to load user entry")
|
||||
return false, errors.New("failed to load user entry")
|
||||
}
|
||||
|
||||
totpCode, _ := utils.PostPara(r, "totp_code")
|
||||
|
||||
if !UserEntry.Enable2FA {
|
||||
//If 2FA is not enabled, return true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !UserEntry.VerifyTotp(totpCode) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
141
src/mod/auth/sso/users.go
Normal file
141
src/mod/auth/sso/users.go
Normal file
@ -0,0 +1,141 @@
|
||||
package sso
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/xlzd/gotp"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
)
|
||||
|
||||
/*
|
||||
users.go
|
||||
|
||||
This file contains the user structure and user management
|
||||
functions for the SSO module.
|
||||
|
||||
If you are looking for handlers, please refer to handlers.go.
|
||||
*/
|
||||
|
||||
type SubdomainAccessRule struct {
|
||||
Subdomain string
|
||||
AllowAccess bool
|
||||
}
|
||||
|
||||
type UserEntry struct {
|
||||
UserID string `json:sub` //User ID
|
||||
Username string `json:"name"` //Username
|
||||
Email string `json:"email"` //Email
|
||||
PasswordHash string `json:"passwordhash"` //Password hash
|
||||
TOTPCode string `json:"totpcode"` //TOTP code
|
||||
Enable2FA bool `json:"enable2fa"` //Enable 2FA
|
||||
Subdomains map[string]*SubdomainAccessRule `json:"subdomains"` //Subdomain access rules
|
||||
LastLogin int64 `json:"lastlogin"` //Last login time
|
||||
LastLoginIP string `json:"lastloginip"` //Last login IP
|
||||
LastLoginCountry string `json:"lastlogincountry"` //Last login country
|
||||
parent *SSOHandler //Parent SSO handler
|
||||
}
|
||||
|
||||
type ClientResponse struct {
|
||||
Sub string `json:"sub"` //User ID
|
||||
Name string `json:"name"` //Username
|
||||
Nickname string `json:"nickname"` //Nickname
|
||||
PreferredUsername string `json:"preferred_username"` //Preferred Username
|
||||
Email string `json:"email"` //Email
|
||||
Locale string `json:"locale"` //Locale
|
||||
Website string `json:"website"` //Website
|
||||
}
|
||||
|
||||
func (s *SSOHandler) SSOUserExists(userid string) bool {
|
||||
//Check if the user exists in the database
|
||||
var userEntry UserEntry
|
||||
err := s.Config.Database.Read("sso_users", userid, &userEntry)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *SSOHandler) GetSSOUser(userid string) (UserEntry, error) {
|
||||
//Load the user entry from database
|
||||
var userEntry UserEntry
|
||||
err := s.Config.Database.Read("sso_users", userid, &userEntry)
|
||||
if err != nil {
|
||||
return UserEntry{}, err
|
||||
}
|
||||
userEntry.parent = s
|
||||
return userEntry, nil
|
||||
}
|
||||
|
||||
func (s *SSOHandler) ListSSOUsers() ([]*UserEntry, error) {
|
||||
entries, err := s.Config.Database.ListTable("sso_users")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ssoUsers := []*UserEntry{}
|
||||
for _, keypairs := range entries {
|
||||
group := new(UserEntry)
|
||||
json.Unmarshal(keypairs[1], &group)
|
||||
group.parent = s
|
||||
ssoUsers = append(ssoUsers, group)
|
||||
}
|
||||
|
||||
return ssoUsers, nil
|
||||
}
|
||||
|
||||
// Validate the username and password
|
||||
func (s *SSOHandler) ValidateUsernameAndPassword(username string, password string) bool {
|
||||
//Validate the username and password
|
||||
var userEntry UserEntry
|
||||
err := s.Config.Database.Read("sso_users", username, &userEntry)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
//TODO: Remove after testing
|
||||
if (username == "test") && (password == "test") {
|
||||
return true
|
||||
}
|
||||
return userEntry.VerifyPassword(password)
|
||||
}
|
||||
|
||||
func (s *UserEntry) VerifyPassword(password string) bool {
|
||||
return s.PasswordHash == auth.Hash(password)
|
||||
}
|
||||
|
||||
// Write changes in the user entry back to the database
|
||||
func (u *UserEntry) Update() error {
|
||||
js, _ := json.Marshal(u)
|
||||
err := u.parent.Config.Database.Write("sso_users", u.UserID, string(js))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset and update the TOTP code for the current user
|
||||
// Return the provision uri of the new TOTP code for Google Authenticator
|
||||
func (u *UserEntry) ResetTotp(accountName string, issuerName string) (string, error) {
|
||||
u.TOTPCode = gotp.RandomSecret(16)
|
||||
totp := gotp.NewDefaultTOTP(u.TOTPCode)
|
||||
err := u.Update()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return totp.ProvisioningUri(accountName, issuerName), nil
|
||||
}
|
||||
|
||||
// Verify the TOTP code at current time
|
||||
func (u *UserEntry) VerifyTotp(enteredCode string) bool {
|
||||
totp := gotp.NewDefaultTOTP(u.TOTPCode)
|
||||
return totp.Verify(enteredCode, time.Now().Unix())
|
||||
}
|
||||
|
||||
func (u *UserEntry) GetClientResponse() ClientResponse {
|
||||
return ClientResponse{
|
||||
Sub: u.UserID,
|
||||
Name: u.Username,
|
||||
Nickname: u.Username,
|
||||
PreferredUsername: u.Username,
|
||||
Email: u.Email,
|
||||
Locale: "en",
|
||||
Website: "",
|
||||
}
|
||||
}
|
@ -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))
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
- Blacklist
|
||||
- Whitelist
|
||||
- Rate Limitor
|
||||
- SSO Auth
|
||||
- Basic Auth
|
||||
- Vitrual Directory Proxy
|
||||
- Subdomain Proxy
|
||||
@ -77,7 +78,16 @@ 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)
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 307)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//SSO Interception Mode
|
||||
if sep.UseSSOIntercept {
|
||||
allowPass := h.Parent.Option.SSOHandler.ServeForwardAuth(w, r)
|
||||
if !allowPass {
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "sso-x", 307)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -163,7 +173,6 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
fallthrough
|
||||
case DefaultSite_ReverseProxy:
|
||||
//They both share the same behavior
|
||||
|
||||
//Check if any virtual directory rules matches
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||
@ -188,8 +197,13 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
redirectTarget = "about:blank"
|
||||
}
|
||||
|
||||
//Check if the default site values start with http or https
|
||||
if !strings.HasPrefix(redirectTarget, "http://") && !strings.HasPrefix(redirectTarget, "https://") {
|
||||
redirectTarget = "http://" + redirectTarget
|
||||
}
|
||||
|
||||
//Check if it is an infinite loopback redirect
|
||||
parsedURL, err := url.Parse(proot.DefaultSiteValue)
|
||||
parsedURL, err := url.Parse(redirectTarget)
|
||||
if err != nil {
|
||||
//Error when parsing target. Send to root
|
||||
h.hostRequest(w, r, h.Parent.Root)
|
||||
|
@ -49,6 +49,9 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
|
||||
for _, cred := range pe.BasicAuthCredentials {
|
||||
if u == cred.Username && hashedPassword == cred.PasswordHash {
|
||||
matchingFound = true
|
||||
|
||||
//Set the X-Remote-User header
|
||||
r.Header.Set("X-Remote-User", u)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +1,12 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
)
|
||||
|
||||
/*
|
||||
CustomHeader.go
|
||||
|
||||
This script handle parsing and injecting custom headers
|
||||
into the dpcore routing logic
|
||||
|
||||
Updates: 2024-10-26
|
||||
Contents from this file has been moved to rewrite/rewrite.go
|
||||
This file is kept for contributors to understand the structure
|
||||
*/
|
||||
|
||||
// SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers
|
||||
// return upstream header and downstream header key-value pairs
|
||||
// if the header is expected to be deleted, the value will be set to empty string
|
||||
func (ept *ProxyEndpoint) SplitInboundOutboundHeaders() ([][]string, [][]string) {
|
||||
if len(ept.UserDefinedHeaders) == 0 && ept.HSTSMaxAge == 0 && !ept.EnablePermissionPolicyHeader {
|
||||
//Early return if there are no defined headers
|
||||
return [][]string{}, [][]string{}
|
||||
}
|
||||
|
||||
//Use pre-allocation for faster performance
|
||||
//Downstream +2 for Permission Policy and HSTS
|
||||
upstreamHeaders := make([][]string, len(ept.UserDefinedHeaders))
|
||||
downstreamHeaders := make([][]string, len(ept.UserDefinedHeaders)+2)
|
||||
upstreamHeaderCounter := 0
|
||||
downstreamHeaderCounter := 0
|
||||
|
||||
//Sort the headers into upstream or downstream
|
||||
for _, customHeader := range ept.UserDefinedHeaders {
|
||||
thisHeaderSet := make([]string, 2)
|
||||
thisHeaderSet[0] = customHeader.Key
|
||||
thisHeaderSet[1] = customHeader.Value
|
||||
if customHeader.IsRemove {
|
||||
//Prevent invalid config
|
||||
thisHeaderSet[1] = ""
|
||||
}
|
||||
|
||||
//Assign to slice
|
||||
if customHeader.Direction == HeaderDirection_ZoraxyToUpstream {
|
||||
upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet
|
||||
upstreamHeaderCounter++
|
||||
} else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream {
|
||||
downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet
|
||||
downstreamHeaderCounter++
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the endpoint require HSTS headers
|
||||
if ept.HSTSMaxAge > 0 {
|
||||
if ept.ContainsWildcardName(true) {
|
||||
//Endpoint listening domain includes wildcards.
|
||||
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge)) + "; includeSubdomains"}
|
||||
} else {
|
||||
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge))}
|
||||
}
|
||||
|
||||
downstreamHeaderCounter++
|
||||
}
|
||||
|
||||
//Check if the endpoint require Permission Policy
|
||||
if ept.EnablePermissionPolicyHeader {
|
||||
var usingPermissionPolicy *permissionpolicy.PermissionsPolicy
|
||||
if ept.PermissionPolicy != nil {
|
||||
//Custom permission policy
|
||||
usingPermissionPolicy = ept.PermissionPolicy
|
||||
} else {
|
||||
//Permission policy is enabled but not customized. Use default
|
||||
usingPermissionPolicy = permissionpolicy.GetDefaultPermissionPolicy()
|
||||
}
|
||||
|
||||
downstreamHeaders[downstreamHeaderCounter] = usingPermissionPolicy.ToKeyValueHeader()
|
||||
downstreamHeaderCounter++
|
||||
}
|
||||
|
||||
return upstreamHeaders, downstreamHeaders
|
||||
}
|
||||
|
@ -1,11 +1,19 @@
|
||||
package domainsniff
|
||||
|
||||
/*
|
||||
Domainsniff
|
||||
|
||||
This package contain codes that perform project / domain specific behavior in Zoraxy
|
||||
If you want Zoraxy to handle a particular domain or open source project in a special way,
|
||||
you can add the checking logic here.
|
||||
|
||||
*/
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
//Check if the domain is reachable and return err if not reachable
|
||||
// Check if the domain is reachable and return err if not reachable
|
||||
func DomainReachableWithError(domain string) error {
|
||||
timeout := 1 * time.Second
|
||||
conn, err := net.DialTimeout("tcp", domain, timeout)
|
||||
@ -17,7 +25,7 @@ func DomainReachableWithError(domain string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Check if domain reachable
|
||||
// Check if domain reachable
|
||||
func DomainReachable(domain string) bool {
|
||||
return DomainReachableWithError(domain) == nil
|
||||
}
|
||||
|
21
src/mod/dynamicproxy/domainsniff/proxmox.go
Normal file
21
src/mod/dynamicproxy/domainsniff/proxmox.go
Normal 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
|
||||
}
|
@ -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,8 +68,13 @@ type ResponseRewriteRuleSet struct {
|
||||
PathPrefix string //Vdir prefix for root, / will be rewrite to this
|
||||
UpstreamHeaders [][]string
|
||||
DownstreamHeaders [][]string
|
||||
NoRemoveHopByHop bool //Do not remove hop-by-hop headers, dangerous
|
||||
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 {
|
||||
@ -73,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 {
|
||||
@ -252,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)
|
||||
@ -281,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
|
||||
}
|
||||
@ -291,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)
|
||||
@ -302,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 {
|
||||
@ -309,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 {
|
||||
@ -328,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)
|
||||
|
||||
@ -375,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.
|
||||
@ -405,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()
|
||||
@ -420,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)
|
||||
@ -429,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
|
||||
@ -448,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)
|
||||
@ -457,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"))
|
||||
@ -466,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() {
|
||||
@ -479,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)
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,67 @@
|
||||
package dpcore_test
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
)
|
||||
|
||||
func TestReplaceLocationHost(t *testing.T) {
|
||||
urlString := "http://private.com/test/newtarget/"
|
||||
rrr := &dpcore.ResponseRewriteRuleSet{
|
||||
OriginalHost: "test.example.com",
|
||||
ProxyDomain: "private.com/test",
|
||||
UseTLS: true,
|
||||
}
|
||||
useTLS := true
|
||||
tests := []struct {
|
||||
name string
|
||||
urlString string
|
||||
rrr *dpcore.ResponseRewriteRuleSet
|
||||
useTLS bool
|
||||
expectedResult string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Basic HTTP to HTTPS redirection",
|
||||
urlString: "http://example.com/resource",
|
||||
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "example.com", OriginalHost: "proxy.example.com", UseTLS: true},
|
||||
useTLS: true,
|
||||
expectedResult: "https://proxy.example.com/resource",
|
||||
expectError: false,
|
||||
},
|
||||
|
||||
expectedResult := "https://test.example.com/newtarget/"
|
||||
|
||||
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred: %v", err)
|
||||
{
|
||||
name: "Basic HTTPS to HTTP redirection",
|
||||
urlString: "https://proxy.example.com/resource",
|
||||
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "proxy.example.com", OriginalHost: "proxy.example.com", UseTLS: false},
|
||||
useTLS: false,
|
||||
expectedResult: "http://proxy.example.com/resource",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "No rewrite on mismatched domain",
|
||||
urlString: "http://anotherdomain.com/resource",
|
||||
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "proxy.example.com", OriginalHost: "proxy.example.com", UseTLS: true},
|
||||
useTLS: true,
|
||||
expectedResult: "http://anotherdomain.com/resource",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Subpath trimming with HTTPS",
|
||||
urlString: "https://blog.example.com/post?id=1",
|
||||
rrr: &dpcore.ResponseRewriteRuleSet{ProxyDomain: "blog.example.com", OriginalHost: "proxy.example.com/blog", UseTLS: true},
|
||||
useTLS: true,
|
||||
expectedResult: "https://proxy.example.com/blog/post?id=1",
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
if result != expectedResult {
|
||||
t.Errorf("Expected: %s, but got: %s", expectedResult, result)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := dpcore.ReplaceLocationHost(tt.urlString, tt.rrr, tt.useTLS)
|
||||
if (err != nil) != tt.expectError {
|
||||
t.Errorf("Expected error: %v, got: %v", tt.expectError, err)
|
||||
}
|
||||
if result != tt.expectedResult {
|
||||
result, _ = url.QueryUnescape(result)
|
||||
t.Errorf("Expected result: %s, got: %s", tt.expectedResult, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +74,7 @@ func TestReplaceLocationHostRelative(t *testing.T) {
|
||||
}
|
||||
useTLS := true
|
||||
|
||||
expectedResult := "https://test.example.com/api/"
|
||||
expectedResult := "api/"
|
||||
|
||||
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
|
||||
if err != nil {
|
||||
|
@ -1,7 +1,10 @@
|
||||
package dpcore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
@ -57,7 +60,7 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// Debug functions
|
||||
// Debug functions for replaceLocationHost
|
||||
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
|
||||
return replaceLocationHost(urlString, rrr, useTLS)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
@ -158,12 +158,13 @@ func (router *Router) StartProxyService() error {
|
||||
router.logRequest(r, false, 404, "vdir-http", r.Host)
|
||||
}
|
||||
selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: selectedUpstream.OriginIpOrDomain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: selectedUpstream.RequireTLS,
|
||||
NoRemoveHopByHop: sep.DisableHopByHopHeaderRemoval,
|
||||
PathPrefix: "",
|
||||
Version: sep.parent.Option.HostVersion,
|
||||
ProxyDomain: selectedUpstream.OriginIpOrDomain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: selectedUpstream.RequireTLS,
|
||||
HostHeaderOverwrite: sep.RequestHostOverwrite,
|
||||
NoRemoveHopByHop: sep.DisableHopByHopHeaderRemoval,
|
||||
PathPrefix: "",
|
||||
Version: sep.parent.Option.HostVersion,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -36,7 +37,7 @@ func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
|
||||
|
||||
// Remvoe a user defined header from the list
|
||||
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
|
||||
newHeaderList := []*UserDefinedHeader{}
|
||||
newHeaderList := []*rewrite.UserDefinedHeader{}
|
||||
for _, header := range ep.UserDefinedHeaders {
|
||||
if !strings.EqualFold(header.Key, key) {
|
||||
newHeaderList = append(newHeaderList, header)
|
||||
@ -49,7 +50,7 @@ func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
|
||||
}
|
||||
|
||||
// Add a user defined header to the list, duplicates will be automatically removed
|
||||
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *UserDefinedHeader) error {
|
||||
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *rewrite.UserDefinedHeader) error {
|
||||
if ep.UserDefinedHeaderExists(newHeaderRule.Key) {
|
||||
ep.RemoveUserDefinedHeader(newHeaderRule.Key)
|
||||
}
|
||||
|
@ -83,6 +83,10 @@ func GetUpstreamsAsString(upstreams []*Upstream) string {
|
||||
for _, upstream := range upstreams {
|
||||
targets = append(targets, upstream.String())
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
//No upstream
|
||||
return "(no upstream config)"
|
||||
}
|
||||
return strings.Join(targets, ", ")
|
||||
}
|
||||
|
||||
@ -93,7 +97,7 @@ func (m *RouteManager) Close() {
|
||||
|
||||
}
|
||||
|
||||
// Print debug message
|
||||
func (m *RouteManager) debugPrint(message string, err error) {
|
||||
// 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)
|
||||
}
|
||||
|
@ -2,8 +2,6 @@ package loadbalance
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
)
|
||||
@ -29,7 +27,7 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
|
||||
//No valid session found. Assign a new upstream
|
||||
targetOrigin, index, err := getRandomUpstreamByWeight(origins)
|
||||
if err != nil {
|
||||
fmt.Println("Oops. Unable to get random upstream")
|
||||
m.println("Unable to get random upstream", err)
|
||||
targetOrigin = origins[0]
|
||||
index = 0
|
||||
}
|
||||
@ -44,7 +42,7 @@ func (m *RouteManager) GetRequestUpstreamTarget(w http.ResponseWriter, r *http.R
|
||||
var err error
|
||||
targetOrigin, _, err = getRandomUpstreamByWeight(origins)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
m.println("Failed to get next origin", err)
|
||||
targetOrigin = origins[0]
|
||||
}
|
||||
|
||||
@ -102,42 +100,66 @@ func (m *RouteManager) getSessionHandler(r *http.Request, upstreams []*Upstream)
|
||||
/* 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) {
|
||||
var ret *Upstream
|
||||
sum := 0
|
||||
for _, c := range upstreams {
|
||||
sum += c.Weight
|
||||
}
|
||||
r, err := intRange(0, sum)
|
||||
if err != nil {
|
||||
return ret, -1, err
|
||||
}
|
||||
counter := 0
|
||||
for _, c := range upstreams {
|
||||
r -= c.Weight
|
||||
if r < 0 {
|
||||
return c, counter, nil
|
||||
}
|
||||
counter++
|
||||
// If there is only one upstream, return it
|
||||
if len(upstreams) == 1 {
|
||||
return upstreams[0], 0, nil
|
||||
}
|
||||
|
||||
if ret == nil {
|
||||
//All fallback
|
||||
//use the first one that is with weight = 0
|
||||
fallbackUpstreams := []*Upstream{}
|
||||
fallbackUpstreamsOriginalID := []int{}
|
||||
for ix, upstream := range upstreams {
|
||||
if upstream.Weight == 0 {
|
||||
fallbackUpstreams = append(fallbackUpstreams, upstream)
|
||||
fallbackUpstreamsOriginalID = append(fallbackUpstreamsOriginalID, ix)
|
||||
}
|
||||
}
|
||||
upstreamID := rand.Intn(len(fallbackUpstreams))
|
||||
return fallbackUpstreams[upstreamID], fallbackUpstreamsOriginalID[upstreamID], nil
|
||||
// Preserve the index with upstreams
|
||||
type upstreamWithIndex struct {
|
||||
Upstream *Upstream
|
||||
Index int
|
||||
}
|
||||
return ret, -1, errors.New("failed to pick an upstream origin server")
|
||||
|
||||
// Calculate total weight for upstreams with weight > 0
|
||||
totalWeight := 0
|
||||
fallbackUpstreams := make([]upstreamWithIndex, 0, len(upstreams))
|
||||
|
||||
for index, upstream := range upstreams {
|
||||
if upstream.Weight > 0 {
|
||||
totalWeight += upstream.Weight
|
||||
} else {
|
||||
// Collect fallback upstreams
|
||||
fallbackUpstreams = append(fallbackUpstreams, upstreamWithIndex{upstream, index})
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no upstreams with weight > 0, return a fallback upstream if available
|
||||
if totalWeight == 0 {
|
||||
if len(fallbackUpstreams) > 0 {
|
||||
// Randomly select one of the fallback upstreams
|
||||
randIndex := rand.Intn(len(fallbackUpstreams))
|
||||
return fallbackUpstreams[randIndex].Upstream, fallbackUpstreams[randIndex].Index, nil
|
||||
}
|
||||
// No upstreams available at all
|
||||
return nil, -1, errors.New("no valid upstream servers available")
|
||||
}
|
||||
|
||||
// Random weight between 0 and total weight
|
||||
randomWeight := rand.Intn(totalWeight)
|
||||
|
||||
// Select an upstream based on the random weight
|
||||
for index, upstream := range upstreams {
|
||||
if upstream.Weight > 0 { // Only consider upstreams with weight > 0
|
||||
if randomWeight < upstream.Weight {
|
||||
// Return the selected upstream and its index
|
||||
return upstream, index, nil
|
||||
}
|
||||
randomWeight -= upstream.Weight
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, it means we should return a fallback upstream if available
|
||||
if len(fallbackUpstreams) > 0 {
|
||||
randIndex := rand.Intn(len(fallbackUpstreams))
|
||||
return fallbackUpstreams[randIndex].Upstream, fallbackUpstreams[randIndex].Index, nil
|
||||
}
|
||||
|
||||
return nil, -1, errors.New("failed to pick an upstream origin server")
|
||||
}
|
||||
|
||||
// IntRange returns a random integer in the range from min to max.
|
||||
/*
|
||||
func intRange(min, max int) (int, error) {
|
||||
var result int
|
||||
switch {
|
||||
@ -152,3 +174,4 @@ func intRange(min, max int) (int, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
*/
|
||||
|
100
src/mod/dynamicproxy/loadbalance/originPicker_test.go
Normal file
100
src/mod/dynamicproxy/loadbalance/originPicker_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
package loadbalance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// func getRandomUpstreamByWeight(upstreams []*Upstream) (*Upstream, int, error) { ... }
|
||||
func TestRandomUpstreamSelection(t *testing.T) {
|
||||
rand.Seed(time.Now().UnixNano()) // Seed for randomness
|
||||
|
||||
// Define some test upstreams
|
||||
upstreams := []*Upstream{
|
||||
{
|
||||
OriginIpOrDomain: "192.168.1.1:8080",
|
||||
RequireTLS: false,
|
||||
SkipCertValidations: false,
|
||||
SkipWebSocketOriginCheck: false,
|
||||
Weight: 1,
|
||||
MaxConn: 0, // No connection limit for now
|
||||
},
|
||||
{
|
||||
OriginIpOrDomain: "192.168.1.2:8080",
|
||||
RequireTLS: false,
|
||||
SkipCertValidations: false,
|
||||
SkipWebSocketOriginCheck: false,
|
||||
Weight: 1,
|
||||
MaxConn: 0,
|
||||
},
|
||||
{
|
||||
OriginIpOrDomain: "192.168.1.3:8080",
|
||||
RequireTLS: true,
|
||||
SkipCertValidations: true,
|
||||
SkipWebSocketOriginCheck: true,
|
||||
Weight: 1,
|
||||
MaxConn: 0,
|
||||
},
|
||||
{
|
||||
OriginIpOrDomain: "192.168.1.4:8080",
|
||||
RequireTLS: true,
|
||||
SkipCertValidations: true,
|
||||
SkipWebSocketOriginCheck: true,
|
||||
Weight: 1,
|
||||
MaxConn: 0,
|
||||
},
|
||||
}
|
||||
|
||||
// Track how many times each upstream is selected
|
||||
selectionCount := make(map[string]int)
|
||||
totalPicks := 10000 // Number of times to call getRandomUpstreamByWeight
|
||||
//expectedPickCount := totalPicks / len(upstreams) // Ideal count for each upstream
|
||||
|
||||
// Pick upstreams and record their selection count
|
||||
for i := 0; i < totalPicks; i++ {
|
||||
upstream, _, err := getRandomUpstreamByWeight(upstreams)
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting random upstream: %v", err)
|
||||
}
|
||||
selectionCount[upstream.OriginIpOrDomain]++
|
||||
}
|
||||
|
||||
// Condition 1: Ensure every upstream has been picked at least once
|
||||
for _, upstream := range upstreams {
|
||||
if selectionCount[upstream.OriginIpOrDomain] == 0 {
|
||||
t.Errorf("Upstream %s was never selected", upstream.OriginIpOrDomain)
|
||||
}
|
||||
}
|
||||
|
||||
// Condition 2: Check that the distribution is within 1-2 standard deviations
|
||||
counts := make([]float64, len(upstreams))
|
||||
for i, upstream := range upstreams {
|
||||
counts[i] = float64(selectionCount[upstream.OriginIpOrDomain])
|
||||
}
|
||||
|
||||
mean := float64(totalPicks) / float64(len(upstreams))
|
||||
stddev := calculateStdDev(counts, mean)
|
||||
|
||||
tolerance := 2 * stddev // Allowing up to 2 standard deviations
|
||||
for i, count := range counts {
|
||||
if math.Abs(count-mean) > tolerance {
|
||||
t.Errorf("Selection of upstream %s is outside acceptable range: %v picks (mean: %v, stddev: %v)", upstreams[i].OriginIpOrDomain, count, mean, stddev)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Selection count:", selectionCount)
|
||||
fmt.Printf("Mean: %.2f, StdDev: %.2f\n", mean, stddev)
|
||||
}
|
||||
|
||||
// Helper function to calculate standard deviation
|
||||
func calculateStdDev(data []float64, mean float64) float64 {
|
||||
var sumOfSquares float64
|
||||
for _, value := range data {
|
||||
sumOfSquares += (value - mean) * (value - mean)
|
||||
}
|
||||
variance := sumOfSquares / float64(len(data))
|
||||
return math.Sqrt(variance)
|
||||
}
|
@ -61,8 +61,8 @@ func (u *Upstream) Clone() *Upstream {
|
||||
return &newUpstream
|
||||
}
|
||||
|
||||
// ServeHTTP uses this upstream proxy router to route the current request
|
||||
func (u *Upstream) ServeHTTP(w http.ResponseWriter, r *http.Request, rrr *dpcore.ResponseRewriteRuleSet) error {
|
||||
// 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
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/websocketproxy"
|
||||
@ -112,13 +113,17 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
|
||||
func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
|
||||
/* Load balancing */
|
||||
selectedUpstream, err := h.Parent.loadBalancer.GetRequestUpstreamTarget(w, r, target.ActiveOrigins, target.UseStickySession)
|
||||
if err != nil {
|
||||
http.ServeFile(w, r, "./web/rperror.html")
|
||||
log.Println(err.Error())
|
||||
h.Parent.Option.Logger.PrintAndLog("proxy", "Failed to assign an upstream for this request", err)
|
||||
h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname())
|
||||
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
|
||||
@ -140,6 +145,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
|
||||
SkipTLSValidation: selectedUpstream.SkipCertValidations,
|
||||
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
|
||||
Logger: h.Parent.Option.Logger,
|
||||
})
|
||||
wspHandler.ServeHTTP(w, r)
|
||||
return
|
||||
@ -153,35 +159,45 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
//Build downstream and upstream header rules
|
||||
upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders()
|
||||
//Populate the user-defined headers with the values from the request
|
||||
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.UserDefinedHeaders)
|
||||
|
||||
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,
|
||||
NoRemoveHopByHop: target.DisableHopByHopHeaderRemoval,
|
||||
Version: target.parent.Option.HostVersion,
|
||||
//Build downstream and upstream header rules
|
||||
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
|
||||
UserDefinedHeaders: rewrittenUserDefinedHeaders,
|
||||
HSTSMaxAge: target.HSTSMaxAge,
|
||||
HSTSIncludeSubdomains: target.ContainsWildcardName(true),
|
||||
EnablePermissionPolicyHeader: target.EnablePermissionPolicyHeader,
|
||||
PermissionPolicy: target.PermissionPolicy,
|
||||
})
|
||||
|
||||
//Handle the request reverse proxy
|
||||
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.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname())
|
||||
} else {
|
||||
http.ServeFile(w, r, "./web/rperror.html")
|
||||
log.Println(err.Error())
|
||||
//TODO: Take this upstream offline automatically
|
||||
h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname())
|
||||
}
|
||||
}
|
||||
|
||||
h.Parent.logRequest(r, true, 200, "host-http", r.URL.Hostname())
|
||||
h.Parent.logRequest(r, true, statusCode, "host-http", r.URL.Hostname())
|
||||
}
|
||||
|
||||
// Handle vdir type request
|
||||
@ -207,6 +223,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
|
||||
SkipTLSValidation: target.SkipCertValidations,
|
||||
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
|
||||
@ -220,17 +237,28 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
//Build downstream and upstream header rules
|
||||
upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders()
|
||||
//Populate the user-defined headers with the values from the request
|
||||
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.parent.UserDefinedHeaders)
|
||||
|
||||
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,
|
||||
//Build downstream and upstream header rules, use the parent (subdomain) endpoint's headers
|
||||
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
|
||||
UserDefinedHeaders: rewrittenUserDefinedHeaders,
|
||||
HSTSMaxAge: target.parent.HSTSMaxAge,
|
||||
HSTSIncludeSubdomains: target.parent.ContainsWildcardName(true),
|
||||
EnablePermissionPolicyHeader: target.parent.EnablePermissionPolicyHeader,
|
||||
PermissionPolicy: target.parent.PermissionPolicy,
|
||||
})
|
||||
|
||||
//Handle the virtual directory reverse proxy request
|
||||
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
|
||||
@ -245,7 +273,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
h.Parent.logRequest(r, false, 521, "vdir-http", target.Domain)
|
||||
}
|
||||
}
|
||||
h.Parent.logRequest(r, true, 200, "vdir-http", target.Domain)
|
||||
h.Parent.logRequest(r, true, statusCode, "vdir-http", target.Domain)
|
||||
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
63
src/mod/dynamicproxy/rewrite/headervars.go
Normal file
63
src/mod/dynamicproxy/rewrite/headervars.go
Normal file
@ -0,0 +1,63 @@
|
||||
package rewrite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetHeaderVariableValuesFromRequest returns a map of header variables and their values
|
||||
// note that variables behavior is not exactly identical to nginx variables
|
||||
func GetHeaderVariableValuesFromRequest(r *http.Request) map[string]string {
|
||||
vars := make(map[string]string)
|
||||
|
||||
// Request-specific variables
|
||||
vars["$host"] = r.Host
|
||||
vars["$remote_addr"] = r.RemoteAddr
|
||||
vars["$request_uri"] = r.RequestURI
|
||||
vars["$request_method"] = r.Method
|
||||
vars["$content_length"] = fmt.Sprintf("%d", r.ContentLength)
|
||||
vars["$content_type"] = r.Header.Get("Content-Type")
|
||||
|
||||
// Parsed URI elements
|
||||
vars["$uri"] = r.URL.Path
|
||||
vars["$args"] = r.URL.RawQuery
|
||||
vars["$scheme"] = r.URL.Scheme
|
||||
vars["$query_string"] = r.URL.RawQuery
|
||||
|
||||
// User agent and referer
|
||||
vars["$http_user_agent"] = r.UserAgent()
|
||||
vars["$http_referer"] = r.Referer()
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
// CustomHeadersIncludeDynamicVariables checks if the user-defined headers contain dynamic variables
|
||||
// use for early exit when processing the headers
|
||||
func CustomHeadersIncludeDynamicVariables(userDefinedHeaders []*UserDefinedHeader) bool {
|
||||
for _, header := range userDefinedHeaders {
|
||||
if strings.Contains(header.Value, "$") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PopulateRequestHeaderVariables populates the user-defined headers with the values from the request
|
||||
func PopulateRequestHeaderVariables(r *http.Request, userDefinedHeaders []*UserDefinedHeader) []*UserDefinedHeader {
|
||||
if !CustomHeadersIncludeDynamicVariables(userDefinedHeaders) {
|
||||
// Early exit if there are no dynamic variables
|
||||
return userDefinedHeaders
|
||||
}
|
||||
vars := GetHeaderVariableValuesFromRequest(r)
|
||||
populatedHeaders := []*UserDefinedHeader{}
|
||||
// Populate the user-defined headers with the values from the request
|
||||
for _, header := range userDefinedHeaders {
|
||||
thisHeader := header.Copy()
|
||||
for key, value := range vars {
|
||||
thisHeader.Value = strings.ReplaceAll(thisHeader.Value, key, value)
|
||||
}
|
||||
populatedHeaders = append(populatedHeaders, thisHeader)
|
||||
}
|
||||
return populatedHeaders
|
||||
}
|
172
src/mod/dynamicproxy/rewrite/headervars_test.go
Normal file
172
src/mod/dynamicproxy/rewrite/headervars_test.go
Normal file
@ -0,0 +1,172 @@
|
||||
package rewrite
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetHeaderVariableValuesFromRequest(t *testing.T) {
|
||||
// Create a sample request
|
||||
req := httptest.NewRequest("GET", "https://example.com/test?foo=bar", nil)
|
||||
req.Host = "example.com"
|
||||
req.RemoteAddr = "192.168.1.1:12345"
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "TestAgent")
|
||||
req.Header.Set("Referer", "https://referer.com")
|
||||
|
||||
// Call the function
|
||||
vars := GetHeaderVariableValuesFromRequest(req)
|
||||
|
||||
// Expected results
|
||||
expected := map[string]string{
|
||||
"$host": "example.com",
|
||||
"$remote_addr": "192.168.1.1:12345",
|
||||
"$request_uri": "https://example.com/test?foo=bar",
|
||||
"$request_method": "GET",
|
||||
"$content_length": "0", // ContentLength is 0 because there's no body in the request
|
||||
"$content_type": "application/json",
|
||||
"$uri": "/test",
|
||||
"$args": "foo=bar",
|
||||
"$scheme": "https",
|
||||
"$query_string": "foo=bar",
|
||||
"$http_user_agent": "TestAgent",
|
||||
"$http_referer": "https://referer.com",
|
||||
}
|
||||
|
||||
// Check each expected variable
|
||||
for key, expectedValue := range expected {
|
||||
if vars[key] != expectedValue {
|
||||
t.Errorf("Expected %s to be %s, but got %s", key, expectedValue, vars[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomHeadersIncludeDynamicVariables(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
headers []*UserDefinedHeader
|
||||
expectedHasVar bool
|
||||
}{
|
||||
{
|
||||
name: "No headers",
|
||||
headers: []*UserDefinedHeader{},
|
||||
expectedHasVar: false,
|
||||
},
|
||||
{
|
||||
name: "Headers without dynamic variables",
|
||||
headers: []*UserDefinedHeader{
|
||||
{
|
||||
Direction: HeaderDirection_ZoraxyToUpstream,
|
||||
Key: "X-Custom-Header",
|
||||
Value: "staticValue",
|
||||
IsRemove: false,
|
||||
},
|
||||
{
|
||||
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||
Key: "X-Another-Header",
|
||||
Value: "staticValue",
|
||||
IsRemove: false,
|
||||
},
|
||||
},
|
||||
expectedHasVar: false,
|
||||
},
|
||||
{
|
||||
name: "Headers with one dynamic variable",
|
||||
headers: []*UserDefinedHeader{
|
||||
{
|
||||
Direction: HeaderDirection_ZoraxyToUpstream,
|
||||
Key: "X-Custom-Header",
|
||||
Value: "$dynamicValue",
|
||||
IsRemove: false,
|
||||
},
|
||||
},
|
||||
expectedHasVar: true,
|
||||
},
|
||||
{
|
||||
name: "Headers with multiple dynamic variables",
|
||||
headers: []*UserDefinedHeader{
|
||||
{
|
||||
Direction: HeaderDirection_ZoraxyToUpstream,
|
||||
Key: "X-Custom-Header",
|
||||
Value: "$dynamicValue1",
|
||||
IsRemove: false,
|
||||
},
|
||||
{
|
||||
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||
Key: "X-Another-Header",
|
||||
Value: "$dynamicValue2",
|
||||
IsRemove: false,
|
||||
},
|
||||
},
|
||||
expectedHasVar: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hasVar := CustomHeadersIncludeDynamicVariables(tt.headers)
|
||||
if hasVar != tt.expectedHasVar {
|
||||
t.Errorf("Expected %v, but got %v", tt.expectedHasVar, hasVar)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPopulateRequestHeaderVariables(t *testing.T) {
|
||||
// Create a sample request with specific values
|
||||
req := httptest.NewRequest("GET", "https://example.com/test?foo=bar", nil)
|
||||
req.Host = "example.com"
|
||||
req.RemoteAddr = "192.168.1.1:12345"
|
||||
req.Header.Set("User-Agent", "TestAgent")
|
||||
req.Header.Set("Referer", "https://referer.com")
|
||||
|
||||
// Define user-defined headers with dynamic variables
|
||||
userDefinedHeaders := []*UserDefinedHeader{
|
||||
{
|
||||
Direction: HeaderDirection_ZoraxyToUpstream,
|
||||
Key: "X-Forwarded-Host",
|
||||
Value: "$host",
|
||||
},
|
||||
{
|
||||
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||
Key: "X-Client-IP",
|
||||
Value: "$remote_addr",
|
||||
},
|
||||
{
|
||||
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||
Key: "X-Custom-Header",
|
||||
Value: "$request_uri",
|
||||
},
|
||||
}
|
||||
|
||||
// Call the function with the test data
|
||||
resultHeaders := PopulateRequestHeaderVariables(req, userDefinedHeaders)
|
||||
|
||||
// Expected results after variable substitution
|
||||
expectedHeaders := []*UserDefinedHeader{
|
||||
{
|
||||
Direction: HeaderDirection_ZoraxyToUpstream,
|
||||
Key: "X-Forwarded-Host",
|
||||
Value: "example.com",
|
||||
},
|
||||
{
|
||||
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||
Key: "X-Client-IP",
|
||||
Value: "192.168.1.1:12345",
|
||||
},
|
||||
{
|
||||
Direction: HeaderDirection_ZoraxyToDownstream,
|
||||
Key: "X-Custom-Header",
|
||||
Value: "https://example.com/test?foo=bar",
|
||||
},
|
||||
}
|
||||
|
||||
// Validate results
|
||||
for i, expected := range expectedHeaders {
|
||||
if resultHeaders[i].Direction != expected.Direction ||
|
||||
resultHeaders[i].Key != expected.Key ||
|
||||
resultHeaders[i].Value != expected.Value {
|
||||
t.Errorf("Expected header %v, but got %v", expected, resultHeaders[i])
|
||||
}
|
||||
}
|
||||
}
|
79
src/mod/dynamicproxy/rewrite/rewrite.go
Normal file
79
src/mod/dynamicproxy/rewrite/rewrite.go
Normal file
@ -0,0 +1,79 @@
|
||||
package rewrite
|
||||
|
||||
/*
|
||||
rewrite.go
|
||||
|
||||
This script handle the rewrite logic for custom headers
|
||||
*/
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
)
|
||||
|
||||
// SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers
|
||||
// return upstream header and downstream header key-value pairs
|
||||
// if the header is expected to be deleted, the value will be set to empty string
|
||||
func SplitUpDownStreamHeaders(rewriteOptions *HeaderRewriteOptions) ([][]string, [][]string) {
|
||||
if len(rewriteOptions.UserDefinedHeaders) == 0 && rewriteOptions.HSTSMaxAge == 0 && !rewriteOptions.EnablePermissionPolicyHeader {
|
||||
//Early return if there are no defined headers
|
||||
return [][]string{}, [][]string{}
|
||||
}
|
||||
|
||||
//Use pre-allocation for faster performance
|
||||
//Downstream +2 for Permission Policy and HSTS
|
||||
upstreamHeaders := make([][]string, len(rewriteOptions.UserDefinedHeaders))
|
||||
downstreamHeaders := make([][]string, len(rewriteOptions.UserDefinedHeaders)+2)
|
||||
upstreamHeaderCounter := 0
|
||||
downstreamHeaderCounter := 0
|
||||
|
||||
//Sort the headers into upstream or downstream
|
||||
for _, customHeader := range rewriteOptions.UserDefinedHeaders {
|
||||
thisHeaderSet := make([]string, 2)
|
||||
thisHeaderSet[0] = customHeader.Key
|
||||
thisHeaderSet[1] = customHeader.Value
|
||||
if customHeader.IsRemove {
|
||||
//Prevent invalid config
|
||||
thisHeaderSet[1] = ""
|
||||
}
|
||||
|
||||
//Assign to slice
|
||||
if customHeader.Direction == HeaderDirection_ZoraxyToUpstream {
|
||||
upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet
|
||||
upstreamHeaderCounter++
|
||||
} else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream {
|
||||
downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet
|
||||
downstreamHeaderCounter++
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the endpoint require HSTS headers
|
||||
if rewriteOptions.HSTSMaxAge > 0 {
|
||||
if rewriteOptions.HSTSIncludeSubdomains {
|
||||
//Endpoint listening domain includes wildcards.
|
||||
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(rewriteOptions.HSTSMaxAge)) + "; includeSubdomains"}
|
||||
} else {
|
||||
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(rewriteOptions.HSTSMaxAge))}
|
||||
}
|
||||
|
||||
downstreamHeaderCounter++
|
||||
}
|
||||
|
||||
//Check if the endpoint require Permission Policy
|
||||
if rewriteOptions.EnablePermissionPolicyHeader {
|
||||
var usingPermissionPolicy *permissionpolicy.PermissionsPolicy
|
||||
if rewriteOptions.PermissionPolicy != nil {
|
||||
//Custom permission policy
|
||||
usingPermissionPolicy = rewriteOptions.PermissionPolicy
|
||||
} else {
|
||||
//Permission policy is enabled but not customized. Use default
|
||||
usingPermissionPolicy = permissionpolicy.GetDefaultPermissionPolicy()
|
||||
}
|
||||
|
||||
downstreamHeaders[downstreamHeaderCounter] = usingPermissionPolicy.ToKeyValueHeader()
|
||||
downstreamHeaderCounter++
|
||||
}
|
||||
|
||||
return upstreamHeaders, downstreamHeaders
|
||||
}
|
51
src/mod/dynamicproxy/rewrite/typedef.go
Normal file
51
src/mod/dynamicproxy/rewrite/typedef.go
Normal file
@ -0,0 +1,51 @@
|
||||
package rewrite
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
)
|
||||
|
||||
/*
|
||||
typdef.go
|
||||
|
||||
This script handle the type definition for custom headers
|
||||
*/
|
||||
|
||||
/* Custom Header Related Data structure */
|
||||
// Header injection direction type
|
||||
type HeaderDirection int
|
||||
|
||||
const (
|
||||
HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
|
||||
HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
|
||||
)
|
||||
|
||||
// User defined headers to add into a proxy endpoint
|
||||
type UserDefinedHeader struct {
|
||||
Direction HeaderDirection
|
||||
Key string
|
||||
Value string
|
||||
IsRemove bool //Instead of set, remove this key instead
|
||||
}
|
||||
|
||||
type HeaderRewriteOptions struct {
|
||||
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
|
||||
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
|
||||
HSTSIncludeSubdomains bool //Include subdomains in HSTS header
|
||||
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
|
||||
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
|
||||
}
|
||||
|
||||
// Utilities for header rewrite
|
||||
func (h *UserDefinedHeader) GetDirection() HeaderDirection {
|
||||
return h.Direction
|
||||
}
|
||||
|
||||
// Copy eturns a deep copy of the UserDefinedHeader
|
||||
func (h *UserDefinedHeader) Copy() *UserDefinedHeader {
|
||||
result := UserDefinedHeader{}
|
||||
js, _ := json.Marshal(h)
|
||||
json.Unmarshal(js, &result)
|
||||
return &result
|
||||
}
|
@ -70,6 +70,11 @@ 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 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")
|
||||
|
@ -7,10 +7,12 @@ import (
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/auth/sso"
|
||||
"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/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
@ -44,6 +46,7 @@ type RouterOption struct {
|
||||
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
|
||||
SSOHandler *sso.SSOHandler //SSO handler for handling SSO requests, interception mode only
|
||||
Logger *logger.Logger //Logger for reverse proxy requets
|
||||
}
|
||||
|
||||
@ -82,23 +85,6 @@ type BasicAuthExceptionRule struct {
|
||||
PathPrefix string
|
||||
}
|
||||
|
||||
/* Custom Header Related Data structure */
|
||||
// Header injection direction type
|
||||
type HeaderDirection int
|
||||
|
||||
const (
|
||||
HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
|
||||
HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
|
||||
)
|
||||
|
||||
// User defined headers to add into a proxy endpoint
|
||||
type UserDefinedHeader struct {
|
||||
Direction HeaderDirection
|
||||
Key string
|
||||
Value string
|
||||
IsRemove bool //Instead of set, remove this key instead
|
||||
}
|
||||
|
||||
/* Routing Rule Data Structures */
|
||||
|
||||
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
|
||||
@ -131,16 +117,18 @@ type ProxyEndpoint struct {
|
||||
VirtualDirectories []*VirtualDirectoryEndpoint
|
||||
|
||||
//Custom Headers
|
||||
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
|
||||
UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
|
||||
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
|
||||
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
|
||||
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
|
||||
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
|
||||
DisableHopByHopHeaderRemoval bool //TODO: Do not remove hop-by-hop headers
|
||||
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
|
||||
|
||||
//Authentication
|
||||
RequireBasicAuth bool //Set to true to request basic auth before proxy
|
||||
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
||||
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
||||
UseSSOIntercept bool //Allow SSO to intercept this endpoint and provide authentication via Oauth2 credentials
|
||||
|
||||
// Rate Limiting
|
||||
RequireRateLimit bool
|
||||
|
@ -16,7 +16,7 @@ type Sender struct {
|
||||
Port int //E.g. 587
|
||||
Username string //Username of the email account
|
||||
Password string //Password of the email account
|
||||
SenderAddr string //e.g. admin@arozos.com
|
||||
SenderAddr string //e.g. admin@aroz.org
|
||||
}
|
||||
|
||||
// Create a new email sender object
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,7 @@
|
||||
package ganserv
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
@ -85,6 +86,7 @@ func NewNetworkManager(option *NetworkManagerOptions) *NetworkManager {
|
||||
//Get controller info
|
||||
instanceInfo, err := getControllerInfo(option.AuthToken, option.ApiPort)
|
||||
if err != nil {
|
||||
log.Println("ZeroTier connection failed: ", err.Error())
|
||||
return &NetworkManager{
|
||||
authToken: option.AuthToken,
|
||||
apiPort: option.ApiPort,
|
||||
|
@ -28,11 +28,17 @@ type NodeInfo struct {
|
||||
Clock int64 `json:"clock"`
|
||||
Config struct {
|
||||
Settings struct {
|
||||
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay"`
|
||||
PortMappingEnabled bool `json:"portMappingEnabled"`
|
||||
PrimaryPort int `json:"primaryPort"`
|
||||
SoftwareUpdate string `json:"softwareUpdate"`
|
||||
SoftwareUpdateChannel string `json:"softwareUpdateChannel"`
|
||||
AllowTCPFallbackRelay bool `json:"allowTcpFallbackRelay,omitempty"`
|
||||
ForceTCPRelay bool `json:"forceTcpRelay,omitempty"`
|
||||
HomeDir string `json:"homeDir,omitempty"`
|
||||
ListeningOn []string `json:"listeningOn,omitempty"`
|
||||
PortMappingEnabled bool `json:"portMappingEnabled,omitempty"`
|
||||
PrimaryPort int `json:"primaryPort,omitempty"`
|
||||
SecondaryPort int `json:"secondaryPort,omitempty"`
|
||||
SoftwareUpdate string `json:"softwareUpdate,omitempty"`
|
||||
SoftwareUpdateChannel string `json:"softwareUpdateChannel,omitempty"`
|
||||
SurfaceAddresses []string `json:"surfaceAddresses,omitempty"`
|
||||
TertiaryPort int `json:"tertiaryPort,omitempty"`
|
||||
} `json:"settings"`
|
||||
} `json:"config"`
|
||||
Online bool `json:"online"`
|
||||
@ -46,7 +52,6 @@ type NodeInfo struct {
|
||||
VersionMinor int `json:"versionMinor"`
|
||||
VersionRev int `json:"versionRev"`
|
||||
}
|
||||
|
||||
type ErrResp struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
119523
src/mod/geodb/geoipv4.csv
119523
src/mod/geodb/geoipv4.csv
File diff suppressed because it is too large
Load Diff
111082
src/mod/geodb/geoipv6.csv
111082
src/mod/geodb/geoipv6.csv
File diff suppressed because it is too large
Load Diff
@ -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 := ""
|
||||
|
67
src/mod/geodb/locale.go
Normal file
67
src/mod/geodb/locale.go
Normal file
@ -0,0 +1,67 @@
|
||||
package geodb
|
||||
|
||||
import "net/http"
|
||||
|
||||
// GetRequesterCountryISOCode get the locale of the requester
|
||||
func (s *Store) GetLocaleFromRequest(r *http.Request) (string, error) {
|
||||
cc := s.GetRequesterCountryISOCode(r)
|
||||
return GetLocaleFromCountryCode(cc), nil
|
||||
}
|
||||
|
||||
// GetLocaleFromCountryCode get the locale given the country code
|
||||
func GetLocaleFromCountryCode(cc string) string {
|
||||
//If you find your country is not in the list, please add it here
|
||||
mapCountryToLocale := map[string]string{
|
||||
"aa": "ar_AA",
|
||||
"by": "be_BY",
|
||||
"bg": "bg_BG",
|
||||
"es": "ca_ES",
|
||||
"cz": "cs_CZ",
|
||||
"dk": "da_DK",
|
||||
"ch": "de_CH",
|
||||
"de": "de_DE",
|
||||
"gr": "el_GR",
|
||||
"au": "en_AU",
|
||||
"be": "en_BE",
|
||||
"gb": "en_GB",
|
||||
"jp": "en_JP",
|
||||
"us": "en_US",
|
||||
"za": "en_ZA",
|
||||
"fi": "fi_FI",
|
||||
"ca": "fr_CA",
|
||||
"fr": "fr_FR",
|
||||
"hr": "hr_HR",
|
||||
"hu": "hu_HU",
|
||||
"is": "is_IS",
|
||||
"it": "it_IT",
|
||||
"il": "iw_IL",
|
||||
"kr": "ko_KR",
|
||||
"lt": "lt_LT",
|
||||
"lv": "lv_LV",
|
||||
"mk": "mk_MK",
|
||||
"nl": "nl_NL",
|
||||
"no": "no_NO",
|
||||
"pl": "pl_PL",
|
||||
"br": "pt_BR",
|
||||
"pt": "pt_PT",
|
||||
"ro": "ro_RO",
|
||||
"ru": "ru_RU",
|
||||
"sp": "sh_SP",
|
||||
"sk": "sk_SK",
|
||||
"sl": "sl_SL",
|
||||
"al": "sq_AL",
|
||||
"se": "sv_SE",
|
||||
"th": "th_TH",
|
||||
"tr": "tr_TR",
|
||||
"ua": "uk_UA",
|
||||
"cn": "zh_CN",
|
||||
"tw": "zh_TW",
|
||||
"hk": "zh_HK",
|
||||
}
|
||||
locale, ok := mapCountryToLocale[cc]
|
||||
if !ok {
|
||||
return "en-US"
|
||||
}
|
||||
|
||||
return locale
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package logviewer
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -105,7 +104,6 @@ func (v *Viewer) LoadLogFile(filename string) (string, error) {
|
||||
filename = filepath.ToSlash(filename)
|
||||
filename = strings.ReplaceAll(filename, "../", "")
|
||||
logFilepath := filepath.Join(v.option.RootFolder, filename)
|
||||
fmt.Println(logFilepath)
|
||||
if utils.FileExists(logFilepath) {
|
||||
//Load it
|
||||
content, err := os.ReadFile(logFilepath)
|
||||
|
79
src/mod/ipscan/handlers.go
Normal file
79
src/mod/ipscan/handlers.go
Normal file
@ -0,0 +1,79 @@
|
||||
package ipscan
|
||||
|
||||
/*
|
||||
ipscan http handlers
|
||||
|
||||
This script provide http handlers for ipscan module
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// HandleScanPort is the HTTP handler for scanning opened ports on a given IP address
|
||||
func HandleScanPort(w http.ResponseWriter, r *http.Request) {
|
||||
targetIp, err := utils.GetPara(r, "ip")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "target IP address not given")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the IP is a valid IP address
|
||||
ip := net.ParseIP(targetIp)
|
||||
if ip == nil {
|
||||
utils.SendErrorResponse(w, "invalid IP address")
|
||||
return
|
||||
}
|
||||
|
||||
// Scan the ports
|
||||
openPorts := ScanPorts(targetIp)
|
||||
jsonData, err := json.Marshal(openPorts)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to marshal JSON")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendJSONResponse(w, string(jsonData))
|
||||
}
|
||||
|
||||
// HandleIpScan is the HTTP handler for scanning IP addresses in a given range or CIDR
|
||||
func HandleIpScan(w http.ResponseWriter, r *http.Request) {
|
||||
cidr, err := utils.PostPara(r, "cidr")
|
||||
if err != nil {
|
||||
//Ip range mode
|
||||
start, err := utils.PostPara(r, "start")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "missing start ip")
|
||||
return
|
||||
}
|
||||
|
||||
end, err := utils.PostPara(r, "end")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "missing end ip")
|
||||
return
|
||||
}
|
||||
|
||||
discoveredHosts, err := ScanIpRange(start, end)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(discoveredHosts)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
//CIDR mode
|
||||
discoveredHosts, err := ScanCIDRRange(cidr)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(discoveredHosts)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ type DiscoveredHost struct {
|
||||
HttpsPortDetected bool
|
||||
}
|
||||
|
||||
//Scan an IP range given the start and ending ip address
|
||||
// Scan an IP range given the start and ending ip address
|
||||
func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
|
||||
ipStart := net.ParseIP(start)
|
||||
ipEnd := net.ParseIP(end)
|
||||
@ -57,7 +57,6 @@ func ScanIpRange(start, end string) ([]*DiscoveredHost, error) {
|
||||
host.CheckHostname()
|
||||
host.CheckPort("http", 80, &host.HttpPortDetected)
|
||||
host.CheckPort("https", 443, &host.HttpsPortDetected)
|
||||
fmt.Println("OK", host)
|
||||
hosts = append(hosts, host)
|
||||
|
||||
}(thisIp)
|
||||
@ -118,7 +117,7 @@ func (host *DiscoveredHost) CheckPing() error {
|
||||
func (host *DiscoveredHost) CheckHostname() {
|
||||
// lookup the hostname for the IP address
|
||||
names, err := net.LookupAddr(host.IP)
|
||||
fmt.Println(names, err)
|
||||
//fmt.Println(names, err)
|
||||
if err == nil && len(names) > 0 {
|
||||
host.Hostname = names[0]
|
||||
}
|
||||
|
48
src/mod/ipscan/portscan.go
Normal file
48
src/mod/ipscan/portscan.go
Normal file
@ -0,0 +1,48 @@
|
||||
package ipscan
|
||||
|
||||
/*
|
||||
Port Scanner
|
||||
|
||||
This module scan the given IP address and scan all the opened port
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OpenedPort holds information about an open port and its service type
|
||||
type OpenedPort struct {
|
||||
Port int
|
||||
IsTCP bool
|
||||
}
|
||||
|
||||
// ScanPorts scans all the opened ports on a given host IP (both IPv4 and IPv6)
|
||||
func ScanPorts(host string) []*OpenedPort {
|
||||
var openPorts []*OpenedPort
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
for port := 1; port <= 65535; port++ {
|
||||
wg.Add(1)
|
||||
go func(port int) {
|
||||
defer wg.Done()
|
||||
address := fmt.Sprintf("%s:%d", host, port)
|
||||
|
||||
// Check TCP
|
||||
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
openPorts = append(openPorts, &OpenedPort{Port: port, IsTCP: true})
|
||||
mu.Unlock()
|
||||
conn.Close()
|
||||
}
|
||||
}(port)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return openPorts
|
||||
}
|
@ -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,10 +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++ {
|
||||
@ -65,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++
|
||||
}
|
||||
@ -94,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
|
||||
}
|
||||
|
||||
@ -173,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
|
||||
@ -193,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
|
||||
@ -262,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" {
|
||||
|
@ -50,21 +50,6 @@ func NewSSHProxyManager() *Manager {
|
||||
}
|
||||
}
|
||||
|
||||
// Get the next free port in the list
|
||||
func (m *Manager) GetNextPort() int {
|
||||
nextPort := m.StartingPort
|
||||
occupiedPort := make(map[int]bool)
|
||||
for _, instance := range m.Instances {
|
||||
occupiedPort[instance.AssignedPort] = true
|
||||
}
|
||||
for {
|
||||
if !occupiedPort[nextPort] {
|
||||
return nextPort
|
||||
}
|
||||
nextPort++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWriter, r *http.Request) {
|
||||
targetInstance, err := m.GetInstanceById(instanceId)
|
||||
if err != nil {
|
||||
@ -88,6 +73,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
|
||||
@ -167,6 +153,17 @@ func (i *Instance) CreateNewConnection(listenPort int, username string, remoteIp
|
||||
if username != "" {
|
||||
connAddr = username + "@" + remoteIpAddr
|
||||
}
|
||||
|
||||
//Trim the space in the username and remote address
|
||||
username = strings.TrimSpace(username)
|
||||
remoteIpAddr = strings.TrimSpace(remoteIpAddr)
|
||||
|
||||
//Validate the username and remote address
|
||||
err := ValidateUsernameAndRemoteAddr(username, remoteIpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := filepath.Join(filepath.Dir(i.ExecPath), ".gotty")
|
||||
title := username + "@" + remoteIpAddr
|
||||
if remotePort != 22 {
|
||||
|
66
src/mod/sshprox/sshprox_test.go
Normal file
66
src/mod/sshprox/sshprox_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
package sshprox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInstance_Destroy(t *testing.T) {
|
||||
manager := NewSSHProxyManager()
|
||||
instance, err := manager.NewSSHProxy("/tmp")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new SSH proxy: %v", err)
|
||||
}
|
||||
|
||||
instance.Destroy()
|
||||
|
||||
if len(manager.Instances) != 0 {
|
||||
t.Errorf("Expected Instances to be empty, got %d", len(manager.Instances))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstance_ValidateUsernameAndRemoteAddr(t *testing.T) {
|
||||
tests := []struct {
|
||||
username string
|
||||
remoteAddr string
|
||||
expectError bool
|
||||
}{
|
||||
{"validuser", "127.0.0.1", false},
|
||||
{"valid.user", "example.com", false},
|
||||
{"; bash ;", "example.com", true},
|
||||
{"valid-user", "example.com", false},
|
||||
{"invalid user", "127.0.0.1", true},
|
||||
{"validuser", "invalid address", true},
|
||||
{"invalid@user", "127.0.0.1", true},
|
||||
{"validuser", "invalid@address", true},
|
||||
{"injection; rm -rf /", "127.0.0.1", true},
|
||||
{"validuser", "127.0.0.1; rm -rf /", true},
|
||||
{"$(reboot)", "127.0.0.1", true},
|
||||
{"validuser", "$(reboot)", true},
|
||||
{"validuser", "127.0.0.1; $(reboot)", true},
|
||||
{"validuser", "127.0.0.1 | ls", true},
|
||||
{"validuser", "127.0.0.1 & ls", true},
|
||||
{"validuser", "127.0.0.1 && ls", true},
|
||||
{"validuser", "127.0.0.1 |& ls", true},
|
||||
{"validuser", "127.0.0.1 ; ls", true},
|
||||
{"validuser", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", false},
|
||||
{"validuser", "2001:db8::ff00:42:8329", false},
|
||||
{"validuser", "2001:db8:0:1234:0:567:8:1", false},
|
||||
{"validuser", "2001:db8::1234:0:567:8:1", false},
|
||||
{"validuser", "2001:db8:0:0:0:0:2:1", false},
|
||||
{"validuser", "2001:db8::2:1", false},
|
||||
{"validuser", "2001:db8:0:0:8:800:200c:417a", false},
|
||||
{"validuser", "2001:db8::8:800:200c:417a", false},
|
||||
{"validuser", "2001:db8:0:0:8:800:200c:417a; rm -rf /", true},
|
||||
{"validuser", "2001:db8::8:800:200c:417a; rm -rf /", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := ValidateUsernameAndRemoteAddr(test.username, test.remoteAddr)
|
||||
if test.expectError && err == nil {
|
||||
t.Errorf("Expected error for username %s and remoteAddr %s, but got none", test.username, test.remoteAddr)
|
||||
}
|
||||
if !test.expectError && err != nil {
|
||||
t.Errorf("Did not expect error for username %s and remoteAddr %s, but got %v", test.username, test.remoteAddr, err)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
package sshprox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@ -34,6 +36,21 @@ func IsWebSSHSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Get the next free port in the list
|
||||
func (m *Manager) GetNextPort() int {
|
||||
nextPort := m.StartingPort
|
||||
occupiedPort := make(map[int]bool)
|
||||
for _, instance := range m.Instances {
|
||||
occupiedPort[instance.AssignedPort] = true
|
||||
}
|
||||
for {
|
||||
if !occupiedPort[nextPort] {
|
||||
return nextPort
|
||||
}
|
||||
nextPort++
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a given domain and port is a valid ssh server
|
||||
func IsSSHConnectable(ipOrDomain string, port int) bool {
|
||||
timeout := time.Second * 3
|
||||
@ -60,13 +77,47 @@ func IsSSHConnectable(ipOrDomain string, port int) bool {
|
||||
return string(buf[:7]) == "SSH-2.0"
|
||||
}
|
||||
|
||||
// Check if the port is used by other process or application
|
||||
func isPortInUse(port int) bool {
|
||||
address := fmt.Sprintf(":%d", port)
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
// Validate the username and remote address to prevent injection
|
||||
func ValidateUsernameAndRemoteAddr(username string, remoteIpAddr string) error {
|
||||
// Validate and sanitize the username to prevent ssh injection
|
||||
validUsername := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||
if !validUsername.MatchString(username) {
|
||||
return errors.New("invalid username, only alphanumeric characters, dots, underscores and dashes are allowed")
|
||||
}
|
||||
|
||||
//Check if the remoteIpAddr is a valid ipv4 or ipv6 address
|
||||
if net.ParseIP(remoteIpAddr) != nil {
|
||||
//A valid IP address do not need further validation
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate and sanitize the remote domain to prevent injection
|
||||
validRemoteAddr := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
|
||||
if !validRemoteAddr.MatchString(remoteIpAddr) {
|
||||
return errors.New("invalid remote address, only alphanumeric characters, dots, underscores and dashes are allowed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if the given ip or domain is a loopback address
|
||||
// or resolves to a loopback address
|
||||
func IsLoopbackIPOrDomain(ipOrDomain string) bool {
|
||||
if strings.EqualFold(strings.TrimSpace(ipOrDomain), "localhost") || strings.TrimSpace(ipOrDomain) == "127.0.0.1" {
|
||||
return true
|
||||
}
|
||||
listener.Close()
|
||||
|
||||
//Check if the ipOrDomain resolves to a loopback address
|
||||
ips, err := net.LookupIP(ipOrDomain)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if ip.IsLoopback() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -1,15 +1,18 @@
|
||||
package streamproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -48,9 +51,10 @@ type ProxyRelayConfig struct {
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Database *database.Database
|
||||
DefaultTimeout int
|
||||
AccessControlHandler func(net.Conn) bool
|
||||
ConfigStore string //Folder to store the config files, will be created if not exists
|
||||
Logger *logger.Logger //Logger for the stream proxy
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
@ -63,13 +67,37 @@ type Manager struct {
|
||||
|
||||
}
|
||||
|
||||
func NewStreamProxy(options *Options) *Manager {
|
||||
options.Database.NewTable("tcprox")
|
||||
func NewStreamProxy(options *Options) (*Manager, error) {
|
||||
if !utils.FileExists(options.ConfigStore) {
|
||||
err := os.MkdirAll(options.ConfigStore, 0775)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
//Load relay configs from db
|
||||
previousRules := []*ProxyRelayConfig{}
|
||||
if options.Database.KeyExists("tcprox", "rules") {
|
||||
options.Database.Read("tcprox", "rules", &previousRules)
|
||||
streamProxyConfigFiles, err := filepath.Glob(options.ConfigStore + "/*.config")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, configFile := range streamProxyConfigFiles {
|
||||
//Read file into bytes
|
||||
configBytes, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
options.Logger.PrintAndLog("stream-prox", "Read stream proxy config failed", err)
|
||||
continue
|
||||
}
|
||||
thisRelayConfig := &ProxyRelayConfig{}
|
||||
err = json.Unmarshal(configBytes, thisRelayConfig)
|
||||
if err != nil {
|
||||
options.Logger.PrintAndLog("stream-prox", "Unmarshal stream proxy config failed", err)
|
||||
continue
|
||||
}
|
||||
|
||||
//Append the config to the list
|
||||
previousRules = append(previousRules, thisRelayConfig)
|
||||
}
|
||||
|
||||
//Check if the AccessControlHandler is empty. If yes, set it to always allow access
|
||||
@ -91,14 +119,27 @@ func NewStreamProxy(options *Options) *Manager {
|
||||
rule.parent = &thisManager
|
||||
if rule.Running {
|
||||
//This was previously running. Start it again
|
||||
log.Println("[Stream Proxy] Resuming stream proxy rule " + rule.Name)
|
||||
thisManager.logf("Resuming stream proxy rule "+rule.Name, nil)
|
||||
rule.Start()
|
||||
}
|
||||
}
|
||||
|
||||
thisManager.Configs = previousRules
|
||||
|
||||
return &thisManager
|
||||
return &thisManager, nil
|
||||
}
|
||||
|
||||
// Wrapper function to log error
|
||||
func (m *Manager) logf(message string, originalError error) {
|
||||
if m.Options.Logger == nil {
|
||||
//Print to fmt
|
||||
if originalError != nil {
|
||||
message += ": " + originalError.Error()
|
||||
}
|
||||
println(message)
|
||||
return
|
||||
}
|
||||
m.Options.Logger.PrintAndLog("stream-prox", message, originalError)
|
||||
}
|
||||
|
||||
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
||||
@ -179,6 +220,11 @@ func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr
|
||||
}
|
||||
|
||||
func (m *Manager) RemoveConfig(configUUID string) error {
|
||||
//Remove the config from file
|
||||
err := os.Remove(filepath.Join(m.Options.ConfigStore, configUUID+".config"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Find and remove the config with the specified UUID
|
||||
for i, config := range m.Configs {
|
||||
if config.UUID == configUUID {
|
||||
@ -190,8 +236,19 @@ func (m *Manager) RemoveConfig(configUUID string) error {
|
||||
return errors.New("config not found")
|
||||
}
|
||||
|
||||
// Save all configs to ConfigStore folder
|
||||
func (m *Manager) SaveConfigToDatabase() {
|
||||
m.Options.Database.Write("tcprox", "rules", m.Configs)
|
||||
for _, config := range m.Configs {
|
||||
configBytes, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
m.logf("Failed to marshal stream proxy config", err)
|
||||
continue
|
||||
}
|
||||
err = os.WriteFile(m.Options.ConfigStore+"/"+config.UUID+".config", configBytes, 0775)
|
||||
if err != nil {
|
||||
m.logf("Failed to save stream proxy config", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -217,9 +274,10 @@ func (c *ProxyRelayConfig) Start() error {
|
||||
if err != nil {
|
||||
if !c.UseTCP {
|
||||
c.Running = false
|
||||
c.udpStopChan = nil
|
||||
c.parent.SaveConfigToDatabase()
|
||||
}
|
||||
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error())
|
||||
c.parent.logf("[proto:udp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@ -231,8 +289,9 @@ func (c *ProxyRelayConfig) Start() error {
|
||||
err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
|
||||
if err != nil {
|
||||
c.Running = false
|
||||
c.tcpStopChan = nil
|
||||
c.parent.SaveConfigToDatabase()
|
||||
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error())
|
||||
c.parent.logf("[proto:tcp] Error starting stream proxy "+c.Name+"("+c.UUID+")", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@ -253,27 +312,27 @@ func (c *ProxyRelayConfig) Restart() {
|
||||
if c.IsRunning() {
|
||||
c.Stop()
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
time.Sleep(3000 * time.Millisecond)
|
||||
c.Start()
|
||||
}
|
||||
|
||||
// Stop a running proxy if running
|
||||
func (c *ProxyRelayConfig) Stop() {
|
||||
log.Println("[STREAM PROXY] Stopping Stream Proxy " + c.Name)
|
||||
c.parent.logf("Stopping Stream Proxy "+c.Name, nil)
|
||||
|
||||
if c.udpStopChan != nil {
|
||||
log.Println("[STREAM PROXY] Stopping UDP for " + c.Name)
|
||||
c.parent.logf("Stopping UDP for "+c.Name, nil)
|
||||
c.udpStopChan <- true
|
||||
c.udpStopChan = nil
|
||||
}
|
||||
|
||||
if c.tcpStopChan != nil {
|
||||
log.Println("[STREAM PROXY] Stopping TCP for " + c.Name)
|
||||
c.parent.logf("Stopping TCP for "+c.Name, nil)
|
||||
c.tcpStopChan <- true
|
||||
c.tcpStopChan = nil
|
||||
}
|
||||
|
||||
log.Println("[STREAM PROXY] Stopped Stream Proxy " + c.Name)
|
||||
c.parent.logf("Stopped Stream Proxy "+c.Name, nil)
|
||||
c.Running = false
|
||||
|
||||
//Update the running status
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
v308 "imuslab.com/zoraxy/mod/update/v308"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -22,6 +21,13 @@ import (
|
||||
// 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
|
||||
@ -49,8 +55,8 @@ func RunConfigUpdate(fromVersion int, toVersion int) {
|
||||
|
||||
//Do iterate update
|
||||
for i := fromVersion; i < toVersion; i++ {
|
||||
oldVersion := fromVersion
|
||||
newVersion := fromVersion + 1
|
||||
oldVersion := i
|
||||
newVersion := i + 1
|
||||
fmt.Println("Updating from v", oldVersion, " to v", newVersion)
|
||||
runUpdateRoutineWithVersion(oldVersion, newVersion)
|
||||
//Write the updated version to file
|
||||
@ -65,12 +71,37 @@ func GetVersionIntFromVersionNumber(version string) int {
|
||||
return versionInt
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
16
src/mod/update/updatelogic.go
Normal file
16
src/mod/update/updatelogic.go
Normal 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
|
||||
}
|
@ -3,7 +3,6 @@ package uptime
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"strconv"
|
||||
@ -242,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{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -41,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
|
||||
@ -67,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
|
||||
}
|
||||
|
||||
@ -96,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)
|
||||
@ -173,4 +199,4 @@ func ValidateListeningAddress(address string) bool {
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
@ -42,6 +42,13 @@ 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
|
||||
isValidRequest := validatePathEscape(targetDir, fm.Directory)
|
||||
if !isValidRequest {
|
||||
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Open the target directory
|
||||
dirEntries, err := os.ReadDir(targetDir)
|
||||
if err != nil {
|
||||
@ -118,6 +125,14 @@ func (fm *FileManager) HandleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Specify the directory where you want to save the uploaded file
|
||||
uploadDir := filepath.Join(fm.Directory, dir)
|
||||
|
||||
// Clean path to prevent path escape #274
|
||||
isValidRequest := validatePathEscape(uploadDir, fm.Directory)
|
||||
if !isValidRequest {
|
||||
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.FileExists(uploadDir) {
|
||||
utils.SendErrorResponse(w, "upload target directory not exists")
|
||||
return
|
||||
@ -157,14 +172,20 @@ func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
filePath := filepath.Join(fm.Directory, filename)
|
||||
// Clean path to prevent path escape #274
|
||||
isValidRequest := validatePathEscape(filePath, fm.Directory)
|
||||
if !isValidRequest {
|
||||
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
previewMode, _ := utils.GetPara(r, "preview")
|
||||
if previewMode == "true" {
|
||||
// Serve the file using http.ServeFile
|
||||
filePath := filepath.Join(fm.Directory, filename)
|
||||
http.ServeFile(w, r, filePath)
|
||||
} else {
|
||||
// Trigger a download with content disposition headers
|
||||
filePath := filepath.Join(fm.Directory, filename)
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename))
|
||||
http.ServeFile(w, r, filePath)
|
||||
}
|
||||
@ -173,7 +194,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
|
||||
@ -185,6 +206,11 @@ func (fm *FileManager) HandleNewFolder(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Specify the directory where you want to create the new folder
|
||||
newFolderPath := filepath.Join(fm.Directory, dirName)
|
||||
isValidRequest := validatePathEscape(newFolderPath, fm.Directory)
|
||||
if !isValidRequest {
|
||||
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the folder already exists
|
||||
if _, err := os.Stat(newFolderPath); os.IsNotExist(err) {
|
||||
@ -226,6 +252,18 @@ func (fm *FileManager) HandleFileCopy(w http.ResponseWriter, r *http.Request) {
|
||||
absSrcPath := filepath.Join(fm.Directory, srcPath)
|
||||
absDestPath := filepath.Join(fm.Directory, destPath)
|
||||
|
||||
//Make sure the copy source and dest are within web directory folder
|
||||
isValidRequest := validatePathEscape(absSrcPath, fm.Directory)
|
||||
if !isValidRequest {
|
||||
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
isValidRequest = validatePathEscape(absDestPath, fm.Directory)
|
||||
if !isValidRequest {
|
||||
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the source path exists
|
||||
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
||||
utils.SendErrorResponse(w, "source path does not exist")
|
||||
@ -268,13 +306,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
|
||||
@ -288,6 +326,18 @@ func (fm *FileManager) HandleFileMove(w http.ResponseWriter, r *http.Request) {
|
||||
absSrcPath := filepath.Join(fm.Directory, srcPath)
|
||||
absDestPath := filepath.Join(fm.Directory, destPath)
|
||||
|
||||
//Make sure move source and target are within web server directory
|
||||
isValidRequest := validatePathEscape(absSrcPath, fm.Directory)
|
||||
if !isValidRequest {
|
||||
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
isValidRequest = validatePathEscape(absDestPath, fm.Directory)
|
||||
if !isValidRequest {
|
||||
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the source path exists
|
||||
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
||||
utils.SendErrorResponse(w, "source path does not exist")
|
||||
@ -319,6 +369,11 @@ func (fm *FileManager) HandleFileProperties(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
// Construct the absolute path to the target file or directory
|
||||
absPath := filepath.Join(fm.Directory, filePath)
|
||||
isValidRequest := validatePathEscape(absPath, fm.Directory)
|
||||
if !isValidRequest {
|
||||
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the target path exists
|
||||
_, err = os.Stat(absPath)
|
||||
@ -386,6 +441,11 @@ func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Construct the absolute path to the target file or directory
|
||||
absPath := filepath.Join(fm.Directory, filePath)
|
||||
isValidRequest := validatePathEscape(absPath, fm.Directory)
|
||||
if !isValidRequest {
|
||||
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the target path exists
|
||||
_, err = os.Stat(absPath)
|
||||
@ -404,3 +464,25 @@ func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request)
|
||||
// Respond with a success message or appropriate response
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Return true if the path is within the root path
|
||||
func validatePathEscape(reqestPath string, rootPath string) bool {
|
||||
reqestPath = filepath.ToSlash(filepath.Clean(reqestPath))
|
||||
rootPath = filepath.ToSlash(filepath.Clean(rootPath))
|
||||
|
||||
requestPathAbs, err := filepath.Abs(reqestPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
rootPathAbs, err := filepath.Abs(rootPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.HasPrefix(requestPathAbs, rootPathAbs) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -51,7 +51,7 @@
|
||||
You can upload your html files to your web directory via the <b>Web Directory Manager</b>.
|
||||
</p>
|
||||
<p>
|
||||
For online documentation, please refer to <a href="//zoraxy.arozos.com">zoraxy.arozos.com</a> or the <a href="https://github.com/tobychui/zoraxy/wiki">project wiki</a>.<br>
|
||||
For online documentation, please refer to <a href="//zoraxy.aroz.org">zoraxy.aroz.org</a> or the <a href="https://github.com/tobychui/zoraxy/wiki">project wiki</a>.<br>
|
||||
Thank you for using Zoraxy!
|
||||
</p>
|
||||
</div>
|
||||
|
@ -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()
|
||||
|
@ -31,6 +31,7 @@ func TestProxy(t *testing.T) {
|
||||
proxy := NewProxy(u, Options{
|
||||
SkipTLSValidation: false,
|
||||
SkipOriginCheck: false,
|
||||
Logger: nil,
|
||||
})
|
||||
proxy.Upgrader = upgrader
|
||||
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
@ -98,6 +99,7 @@ func ReverseProxtInit() {
|
||||
WebDirectory: *staticWebServerRoot,
|
||||
AccessController: accessController,
|
||||
LoadBalancer: loadBalancer,
|
||||
SSOHandler: ssoHandler,
|
||||
Logger: SystemWideLogger,
|
||||
})
|
||||
if err != nil {
|
||||
@ -331,7 +333,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
//VDir
|
||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||
//Custom headers
|
||||
UserDefinedHeaders: []*dynamicproxy.UserDefinedHeader{},
|
||||
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
|
||||
//Auth
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
@ -509,6 +511,9 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
//Save it to file
|
||||
SaveReverseProxyConfig(newProxyEndpoint)
|
||||
|
||||
//Update uptime monitor targets
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
@ -569,7 +574,7 @@ func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
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
|
||||
@ -589,12 +594,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()
|
||||
|
||||
@ -913,7 +912,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 {
|
||||
@ -922,7 +920,6 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
||||
PasswordHash: "",
|
||||
})
|
||||
}
|
||||
|
||||
thisEndpoint.BasicAuthCredentials = cleanedCredentials
|
||||
results = append(results, thisEndpoint)
|
||||
return true
|
||||
@ -944,18 +941,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", ¤tEnabled)
|
||||
err := sysdb.Read("settings", "listenP80", ¤tEnabled)
|
||||
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")
|
||||
@ -968,38 +969,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", ¤tRedirectToHttps)
|
||||
err := sysdb.Read("settings", "redirect", ¤tRedirectToHttps)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1089,13 +1100,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
|
||||
@ -1117,7 +1128,7 @@ func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
|
||||
//List all custom headers
|
||||
customHeaderList := targetProxyEndpoint.UserDefinedHeaders
|
||||
if customHeaderList == nil {
|
||||
customHeaderList = []*dynamicproxy.UserDefinedHeader{}
|
||||
customHeaderList = []*rewrite.UserDefinedHeader{}
|
||||
}
|
||||
js, _ := json.Marshal(customHeaderList)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
@ -1163,11 +1174,11 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
//Create a Custom Header Defination type
|
||||
var rewriteDirection dynamicproxy.HeaderDirection
|
||||
var rewriteDirection rewrite.HeaderDirection
|
||||
if direction == "toOrigin" {
|
||||
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToUpstream
|
||||
rewriteDirection = rewrite.HeaderDirection_ZoraxyToUpstream
|
||||
} else if direction == "toClient" {
|
||||
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToDownstream
|
||||
rewriteDirection = rewrite.HeaderDirection_ZoraxyToDownstream
|
||||
} else {
|
||||
//Unknown direction
|
||||
utils.SendErrorResponse(w, "header rewrite direction not supported")
|
||||
@ -1178,7 +1189,8 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
if rewriteType == "remove" {
|
||||
isRemove = true
|
||||
}
|
||||
headerRewriteDefination := dynamicproxy.UserDefinedHeader{
|
||||
|
||||
headerRewriteDefinition := rewrite.UserDefinedHeader{
|
||||
Key: name,
|
||||
Value: value,
|
||||
Direction: rewriteDirection,
|
||||
@ -1186,7 +1198,7 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
//Create a new custom header object
|
||||
err = targetProxyEndpoint.AddUserDefinedHeader(&headerRewriteDefination)
|
||||
err = targetProxyEndpoint.AddUserDefinedHeader(&headerRewriteDefinition)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to add header rewrite rule: "+err.Error())
|
||||
return
|
||||
@ -1238,6 +1250,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")
|
||||
|
@ -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))
|
||||
}
|
||||
|
46
src/start.go
46
src/start.go
@ -36,7 +36,10 @@ import (
|
||||
Startup Sequence
|
||||
|
||||
This function starts the startup sequence of all
|
||||
required modules
|
||||
required modules. Their startup sequences are inter-dependent
|
||||
and must be started in a specific order.
|
||||
|
||||
Don't touch this function unless you know what you are doing
|
||||
*/
|
||||
|
||||
var (
|
||||
@ -84,7 +87,7 @@ func startupSequence() {
|
||||
})
|
||||
|
||||
//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)
|
||||
}
|
||||
@ -124,6 +127,22 @@ func startupSequence() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
/*
|
||||
//Create an SSO handler
|
||||
ssoHandler, err = sso.NewSSOHandler(&sso.SSOConfig{
|
||||
SystemUUID: nodeUUID,
|
||||
PortalServerPort: 5488,
|
||||
AuthURL: "http://auth.localhost",
|
||||
Database: sysdb,
|
||||
Logger: SystemWideLogger,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
//Restore the SSO handler to previous state before shutdown
|
||||
ssoHandler.RestorePreviousRunningState()
|
||||
*/
|
||||
|
||||
//Create a statistic collector
|
||||
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
||||
Database: sysdb,
|
||||
@ -145,7 +164,7 @@ func startupSequence() {
|
||||
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)
|
||||
@ -187,7 +206,7 @@ func startupSequence() {
|
||||
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
|
||||
HostName: hostName,
|
||||
Port: portInt,
|
||||
Domain: "zoraxy.arozos.com",
|
||||
Domain: "zoraxy.aroz.org",
|
||||
Model: "Network Gateway",
|
||||
UUID: nodeUUID,
|
||||
Vendor: "imuslab.com",
|
||||
@ -244,10 +263,14 @@ func startupSequence() {
|
||||
webSshManager = sshprox.NewSSHProxyManager()
|
||||
|
||||
//Create TCP Proxy Manager
|
||||
streamProxyManager = streamproxy.NewStreamProxy(&streamproxy.Options{
|
||||
Database: sysdb,
|
||||
streamProxyManager, err = streamproxy.NewStreamProxy(&streamproxy.Options{
|
||||
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
|
||||
ConfigStore: "./conf/streamproxy",
|
||||
Logger: SystemWideLogger,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create WoL MAC storage table
|
||||
sysdb.NewTable("wolmac")
|
||||
@ -279,14 +302,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)
|
||||
|
||||
|
@ -19,7 +19,7 @@ import (
|
||||
|
||||
// List upstreams from a endpoint
|
||||
func ReverseProxyUpstreamList(w http.ResponseWriter, r *http.Request) {
|
||||
endpoint, err := utils.PostPara(r, "ep")
|
||||
endpoint, err := utils.GetPara(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)
|
||||
}
|
||||
|
||||
|
@ -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},
|
||||
|
@ -59,9 +59,9 @@
|
||||
</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">
|
||||
<table class="ui unstackable basic celled table">
|
||||
<thead>
|
||||
<tr><th>Domain</th>
|
||||
<th>Last Update</th>
|
||||
@ -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;
|
||||
@ -157,18 +161,36 @@
|
||||
msgbox("Requesting certificate via " + defaultCA +"...");
|
||||
|
||||
//Request ACME for certificate
|
||||
let buttonOriginalHTML = "";
|
||||
if (btn != undefined){
|
||||
buttonOriginalHTML = $(btn).html();
|
||||
$(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');
|
||||
if (succ){
|
||||
$(btn).html(`<i class="ui green check icon"></i>`);
|
||||
if ($(btn).hasClass("icon")){
|
||||
//Only change the button icon
|
||||
if (succ){
|
||||
$(btn).html(`<i class="ui green check icon"></i>`);
|
||||
}else{
|
||||
$(btn).html(`<i class="ui red times icon"></i>`);
|
||||
}
|
||||
}else{
|
||||
$(btn).html(`<i class="ui red times icon"></i>`);
|
||||
//Show error or success icon with text
|
||||
if (succ){
|
||||
$(btn).html(`<i class="ui green check icon"></i> Requested`);
|
||||
}else{
|
||||
$(btn).html(`<i class="ui red times icon"></i> Error`);
|
||||
}
|
||||
}
|
||||
|
||||
//Restore the button after 3 seconds
|
||||
setTimeout(function(){
|
||||
$(btn).html(buttonOriginalHTML);
|
||||
}, 3000);
|
||||
|
||||
setTimeout(function(){
|
||||
initManagedDomainCertificateList();
|
||||
@ -256,7 +278,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 +337,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
$.cjax({
|
||||
url: "/api/acme/autoRenew/email",
|
||||
method: "POST",
|
||||
data: {"set": newDefaultEmail},
|
||||
@ -329,7 +351,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
$.cjax({
|
||||
url: "/api/acme/autoRenew/ca",
|
||||
data: {"set": newDefaultCA},
|
||||
method: "POST",
|
||||
@ -356,13 +378,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 +422,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 +444,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 +453,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 +470,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 +491,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 +518,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 +556,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 +570,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) {
|
||||
|
@ -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",
|
||||
|
@ -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: {
|
||||
|
@ -348,6 +348,32 @@
|
||||
`);
|
||||
}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 enableQuickRequestButton = 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);
|
||||
}
|
||||
|
||||
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
|
||||
if (payload.RootOrMatchingDomain.indexOf("*") > -1){
|
||||
enableQuickRequestButton = false;
|
||||
}
|
||||
|
||||
if (payload.MatchingDomainAlias != undefined){
|
||||
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
|
||||
if (payload.MatchingDomainAlias[i].indexOf("*") > -1){
|
||||
enableQuickRequestButton = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//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;">
|
||||
@ -355,9 +381,9 @@
|
||||
<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 ${enableQuickRequestButton?"":"disabled"} button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="requestCertificateForExistingHost('${uuid}', '${certificateDomains}', this);"><i class="green lock icon"></i> Get Certificate</button>
|
||||
`);
|
||||
|
||||
$(".hostAccessRuleSelector").dropdown();
|
||||
@ -400,7 +426,7 @@
|
||||
let rateLimit = $(row).find(".RateLimit").val();
|
||||
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
|
||||
|
||||
$.ajax({
|
||||
$.cjax({
|
||||
url: "/api/proxy/edit",
|
||||
method: "POST",
|
||||
data: {
|
||||
@ -422,6 +448,28 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//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){
|
||||
@ -474,7 +522,7 @@
|
||||
function handleProxyRuleToggle(object){
|
||||
let endpointUUID = $(object).attr("eptuuid");
|
||||
let isChecked = object.checked;
|
||||
$.ajax({
|
||||
$.cjax({
|
||||
url: "/api/proxy/toggle",
|
||||
data: {
|
||||
"ep": endpointUUID,
|
||||
@ -495,6 +543,35 @@
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Certificate Shortcut
|
||||
*/
|
||||
|
||||
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains, btn=undefined){
|
||||
RootAndAliasDomains = JSON.parse(decodeURIComponent(RootAndAliasDomains))
|
||||
let renewDomainKey = RootAndAliasDomains.join(",");
|
||||
let preferedACMEEmail = $("#prefACMEEmail").val();
|
||||
if (preferedACMEEmail == ""){
|
||||
msgbox("Preferred email for ACME registration not set", false);
|
||||
return;
|
||||
}
|
||||
let defaultCA = $("#defaultCA").dropdown("get value");
|
||||
if (defaultCA == ""){
|
||||
defaultCA = "Let's Encrypt";
|
||||
}
|
||||
|
||||
//Check if the root or the alias domain contain wildcard character, if yes, return error
|
||||
for (var i = 0; i < RootAndAliasDomains.length; i++){
|
||||
if (RootAndAliasDomains[i].indexOf("*") != -1){
|
||||
msgbox("Wildcard domain can only be setup via ACME tool", false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//Renew the certificate
|
||||
renewCertificate(renewDomainKey, false, btn);
|
||||
}
|
||||
|
||||
//Bind on tab switch events
|
||||
tabSwitchEventBind["httprp"] = function(){
|
||||
listProxyEndpoints();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user