71 Commits

Author SHA1 Message Date
d1e5581eea Merge pull request #449 from tobychui/v3.1.6
- Exposed log file, sys.uuid and static web server path to start flag
- Optimized connection close implementation
- Added toggle for uptime monitor
- Added optional copy HTTP custom headers to websocket connection
2024-12-31 21:49:41 +08:00
be5797c8a5 Updated geodb and minor instructions 2024-12-31 21:47:19 +08:00
ebd316a7f1 Exposed log and db filepath setting 2024-12-31 21:14:37 +08:00
84aec4387a Added CF and Fastly IP in access list
Added CF and Fastly Client IP passthrough header for access control ip resolver
2024-12-31 20:30:36 +08:00
30dfb9cb65 Added new UI feature
- Added toggle for uptime monitor
- Added toggle for enable custom header passthrough to websocket
2024-12-30 21:41:15 +08:00
0b1768ab5b Added manual toggle for websocket header copy
- Added setting for toggling websocket header copy
- Added close connection in TLS mode
2024-12-30 21:07:29 +08:00
ad4721820b Added websocket header test and benchmark tool 2024-12-30 21:01:45 +08:00
1d4c275db3 Fixed nil pointer exception in new setups 2024-12-29 16:11:00 +08:00
b3ad97743c Fixed #444
- Restored legacy behavior if proxmox cookie is detected in request
2024-12-29 15:09:24 +08:00
1a6a87e79b Merge pull request #443 from Morethanevil/main
Update CHANGELOG.md
2024-12-28 15:19:43 +08:00
749fd4b7af Update CHANGELOG.md 2024-12-28 05:25:00 +01:00
85422c0a74 Merge pull request #439 from tobychui/v3.1.5
Fixed hostname case sensitive bug
Fixed ACME table too wide css bug
Fixed HSTS toggle button bug
Fixed slow GeoIP resolve mode concurrent r/w bug
Added close connection as default site option
Added experimental authelia support
Added custom header support to websocket
Added levelDB as database implementation (not currently used)
Added external GeoIP db loading support
Restructured a lot of modules
2024-12-27 22:12:55 +08:00
73999c1ae9 Merge pull request #440 from PassiveLemon/docker-3.1.5
Add 2 new flags to Docker container and image build instructions
2024-12-27 21:26:18 +08:00
0ad84b3415 Add 2 new flags 2024-12-26 16:17:02 -05:00
64b6769695 Added external geoip db option
- Added support for loading geoip db from external file
- Added -update_geoip flag for automatically update the geoip
2024-12-24 21:12:26 +08:00
e72b2f9e09 Updated geoip database 2024-12-24 20:34:10 +08:00
992dd231f2 Fixed #435 2024-12-22 13:25:16 +08:00
49555c1191 Fixed #430
+ Added no response and I'm a Teapot (config file editing only) to default site options
2024-12-17 22:08:32 +08:00
2fca458bd0 Image building instructions and README touch-ups 2024-12-16 18:14:02 -05:00
2423d0fb3a Added experimental authelia support
- Integrated #33 code snippet
- Added UI for setting Authelia server address
- Updated authentication provider implementation
2024-12-15 15:52:59 +08:00
bb0f55018c System arch optimization
- Optimized types and definitions
- Moved shutdown seq to start.go file
- Moved authelia to auth/sso module
- Added different auth types support (wip)
- Updated proxy config structure
- Added v3.1.4 to v3.1.5 auto upgrade utilities
- Fixed #426
- Optimized status page UI
- Added options to disable uptime montior in config
2024-12-12 20:49:53 +08:00
9e95d84627 Fixed #422
- Added scroll to acme domain table
2024-12-10 21:13:26 +08:00
e73841786b Merge pull request #421 from 7brend7/authelia-integration
Add authelia-verify support
2024-12-10 21:02:58 +08:00
d5449c947a Add authelia-verify suppport 2024-12-09 15:19:07 +02:00
8ff51044bb Fixed #414
- Added sticky menu
- Optimized terminate routine for nil check
- Added test case for statistic module
2024-12-08 12:54:50 +08:00
cc08c704de Database update
- Removed read-only mode
- Added LevelDB for big data storage

TODO: Update backup utilities to support new db structure
2024-12-06 23:34:21 +08:00
2f1a6b5ba4 Merge pull request #416 from tobychui/main
Sync update from main branch
2024-12-06 19:46:51 +08:00
4d163fe80f Merge pull request #406 from Sickjuicy/main
Domain Name Server Option
2024-12-06 19:29:29 +08:00
24371ed22e Fixed #415
- Fixed UI issue on the HSTS toggle
- Added error message on save error for HSTS
2024-12-06 19:06:59 +08:00
12358d3522 added downward compality and spaces are cut from the json 2024-12-01 16:01:23 +01:00
c39af1ff8e Update def.go
Updated version number
2024-12-01 21:22:43 +08:00
6bf944e13c Fixed #401
- Fixed high concurrency panic on slow geoIP resolve mode
- Added test case for concurrent geodb access
2024-12-01 21:21:53 +08:00
b653b805b8 Update autorenew.go 2024-12-01 04:29:29 +01:00
eb91865b70 Added to read json for the renew cert and fixed bug where on creation of a new cert the old NameServer ware used 2024-12-01 04:25:01 +01:00
57e72a8a90 added some commands back 2024-11-30 04:38:29 +01:00
4dbf110edc more Cleanup 2024-11-30 04:20:39 +01:00
1eefa99b72 Cleanup 2024-11-30 03:35:30 +01:00
e6b2d458f7 Added Custom Name Server Option 2024-11-26 23:30:24 +01:00
4a4483e09d Merge pull request #400 from Morethanevil/main
Update CHANGELOG.md
2024-11-24 20:05:02 +08:00
4485d1f811 Update CHANGELOG.md
Updated changelog

Great Work as always, dark mode looks cool. If you want a suggestion about colors, I recommend [Catppuccin](https://github.com/catppuccin)
2024-11-24 11:52:03 +01:00
0eb0696670 Merge pull request #399 from tobychui/v3.1.4
V3.1.4
2024-11-24 14:47:53 +08:00
9fca2354c6 Update darktheme.css
Fixed docker container list text theme color
2024-11-24 14:41:01 +08:00
e56b045689 Added dark theme to docker container list 2024-11-24 13:58:46 +08:00
763ccb4d60 Remove deprecated ZeroTier config directory from Docker readme 2024-11-24 00:44:52 -05:00
4d4492069d Merge branch 'main' into v3.1.4 2024-11-24 12:37:58 +08:00
f3591aa171 Update dockerContainersList.html
Merged PR into dark theme branch
2024-11-24 12:35:26 +08:00
2dcf578cbe Update README.md 2024-11-24 11:52:57 +08:00
23a5c6ceb0 Updated geoIP database 2024-11-24 11:46:49 +08:00
015889851a Optimized UX and code structure
+ Added automatic self-sign certificate sniffing
+ Moved all constant into def.go
+ Added auto restart on port change when proxy server is running
+ Optimized slow search geoIP resolver by introducing new cache mechanism
+ Updated default incoming port to HTTPS instead of HTTP
2024-11-24 11:38:01 +08:00
093ed9c212 Merge pull request #395 from eyerrock/container-searchbar
search bar for Docker container list
2024-11-21 21:47:38 +08:00
0af8c67346 Updated API register function
- Seperated different register for APIs
2024-11-19 21:13:02 +08:00
c5170bcb94 Refactorized main entry function
- Moved constants to def.go
- Added acme close function (not used for now)
- Added robots.txt to prevent webmin panel being scanned by search engine
2024-11-19 20:30:36 +08:00
cd48388c02 refactored docker container list 2024-11-18 21:01:54 +01:00
373845f8fd added searchbar to docker container list 2024-11-18 18:16:07 +01:00
293a527ffc Completed dark theme 2024-11-18 21:04:25 +08:00
e4facbc7b6 Added more dark themes
- Added wrappers for snippet dark theme
- Optimized color pallets
2024-11-17 17:41:22 +08:00
1c79fa4e96 Fixed #394 2024-11-17 08:38:13 +08:00
6515eb99e3 Fixed #393
Updated version code manually
2024-11-15 06:48:35 +08:00
ec5c24b9b8 Added more darktheme
- Added more dark theme css
- Merged main branch fixes and new features
- Added todo tag for custom timeout
2024-11-14 21:18:05 +08:00
df88084375 Merge pull request #391 from eyerrock/list-containers-with-unexposed-ports
list containers with unexposed ports
2024-11-14 20:06:31 +08:00
74017baecf Merge pull request #392 from PassiveLemon/zoraxy-volume
Symlink ZeroTier var to Zoraxy config
2024-11-13 18:42:35 +08:00
294d504ee6 Symlink ZeroTier var to Zoraxy config 2024-11-12 12:40:08 -05:00
477429900e list containers with unexposed ports 2024-11-11 21:07:07 +01:00
2e9bc77a5d Merge commit from fork
Fixed web ssh security bug
2024-11-10 13:57:01 +08:00
ed178d857a Fixed web ssh security bug 2024-11-10 13:22:32 +08:00
4cf5d29692 Added more dark theme 2024-11-09 16:12:41 +08:00
634e9c9855 v3.1.3 init commit
- Fixed #378
- Added wip dark theme
- Fixed in code typo
- Fixed int conversion bug in some DNS challenge supplier
2024-11-08 22:24:07 +08:00
e79a70b7ac Merge pull request #376 from PassiveLemon/actions-cache
Add layer caching to Docker action
2024-11-06 06:58:52 +08:00
779115d06b Add layer caching to Docker action 2024-11-04 20:39:47 -05:00
9cb315ea67 Merge pull request #373 from Morethanevil/main
Update CHANGELOG.md
2024-11-03 17:41:49 +08:00
43ba00ec8d Update CHANGELOG.md
Thanks for your work :)
2024-11-03 10:11:20 +01:00
125 changed files with 47106 additions and 38012 deletions

43
.github/workflows/docker.yml vendored Normal file
View 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

View File

@ -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 \
.

2
.gitignore vendored
View File

@ -30,7 +30,7 @@ src/certs/*
src/rules/*
src/README.md
docker/ContainerTester.sh
docker/ImagePublisher.sh
docker/docker-compose.yaml
src/mod/acme/test/stackoverflow.pem
/tools/dns_challenge_update/code-gen/acmedns
/tools/dns_challenge_update/code-gen/lego

View File

@ -1,3 +1,49 @@
# v3.1.5 28 Dec 2024
+ Fixed hostname case sensitive bug [#435](https://github.com/tobychui/zoraxy/issues/435)
+ Fixed ACME table too wide css bug [#422](https://github.com/tobychui/zoraxy/issues/422)
+ Fixed HSTS toggle button bug [#415](https://github.com/tobychui/zoraxy/issues/415)
+ Fixed slow GeoIP resolve mode concurrent r/w bug [#401](https://github.com/tobychui/zoraxy/issues/401)
+ Added close connection as default site option [#430](https://github.com/tobychui/zoraxy/issues/430)
+ Added experimental authelia support [#384](https://github.com/tobychui/zoraxy/issues/384)
+ Added custom header support to websocket [#426](https://github.com/tobychui/zoraxy/issues/426)
+ Added levelDB as database implementation (not currently used)
+ Added external GeoIP db loading support
+ Restructured a lot of modules
# v3.1.4 24 Nov 2024
+ **Added Dark Theme Mode** [#390](https://github.com/tobychui/zoraxy/issues/390) [#82](https://github.com/tobychui/zoraxy/issues/82)
+ Added an auto sniffer for self-signed certificates
+ Added robots.txt to the project
+ Introduced an EU wrapper in the front-end for automatic registration of 26 countries [#378](https://github.com/tobychui/zoraxy/issues/378)
+ Moved all hard-coded values to a dedicated def.go file
+ Fixed a panic issue occurring on unsupported platform exits
+ Integrated fixes for SSH proxy and Docker snippet updates [#330](https://github.com/tobychui/zoraxy/issues/330) [#348](https://github.com/tobychui/zoraxy/issues/348)
+ **Changed the default listening port to 443 and enable TLS by default**
+ Optimized GeoIP database slow-search mode CPU usage
# v3.1.3 12 Nov 2024
+ Fixed a critical security bug [CVE-2024-52010](https://github.com/advisories/GHSA-7hpf-g48v-hw3j)
# 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)

View File

@ -33,6 +33,7 @@ A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
- Basic single-admin management mode
- External permission management system for easy system integration
- SMTP config for password reset
- Dark Theme Mode
## Downloads
@ -102,6 +103,8 @@ Usage of zoraxy:
Enable auto config upgrade if breaking change is detected (default true)
-docker
Run Zoraxy in docker compatibility mode
-earlyrenew int
Number of days to early renew a soon expiring certificate (days) (default 30)
-fastgeoip
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
-mdns
@ -119,7 +122,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 change in start parameters (default "./www")
Static web server root folder. Only allow chnage in start paramters (default "./www")
-ztauth string
ZeroTier authtoken for the local node
-ztport int
@ -134,7 +137,8 @@ If you already have an upstream reverse proxy server in place with permission ma
./zoraxy -noauth=true
```
*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.*
> [!WARNING]
> 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
@ -154,7 +158,7 @@ This project also compatible with [ZeroTier](https://www.zerotier.com/). However
To use Zoraxy with ZeroTier, assuming you already have a valid license, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken in the correct location on your host.
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags::
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags:
```bash
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
@ -175,7 +179,7 @@ Web SSH currently only supports Linux based OSes. The following platforms are su
### Loopback Connection
Loopback web SSH connection, by default, is disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
Loopback web SSH connections, by default, are disabled. This means that if you are trying to connect to an address like 127.0.0.1 or localhost, the system will reject your connection for security reasons. To enable loopback for testing or development purpose, use the following flags to override the loopback checking:
```bash
./zoraxy -sshlb=true

View File

@ -44,6 +44,7 @@ ENV ZEROTIER="false"
ENV AUTORENEW="86400"
ENV CFGUPGRADE="true"
ENV DB="auto"
ENV DOCKER="true"
ENV EARLYRENEW="30"
ENV FASTGEOIP="false"
@ -52,13 +53,14 @@ ENV MDNSNAME="''"
ENV NOAUTH="false"
ENV PORT="8000"
ENV SSHLB="false"
ENV UPDATE_GEOIP="false"
ENV VERSION="false"
ENV WEBFM="true"
ENV WEBROOT="./www"
ENV ZTAUTH=""
ENV ZTPORT="9993"
VOLUME [ "/opt/zoraxy/config/", "/var/lib/zerotier-one/" ]
VOLUME [ "/opt/zoraxy/config/" ]
ENTRYPOINT [ "/opt/zoraxy/entrypoint.sh" ]

View File

@ -9,7 +9,7 @@
If you are attempting to access your service from outside your network, make sure to forward ports 80 and 443 to the Zoraxy host to allow web traffic. If you know how to do this, great! If not, find the manufacturer of your router and search on how to do that. There are too many to be listed here. Read more about it from [whatismyip](https://www.whatismyip.com/port-forwarding/).
In the examples below, make sure to update `/path/to/zoraxy/config/` with your actual path. If a path is not provided, a Docker volume will be created at the location but it is recommended to store the data at a defined host location.
In the examples below, make sure to update `/path/to/zoraxy/config/`. 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 or a named Docker volume.
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.
@ -23,11 +23,9 @@ docker run -d \
-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
```
@ -45,12 +43,10 @@ services:
- 8000:8000
volumes:
- /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:
FASTGEOIP: "true"
ZEROTIER: "true"
```
### Ports
@ -66,7 +62,6 @@ services:
| 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. |
@ -78,6 +73,7 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
|:-|:-|:-|
| `AUTORENEW` | `86400` (Integer) | ACME auto TLS/SSL certificate renew check interval. |
| `CFGUPGRADE` | `true` (Boolean) | Enable auto config upgrade if breaking change is detected. |
| `DB` | `auto` (String) | Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV (default "auto"). |
| `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). |
@ -86,6 +82,7 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
| `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). |
| `UPDATE_GEOIP` | `false` (Boolean) | Download the latest GeoIP data and exit. |
| `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. |
@ -96,3 +93,12 @@ Variables are the same as those in [Start Parameters](https://github.com/tobychu
> [!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.
### Building
To build the Docker image:
- Check out the repository/branch.
- Copy the Zoraxy `src/` directory into the `docker/` (here) directory.
- Run the build command with `docker build -t zoraxy_build .`
- You can now use the image `zoraxy_build`
- If you wish to change the image name, then modify`zoraxy_build` in the previous step and then build again.

View File

@ -1,17 +1,25 @@
#!/usr/bin/env bash
update-ca-certificates
echo "CA certificates updated"
echo "CA certificates updated."
zoraxy -update_geoip=true
echo "Updated GeoIP data."
if [ "$ZEROTIER" = "true" ]; then
if [ ! -d "/opt/zoraxy/config/zerotier/" ]; then
mkdir -p /opt/zoraxy/config/zerotier/
fi
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
zerotier-one -d
echo "ZeroTier daemon started"
echo "ZeroTier daemon started."
fi
echo "Starting Zoraxy..."
exec zoraxy \
-autorenew="$AUTORENEW" \
-cfgupgrade="$CFGUPGRADE" \
-db="$DB" \
-docker="$DOCKER" \
-earlyrenew="$EARLYRENEW" \
-fastgeoip="$FASTGEOIP" \
@ -20,6 +28,7 @@ exec zoraxy \
-noauth="$NOAUTH" \
-port=:"$PORT" \
-sshlb="$SSHLB" \
-update_geoip="$UPDATE_GEOIP" \
-version="$VERSION" \
-webfm="$WEBFM" \
-webroot="$WEBROOT" \

View File

@ -230,7 +230,17 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
return
}
rule.AddCountryCodeToBlackList(countryCode, comment)
//Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.AddCountryCodeToBlackList(code, comment)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.AddCountryCodeToBlackList(countryCode, comment)
}
utils.SendOK(w)
}
@ -254,7 +264,17 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
return
}
rule.RemoveCountryCodeFromBlackList(countryCode)
//Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.RemoveCountryCodeFromBlackList(code)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.RemoveCountryCodeFromBlackList(countryCode)
}
utils.SendOK(w)
}
@ -397,7 +417,17 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
p := bluemonday.StrictPolicy()
comment = p.Sanitize(comment)
rule.AddCountryCodeToWhitelist(countryCode, comment)
//Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.AddCountryCodeToWhitelist(code, comment)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.AddCountryCodeToWhitelist(countryCode, comment)
}
utils.SendOK(w)
}
@ -420,7 +450,17 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
return
}
rule.RemoveCountryCodeFromWhitelist(countryCode)
//Check if the country code contains comma, if yes, split it
if strings.Contains(countryCode, ",") {
codes := strings.Split(countryCode, ",")
for _, code := range codes {
code = strings.TrimSpace(code)
rule.RemoveCountryCodeFromWhitelist(code)
}
} else {
countryCode = strings.TrimSpace(countryCode)
rule.RemoveCountryCodeFromWhitelist(countryCode)
}
utils.SendOK(w)
}

View File

@ -41,6 +41,20 @@ func initACME() *acme.ACMEHandler {
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb, SystemWideLogger)
}
// Restart ACME handler and auto renewer
func restartACMEHandler() {
SystemWideLogger.Println("Restarting ACME handler")
//Clos the current handler and auto renewer
acmeHandler.Close()
acmeAutoRenewer.Close()
acmeDeregisterSpecialRoutingRule()
//Reinit the handler with a new random port
acmeHandler = initACME()
acmeRegisterSpecialRoutingRule()
}
// create the special routing rule for ACME
func acmeRegisterSpecialRoutingRule() {
SystemWideLogger.Println("Assigned temporary port:" + acmeHandler.Getport())
@ -82,6 +96,12 @@ func acmeRegisterSpecialRoutingRule() {
}
}
// remove the special routing rule for ACME
func acmeDeregisterSpecialRoutingRule() {
SystemWideLogger.Println("Removing ACME routing rule")
dynamicProxyRouter.RemoveRoutingRule("acme-autorenew")
}
// This function check if the renew setup is satisfied. If not, toggle them automatically
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
isForceHttpsRedirectEnabledOriginally := false

View File

@ -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/dynamicproxy/domainsniff"
"imuslab.com/zoraxy/mod/ipscan"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/netutils"
@ -18,34 +19,11 @@ import (
API.go
This file contains all the API called by the web management interface
*/
var requireAuth = true
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)
},
})
//Register the standard web services urls
fs := http.FileServer(http.FS(webres))
if development {
fs = http.FileServer(http.Dir("web/"))
}
//Add a layer of middleware for advance control
advHandler := FSHandler(fs)
targetMux.Handle("/", advHandler)
//Authentication APIs
registerAuthAPIs(requireAuth, targetMux)
//Reverse proxy
// Register the APIs for HTTP proxy management functions
func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
/* Reverse Proxy Settings & Status */
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
@ -56,24 +34,24 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS)
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
//Reverse proxy upstream (load balance) APIs
/* Reverse proxy upstream (load balance) */
authRouter.HandleFunc("/api/proxy/upstream/list", ReverseProxyUpstreamList)
authRouter.HandleFunc("/api/proxy/upstream/add", ReverseProxyUpstreamAdd)
authRouter.HandleFunc("/api/proxy/upstream/setPriority", ReverseProxyUpstreamSetPriority)
authRouter.HandleFunc("/api/proxy/upstream/update", ReverseProxyUpstreamUpdate)
authRouter.HandleFunc("/api/proxy/upstream/remove", ReverseProxyUpstreamDelete)
//Reverse proxy virtual directory APIs
/* Reverse proxy virtual directory */
authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
//Reverse proxy user define header apis
/* Reverse proxy user-defined header */
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
@ -81,12 +59,15 @@ func initAPIs(targetMux *http.ServeMux) {
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/header/handleWsHeaderBehavior", HandleWsHeaderBehavior)
/* Reverse proxy auth related */
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/delete", RemoveProxyBasicAuthExceptionPaths)
}
//TLS / SSL config
// Register the APIs for TLS / SSL certificate management functions
func RegisterTLSAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
@ -95,63 +76,72 @@ func initAPIs(targetMux *http.ServeMux) {
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)
// Register the APIs for Authentication handlers like Authelia and OAUTH2
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS)
}
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
// Register the APIs for redirection rules management functions
func RegisterRedirectionAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
}
//Access Rules API
// Register the APIs for access rules management functions
func RegisterAccessRuleAPIs(authRouter *auth.RouterDef) {
/* Access Rules Settings & Status */
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
//Blacklist APIs
/* Blacklist */
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove)
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
//Whitelist APIs
/* Whitelist */
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
authRouter.HandleFunc("/api/whitelist/country/remove", handleCountryWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/ip/add", handleIpWhitelistAdd)
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
}
//Path Blocker APIs
// Register the APIs for path blocking rules management functions, WIP
func RegisterPathRuleAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath)
authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath)
authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath)
}
//Statistic & uptime monitoring API
// Register the APIs statistic anlysis and uptime monitoring functions
func RegisterStatisticalAPIs(authRouter *auth.RouterDef) {
/* Traffic Summary */
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
authRouter.HandleFunc("/api/stats/netstat", netstatBuffers.HandleGetNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/netstatgraph", netstatBuffers.HandleGetBufferedNetworkInterfaceStats)
authRouter.HandleFunc("/api/stats/listnic", netstat.HandleListNetworkInterfaces)
/* Zoraxy Analytic */
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
/* UpTime Monitor */
authRouter.HandleFunc("/api/utm/list", HandleUptimeMonitorListing)
}
//Global Area Network APIs
// Register the APIs for Global Area Network management functions, Will be moving to plugin soon
func RegisterGANAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/gan/network/info", ganManager.HandleGetNodeID)
authRouter.HandleFunc("/api/gan/network/add", ganManager.HandleAddNetwork)
authRouter.HandleFunc("/api/gan/network/remove", ganManager.HandleRemoveNetwork)
@ -166,8 +156,10 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
}
//Stream (TCP / UDP) Proxy
// Register the APIs for Stream (TCP / UDP) Proxy management functions
func RegisterStreamProxyAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
@ -175,19 +167,57 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy)
authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy)
authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
}
//mDNS APIs
// Register the APIs for mDNS service management functions
func RegisterMDNSAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)
authRouter.HandleFunc("/api/mdns/discover", HandleMdnsScanning)
}
//Zoraxy Analytic
authRouter.HandleFunc("/api/analytic/list", AnalyticLoader.HandleSummaryList)
authRouter.HandleFunc("/api/analytic/load", AnalyticLoader.HandleLoadTargetDaySummary)
authRouter.HandleFunc("/api/analytic/loadRange", AnalyticLoader.HandleLoadTargetRangeSummary)
authRouter.HandleFunc("/api/analytic/exportRange", AnalyticLoader.HandleRangeExport)
authRouter.HandleFunc("/api/analytic/resetRange", AnalyticLoader.HandleRangeReset)
// Register the APIs for ACME and Auto Renewer management functions
func RegisterACMEAndAutoRenewerAPIs(authRouter *auth.RouterDef) {
/* ACME Core */
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
/* Auto Renewer */
authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable)
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HandleSetDNS)
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson)
/* ACME Wizard */
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck)
}
//Network utilities
// Register the APIs for Static Web Server management functions
func RegisterStaticWebServerAPIs(authRouter *auth.RouterDef) {
/* Static Web Server Controls */
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
/* File Manager */
if *allowWebFileManager {
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
}
}
// Register the APIs for Network Utilities functions
func RegisterNetworkUtilsAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/tools/ipscan", ipscan.HandleIpScan)
authRouter.HandleFunc("/api/tools/portscan", ipscan.HandleScanPort)
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
@ -202,66 +232,10 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/tools/smtp/test", HandleTestEmailSend)
authRouter.HandleFunc("/api/tools/fwdproxy/enable", forwardProxy.HandleToogle)
authRouter.HandleFunc("/api/tools/fwdproxy/port", forwardProxy.HandlePort)
//Account Reset
targetMux.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup)
//ACME & Auto Renewer
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable)
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HanldeSetDNS)
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson)
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
//Static Web Server
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
if *allowWebFileManager {
//Web Directory Manager file operation functions
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
}
//Docker UX Optimizations
authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable)
authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList)
//Others
targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo)
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
//Debug
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
//If you got APIs to add, append them here
}
// Function to renders Auth related APIs
func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
//Auth APIs
// Register the APIs for Auth functions, due to scoping issue some functions are defined here
func RegisterAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
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) {
@ -277,21 +251,17 @@ func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
js, _ := json.Marshal(username)
utils.SendJSONResponse(w, string(js))
})
targetMux.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
uc := authAgent.GetUserCounts()
js, _ := json.Marshal(uc)
js, _ := json.Marshal(authAgent.GetUserCounts())
utils.SendJSONResponse(w, string(js))
})
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) {
})
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {})
} else {
//This function is disabled
utils.SendErrorResponse(w, "Root management account already exists")
@ -332,5 +302,60 @@ func registerAuthAPIs(requireAuth bool, targetMux *http.ServeMux) {
authAgent.UnregisterUser(username)
authAgent.CreateUserAccount(username, newPassword, "")
})
}
/* Register all the APIs */
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)
},
})
//Register the standard web services urls
fs := http.FileServer(http.FS(webres))
if DEVELOPMENT_BUILD {
fs = http.FileServer(http.Dir("web/"))
}
//Add a layer of middleware for advance control
advHandler := FSHandler(fs)
targetMux.Handle("/", advHandler)
//Register the APIs
RegisterAuthAPIs(requireAuth, targetMux)
RegisterHTTPProxyAPIs(authRouter)
RegisterTLSAPIs(authRouter)
RegisterAuthenticationHandlerAPIs(authRouter)
RegisterRedirectionAPIs(authRouter)
RegisterAccessRuleAPIs(authRouter)
RegisterPathRuleAPIs(authRouter)
RegisterStatisticalAPIs(authRouter)
RegisterGANAPIs(authRouter)
RegisterStreamProxyAPIs(authRouter)
RegisterMDNSAPIs(authRouter)
RegisterNetworkUtilsAPIs(authRouter)
RegisterACMEAndAutoRenewerAPIs(authRouter)
RegisterStaticWebServerAPIs(authRouter)
//Account Reset
targetMux.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
targetMux.HandleFunc("/api/account/new", HandleNewPasswordSetup)
//Docker UX Optimizations
authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable)
authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList)
//Others
targetMux.HandleFunc("/api/info/x", HandleZoraxyInfo)
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
//Debug
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
}

