84 Commits
2.6 ... 2.6.8

Author SHA1 Message Date
20cf290d37 Merge pull request #84 from tobychui/2.6.8
Update v2.6.8
2023-11-25 22:25:22 +08:00
4ca0fcc6d1 Update main.go
Swap to use embedded web fs
2023-11-25 16:38:15 +08:00
ce4ce72820 Added optional TLS bypass mechanism
+ Added opt-out for subdomains for global TLS settings #44
+ Optimized subdomain / vdir editing interface
2023-11-25 15:54:28 +08:00
e363d55899 Fixed #77 and added systemwide logger
+ Added system wide logger (wip)
+ Fixed issue #77 uptime monitor bug
+ Added backend options for bypass global TLS (or allow per rule TLS settings, wip)
+
2023-11-25 12:50:30 +08:00
172479e4fb Added default HTTP/2 Mode
+ Added automatic HTTP/2 switch to TLS mode (Related to #40)
2023-10-20 13:44:47 +08:00
156fa5dace Bug fix
+ Added potential fix for #67
+ Update version number
+ Changed default static web port to 5487 so it is even more unlikely to be used
+
2023-10-20 11:01:28 +08:00
50f222cced Merge pull request #68 from Morethanevil/patch-6
Update CHANGELOG.md
2023-09-26 21:30:23 +08:00
640e1adf96 Update CHANGELOG.md
Updated to latest version
2023-09-26 09:28:57 +02:00
d4bb84180c Merge pull request #66 from tobychui/2.6.7
v2.6.7 Update
2023-09-26 11:24:09 +08:00
bda47fc36b Added default Ca features
+ Added default CA feature
+ Fixed RWD issue in TLS cert table
+ Optimized ACME UI in the TLS page
2023-09-25 20:54:50 +08:00
fd6ba56143 Added web directory manager
+ Added web directory manager
+ Added dummy service expose proxy page
+ Moved ACME and renew to a new section in TLS management page
2023-09-24 23:44:48 +08:00
b63a0fc246 Added static web server and black / whitelist template
- Added static web server
- Added static web server default index
- Added embeded templates to blacklist / whitelist
- Added wip Web Directory Manager

Place the templates at ./www/templates/blacklist.html or whitelist.html to replace the build in embedded template for access control 403 error
-
2023-09-14 16:15:40 +08:00
ed92cccf0e Merge pull request #65 from daluntw/2.6.7
Fix the out of range problem when certificate auto renew
2023-09-13 17:21:29 +08:00
95892802fd use issuer org as failover for json file not exist 2023-09-13 04:28:33 +00:00
8a5004e828 handle buypass issuer not match 2023-09-13 04:27:11 +00:00
c6c523e005 prevent out of range when check issuer exist 2023-09-13 00:39:29 +00:00
a692ec818d Static web server
- Fixed arm64 build bug in Make file
- Added wip static web server
2023-09-12 16:41:52 +08:00
c65f780613 Merge pull request #64 from daluntw/multidomain_fix
Fix multidomain UI handle incorrectly
2023-09-10 17:42:07 +08:00
507c2ab468 Updated 2.6.7 init 2023-09-09 12:28:24 +08:00
1180da8d11 fix multidomain missing logic 2023-09-08 23:18:47 +00:00
83f574e3ab Merge pull request #60 from Morethanevil/main
Update CHANGELOG.md
2023-08-30 22:47:42 +08:00
60837f307d Update CHANGELOG.md 2023-08-30 16:34:43 +02:00
50d5dedabe Merge pull request #59 from tobychui/2.6.6-experimental
2.6.6
2023-08-30 15:36:50 +08:00
f15c774c70 Updated go mod
+ Updated go mod to latest version
+ Fixed minor UX problem pn acme snippet
2023-08-30 12:16:53 +08:00
069f4805f6 Merge pull request #57 from daluntw/dev-custom-acme
Add Skip TLS Verify Feature For ACME Server
2023-08-29 10:06:52 +08:00
eb98624a6a fix naming convention 2023-08-28 03:35:20 +00:00
6a0c7cf499 add skipTLS for custom acme server 2023-08-28 03:31:33 +00:00
73ab9ca778 Optimized memory usage and root routing
+ Added unset subdomain custom redirection feature #46
+ Optimized memory usage by space time tradeoff in geoip lookup to fix #52
+ Replaced all stori/go.uuid to google/uuid for security reasons #55
2023-08-27 10:18:49 +08:00
9f9e0750e1 Update README.md 2023-08-26 15:05:53 +08:00
5664965491 Update README.md
Added donation link (wip)
2023-08-26 11:59:51 +08:00
db4016e79f Merge pull request #54 from PassiveLemon/Docker-Rework
Completely rework the container
2023-08-25 11:03:38 +08:00
f84c4370cf doc: update readme 2023-08-24 17:59:35 -04:00
b39cb6391b Completely rework the container 2023-08-24 17:48:57 -04:00
4f7f60188f Added basic auth exception paths
Added feature request from #25
2023-08-22 23:46:54 +08:00
dce58343db Merge pull request #48 from daluntw/dev-custom-acme
Add custom ACME server feature in backend
2023-08-22 14:05:42 +08:00
415838ad39 add frontend support 2023-08-21 13:19:33 +00:00
ce0b1a7585 fix missing space 2023-08-21 12:55:14 +00:00
352995e852 add custom acme feature in backend 2023-08-20 21:01:33 +00:00
a3d55a3274 Patching redirection bug
+ Added wip basic auth editor custom exception rules
+ Added custom logic to handle apache screw up redirect header
2023-08-20 14:50:25 +08:00
70adadf129 bug fixes for 2.6.5
+ Added potential fixes for #34 and #39
+ Added better error handling for #43
2023-08-17 14:08:48 +08:00
d42ac8a146 Merge pull request #36 from tobychui/Docker-2.1.0
Docker 2.1.0
2023-07-29 00:00:09 +08:00
f304ff8862 feature: network connectivity test 2023-07-28 10:23:20 -04:00
7d91e02dc9 feature: golang specific image
smaller size
2023-07-28 10:22:46 -04:00
dae510ae0a doc: VERSION 2023-07-28 10:22:22 -04:00
cd382a78a5 Merge pull request #35 from Morethanevil/patch-5
Update CHANGELOG.md
2023-07-27 00:22:57 +08:00
987de4a7be Update CHANGELOG.md 2023-07-26 17:21:09 +02:00
52d3b2f8c2 Fixed memory leaking
+ Patch on memory leaking for Windows netstat module (do not effect any of the previous non Windows builds)
+ Fixed potential memory leak in acme handler logic
+ Added "do you want to get a TLS certificate for this subdomain?" dialog when a new subdomain proxy rule is created
2023-07-26 19:17:43 +08:00
5038429a70 Update README.md 2023-07-20 22:04:16 -04:00
2acbf0f3f5 Merge pull request #31 from tobychui/DockerMerge
Docker 2.0.0
2023-07-20 10:30:55 +08:00
aed703e260 Merge branch 'main' into DockerMerge 2023-07-20 10:30:07 +08:00
5ece7c0da4 2.0.0 init
Completely revamped the container to support the new external configuration files.
2023-07-19 16:39:51 -04:00
7eda6ba501 Merge pull request #29 from Morethanevil/patch-4
Updated change log
2023-07-19 11:42:13 +08:00
2da5ef048f Update CHANGELOG.md 2023-07-19 05:14:42 +02:00
6c48939316 Update CHANGELOG.md 2023-07-19 05:10:08 +02:00
544894bbba Merge pull request #28 from tobychui/autorenew_experimental
Auto-renew with ACME
2023-07-19 09:43:23 +08:00
153d056bdf ACME compatibility fix for /.well-known/
+ Updated acme well known take-over regrex
+ Added experimental config export and import
+ Added unit test for location rewrite in dpcore
+ Moved all config files to ./conf and original proxy files to ./conf/proxy
+ Minor optimization on UI regarding TLS verification logo on subdomain and vdir list
2023-07-12 21:42:09 +08:00
12c1118af9 Merge pull request #27 from tobychui/main
Update auto-renew branch to catch up with main
2023-07-09 14:43:07 +08:00
67ba143999 Merge branch 'autorenew_experimental' into main 2023-07-09 14:41:34 +08:00
0a8a821394 Update main.go
Switched dev mode to false (Note: The advance routing rule system is still work in progress, do not use them)
2023-07-06 11:03:41 +08:00
36b17ce4cf acme and redirection patch
+ Added experimental fix for redirection tailing problem
+ Added acme widget for first time users to setup https
2023-07-06 11:01:33 +08:00
519372069f Update README.md 2023-07-04 07:58:40 -04:00
2f14d6f271 Fixed minor bugs in renew policy toggle 2023-06-29 18:59:24 +08:00
44ac7144ec Added access control bypass for /.well-known router 2023-06-23 23:45:49 +08:00
741d3f8de1 Minor fix to renew now function 2023-06-23 23:22:19 +08:00
23eca5afae Added experimental acme renew from Let's Encrypt 2023-06-23 23:09:10 +08:00
050fab9481 Merge pull request #24 from tobychui/DockerMerge
Updates + Changes
2023-06-22 11:12:26 +08:00
3fc92bac27 Updates + Changes
Some code improvements. If the version ever changes, it will download the new binary if it doesn't already exist. Old ones will be kept however.

Some general changes to documentation.
2023-06-21 13:04:32 -04:00
594f75da97 Update README.md 2023-06-21 23:59:24 +08:00
3fbf246fb4 Added docker file (Fork from PassiveLemon) 2023-06-21 19:45:58 +08:00
828af6263d Merge pull request #23 from Morethanevil/patch-3
Update CHANGELOG.md
2023-06-16 21:46:32 +08:00
ab42cec31f Update CHANGELOG.md 2023-06-15 19:23:45 +02:00
a8bf07dbba Update start.go
Patched the rule folder name
2023-06-16 00:54:55 +08:00
48dc85ea3e Updates 2.6.4
+ Added force TLS v1.2 above toggle
+ Added trace route
+ Added ICMP ping
+ Added special routing rules module for up-coming acme integration
+ Fixed IPv6 check bug in black/whitelist
+ Optimized UI for TCP Proxy
+
2023-06-16 00:48:39 +08:00
a73a7944ec Merge pull request #19 from Morethanevil/patch-2
Update CHANGELOG.md
2023-06-09 00:22:33 +08:00
d187124db6 Quick fix on the edit not working bug 2023-06-09 00:01:11 +08:00
0dd9e5d73c Update CHANGELOG.md 2023-06-08 17:05:41 +02:00
5e7599756f Updates 2.6.3
+ Added X-Forwarded-Proto for automatic proxy detector
+ Split blacklist and whitelist from geodb script file
+ Optimized compile binary size
+ Added access control to TCP proxy
+ Added "invalid config detect" in up time monitor for isse #7
+ Fixed minor bugs in advance stats panel
+ Reduced file size of embedded materials
2023-06-08 21:42:03 +08:00
5db50c1ca2 Merge pull request #16 from Morethanevil/main-1
Create CHANGELOG.md
2023-06-06 14:38:03 +08:00
884507b45a Create CHANGELOG.md
Create Changelog.md for users to see progress of development
2023-06-05 19:18:28 +02:00
2574d0504e Updates v2.6.2
+ Added advance stats operation tab
+ Added statistic reset #13
+ Added statistic export to csv and json (please use json)
+ Make subdomain clickable (not vdir) #12
+ Added TCP Proxy
+ Updates SMTP setup UI to make it more straight forward to setup
2023-06-04 23:59:56 +08:00
9535abe314 Update README.md 2023-06-03 18:08:36 +08:00
8e6a60f684 Added two screenshots for attractiveness 2023-06-03 18:08:03 +08:00
ead26ea16d Merge pull request #15 from Morethanevil/patch-1
Update README.md
2023-06-03 17:53:23 +08:00
3d66c01d7b Update README.md 2023-06-03 11:13:10 +02:00
138 changed files with 26288 additions and 1126 deletions

46
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Image Publisher
on:
release:
types: [ published ]
jobs:
setup-build-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ${{ github.event.release.tag_name }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Dockerhub
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 \
--build-arg VERSION=${{ github.event.release.tag_name }} \
--provenance=false \
--platform linux/amd64,linux/arm64 \
--tag zoraxydocker/zoraxy:${{ github.event.release.tag_name }} \
.
docker buildx build --push \
--build-arg VERSION=${{ github.event.release.tag_name }} \
--provenance=false \
--platform linux/amd64,linux/arm64 \
--tag zoraxydocker/zoraxy:latest \
.

3
.gitignore vendored
View File

@ -29,3 +29,6 @@ src/Zoraxy_*_*
src/certs/*
src/rules/*
src/README.md
docker/ContainerTester.sh
docker/ImagePublisher.sh
src/mod/acme/test/stackoverflow.pem

79
CHANGELOG.md Normal file
View File

@ -0,0 +1,79 @@
# v2.6.7 Sep 26 2023
+ Added Static Web Server function [#56](https://github.com/tobychui/zoraxy/issues/56)
+ Web Directory Manager (see static webserver tab)
+ Added static web server and black / whitelist template [#38](https://github.com/tobychui/zoraxy/issues/38)
+ Added default / preferred CA features for ACME [#47](https://github.com/tobychui/zoraxy/issues/47)
+ Optimized TLS/SSL page and added dedicated section for ACME related features
+ Bugfixes [#61](https://github.com/tobychui/zoraxy/issues/61) [#58](https://github.com/tobychui/zoraxy/issues/58)
# v2.6.6 Aug 30 2023
+ Added basic auth editor custom exception rules
+ Fixed redirection bug under another reverse proxy and Apache location headers [#39](https://github.com/tobychui/zoraxy/issues/39)
+ Optimized memory usage (from 1.2GB to 61MB for low speed geoip lookup) [#52](https://github.com/tobychui/zoraxy/issues/52)
+ Added unset subdomain custom redirection feature [#46](https://github.com/tobychui/zoraxy/issues/46)
+ Fixed potential security issue in satori/go.uuid [#55](https://github.com/tobychui/zoraxy/issues/55)
+ Added custom acme feature in back-end, thx [@daluntw](https://github.com/daluntw)
+ Added bypass TLS check for custom acme server, thx [@daluntw](https://github.com/daluntw)
+ Introduce new startparameter `-fastgeoip=true`, see [Releases](https://github.com/tobychui/zoraxy/releases/tag/2.6.6)
# v2.6.5.1 Jul 26 2023
+ Patch on memory leaking for Windows netstat module (do not effect any of the previous non Windows builds)
+ Fixed potential memory leak in acme handler logic
+ Added "Do you want to get a TLS certificate for this subdomain?" dialog when a new subdomain proxy rule is created
# v2.6.5 Jul 19 2023
+ Added Import / Export-Feature
+ Moved configurationfiles to a separate folder [#26](https://github.com/tobychui/zoraxy/issues/26)
+ Added auto-renew with ACME [#6](https://github.com/tobychui/zoraxy/issues/6)
+ Fixed Whitelistbug [#18](https://github.com/tobychui/zoraxy/issues/18)
+ Added Whois
# v2.6.4 Jun 15 2023
+ Added force TLS v1.2 above toggle
+ Added trace route
+ Added ICMP ping
+ Added special routing rules module for up-coming acme integration
+ Fixed IPv6 check bug in black/whitelist
+ Optimized UI for TCP Proxy
# v2.6.3 Jun 8 2023
+ Added X-Forwarded-Proto for automatic proxy detector
+ Split blacklist and whitelist from geodb script file
+ Optimized compile binary size
+ Added access control to TCP proxy
+ Added "invalid config detect" in up time monitor for isse [#7](https://github.com/tobychui/zoraxy/issues/7)
+ Fixed minor bugs in advance stats panel
+ Reduced file size of embedded materials
# v2.6.2 Jun 4 2023
+ Added advance stats operation tab
+ Added statistic reset [#13](https://github.com/tobychui/zoraxy/issues/13)
+ Added statistic export to csv and json (please use json)
+ Make subdomain clickable (not vdir) [#12](https://github.com/tobychui/zoraxy/issues/12)
+ Added TCP Proxy
+ Updates SMTP setup UI to make it more straight forward to setup
# v2.6.1 May 31 2023
+ Added reverse proxy TLS skip verification
+ Added basic auth
+ Edit proxy settings
+ Whitelist
+ TCP Proxy (experimental)
+ Info (Utilities page)
# v2.6 May 27 2023
+ Basic auth
+ Support TLS verification skip (for self signed certs)
+ Added trend analysis
+ Added referer and file type analysis
+ Added cert expire day display
+ Moved subdomain proxy logic to dpcore

View File

@ -27,7 +27,7 @@ General purpose request (reverse) proxy and forwarding tool for low power device
- Basic single-admin management mode
- External permission management system for easy system integration
- SMTP config for password reset
## Build from Source
Require Go 1.20 or above
@ -67,11 +67,7 @@ The installation method is same as Linux. If you are using Raspberry Pi 4 or new
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
#### Docker
Thanks for cyb3rdoc and PassiveLemon for providing support over the Docker installation. You can check out their repo over here.
[https://github.com/cyb3rdoc/zoraxy-docker](https://github.com/cyb3rdoc/zoraxy-docker)
[https://github.com/PassiveLemon/zoraxy-docker](https://github.com/PassiveLemon/zoraxy-docker)
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details
### External Permission Management Mode
@ -120,27 +116,13 @@ To start the module, go to System Settings > Modules > Subservice and enable it
![](img/screenshots/0_1.png)
![](img/screenshots/0_2.png)
![](img/screenshots/1.png)
![](img/screenshots/2.png)
More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)!
![](img/screenshots/3.png)
## FAQ
![](img/screenshots/4.png)
![](img/screenshots/5.png)
![](img/screenshots/7.png)
![](img/screenshots/8.png)
![](img/screenshots/9.png)
![](img/screenshots/10_1.png)
![](img/screenshots/10_2.png)
There is a wikipage with [Frequently-Asked-Questions](https://github.com/tobychui/zoraxy/wiki/FAQ---Frequently-Asked-Questions)!
## Global Area Network Controller
@ -175,9 +157,11 @@ Loopback web ssh connection, by default, is disabled. This means that if you are
./zoraxy -sshlb=true
```
## FAQ
- [How to run Zoraxy as system daemon?](https://github.com/tobychui/zoraxy/issues/8#issuecomment-1561539919)
-
## Sponsor This Project
If you like the project and want to support us, please consider a donation. You can use the links below
- [tobychui (Primary author)](https://paypal.me/tobychui)
- PassiveLemon (Docker compatibility maintainer)
## License

38
docker/Dockerfile Normal file
View File

@ -0,0 +1,38 @@
FROM docker.io/golang:alpine
# VERSION comes from the main.yml workflow --build-arg
ARG VERSION
RUN apk add --no-cache bash netcat-openbsd sudo
RUN mkdir -p /opt/zoraxy/source/ &&\
mkdir -p /opt/zoraxy/config/ &&\
mkdir -p /usr/local/bin/
COPY entrypoint.sh /opt/zoraxy/
RUN chmod -R 755 /opt/zoraxy/ &&\
chmod +x /opt/zoraxy/entrypoint.sh
VOLUME [ "/opt/zoraxy/config/" ]
# If you build it yourself, you will need to add the src directory into the docker directory.
COPY ./src/ /opt/zoraxy/source/
WORKDIR /opt/zoraxy/source/
RUN go mod tidy &&\
go build -o /usr/local/bin/zoraxy &&\
rm -r /opt/zoraxy/source/
RUN chmod +x /usr/local/bin/zoraxy
WORKDIR /opt/zoraxy/config/
ENV VERSION=$VERSION
ENV ARGS="-noauth=false"
ENTRYPOINT ["/opt/zoraxy/entrypoint.sh"]
HEALTHCHECK --interval=5s --timeout=5s --retries=2 CMD nc -vz 127.0.0.1 8000 || exit 1

65
docker/README.md Normal file
View File

@ -0,0 +1,65 @@
# [zoraxy](https://github.com/tobychui/zoraxy/) </br>
[![Repo](https://img.shields.io/badge/Docker-Repo-007EC6?labelColor-555555&color-007EC6&logo=docker&logoColor=fff&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Version](https://img.shields.io/docker/v/zoraxydocker/zoraxy/latest?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Size](https://img.shields.io/docker/image-size/zoraxydocker/zoraxy/latest?sort=semver&labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
[![Pulls](https://img.shields.io/docker/pulls/zoraxydocker/zoraxy?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/zoraxydocker/zoraxy)
## Setup: </br>
Although not required, it is recommended to give Zoraxy a dedicated location on the host to mount the container. That way, the host/user can access them whenever needed. A volume will be created automatically within Docker if a location is not specified. </br>
You may also need to portforward your 80/443 to allow http and https traffic. If you are accessing the interface from outside of the local network, you may also need to forward your management port. If you know how to do this, great! If not, find the manufacturer of your router and search on how to do that. There are too many to be listed here. </br>
### Using Docker run </br>
```
docker run -d --name (container name) -p (ports) -v (path to storage directory):/opt/zoraxy/data/ -e ARGS='(your arguments)' zoraxydocker/zoraxy:latest
```
### Using Docker Compose </br>
```yml
version: '3.3'
services:
zoraxy-docker:
image: zoraxydocker/zoraxy:latest
container_name: (container name)
ports:
- 80:80
- 443:443
- (external):8000
volumes:
- (path to storage directory):/opt/zoraxy/config/
environment:
ARGS: '(your arguments)'
```
| Operator | Need | Details |
|:-|:-|:-|
| `-d` | Yes | will run the container in the background. |
| `--name (container name)` | No | Sets the name of the container to the following word. You can change this to whatever you want. |
| `-p (ports)` | Yes | Depending on how your network is setup, you may need to portforward 80, 443, and the management port. |
| `-v (path to storage directory):/opt/zoraxy/config/` | Recommend | Sets the folder that holds your files. This should be the place you just chose. By default, it will create a Docker volume for the files for persistency but they will not be accessible. |
| `-e ARGS='(your arguments)'` | No | Sets the arguments to run Zoraxy with. Enter them as you would normally. By default, it is ran with `-noauth=false` but <b>you cannot change the management port.</b> This is required for the healthcheck to work. |
| `zoraxydocker/zoraxy:latest` | Yes | The repository on Docker hub. By default, it is the latest version that I have published. |
## Examples: </br>
### Docker Run </br>
```
docker run -d --name zoraxy -p 80:80 -p 443:443 -p 8005:8000/tcp -v /home/docker/Containers/Zoraxy:/opt/zoraxy/config/ -e ARGS='-noauth=false' zoraxydocker/zoraxy:latest
```
### Docker Compose </br>
```yml
version: '3.3'
services:
zoraxy-docker:
image: zoraxydocker/zoraxy:latest
container_name: zoraxy
ports:
- 80:80
- 443:443
- 8005:8000/tcp
volumes:
- /home/docker/Containers/Zoraxy:/opt/zoraxy/config/
environment:
ARGS: '-noauth=false'
```

4
docker/entrypoint.sh Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
echo "Zoraxy version $VERSION"
zoraxy -port=:8000 ${ARGS}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 100 KiB

BIN
img/screenshots/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

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

136
src/acme.go Normal file
View File

@ -0,0 +1,136 @@
package main
import (
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"regexp"
"strconv"
"time"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/utils"
)
/*
acme.go
This script handle special routing required for acme auto cert renew functions
*/
// Helper function to generate a random port above a specified value
func getRandomPort(minPort int) int {
return rand.Intn(65535-minPort) + minPort
}
// init the new ACME instance
func initACME() *acme.ACMEHandler {
SystemWideLogger.Println("Starting ACME handler")
rand.Seed(time.Now().UnixNano())
// Generate a random port above 30000
port := getRandomPort(30000)
// Check if the port is already in use
for acme.IsPortInUse(port) {
port = getRandomPort(30000)
}
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
}
// create the special routing rule for ACME
func acmeRegisterSpecialRoutingRule() {
SystemWideLogger.Println("Assigned temporary port:" + acmeHandler.Getport())
err := dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
ID: "acme-autorenew",
MatchRule: func(r *http.Request) bool {
found, _ := regexp.MatchString("/.well-known/acme-challenge/*", r.RequestURI)
return found
},
RoutingHandler: func(w http.ResponseWriter, r *http.Request) {
req, err := http.NewRequest(http.MethodGet, "http://localhost:"+acmeHandler.Getport()+r.RequestURI, nil)
req.Host = r.Host
if err != nil {
fmt.Printf("client: could not create request: %s\n", err)
return
}
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("client: error making http request: %s\n", err)
return
}
resBody, err := io.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
fmt.Printf("error reading: %s\n", err)
return
}
w.Write(resBody)
},
Enabled: true,
UseSystemAccessControl: false,
})
if err != nil {
SystemWideLogger.PrintAndLog("ACME", "Unable register temp port for DNS resolver", err)
}
}
// This function check if the renew setup is satisfied. If not, toggle them automatically
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
isForceHttpsRedirectEnabledOriginally := false
if dynamicProxyRouter.Option.Port == 443 {
//Enable port 80 to 443 redirect
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
} else {
//Set this to true, so after renew, do not turn it off
isForceHttpsRedirectEnabledOriginally = true
}
} else if dynamicProxyRouter.Option.Port == 80 {
//Go ahead
} else {
//This port do not support ACME
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
}
// Pass over to the acmeHandler to deal with the communication
acmeHandler.HandleRenewCertificate(w, r)
if dynamicProxyRouter.Option.Port == 443 {
if !isForceHttpsRedirectEnabledOriginally {
//Default is off. Turn the redirection off
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
}
}
}
// HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation
func HandleACMEPreferredCA(w http.ResponseWriter, r *http.Request) {
ca, err := utils.PostPara(r, "set")
if err != nil {
//Return the current ca to user
prefCA := "Let's Encrypt"
sysdb.Read("acmepref", "prefca", &prefCA)
js, _ := json.Marshal(prefCA)
utils.SendJSONResponse(w, string(js))
} else {
//Check if the CA is supported
acme.IsSupportedCA(ca)
//Set the new config
sysdb.Write("acmepref", "prefca", ca)
SystemWideLogger.Println("Updating prefered ACME CA to " + ca)
utils.SendOK(w)
}
}

View File

@ -3,9 +3,12 @@ package main
import (
"encoding/json"
"net/http"
"net/http/pprof"
"imuslab.com/zoraxy/mod/acme/acmewizard"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/utils"
)
@ -51,12 +54,22 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/tlscheck", 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)
//Reverse proxy root related APIs
authRouter.HandleFunc("/api/proxy/root/listOptions", HandleRootRouteOptionList)
authRouter.HandleFunc("/api/proxy/root/updateOptions", HandleRootRouteOptionsUpdate)
//Reverse proxy auth related APIs
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
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
@ -81,6 +94,11 @@ func initAPIs() {
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
//Path Blocker APIs
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
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
@ -121,9 +139,14 @@ func initAPIs() {
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)
//Network utilities
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession)
authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck)
authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan)
@ -136,8 +159,44 @@ func initAPIs() {
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
http.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/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
//Static Web Server
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
authRouter.HandleFunc("/api/webserv/setPort", staticWebServer.HandlePortChange)
authRouter.HandleFunc("/api/webserv/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)
}
//Others
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
//Debug
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
//If you got APIs to add, append them here
}

View File

@ -6,10 +6,11 @@ import (
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
)
@ -44,6 +45,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
Domain string
LastModifiedDate string
ExpireDate string
RemainingDays int
}
results := []*CertInfo{}
@ -60,6 +62,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
certExpireTime := "Unknown"
certBtyes, err := os.ReadFile(certFilepath)
expiredIn := 0
if err != nil {
//Unable to load this file
continue
@ -70,6 +73,11 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
duration := cert.NotAfter.Sub(time.Now())
// Convert the duration to days
expiredIn = int(duration.Hours() / 24)
}
}
}
@ -78,6 +86,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
Domain: filename,
LastModifiedDate: modifiedTime,
ExpireDate: certExpireTime,
RemainingDays: expiredIn,
}
results = append(results, &thisCertInfo)
@ -99,6 +108,64 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
}
// List all certificates and map all their domains to the cert filename
func handleListDomains(w http.ResponseWriter, r *http.Request) {
filenames, err := os.ReadDir("./conf/certs/")
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
certnameToDomainMap := map[string]string{}
for _, filename := range filenames {
if filename.IsDir() {
continue
}
certFilepath := filepath.Join("./conf/certs/", filename.Name())
certBtyes, err := os.ReadFile(certFilepath)
if err != nil {
// Unable to load this file
SystemWideLogger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
continue
} else {
// Cert loaded. Check its expiry time
block, _ := pem.Decode(certBtyes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
certname := strings.TrimSuffix(filepath.Base(certFilepath), filepath.Ext(certFilepath))
for _, dnsName := range cert.DNSNames {
certnameToDomainMap[dnsName] = certname
}
certnameToDomainMap[cert.Subject.CommonName] = certname
}
}
}
}
requireCompact, _ := utils.GetPara(r, "compact")
if requireCompact == "true" {
result := make(map[string][]string)
for key, value := range certnameToDomainMap {
if _, ok := result[value]; !ok {
result[value] = make([]string, 0)
}
result[value] = append(result[value], key)
}
js, _ := json.Marshal(result)
utils.SendJSONResponse(w, string(js))
return
}
js, _ := json.Marshal(certnameToDomainMap)
utils.SendJSONResponse(w, string(js))
}
// Handle front-end toggling TLS mode
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
currentTlsSetting := false
@ -114,11 +181,11 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
} else {
if newState == "true" {
sysdb.Write("settings", "usetls", true)
log.Println("Enabling TLS mode on reverse proxy")
SystemWideLogger.Println("Enabling TLS mode on reverse proxy")
dynamicProxyRouter.UpdateTLSSetting(true)
} else if newState == "false" {
sysdb.Write("settings", "usetls", false)
log.Println("Disabling TLS mode on reverse proxy")
SystemWideLogger.Println("Disabling TLS mode on reverse proxy")
dynamicProxyRouter.UpdateTLSSetting(false)
} else {
utils.SendErrorResponse(w, "invalid state given. Only support true or false")
@ -130,6 +197,33 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
}
}
// Handle the GET and SET of reverse proxy TLS versions
func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
newState, err := utils.PostPara(r, "set")
if err != nil {
//GET
var reqLatestTLS bool = false
if sysdb.KeyExists("settings", "forceLatestTLS") {
sysdb.Read("settings", "forceLatestTLS", &reqLatestTLS)
}
js, _ := json.Marshal(reqLatestTLS)
utils.SendJSONResponse(w, string(js))
} else {
if newState == "true" {
sysdb.Write("settings", "forceLatestTLS", true)
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
dynamicProxyRouter.UpdateTLSVersion(true)
} else if newState == "false" {
sysdb.Write("settings", "forceLatestTLS", false)
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
dynamicProxyRouter.UpdateTLSVersion(false)
} else {
utils.SendErrorResponse(w, "invalid state given")
}
}
}
// Handle upload of the certificate
func handleCertUpload(w http.ResponseWriter, r *http.Request) {
// check if request method is POST
@ -178,8 +272,8 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
defer file.Close()
// create file in upload directory
os.MkdirAll("./certs", 0775)
f, err := os.Create(filepath.Join("./certs", overWriteFilename))
os.MkdirAll("./conf/certs", 0775)
f, err := os.Create(filepath.Join("./conf/certs", overWriteFilename))
if err != nil {
http.Error(w, "Failed to create file", http.StatusInternalServerError)
return

View File

@ -1,12 +1,16 @@
package main
import (
"archive/zip"
"encoding/json"
"io/ioutil"
"log"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/utils"
@ -20,18 +24,21 @@ import (
*/
type Record struct {
ProxyType string
Rootname string
ProxyTarget string
UseTLS bool
SkipTlsValidation bool
RequireBasicAuth bool
BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials
ProxyType string
Rootname string
ProxyTarget string
UseTLS bool
BypassGlobalTLS bool
SkipTlsValidation bool
RequireBasicAuth bool
BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials
BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule
}
func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
// Save a reverse proxy config record to file
func SaveReverseProxyConfigToFile(proxyConfigRecord *Record) error {
//TODO: Make this accept new def types
os.MkdirAll("conf", 0775)
os.MkdirAll("./conf/proxy/", 0775)
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
//Generate record
@ -39,17 +46,26 @@ func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
//Write to file
js, _ := json.MarshalIndent(thisRecord, "", " ")
return ioutil.WriteFile(filepath.Join("conf", filename), js, 0775)
return os.WriteFile(filepath.Join("./conf/proxy/", filename), js, 0775)
}
func RemoveReverseProxyConfig(rootname string) error {
// Save a running reverse proxy endpoint to file (with automatic endpoint to record conversion)
func SaveReverseProxyEndpointToFile(proxyEndpoint *dynamicproxy.ProxyEndpoint) error {
recordToSave, err := ConvertProxyEndpointToRecord(proxyEndpoint)
if err != nil {
return err
}
return SaveReverseProxyConfigToFile(recordToSave)
}
func RemoveReverseProxyConfigFile(rootname string) error {
filename := getFilenameFromRootName(rootname)
removePendingFile := strings.ReplaceAll(filepath.Join("conf", filename), "\\", "/")
log.Println("Config Removed: ", removePendingFile)
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
SystemWideLogger.Println("Config Removed: ", removePendingFile)
if utils.FileExists(removePendingFile) {
err := os.Remove(removePendingFile)
if err != nil {
log.Println(err.Error())
SystemWideLogger.PrintAndLog("Proxy", "Unabel to remove config file", err)
return err
}
}
@ -60,8 +76,19 @@ func RemoveReverseProxyConfig(rootname string) error {
// Return ptype, rootname and proxyTarget, error if any
func LoadReverseProxyConfig(filename string) (*Record, error) {
thisRecord := Record{}
configContent, err := ioutil.ReadFile(filename)
thisRecord := Record{
ProxyType: "",
Rootname: "",
ProxyTarget: "",
UseTLS: false,
BypassGlobalTLS: false,
SkipTlsValidation: false,
RequireBasicAuth: false,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
}
configContent, err := os.ReadFile(filename)
if err != nil {
return &thisRecord, err
}
@ -76,6 +103,23 @@ func LoadReverseProxyConfig(filename string) (*Record, error) {
return &thisRecord, nil
}
// Convert a running proxy endpoint object into a save-able record struct
func ConvertProxyEndpointToRecord(targetProxyEndpoint *dynamicproxy.ProxyEndpoint) (*Record, error) {
thisProxyConfigRecord := Record{
ProxyType: targetProxyEndpoint.GetProxyTypeString(),
Rootname: targetProxyEndpoint.RootOrMatchingDomain,
ProxyTarget: targetProxyEndpoint.Domain,
UseTLS: targetProxyEndpoint.RequireTLS,
BypassGlobalTLS: targetProxyEndpoint.BypassGlobalTLS,
SkipTlsValidation: targetProxyEndpoint.SkipCertValidations,
RequireBasicAuth: targetProxyEndpoint.RequireBasicAuth,
BasicAuthCredentials: targetProxyEndpoint.BasicAuthCredentials,
BasicAuthExceptionRules: targetProxyEndpoint.BasicAuthExceptionRules,
}
return &thisProxyConfigRecord, nil
}
func getFilenameFromRootName(rootname string) string {
//Generate a filename for this rootname
filename := strings.ReplaceAll(rootname, ".", "_")
@ -83,3 +127,202 @@ func getFilenameFromRootName(rootname string) string {
filename = filename + ".config"
return filename
}
/*
Importer and Exporter of Zoraxy proxy config
*/
func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
includeSysDBRaw, err := utils.GetPara(r, "includeDB")
includeSysDB := false
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/"
// Set the Content-Type header to indicate it's a zip file
w.Header().Set("Content-Type", "application/zip")
// Set the Content-Disposition header to specify the file name
w.Header().Set("Content-Disposition", "attachment; filename=\"config.zip\"")
// Create a zip writer
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
// Walk through the folder and add files to the zip
err = filepath.Walk(folderPath, func(filePath string, fileInfo os.FileInfo, err error) error {
if err != nil {
return err
}
if folderPath == filePath {
//Skip root folder
return nil
}
// Create a new file in the zip
if !utils.IsDir(filePath) {
zipFile, err := zipWriter.Create(filePath)
if err != nil {
return err
}
// Open the file on disk
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
// Copy the file contents to the zip file
_, err = io.Copy(zipFile, file)
if err != nil {
return err
}
}
return nil
})
if includeSysDB {
//Also zip in the sysdb
zipFile, err := zipWriter.Create("sys.db")
if err != nil {
SystemWideLogger.PrintAndLog("Backup", "Unable to zip sysdb", err)
return
}
// Open the file on disk
file, err := os.Open("sys.db")
if err != nil {
SystemWideLogger.PrintAndLog("Backup", "Unable to open sysdb", err)
return
}
defer file.Close()
// Copy the file contents to the zip file
_, err = io.Copy(zipFile, file)
if err != nil {
SystemWideLogger.Println(err)
return
}
//Restore sysdb state
sysdb.ReadOnly = false
}
if err != nil {
// Handle the error and send an HTTP response with the error message
http.Error(w, fmt.Sprintf("Failed to zip folder: %v", err), http.StatusInternalServerError)
return
}
}
func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
// Check if the request is a POST with a file upload
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusBadRequest)
return
}
// Max file size limit (10 MB in this example)
r.ParseMultipartForm(10 << 20)
// Get the uploaded file
file, handler, err := r.FormFile("file")
if err != nil {
http.Error(w, "Failed to retrieve uploaded file", http.StatusInternalServerError)
return
}
defer file.Close()
if filepath.Ext(handler.Filename) != ".zip" {
http.Error(w, "Upload file is not a zip file", http.StatusInternalServerError)
return
}
// Create the target directory to unzip the files
targetDir := "./conf"
if utils.FileExists(targetDir) {
//Backup the old config to old
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
}
err = os.MkdirAll(targetDir, os.ModePerm)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to create target directory: %v", err), http.StatusInternalServerError)
return
}
// Open the zip file
zipReader, err := zip.NewReader(file, handler.Size)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to open zip file: %v", err), http.StatusInternalServerError)
return
}
restoreDatabase := false
// Extract each file from the zip archive
for _, zipFile := range zipReader.File {
// Open the file in the zip archive
rc, err := zipFile.Open()
if err != nil {
http.Error(w, fmt.Sprintf("Failed to open file in zip: %v", err), http.StatusInternalServerError)
return
}
defer rc.Close()
// Create the corresponding file on disk
zipFile.Name = strings.ReplaceAll(zipFile.Name, "../", "")
fmt.Println("Restoring: " + strings.ReplaceAll(zipFile.Name, "\\", "/"))
if zipFile.Name == "sys.db" {
//Sysdb replacement. Close the database and restore
sysdb.Close()
restoreDatabase = true
} else if !strings.HasPrefix(strings.ReplaceAll(zipFile.Name, "\\", "/"), "conf/") {
//Malformed zip file.
http.Error(w, fmt.Sprintf("Invalid zip file structure or version too old"), http.StatusInternalServerError)
return
}
//Check if parent dir exists
if !utils.FileExists(filepath.Dir(zipFile.Name)) {
os.MkdirAll(filepath.Dir(zipFile.Name), 0775)
}
//Create the file
newFile, err := os.Create(zipFile.Name)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to create file: %v", err), http.StatusInternalServerError)
return
}
defer newFile.Close()
// Copy the file contents from the zip to the new file
_, err = io.Copy(newFile, rc)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to extract file from zip: %v", err), http.StatusInternalServerError)
return
}
}
// Send a success response
w.WriteHeader(http.StatusOK)
SystemWideLogger.Println("Configuration restored")
fmt.Fprintln(w, "Configuration restored")
if restoreDatabase {
go func() {
SystemWideLogger.Println("Database altered. Restarting in 3 seconds...")
time.Sleep(3 * time.Second)
os.Exit(0)
}()
}
}

View File

@ -9,7 +9,7 @@ import (
"strings"
"time"
uuid "github.com/satori/go.uuid"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/email"
"imuslab.com/zoraxy/mod/utils"
)
@ -180,7 +180,7 @@ func setSMTPAdminAddress(adminAddr string) error {
return sysdb.Write("smtp", "admin", adminAddr)
}
//Load SMTP admin address. Return empty string if not set
// Load SMTP admin address. Return empty string if not set
func loadSMTPAdminAddr() string {
adminAddr := ""
if sysdb.KeyExists("smtp", "admin") {
@ -223,7 +223,7 @@ func HandleAdminAccountResetEmail(w http.ResponseWriter, r *http.Request) {
return
}
passwordResetAccessToken = uuid.NewV4().String()
passwordResetAccessToken = uuid.New().String()
//SMTP info exists. Send reset account email
lastAccountResetEmail = time.Now().Unix()

View File

@ -1,39 +0,0 @@
package main
import (
"net"
"net/http"
"strings"
"github.com/oschwald/geoip2-golang"
)
func getCountryCodeFromRequest(r *http.Request) string {
countryCode := ""
// Get the IP address of the user from the request headers
ipAddress := r.Header.Get("X-Forwarded-For")
if ipAddress == "" {
ipAddress = strings.Split(r.RemoteAddr, ":")[0]
}
// Open the GeoIP database
db, err := geoip2.Open("./tmp/GeoIP2-Country.mmdb")
if err != nil {
// Handle the error
return countryCode
}
defer db.Close()
// Look up the country code for the IP address
record, err := db.Country(net.ParseIP(ipAddress))
if err != nil {
// Handle the error
return countryCode
}
// Get the ISO country code from the record
countryCode = record.Country.IsoCode
return countryCode
}

View File

@ -4,13 +4,15 @@ go 1.16
require (
github.com/boltdb/bolt v1.3.1
github.com/go-acme/lego/v4 v4.14.0
github.com/go-ping/ping v1.1.0
github.com/google/uuid v1.3.0
github.com/google/uuid v1.3.1
github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.4.2
github.com/gorilla/websocket v1.5.0
github.com/grandcat/zeroconf v1.0.0
github.com/oschwald/geoip2-golang v1.8.0
github.com/satori/go.uuid v1.2.0
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0
github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.25
golang.org/x/net v0.14.0
golang.org/x/sys v0.11.0
golang.org/x/tools v0.12.0 // indirect
)

1758
src/go.sum

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ import (
"time"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/aroz"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/database"
@ -19,8 +20,10 @@ import (
"imuslab.com/zoraxy/mod/email"
"imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"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"
@ -28,19 +31,27 @@ import (
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
"imuslab.com/zoraxy/mod/webserv"
)
// General flags
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 ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
var (
name = "Zoraxy"
version = "2.6.1"
version = "2.6.8"
nodeUUID = "generic"
development = true //Set this to false to use embedded web fs
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()
/*
@ -57,6 +68,7 @@ var (
authAgent *auth.AuthAgent //Authentication agent
tlsCertManager *tlscert.Manager //TLS / SSL management
redirectTable *redirection.RuleTable //Handle special redirection rule sets
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, also handle black list and whitelist features
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
statisticCollector *statistic.Collector //Collecting statistic from visitors
@ -65,10 +77,14 @@ var (
ganManager *ganserv.NetworkManager //Global Area Network Manager
webSshManager *sshprox.Manager //Web SSH connection service
tcpProxyManager *tcpprox.Manager //TCP Proxy Manager
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
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
EmailSender *email.Sender //Email sender that handle email sending
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
SystemWideLogger *logger.Logger //Logger for Zoraxy
)
// Kill signal handler. Do something before the system the core terminate.
@ -77,29 +93,40 @@ func SetupCloseHandler() {
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
fmt.Println("- Shutting down " + name)
fmt.Println("- Closing GeoDB ")
geodbStore.Close()
fmt.Println("- Closing Netstats Listener")
netstatBuffers.Close()
fmt.Println("- Closing Statistic Collector")
statisticCollector.Close()
fmt.Println("- Stopping mDNS Discoverer")
//Stop the mdns service
mdnsTickerStop <- true
mdnsScanner.Close()
//Remove the tmp folder
fmt.Println("- Cleaning up tmp files")
os.RemoveAll("./tmp")
//Close database, final
fmt.Println("- Stopping system database")
sysdb.Close()
ShutdownSeq()
os.Exit(0)
}()
}
func ShutdownSeq() {
fmt.Println("- Shutting down " + name)
fmt.Println("- Closing GeoDB ")
geodbStore.Close()
fmt.Println("- Closing Netstats Listener")
netstatBuffers.Close()
fmt.Println("- Closing Statistic Collector")
statisticCollector.Close()
if mdnsTickerStop != nil {
fmt.Println("- Stopping mDNS Discoverer (might take a few minutes)")
// Stop the mdns service
mdnsTickerStop <- true
}
mdnsScanner.Close()
fmt.Println("- Closing Certificates Auto Renewer")
acmeAutoRenewer.Close()
//Remove the tmp folder
fmt.Println("- Cleaning up tmp files")
os.RemoveAll("./tmp")
fmt.Println("- Closing system wide logger")
SystemWideLogger.Close()
//Close database, final
fmt.Println("- Stopping system database")
sysdb.Close()
}
func main() {
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
@ -130,7 +157,7 @@ func main() {
}
uuidBytes, err := os.ReadFile(uuidRecord)
if err != nil {
log.Println("Unable to read system uuid from file system")
SystemWideLogger.PrintAndLog("ZeroTier", "Unable to read system uuid from file system", nil)
panic(err)
}
nodeUUID = string(uuidBytes)
@ -149,7 +176,10 @@ func main() {
time.Sleep(500 * time.Millisecond)
log.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
//Start the finalize sequences
finalSequence()
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
err = http.ListenAndServe(handler.Port, nil)
if err != nil {

377
src/mod/acme/acme.go Normal file
View File

@ -0,0 +1,377 @@
package acme
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"log"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"imuslab.com/zoraxy/mod/utils"
)
type CertificateInfoJSON struct {
AcmeName string `json:"acme_name"`
AcmeUrl string `json:"acme_url"`
SkipTLS bool `json:"skip_tls"`
}
// ACMEUser represents a user in the ACME system.
type ACMEUser struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
// GetEmail returns the email of the ACMEUser.
func (u *ACMEUser) GetEmail() string {
return u.Email
}
// GetRegistration returns the registration resource of the ACMEUser.
func (u ACMEUser) GetRegistration() *registration.Resource {
return u.Registration
}
// GetPrivateKey returns the private key of the ACMEUser.
func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
return u.key
}
// ACMEHandler handles ACME-related operations.
type ACMEHandler struct {
DefaultAcmeServer string
Port string
}
// NewACME creates a new ACMEHandler instance.
func NewACME(acmeServer string, port string) *ACMEHandler {
return &ACMEHandler{
DefaultAcmeServer: acmeServer,
Port: port,
}
}
// ObtainCert obtains a certificate for the specified domains.
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool) (bool, error) {
log.Println("[ACME] Obtaining certificate...")
// generate private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Println(err)
return false, err
}
// create a admin user for our new generation
adminUser := ACMEUser{
Email: email,
key: privateKey,
}
// create config
config := lego.NewConfig(&adminUser)
// skip TLS verify if need
// Ref: https://github.com/go-acme/lego/blob/6af2c756ac73a9cb401621afca722d0f4112b1b8/lego/client_config.go#L74
if skipTLS {
log.Println("[INFO] Ignore TLS/SSL Verification Error for ACME Server")
config.HTTPClient.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 30 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
// setup the custom ACME url endpoint.
if caUrl != "" {
config.CADirURL = caUrl
}
// if not custom ACME url, load it from ca.json
if caName == "custom" {
log.Println("[INFO] Using Custom ACME " + caUrl + " for CA Directory URL")
} else {
caLinkOverwrite, err := loadCAApiServerFromName(caName)
if err == nil {
config.CADirURL = caLinkOverwrite
log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
} else {
// (caName == "" || caUrl == "") will use default acme
config.CADirURL = a.DefaultAcmeServer
log.Println("[INFO] Using Default ACME " + a.DefaultAcmeServer + " for CA Directory URL")
}
}
config.Certificate.KeyType = certcrypto.RSA2048
client, err := lego.NewClient(config)
if err != nil {
log.Println(err)
return false, err
}
// setup how to receive challenge
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
if err != nil {
log.Println(err)
return false, err
}
// New users will need to register
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Println(err)
return false, err
}
adminUser.Registration = reg
// obtain the certificate
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
}
certificates, err := client.Certificate.Obtain(request)
if err != nil {
log.Println(err)
return false, err
}
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL.
err = os.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
if err != nil {
log.Println(err)
return false, err
}
err = os.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
if err != nil {
log.Println(err)
return false, err
}
// Save certificate's ACME info for renew usage
certInfo := &CertificateInfoJSON{
AcmeName: caName,
AcmeUrl: caUrl,
SkipTLS: skipTLS,
}
certInfoBytes, err := json.Marshal(certInfo)
if err != nil {
log.Println(err)
return false, err
}
err = os.WriteFile("./conf/certs/"+certificateName+".json", certInfoBytes, 0777)
if err != nil {
log.Println(err)
return false, err
}
return true, nil
}
// CheckCertificate returns a list of domains that are in expired certificates.
// It will return all domains that is in expired certificates
// *** if there is a vaild certificate contains the domain and there is a expired certificate contains the same domain
// it will said expired as well!
func (a *ACMEHandler) CheckCertificate() []string {
// read from dir
filenames, err := os.ReadDir("./conf/certs/")
expiredCerts := []string{}
if err != nil {
log.Println(err)
return []string{}
}
for _, filename := range filenames {
certFilepath := filepath.Join("./conf/certs/", filename.Name())
certBytes, err := os.ReadFile(certFilepath)
if err != nil {
// Unable to load this file
continue
} else {
// Cert loaded. Check its expiry time
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
elapsed := time.Since(cert.NotAfter)
if elapsed > 0 {
// if it is expired then add it in
// make sure it's uniqueless
for _, dnsName := range cert.DNSNames {
if !contains(expiredCerts, dnsName) {
expiredCerts = append(expiredCerts, dnsName)
}
}
if !contains(expiredCerts, cert.Subject.CommonName) {
expiredCerts = append(expiredCerts, cert.Subject.CommonName)
}
}
}
}
}
}
return expiredCerts
}
// return the current port number
func (a *ACMEHandler) Getport() string {
return a.Port
}
// contains checks if a string is present in a slice.
func contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
// HandleGetExpiredDomains handles the HTTP GET request to retrieve the list of expired domains.
// It calls the CheckCertificate method to obtain the expired domains and sends a JSON response
// containing the list of expired domains.
func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Request) {
type ExpiredDomains struct {
Domain []string `json:"domain"`
}
info := ExpiredDomains{
Domain: a.CheckCertificate(),
}
js, _ := json.MarshalIndent(info, "", " ")
utils.SendJSONResponse(w, string(js))
}
// HandleRenewCertificate handles the HTTP GET request to renew a certificate for the provided domains.
// It retrieves the domains and filename parameters from the request, calls the ObtainCert method
// to renew the certificate, and sends a JSON response indicating the result of the renewal process.
func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
domainPara, err := utils.PostPara(r, "domains")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
filename, err := utils.PostPara(r, "filename")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
email, err := utils.PostPara(r, "email")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
var caUrl string
ca, err := utils.PostPara(r, "ca")
if err != nil {
log.Println("[INFO] CA not set. Using default")
ca, caUrl = "", ""
}
if ca == "custom" {
caUrl, err = utils.PostPara(r, "caURL")
if err != nil {
log.Println("[INFO] Custom CA set but no URL provide, Using default")
ca, caUrl = "", ""
}
}
if ca == "" {
//default. Use Let's Encrypt
ca = "Let's Encrypt"
}
var skipTLS bool
if skipTLSString, err := utils.PostPara(r, "skipTLS"); err != nil {
skipTLS = false
} else if skipTLSString != "true" {
skipTLS = false
} else {
skipTLS = true
}
domains := strings.Split(domainPara, ",")
result, err := a.ObtainCert(domains, filename, email, ca, caUrl, skipTLS)
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
utils.SendJSONResponse(w, strconv.FormatBool(result))
}
// Escape JSON string
func jsonEscape(i string) string {
b, err := json.Marshal(i)
if err != nil {
log.Println("Unable to escape json data: " + err.Error())
return i
}
s := string(b)
return s[1 : len(s)-1]
}
// Helper function to check if a port is in use
func IsPortInUse(port int) bool {
address := fmt.Sprintf(":%d", port)
listener, err := net.Listen("tcp", address)
if err != nil {
return true // Port is in use
}
defer listener.Close()
return false // Port is not in use
}
// Load cert information from json file
func loadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
certInfoBytes, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
certInfo := &CertificateInfoJSON{}
if err = json.Unmarshal(certInfoBytes, certInfo); err != nil {
return nil, err
}
return certInfo, nil
}

24
src/mod/acme/acme_test.go Normal file
View File

@ -0,0 +1,24 @@
package acme_test
import (
"fmt"
"testing"
"imuslab.com/zoraxy/mod/acme"
)
// Test if the issuer extraction is working
func TestExtractIssuerNameFromPEM(t *testing.T) {
pemFilePath := "test/stackoverflow.pem"
expectedIssuer := "Let's Encrypt"
issuerName, err := acme.ExtractIssuerNameFromPEM(pemFilePath)
fmt.Println(issuerName)
if err != nil {
t.Errorf("Error extracting issuer name: %v", err)
}
if issuerName != expectedIssuer {
t.Errorf("Unexpected issuer name. Expected: %s, Got: %s", expectedIssuer, issuerName)
}
}

View File

@ -0,0 +1,163 @@
package acmewizard
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"strconv"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
)
/*
ACME Wizard
This wizard help validate the acme settings and configurations
*/
func HandleGuidedStepCheck(w http.ResponseWriter, r *http.Request) {
stepNoStr, err := utils.GetPara(r, "step")
if err != nil {
utils.SendErrorResponse(w, "invalid step number given")
return
}
stepNo, err := strconv.Atoi(stepNoStr)
if err != nil {
utils.SendErrorResponse(w, "invalid step number given")
return
}
if stepNo == 1 {
isListening, err := isLocalhostListening()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(isListening)
utils.SendJSONResponse(w, string(js))
} else if stepNo == 2 {
publicIp, err := getPublicIPAddress()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
publicIp = strings.TrimSpace(publicIp)
httpServerReachable := isHTTPServerAvailable(publicIp)
js, _ := json.Marshal(httpServerReachable)
utils.SendJSONResponse(w, string(js))
} else if stepNo == 3 {
domain, err := utils.GetPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "domain cannot be empty")
return
}
domain = strings.TrimSpace(domain)
//Check if the domain is reachable
reachable := isDomainReachable(domain)
if !reachable {
utils.SendErrorResponse(w, "domain is not reachable")
return
}
//Check http is setup correctly
httpServerReachable := isHTTPServerAvailable(domain)
js, _ := json.Marshal(httpServerReachable)
utils.SendJSONResponse(w, string(js))
} else {
utils.SendErrorResponse(w, "invalid step number")
}
}
// Step 1
func isLocalhostListening() (isListening bool, err error) {
timeout := 2 * time.Second
isListening = false
// Check if localhost is listening on port 80 (HTTP)
conn, err := net.DialTimeout("tcp", "localhost:80", timeout)
if err == nil {
isListening = true
conn.Close()
}
// Check if localhost is listening on port 443 (HTTPS)
conn, err = net.DialTimeout("tcp", "localhost:443", timeout)
if err == nil {
isListening = true
conn.Close()
}
if isListening {
return true, nil
}
return isListening, err
}
// Step 2
func getPublicIPAddress() (string, error) {
resp, err := http.Get("http://checkip.amazonaws.com/")
if err != nil {
return "", err
}
defer resp.Body.Close()
ip, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(ip), nil
}
func isHTTPServerAvailable(ipAddress string) bool {
client := http.Client{
Timeout: 5 * time.Second, // Timeout for the HTTP request
}
urls := []string{
"http://" + ipAddress + ":80",
"https://" + ipAddress + ":443",
}
for _, url := range urls {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println(err, url)
continue // Ignore invalid URLs
}
// Disable TLS verification to handle invalid certificates
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
resp, err := client.Do(req)
if err == nil {
resp.Body.Close()
return true // HTTP server is available
}
}
return false // HTTP server is not available
}
// Step 3
func isDomainReachable(domain string) bool {
_, err := net.LookupHost(domain)
if err != nil {
return false // Domain is not reachable
}
return true // Domain is reachable
}

375
src/mod/acme/autorenew.go Normal file
View File

@ -0,0 +1,375 @@
package acme
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"net/mail"
"os"
"path/filepath"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
)
/*
autorenew.go
This script handle auto renew
*/
type AutoRenewConfig struct {
Enabled bool //Automatic renew is enabled
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
}
type AutoRenewer struct {
ConfigFilePath string
CertFolder string
AcmeHandler *ACMEHandler
RenewerConfig *AutoRenewConfig
RenewTickInterval int64
TickerstopChan chan bool
}
type ExpiredCerts struct {
Domains []string
Filepath string
}
// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
// Set renew check interval to 0 for auto (1 day)
func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, AcmeHandler *ACMEHandler) (*AutoRenewer, error) {
if renewCheckInterval == 0 {
renewCheckInterval = 86400 //1 day
}
//Load the config file. If not found, create one
if !utils.FileExists(config) {
//Create one
os.MkdirAll(filepath.Dir(config), 0775)
newConfig := AutoRenewConfig{
RenewAll: true,
FilesToRenew: []string{},
}
js, _ := json.MarshalIndent(newConfig, "", " ")
err := os.WriteFile(config, js, 0775)
if err != nil {
return nil, errors.New("Failed to create acme auto renewer config: " + err.Error())
}
}
renewerConfig := AutoRenewConfig{}
content, err := os.ReadFile(config)
if err != nil {
return nil, errors.New("Failed to open acme auto renewer config: " + err.Error())
}
err = json.Unmarshal(content, &renewerConfig)
if err != nil {
return nil, errors.New("Malformed acme config file: " + err.Error())
}
//Create an Auto renew object
thisRenewer := AutoRenewer{
ConfigFilePath: config,
CertFolder: certFolder,
AcmeHandler: AcmeHandler,
RenewerConfig: &renewerConfig,
RenewTickInterval: renewCheckInterval,
}
if thisRenewer.RenewerConfig.Enabled {
//Start the renew ticker
thisRenewer.StartAutoRenewTicker()
//Check and renew certificate on startup
go thisRenewer.CheckAndRenewCertificates()
}
return &thisRenewer, nil
}
func (a *AutoRenewer) StartAutoRenewTicker() {
//Stop the previous ticker if still running
if a.TickerstopChan != nil {
a.TickerstopChan <- true
}
time.Sleep(1 * time.Second)
ticker := time.NewTicker(time.Duration(a.RenewTickInterval) * time.Second)
done := make(chan bool)
//Start the ticker to check and renew every x seconds
go func(a *AutoRenewer) {
for {
select {
case <-done:
return
case <-ticker.C:
log.Println("Check and renew certificates in progress")
a.CheckAndRenewCertificates()
}
}
}(a)
a.TickerstopChan = done
}
func (a *AutoRenewer) StopAutoRenewTicker() {
if a.TickerstopChan != nil {
a.TickerstopChan <- true
}
a.TickerstopChan = nil
}
// Handle update auto renew domains
// Set opr for different mode of operations
// opr = setSelected -> Enter a list of file names (or matching rules) for auto renew
// opr = setAuto -> Set to use auto detect certificates and renew
func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
opr, err := utils.GetPara(r, "opr")
if err != nil {
utils.SendErrorResponse(w, "Operation not set")
return
}
if opr == "setSelected" {
files, err := utils.PostPara(r, "domains")
if err != nil {
utils.SendErrorResponse(w, "Domains is not defined")
return
}
//Parse it int array of string
matchingRuleFiles := []string{}
err = json.Unmarshal([]byte(files), &matchingRuleFiles)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Update the configs
a.RenewerConfig.RenewAll = false
a.RenewerConfig.FilesToRenew = matchingRuleFiles
a.saveRenewConfigToFile()
utils.SendOK(w)
} else if opr == "setAuto" {
a.RenewerConfig.RenewAll = true
a.saveRenewConfigToFile()
utils.SendOK(w)
}
}
// if auto renew all is true (aka auto scan), it will return []string{"*"}
func (a *AutoRenewer) HandleLoadAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
results := []string{}
if a.RenewerConfig.RenewAll {
//Auto pick which cert to renew.
results = append(results, "*")
} else {
//Manually set the files to renew
results = a.RenewerConfig.FilesToRenew
}
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
}
func (a *AutoRenewer) HandleRenewPolicy(w http.ResponseWriter, r *http.Request) {
//Load the current value
js, _ := json.Marshal(a.RenewerConfig.RenewAll)
utils.SendJSONResponse(w, string(js))
}
func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
renewedDomains, err := a.CheckAndRenewCertificates()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
message := "Domains renewed"
if len(renewedDomains) == 0 {
message = ("All certificates are up-to-date!")
} else {
message = ("The following domains have been renewed: " + strings.Join(renewedDomains, ","))
}
js, _ := json.Marshal(message)
utils.SendJSONResponse(w, string(js))
}
func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Request) {
val, err := utils.PostPara(r, "enable")
if err != nil {
js, _ := json.Marshal(a.RenewerConfig.Enabled)
utils.SendJSONResponse(w, string(js))
} else {
if val == "true" {
//Check if the email is not empty
if a.RenewerConfig.Email == "" {
utils.SendErrorResponse(w, "Email is not set")
return
}
a.RenewerConfig.Enabled = true
a.saveRenewConfigToFile()
log.Println("[ACME] ACME auto renew enabled")
a.StartAutoRenewTicker()
} else {
a.RenewerConfig.Enabled = false
a.saveRenewConfigToFile()
log.Println("[ACME] ACME auto renew disabled")
a.StopAutoRenewTicker()
}
}
}
func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
email, err := utils.PostPara(r, "set")
if err != nil {
//Return the current email to user
js, _ := json.Marshal(a.RenewerConfig.Email)
utils.SendJSONResponse(w, string(js))
} else {
//Check if the email is valid
_, err := mail.ParseAddress(email)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Set the new config
a.RenewerConfig.Email = email
a.saveRenewConfigToFile()
}
}
// Check and renew certificates. This check all the certificates in the
// certificate folder and return a list of certs that is renewed in this call
// Return string array with length 0 when no cert is expired
func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
certFolder := a.CertFolder
files, err := os.ReadDir(certFolder)
if err != nil {
log.Println("Unable to renew certificates: " + err.Error())
return []string{}, err
}
expiredCertList := []*ExpiredCerts{}
if a.RenewerConfig.RenewAll {
//Scan and renew all
for _, file := range files {
if filepath.Ext(file.Name()) == ".crt" || filepath.Ext(file.Name()) == ".pem" {
//This is a public key file
certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
if err != nil {
continue
}
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
//This cert is expired
DNSName, err := ExtractDomains(certBytes)
if err != nil {
//Maybe self signed. Ignore this
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
continue
}
expiredCertList = append(expiredCertList, &ExpiredCerts{
Filepath: filepath.Join(certFolder, file.Name()),
Domains: DNSName,
})
}
}
}
} else {
//Only renew those in the list
for _, file := range files {
fileName := file.Name()
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
if contains(a.RenewerConfig.FilesToRenew, certName) {
//This is the one to auto renew
certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
if err != nil {
continue
}
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
//This cert is expired
DNSName, err := ExtractDomains(certBytes)
if err != nil {
//Maybe self signed. Ignore this
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
continue
}
expiredCertList = append(expiredCertList, &ExpiredCerts{
Filepath: filepath.Join(certFolder, file.Name()),
Domains: DNSName,
})
}
}
}
}
return a.renewExpiredDomains(expiredCertList)
}
func (a *AutoRenewer) Close() {
if a.TickerstopChan != nil {
a.TickerstopChan <- true
}
}
// Renew the certificate by filename extract all DNS name from the
// certificate and renew them one by one by calling to the acmeHandler
func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
renewedCertFiles := []string{}
for _, expiredCert := range certs {
log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
fileName := filepath.Base(expiredCert.Filepath)
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
// Load certificate info for ACME detail
certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
certInfo, err := loadCertInfoJSON(certInfoFilename)
if err != nil {
log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, trying org section as ca", certName, err)
if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil {
log.Printf("extract issuer name for cert error: %v, using default ca", extractErr)
certInfo = &CertificateInfoJSON{}
} else {
certInfo = &CertificateInfoJSON{AcmeName: CAName}
}
}
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS)
if err != nil {
log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
} else {
log.Println("Successfully renewed " + filepath.Base(expiredCert.Filepath))
renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
}
}
return renewedCertFiles, nil
}
// Write the current renewer config to file
func (a *AutoRenewer) saveRenewConfigToFile() error {
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
return os.WriteFile(a.ConfigFilePath, js, 0775)
}

56
src/mod/acme/ca.go Normal file
View File

@ -0,0 +1,56 @@
package acme
/*
CA.go
This script load CA defination from embedded ca.json
*/
import (
_ "embed"
"encoding/json"
"errors"
"log"
"strings"
)
// CA Defination, load from embeded json when startup
type CaDef struct {
Production map[string]string
Test map[string]string
}
//go:embed ca.json
var caJson []byte
var caDef CaDef = CaDef{}
func init() {
runtimeCaDef := CaDef{}
err := json.Unmarshal(caJson, &runtimeCaDef)
if err != nil {
log.Println("[ERR] Unable to unmarshal CA def from embedded file. You sure your ca.json is valid?")
return
}
caDef = runtimeCaDef
}
// Get the CA ACME server endpoint and error if not found
func loadCAApiServerFromName(caName string) (string, error) {
// handle BuyPass cert org section (Buypass AS-983163327)
if strings.HasPrefix(caName, "Buypass AS") {
caName = "Buypass"
}
val, ok := caDef.Production[caName]
if !ok {
return "", errors.New("This CA is not supported")
}
return val, nil
}
func IsSupportedCA(caName string) bool {
_, err := loadCAApiServerFromName(caName)
return err == nil
}

15
src/mod/acme/ca.json Normal file
View File

@ -0,0 +1,15 @@
{
"production": {
"Let's Encrypt": "https://acme-v02.api.letsencrypt.org/directory",
"Buypass": "https://api.buypass.com/acme/directory",
"ZeroSSL": "https://acme.zerossl.com/v2/DV90",
"Google": "https://dv.acme-v02.api.pki.goog/directory"
},
"test":{
"Let's Encrypt": "https://acme-staging-v02.api.letsencrypt.org/directory",
"Buypass": "https://api.test4.buypass.no/acme/directory",
"Google": "https://dv.acme-v02.test-api.pki.goog/directory"
}
}

99
src/mod/acme/utils.go Normal file
View File