View File

@ -177,7 +177,10 @@ func handleListDomains(w http.ResponseWriter, r *http.Request) {
// Handle front-end toggling TLS mode
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
currentTlsSetting := false
currentTlsSetting := true //Default to true
if dynamicProxyRouter.Option != nil {
currentTlsSetting = dynamicProxyRouter.Option.UseTls
}
if sysdb.KeyExists("settings", "usetls") {
sysdb.Read("settings", "usetls", &currentTlsSetting)
}

View File

@ -48,7 +48,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
}
//Parse it into dynamic proxy endpoint
thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
thisConfigEndpoint := dynamicproxy.GetDefaultProxyEndpoint()
err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
if err != nil {
return err
@ -59,7 +59,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
thisConfigEndpoint.RootOrMatchingDomain = "/"
}
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Root {
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
//This is a root config file
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
@ -68,7 +68,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Host {
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyTypeHost {
//This is a host config file
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
@ -97,7 +97,7 @@ func filterProxyConfigFilename(filename string) string {
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
//Get filename for saving
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
if endpoint.ProxyType == dynamicproxy.ProxyType_Root {
if endpoint.ProxyType == dynamicproxy.ProxyTypeRoot {
filename = "./conf/proxy/root.config"
}
@ -129,27 +129,23 @@ func RemoveReverseProxyConfig(endpoint string) error {
// Get the default root config that point to the internal static web server
// this will be used if root config is not found (new deployment / missing root.config file)
func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
//Default settings
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyType_Root,
RootOrMatchingDomain: "/",
ActiveOrigins: []*loadbalance.Upstream{
{
OriginIpOrDomain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
RequireTLS: false,
SkipCertValidations: false,
Weight: 0,
},
//Get the default proxy endpoint
rootProxyEndpointConfig := dynamicproxy.GetDefaultProxyEndpoint()
rootProxyEndpointConfig.ProxyType = dynamicproxy.ProxyTypeRoot
rootProxyEndpointConfig.RootOrMatchingDomain = "/"
rootProxyEndpointConfig.ActiveOrigins = []*loadbalance.Upstream{
{
OriginIpOrDomain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
RequireTLS: false,
SkipCertValidations: false,
Weight: 0,
},
InactiveOrigins: []*loadbalance.Upstream{},
BypassGlobalTLS: false,
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
RequireBasicAuth: false,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
})
}
rootProxyEndpointConfig.DefaultSiteOption = dynamicproxy.DefaultSite_InternalStaticWebServer
rootProxyEndpointConfig.DefaultSiteValue = ""
//Default settings
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&rootProxyEndpointConfig)
if err != nil {
return nil, err
}
@ -167,12 +163,15 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
if includeSysDBRaw == "true" {
//Include the system database in backup snapshot
//Temporary set it to read only
sysdb.ReadOnly = true
includeSysDB = true
}
// Specify the folder path to be zipped
folderPath := "./conf/"
if !utils.FileExists("./conf") {
SystemWideLogger.PrintAndLog("Backup", "Configuration folder not found", nil)
return
}
folderPath := "./conf"
// Set the Content-Type header to indicate it's a zip file
w.Header().Set("Content-Type", "application/zip")
@ -227,7 +226,7 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
}
// Open the file on disk
file, err := os.Open("sys.db")
file, err := os.Open("./sys.db")
if err != nil {
SystemWideLogger.PrintAndLog("Backup", "Unable to open sysdb", err)
return
@ -241,8 +240,6 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
return
}
//Restore sysdb state
sysdb.ReadOnly = false
}
if err != nil {
@ -278,6 +275,8 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
targetDir := "./conf"
if utils.FileExists(targetDir) {
//Backup the old config to old
//backupPath := filepath.Dir(*path_conf) + filepath.Base(*path_conf) + ".old_" + strconv.Itoa(int(time.Now().Unix()))
//os.Rename(*path_conf, backupPath)
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
}

148
src/def.go Normal file
View File

@ -0,0 +1,148 @@
package main
/*
Type and flag definations
This file contains all the type and flag definations
Author: tobychui
*/
import (
"embed"
"flag"
"net/http"
"time"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dockerux"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/email"
"imuslab.com/zoraxy/mod/forwardproxy"
"imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/info/logviewer"
"imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/pathrule"
"imuslab.com/zoraxy/mod/sshprox"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/statistic/analytic"
"imuslab.com/zoraxy/mod/streamproxy"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/webserv"
)
const (
/* Build Constants */
SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.1.6"
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
/* System Constants */
TMP_FOLDER = "./tmp"
WEBSERV_DEFAULT_PORT = 5487
MDNS_HOSTNAME_PREFIX = "zoraxy_" /* Follow by node UUID */
MDNS_IDENTIFY_DEVICE_TYPE = "Network Gateway"
MDNS_IDENTIFY_DOMAIN = "zoraxy.aroz.org"
MDNS_IDENTIFY_VENDOR = "imuslab.com"
MDNS_SCAN_TIMEOUT = 30 /* Seconds */
MDNS_SCAN_UPDATE_INTERVAL = 15 /* Minutes */
GEODB_CACHE_CLEAR_INTERVAL = 15 /* Minutes */
ACME_AUTORENEW_CONFIG_PATH = "./conf/acme_conf.json"
CSRF_COOKIENAME = "zoraxy_csrf"
LOG_PREFIX = "zr"
LOG_EXTENSION = ".log"
/* Configuration Folder Storage Path Constants */
CONF_HTTP_PROXY = "./conf/proxy"
CONF_STREAM_PROXY = "./conf/streamproxy"
CONF_CERT_STORE = "./conf/certs"
CONF_REDIRECTION = "./conf/redirect"
CONF_ACCESS_RULE = "./conf/access"
CONF_PATH_RULE = "./conf/rules/pathrules"
)
/* System Startup Flags */
var (
webUIPort = flag.String("port", ":8000", "Management web interface listening port")
databaseBackend = flag.String("db", "auto", "Database backend to use (leveldb, boltdb, auto) Note that fsdb will be used on unsupported platforms like RISCV")
noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
showver = flag.Bool("version", false, "Show version of this server")
allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode")
acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)")
enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
/* Path Configuration Flags */
//path_database = flag.String("dbpath", "./sys.db", "Database path")
//path_conf = flag.String("conf", "./conf", "Configuration folder path")
path_uuid = flag.String("uuid", "./sys.uuid", "sys.uuid file path")
path_logFile = flag.String("log", "./log", "Log folder path")
path_webserver = flag.String("webroot", "./www", "Static web server root folder. Only allow change in start paramters")
/* Maintaince Function Flags */
geoDbUpdate = flag.Bool("update_geoip", false, "Download the latest GeoIP data and exit")
)
/* Global Variables and Handlers */
var (
nodeUUID = "generic" //System uuid in uuidv4 format, load from database on startup
bootTime = time.Now().Unix()
requireAuth = true //Require authentication for webmin panel, override from flag
/*
Binary Embedding File System
*/
//go:embed web/*
webres embed.FS
/*
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
webminPanelMux *http.ServeMux //Server mux for handling webmin panel APIs
csrfMiddleware func(http.Handler) http.Handler //CSRF protection middleware
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
accessController *access.Controller //Access controller, handle black list and white list
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
statisticCollector *statistic.Collector //Collecting statistic from visitors
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
mdnsScanner *mdns.MDNSHost //mDNS discovery services
ganManager *ganserv.NetworkManager //Global Area Network Manager
webSshManager *sshprox.Manager //Web SSH connection service
streamProxyManager *streamproxy.Manager //Stream Proxy Manager for TCP / UDP forwarding
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing
//Authentication Provider
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only
SystemWideLogger *logger.Logger //Logger for Zoraxy
LogViewer *logviewer.Viewer //Log viewer HTTP handlers
)

View File

@ -28,9 +28,11 @@ require (
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/golang/snappy v0.0.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/syndtr/goleveldb v1.0.0 // 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

View File

@ -277,6 +277,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -528,6 +530,7 @@ github.com/nzdjb/go-metaname v1.0.0 h1:sNASlZC1RM3nSudtBTE1a3ZVTDyTpjqI5WXRPrdZ9
github.com/nzdjb/go-metaname v1.0.0/go.mod h1:0GR0LshZax1Lz4VrOrfNSE4dGvTp7HGjiemdczXT2H4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
@ -536,6 +539,7 @@ github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
@ -660,6 +664,8 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 h1:RE84sHFFx6t24DJvSnF9fS1DzBNv9OpctzHK3t7AY+I=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E=

View File

@ -1,7 +1,36 @@
package main
/*
______
|___ /
/ / ___ _ __ __ ___ ___ _
/ / / _ \| '__/ _` \ \/ / | | |
/ /_| (_) | | | (_| |> <| |_| |
/_____\___/|_| \__,_/_/\_\\__, |
__/ |
|___/
Zoraxy - A general purpose HTTP reverse proxy and forwarding tool
Author: tobychui
License: AGPLv3
--------------------------------------------
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3 of the License or any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"embed"
"flag"
"fmt"
"log"
@ -13,100 +42,12 @@ import (
"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"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/email"
"imuslab.com/zoraxy/mod/forwardproxy"
"imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/info/logviewer"
"imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/pathrule"
"imuslab.com/zoraxy/mod/sshprox"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/statistic/analytic"
"imuslab.com/zoraxy/mod/streamproxy"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/update"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
"imuslab.com/zoraxy/mod/webserv"
)
// General flags
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
var showver = flag.Bool("version", false, "Show version of this server")
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
var mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
var runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode")
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
var acmeCertAutoRenewDays = flag.Int("earlyrenew", 30, "Number of days to early renew a soon expiring certificate (days)")
var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
var enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
var (
name = "Zoraxy"
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()
/*
Binary Embedding File System
*/
//go:embed web/*
webres embed.FS
/*
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
webminPanelMux *http.ServeMux //Server mux for handling webmin panel APIs
csrfMiddleware func(http.Handler) http.Handler //CSRF protection middleware
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
accessController *access.Controller //Access controller, handle black list and white list
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
statisticCollector *statistic.Collector //Collecting statistic from visitors
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
mdnsScanner *mdns.MDNSHost //mDNS discovery services
ganManager *ganserv.NetworkManager //Global Area Network Manager
webSshManager *sshprox.Manager //Web SSH connection service
streamProxyManager *streamproxy.Manager //Stream Proxy Manager for TCP / UDP forwarding
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser
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
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only
SystemWideLogger *logger.Logger //Logger for Zoraxy
LogViewer *logviewer.Viewer
)
// Kill signal handler. Do something before the system the core terminate.
/* SIGTERM handler, do shutdown sequences before closing */
func SetupCloseHandler() {
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
@ -117,45 +58,21 @@ func SetupCloseHandler() {
}()
}
func ShutdownSeq() {
SystemWideLogger.Println("Shutting down " + name)
//SystemWideLogger.Println("Closing GeoDB")
//geodbStore.Close()
SystemWideLogger.Println("Closing Netstats Listener")
netstatBuffers.Close()
SystemWideLogger.Println("Closing Statistic Collector")
statisticCollector.Close()
if mdnsTickerStop != nil {
SystemWideLogger.Println("Stopping mDNS Discoverer (might take a few minutes)")
// Stop the mdns service
mdnsTickerStop <- true
}
mdnsScanner.Close()
SystemWideLogger.Println("Shutting down load balancer")
loadBalancer.Close()
SystemWideLogger.Println("Closing Certificates Auto Renewer")
acmeAutoRenewer.Close()
//Remove the tmp folder
SystemWideLogger.Println("Cleaning up tmp files")
os.RemoveAll("./tmp")
//Close database
SystemWideLogger.Println("Stopping system database")
sysdb.Close()
//Close logger
SystemWideLogger.Println("Closing system wide logger")
SystemWideLogger.Close()
}
func main() {
//Parse startup flags
flag.Parse()
/* Maintaince Function Modes */
if *showver {
fmt.Println(name + " - Version " + version)
fmt.Println(SYSTEM_NAME + " - Version " + SYSTEM_VERSION)
os.Exit(0)
}
if *geoDbUpdate {
geodb.DownloadGeoDBUpdate("./conf/geodb")
os.Exit(0)
}
/* Main Zoraxy Routines */
if !utils.ValidateListeningAddress(*webUIPort) {
fmt.Println("Malformed -port (listening address) paramter. Do you mean -port=:" + *webUIPort + "?")
os.Exit(0)
@ -163,13 +80,13 @@ func main() {
if *enableAutoUpdate {
fmt.Println("Checking required config update")
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(version))
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(SYSTEM_VERSION))
}
SetupCloseHandler()
//Read or create the system uuid
uuidRecord := "./sys.uuid"
uuidRecord := *path_uuid
if !utils.FileExists(uuidRecord) {
newSystemUUID := uuid.New().String()
os.WriteFile(uuidRecord, []byte(newSystemUUID), 0775)
@ -185,13 +102,13 @@ func main() {
webminPanelMux = http.NewServeMux()
csrfMiddleware = csrf.Protect(
[]byte(nodeUUID),
csrf.CookieName("zoraxy-csrf"),
csrf.CookieName(CSRF_COOKIENAME),
csrf.Secure(false),
csrf.Path("/"),
csrf.SameSite(csrf.SameSiteLaxMode),
)
//Startup all modules
//Startup all modules, see start.go
startupSequence()
//Initiate management interface APIs
@ -208,11 +125,10 @@ func main() {
//Start the finalize sequences
finalSequence()
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
SystemWideLogger.Println(SYSTEM_NAME + " started. Visit control panel at http://localhost" + *webUIPort)
err = http.ListenAndServe(*webUIPort, csrfMiddleware(webminPanelMux))
if err != nil {
log.Fatal(err)
}
}

View File

@ -21,6 +21,7 @@ import (
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
@ -29,12 +30,20 @@ import (
"imuslab.com/zoraxy/mod/utils"
)
var defaultNameservers = []string{
"8.8.8.8:53", // Google DNS
"8.8.4.4:53", // Google DNS
"1.1.1.1:53", // Cloudflare DNS
"1.0.0.1:53", // Cloudflare DNS
}
type CertificateInfoJSON struct {
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
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
DNSServers []string `json:"dnsServers"` // DNS servers
}
// ACMEUser represents a user in the ACME system.
@ -86,8 +95,15 @@ func (a *ACMEHandler) Logf(message string, err error) {
a.Logger.PrintAndLog("ACME", message, err)
}
// Close closes the ACMEHandler.
// ACME Handler does not need to close anything
// Function defined for future compatibility
func (a *ACMEHandler) Close() error {
return nil
}
// 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, propagationTimeout int) (bool, error) {
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool, propagationTimeout int, dnsServers string) (bool, error) {
a.Logf("Obtaining certificate for: "+strings.Join(domains, ", "), nil)
// generate private key
@ -157,15 +173,31 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
return false, err
}
// Load certificate info from JSON file
certInfo, err := LoadCertInfoJSON(fmt.Sprintf("./conf/certs/%s.json", certificateName))
if err == nil {
useDNS = certInfo.UseDNS
if dnsServers == "" && certInfo.DNSServers != nil && len(certInfo.DNSServers) > 0 {
dnsServers = strings.Join(certInfo.DNSServers, ",")
}
propagationTimeout = certInfo.PropTimeout
}
// Clean DNS servers
dnsNameservers := strings.Split(dnsServers, ",")
for i := range dnsNameservers {
dnsNameservers[i] = strings.TrimSpace(dnsNameservers[i])
}
// setup how to receive challenge
if useDNS {
if !a.Database.TableExists("acme") {
a.Database.NewTable("acme")
return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -1)")
return false, errors.New("DNS Provider and DNS Credential configuration required for ACME Provider (Error -1)")
}
if !a.Database.KeyExists("acme", certificateName+"_dns_provider") || !a.Database.KeyExists("acme", certificateName+"_dns_credentials") {
return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -2)")
return false, errors.New("DNS Provider and DNS Credential configuration required for ACME Provider (Error -2)")
}
var dnsCredentials string
@ -188,7 +220,13 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
return false, err
}
err = client.Challenge.SetDNS01Provider(provider)
if len(dnsNameservers) > 0 && dnsNameservers[0] != "" {
a.Logf("Using DNS servers: "+strings.Join(dnsNameservers, ", "), nil)
err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(dnsNameservers))
} else {
// Use default DNS-01 nameservers if dnsServers is empty
err = client.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers(defaultNameservers))
}
if err != nil {
a.Logf("Failed to resolve DNS01 Provider", err)
return false, err
@ -285,12 +323,13 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
}
// Save certificate's ACME info for renew usage
certInfo := &CertificateInfoJSON{
certInfo = &CertificateInfoJSON{
AcmeName: caName,
AcmeUrl: caUrl,
SkipTLS: skipTLS,
UseDNS: useDNS,
PropTimeout: propagationTimeout,
DNSServers: dnsNameservers,
}
certInfoBytes, err := json.Marshal(certInfo)
@ -477,7 +516,21 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
for _, domain := range domains {
cleanedDomains = append(cleanedDomains, strings.TrimSpace(domain))
}
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout)
// Extract DNS servers from the request
var dnsServers []string
dnsServersPara, err := utils.PostPara(r, "dnsServers")
if err == nil && dnsServersPara != "" {
dnsServers = strings.Split(dnsServersPara, ",")
for i := range dnsServers {
dnsServers[i] = strings.TrimSpace(dnsServers[i])
}
}
// Convert DNS servers slice to a single string
dnsServersString := strings.Join(dnsServers, ",")
result, err := a.ObtainCert(cleanedDomains, filename, email, ca, caUrl, skipTLS, dns, propagationTimeout, dnsServersString)
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
@ -520,5 +573,10 @@ func LoadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
return nil, err
}
// Clean DNS servers
for i := range certInfo.DNSServers {
certInfo.DNSServers[i] = strings.TrimSpace(certInfo.DNSServers[i])
}
return certInfo, nil
}

View File

@ -26,6 +26,7 @@ type AutoRenewConfig struct {
Email string //Email for acme
RenewAll bool //Renew all or selective renew with the slice below
FilesToRenew []string //If RenewAll is false, renew these certificate files
DNSServers string // DNS servers
}
type AutoRenewer struct {
@ -308,7 +309,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
}
if CertExpireSoon(certBytes, a.EarlyRenewDays) || CertIsExpired(certBytes) {
//This cert is expired
DNSName, err := ExtractDomains(certBytes)
if err != nil {
//Maybe self signed. Ignore this
@ -355,6 +355,7 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
return a.renewExpiredDomains(expiredCertList)
}
// Close the auto renewer
func (a *AutoRenewer) Close() {
if a.TickerstopChan != nil {
a.TickerstopChan <- true
@ -390,7 +391,13 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
certInfo.PropTimeout = 300
}
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout)
// Extract DNS servers from the certificate info if available
var dnsServers string
if len(certInfo.DNSServers) > 0 {
dnsServers = strings.Join(certInfo.DNSServers, ",")
}
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS, certInfo.PropTimeout, dnsServers)
if err != nil {
a.Logf("Renew "+fileName+"("+strings.Join(expiredCert.Domains, ",")+") failed", err)
} else {
@ -440,7 +447,7 @@ func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
}
// Handle update auto renew DNS configuration
func (a *AutoRenewer) HanldeSetDNS(w http.ResponseWriter, r *http.Request) {
func (a *AutoRenewer) HandleSetDNS(w http.ResponseWriter, r *http.Request) {
dnsProvider, err := utils.PostPara(r, "dnsProvider")
if err != nil {
utils.SendErrorResponse(w, "dnsProvider not set")
@ -459,12 +466,18 @@ func (a *AutoRenewer) HanldeSetDNS(w http.ResponseWriter, r *http.Request) {
return
}
dnsServers, err := utils.PostPara(r, "dnsServers")
if err != nil {
dnsServers = ""
}
if !a.AcmeHandler.Database.TableExists("acme") {
a.AcmeHandler.Database.NewTable("acme")
}
a.AcmeHandler.Database.Write("acme", filename+"_dns_provider", dnsProvider)
a.AcmeHandler.Database.Write("acme", filename+"_dns_credentials", dnsCredentials)
a.AcmeHandler.Database.Write("acme", filename+"_dns_servers", dnsServers)
utils.SendOK(w)

View File

@ -3,7 +3,7 @@ package acme
/*
CA.go
This script load CA defination from embedded ca.json
This script load CA definition from embedded ca.json
*/
import (
_ "embed"
@ -13,7 +13,7 @@ import (
"strings"
)
// CA Defination, load from embeded json when startup
// CA definition, load from embeded json when startup
type CaDef struct {
Production map[string]string
Test map[string]string

View File

@ -210,8 +210,8 @@ func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
}
session.Values["authenticated"] = false
session.Values["username"] = nil
session.Save(r, w)
return nil
session.Options.MaxAge = -1
return session.Save(r, w)
}
// Get the current session username from request
@ -339,6 +339,7 @@ func (a *AuthAgent) CheckAuth(r *http.Request) bool {
if err != nil {
return false
}
// Check if user is authenticated
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
return false

View File

@ -1,34 +0,0 @@
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
}

View File

@ -0,0 +1,136 @@
package authelia
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type AutheliaRouterOptions struct {
UseHTTPS bool //If the Authelia server is using HTTPS
AutheliaURL string //The URL of the Authelia server
Logger *logger.Logger
Database *database.Database
}
type AutheliaRouter struct {
options *AutheliaRouterOptions
}
// NewAutheliaRouter creates a new AutheliaRouter object
func NewAutheliaRouter(options *AutheliaRouterOptions) *AutheliaRouter {
options.Database.NewTable("authelia")
//Read settings from database, if exists
options.Database.Read("authelia", "autheliaURL", &options.AutheliaURL)
options.Database.Read("authelia", "useHTTPS", &options.UseHTTPS)
return &AutheliaRouter{
options: options,
}
}
// HandleSetAutheliaURLAndHTTPS is the internal handler for setting the Authelia URL and HTTPS
func (ar *AutheliaRouter) HandleSetAutheliaURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current settings
js, _ := json.Marshal(map[string]interface{}{
"useHTTPS": ar.options.UseHTTPS,
"autheliaURL": ar.options.AutheliaURL,
})
utils.SendJSONResponse(w, string(js))
return
} else if r.Method == http.MethodPost {
//Update the settings
autheliaURL, err := utils.PostPara(r, "autheliaURL")
if err != nil {
utils.SendErrorResponse(w, "autheliaURL not found")
return
}
useHTTPS, err := utils.PostBool(r, "useHTTPS")
if err != nil {
useHTTPS = false
}
//Write changes to runtime
ar.options.AutheliaURL = autheliaURL
ar.options.UseHTTPS = useHTTPS
//Write changes to database
ar.options.Database.Write("authelia", "autheliaURL", autheliaURL)
ar.options.Database.Write("authelia", "useHTTPS", useHTTPS)
utils.SendOK(w)
} else {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
}
// handleAutheliaAuth is the internal handler for Authelia authentication
// Set useHTTPS to true if your authelia server is using HTTPS
// Set autheliaURL to the URL of the Authelia server, e.g. authelia.example.com
func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
client := &http.Client{}
if ar.options.AutheliaURL == "" {
ar.options.Logger.PrintAndLog("Authelia", "Authelia URL not set", nil)
w.WriteHeader(500)
w.Write([]byte("500 - Internal Server Error"))
return errors.New("authelia URL not set")
}
protocol := "http"
if ar.options.UseHTTPS {
protocol = "https"
}
autheliaBaseURL := protocol + "://" + ar.options.AutheliaURL
//Remove tailing slash if any
if autheliaBaseURL[len(autheliaBaseURL)-1] == '/' {
autheliaBaseURL = autheliaBaseURL[:len(autheliaBaseURL)-1]
}
//Make a request to Authelia to verify the request
req, err := http.NewRequest("POST", autheliaBaseURL+"/api/verify", nil)
if err != nil {
ar.options.Logger.PrintAndLog("Authelia", "Unable to create request", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
req.Header.Add("X-Original-URL", fmt.Sprintf("%s://%s", scheme, r.Host))
// Copy cookies from the incoming request
for _, cookie := range r.Cookies() {
req.AddCookie(cookie)
}
// Making the verification request
resp, err := client.Do(req)
if err != nil {
ar.options.Logger.PrintAndLog("Authelia", "Unable to verify", err)
w.WriteHeader(401)
return errors.New("unauthorized")
}
if resp.StatusCode != 200 {
redirectURL := autheliaBaseURL + "/?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String()) + "&rm=" + r.Method
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return errors.New("unauthorized")
}
return nil
}

View File

@ -1,271 +0,0 @@
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)
}
}