@ -0,0 +1,99 @@
package acme
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"time"
)
// Get the issuer name from pem file
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
// Read the PEM file
pemData, err := ioutil.ReadFile(pemFilePath)
if err != nil {
return "", err
}
return ExtractIssuerName(pemData)
}
// Get the DNSName in the cert
func ExtractDomains(certBytes []byte) ([]string, error) {
domains := []string{}
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return []string{}, err
}
for _, dnsName := range cert.DNSNames {
if !contains(domains, dnsName) {
domains = append(domains, dnsName)
}
}
return domains, nil
}
return []string{}, errors.New("decode cert bytes failed")
}
func ExtractIssuerName(certBytes []byte) (string, error) {
// Parse the PEM block
block, _ := pem.Decode(certBytes)
if block == nil || block.Type != "CERTIFICATE" {
return "", fmt.Errorf("failed to decode PEM block containing certificate")
}
// Parse the certificate
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", fmt.Errorf("failed to parse certificate: %v", err)
}
// Check if exist incase some acme server didn't have org section
if len(cert.Issuer.Organization) == 0 {
return "", fmt.Errorf("cert didn't have org section exist")
}
// Extract the issuer name
issuer := cert.Issuer.Organization[0]
return issuer, nil
}
// Check if a cert is expired by public key
func CertIsExpired(certBytes []byte) bool {
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
elapsed := time.Since(cert.NotAfter)
if elapsed > 0 {
// if it is expired then add it in
// make sure it's uniqueless
return true
}
}
}
return false
}
func CertExpireSoon(certBytes []byte) bool {
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
expirationDate := cert.NotAfter
threshold := 14 * 24 * time.Hour // 14 days
timeRemaining := time.Until(expirationDate)
if timeRemaining <= threshold {
return true
}
}
}
return false
}

View File

@ -1,8 +1,13 @@
package dynamicproxy
import (
_ "embed"
"errors"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/geodb"
@ -21,37 +26,39 @@ import (
- Vitrual Directory Routing
*/
var (
//go:embed tld.json
rawTldMap []byte
)
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
Special Routing Rules, bypass most of the limitations
*/
//Check if there are external routing rule matches.
//If yes, route them via external rr
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil {
//Matching routing rule found. Let the sub-router handle it
if matchedRoutingRule.UseSystemAccessControl {
//This matching rule request system access control.
//check access logic
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
}
matchedRoutingRule.Route(w, r)
return
}
/*
General Access Check
*/
//Check if this ip is in blacklist
clientIpAddr := geodb.GetRequesterIP(r)
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile("./web/forbidden.html")
if err != nil {
w.Write([]byte("403 - Forbidden"))
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "blacklist", "")
return
}
//Check if this ip is in whitelist
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile("./web/forbidden.html")
if err != nil {
w.Write([]byte("403 - Forbidden"))
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "whitelist", "")
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
@ -65,15 +72,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
//Check if there are external routing rule matches.
//If yes, route them via external rr
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil {
//Matching routing rule found. Let the sub-router handle it
matchedRoutingRule.Route(w, r)
return
}
//Extract request host to see if it is virtual directory or subdomain
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
@ -115,16 +113,150 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.proxyRequest(w, r, targetProxyEndpoint)
} else if !strings.HasSuffix(proxyingPath, "/") {
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil {
//Missing tailing slash. Redirect to target proxy endpoint
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
} else {
//Passthrough the request to root
h.proxyRequest(w, r, h.Parent.Root)
h.handleRootRouting(w, r)
}
} else {
//No routing rules found. Route to root.
//No routing rules found.
h.handleRootRouting(w, r)
}
}
/*
handleRootRouting
This function handle root routing 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
for the routing logic.
*/
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
if h.Parent.RootRoutingOptions.EnableRedirectForUnsetRules {
//Route to custom domain
if h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget == "" {
//Not set. Redirect to first level of domain redirectable
fld, err := h.getTopLevelRedirectableDomain(domainOnly)
if err != nil {
//Redirect to proxy root
h.proxyRequest(w, r, h.Parent.Root)
} else {
log.Println("[Router] Redirecting request from " + domainOnly + " to " + fld)
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, fld, http.StatusTemporaryRedirect)
}
return
} else if h.isTopLevelRedirectableDomain(domainOnly) {
//This is requesting a top level private domain that should be serving root
h.proxyRequest(w, r, h.Parent.Root)
} else {
//Validate the redirection target URL
parsedURL, err := url.Parse(h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget)
if err != nil {
//Error when parsing target. Send to root
h.proxyRequest(w, r, h.Parent.Root)
return
}
hostname := parsedURL.Hostname()
if domainOnly != hostname {
//Redirect to target
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget, http.StatusTemporaryRedirect)
return
} else {
//Loopback request due to bad settings (Shd leave it empty)
//Forward it to root proxy
h.proxyRequest(w, r, h.Parent.Root)
}
}
} else {
//Route to root
h.proxyRequest(w, r, h.Parent.Root)
}
}
// Handle access routing logic. Return true if the request is handled or blocked by the access control logic
// if the return value is false, you can continue process the response writer
func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Request) bool {
//Check if this ip is in blacklist
clientIpAddr := geodb.GetRequesterIP(r)
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/blacklist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "blacklist", "")
return true
}
//Check if this ip is in whitelist
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/whitelist.html"))
if err != nil {
w.Write(page_forbidden)
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "whitelist", "")
return true
}
return false
}
// Return if the given host is already topped (e.g. example.com or example.co.uk) instead of
// a host with subdomain (e.g. test.example.com)
func (h *ProxyHandler) isTopLevelRedirectableDomain(requestHost string) bool {
parts := strings.Split(requestHost, ".")
if len(parts) > 2 {
//Cases where strange tld is used like .co.uk or .com.hk
_, ok := h.Parent.tldMap[strings.Join(parts[1:], ".")]
if ok {
//Already topped
return true
}
} else {
//Already topped
return true
}
return false
}
// GetTopLevelRedirectableDomain returns the toppest level of domain
// that is redirectable. E.g. a.b.c.example.co.uk will return example.co.uk
func (h *ProxyHandler) getTopLevelRedirectableDomain(unsetSubdomainHost string) (string, error) {
parts := strings.Split(unsetSubdomainHost, ".")
if h.isTopLevelRedirectableDomain(unsetSubdomainHost) {
//Already topped
return "", errors.New("already at top level domain")
}
for i := 0; i < len(parts); i++ {
possibleTld := parts[i:]
_, ok := h.Parent.tldMap[strings.Join(possibleTld, ".")]
if ok {
//This is tld length
tld := strings.Join(parts[i-1:], ".")
return "//" + tld, nil
}
}
return "", errors.New("unsupported top level domain given")
}

View File

@ -3,6 +3,7 @@ package dynamicproxy
import (
"errors"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/auth"
)
@ -15,6 +16,16 @@ import (
*/
func (h *ProxyHandler) handleBasicAuthRouting(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
}
}
}
proxyType := "vdir-auth"
if pe.ProxyType == ProxyType_Subdomain {
proxyType = "subd-auth"

View File

@ -14,10 +14,6 @@ import (
var onExitFlushLoop func()
const (
defaultTimeout = time.Minute * 5
)
// ReverseProxy is an HTTP Handler that takes an incoming request and
// sends it to another server, proxying the response back to the
// client, support http, also support https tunnel using http.hijacker
@ -91,11 +87,12 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
//Hack the default transporter to handle more connections
thisTransporter := http.DefaultTransport
thisTransporter.(*http.Transport).MaxIdleConns = 3000
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = 3000
thisTransporter.(*http.Transport).IdleConnTimeout = 10 * time.Second
thisTransporter.(*http.Transport).MaxConnsPerHost = 0
//thisTransporter.(*http.Transport).DisableCompression = true
optimalConcurrentConnection := 32
thisTransporter.(*http.Transport).MaxIdleConns = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = optimalConcurrentConnection
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).DisableCompression = true
if ignoreTLSVerification {
//Ignore TLS certificate validation error
@ -278,6 +275,12 @@ func addXForwardedForHeader(req *http.Request) {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
req.Header.Set("X-Forwarded-For", clientIP)
if req.TLS != nil {
req.Header.Set("X-Forwarded-Proto", "https")
} else {
req.Header.Set("X-Forwarded-Proto", "http")
}
}
}
@ -351,11 +354,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
//Custom header rewriter functions
if res.Header.Get("Location") != "" {
/*
fmt.Println(">>> REQ", req)
fmt.Println(">>> OUTR", outreq)
fmt.Println(">>> RESP", res)
*/
locationRewrite := res.Header.Get("Location")
originLocation := res.Header.Get("Location")
res.Header.Set("zr-origin-location", originLocation)
@ -363,12 +361,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") {
//Full path
//Replace the forwarded target with expected Host
lr, err := replaceLocationHost(locationRewrite, rrr.OriginalHost, req.TLS != nil)
lr, err := replaceLocationHost(locationRewrite, rrr, req.TLS != nil)
if err == nil {
locationRewrite = lr
}
//locationRewrite = strings.ReplaceAll(locationRewrite, rrr.ProxyDomain, rrr.OriginalHost)
//locationRewrite = strings.ReplaceAll(locationRewrite, domainWithoutPort, rrr.OriginalHost)
} else if strings.HasPrefix(originLocation, "/") && rrr.PathPrefix != "" {
//Back to the root of this proxy object
//fmt.Println(rrr.ProxyDomain, rrr.OriginalHost)
@ -381,6 +377,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
//Custom redirection to this rproxy relative path
res.Header.Set("Location", locationRewrite)
}
// Copy header from response to client.
copyHeader(rw.Header(), res.Header)

View File

@ -0,0 +1,49 @@
package dpcore_test
import (
"testing"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
)
func TestReplaceLocationHost(t *testing.T) {
urlString := "http://private.com/test/newtarget/"
rrr := &dpcore.ResponseRewriteRuleSet{
OriginalHost: "test.example.com",
ProxyDomain: "private.com/test",
UseTLS: true,
}
useTLS := true
expectedResult := "https://test.example.com/newtarget/"
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
if err != nil {
t.Errorf("Error occurred: %v", err)
}
if result != expectedResult {
t.Errorf("Expected: %s, but got: %s", expectedResult, result)
}
}
func TestReplaceLocationHostRelative(t *testing.T) {
urlString := "api/"
rrr := &dpcore.ResponseRewriteRuleSet{
OriginalHost: "test.example.com",
ProxyDomain: "private.com/test",
UseTLS: true,
}
useTLS := true
expectedResult := "https://test.example.com/api/"
result, err := dpcore.ReplaceLocationHost(urlString, rrr, useTLS)
if err != nil {
t.Errorf("Error occurred: %v", err)
}
if result != expectedResult {
t.Errorf("Expected: %s, but got: %s", expectedResult, result)
}
}

View File

@ -2,20 +2,61 @@ package dpcore
import (
"net/url"
"strings"
)
func replaceLocationHost(urlString string, newHost string, useTLS bool) (string, error) {
// replaceLocationHost rewrite the backend server's location header to a new URL based on the given proxy rules
// If you have issues with tailing slash, you can try to fix them here (and remember to PR :D )
func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
u, err := url.Parse(urlString)
if err != nil {
return "", err
}
//Update the schemetic if the proxying target is http
//but exposed as https to the internet via Zoraxy
if useTLS {
u.Scheme = "https"
} else {
u.Scheme = "http"
}
u.Host = newHost
//Issue #39: Check if it is location target match the proxying domain
//E.g. Proxy config: blog.example.com -> example.com/blog
//Check if it is actually redirecting to example.com instead of a new domain
//like news.example.com.
// The later check bypass apache screw up method of redirection header
// e.g. https://imuslab.com -> http://imuslab.com:443
if rrr.ProxyDomain != u.Host && !strings.Contains(u.Host, rrr.OriginalHost+":") {
//New location domain not matching proxy target domain.
//Do not modify location header
return urlString, nil
}
u.Host = rrr.OriginalHost
if strings.Contains(rrr.ProxyDomain, "/") {
//The proxy domain itself seems contain subpath.
//Trim it off from Location header to prevent URL segment duplicate
//E.g. Proxy config: blog.example.com -> example.com/blog
//Location Header: /blog/post?id=1
//Expected Location Header send to client:
// blog.example.com/post?id=1 instead of blog.example.com/blog/post?id=1
ProxyDomainURL := "http://" + rrr.ProxyDomain
if rrr.UseTLS {
ProxyDomainURL = "https://" + rrr.ProxyDomain
}
ru, err := url.Parse(ProxyDomainURL)
if err == nil {
//Trim off the subpath
u.Path = strings.TrimPrefix(u.Path, ru.Path)
}
}
return u.String(), nil
}
// Debug functions
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
return replaceLocationHost(urlString, rrr, useTLS)
}

View File

@ -3,6 +3,7 @@ package dynamicproxy
import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"log"
"net/http"
@ -29,12 +30,19 @@ func NewDynamicProxy(option RouterOption) (*Router, error) {
Running: false,
server: nil,
routingRules: []*RoutingRule{},
tldMap: map[string]int{},
}
thisRouter.mux = &ProxyHandler{
Parent: &thisRouter,
}
//Prase the tld map for tld redirection in main router
//See Server.go declarations
if len(rawTldMap) > 0 {
json.Unmarshal(rawTldMap, &thisRouter.tldMap)
}
return &thisRouter, nil
}
@ -45,6 +53,19 @@ func (router *Router) UpdateTLSSetting(tlsEnabled bool) {
router.Restart()
}
// Update TLS Version in runtime. Will restart proxy server if running.
// Set this to true to force TLS 1.2 or above
func (router *Router) UpdateTLSVersion(requireLatest bool) {
router.Option.ForceTLSLatest = requireLatest
router.Restart()
}
// Update port 80 listener state
func (router *Router) UpdatePort80ListenerState(useRedirect bool) {
router.Option.ListenOnPort80 = useRedirect
router.Restart()
}
// Update https redirect, which will require updates
func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
router.Option.ForceHttpsRedirect = useRedirect
@ -58,36 +79,95 @@ func (router *Router) StartProxyService() error {
return errors.New("Reverse proxy server already running")
}
//Check if root route is set
if router.Root == nil {
return errors.New("Reverse proxy router root not set")
}
//Load root options from file
loadedRootOption, err := loadRootRoutingOptionsFromFile()
if err != nil {
return err
}
router.RootRoutingOptions = loadedRootOption
minVersion := tls.VersionTLS10
if router.Option.ForceTLSLatest {
minVersion = tls.VersionTLS12
}
config := &tls.Config{
GetCertificate: router.Option.TlsManager.GetCert,
MinVersion: uint16(minVersion),
}
if router.Option.UseTls {
//Serve with TLS mode
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
if err != nil {
log.Println(err)
router.Running = false
return err
/*
//Serve with TLS mode
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
if err != nil {
log.Println(err)
router.Running = false
return err
}
router.tlsListener = ln
*/
router.server = &http.Server{
Addr: ":" + strconv.Itoa(router.Option.Port),
Handler: router.mux,
TLSConfig: config,
}
router.tlsListener = ln
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
router.Running = true
if router.Option.Port != 80 && router.Option.ForceHttpsRedirect {
if router.Option.Port != 80 && router.Option.ListenOnPort80 {
//Add a 80 to 443 redirector
httpServer := &http.Server{
Addr: ":80",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
protocol := "https://"
if router.Option.Port == 443 {
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
//Check if the domain requesting allow non TLS mode
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0]
}
sep := router.getSubdomainProxyEndpointFromHostname(domainOnly)
if sep != nil && sep.BypassGlobalTLS {
//Allow routing via non-TLS handler
originalHostHeader := r.Host
if r.URL != nil {
r.Host = r.URL.Host
} else {
//Fallback when the upstream proxy screw something up in the header
r.URL, _ = url.Parse(originalHostHeader)
}
sep.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: sep.Domain,
OriginalHost: originalHostHeader,
UseTLS: sep.RequireTLS,
PathPrefix: "",
})
return
}
if router.Option.ForceHttpsRedirect {
//Redirect to https is enabled
protocol := "https://"
if router.Option.Port == 443 {
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
} else {
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
}
} else {
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
//Do not do redirection
if sep != nil {
//Sub-domain exists but not allow non-TLS access
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("400 - Bad Request"))
} else {
//No defined sub-domain
http.NotFound(w, r)
}
}
}),
@ -115,7 +195,7 @@ func (router *Router) StartProxyService() error {
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
//Unable to startup port 80 listener. Handle shutdown process gracefully
stopChan <- true
log.Fatalf("Could not start server: %v\n", err)
log.Fatalf("Could not start redirection server: %v\n", err)
}
}()
router.tlsRedirectStop = stopChan
@ -124,8 +204,8 @@ func (router *Router) StartProxyService() error {
//Start the TLS server
log.Println("Reverse proxy service started in the background (TLS mode)")
go func() {
if err := router.server.Serve(ln); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not start server: %v\n", err)
if err := router.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not start proxy server: %v\n", err)
}
}()
} else {
@ -171,18 +251,22 @@ func (router *Router) StopProxyService() error {
}
// Restart the current router if it is running.
// Startup the server if it is not running initially
func (router *Router) Restart() error {
//Stop the router if it is already running
var err error = nil
if router.Running {
err := router.StopProxyService()
if err != nil {
return err
}
// Start the server
err = router.StartProxyService()
if err != nil {
return err
}
}
//Start the server
err := router.StartProxyService()
return err
}
@ -230,14 +314,15 @@ func (router *Router) AddVirtualDirectoryProxyService(options *VdirOptions) erro
proxy := dpcore.NewDynamicProxyCore(path, options.RootName, options.SkipCertValidations)
endpointObject := ProxyEndpoint{
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: options.RootName,
Domain: domain,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
Proxy: proxy,
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: options.RootName,
Domain: domain,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
Proxy: proxy,
}
router.ProxyEndpoints.Store(options.RootName, &endpointObject)
@ -255,46 +340,24 @@ func (router *Router) LoadProxy(ptype string, key string) (*ProxyEndpoint, error
if !ok {
return nil, errors.New("target proxy not found")
}
return proxy.(*ProxyEndpoint), nil
targetProxy := proxy.(*ProxyEndpoint)
targetProxy.parent = router
return targetProxy, nil
} else if ptype == "subd" {
proxy, ok := router.SubdomainEndpoint.Load(key)
if !ok {
return nil, errors.New("target proxy not found")
}
return proxy.(*ProxyEndpoint), nil
targetProxy := proxy.(*ProxyEndpoint)
targetProxy.parent = router
return targetProxy, nil
}
return nil, errors.New("unsupported ptype")
}
/*
Save routing from RP
*/
func (router *Router) SaveProxy(ptype string, key string, newConfig *ProxyEndpoint) {
if ptype == "vdir" {
router.ProxyEndpoints.Store(key, newConfig)
} else if ptype == "subd" {
router.SubdomainEndpoint.Store(key, newConfig)
}
}
/*
Remove routing from RP
*/
func (router *Router) RemoveProxy(ptype string, key string) error {
//fmt.Println(ptype, key)
if ptype == "vdir" {
router.ProxyEndpoints.Delete(key)
return nil
} else if ptype == "subd" {
router.SubdomainEndpoint.Delete(key)
return nil
}
return errors.New("invalid ptype")
}
/*
Add an default router for the proxy server
*/
@ -319,14 +382,15 @@ func (router *Router) SetRootProxy(options *RootOptions) error {
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
rootEndpoint := ProxyEndpoint{
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: "/",
Domain: proxyLocation,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
Proxy: proxy,
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: "/",
Domain: proxyLocation,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
Proxy: proxy,
}
router.Root = &rootEndpoint

View File

@ -0,0 +1,68 @@
package dynamicproxy
import "errors"
/*
ProxyEndpoint.go
author: tobychui
This script handle the proxy endpoint object actions
so proxyEndpoint can be handled like a proper oop object
Most of the functions are implemented in dynamicproxy.go
*/
//Get the string version of proxy type
func (ep *ProxyEndpoint) GetProxyTypeString() string {
if ep.ProxyType == ProxyType_Subdomain {
return "subd"
} else if ep.ProxyType == ProxyType_Vdir {
return "vdir"
}
return "unknown"
}
//Update change in the current running proxy endpoint config
func (ep *ProxyEndpoint) UpdateToRuntime() {
if ep.IsVdir() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
} else if ep.IsSubDomain() {
ep.parent.SubdomainEndpoint.Store(ep.RootOrMatchingDomain, ep)
}
}
//Return true if the endpoint type is virtual directory
func (ep *ProxyEndpoint) IsVdir() bool {
return ep.ProxyType == ProxyType_Vdir
}
//Return true if the endpoint type is subdomain
func (ep *ProxyEndpoint) IsSubDomain() bool {
return ep.ProxyType == ProxyType_Subdomain
}
//Remove this proxy endpoint from running proxy endpoint list
func (ep *ProxyEndpoint) Remove() error {
//fmt.Println(ptype, key)
if ep.IsVdir() {
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
return nil
} else if ep.IsSubDomain() {
ep.parent.SubdomainEndpoint.Delete(ep.RootOrMatchingDomain)
return nil
}
return errors.New("invalid or unsupported type")
}
//ProxyEndpoint remove provide global access by key
func (router *Router) RemoveProxyEndpointByRootname(proxyType string, rootnameOrMatchingDomain string) error {
targetEpt, err := router.LoadProxy(proxyType, rootnameOrMatchingDomain)
if err != nil {
return err
}
return targetEpt.Remove()
}

View File

@ -57,6 +57,7 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
// Handle subdomain request
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
@ -94,6 +95,7 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
UseTLS: target.RequireTLS,
PathPrefix: "",
})
var dnsError *net.DNSError
if err != nil {
if errors.As(err, &dnsError) {
@ -116,6 +118,7 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
r.URL, _ = url.Parse(rewriteURL)
r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket")
@ -180,6 +183,5 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
}
h.Parent.Option.StatisticCollector.RecordRequest(requestInfo)
}()
}
}

View File