View File

@ -1,295 +0,0 @@
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)
}

View File

@ -1 +0,0 @@
package sso

View File

@ -1,58 +0,0 @@
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)
}

View File

@ -1,132 +0,0 @@
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)
}

View File

@ -1,158 +0,0 @@
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)
}

View File

@ -1,33 +0,0 @@
<!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>

View File

@ -1,43 +0,0 @@
<!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>

View File

@ -1,29 +0,0 @@
<!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>

View File

@ -1,309 +0,0 @@
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
}

View File

@ -1,141 +0,0 @@
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: "",
}
}

View File

@ -9,17 +9,39 @@ package database
*/
import (
"sync"
"log"
"runtime"
"imuslab.com/zoraxy/mod/database/dbinc"
)
type Database struct {
Db interface{} //This will be nil on openwrt and *bolt.DB in the rest of the systems
Tables sync.Map
ReadOnly bool
Db interface{} //This will be nil on openwrt, leveldb.DB on x64 platforms or bolt.DB on other platforms
BackendType dbinc.BackendType
Backend dbinc.Backend
}
func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
return newDatabase(dbfile, readOnlyMode)
func NewDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
if runtime.GOARCH == "riscv64" {
log.Println("RISCV hardware detected, ignoring the backend type and using FS emulated database")
}
return newDatabase(dbfile, backendType)
}
// Get the recommended backend type for the current system
func GetRecommendedBackendType() dbinc.BackendType {
//Check if the system is running on RISCV hardware
if runtime.GOARCH == "riscv64" {
//RISCV hardware, currently only support FS emulated database
return dbinc.BackendFSOnly
} else if runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "amd64") {
//Powerful hardware
return dbinc.BackendBoltDB
//return dbinc.BackendLevelDB
}
//Default to BoltDB, the safest option
return dbinc.BackendBoltDB
}
/*
@ -29,39 +51,33 @@ func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
err := sysdb.DropTable("MyTable")
*/
func (d *Database) UpdateReadWriteMode(readOnly bool) {
d.ReadOnly = readOnly
}
//Dump the whole db into a log file
func (d *Database) Dump(filename string) ([]string, error) {
return d.dump(filename)
}
//Create a new table
// Create a new table
func (d *Database) NewTable(tableName string) error {
return d.newTable(tableName)
}
//Check is table exists
// Check is table exists
func (d *Database) TableExists(tableName string) bool {
return d.tableExists(tableName)
}
//Drop the given table
// Drop the given table
func (d *Database) DropTable(tableName string) error {
return d.dropTable(tableName)
}
/*
Write to database with given tablename and key. Example Usage:
Write to database with given tablename and key. Example Usage:
type demo struct{
content string
}
thisDemo := demo{
content: "Hello World",
}
err := sysdb.Write("MyTable", "username/message",thisDemo);
err := sysdb.Write("MyTable", "username/message",thisDemo);
*/
func (d *Database) Write(tableName string, key string, value interface{}) error {
return d.write(tableName, key, value)
@ -81,14 +97,21 @@ func (d *Database) Read(tableName string, key string, assignee interface{}) erro
return d.read(tableName, key, assignee)
}
/*
Check if a key exists in the database table given tablename and key
if sysdb.KeyExists("MyTable", "username/message"){
log.Println("Key exists")
}
*/
func (d *Database) KeyExists(tableName string, key string) bool {
return d.keyExists(tableName, key)
}
/*
Delete a value from the database table given tablename and key
Delete a value from the database table given tablename and key
err := sysdb.Delete("MyTable", "username/message");
err := sysdb.Delete("MyTable", "username/message");
*/
func (d *Database) Delete(tableName string, key string) error {
return d.delete(tableName, key)
@ -115,6 +138,9 @@ func (d *Database) ListTable(tableName string) ([][][]byte, error) {
return d.listTable(tableName)
}
/*
Close the database connection
*/
func (d *Database) Close() {
d.close()
}

View File

@ -4,183 +4,67 @@
package database
import (
"encoding/json"
"errors"
"log"
"sync"
"github.com/boltdb/bolt"
"imuslab.com/zoraxy/mod/database/dbbolt"
"imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/database/dbleveldb"
)
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
db, err := bolt.Open(dbfile, 0600, nil)
if err != nil {
return nil, err
func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
if backendType == dbinc.BackendFSOnly {
return nil, errors.New("Unsupported backend type for this platform")
}
tableMap := sync.Map{}
//Build the table list from database
err = db.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(name []byte, _ *bolt.Bucket) error {
tableMap.Store(string(name), "")
return nil
})
})
if backendType == dbinc.BackendLevelDB {
db, err := dbleveldb.NewDB(dbfile)
return &Database{
Db: nil,
BackendType: backendType,
Backend: db,
}, err
}
db, err := dbbolt.NewBoltDatabase(dbfile)
return &Database{
Db: db,
Tables: tableMap,
ReadOnly: readOnlyMode,
Db: nil,
BackendType: backendType,
Backend: db,
}, err
}
//Dump the whole db into a log file
func (d *Database) dump(filename string) ([]string, error) {
results := []string{}
d.Tables.Range(func(tableName, v interface{}) bool {
entries, err := d.ListTable(tableName.(string))
if err != nil {
log.Println("Reading table " + tableName.(string) + " failed: " + err.Error())
return false
}
for _, keypairs := range entries {
results = append(results, string(keypairs[0])+":"+string(keypairs[1])+"\n")
}
return true
})
return results, nil
}
//Create a new table
func (d *Database) newTable(tableName string) error {
if d.ReadOnly == true {
return errors.New("Operation rejected in ReadOnly mode")
}
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
return nil
})
d.Tables.Store(tableName, "")
return err
return d.Backend.NewTable(tableName)
}
//Check is table exists
func (d *Database) tableExists(tableName string) bool {
if _, ok := d.Tables.Load(tableName); ok {
return true
}
return false
return d.Backend.TableExists(tableName)
}
//Drop the given table
func (d *Database) dropTable(tableName string) error {
if d.ReadOnly == true {
return errors.New("Operation rejected in ReadOnly mode")
}
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
err := tx.DeleteBucket([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
return d.Backend.DropTable(tableName)
}
//Write to table
func (d *Database) write(tableName string, key string, value interface{}) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
jsonString, err := json.Marshal(value)
if err != nil {
return err
}
err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
b := tx.Bucket([]byte(tableName))
err = b.Put([]byte(key), jsonString)
return err
})
return err
return d.Backend.Write(tableName, key, value)
}
func (d *Database) read(tableName string, key string, assignee interface{}) error {
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
json.Unmarshal(v, &assignee)
return nil
})
return err
return d.Backend.Read(tableName, key, assignee)
}
func (d *Database) keyExists(tableName string, key string) bool {
resultIsNil := false
if !d.TableExists(tableName) {
//Table not exists. Do not proceed accessing key
log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
return false
}
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
if v == nil {
resultIsNil = true
}
return nil
})
if err != nil {
return false
} else {
if resultIsNil {
return false
} else {
return true
}
}
return d.Backend.KeyExists(tableName, key)
}
func (d *Database) delete(tableName string, key string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte(tableName)).Delete([]byte(key))
return nil
})
return err
return d.Backend.Delete(tableName, key)
}
func (d *Database) listTable(tableName string) ([][][]byte, error) {
var results [][][]byte
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
results = append(results, [][]byte{k, v})
}
return nil
})
return results, err
return d.Backend.ListTable(tableName)
}
func (d *Database) close() {
d.Db.(*bolt.DB).Close()
d.Backend.Close()
}

View File

@ -10,10 +10,19 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"imuslab.com/zoraxy/mod/database/dbinc"
)
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
/*
OpenWRT or RISCV backend
For OpenWRT or RISCV platform, we will use the filesystem as the database backend
as boltdb or leveldb is not supported on these platforms, including boltDB and LevelDB
in conditional compilation will create a build error on these platforms
*/
func newDatabase(dbfile string, backendType dbinc.BackendType) (*Database, error) {
dbRootPath := filepath.ToSlash(filepath.Clean(dbfile))
dbRootPath = "fsdb/" + dbRootPath
err := os.MkdirAll(dbRootPath, 0755)
@ -21,24 +30,11 @@ func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
return nil, err
}
tableMap := sync.Map{}
//build the table list from file system
files, err := filepath.Glob(filepath.Join(dbRootPath, "/*"))
if err != nil {
return nil, err
}
for _, file := range files {
if isDirectory(file) {
tableMap.Store(filepath.Base(file), "")
}
}
log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath)
return &Database{
Db: dbRootPath,
Tables: tableMap,
ReadOnly: readOnlyMode,
Db: dbRootPath,
BackendType: dbinc.BackendFSOnly,
Backend: nil,
}, nil
}
@ -61,9 +57,7 @@ func (d *Database) dump(filename string) ([]string, error) {
}
func (d *Database) newTable(tableName string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
if !fileExists(tablePath) {
return os.MkdirAll(tablePath, 0755)
@ -85,9 +79,7 @@ func (d *Database) tableExists(tableName string) bool {
}
func (d *Database) dropTable(tableName string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
if d.tableExists(tableName) {
return os.RemoveAll(tablePath)
@ -98,9 +90,7 @@ func (d *Database) dropTable(tableName string) error {
}
func (d *Database) write(tableName string, key string, value interface{}) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
js, err := json.Marshal(value)
if err != nil {
@ -138,9 +128,7 @@ func (d *Database) keyExists(tableName string, key string) bool {
}
func (d *Database) delete(tableName string, key string) error {
if d.ReadOnly {
return errors.New("Operation rejected in ReadOnly mode")
}
if !d.keyExists(tableName, key) {
return errors.New("key not exists")
}

View File

@ -0,0 +1,141 @@
package dbbolt
import (
"encoding/json"
"errors"
"github.com/boltdb/bolt"
)
type Database struct {
Db interface{} //This is the bolt database object
}
func NewBoltDatabase(dbfile string) (*Database, error) {
db, err := bolt.Open(dbfile, 0600, nil)
if err != nil {
return nil, err
}
return &Database{
Db: db,
}, err
}
// Create a new table
func (d *Database) NewTable(tableName string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
}
// Check is table exists
func (d *Database) TableExists(tableName string) bool {
return d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
if b == nil {
return errors.New("table not exists")
}
return nil
}) == nil
}
// Drop the given table
func (d *Database) DropTable(tableName string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
err := tx.DeleteBucket([]byte(tableName))
if err != nil {
return err
}
return nil
})
return err
}
// Write to table
func (d *Database) Write(tableName string, key string, value interface{}) error {
jsonString, err := json.Marshal(value)
if err != nil {
return err
}
err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
if err != nil {
return err
}
b := tx.Bucket([]byte(tableName))
err = b.Put([]byte(key), jsonString)
return err
})
return err
}
func (d *Database) Read(tableName string, key string, assignee interface{}) error {
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
json.Unmarshal(v, &assignee)
return nil
})
return err
}
func (d *Database) KeyExists(tableName string, key string) bool {
resultIsNil := false
if !d.TableExists(tableName) {
//Table not exists. Do not proceed accessing key
//log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
return false
}
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
v := b.Get([]byte(key))
if v == nil {
resultIsNil = true
}
return nil
})
if err != nil {
return false
} else {
if resultIsNil {
return false
} else {
return true
}
}
}
func (d *Database) Delete(tableName string, key string) error {
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte(tableName)).Delete([]byte(key))
return nil
})
return err
}
func (d *Database) ListTable(tableName string) ([][][]byte, error) {
var results [][][]byte
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(tableName))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
results = append(results, [][]byte{k, v})
}
return nil
})
return results, err
}
func (d *Database) Close() {
d.Db.(*bolt.DB).Close()
}

View File

@ -0,0 +1,67 @@
package dbbolt_test
import (
"os"
"testing"
"imuslab.com/zoraxy/mod/database/dbbolt"
)
func TestNewBoltDatabase(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
if db.Db == nil {
t.Fatalf("Expected non-nil database object")
}
}
func TestNewTable(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
err = db.NewTable("testTable")
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
}
func TestTableExists(t *testing.T) {
dbfile := "test.db"
defer os.Remove(dbfile)
db, err := dbbolt.NewBoltDatabase(dbfile)
if err != nil {
t.Fatalf("Failed to create new Bolt database: %v", err)
}
defer db.Close()
tableName := "testTable"
err = db.NewTable(tableName)
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
exists := db.TableExists(tableName)
if !exists {
t.Fatalf("Expected table %s to exist", tableName)
}
nonExistentTable := "nonExistentTable"
exists = db.TableExists(nonExistentTable)
if exists {
t.Fatalf("Expected table %s to not exist", nonExistentTable)
}
}

View File

@ -0,0 +1,39 @@
package dbinc
/*
dbinc is the interface for all database backend
*/
type BackendType int
const (
BackendBoltDB BackendType = iota //Default backend
BackendFSOnly //OpenWRT or RISCV backend
BackendLevelDB //LevelDB backend
BackEndAuto = BackendBoltDB
)
type Backend interface {
NewTable(tableName string) error
TableExists(tableName string) bool
DropTable(tableName string) error
Write(tableName string, key string, value interface{}) error
Read(tableName string, key string, assignee interface{}) error
KeyExists(tableName string, key string) bool
Delete(tableName string, key string) error
ListTable(tableName string) ([][][]byte, error)
Close()
}
func (b BackendType) String() string {
switch b {
case BackendBoltDB:
return "BoltDB"
case BackendFSOnly:
return "File System Emulated Key-Value Store"
case BackendLevelDB:
return "LevelDB"
default:
return "Unknown"
}
}

View File

@ -0,0 +1,152 @@
package dbleveldb
import (
"encoding/json"
"log"
"path/filepath"
"strings"
"sync"
"time"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
"imuslab.com/zoraxy/mod/database/dbinc"
)
// Ensure the DB struct implements the Backend interface
var _ dbinc.Backend = (*DB)(nil)
type DB struct {
db *leveldb.DB
Table sync.Map //For emulating table creation
batch leveldb.Batch //Batch write
writeFlushTicker *time.Ticker //Ticker for flushing data into disk
writeFlushStop chan bool //Stop channel for write flush ticker
}
func NewDB(path string) (*DB, error) {
//If the path is not a directory (e.g. /tmp/dbfile.db), convert the filename to directory
if filepath.Ext(path) != "" {
path = strings.ReplaceAll(path, ".", "_")
}
db, err := leveldb.OpenFile(path, nil)
if err != nil {
return nil, err
}
thisDB := &DB{
db: db,
Table: sync.Map{},
batch: leveldb.Batch{},
}
//Create a ticker to flush data into disk every 1 seconds
writeFlushTicker := time.NewTicker(1 * time.Second)
writeFlushStop := make(chan bool)
go func() {
for {
select {
case <-writeFlushTicker.C:
if thisDB.batch.Len() == 0 {
//No flushing needed
continue
}
err = db.Write(&thisDB.batch, nil)
if err != nil {
log.Println("[LevelDB] Failed to flush data into disk: ", err)
}
thisDB.batch.Reset()
case <-writeFlushStop:
return
}
}
}()
thisDB.writeFlushTicker = writeFlushTicker
thisDB.writeFlushStop = writeFlushStop
return thisDB, nil
}
func (d *DB) NewTable(tableName string) error {
//Create a table entry in the sync.Map
d.Table.Store(tableName, true)
return nil
}
func (d *DB) TableExists(tableName string) bool {
_, ok := d.Table.Load(tableName)
return ok
}
func (d *DB) DropTable(tableName string) error {
d.Table.Delete(tableName)
iter := d.db.NewIterator(nil, nil)
defer iter.Release()
for iter.Next() {
key := iter.Key()
if filepath.Dir(string(key)) == tableName {
err := d.db.Delete(key, nil)
if err != nil {
return err
}
}
}
return nil
}
func (d *DB) Write(tableName string, key string, value interface{}) error {
data, err := json.Marshal(value)
if err != nil {
return err
}
d.batch.Put([]byte(filepath.ToSlash(filepath.Join(tableName, key))), data)
return nil
}
func (d *DB) Read(tableName string, key string, assignee interface{}) error {
data, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
if err != nil {
return err
}
return json.Unmarshal(data, assignee)
}
func (d *DB) KeyExists(tableName string, key string) bool {
_, err := d.db.Get([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
return err == nil
}
func (d *DB) Delete(tableName string, key string) error {
return d.db.Delete([]byte(filepath.ToSlash(filepath.Join(tableName, key))), nil)
}
func (d *DB) ListTable(tableName string) ([][][]byte, error) {
iter := d.db.NewIterator(util.BytesPrefix([]byte(tableName+"/")), nil)
defer iter.Release()
var result [][][]byte
for iter.Next() {
key := iter.Key()
//The key contains the table name as prefix. Trim it before returning
value := iter.Value()
result = append(result, [][]byte{[]byte(strings.TrimPrefix(string(key), tableName+"/")), value})
}
err := iter.Error()
if err != nil {
return nil, err
}
return result, nil
}
func (d *DB) Close() {
//Write the remaining data in batch back into disk
d.writeFlushStop <- true
d.writeFlushTicker.Stop()
d.db.Write(&d.batch, nil)
d.db.Close()
}

View File

@ -0,0 +1,141 @@
package dbleveldb_test
import (
"os"
"testing"
"imuslab.com/zoraxy/mod/database/dbleveldb"
)
func TestNewDB(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
}
func TestNewTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
err = db.NewTable("testTable")
if err != nil {
t.Fatalf("Failed to create new table: %v", err)
}
}
func TestTableExists(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
if !db.TableExists("testTable") {
t.Fatalf("Table should exist")
}
}
func TestDropTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.DropTable("testTable")
if err != nil {
t.Fatalf("Failed to drop table: %v", err)
}
if db.TableExists("testTable") {
t.Fatalf("Table should not exist")
}
}
func TestWriteAndRead(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.Write("testTable", "testKey", "testValue")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
var value string
err = db.Read("testTable", "testKey", &value)
if err != nil {
t.Fatalf("Failed to read from table: %v", err)
}
if value != "testValue" {
t.Fatalf("Expected 'testValue', got '%v'", value)
}
}
func TestListTable(t *testing.T) {
path := "/tmp/testdb"
defer os.RemoveAll(path)
db, err := dbleveldb.NewDB(path)
if err != nil {
t.Fatalf("Failed to create new DB: %v", err)
}
defer db.Close()
db.NewTable("testTable")
err = db.Write("testTable", "testKey1", "testValue1")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
err = db.Write("testTable", "testKey2", "testValue2")
if err != nil {
t.Fatalf("Failed to write to table: %v", err)
}
result, err := db.ListTable("testTable")
if err != nil {
t.Fatalf("Failed to list table: %v", err)
}
if len(result) != 2 {
t.Fatalf("Expected 2 entries, got %v", len(result))
}
expected := map[string]string{
"testTable/testKey1": "\"testValue1\"",
"testTable/testKey2": "\"testValue2\"",
}
for _, entry := range result {
key := string(entry[0])
value := string(entry[1])
if expected[key] != value {
t.Fatalf("Expected value '%v' for key '%v', got '%v'", expected[key], key, value)
}
}
}

View File

@ -83,22 +83,11 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
//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
}
}
//Validate basic auth
if sep.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return
}
respWritten := handleAuthProviderRouting(sep, w, r, h)
if respWritten {
//Request handled by subroute
return
}
//Check if any virtual directory rules matches
@ -108,7 +97,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint)
return
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root {
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyTypeRoot {
potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint
@ -153,7 +142,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
handleRootRouting
This function handle root routing situations where there are no subdomain
This function handle root routing (aka default sites) situations where there are no subdomain
, vdir or special routing rule matches the requested URI.
Once entered this routing segment, the root routing options will take over
@ -180,7 +169,7 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
//Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint)
return
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root {
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyTypeRoot {
potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint
@ -228,5 +217,27 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
} else {
w.Write(template)
}
case DefaultSite_NoResponse:
//No response. Just close the connection
h.Parent.logRequest(r, false, 444, "root-noresponse", domainOnly)
hijacker, ok := w.(http.Hijacker)
if !ok {
w.Header().Set("Connection", "close")
return
}
conn, _, err := hijacker.Hijack()
if err != nil {
w.Header().Set("Connection", "close")
return
}
conn.Close()
case DefaultSite_TeaPot:
//I'm a teapot
h.Parent.logRequest(r, false, 418, "root-teapot", domainOnly)
http.Error(w, "I'm a teapot", http.StatusTeapot)
default:
//Unknown routing option. Send empty response
h.Parent.logRequest(r, false, 544, "root-unknown", domainOnly)
http.Error(w, "544 - No Route Defined", 544)
}
}

View File

@ -1,7 +1,6 @@
package dynamicproxy
import (
"log"
"net/http"
"os"
"path/filepath"
@ -16,7 +15,7 @@ func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter,
accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID)
if err != nil {
//Unable to load access rule. Target rule not found?
log.Println("[Proxy] Unable to load access rule: " + ruleID)
h.Parent.Option.Logger.PrintAndLog("proxy-access", "Unable to load access rule: "+ruleID, err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("500 - Internal Server Error"))
return true

View File

@ -0,0 +1,108 @@
package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
/*
authProviders.go
This script handle authentication providers
*/
/*
Central Authentication Provider Router
This function will route the request to the correct authentication provider
if the return value is true, do not continue to the next handler
handleAuthProviderRouting takes in 4 parameters:
- sep: the ProxyEndpoint object
- w: the http.ResponseWriter object
- r: the http.Request object
- h: the ProxyHandler object
and return a boolean indicate if the request is written to http.ResponseWriter
- true: the request is handled, do not write to http.ResponseWriter
- false: the request is not handled (usually means auth ok), continue to the next handler
*/
func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool {
if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return true
}
} else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthelia {
err := h.handleAutheliaAuth(w, r)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
return true
}
}
//No authentication provider, do not need to handle
return false
}
/* Basic Auth */
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname())
}
return err
}
// Handle basic auth logic
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.AuthenticationProvider.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.AuthenticationProvider.BasicAuthExceptionRules {
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
}
}
}
u, p, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
//Check for the credentials to see if there is one matching
hashedPassword := auth.Hash(p)
matchingFound := false
for _, cred := range pe.AuthenticationProvider.BasicAuthCredentials {
if u == cred.Username && hashedPassword == cred.PasswordHash {
matchingFound = true
//Set the X-Remote-User header
r.Header.Set("X-Remote-User", u)
break
}
}
if !matchingFound {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
return nil
}
/* Authelia */
// Handle authelia auth routing
func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r)
}