@ -28,13 +28,15 @@ func (t *RuleTable) HandleRedirect(w http.ResponseWriter, r *http.Request) int {
rr := t.MatchRedirectRule(requestPath)
if rr != nil {
redirectTarget := rr.TargetURL
//Always pad a / at the back of the target URL
if redirectTarget[len(redirectTarget)-1:] != "/" {
redirectTarget += "/"
}
if rr.ForwardChildpath {
//Remove the first / in the path
redirectTarget += strings.TrimPrefix(r.URL.Path, "/")
//Remove the first / in the path if the redirect target already have tailing slash
if strings.HasSuffix(redirectTarget, "/") {
redirectTarget += strings.TrimPrefix(r.URL.Path, "/")
} else {
redirectTarget += r.URL.Path
}
if r.URL.RawQuery != "" {
redirectTarget += "?" + r.URL.RawQuery
}

View File

@ -0,0 +1,51 @@
package dynamicproxy
import (
"encoding/json"
"errors"
"log"
"os"
"imuslab.com/zoraxy/mod/utils"
)
/*
rootRoute.go
This script handle special case in routing where the root proxy
entity is involved. This also include its setting object
RootRoutingOptions
*/
var rootConfigFilepath string = "conf/root_config.json"
func loadRootRoutingOptionsFromFile() (*RootRoutingOptions, error) {
if !utils.FileExists(rootConfigFilepath) {
//Not found. Create a root option
js, _ := json.MarshalIndent(RootRoutingOptions{}, "", " ")
err := os.WriteFile(rootConfigFilepath, js, 0775)
if err != nil {
return nil, errors.New("Unable to write root config to file: " + err.Error())
}
}
newRootOption := RootRoutingOptions{}
rootOptionsBytes, err := os.ReadFile(rootConfigFilepath)
if err != nil {
log.Println("[Error] Unable to read root config file at " + rootConfigFilepath + ": " + err.Error())
return nil, err
}
err = json.Unmarshal(rootOptionsBytes, &newRootOption)
if err != nil {
log.Println("[Error] Unable to parse root config file: " + err.Error())
return nil, err
}
return &newRootOption, nil
}
// Save the new config to file. Note that this will not overwrite the runtime one
func (opt *RootRoutingOptions) SaveToFile() error {
js, _ := json.MarshalIndent(opt, "", " ")
err := os.WriteFile(rootConfigFilepath, js, 0775)
return err
}

View File

@ -13,14 +13,15 @@ import (
*/
type RoutingRule struct {
ID string
MatchRule func(r *http.Request) bool
RoutingHandler http.Handler
Enabled bool
ID string //ID of the routing rule
Enabled bool //If the routing rule enabled
UseSystemAccessControl bool //Pass access control check to system white/black list, set this to false to bypass white/black list
MatchRule func(r *http.Request) bool
RoutingHandler func(http.ResponseWriter, *http.Request)
}
//Router functions
//Check if a routing rule exists given its id
// Router functions
// Check if a routing rule exists given its id
func (router *Router) GetRoutingRuleById(rrid string) (*RoutingRule, error) {
for _, rr := range router.routingRules {
if rr.ID == rrid {
@ -31,19 +32,19 @@ func (router *Router) GetRoutingRuleById(rrid string) (*RoutingRule, error) {
return nil, errors.New("routing rule with given id not found")
}
//Add a routing rule to the router
// Add a routing rule to the router
func (router *Router) AddRoutingRules(rr *RoutingRule) error {
_, err := router.GetRoutingRuleById(rr.ID)
if err != nil {
if err == nil {
//routing rule with given id already exists
return err
return errors.New("routing rule with same id already exists")
}
router.routingRules = append(router.routingRules, rr)
return nil
}
//Remove a routing rule from the router
// Remove a routing rule from the router
func (router *Router) RemoveRoutingRule(rrid string) {
newRoutingRules := []*RoutingRule{}
for _, rr := range router.routingRules {
@ -55,13 +56,13 @@ func (router *Router) RemoveRoutingRule(rrid string) {
router.routingRules = newRoutingRules
}
//Get all routing rules
// Get all routing rules
func (router *Router) GetAllRoutingRules() []*RoutingRule {
return router.routingRules
}
//Get the matching routing rule that describe this request.
//Return nil if no routing rule is match
// Get the matching routing rule that describe this request.
// Return nil if no routing rule is match
func (router *Router) GetMatchingRoutingRule(r *http.Request) *RoutingRule {
for _, thisRr := range router.routingRules {
if thisRr.IsMatch(r) {
@ -71,8 +72,8 @@ func (router *Router) GetMatchingRoutingRule(r *http.Request) *RoutingRule {
return nil
}
//Routing Rule functions
//Check if a request object match the
// Routing Rule functions
// Check if a request object match the
func (e *RoutingRule) IsMatch(r *http.Request) bool {
if !e.Enabled {
return false
@ -81,5 +82,5 @@ func (e *RoutingRule) IsMatch(r *http.Request) bool {
}
func (e *RoutingRule) Route(w http.ResponseWriter, r *http.Request) {
e.RoutingHandler.ServeHTTP(w, r)
e.RoutingHandler(w, r)
}

View File

@ -34,13 +34,15 @@ func (router *Router) AddSubdomainRoutingService(options *SubdOptions) error {
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
router.SubdomainEndpoint.Store(options.MatchingDomain, &ProxyEndpoint{
RootOrMatchingDomain: options.MatchingDomain,
Domain: domain,
RequireTLS: options.RequireTLS,
Proxy: proxy,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
RootOrMatchingDomain: options.MatchingDomain,
Domain: domain,
RequireTLS: options.RequireTLS,
Proxy: proxy,
BypassGlobalTLS: options.BypassGlobalTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
})
log.Println("Adding Subdomain Rule: ", options.MatchingDomain+" to "+domain)

View File

@ -0,0 +1,55 @@
<html>
<head>
<!-- Zoraxy Forbidden Template -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css">
<script type="text/javascript" src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js"></script>
<title>Forbidden</title>
<style>
#msg{
position: absolute;
top: calc(50% - 150px);
left: calc(50% - 250px);
width: 500px;
height: 300px;
text-align: center;
}
#footer{
position: fixed;
padding: 2em;
padding-left: 5em;
padding-right: 5em;
bottom: 0px;
left: 0px;
width: 100%;
}
small{
word-break: break-word;
}
</style>
</head>
<body>
<div id="msg">
<h1 style="font-size: 6em; margin-bottom: 0px;"><i class="red ban icon"></i></h1>
<div>
<h3 style="margin-top: 1em;">403 - Forbidden</h3>
<div class="ui divider"></div>
<p>You do not have permission to view this directory or page. <br>
This might cause by the region limit setting of this site.</p>
<div class="ui divider"></div>
<div style="text-align: left;">
<small>Request time: <span id="reqtime"></span></small><br>
<small id="reqURLDisplay">Request URI: <span id="requrl"></span></small>
</div>
</div>
</div>
<script>
$("#reqtime").text(new Date().toLocaleString(undefined, {year: 'numeric', month: '2-digit', day: '2-digit', weekday:"long", hour: '2-digit', hour12: false, minute:'2-digit', second:'2-digit'}));
$("#requrl").text(window.location.href);
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
package dynamicproxy
import (
_ "embed"
"net"
"net/http"
"sync"
@ -22,27 +23,33 @@ type ProxyHandler struct {
}
type RouterOption struct {
Port int
UseTls bool
ForceHttpsRedirect bool
HostUUID string //The UUID 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
ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager
RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store
GeodbStore *geodb.Store //GeoIP blacklist and whitelist
StatisticCollector *statistic.Collector
WebDirectory string //The static web server directory containing the templates folder
}
type Router struct {
Option *RouterOption
ProxyEndpoints *sync.Map
SubdomainEndpoint *sync.Map
Running bool
Root *ProxyEndpoint
mux http.Handler
server *http.Server
tlsListener net.Listener
routingRules []*RoutingRule
Option *RouterOption
ProxyEndpoints *sync.Map
SubdomainEndpoint *sync.Map
Running bool
Root *ProxyEndpoint
RootRoutingOptions *RootRoutingOptions
mux http.Handler
server *http.Server
tlsListener net.Listener
routingRules []*RoutingRule
tlsRedirectStop chan bool
tlsRedirectStop chan bool //Stop channel for tls redirection server
tldMap map[string]int //Top level domain map, see tld.json
}
// Auth credential for basic auth on certain endpoints
@ -57,56 +64,73 @@ type BasicAuthUnhashedCredentials struct {
Password string
}
// A proxy endpoint record
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Root for vdir or Matching domain for subd
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials `json:"-"`
Proxy *dpcore.ReverseProxy `json:"-"`
// Paths to exclude in basic auth enabled proxy handler
type BasicAuthExceptionRule struct {
PathPrefix string
}
// A proxy endpoint record
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Root for vdir or Matching domain for subd, also act as key
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
SkipCertValidations bool //Set to true to accept self signed certs
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials `json:"-"` //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
Proxy *dpcore.ReverseProxy `json:"-"`
parent *Router
}
// Root options are those that are required for reverse proxy handler to work
type RootOptions struct {
ProxyLocation string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
ProxyLocation string //Proxy Root target, all unset traffic will be forward to here
RequireTLS bool //Proxy root target require TLS connection (not recommended)
BypassGlobalTLS bool //Bypass global TLS setting and make root http only (not recommended)
SkipCertValidations bool //Skip cert validation, suitable for self-signed certs, CURRENTLY NOT USED
//Basic Auth Related
RequireBasicAuth bool //Require basic auth, CURRENTLY NOT USED
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
// Additional options are here for letting router knows how to route exception cases for root
type RootRoutingOptions struct {
//Root only configs
EnableRedirectForUnsetRules bool //Force unset rules to redirect to custom domain
UnsetRuleRedirectTarget string //Custom domain to redirect to for unset rules
}
type VdirOptions struct {
RootName string
Domain string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
RootName string
Domain string
RequireTLS bool
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
type SubdOptions struct {
MatchingDomain string
Domain string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
MatchingDomain string
Domain string
RequireTLS bool
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
/*
type ProxyEndpoint struct {
Root string
Domain string
RequireTLS bool
Proxy *reverseproxy.ReverseProxy `json:"-"`
}
type SubdomainEndpoint struct {
MatchingDomain string
Domain string
RequireTLS bool
Proxy *reverseproxy.ReverseProxy `json:"-"`
}
Web Templates
*/
var (
//go:embed templates/forbidden.html
page_forbidden []byte
)

16
src/mod/expose/expose.go Normal file
View File

@ -0,0 +1,16 @@
package expose
/*
Service Expose Proxy
A tunnel for getting your local server online in one line
(No, this is not ngrok)
*/
type Router struct {
}
//Create a new service expose router
func NewServiceExposeRouter() {
}

111
src/mod/expose/security.go Normal file
View File

@ -0,0 +1,111 @@
package expose
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha512"
"crypto/x509"
"encoding/pem"
"errors"
"log"
)
// GenerateKeyPair generates a new key pair
func GenerateKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) {
privkey, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return nil, nil, err
}
return privkey, &privkey.PublicKey, nil
}
// PrivateKeyToBytes private key to bytes
func PrivateKeyToBytes(priv *rsa.PrivateKey) []byte {
privBytes := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(priv),
},
)
return privBytes
}
// PublicKeyToBytes public key to bytes
func PublicKeyToBytes(pub *rsa.PublicKey) ([]byte, error) {
pubASN1, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return []byte(""), err
}
pubBytes := pem.EncodeToMemory(&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: pubASN1,
})
return pubBytes, nil
}
// BytesToPrivateKey bytes to private key
func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(priv)
enc := x509.IsEncryptedPEMBlock(block)
b := block.Bytes
var err error
if enc {
log.Println("is encrypted pem block")
b, err = x509.DecryptPEMBlock(block, nil)
if err != nil {
return nil, err
}
}
key, err := x509.ParsePKCS1PrivateKey(b)
if err != nil {
return nil, err
}
return key, nil
}
// BytesToPublicKey bytes to public key
func BytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
block, _ := pem.Decode(pub)
enc := x509.IsEncryptedPEMBlock(block)
b := block.Bytes
var err error
if enc {
log.Println("is encrypted pem block")
b, err = x509.DecryptPEMBlock(block, nil)
if err != nil {
return nil, err
}
}
ifc, err := x509.ParsePKIXPublicKey(b)
if err != nil {
return nil, err
}
key, ok := ifc.(*rsa.PublicKey)
if !ok {
return nil, errors.New("key not valid")
}
return key, nil
}
// EncryptWithPublicKey encrypts data with public key
func EncryptWithPublicKey(msg []byte, pub *rsa.PublicKey) ([]byte, error) {
hash := sha512.New()
ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil)
if err != nil {
return []byte(""), err
}
return ciphertext, nil
}
// DecryptWithPrivateKey decrypts data with private key
func DecryptWithPrivateKey(ciphertext []byte, priv *rsa.PrivateKey) ([]byte, error) {
hash := sha512.New()
plaintext, err := rsa.DecryptOAEP(hash, rand.Reader, priv, ciphertext, nil)
if err != nil {
return []byte(""), err
}
return plaintext, nil
}

View File

@ -13,8 +13,8 @@ import (
)
func readAuthTokenAsAdmin() (string, error) {
if utils.FileExists("./authtoken.secret") {
authKey, err := os.ReadFile("./authtoken.secret")
if utils.FileExists("./conf/authtoken.secret") {
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err == nil {
return strings.TrimSpace(string(authKey)), nil
}

View File

@ -19,8 +19,8 @@ import (
// Use admin permission to read auth token on Windows
func readAuthTokenAsAdmin() (string, error) {
//Check if the previous startup already extracted the authkey
if utils.FileExists("./authtoken.secret") {
authKey, err := os.ReadFile("./authtoken.secret")
if utils.FileExists("./conf/authtoken.secret") {
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err == nil {
return strings.TrimSpace(string(authKey)), nil
}
@ -30,7 +30,7 @@ func readAuthTokenAsAdmin() (string, error) {
exe := "cmd.exe"
cwd, _ := os.Getwd()
output, _ := filepath.Abs(filepath.Join("./", "authtoken.secret"))
output, _ := filepath.Abs(filepath.Join("./conf/", "authtoken.secret"))
os.WriteFile(output, []byte(""), 0775)
args := fmt.Sprintf("/C type \"C:\\ProgramData\\ZeroTier\\One\\authtoken.secret\" > \"" + output + "\"")
@ -49,13 +49,13 @@ func readAuthTokenAsAdmin() (string, error) {
log.Println("Please click agree to allow access to ZeroTier authtoken from ProgramData")
retry := 0
time.Sleep(3 * time.Second)
for !utils.FileExists("./authtoken.secret") && retry < 10 {
for !utils.FileExists("./conf/authtoken.secret") && retry < 10 {
time.Sleep(3 * time.Second)
log.Println("Waiting for ZeroTier authtoken extraction...")
retry++
}
authKey, err := os.ReadFile("./authtoken.secret")
authKey, err := os.ReadFile("./conf/authtoken.secret")
if err != nil {
return "", err
}

View File

@ -0,0 +1,91 @@
package geodb
import "strings"
/*
Blacklist.go
This script store the blacklist related functions
*/
//Geo Blacklist
func (s *Store) AddCountryCodeToBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("blacklist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("blacklist-cn", countryCode)
}
func (s *Store) IsCountryCodeBlacklisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isBlacklisted bool = false
s.sysdb.Read("blacklist-cn", countryCode, &isBlacklisted)
return isBlacklisted
}
func (s *Store) GetAllBlacklistedCountryCode() []string {
bannedCountryCodes := []string{}
entries, err := s.sysdb.ListTable("blacklist-cn")
if err != nil {
return bannedCountryCodes
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedCountryCodes = append(bannedCountryCodes, ip)
}
return bannedCountryCodes
}
//IP Blacklsits
func (s *Store) AddIPToBlackList(ipAddr string) {
s.sysdb.Write("blacklist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromBlackList(ipAddr string) {
s.sysdb.Delete("blacklist-ip", ipAddr)
}
func (s *Store) GetAllBlacklistedIp() []string {
bannedIps := []string{}
entries, err := s.sysdb.ListTable("blacklist-ip")
if err != nil {
return bannedIps
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedIps = append(bannedIps, ip)
}
return bannedIps
}
func (s *Store) IsIPBlacklisted(ipAddr string) bool {
var isBlacklisted bool = false
s.sysdb.Read("blacklist-ip", ipAddr, &isBlacklisted)
if isBlacklisted {
return true
}
//Check for IP wildcard and CIRD rules
AllBlacklistedIps := s.GetAllBlacklistedIp()
for _, blacklistRule := range AllBlacklistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
if cidrMatch {
return true
}
}
return false
}

View File

@ -3,8 +3,8 @@ package geodb
import (
_ "embed"
"log"
"net"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/database"
)
@ -20,13 +20,16 @@ type Store struct {
WhitelistEnabled bool
geodb [][]string //Parsed geodb list
geodbIpv6 [][]string //Parsed geodb list for ipv6
geotrie *trie
geotrieIpv6 *trie
geotrie *trie
geotrieIpv6 *trie
//geoipCache sync.Map
sysdb *database.Database
option *StoreOptions
}
sysdb *database.Database
type StoreOptions struct {
AllowSlowIpv4LookUp bool
AllowSloeIpv6Lookup bool
}
type CountryInfo struct {
@ -34,7 +37,7 @@ type CountryInfo struct {
ContinetCode string
}
func NewGeoDb(sysdb *database.Database) (*Store, error) {
func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
parsedGeoData, err := parseCSV(geoipv4)
if err != nil {
return nil, err
@ -79,14 +82,25 @@ func NewGeoDb(sysdb *database.Database) (*Store, error) {
log.Println("Database pointer set to nil: Entering debug mode")
}
var ipv4Trie *trie
if !option.AllowSlowIpv4LookUp {
ipv4Trie = constrctTrieTree(parsedGeoData)
}
var ipv6Trie *trie
if !option.AllowSloeIpv6Lookup {
ipv6Trie = constrctTrieTree(parsedGeoDataIpv6)
}
return &Store{
BlacklistEnabled: blacklistEnabled,
WhitelistEnabled: whitelistEnabled,
geodb: parsedGeoData,
geotrie: constrctTrieTree(parsedGeoData),
geotrie: ipv4Trie,
geodbIpv6: parsedGeoDataIpv6,
geotrieIpv6: constrctTrieTree(parsedGeoDataIpv6),
geotrieIpv6: ipv6Trie,
sysdb: sysdb,
option: option,
}, nil
}
@ -106,176 +120,13 @@ func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error)
CountryIsoCode: cc,
ContinetCode: "",
}, nil
}
func (s *Store) Close() {
}
/*
Country code based black / white list
*/
func (s *Store) AddCountryCodeToBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("blacklist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("blacklist-cn", countryCode)
}
func (s *Store) AddCountryCodeToWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("whitelist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("whitelist-cn", countryCode)
}
func (s *Store) IsCountryCodeBlacklisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isBlacklisted bool = false
s.sysdb.Read("blacklist-cn", countryCode, &isBlacklisted)
return isBlacklisted
}
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isWhitelisted bool = false
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
return isWhitelisted
}
func (s *Store) GetAllBlacklistedCountryCode() []string {
bannedCountryCodes := []string{}
entries, err := s.sysdb.ListTable("blacklist-cn")
if err != nil {
return bannedCountryCodes
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedCountryCodes = append(bannedCountryCodes, ip)
}
return bannedCountryCodes
}
func (s *Store) GetAllWhitelistedCountryCode() []string {
whitelistedCountryCode := []string{}
entries, err := s.sysdb.ListTable("whitelist-cn")
if err != nil {
return whitelistedCountryCode
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedCountryCode = append(whitelistedCountryCode, ip)
}
return whitelistedCountryCode
}
/*
IP based black / whitelist
*/
func (s *Store) AddIPToBlackList(ipAddr string) {
s.sysdb.Write("blacklist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromBlackList(ipAddr string) {
s.sysdb.Delete("blacklist-ip", ipAddr)
}
func (s *Store) AddIPToWhiteList(ipAddr string) {
s.sysdb.Write("whitelist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
s.sysdb.Delete("whitelist-ip", ipAddr)
}
func (s *Store) IsIPBlacklisted(ipAddr string) bool {
var isBlacklisted bool = false
s.sysdb.Read("blacklist-ip", ipAddr, &isBlacklisted)
if isBlacklisted {
return true
}
//Check for IP wildcard and CIRD rules
AllBlacklistedIps := s.GetAllBlacklistedIp()
for _, blacklistRule := range AllBlacklistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
if cidrMatch {
return true
}
}
return false
}
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
var isBlacklisted bool = false
s.sysdb.Read("whitelist-ip", ipAddr, &isBlacklisted)
if isBlacklisted {
return true
}
//Check for IP wildcard and CIRD rules
AllBlacklistedIps := s.GetAllBlacklistedIp()
for _, blacklistRule := range AllBlacklistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
if cidrMatch {
return true
}
}
return false
}
func (s *Store) GetAllBlacklistedIp() []string {
bannedIps := []string{}
entries, err := s.sysdb.ListTable("blacklist-ip")
if err != nil {
return bannedIps
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedIps = append(bannedIps, ip)
}
return bannedIps
}
func (s *Store) GetAllWhitelistedIp() []string {
whitelistedIp := []string{}
entries, err := s.sysdb.ListTable("whitelist-ip")
if err != nil {
return whitelistedIp
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedIp = append(whitelistedIp, ip)
}
return whitelistedIp
}
/*
Check if a IP address is blacklisted, in either country or IP blacklist
IsBlacklisted default return is false (allow access)
@ -341,6 +192,23 @@ func (s *Store) IsWhitelisted(ipAddr string) bool {
return false
}
// A helper function that check both blacklist and whitelist for access
// for both geoIP and ip / CIDR ranges
func (s *Store) AllowIpAccess(ipaddr string) bool {
if s.IsBlacklisted(ipaddr) {
return false
}
return s.IsWhitelisted(ipaddr)
}
func (s *Store) AllowConnectionAccess(conn net.Conn) bool {
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
return s.AllowIpAccess(addr.IP.String())
}
return true
}
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
ipAddr := GetRequesterIP(r)
if ipAddr == "" {

View File

@ -41,7 +41,10 @@ func TestTrieConstruct(t *testing.T) {
func TestResolveCountryCodeFromIP(t *testing.T) {
// Create a new store
store, err := geodb.NewGeoDb(nil)
store, err := geodb.NewGeoDb(nil, &geodb.StoreOptions{
false,
false,
})
if err != nil {
t.Errorf("error creating store: %v", err)
return

View File

@ -4,7 +4,6 @@ import (
"bytes"
"encoding/csv"
"io"
"net"
"strings"
)
@ -26,9 +25,17 @@ func (s *Store) search(ip string) string {
//Search in geotrie tree
cc := ""
if IsIPv6(ip) {
cc = s.geotrieIpv6.search(ip)
if s.geotrieIpv6 == nil {
cc = s.slowSearchIpv6(ip)
} else {
cc = s.geotrieIpv6.search(ip)
}
} else {
cc = s.geotrie.search(ip)
if s.geotrie == nil {
cc = s.slowSearchIpv4(ip)
} else {
cc = s.geotrie.search(ip)
}
}
/*
@ -69,27 +76,3 @@ func parseCSV(content []byte) ([][]string, error) {
}
return records, nil
}
// Check if a ip string is within the range of two others
func isIPInRange(ip, start, end string) bool {
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
return false
}
startAddr := net.ParseIP(start)
if startAddr == nil {
return false
}
endAddr := net.ParseIP(end)
if endAddr == nil {
return false
}
if ipAddr.To4() == nil || startAddr.To4() == nil || endAddr.To4() == nil {
return false
}
return bytes.Compare(ipAddr.To4(), startAddr.To4()) >= 0 && bytes.Compare(ipAddr.To4(), endAddr.To4()) <= 0
}

View File

@ -0,0 +1,81 @@
package geodb
import (
"errors"
"math/big"
"net"
)
/*
slowSearch.go
This script implement the slow search method for ip to country code
lookup. If you have the memory allocation for near O(1) lookup,
you should not be using slow search mode.
*/
func ipv4ToUInt32(ip net.IP) uint32 {
ip = ip.To4()
return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
}
func isIPv4InRange(startIP, endIP, testIP string) (bool, error) {
start := net.ParseIP(startIP)
end := net.ParseIP(endIP)
test := net.ParseIP(testIP)
if start == nil || end == nil || test == nil {
return false, errors.New("invalid IP address format")
}
startUint := ipv4ToUInt32(start)
endUint := ipv4ToUInt32(end)
testUint := ipv4ToUInt32(test)
return testUint >= startUint && testUint <= endUint, nil
}
func isIPv6InRange(startIP, endIP, testIP string) (bool, error) {
start := net.ParseIP(startIP)
end := net.ParseIP(endIP)
test := net.ParseIP(testIP)
if start == nil || end == nil || test == nil {
return false, errors.New("invalid IP address format")
}
startInt := new(big.Int).SetBytes(start.To16())
endInt := new(big.Int).SetBytes(end.To16())
testInt := new(big.Int).SetBytes(test.To16())
return testInt.Cmp(startInt) >= 0 && testInt.Cmp(endInt) <= 0, nil
}
// Slow country code lookup for
func (s *Store) slowSearchIpv4(ipAddr string) string {
for _, ipRange := range s.geodb {
startIp := ipRange[0]
endIp := ipRange[1]
cc := ipRange[2]
inRange, _ := isIPv4InRange(startIp, endIp, ipAddr)
if inRange {
return cc
}
}
return ""
}
func (s *Store) slowSearchIpv6(ipAddr string) string {
for _, ipRange := range s.geodbIpv6 {
startIp := ipRange[0]
endIp := ipRange[1]
cc := ipRange[2]
inRange, _ := isIPv6InRange(startIp, endIp, ipAddr)
if inRange {
return cc
}
}
return ""
}

View File

@ -1,15 +1,12 @@
package geodb
import (
"fmt"
"math"
"net"
"strconv"
"strings"
)
type trie_Node struct {
childrens [2]*trie_Node
ends bool
cc string
}
@ -18,7 +15,7 @@ type trie struct {
root *trie_Node
}
func ipToBitString(ip string) string {
func ipToBytes(ip string) []byte {
// Parse the IP address string into a net.IP object
parsedIP := net.ParseIP(ip)
@ -29,49 +26,7 @@ func ipToBitString(ip string) string {
ipBytes = parsedIP.To16()
}
// Convert each byte in the IP address to its 8-bit binary representation
var result []string
for _, b := range ipBytes {
result = append(result, fmt.Sprintf("%08b", b))
}
// Join the binary representation of each byte with dots to form the final bit string
return strings.Join(result, "")
}
func bitStringToIp(bitString string) string {
// Check if the bit string represents an IPv4 or IPv6 address
isIPv4 := len(bitString) == 32
// Split the bit string into 8-bit segments
segments := make([]string, 0)
if isIPv4 {
for i := 0; i < 4; i++ {
segments = append(segments, bitString[i*8:(i+1)*8])
}
} else {
for i := 0; i < 16; i++ {
segments = append(segments, bitString[i*8:(i+1)*8])
}
}
// Convert each segment to its decimal equivalent
decimalSegments := make([]int, len(segments))
for i, s := range segments {
val, _ := strconv.ParseInt(s, 2, 64)
decimalSegments[i] = int(val)
}
// Construct the IP address string based on the type (IPv4 or IPv6)
if isIPv4 {
return fmt.Sprintf("%d.%d.%d.%d", decimalSegments[0], decimalSegments[1], decimalSegments[2], decimalSegments[3])
} else {
ip := make(net.IP, net.IPv6len)
for i := 0; i < net.IPv6len; i++ {
ip[i] = byte(decimalSegments[i])
}
return ip.String()
}
return ipBytes
}
// inititlaizing a new trie
@ -83,20 +38,39 @@ func newTrie() *trie {
// Passing words to trie
func (t *trie) insert(ipAddr string, cc string) {
word := ipToBitString(ipAddr)
ipBytes := ipToBytes(ipAddr)
current := t.root
for _, wr := range word {
index := wr - '0'
if current.childrens[index] == nil {
current.childrens[index] = &trie_Node{
childrens: [2]*trie_Node{},
ends: false,
cc: cc,
for _, b := range ipBytes {
//For each byte in the ip address
//each byte is 8 bit
for j := 0; j < 8; j++ {
bitwise := (b&uint8(math.Pow(float64(2), float64(j))) > 0)
bit := 0b0000
if bitwise {
bit = 0b0001
}
if current.childrens[bit] == nil {
current.childrens[bit] = &trie_Node{
childrens: [2]*trie_Node{},
cc: cc,
}
}
current = current.childrens[bit]
}
current = current.childrens[index]
}
current.ends = true
/*
for i := 63; i >= 0; i-- {
bit := (ipInt64 >> uint(i)) & 1
if current.childrens[bit] == nil {
current.childrens[bit] = &trie_Node{
childrens: [2]*trie_Node{},
cc: cc,
}
}
current = current.childrens[bit]
}
*/
}
func isReservedIP(ip string) bool {
@ -126,16 +100,34 @@ func (t *trie) search(ipAddr string) string {
if isReservedIP(ipAddr) {
return ""
}
word := ipToBitString(ipAddr)
ipBytes := ipToBytes(ipAddr)
current := t.root
for _, wr := range word {
index := wr - '0'
if current.childrens[index] == nil {
return current.cc
for _, b := range ipBytes {
//For each byte in the ip address
//each byte is 8 bit
for j := 0; j < 8; j++ {
bitwise := (b&uint8(math.Pow(float64(2), float64(j))) > 0)
bit := 0b0000
if bitwise {
bit = 0b0001
}
if current.childrens[bit] == nil {
return current.cc
}
current = current.childrens[bit]
}
current = current.childrens[index]
}
if current.ends {
/*
for i := 63; i >= 0; i-- {
bit := (ipInt64 >> uint(i)) & 1
if current.childrens[bit] == nil {
return current.cc
}
current = current.childrens[bit]
}
*/
if len(current.childrens) == 0 {
return current.cc
}

View File

@ -0,0 +1,91 @@
package geodb
import "strings"
/*
Whitelist.go
This script handles whitelist related functions
*/
//Geo Whitelist
func (s *Store) AddCountryCodeToWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("whitelist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("whitelist-cn", countryCode)
}
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isWhitelisted bool = false
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
return isWhitelisted
}
func (s *Store) GetAllWhitelistedCountryCode() []string {
whitelistedCountryCode := []string{}
entries, err := s.sysdb.ListTable("whitelist-cn")
if err != nil {
return whitelistedCountryCode
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedCountryCode = append(whitelistedCountryCode, ip)
}
return whitelistedCountryCode
}
//IP Whitelist
func (s *Store) AddIPToWhiteList(ipAddr string) {
s.sysdb.Write("whitelist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
s.sysdb.Delete("whitelist-ip", ipAddr)
}
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
var isWhitelisted bool = false
s.sysdb.Read("whitelist-ip", ipAddr, &isWhitelisted)
if isWhitelisted {
return true
}
//Check for IP wildcard and CIRD rules
AllWhitelistedIps := s.GetAllWhitelistedIp()
for _, whitelistRules := range AllWhitelistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, whitelistRules)
if cidrMatch {
return true
}
}
return false
}
func (s *Store) GetAllWhitelistedIp() []string {
whitelistedIp := []string{}
entries, err := s.sysdb.ListTable("whitelist-ip")
if err != nil {
return whitelistedIp
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedIp = append(whitelistedIp, ip)
}
return whitelistedIp
}

View File

@ -0,0 +1,103 @@
package logger
import (
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"time"
)
/*
Zoraxy Logger
This script is designed to make a managed log for the Zoraxy
and replace the ton of log.Println in the system core
*/
type Logger struct {
LogToFile bool //Set enable write to file
Prefix string //Prefix for log files
LogFolder string //Folder to store the log file
CurrentLogFile string //Current writing filename
file *os.File
}
func NewLogger(logFilePrefix string, logFolder string, logToFile bool) (*Logger, error) {
err := os.MkdirAll(logFolder, 0775)
if err != nil {
return nil, err
}
thisLogger := Logger{
LogToFile: logToFile,
Prefix: logFilePrefix,
LogFolder: logFolder,
}
logFilePath := thisLogger.getLogFilepath()
f, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
if err != nil {
return nil, err
}
thisLogger.CurrentLogFile = logFilePath
thisLogger.file = f
return &thisLogger, nil
}
func (l *Logger) getLogFilepath() string {
year, month, _ := time.Now().Date()
return filepath.Join(l.LogFolder, l.Prefix+"_"+strconv.Itoa(year)+"-"+strconv.Itoa(int(month))+".log")
}
// PrintAndLog will log the message to file and print the log to STDOUT
func (l *Logger) PrintAndLog(title string, message string, originalError error) {
go func() {
l.Log(title, message, originalError)
}()
log.Println("[" + title + "] " + message)
}
// Println is a fast snap-in replacement for log.Println
func (l *Logger) Println(v ...interface{}) {
//Convert the array of interfaces into string
message := fmt.Sprint(v...)
go func() {
l.Log("info", string(message), nil)
}()
log.Println("[INFO] " + string(message))
}
func (l *Logger) Log(title string, errorMessage string, originalError error) {
l.ValidateAndUpdateLogFilepath()
if l.LogToFile {
if originalError == nil {
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [INFO]" + errorMessage + "\n")
} else {
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [ERROR]" + errorMessage + " " + originalError.Error() + "\n")
}
}
}
// Validate if the logging target is still valid (detect any months change)
func (l *Logger) ValidateAndUpdateLogFilepath() {
expectedCurrentLogFilepath := l.getLogFilepath()
if l.CurrentLogFile != expectedCurrentLogFilepath {
//Change of month. Update to a new log file
l.file.Close()
f, err := os.OpenFile(expectedCurrentLogFilepath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
if err != nil {
log.Println("[Logger] Unable to create new log. Logging to file disabled.")
l.LogToFile = false
return
}
l.CurrentLogFile = expectedCurrentLogFilepath
l.file = f
}
}
func (l *Logger) Close() {
l.file.Close()
}

View File

@ -0,0 +1,122 @@
package logviewer
import (
"encoding/json"
"errors"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
type ViewerOption struct {
RootFolder string //The root folder to scan for log
Extension string //The extension the root files use, include the . in your ext (e.g. .log)
}
type Viewer struct {
option *ViewerOption
}
type LogFile struct {
Title string
Filename string
Fullpath string
Filesize int64
}
func NewLogViewer(option *ViewerOption) *Viewer {
return &Viewer{option: option}
}
/*
Log Request Handlers
*/
//List all the log files in the log folder. Return in map[string]LogFile format
func (v *Viewer) HandleListLog(w http.ResponseWriter, r *http.Request) {
logFiles := v.ListLogFiles(false)
js, _ := json.Marshal(logFiles)
utils.SendJSONResponse(w, string(js))
}
// Read log of a given catergory and filename
// Require GET varaible: file and catergory
func (v *Viewer) HandleReadLog(w http.ResponseWriter, r *http.Request) {
filename, err := utils.GetPara(r, "file")
if err != nil {
utils.SendErrorResponse(w, "invalid filename given")
return
}
catergory, err := utils.GetPara(r, "catergory")
if err != nil {
utils.SendErrorResponse(w, "invalid catergory given")
return
}
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(catergory)), strings.TrimSpace(filepath.Base(filename)))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendTextResponse(w, content)
}
/*
Log Access Functions
*/
func (v *Viewer) ListLogFiles(showFullpath bool) map[string][]*LogFile {
result := map[string][]*LogFile{}
filepath.WalkDir(v.option.RootFolder, func(path string, di fs.DirEntry, err error) error {
if filepath.Ext(path) == v.option.Extension {
catergory := filepath.Base(filepath.Dir(path))
logList, ok := result[catergory]
if !ok {
//this catergory hasn't been scanned before.
logList = []*LogFile{}
}
fullpath := filepath.ToSlash(path)
if !showFullpath {
fullpath = ""
}
st, err := os.Stat(path)
if err != nil {
return nil
}
logList = append(logList, &LogFile{
Title: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)),
Filename: filepath.Base(path),
Fullpath: fullpath,
Filesize: st.Size(),
})
result[catergory] = logList
}
return nil
})
return result
}
func (v *Viewer) LoadLogFile(catergory string, filename string) (string, error) {
logFilepath := filepath.Join(v.option.RootFolder, catergory, filename)
if utils.FileExists(logFilepath) {
//Load it
content, err := os.ReadFile(logFilepath)
if err != nil {
return "", err
}
return string(content), nil
} else {
return "", errors.New("log file not found")
}
}

View File

@ -226,7 +226,7 @@ func (m *MDNSHost) Scan(timeout int, domainFilter string) []*NetworkHost {
return discoveredHost
}
//Get all mac address of all interfaces
// Get all mac address of all interfaces
func getMacAddr() ([]string, error) {
ifas, err := net.Interfaces()
if err != nil {

View File

@ -213,6 +213,7 @@ func GetNetworkInterfaceStats() (int64, int64, error) {
out, err := cmd.Output()
if err != nil {
callbackChan <- wmicResult{0, 0, err}
return
}
//Filter out the first line
@ -251,18 +252,16 @@ func GetNetworkInterfaceStats() (int64, int64, error) {
go func() {
//Spawn a timer to terminate the cmd process if timeout
var timer *time.Timer
timer = time.AfterFunc(3*time.Second, func() {
timer.Stop()
if cmd != nil && cmd.Process != nil {
cmd.Process.Kill()
}
time.Sleep(3 * time.Second)
if cmd != nil && cmd.Process != nil {
cmd.Process.Kill()
callbackChan <- wmicResult{0, 0, errors.New("wmic execution timeout")}
})
}
}()
result := wmicResult{}
result = <-callbackChan
cmd = nil
if result.Err != nil {
log.Println("Unable to extract NIC info from wmic: " + result.Err.Error())
}

View File

@ -0,0 +1,159 @@
package netutils
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"github.com/likexian/whois"
"imuslab.com/zoraxy/mod/utils"
)
/*
This script handles basic network utilities like
- traceroute
- ping
*/
func HandleTraceRoute(w http.ResponseWriter, r *http.Request) {
targetIpOrDomain, err := utils.GetPara(r, "target")
if err != nil {
utils.SendErrorResponse(w, "invalid target (domain or ip) address given")
return
}
maxhopsString, err := utils.GetPara(r, "maxhops")
if err != nil {
maxhopsString = "64"
}
maxHops, err := strconv.Atoi(maxhopsString)
if err != nil {
maxHops = 64
}
results, err := TraceRoute(targetIpOrDomain, maxHops)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
}
func TraceRoute(targetIpOrDomain string, maxHops int) ([]string, error) {
return traceroute(targetIpOrDomain, maxHops)
}
func HandleWhois(w http.ResponseWriter, r *http.Request) {
targetIpOrDomain, err := utils.GetPara(r, "target")
if err != nil {
utils.SendErrorResponse(w, "invalid target (domain or ip) address given")
return
}
raw, _ := utils.GetPara(r, "raw")
result, err := whois.Whois(targetIpOrDomain)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if raw == "true" {
utils.SendTextResponse(w, result)
} else {
if isDomainName(targetIpOrDomain) {
//Is Domain
parsedOutput, err := ParseWHOISResponse(result)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(parsedOutput)
utils.SendJSONResponse(w, string(js))
} else {
//Is IP
parsedOutput, err := ParseWhoisIpData(result)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(parsedOutput)
utils.SendJSONResponse(w, string(js))
}
}
}
func HandlePing(w http.ResponseWriter, r *http.Request) {
targetIpOrDomain, err := utils.GetPara(r, "target")
if err != nil {
utils.SendErrorResponse(w, "invalid target (domain or ip) address given")
return
}
type MixedPingResults struct {
ICMP []string
TCP []string
UDP []string
}
results := MixedPingResults{
ICMP: []string{},
TCP: []string{},
UDP: []string{},
}
//Ping ICMP
for i := 0; i < 4; i++ {
realIP, pingTime, ttl, err := PingIP(targetIpOrDomain)
if err != nil {
results.ICMP = append(results.ICMP, "Reply from "+realIP+": "+err.Error())
} else {
results.ICMP = append(results.ICMP, fmt.Sprintf("Reply from %s: Time=%dms TTL=%d", realIP, pingTime.Milliseconds(), ttl))
}
}
//Ping TCP
for i := 0; i < 4; i++ {
pingTime, err := TCPPing(targetIpOrDomain)
if err != nil {
results.TCP = append(results.TCP, "Reply from "+resolveIpFromDomain(targetIpOrDomain)+": "+err.Error())
} else {
results.TCP = append(results.TCP, fmt.Sprintf("Reply from %s: Time=%dms", resolveIpFromDomain(targetIpOrDomain), pingTime.Milliseconds()))
}
}
//Ping UDP
for i := 0; i < 4; i++ {
pingTime, err := UDPPing(targetIpOrDomain)
if err != nil {
results.UDP = append(results.UDP, "Reply from "+resolveIpFromDomain(targetIpOrDomain)+": "+err.Error())
} else {
results.UDP = append(results.UDP, fmt.Sprintf("Reply from %s: Time=%dms", resolveIpFromDomain(targetIpOrDomain), pingTime.Milliseconds()))
}
}
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
}
func resolveIpFromDomain(targetIpOrDomain string) string {
//Resolve target ip address
targetIpAddrString := ""
ipAddr, err := net.ResolveIPAddr("ip", targetIpOrDomain)
if err != nil {
targetIpAddrString = targetIpOrDomain
} else {
targetIpAddrString = ipAddr.IP.String()
}
return targetIpAddrString
}

View File

@ -0,0 +1,28 @@
package netutils_test
import (
"testing"
"imuslab.com/zoraxy/mod/netutils"
)
func TestHandleTraceRoute(t *testing.T) {
results, err := netutils.TraceRoute("imuslab.com", 64)
if err != nil {
t.Fatal(err)
}
t.Log(results)
}
func TestHandlePing(t *testing.T) {
ipOrDomain := "example.com"
realIP, pingTime, ttl, err := netutils.PingIP(ipOrDomain)
if err != nil {
t.Fatal("Error:", err)
return
}
t.Log(realIP, pingTime, ttl)
}

View File

@ -0,0 +1,81 @@
package netutils
import (
"fmt"
"net"
"time"
)
// TCP ping
func TCPPing(ipOrDomain string) (time.Duration, error) {
start := time.Now()
conn, err := net.DialTimeout("tcp", ipOrDomain+":80", 3*time.Second)
if err != nil {
return 0, fmt.Errorf("failed to establish TCP connection: %v", err)
}
defer conn.Close()
elapsed := time.Since(start)
pingTime := elapsed.Round(time.Millisecond)
return pingTime, nil
}
// UDP Ping
func UDPPing(ipOrDomain string) (time.Duration, error) {
start := time.Now()
conn, err := net.DialTimeout("udp", ipOrDomain+":80", 3*time.Second)
if err != nil {
return 0, fmt.Errorf("failed to establish UDP connection: %v", err)
}
defer conn.Close()
elapsed := time.Since(start)
pingTime := elapsed.Round(time.Millisecond)
return pingTime, nil
}
// Traditional ICMP ping
func PingIP(ipOrDomain string) (string, time.Duration, int, error) {
ipAddr, err := net.ResolveIPAddr("ip", ipOrDomain)
if err != nil {
return "", 0, 0, fmt.Errorf("failed to resolve IP address: %v", err)
}
ip := ipAddr.IP.String()
start := time.Now()
conn, err := net.Dial("ip:icmp", ip)
if err != nil {
return ip, 0, 0, fmt.Errorf("failed to establish ICMP connection: %v", err)
}
defer conn.Close()
icmpMsg := []byte{8, 0, 0, 0, 0, 1, 0, 0}
_, err = conn.Write(icmpMsg)
if err != nil {
return ip, 0, 0, fmt.Errorf("failed to send ICMP message: %v", err)
}
reply := make([]byte, 1500)
err = conn.SetReadDeadline(time.Now().Add(3 * time.Second))
if err != nil {
return ip, 0, 0, fmt.Errorf("failed to set read deadline: %v", err)
}
_, err = conn.Read(reply)
if err != nil {
return ip, 0, 0, fmt.Errorf("failed to read ICMP reply: %v", err)
}
elapsed := time.Since(start)
pingTime := elapsed.Round(time.Millisecond)
ttl := int(reply[8])
return ip, pingTime, ttl, nil
}

View File

@ -0,0 +1,212 @@
package netutils
import (
"fmt"
"net"
"os"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
)
const (
protocolICMP = 1
)
// liveTraceRoute return realtime tracing information to live response handler
func liveTraceRoute(dst string, maxHops int, liveRespHandler func(string)) error {
timeout := time.Second * 3
// resolve the host name to an IP address
ipAddr, err := net.ResolveIPAddr("ip4", dst)
if err != nil {
return fmt.Errorf("failed to resolve IP address for %s: %v", dst, err)
}
// create a socket to listen for incoming ICMP packets
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
return fmt.Errorf("failed to create ICMP listener: %v", err)
}
defer conn.Close()
id := os.Getpid() & 0xffff
seq := 0
loop_ttl:
for ttl := 1; ttl <= maxHops; ttl++ {
// set the TTL on the socket
if err := conn.IPv4PacketConn().SetTTL(ttl); err != nil {
return fmt.Errorf("failed to set TTL: %v", err)
}
seq++
// create an ICMP message
msg := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: id,
Seq: seq,
Data: []byte("zoraxy_trace"),
},
}
// serialize the ICMP message
msgBytes, err := msg.Marshal(nil)
if err != nil {
return fmt.Errorf("failed to serialize ICMP message: %v", err)
}
// send the ICMP message
start := time.Now()
if _, err := conn.WriteTo(msgBytes, ipAddr); err != nil {
//log.Printf("%d: %v", ttl, err)
liveRespHandler(fmt.Sprintf("%d: %v", ttl, err))
continue loop_ttl
}
// listen for the reply
replyBytes := make([]byte, 1500)
if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return fmt.Errorf("failed to set read deadline: %v", err)
}
for i := 0; i < 3; i++ {
n, peer, err := conn.ReadFrom(replyBytes)
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
//fmt.Printf("%d: *\n", ttl)
liveRespHandler(fmt.Sprintf("%d: *\n", ttl))
continue loop_ttl
} else {
liveRespHandler(fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
}
continue
}
// parse the ICMP message
replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n])
if err != nil {
liveRespHandler(fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
continue
}
// check if the reply is an echo reply
if replyMsg.Type == ipv4.ICMPTypeEchoReply {
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok || echoReply.ID != id || echoReply.Seq != seq {
continue
}
liveRespHandler(fmt.Sprintf("%d: %v %v\n", ttl, peer, time.Since(start)))
break loop_ttl
}
if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok || echoReply.ID != id || echoReply.Seq != seq {
continue
}
var raddr = peer.String()
names, _ := net.LookupAddr(raddr)
if len(names) > 0 {
raddr = names[0] + " (" + raddr + ")"
} else {
raddr = raddr + " (" + raddr + ")"
}
liveRespHandler(fmt.Sprintf("%d: %v %v\n", ttl, raddr, time.Since(start)))
continue loop_ttl
}
}
}
return nil
}
// Standard traceroute, return results after complete
func traceroute(dst string, maxHops int) ([]string, error) {
results := []string{}
timeout := time.Second * 3
// resolve the host name to an IP address
ipAddr, err := net.ResolveIPAddr("ip4", dst)
if err != nil {
return results, fmt.Errorf("failed to resolve IP address for %s: %v", dst, err)
}
// create a socket to listen for incoming ICMP packets
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
return results, fmt.Errorf("failed to create ICMP listener: %v", err)
}
defer conn.Close()
id := os.Getpid() & 0xffff
seq := 0
loop_ttl:
for ttl := 1; ttl <= maxHops; ttl++ {
// set the TTL on the socket
if err := conn.IPv4PacketConn().SetTTL(ttl); err != nil {
return results, fmt.Errorf("failed to set TTL: %v", err)
}
seq++
// create an ICMP message
msg := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: id,
Seq: seq,
Data: []byte("zoraxy_trace"),
},
}
// serialize the ICMP message
msgBytes, err := msg.Marshal(nil)
if err != nil {
return results, fmt.Errorf("failed to serialize ICMP message: %v", err)
}
// send the ICMP message
start := time.Now()
if _, err := conn.WriteTo(msgBytes, ipAddr); err != nil {
//log.Printf("%d: %v", ttl, err)
results = append(results, fmt.Sprintf("%d: %v", ttl, err))
continue loop_ttl
}
// listen for the reply
replyBytes := make([]byte, 1500)
if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return results, fmt.Errorf("failed to set read deadline: %v", err)
}
for i := 0; i < 3; i++ {
n, peer, err := conn.ReadFrom(replyBytes)
if err != nil {
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
//fmt.Printf("%d: *\n", ttl)
results = append(results, fmt.Sprintf("%d: *", ttl))
continue loop_ttl
} else {
results = append(results, fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
}
continue
}
// parse the ICMP message
replyMsg, err := icmp.ParseMessage(protocolICMP, replyBytes[:n])
if err != nil {
results = append(results, fmt.Sprintf("%d: Failed to parse ICMP message: %v", ttl, err))
continue
}
// check if the reply is an echo reply
if replyMsg.Type == ipv4.ICMPTypeEchoReply {
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok || echoReply.ID != id || echoReply.Seq != seq {
continue
}
results = append(results, fmt.Sprintf("%d: %v %v", ttl, peer, time.Since(start)))
break loop_ttl
}
if replyMsg.Type == ipv4.ICMPTypeTimeExceeded {
echoReply, ok := msg.Body.(*icmp.Echo)
if !ok || echoReply.ID != id || echoReply.Seq != seq {
continue
}
var raddr = peer.String()
names, _ := net.LookupAddr(raddr)
if len(names) > 0 {
raddr = names[0] + " (" + raddr + ")"
} else {
raddr = raddr + " (" + raddr + ")"
}
results = append(results, fmt.Sprintf("%d: %v %v", ttl, raddr, time.Since(start)))
continue loop_ttl
}
}
}
return results, nil
}

199
src/mod/netutils/whois.go Normal file
View File

@ -0,0 +1,199 @@
package netutils
import (
"net"
"strings"
"time"
)
type WHOISResult struct {
DomainName string `json:"domainName"`
RegistryDomainID string `json:"registryDomainID"`
Registrar string `json:"registrar"`
UpdatedDate time.Time `json:"updatedDate"`
CreationDate time.Time `json:"creationDate"`
ExpiryDate time.Time `json:"expiryDate"`
RegistrantID string `json:"registrantID"`
RegistrantName string `json:"registrantName"`
RegistrantEmail string `json:"registrantEmail"`
AdminID string `json:"adminID"`
AdminName string `json:"adminName"`
AdminEmail string `json:"adminEmail"`
TechID string `json:"techID"`
TechName string `json:"techName"`
TechEmail string `json:"techEmail"`
NameServers []string `json:"nameServers"`
DNSSEC string `json:"dnssec"`
}
func ParseWHOISResponse(response string) (WHOISResult, error) {
result := WHOISResult{}
lines := strings.Split(response, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Domain Name:") {
result.DomainName = strings.TrimSpace(strings.TrimPrefix(line, "Domain Name:"))
} else if strings.HasPrefix(line, "Registry Domain ID:") {
result.RegistryDomainID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Domain ID:"))
} else if strings.HasPrefix(line, "Registrar:") {
result.Registrar = strings.TrimSpace(strings.TrimPrefix(line, "Registrar:"))
} else if strings.HasPrefix(line, "Updated Date:") {
dateStr := strings.TrimSpace(strings.TrimPrefix(line, "Updated Date:"))
updatedDate, err := time.Parse("2006-01-02T15:04:05Z", dateStr)
if err == nil {
result.UpdatedDate = updatedDate
}
} else if strings.HasPrefix(line, "Creation Date:") {
dateStr := strings.TrimSpace(strings.TrimPrefix(line, "Creation Date:"))
creationDate, err := time.Parse("2006-01-02T15:04:05Z", dateStr)
if err == nil {
result.CreationDate = creationDate
}
} else if strings.HasPrefix(line, "Registry Expiry Date:") {
dateStr := strings.TrimSpace(strings.TrimPrefix(line, "Registry Expiry Date:"))
expiryDate, err := time.Parse("2006-01-02T15:04:05Z", dateStr)
if err == nil {
result.ExpiryDate = expiryDate
}
} else if strings.HasPrefix(line, "Registry Registrant ID:") {
result.RegistrantID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Registrant ID:"))
} else if strings.HasPrefix(line, "Registrant Name:") {
result.RegistrantName = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Name:"))
} else if strings.HasPrefix(line, "Registrant Email:") {
result.RegistrantEmail = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Email:"))
} else if strings.HasPrefix(line, "Registry Admin ID:") {
result.AdminID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Admin ID:"))
} else if strings.HasPrefix(line, "Admin Name:") {
result.AdminName = strings.TrimSpace(strings.TrimPrefix(line, "Admin Name:"))
} else if strings.HasPrefix(line, "Admin Email:") {
result.AdminEmail = strings.TrimSpace(strings.TrimPrefix(line, "Admin Email:"))
} else if strings.HasPrefix(line, "Registry Tech ID:") {
result.TechID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Tech ID:"))
} else if strings.HasPrefix(line, "Tech Name:") {
result.TechName = strings.TrimSpace(strings.TrimPrefix(line, "Tech Name:"))
} else if strings.HasPrefix(line, "Tech Email:") {
result.TechEmail = strings.TrimSpace(strings.TrimPrefix(line, "Tech Email:"))
} else if strings.HasPrefix(line, "Name Server:") {
ns := strings.TrimSpace(strings.TrimPrefix(line, "Name Server:"))
result.NameServers = append(result.NameServers, ns)
} else if strings.HasPrefix(line, "DNSSEC:") {
result.DNSSEC = strings.TrimSpace(strings.TrimPrefix(line, "DNSSEC:"))
}
}
return result, nil
}
type WhoisIpLookupEntry struct {
NetRange string
CIDR string
NetName string
NetHandle string
Parent string
NetType string
OriginAS string
Organization Organization
RegDate time.Time
Updated time.Time
Ref string
}
type Organization struct {
OrgName string
OrgId string
Address string
City string
StateProv string
PostalCode string
Country string
/*
RegDate time.Time
Updated time.Time
OrgTechHandle string
OrgTechName string
OrgTechPhone string
OrgTechEmail string
OrgAbuseHandle string
OrgAbuseName string
OrgAbusePhone string
OrgAbuseEmail string
OrgRoutingHandle string
OrgRoutingName string
OrgRoutingPhone string
OrgRoutingEmail string
*/
}
func ParseWhoisIpData(data string) (WhoisIpLookupEntry, error) {
var entry WhoisIpLookupEntry = WhoisIpLookupEntry{}
var org Organization = Organization{}
lines := strings.Split(data, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "NetRange:") {
entry.NetRange = strings.TrimSpace(strings.TrimPrefix(line, "NetRange:"))
} else if strings.HasPrefix(line, "CIDR:") {
entry.CIDR = strings.TrimSpace(strings.TrimPrefix(line, "CIDR:"))
} else if strings.HasPrefix(line, "NetName:") {
entry.NetName = strings.TrimSpace(strings.TrimPrefix(line, "NetName:"))
} else if strings.HasPrefix(line, "NetHandle:") {
entry.NetHandle = strings.TrimSpace(strings.TrimPrefix(line, "NetHandle:"))
} else if strings.HasPrefix(line, "Parent:") {
entry.Parent = strings.TrimSpace(strings.TrimPrefix(line, "Parent:"))
} else if strings.HasPrefix(line, "NetType:") {
entry.NetType = strings.TrimSpace(strings.TrimPrefix(line, "NetType:"))
} else if strings.HasPrefix(line, "OriginAS:") {
entry.OriginAS = strings.TrimSpace(strings.TrimPrefix(line, "OriginAS:"))
} else if strings.HasPrefix(line, "Organization:") {
org.OrgName = strings.TrimSpace(strings.TrimPrefix(line, "Organization:"))
} else if strings.HasPrefix(line, "OrgId:") {
org.OrgId = strings.TrimSpace(strings.TrimPrefix(line, "OrgId:"))
} else if strings.HasPrefix(line, "Address:") {
org.Address = strings.TrimSpace(strings.TrimPrefix(line, "Address:"))
} else if strings.HasPrefix(line, "City:") {
org.City = strings.TrimSpace(strings.TrimPrefix(line, "City:"))
} else if strings.HasPrefix(line, "StateProv:") {
org.StateProv = strings.TrimSpace(strings.TrimPrefix(line, "StateProv:"))
} else if strings.HasPrefix(line, "PostalCode:") {
org.PostalCode = strings.TrimSpace(strings.TrimPrefix(line, "PostalCode:"))
} else if strings.HasPrefix(line, "Country:") {
org.Country = strings.TrimSpace(strings.TrimPrefix(line, "Country:"))
} else if strings.HasPrefix(line, "RegDate:") {
entry.RegDate, _ = parseDate(strings.TrimSpace(strings.TrimPrefix(line, "RegDate:")))
} else if strings.HasPrefix(line, "Updated:") {
entry.Updated, _ = parseDate(strings.TrimSpace(strings.TrimPrefix(line, "Updated:")))
} else if strings.HasPrefix(line, "Ref:") {
entry.Ref = strings.TrimSpace(strings.TrimPrefix(line, "Ref:"))
}
}
entry.Organization = org
return entry, nil
}
func parseDate(dateStr string) (time.Time, error) {
dateLayout := "2006-01-02"
date, err := time.Parse(dateLayout, strings.TrimSpace(dateStr))
if err != nil {
return time.Time{}, err
}
return date, nil
}
func isDomainName(input string) bool {
ip := net.ParseIP(input)
if ip != nil {
// Check if it's IPv4 or IPv6
if ip.To4() != nil {
return false
} else if ip.To16() != nil {
return false
}
}
_, err := net.LookupHost(input)
return err == nil
}

100
src/mod/pathrule/handler.go Normal file
View File

@ -0,0 +1,100 @@
package pathrule
import (
"encoding/json"
"net/http"
"strconv"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/utils"
)
/*
handler.go
This script handles pathblock api
*/
func (h *Handler) HandleListBlockingPath(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(h.BlockingPaths)
utils.SendJSONResponse(w, string(js))
}
func (h *Handler) HandleAddBlockingPath(w http.ResponseWriter, r *http.Request) {
matchingPath, err := utils.PostPara(r, "matchingPath")
if err != nil {
utils.SendErrorResponse(w, "invalid matching path given")
return
}
exactMatch, err := utils.PostPara(r, "exactMatch")
if err != nil {
utils.SendErrorResponse(w, "invalid exact match value given")
return
}
statusCodeString, err := utils.PostPara(r, "statusCode")
if err != nil {
utils.SendErrorResponse(w, "invalid status code given")
return
}
statusCode, err := strconv.Atoi(statusCodeString)
if err != nil {
utils.SendErrorResponse(w, "invalid status code given")
return
}
enabled, err := utils.PostPara(r, "enabled")
if err != nil {
utils.SendErrorResponse(w, "invalid enabled value given")
return
}
caseSensitive, err := utils.PostPara(r, "caseSensitive")
if err != nil {
utils.SendErrorResponse(w, "invalid case sensitive value given")
return
}
targetBlockingPath := BlockingPath{
UUID: uuid.New().String(),
MatchingPath: matchingPath,
ExactMatch: exactMatch == "true",
StatusCode: statusCode,
CustomHeaders: http.Header{},
CustomHTML: []byte(""),
Enabled: enabled == "true",
CaseSenitive: caseSensitive == "true",
}
err = h.AddBlockingPath(&targetBlockingPath)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
func (h *Handler) HandleRemoveBlockingPath(w http.ResponseWriter, r *http.Request) {
blockerUUID, err := utils.PostPara(r, "uuid")
if err != nil {
utils.SendErrorResponse(w, "invalid uuid given")
return
}
targetRule := h.GetPathBlockerFromUUID(blockerUUID)
if targetRule == nil {
//Not found
utils.SendErrorResponse(w, "target path blocker not found")
return
}
err = h.RemoveBlockingPathByUUID(blockerUUID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}

View File

@ -0,0 +1,174 @@
package pathrule
import (
"encoding/json"
"errors"
"net/http"
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
/*
Pathrules.go
This script handle advance path settings and rules on particular
paths of the incoming requests
*/
type Options struct {
Enabled bool //If the pathrule is enabled.
ConfigFolder string //The folder to store the path blocking config files
}
type BlockingPath struct {
UUID string
MatchingPath string
ExactMatch bool
StatusCode int
CustomHeaders http.Header
CustomHTML []byte
Enabled bool
CaseSenitive bool
}
type Handler struct {
Options *Options
BlockingPaths []*BlockingPath
}
// Create a new path blocker handler
func NewPathRuleHandler(options *Options) *Handler {
//Create folder if not exists
if !utils.FileExists(options.ConfigFolder) {
os.Mkdir(options.ConfigFolder, 0775)
}
//Load the configs from file
//TODO
return &Handler{
Options: options,
BlockingPaths: []*BlockingPath{},
}
}
func (h *Handler) ListBlockingPath() []*BlockingPath {
return h.BlockingPaths
}
// Get the blocker from matching path (path match, ignore tailing slash)
func (h *Handler) GetPathBlockerFromMatchingPath(matchingPath string) *BlockingPath {
for _, blocker := range h.BlockingPaths {
if blocker.MatchingPath == matchingPath {
return blocker
} else if strings.TrimSuffix(blocker.MatchingPath, "/") == strings.TrimSuffix(matchingPath, "/") {
return blocker
}
}
return nil
}
func (h *Handler) GetPathBlockerFromUUID(UUID string) *BlockingPath {
for _, blocker := range h.BlockingPaths {
if blocker.UUID == UUID {
return blocker
}
}
return nil
}
func (h *Handler) AddBlockingPath(pathBlocker *BlockingPath) error {
//Check if the blocker exists
blockerPath := pathBlocker.MatchingPath
targetBlocker := h.GetPathBlockerFromMatchingPath(blockerPath)
if targetBlocker != nil {
//Blocker with the same matching path already exists
return errors.New("path blocker with the same path already exists")
}
h.BlockingPaths = append(h.BlockingPaths, pathBlocker)
//Write the new config to file
return h.SaveBlockerToFile(pathBlocker)
}
func (h *Handler) RemoveBlockingPathByUUID(uuid string) error {
newBlockingList := []*BlockingPath{}
for _, thisBlocker := range h.BlockingPaths {
if thisBlocker.UUID != uuid {
newBlockingList = append(newBlockingList, thisBlocker)
}
}
if len(h.BlockingPaths) == len(newBlockingList) {
//Nothing is removed
return errors.New("given matching path blocker not exists")
}
h.BlockingPaths = newBlockingList
return h.RemoveBlockerFromFile(uuid)
}
func (h *Handler) SaveBlockerToFile(pathBlocker *BlockingPath) error {
saveFilename := filepath.Join(h.Options.ConfigFolder, pathBlocker.UUID)
js, _ := json.MarshalIndent(pathBlocker, "", " ")
return os.WriteFile(saveFilename, js, 0775)
}
func (h *Handler) RemoveBlockerFromFile(uuid string) error {
expectedConfigFile := filepath.Join(h.Options.ConfigFolder, uuid)
if !utils.FileExists(expectedConfigFile) {
return errors.New("config file not found on disk")
}
return os.Remove(expectedConfigFile)
}
// Get all the matching blockers for the given URL path
// return all the path blockers and the max length matching rule
func (h *Handler) GetMatchingBlockers(urlPath string) ([]*BlockingPath, *BlockingPath) {
urlPath = strings.TrimSuffix(urlPath, "/")
matchingBlockers := []*BlockingPath{}
var longestMatchingPrefix *BlockingPath = nil
for _, thisBlocker := range h.BlockingPaths {
if thisBlocker.Enabled == false {
//This blocker is not enabled. Ignore this
continue
}
incomingURLPath := urlPath
matchingPath := strings.TrimSuffix(thisBlocker.MatchingPath, "/")
if !thisBlocker.CaseSenitive {
//This is not case sensitive
incomingURLPath = strings.ToLower(incomingURLPath)
matchingPath = strings.ToLower(matchingPath)
}
if matchingPath == incomingURLPath {
//This blocker have exact url path match
matchingBlockers = append(matchingBlockers, thisBlocker)
if longestMatchingPrefix == nil || len(thisBlocker.MatchingPath) > len(longestMatchingPrefix.MatchingPath) {
longestMatchingPrefix = thisBlocker
}
continue
}
if !thisBlocker.ExactMatch && strings.HasPrefix(incomingURLPath, matchingPath) {
//This blocker have prefix url match
matchingBlockers = append(matchingBlockers, thisBlocker)
if longestMatchingPrefix == nil || len(thisBlocker.MatchingPath) > len(longestMatchingPrefix.MatchingPath) {
longestMatchingPrefix = thisBlocker
}
continue
}
}
return matchingBlockers, longestMatchingPrefix
}

16
src/mod/sshprox/embed.go Normal file
View File

@ -0,0 +1,16 @@
//go:build (windows && amd64) || (linux && mipsle) || (linux && riscv64)
// +build windows,amd64 linux,mipsle linux,riscv64
package sshprox
import "embed"
/*
Bianry embedding
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/LICENSE
gotty embed.FS
)

View File

@ -0,0 +1,18 @@
//go:build linux && 386
// +build linux,386
package sshprox
import "embed"
/*
Bianry embedding for i386 builds
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/gotty_linux_386
//go:embed gotty/.gotty
//go:embed gotty/LICENSE
gotty embed.FS
)

View File

@ -0,0 +1,18 @@
//go:build linux && amd64
// +build linux,amd64
package sshprox
import "embed"
/*
Bianry embedding for AMD64 builds
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/gotty_linux_amd64
//go:embed gotty/.gotty
//go:embed gotty/LICENSE
gotty embed.FS
)

View File

@ -0,0 +1,18 @@
//go:build linux && arm
// +build linux,arm
package sshprox
import "embed"
/*
Bianry embedding for ARM(v6/7) builds
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/gotty_linux_arm
//go:embed gotty/.gotty
//go:embed gotty/LICENSE
gotty embed.FS
)

View File

@ -0,0 +1,18 @@
//go:build linux && arm64
// +build linux,arm64
package sshprox
import "embed"
/*
Bianry embedding for ARM64 builds
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/gotty_linux_arm64
//go:embed gotty/.gotty
//go:embed gotty/LICENSE
gotty embed.FS
)

View File

@ -1,7 +1,6 @@
package sshprox
import (
"embed"
"errors"
"fmt"
"log"
@ -28,16 +27,6 @@ import (
online ssh terminal
*/
/*
Bianry embedding
Make sure when compile, gotty binary exists in static.gotty
*/
var (
//go:embed gotty/*
gotty embed.FS
)
type Manager struct {
StartingPort int
Instances []*Instance

View File

@ -1,10 +1,9 @@
package analytic
import (
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/statistic"
@ -24,105 +23,49 @@ func NewDataLoader(db *database.Database, sc *statistic.Collector) *DataLoader {
}
}
func (d *DataLoader) HandleSummaryList(w http.ResponseWriter, r *http.Request) {
entries, err := d.Database.ListTable("stats")
// GetAllStatisticSummaryInRange return all the statisics within the time frame. The second array is the key (dates) of the statistic
func (d *DataLoader) GetAllStatisticSummaryInRange(start, end string) ([]*statistic.DailySummaryExport, []string, error) {
dailySummaries := []*statistic.DailySummaryExport{}
collectedDates := []string{}
//Generate all the dates in between the range
keys, err := generateDateRange(start, end)
if err != nil {
utils.SendErrorResponse(w, "unable to load data from database")
return
return dailySummaries, collectedDates, err
}
entryDates := []string{}
for _, keypairs := range entries {
entryDates = append(entryDates, string(keypairs[0]))
}
js, _ := json.MarshalIndent(entryDates, "", " ")
utils.SendJSONResponse(w, string(js))
}
func (d *DataLoader) HandleLoadTargetDaySummary(w http.ResponseWriter, r *http.Request) {
day, err := utils.GetPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "id cannot be empty")
return
}
if strings.Contains(day, "-") {
//Must be underscore
day = strings.ReplaceAll(day, "-", "_")
}
if !statistic.IsBeforeToday(day) {
utils.SendErrorResponse(w, "given date is in the future")
return
}
var targetDailySummary statistic.DailySummaryExport
if day == time.Now().Format("2006_01_02") {
targetDailySummary = *d.StatisticCollector.GetExportSummary()
} else {
//Not today data
err = d.Database.Read("stats", day, &targetDailySummary)
if err != nil {
utils.SendErrorResponse(w, "target day data not found")
return
//Load all the data from database
for _, key := range keys {
thisStat := statistic.DailySummaryExport{}
err = d.Database.Read("stats", key, &thisStat)
if err == nil {
dailySummaries = append(dailySummaries, &thisStat)
collectedDates = append(collectedDates, key)
}
}
js, _ := json.Marshal(targetDailySummary)
utils.SendJSONResponse(w, string(js))
return dailySummaries, collectedDates, nil
}
func (d *DataLoader) HandleLoadTargetRangeSummary(w http.ResponseWriter, r *http.Request) {
//Get the start date from POST para
func (d *DataLoader) GetStartAndEndDatesFromRequest(r *http.Request) (string, string, error) {
// Get the start date from POST para
start, err := utils.GetPara(r, "start")
if err != nil {
utils.SendErrorResponse(w, "start date cannot be empty")
return
return "", "", errors.New("start date cannot be empty")
}
if strings.Contains(start, "-") {
//Must be underscore
start = strings.ReplaceAll(start, "-", "_")
}
//Get end date from POST para
// Get end date from POST para
end, err := utils.GetPara(r, "end")
if err != nil {
utils.SendErrorResponse(w, "emd date cannot be empty")
return
return "", "", errors.New("end date cannot be empty")
}
if strings.Contains(end, "-") {
//Must be underscore
end = strings.ReplaceAll(end, "-", "_")
}
//Generate all the dates in between the range
keys, err := generateDateRange(start, end)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Load all the data from database
dailySummaries := []*statistic.DailySummaryExport{}
for _, key := range keys {
thisStat := statistic.DailySummaryExport{}
err = d.Database.Read("stats", key, &thisStat)
if err == nil {
dailySummaries = append(dailySummaries, &thisStat)
}
}
//Merge the summaries into one
mergedSummary := mergeDailySummaryExports(dailySummaries)
js, _ := json.Marshal(struct {
Summary *statistic.DailySummaryExport
Records []*statistic.DailySummaryExport
}{
Summary: mergedSummary,
Records: dailySummaries,
})
utils.SendJSONResponse(w, string(js))
return start, end, nil
}

View File

@ -0,0 +1,218 @@
package analytic
import (
"encoding/csv"
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
"time"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/utils"
)
func (d *DataLoader) HandleSummaryList(w http.ResponseWriter, r *http.Request) {
entries, err := d.Database.ListTable("stats")
if err != nil {
utils.SendErrorResponse(w, "unable to load data from database")
return
}
entryDates := []string{}
for _, keypairs := range entries {
entryDates = append(entryDates, string(keypairs[0]))
}
js, _ := json.MarshalIndent(entryDates, "", " ")
utils.SendJSONResponse(w, string(js))
}
func (d *DataLoader) HandleLoadTargetDaySummary(w http.ResponseWriter, r *http.Request) {
day, err := utils.GetPara(r, "id")
if err != nil {
utils.SendErrorResponse(w, "id cannot be empty")
return
}
if strings.Contains(day, "-") {
//Must be underscore
day = strings.ReplaceAll(day, "-", "_")
}
if !statistic.IsBeforeToday(day) {
utils.SendErrorResponse(w, "given date is in the future")
return
}
var targetDailySummary statistic.DailySummaryExport
if day == time.Now().Format("2006_01_02") {
targetDailySummary = *d.StatisticCollector.GetExportSummary()
} else {
//Not today data
err = d.Database.Read("stats", day, &targetDailySummary)
if err != nil {
utils.SendErrorResponse(w, "target day data not found")
return
}
}
js, _ := json.Marshal(targetDailySummary)
utils.SendJSONResponse(w, string(js))
}
func (d *DataLoader) HandleLoadTargetRangeSummary(w http.ResponseWriter, r *http.Request) {
start, end, err := d.GetStartAndEndDatesFromRequest(r)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
dailySummaries, _, err := d.GetAllStatisticSummaryInRange(start, end)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Merge the summaries into one
mergedSummary := mergeDailySummaryExports(dailySummaries)
js, _ := json.Marshal(struct {
Summary *statistic.DailySummaryExport
Records []*statistic.DailySummaryExport
}{
Summary: mergedSummary,
Records: dailySummaries,
})
utils.SendJSONResponse(w, string(js))
}
// Handle exporting of a given range statistics
func (d *DataLoader) HandleRangeExport(w http.ResponseWriter, r *http.Request) {
start, end, err := d.GetStartAndEndDatesFromRequest(r)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
dailySummaries, dates, err := d.GetAllStatisticSummaryInRange(start, end)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
format, err := utils.GetPara(r, "format")
if err != nil {
format = "json"
}
if format == "csv" {
// Create a buffer to store CSV content
var csvContent strings.Builder
// Create a CSV writer
writer := csv.NewWriter(&csvContent)
// Write the header row
header := []string{"Date", "TotalRequest", "ErrorRequest", "ValidRequest", "ForwardTypes", "RequestOrigin", "RequestClientIp", "Referer", "UserAgent", "RequestURL"}
err := writer.Write(header)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Write each data row
for i, item := range dailySummaries {
row := []string{
dates[i],
strconv.FormatInt(item.TotalRequest, 10),
strconv.FormatInt(item.ErrorRequest, 10),
strconv.FormatInt(item.ValidRequest, 10),
// Convert map values to a comma-separated string
strings.Join(mapToStringSlice(item.ForwardTypes), ","),
strings.Join(mapToStringSlice(item.RequestOrigin), ","),
strings.Join(mapToStringSlice(item.RequestClientIp), ","),
strings.Join(mapToStringSlice(item.Referer), ","),
strings.Join(mapToStringSlice(item.UserAgent), ","),
strings.Join(mapToStringSlice(item.RequestURL), ","),
}
err = writer.Write(row)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// Flush the CSV writer
writer.Flush()
// Check for any errors during writing
if err := writer.Error(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Set the response headers
w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", "attachment; filename=analytics_"+start+"_to_"+end+".csv")
// Write the CSV content to the response writer
_, err = w.Write([]byte(csvContent.String()))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} else if format == "json" {
type exportData struct {
Stats []*statistic.DailySummaryExport
Dates []string
}
results := exportData{
Stats: dailySummaries,
Dates: dates,
}
js, _ := json.MarshalIndent(results, "", " ")
w.Header().Set("Content-Disposition", "attachment; filename=analytics_"+start+"_to_"+end+".json")
utils.SendJSONResponse(w, string(js))
} else {
utils.SendErrorResponse(w, "Unsupported export format")
}
}
// Reset all the keys within the given time period
func (d *DataLoader) HandleRangeReset(w http.ResponseWriter, r *http.Request) {
start, end, err := d.GetStartAndEndDatesFromRequest(r)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if r.Method != http.MethodDelete {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
keys, err := generateDateRange(start, end)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
for _, key := range keys {
log.Println("DELETING statistics " + key)
d.Database.Delete("stats", key)
if isTodayDate(key) {
//It is today's date. Also reset statistic collector value
log.Println("RESETING today's in-memory statistics")
d.StatisticCollector.ResetSummaryOfDay()
}
}
utils.SendOK(w)
}

View File

@ -70,3 +70,25 @@ func mergeDailySummaryExports(exports []*statistic.DailySummaryExport) *statisti
return mergedExport
}
func mapToStringSlice(m map[string]int) []string {
slice := make([]string, 0, len(m))
for k := range m {
slice = append(slice, k)
}
return slice
}
func isTodayDate(dateStr string) bool {
today := time.Now().Local().Format("2006-01-02")
inputDate, err := time.Parse("2006-01-02", dateStr)
if err != nil {
inputDate, err = time.Parse("2006_01_02", dateStr)
if err != nil {
fmt.Println("Invalid date format")
return false
}
}
return inputDate.Format("2006-01-02") == today
}

View File

@ -6,6 +6,7 @@ import (
"sync"
"time"
"github.com/microcosm-cc/bluemonday"
"imuslab.com/zoraxy/mod/database"
)
@ -96,6 +97,11 @@ func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *Daily
return &targetSummary
}
// Reset today summary, for debug or restoring injections
func (c *Collector) ResetSummaryOfDay() {
c.DailySummary = newDailySummary()
}
// This function gives the current slot in the 288- 5 minutes interval of the day
func (c *Collector) GetCurrentRealtimeStatIntervalId() int {
now := time.Now()
@ -160,11 +166,15 @@ func (c *Collector) RecordRequest(ri RequestInfo) {
}
//Record the referer
rf, ok := c.DailySummary.Referer.Load(ri.Referer)
p := bluemonday.StripTagsPolicy()
filteredReferer := p.Sanitize(
ri.Referer,
)
rf, ok := c.DailySummary.Referer.Load(filteredReferer)
if !ok {
c.DailySummary.Referer.Store(ri.Referer, 1)
c.DailySummary.Referer.Store(filteredReferer, 1)
} else {
c.DailySummary.Referer.Store(ri.Referer, rf.(int)+1)
c.DailySummary.Referer.Store(filteredReferer, rf.(int)+1)
}
//Record the UserAgent

View File

@ -58,11 +58,23 @@ func forward(conn1 net.Conn, conn2 net.Conn, aTob *int64, bToa *int64) {
wg.Wait()
}
func accept(listener net.Listener) (net.Conn, error) {
func (c *ProxyRelayConfig) accept(listener net.Listener) (net.Conn, error) {
conn, err := listener.Accept()
if err != nil {
return nil, err
}
//Check if connection in blacklist or whitelist
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
if !c.parent.Options.AccessControlHandler(conn) {
time.Sleep(300 * time.Millisecond)
conn.Close()
log.Println("[x]", "Connection from "+addr.IP.String()+" rejected by access control policy")
return nil, errors.New("Connection from " + addr.IP.String() + " rejected by access control policy")
}
}
log.Println("[√]", "accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]")
return conn, err
}
@ -203,7 +215,7 @@ func (c *ProxyRelayConfig) Port2port(port1 string, port2 string, stopChan chan b
}()
for {
conn1, err := accept(listen1)
conn1, err := c.accept(listen1)
if err != nil {
if !c.Running {
return nil
@ -211,7 +223,7 @@ func (c *ProxyRelayConfig) Port2port(port1 string, port2 string, stopChan chan b
continue
}
conn2, err := accept(listen2)
conn2, err := c.accept(listen2)
if err != nil {
if !c.Running {
return nil
@ -224,7 +236,7 @@ func (c *ProxyRelayConfig) Port2port(port1 string, port2 string, stopChan chan b
time.Sleep(time.Duration(c.Timeout) * time.Second)
continue
}
forward(conn1, conn2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
go forward(conn1, conn2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
}
}
@ -248,7 +260,7 @@ func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, sto
//Start blocking loop for accepting connections
for {
conn, err := accept(server)
conn, err := c.accept(server)
if conn == nil || err != nil {
if !c.Running {
//Terminate by stop chan. Exit listener loop
@ -322,7 +334,7 @@ func (c *ProxyRelayConfig) Host2host(address1, address2 string, stopChan chan bo
return nil
}
}
forward(host1, host2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
go forward(host1, host2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
}
return nil

View File

@ -2,8 +2,9 @@ package tcpprox
import (
"errors"
"net"
uuid "github.com/satori/go.uuid"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/database"
)
@ -40,11 +41,14 @@ type ProxyRelayConfig struct {
stopChan chan bool //Stop channel to stop the listener
aTobAccumulatedByteTransfer int64 //Accumulated byte transfer from A to B
bToaAccumulatedByteTransfer int64 //Accumulated byte transfer from B to A
parent *Manager `json:"-"`
}
type Options struct {
Database *database.Database
DefaultTimeout int
Database *database.Database
DefaultTimeout int
AccessControlHandler func(net.Conn) bool
}
type Manager struct {
@ -59,21 +63,39 @@ type Manager struct {
func NewTCProxy(options *Options) *Manager {
options.Database.NewTable("tcprox")
//Load relay configs from db
previousRules := []*ProxyRelayConfig{}
if options.Database.KeyExists("tcprox", "rules") {
options.Database.Read("tcprox", "rules", &previousRules)
}
return &Manager{
//Check if the AccessControlHandler is empty. If yes, set it to always allow access
if options.AccessControlHandler == nil {
options.AccessControlHandler = func(conn net.Conn) bool {
//Always allow access
return true
}
}
//Create a new proxy manager for TCP
thisManager := Manager{
Options: options,
Configs: previousRules,
Connections: 0,
}
//Inject manager into the rules
for _, rule := range previousRules {
rule.parent = &thisManager
}
thisManager.Configs = previousRules
return &thisManager
}
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
//Generate a new config from options
configUUID := uuid.NewV4().String()
configUUID := uuid.New().String()
thisConfig := ProxyRelayConfig{
UUID: configUUID,
Name: config.Name,
@ -85,6 +107,8 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
stopChan: nil,
aTobAccumulatedByteTransfer: 0,
bToaAccumulatedByteTransfer: 0,
parent: m,
}
m.Configs = append(m.Configs, &thisConfig)
m.SaveConfigToDatabase()

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"log"
"net/http"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
@ -92,8 +93,6 @@ func (m *Monitor) ExecuteUptimeCheck() {
Latency: laterncy,
}
//fmt.Println(thisRecord)
} else {
log.Println("Unknown protocol: " + target.Protocol + ". Skipping")
continue
@ -220,9 +219,28 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
func getWebsiteStatus(url string) (int, error) {
resp, err := http.Get(url)
if err != nil {
return 0, err
//Try replace the http with https and vise versa
rewriteURL := ""
if strings.Contains(url, "https://") {
rewriteURL = strings.ReplaceAll(url, "https://", "http://")
} else if strings.Contains(url, "http://") {
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
}
resp, err = http.Get(rewriteURL)
if err != nil {
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
//Invalid downstream reverse proxy settings, but it is online
//return SSL handshake failed
return 525, nil
}
return 0, err
}
defer resp.Body.Close()
status_code := resp.StatusCode
return status_code, nil
}
defer resp.Body.Close()
status_code := resp.StatusCode
resp.Body.Close()
return status_code, nil
}

View File

@ -1,13 +1,11 @@
package utils
import (
"bufio"
"encoding/base64"
"errors"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
@ -40,46 +38,6 @@ func SendOK(w http.ResponseWriter) {
w.Write([]byte("\"OK\""))
}
/*
The paramter move function (mv)
You can find similar things in the PHP version of ArOZ Online Beta. You need to pass in
r (HTTP Request Object)
getParamter (string, aka $_GET['This string])
Will return
Paramter string (if any)
Error (if error)
*/
/*
func Mv(r *http.Request, getParamter string, postMode bool) (string, error) {
if postMode == false {
//Access the paramter via GET
keys, ok := r.URL.Query()[getParamter]
if !ok || len(keys[0]) < 1 {
//log.Println("Url Param " + getParamter +" is missing")
return "", errors.New("GET paramter " + getParamter + " not found or it is empty")
}
// Query()["key"] will return an array of items,
// we only want the single item.
key := keys[0]
return string(key), nil
} else {
//Access the parameter via POST
r.ParseForm()
x := r.Form.Get(getParamter)
if len(x) == 0 || x == "" {
return "", errors.New("POST paramter " + getParamter + " not found or it is empty")
}
return string(x), nil
}
}
*/
// Get GET parameter
func GetPara(r *http.Request, key string) (string, error) {
keys, ok := r.URL.Query()[key]
@ -101,6 +59,40 @@ func PostPara(r *http.Request, key string) (string, error) {
}
}
// Get POST paramter as boolean, accept 1 or true
func PostBool(r *http.Request, key string) (bool, error) {
x, err := PostPara(r, key)
if err != nil {
return false, err
}
x = strings.TrimSpace(x)
if x == "1" || strings.ToLower(x) == "true" {
return true, nil
} else if x == "0" || strings.ToLower(x) == "false" {
return false, nil
}
return false, errors.New("invalid boolean given")
}
// Get POST paramter as int
func PostInt(r *http.Request, key string) (int, error) {
x, err := PostPara(r, key)
if err != nil {
return 0, err
}
x = strings.TrimSpace(x)
rx, err := strconv.Atoi(x)
if err != nil {
return 0, err
}
return rx, nil
}
func FileExists(filename string) bool {
_, err := os.Stat(filename)
if os.IsNotExist(err) {
@ -131,30 +123,6 @@ func TimeToString(targetTime time.Time) string {
return targetTime.Format("2006-01-02 15:04:05")
}
func LoadImageAsBase64(filepath string) (string, error) {
if !FileExists(filepath) {
return "", errors.New("File not exists")
}
f, _ := os.Open(filepath)
reader := bufio.NewReader(f)
content, _ := io.ReadAll(reader)
encoded := base64.StdEncoding.EncodeToString(content)
return string(encoded), nil
}
// Use for redirections
func ConstructRelativePathFromRequestURL(requestURI string, redirectionLocation string) string {
if strings.Count(requestURI, "/") == 1 {
//Already root level
return redirectionLocation
}
for i := 0; i < strings.Count(requestURI, "/")-1; i++ {
redirectionLocation = "../" + redirectionLocation
}
return redirectionLocation
}
// Check if given string in a given slice
func StringInArray(arr []string, str string) bool {
for _, a := range arr {

View File

@ -0,0 +1,406 @@
package filemanager
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
/*
File Manager
This is a simple package that handles file management
under the web server directory
*/
type FileManager struct {
Directory string
}
// Create a new file manager with directory as root
func NewFileManager(directory string) *FileManager {
return &FileManager{
Directory: directory,
}
}
// Handle listing of a given directory
func (fm *FileManager) HandleList(w http.ResponseWriter, r *http.Request) {
directory, err := utils.GetPara(r, "dir")
if err != nil {
utils.SendErrorResponse(w, "invalid directory given")
return
}
// Construct the absolute path to the target directory
targetDir := filepath.Join(fm.Directory, directory)
// Open the target directory
dirEntries, err := os.ReadDir(targetDir)
if err != nil {
utils.SendErrorResponse(w, "unable to open directory")
return
}
// Create a slice to hold the file information
var files []map[string]interface{} = []map[string]interface{}{}
// Iterate through the directory entries
for _, dirEntry := range dirEntries {
fileInfo := make(map[string]interface{})
fileInfo["filename"] = dirEntry.Name()
fileInfo["filepath"] = filepath.Join(directory, dirEntry.Name())
fileInfo["isDir"] = dirEntry.IsDir()
// Get file size and last modified time
finfo, err := dirEntry.Info()
if err != nil {
//unable to load its info. Skip this file
continue
}
fileInfo["lastModified"] = finfo.ModTime().Unix()
if !dirEntry.IsDir() {
// If it's a file, get its size
fileInfo["size"] = finfo.Size()
} else {
// If it's a directory, set size to 0
fileInfo["size"] = 0
}
// Append file info to the list
files = append(files, fileInfo)
}
// Serialize the file info slice to JSON
jsonData, err := json.Marshal(files)
if err != nil {
utils.SendErrorResponse(w, "unable to marshal JSON")
return
}
// Set response headers and send the JSON response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}
// Handle upload of a file (multi-part), 25MB max
func (fm *FileManager) HandleUpload(w http.ResponseWriter, r *http.Request) {
dir, err := utils.PostPara(r, "dir")
if err != nil {
log.Println("no dir given")
utils.SendErrorResponse(w, "invalid dir given")
return
}
// Parse the multi-part form data
err = r.ParseMultipartForm(25 << 20)
if err != nil {
utils.SendErrorResponse(w, "unable to parse form data")
return
}
// Get the uploaded file
file, fheader, err := r.FormFile("file")
if err != nil {
log.Println(err.Error())
utils.SendErrorResponse(w, "unable to get uploaded file")
return
}
defer file.Close()
// Specify the directory where you want to save the uploaded file
uploadDir := filepath.Join(fm.Directory, dir)
if !utils.FileExists(uploadDir) {
utils.SendErrorResponse(w, "upload target directory not exists")
return
}
filename := sanitizeFilename(fheader.Filename)
if !isValidFilename(filename) {
utils.SendErrorResponse(w, "filename contain invalid or reserved characters")
return
}
// Create the file on the server
filePath := filepath.Join(uploadDir, filepath.Base(filename))
out, err := os.Create(filePath)
if err != nil {
utils.SendErrorResponse(w, "unable to create file on the server")
return
}
defer out.Close()
// Copy the uploaded file to the server
_, err = io.Copy(out, file)
if err != nil {
utils.SendErrorResponse(w, "unable to copy file to server")
return
}
// Respond with a success message or appropriate response
utils.SendOK(w)
}
// Handle download of a selected file, serve with content dispose header
func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) {
filename, err := utils.GetPara(r, "file")
if err != nil {
utils.SendErrorResponse(w, "invalid filepath given")
return
}
previewMode, _ := utils.GetPara(r, "preview")
if previewMode == "true" {
// Serve the file using http.ServeFile
filePath := filepath.Join(fm.Directory, filename)
http.ServeFile(w, r, filePath)
} else {
// Trigger a download with content disposition headers
filePath := filepath.Join(fm.Directory, filename)
w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename))
http.ServeFile(w, r, filePath)
}
}
// HandleNewFolder creates a new folder in the specified directory
func (fm *FileManager) HandleNewFolder(w http.ResponseWriter, r *http.Request) {
// Parse the directory name from the request
dirName, err := utils.GetPara(r, "path")
if err != nil {
utils.SendErrorResponse(w, "invalid directory name")
return
}
//Prevent path escape
dirName = strings.ReplaceAll(dirName, "\\", "/")
dirName = strings.ReplaceAll(dirName, "../", "")
// Specify the directory where you want to create the new folder
newFolderPath := filepath.Join(fm.Directory, dirName)
// Check if the folder already exists
if _, err := os.Stat(newFolderPath); os.IsNotExist(err) {
// Create the new folder
err := os.Mkdir(newFolderPath, os.ModePerm)
if err != nil {
utils.SendErrorResponse(w, "unable to create the new folder")
return
}
// Respond with a success message or appropriate response
utils.SendOK(w)
} else {
// If the folder already exists, respond with an error
utils.SendErrorResponse(w, "folder already exists")
}
}
// HandleFileCopy copies a file or directory from the source path to the destination path
func (fm *FileManager) HandleFileCopy(w http.ResponseWriter, r *http.Request) {
// Parse the source and destination paths from the request
srcPath, err := utils.PostPara(r, "srcpath")
if err != nil {
utils.SendErrorResponse(w, "invalid source path")
return
}
destPath, err := utils.PostPara(r, "destpath")
if err != nil {
utils.SendErrorResponse(w, "invalid destination path")
return
}
// Validate and sanitize the source and destination paths
srcPath = filepath.Clean(srcPath)
destPath = filepath.Clean(destPath)
// Construct the absolute paths
absSrcPath := filepath.Join(fm.Directory, srcPath)
absDestPath := filepath.Join(fm.Directory, destPath)
// Check if the source path exists
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
utils.SendErrorResponse(w, "source path does not exist")
return
}
// Check if the destination path exists
if _, err := os.Stat(absDestPath); os.IsNotExist(err) {
utils.SendErrorResponse(w, "destination path does not exist")
return
}
//Join the name to create final paste filename
absDestPath = filepath.Join(absDestPath, filepath.Base(absSrcPath))
//Reject opr if already exists
if utils.FileExists(absDestPath) {
utils.SendErrorResponse(w, "target already exists")
return
}
// Perform the copy operation based on whether the source is a file or directory
if isDir(absSrcPath) {
// Recursive copy for directories
err := copyDirectory(absSrcPath, absDestPath)
if err != nil {
utils.SendErrorResponse(w, fmt.Sprintf("error copying directory: %v", err))
return
}
} else {
// Copy a single file
err := copyFile(absSrcPath, absDestPath)
if err != nil {
utils.SendErrorResponse(w, fmt.Sprintf("error copying file: %v", err))
return
}
}
utils.SendOK(w)
}
func (fm *FileManager) HandleFileMove(w http.ResponseWriter, r *http.Request) {
// Parse the source and destination paths from the request
srcPath, err := utils.GetPara(r, "srcpath")
if err != nil {
utils.SendErrorResponse(w, "invalid source path")
return
}
destPath, err := utils.GetPara(r, "destpath")
if err != nil {
utils.SendErrorResponse(w, "invalid destination path")
return
}
// Validate and sanitize the source and destination paths
srcPath = filepath.Clean(srcPath)
destPath = filepath.Clean(destPath)
// Construct the absolute paths
absSrcPath := filepath.Join(fm.Directory, srcPath)
absDestPath := filepath.Join(fm.Directory, destPath)
// Check if the source path exists
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
utils.SendErrorResponse(w, "source path does not exist")
return
}
// Check if the destination path exists
if _, err := os.Stat(absDestPath); !os.IsNotExist(err) {
utils.SendErrorResponse(w, "destination path already exists")
return
}
// Rename the source to the destination
err = os.Rename(absSrcPath, absDestPath)
if err != nil {
utils.SendErrorResponse(w, fmt.Sprintf("error moving file/directory: %v", err))
return
}
utils.SendOK(w)
}
func (fm *FileManager) HandleFileProperties(w http.ResponseWriter, r *http.Request) {
// Parse the target file or directory path from the request
filePath, err := utils.GetPara(r, "file")
if err != nil {
utils.SendErrorResponse(w, "invalid file path")
return
}
// Construct the absolute path to the target file or directory
absPath := filepath.Join(fm.Directory, filePath)
// Check if the target path exists
_, err = os.Stat(absPath)
if err != nil {
utils.SendErrorResponse(w, "file or directory does not exist")
return
}
// Initialize a map to hold file properties
fileProps := make(map[string]interface{})
fileProps["filename"] = filepath.Base(absPath)
fileProps["filepath"] = filePath
fileProps["isDir"] = isDir(absPath)
// Get file size and last modified time
finfo, err := os.Stat(absPath)
if err != nil {
utils.SendErrorResponse(w, "unable to retrieve file properties")
return
}
fileProps["lastModified"] = finfo.ModTime().Unix()
if !isDir(absPath) {
// If it's a file, get its size
fileProps["size"] = finfo.Size()
} else {
// If it's a directory, calculate its total size containing all child files and folders
totalSize, err := calculateDirectorySize(absPath)
if err != nil {
utils.SendErrorResponse(w, "unable to calculate directory size")
return
}
fileProps["size"] = totalSize
}
// Count the number of sub-files and sub-folders
numSubFiles, numSubFolders, err := countSubFilesAndFolders(absPath)
if err != nil {
utils.SendErrorResponse(w, "unable to count sub-files and sub-folders")
return
}
fileProps["fileCounts"] = numSubFiles
fileProps["folderCounts"] = numSubFolders
// Serialize the file properties to JSON
jsonData, err := json.Marshal(fileProps)
if err != nil {
utils.SendErrorResponse(w, "unable to marshal JSON")
return
}
// Set response headers and send the JSON response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}
// HandleFileDelete deletes a file or directory
func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request) {
// Parse the target file or directory path from the request
filePath, err := utils.PostPara(r, "target")
if err != nil {
utils.SendErrorResponse(w, "invalid file path")
return
}
// Construct the absolute path to the target file or directory
absPath := filepath.Join(fm.Directory, filePath)
// Check if the target path exists
_, err = os.Stat(absPath)
if err != nil {
utils.SendErrorResponse(w, "file or directory does not exist")
return
}
// Delete the file or directory
err = os.RemoveAll(absPath)
if err != nil {
utils.SendErrorResponse(w, "error deleting file or directory")
return
}
// Respond with a success message or appropriate response
utils.SendOK(w)
}

View File

@ -0,0 +1,156 @@
package filemanager
import (
"io"
"os"
"path/filepath"
"strings"
)
// isValidFilename checks if a given filename is safe and valid.
func isValidFilename(filename string) bool {
// Define a list of disallowed characters and reserved names
disallowedChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} // Add more if needed
reservedNames := []string{"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"} // Add more if needed
// Check for disallowed characters
for _, char := range disallowedChars {
if strings.Contains(filename, char) {
return false
}
}
// Check for reserved names (case-insensitive)
lowerFilename := strings.ToUpper(filename)
for _, reserved := range reservedNames {
if lowerFilename == reserved {
return false
}
}
// Check for empty filename
if filename == "" {
return false
}
// The filename is considered valid
return true
}
// sanitizeFilename sanitizes a given filename by removing disallowed characters.
func sanitizeFilename(filename string) string {
disallowedChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} // Add more if needed
// Replace disallowed characters with underscores
for _, char := range disallowedChars {
filename = strings.ReplaceAll(filename, char, "_")
}
return filename
}
// copyFile copies a single file from source to destination
func copyFile(srcPath, destPath string) error {
srcFile, err := os.Open(srcPath)
if err != nil {
return err
}
defer srcFile.Close()
destFile, err := os.Create(destPath)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, srcFile)
if err != nil {
return err
}
return nil
}
// copyDirectory recursively copies a directory and its contents from source to destination
func copyDirectory(srcPath, destPath string) error {
// Create the destination directory
err := os.MkdirAll(destPath, os.ModePerm)
if err != nil {
return err
}
entries, err := os.ReadDir(srcPath)
if err != nil {
return err
}
for _, entry := range entries {
srcEntryPath := filepath.Join(srcPath, entry.Name())
destEntryPath := filepath.Join(destPath, entry.Name())
if entry.IsDir() {
err := copyDirectory(srcEntryPath, destEntryPath)
if err != nil {
return err
}
} else {
err := copyFile(srcEntryPath, destEntryPath)
if err != nil {
return err
}
}
}
return nil
}
// isDir checks if the given path is a directory
func isDir(path string) bool {
fileInfo, err := os.Stat(path)
if err != nil {
return false
}
return fileInfo.IsDir()
}
// calculateDirectorySize calculates the total size of a directory and its contents
func calculateDirectorySize(dirPath string) (int64, error) {
var totalSize int64
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
totalSize += info.Size()
return nil
})
if err != nil {
return 0, err
}
return totalSize, nil
}
// countSubFilesAndFolders counts the number of sub-files and sub-folders within a directory
func countSubFilesAndFolders(dirPath string) (int, int, error) {
var numSubFiles, numSubFolders int
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
numSubFolders++
} else {
numSubFiles++
}
return nil
})
if err != nil {
return 0, 0, err
}
// Subtract 1 from numSubFolders to exclude the root directory itself
return numSubFiles, numSubFolders - 1, nil
}

View File

@ -0,0 +1,88 @@
package webserv
import (
"encoding/json"
"net/http"
"strconv"
"imuslab.com/zoraxy/mod/utils"
)
/*
Handler.go
Handler for web server options change
web server is directly listening to the TCP port
handlers in this script are for setting change only
*/
type StaticWebServerStatus struct {
ListeningPort int
EnableDirectoryListing bool
WebRoot string
Running bool
EnableWebDirManager bool
}
// Handle getting current static web server status
func (ws *WebServer) HandleGetStatus(w http.ResponseWriter, r *http.Request) {
listeningPortInt, _ := strconv.Atoi(ws.option.Port)
currentStatus := StaticWebServerStatus{
ListeningPort: listeningPortInt,
EnableDirectoryListing: ws.option.EnableDirectoryListing,
WebRoot: ws.option.WebRoot,
Running: ws.isRunning,
EnableWebDirManager: ws.option.EnableWebDirManager,
}
js, _ := json.Marshal(currentStatus)
utils.SendJSONResponse(w, string(js))
}
// Handle request for starting the static web server
func (ws *WebServer) HandleStartServer(w http.ResponseWriter, r *http.Request) {
err := ws.Start()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle request for stopping the static web server
func (ws *WebServer) HandleStopServer(w http.ResponseWriter, r *http.Request) {
err := ws.Stop()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle change server listening port request
func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
newPort, err := utils.PostInt(r, "port")
if err != nil {
utils.SendErrorResponse(w, "invalid port number given")
return
}
err = ws.ChangePort(strconv.Itoa(newPort))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Change enable directory listing settings
func (ws *WebServer) SetEnableDirectoryListing(w http.ResponseWriter, r *http.Request) {
enableList, err := utils.PostBool(r, "enable")
if err != nil {
utils.SendErrorResponse(w, "invalid setting given")
return
}
ws.option.EnableDirectoryListing = enableList
utils.SendOK(w)
}

View File

@ -0,0 +1,41 @@
package webserv
import (
"net/http"
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/utils"
)
// Convert a request path (e.g. /index.html) into physical path on disk
func (ws *WebServer) resolveFileDiskPath(requestPath string) string {
fileDiskpath := filepath.Join(ws.option.WebRoot, "html", requestPath)
//Force convert it to slash even if the host OS is on Windows
fileDiskpath = filepath.Clean(fileDiskpath)
fileDiskpath = strings.ReplaceAll(fileDiskpath, "\\", "/")
return fileDiskpath
}
// File server middleware to handle directory listing (and future expansion)
func (ws *WebServer) fsMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !ws.option.EnableDirectoryListing {
if strings.HasSuffix(r.URL.Path, "/") {
//This is a folder. Let check if index exists
if utils.FileExists(filepath.Join(ws.resolveFileDiskPath(r.URL.Path), "index.html")) {
} else if utils.FileExists(filepath.Join(ws.resolveFileDiskPath(r.URL.Path), "index.htm")) {
} else {
http.NotFound(w, r)
return
}
}
}
h.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,61 @@
<html>
<head>
<title>Hello Zoraxy</title>
<style>
body {
font-family: Tahoma, sans-serif;
background-color: #f6f6f6;
color: #2d2e30;
}
.sectionHeader{
background-color: #c4d0d9;
padding: 0.1em;
}
.sectionHeader h3{
text-align: center;
}
.container{
margin: 4em;
margin-left: 10em;
margin-right: 10em;
background-color: #fefefe;
}
@media (max-width:960px) {
.container{
margin-left: 1em;
margin-right: 1em;
}
.sectionHeader{
padding-left: 1em;
padding-right: 1em;
}
}
.textcontainer{
padding: 1em;
}
</style>
</head>
<body>
<div class="container">
<div class="sectionHeader">
<h3>Welcome to Zoraxy Static Web Server!</h3>
</div>
<div class="textcontainer">
<p>If you see this page, that means your static web server is running.<br>
By default, all the html files are stored under <code>./web/html/</code>
relative to the zoraxy runtime directory.<br>
You can upload your html files to your web directory via the <b>Web Directory Manager</b>.
</p>
<p>
For online documentation, please refer to <a href="//zoraxy.arozos.com">zoraxy.arozos.com</a> or the <a href="https://github.com/tobychui/zoraxy/wiki">project wiki</a>.<br>
Thank you for using Zoraxy!
</p>
</div>
</div>
</body>
</html>

18
src/mod/webserv/utils.go Normal file
View File

@ -0,0 +1,18 @@
package webserv
import (
"net"
)
// IsPortInUse checks if a port is in use.
func IsPortInUse(port string) bool {
listener, err := net.Listen("tcp", "localhost:"+port)
if err != nil {
// If there was an error, the port is in use.
return true
}
defer listener.Close()
// No error means the port is available.
return false
}

195
src/mod/webserv/webserv.go Normal file
View File

@ -0,0 +1,195 @@
package webserv
import (
"embed"
_ "embed"
"errors"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/utils"
"imuslab.com/zoraxy/mod/webserv/filemanager"
)
/*
Static Web Server package
This module host a static web server
*/
//go:embed templates/*
var templates embed.FS
type WebServerOptions struct {
Port string //Port for listening
EnableDirectoryListing bool //Enable listing of directory
WebRoot string //Folder for stroing the static web folders
EnableWebDirManager bool //Enable web file manager to handle files in web directory
Sysdb *database.Database //Database for storing configs
}
type WebServer struct {
FileManager *filemanager.FileManager
mux *http.ServeMux
server *http.Server
option *WebServerOptions
isRunning bool
mu sync.Mutex
}
// NewWebServer creates a new WebServer instance. One instance only
func NewWebServer(options *WebServerOptions) *WebServer {
if !utils.FileExists(options.WebRoot) {
//Web root folder not exists. Create one with default templates
os.MkdirAll(filepath.Join(options.WebRoot, "html"), 0775)
os.MkdirAll(filepath.Join(options.WebRoot, "templates"), 0775)
indexTemplate, err := templates.ReadFile("templates/index.html")
if err != nil {
log.Println("Failed to read static wev server template file: ", err.Error())
} else {
os.WriteFile(filepath.Join(options.WebRoot, "html", "index.html"), indexTemplate, 0775)
}
}
//Create a new file manager if it is enabled
var newDirManager *filemanager.FileManager
if options.EnableWebDirManager {
fm := filemanager.NewFileManager(filepath.Join(options.WebRoot, "/html"))
newDirManager = fm
}
//Create new table to store the config
options.Sysdb.NewTable("webserv")
return &WebServer{
mux: http.NewServeMux(),
FileManager: newDirManager,
option: options,
isRunning: false,
mu: sync.Mutex{},
}
}
// Restore the configuration to previous config
func (ws *WebServer) RestorePreviousState() {
//Set the port
port := ws.option.Port
ws.option.Sysdb.Read("webserv", "port", &port)
ws.option.Port = port
//Set the enable directory list
enableDirList := ws.option.EnableDirectoryListing
ws.option.Sysdb.Read("webserv", "dirlist", &enableDirList)
ws.option.EnableDirectoryListing = enableDirList
//Check the running state
webservRunning := false
ws.option.Sysdb.Read("webserv", "enabled", &webservRunning)
if webservRunning {
ws.Start()
} else {
ws.Stop()
}
}
// ChangePort changes the server's port.
func (ws *WebServer) ChangePort(port string) error {
if IsPortInUse(port) {
return errors.New("Selected port is used by another process")
}
if ws.isRunning {
if err := ws.Stop(); err != nil {
return err
}
}
ws.option.Port = port
ws.server.Addr = ":" + port
err := ws.Start()
if err != nil {
return err
}
ws.option.Sysdb.Write("webserv", "port", port)
return nil
}
// Start starts the web server.
func (ws *WebServer) Start() error {
ws.mu.Lock()
defer ws.mu.Unlock()
//Check if server already running
if ws.isRunning {
return fmt.Errorf("web server is already running")
}
//Check if the port is usable
if IsPortInUse(ws.option.Port) {
return errors.New("Port already in use or access denied by host OS")
}
//Dispose the old mux and create a new one
ws.mux = http.NewServeMux()
//Create a static web server
fs := http.FileServer(http.Dir(filepath.Join(ws.option.WebRoot, "html")))
ws.mux.Handle("/", ws.fsMiddleware(fs))
ws.server = &http.Server{
Addr: ":" + ws.option.Port,
Handler: ws.mux,
}
go func() {
if err := ws.server.ListenAndServe(); err != nil {
if err != http.ErrServerClosed {
fmt.Printf("Web server error: %v\n", err)
}
}
}()
log.Println("Static Web Server started. Listeing on :" + ws.option.Port)
ws.isRunning = true
ws.option.Sysdb.Write("webserv", "enabled", true)
return nil
}
// Stop stops the web server.
func (ws *WebServer) Stop() error {
ws.mu.Lock()
defer ws.mu.Unlock()
if !ws.isRunning {
return fmt.Errorf("web server is not running")
}
if err := ws.server.Close(); err != nil {
return err
}
ws.isRunning = false
ws.option.Sysdb.Write("webserv", "enabled", false)
return nil
}
// UpdateDirectoryListing enables or disables directory listing.
func (ws *WebServer) UpdateDirectoryListing(enable bool) {
ws.option.EnableDirectoryListing = enable
ws.option.Sysdb.Write("webserv", "dirlist", enable)
}
// Close stops the web server without returning an error.
func (ws *WebServer) Close() {
ws.Stop()
}

View File

@ -2,7 +2,6 @@ package main
import (
"encoding/json"
"log"
"net/http"
"path/filepath"
"sort"
@ -25,49 +24,71 @@ func ReverseProxtInit() {
inboundPort := 80
if sysdb.KeyExists("settings", "inbound") {
sysdb.Read("settings", "inbound", &inboundPort)
log.Println("Serving inbound port ", inboundPort)
SystemWideLogger.Println("Serving inbound port ", inboundPort)
} else {
log.Println("Inbound port not set. Using default (80)")
SystemWideLogger.Println("Inbound port not set. Using default (80)")
}
useTls := false
sysdb.Read("settings", "usetls", &useTls)
if useTls {
log.Println("TLS mode enabled. Serving proxxy request with TLS")
SystemWideLogger.Println("TLS mode enabled. Serving proxxy request with TLS")
} else {
log.Println("TLS mode disabled. Serving proxy request with plain http")
SystemWideLogger.Println("TLS mode disabled. Serving proxy request with plain http")
}
forceLatestTLSVersion := false
sysdb.Read("settings", "forceLatestTLS", &forceLatestTLSVersion)
if forceLatestTLSVersion {
SystemWideLogger.Println("Force latest TLS mode enabled. Minimum TLS LS version is set to v1.2")
} else {
SystemWideLogger.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
}
listenOnPort80 := false
sysdb.Read("settings", "listenP80", &listenOnPort80)
if listenOnPort80 {
SystemWideLogger.Println("Port 80 listener enabled")
} else {
SystemWideLogger.Println("Port 80 listener disabled")
}
forceHttpsRedirect := false
sysdb.Read("settings", "redirect", &forceHttpsRedirect)
if forceHttpsRedirect {
log.Println("Force HTTPS mode enabled")
SystemWideLogger.Println("Force HTTPS mode enabled")
//Port 80 listener must be enabled to perform http -> https redirect
listenOnPort80 = true
} else {
log.Println("Force HTTPS mode disabled")
SystemWideLogger.Println("Force HTTPS mode disabled")
}
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
HostUUID: nodeUUID,
Port: inboundPort,
UseTls: useTls,
ForceTLSLatest: forceLatestTLSVersion,
ListenOnPort80: listenOnPort80,
ForceHttpsRedirect: forceHttpsRedirect,
TlsManager: tlsCertManager,
RedirectRuleTable: redirectTable,
GeodbStore: geodbStore,
StatisticCollector: statisticCollector,
WebDirectory: *staticWebServerRoot,
})
if err != nil {
log.Println(err.Error())
SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err)
return
}
dynamicProxyRouter = dprouter
//Load all conf from files
confs, _ := filepath.Glob("./conf/*.config")
confs, _ := filepath.Glob("./conf/proxy/*.config")
for _, conf := range confs {
record, err := LoadReverseProxyConfig(conf)
if err != nil {
log.Println("Failed to load "+filepath.Base(conf), err.Error())
SystemWideLogger.PrintAndLog("Proxy", "Failed to load config file: "+filepath.Base(conf), err)
return
}
@ -78,24 +99,28 @@ func ReverseProxtInit() {
})
} else if record.ProxyType == "subd" {
dynamicProxyRouter.AddSubdomainRoutingService(&dynamicproxy.SubdOptions{
MatchingDomain: record.Rootname,
Domain: record.ProxyTarget,
RequireTLS: record.UseTLS,
SkipCertValidations: record.SkipTlsValidation,
RequireBasicAuth: record.RequireBasicAuth,
BasicAuthCredentials: record.BasicAuthCredentials,
MatchingDomain: record.Rootname,
Domain: record.ProxyTarget,
RequireTLS: record.UseTLS,
BypassGlobalTLS: record.BypassGlobalTLS,
SkipCertValidations: record.SkipTlsValidation,
RequireBasicAuth: record.RequireBasicAuth,
BasicAuthCredentials: record.BasicAuthCredentials,
BasicAuthExceptionRules: record.BasicAuthExceptionRules,
})
} else if record.ProxyType == "vdir" {
dynamicProxyRouter.AddVirtualDirectoryProxyService(&dynamicproxy.VdirOptions{
RootName: record.Rootname,
Domain: record.ProxyTarget,
RequireTLS: record.UseTLS,
SkipCertValidations: record.SkipTlsValidation,
RequireBasicAuth: record.RequireBasicAuth,
BasicAuthCredentials: record.BasicAuthCredentials,
RootName: record.Rootname,
Domain: record.ProxyTarget,
RequireTLS: record.UseTLS,
BypassGlobalTLS: record.BypassGlobalTLS,
SkipCertValidations: record.SkipTlsValidation,
RequireBasicAuth: record.RequireBasicAuth,
BasicAuthCredentials: record.BasicAuthCredentials,
BasicAuthExceptionRules: record.BasicAuthExceptionRules,
})
} else {
log.Println("Unsupported endpoint type: " + record.ProxyType + ". Skipping " + filepath.Base(conf))
SystemWideLogger.PrintAndLog("Proxy", "Unsupported endpoint type: "+record.ProxyType+". Skipping "+filepath.Base(conf), nil)
}
}
@ -104,7 +129,7 @@ func ReverseProxtInit() {
//reverse proxy server in front of this service
time.Sleep(300 * time.Millisecond)
dynamicProxyRouter.StartProxyService()
log.Println("Dynamic Reverse Proxy service started")
SystemWideLogger.Println("Dynamic Reverse Proxy service started")
//Add all proxy services to uptime monitor
//Create a uptime monitor service
@ -115,7 +140,7 @@ func ReverseProxtInit() {
Interval: 300, //5 minutes
MaxRecordsStore: 288, //1 day
})
log.Println("Uptime Monitor background service started")
SystemWideLogger.Println("Uptime Monitor background service started")
}()
}
@ -167,6 +192,13 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
useTLS := (tls == "true")
bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS")
if bypassGlobalTLS == "" {
bypassGlobalTLS = "false"
}
useBypassGlobalTLS := bypassGlobalTLS == "true"
stv, _ := utils.PostPara(r, "tlsval")
if stv == "" {
stv = "false"
@ -227,6 +259,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
RootName: vdir,
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
@ -244,6 +277,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
MatchingDomain: subdomain,
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
@ -268,11 +302,12 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
Rootname: rootname,
ProxyTarget: endpoint,
UseTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipTlsValidation: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
}
SaveReverseProxyConfig(&thisProxyConfigRecord)
SaveReverseProxyConfigToFile(&thisProxyConfigRecord)
//Update utm if exists
if uptimeMonitor != nil {
@ -319,9 +354,15 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
if stv == "" {
stv = "false"
}
skipTlsValidation := (stv == "true")
//Load bypass TLS option
bpgtls, _ := utils.PostPara(r, "bpgtls")
if bpgtls == "" {
bpgtls = "false"
}
bypassGlobalTLS := (bpgtls == "true")
rba, _ := utils.PostPara(r, "bauth")
if rba == "" {
rba = "false"
@ -341,10 +382,12 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
RootName: targetProxyEntry.RootOrMatchingDomain,
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: false,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
targetProxyEntry.Remove()
dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)
} else if eptype == "subd" {
@ -352,10 +395,12 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
MatchingDomain: targetProxyEntry.RootOrMatchingDomain,
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: bypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
targetProxyEntry.Remove()
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
}
@ -369,14 +414,10 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
SaveReverseProxyConfig(&thisProxyConfigRecord)
SaveReverseProxyConfigToFile(&thisProxyConfigRecord)
//Update the current running config
targetProxyEntry.Domain = endpoint
targetProxyEntry.RequireTLS = useTLS
targetProxyEntry.SkipCertValidations = skipTlsValidation
targetProxyEntry.RequireBasicAuth = requireBasicAuth
dynamicProxyRouter.SaveProxy(eptype, targetProxyEntry.RootOrMatchingDomain, targetProxyEntry)
//Update uptime monitor
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}
@ -394,13 +435,15 @@ func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
err = dynamicProxyRouter.RemoveProxy(ptype, ep)
//Remove the config from runtime
err = dynamicProxyRouter.RemoveProxyEndpointByRootname(ptype, ep)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
RemoveReverseProxyConfig(ep)
//Remove the config from file
RemoveReverseProxyConfigFile(ep)
//Update utm if exists
if uptimeMonitor != nil {
@ -408,6 +451,9 @@ func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
uptimeMonitor.CleanRecords()
}
//Update uptime monitor
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}
@ -524,19 +570,10 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
targetProxy.BasicAuthCredentials = mergedCredentials
//Save it to file
thisProxyConfigRecord := Record{
ProxyType: ptype,
Rootname: targetProxy.RootOrMatchingDomain,
ProxyTarget: targetProxy.Domain,
UseTLS: targetProxy.RequireTLS,
SkipTlsValidation: targetProxy.SkipCertValidations,
RequireBasicAuth: targetProxy.RequireBasicAuth,
BasicAuthCredentials: targetProxy.BasicAuthCredentials,
}
SaveReverseProxyConfig(&thisProxyConfigRecord)
SaveReverseProxyEndpointToFile(targetProxy)
//Replace runtime configuration
dynamicProxyRouter.SaveProxy(ptype, ep, targetProxy)
targetProxy.UpdateToRuntime()
utils.SendOK(w)
} else {
http.Error(w, "invalid usage", http.StatusMethodNotAllowed)
@ -544,6 +581,147 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
}
// List, Update or Remove the exception paths for basic auth.
func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
ep, err := utils.GetPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "Invalid ep given")
return
}
ptype, err := utils.GetPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
//Load the target proxy object from router
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//List all the exception paths for this proxy
results := targetProxy.BasicAuthExceptionRules
if results == nil {
//It is a config from a really old version of zoraxy. Overwrite it with empty array
results = []*dynamicproxy.BasicAuthExceptionRule{}
}
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
return
}
func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
ep, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "Invalid ep given")
return
}
ptype, err := utils.PostPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
matchingPrefix, err := utils.PostPara(r, "prefix")
if err != nil {
utils.SendErrorResponse(w, "Invalid matching prefix given")
return
}
//Load the target proxy object from router
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Check if the prefix starts with /. If not, prepend it
if !strings.HasPrefix(matchingPrefix, "/") {
matchingPrefix = "/" + matchingPrefix
}
//Add a new exception rule if it is not already exists
alreadyExists := false
for _, thisExceptionRule := range targetProxy.BasicAuthExceptionRules {
if thisExceptionRule.PathPrefix == matchingPrefix {
alreadyExists = true
break
}
}
if alreadyExists {
utils.SendErrorResponse(w, "This matching path already exists")
return
}
targetProxy.BasicAuthExceptionRules = append(targetProxy.BasicAuthExceptionRules, &dynamicproxy.BasicAuthExceptionRule{
PathPrefix: strings.TrimSpace(matchingPrefix),
})
//Save configs to runtime and file
targetProxy.UpdateToRuntime()
SaveReverseProxyEndpointToFile(targetProxy)
utils.SendOK(w)
}
func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
// Delete a rule
ep, err := utils.PostPara(r, "ep")
if err != nil {
utils.SendErrorResponse(w, "Invalid ep given")
return
}
ptype, err := utils.PostPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
matchingPrefix, err := utils.PostPara(r, "prefix")
if err != nil {
utils.SendErrorResponse(w, "Invalid matching prefix given")
return
}
// Load the target proxy object from router
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
newExceptionRuleList := []*dynamicproxy.BasicAuthExceptionRule{}
matchingExists := false
for _, thisExceptionalRule := range targetProxy.BasicAuthExceptionRules {
if thisExceptionalRule.PathPrefix != matchingPrefix {
newExceptionRuleList = append(newExceptionRuleList, thisExceptionalRule)
} else {
matchingExists = true
}
}
if !matchingExists {
utils.SendErrorResponse(w, "target matching rule not exists")
return
}
targetProxy.BasicAuthExceptionRules = newExceptionRuleList
// Save configs to runtime and file
targetProxy.UpdateToRuntime()
SaveReverseProxyEndpointToFile(targetProxy)
utils.SendOK(w)
}
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(dynamicProxyRouter)
utils.SendJSONResponse(w, string(js))
@ -590,6 +768,35 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
}
}
// Handle port 80 incoming traffics
func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
enabled, err := utils.GetPara(r, "enable")
if err != nil {
//Load the current status
currentEnabled := false
err = sysdb.Read("settings", "listenP80", &currentEnabled)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(currentEnabled)
utils.SendJSONResponse(w, string(js))
} else {
if enabled == "true" {
sysdb.Write("settings", "listenP80", true)
SystemWideLogger.Println("Enabling port 80 listener")
dynamicProxyRouter.UpdatePort80ListenerState(true)
} else if enabled == "false" {
sysdb.Write("settings", "listenP80", false)
SystemWideLogger.Println("Disabling port 80 listener")
dynamicProxyRouter.UpdatePort80ListenerState(true)
} else {
utils.SendErrorResponse(w, "invalid mode given: "+enabled)
}
utils.SendOK(w)
}
}
// Handle https redirect
func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) {
useRedirect, err := utils.GetPara(r, "set")
@ -604,13 +811,17 @@ func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(currentRedirectToHttps)
utils.SendJSONResponse(w, string(js))
} else {
if dynamicProxyRouter.Option.Port == 80 {
utils.SendErrorResponse(w, "This option is not available when listening on port 80")
return
}
if useRedirect == "true" {
sysdb.Write("settings", "redirect", true)
log.Println("Updating force HTTPS redirection to true")
SystemWideLogger.Println("Updating force HTTPS redirection to true")
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
} else if useRedirect == "false" {
sysdb.Write("settings", "redirect", false)
log.Println("Updating force HTTPS redirection to false")
SystemWideLogger.Println("Updating force HTTPS redirection to false")
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
}
@ -636,11 +847,18 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
newIncomingPortInt, err := strconv.Atoi(newIncomingPort)
if err != nil {
utils.SendErrorResponse(w, "invalid incoming port given")
utils.SendErrorResponse(w, "Invalid incoming port given")
return
}
//Check if it is identical as proxy root (recursion!)
if dynamicProxyRouter.Root == nil || dynamicProxyRouter.Root.Domain == "" {
//Check if proxy root is set before checking recursive listen
//Fixing issue #43
utils.SendErrorResponse(w, "Set Proxy Root before changing inbound port")
return
}
proxyRoot := strings.TrimSuffix(dynamicProxyRouter.Root.Domain, "/")
if strings.HasPrefix(proxyRoot, "localhost:"+strconv.Itoa(newIncomingPortInt)) || strings.HasPrefix(proxyRoot, "127.0.0.1:"+strconv.Itoa(newIncomingPortInt)) {
//Listening port is same as proxy root
@ -663,3 +881,34 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w)
}
// Handle list of root route options
func HandleRootRouteOptionList(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(dynamicProxyRouter.RootRoutingOptions)
utils.SendJSONResponse(w, string(js))
}
// Handle update of the root route edge case options. See dynamicproxy/rootRoute.go
func HandleRootRouteOptionsUpdate(w http.ResponseWriter, r *http.Request) {
enableUnsetSubdomainRedirect, err := utils.PostBool(r, "unsetRedirect")
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
unsetRedirectTarget, _ := utils.PostPara(r, "unsetRedirectTarget")
newRootOption := dynamicproxy.RootRoutingOptions{
EnableRedirectForUnsetRules: enableUnsetSubdomainRedirect,
UnsetRuleRedirectTarget: unsetRedirectTarget,
}
dynamicProxyRouter.RootRoutingOptions = &newRootOption
err = newRootOption.SaveToFile()
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}

View File

@ -8,18 +8,22 @@ import (
"strings"
"time"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/ganserv"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"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/tcpprox"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/webserv"
)
/*
@ -47,8 +51,9 @@ func startupSequence() {
//Create tables for the database
sysdb.NewTable("settings")
//Create tmp folder
//Create tmp folder and conf folder
os.MkdirAll("./tmp", 0775)
os.MkdirAll("./conf/proxy/", 0775)
//Create an auth agent
sessionKey, err := auth.GetSessionKey(sysdb)
@ -61,19 +66,22 @@ func startupSequence() {
})
//Create a TLS certificate manager
tlsCertManager, err = tlscert.NewManager("./certs", development)
tlsCertManager, err = tlscert.NewManager("./conf/certs", development)
if err != nil {
panic(err)
}
//Create a redirection rule table
redirectTable, err = redirection.NewRuleTable("./rules")
redirectTable, err = redirection.NewRuleTable("./conf/redirect")
if err != nil {
panic(err)
}
//Create a geodb store
geodbStore, err = geodb.NewGeoDb(sysdb)
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
AllowSlowIpv4LookUp: !*enableHighSpeedGeoIPLookup,
AllowSloeIpv6Lookup: !*enableHighSpeedGeoIPLookup,
})
if err != nil {
panic(err)
}
@ -86,59 +94,81 @@ func startupSequence() {
panic(err)
}
//Create a system wide logger
l, err := logger.NewLogger("zr", "./log", *logOutputToFile)
if err == nil {
SystemWideLogger = l
} else {
panic(err)
}
//Create a netstat buffer
netstatBuffers, err = netstat.NewNetStatBuffer(300)
if err != nil {
log.Println("Failed to load network statistic info")
SystemWideLogger.PrintAndLog("Network", "Failed to load network statistic info", err)
panic(err)
}
/*
Path Rules
This section of starutp script start the path rules where
user can define their own routing logics
*/
pathRuleHandler = pathrule.NewPathRuleHandler(&pathrule.Options{
Enabled: false,
ConfigFolder: "./conf/rules/pathrules",
})
/*
MDNS Discovery Service
This discover nearby ArozOS Nodes or other services
that provide mDNS discovery with domain (e.g. Synology NAS)
*/
portInt, err := strconv.Atoi(strings.Split(handler.Port, ":")[1])
if err != nil {
portInt = 8000
}
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
HostName: "zoraxy_" + nodeUUID,
Port: portInt,
Domain: "zoraxy.imuslab.com",
Model: "Network Gateway",
UUID: nodeUUID,
Vendor: "imuslab.com",
BuildVersion: version,
}, "")
if err != nil {
panic(err)
}
//Start initial scanning
go func() {
hosts := mdnsScanner.Scan(30, "")
previousmdnsScanResults = hosts
log.Println("mDNS Startup scan completed")
}()
//Create a ticker to update mDNS results every 5 minutes
ticker := time.NewTicker(15 * time.Minute)
stopChan := make(chan bool)
go func() {
for {
select {
case <-stopChan:
ticker.Stop()
case <-ticker.C:
if *allowMdnsScanning {
portInt, err := strconv.Atoi(strings.Split(handler.Port, ":")[1])
if err != nil {
portInt = 8000
}
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
HostName: "zoraxy_" + nodeUUID,
Port: portInt,
Domain: "zoraxy.arozos.com",
Model: "Network Gateway",
UUID: nodeUUID,
Vendor: "imuslab.com",
BuildVersion: version,
}, "")
if err != nil {
SystemWideLogger.Println("Unable to startup mDNS service. Disabling mDNS services")
} else {
//Start initial scanning
go func() {
hosts := mdnsScanner.Scan(30, "")
previousmdnsScanResults = hosts
log.Println("mDNS scan result updated")
}
SystemWideLogger.Println("mDNS Startup scan completed")
}()
//Create a ticker to update mDNS results every 5 minutes
ticker := time.NewTicker(15 * time.Minute)
stopChan := make(chan bool)
go func() {
for {
select {
case <-stopChan:
ticker.Stop()
case <-ticker.C:
hosts := mdnsScanner.Scan(30, "")
previousmdnsScanResults = hosts
SystemWideLogger.Println("mDNS scan result updated")
}
}
}()
mdnsTickerStop = stopChan
}
}()
mdnsTickerStop = stopChan
}
/*
Global Area Network
@ -149,7 +179,7 @@ func startupSequence() {
if usingZtAuthToken == "" {
usingZtAuthToken, err = ganserv.TryLoadorAskUserForAuthkey()
if err != nil {
log.Println("Failed to load ZeroTier controller API authtoken")
SystemWideLogger.Println("Failed to load ZeroTier controller API authtoken")
}
}
ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{
@ -163,7 +193,8 @@ func startupSequence() {
//Create TCP Proxy Manager
tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{
Database: sysdb,
Database: sysdb,
AccessControlHandler: geodbStore.AllowConnectionAccess,
})
//Create WoL MAC storage table
@ -175,4 +206,39 @@ func startupSequence() {
//Create an analytic loader
AnalyticLoader = analytic.NewDataLoader(sysdb, statisticCollector)
/*
ACME API
Obtaining certificates from ACME Server
*/
//Create a table just to store acme related preferences
sysdb.NewTable("acmepref")
acmeHandler = initACME()
acmeAutoRenewer, err = acme.NewAutoRenewer("./conf/acme_conf.json", "./conf/certs/", int64(*acmeAutoRenewInterval), acmeHandler)
if err != nil {
log.Fatal(err)
}
/*
Static Web Server
Start the static web server
*/
staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
Sysdb: sysdb,
Port: "5487", //Default Port
WebRoot: *staticWebServerRoot,
EnableDirectoryListing: true,
EnableWebDirManager: *allowWebFileManager,
})
//Restore the web server to previous shutdown state
staticWebServer.RestorePreviousState()
}
// This sequence start after everything is initialized
func finalSequence() {
//Start ACME renew agent
acmeRegisterSpecialRoutingRule()
}

View File

@ -308,7 +308,7 @@
<div class="ui message">
<i class="ui info circle icon"></i> IP Address support the following formats
<div class="ui bulleted list">
<div class="item">Fixed IP Address (e.g. 192.128.4.100)</div>
<div class="item">Fixed IP Address (e.g. 192.128.4.100 or fe80::210:5aff:feaa:20a2)</div>
<div class="item">IP Wildcard (e.g. 172.164.*.*)</div>
<div class="item">CIDR String (e.g. 128.32.0.1/16)</div>
</div>
@ -625,7 +625,7 @@
<div class="ui message">
<i class="ui info circle icon"></i> IP Address support the following formats
<div class="ui bulleted list">
<div class="item">Fixed IP Address (e.g. 192.128.4.100)</div>
<div class="item">Fixed IP Address (e.g. 192.128.4.100 or fe80::210:5aff:feaa:20a2)</div>
<div class="item">IP Wildcard (e.g. 172.164.*.*)</div>
<div class="item">CIDR String (e.g. 128.32.0.1/16)</div>
</div>
@ -1082,24 +1082,51 @@
//Check if a input is a valid IP address, wildcard of a IP address or a CIDR string
function isValidIpFilter(input) {
// Check if input is a valid IP address
const isValidIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(input);
// Check if input is a valid IPv4 address
const isValidIPv4 = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(input);
if (isValidIp) {
if (isValidIPv4) {
return true;
}
// Check if input is a wildcard IP address
const isValidWildcardIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])$/.test(input);
// Check if input is a valid IPv4 wildcard address
const isValidIPv4Wildcard = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])$/.test(input);
if (isValidWildcardIp) {
if (isValidIPv4Wildcard) {
return true;
}
// Check if input is a valid CIDR address string
const isValidCidr = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2][0-9]|3[0-2])$/.test(input);
// Check if input is a valid IPv4 CIDR address
const isValidIPv4CIDR = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2][0-9]|3[0-2])$/.test(input);
if (isValidCidr) {
if (isValidIPv4CIDR) {
return true;
}
// Check if input is loopback ipv6
if (input == "::1"){
return true;
}
// Check if input is a valid IPv6 address
const isValidIPv6 = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/.test(input);
if (isValidIPv6) {
return true;
}
//Pure magic, I have no idea how this works
//src: https://stackoverflow.com/questions/70348674/alternate-solution-validate-ipv4-and-ipv6-with-wildcard-characters-using-r
function evalIp6(t){var e=t.split(":"),n=t.split("::").length-1;if(8<e.length&&(9!=t.split(":").length||""!=e[e.length-1]||1!=n))return!1;if(1<n)return!1;if(-1!=t.indexOf("::*")||-1!=t.indexOf("*::"))return!1;var r=!1;for(let t=0;t<e.length;t++){if(!isIPV6Group(e[t]))return!1;"*"==e[t]&&(r=!0)}return!(!r&&0==n&&8!=e.length)}function isIPV6Group(t){var e="^(([0-9A-Fa-f]{1,4})|\\*|)$";return(e=new RegExp(e)).test(t)}
if (evalIp6(input)){
return true;
}
// Check if input is a valid IPv6 CIDR address
const isValidIPv6CIDR = /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/.test(input);
if (isValidIPv6CIDR) {
return true;
}

View File

@ -1,3 +1,13 @@
<style>
.expired.certdate{
font-weight: bolder;
color: #bd001c;
}
.valid.certdate{
color: #31c071;
}
</style>
<div class="standardContainer">
<div class="ui basic segment">
<h2>TLS / SSL Certificates</h2>
@ -55,17 +65,20 @@
</div>
<br>
<div>
<table class="ui sortable unstackable celled table">
<thead>
<tr><th>Domain</th>
<th>Last Update</th>
<th>Expire At</th>
<th class="no-sort">Remove</th>
</tr></thead>
<tbody id="certifiedDomainList">
</tbody>
</table>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui sortable unstackable celled table">
<thead>
<tr><th>Domain</th>
<th>Last Update</th>
<th>Expire At</th>
<th class="no-sort">Remove</th>
</tr></thead>
<tbody id="certifiedDomainList">
</tbody>
</table>
</div>
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
</div>
<div class="ui message">
@ -74,11 +87,49 @@
depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for <code>a.example.com</code> and <code>b.example.com</code>).<br>
If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
</div>
<div class="ui divider"></div>
<h4>Certificate Authority (CA) and Auto Renew (ACME)</h4>
<p>Management features regarding CA and ACME</p>
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
<div class="ui fluid form">
<div class="field">
<label>Preferred CA</label>
<div class="ui selection dropdown" id="defaultCA">
<input type="hidden" name="defaultCA">
<i class="dropdown icon"></i>
<div class="default text">Let's Encrypt</div>
<div class="menu">
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
</div>
</div>
</div>
<div class="field">
<label>ACME Email</label>
<input id="prefACMEEmail" type="text" placeholder="ACME Email">
</div>
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
</div><br>
<h5>Certificate Renew / Generation (ACME) Settings</h5>
<div class="ui basic segment">
<h4 class="ui header" id="acmeAutoRenewer">
<i class="red circle icon"></i>
<div class="content">
<span id="acmeAutoRenewerStatus">Disabled</span>
<div class="sub header">Auto-Renewer Status</div>
</div>
</h4>
</div>
<p>This tool provide you a graphical interface to setup auto certificate renew on your (sub)domains. You can also manually generate a certificate if one of your domain do not have certificate.</p>
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
</div>
<script>
var uploadPendingPublicKey = undefined;
var uploadPendingPrivateKey = undefined;
$("#defaultCA").dropdown();
//Delete the certificate by its domain
function deleteCertificate(domain){
if (confirm("Confirm delete certificate for " + domain + " ?")){
@ -99,6 +150,62 @@
}
function initAcmeStatus(){
//Initialize the current default CA options
$.get("/api/acme/autoRenew/email", function(data){
$("#prefACMEEmail").val(data);
});
$.get("/api/acme/autoRenew/ca", function(data){
$("#defaultCA").dropdown("set value", data);
});
$.get("/api/acme/autoRenew/enable", function(data){
setACMEEnableStates(data);
})
}
//Set the status of the acme enable icon
function setACMEEnableStates(enabled){
$("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
$("#acmeAutoRenewer").find("i").attr("class", enabled?"green circle icon":"red circle icon");
}
initAcmeStatus();
function saveDefaultCA(){
let newDefaultEmail = $("#prefACMEEmail").val().trim();
let newDefaultCA = $("#defaultCA").dropdown("get value");
if (newDefaultEmail == ""){
msgbox("Invalid acme email given", false);
return;
}
$.ajax({
url: "/api/acme/autoRenew/email",
method: "POST",
data: {"set": newDefaultEmail},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}
}
});
$.ajax({
url: "/api/acme/autoRenew/ca",
data: {"set": newDefaultCA},
method: "POST",
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}
}
});
msgbox("Settings updated");
}
//List the stored certificates
function initManagedDomainCertificateList(){
$.get("/api/cert/list?date=true", function(data){
@ -106,11 +213,16 @@
msgbox(data.error, false, 5000);
}else{
$("#certifiedDomainList").html("");
data.sort((a,b) => {
return a.Domain > b.Domain
});
data.forEach(entry => {
let isExpired = entry.RemainingDays <= 0;
$("#certifiedDomainList").append(`<tr>
<td>${entry.Domain}</td>
<td>${entry.LastModifiedDate}</td>
<td>${entry.ExpireDate}</td>
<td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
</tr>`);
});
@ -125,6 +237,10 @@
}
initManagedDomainCertificateList();
function openACMEManager(){
showSideWrapper('snippet/acme.html');
}
function handleDomainUploadByKeypress(){
handleDomainKeysUpload(function(){
$("#certUploadingDomain").text($("#certdomain").val().trim());

View File

@ -12,13 +12,50 @@
</div>
<div class="ui bottom attached tab segment nettoolstab active" data-tab="tab1">
<!-- MDNS Scanner-->
<h2>Multicast DNS (mDNS) Scanner</h2>
<p>Discover mDNS enabled service in this gateway forwarded network</p>
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/mdns.html',1000, 640);">View Discovery</button>
<div class="ui divider"></div>
<!-- IP Scanner-->
<h2>IP Scanner</h2>
<p>Discover local area network devices by pinging them one by one</p>
<button class="ui basic larger circular button" onclick="launchToolWithSize('./tools/ipscan.html',1000, 640);">Start Scanner</button>
<div class="ui divider"></div>
<!-- Traceroute-->
<h2>Traceroute / Ping</h2>
<p>Trace the network nodes that your packets hops through</p>
<div class="ui form">
<div class="two fields">
<div class="field">
<label>Target domain or IP</label>
<input type="text" id="traceroute_domain" placeholder="1.1.1.1">
</div>
<div class="field">
<label>Max Hops</label>
<input type="number" min="1" step="1" id="traceroute_maxhops" placeholder="64" value="64">
</div>
</div>
<button class="ui basic button" onclick="traceroute();"><i class="ui blue location arrow icon"></i> Start Tracing</button>
<button class="ui basic button" onclick="ping();"><i class="ui teal circle outline icon"></i> Ping</button>
<br><br>
<div class="field">
<label>Results</label>
<textarea id="traceroute_results" rows="10" style=""></textarea>
</div>
</div>
<div class="ui divider"></div>
<!-- Whois-->
<h2>Whois</h2>
<p>Check the owner and registration information of a given domain</p>
<div class="ui icon input">
<input id="whoisdomain" type="text" onkeypress="if(event.keyCode === 13) { performWhoisLookup(); }" placeholder="Domain or IP">
<i onclick="performWhoisLookup();" class="circular search link icon"></i>
</div><br>
<small>Lookup might take a few minutes to complete</small>
<br>
<div id="whois_table"></div>
</div>
<div class="ui bottom attached tab segment nettoolstab" data-tab="tab2">
@ -435,6 +472,91 @@ function updateMDNSListForWoL(){
}
updateMDNSListForWoL();
function traceroute(){
let domain = $("#traceroute_domain").val().trim();
let maxhops = $("#traceroute_maxhops").val().trim();
$("#traceroute_results").val("Loading...");
$.get("/api/tools/traceroute?target=" + domain + "&maxhops=" + maxhops, function(data){
if (data.error != undefined){
$("#traceroute_results").val("");
msgbox(data.error, false, 6000);
}else{
$("#traceroute_results").val(data.join("\n"));
}
});
}
function ping(){
let domain = $("#traceroute_domain").val().trim();
$("#traceroute_results").val("Loading...");
$.get("/api/tools/ping?target=" + domain, function(data){
if (data.error != undefined){
$("#traceroute_results").val("");
msgbox(data.error, false, 6000);
}else{
$("#traceroute_results").val(`--------- ICMP Ping -------------
${data.ICMP.join("\n")}\n
---------- TCP Ping -------------
${data.TCP.join("\n")}\n
---------- UDP Ping -------------
${data.UDP.join("\n")}`);
}
});
}
function performWhoisLookup(){
let whoisDomain = $("#whoisdomain").val().trim();
$("#whoisdomain").parent().addClass("disabled");
$("#whoisdomain").parent().css({
"cursor": "wait"
});
$.get("/api/tools/whois?target=" + whoisDomain, function(data){
$("#whoisdomain").parent().removeClass("disabled");
$("#whoisdomain").parent().css({
"cursor": "auto"
});
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
renderWhoisDomainTable(data);
}
})
}
function renderWhoisDomainTable(jsonData) {
function formatDate(dateString) {
var date = new Date(dateString);
return date.toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
var table = $('<table>').addClass('ui definition table');
// Create table body
var body = $('<tbody>');
for (var key in jsonData) {
var value = jsonData[key];
var row = $('<tr>');
row.append($('<td>').text(key));
if (key.endsWith('Date')) {
row.append($('<td>').text(formatDate(value)));
} else if (Array.isArray(value)) {
row.append($('<td>').text(value.join(', ')));
}else if (typeof(value) == "object"){
row.append($('<td>').text(JSON.stringify(value)));
} else {
row.append($('<td>').text(value));
}
body.append(row);
}
// Append the table body to the table
table.append(body);
// Append the table to the target element
$('#whois_table').empty().append(table);
}
</script>

View File

@ -1,78 +1,100 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Redirection Rules</h2>
<p>Add exception case for redirecting any matching URLs</p>
</div>
<div style="width: 100%; overflow-x: auto;">
<table class="ui sortable unstackable celled table" >
<thead>
<tr>
<th>Redirection URL</th>
<th>Destination URL</th>
<th class="no-sort">Copy Pathname</th>
<th class="no-sort">Status Code</th>
<th class="no-sort">Remove</th>
</tr>
</thead>
<tbody id="redirectionRuleList">
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="ui green message" id="delRuleSucc" style="display:none;">
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
</div>
<div class="ui divider"></div>
<h4>Add Redirection Rule</h4>
<div class="ui form">
<div class="field">
<label>Redirection URL (From)</label>
<input type="text" id="rurl" name="redirection-url" placeholder="Redirection URL">
<small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com</small>
</div>
<div class="field">
<label>Destination URL (To)</label>
<input type="text" name="destination-url" placeholder="Destination URL">
<small><i class="ui circle info icon"></i> The target URL request being redirected to, e.g. dest.example.com/mysite</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="forward-childpath" tabindex="0" class="hidden" checked>
<label>Forward Pathname</label>
<div class="ui basic segment">
<h2>Redirection Rules</h2>
<p>Add exception case for redirecting any matching URLs</p>
</div>
<div style="width: 100%; overflow-x: auto;">
<table class="ui sortable unstackable celled table" >
<thead>
<tr>
<th>Redirection URL</th>
<th>Destination URL</th>
<th class="no-sort">Copy Pathname</th>
<th class="no-sort">Status Code</th>
<th class="no-sort">Remove</th>
</tr>
</thead>
<tbody id="redirectionRuleList">
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="ui green message" id="delRuleSucc" style="display:none;">
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
</div>
<div class="ui divider"></div>
<h4>Add Redirection Rule</h4>
<div class="ui form">
<div class="field">
<label>Redirection URL (From)</label>
<input type="text" id="rurl" name="redirection-url" placeholder="Redirection URL">
<small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com</small>
</div>
<div class="ui message">
<p>Append the current pathname after the redirect destination</p>
<i class="check square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com<b>/blog?post=13</b> <br>
<i class="square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com
<div class="field">
<label>Destination URL (To)</label>
<input type="text" name="destination-url" placeholder="Destination URL">
<small><i class="ui circle info icon"></i> The target URL request being redirected to, e.g. dest.example.com/mysite/ or dest.example.com/script.php, <b>sometime you might need to add tailing slash (/) to your URL depending on your use cases</b></small>
</div>
</div>
<div class="grouped fields">
<label>Redirection Status Code</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="redirect-type" value="307" checked>
<label>Temporary Redirect <br><small>Status Code: 307</small></label>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="forward-childpath" tabindex="0" class="hidden" checked>
<label>Forward Pathname</label>
</div>
<div class="ui message">
<p>Append the current pathname after the redirect destination</p>
<i class="check square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com<b>/blog?post=13</b> <br>
<i class="square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com
</div>
</div>
<div class="grouped fields">
<label>Redirection Status Code</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="redirect-type" value="307" checked>
<label>Temporary Redirect <br><small>Status Code: 307</small></label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="redirect-type" value="301">
<label>Moved Permanently <br><small>Status Code: 301</small></label>
</div>
</div>
</div>
<button class="ui basic button" onclick="addRules();"><i class="ui teal plus icon"></i> Add Redirection Rule</button>
<div class="ui green message" id="ruleAddSucc" style="display:none;">
<i class="ui green checkmark icon"></i> Redirection Rules Added
</div>
<br><br>
<div class="advancezone ui basic segment">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Options
</div>
<div class="content">
<p>If you need custom header, content or status code other than basic redirects, you can use the advance path rules editor.</p>
<button class="ui black basic button" onclick="createAdvanceRules();"><i class="ui black external icon"></i> Open Advance Rules Editor</button>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="redirect-type" value="301">
<label>Moved Permanently <br><small>Status Code: 301</small></label>
</div>
</div>
</div>
<button class="ui basic button" onclick="addRules();"><i class="ui teal plus icon"></i> Add Redirection Rule</button>
<div class="ui green message" id="ruleAddSucc" style="display:none;">
<i class="ui green checkmark icon"></i> Redirection Rules Added
</div>
</div>
</div>
</div>
</div>
<script>
$(".advanceSettings").accordion();
/*
Redirection functions
*/
$(".checkbox").checkbox();
function resetForm() {
@ -129,6 +151,10 @@
}
}
function createAdvanceRules(){
showSideWrapper("snippet/advancePathRules.html?t=" + Date.now(), true);
}
function initRedirectionRuleList(){
$("#redirectionRuleList").html("");
$.get("/api/redirect/list", function(data){

View File

@ -1,7 +1,7 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Set Proxy Root</h2>
<p>For all routing not found in the proxy rules, request will be redirected to the proxy root server.</p>
<p>The default routing point for all incoming traffics. For all routing not found in the proxy rules, request will be redirected to the proxy root server.</p>
<div class="ui form">
<div class="field">
<label>Proxy Root</label>
@ -10,17 +10,86 @@
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="rootReqTLS" >
<label>Root require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
<input type="checkbox" id="rootReqTLS">
<label>Root require TLS connection <br><small>Check this if your proxy root URL starts with https://</small></label>
</div>
</div>
<div class="ui horizontal divider">OR</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="useStaticWebServer" onchange="handleUseStaticWebServerAsRoot()">
<label>Use Static Web Server as Root <br><small>Check this if you prefer a more Apache Web Server like experience</small></label>
</div>
</div>
<br>
<button class="ui basic button" onclick="setProxyRoot()"><i class="teal home icon" ></i> Update Proxy Root</button>
<div class="ui divider"></div>
<div class="field">
<h4>Root Routing Options</h4>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="unsetRedirect">
<label>Enable redirect for unset subdomains <br><small>Redirect subdomain that is not found to custom domain</small></label>
</div>
</div>
<div class="ui basic segment" id="unsetRedirectDomainWrapper" style="background-color: #f7f7f7; border-radius: 1em; margin-left: 2em; padding-left: 2em; display:none;">
<div style="
position: absolute;
top:0;
left: 1em;
width: 0px;
height: 0px;
margin-top: -10px;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid #f7f7f7;">
</div>
<div class="field">
<label>Redirect target domain</label>
<div class="ui input">
<input id="unsetRedirectDomain" type="text" placeholder="http://example.com">
</div>
<small>Unset subdomain will be redirected to the link above. Remember to include the protocol (e.g. http:// or https://)<br>
Leave empty for redirecting to upper level domain (e.g. notfound.example.com <i class="right arrow icon"></i> example.com)</small>
</div>
</div>
<br>
<button class="ui basic button" onclick="updateRootOptions()"><i class="blue save icon" ></i> Save Root Options</button>
</div>
<br>
<button class="ui basic button" onclick="setProxyRoot()"><i class="teal home icon" ></i> Update Proxy Root</button>
</div>
</div>
<script>
function initRootInfo(){
$("#advanceRootSettings").accordion();
function handleUseStaticWebServerAsRoot(){
let useStaticWebServer = $("#useStaticWebServer")[0].checked;
if (useStaticWebServer){
let staticWebServerURL = "127.0.0.1:" + $("#webserv_listenPort").val();
$("#proxyRoot").val(staticWebServerURL);
$("#proxyRoot").parent().addClass("disabled");
$("#rootReqTLS").parent().checkbox("set unchecked");
$("#rootReqTLS").parent().addClass("disabled");
//Check if web server is enabled. If not, ask if the user want to enable it
/*if (!$("#webserv_enable").parent().checkbox("is checked")){
confirmBox("Enable static web server now?", function(choice){
if (choice == true){
$("#webserv_enable").parent().checkbox("set checked");
}
});
}*/
}else{
$("#rootReqTLS").parent().removeClass("disabled");
$("#proxyRoot").parent().removeClass("disabled");
initRootInfo();
}
}
function initRootInfo(callback=undefined){
$.get("/api/proxy/list?type=root", function(data){
if (data == null){
@ -28,11 +97,91 @@
$("#proxyRoot").val(data.Domain);
checkRootRequireTLS(data.Domain);
}
if (callback != undefined){
callback();
}
});
}
initRootInfo();
initRootInfo(function(){
updateWebServerLinkSettings();
});
//Update the current web server port settings
function updateWebServerLinkSettings(){
isUsingStaticWebServerAsRoot(function(isUsingWebServ){
if (isUsingWebServ){
$(".webservRootDisabled").addClass("disabled");
$("#useStaticWebServer").parent().checkbox("set checked");
}else{
$(".webservRootDisabled").removeClass("disabled");
$("#useStaticWebServer").parent().checkbox("set unchecked");
}
})
}
function isUsingStaticWebServerAsRoot(callback){
let currentProxyRoot = $("#proxyRoot").val().trim();
$.get("/api/webserv/status", function(webservStatus){
if (currentProxyRoot == "127.0.0.1:" + webservStatus.ListeningPort || currentProxyRoot == "localhost:" + webservStatus.ListeningPort){
return callback(true);
}
return callback(false);
});
}
function updateRootSettingStates(){
$.get("/api/cert/tls", function(data){
if (data == true){
$("#disableRootTLS").parent().removeClass('disabled').attr("title", "");
}else{
$("#disableRootTLS").parent().addClass('disabled').attr("title", "TLS listener is not enabled");
}
});
}
//Bind event to tab switch
tabSwitchEventBind["setroot"] = function(){
//On switch over to this page, update root info
updateRootSettingStates();
}
//Toggle the display status of the input box for domain setting
function updateRedirectionDomainSettingInputBox(useRedirect){
if(useRedirect){
$("#unsetRedirectDomainWrapper").stop().finish().slideDown("fast");
}else{
$("#unsetRedirectDomainWrapper").stop().finish().slideUp("fast");
}
}
function checkCustomRedirectForUnsetSubd(){
$.get("/api/proxy/root/listOptions", function(data){
$("#unsetRedirect")[0].checked = data.EnableRedirectForUnsetRules || false;
$("#unsetRedirectDomain").val(data.UnsetRuleRedirectTarget);
updateRedirectionDomainSettingInputBox(data.EnableRedirectForUnsetRules);
//Bind event to the checkbox
$("#unsetRedirect").off("change").on("change", function(){
let useRedirect = $("#unsetRedirect")[0].checked;
updateRedirectionDomainSettingInputBox(useRedirect);
});
});
}
checkCustomRedirectForUnsetSubd();
//Check if the given domain will redirect to https
function checkRootRequireTLS(targetDomain){
//Trim off the http or https from the origin
if (targetDomain.startsWith("http://")){
targetDomain = targetDomain.substring(7);
$("#proxyRoot").val(targetDomain);
}else if (targetDomain.startsWith("https://")){
targetDomain = targetDomain.substring(8);
$("#proxyRoot").val(targetDomain);
}
$.ajax({
url: "/api/proxy/tlscheck",
data: {url: targetDomain},
@ -44,11 +193,13 @@
}else if (data == "http"){
$("#rootReqTLS").parent().checkbox("set unchecked");
}
}
})
}
//Set the new proxy root option
function setProxyRoot(){
var newpr = $("#proxyRoot").val();
if (newpr.trim() == ""){
@ -66,14 +217,51 @@
data: {"type": "root", tls: rootReqTls, ep: newpr},
success: function(data){
if (data.error != undefined){
alert(data.error);
msgbox(data.error, false, 5000);
}else{
//OK
initRootInfo();
msgbox("Proxy Root Updated")
initRootInfo(function(){
//Check if WebServ is enabled
isUsingStaticWebServerAsRoot(function(isUsingWebServ){
if (isUsingWebServ){
//Force enable static web server
//See webserv.html for details
setWebServerRunningState(true);
}
setTimeout(function(){
//Update the checkbox
updateWebServerLinkSettings();
msgbox("Proxy Root Updated");
}, 1000);
})
});
}
}
});
}
function updateRootOptions(){
$.ajax({
type: "POST",
url: "/api/proxy/root/updateOptions",
data: {
unsetRedirect: $("#unsetRedirect")[0].checked,
unsetRedirectTarget: $("#unsetRedirectDomain").val().trim(),
},
success: function(data) {
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Root Routing Options updated");
}
},
error: function(error) {
console.log("Error:", error);
}
});
}
</script>

View File

@ -8,7 +8,7 @@
<div class="field">
<label>Proxy Type</label>
<div class="ui selection dropdown">
<input type="hidden" id="ptype" value="subd">
<input type="hidden" id="ptype" value="subd" onchange="handleProxyTypeOptionChange(this.value)">
<i class="dropdown icon"></i>
<div class="default text">Proxy Type</div>
<div class="menu">
@ -22,7 +22,7 @@
<input type="text" id="rootname" placeholder="s1.mydomain.com">
</div>
<div class="field">
<label>IP Address or Domain Name with port</label>
<label>Target IP Address or Domain Name with port</label>
<input type="text" id="proxyDomain" onchange="autoCheckTls(this.value);">
<small>E.g. 192.168.0.101:8000 or example.com</small>
</div>
@ -44,7 +44,13 @@
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="skipTLSValidation">
<label>Ignore TLS/SSL Verification Error<br><small>E.g. self-signed, expired certificate (Not Recommended)</small></label>
<label>Ignore TLS/SSL Verification Error<br><small>For targets that is using self-signed, expired certificate (Not Recommended)</small></label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="bypassGlobalTLS">
<label>Allow plain HTTP access<br><small>Allow this subdomain to be connected without TLS (Require HTTP server enabled on port 80)</small></label>
</div>
</div>
<div class="field">
@ -123,6 +129,7 @@
var proxyDomain = $("#proxyDomain").val();
var useTLS = $("#reqTls")[0].checked;
var skipTLSValidation = $("#skipTLSValidation")[0].checked;
var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
var requireBasicAuth = $("#requireBasicAuth")[0].checked;
if (type === "vdir") {
@ -162,6 +169,7 @@
tls: useTLS,
ep: proxyDomain,
tlsval: skipTLSValidation,
bypassGlobalTLS: bypassGlobalTLS,
bauth: requireBasicAuth,
cred: JSON.stringify(credentials),
},
@ -172,19 +180,48 @@
//OK
listVdirs();
listSubd();
msgbox("Proxy Endpoint Added");
//Clear old data
$("#rootname").val("");
$("#proxyDomain").val("");
credentials = [];
updateTable();
//Check if it is a new subdomain and TLS enabled
if (type == "subd" && $("#tls").checkbox("is checked")){
confirmBox("Request new SSL Cert for this subdomain?", function(choice){
if (choice == true){
//Load the prefer CA from TLS page
let defaultCA = $("#defaultCA").dropdown("get value");
if (defaultCA.trim() == ""){
defaultCA = "Let's Encrypt";
}
//Get a new cert using ACME
msgbox("Requesting certificate via " + defaultCA +"...");
console.log("Trying to get a new certificate via ACME");
obtainCertificate(rootname, defaultCA.trim());
}else{
msgbox("Proxy Endpoint Added");
}
});
}else{
msgbox("Proxy Endpoint Added");
}
}
}
});
}
function handleProxyTypeOptionChange(newType){
if (newType == "subd"){
$("#bypassGlobalTLS").parent().removeClass("disabled");
}else if (newType == "vdir"){
$("#bypassGlobalTLS").parent().addClass("disabled");
}
}
//Generic functions for delete rp endpoints
function deleteEndpoint(ptype, epoint){
if (confirm("Confirm remove proxy for :" + epoint + " (type: " + ptype + ")?")){
@ -310,7 +347,7 @@
var columns = row.find('td[data-label]');
var payload = $(row).attr("payload");
payload = JSON.parse(decodeURIComponent(payload));
console.log(payload);
//console.log(payload);
columns.each(function(index) {
var column = $(this);
@ -326,34 +363,37 @@
var datatype = $(this).attr("datatype");
if (datatype == "domain"){
let domain = payload.Domain;
//Target require TLS for proxying
let tls = payload.RequireTLS;
if (tls){
tls = "checked";
}else{
tls = "";
}
//Require TLS validation
let skipTLSValidation = payload.SkipCertValidations;
let checkstate = "";
if (skipTLSValidation){
checkstate = "checked";
}
input = `
<div class="ui mini fluid input">
<input type="text" class="Domain" value="${domain}">
</div>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="RequireTLS" ${tls}>
<label>Require TLS</label>
<label>Require TLS<br>
<small>Proxy target require HTTPS connection</small></label>
</div><br>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
<label>Skip Verification<br>
<small>Check this if proxy target is using self signed certificates</small></label>
</div>
`;
column.empty().append(input);
}else if (datatype == "skipver"){
let skipTLSValidation = payload.SkipCertValidations;
let checkstate = "";
if (skipTLSValidation){
checkstate = "checked";
}
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
<label>Skip Verification</label>
<small>Check this if you are using self signed certificates</small>
</div>`);
}else if (datatype == "basicauth"){
let requireBasicAuth = payload.RequireBasicAuth;
let checkstate = "";
@ -363,13 +403,24 @@
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="RequireBasicAuth" ${checkstate}>
<label>Require Basic Auth</label>
</div> <button class="ui basic tiny button" style="margin-left: 0.4em;" onclick="editBasicAuthCredentials('${endpointType}','${uuid}');"><i class="ui blue lock icon"></i> Edit Credentials</button>`);
</div>
<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${endpointType}','${uuid}');"><i class="ui blue lock icon"></i> Edit Settings</button>`);
}else if (datatype == 'action'){
column.empty().append(`
<button title="Cancel" onclick="exitProxyInlineEdit('${endpointType}');" class="ui basic small circular icon button"><i class="ui remove icon"></i></button>
<button title="Save" onclick="saveProxyInlineEdit('${uuid}');" class="ui basic small circular icon button"><i class="ui green save icon"></i></button>
`);
}else if (datatype == "inbound" && payload.ProxyType == 0){
let originalContent = $(column).html();
column.empty().append(`${originalContent}
<div class="ui divider"></div>
<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="BypassGlobalTLS" ${payload.BypassGlobalTLS?"checked":""}>
<label>Allow plain HTTP access<br>
<small>Allow inbound connections without TLS/SSL</small></label>
</div><br>
`);
}else{
//Unknown field. Leave it untouched
}
@ -401,6 +452,7 @@
let requireTLS = $(row).find(".RequireTLS")[0].checked;
let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
@ -411,6 +463,7 @@
"type": epttype,
"rootname": uuid,
"ep":newDomain,
"bpgtls": bypassGlobalTLS,
"tls" :requireTLS,
"tlsval": skipCertValidations,
"bauth" :requireBasicAuth,
@ -437,4 +490,67 @@
}));
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
}
/*
Obtain Certificate via ACME
*/
//Load the ACME email from server side
let acmeEmail = "";
$.get("/api/acme/autoRenew/email", function(data){
if (data != "" && data != undefined && data != null){
acmeEmail = data;
}
});
// Obtain certificate from API, only support one domain
function obtainCertificate(domains, usingCa = "Let's Encrypt") {
let filename = "";
let email = acmeEmail;
if (acmeEmail == ""){
let rootDomain = domains.split(".").pop();
email = "admin@" + rootDomain;
}
if (filename.trim() == "" && !domains.includes(",")){
//Zoraxy filename are the matching name for domains.
//Use the same as domains
filename = domains;
}else if (filename != "" && !domains.includes(",")){
//Invalid settings. Force the filename to be same as domain
//if there are only 1 domain
filename = domains;
}else{
parent.msgbox("Filename cannot be empty for certs containing multiple domains.")
return;
}
$.ajax({
url: "/api/acme/obtainCert",
method: "GET",
data: {
domains: domains,
filename: filename,
email: email,
ca: usingCa,
},
success: function(response) {
if (response.error) {
console.log("Error:", response.error);
// Show error message
msgbox(response.error, false, 12000);
} else {
console.log("Certificate installed successfully");
// Show success message
msgbox("Certificate installed successfully");
// Renew the parent certificate list
initManagedDomainCertificateList();
}
},
error: function(error) {
console.log("Failed to install certificate:", error);
}
});
}
</script>