View File

@ -1,66 +0,0 @@
package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
/*
BasicAuth.go
This file handles the basic auth on proxy endpoints
if RequireBasicAuth is set to true
*/
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
err := handleBasicAuth(w, r, pe)
if err != nil {
h.Parent.logRequest(r, false, 401, "host", r.URL.Hostname())
}
return err
}
// Handle basic auth logic
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
if len(pe.BasicAuthExceptionRules) > 0 {
//Check if the current path matches the exception rules
for _, exceptionRule := range pe.BasicAuthExceptionRules {
if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) {
//This path is excluded from basic auth
return nil
}
}
}
u, p, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
//Check for the credentials to see if there is one matching
hashedPassword := auth.Hash(p)
matchingFound := false
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
}
}
if !matchingFound {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
return nil
}

View File

@ -0,0 +1,65 @@
package dynamicproxy
import (
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
)
/*
Default Provider
This script provide the default options for all datatype
provided by dynamicproxy module
*/
// GetDefaultAuthenticationProvider return a default authentication provider
func GetDefaultAuthenticationProvider() *AuthenticationProvider {
return &AuthenticationProvider{
AuthMethod: AuthMethodNone,
BasicAuthCredentials: []*BasicAuthCredentials{},
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
BasicAuthGroupIDs: []string{},
AutheliaURL: "",
UseHTTPS: false,
}
}
// GetDefaultHeaderRewriteRules return a default header rewrite rules
func GetDefaultHeaderRewriteRules() *HeaderRewriteRules {
return &HeaderRewriteRules{
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
RequestHostOverwrite: "",
HSTSMaxAge: 0,
EnablePermissionPolicyHeader: false,
PermissionPolicy: nil,
DisableHopByHopHeaderRemoval: false,
}
}
// GetDefaultProxyEndpoint return a default proxy endpoint
func GetDefaultProxyEndpoint() ProxyEndpoint {
randomPrefix := uuid.New().String()
return ProxyEndpoint{
ProxyType: ProxyTypeHost,
RootOrMatchingDomain: randomPrefix + ".internal",
MatchingDomainAlias: []string{},
ActiveOrigins: []*loadbalance.Upstream{},
InactiveOrigins: []*loadbalance.Upstream{},
UseStickySession: false,
UseActiveLoadBalance: false,
Disabled: false,
BypassGlobalTLS: false,
VirtualDirectories: []*VirtualDirectoryEndpoint{},
HeaderRewriteRules: GetDefaultHeaderRewriteRules(),
EnableWebsocketCustomHeaders: false,
AuthenticationProvider: GetDefaultAuthenticationProvider(),
RequireRateLimit: false,
RateLimit: 0,
DisableUptimeMonitor: false,
AccessFilterUUID: "default",
DefaultSiteOption: DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
}
}

View File

@ -9,8 +9,15 @@ package domainsniff
*/
import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
)
// Check if the domain is reachable and return err if not reachable
@ -25,7 +32,132 @@ func DomainReachableWithError(domain string) error {
return nil
}
// Check if a domain have TLS but it is self-signed or expired
// Return false if sniff error
func DomainIsSelfSigned(domain string) bool {
//Extract the domain from URl in case the user input the full URL
host, port, err := net.SplitHostPort(domain)
if err != nil {
host = domain
} else {
domain = host + ":" + port
}
if !strings.Contains(domain, ":") {
domain = domain + ":443"
}
//Get the certificate
conn, err := net.Dial("tcp", domain)
if err != nil {
return false
}
defer conn.Close()
//Connect with TLS using secure verify
tlsConn := tls.Client(conn, nil)
err = tlsConn.Handshake()
if err == nil {
//This is a valid certificate
fmt.Println()
return false
}
//Connect with TLS using insecure skip verify
config := &tls.Config{
InsecureSkipVerify: true,
}
tlsConn = tls.Client(conn, config)
err = tlsConn.Handshake()
//If the handshake is successful, this is a self-signed certificate
return err == nil
}
// Check if domain reachable
func DomainReachable(domain string) bool {
return DomainReachableWithError(domain) == nil
}
// Check if domain is served by a web server using HTTPS
func DomainUsesTLS(targetURL string) bool {
//Check if the site support https
httpsUrl := fmt.Sprintf("https://%s", targetURL)
httpUrl := fmt.Sprintf("http://%s", targetURL)
client := http.Client{Timeout: 5 * time.Second}
resp, err := client.Head(httpsUrl)
if err == nil && resp.StatusCode == http.StatusOK {
return true
}
resp, err = client.Head(httpUrl)
if err == nil && resp.StatusCode == http.StatusOK {
return false
}
//If the site is not reachable, return false
return false
}
/*
WebSocket Header Sniff
*/
// Check if the requst is a special case where
// user defined header shall not be passed over
func RequireWebsocketHeaderCopy(r *http.Request) bool {
//Return false for proxmox
if IsProxmox(r) {
return false
}
//Add more edge cases here
return true
}
/*
Request Handlers
*/
//Check if site support TLS
//Pass in ?selfsignchk=true to also check for self-signed certificate
func HandleCheckSiteSupportTLS(w http.ResponseWriter, r *http.Request) {
targetURL, err := utils.PostPara(r, "url")
if err != nil {
utils.SendErrorResponse(w, "invalid url given")
return
}
//If the selfsign flag is set, also chec for self-signed certificate
_, err = utils.PostBool(r, "selfsignchk")
if err == nil {
//Return the https and selfsign status
type result struct {
Protocol string `json:"protocol"`
SelfSign bool `json:"selfsign"`
}
scanResult := result{Protocol: "http", SelfSign: false}
if DomainUsesTLS(targetURL) {
scanResult.Protocol = "https"
if DomainIsSelfSigned(targetURL) {
scanResult.SelfSign = true
}
}
js, _ := json.Marshal(scanResult)
utils.SendJSONResponse(w, string(js))
return
}
if DomainUsesTLS(targetURL) {
js, _ := json.Marshal("https")
utils.SendJSONResponse(w, string(js))
return
} else {
js, _ := json.Marshal("http")
utils.SendJSONResponse(w, string(js))
return
}
}

View File

@ -109,6 +109,8 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).DisableCompression = true
//TODO: Add user adjustable timeout option here
if dpcOptions.IgnoreTLSVerification {
//Ignore TLS certificate validation error
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true

View File

@ -144,7 +144,7 @@ func (router *Router) StartProxyService() error {
}
//Validate basic auth
if sep.RequireBasicAuth {
if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
err := handleBasicAuth(w, r, sep)
if err != nil {
return
@ -157,12 +157,18 @@ func (router *Router) StartProxyService() error {
router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err)
router.logRequest(r, false, 404, "vdir-http", r.Host)
}
endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
if sep.HeaderRewriteRules != nil {
endpointProxyRewriteRules = sep.HeaderRewriteRules
}
selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: selectedUpstream.OriginIpOrDomain,
OriginalHost: originalHostHeader,
UseTLS: selectedUpstream.RequireTLS,
HostHeaderOverwrite: sep.RequestHostOverwrite,
NoRemoveHopByHop: sep.DisableHopByHopHeaderRemoval,
HostHeaderOverwrite: endpointProxyRewriteRules.RequestHostOverwrite,
NoRemoveHopByHop: endpointProxyRewriteRules.DisableHopByHopHeaderRemoval,
PathPrefix: "",
Version: sep.parent.Option.HostVersion,
})
@ -291,7 +297,7 @@ func (router *Router) Restart() error {
return err
}
time.Sleep(300 * time.Millisecond)
time.Sleep(800 * time.Millisecond)
// Start the server
err = router.StartProxyService()
if err != nil {

View File

@ -27,7 +27,12 @@ import (
// Check if a user define header exists in this endpoint, ignore case
func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
for _, header := range ep.UserDefinedHeaders {
endpointProxyRewriteRules := GetDefaultHeaderRewriteRules()
if ep.HeaderRewriteRules != nil {
endpointProxyRewriteRules = ep.HeaderRewriteRules
}
for _, header := range endpointProxyRewriteRules.UserDefinedHeaders {
if strings.EqualFold(header.Key, key) {
return true
}
@ -38,13 +43,16 @@ func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
// Remvoe a user defined header from the list
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
newHeaderList := []*rewrite.UserDefinedHeader{}
for _, header := range ep.UserDefinedHeaders {
if ep.HeaderRewriteRules == nil {
ep.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
for _, header := range ep.HeaderRewriteRules.UserDefinedHeaders {
if !strings.EqualFold(header.Key, key) {
newHeaderList = append(newHeaderList, header)
}
}
ep.UserDefinedHeaders = newHeaderList
ep.HeaderRewriteRules.UserDefinedHeaders = newHeaderList
return nil
}
@ -55,8 +63,11 @@ func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *rewrite.UserDefined
ep.RemoveUserDefinedHeader(newHeaderRule.Key)
}
if ep.HeaderRewriteRules == nil {
ep.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
newHeaderRule.Key = cases.Title(language.Und, cases.NoLower).String(newHeaderRule.Key)
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, newHeaderRule)
ep.HeaderRewriteRules.UserDefinedHeaders = append(ep.HeaderRewriteRules.UserDefinedHeaders, newHeaderRule)
return nil
}
@ -106,7 +117,7 @@ func (ep *ProxyEndpoint) RemoveVirtualDirectoryRuleByMatchingPath(matchingPath s
return errors.New("target virtual directory routing rule not found")
}
// Delete a vdir rule by its matching path
// Add a vdir rule by its matching path
func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint) (*ProxyEndpoint, error) {
//Check for matching path duplicate
if ep.GetVirtualDirectoryRuleByMatchingPath(vdir.MatchingPath) != nil {
@ -123,9 +134,9 @@ func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint)
return nil, err
}
if ep.ProxyType == ProxyType_Root {
if ep.ProxyType == ProxyTypeRoot {
parentRouter.Root = readyRoutingRule
} else if ep.ProxyType == ProxyType_Host {
} else if ep.ProxyType == ProxyTypeHost {
ep.Remove()
parentRouter.AddProxyRouteToRuntime(readyRoutingRule)
} else {
@ -264,5 +275,6 @@ func (ep *ProxyEndpoint) Remove() error {
// use prepare -> remove -> add if you change anything in the endpoint
// that effects the proxy routing src / dest
func (ep *ProxyEndpoint) UpdateToRuntime() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
lookupHostname := strings.ToLower(ep.RootOrMatchingDomain)
ep.parent.ProxyEndpoints.Store(lookupHostname, ep)
}

View File

@ -10,6 +10,7 @@ import (
"sort"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/netutils"
@ -35,6 +36,7 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
// Get the proxy endpoint from hostname, which might includes checking of wildcard certificates
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
var targetSubdomainEndpoint *ProxyEndpoint = nil
hostname = strings.ToLower(hostname)
ep, ok := router.ProxyEndpoints.Load(hostname)
if ok {
//Exact hit
@ -142,10 +144,17 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
}
h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain)
if target.HeaderRewriteRules == nil {
target.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
SkipTLSValidation: selectedUpstream.SkipCertValidations,
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
Logger: h.Parent.Option.Logger,
SkipTLSValidation: selectedUpstream.SkipCertValidations,
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
CopyAllHeaders: target.EnableWebsocketCustomHeaders,
UserDefinedHeaders: target.HeaderRewriteRules.UserDefinedHeaders,
Logger: h.Parent.Option.Logger,
})
wspHandler.ServeHTTP(w, r)
return
@ -160,15 +169,19 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
}
//Populate the user-defined headers with the values from the request
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.UserDefinedHeaders)
headerRewriteOptions := GetDefaultHeaderRewriteRules()
if target.HeaderRewriteRules != nil {
headerRewriteOptions = target.HeaderRewriteRules
}
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, headerRewriteOptions.UserDefinedHeaders)
//Build downstream and upstream header rules
upstreamHeaders, downstreamHeaders := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
HSTSMaxAge: target.HSTSMaxAge,
HSTSMaxAge: headerRewriteOptions.HSTSMaxAge,
HSTSIncludeSubdomains: target.ContainsWildcardName(true),
EnablePermissionPolicyHeader: target.EnablePermissionPolicyHeader,
PermissionPolicy: target.PermissionPolicy,
EnablePermissionPolicyHeader: headerRewriteOptions.EnablePermissionPolicyHeader,
PermissionPolicy: headerRewriteOptions.PermissionPolicy,
})
//Handle the request reverse proxy
@ -180,8 +193,8 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
PathPrefix: "",
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
HostHeaderOverwrite: target.RequestHostOverwrite,
NoRemoveHopByHop: target.DisableHopByHopHeaderRemoval,
HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
NoRemoveHopByHop: headerRewriteOptions.DisableHopByHopHeaderRemoval,
Version: target.parent.Option.HostVersion,
})
@ -219,11 +232,18 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
if target.RequireTLS {
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
}
if target.parent.HeaderRewriteRules != nil {
target.parent.HeaderRewriteRules = GetDefaultHeaderRewriteRules()
}
h.Parent.logRequest(r, true, 101, "vdir-websocket", target.Domain)
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,
SkipTLSValidation: target.SkipCertValidations,
SkipOriginCheck: target.parent.EnableWebsocketCustomHeaders, //You should not use websocket via virtual directory. But keep this to true for compatibility
CopyAllHeaders: domainsniff.RequireWebsocketHeaderCopy(r), //Left this as default to prevent nginx user setting / as vdir
UserDefinedHeaders: target.parent.HeaderRewriteRules.UserDefinedHeaders,
Logger: h.Parent.Option.Logger,
})
wspHandler.ServeHTTP(w, r)
return
@ -238,15 +258,20 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
}
//Populate the user-defined headers with the values from the request
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, target.parent.UserDefinedHeaders)
headerRewriteOptions := GetDefaultHeaderRewriteRules()
if target.parent.HeaderRewriteRules != nil {
headerRewriteOptions = target.parent.HeaderRewriteRules
}
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(r, headerRewriteOptions.UserDefinedHeaders)
//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,
HSTSMaxAge: headerRewriteOptions.HSTSMaxAge,
HSTSIncludeSubdomains: target.parent.ContainsWildcardName(true),
EnablePermissionPolicyHeader: target.parent.EnablePermissionPolicyHeader,
PermissionPolicy: target.parent.PermissionPolicy,
EnablePermissionPolicyHeader: headerRewriteOptions.EnablePermissionPolicyHeader,
PermissionPolicy: headerRewriteOptions.PermissionPolicy,
})
//Handle the virtual directory reverse proxy request
@ -257,7 +282,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
PathPrefix: target.MatchingPath,
UpstreamHeaders: upstreamHeaders,
DownstreamHeaders: downstreamHeaders,
HostHeaderOverwrite: target.parent.RequestHostOverwrite,
HostHeaderOverwrite: headerRewriteOptions.RequestHostOverwrite,
Version: target.parent.parent.Option.HostVersion,
})

View File

@ -70,9 +70,10 @@ 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 {
lookupHostname := strings.ToLower(endpoint.RootOrMatchingDomain)
if len(endpoint.ActiveOrigins) == 0 {
//There are no active origins. No need to check for ready
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint)
router.ProxyEndpoints.Store(lookupHostname, endpoint)
return nil
}
if !router.loadBalancer.UpstreamsReady(endpoint.ActiveOrigins) {
@ -80,7 +81,7 @@ func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
}
// Push record into running subdomain endpoints
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint)
router.ProxyEndpoints.Store(lookupHostname, endpoint)
return nil
}

View File