View File

@ -15,8 +15,8 @@
<input type="text" id="statsRangeEnd" placeholder="End date">
</div>
</div>
<button onclick="handleLoadStatisticButtonPress();" class="ui basic button"><i class="blue search icon"></i> Search</button>
<button onclick="clearStatisticDateRange();" class="ui yellow basic button"><i class="eraser icon"></i> Clear Range</button>
<button onclick="handleLoadStatisticButtonPress();" class="ui basic button"><i class="blue search icon"></i> Load</button>
<button onclick="clearStatisticDateRange();" class="ui basic button"><i class="eraser icon"></i> Clear Search</button>
<br>
<small>Leave end range as empty for showing starting day only statistic</small>
</div>
@ -193,7 +193,9 @@
<canvas id="requestTrends"></canvas>
</div>
</div>
<button onclick="showSideWrapper('snippet/advanceStatsOprs.html?t=' + Date.now() + '#' + encodeURIComponent(JSON.stringify(getStatisticDateRange())));" class="ui basic right floated black button"><i class="external square alternate icon"></i> Advance Operations</button>
</div>
<!-- <button class="ui icon right floated basic button" onclick="initStatisticSummery();"><i class="green refresh icon"></i> Refresh</button> -->
<br><br>
</div>
@ -360,6 +362,28 @@
initStatisticSummery(sd, ed);
}
function getStatisticDateRange(){
var sd = $("#statsRangeStart").val();
var ed = $("#statsRangeEnd").val();
if (ed == ""){
ed = sd;
}
if (sd == "" && ed == ""){
var sk = getTodayStatisticKey();
sd = sk;
ed = sk;
}
//Swap them if sd is later than ed
if (sd != "" && ed != "" && sd > ed) {
ed = [sd, sd = ed][0];
}
return [sd, ed];
}
function clearStatisticDateRange(){
$("#statsRangeStart").val("");

View File

@ -72,11 +72,35 @@
<label>Use TLS to serve proxy request</label>
</div>
<br>
<div id="redirect" class="ui toggle notloopbackOnly checkbox" style="margin-top: 0.6em;">
<div id="listenP80" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;" >
<input type="checkbox">
<label>Force redirect HTTP request to HTTPS<br>
<small>(Only apply when listening port is not 80)</small></label>
<label>Enable HTTP server on port 80<br>
<small>(Only apply when TLS enabled and not using port 80)</small></label>
</div>
<br>
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em; padding-left: 2em;">
<input type="checkbox">
<label>Force redirect HTTP request to HTTPS</label>
</div>
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<p>If you have no idea what are these, you can leave them as default :)</p>
<div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
<input type="checkbox">
<label>Force TLS v1.2 or above<br>
<small>(Enhance security, but not compatible with legacy browsers)</small></label>
</div>
<br>
</div>
</div>
</div>
<br><br>
<button id="startbtn" class="ui teal button" onclick="startService();">Start Service</button>
<button id="stopbtn" class="ui red notloopbackOnly disabled button" onclick="stopService();">Stop Service</button>
@ -128,6 +152,8 @@
</div>
<script>
let loopbackProxiedInterface = false;
$(".advanceSettings").accordion();
//Initial the start stop button if this is reverse proxied
$.get("/api/proxy/requestIsProxied", function(data){
if (data == true){
@ -160,6 +186,7 @@
$("#serverstatus").removeClass("green");
}
$("#incomingPort").val(data.Option.Port);
});
}
@ -284,6 +311,27 @@
});
}
function handleP80ListenerStateChange(enabled){
$.ajax({
url: "/api/proxy/listenPort80",
data: {"enable": enabled},
success: function(data){
if (data.error != undefined){
console.log(data.error);
return;
}
if (enabled){
$("#redirect").show();
msgbox("Port 80 listener enabled");
}else{
$("#redirect").hide();
msgbox("Port 80 listener disabled");
}
}
});
}
function handlePortChange(){
var newPortValue = $("#incomingPort").val();
@ -302,6 +350,25 @@
});
}
function initPort80ListenerSetting(){
$.get("/api/proxy/listenPort80", function(data){
if (data){
$("#listenP80").checkbox("set checked");
$("#redirect").show();
}else{
$("#listenP80").checkbox("set unchecked");
$("#redirect").hide();
}
$("#listenP80").find("input").on("change", function(){
let enabled = $(this)[0].checked;
handleP80ListenerStateChange(enabled);
})
});
}
initPort80ListenerSetting();
function initHTTPtoHTTPSRedirectSetting(){
$.get("/api/proxy/useHttpsRedirect", function(data){
if (data == true){
@ -316,7 +383,16 @@
data: {set: thisValue},
success: function(data){
if (data.error != undefined){
alert(data.error);
msgbox(data.error, false, 8000);
//Restore backend value to make sure the UI is always in sync
$.get("/api/proxy/useHttpsRedirect", function(data){
if (data == true){
$("#redirect").checkbox("set checked");
}else{
$("#redirect").checkbox("set unchecked");
}
});
}else{
//Updated
msgbox("Setting Updated");
@ -326,26 +402,53 @@
})
});
});
}
initHTTPtoHTTPSRedirectSetting();
function initTlsVersionSetting(){
$.get("/api/cert/tlsRequireLatest", function(data){
if (data == true){
$("#tlsMinVer").checkbox("set checked");
}else{
$("#tlsMinVer").checkbox("set unchecked");
}
//Bind events to the checkbox
$("#tlsMinVer").find("input").on("change", function(){
let thisValue = $("#tlsMinVer").checkbox("is checked");
$.ajax({
url: "/api/cert/tlsRequireLatest",
data: {"set": thisValue},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("TLS Version Setting Updated");
}
}
})
});
});
}
initTlsVersionSetting();
function initTlsSetting(){
$.get("/api/cert/tls", function(data){
if (data == true){
$("#tls").checkbox("set checked");
}else{
$("#redirect").addClass('disabled');
$(".tlsEnabledOnly").addClass('disabled');
$(".tlsEnabledOnly").addClass('disabled');
}
//Initiate the input listener on the checkbox
$("#tls").find("input").on("change", function(){
let thisValue = $("#tls").checkbox("is checked");
if (thisValue){
$("#redirect").removeClass('disabled');
$(".tlsEnabledOnly").removeClass('disabled');
}else{
$("#redirect").addClass('disabled');
$(".tlsEnabledOnly").addClass('disabled');
}
$.ajax({
url: "/api/cert/tls",
@ -355,7 +458,27 @@
alert(data.error);
}else{
//Updated
msgbox("Setting Updated");
//Check for case if the port is invalid default ports
if ($("#incomingPort").val() == "80" && thisValue == true){
confirmBox("Change listen port to :443?", function(choice){
if (choice == true){
$("#incomingPort").val("443");
handlePortChange();
}
});
}else if ($("#incomingPort").val() == "443" && thisValue == false){
confirmBox("Change listen port to :80?", function(choice){
if (choice == true){
$("#incomingPort").val("80");
handlePortChange();
}
});
}else{
msgbox("Setting Updated");
}
initRPStaste();
}
}

View File

@ -9,7 +9,6 @@
<tr>
<th>Matching Domain</th>
<th>Proxy To</th>
<th>TLS/SSL Verification</th>
<th>Basic Auth</th>
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
</tr>
@ -41,11 +40,14 @@
let subdData = encodeURIComponent(JSON.stringify(subd));
if (subd.RequireTLS){
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
if (subd.SkipCertValidations){
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
}
}
$("#subdList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
<td data-label="" editable="false">${subd.RootOrMatchingDomain}</td>
<td data-label="" editable="true" datatype="inbound"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a></td>
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="skipver">${!subd.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`}</td>
<td data-label="" editable="true" datatype="basicauth">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
<td class="center aligned" editable="true" datatype="action" data-label="">
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="edit icon"></i></button>

View File

@ -4,7 +4,7 @@
<p>Proxy traffic flow on layer 3 via TCP/IP</p>
</div>
<button class="ui basic orange button" id="addProxyConfigButton"><i class="ui add icon"></i> Add Proxy Config</button>
<button class="ui basic circular right floated icon button" title="Refresh List"><i class="ui green refresh icon"></i></button>
<button class="ui basic circular right floated icon button" onclick="initProxyConfigList();" title="Refresh List"><i class="ui green refresh icon"></i></button>
<div class="ui divider"></div>
<div class="ui basic segment" id="addproxyConfig" style="display:none;">
<h3>TCP Proxy Config</h3>
@ -42,11 +42,75 @@
<button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui blue add icon"></i> Create</button>
<button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);"><i class="ui blue save icon"></i> Update</button>
<button class="ui basic red button" onclick="event.preventDefault(); cancelTCPProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
<div class="ui basic inverted segment" style="background-color: #414141; border-radius: 0.6em;">
<h3>Proxy Mode Instructions</h3>
<p>TCP Proxy support the following TCP sockets proxy modes</p>
<table class="ui celled padded inverted basic table">
<thead>
<tr><th class="single line">Mode</th>
<th>Public-IP</th>
<th>Concurrent Access</th>
<th>Flow Diagram</th>
</tr></thead>
<tbody>
<tr>
<td>
<h4 class="ui center aligned inverted header">Transport</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui green check icon"></i> (or same LAN)<br>
</td>
<td>
<i class="ui green check icon"></i>
</td>
<td>Port A (e.g. 25565) <i class="arrow right icon"></i> Server<br>
Server <i class="arrow right icon"></i> Port B (e.g. 192.168.0.2:25565)<br>
<small>Traffic from Port A will be forward to Port B's (IP if provided and) Port</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Listen</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui remove icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Port A (e.g. 8080) <i class="arrow right icon"></i> Server<br>
Port B (e.g. 8081) <i class="arrow right icon"></i> Server<br>
<small>Server will act as a bridge to proxy traffic between Port A and B</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Starter</h4>
</td>
<td class="single line">
Server: <i class="ui times icon"></i><br>
A: <i class="ui green check icon"></i><br>
B: <i class="ui green check icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Server <i class="arrow right icon"></i> Port A (e.g. remote.local.:8080) <br>
Server <i class="arrow right icon"></i> Port B (e.g. recv.local.:8081) <br>
<small>Port A and B will be actively bridged</small>
</td>
</tr>
</tbody>
</table>
</div>
</form>
<div class="ui divider"></div>
</div>
<div class="ui basic segment">
<div class="ui basic segment" style="margin-top: 0;">
<h3>TCP Proxy Configs</h3>
<p>A list of TCP proxy configs created on this host. To enable them, use the toggle button on the right.</p>
<div style="overflow-x: auto; min-height: 400px;">
@ -67,72 +131,7 @@
</table>
</div>
</div>
<div class="ui basic inverted segment" style="background-color: #414141; border-radius: 0.6em;">
<h3>Proxy Mode</h3>
<p>TCP Proxy support the following TCP sockets proxy modes</p>
<table class="ui celled padded inverted basic table">
<thead>
<tr><th class="single line">Mode</th>
<th>Public-IP</th>
<th>Concurrent Access</th>
<th>Flow Diagram</th>
</tr></thead>
<tbody>
<tr>
<td>
<h4 class="ui center aligned inverted header">Transport</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui green check icon"></i> (or same LAN)<br>
</td>
<td>
<i class="ui green check icon"></i>
</td>
<td>Port A (e.g. 25565) <i class="arrow right icon"></i> Server<br>
Server <i class="arrow right icon"></i> Port B (e.g. 192.168.0.2:25565)<br>
<small>Traffic from Port A will be forward to Port B's (IP if provided and) Port</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Listen</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui remove icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Port A (e.g. 8080) <i class="arrow right icon"></i> Server<br>
Port B (e.g. 8081) <i class="arrow right icon"></i> Server<br>
<small>Server will act as a bridge to proxy traffic between Port A and B</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Starter</h4>
</td>
<td class="single line">
Server: <i class="ui times icon"></i><br>
A: <i class="ui green check icon"></i><br>
B: <i class="ui green check icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Server <i class="arrow right icon"></i> Port A (e.g. remote.local.:8080) <br>
Server <i class="arrow right icon"></i> Port B (e.g. recv.local.:8081) <br>
<small>Port A and B will be actively bridged</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
let editingTCPProxyConfigUUID = ""; //The current editing TCP Proxy config UUID
@ -230,11 +229,13 @@
} else {
proxyConfigs.forEach(function(config) {
var runningLogo = '<i class="red circle icon"></i>';
var runningLogo = 'Stopped';
var runningClass = "stopped";
var startButton = `<button onclick="startTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="play icon"></i> Start Proxy</button>`;
if (config.Running){
runningLogo = '<i class="green circle icon"></i>';
runningLogo = 'Running';
startButton = `<button onclick="stopTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="red stop icon"></i> Stop Proxy</button>`;
runningClass = "running"
}
var modeText = "Unknown";
@ -248,8 +249,10 @@
var thisConfig = encodeURIComponent(JSON.stringify(config));
var row = $(`<tr class="tcproxConfig" uuid="${config.UUID}" config="${thisConfig}">`);
row.append($('<td>').html(runningLogo + config.Name));
var row = $(`<tr class="tcproxConfig ${runningClass}" uuid="${config.UUID}" config="${thisConfig}">`);
row.append($('<td>').html(`
${config.Name}
<div class="statusText">${runningLogo}</div>`));
row.append($('<td>').text(config.PortA));
row.append($('<td>').text(config.PortB));
row.append($('<td>').text(modeText));

View File

@ -86,7 +86,7 @@
let id = value[0].ID;
let name = value[0].Name;
let url = value[0].URL;
let url = value[value.length - 1].URL;
let protocol = value[0].Protocol;
//Generate the status dot
@ -109,7 +109,16 @@
}
ontimeRate++;
}else{
dotType = "offline";
if (thisStatus.StatusCode >= 500 && thisStatus.StatusCode < 600){
//Special type of error, cause by downstream reverse proxy
dotType = "error";
}else if (thisStatus.StatusCode == 401){
//Unauthorized error
dotType = "error";
}else{
dotType = "offline";
}
}
let datetime = format_time(thisStatus.Timestamp);
@ -126,12 +135,42 @@
//Check of online status now
let currentOnlineStatus = "Unknown";
let onlineStatusCss = ``;
let reminderEle = ``;
if (value[value.length - 1].Online){
currentOnlineStatus = `<i class="circle icon"></i> Online`;
onlineStatusCss = `color: #3bd671;`;
}else{
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
onlineStatusCss = `color: #df484a;`;
if (value[value.length - 1].StatusCode >= 500 && value[value.length - 1].StatusCode < 600){
currentOnlineStatus = `<i class="exclamation circle icon"></i> Misconfigured`;
onlineStatusCss = `color: #f38020;`;
reminderEle = `<small style="${onlineStatusCss}">Downstream proxy server is online with misconfigured settings</small>`;
}else if (value[value.length - 1].StatusCode >= 400 && value[value.length - 1].StatusCode <= 405){
switch(value[value.length - 1].StatusCode){
case 400:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Bad Request`;
break;
case 401:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Unauthorized`;
break;
case 403:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Forbidden`;
break;
case 404:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Not Found`;
break;
case 405:
currentOnlineStatus = `<i class="exclamation circle icon"></i> Method Not Allowed`;
break;
}
onlineStatusCss = `color: #f38020;`;
reminderEle = `<small style="${onlineStatusCss}">Target online but not accessible</small>`;
}else{
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
onlineStatusCss = `color: #df484a;`;
}
}
//Generate the html
@ -151,6 +190,7 @@
<div class="status" style="marign-top: 1em;">
${statusDotList}
</div>
${reminderEle}
<div class="ui divider"></div>
</div>`);
}

View File

@ -116,7 +116,12 @@
</div>
<p>Results: <div id="ipRangeOutput">N/A</div></p>
</div>
<!-- Config Tools -->
<div class="ui divider"></div>
<h3>System Backup & Restore</h3>
<p>Options related to system backup, migrate and restore.</p>
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
<!-- System Information -->
<div class="ui divider"></div>
<div id="zoraxyinfo">
<h3 class="ui header">

View File

@ -9,7 +9,6 @@
<tr>
<th>Virtual Directory</th>
<th>Proxy To</th>
<th>TLS/SSL Verification</th>
<th>Basic Auth</th>
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
</tr>
@ -43,11 +42,21 @@
let vdirData = encodeURIComponent(JSON.stringify(vdir));
if (vdir.RequireTLS){
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
if (vdir.SkipCertValidations){
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
}
}
let tlsVerificationField = "";
if (vdir.RequireTLS){
tlsVerificationField = !vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`
}else{
tlsVerificationField = "N/A"
}
$("#vdirList").append(`<tr eptuuid="${vdir.RootOrMatchingDomain}" payload="${vdirData}" class="vdirEntry">
<td data-label="" editable="false">${vdir.RootOrMatchingDomain}</td>
<td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="skipver">${!vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`}</td>
<td data-label="" editable="true" datatype="basicauth">${vdir.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
<td class="center aligned" editable="true" datatype="action" data-label="">
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="edit icon"></i></button>

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