@ -1,5 +1,12 @@
package dynamicproxy
/*
typdef.go
This script handle the type definition for dynamic proxy and endpoints
If you are looking for the default object initailization, please refer to default.go
*/
import (
_ "embed"
"net"
@ -7,7 +14,7 @@ import (
"sync"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/auth/sso"
"imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
@ -19,10 +26,12 @@ import (
"imuslab.com/zoraxy/mod/tlscert"
)
type ProxyType int
const (
ProxyType_Root = 0
ProxyType_Host = 1
ProxyType_Vdir = 2
ProxyTypeRoot ProxyType = iota //Root Proxy, everything not matching will be routed here
ProxyTypeHost //Host Proxy, match by host (domain) name
ProxyTypeVdir //Virtual Directory Proxy, match by path prefix
)
type ProxyHandler struct {
@ -31,14 +40,17 @@ type ProxyHandler struct {
/* Router Object Options */
type RouterOption struct {
HostUUID string //The UUID of Zoraxy, use for heading mod
HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above
NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint
/* Basic Settings */
HostUUID string //The UUID of Zoraxy, use for heading mod
HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above
NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint
/* Routing Service Managers */
TlsManager *tlscert.Manager //TLS manager for serving SAN certificates
RedirectRuleTable *redirection.RuleTable //Redirection rules handler and table
GeodbStore *geodb.Store //GeoIP resolver
@ -46,21 +58,25 @@ 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
/* Authentication Providers */
AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
/* Utilities */
Logger *logger.Logger //Logger for reverse proxy requets
}
/* Router Object */
type Router struct {
Option *RouterOption
ProxyEndpoints *sync.Map
Running bool
Root *ProxyEndpoint
mux http.Handler
server *http.Server
tlsListener net.Listener
ProxyEndpoints *sync.Map //Map of ProxyEndpoint objects, each ProxyEndpoint object is a routing rule that handle incoming requests
Running bool //If the router is running
Root *ProxyEndpoint //Root proxy endpoint, default site
mux http.Handler //HTTP handler
server *http.Server //HTTP server
tlsListener net.Listener //TLS listener, handle SNI routing
loadBalancer *loadbalance.RouteManager //Load balancer routing manager
routingRules []*RoutingRule
routingRules []*RoutingRule //Special routing rules, handle high priority routing like ACME request handling
tlsRedirectStop chan bool //Stop channel for tls redirection server
rateLimterStop chan bool //Stop channel for rate limiter
@ -99,9 +115,48 @@ type VirtualDirectoryEndpoint struct {
parent *ProxyEndpoint `json:"-"`
}
// Rules and settings for header rewriting
type HeaderRewriteRules struct {
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 //Do not remove hop-by-hop headers
}
/*
Authentication Provider
TODO: Move these into a dedicated module
*/
type AuthMethod int
const (
AuthMethodNone AuthMethod = iota //No authentication required
AuthMethodBasic //Basic Auth
AuthMethodAuthelia //Authelia
AuthMethodOauth2 //Oauth2
)
type AuthenticationProvider struct {
AuthMethod AuthMethod //The authentication method to use
/* Basic Auth Settings */
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint
/* Authelia Settings */
AutheliaURL string //URL of the Authelia server, leave empty to use global settings e.g. authelia.example.com
UseHTTPS bool //Whether to use HTTPS for the Authelia server
}
// A proxy endpoint record, a general interface for handling inbound routing
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
ProxyType ProxyType //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
@ -117,23 +172,19 @@ type ProxyEndpoint struct {
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
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 //Do not remove hop-by-hop headers
HeaderRewriteRules *HeaderRewriteRules
EnableWebsocketCustomHeaders bool //Enable custom headers for websocket connections as well (default only http reqiests)
//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
AuthenticationProvider *AuthenticationProvider
// Rate Limiting
RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second
//Uptime Monitor
DisableUptimeMonitor bool //Disable uptime monitor for this endpoint
//Access Control
AccessFilterUUID string //Access filter ID
@ -158,6 +209,9 @@ const (
DefaultSite_ReverseProxy = 1
DefaultSite_Redirect = 2
DefaultSite_NotFoundPage = 3
DefaultSite_NoResponse = 4
DefaultSite_TeaPot = 418 //I'm a teapot
)
/*

View File

@ -3,9 +3,14 @@ package geodb
import (
_ "embed"
"net/http"
"os"
"sync"
"time"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/utils"
)
//go:embed geoipv4.csv
@ -15,17 +20,23 @@ var geoipv4 []byte //Geodb dataset for ipv4
var geoipv6 []byte //Geodb dataset for ipv6
type Store struct {
geodb [][]string //Parsed geodb list
geodbIpv6 [][]string //Parsed geodb list for ipv6
geotrie *trie
geotrieIpv6 *trie
sysdb *database.Database
option *StoreOptions
geodb [][]string //Parsed geodb list
geodbIpv6 [][]string //Parsed geodb list for ipv6
geotrie *trie
geotrieIpv6 *trie
sysdb *database.Database
slowLookupCacheIpv4 sync.Map //Cache for slow lookup, ip -> cc
slowLookupCacheIpv6 sync.Map //Cache for slow lookup ipv6, ip -> cc
cacheClearTicker *time.Ticker //Ticker for clearing cache
cacheClearTickerStopChan chan bool //Stop channel for cache clear ticker
option *StoreOptions
}
type StoreOptions struct {
AllowSlowIpv4LookUp bool
AllowSloeIpv6Lookup bool
AllowSlowIpv4LookUp bool
AllowSlowIpv6Lookup bool
Logger *logger.Logger
SlowLookupCacheClearInterval time.Duration //Clear slow lookup cache interval
}
type CountryInfo struct {
@ -34,6 +45,23 @@ type CountryInfo struct {
}
func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
//Check if external geoDB data is available
if utils.FileExists("./conf/geodb/geoipv4.csv") {
externalV4Db, err := os.ReadFile("./conf/geodb/geoipv4.csv")
if err == nil {
option.Logger.PrintAndLog("GeoDB", "External GeoDB data found, using external IPv4 GeoIP data", nil)
geoipv4 = externalV4Db
}
}
if utils.FileExists("./conf/geodb/geoipv6.csv") {
externalV6Db, err := os.ReadFile("./conf/geodb/geoipv6.csv")
if err == nil {
option.Logger.PrintAndLog("GeoDB", "External GeoDB data found, using external IPv6 GeoIP data", nil)
geoipv6 = externalV6Db
}
}
parsedGeoData, err := parseCSV(geoipv4)
if err != nil {
return nil, err
@ -50,18 +78,44 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
}
var ipv6Trie *trie
if !option.AllowSloeIpv6Lookup {
if !option.AllowSlowIpv6Lookup {
ipv6Trie = constrctTrieTree(parsedGeoDataIpv6)
}
return &Store{
geodb: parsedGeoData,
geotrie: ipv4Trie,
geodbIpv6: parsedGeoDataIpv6,
geotrieIpv6: ipv6Trie,
sysdb: sysdb,
option: option,
}, nil
if option.SlowLookupCacheClearInterval == 0 {
option.SlowLookupCacheClearInterval = 30 * time.Minute
}
//Create a new store
thisGeoDBStore := &Store{
geodb: parsedGeoData,
geotrie: ipv4Trie,
geodbIpv6: parsedGeoDataIpv6,
geotrieIpv6: ipv6Trie,
sysdb: sysdb,
slowLookupCacheIpv4: sync.Map{},
slowLookupCacheIpv6: sync.Map{},
cacheClearTicker: time.NewTicker(option.SlowLookupCacheClearInterval),
cacheClearTickerStopChan: make(chan bool),
option: option,
}
//Start cache clear ticker
if option.AllowSlowIpv4LookUp || option.AllowSlowIpv6Lookup {
go func(store *Store) {
for {
select {
case <-store.cacheClearTickerStopChan:
return
case <-thisGeoDBStore.cacheClearTicker.C:
thisGeoDBStore.slowLookupCacheIpv4 = sync.Map{}
thisGeoDBStore.slowLookupCacheIpv6 = sync.Map{}
}
}
}(thisGeoDBStore)
}
return thisGeoDBStore, nil
}
func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
@ -73,8 +127,12 @@ func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error)
}
// Close the store
func (s *Store) Close() {
if s.option.AllowSlowIpv4LookUp || s.option.AllowSlowIpv6Lookup {
//Stop cache clear ticker
s.cacheClearTickerStopChan <- true
}
}
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {

View File

@ -4,6 +4,7 @@ import (
"testing"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
)
/*
@ -42,8 +43,10 @@ func TestTrieConstruct(t *testing.T) {
func TestResolveCountryCodeFromIP(t *testing.T) {
// Create a new store
store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{
false,
true,
true,
&logger.Logger{},
0,
})
if err != nil {
t.Errorf("error creating store: %v", err)
@ -83,4 +86,24 @@ func TestResolveCountryCodeFromIP(t *testing.T) {
if info.CountryIsoCode != expected {
t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip)
}
// Test for issue #401
// Create 100 concurrent goroutines to resolve country code for random IP addresses in the test cases above
for i := 0; i < 100; i++ {
go func() {
for _, testcase := range knownIpCountryMap {
ip := testcase[0]
expected := testcase[1]
info, err := store.ResolveCountryCodeFromIP(ip)
if err != nil {
t.Errorf("error resolving country code for IP %s: %v", ip, err)
return
}
if info.CountryIsoCode != expected {
t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip)
}
}
}()
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -56,6 +56,13 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
if isReservedIP(ipAddr) {
return ""
}
//Check if already in cache
cc := s.GetSlowSearchCachedIpv4(ipAddr)
if cc != "" {
return cc
}
for _, ipRange := range s.geodb {
startIp := ipRange[0]
endIp := ipRange[1]
@ -63,6 +70,8 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
inRange, _ := isIPv4InRange(startIp, endIp, ipAddr)
if inRange {
//Add to cache
s.slowLookupCacheIpv4.Store(ipAddr, cc)
return cc
}
}
@ -73,6 +82,13 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
if isReservedIP(ipAddr) {
return ""
}
//Check if already in cache
cc := s.GetSlowSearchCachedIpv6(ipAddr)
if cc != "" {
return cc
}
for _, ipRange := range s.geodbIpv6 {
startIp := ipRange[0]
endIp := ipRange[1]
@ -80,8 +96,28 @@ func (s *Store) slowSearchIpv6(ipAddr string) string {
inRange, _ := isIPv6InRange(startIp, endIp, ipAddr)
if inRange {
//Add to cache
s.slowLookupCacheIpv6.Store(ipAddr, cc)
return cc
}
}
return ""
}
// GetSlowSearchCachedIpv4 return the country code for the given ipv4 address, return empty string if not found
func (s *Store) GetSlowSearchCachedIpv4(ipAddr string) string {
cc, ok := s.slowLookupCacheIpv4.Load(ipAddr)
if ok {
return cc.(string)
}
return ""
}
// GetSlowSearchCachedIpv6 return the country code for the given ipv6 address, return empty string if not found
func (s *Store) GetSlowSearchCachedIpv6(ipAddr string) string {
cc, ok := s.slowLookupCacheIpv6.Load(ipAddr)
if ok {
return cc.(string)
}
return ""
}

56
src/mod/geodb/updater.go Normal file
View File

@ -0,0 +1,56 @@
package geodb
import (
"io"
"log"
"net/http"
"os"
"imuslab.com/zoraxy/mod/utils"
)
const (
ipv4UpdateSource = "https://cdn.jsdelivr.net/npm/@ip-location-db/geo-whois-asn-country/geo-whois-asn-country-ipv4.csv"
ipv6UpdateSource = "https://cdn.jsdelivr.net/npm/@ip-location-db/geo-whois-asn-country/geo-whois-asn-country-ipv6.csv"
)
// DownloadGeoDBUpdate download the latest geodb update
func DownloadGeoDBUpdate(externalGeoDBStoragePath string) {
//Create the storage path if not exist
if !utils.FileExists(externalGeoDBStoragePath) {
os.MkdirAll(externalGeoDBStoragePath, 0755)
}
//Download the update
log.Println("Downloading IPv4 database update...")
err := downloadFile(ipv4UpdateSource, externalGeoDBStoragePath+"/geoipv4.csv")
if err != nil {
log.Println(err)
return
}
log.Println("Downloading IPv6 database update...")
err = downloadFile(ipv6UpdateSource, externalGeoDBStoragePath+"/geoipv6.csv")
if err != nil {
log.Println(err)
return
}
log.Println("GeoDB update stored at: " + externalGeoDBStoragePath)
log.Println("Exiting...")
}
// Utility functions
func downloadFile(url string, savepath string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
fileContent, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return os.WriteFile(savepath, fileContent, 0644)
}

View File

@ -169,9 +169,16 @@ func (n *NetStatBuffers) HandleGetBufferedNetworkInterfaceStats(w http.ResponseW
}
func (n *NetStatBuffers) Close() {
n.StopChan <- true
time.Sleep(300 * time.Millisecond)
n.EventTicker.Stop()
//Fixed issue #394 for stopping netstat listener on platforms not supported platforms
if n.StopChan != nil {
n.StopChan <- true
time.Sleep(300 * time.Millisecond)
}
if n.EventTicker != nil {
n.EventTicker.Stop()
}
}
func (n *NetStatBuffers) HandleGetNetworkInterfaceStats(w http.ResponseWriter, r *http.Request) {
@ -270,11 +277,11 @@ func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
allIfaceRxByteFiles, err := filepath.Glob("/sys/class/net/*/statistics/rx_bytes")
if err != nil {
//Permission denied
return 0, 0, errors.New("Access denied")
return 0, 0, errors.New("access denied")
}
if len(allIfaceRxByteFiles) == 0 {
return 0, 0, errors.New("No valid iface found")
return 0, 0, errors.New("no valid iface found")
}
rxSum := int64(0)
@ -334,5 +341,5 @@ func (n *NetStatBuffers) GetNetworkInterfaceStats() (int64, int64, error) {
return 0, 0, nil //no ethernet adapters with en*/<Link#*>
}
return 0, 0, errors.New("Platform not supported")
return 0, 0, errors.New("platform not supported")
}

View File

@ -16,6 +16,15 @@ import (
func GetRequesterIP(r *http.Request) string {
ip := r.Header.Get("X-Real-Ip")
if ip == "" {
CF_Connecting_IP := r.Header.Get("CF-Connecting-IP")
Fastly_Client_IP := r.Header.Get("Fastly-Client-IP")
if CF_Connecting_IP != "" {
//Use CF Connecting IP
return CF_Connecting_IP
} else if Fastly_Client_IP != "" {
//Use Fastly Client IP
return Fastly_Client_IP
}
ip = r.Header.Get("X-Forwarded-For")
}
if ip == "" {

View File

@ -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 {
@ -168,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 {

View 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)
}
}
}

View File

@ -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
}

View File

@ -33,15 +33,15 @@ type DailySummary struct {
}
type RequestInfo struct {
IpAddr string
RequestOriginalCountryISOCode string
Succ bool
StatusCode int
ForwardType string
Referer string
UserAgent string
RequestURL string
Target string
IpAddr string //IP address of the downstream request
RequestOriginalCountryISOCode string //ISO code of the country where the request originated
Succ bool //If the request is successful and resp generated by upstream instead of Zoraxy (except static web server)
StatusCode int //HTTP status code of the request
ForwardType string //Forward type of the request, usually the proxy type (e.g. host-http, subdomain-websocket or vdir-http or any of the combination)
Referer string //Referer of the downstream request
UserAgent string //UserAgent of the downstream request
RequestURL string //Request URL
Target string //Target domain or hostname
}
type CollectorOption struct {
@ -59,7 +59,7 @@ func NewStatisticCollector(option CollectorOption) (*Collector, error) {
//Create the collector object
thisCollector := Collector{
DailySummary: newDailySummary(),
DailySummary: NewDailySummary(),
Option: &option,
}
@ -87,6 +87,11 @@ func (c *Collector) SaveSummaryOfDay() {
c.Option.Database.Write("stats", summaryKey, saveData)
}
// Get the daily summary up until now
func (c *Collector) GetCurrentDailySummary() *DailySummary {
return c.DailySummary
}
// Load the summary of a day given
func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *DailySummary {
date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
@ -99,7 +104,7 @@ func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *Daily
// Reset today summary, for debug or restoring injections
func (c *Collector) ResetSummaryOfDay() {
c.DailySummary = newDailySummary()
c.DailySummary = NewDailySummary()
}
// This function gives the current slot in the 288- 5 minutes interval of the day
@ -185,8 +190,6 @@ func (c *Collector) RecordRequest(ri RequestInfo) {
c.DailySummary.UserAgent.Store(ri.UserAgent, ua.(int)+1)
}
//ADD MORE HERE IF NEEDED
//Record request URL, if it is a page
ext := filepath.Ext(ri.RequestURL)
@ -201,6 +204,8 @@ func (c *Collector) RecordRequest(ri RequestInfo) {
c.DailySummary.RequestURL.Store(ri.RequestURL, ru.(int)+1)
}
}()
//ADD MORE HERE IF NEEDED
}
// nightly task
@ -223,7 +228,7 @@ func (c *Collector) ScheduleResetRealtimeStats() chan bool {
case <-time.After(duration):
// store daily summary to database and reset summary
c.SaveSummaryOfDay()
c.DailySummary = newDailySummary()
c.DailySummary = NewDailySummary()
case <-doneCh:
// stop the routine
return
@ -234,7 +239,7 @@ func (c *Collector) ScheduleResetRealtimeStats() chan bool {
return doneCh
}
func newDailySummary() *DailySummary {
func NewDailySummary() *DailySummary {
return &DailySummary{
TotalRequest: 0,
ErrorRequest: 0,
@ -247,3 +252,30 @@ func newDailySummary() *DailySummary {
RequestURL: &sync.Map{},
}
}
func PrintDailySummary(summary *DailySummary) {
summary.ForwardTypes.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.RequestOrigin.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.RequestClientIp.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.Referer.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.UserAgent.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
summary.RequestURL.Range(func(key, value interface{}) bool {
println(key.(string), value.(int))
return true
})
}

View File

@ -0,0 +1,215 @@
package statistic_test
import (
"net"
"os"
"testing"
"time"
"math/rand"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/statistic"
)
const test_db_path = "test_db"
func getNewDatabase() *database.Database {
db, err := database.NewDatabase(test_db_path, dbinc.BackendLevelDB)
if err != nil {
panic(err)
}
db.NewTable("stats")
return db
}
func clearDatabase(db *database.Database) {
db.Close()
os.RemoveAll(test_db_path)
}
func TestNewStatisticCollector(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, err := statistic.NewStatisticCollector(option)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if collector == nil {
t.Fatalf("Expected collector, got nil")
}
}
func TestSaveSummaryOfDay(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
collector.SaveSummaryOfDay()
// Add assertions to check if data is saved correctly
}
func TestLoadSummaryOfDay(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
year, month, day := time.Now().Date()
summary := collector.LoadSummaryOfDay(year, month, day)
if summary == nil {
t.Fatalf("Expected summary, got nil")
}
}
func TestResetSummaryOfDay(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
collector.ResetSummaryOfDay()
if collector.DailySummary.TotalRequest != 0 {
t.Fatalf("Expected TotalRequest to be 0, got %v", collector.DailySummary.TotalRequest)
}
}
func TestGetCurrentRealtimeStatIntervalId(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
intervalId := collector.GetCurrentRealtimeStatIntervalId()
if intervalId < 0 || intervalId > 287 {
t.Fatalf("Expected intervalId to be between 0 and 287, got %v", intervalId)
}
}
func TestRecordRequest(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
requestInfo := statistic.RequestInfo{
IpAddr: "127.0.0.1",
RequestOriginalCountryISOCode: "US",
Succ: true,
StatusCode: 200,
ForwardType: "type1",
Referer: "http://example.com",
UserAgent: "Mozilla/5.0",
RequestURL: "/test",
Target: "target1",
}
collector.RecordRequest(requestInfo)
time.Sleep(1 * time.Second) // Wait for the goroutine to finish
if collector.DailySummary.TotalRequest != 1 {
t.Fatalf("Expected TotalRequest to be 1, got %v", collector.DailySummary.TotalRequest)
}
}
func TestScheduleResetRealtimeStats(t *testing.T) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
stopChan := collector.ScheduleResetRealtimeStats()
if stopChan == nil {
t.Fatalf("Expected stopChan, got nil")
}
collector.Close()
}
func TestNewDailySummary(t *testing.T) {
summary := statistic.NewDailySummary()
if summary.TotalRequest != 0 {
t.Fatalf("Expected TotalRequest to be 0, got %v", summary.TotalRequest)
}
if summary.ForwardTypes == nil {
t.Fatalf("Expected ForwardTypes to be initialized, got nil")
}
if summary.RequestOrigin == nil {
t.Fatalf("Expected RequestOrigin to be initialized, got nil")
}
if summary.RequestClientIp == nil {
t.Fatalf("Expected RequestClientIp to be initialized, got nil")
}
if summary.Referer == nil {
t.Fatalf("Expected Referer to be initialized, got nil")
}
if summary.UserAgent == nil {
t.Fatalf("Expected UserAgent to be initialized, got nil")
}
if summary.RequestURL == nil {
t.Fatalf("Expected RequestURL to be initialized, got nil")
}
}
func generateTestRequestInfo(db *database.Database) statistic.RequestInfo {
//Generate a random IPv4 address
randomIpAddr := ""
for {
ip := net.IPv4(byte(rand.Intn(256)), byte(rand.Intn(256)), byte(rand.Intn(256)), byte(rand.Intn(256)))
if !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsMulticast() && !ip.IsUnspecified() {
randomIpAddr = ip.String()
break
}
}
//Resolve the country code for this IP
ipLocation := "unknown"
geoIpResolver, err := geodb.NewGeoDb(db, &geodb.StoreOptions{
AllowSlowIpv4LookUp: false,
AllowSlowIpv6Lookup: true, //Just to save some RAM
})
if err == nil {
ipInfo, _ := geoIpResolver.ResolveCountryCodeFromIP(randomIpAddr)
ipLocation = ipInfo.CountryIsoCode
}
forwardType := "host-http"
//Generate a random forward type between "subdomain-http" and "host-https"
if rand.Intn(2) == 1 {
forwardType = "subdomain-http"
}
//Generate 5 random refers URL and pick from there
referers := []string{"https://example.com", "https://example.org", "https://example.net", "https://example.io", "https://example.co"}
referer := referers[rand.Intn(5)]
return statistic.RequestInfo{
IpAddr: randomIpAddr,
RequestOriginalCountryISOCode: ipLocation,
Succ: true,
StatusCode: 200,
ForwardType: forwardType,
Referer: referer,
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
RequestURL: "/benchmark",
Target: "test.imuslab.internal",
}
}
func BenchmarkRecordRequest(b *testing.B) {
db := getNewDatabase()
defer clearDatabase(db)
option := statistic.CollectorOption{Database: db}
collector, _ := statistic.NewStatisticCollector(option)
var requestInfo statistic.RequestInfo = generateTestRequestInfo(db)
b.ResetTimer()
for i := 0; i < b.N; i++ {
collector.RecordRequest(requestInfo)
collector.SaveSummaryOfDay()
}
//Write the current in-memory summary to database file
b.StopTimer()
//Print the generated summary
//testSummary := collector.GetCurrentDailySummary()
//statistic.PrintDailySummary(testSummary)
}

View File

@ -1,6 +1,9 @@
package update
import v308 "imuslab.com/zoraxy/mod/update/v308"
import (
v308 "imuslab.com/zoraxy/mod/update/v308"
v315 "imuslab.com/zoraxy/mod/update/v315"
)
// Updater Core logic
func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
@ -10,6 +13,12 @@ func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
if err != nil {
panic(err)
}
} else if fromVersion == 314 && toVersion == 315 {
//Updating from v3.1.4 to v3.1.5
err := v315.UpdateFrom314To315()
if err != nil {
panic(err)
}
}
//ADD MORE VERSIONS HERE

View File

@ -0,0 +1,24 @@
package updateutil
import (
"io"
"os"
)
// Helper function to copy files
func CopyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destinationFile, err := os.Create(dst)
if err != nil {
return err
}
defer destinationFile.Close()
_, err = io.Copy(destinationFile, sourceFile)
return err
}

View File

@ -1,7 +1,7 @@
package v308
/*
v307 type definations
v307 type definitions
This file wrap up the self-contained data structure
for v3.0.7 structure and allow automatic updates

View File

@ -1,7 +1,7 @@
package v308
/*
v308 type definations
v308 type definition
This file wrap up the self-contained data structure
for v3.0.8 structure and allow automatic updates

View File

@ -0,0 +1,50 @@
package v315
import (
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
)
// A proxy endpoint record, a general interface for handling inbound routing
type v314ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
InactiveOrigins []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
UseStickySession bool //Use stick session for load balancing
UseActiveLoadBalance bool //Use active loadbalancing, default passive
Disabled bool //If the rule is disabled
//Inbound TLS/SSL Related
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
UserDefinedHeaders []*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 //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
RateLimit int64 // Rate limit in requests per second
//Access Control
AccessFilterUUID string //Access filter ID
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
}

View File

@ -0,0 +1,106 @@
package v315
import (
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
)
type ProxyType int
const (
ProxyTypeRoot ProxyType = iota //Root Proxy, everything not matching will be routed here
ProxyTypeHost //Host Proxy, match by host (domain) name
ProxyTypeVdir //Virtual Directory Proxy, match by path prefix
)
/* Basic Auth Related Data structure*/
// Auth credential for basic auth on certain endpoints
type BasicAuthCredentials struct {
Username string
PasswordHash string
}
// Auth credential for basic auth on certain endpoints
type BasicAuthUnhashedCredentials struct {
Username string
Password string
}
// Paths to exclude in basic auth enabled proxy handler
type BasicAuthExceptionRule struct {
PathPrefix string
}
/* Routing Rule Data Structures */
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
// program structure than directly using ProxyEndpoint
type VirtualDirectoryEndpoint struct {
MatchingPath string //Matching prefix of the request path, also act as key
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
Disabled bool //If the rule is enabled
}
// Rules and settings for header rewriting
type HeaderRewriteRules struct {
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 //Do not remove hop-by-hop headers
}
type AuthProvider int
const (
AuthProviderNone AuthProvider = iota
AuthProviderBasicAuth
AuthProviderAuthelia
AuthProviderOauth2
)
type AuthenticationProvider struct {
AuthProvider AuthProvider //The type of authentication provider
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
}
// A proxy endpoint record, a general interface for handling inbound routing
type v315ProxyEndpoint struct {
ProxyType ProxyType //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
InactiveOrigins []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
UseStickySession bool //Use stick session for load balancing
UseActiveLoadBalance bool //Use active loadbalancing, default passive
Disabled bool //If the rule is disabled
//Inbound TLS/SSL Related
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
HeaderRewriteRules *HeaderRewriteRules
//Authentication
AuthenticationProvider *AuthenticationProvider
// Rate Limiting
RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second
//Access Control
AccessFilterUUID string //Access filter ID
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
}

124
src/mod/update/v315/v315.go Normal file
View File

@ -0,0 +1,124 @@
package v315
import (
"encoding/json"
"log"
"os"
"path/filepath"
"imuslab.com/zoraxy/mod/update/updateutil"
)
func UpdateFrom314To315() error {
//Load the configs
oldConfigFiles, err := filepath.Glob("./conf/proxy/*.config")
if err != nil {
return err
}
//Backup all the files
err = os.MkdirAll("./conf/proxy-314.old/", 0775)
if err != nil {
return err
}
for _, oldConfigFile := range oldConfigFiles {
// Extract the file name from the path
fileName := filepath.Base(oldConfigFile)
// Construct the backup file path
backupFile := filepath.Join("./conf/proxy-314.old/", fileName)
// Copy the file to the backup directory
err := updateutil.CopyFile(oldConfigFile, backupFile)
if err != nil {
return err
}
}
//read the config into the old struct
for _, oldConfigFile := range oldConfigFiles {
configContent, err := os.ReadFile(oldConfigFile)
if err != nil {
log.Println("Unable to read config file "+filepath.Base(oldConfigFile), err.Error())
continue
}
thisOldConfigStruct := v314ProxyEndpoint{}
err = json.Unmarshal(configContent, &thisOldConfigStruct)
if err != nil {
log.Println("Unable to parse file "+filepath.Base(oldConfigFile), err.Error())
continue
}
//Convert the old struct to the new struct
thisNewConfigStruct := convertV314ToV315(thisOldConfigStruct)
//Write the new config to file
newConfigContent, err := json.MarshalIndent(thisNewConfigStruct, "", " ")
if err != nil {
log.Println("Unable to marshal new config "+filepath.Base(oldConfigFile), err.Error())
continue
}
err = os.WriteFile(oldConfigFile, newConfigContent, 0664)
if err != nil {
log.Println("Unable to write new config "+filepath.Base(oldConfigFile), err.Error())
continue
}
}
return nil
}
func convertV314ToV315(thisOldConfigStruct v314ProxyEndpoint) v315ProxyEndpoint {
//Move old header and auth configs into struct
newHeaderRewriteRules := HeaderRewriteRules{
UserDefinedHeaders: thisOldConfigStruct.UserDefinedHeaders,
RequestHostOverwrite: thisOldConfigStruct.RequestHostOverwrite,
HSTSMaxAge: thisOldConfigStruct.HSTSMaxAge,
EnablePermissionPolicyHeader: thisOldConfigStruct.EnablePermissionPolicyHeader,
PermissionPolicy: thisOldConfigStruct.PermissionPolicy,
DisableHopByHopHeaderRemoval: thisOldConfigStruct.DisableHopByHopHeaderRemoval,
}
newAuthenticationProvider := AuthenticationProvider{
RequireBasicAuth: thisOldConfigStruct.RequireBasicAuth,
BasicAuthCredentials: thisOldConfigStruct.BasicAuthCredentials,
BasicAuthExceptionRules: thisOldConfigStruct.BasicAuthExceptionRules,
}
//Convert proxy type int to enum
var newConfigProxyType ProxyType
if thisOldConfigStruct.ProxyType == 0 {
newConfigProxyType = ProxyTypeRoot
} else if thisOldConfigStruct.ProxyType == 1 {
newConfigProxyType = ProxyTypeHost
} else if thisOldConfigStruct.ProxyType == 2 {
newConfigProxyType = ProxyTypeVdir
}
//Update the config struct
thisNewConfigStruct := v315ProxyEndpoint{
ProxyType: newConfigProxyType,
RootOrMatchingDomain: thisOldConfigStruct.RootOrMatchingDomain,
MatchingDomainAlias: thisOldConfigStruct.MatchingDomainAlias,
ActiveOrigins: thisOldConfigStruct.ActiveOrigins,
InactiveOrigins: thisOldConfigStruct.InactiveOrigins,
UseStickySession: thisOldConfigStruct.UseStickySession,
UseActiveLoadBalance: thisOldConfigStruct.UseActiveLoadBalance,
Disabled: thisOldConfigStruct.Disabled,
BypassGlobalTLS: thisOldConfigStruct.BypassGlobalTLS,
VirtualDirectories: thisOldConfigStruct.VirtualDirectories,
RequireRateLimit: thisOldConfigStruct.RequireRateLimit,
RateLimit: thisOldConfigStruct.RateLimit,
AccessFilterUUID: thisOldConfigStruct.AccessFilterUUID,
DefaultSiteOption: thisOldConfigStruct.DefaultSiteOption,
DefaultSiteValue: thisOldConfigStruct.DefaultSiteValue,
//Append the new struct into the new config
HeaderRewriteRules: &newHeaderRewriteRules,
AuthenticationProvider: &newAuthenticationProvider,
}
return thisNewConfigStruct
}

View File

@ -83,7 +83,11 @@ func (ws *WebServer) SetEnableDirectoryListing(w http.ResponseWriter, r *http.Re
utils.SendErrorResponse(w, "invalid setting given")
return
}
err = ws.option.Sysdb.Write("webserv", "dirlist", enableList)
if err != nil {
utils.SendErrorResponse(w, "unable to save setting")
return
}
ws.option.EnableDirectoryListing = enableList
utils.SendOK(w)
}

View File

@ -13,6 +13,7 @@ import (
"strings"
"github.com/gorilla/websocket"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/info/logger"
)
@ -56,9 +57,11 @@ type WebsocketProxy struct {
// Additional options for websocket proxy runtime
type Options struct {
SkipTLSValidation bool //Skip backend TLS validation
SkipOriginCheck bool //Skip origin check
Logger *logger.Logger //Logger, can be nil
SkipTLSValidation bool //Skip backend TLS validation
SkipOriginCheck bool //Skip origin check
CopyAllHeaders bool //Copy all headers from incoming request to backend request
UserDefinedHeaders []*rewrite.UserDefinedHeader //User defined headers
Logger *logger.Logger //Logger, can be nil
}
// ProxyHandler returns a new http.Handler interface that reverse proxies the
@ -78,7 +81,14 @@ func NewProxy(target *url.URL, options Options) *WebsocketProxy {
u.RawQuery = r.URL.RawQuery
return &u
}
return &WebsocketProxy{Backend: backend, Verbal: false, Options: options}
// Create a new websocket proxy
wsprox := &WebsocketProxy{Backend: backend, Verbal: false, Options: options}
if options.CopyAllHeaders {
wsprox.Director = DefaultDirector
}
return wsprox
}
// Utilities function for log printing
@ -90,6 +100,35 @@ func (w *WebsocketProxy) Println(messsage string, err error) {
log.Println("[websocketproxy] [system:info]"+messsage, err)
}
// DefaultDirector is the default implementation of Director, which copies
// all headers from the incoming request to the outgoing request.
func DefaultDirector(r *http.Request, h http.Header) {
//Copy all header values from request to target header
for k, vv := range r.Header {
for _, v := range vv {
h.Set(k, v)
}
}
// Remove hop-by-hop headers
for _, removePendingHeader := range []string{
"Connection",
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Authorization",
"Te",
"Trailers",
"Transfer-Encoding",
"Sec-WebSocket-Extensions",
"Sec-WebSocket-Key",
"Sec-WebSocket-Protocol",
"Sec-WebSocket-Version",
"Upgrade",
} {
h.Del(removePendingHeader)
}
}
// ServeHTTP implements the http.Handler that proxies WebSocket connections.
func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if w.Backend == nil {
@ -133,6 +172,11 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if req.Host != "" {
requestHeader.Set("Host", req.Host)
}
if userAgent := req.Header.Get("User-Agent"); userAgent != "" {
requestHeader.Set("User-Agent", userAgent)
} else {
requestHeader.Set("User-Agent", "zoraxy-wsproxy/1.1")
}
// Pass X-Forwarded-For headers too, code below is a part of
// httputil.ReverseProxy. See http://en.wikipedia.org/wiki/X-Forwarded-For
@ -156,10 +200,29 @@ func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
requestHeader.Set("X-Forwarded-Proto", "https")
}
// Enable the director to copy any additional headers it desires for
// forwarding to the remote server.
if w.Director != nil {
w.Director(req, requestHeader)
// Replace header variables and copy user-defined headers
if w.Options.CopyAllHeaders {
// Rewrite the user defined headers
// This is reported to be not compatible with Proxmox and Home Assistant
// but required by some other projects like MeshCentral
// we will make this optional
rewrittenUserDefinedHeaders := rewrite.PopulateRequestHeaderVariables(req, w.Options.UserDefinedHeaders)
upstreamHeaders, _ := rewrite.SplitUpDownStreamHeaders(&rewrite.HeaderRewriteOptions{
UserDefinedHeaders: rewrittenUserDefinedHeaders,
})
for _, headerValuePair := range upstreamHeaders {
//Do not copy Upgrade and Connection headers, it will be handled by the upgrader
if strings.EqualFold(headerValuePair[0], "Upgrade") || strings.EqualFold(headerValuePair[0], "Connection") {
continue
}
requestHeader.Set(headerValuePair[0], headerValuePair[1])
}
// Enable the director to copy any additional headers it desires for
// forwarding to the remote server.
if w.Director != nil {
w.Director(req, requestHeader)
}
}
// Connect to the backend URL, also pass the headers we get from the requst

View File

@ -27,18 +27,18 @@ func ReverseProxtInit() {
/*
Load Reverse Proxy Global Settings
*/
inboundPort := 80
inboundPort := 443
if sysdb.KeyExists("settings", "inbound") {
sysdb.Read("settings", "inbound", &inboundPort)
SystemWideLogger.Println("Serving inbound port ", inboundPort)
} else {
SystemWideLogger.Println("Inbound port not set. Using default (80)")
SystemWideLogger.Println("Inbound port not set. Using default (443)")
}
useTls := false
useTls := true
sysdb.Read("settings", "usetls", &useTls)
if useTls {
SystemWideLogger.Println("TLS mode enabled. Serving proxxy request with TLS")
SystemWideLogger.Println("TLS mode enabled. Serving proxy request with TLS")
} else {
SystemWideLogger.Println("TLS mode disabled. Serving proxy request with plain http")
}
@ -59,7 +59,7 @@ func ReverseProxtInit() {
SystemWideLogger.Println("Development mode disabled. Proxying with default Cache Control policy")
}
listenOnPort80 := false
listenOnPort80 := true
sysdb.Read("settings", "listenP80", &listenOnPort80)
if listenOnPort80 {
SystemWideLogger.Println("Port 80 listener enabled")
@ -67,7 +67,7 @@ func ReverseProxtInit() {
SystemWideLogger.Println("Port 80 listener disabled")
}
forceHttpsRedirect := false
forceHttpsRedirect := true
sysdb.Read("settings", "redirect", &forceHttpsRedirect)
if forceHttpsRedirect {
SystemWideLogger.Println("Force HTTPS mode enabled")
@ -85,7 +85,7 @@ func ReverseProxtInit() {
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
HostUUID: nodeUUID,
HostVersion: version,
HostVersion: SYSTEM_VERSION,
Port: inboundPort,
UseTls: useTls,
ForceTLSLatest: forceLatestTLSVersion,
@ -96,10 +96,10 @@ func ReverseProxtInit() {
RedirectRuleTable: redirectTable,
GeodbStore: geodbStore,
StatisticCollector: statisticCollector,
WebDirectory: *staticWebServerRoot,
WebDirectory: *path_webserver,
AccessController: accessController,
AutheliaRouter: autheliaRouter,
LoadBalancer: loadBalancer,
SSOHandler: ssoHandler,
Logger: SystemWideLogger,
})
if err != nil {
@ -309,10 +309,21 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
}
}
//Generate a default authenticaion provider
authMethod := dynamicproxy.AuthMethodNone
if requireBasicAuth {
authMethod = dynamicproxy.AuthMethodBasic
}
thisAuthenticationProvider := dynamicproxy.AuthenticationProvider{
AuthMethod: authMethod,
BasicAuthCredentials: basicAuthCredentials,
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
//Generate a proxy endpoint object
thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
//I/O
ProxyType: dynamicproxy.ProxyType_Host,
ProxyType: dynamicproxy.ProxyTypeHost,
RootOrMatchingDomain: rootOrMatchingDomain,
MatchingDomainAlias: aliasHostnames,
ActiveOrigins: []*loadbalance.Upstream{
@ -333,13 +344,16 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
//VDir
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
//Custom headers
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
//Auth
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
DefaultSiteOption: 0,
DefaultSiteValue: "",
AuthenticationProvider: &thisAuthenticationProvider,
//Header Rewrite
HeaderRewriteRules: dynamicproxy.GetDefaultHeaderRewriteRules(),
//Default Site
DefaultSiteOption: 0,
DefaultSiteValue: "",
// Rate Limit
RequireRateLimit: requireRateLimit,
RateLimit: int64(proxyRateLimit),
@ -379,7 +393,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
//Write the root options to file
rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyType_Root,
ProxyType: dynamicproxy.ProxyTypeRoot,
RootOrMatchingDomain: "/",
ActiveOrigins: []*loadbalance.Upstream{
{
@ -453,13 +467,23 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
}
bypassGlobalTLS := (bpgtls == "true")
// Basic Auth
rba, _ := utils.PostPara(r, "bauth")
if rba == "" {
rba = "false"
//Disable uptime monitor
disbleUtm, err := utils.PostBool(r, "dutm")
if err != nil {
disbleUtm = false
}
requireBasicAuth := (rba == "true")
// Auth Provider
authProviderTypeStr, _ := utils.PostPara(r, "authprovider")
if authProviderTypeStr == "" {
authProviderTypeStr = "0"
}
authProviderType, err := strconv.Atoi(authProviderTypeStr)
if err != nil {
utils.SendErrorResponse(w, "Invalid auth provider type")
return
}
// Rate Limiting?
rl, _ := utils.PostPara(r, "rate")
@ -494,10 +518,27 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
//Generate a new proxyEndpoint from the new config
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
newProxyEndpoint.RequireBasicAuth = requireBasicAuth
if newProxyEndpoint.AuthenticationProvider == nil {
newProxyEndpoint.AuthenticationProvider = &dynamicproxy.AuthenticationProvider{
AuthMethod: dynamicproxy.AuthMethodNone,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
}
if authProviderType == 1 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic
} else if authProviderType == 2 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia
} else if authProviderType == 3 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2
} else {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone
}
newProxyEndpoint.RequireRateLimit = requireRateLimit
newProxyEndpoint.RateLimit = proxyRateLimit
newProxyEndpoint.UseStickySession = useStickySession
newProxyEndpoint.DisableUptimeMonitor = disbleUtm
//Prepare to replace the current routing rule
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
@ -624,7 +665,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
}
usernames := []string{}
for _, cred := range targetProxy.BasicAuthCredentials {
for _, cred := range targetProxy.AuthenticationProvider.BasicAuthCredentials {
usernames = append(usernames, cred.Username)
}
@ -668,7 +709,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
if credential.Password == "" {
//Check if exists in the old credential files
keepUnchange := false
for _, oldCredEntry := range targetProxy.BasicAuthCredentials {
for _, oldCredEntry := range targetProxy.AuthenticationProvider.BasicAuthCredentials {
if oldCredEntry.Username == credential.Username {
//Exists! Reuse the old hash
mergedCredentials = append(mergedCredentials, &dynamicproxy.BasicAuthCredentials{
@ -693,7 +734,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
}
}
targetProxy.BasicAuthCredentials = mergedCredentials
targetProxy.AuthenticationProvider.BasicAuthCredentials = mergedCredentials
//Save it to file
SaveReverseProxyConfig(targetProxy)
@ -727,7 +768,7 @@ func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
}
//List all the exception paths for this proxy
results := targetProxy.BasicAuthExceptionRules
results := targetProxy.AuthenticationProvider.BasicAuthExceptionRules
if results == nil {
//It is a config from a really old version of zoraxy. Overwrite it with empty array
results = []*dynamicproxy.BasicAuthExceptionRule{}
@ -764,7 +805,7 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
//Add a new exception rule if it is not already exists
alreadyExists := false
for _, thisExceptionRule := range targetProxy.BasicAuthExceptionRules {
for _, thisExceptionRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules {
if thisExceptionRule.PathPrefix == matchingPrefix {
alreadyExists = true
break
@ -774,7 +815,7 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, "This matching path already exists")
return
}
targetProxy.BasicAuthExceptionRules = append(targetProxy.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{
targetProxy.AuthenticationProvider.BasicAuthExceptionRules = append(targetProxy.AuthenticationProvider.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{
PathPrefix: strings.TrimSpace(matchingPrefix),
})
@ -808,7 +849,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
newExceptionRuleList := []*dynamicproxy.BasicAuthExceptionRule{}
matchingExists := false
for _, thisExceptionalRule := range targetProxy.BasicAuthExceptionRules {
for _, thisExceptionalRule := range targetProxy.AuthenticationProvider.BasicAuthExceptionRules {
if thisExceptionalRule.PathPrefix != matchingPrefix {
newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule)
} else {
@ -821,7 +862,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
return
}
targetProxy.BasicAuthExceptionRules = newExceptionRuleList
targetProxy.AuthenticationProvider.BasicAuthExceptionRules = newExceptionRuleList
// Save configs to runtime and file
targetProxy.UpdateToRuntime()
@ -885,6 +926,7 @@ func ReverseProxyListDetail(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, "epname not defined")
return
}
epname = strings.ToLower(strings.TrimSpace(epname))
endpointRaw, ok := dynamicProxyRouter.ProxyEndpoints.Load(epname)
if !ok {
utils.SendErrorResponse(w, "proxy rule not found")
@ -914,13 +956,13 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
thisEndpoint := dynamicproxy.CopyEndpoint(value.(*dynamicproxy.ProxyEndpoint))
//Clear the auth passwords before showing to front-end
cleanedCredentials := []*dynamicproxy.BasicAuthCredentials{}
for _, user := range thisEndpoint.BasicAuthCredentials {
for _, user := range thisEndpoint.AuthenticationProvider.BasicAuthCredentials {
cleanedCredentials = append(cleanedCredentials, &dynamicproxy.BasicAuthCredentials{
Username: user.Username,
PasswordHash: "",
})
}
thisEndpoint.BasicAuthCredentials = cleanedCredentials
thisEndpoint.AuthenticationProvider.BasicAuthCredentials = cleanedCredentials
results = append(results, thisEndpoint)
return true
})
@ -1085,6 +1127,7 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
if dynamicProxyRouter.Running {
dynamicProxyRouter.StopProxyService()
dynamicProxyRouter.Option.Port = newIncomingPortInt
time.Sleep(1 * time.Second) //Fixed start fail issue
dynamicProxyRouter.StartProxyService()
} else {
//Only change setting but not starting the proxy service
@ -1126,7 +1169,7 @@ func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
}
//List all custom headers
customHeaderList := targetProxyEndpoint.UserDefinedHeaders
customHeaderList := targetProxyEndpoint.HeaderRewriteRules.UserDefinedHeaders
if customHeaderList == nil {
customHeaderList = []*rewrite.UserDefinedHeader{}
}
@ -1173,7 +1216,7 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
return
}
//Create a Custom Header Defination type
//Create a Custom Header Definition type
var rewriteDirection rewrite.HeaderDirection
if direction == "toOrigin" {
rewriteDirection = rewrite.HeaderDirection_ZoraxyToUpstream
@ -1268,7 +1311,7 @@ func HandleHostOverwrite(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Get the current host header
js, _ := json.Marshal(targetProxyEndpoint.RequestHostOverwrite)
js, _ := json.Marshal(targetProxyEndpoint.HeaderRewriteRules.RequestHostOverwrite)
utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost {
//Set the new host header
@ -1277,7 +1320,7 @@ func HandleHostOverwrite(w http.ResponseWriter, r *http.Request) {
//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
newProxyEndpoint.HeaderRewriteRules.RequestHostOverwrite = newHostname
//Save proxy endpoint
err = SaveReverseProxyConfig(newProxyEndpoint)
if err != nil {
@ -1340,7 +1383,7 @@ func HandleHopByHop(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Get the current hop by hop header state
js, _ := json.Marshal(!targetProxyEndpoint.DisableHopByHopHeaderRemoval)
js, _ := json.Marshal(!targetProxyEndpoint.HeaderRewriteRules.DisableHopByHopHeaderRemoval)
utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost {
//Set the hop by hop header state
@ -1350,7 +1393,7 @@ func HandleHopByHop(w http.ResponseWriter, r *http.Request) {
//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
newProxyEndpoint.HeaderRewriteRules.DisableHopByHopHeaderRemoval = !enableHopByHopRemover
//Save proxy endpoint
err = SaveReverseProxyConfig(newProxyEndpoint)
@ -1413,7 +1456,7 @@ func HandleHSTSState(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return current HSTS enable state
hstsAge := targetProxyEndpoint.HSTSMaxAge
hstsAge := targetProxyEndpoint.HeaderRewriteRules.HSTSMaxAge
js, _ := json.Marshal(hstsAge)
utils.SendJSONResponse(w, string(js))
return
@ -1425,8 +1468,12 @@ func HandleHSTSState(w http.ResponseWriter, r *http.Request) {
}
if newMaxAge == 0 || newMaxAge >= 31536000 {
targetProxyEndpoint.HSTSMaxAge = int64(newMaxAge)
SaveReverseProxyConfig(targetProxyEndpoint)
targetProxyEndpoint.HeaderRewriteRules.HSTSMaxAge = int64(newMaxAge)
err = SaveReverseProxyConfig(targetProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, "save HSTS state failed: "+err.Error())
return
}
targetProxyEndpoint.UpdateToRuntime()
} else {
utils.SendErrorResponse(w, "invalid max age given")
@ -1463,11 +1510,11 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
}
currentPolicy := permissionpolicy.GetDefaultPermissionPolicy()
if targetProxyEndpoint.PermissionPolicy != nil {
currentPolicy = targetProxyEndpoint.PermissionPolicy
if targetProxyEndpoint.HeaderRewriteRules.PermissionPolicy != nil {
currentPolicy = targetProxyEndpoint.HeaderRewriteRules.PermissionPolicy
}
result := CurrentPolicyState{
PPEnabled: targetProxyEndpoint.EnablePermissionPolicyHeader,
PPEnabled: targetProxyEndpoint.HeaderRewriteRules.EnablePermissionPolicyHeader,
CurrentPolicy: currentPolicy,
}
@ -1482,7 +1529,7 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
return
}
targetProxyEndpoint.EnablePermissionPolicyHeader = enableState
targetProxyEndpoint.HeaderRewriteRules.EnablePermissionPolicyHeader = enableState
SaveReverseProxyConfig(targetProxyEndpoint)
targetProxyEndpoint.UpdateToRuntime()
utils.SendOK(w)
@ -1504,7 +1551,7 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
}
//Save it to file
targetProxyEndpoint.PermissionPolicy = newPermissionPolicy
targetProxyEndpoint.HeaderRewriteRules.PermissionPolicy = newPermissionPolicy
SaveReverseProxyConfig(targetProxyEndpoint)
targetProxyEndpoint.UpdateToRuntime()
utils.SendOK(w)
@ -1513,3 +1560,39 @@ func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
func HandleWsHeaderBehavior(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 {
js, _ := json.Marshal(targetProxyEndpoint.EnableWebsocketCustomHeaders)
utils.SendJSONResponse(w, string(js))
} else if r.Method == http.MethodPost {
enableWsHeader, err := utils.PostBool(r, "enable")
if err != nil {
utils.SendErrorResponse(w, "invalid enable state given")
return
}
targetProxyEndpoint.EnableWebsocketCustomHeaders = enableWsHeader
SaveReverseProxyConfig(targetProxyEndpoint)
targetProxyEndpoint.UpdateToRuntime()
utils.SendOK(w)
} else {
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
}
}

View File

@ -27,7 +27,7 @@ func FSHandler(handler http.Handler) http.Handler {
Development Mode Override
=> Web root is located in /
*/
if development && strings.HasPrefix(r.URL.Path, "/web/") {
if DEVELOPMENT_BUILD && strings.HasPrefix(r.URL.Path, "/web/") {
u, _ := url.Parse(strings.TrimPrefix(r.URL.Path, "/web"))
r.URL = u
}
@ -36,7 +36,7 @@ func FSHandler(handler http.Handler) http.Handler {
Production Mode Override
=> Web root is located in /web
*/
if !development && r.URL.Path == "/" {
if !DEVELOPMENT_BUILD && r.URL.Path == "/" {
//Redirect to web UI
http.Redirect(w, r, "/web/", http.StatusTemporaryRedirect)
return
@ -93,7 +93,7 @@ func FSHandler(handler http.Handler) http.Handler {
// Production path fix wrapper. Fix the path on production or development environment
func ppf(relativeFilepath string) string {
if !development {
if !DEVELOPMENT_BUILD {
return strings.ReplaceAll(filepath.Join("/web/", relativeFilepath), "\\", "/")
}
return relativeFilepath
@ -111,7 +111,7 @@ func handleInjectHTML(w http.ResponseWriter, r *http.Request, relativeFilepath s
if len(relativeFilepath) > 0 && relativeFilepath[len(relativeFilepath)-1:] == "/" {
relativeFilepath = relativeFilepath + "index.html"
}
if development {
if DEVELOPMENT_BUILD {
//Load from disk
targetFilePath := strings.ReplaceAll(filepath.Join("web/", relativeFilepath), "\\", "/")
content, err = os.ReadFile(targetFilePath)

View File

@ -12,7 +12,9 @@ import (
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/dockerux"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
@ -52,19 +54,26 @@ var (
func startupSequence() {
//Start a system wide logger and log viewer
l, err := logger.NewLogger("zr", "./log")
l, err := logger.NewLogger(LOG_PREFIX, *path_logFile)
if err == nil {
SystemWideLogger = l
} else {
panic(err)
}
LogViewer = logviewer.NewLogViewer(&logviewer.ViewerOption{
RootFolder: "./log",
Extension: ".log",
RootFolder: *path_logFile,
Extension: LOG_EXTENSION,
})
//Create database
db, err := database.NewDatabase("sys.db", false)
backendType := database.GetRecommendedBackendType()
if *databaseBackend == "leveldb" {
backendType = dbinc.BackendLevelDB
} else if *databaseBackend == "boltdb" {
backendType = dbinc.BackendBoltDB
}
l.PrintAndLog("database", "Using "+backendType.String()+" as the database backend", nil)
db, err := database.NewDatabase("./sys.db", backendType)
if err != nil {
log.Fatal(err)
}
@ -73,21 +82,21 @@ func startupSequence() {
sysdb.NewTable("settings")
//Create tmp folder and conf folder
os.MkdirAll("./tmp", 0775)
os.MkdirAll("./conf/proxy/", 0775)
os.MkdirAll(TMP_FOLDER, 0775)
os.MkdirAll(CONF_HTTP_PROXY, 0775)
//Create an auth agent
sessionKey, err := auth.GetSessionKey(sysdb, SystemWideLogger)
if err != nil {
log.Fatal(err)
}
authAgent = auth.NewAuthenticationAgent(name, []byte(sessionKey), sysdb, true, SystemWideLogger, func(w http.ResponseWriter, r *http.Request) {
authAgent = auth.NewAuthenticationAgent(SYSTEM_NAME, []byte(sessionKey), sysdb, true, SystemWideLogger, func(w http.ResponseWriter, r *http.Request) {
//Not logged in. Redirecting to login page
http.Redirect(w, r, ppf("/login.html"), http.StatusTemporaryRedirect)
})
//Create a TLS certificate manager
tlsCertManager, err = tlscert.NewManager("./conf/certs", development, SystemWideLogger)
tlsCertManager, err = tlscert.NewManager(CONF_CERT_STORE, DEVELOPMENT_BUILD, SystemWideLogger)
if err != nil {
panic(err)
}
@ -96,15 +105,17 @@ func startupSequence() {
db.NewTable("redirect")
redirectAllowRegexp := false
db.Read("redirect", "regex", &redirectAllowRegexp)
redirectTable, err = redirection.NewRuleTable("./conf/redirect", redirectAllowRegexp, SystemWideLogger)
redirectTable, err = redirection.NewRuleTable(CONF_REDIRECTION, redirectAllowRegexp, SystemWideLogger)
if err != nil {
panic(err)
}
//Create a geodb store
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup,
AllowSloeIpv6Lookup: !*enableHighSpeedGeoIPLookup,
AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup,
AllowSlowIpv6Lookup: !*enableHighSpeedGeoIPLookup,
Logger: SystemWideLogger,
SlowLookupCacheClearInterval: GEODB_CACHE_CLEAR_INTERVAL * time.Minute,
})
if err != nil {
panic(err)
@ -121,27 +132,19 @@ func startupSequence() {
accessController, err = access.NewAccessController(&access.Options{
Database: sysdb,
GeoDB: geodbStore,
ConfigFolder: "./conf/access",
ConfigFolder: CONF_ACCESS_RULE,
})
if err != nil {
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 authentication providers
autheliaRouter = authelia.NewAutheliaRouter(&authelia.AutheliaRouterOptions{
UseHTTPS: false, // Automatic populate in router initiation
AutheliaURL: "", // Automatic populate in router initiation
Logger: SystemWideLogger,
Database: sysdb,
})
//Create a statistic collector
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
@ -154,8 +157,8 @@ func startupSequence() {
//Start the static web server
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
Sysdb: sysdb,
Port: "5487", //Default Port
WebRoot: *staticWebServerRoot,
Port: strconv.Itoa(WEBSERV_DEFAULT_PORT), //Default Port
WebRoot: *path_webserver,
EnableDirectoryListing: true,
EnableWebDirManager: *allowWebFileManager,
Logger: SystemWideLogger,
@ -179,7 +182,7 @@ func startupSequence() {
pathRuleHandler = pathrule.NewPathRuleHandler(&pathrule.Options{
Enabled: false,
ConfigFolder: "./conf/rules/pathrules",
ConfigFolder: CONF_PATH_RULE,
})
/*
@ -197,7 +200,7 @@ func startupSequence() {
hostName := *mdnsName
if hostName == "" {
hostName = "zoraxy_" + nodeUUID
hostName = MDNS_HOSTNAME_PREFIX + nodeUUID
} else {
//Trim off the suffix
hostName = strings.TrimSuffix(hostName, ".local")
@ -206,24 +209,24 @@ func startupSequence() {
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
HostName: hostName,
Port: portInt,
Domain: "zoraxy.aroz.org",
Model: "Network Gateway",
Domain: MDNS_IDENTIFY_DOMAIN,
Model: MDNS_IDENTIFY_DEVICE_TYPE,
UUID: nodeUUID,
Vendor: "imuslab.com",
BuildVersion: version,
Vendor: MDNS_IDENTIFY_VENDOR,
BuildVersion: SYSTEM_VERSION,
}, "")
if err != nil {
SystemWideLogger.Println("Unable to startup mDNS service. Disabling mDNS services")
} else {
//Start initial scanning
go func() {
hosts := mdnsScanner.Scan(30, "")
hosts := mdnsScanner.Scan(MDNS_SCAN_TIMEOUT, "")
previousmdnsScanResults = hosts
SystemWideLogger.Println("mDNS Startup scan completed")
}()
//Create a ticker to update mDNS results every 5 minutes
ticker := time.NewTicker(15 * time.Minute)
ticker := time.NewTicker(MDNS_SCAN_UPDATE_INTERVAL * time.Minute)
stopChan := make(chan bool)
go func() {
for {
@ -231,7 +234,7 @@ func startupSequence() {
case <-stopChan:
ticker.Stop()
case <-ticker.C:
hosts := mdnsScanner.Scan(30, "")
hosts := mdnsScanner.Scan(MDNS_SCAN_TIMEOUT, "")
previousmdnsScanResults = hosts
SystemWideLogger.Println("mDNS scan result updated")
}
@ -265,7 +268,7 @@ func startupSequence() {
//Create TCP Proxy Manager
streamProxyManager, err = streamproxy.NewStreamProxy(&streamproxy.Options{
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
ConfigStore: "./conf/streamproxy",
ConfigStore: CONF_STREAM_PROXY,
Logger: SystemWideLogger,
})
if err != nil {
@ -303,8 +306,8 @@ func startupSequence() {
sysdb.NewTable("acmepref")
acmeHandler = initACME()
acmeAutoRenewer, err = acme.NewAutoRenewer(
"./conf/acme_conf.json",
"./conf/certs/",
ACME_AUTORENEW_CONFIG_PATH,
CONF_CERT_STORE,
int64(*acmeAutoRenewInterval),
*acmeCertAutoRenewDays,
acmeHandler,
@ -322,6 +325,7 @@ func startupSequence() {
}
/* Finalize Startup Sequence */
// This sequence start after everything is initialized
func finalSequence() {
//Start ACME renew agent
@ -330,3 +334,45 @@ func finalSequence() {
//Inject routing rules
registerBuildInRoutingRules()
}
/* Shutdown Sequence */
func ShutdownSeq() {
SystemWideLogger.Println("Shutting down " + SYSTEM_NAME)
SystemWideLogger.Println("Closing Netstats Listener")
if netstatBuffers != nil {
netstatBuffers.Close()
}
SystemWideLogger.Println("Closing Statistic Collector")
if statisticCollector != nil {
statisticCollector.Close()
}
if mdnsTickerStop != nil {
SystemWideLogger.Println("Stopping mDNS Discoverer (might take a few minutes)")
// Stop the mdns service
mdnsTickerStop <- true
}
if mdnsScanner != nil {
mdnsScanner.Close()
}
SystemWideLogger.Println("Shutting down load balancer")
if loadBalancer != nil {
loadBalancer.Close()
}
SystemWideLogger.Println("Closing Certificates Auto Renewer")
if acmeAutoRenewer != nil {
acmeAutoRenewer.Close()
}
//Remove the tmp folder
SystemWideLogger.Println("Cleaning up tmp files")
os.RemoveAll("./tmp")
//Close database
SystemWideLogger.Println("Stopping system database")
sysdb.Close()
//Close logger
SystemWideLogger.Println("Closing system wide logger")
SystemWideLogger.Close()
}

View File

@ -841,6 +841,25 @@
function initBannedCountryList(){
$.get("/api/blacklist/list?type=country&id=" + currentEditingAccessRule, function(data) {
let bannedListHtml = '';
//Check if the country code list contains all eu countries. If yes, replace it with "EU"
let allEu = true;
let euCountries = getEUCCs();
for (var i = 0; i < euCountries.length; i++){
if (!data.includes(euCountries[i])){
allEu = false;
break;
}
}
if (allEu){
//Remove EU countries from the list and replace it with EU
data = data.filter(function(value, index, arr){
return !euCountries.includes(value);
});
data.push("eu");
}
data.forEach((countryCode) => {
bannedListHtml += `
<tr>
@ -919,18 +938,48 @@
//Whitelist country table
function initWhitelistCountryList(){
$.get("/api/whitelist/list?type=country&id=" + currentEditingAccessRule, function(data) {
let bannedListHtml = '';
let whiteListHTML = '';
//Check if the country code list contains all eu countries. If yes, replace it with "EU"
let allEu = true;
let euCountries = getEUCCs();
let countryCodesIndata = data.map(function(item){
//data[n].CC is the country code
return item.CC;
});
for (var i = 0; i < euCountries.length; i++){
if (!countryCodesIndata.includes(euCountries[i])){
allEu = false;
break;
}
}
if (allEu){
//Remove EU countries from the list and replace it with EU
data = data.filter(function(value, index, arr){
return !euCountries.includes(value.CC);
});
data.push({
CC: "eu"
});
}
data.forEach((countryWhitelistEntry) => {
let countryCode = countryWhitelistEntry.CC;
bannedListHtml += `
whiteListHTML += `
<tr>
<td><i class="${countryCode} flag"></i> ${getCountryName(countryCode)} (${countryCode.toUpperCase()})</td>
<td><button class="ui red basic mini icon button" onclick="removeFromWhiteList('${countryCode}')"><i class="trash icon"></i></button></td>
</tr>
`;
});
$('#whitelistCountryList').html(bannedListHtml);
filterCountries(data, "#countrySelectorWhitelist .menu .item");
$('#whitelistCountryList').html(whiteListHTML);
//Map the data.CC to the country code
let countryCodes = data.map(function(item){
return item.CC;
});
filterCountries(countryCodes, "#countrySelectorWhitelist .menu .item");
if (data.length === 0) {
$('#whitelistCountryList').append(`
<tr>
@ -1016,6 +1065,10 @@
});
}
function getEUCCs(){
return ["at","be","bg","cy","cz","de","dk","ee","es","fi","fr","gr","hr","hu","ie","it","lt","lu","lv","mt","nl","pl","pt","se","si","sk"];
}
function addCountryToBlacklist() {
var countryCode = $("#countrySelector").dropdown("get value").toLowerCase();
let ccs = [countryCode];
@ -1025,48 +1078,50 @@
ccs = countryCode.split(",");
}
let counter = 0;
for(var i = 0; i < ccs.length; i++){
let thisCountryCode = ccs[i];
$.cjax({
type: "POST",
url: "/api/blacklist/country/add",
method: "POST",
data: { cc: thisCountryCode, id: currentEditingAccessRule},
success: function(response) {
if (response.error != undefined){
msgbox(response.error, false);
}
if (counter == (ccs.length - 1)){
//Last item
setTimeout(function(){
initBannedCountryList();
if (ccs.length == 1){
//Single country
msgbox(`Added ${getCountryName(ccs[0])} to blacklist`);
}else{
msgbox(ccs.length + " countries added to blacklist");
}
}, (ccs.length==1)?0:100);
}
counter++;
},
error: function(xhr, status, error) {
// handle error response
}
//If the ccs includes "eu", remove the "eu" and add all eu country code to the list
if (ccs.includes("eu")){
ccs = ccs.concat(getEUCCs());
ccs = ccs.filter(function(item){
return item != "eu";
});
}
let counter = ccs.length;
$.cjax({
type: "POST",
url: "/api/blacklist/country/add",
method: "POST",
data: { cc: ccs.join(","), id: currentEditingAccessRule},
success: function(response) {
if (response.error != undefined){
msgbox(response.error, false);
}
initBannedCountryList();
if (ccs.length == 1){
//Single country
msgbox(`Added ${getCountryName(ccs[0])} to blacklist`);
}else{
msgbox(ccs.length + " countries added to blacklist");
}
},
error: function(xhr, status, error) {
// handle error response
}
});
$('#countrySelector').dropdown('clear');
}
function removeFromBannedList(countryCode){
countryCode = countryCode.toLowerCase();
let countryName = getCountryName(countryCode);
if (countryCode == "eu"){
let euCountries = getEUCCs();
countryCode = euCountries.join(",");
countryName = "European Union";
}else{
countryCode = countryCode.toLowerCase();
}
$.cjax({
url: "/api/blacklist/country/remove",
method: "POST",
@ -1162,44 +1217,53 @@
//Usually just a few countries a for loop will get the job done
ccs = countryCode.split(",");
}
let counter = 0;
for(var i = 0; i < ccs.length; i++){
let thisCountryCode = ccs[i];
$.cjax({
type: "POST",
url: "/api/whitelist/country/add",
data: { cc: thisCountryCode , id: currentEditingAccessRule},
success: function(response) {
if (response.error != undefined){
msgbox(response.error, false);
}
if (counter == (ccs.length - 1)){
setTimeout(function(){
initWhitelistCountryList();
if (ccs.length == 1){
//Single country
msgbox(`Added ${getCountryName(ccs[0])} to whitelist`);
}else{
msgbox(ccs.length + " countries added to whitelist");
}
}, (ccs.length==1)?0:100);
}
counter++;
},
error: function(xhr, status, error) {
// handle error response
}
//If the ccs includes "eu", remove the "eu" and add all eu country code to the list
if (ccs.includes("eu")){
ccs = ccs.filter(function(item){
return item != "eu";
});
ccs = ccs.concat(getEUCCs());
}
let counter = ccs.length;
$.cjax({
type: "POST",
url: "/api/whitelist/country/add",
data: { cc: ccs.join(",") , id: currentEditingAccessRule},
success: function(response) {
if (response.error != undefined){
msgbox(response.error, false);
}
initWhitelistCountryList();
if (ccs.length == 1){
//Single country
msgbox(`Added ${getCountryName(ccs[0])} to whitelist`);
}else{
msgbox(ccs.length + " countries added to whitelist");
}
},
error: function(xhr, status, error) {
// handle error response
}
});
$('#countrySelectorWhitelist').dropdown('clear');
}
function removeFromWhiteList(countryCode){
if (confirm("Confirm removing " + getCountryName(countryCode) + " from whitelist?")){
//Remove from whitelist, accepts a country code or "eu" for all EU countries
function removeFromWhiteList(countryCode, skipConfirm = true){
let countryName = getCountryName(countryCode);
if (countryCode == "eu"){
let euCountries = getEUCCs();
countryCode = euCountries.join(",");
countryName = "European Union";
}else{
countryCode = countryCode.toLowerCase();
}
if (skipConfirm || confirm("Confirm removing " + getCountryName(countryCode) + " from whitelist?")){
$.cjax({
url: "/api/whitelist/country/remove",
method: "POST",
@ -1208,6 +1272,7 @@
if (response.error != undefined){
msgbox(response.error, false);
}
msgbox(countryName + " removed from whitelist");
initWhitelistCountryList();
},
error: function(xhr, status, error) {
@ -1276,19 +1341,27 @@
/*
Common Utilities
*/
function filterCountries(codesToShow, selector="#countrySelector .menu .item") {
function filterCountries(alreadySelectedCCs, selector="#countrySelector .menu .item") {
// get all items in the dropdown
const items = document.querySelectorAll(selector);
const euCountries = getEUCCs();
//Replce "eu" in alreadySelectedCCs with all EU countries
if (alreadySelectedCCs.includes("eu")){
alreadySelectedCCs = alreadySelectedCCs.filter(function(item){
return item != "eu";
});
alreadySelectedCCs = alreadySelectedCCs.concat(euCountries);
}
// loop through all items
items.forEach(item => {
// get the value of the item (i.e. the country code)
const code = item.dataset.value;
// if the code is in the array of codes to show, show the item
if (codesToShow.includes(code)) {
if (alreadySelectedCCs.includes(code)) {
//This country code already selected. Hide it
item.style.display = 'none';
}
// otherwise, hide the item
else {
} else {
// otherwise, show the item
item.style.display = 'block';
}
});

View File

@ -101,7 +101,7 @@
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
<div class="ui buttons">
<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>
<button class="ui basic button" onclick="uploadPrivateKey();"><i class="grey lock icon"></i> Private Key</button>
</div>
</div>
<div class="ui divider"></div>

View File

@ -125,10 +125,12 @@
</td>
<td data-label="" editable="true" datatype="vdir">${vdList}</td>
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
${subd.RequireBasicAuth?`<i class="ui green check icon"></i> Basic Auth`:``}
${subd.RequireBasicAuth && subd.RequireRateLimit?"<br>":""}
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Authelia`:``}
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> Oauth2`:``}
${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"<br>":""}
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
${!subd.RequireBasicAuth && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
</td>
<td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
<div class="ui toggle tiny fitted checkbox" style="margin-bottom: -0.5em; margin-right: 0.4em;" title="Enable / Disable Rule">
@ -194,6 +196,11 @@
}
let rule = accessRuleMap[thisAccessRuleID];
if (rule == undefined){
//Missing config or config too old
$(this).html(`<i class="ui red exclamation triangle icon"></i> <b style="color: #db2828;">Access Rule Error</b>`);
return;
}
let icon = `<i class="ui grey filter icon"></i>`;
if (rule.ID == "default"){
icon = `<i class="ui yellow star icon"></i>`;
@ -251,6 +258,13 @@
if (payload.UseStickySession){
useStickySessionChecked = "checked";
}
let enableUptimeMonitor = "";
//Note the config file store the uptime monitor as disable, so we need to reverse the logic
if (!payload.DisableUptimeMonitor){
enableUptimeMonitor = "checked";
}
input = `<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 1em;" onclick="editUpstreams('${uuid}');"><i class="grey server icon"></i> Edit Upstreams</button>
<div class="ui divider"></div>
<div class="ui checkbox" style="margin-top: 0.4em;">
@ -258,7 +272,11 @@
<label>Use Sticky Session<br>
<small>Enable stick session on load balancing</small></label>
</div>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="EnableUptimeMonitor" ${enableUptimeMonitor}>
<label>Monitor Uptime<br>
<small>Enable active uptime monitor</small></label>
</div>
`;
column.append(input);
$(column).find(".upstreamList").addClass("editing");
@ -269,12 +287,8 @@
</button>`);
}else if (datatype == "advanced"){
let requireBasicAuth = payload.RequireBasicAuth;
let basicAuthCheckstate = "";
if (requireBasicAuth){
basicAuthCheckstate = "checked";
}
let authProvider = payload.AuthenticationProvider.AuthMethod;
let skipWebSocketOriginCheck = payload.SkipWebSocketOriginCheck;
let wsCheckstate = "";
if (skipWebSocketOriginCheck){
@ -296,13 +310,29 @@
rateLimitDisableState = "disabled";
}
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="RequireBasicAuth" ${basicAuthCheckstate}>
<label>Require Basic Auth</label>
column.empty().append(`
<div class="grouped fields authProviderPicker">
<label><b>Authentication Provider</b></label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="0" name="authProviderType" ${authProvider==0x0?"checked":""}>
<label>None (Anyone can access)</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="1" name="authProviderType" ${authProvider==0x1?"checked":""}>
<label>Basic Auth</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}>
<label>Authelia</label>
</div>
</div>
</div>
<br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
<br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
<div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;">
@ -328,6 +358,7 @@
<div>
`);
$('.authProviderPicker .ui.checkbox').checkbox();
} else if (datatype == "ratelimit"){
column.empty().append(`
@ -421,7 +452,8 @@
var epttype = "host";
let useStickySession = $(row).find(".UseStickySession")[0].checked;
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
let DisableUptimeMonitor = !$(row).find(".EnableUptimeMonitor")[0].checked;
let authProviderType = $(row).find(".authProviderPicker input[type='radio']:checked").val();
let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked;
let rateLimit = $(row).find(".RateLimit").val();
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
@ -433,8 +465,9 @@
"type": epttype,
"rootname": uuid,
"ss":useStickySession,
"dutm": DisableUptimeMonitor,
"bpgtls": bypassGlobalTLS,
"bauth" :requireBasicAuth,
"authprovider" :authProviderType,
"rate" :requireRateLimit,
"ratenum" :rateLimit,
},

View File

@ -30,7 +30,7 @@
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
</div>
<!-- Options -->
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui basic segment advanceoptions">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
@ -173,7 +173,7 @@
});
if (data.length == 0){
$("#redirectionRuleList").append(`<tr colspan="4"><td><i class="green check circle icon"></i> No redirection rule</td></tr>`);
$("#redirectionRuleList").append(`<tr><td colspan="5"><i class="green check circle icon"></i> No redirection rule</td></tr>`);
}
});

View File

@ -37,6 +37,14 @@
</label>
</div>
</div>
<div class="field">
<div class="ui radio defaultsite checkbox">
<input type="radio" name="defaultsiteOption" value="closeresp">
<label>Close Connection<br>
<small>Close the connection without any response</small>
</label>
</div>
</div>
</div>
</div>
@ -105,6 +113,8 @@
currentDefaultSiteOption = 2;
}else if (selectedDefaultSite == "notfound"){
currentDefaultSiteOption = 3;
}else if (selectedDefaultSite == "closeresp"){
currentDefaultSiteOption = 4;
}else{
//Unknown option
return;
@ -137,6 +147,8 @@
$("#redirectDomain").val(data.DefaultSiteValue);
}else if (proxyType == 3){
$radios.filter('[value=notfound]').prop('checked', true);
}else if (proxyType == 4){
$radios.filter('[value=closeresp]').prop('checked', true);
}
updateAvaibleDefaultSiteOptions();

View File

@ -50,7 +50,7 @@
</div>
</div>
<!-- Advance configs -->
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui basic segment advanceoptions">
<div id="advanceProxyRules" class="ui fluid accordion">
<div class="title">
<i class="dropdown icon"></i>
@ -295,15 +295,25 @@
//Automatic check if the site require TLS and check the checkbox if needed
function autoCheckTls(targetDomain){
$.cjax({
url: "/api/proxy/tlscheck",
url: "/api/proxy/tlscheck?selfsignchk=true",
data: {url: targetDomain},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else if (data == "https"){
$("#reqTls").parent().checkbox("set checked");
}else if (data == "http"){
$("#reqTls").parent().checkbox("set unchecked");
}else{
//Check if the site require TLS
if (data.protocol == "https"){
$("#reqTls").parent().checkbox("set checked");
}else if (data.protocol == "http"){
$("#reqTls").parent().checkbox("set unchecked");
}
//Check if the site is using self-signed cert
if (data.selfsign){
$("#skipTLSValidation").parent().checkbox("set checked");
}else{
$("#skipTLSValidation").parent().checkbox("set unchecked");
}
}
}
})

View File

@ -1,381 +1,79 @@
<div class="standardContainer">
<div class="ui basic segment">
<div class="ui message">
<div class="header">
Work in Progress
</div>
<p>The SSO feature is currently under development.</p>
</div>
<h2>SSO</h2>
<p>Single Sign-On (SSO) and authentication providers settings </p>
</div>
</div>
<!--
<div class="standardContainer">
<div class="ui basic segment">
<h2>Zoraxy SSO / Oauth</h2>
<p>A centralized authentication system for all your subdomains</p>
<div class="ui divider"></div>
<div class="ui basic segment enabled ssoRunningState">
<h4 class="ui header" id="ssoRunningState">
<i class="circle check icon"></i>
<div class="content">
<span class="webserv_status">Running</span>
<div class="sub header">Listen port :<span class="oauthserv_port">8081</span></div>
</div>
</h4>
</div>
<div class="ui form">
<h3 class="ui dividing header">Oauth2 Server Settings</h3>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" name="enableOauth2">
<label>Enable Oauth2 Server<br>
<small>Oauth2 server for handling external authentication requests</small></label>
</div>
</div>
<div class="field">
<label>Oauth2 Server Port</label>
<div class="ui action input">
<input type="number" name="oauth2Port" placeholder="Port" value="5488">
<button id="saveOauthServerPortBtn" class="ui basic green button"><i class="ui green circle check icon"></i> Update</button>
</div>
<small>Listening port of the Zoraxy internal Oauth2 Server.You can create a subdomain proxy rule to <code>127.0.0.1:<span class="ssoPort">5488</span></code></small>
</div>
<div class="field">
<label>Auth URL</label>
<div class="ui action input">
<input type="text" name="authURL" placeholder="https://auth.yourdomain.com">
<button id="saveAuthURLBtn" class="ui basic blue button"><i class="ui blue save icon"></i> Save</button>
</div>
<small>The exposed authentication URL of the Oauth2 server, usually <code>https://auth.example.com</code> or <code>https://sso.yourdomain.com</code>. <b>Remember to include the http:// or https:// in your URL.</b></small>
</div>
</div>
<br>
<div class="ui form">
<h3 class="ui dividing header">Zoraxy SSO Settings</h3>
<div class="field">
<label>Default Redirection URL </label>
<div class="ui fluid input">
<input type="text" name="defaultSiteURL" placeholder="https://yourdomain.com">
</div>
<small>The default URL to redirect to after login if redirection target is not set</small>
</div>
<button class="ui basic button"> <i class="ui green check icon"></i> Apply Changes </button>
</div>
<div class="ui basic message">
<div class="ui basic segment">
<div class="ui yellow message">
<div class="header">
<i class="ui yellow exclamation triangle icon"></i> Important Notes about Zoraxy SSO
Experimental Feature
</div>
<p>Zoraxy SSO, if enabled in HTTP Proxy rule, will automatically intercept the proxy request and provide an SSO interface on upstreams that do not support OAuth natively.
It is basically like basic auth with a login page. <b> The same user credential can be used in OAuth sign-in and Zoraxy SSO sign-in.</b>
</p>
</div>
<div class="ui divider"></div>
<div>
<h3 class="ui header">
<i class="ui blue user circle icon"></i>
<div class="content">
Registered Users
<div class="sub header">A list of users that are registered with the SSO server</div>
</div>
</h3>
<table class="ui celled table">
<thead>
<tr>
<th>Username</th>
<th>Registered On</th>
<th>Reset Password</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="registeredSsoUsers">
<tr>
<td>admin</td>
<td>2020-01-01</td>
<td><button class="ui blue basic small icon button"><i class="ui blue key icon"></i></button></td>
<td><button class="ui red basic small icon button"><i class="ui red trash icon"></i></button></td>
</tr>
</tbody>
</table>
<button onclick="handleUserListRefresh();" class="ui basic right floated button"><i class="ui green refresh icon"></i> Refresh</button>
<button onclick="openRegisteredUserManager();" class="ui basic button"><i class="ui blue users icon"></i> Manage Registered Users</button>
</div>
<div class="ui divider"></div>
<div>
<h3 class="ui header">
<i class="ui green th icon"></i>
<div class="content">
Registered Apps
<div class="sub header">A list of apps that are registered with the SSO server</div>
</div>
</h3>
<table class="ui celled table">
<thead>
<tr>
<th>App Name</th>
<th>Domain</th>
<th>App ID</th>
<th>Registered On</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="registeredSsoApps">
<tr>
<td>My App</td>
<td><a href="//example.com" target="_blank">example.com</a></td>
<td>123456</td>
<td>2020-01-01</td>
<td><button class="ui red basic small icon button"><i class="ui red trash icon"></i></button></td>
</tr>
</tbody>
</table>
<button onclick="handleRegisterAppListRefresh();" class="ui basic right floated button"><i class="ui green refresh icon"></i> Refresh</button>
<button onclick="openRegisterAppManagementSnippet();" class="ui basic button"><i style="font-size: 1em; margin-top: -0.2em;" class="ui green th large icon"></i> Manage Registered App</button>
<p></p>
<p>Please note that this feature is still in development and may not work as expected.</p>
</div>
</div>
<div class="ui divider"></div>
<div class="ui basic segment">
<h3>Authelia</h3>
<p>Configuration settings for Authelia authentication provider.</p>
<form class="ui form">
<div class="field">
<label for="autheliaServerUrl">Authelia Server URL</label>
<input type="text" id="autheliaServerUrl" name="autheliaServerUrl" placeholder="Enter Authelia Server URL">
<small>Example: auth.example.com</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="useHttps" name="useHttps">
<label for="useHttps">Use HTTPS</label>
<small>Check this if your authelia server uses HTTPS</small>
</div>
</div>
<button class="ui basic button" onclick="event.preventDefault(); updateAutheliaSettings();"><i class="green check icon"></i> Apply Change</button>
</form>
</div>
<div class="ui divider"></div>
</div>
<script>
$("input[name=oauth2Port]").on("change", function() {
$(".ssoPort").text($(this).val());
});
function updateSSOStatus(){
$.get("/api/sso/status", function(data){
if(data.error != undefined){
//Show error message
$(".ssoRunningState").removeClass("enabled").addClass("disabled");
$("#ssoRunningState .webserv_status").html('Error: '+data.error);
}else{
if (data.Enabled){
$(".ssoRunningState").addClass("enabled");
$("#ssoRunningState .webserv_status").html('Running');
$(".ssoRunningState i").attr("class", "circle check icon");
$("input[name=enableOauth2]").parent().checkbox("set checked");
}else{
$(".ssoRunningState").removeClass("enabled");
$("#ssoRunningState .webserv_status").html('Stopped');
$(".ssoRunningState i").attr("class", "circle times icon");
$("input[name=enableOauth2]").parent().checkbox("set unchecked");
}
$("input[name=oauth2Port]").val(data.ListeningPort);
$(".oauthserv_port").text(data.ListeningPort);
$("input[name=authURL]").val(data.AuthURL);
$(document).ready(function() {
$.cjax({
url: '/api/sso/Authelia',
method: 'GET',
dataType: 'json',
success: function(data) {
$('#autheliaServerUrl').val(data.autheliaURL);
$('#useHttps').prop('checked', data.useHTTPS);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown);
}
});
}
});
function updateAutheliaSettings(){
var autheliaServerUrl = $('#autheliaServerUrl').val();
var useHttps = $('#useHttps').prop('checked');
function initSSOStatus(){
$.get("/api/sso/status", function(data){
//Update the SSO status from the server
updateSSOStatus();
//Bind events to the enable checkbox
$("input[name=enableOauth2]").off("change").on("change", function(){
var checked = $(this).prop("checked");
$.cjax({
url: "/api/sso/enable",
method: "POST",
data: {
enable: checked
},
success: function(data){
if(data.error != undefined){
msgbox("Failed to toggle SSO: " + data.error, false);
//Unbind the event to prevent infinite loop
$("input[name=enableOauth2]").off("change");
}else{
initSSOStatus();
}
}
});
});
});
}
initSSOStatus();
/* Save the Oauth server port */
function saveOauthServerPort(){
var port = $("input[name=oauth2Port]").val();
//Check if the port is valid
if (port < 1 || port > 65535){
msgbox("Invalid port number", false);
return;
}
//Use cjax to send the port to the server with csrf token
$.cjax({
url: "/api/sso/setPort",
method: "POST",
url: '/api/sso/Authelia',
method: 'POST',
data: {
port: port
autheliaURL: autheliaServerUrl,
useHTTPS: useHttps
},
success: function(data) {
if (data.error != undefined) {
msgbox("Failed to update Oauth server port: " + data.error, false);
} else {
msgbox("Oauth server port updated", true);
$.msgbox(data.error, false);
return;
}
updateSSOStatus();
}
});
}
//Bind the save button to the saveOauthServerPort function
$("#saveOauthServerPortBtn").on("click", function() {
saveOauthServerPort();
});
$("input[name=oauth2Port]").on("keypress", function(e) {
if (e.which == 13) {
saveOauthServerPort();
}
});
/* Save the Oauth server URL (aka AuthURL) */
function saveAuthURL(){
var url = $("input[name=authURL]").val();
//Make sure the url contains http:// or https://
if (!url.startsWith("http://") && !url.startsWith("https://")){
msgbox("Invalid URL. Make sure to include http:// or https://", false);
$("input[name=authURL]").parent().parent().addClass("error");
return;
}else{
$("input[name=authURL]").parent().parent().removeClass("error");
}
//Use cjax to send the port to the server with csrf token
$.cjax({
url: "/api/sso/setAuthURL",
method: "POST",
data: {
"auth_url": url
msgbox('Authelia settings updated', true);
console.log('Authelia settings updated:', data);
},
success: function(data) {
if (data.error != undefined) {
msgbox("Failed to update Oauth server port: " + data.error, false);
} else {
msgbox("Oauth server port updated", true);
}
updateSSOStatus();
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error updating Authelia settings:', textStatus, errorThrown);
}
});
}
//Bind the save button to the saveAuthURL function
$("#saveAuthURLBtn").on("click", function() {
saveAuthURL();
});
$("input[name=authURL]").on("keypress", function(e) {
if (e.which == 13) {
saveAuthURL();
}
});
/* Registered Apps Event Handlers */
//Function to initialize the registered app table
function initRegisteredAppTable(){
$.get("/api/sso/app/list", function(data){
if(data.error != undefined){
msgbox("Failed to get registered apps: " + data.error, false);
}else{
var tbody = $("#registeredSsoApps");
tbody.empty();
for(var i = 0; i < data.length; i++){
var app = data[i];
var tr = $("<tr>");
tr.append($("<td>").text(app.AppName));
tr.append($("<td>").html('<a href="//'+app.Domain+'" target="_blank">'+app.Domain+'</a>'));
tr.append($("<td>").text(app.AppID));
tr.append($("<td>").text(app.RegisteredOn));
var removeBtn = $("<button>").addClass("ui red basic small icon button").html('<i class="ui red trash icon"></i>');
removeBtn.on("click", function(){
removeApp(app.AppID);
});
tr.append($("<td>").append(removeBtn));
tbody.append(tr);
}
if (data.length == 0){
tbody.append($("<tr>").append($("<td>").attr("colspan", 4).html(`<i class="ui green circle check icon"></i> No registered users`)));
}
}
});
}
initRegisteredAppTable();
//Also bind the refresh button to the initRegisteredAppTable function
function handleRegisterAppListRefresh(){
initRegisteredAppTable();
}
function openRegisterAppManagementSnippet(){
//Open the register app management snippet
showSideWrapper("snippet/sso_app.html");
}
//Bind the remove button to the removeApp function
function removeApp(appID){
$.cjax({
url: "/api/sso/removeApp",
method: "POST",
data: {
appID: appID
},
success: function(data){
if(data.error != undefined){
msgbox("Failed to remove app: " + data.error, false);
}else{
msgbox("App removed", true);
updateSSOStatus();
}
}
});
}
/* Registered Users Event Handlers */
function initUserList(){
$.get("/api/sso/user/list", function(data){
if(data.error != undefined){
msgbox("Failed to get registered users: " + data.error, false);
}else{
var tbody = $("#registeredSsoUsers");
tbody.empty();
for(var i = 0; i < data.length; i++){
var user = data[i];
var tr = $("<tr>");
tr.append($("<td>").text(user.Username));
tr.append($("<td>").text(user.RegisteredOn));
var resetBtn = $("<button>").addClass("ui blue basic small icon button").html('<i class="ui blue key icon"></i>');
resetBtn.on("click", function(){
resetPassword(user.Username);
});
tr.append($("<td>").append(resetBtn));
var removeBtn = $("<button>").addClass("ui red basic small icon button").html('<i class="ui red trash icon"></i>');
removeBtn.on("click", function(){
removeUser(user.Username);
});
tr.append($("<td>").append(removeBtn));
tbody.append(tr);
}
if (data.length == 0){
tbody.append($("<tr>").append($("<td>").attr("colspan", 4).html(`<i class="ui green circle check icon"></i> No registered users`)));
}
}
});
}
//Bind the refresh button to the initUserList function
function handleUserListRefresh(){
initUserList();
}
function openRegisteredUserManager(){
//Open the registered user management snippet
showSideWrapper("snippet/sso_user.html");
}
</script>
-->
</script>

View File

@ -1,3 +1,10 @@
<style>
#redirect.disabled{
opacity: 0.7;
pointer-events: none;
user-select: none;
}
</style>
<div class="ui stackable grid">
<div class="ten wide column serverstatusWrapper">
<div id="serverstatus" class="ui statustab inverted segment">
@ -73,28 +80,30 @@
<p>Inbound Port (Reverse Proxy Listening Port)</p>
<div class="ui action fluid notloopbackOnly input" tourstep="incomingPort">
<small id="applyButtonReminder">Click "Apply" button to confirm listening port changes</small>
<input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
<input type="text" id="incomingPort" placeholder="Incoming Port" value="443">
<button class="ui green notloopbackOnly button" style="background: linear-gradient(60deg, #27e7ff, #00ca52);" onclick="handlePortChange();"><i class="ui checkmark icon"></i> Apply</button>
</div>
<br>
<div id="tls" class="ui toggle notloopbackOnly checkbox">
<input type="checkbox">
<label>Use TLS to serve proxy request</label>
<label>Use TLS to serve proxy request<br>
<small>Also known as HTTPS mode</small></label>
</div>
<br>
<div id="listenP80" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;" >
<div id="listenP80" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.4em;" >
<input type="checkbox">
<label>Enable HTTP server on port 80<br>
<small>(Only apply when TLS enabled and not using port 80)</small></label>
<small>Accept HTTP requests even if you are using HTTPS mode</small></label>
</div>
<br>
<div tourstep="forceHttpsRedirect" style="display: inline-block;">
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em; padding-left: 2em;">
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.4em;">
<input type="checkbox">
<label>Force redirect HTTP request to HTTPS</label>
<label>Force redirect HTTP request to HTTPS<br>
<small>Redirect web traffic from port 80 to 443, require enabling HTTP server on port 80</small></label>
</div>
</div>
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui basic segment advanceoptions">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
@ -359,10 +368,12 @@
return;
}
if (enabled){
$("#redirect").show();
//$("#redirect").show();
$("#redirect").removeClass("disabled");
msgbox("Port 80 listener enabled");
}else{
$("#redirect").hide();
//$("#redirect").hide();
$("#redirect").addClass("disabled");
msgbox("Port 80 listener disabled");
}
}
@ -400,10 +411,12 @@
$.get("/api/proxy/listenPort80", function(data){
if (data){
$("#listenP80").checkbox("set checked");
$("#redirect").show();
$("#redirect").removeClass("disabled");
//$("#redirect").show();
}else{
$("#listenP80").checkbox("set unchecked");
$("#redirect").hide();
$("#redirect").addClass("disabled");
//$("#redirect").hide();
}
$("#listenP80").find("input").on("change", function(){
@ -579,7 +592,7 @@
let timestamps = [];
for(var i = 0; i < dataCount; i++){
timestamps.push(parseInt(Date.now() / 1000) + i);
timestamps.push(new Date(Date.now() + i * 1000).toLocaleString().replace(',', ''));
}
function fetchData() {
@ -600,10 +613,8 @@
txValues.shift();
}
timestamps.push(parseInt(Date.now() / 1000));
timestamps.push(new Date(Date.now()).toLocaleString().replace(',', ''));
timestamps.shift();
updateChart();
}
})

View File

@ -70,7 +70,7 @@
initUptimeTable();
function reloadUptimeList(){
$("#utmrender").html(`<div class="ui segment">
$("#utmrender").html(`<div class="ui utmloading segment">
<div class="ui active inverted dimmer" style="z-index: 2;">
<div class="ui text loader">Loading</div>
</div>

View File

@ -69,7 +69,7 @@
</div>
<!-- Advance configs -->
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui basic segment advanceoptions">
<div id="advanceProxyRules" class="ui fluid accordion">
<div class="title">
<i class="dropdown icon"></i>
@ -191,14 +191,19 @@
var targetDomain = $("#virtualDirectoryDomain").val().trim();
if (targetDomain != ""){
$.cjax({
url: "/api/proxy/tlscheck",
url: "/api/proxy/tlscheck?selfsignchk=true",
data: {url: targetDomain},
success: function(data){
if (data.error != undefined){
}else if (data == "https"){
}else if (data.protocol == "https"){
$("#vdReqTls").parent().checkbox("set checked");
}else if (data == "http"){
if (data.selfsign){
$("#vdSkipTLSValidation").parent().checkbox("set checked");
}else{
$("#vdSkipTLSValidation").parent().checkbox("set unchecked");
}
}else if (data.protocol == "http"){
$("#vdReqTls").parent().checkbox("set unchecked");
}
}

1133
src/web/darktheme.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -16,22 +16,24 @@
<script src="script/chart.js"></script>
<script src="script/utils.js"></script>
<link rel="stylesheet" href="main.css">
<link rel="stylesheet" href="darktheme.css">
</head>
<body>
<script src="script/darktheme.js"></script>
<div class="menubar">
<div class="item">
<img class="logo" src="img/logo.svg">
</div>
<div class="ui right floated buttons menutoggle" style="padding-top: 2px;">
<button class="ui basic icon button" onclick="$('.toolbar').fadeToggle('fast');"><i class="content icon"></i></button>
<button id="sidemenuBtn" class="ui basic icon button" onclick="$('.toolbar').fadeToggle('fast');"><i class="content icon"></i></button>
</div>
<div class="ui right floated buttons" style="padding-top: 2px; padding-right: 0.4em;">
<button class="ui basic white icon button" onclick="logout();"><i class="sign-out icon"></i></button>
</div>
<!-- <div class="ui right floated buttons" style="padding-top: 2px;">
<div class="ui right floated buttons" style="padding-top: 2px; margin-right: 0.4em;">
<button id="themeColorButton" class="ui icon button" onclick="toggleTheme();"><i class="sun icon"></i></button>
</div> -->
</div>
</div>
<div class="wrapper">
<div class="toolbar">
@ -269,11 +271,18 @@
function toggleTheme(){
if ($("body").hasClass("darkTheme")){
$("body").removeClass("darkTheme")
$("#themeColorButton").html(`<i class="ui moon icon"></i>`);
setDarkTheme(false);
//Check if the snippet iframe is opened. If yes, set the dark theme to the iframe
if ($(".sideWrapper").is(":visible")){
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(false);
}
}else{
$("body").addClass("darkTheme");
$("#themeColorButton").html(`<i class="ui sun icon"></i>`);
setDarkTheme(true);
//Check if the snippet iframe is opened. If yes, set the dark theme to the iframe
if ($(".sideWrapper").is(":visible")){
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
}
}
}

View File

@ -1,50 +1,8 @@
/*
index.html style overwrite
*/
:root{
--theme_background: linear-gradient(60deg, rgb(84, 58, 183) 0%, rgb(0, 172, 193) 100%);
--theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
--theme_green: linear-gradient(270deg, #27e7ff, #00ca52);
--theme_red: linear-gradient(203deg, rgba(250,172,38,1) 17%, rgba(202,0,37,1) 78%);
}
/* Theme Color Definations */
body:not(.darkTheme){
--theme_bg: #f6f6f6;
--theme_bg_primary: #ffffff;
--theme_bg_secondary: #ffffff;
--theme_bg_active: #ececec;
--theme_highlight: #a9d1f3;
--theme_bg_inverted: #27292d;
--theme_advance: #f8f8f9;
--item_color: #5e5d5d;
--item_color_select: rgba(0,0,0,.87);
--text_color: #414141;
--input_color: white;
--divider_color: #cacaca;
--text_color_inverted: #fcfcfc;
--button_text_color: #878787;
--button_border_color: #dedede;
}
body.darkTheme{
--theme_bg: #27292d;
--theme_bg_primary: #3d3f47;
--theme_bg_secondary: #373a42;
--theme_highlight: #6682c4;
--theme_bg_active: #292929;
--theme_bg_inverted: #f8f8f9;
--theme_advance: #333333;
--item_color: #cacaca;
--text_color: #fcfcfc;
--text_color_secondary: #dfdfdf;
--input_color: black;
--divider_color: #3b3b3b;
--item_color_select: rgba(255, 255, 255, 0.87);
--text_color_inverted: #414141;
--button_text_color: #e9e9e9;
--button_border_color: #646464;
}
/* Theme color palletes are defined in darktheme.css */
/* Theme Toggle CSS */
#themeColorButton{
@ -107,6 +65,8 @@ body{
height: calc(100% - 51px);
overflow-y: auto;
width: 240px;
position: sticky;
top: 4em;
}
.contentWindow{
@ -368,7 +328,7 @@ body{
}
.basic.segment.advanceoptions{
background-color: #f7f7f7;
background-color: var(--theme_advance);
border-radius: 1em;
}

6
src/web/robots.txt Normal file
View File

@ -0,0 +1,6 @@
# robots.txt for Zoraxy project
# In general, you should not expose the management interface to the internet.
# In case you do, this file (hopefully) protects you from web crawlers.
User-agent: *
Disallow: /

View File

@ -0,0 +1,51 @@
/*
Dark Theme Toggle Manager
This script is used to manage the dark theme toggle button in the header of the website.
It will change the theme of the website to dark mode when the toggle is clicked and back to light mode when clicked again.
Must be included just after the start of body tag in the HTML file.
*/
function _whiteThemeHandleApplyChange(){
$(".menubar .logo").attr("src", "img/logo.svg");
}
function _darkThemeHandleApplyChange(){
$(".menubar .logo").attr("src", "img/logo_white.svg");
}
//Check if the theme is dark, must be done before the body is loaded to prevent flickering
function setDarkTheme(isDarkTheme = false){
if (isDarkTheme){
$("body").addClass("darkTheme");
$("#themeColorButton").html(`<i class="ui sun icon"></i>`);
localStorage.setItem("theme", "dark");
//Check if the page is still loading, if not change the logo
if (document.readyState == "complete"){
_darkThemeHandleApplyChange();
}else{
//Wait for the page to load and then change the logo
$(document).ready(function(){
_darkThemeHandleApplyChange();
});
}
}else{
$("body").removeClass("darkTheme")
$("#themeColorButton").html(`<i class="ui moon icon"></i>`);
localStorage.setItem("theme", "light");
//By default the page is white theme. So no need to change the logo if page is still loading
if (document.readyState == "complete"){
//Switching back to light theme
_whiteThemeHandleApplyChange();
}
}
}
if (localStorage.getItem("theme") == "dark"){
setDarkTheme(true);
}else{
setDarkTheme(false);
}

View File

@ -14,69 +14,72 @@
top: 0.4em;
right: 1em;
}
</style>
</head>
<body>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Access Rule Editor
<div class="sub header">Create, Edit or Remove Access Rules</div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui top attached tabular menu">
<a class="active item" data-tab="new"><i class="ui green add icon"></i> New</a>
<a class="item" data-tab="edit"><i class="ui grey edit icon"></i> Edit</a>
</div>
<div class="ui bottom attached active tab segment" data-tab="new">
<p>Create a new Access Rule</p>
<form class="ui form" id="accessRuleForm">
<div class="field">
<label>Rule Name</label>
<input type="text" name="accessRuleName" placeholder="Rule Name" required>
</div>
<div class="field">
<label>Description</label>
<textarea name="description" placeholder="Description" required></textarea>
</div>
<button class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
</form>
<link rel="stylesheet" href="../darktheme.css">
<script src="../script/darktheme.js"></script>
<br>
</div>
<div class="ui bottom attached tab segment" data-tab="edit">
<p>Select an Access Rule to edit</p>
<button id="refreshAccessRuleListBtn" class="ui circular basic icon button" onclick="reloadAccessRuleList()"><i class="ui green refresh icon"></i></button>
<div class="ui selection fluid dropdown" id="accessRuleSelector">
<input type="hidden" name="targetAccessRule" value="default">
<i class="dropdown icon"></i>
<div class="default text"></div>
<div class="menu" id="accessRuleList">
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
<div class="ui container">
<div class="ui header">
<div class="content">
Access Rule Editor
<div class="sub header">Create, Edit or Remove Access Rules</div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui top attached tabular menu">
<a class="active item" data-tab="new"><i class="ui green add icon"></i> New</a>
<a class="item" data-tab="edit"><i class="ui grey edit icon"></i> Edit</a>
</div>
<div class="ui bottom attached active tab segment" data-tab="new">
<p>Create a new Access Rule</p>
<form class="ui form" id="accessRuleForm">
<div class="field">
<label>Rule Name</label>
<input type="text" name="accessRuleName" placeholder="Rule Name" required>
</div>
<div class="field">
<label>Description</label>
<textarea name="description" placeholder="Description" required></textarea>
</div>
<button class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
</form>
<br>
</div>
<div class="ui bottom attached tab segment" data-tab="edit">
<p>Select an Access Rule to edit</p>
<button id="refreshAccessRuleListBtn" class="ui circular basic icon button" onclick="reloadAccessRuleList()"><i class="ui green refresh icon"></i></button>
<div class="ui selection fluid dropdown" id="accessRuleSelector">
<input type="hidden" name="targetAccessRule" value="default">
<i class="dropdown icon"></i>
<div class="default text"></div>
<div class="menu" id="accessRuleList">
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
</div>
</div>
<br>
<form class="ui form" id="modifyRuleInfo">
<div class="disabled field">
<label>Rule ID</label>
<input type="text" name="accessRuleUUID">
</div>
<div class="field">
<label>Rule Name</label>
<input type="text" name="accessRuleName" placeholder="Rule Name" required>
</div>
<div class="field">
<label>Description</label>
<textarea name="description" placeholder="Description" required></textarea>
</div>
<button class="ui basic button" type="submit"><i class="ui green save icon"></i> Save Changes</button>
<button class="ui basic button" onclick="removeAccessRule(event);"><i class="ui red trash icon"></i> Remove Rule</button>
</form>
</div>
<br>
<form class="ui form" id="modifyRuleInfo">
<div class="disabled field">
<label>Rule ID</label>
<input type="text" name="accessRuleUUID">
</div>
<div class="field">
<label>Rule Name</label>
<input type="text" name="accessRuleName" placeholder="Rule Name" required>
</div>
<div class="field">
<label>Description</label>
<textarea name="description" placeholder="Description" required></textarea>
</div>
<button class="ui basic button" type="submit"><i class="ui green save icon"></i> Save Changes</button>
<button class="ui basic button" onclick="removeAccessRule(event);"><i class="ui red trash icon"></i> Remove Rule</button>
</form>
</div>
<br>
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
<br><br><br>
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
<br><br><br>
</div>
<script>

View File

@ -25,6 +25,8 @@
</style>
</head>
<body>
<link rel="stylesheet" href="../darktheme.css">
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui header">
@ -50,7 +52,7 @@
</div>
<small>If you don't want to share your private email address, you can also fill in an email address that point to a mailbox not exists on your domain.</small>
</div>
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui basic segment advanceoptions">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
@ -65,16 +67,18 @@
<button id="renewNowBtn" onclick="renewNow();" class="ui basic right floated button" style="margin-top: -2em;"><i class="yellow refresh icon"></i> Renew Now</button>
<div class="ui horizontal divider"> OR </div>
<p>Select the certificates to automatic renew in the list below</p>
<table id="domainCertFileTable" class="ui very compact unstackable basic disabled table">
<thead>
<tr>
<th>Domain Name</th>
<th>Match Rule</th>
<th>Auto-Renew</th>
</tr>
</thead>
<tbody id="domainTableBody"></tbody>
</table>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table id="domainCertFileTable" class="ui very compact unstackable basic disabled table">
<thead>
<tr>
<th>Domain Name</th>
<th>Match Rule</th>
<th>Auto-Renew</th>
</tr>
</thead>
<tbody id="domainTableBody"></tbody>
</table>
</div>
<small><i class="ui red info circle icon"></i> Domain in red are expired</small><br>
<div class="ui yellow message">
Certificate Renew only works on the certification authority (CA) supported by Zoraxy. Check Zoraxy wiki for more information on supported list of CAs.
@ -159,6 +163,11 @@
</div>
-->
</div>
<div class="field dnsChallengeOnly" style="display:none;">
<label>Domain Name Server (optional)</label>
<input id="dnsInput" type="text" placeholder="ns.example.com">
<small>If you have more than one DNS server, enter them separated by commas (e.g. ns1.example.com,ns2.example.com)</small>
</div>
<div class="field" id="caInput" style="display:none;">
<label>ACME Server URL</label>
<input id="caURL" type="text" placeholder="https://example.com/acme/dictionary">
@ -437,11 +446,15 @@
let optionalFieldsHTML = "";
for (const [key, datatype] of Object.entries(data)) {
if (datatype == "int"){
$("#dnsProviderAPIFields").append(`<div class="ui fluid labeled dnsConfigField input" key="${key}" style="margin-top: 0.2em;">
let defaultValue = 10;
if (key == "HTTPTimeout"){
defaultValue = 300;
}
$("#dnsProviderAPIFields").append(`<div class="ui fluid labeled dnsConfigField input typeint" key="${key}" style="margin-top: 0.2em;">
<div class="ui basic blue label" style="font-weight: 300;">
${key}
</div>
<input type="number" value="300">
<input type="number" value="${defaultValue}">
</div>`);
}else if (datatype == "bool"){
booleanFieldsHTML += (`<div class="ui checkbox dnsConfigField" key="${key}" style="margin-top: 1em !important; padding-left: 0.4em;">
@ -600,8 +613,12 @@
//Boolean option
let checked = $(this).find("input")[0].checked;
dnsCredentials[thisKey] = checked;
}else if ($(this).hasClass("typeint")){
//Int options
let value = $(this).find("input").val();
dnsCredentials[thisKey] = parseInt(value);
}else{
//String or int options
//String options
let value = $(this).find("input").val().trim();
dnsCredentials[thisKey] = value;
}
@ -715,8 +732,7 @@
if (callback != undefined){callback(false);}
return;
}
var ca = $("#ca").dropdown("get value");
var caURL = "";
if (ca == "Custom ACME Server") {
@ -724,9 +740,9 @@
caURL = $("#caURL").val();
}
var dns = $("#useDnsChallenge")[0].checked;
var skipTLSValue = $("#skipTLSCheckbox")[0].checked;
var dnsServers = $("#dnsInput").val(); // Erfassen der DNS-Server
$.ajax({
url: "/api/acme/obtainCert",
@ -739,6 +755,7 @@
caURL: caURL,
skipTLS: skipTLSValue,
dns: dns,
dnsServers: dnsServers // DNS-Server in die Anfrage einfügen
},
success: function(response) {
$("#obtainButton").removeClass("loading").removeClass("disabled");
@ -751,7 +768,6 @@
console.log("Certificate renewed successfully");
// Show success message
parent.msgbox("Certificate renewed successfully");
// Renew the parent certificate list
parent.initManagedDomainCertificateList();

View File

@ -10,6 +10,8 @@
<script src="../script/utils.js"></script>
</head>
<body>
<link rel="stylesheet" href="../darktheme.css">
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui header">

View File

@ -10,6 +10,8 @@
<script src="../script/utils.js"></script>
</head>
<body>
<link rel="stylesheet" href="../darktheme.css">
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui header">

View File

@ -10,6 +10,8 @@
<script src="../script/utils.js"></script>
</head>
<body>
<link rel="stylesheet" href="../darktheme.css">
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui header">

View File

@ -10,6 +10,8 @@
<script src="../script/utils.js"></script>
</head>
<body>
<link rel="stylesheet" href="../darktheme.css">
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui header">

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