mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-07-01 03:41:45 +02:00
Compare commits
108 Commits
Author | SHA1 | Date | |
---|---|---|---|
57135a867e | |||
547855f30f | |||
05b477e90a | |||
3519c7841c | |||
e7b4054248 | |||
973d0b3372 | |||
704980d4f8 | |||
03974163d4 | |||
dfb81513b1 | |||
b604c66a2f | |||
dd84864dd4 | |||
443cd961d2 | |||
10048150bb | |||
85f9b297c4 | |||
07e524a007 | |||
25c7e8ac1a | |||
49babbd60f | |||
fa11422748 | |||
bb1b161ae2 | |||
9545343151 | |||
61e4d45430 | |||
6026c4fd53 | |||
e3f8c99ed3 | |||
fc88dfe72e | |||
d43322f7a5 | |||
83536a83f7 | |||
1183b0ed55 | |||
b00e302f6d | |||
deddb17803 | |||
aa96d831e1 | |||
c6f7f37aaf | |||
63f12dedcf | |||
136d1ecafb | |||
7193defad7 | |||
cf4c57298e | |||
d82a531a41 | |||
7694e317f7 | |||
ed4945ab7e | |||
ce8741bfc8 | |||
7a3db09811 | |||
e73f9b47d3 | |||
c248dacccf | |||
d596d6b843 | |||
6feb2d105d | |||
3a26a5b4d3 | |||
2cdd5654ed | |||
a0d362df4e | |||
334c1ab131 | |||
08d52024ab | |||
a3e16594e8 | |||
cced07ba2d | |||
2003992d75 | |||
71423d98b1 | |||
8ca716c59f | |||
fe48a9a0c3 | |||
ec973eb3bc | |||
7b69b5fa63 | |||
ce4f46cb50 | |||
3454a9b975 | |||
55bc939a37 | |||
1d63b679dc | |||
3df96350a3 | |||
34fab7b3d0 | |||
46817d0664 | |||
1db2ca61fa | |||
0b601406de | |||
b4c771cdee | |||
a486d42351 | |||
90c2199a1b | |||
161c61fac7 | |||
5ffacb1d06 | |||
75ebd0ffbe | |||
dc069f3c57 | |||
e1b512f78f | |||
8854a38f49 | |||
7583a4628c | |||
73c0ea0896 | |||
7dad7c7305 | |||
faa95b4e21 | |||
cb0e13976d | |||
ccd8dcff56 | |||
750656fd7f | |||
d9f515fdba | |||
176249a7d9 | |||
e2a449a7bc | |||
a9695e969e | |||
7ba997dfc2 | |||
d00117e878 | |||
43a84a3f1c | |||
e24f31bdef | |||
fc9240fbac | |||
e0f5431215 | |||
de658a3c6c | |||
73276b1918 | |||
abdb7d4d75 | |||
72299ace15 | |||
4d6c79f51b | |||
2c045f4f40 | |||
b8cf046ca6 | |||
026dd6b89d | |||
5805fe6ed2 | |||
3c78211800 | |||
8e648a8e1f | |||
a000893dd1 | |||
db88bfb752 | |||
b1a14872c3 | |||
df9deb3fbb | |||
9369237229 |
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -33,6 +33,7 @@ If applicable, add screenshots to help explain your problem.
|
||||
- Device: [e.g. Bananapi R2 PRO]
|
||||
- OS: [e.g. Armbian]
|
||||
- Version [e.g. 23.02 Bullseye ]
|
||||
- Docker Version (if you are running Zoraxy in docker): [e.g. 3.0.4]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
25
.github/ISSUE_TEMPLATE/help-needed.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/help-needed.md
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Help Needed
|
||||
about: Something went wrong but I don't know why
|
||||
title: "[HELP]"
|
||||
labels: help wanted
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What happened?**
|
||||
A clear and concise description of what the problem is. Ex. I tried to create a proxy rule but it doesn't work. When I connects to my domain, I see [...]
|
||||
|
||||
**Describe what have you tried**
|
||||
A clear and concise description of what you expect to see and what you have tried to debug it.
|
||||
|
||||
**Describe the networking setup you are using**
|
||||
Here are some example, commonly asked questions from our maintainers:
|
||||
- Are you using the docker build of Zoraxy? [yes (with docker setup & networking config attach) /no]
|
||||
- Your Zoraxy version? [e.g. 3.0.4]
|
||||
- Are you using Cloudflare? [yes/no]
|
||||
- Are your system hosted under a NAT router? [e.g. yes, with subnet is e.g. 192.168.0.0/24 and include port forwarding config if any]
|
||||
- DNS record (if any)
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -32,3 +32,11 @@ src/README.md
|
||||
docker/ContainerTester.sh
|
||||
docker/ImagePublisher.sh
|
||||
src/mod/acme/test/stackoverflow.pem
|
||||
/tools/dns_challenge_update/code-gen/acmedns
|
||||
/tools/dns_challenge_update/code-gen/lego
|
||||
src/tmp/localhost.key
|
||||
src/tmp/localhost.pem
|
||||
src/www/html/index.html
|
||||
src/sys.uuid
|
||||
src/zoraxy
|
||||
src/log/
|
82
CHANGELOG.md
82
CHANGELOG.md
@ -1,3 +1,85 @@
|
||||
# v3.0.6 10 Jun 2024
|
||||
|
||||
+ Added fastly_client_ip to X-Real-IP auto rewrite
|
||||
+ Added atomic accumulator to TCP proxy
|
||||
+ Added white logo for future dark theme
|
||||
+ Added multi selection for white / blacklist [#176](https://github.com/tobychui/zoraxy/issues/176)
|
||||
+ Moved custom header rewrite to dpcore
|
||||
+ Restructure dpcore header rewrite sequence
|
||||
+ Added advance custom header settings (zoraxy to upstream and zoraxy to downstream mode)
|
||||
+ Added header remove feature
|
||||
+ Removed password requirement for SMTP [#162](https://github.com/tobychui/zoraxy/issues/162) [#80](https://github.com/tobychui/zoraxy/issues/80)
|
||||
+ Restructured TCP proxy into Stream Proxy (Support both TCP and UDP) [#147](https://github.com/tobychui/zoraxy/issues/147)
|
||||
+ Added stream proxy auto start [#169](https://github.com/tobychui/zoraxy/issues/169)
|
||||
+ Optimized UX for reminding user to click Apply after port change
|
||||
+ Added version number to footer [#160](https://github.com/tobychui/zoraxy/issues/160)
|
||||
+ Fixed missing / unnecessary error check [PR187](https://github.com/tobychui/zoraxy/pull/187) by [Kirari04](https://github.com/Kirari04)
|
||||
|
||||
# v3.0.5 May 26 2024
|
||||
|
||||
|
||||
+ Optimized uptime monitor error message [#121](https://github.com/tobychui/zoraxy/issues/121)
|
||||
+ Optimized detection logic for internal proxy target and header rewrite condition for HTTP_HOST [#164](https://github.com/tobychui/zoraxy/issues/164)
|
||||
+ Fixed ovh DNS challenge provider form generator bug [#161](https://github.com/tobychui/zoraxy/issues/161)
|
||||
+ Added permission policy module (not enabled)
|
||||
+ Added single-use cookiejar to uptime monitor request client to handle cookie issues on some poorly written back-end server [#149](https://github.com/tobychui/zoraxy/issues/149)
|
||||
|
||||
|
||||
# v3.0.4 May 18 2024
|
||||
|
||||
## This release tidied up the contribution by [Teifun2](https://github.com/Teifun2) and added a new way to generate DNS challenge based certificate (e.g. wildcards) from Let's Encrypt without changing any environment variables. This also fixes a few previous ACME module EAB settings bug related to concurrent save.
|
||||
|
||||
You can find the DNS challenge settings under TLS / SSL > ACME snippet > Generate New Certificate > (Check the "Use a DNS Challenge" checkbox)
|
||||
|
||||
+ Optimized DNS challenge implementation [thanks to Teifun2](https://github.com/Teifun2) / Issues [#49](https://github.com/tobychui/zoraxy/issues/49) [#79](https://github.com/tobychui/zoraxy/issues/79)
|
||||
+ Removed dependencies on environment variable write and keep all data contained
|
||||
+ Fixed panic on loading certificate generated by Zoraxy v2
|
||||
+ Added automatic form generator for DNS challenge / providers
|
||||
+ Added CA name default value
|
||||
+ Added code generator for acmedns module (storing the DNS challenge provider contents extracted from lego)
|
||||
+ Fixed ACME snippet "Obtain Certificate" concurrent issues in save EAB and DNS credentials
|
||||
|
||||
|
||||
# v3.0.3 Apr 30 2024
|
||||
## Breaking Change
|
||||
|
||||
For users using SMTP with older versions, you might need to update the settings by moving the domains (the part after @ in the username and domain setup field) into the username field.
|
||||
|
||||
+ Updated SMTP UI for non email login username [#129](https://github.com/tobychui/zoraxy/issues/129)
|
||||
+ Fixed ACME cert store reload after cert request [#126](https://github.com/tobychui/zoraxy/issues/126)
|
||||
+ Fixed default rule not applying to default site when default site is set to proxy target [#130](https://github.com/tobychui/zoraxy/issues/130)
|
||||
+ Fixed blacklist-ip not working with CIDR bug
|
||||
+ Fixed minor vdir bug in tailing slash detection and redirect logic
|
||||
+ Added custom mdns name support (-mdnsname flag)
|
||||
+ Added LAN tag in statistic [#131](https://github.com/tobychui/zoraxy/issues/131)
|
||||
|
||||
|
||||
# v3.0.2 Apr 24 2024
|
||||
|
||||
+ Added alias for HTTP proxy host names [#76](https://github.com/tobychui/zoraxy/issues/76)
|
||||
+ Added separator support for create new proxy rules (use "," to add alias when creating new proxy rule)
|
||||
+ Added HTTP proxy host based access rules [#69](https://github.com/tobychui/zoraxy/issues/69)
|
||||
+ Added EAD Configuration for ACME (by [yeungalan](https://github.com/yeungalan)) [#45](https://github.com/tobychui/zoraxy/issues/45)
|
||||
+ Fixed bug for bypassGlobalTLS endpoint do not support basic-auth
|
||||
+ Fixed panic due to empty domain field in json config [#120](https://github.com/tobychui/zoraxy/issues/120)
|
||||
+ Removed dependencies on management panel css for online font files
|
||||
|
||||
# v3.0.1 Apr 04 2024
|
||||
|
||||
## Bugfixupdate for big release of V3, read update notes from V3 if you are still on V2
|
||||
|
||||
+ Added regex support for redirect (slow, don't use it unless you really needs it) [#42](https://github.com/tobychui/zoraxy/issues/42)
|
||||
+ Added new dpcore implementations for faster proxy speed
|
||||
+ Added support for CF-Connecting-IP to X-Real-IP auto rewrite [#114](https://github.com/tobychui/zoraxy/issues/114)
|
||||
+ Added enable / disable of HTTP proxy rules in runtime via slider [#108](https://github.com/tobychui/zoraxy/issues/108)
|
||||
+ Added better 404 page
|
||||
+ Added option to bypass websocket origin check [#107](https://github.com/tobychui/zoraxy/issues/107)
|
||||
+ Updated project homepage design
|
||||
+ Fixed recursive port detection logic
|
||||
+ Fixed UserAgent in resp bug
|
||||
+ Updated minimum required Go version to v1.22 (Notes: Windows 7 support is dropped) [#112](https://github.com/tobychui/zoraxy/issues/112)
|
||||
|
||||
|
||||
# v3.0.0 Feb 18 2024
|
||||
|
||||
## IMPORTANT: V3 is a big rewrite and it is incompatible with V2! There is NO migration, if you want to stay on V2, please use V2 branch!
|
||||
|
35
README.md
35
README.md
@ -2,22 +2,23 @@
|
||||
|
||||
# Zoraxy
|
||||
|
||||
General purpose request (reverse) proxy and forwarding tool for networking noobs. Now written in Go!
|
||||
A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
|
||||
|
||||
*Zoraxy v3 HTTP proxy config is not compatible with the older v2. If you are looking for the legacy version of Zoraxy, take a look at the [v2 branch](https://github.com/tobychui/zoraxy/tree/v2)*
|
||||
|
||||
### Features
|
||||
|
||||
- Simple to use interface with detail in-system instructions
|
||||
- Reverse Proxy
|
||||
- Reverse Proxy (HTTP/2)
|
||||
- Virtual Directory
|
||||
- WebSocket Proxy (automatic, no set-up needed)
|
||||
- Basic Auth
|
||||
- Alias Hostnames
|
||||
- Custom Headers
|
||||
- Redirection Rules
|
||||
- TLS / SSL setup and deploy
|
||||
- ACME features like auto-renew to serve your sites in http**s**
|
||||
- SNI support (one certificate contains multiple host names)
|
||||
|
||||
- SNI support (and SAN certs)
|
||||
- DNS Challenge for Let's Encrypt and [these DNS providers](https://go-acme.github.io/lego/dns/)
|
||||
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- Global Area Network Controller Web UI (ZeroTier not included)
|
||||
- TCP Tunneling / Proxy
|
||||
@ -33,13 +34,21 @@ General purpose request (reverse) proxy and forwarding tool for networking noobs
|
||||
- SMTP config for password reset
|
||||
|
||||
## Downloads
|
||||
|
||||
[Windows](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_windows_amd64.exe)
|
||||
/[Linux (amd64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_amd64)
|
||||
/[Linux (arm64)](https://github.com/tobychui/zoraxy/releases/latest/download/zoraxy_linux_arm64)
|
||||
|
||||
For other systems or architectures, please see [Release](https://github.com/tobychui/zoraxy/releases/latest/)
|
||||
|
||||
## Getting Started
|
||||
[Installing Zoraxy Reverse Proxy: Your Gateway to Efficient Web Routing](https://geekscircuit.com/installing-zoraxy-reverse-proxy-your-gateway-to-efficient-web-routing/)
|
||||
|
||||
Thank you for the well written and easy to follow tutorial by Reddit users [itsvmn](https://www.reddit.com/user/itsvmn/)!
|
||||
If you have no background in setting up reverse proxy or web routing, you should check this out before you start setting up your Zoraxy.
|
||||
|
||||
## Build from Source
|
||||
|
||||
Requires Go 1.22 or higher
|
||||
|
||||
```bash
|
||||
@ -57,7 +66,7 @@ Zoraxy provides basic authentication system for standalone mode. To use it in st
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Standalone mode is the default mode for Zoraxy. This allows a single account to manage your reverse proxy server, just like a home router. This mode is suitable for new owners to homelabs or makers starting growing their web services into multiple servers.
|
||||
Standalone mode is the default mode for Zoraxy. This allows a single account to manage your reverse proxy server just like a basic home router. This mode is suitable for new owners to homelabs or makers starting growing their web services into multiple servers. A full "Getting Started" guide can be found [here](https://github.com/tobychui/zoraxy/wiki/Getting-Started).
|
||||
|
||||
#### Linux
|
||||
|
||||
@ -78,21 +87,25 @@ The installation method is same as Linux. If you are using a Raspberry Pi 4 or n
|
||||
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
|
||||
|
||||
#### Docker
|
||||
|
||||
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details.
|
||||
|
||||
### Start Paramters
|
||||
|
||||
```
|
||||
Usage of zoraxy:
|
||||
-autorenew int
|
||||
ACME auto TLS/SSL certificate renew check interval (seconds) (default 86400)
|
||||
-docker
|
||||
Run Zoraxy in docker compatibility mode
|
||||
-fastgeoip
|
||||
Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)
|
||||
-info
|
||||
Show information about this program in JSON
|
||||
-log
|
||||
Log terminal output to file (default true)
|
||||
-mdns
|
||||
Enable mDNS scanner and transponder (default true)
|
||||
-mdnsname string
|
||||
mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)
|
||||
-noauth
|
||||
Disable authentication for management interface
|
||||
-port string
|
||||
@ -152,6 +165,7 @@ This allows you to have an infinite number of network members in your Global Are
|
||||
## Web SSH
|
||||
|
||||
Web SSH currently only supports Linux based OSes. The following platforms are supported:
|
||||
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/armv6 (experimental)
|
||||
@ -166,12 +180,13 @@ Loopback web SSH connection, by default, is disabled. This means that if you are
|
||||
```
|
||||
|
||||
## Sponsor This Project
|
||||
|
||||
If you like the project and want to support us, please consider a donation. You can use the links below
|
||||
|
||||
- [tobychui (Primary author)](https://paypal.me/tobychui)
|
||||
- PassiveLemon (Docker compatibility maintainer)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This project is open-sourced under AGPL. I open-sourced this project so everyone can check for security issues and benefit all users. **If you plan to use this project in a commercial environment (which violate the AGPL terms), please contact toby@imuslab.com for an alternative license.**
|
||||
This project is open-sourced under AGPL. I open-sourced this project so everyone can check for security issues and benefit all users. **This software is intended to be free of charge. If you have acquired this software from a third-party seller, the authors of this repository bears no responsibility for any technical difficulties assistance or support.**
|
||||
|
||||
|
@ -8,10 +8,7 @@ RUN mkdir -p /opt/zoraxy/source/ &&\
|
||||
mkdir -p /opt/zoraxy/config/ &&\
|
||||
mkdir -p /usr/local/bin/
|
||||
|
||||
COPY entrypoint.sh /opt/zoraxy/
|
||||
|
||||
RUN chmod -R 755 /opt/zoraxy/ &&\
|
||||
chmod +x /opt/zoraxy/entrypoint.sh
|
||||
RUN chmod -R 770 /opt/zoraxy/
|
||||
|
||||
VOLUME [ "/opt/zoraxy/config/" ]
|
||||
|
||||
@ -24,15 +21,15 @@ RUN go mod tidy &&\
|
||||
go build -o /usr/local/bin/zoraxy &&\
|
||||
rm -r /opt/zoraxy/source/
|
||||
|
||||
RUN chmod +x /usr/local/bin/zoraxy
|
||||
RUN chmod 755 /usr/local/bin/zoraxy &&\
|
||||
chmod +x /usr/local/bin/zoraxy
|
||||
|
||||
WORKDIR /opt/zoraxy/config/
|
||||
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
ENV ARGS="-noauth=false"
|
||||
|
||||
ENTRYPOINT ["/opt/zoraxy/entrypoint.sh"]
|
||||
ENTRYPOINT "zoraxy" "-port=:8000" "-docker=true" "${ARGS}"
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=5s --retries=2 CMD nc -vz 127.0.0.1 8000 || exit 1
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
echo "Zoraxy version $VERSION"
|
||||
|
||||
zoraxy -port=:8000 ${ARGS}
|
@ -80,7 +80,7 @@
|
||||
<div class="bannerHeaderWrapper">
|
||||
<h1 class="bannerHeader">Zoraxy</h1>
|
||||
<div class="ui divider"></div><br>
|
||||
<p class="bannerSubheader">All in one homelab network routing solution</p>
|
||||
<p class="bannerSubheader">Beyond Reverse Proxy: Your Ultimate Homelab Network Tool</p>
|
||||
</div>
|
||||
<br><br>
|
||||
<a class="ui basic big button" style="background-color: white;" href="#features"><i class="ui blue arrow down icon"></i> Learn More</a>
|
||||
|
26
example/README.md
Normal file
26
example/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Example www Folder
|
||||
|
||||
This is an example www folder that contains two sub-folders.
|
||||
|
||||
- `html/`
|
||||
- `templates/`
|
||||
|
||||
The html file contain static resources that will be served by Zoraxy build-in static web server. You can use it as a generic web server with a static site generator like [Hugo](https://gohugo.io/) or use it as a small CDN for serving your scripts / image that commonly use across many of your sites.
|
||||
|
||||
The templates folder contains the template for overriding the build in error or access denied pages. The following templates are supported
|
||||
|
||||
- notfound.html (Default site Not-Found error page)
|
||||
- whitelist.html (Error page when client being blocked by whitelist rule)
|
||||
- blacklist.html (Error page when client being blocked by blacklist rule)
|
||||
|
||||
To use the template, copy and paste the `wwww` folder to the same directory as zoraxy executable (aka the src/ file if you `go build` with the current folder tree).
|
||||
|
||||
|
||||
|
||||
### Other Templates
|
||||
|
||||
There are a few pre-built templates that works with Zoraxy where you can find in the `other-templates` folder. Copy the folder into `www` and rename the folder to `templates` to active them.
|
||||
|
||||
|
||||
|
||||
It is worth mentioning that the uwu icons for not-found and access-denied are created by @SAWARATSUKI
|
185
example/other-templates/templates_cf/blacklist.html
Normal file
185
example/other-templates/templates_cf/blacklist.html
Normal file
File diff suppressed because one or more lines are too long
154
example/other-templates/templates_cf/notfound.html
Normal file
154
example/other-templates/templates_cf/notfound.html
Normal file
@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="theme-color" content="#4b75ff">
|
||||
<link rel="icon" type="image/png" href="img/small_icon.png"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700;900&display=swap" rel="stylesheet">
|
||||
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script>
|
||||
<title>404 - Host Not Found</title>
|
||||
<style>
|
||||
h1, h2, h3, h4, h5, p, a, span, .ui.list .item{
|
||||
font-family: 'Noto Sans TC', sans-serif;
|
||||
font-weight: 300;
|
||||
color: rgb(88, 88, 88)
|
||||
}
|
||||
|
||||
.diagram{
|
||||
background-color: #ebebeb;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
.diagramHeader{
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
|
||||
@media (max-width:512px) {
|
||||
.widescreenOnly{
|
||||
display: none !important;
|
||||
|
||||
}
|
||||
|
||||
.four.wide.column:not(.widescreenOnly){
|
||||
width: 50% !important;
|
||||
}
|
||||
|
||||
.ui.grid{
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<br><br>
|
||||
<div class="ui container">
|
||||
<h1 style="font-size: 4rem;">Error 404</h1>
|
||||
<p style="font-size: 2rem; margin-bottom: 0.4em;">Target Host Not Found</p>
|
||||
<small id="timestamp"></small>
|
||||
</div>
|
||||
<br><br>
|
||||
</div>
|
||||
<div class="diagram">
|
||||
<div class="ui text container">
|
||||
<div class="ui grid">
|
||||
<div class="four wide column widescreenOnly" align="center">
|
||||
<svg version="1.1" id="client_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<path fill="#C9CACA" d="M184.795,143.037c0,9.941-8.059,18-18,18H33.494c-9.941,0-18-8.059-18-18V44.952c0-9.941,8.059-18,18-18
|
||||
h133.301c9.941,0,18,8.059,18,18V143.037z"/>
|
||||
<circle fill="#FFFFFF" cx="37.39" cy="50.88" r="6.998"/>
|
||||
<circle fill="#FFFFFF" cx="54.115" cy="50.88" r="6.998"/>
|
||||
<path fill="#FFFFFF" d="M167.188,50.88c0,3.865-3.133,6.998-6.998,6.998H72.379c-3.865,0-6.998-3.133-6.998-6.998l0,0
|
||||
c0-3.865,3.133-6.998,6.998-6.998h87.811C164.055,43.882,167.188,47.015,167.188,50.88L167.188,50.88z"/>
|
||||
<rect x="31.296" y="66.907" fill="#FFFFFF" width="132.279" height="77.878"/>
|
||||
<circle fill="#9BCA3E" cx="96.754" cy="144.785" r="37.574"/>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="108.497,133.047 93.373,153.814
|
||||
82.989,143.204 "/>
|
||||
</svg>
|
||||
<small>You</small>
|
||||
<h2 class="diagramHeader">Browser</h2>
|
||||
<p style="font-weight: 500; color: #9bca3e;">Working</p>
|
||||
</div>
|
||||
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
|
||||
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
|
||||
</div>
|
||||
<div class="four wide column widescreenOnly" align="center">
|
||||
<svg version="1.1" id="cloud_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<ellipse fill="#9FA0A0" cx="46.979" cy="108.234" rx="25.399" ry="25.139"/>
|
||||
<circle fill="#9FA0A0" cx="109.407" cy="100.066" r="50.314"/>
|
||||
<circle fill="#9FA0A0" cx="22.733" cy="129.949" r="19.798"/>
|
||||
<circle fill="#9FA0A0" cx="172.635" cy="125.337" r="24.785"/>
|
||||
<path fill="#9FA0A0" d="M193.514,133.318c0,9.28-7.522,16.803-16.803,16.803H28.223c-9.281,0-16.803-7.522-16.803-16.803l0,0
|
||||
c0-9.28,7.522-16.804,16.803-16.804h148.488C185.991,116.515,193.514,124.038,193.514,133.318L193.514,133.318z"/>
|
||||
<circle fill="#9BCA3D" cx="100" cy="149.572" r="38.267"/>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="113.408,136.402 95.954,160.369
|
||||
83.971,148.123 "/>
|
||||
</svg>
|
||||
|
||||
<small>Gateway Node</small>
|
||||
<h2 class="diagramHeader">Reverse Proxy</h2>
|
||||
<p style="font-weight: 500; color: #9bca3e;">Working</p>
|
||||
</div>
|
||||
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
|
||||
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
|
||||
</div>
|
||||
<div class="four wide column" align="center">
|
||||
<svg version="1.1" id="host_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<path fill="#999999" d="M168.484,113.413c0,9.941,3.317,46.324-6.624,46.324H35.359c-9.941,0-5.873-39.118-5.715-46.324
|
||||
l17.053-50.909c1.928-9.879,8.059-18,18-18h69.419c9.941,0,15.464,7.746,18,18L168.484,113.413z"/>
|
||||
<rect x="38.068" y="118.152" fill="#FFFFFF" width="122.573" height="34.312"/>
|
||||
<circle fill="#BD2426" cx="141.566" cy="135.873" r="8.014"/>
|
||||
<circle fill="#BD2426" cx="99.354" cy="152.464" r="36.343"/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="144.125" x2="107.594" y2="161.946"/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="161.946" x2="107.594" y2="144.79"/>
|
||||
</svg>
|
||||
<small id="host"></small>
|
||||
<h2 class="diagramHeader">Host</h2>
|
||||
<p style="font-weight: 500; color: #bd2426;">Not Found</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui stackable grid">
|
||||
<div class="eight wide column">
|
||||
<h1>What happend?</h1>
|
||||
<p>The reverse proxy target domain is not found.<br>For more information, see the error message on the reverse proxy terminal.</p>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<h1>What can I do?</h1>
|
||||
<h5 style="font-weight: 500;">If you are a visitor of this website: </h5>
|
||||
<p>Please try again in a few minutes</p>
|
||||
<h5 style="font-weight: 500;">If you are the owner of this website:</h5>
|
||||
<div class="ui bulleted list">
|
||||
<div class="item">Check if the proxy rules that match this hostname exists</div>
|
||||
<div class="item">Visit the Reverse Proxy management interface to correct any setting errors</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui container" style="color: grey; font-size: 90%">
|
||||
<p>Powered by Zoraxy</p>
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
<script>
|
||||
$("#timestamp").text(new Date());
|
||||
$("#host").text(location.href);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
185
example/other-templates/templates_cf/whitelist.html
Normal file
185
example/other-templates/templates_cf/whitelist.html
Normal file
File diff suppressed because one or more lines are too long
52
example/other-templates/templates_uwu/blacklist.html
Normal file
52
example/other-templates/templates_uwu/blacklist.html
Normal file
File diff suppressed because one or more lines are too long
42
example/other-templates/templates_uwu/notfound.html
Normal file
42
example/other-templates/templates_uwu/notfound.html
Normal file
File diff suppressed because one or more lines are too long
52
example/other-templates/templates_uwu/whitelist.html
Normal file
52
example/other-templates/templates_uwu/whitelist.html
Normal file
File diff suppressed because one or more lines are too long
229
example/www/html/index.html
Normal file
229
example/www/html/index.html
Normal file
@ -0,0 +1,229 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Zoraxy Firework!</title>
|
||||
<style>
|
||||
body{
|
||||
margin: 0 !important;
|
||||
}
|
||||
canvas {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.2/anime.min.js" integrity="sha512-aNMyYYxdIxIaot0Y1/PLuEu3eipGCmsEUBrUq+7aVyPGMFH8z0eTP0tkqAvv34fzN6z+201d3T8HPb1svWSKHQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="c"></canvas>
|
||||
<script>
|
||||
var c = document.getElementById("c");
|
||||
var ctx = c.getContext("2d");
|
||||
var cH;
|
||||
var cW;
|
||||
var bgColor = "#FF6138";
|
||||
var animations = [];
|
||||
var circles = [];
|
||||
|
||||
var colorPicker = (function() {
|
||||
var colors = ["#FF6138", "#FFBE53", "#2980B9", "#FCFCFC", "#282741"];
|
||||
var index = 0;
|
||||
function next() {
|
||||
index = index++ < colors.length-1 ? index : 0;
|
||||
return colors[index];
|
||||
}
|
||||
function current() {
|
||||
return colors[index]
|
||||
}
|
||||
return {
|
||||
next: next,
|
||||
current: current
|
||||
}
|
||||
})();
|
||||
|
||||
function removeAnimation(animation) {
|
||||
var index = animations.indexOf(animation);
|
||||
if (index > -1) animations.splice(index, 1);
|
||||
}
|
||||
|
||||
function calcPageFillRadius(x, y) {
|
||||
var l = Math.max(x - 0, cW - x);
|
||||
var h = Math.max(y - 0, cH - y);
|
||||
return Math.sqrt(Math.pow(l, 2) + Math.pow(h, 2));
|
||||
}
|
||||
|
||||
function addClickListeners() {
|
||||
document.addEventListener("touchstart", handleEvent);
|
||||
document.addEventListener("mousedown", handleEvent);
|
||||
};
|
||||
|
||||
function handleEvent(e) {
|
||||
if (e.touches) {
|
||||
e.preventDefault();
|
||||
e = e.touches[0];
|
||||
}
|
||||
var currentColor = colorPicker.current();
|
||||
var nextColor = colorPicker.next();
|
||||
var targetR = calcPageFillRadius(e.pageX, e.pageY);
|
||||
var rippleSize = Math.min(200, (cW * .4));
|
||||
var minCoverDuration = 750;
|
||||
|
||||
var pageFill = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
r: 0,
|
||||
fill: nextColor
|
||||
});
|
||||
var fillAnimation = anime({
|
||||
targets: pageFill,
|
||||
r: targetR,
|
||||
duration: Math.max(targetR / 2 , minCoverDuration ),
|
||||
easing: "easeOutQuart",
|
||||
complete: function(){
|
||||
bgColor = pageFill.fill;
|
||||
removeAnimation(fillAnimation);
|
||||
}
|
||||
});
|
||||
|
||||
var ripple = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
r: 0,
|
||||
fill: currentColor,
|
||||
stroke: {
|
||||
width: 3,
|
||||
color: currentColor
|
||||
},
|
||||
opacity: 1
|
||||
});
|
||||
var rippleAnimation = anime({
|
||||
targets: ripple,
|
||||
r: rippleSize,
|
||||
opacity: 0,
|
||||
easing: "easeOutExpo",
|
||||
duration: 900,
|
||||
complete: removeAnimation
|
||||
});
|
||||
|
||||
var particles = [];
|
||||
for (var i=0; i<32; i++) {
|
||||
var particle = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
fill: currentColor,
|
||||
r: anime.random(24, 48)
|
||||
})
|
||||
particles.push(particle);
|
||||
}
|
||||
var particlesAnimation = anime({
|
||||
targets: particles,
|
||||
x: function(particle){
|
||||
return particle.x + anime.random(rippleSize, -rippleSize);
|
||||
},
|
||||
y: function(particle){
|
||||
return particle.y + anime.random(rippleSize * 1.15, -rippleSize * 1.15);
|
||||
},
|
||||
r: 0,
|
||||
easing: "easeOutExpo",
|
||||
duration: anime.random(1000,1300),
|
||||
complete: removeAnimation
|
||||
});
|
||||
animations.push(fillAnimation, rippleAnimation, particlesAnimation);
|
||||
}
|
||||
|
||||
function extend(a, b){
|
||||
for(var key in b) {
|
||||
if(b.hasOwnProperty(key)) {
|
||||
a[key] = b[key];
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
var Circle = function(opts) {
|
||||
extend(this, opts);
|
||||
}
|
||||
|
||||
Circle.prototype.draw = function() {
|
||||
ctx.globalAlpha = this.opacity || 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
|
||||
if (this.stroke) {
|
||||
ctx.strokeStyle = this.stroke.color;
|
||||
ctx.lineWidth = this.stroke.width;
|
||||
ctx.stroke();
|
||||
}
|
||||
if (this.fill) {
|
||||
ctx.fillStyle = this.fill;
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
var animate = anime({
|
||||
duration: Infinity,
|
||||
update: function() {
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fillRect(0, 0, cW, cH);
|
||||
animations.forEach(function(anim) {
|
||||
anim.animatables.forEach(function(animatable) {
|
||||
animatable.target.draw();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var resizeCanvas = function() {
|
||||
cW = window.innerWidth;
|
||||
cH = window.innerHeight;
|
||||
c.width = cW * devicePixelRatio;
|
||||
c.height = cH * devicePixelRatio;
|
||||
ctx.scale(devicePixelRatio, devicePixelRatio);
|
||||
};
|
||||
|
||||
(function init() {
|
||||
resizeCanvas();
|
||||
if (window.CP) {
|
||||
// CodePen's loop detection was causin' problems
|
||||
// and I have no idea why, so...
|
||||
window.CP.PenTimer.MAX_TIME_IN_LOOP_WO_EXIT = 6000;
|
||||
}
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
addClickListeners();
|
||||
if (!!window.location.pathname.match(/fullcpgrid/)) {
|
||||
startFauxClicking();
|
||||
}
|
||||
handleInactiveUser();
|
||||
})();
|
||||
|
||||
function handleInactiveUser() {
|
||||
var inactive = setTimeout(function(){
|
||||
fauxClick(cW/2, cH/2);
|
||||
}, 2000);
|
||||
|
||||
function clearInactiveTimeout() {
|
||||
clearTimeout(inactive);
|
||||
document.removeEventListener("mousedown", clearInactiveTimeout);
|
||||
document.removeEventListener("touchstart", clearInactiveTimeout);
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", clearInactiveTimeout);
|
||||
document.addEventListener("touchstart", clearInactiveTimeout);
|
||||
}
|
||||
|
||||
function startFauxClicking() {
|
||||
setTimeout(function(){
|
||||
fauxClick(anime.random( cW * .2, cW * .8), anime.random(cH * .2, cH * .8));
|
||||
startFauxClicking();
|
||||
}, anime.random(200, 900));
|
||||
}
|
||||
|
||||
function fauxClick(x, y) {
|
||||
var fauxClick = new Event("mousedown");
|
||||
fauxClick.pageX = x;
|
||||
fauxClick.pageY = y;
|
||||
document.dispatchEvent(fauxClick);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
52
example/www/templates/blacklist.html
Normal file
52
example/www/templates/blacklist.html
Normal file
File diff suppressed because one or more lines are too long
42
example/www/templates/notfound.html
Normal file
42
example/www/templates/notfound.html
Normal file
File diff suppressed because one or more lines are too long
52
example/www/templates/whitelist.html
Normal file
52
example/www/templates/whitelist.html
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
Binary file not shown.
BIN
img/title.png
BIN
img/title.png
Binary file not shown.
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 69 KiB |
BIN
img/title.psd
BIN
img/title.psd
Binary file not shown.
@ -3,9 +3,12 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -17,6 +20,157 @@ import (
|
||||
banning / whitelist a specific IP address or country code
|
||||
*/
|
||||
|
||||
/*
|
||||
General Function
|
||||
*/
|
||||
|
||||
func handleListAccessRules(w http.ResponseWriter, r *http.Request) {
|
||||
allAccessRules := accessController.ListAllAccessRules()
|
||||
js, _ := json.Marshal(allAccessRules)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func handleAttachRuleToHost(w http.ResponseWriter, r *http.Request) {
|
||||
ruleid, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule name")
|
||||
return
|
||||
}
|
||||
|
||||
host, err := utils.PostPara(r, "host")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule name")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if access rule and proxy rule exists
|
||||
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(host)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid host given")
|
||||
return
|
||||
}
|
||||
if !accessController.AccessRuleExists(ruleid) {
|
||||
utils.SendErrorResponse(w, "access rule not exists")
|
||||
return
|
||||
}
|
||||
|
||||
//Update the proxy host acess rule id
|
||||
targetProxyEndpoint.AccessFilterUUID = ruleid
|
||||
targetProxyEndpoint.UpdateToRuntime()
|
||||
err = SaveReverseProxyConfig(targetProxyEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Create a new access rule, require name and desc only
|
||||
func handleCreateAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
ruleName, err := utils.PostPara(r, "name")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule name")
|
||||
return
|
||||
}
|
||||
ruleDesc, _ := utils.PostPara(r, "desc")
|
||||
|
||||
//Filter out injection if any
|
||||
p := bluemonday.StripTagsPolicy()
|
||||
ruleName = p.Sanitize(ruleName)
|
||||
ruleDesc = p.Sanitize(ruleDesc)
|
||||
|
||||
ruleUUID := uuid.New().String()
|
||||
newAccessRule := access.AccessRule{
|
||||
ID: ruleUUID,
|
||||
Name: ruleName,
|
||||
Desc: ruleDesc,
|
||||
BlacklistEnabled: false,
|
||||
WhitelistEnabled: false,
|
||||
}
|
||||
|
||||
//Add it to runtime
|
||||
err = accessController.AddNewAccessRule(&newAccessRule)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Handle removing an access rule. All proxy endpoint using this rule will be
|
||||
// set to use the default rule
|
||||
func handleRemoveAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule id given")
|
||||
return
|
||||
}
|
||||
|
||||
if ruleID == "default" {
|
||||
utils.SendErrorResponse(w, "default access rule cannot be removed")
|
||||
return
|
||||
}
|
||||
|
||||
ruleID = strings.TrimSpace(ruleID)
|
||||
|
||||
//Set all proxy hosts that use this access rule back to using "default"
|
||||
allProxyEndpoints := dynamicProxyRouter.GetProxyEndpointsAsMap()
|
||||
for _, proxyEndpoint := range allProxyEndpoints {
|
||||
if strings.EqualFold(proxyEndpoint.AccessFilterUUID, ruleID) {
|
||||
//This proxy endpoint is using the current access filter.
|
||||
//set it to default
|
||||
proxyEndpoint.AccessFilterUUID = "default"
|
||||
proxyEndpoint.UpdateToRuntime()
|
||||
err = SaveReverseProxyConfig(proxyEndpoint)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Access", "Unable to save updated proxy endpoint "+proxyEndpoint.RootOrMatchingDomain, err)
|
||||
} else {
|
||||
SystemWideLogger.PrintAndLog("Access", "Updated "+proxyEndpoint.RootOrMatchingDomain+" access filter to \"default\"", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Remove the access rule by ID
|
||||
err = accessController.RemoveAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
SystemWideLogger.PrintAndLog("Access", "Access Rule "+ruleID+" removed", nil)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Only the name and desc, for other properties use blacklist / whitelist api
|
||||
func handleUpadateAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule id")
|
||||
return
|
||||
}
|
||||
ruleName, err := utils.PostPara(r, "name")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rule name")
|
||||
return
|
||||
}
|
||||
ruleDesc, _ := utils.PostPara(r, "desc")
|
||||
|
||||
//Filter anything weird
|
||||
p := bluemonday.StrictPolicy()
|
||||
ruleName = p.Sanitize(ruleName)
|
||||
ruleDesc = p.Sanitize(ruleDesc)
|
||||
|
||||
err = accessController.UpdateAccessRule(ruleID, ruleName, ruleDesc)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
/*
|
||||
Blacklist Related
|
||||
*/
|
||||
@ -28,11 +182,24 @@ func handleListBlacklisted(w http.ResponseWriter, r *http.Request) {
|
||||
bltype = "country"
|
||||
}
|
||||
|
||||
ruleID, err := utils.GetPara(r, "id")
|
||||
if err != nil {
|
||||
//Use default if not set
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
//Load the target rule from access controller
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resulst := []string{}
|
||||
if bltype == "country" {
|
||||
resulst = geodbStore.GetAllBlacklistedCountryCode()
|
||||
resulst = rule.GetAllBlacklistedCountryCode()
|
||||
} else if bltype == "ip" {
|
||||
resulst = geodbStore.GetAllBlacklistedIp()
|
||||
resulst = rule.GetAllBlacklistedIp()
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(resulst)
|
||||
@ -47,7 +214,23 @@ func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddCountryCodeToBlackList(countryCode)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
comment, _ := utils.PostPara(r, "comment")
|
||||
p := bluemonday.StripTagsPolicy()
|
||||
comment = p.Sanitize(comment)
|
||||
|
||||
//Load the target rule from access controller
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rule.AddCountryCodeToBlackList(countryCode, comment)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -59,7 +242,19 @@ func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveCountryCodeFromBlackList(countryCode)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
//Load the target rule from access controller
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rule.RemoveCountryCodeFromBlackList(countryCode)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -71,7 +266,24 @@ func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddIPToBlackList(ipAddr)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
//Load the target rule from access controller
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
comment, _ := utils.GetPara(r, "comment")
|
||||
p := bluemonday.StripTagsPolicy()
|
||||
comment = p.Sanitize(comment)
|
||||
|
||||
rule.AddIPToBlackList(ipAddr, comment)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
@ -81,23 +293,46 @@ func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveIPFromBlackList(ipAddr)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
//Load the target rule from access controller
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rule.RemoveIPFromBlackList(ipAddr)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) {
|
||||
enable, err := utils.PostPara(r, "enable")
|
||||
enable, _ := utils.PostPara(r, "enable")
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
//Return the current enabled state
|
||||
currentEnabled := geodbStore.BlacklistEnabled
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if enable == "" {
|
||||
//enable paramter not set
|
||||
currentEnabled := rule.BlacklistEnabled
|
||||
js, _ := json.Marshal(currentEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enable == "true" {
|
||||
geodbStore.ToggleBlacklist(true)
|
||||
rule.ToggleBlacklist(true)
|
||||
} else if enable == "false" {
|
||||
geodbStore.ToggleBlacklist(false)
|
||||
rule.ToggleBlacklist(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
||||
return
|
||||
@ -117,11 +352,22 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
|
||||
bltype = "country"
|
||||
}
|
||||
|
||||
resulst := []*geodb.WhitelistEntry{}
|
||||
ruleID, err := utils.GetPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resulst := []*access.WhitelistEntry{}
|
||||
if bltype == "country" {
|
||||
resulst = geodbStore.GetAllWhitelistedCountryCode()
|
||||
resulst = rule.GetAllWhitelistedCountryCode()
|
||||
} else if bltype == "ip" {
|
||||
resulst = geodbStore.GetAllWhitelistedIp()
|
||||
resulst = rule.GetAllWhitelistedIp()
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(resulst)
|
||||
@ -136,11 +382,22 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
comment, _ := utils.PostPara(r, "comment")
|
||||
p := bluemonday.StrictPolicy()
|
||||
comment = p.Sanitize(comment)
|
||||
|
||||
geodbStore.AddCountryCodeToWhitelist(countryCode, comment)
|
||||
rule.AddCountryCodeToWhitelist(countryCode, comment)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -152,7 +409,18 @@ func handleCountryWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveCountryCodeFromWhitelist(countryCode)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rule.RemoveCountryCodeFromWhitelist(countryCode)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -164,11 +432,23 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
comment, _ := utils.PostPara(r, "comment")
|
||||
p := bluemonday.StrictPolicy()
|
||||
comment = p.Sanitize(comment)
|
||||
|
||||
geodbStore.AddIPToWhiteList(ipAddr, comment)
|
||||
rule.AddIPToWhiteList(ipAddr, comment)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
@ -178,23 +458,45 @@ func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveIPFromWhiteList(ipAddr)
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rule.RemoveIPFromWhiteList(ipAddr)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleWhitelistEnable(w http.ResponseWriter, r *http.Request) {
|
||||
enable, err := utils.PostPara(r, "enable")
|
||||
enable, _ := utils.PostPara(r, "enable")
|
||||
ruleID, err := utils.PostPara(r, "id")
|
||||
if err != nil {
|
||||
ruleID = "default"
|
||||
}
|
||||
|
||||
rule, err := accessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if enable == "" {
|
||||
//Return the current enabled state
|
||||
currentEnabled := geodbStore.WhitelistEnabled
|
||||
currentEnabled := rule.WhitelistEnabled
|
||||
js, _ := json.Marshal(currentEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enable == "true" {
|
||||
geodbStore.ToggleWhitelist(true)
|
||||
rule.ToggleWhitelist(true)
|
||||
} else if enable == "false" {
|
||||
geodbStore.ToggleWhitelist(false)
|
||||
rule.ToggleWhitelist(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
||||
return
|
||||
|
12
src/acme.go
12
src/acme.go
@ -38,7 +38,7 @@ func initACME() *acme.ACMEHandler {
|
||||
port = getRandomPort(30000)
|
||||
}
|
||||
|
||||
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
|
||||
return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port), sysdb)
|
||||
}
|
||||
|
||||
// create the special routing rule for ACME
|
||||
@ -85,6 +85,8 @@ func acmeRegisterSpecialRoutingRule() {
|
||||
// This function check if the renew setup is satisfied. If not, toggle them automatically
|
||||
func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
isForceHttpsRedirectEnabledOriginally := false
|
||||
dnsPara, _ := utils.PostBool(r, "dns")
|
||||
if !dnsPara {
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
//Enable port 80 to 443 redirect
|
||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||
@ -101,6 +103,8 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
||||
} else {
|
||||
//This port do not support ACME
|
||||
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Add a 3 second delay to make sure everything is settle down
|
||||
@ -109,7 +113,11 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
||||
// Pass over to the acmeHandler to deal with the communication
|
||||
acmeHandler.HandleRenewCertificate(w, r)
|
||||
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
//Update the TLS cert store buffer
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
|
||||
//Restore original settings
|
||||
if dynamicProxyRouter.Option.Port == 443 && !dnsPara {
|
||||
if !isForceHttpsRedirectEnabledOriginally {
|
||||
//Default is off. Turn the redirection off
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
|
||||
|
36
src/api.go
36
src/api.go
@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
"imuslab.com/zoraxy/mod/acme/acmewizard"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
@ -49,7 +50,9 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
||||
authRouter.HandleFunc("/api/proxy/toggle", ReverseProxyToggleRuleSet)
|
||||
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
|
||||
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
|
||||
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
|
||||
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
||||
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
|
||||
@ -67,6 +70,8 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
|
||||
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
|
||||
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
|
||||
authRouter.HandleFunc("/api/proxy/header/handleHSTS", HandleHSTSState)
|
||||
authRouter.HandleFunc("/api/proxy/header/handlePermissionPolicy", HandlePermissionPolicy)
|
||||
//Reverse proxy auth related APIs
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
|
||||
@ -87,6 +92,12 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
|
||||
|
||||
//Access Rules API
|
||||
authRouter.HandleFunc("/api/access/list", handleListAccessRules)
|
||||
authRouter.HandleFunc("/api/access/attach", handleAttachRuleToHost)
|
||||
authRouter.HandleFunc("/api/access/create", handleCreateAccessRule)
|
||||
authRouter.HandleFunc("/api/access/remove", handleRemoveAccessRule)
|
||||
authRouter.HandleFunc("/api/access/update", handleUpadateAccessRule)
|
||||
//Blacklist APIs
|
||||
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
||||
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
|
||||
@ -94,7 +105,6 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
|
||||
|
||||
//Whitelist APIs
|
||||
authRouter.HandleFunc("/api/whitelist/list", handleListWhitelisted)
|
||||
authRouter.HandleFunc("/api/whitelist/country/add", handleCountryWhitelistAdd)
|
||||
@ -133,14 +143,13 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
|
||||
|
||||
//TCP Proxy
|
||||
authRouter.HandleFunc("/api/tcpprox/config/add", tcpProxyManager.HandleAddProxyConfig)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/edit", tcpProxyManager.HandleEditProxyConfigs)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/list", tcpProxyManager.HandleListConfigs)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/start", tcpProxyManager.HandleStartProxy)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/stop", tcpProxyManager.HandleStopProxy)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/delete", tcpProxyManager.HandleRemoveProxy)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/status", tcpProxyManager.HandleGetProxyStatus)
|
||||
authRouter.HandleFunc("/api/tcpprox/config/validate", tcpProxyManager.HandleConfigValidate)
|
||||
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
|
||||
authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
|
||||
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
|
||||
authRouter.HandleFunc("/api/streamprox/config/start", streamProxyManager.HandleStartProxy)
|
||||
authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy)
|
||||
authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy)
|
||||
authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
|
||||
|
||||
//mDNS APIs
|
||||
authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)
|
||||
@ -179,9 +188,12 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/ca", HandleACMEPreferredCA)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setEAB", acmeAutoRenewer.HanldeSetEAB)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/setDNS", acmeAutoRenewer.HanldeSetDNS)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
|
||||
authRouter.HandleFunc("/api/acme/dns/providers", acmedns.HandleServeProvidersJson)
|
||||
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
|
||||
|
||||
//Static Web Server
|
||||
@ -202,6 +214,10 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
|
||||
}
|
||||
|
||||
//Docker UX Optimizations
|
||||
authRouter.HandleFunc("/api/docker/available", DockerUXOptimizer.HandleDockerAvailable)
|
||||
authRouter.HandleFunc("/api/docker/containers", DockerUXOptimizer.HandleDockerContainersList)
|
||||
|
||||
//Others
|
||||
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
|
||||
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
|
||||
@ -212,6 +228,8 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
||||
|
||||
//If you got APIs to add, append them here
|
||||
// get available docker containers
|
||||
|
||||
}
|
||||
|
||||
// Function to renders Auth related APIs
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -46,6 +47,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
LastModifiedDate string
|
||||
ExpireDate string
|
||||
RemainingDays int
|
||||
UseDNS bool
|
||||
}
|
||||
|
||||
results := []*CertInfo{}
|
||||
@ -81,12 +83,19 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
certInfoFilename := filepath.Join(tlsCertManager.CertStore, filename+".json")
|
||||
useDNSValidation := false //Default to false for HTTP TLS certificates
|
||||
certInfo, err := acme.LoadCertInfoJSON(certInfoFilename) //Note: Not all certs have info json
|
||||
if err == nil {
|
||||
useDNSValidation = certInfo.UseDNS
|
||||
}
|
||||
|
||||
thisCertInfo := CertInfo{
|
||||
Domain: filename,
|
||||
LastModifiedDate: modifiedTime,
|
||||
ExpireDate: certExpireTime,
|
||||
RemainingDays: expiredIn,
|
||||
UseDNS: useDNSValidation,
|
||||
}
|
||||
|
||||
results = append(results, &thisCertInfo)
|
||||
|
@ -155,7 +155,7 @@ func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
|
||||
*/
|
||||
|
||||
func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
includeSysDBRaw, err := utils.GetPara(r, "includeDB")
|
||||
includeSysDBRaw, _ := utils.GetPara(r, "includeDB")
|
||||
includeSysDB := false
|
||||
if includeSysDBRaw == "true" {
|
||||
//Include the system database in backup snapshot
|
||||
@ -177,7 +177,7 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Walk through the folder and add files to the zip
|
||||
err = filepath.Walk(folderPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
err := filepath.Walk(folderPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -25,12 +25,6 @@ func HandleSMTPSet(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "domain cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
portString, err := utils.PostPara(r, "port")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "port must be a valid integer")
|
||||
@ -76,7 +70,6 @@ func HandleSMTPSet(w http.ResponseWriter, r *http.Request) {
|
||||
//Set the email sender properties
|
||||
thisEmailSender := email.Sender{
|
||||
Hostname: strings.TrimSpace(hostname),
|
||||
Domain: strings.TrimSpace(domain),
|
||||
Port: port,
|
||||
Username: strings.TrimSpace(username),
|
||||
Password: strings.TrimSpace(password),
|
||||
@ -206,7 +199,7 @@ var (
|
||||
)
|
||||
|
||||
func HandleAdminAccountResetEmail(w http.ResponseWriter, r *http.Request) {
|
||||
if EmailSender.Username == "" || EmailSender.Domain == "" {
|
||||
if EmailSender.Username == "" {
|
||||
//Reset account not setup
|
||||
utils.SendErrorResponse(w, "Reset account not setup.")
|
||||
return
|
||||
@ -279,17 +272,14 @@ func HandleNewPasswordSetup(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Delete the user account
|
||||
authAgent.UnregisterUser(username)
|
||||
|
||||
//Ok. Set the new password
|
||||
err = authAgent.CreateUserAccount(username, newPassword, "")
|
||||
if err != nil {
|
||||
// Un register the user account
|
||||
if err := authAgent.UnregisterUser(username); err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
//Ok. Set the new password
|
||||
if err := authAgent.CreateUserAccount(username, newPassword, ""); err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
165
src/go.mod
165
src/go.mod
@ -6,6 +6,7 @@ toolchain go1.22.2
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/docker/docker v27.0.0+incompatible
|
||||
github.com/go-acme/lego/v4 v4.16.1
|
||||
github.com/go-ping/ping v1.1.0
|
||||
github.com/google/uuid v1.6.0
|
||||
@ -14,21 +15,177 @@ require (
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/likexian/whois v1.15.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
golang.org/x/net v0.23.0
|
||||
golang.org/x/sys v0.18.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/text v0.15.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.25.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.1.0 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1755 // indirect
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/lightsail v1.34.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.37.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/civo/civogo v0.3.11 // indirect
|
||||
github.com/cloudflare/cloudflare-go v0.86.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.9.1 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dnsimple/dnsimple-go v1.2.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/exoscale/egoscale v0.102.3 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-resty/resty/v2 v2.11.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.2 // indirect
|
||||
github.com/gophercloud/gophercloud v1.0.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
|
||||
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
|
||||
github.com/labbsr0x/goh v1.0.1 // indirect
|
||||
github.com/linode/linodego v1.28.0 // indirect
|
||||
github.com/liquidweb/liquidweb-cli v0.6.9 // indirect
|
||||
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.58 // indirect
|
||||
golang.org/x/crypto v0.21.0 // indirect
|
||||
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
|
||||
github.com/nrdcg/auroradns v1.1.0 // indirect
|
||||
github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect
|
||||
github.com/nrdcg/desec v0.7.0 // indirect
|
||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||
github.com/nrdcg/freemyip v0.2.0 // indirect
|
||||
github.com/nrdcg/goinwx v0.10.0 // indirect
|
||||
github.com/nrdcg/mailinabox v0.2.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/nrdcg/nodion v0.1.0 // indirect
|
||||
github.com/nrdcg/porkbun v0.3.0 // indirect
|
||||
github.com/nzdjb/go-metaname v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/ovh/go-ovh v1.4.3 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pquerna/otp v1.4.0 // indirect
|
||||
github.com/sacloud/api-client-go v0.2.8 // indirect
|
||||
github.com/sacloud/go-http v0.1.6 // indirect
|
||||
github.com/sacloud/iaas-api-go v1.11.1 // indirect
|
||||
github.com/sacloud/packages-go v0.0.9 // indirect
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||
github.com/softlayer/softlayer-go v1.1.3 // indirect
|
||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.490 // indirect
|
||||
github.com/transip/gotransip/v6 v6.23.0 // indirect
|
||||
github.com/ultradns/ultradns-go-sdk v1.6.1-20231103022937-8589b6a // indirect
|
||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||
github.com/vultr/govultr/v2 v2.17.2 // indirect
|
||||
github.com/yandex-cloud/go-genproto v0.0.0-20220805142335-27b56ddae16f // indirect
|
||||
github.com/yandex-cloud/go-sdk v0.0.0-20220805164847-cf028e604997 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
|
||||
go.opentelemetry.io/otel v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.27.0 // indirect
|
||||
go.uber.org/ratelimit v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/oauth2 v0.18.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
google.golang.org/api v0.169.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect
|
||||
google.golang.org/grpc v1.64.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/ns1/ns1-go.v2 v2.7.13 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.5.1 // indirect
|
||||
)
|
||||
|
965
src/go.sum
965
src/go.sum
File diff suppressed because it is too large
Load Diff
16
src/main.go
16
src/main.go
@ -12,9 +12,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dockerux"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/email"
|
||||
"imuslab.com/zoraxy/mod/forwardproxy"
|
||||
@ -27,7 +30,7 @@ import (
|
||||
"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/streamproxy"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
@ -40,8 +43,10 @@ var noauth = flag.Bool("noauth", false, "Disable authentication for management i
|
||||
var showver = flag.Bool("version", false, "Show version of this server")
|
||||
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
||||
var allowMdnsScanning = flag.Bool("mdns", true, "Enable mDNS scanner and transponder")
|
||||
var mdnsName = flag.String("mdnsname", "", "mDNS name, leave empty to use default (zoraxy_{node-uuid}.local)")
|
||||
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
|
||||
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
|
||||
var runningInDocker = flag.Bool("docker", false, "Run Zoraxy in docker compatibility mode")
|
||||
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
|
||||
var 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")
|
||||
@ -50,7 +55,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
|
||||
|
||||
var (
|
||||
name = "Zoraxy"
|
||||
version = "3.0.1"
|
||||
version = "3.0.7"
|
||||
nodeUUID = "generic"
|
||||
development = false //Set this to false to use embedded web fs
|
||||
bootTime = time.Now().Unix()
|
||||
@ -68,15 +73,17 @@ var (
|
||||
authAgent *auth.AuthAgent //Authentication agent
|
||||
tlsCertManager *tlscert.Manager //TLS / SSL management
|
||||
redirectTable *redirection.RuleTable //Handle special redirection rule sets
|
||||
loadbalancer *loadbalance.RouteManager //Load balancer manager to get routing targets from proxy rules
|
||||
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
|
||||
geodbStore *geodb.Store //GeoIP database, also handle black list and whitelist features
|
||||
geodbStore *geodb.Store //GeoIP database, for resolving IP into country code
|
||||
accessController *access.Controller //Access controller, handle black list and white list
|
||||
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
|
||||
statisticCollector *statistic.Collector //Collecting statistic from visitors
|
||||
uptimeMonitor *uptime.Monitor //Uptime monitor service worker
|
||||
mdnsScanner *mdns.MDNSHost //mDNS discovery services
|
||||
ganManager *ganserv.NetworkManager //Global Area Network Manager
|
||||
webSshManager *sshprox.Manager //Web SSH connection service
|
||||
tcpProxyManager *tcpprox.Manager //TCP Proxy Manager
|
||||
streamProxyManager *streamproxy.Manager //Stream Proxy Manager for TCP / UDP forwarding
|
||||
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
|
||||
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
|
||||
staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
|
||||
@ -85,6 +92,7 @@ var (
|
||||
//Helper modules
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
|
||||
DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only
|
||||
SystemWideLogger *logger.Logger //Logger for Zoraxy
|
||||
)
|
||||
|
||||
|
221
src/mod/access/access.go
Normal file
221
src/mod/access/access.go
Normal file
@ -0,0 +1,221 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
Access.go
|
||||
|
||||
This module is the new version of access control system
|
||||
where now the blacklist / whitelist are seperated from
|
||||
geodb module
|
||||
*/
|
||||
|
||||
// Create a new access controller to handle blacklist / whitelist
|
||||
func NewAccessController(options *Options) (*Controller, error) {
|
||||
sysdb := options.Database
|
||||
if sysdb == nil {
|
||||
return nil, errors.New("missing database access")
|
||||
}
|
||||
|
||||
//Create the config folder if not exists
|
||||
confFolder := options.ConfigFolder
|
||||
if !utils.FileExists(confFolder) {
|
||||
err := os.MkdirAll(confFolder, 0775)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Create the global access rule if not exists
|
||||
var defaultAccessRule = AccessRule{
|
||||
ID: "default",
|
||||
Name: "Default",
|
||||
Desc: "Default access rule for all HTTP proxy hosts",
|
||||
BlacklistEnabled: false,
|
||||
WhitelistEnabled: false,
|
||||
WhiteListCountryCode: &map[string]string{},
|
||||
WhiteListIP: &map[string]string{},
|
||||
BlackListContryCode: &map[string]string{},
|
||||
BlackListIP: &map[string]string{},
|
||||
}
|
||||
defaultRuleSettingFile := filepath.Join(confFolder, "default.json")
|
||||
if utils.FileExists(defaultRuleSettingFile) {
|
||||
//Load from file
|
||||
defaultRuleBytes, err := os.ReadFile(defaultRuleSettingFile)
|
||||
if err == nil {
|
||||
err = json.Unmarshal(defaultRuleBytes, &defaultAccessRule)
|
||||
if err != nil {
|
||||
options.Logger.PrintAndLog("Access", "Unable to parse default routing rule config file. Using default", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//Create one
|
||||
js, _ := json.MarshalIndent(defaultAccessRule, "", " ")
|
||||
os.WriteFile(defaultRuleSettingFile, js, 0775)
|
||||
}
|
||||
|
||||
//Generate a controller object
|
||||
thisController := Controller{
|
||||
DefaultAccessRule: &defaultAccessRule,
|
||||
ProxyAccessRule: &sync.Map{},
|
||||
Options: options,
|
||||
}
|
||||
|
||||
//Assign default access rule parent
|
||||
thisController.DefaultAccessRule.parent = &thisController
|
||||
|
||||
//Load all acccess rules from file
|
||||
configFiles, err := filepath.Glob(options.ConfigFolder + "/*.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ProxyAccessRules := sync.Map{}
|
||||
for _, configFile := range configFiles {
|
||||
if filepath.Base(configFile) == "default.json" {
|
||||
//Skip this, as this was already loaded as default
|
||||
continue
|
||||
}
|
||||
|
||||
configContent, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
options.Logger.PrintAndLog("Access", "Unable to load config "+filepath.Base(configFile), err)
|
||||
continue
|
||||
}
|
||||
|
||||
//Parse the config file into AccessRule
|
||||
thisAccessRule := AccessRule{}
|
||||
err = json.Unmarshal(configContent, &thisAccessRule)
|
||||
if err != nil {
|
||||
options.Logger.PrintAndLog("Access", "Unable to parse config "+filepath.Base(configFile), err)
|
||||
continue
|
||||
}
|
||||
thisAccessRule.parent = &thisController
|
||||
ProxyAccessRules.Store(thisAccessRule.ID, &thisAccessRule)
|
||||
}
|
||||
thisController.ProxyAccessRule = &ProxyAccessRules
|
||||
|
||||
return &thisController, nil
|
||||
}
|
||||
|
||||
// Get the global access rule
|
||||
func (c *Controller) GetGlobalAccessRule() (*AccessRule, error) {
|
||||
if c.DefaultAccessRule == nil {
|
||||
return nil, errors.New("global access rule is not set")
|
||||
}
|
||||
return c.DefaultAccessRule, nil
|
||||
}
|
||||
|
||||
// Load access rules to runtime, require rule ID
|
||||
func (c *Controller) GetAccessRuleByID(accessRuleID string) (*AccessRule, error) {
|
||||
if accessRuleID == "default" || accessRuleID == "" {
|
||||
|
||||
return c.DefaultAccessRule, nil
|
||||
}
|
||||
//Load from sync.Map, should be O(1)
|
||||
targetRule, ok := c.ProxyAccessRule.Load(accessRuleID)
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("target access rule not exists")
|
||||
}
|
||||
|
||||
ar, ok := targetRule.(*AccessRule)
|
||||
if !ok {
|
||||
return nil, errors.New("assertion of access rule failed, version too old?")
|
||||
}
|
||||
return ar, nil
|
||||
}
|
||||
|
||||
// Return all the access rules currently in runtime, including default
|
||||
func (c *Controller) ListAllAccessRules() []*AccessRule {
|
||||
results := []*AccessRule{c.DefaultAccessRule}
|
||||
c.ProxyAccessRule.Range(func(key, value interface{}) bool {
|
||||
results = append(results, value.(*AccessRule))
|
||||
return true
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Check if an access rule exists given the rule id
|
||||
func (c *Controller) AccessRuleExists(ruleID string) bool {
|
||||
r, _ := c.GetAccessRuleByID(ruleID)
|
||||
if r != nil {
|
||||
//An access rule with identical ID exists
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Add a new access rule to runtime and save it to file
|
||||
func (c *Controller) AddNewAccessRule(newRule *AccessRule) error {
|
||||
r, _ := c.GetAccessRuleByID(newRule.ID)
|
||||
if r != nil {
|
||||
//An access rule with identical ID exists
|
||||
return errors.New("access rule already exists")
|
||||
}
|
||||
|
||||
//Check if the blacklist and whitelist are populated with empty map
|
||||
if newRule.BlackListContryCode == nil {
|
||||
newRule.BlackListContryCode = &map[string]string{}
|
||||
}
|
||||
if newRule.BlackListIP == nil {
|
||||
newRule.BlackListIP = &map[string]string{}
|
||||
}
|
||||
if newRule.WhiteListCountryCode == nil {
|
||||
newRule.WhiteListCountryCode = &map[string]string{}
|
||||
}
|
||||
if newRule.WhiteListIP == nil {
|
||||
newRule.WhiteListIP = &map[string]string{}
|
||||
}
|
||||
|
||||
//Add access rule to runtime
|
||||
newRule.parent = c
|
||||
c.ProxyAccessRule.Store(newRule.ID, newRule)
|
||||
|
||||
//Save rule to file
|
||||
newRule.SaveChanges()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update the access rule meta info.
|
||||
func (c *Controller) UpdateAccessRule(ruleID string, name string, desc string) error {
|
||||
targetAccessRule, err := c.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
///Update the name and desc
|
||||
targetAccessRule.Name = name
|
||||
targetAccessRule.Desc = desc
|
||||
|
||||
//Overwrite the rule currently in sync map
|
||||
if ruleID == "default" {
|
||||
c.DefaultAccessRule = targetAccessRule
|
||||
} else {
|
||||
c.ProxyAccessRule.Store(ruleID, targetAccessRule)
|
||||
}
|
||||
return targetAccessRule.SaveChanges()
|
||||
}
|
||||
|
||||
// Remove the access rule by its id
|
||||
func (c *Controller) RemoveAccessRuleByID(ruleID string) error {
|
||||
if !c.AccessRuleExists(ruleID) {
|
||||
return errors.New("access rule not exists")
|
||||
}
|
||||
|
||||
//Default cannot be removed
|
||||
if ruleID == "default" {
|
||||
return errors.New("default access rule cannot be removed")
|
||||
}
|
||||
|
||||
//Remove it
|
||||
return c.DeleteAccessRuleByID(ruleID)
|
||||
}
|
153
src/mod/access/accessRule.go
Normal file
153
src/mod/access/accessRule.go
Normal file
@ -0,0 +1,153 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Check both blacklist and whitelist for access for both geoIP and ip / CIDR ranges
|
||||
func (s *AccessRule) AllowIpAccess(ipaddr string) bool {
|
||||
if s.IsBlacklisted(ipaddr) {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.IsWhitelisted(ipaddr)
|
||||
}
|
||||
|
||||
// Check both blacklist and whitelist for access using net.Conn
|
||||
func (s *AccessRule) AllowConnectionAccess(conn net.Conn) bool {
|
||||
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
|
||||
return s.AllowIpAccess(addr.IP.String())
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Toggle black list
|
||||
func (s *AccessRule) ToggleBlacklist(enabled bool) {
|
||||
s.BlacklistEnabled = enabled
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
// Toggel white list
|
||||
func (s *AccessRule) ToggleWhitelist(enabled bool) {
|
||||
s.WhitelistEnabled = enabled
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
/*
|
||||
Check if a IP address is blacklisted, in either country or IP blacklist
|
||||
IsBlacklisted default return is false (allow access)
|
||||
*/
|
||||
func (s *AccessRule) IsBlacklisted(ipAddr string) bool {
|
||||
if !s.BlacklistEnabled {
|
||||
//Blacklist not enabled. Always return false
|
||||
return false
|
||||
}
|
||||
|
||||
if ipAddr == "" {
|
||||
//Unable to get the target IP address
|
||||
return false
|
||||
}
|
||||
|
||||
countryCode, err := s.parent.Options.GeoDB.ResolveCountryCodeFromIP(ipAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.IsIPBlacklisted(ipAddr) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
IsWhitelisted check if a given IP address is in the current
|
||||
server's white list.
|
||||
|
||||
Note that the Whitelist default result is true even
|
||||
when encountered error
|
||||
*/
|
||||
func (s *AccessRule) IsWhitelisted(ipAddr string) bool {
|
||||
if !s.WhitelistEnabled {
|
||||
//Whitelist not enabled. Always return true (allow access)
|
||||
return true
|
||||
}
|
||||
|
||||
if ipAddr == "" {
|
||||
//Unable to get the target IP address, assume ok
|
||||
return true
|
||||
}
|
||||
|
||||
countryCode, err := s.parent.Options.GeoDB.ResolveCountryCodeFromIP(ipAddr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.IsCountryCodeWhitelisted(countryCode.CountryIsoCode) {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.IsIPWhitelisted(ipAddr) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/* Utilities function */
|
||||
|
||||
// Update the current access rule to json file
|
||||
func (s *AccessRule) SaveChanges() error {
|
||||
if s.parent == nil {
|
||||
return errors.New("save failed: access rule detached from controller")
|
||||
}
|
||||
saveTarget := filepath.Join(s.parent.Options.ConfigFolder, s.ID+".json")
|
||||
js, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(saveTarget, js, 0775)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete this access rule, this will only delete the config file.
|
||||
// for runtime delete, use DeleteAccessRuleByID from parent Controller
|
||||
func (s *AccessRule) DeleteConfigFile() error {
|
||||
saveTarget := filepath.Join(s.parent.Options.ConfigFolder, s.ID+".json")
|
||||
return os.Remove(saveTarget)
|
||||
}
|
||||
|
||||
// Delete the access rule by given ID
|
||||
func (c *Controller) DeleteAccessRuleByID(accessRuleID string) error {
|
||||
targetAccessRule, err := c.GetAccessRuleByID(accessRuleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Delete config file associated with this access rule
|
||||
err = targetAccessRule.DeleteConfigFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Delete the access rule in runtime
|
||||
c.ProxyAccessRule.Delete(accessRuleID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a deep copy object of the access rule list
|
||||
func deepCopy(valueList map[string]string) map[string]string {
|
||||
result := map[string]string{}
|
||||
js, _ := json.Marshal(valueList)
|
||||
json.Unmarshal(js, &result)
|
||||
return result
|
||||
}
|
94
src/mod/access/blacklist.go
Normal file
94
src/mod/access/blacklist.go
Normal file
@ -0,0 +1,94 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
)
|
||||
|
||||
/*
|
||||
Blacklist.go
|
||||
|
||||
This script store the blacklist related functions
|
||||
*/
|
||||
|
||||
// Geo Blacklist
|
||||
func (s *AccessRule) AddCountryCodeToBlackList(countryCode string, comment string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
newBlacklistCountryCode := deepCopy(*s.BlackListContryCode)
|
||||
newBlacklistCountryCode[countryCode] = comment
|
||||
s.BlackListContryCode = &newBlacklistCountryCode
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
func (s *AccessRule) RemoveCountryCodeFromBlackList(countryCode string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
newBlacklistCountryCode := deepCopy(*s.BlackListContryCode)
|
||||
delete(newBlacklistCountryCode, countryCode)
|
||||
s.BlackListContryCode = &newBlacklistCountryCode
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
func (s *AccessRule) IsCountryCodeBlacklisted(countryCode string) bool {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
blacklistMap := *s.BlackListContryCode
|
||||
_, ok := blacklistMap[countryCode]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *AccessRule) GetAllBlacklistedCountryCode() []string {
|
||||
bannedCountryCodes := []string{}
|
||||
blacklistMap := *s.BlackListContryCode
|
||||
for cc, _ := range blacklistMap {
|
||||
bannedCountryCodes = append(bannedCountryCodes, cc)
|
||||
}
|
||||
return bannedCountryCodes
|
||||
}
|
||||
|
||||
// IP Blacklsits
|
||||
func (s *AccessRule) AddIPToBlackList(ipAddr string, comment string) {
|
||||
newBlackListIP := deepCopy(*s.BlackListIP)
|
||||
newBlackListIP[ipAddr] = comment
|
||||
s.BlackListIP = &newBlackListIP
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
func (s *AccessRule) RemoveIPFromBlackList(ipAddr string) {
|
||||
newBlackListIP := deepCopy(*s.BlackListIP)
|
||||
delete(newBlackListIP, ipAddr)
|
||||
s.BlackListIP = &newBlackListIP
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
func (s *AccessRule) GetAllBlacklistedIp() []string {
|
||||
bannedIps := []string{}
|
||||
blacklistMap := *s.BlackListIP
|
||||
for ip, _ := range blacklistMap {
|
||||
bannedIps = append(bannedIps, ip)
|
||||
}
|
||||
|
||||
return bannedIps
|
||||
}
|
||||
|
||||
func (s *AccessRule) IsIPBlacklisted(ipAddr string) bool {
|
||||
IPBlacklist := *s.BlackListIP
|
||||
_, ok := IPBlacklist[ipAddr]
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
|
||||
//Check for CIDR
|
||||
for ipOrCIDR, _ := range IPBlacklist {
|
||||
wildcardMatch := netutils.MatchIpWildcard(ipAddr, ipOrCIDR)
|
||||
if wildcardMatch {
|
||||
return true
|
||||
}
|
||||
|
||||
cidrMatch := netutils.MatchIpCIDR(ipAddr, ipOrCIDR)
|
||||
if cidrMatch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
38
src/mod/access/typedef.go
Normal file
38
src/mod/access/typedef.go
Normal file
@ -0,0 +1,38 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Logger logger.Logger
|
||||
ConfigFolder string //Path for storing config files
|
||||
GeoDB *geodb.Store //For resolving country code
|
||||
Database *database.Database //System key-value database
|
||||
}
|
||||
|
||||
type AccessRule struct {
|
||||
ID string
|
||||
Name string
|
||||
Desc string
|
||||
BlacklistEnabled bool
|
||||
WhitelistEnabled bool
|
||||
|
||||
/* Whitelist Blacklist Table, value is comment if supported */
|
||||
WhiteListCountryCode *map[string]string
|
||||
WhiteListIP *map[string]string
|
||||
BlackListContryCode *map[string]string
|
||||
BlackListIP *map[string]string
|
||||
|
||||
parent *Controller
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
DefaultAccessRule *AccessRule
|
||||
ProxyAccessRule *sync.Map
|
||||
Options *Options
|
||||
}
|
112
src/mod/access/whitelist.go
Normal file
112
src/mod/access/whitelist.go
Normal file
@ -0,0 +1,112 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
)
|
||||
|
||||
/*
|
||||
Whitelist.go
|
||||
|
||||
This script handles whitelist related functions
|
||||
*/
|
||||
|
||||
const (
|
||||
EntryType_CountryCode int = 0
|
||||
EntryType_IP int = 1
|
||||
)
|
||||
|
||||
type WhitelistEntry struct {
|
||||
EntryType int //Entry type of whitelist, Country Code or IP
|
||||
CC string //ISO Country Code
|
||||
IP string //IP address or range
|
||||
Comment string //Comment for this entry
|
||||
}
|
||||
|
||||
//Geo Whitelist
|
||||
|
||||
func (s *AccessRule) AddCountryCodeToWhitelist(countryCode string, comment string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
newWhitelistCC := deepCopy(*s.WhiteListCountryCode)
|
||||
newWhitelistCC[countryCode] = comment
|
||||
s.WhiteListCountryCode = &newWhitelistCC
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
func (s *AccessRule) RemoveCountryCodeFromWhitelist(countryCode string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
newWhitelistCC := deepCopy(*s.WhiteListCountryCode)
|
||||
delete(newWhitelistCC, countryCode)
|
||||
s.WhiteListCountryCode = &newWhitelistCC
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
func (s *AccessRule) IsCountryCodeWhitelisted(countryCode string) bool {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
whitelistCC := *s.WhiteListCountryCode
|
||||
_, ok := whitelistCC[countryCode]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *AccessRule) GetAllWhitelistedCountryCode() []*WhitelistEntry {
|
||||
whitelistedCountryCode := []*WhitelistEntry{}
|
||||
whitelistCC := *s.WhiteListCountryCode
|
||||
for cc, comment := range whitelistCC {
|
||||
whitelistedCountryCode = append(whitelistedCountryCode, &WhitelistEntry{
|
||||
EntryType: EntryType_CountryCode,
|
||||
CC: cc,
|
||||
Comment: comment,
|
||||
})
|
||||
}
|
||||
return whitelistedCountryCode
|
||||
}
|
||||
|
||||
//IP Whitelist
|
||||
|
||||
func (s *AccessRule) AddIPToWhiteList(ipAddr string, comment string) {
|
||||
newWhitelistIP := deepCopy(*s.WhiteListIP)
|
||||
newWhitelistIP[ipAddr] = comment
|
||||
s.WhiteListIP = &newWhitelistIP
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
func (s *AccessRule) RemoveIPFromWhiteList(ipAddr string) {
|
||||
newWhitelistIP := deepCopy(*s.WhiteListIP)
|
||||
delete(newWhitelistIP, ipAddr)
|
||||
s.WhiteListIP = &newWhitelistIP
|
||||
s.SaveChanges()
|
||||
}
|
||||
|
||||
func (s *AccessRule) IsIPWhitelisted(ipAddr string) bool {
|
||||
//Check for IP wildcard and CIRD rules
|
||||
WhitelistedIP := *s.WhiteListIP
|
||||
for ipOrCIDR, _ := range WhitelistedIP {
|
||||
wildcardMatch := netutils.MatchIpWildcard(ipAddr, ipOrCIDR)
|
||||
if wildcardMatch {
|
||||
return true
|
||||
}
|
||||
|
||||
cidrMatch := netutils.MatchIpCIDR(ipAddr, ipOrCIDR)
|
||||
if cidrMatch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *AccessRule) GetAllWhitelistedIp() []*WhitelistEntry {
|
||||
whitelistedIp := []*WhitelistEntry{}
|
||||
currentWhitelistedIP := *s.WhiteListIP
|
||||
for ipOrCIDR, comment := range currentWhitelistedIP {
|
||||
thisEntry := WhitelistEntry{
|
||||
EntryType: EntryType_IP,
|
||||
IP: ipOrCIDR,
|
||||
Comment: comment,
|
||||
}
|
||||
whitelistedIp = append(whitelistedIp, &thisEntry)
|
||||
}
|
||||
|
||||
return whitelistedIp
|
||||
}
|
@ -9,6 +9,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@ -24,6 +25,7 @@ import (
|
||||
"github.com/go-acme/lego/v4/challenge/http01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -31,6 +33,7 @@ type CertificateInfoJSON struct {
|
||||
AcmeName string `json:"acme_name"`
|
||||
AcmeUrl string `json:"acme_url"`
|
||||
SkipTLS bool `json:"skip_tls"`
|
||||
UseDNS bool `json:"dns"`
|
||||
}
|
||||
|
||||
// ACMEUser represents a user in the ACME system.
|
||||
@ -40,6 +43,11 @@ type ACMEUser struct {
|
||||
key crypto.PrivateKey
|
||||
}
|
||||
|
||||
type EABConfig struct {
|
||||
Kid string `json:"kid"`
|
||||
HmacKey string `json:"HmacKey"`
|
||||
}
|
||||
|
||||
// GetEmail returns the email of the ACMEUser.
|
||||
func (u *ACMEUser) GetEmail() string {
|
||||
return u.Email
|
||||
@ -59,18 +67,20 @@ func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
|
||||
type ACMEHandler struct {
|
||||
DefaultAcmeServer string
|
||||
Port string
|
||||
Database *database.Database
|
||||
}
|
||||
|
||||
// NewACME creates a new ACMEHandler instance.
|
||||
func NewACME(acmeServer string, port string) *ACMEHandler {
|
||||
func NewACME(acmeServer string, port string, database *database.Database) *ACMEHandler {
|
||||
return &ACMEHandler{
|
||||
DefaultAcmeServer: acmeServer,
|
||||
Port: port,
|
||||
Database: database,
|
||||
}
|
||||
}
|
||||
|
||||
// ObtainCert obtains a certificate for the specified domains.
|
||||
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool) (bool, error) {
|
||||
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, useDNS bool) (bool, error) {
|
||||
log.Println("[ACME] Obtaining certificate...")
|
||||
|
||||
// generate private key
|
||||
@ -107,6 +117,11 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
}
|
||||
}
|
||||
|
||||
//Fallback to Let's Encrypt if it is not set
|
||||
if caName == "" {
|
||||
caName = "Let's Encrypt"
|
||||
}
|
||||
|
||||
// setup the custom ACME url endpoint.
|
||||
if caUrl != "" {
|
||||
config.CADirURL = caUrl
|
||||
@ -136,18 +151,108 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
}
|
||||
|
||||
// setup how to receive challenge
|
||||
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
|
||||
if useDNS {
|
||||
if !a.Database.TableExists("acme") {
|
||||
a.Database.NewTable("acme")
|
||||
return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -1)")
|
||||
}
|
||||
|
||||
if !a.Database.KeyExists("acme", certificateName+"_dns_provider") || !a.Database.KeyExists("acme", certificateName+"_dns_credentials") {
|
||||
return false, errors.New("DNS Provider and DNS Credenital configuration required for ACME Provider (Error -2)")
|
||||
}
|
||||
|
||||
var dnsCredentials string
|
||||
err := a.Database.Read("acme", certificateName+"_dns_credentials", &dnsCredentials)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
var dnsProvider string
|
||||
err = a.Database.Read("acme", certificateName+"_dns_provider", &dnsProvider)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
provider, err := GetDnsChallengeProviderByName(dnsProvider, dnsCredentials)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = client.Challenge.SetDNS01Provider(provider)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// New users will need to register
|
||||
/*
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
*/
|
||||
var reg *registration.Resource
|
||||
// New users will need to register
|
||||
if client.GetExternalAccountRequired() {
|
||||
log.Println("External Account Required for this ACME Provider.")
|
||||
// IF KID and HmacEncoded is overidden
|
||||
|
||||
if !a.Database.TableExists("acme") {
|
||||
a.Database.NewTable("acme")
|
||||
return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -1)")
|
||||
}
|
||||
|
||||
if !a.Database.KeyExists("acme", config.CADirURL+"_kid") || !a.Database.KeyExists("acme", config.CADirURL+"_hmacEncoded") {
|
||||
return false, errors.New("kid and HmacEncoded configuration required for ACME Provider (Error -2)")
|
||||
}
|
||||
|
||||
var kid string
|
||||
var hmacEncoded string
|
||||
err := a.Database.Read("acme", config.CADirURL+"_kid", &kid)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = a.Database.Read("acme", config.CADirURL+"_hmacEncoded", &hmacEncoded)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
log.Println("EAB Credential retrieved.", kid, hmacEncoded)
|
||||
if kid != "" && hmacEncoded != "" {
|
||||
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
|
||||
TermsOfServiceAgreed: true,
|
||||
Kid: kid,
|
||||
HmacEncoded: hmacEncoded,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
//return false, errors.New("External Account Required for this ACME Provider.")
|
||||
} else {
|
||||
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
adminUser.Registration = reg
|
||||
|
||||
// obtain the certificate
|
||||
@ -179,6 +284,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
AcmeName: caName,
|
||||
AcmeUrl: caUrl,
|
||||
SkipTLS: skipTLS,
|
||||
UseDNS: useDNS,
|
||||
}
|
||||
|
||||
certInfoBytes, err := json.Marshal(certInfo)
|
||||
@ -291,6 +397,8 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
}
|
||||
//Make sure the wildcard * do not goes into the filename
|
||||
filename = strings.ReplaceAll(filename, "*", "_")
|
||||
|
||||
email, err := utils.PostPara(r, "email")
|
||||
if err != nil {
|
||||
@ -329,8 +437,18 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
|
||||
skipTLS = true
|
||||
}
|
||||
|
||||
var dns bool
|
||||
|
||||
if dnsString, err := utils.PostPara(r, "dns"); err != nil {
|
||||
dns = false
|
||||
} else if dnsString != "true" {
|
||||
dns = false
|
||||
} else {
|
||||
dns = true
|
||||
}
|
||||
|
||||
domains := strings.Split(domainPara, ",")
|
||||
result, err := a.ObtainCert(domains, filename, email, ca, caUrl, skipTLS)
|
||||
result, err := a.ObtainCert(domains, filename, email, ca, caUrl, skipTLS, dns)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, jsonEscape(err.Error()))
|
||||
return
|
||||
@ -362,7 +480,7 @@ func IsPortInUse(port int) bool {
|
||||
}
|
||||
|
||||
// Load cert information from json file
|
||||
func loadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
|
||||
func LoadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
|
||||
certInfoBytes, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
72
src/mod/acme/acme_dns.go
Normal file
72
src/mod/acme/acme_dns.go
Normal file
@ -0,0 +1,72 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
)
|
||||
|
||||
func GetDnsChallengeProviderByName(dnsProvider string, dnsCredentials string) (challenge.Provider, error) {
|
||||
|
||||
//Original Implementation
|
||||
/*credentials, err := extractDnsCredentials(dnsCredentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setCredentialsIntoEnvironmentVariables(credentials)
|
||||
|
||||
provider, err := dns.NewDNSChallengeProviderByName(dnsProvider)
|
||||
*/
|
||||
|
||||
//New implementation using acmedns CICD pipeline generated datatype
|
||||
return acmedns.GetDNSProviderByJsonConfig(dnsProvider, dnsCredentials)
|
||||
}
|
||||
|
||||
/*
|
||||
Original implementation of DNS ACME using OS.Env as payload
|
||||
*/
|
||||
|
||||
func setCredentialsIntoEnvironmentVariables(credentials map[string]string) {
|
||||
for key, value := range credentials {
|
||||
err := os.Setenv(key, value)
|
||||
if err != nil {
|
||||
log.Println("[ERR] Failed to set environment variable %s: %v", key, err)
|
||||
} else {
|
||||
log.Println("[INFO] Environment variable %s set successfully", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractDnsCredentials(input string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
// Split the input string by newline character
|
||||
lines := strings.Split(input, "\n")
|
||||
|
||||
// Iterate over each line
|
||||
for _, line := range lines {
|
||||
// Split the line by "=" character
|
||||
//use SpliyN to make sure not to split the value if the value is base64
|
||||
parts := strings.SplitN(line, "=", 1)
|
||||
|
||||
// Check if the line is in the correct format
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
// Add the key-value pair to the map
|
||||
result[key] = value
|
||||
|
||||
if value == "" || key == "" {
|
||||
//invalid config
|
||||
return result, errors.New("DNS credential extract failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
982
src/mod/acme/acmedns/acmedns.go
Normal file
982
src/mod/acme/acmedns/acmedns.go
Normal file
@ -0,0 +1,982 @@
|
||||
package acmedns
|
||||
/*
|
||||
THIS MODULE IS GENERATED AUTOMATICALLY
|
||||
DO NOT EDIT THIS FILE
|
||||
*/
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/providers/dns/alidns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/allinkl"
|
||||
"github.com/go-acme/lego/v4/providers/dns/arvancloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/auroradns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/autodns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/azure"
|
||||
"github.com/go-acme/lego/v4/providers/dns/azuredns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/bindman"
|
||||
"github.com/go-acme/lego/v4/providers/dns/bluecat"
|
||||
"github.com/go-acme/lego/v4/providers/dns/brandit"
|
||||
"github.com/go-acme/lego/v4/providers/dns/bunny"
|
||||
"github.com/go-acme/lego/v4/providers/dns/checkdomain"
|
||||
"github.com/go-acme/lego/v4/providers/dns/civo"
|
||||
"github.com/go-acme/lego/v4/providers/dns/clouddns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudru"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cloudxns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/conoha"
|
||||
"github.com/go-acme/lego/v4/providers/dns/constellix"
|
||||
"github.com/go-acme/lego/v4/providers/dns/cpanel"
|
||||
"github.com/go-acme/lego/v4/providers/dns/derak"
|
||||
"github.com/go-acme/lego/v4/providers/dns/desec"
|
||||
"github.com/go-acme/lego/v4/providers/dns/digitalocean"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnshomede"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnsimple"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnsmadeeasy"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dnspod"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dode"
|
||||
"github.com/go-acme/lego/v4/providers/dns/domeneshop"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dreamhost"
|
||||
"github.com/go-acme/lego/v4/providers/dns/duckdns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dyn"
|
||||
"github.com/go-acme/lego/v4/providers/dns/dynu"
|
||||
"github.com/go-acme/lego/v4/providers/dns/easydns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/efficientip"
|
||||
"github.com/go-acme/lego/v4/providers/dns/epik"
|
||||
"github.com/go-acme/lego/v4/providers/dns/exoscale"
|
||||
"github.com/go-acme/lego/v4/providers/dns/freemyip"
|
||||
"github.com/go-acme/lego/v4/providers/dns/gandi"
|
||||
"github.com/go-acme/lego/v4/providers/dns/gandiv5"
|
||||
"github.com/go-acme/lego/v4/providers/dns/gcore"
|
||||
"github.com/go-acme/lego/v4/providers/dns/glesys"
|
||||
"github.com/go-acme/lego/v4/providers/dns/godaddy"
|
||||
"github.com/go-acme/lego/v4/providers/dns/googledomains"
|
||||
"github.com/go-acme/lego/v4/providers/dns/hetzner"
|
||||
"github.com/go-acme/lego/v4/providers/dns/hostingde"
|
||||
"github.com/go-acme/lego/v4/providers/dns/hosttech"
|
||||
"github.com/go-acme/lego/v4/providers/dns/httpnet"
|
||||
"github.com/go-acme/lego/v4/providers/dns/hyperone"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ibmcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/iij"
|
||||
"github.com/go-acme/lego/v4/providers/dns/iijdpf"
|
||||
"github.com/go-acme/lego/v4/providers/dns/infoblox"
|
||||
"github.com/go-acme/lego/v4/providers/dns/infomaniak"
|
||||
"github.com/go-acme/lego/v4/providers/dns/internetbs"
|
||||
"github.com/go-acme/lego/v4/providers/dns/inwx"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ionos"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ipv64"
|
||||
"github.com/go-acme/lego/v4/providers/dns/iwantmyname"
|
||||
"github.com/go-acme/lego/v4/providers/dns/joker"
|
||||
"github.com/go-acme/lego/v4/providers/dns/liara"
|
||||
"github.com/go-acme/lego/v4/providers/dns/lightsail"
|
||||
"github.com/go-acme/lego/v4/providers/dns/linode"
|
||||
"github.com/go-acme/lego/v4/providers/dns/liquidweb"
|
||||
"github.com/go-acme/lego/v4/providers/dns/loopia"
|
||||
"github.com/go-acme/lego/v4/providers/dns/luadns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/mailinabox"
|
||||
"github.com/go-acme/lego/v4/providers/dns/metaname"
|
||||
"github.com/go-acme/lego/v4/providers/dns/mydnsjp"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namecheap"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namedotcom"
|
||||
"github.com/go-acme/lego/v4/providers/dns/namesilo"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nearlyfreespeech"
|
||||
"github.com/go-acme/lego/v4/providers/dns/netcup"
|
||||
"github.com/go-acme/lego/v4/providers/dns/netlify"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nicmanager"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nifcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/njalla"
|
||||
"github.com/go-acme/lego/v4/providers/dns/nodion"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ns1"
|
||||
"github.com/go-acme/lego/v4/providers/dns/otc"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ovh"
|
||||
"github.com/go-acme/lego/v4/providers/dns/pdns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/plesk"
|
||||
"github.com/go-acme/lego/v4/providers/dns/porkbun"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rackspace"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rcodezero"
|
||||
"github.com/go-acme/lego/v4/providers/dns/regru"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rfc2136"
|
||||
"github.com/go-acme/lego/v4/providers/dns/rimuhosting"
|
||||
"github.com/go-acme/lego/v4/providers/dns/route53"
|
||||
"github.com/go-acme/lego/v4/providers/dns/safedns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/sakuracloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/scaleway"
|
||||
"github.com/go-acme/lego/v4/providers/dns/selectel"
|
||||
"github.com/go-acme/lego/v4/providers/dns/servercow"
|
||||
"github.com/go-acme/lego/v4/providers/dns/shellrent"
|
||||
"github.com/go-acme/lego/v4/providers/dns/simply"
|
||||
"github.com/go-acme/lego/v4/providers/dns/sonic"
|
||||
"github.com/go-acme/lego/v4/providers/dns/stackpath"
|
||||
"github.com/go-acme/lego/v4/providers/dns/tencentcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/transip"
|
||||
"github.com/go-acme/lego/v4/providers/dns/ultradns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/variomedia"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vegadns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vercel"
|
||||
"github.com/go-acme/lego/v4/providers/dns/versio"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vinyldns"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vkcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vscale"
|
||||
"github.com/go-acme/lego/v4/providers/dns/vultr"
|
||||
"github.com/go-acme/lego/v4/providers/dns/webnames"
|
||||
"github.com/go-acme/lego/v4/providers/dns/websupport"
|
||||
"github.com/go-acme/lego/v4/providers/dns/wedos"
|
||||
"github.com/go-acme/lego/v4/providers/dns/yandex"
|
||||
"github.com/go-acme/lego/v4/providers/dns/yandex360"
|
||||
"github.com/go-acme/lego/v4/providers/dns/yandexcloud"
|
||||
"github.com/go-acme/lego/v4/providers/dns/zoneee"
|
||||
"github.com/go-acme/lego/v4/providers/dns/zonomi"
|
||||
|
||||
)
|
||||
|
||||
//name is the DNS provider name, e.g. cloudflare or gandi
|
||||
//JSON (js) must be in key-value string that match ConfigableFields Title in providers.json, e.g. {"Username":"far","Password":"boo"}
|
||||
func GetDNSProviderByJsonConfig(name string, js string)(challenge.Provider, error){
|
||||
switch name {
|
||||
|
||||
case "alidns":
|
||||
cfg := alidns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return alidns.NewDNSProviderConfig(cfg)
|
||||
case "allinkl":
|
||||
cfg := allinkl.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return allinkl.NewDNSProviderConfig(cfg)
|
||||
case "arvancloud":
|
||||
cfg := arvancloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return arvancloud.NewDNSProviderConfig(cfg)
|
||||
case "auroradns":
|
||||
cfg := auroradns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return auroradns.NewDNSProviderConfig(cfg)
|
||||
case "autodns":
|
||||
cfg := autodns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return autodns.NewDNSProviderConfig(cfg)
|
||||
case "azure":
|
||||
cfg := azure.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return azure.NewDNSProviderConfig(cfg)
|
||||
case "azuredns":
|
||||
cfg := azuredns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return azuredns.NewDNSProviderConfig(cfg)
|
||||
case "bindman":
|
||||
cfg := bindman.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bindman.NewDNSProviderConfig(cfg)
|
||||
case "bluecat":
|
||||
cfg := bluecat.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bluecat.NewDNSProviderConfig(cfg)
|
||||
case "brandit":
|
||||
cfg := brandit.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return brandit.NewDNSProviderConfig(cfg)
|
||||
case "bunny":
|
||||
cfg := bunny.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bunny.NewDNSProviderConfig(cfg)
|
||||
case "checkdomain":
|
||||
cfg := checkdomain.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return checkdomain.NewDNSProviderConfig(cfg)
|
||||
case "civo":
|
||||
cfg := civo.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return civo.NewDNSProviderConfig(cfg)
|
||||
case "clouddns":
|
||||
cfg := clouddns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clouddns.NewDNSProviderConfig(cfg)
|
||||
case "cloudflare":
|
||||
cfg := cloudflare.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloudflare.NewDNSProviderConfig(cfg)
|
||||
case "cloudns":
|
||||
cfg := cloudns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloudns.NewDNSProviderConfig(cfg)
|
||||
case "cloudru":
|
||||
cfg := cloudru.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloudru.NewDNSProviderConfig(cfg)
|
||||
case "cloudxns":
|
||||
cfg := cloudxns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloudxns.NewDNSProviderConfig(cfg)
|
||||
case "conoha":
|
||||
cfg := conoha.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conoha.NewDNSProviderConfig(cfg)
|
||||
case "constellix":
|
||||
cfg := constellix.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return constellix.NewDNSProviderConfig(cfg)
|
||||
case "cpanel":
|
||||
cfg := cpanel.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cpanel.NewDNSProviderConfig(cfg)
|
||||
case "derak":
|
||||
cfg := derak.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return derak.NewDNSProviderConfig(cfg)
|
||||
case "desec":
|
||||
cfg := desec.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desec.NewDNSProviderConfig(cfg)
|
||||
case "digitalocean":
|
||||
cfg := digitalocean.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return digitalocean.NewDNSProviderConfig(cfg)
|
||||
case "dnshomede":
|
||||
cfg := dnshomede.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dnshomede.NewDNSProviderConfig(cfg)
|
||||
case "dnsimple":
|
||||
cfg := dnsimple.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dnsimple.NewDNSProviderConfig(cfg)
|
||||
case "dnsmadeeasy":
|
||||
cfg := dnsmadeeasy.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dnsmadeeasy.NewDNSProviderConfig(cfg)
|
||||
case "dnspod":
|
||||
cfg := dnspod.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dnspod.NewDNSProviderConfig(cfg)
|
||||
case "dode":
|
||||
cfg := dode.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dode.NewDNSProviderConfig(cfg)
|
||||
case "domeneshop":
|
||||
cfg := domeneshop.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return domeneshop.NewDNSProviderConfig(cfg)
|
||||
case "dreamhost":
|
||||
cfg := dreamhost.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dreamhost.NewDNSProviderConfig(cfg)
|
||||
case "duckdns":
|
||||
cfg := duckdns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return duckdns.NewDNSProviderConfig(cfg)
|
||||
case "dyn":
|
||||
cfg := dyn.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dyn.NewDNSProviderConfig(cfg)
|
||||
case "dynu":
|
||||
cfg := dynu.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dynu.NewDNSProviderConfig(cfg)
|
||||
case "easydns":
|
||||
cfg := easydns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return easydns.NewDNSProviderConfig(cfg)
|
||||
case "efficientip":
|
||||
cfg := efficientip.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return efficientip.NewDNSProviderConfig(cfg)
|
||||
case "epik":
|
||||
cfg := epik.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return epik.NewDNSProviderConfig(cfg)
|
||||
case "exoscale":
|
||||
cfg := exoscale.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return exoscale.NewDNSProviderConfig(cfg)
|
||||
case "freemyip":
|
||||
cfg := freemyip.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return freemyip.NewDNSProviderConfig(cfg)
|
||||
case "gandi":
|
||||
cfg := gandi.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gandi.NewDNSProviderConfig(cfg)
|
||||
case "gandiv5":
|
||||
cfg := gandiv5.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gandiv5.NewDNSProviderConfig(cfg)
|
||||
case "gcore":
|
||||
cfg := gcore.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gcore.NewDNSProviderConfig(cfg)
|
||||
case "glesys":
|
||||
cfg := glesys.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return glesys.NewDNSProviderConfig(cfg)
|
||||
case "godaddy":
|
||||
cfg := godaddy.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return godaddy.NewDNSProviderConfig(cfg)
|
||||
case "googledomains":
|
||||
cfg := googledomains.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return googledomains.NewDNSProviderConfig(cfg)
|
||||
case "hetzner":
|
||||
cfg := hetzner.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hetzner.NewDNSProviderConfig(cfg)
|
||||
case "hostingde":
|
||||
cfg := hostingde.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hostingde.NewDNSProviderConfig(cfg)
|
||||
case "hosttech":
|
||||
cfg := hosttech.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hosttech.NewDNSProviderConfig(cfg)
|
||||
case "httpnet":
|
||||
cfg := httpnet.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return httpnet.NewDNSProviderConfig(cfg)
|
||||
case "hyperone":
|
||||
cfg := hyperone.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hyperone.NewDNSProviderConfig(cfg)
|
||||
case "ibmcloud":
|
||||
cfg := ibmcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ibmcloud.NewDNSProviderConfig(cfg)
|
||||
case "iij":
|
||||
cfg := iij.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iij.NewDNSProviderConfig(cfg)
|
||||
case "iijdpf":
|
||||
cfg := iijdpf.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iijdpf.NewDNSProviderConfig(cfg)
|
||||
case "infoblox":
|
||||
cfg := infoblox.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infoblox.NewDNSProviderConfig(cfg)
|
||||
case "infomaniak":
|
||||
cfg := infomaniak.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infomaniak.NewDNSProviderConfig(cfg)
|
||||
case "internetbs":
|
||||
cfg := internetbs.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return internetbs.NewDNSProviderConfig(cfg)
|
||||
case "inwx":
|
||||
cfg := inwx.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inwx.NewDNSProviderConfig(cfg)
|
||||
case "ionos":
|
||||
cfg := ionos.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ionos.NewDNSProviderConfig(cfg)
|
||||
case "ipv64":
|
||||
cfg := ipv64.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ipv64.NewDNSProviderConfig(cfg)
|
||||
case "iwantmyname":
|
||||
cfg := iwantmyname.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iwantmyname.NewDNSProviderConfig(cfg)
|
||||
case "joker":
|
||||
cfg := joker.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return joker.NewDNSProviderConfig(cfg)
|
||||
case "liara":
|
||||
cfg := liara.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return liara.NewDNSProviderConfig(cfg)
|
||||
case "lightsail":
|
||||
cfg := lightsail.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lightsail.NewDNSProviderConfig(cfg)
|
||||
case "linode":
|
||||
cfg := linode.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return linode.NewDNSProviderConfig(cfg)
|
||||
case "liquidweb":
|
||||
cfg := liquidweb.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return liquidweb.NewDNSProviderConfig(cfg)
|
||||
case "loopia":
|
||||
cfg := loopia.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loopia.NewDNSProviderConfig(cfg)
|
||||
case "luadns":
|
||||
cfg := luadns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return luadns.NewDNSProviderConfig(cfg)
|
||||
case "mailinabox":
|
||||
cfg := mailinabox.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mailinabox.NewDNSProviderConfig(cfg)
|
||||
case "metaname":
|
||||
cfg := metaname.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return metaname.NewDNSProviderConfig(cfg)
|
||||
case "mydnsjp":
|
||||
cfg := mydnsjp.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mydnsjp.NewDNSProviderConfig(cfg)
|
||||
case "namecheap":
|
||||
cfg := namecheap.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return namecheap.NewDNSProviderConfig(cfg)
|
||||
case "namedotcom":
|
||||
cfg := namedotcom.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return namedotcom.NewDNSProviderConfig(cfg)
|
||||
case "namesilo":
|
||||
cfg := namesilo.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return namesilo.NewDNSProviderConfig(cfg)
|
||||
case "nearlyfreespeech":
|
||||
cfg := nearlyfreespeech.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nearlyfreespeech.NewDNSProviderConfig(cfg)
|
||||
case "netcup":
|
||||
cfg := netcup.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return netcup.NewDNSProviderConfig(cfg)
|
||||
case "netlify":
|
||||
cfg := netlify.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return netlify.NewDNSProviderConfig(cfg)
|
||||
case "nicmanager":
|
||||
cfg := nicmanager.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nicmanager.NewDNSProviderConfig(cfg)
|
||||
case "nifcloud":
|
||||
cfg := nifcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nifcloud.NewDNSProviderConfig(cfg)
|
||||
case "njalla":
|
||||
cfg := njalla.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return njalla.NewDNSProviderConfig(cfg)
|
||||
case "nodion":
|
||||
cfg := nodion.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nodion.NewDNSProviderConfig(cfg)
|
||||
case "ns1":
|
||||
cfg := ns1.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ns1.NewDNSProviderConfig(cfg)
|
||||
case "otc":
|
||||
cfg := otc.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return otc.NewDNSProviderConfig(cfg)
|
||||
case "ovh":
|
||||
cfg := ovh.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ovh.NewDNSProviderConfig(cfg)
|
||||
case "pdns":
|
||||
cfg := pdns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pdns.NewDNSProviderConfig(cfg)
|
||||
case "plesk":
|
||||
cfg := plesk.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plesk.NewDNSProviderConfig(cfg)
|
||||
case "porkbun":
|
||||
cfg := porkbun.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return porkbun.NewDNSProviderConfig(cfg)
|
||||
case "rackspace":
|
||||
cfg := rackspace.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rackspace.NewDNSProviderConfig(cfg)
|
||||
case "rcodezero":
|
||||
cfg := rcodezero.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rcodezero.NewDNSProviderConfig(cfg)
|
||||
case "regru":
|
||||
cfg := regru.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return regru.NewDNSProviderConfig(cfg)
|
||||
case "rfc2136":
|
||||
cfg := rfc2136.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rfc2136.NewDNSProviderConfig(cfg)
|
||||
case "rimuhosting":
|
||||
cfg := rimuhosting.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rimuhosting.NewDNSProviderConfig(cfg)
|
||||
case "route53":
|
||||
cfg := route53.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return route53.NewDNSProviderConfig(cfg)
|
||||
case "safedns":
|
||||
cfg := safedns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return safedns.NewDNSProviderConfig(cfg)
|
||||
case "sakuracloud":
|
||||
cfg := sakuracloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sakuracloud.NewDNSProviderConfig(cfg)
|
||||
case "scaleway":
|
||||
cfg := scaleway.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return scaleway.NewDNSProviderConfig(cfg)
|
||||
case "selectel":
|
||||
cfg := selectel.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return selectel.NewDNSProviderConfig(cfg)
|
||||
case "servercow":
|
||||
cfg := servercow.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return servercow.NewDNSProviderConfig(cfg)
|
||||
case "shellrent":
|
||||
cfg := shellrent.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return shellrent.NewDNSProviderConfig(cfg)
|
||||
case "simply":
|
||||
cfg := simply.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return simply.NewDNSProviderConfig(cfg)
|
||||
case "sonic":
|
||||
cfg := sonic.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sonic.NewDNSProviderConfig(cfg)
|
||||
case "stackpath":
|
||||
cfg := stackpath.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stackpath.NewDNSProviderConfig(cfg)
|
||||
case "tencentcloud":
|
||||
cfg := tencentcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tencentcloud.NewDNSProviderConfig(cfg)
|
||||
case "transip":
|
||||
cfg := transip.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transip.NewDNSProviderConfig(cfg)
|
||||
case "ultradns":
|
||||
cfg := ultradns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ultradns.NewDNSProviderConfig(cfg)
|
||||
case "variomedia":
|
||||
cfg := variomedia.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return variomedia.NewDNSProviderConfig(cfg)
|
||||
case "vegadns":
|
||||
cfg := vegadns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vegadns.NewDNSProviderConfig(cfg)
|
||||
case "vercel":
|
||||
cfg := vercel.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vercel.NewDNSProviderConfig(cfg)
|
||||
case "versio":
|
||||
cfg := versio.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return versio.NewDNSProviderConfig(cfg)
|
||||
case "vinyldns":
|
||||
cfg := vinyldns.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vinyldns.NewDNSProviderConfig(cfg)
|
||||
case "vkcloud":
|
||||
cfg := vkcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vkcloud.NewDNSProviderConfig(cfg)
|
||||
case "vscale":
|
||||
cfg := vscale.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vscale.NewDNSProviderConfig(cfg)
|
||||
case "vultr":
|
||||
cfg := vultr.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vultr.NewDNSProviderConfig(cfg)
|
||||
case "webnames":
|
||||
cfg := webnames.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return webnames.NewDNSProviderConfig(cfg)
|
||||
case "websupport":
|
||||
cfg := websupport.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return websupport.NewDNSProviderConfig(cfg)
|
||||
case "wedos":
|
||||
cfg := wedos.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wedos.NewDNSProviderConfig(cfg)
|
||||
case "yandex":
|
||||
cfg := yandex.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return yandex.NewDNSProviderConfig(cfg)
|
||||
case "yandex360":
|
||||
cfg := yandex360.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return yandex360.NewDNSProviderConfig(cfg)
|
||||
case "yandexcloud":
|
||||
cfg := yandexcloud.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return yandexcloud.NewDNSProviderConfig(cfg)
|
||||
case "zoneee":
|
||||
cfg := zoneee.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zoneee.NewDNSProviderConfig(cfg)
|
||||
case "zonomi":
|
||||
cfg := zonomi.NewDefaultConfig()
|
||||
err := json.Unmarshal([]byte(js), &cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zonomi.NewDNSProviderConfig(cfg)
|
||||
default:
|
||||
return nil, fmt.Errorf("unrecognized DNS provider: %s", name)
|
||||
}
|
||||
}
|
27
src/mod/acme/acmedns/acmedns_test.go
Normal file
27
src/mod/acme/acmedns/acmedns_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
package acmedns_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/mod/acme/acmedns"
|
||||
)
|
||||
|
||||
// Test if the structure of ACME DNS config can be reflected from lego source code definations
|
||||
func TestACMEDNSConfigStructureReflector(t *testing.T) {
|
||||
providers := []string{
|
||||
"gandi",
|
||||
"cloudflare",
|
||||
"azure",
|
||||
}
|
||||
|
||||
for _, provider := range providers {
|
||||
strcture, err := acmedns.GetProviderConfigStructure(provider)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(strcture)
|
||||
}
|
||||
|
||||
}
|
3553
src/mod/acme/acmedns/providers.json
Normal file
3553
src/mod/acme/acmedns/providers.json
Normal file
File diff suppressed because it is too large
Load Diff
80
src/mod/acme/acmedns/providerutils.go
Normal file
80
src/mod/acme/acmedns/providerutils.go
Normal file
@ -0,0 +1,80 @@
|
||||
package acmedns
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
//go:embed providers.json
|
||||
var providers []byte //A list of providers generated by acmedns code-generator
|
||||
|
||||
type ConfigTemplate struct {
|
||||
Name string `json:"Name"`
|
||||
ConfigableFields []struct {
|
||||
Title string `json:"Title"`
|
||||
Datatype string `json:"Datatype"`
|
||||
} `json:"ConfigableFields"`
|
||||
HiddenFields []struct {
|
||||
Title string `json:"Title"`
|
||||
Datatype string `json:"Datatype"`
|
||||
} `json:"HiddenFields"`
|
||||
}
|
||||
|
||||
// Return a map of string => datatype
|
||||
func GetProviderConfigStructure(providerName string) (map[string]string, error) {
|
||||
//Load the target config template from embedded providers.json
|
||||
configTemplateMap := map[string]ConfigTemplate{}
|
||||
err := json.Unmarshal(providers, &configTemplateMap)
|
||||
if err != nil {
|
||||
return map[string]string{}, err
|
||||
}
|
||||
|
||||
targetConfigTemplate, ok := configTemplateMap[providerName]
|
||||
if !ok {
|
||||
return map[string]string{}, errors.New("provider not supported")
|
||||
}
|
||||
|
||||
results := map[string]string{}
|
||||
for _, field := range targetConfigTemplate.ConfigableFields {
|
||||
results[field.Title] = field.Datatype
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// HandleServeProvidersJson return the list of supported providers as json
|
||||
func HandleServeProvidersJson(w http.ResponseWriter, r *http.Request) {
|
||||
providerName, _ := utils.GetPara(r, "name")
|
||||
if providerName == "" {
|
||||
//Send the current list of providers
|
||||
configTemplateMap := map[string]ConfigTemplate{}
|
||||
err := json.Unmarshal(providers, &configTemplateMap)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "failed to load DNS provider")
|
||||
return
|
||||
}
|
||||
|
||||
//Parse the provider names into an array
|
||||
providers := []string{}
|
||||
for providerName, _ := range configTemplateMap {
|
||||
providers = append(providers, providerName)
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(providers)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
}
|
||||
//Get the config for that provider
|
||||
confTemplate, err := GetProviderConfigStructure(providerName)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(confTemplate)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
@ -344,7 +344,7 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
|
||||
|
||||
// Load certificate info for ACME detail
|
||||
certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
|
||||
certInfo, err := loadCertInfoJSON(certInfoFilename)
|
||||
certInfo, err := LoadCertInfoJSON(certInfoFilename)
|
||||
if err != nil {
|
||||
log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, trying org section as ca", certName, err)
|
||||
|
||||
@ -356,7 +356,7 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
|
||||
}
|
||||
}
|
||||
|
||||
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS)
|
||||
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS, certInfo.UseDNS)
|
||||
if err != nil {
|
||||
log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
|
||||
} else {
|
||||
@ -373,3 +373,65 @@ func (a *AutoRenewer) saveRenewConfigToFile() error {
|
||||
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
|
||||
return os.WriteFile(a.ConfigFilePath, js, 0775)
|
||||
}
|
||||
|
||||
// Handle update auto renew EAD configuration
|
||||
func (a *AutoRenewer) HanldeSetEAB(w http.ResponseWriter, r *http.Request) {
|
||||
kid, err := utils.GetPara(r, "kid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "kid not set")
|
||||
return
|
||||
}
|
||||
|
||||
hmacEncoded, err := utils.GetPara(r, "hmacEncoded")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "hmacEncoded not set")
|
||||
return
|
||||
}
|
||||
|
||||
acmeDirectoryURL, err := utils.GetPara(r, "acmeDirectoryURL")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "acmeDirectoryURL not set")
|
||||
return
|
||||
}
|
||||
|
||||
if !a.AcmeHandler.Database.TableExists("acme") {
|
||||
a.AcmeHandler.Database.NewTable("acme")
|
||||
}
|
||||
|
||||
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_kid", kid)
|
||||
a.AcmeHandler.Database.Write("acme", acmeDirectoryURL+"_hmacEncoded", hmacEncoded)
|
||||
|
||||
utils.SendOK(w)
|
||||
|
||||
}
|
||||
|
||||
// Handle update auto renew DNS configuration
|
||||
func (a *AutoRenewer) HanldeSetDNS(w http.ResponseWriter, r *http.Request) {
|
||||
dnsProvider, err := utils.PostPara(r, "dnsProvider")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "dnsProvider not set")
|
||||
return
|
||||
}
|
||||
|
||||
dnsCredentials, err := utils.PostPara(r, "dnsCredentials")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "dnsCredentials not set")
|
||||
return
|
||||
}
|
||||
|
||||
filename, err := utils.PostPara(r, "filename")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "filename not set")
|
||||
return
|
||||
}
|
||||
|
||||
if !a.AcmeHandler.Database.TableExists("acme") {
|
||||
a.AcmeHandler.Database.NewTable("acme")
|
||||
}
|
||||
|
||||
a.AcmeHandler.Database.Write("acme", filename+"_dns_provider", dnsProvider)
|
||||
a.AcmeHandler.Database.Write("acme", filename+"_dns_credentials", dnsCredentials)
|
||||
|
||||
utils.SendOK(w)
|
||||
|
||||
}
|
||||
|
@ -1,76 +0,0 @@
|
||||
package aroz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
//To be used with arozos system
|
||||
type ArozHandler struct {
|
||||
Port string
|
||||
restfulEndpoint string
|
||||
}
|
||||
|
||||
//Information required for registering this subservice to arozos
|
||||
type ServiceInfo struct {
|
||||
Name string //Name of this module. e.g. "Audio"
|
||||
Desc string //Description for this module
|
||||
Group string //Group of the module, e.g. "system" / "media" etc
|
||||
IconPath string //Module icon image path e.g. "Audio/img/function_icon.png"
|
||||
Version string //Version of the module. Format: [0-9]*.[0-9][0-9].[0-9]
|
||||
StartDir string //Default starting dir, e.g. "Audio/index.html"
|
||||
SupportFW bool //Support floatWindow. If yes, floatWindow dir will be loaded
|
||||
LaunchFWDir string //This link will be launched instead of 'StartDir' if fw mode
|
||||
SupportEmb bool //Support embedded mode
|
||||
LaunchEmb string //This link will be launched instead of StartDir / Fw if a file is opened with this module
|
||||
InitFWSize []int //Floatwindow init size. [0] => Width, [1] => Height
|
||||
InitEmbSize []int //Embedded mode init size. [0] => Width, [1] => Height
|
||||
SupportedExt []string //Supported File Extensions. e.g. ".mp3", ".flac", ".wav"
|
||||
}
|
||||
|
||||
//This function will request the required flag from the startup paramters and parse it to the need of the arozos.
|
||||
func HandleFlagParse(info ServiceInfo) *ArozHandler {
|
||||
var infoRequestMode = flag.Bool("info", false, "Show information about this program in JSON")
|
||||
var port = flag.String("port", ":8000", "Management web interface listening port")
|
||||
var restful = flag.String("rpt", "", "Reserved")
|
||||
//Parse the flags
|
||||
flag.Parse()
|
||||
if *infoRequestMode {
|
||||
//Information request mode
|
||||
jsonString, _ := json.MarshalIndent(info, "", " ")
|
||||
fmt.Println(string(jsonString))
|
||||
os.Exit(0)
|
||||
}
|
||||
return &ArozHandler{
|
||||
Port: *port,
|
||||
restfulEndpoint: *restful,
|
||||
}
|
||||
}
|
||||
|
||||
//Get the username and resources access token from the request, return username, token
|
||||
func (a *ArozHandler) GetUserInfoFromRequest(w http.ResponseWriter, r *http.Request) (string, string) {
|
||||
username := r.Header.Get("aouser")
|
||||
token := r.Header.Get("aotoken")
|
||||
|
||||
return username, token
|
||||
}
|
||||
|
||||
func (a *ArozHandler) IsUsingExternalPermissionManager() bool {
|
||||
return !(a.restfulEndpoint == "")
|
||||
}
|
||||
|
||||
//Request gateway interface for advance permission sandbox control
|
||||
func (a *ArozHandler) RequestGatewayInterface(token string, script string) (*http.Response, error) {
|
||||
resp, err := http.PostForm(a.restfulEndpoint,
|
||||
url.Values{"token": {token}, "script": {script}})
|
||||
if err != nil {
|
||||
// handle error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
Binary file not shown.
60
src/mod/dockerux/docker.go
Normal file
60
src/mod/dockerux/docker.go
Normal file
@ -0,0 +1,60 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package dockerux
|
||||
|
||||
/* Windows docker optimizer*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// Windows build not support docker
|
||||
func (d *UXOptimizer) HandleDockerAvailable(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(d.RunninInDocker)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (d *UXOptimizer) HandleDockerContainersList(w http.ResponseWriter, r *http.Request) {
|
||||
apiClient, err := client.NewClientWithOpts(client.WithVersion("1.43"))
|
||||
if err != nil {
|
||||
d.SystemWideLogger.PrintAndLog("Docker", "Unable to create new docker client", err)
|
||||
utils.SendErrorResponse(w, "Docker client initiation failed")
|
||||
return
|
||||
}
|
||||
defer apiClient.Close()
|
||||
|
||||
containers, err := apiClient.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
d.SystemWideLogger.PrintAndLog("Docker", "List docker container failed", err)
|
||||
utils.SendErrorResponse(w, "List docker container failed")
|
||||
return
|
||||
}
|
||||
|
||||
networks, err := apiClient.NetworkList(context.Background(), types.NetworkListOptions{})
|
||||
if err != nil {
|
||||
d.SystemWideLogger.PrintAndLog("Docker", "List docker network failed", err)
|
||||
utils.SendErrorResponse(w, "List docker network failed")
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"network": networks,
|
||||
"containers": containers,
|
||||
}
|
||||
|
||||
js, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
32
src/mod/dockerux/docker_windows.go
Normal file
32
src/mod/dockerux/docker_windows.go
Normal file
@ -0,0 +1,32 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package dockerux
|
||||
|
||||
/*
|
||||
|
||||
Windows docker UX optimizer dummy
|
||||
|
||||
This is a dummy module for Windows as docker features
|
||||
is useless on Windows and create a larger binary size
|
||||
|
||||
docker on Windows build are trimmed to reduce binary size
|
||||
and make it compatibile with Windows 7
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// Windows build not support docker
|
||||
func (d *UXOptimizer) HandleDockerAvailable(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(d.RunninInDocker)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (d *UXOptimizer) HandleDockerContainersList(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendErrorResponse(w, "Platform not supported")
|
||||
}
|
24
src/mod/dockerux/dockerux.go
Normal file
24
src/mod/dockerux/dockerux.go
Normal file
@ -0,0 +1,24 @@
|
||||
package dockerux
|
||||
|
||||
import "imuslab.com/zoraxy/mod/info/logger"
|
||||
|
||||
/*
|
||||
Docker Optimizer
|
||||
|
||||
This script add support for optimizing docker user experience
|
||||
Note that this module are community contribute only. For bug
|
||||
report, please directly tag the Pull Request author.
|
||||
*/
|
||||
|
||||
type UXOptimizer struct {
|
||||
RunninInDocker bool
|
||||
SystemWideLogger *logger.Logger
|
||||
}
|
||||
|
||||
//Create a new docker optimizer
|
||||
func NewDockerOptimizer(IsRunningInDocker bool, logger *logger.Logger) *UXOptimizer {
|
||||
return &UXOptimizer{
|
||||
RunninInDocker: IsRunningInDocker,
|
||||
SystemWideLogger: logger,
|
||||
}
|
||||
}
|
@ -6,8 +6,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -16,11 +14,16 @@ import (
|
||||
Main server for dynamic proxy core
|
||||
|
||||
Routing Handler Priority (High to Low)
|
||||
- Blacklist
|
||||
- Whitelist
|
||||
- Special Routing Rule (e.g. acme)
|
||||
- Redirectable
|
||||
- Subdomain Routing
|
||||
- Vitrual Directory Routing
|
||||
- Access Router
|
||||
- Blacklist
|
||||
- Whitelist
|
||||
- Basic Auth
|
||||
- Vitrual Directory Proxy
|
||||
- Subdomain Proxy
|
||||
- Root router (default site router)
|
||||
*/
|
||||
|
||||
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@ -32,29 +35,10 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
|
||||
//Inject headers
|
||||
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
|
||||
|
||||
/*
|
||||
General Access Check
|
||||
*/
|
||||
respWritten := h.handleAccessRouting(w, r)
|
||||
if respWritten {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Redirection Routing
|
||||
*/
|
||||
@ -65,19 +49,38 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Extract request host to see if it is virtual directory or subdomain
|
||||
/*
|
||||
Host Routing
|
||||
*/
|
||||
//Extract request host to see if any proxy rule is matched
|
||||
domainOnly := r.Host
|
||||
if strings.Contains(r.Host, ":") {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
|
||||
/*
|
||||
Host Routing
|
||||
*/
|
||||
|
||||
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && !sep.Disabled {
|
||||
//Matching proxy rule found
|
||||
//Access Check (blacklist / whitelist)
|
||||
ruleID := sep.AccessFilterUUID
|
||||
if sep.AccessFilterUUID == "" {
|
||||
//Use default rule
|
||||
ruleID = "default"
|
||||
}
|
||||
if h.handleAccessRouting(ruleID, w, r) {
|
||||
//Request handled by subroute
|
||||
return
|
||||
}
|
||||
|
||||
// Rate Limit
|
||||
if sep.RequireRateLimit {
|
||||
err := h.handleRateLimitRouting(w, r, sep)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Validate basic auth
|
||||
if sep.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, sep)
|
||||
if err != nil {
|
||||
@ -94,7 +97,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root {
|
||||
potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
|
||||
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
@ -109,6 +112,13 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
/*
|
||||
Root Router Handling
|
||||
*/
|
||||
|
||||
//Root access control based on default rule
|
||||
blocked := h.handleAccessRouting("default", w, r)
|
||||
if blocked {
|
||||
return
|
||||
}
|
||||
|
||||
//Clean up the request URI
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
if !strings.HasSuffix(proxyingPath, "/") {
|
||||
@ -136,7 +146,6 @@ 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, ":")
|
||||
@ -203,38 +212,3 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
65
src/mod/dynamicproxy/access.go
Normal file
65
src/mod/dynamicproxy/access.go
Normal file
@ -0,0 +1,65 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
)
|
||||
|
||||
// Handle access check (blacklist / whitelist), return true if request is handled (aka blocked)
|
||||
// if the return value is false, you can continue process the response writer
|
||||
func (h *ProxyHandler) handleAccessRouting(ruleID string, w http.ResponseWriter, r *http.Request) bool {
|
||||
accessRule, err := h.Parent.Option.AccessController.GetAccessRuleByID(ruleID)
|
||||
if err != nil {
|
||||
//Unable to load access rule. Target rule not found?
|
||||
log.Println("[Proxy] Unable to load access rule: " + ruleID)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("500 - Internal Server Error"))
|
||||
return true
|
||||
}
|
||||
|
||||
isBlocked, blockedReason := accessRequestBlocked(accessRule, h.Parent.Option.WebDirectory, w, r)
|
||||
if isBlocked {
|
||||
h.logRequest(r, false, 403, blockedReason, "")
|
||||
}
|
||||
return isBlocked
|
||||
}
|
||||
|
||||
// Return boolean, return true if access is blocked
|
||||
// For string, it will return the blocked reason (if any)
|
||||
func accessRequestBlocked(accessRule *access.AccessRule, templateDirectory string, w http.ResponseWriter, r *http.Request) (bool, string) {
|
||||
//Check if this ip is in blacklist
|
||||
clientIpAddr := netutils.GetRequesterIP(r)
|
||||
if accessRule.IsBlacklisted(clientIpAddr) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/blacklist.html"))
|
||||
if err != nil {
|
||||
w.Write(page_forbidden)
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
|
||||
return true, "blacklist"
|
||||
}
|
||||
|
||||
//Check if this ip is in whitelist
|
||||
if !accessRule.IsWhitelisted(clientIpAddr) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
template, err := os.ReadFile(filepath.Join(templateDirectory, "templates/whitelist.html"))
|
||||
if err != nil {
|
||||
w.Write(page_forbidden)
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
return true, "whitelist"
|
||||
}
|
||||
|
||||
//Not blocked.
|
||||
return false, ""
|
||||
}
|
@ -16,6 +16,16 @@ import (
|
||||
*/
|
||||
|
||||
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
|
||||
err := handleBasicAuth(w, r, pe)
|
||||
if err != nil {
|
||||
h.logRequest(r, false, 401, "host", pe.Domain)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle basic auth logic
|
||||
// do not write to http.ResponseWriter if err return is not nil (already handled by this function)
|
||||
func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
|
||||
if len(pe.BasicAuthExceptionRules) > 0 {
|
||||
//Check if the current path matches the exception rules
|
||||
for _, exceptionRule := range pe.BasicAuthExceptionRules {
|
||||
@ -44,7 +54,6 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if !matchingFound {
|
||||
h.logRequest(r, false, 401, "host", pe.Domain)
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
|
74
src/mod/dynamicproxy/customHeader.go
Normal file
74
src/mod/dynamicproxy/customHeader.go
Normal file
@ -0,0 +1,74 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
)
|
||||
|
||||
/*
|
||||
CustomHeader.go
|
||||
|
||||
This script handle parsing and injecting custom headers
|
||||
into the dpcore routing logic
|
||||
*/
|
||||
|
||||
// SplitInboundOutboundHeaders split user defined headers into upstream and downstream headers
|
||||
// return upstream header and downstream header key-value pairs
|
||||
// if the header is expected to be deleted, the value will be set to empty string
|
||||
func (ept *ProxyEndpoint) SplitInboundOutboundHeaders() ([][]string, [][]string) {
|
||||
if len(ept.UserDefinedHeaders) == 0 && ept.HSTSMaxAge == 0 && !ept.EnablePermissionPolicyHeader {
|
||||
//Early return if there are no defined headers
|
||||
return [][]string{}, [][]string{}
|
||||
}
|
||||
|
||||
//Use pre-allocation for faster performance
|
||||
//Downstream +2 for Permission Policy and HSTS
|
||||
upstreamHeaders := make([][]string, len(ept.UserDefinedHeaders))
|
||||
downstreamHeaders := make([][]string, len(ept.UserDefinedHeaders)+2)
|
||||
upstreamHeaderCounter := 0
|
||||
downstreamHeaderCounter := 0
|
||||
|
||||
//Sort the headers into upstream or downstream
|
||||
for _, customHeader := range ept.UserDefinedHeaders {
|
||||
thisHeaderSet := make([]string, 2)
|
||||
thisHeaderSet[0] = customHeader.Key
|
||||
thisHeaderSet[1] = customHeader.Value
|
||||
if customHeader.IsRemove {
|
||||
//Prevent invalid config
|
||||
thisHeaderSet[1] = ""
|
||||
}
|
||||
|
||||
//Assign to slice
|
||||
if customHeader.Direction == HeaderDirection_ZoraxyToUpstream {
|
||||
upstreamHeaders[upstreamHeaderCounter] = thisHeaderSet
|
||||
upstreamHeaderCounter++
|
||||
} else if customHeader.Direction == HeaderDirection_ZoraxyToDownstream {
|
||||
downstreamHeaders[downstreamHeaderCounter] = thisHeaderSet
|
||||
downstreamHeaderCounter++
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the endpoint require HSTS headers
|
||||
if ept.HSTSMaxAge > 0 {
|
||||
downstreamHeaders[downstreamHeaderCounter] = []string{"Strict-Transport-Security", "max-age=" + strconv.Itoa(int(ept.HSTSMaxAge))}
|
||||
downstreamHeaderCounter++
|
||||
}
|
||||
|
||||
//Check if the endpoint require Permission Policy
|
||||
if ept.EnablePermissionPolicyHeader {
|
||||
var usingPermissionPolicy *permissionpolicy.PermissionsPolicy
|
||||
if ept.PermissionPolicy != nil {
|
||||
//Custom permission policy
|
||||
usingPermissionPolicy = ept.PermissionPolicy
|
||||
} else {
|
||||
//Permission policy is enabled but not customized. Use default
|
||||
usingPermissionPolicy = permissionpolicy.GetDefaultPermissionPolicy()
|
||||
}
|
||||
|
||||
downstreamHeaders[downstreamHeaderCounter] = usingPermissionPolicy.ToKeyValueHeader()
|
||||
downstreamHeaderCounter++
|
||||
}
|
||||
|
||||
return upstreamHeaders, downstreamHeaders
|
||||
}
|
@ -10,6 +10,8 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
)
|
||||
|
||||
// ReverseProxy is an HTTP Handler that takes an incoming request and
|
||||
@ -60,6 +62,9 @@ type ResponseRewriteRuleSet struct {
|
||||
UseTLS bool
|
||||
NoCache bool
|
||||
PathPrefix string //Vdir prefix for root, / will be rewrite to this
|
||||
UpstreamHeaders [][]string
|
||||
DownstreamHeaders [][]string
|
||||
Version string //Version number of Zoraxy, use for X-Proxy-By
|
||||
}
|
||||
|
||||
type requestCanceler interface {
|
||||
@ -246,78 +251,6 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func removeHeaders(header http.Header, noCache bool) {
|
||||
// Remove hop-by-hop headers listed in the "Connection" header.
|
||||
if c := header.Get("Connection"); c != "" {
|
||||
for _, f := range strings.Split(c, ",") {
|
||||
if f = strings.TrimSpace(f); f != "" {
|
||||
header.Del(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove hop-by-hop headers
|
||||
for _, h := range hopHeaders {
|
||||
if header.Get(h) != "" {
|
||||
header.Del(h)
|
||||
}
|
||||
}
|
||||
|
||||
//Restore the Upgrade header if any
|
||||
if header.Get("Zr-Origin-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
|
||||
header.Del("Zr-Origin-Upgrade")
|
||||
}
|
||||
|
||||
//Disable cache if nocache is set
|
||||
if noCache {
|
||||
header.Del("Cache-Control")
|
||||
header.Set("Cache-Control", "no-store")
|
||||
}
|
||||
|
||||
//Hide Go-HTTP-Client UA if the client didnt sent us one
|
||||
if _, ok := header["User-Agent"]; !ok {
|
||||
// If the outbound request doesn't have a User-Agent header set,
|
||||
// don't send the default Go HTTP client User-Agent.
|
||||
header.Set("User-Agent", "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func addXForwardedForHeader(req *http.Request) {
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
// If we aren't the first proxy retain prior
|
||||
// X-Forwarded-For information as a comma+space
|
||||
// separated list and fold multiple headers into one.
|
||||
if prior, ok := req.Header["X-Forwarded-For"]; ok {
|
||||
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")
|
||||
}
|
||||
|
||||
if req.Header.Get("X-Real-Ip") == "" {
|
||||
//Check if CF-Connecting-IP header exists
|
||||
CF_Connecting_IP := req.Header.Get("CF-Connecting-IP")
|
||||
if CF_Connecting_IP != "" {
|
||||
//Use CF Connecting IP
|
||||
req.Header.Set("X-Real-Ip", CF_Connecting_IP)
|
||||
} else {
|
||||
// Not exists. Fill it in with first entry in X-Forwarded-For
|
||||
ips := strings.Split(clientIP, ",")
|
||||
if len(ips) > 0 {
|
||||
req.Header.Set("X-Real-Ip", strings.TrimSpace(ips[0]))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error {
|
||||
transport := p.Transport
|
||||
|
||||
@ -346,9 +279,9 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
p.Director(outreq)
|
||||
outreq.Close = false
|
||||
|
||||
if !rrr.UseTLS {
|
||||
//This seems to be routing to external sites
|
||||
//Do not keep the original host
|
||||
//Only skip origin rewrite iff proxy target require TLS and it is external domain name like github.com
|
||||
if !(rrr.UseTLS && isExternalDomainName(rrr.ProxyDomain)) {
|
||||
// Always use the original host, see issue #164
|
||||
outreq.Host = rrr.OriginalHost
|
||||
}
|
||||
|
||||
@ -356,12 +289,18 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
outreq.Header = make(http.Header)
|
||||
copyHeader(outreq.Header, req.Header)
|
||||
|
||||
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
|
||||
// Remove hop-by-hop headers.
|
||||
removeHeaders(outreq.Header, rrr.NoCache)
|
||||
|
||||
// Add X-Forwarded-For Header.
|
||||
addXForwardedForHeader(outreq)
|
||||
|
||||
// Add user defined headers (to upstream)
|
||||
injectUserDefinedHeaders(outreq.Header, rrr.UpstreamHeaders)
|
||||
|
||||
// Rewrite outbound UA, must be after user headers
|
||||
rewriteUserAgent(outreq.Header, "Zoraxy/"+rrr.Version)
|
||||
|
||||
res, err := transport.RoundTrip(outreq)
|
||||
if err != nil {
|
||||
if p.Verbal {
|
||||
@ -392,13 +331,17 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Figure out a way to proxy for proxmox
|
||||
//if res.StatusCode == 501 || res.StatusCode == 500 {
|
||||
// fmt.Println(outreq.Proto, outreq.RemoteAddr, outreq.RequestURI)
|
||||
// fmt.Println(">>>", outreq.Method, res.Header, res.ContentLength, res.StatusCode)
|
||||
// fmt.Println(outreq.Header, req.Host)
|
||||
//}
|
||||
|
||||
//Custom header rewriter functions
|
||||
//Add debug X-Proxy-By tracker
|
||||
res.Header.Set("x-proxy-by", "zoraxy/"+rrr.Version)
|
||||
|
||||
//Custom Location header rewriter functions
|
||||
if res.Header.Get("Location") != "" {
|
||||
locationRewrite := res.Header.Get("Location")
|
||||
originLocation := res.Header.Get("Location")
|
||||
@ -424,9 +367,16 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
||||
res.Header.Set("Location", locationRewrite)
|
||||
}
|
||||
|
||||
// Add user defined headers (to downstream)
|
||||
injectUserDefinedHeaders(res.Header, rrr.DownstreamHeaders)
|
||||
|
||||
// Copy header from response to client.
|
||||
copyHeader(rw.Header(), res.Header)
|
||||
|
||||
// inject permission policy headers
|
||||
//TODO: Load permission policy from rrr
|
||||
permissionpolicy.InjectPermissionPolicyHeader(rw, nil)
|
||||
|
||||
// The "Trailer" header isn't included in the Transport's response, Build it up from Trailer.
|
||||
if len(res.Trailer) > 0 {
|
||||
trailerKeys := make([]string, 0, len(res.Trailer))
|
||||
|
120
src/mod/dynamicproxy/dpcore/header.go
Normal file
120
src/mod/dynamicproxy/dpcore/header.go
Normal file
@ -0,0 +1,120 @@
|
||||
package dpcore
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Header.go
|
||||
|
||||
This script handles headers rewrite and remove
|
||||
in dpcore.
|
||||
|
||||
Added in Zoraxy v3.0.6 by tobychui
|
||||
*/
|
||||
|
||||
// removeHeaders Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
|
||||
func removeHeaders(header http.Header, noCache bool) {
|
||||
// Remove hop-by-hop headers listed in the "Connection" header.
|
||||
if c := header.Get("Connection"); c != "" {
|
||||
for _, f := range strings.Split(c, ",") {
|
||||
if f = strings.TrimSpace(f); f != "" {
|
||||
header.Del(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove hop-by-hop headers
|
||||
for _, h := range hopHeaders {
|
||||
if header.Get(h) != "" {
|
||||
header.Del(h)
|
||||
}
|
||||
}
|
||||
|
||||
//Restore the Upgrade header if any
|
||||
if header.Get("Zr-Origin-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
|
||||
header.Del("Zr-Origin-Upgrade")
|
||||
}
|
||||
|
||||
//Disable cache if nocache is set
|
||||
if noCache {
|
||||
header.Del("Cache-Control")
|
||||
header.Set("Cache-Control", "no-store")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// rewriteUserAgent rewrite the user agent based on incoming request
|
||||
func rewriteUserAgent(header http.Header, UA string) {
|
||||
//Hide Go-HTTP-Client UA if the client didnt sent us one
|
||||
if header.Get("User-Agent") == "" {
|
||||
// If the outbound request doesn't have a User-Agent header set,
|
||||
// don't send the default Go HTTP client User-Agent
|
||||
header.Del("User-Agent")
|
||||
header.Set("User-Agent", UA)
|
||||
}
|
||||
}
|
||||
|
||||
// Add X-Forwarded-For Header and rewrite X-Real-Ip according to sniffing logics
|
||||
func addXForwardedForHeader(req *http.Request) {
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
// If we aren't the first proxy retain prior
|
||||
// X-Forwarded-For information as a comma+space
|
||||
// separated list and fold multiple headers into one.
|
||||
if prior, ok := req.Header["X-Forwarded-For"]; ok {
|
||||
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")
|
||||
}
|
||||
|
||||
if req.Header.Get("X-Real-Ip") == "" {
|
||||
//Check if CF-Connecting-IP header exists
|
||||
CF_Connecting_IP := req.Header.Get("CF-Connecting-IP")
|
||||
Fastly_Client_IP := req.Header.Get("Fastly-Client-IP")
|
||||
if CF_Connecting_IP != "" {
|
||||
//Use CF Connecting IP
|
||||
req.Header.Set("X-Real-Ip", CF_Connecting_IP)
|
||||
} else if Fastly_Client_IP != "" {
|
||||
//Use Fastly Client IP
|
||||
req.Header.Set("X-Real-Ip", Fastly_Client_IP)
|
||||
} else {
|
||||
// Not exists. Fill it in with first entry in X-Forwarded-For
|
||||
ips := strings.Split(clientIP, ",")
|
||||
if len(ips) > 0 {
|
||||
req.Header.Set("X-Real-Ip", strings.TrimSpace(ips[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// injectUserDefinedHeaders inject the user headers from slice
|
||||
// if a value is empty string, the key will be removed from header.
|
||||
// if a key is empty string, the function will return immediately
|
||||
func injectUserDefinedHeaders(header http.Header, userHeaders [][]string) {
|
||||
for _, userHeader := range userHeaders {
|
||||
if len(userHeader) == 0 {
|
||||
//End of header slice
|
||||
return
|
||||
}
|
||||
headerKey := userHeader[0]
|
||||
headerValue := userHeader[1]
|
||||
if headerValue == "" {
|
||||
//Remove header from head
|
||||
header.Del(headerKey)
|
||||
continue
|
||||
}
|
||||
|
||||
//Default: Set header value
|
||||
header.Del(headerKey) //Remove header if it already exists
|
||||
header.Set(headerKey, headerValue)
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package dpcore
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
@ -60,3 +61,34 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
|
||||
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
|
||||
return replaceLocationHost(urlString, rrr, useTLS)
|
||||
}
|
||||
|
||||
// isExternalDomainName check and return if the hostname is external domain name (e.g. github.com)
|
||||
// instead of internal (like 192.168.1.202:8443 (ip address) or domains end with .local or .internal)
|
||||
func isExternalDomainName(hostname string) bool {
|
||||
host, _, err := net.SplitHostPort(hostname)
|
||||
if err != nil {
|
||||
//hostname doesnt contain port
|
||||
ip := net.ParseIP(hostname)
|
||||
if ip != nil {
|
||||
//IP address, not a domain name
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
//Hostname contain port, use hostname without port to check if it is ip
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil {
|
||||
//IP address, not a domain name
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//Check if it is internal DNS assigned domains
|
||||
internalDNSTLD := []string{".local", ".internal", ".localhost", ".home.arpa"}
|
||||
for _, tld := range internalDNSTLD {
|
||||
if strings.HasSuffix(strings.ToLower(hostname), tld) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ func NewDynamicProxy(option RouterOption) (*Router, error) {
|
||||
Running: false,
|
||||
server: nil,
|
||||
routingRules: []*RoutingRule{},
|
||||
tldMap: map[string]int{},
|
||||
rateLimitCounter: RequestCountPerIpTable{},
|
||||
}
|
||||
|
||||
thisRouter.mux = &ProxyHandler{
|
||||
@ -85,6 +85,12 @@ func (router *Router) StartProxyService() error {
|
||||
MinVersion: uint16(minVersion),
|
||||
}
|
||||
|
||||
//Start rate limitor
|
||||
err := router.startRateLimterCounterResetTicker()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if router.Option.UseTls {
|
||||
router.server = &http.Server{
|
||||
Addr: ":" + strconv.Itoa(router.Option.Port),
|
||||
@ -115,11 +121,41 @@ func (router *Router) StartProxyService() error {
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
//Access Check (blacklist / whitelist)
|
||||
ruleID := sep.AccessFilterUUID
|
||||
if sep.AccessFilterUUID == "" {
|
||||
//Use default rule
|
||||
ruleID = "default"
|
||||
}
|
||||
accessRule, err := router.Option.AccessController.GetAccessRuleByID(ruleID)
|
||||
if err == nil {
|
||||
isBlocked, _ := accessRequestBlocked(accessRule, router.Option.WebDirectory, w, r)
|
||||
if isBlocked {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Rate Limit
|
||||
if sep.RequireRateLimit {
|
||||
if err := router.handleRateLimit(w, r, sep); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Validate basic auth
|
||||
if sep.RequireBasicAuth {
|
||||
err := handleBasicAuth(w, r, sep)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: sep.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: sep.RequireTLS,
|
||||
PathPrefix: "",
|
||||
Version: sep.parent.Option.HostVersion,
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -209,10 +245,23 @@ func (router *Router) StopProxyService() error {
|
||||
return err
|
||||
}
|
||||
|
||||
//Stop TLS listener
|
||||
if router.tlsListener != nil {
|
||||
router.tlsListener.Close()
|
||||
}
|
||||
|
||||
//Stop rate limiter
|
||||
if router.rateLimterStop != nil {
|
||||
go func() {
|
||||
// As the rate timer loop has a 1 sec ticker
|
||||
// stop the rate limiter in go routine can prevent
|
||||
// front end from freezing for 1 sec
|
||||
router.rateLimterStop <- true
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
//Stop TLS redirection (from port 80)
|
||||
if router.tlsRedirectStop != nil {
|
||||
router.tlsRedirectStop <- true
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@ -49,16 +48,13 @@ func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
|
||||
}
|
||||
|
||||
// Add a user defined header to the list, duplicates will be automatically removed
|
||||
func (ep *ProxyEndpoint) AddUserDefinedHeader(key string, value string) error {
|
||||
if ep.UserDefinedHeaderExists(key) {
|
||||
ep.RemoveUserDefinedHeader(key)
|
||||
func (ep *ProxyEndpoint) AddUserDefinedHeader(newHeaderRule *UserDefinedHeader) error {
|
||||
if ep.UserDefinedHeaderExists(newHeaderRule.Key) {
|
||||
ep.RemoveUserDefinedHeader(newHeaderRule.Key)
|
||||
}
|
||||
|
||||
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, &UserDefinedHeader{
|
||||
Key: cases.Title(language.Und, cases.NoLower).String(key), //e.g. x-proxy-by -> X-Proxy-By
|
||||
Value: value,
|
||||
})
|
||||
|
||||
newHeaderRule.Key = cases.Title(language.Und, cases.NoLower).String(newHeaderRule.Key)
|
||||
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, newHeaderRule)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -70,7 +66,8 @@ func (ep *ProxyEndpoint) AddUserDefinedHeader(key string, value string) error {
|
||||
func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI string) *VirtualDirectoryEndpoint {
|
||||
for _, vdir := range ep.VirtualDirectories {
|
||||
if strings.HasPrefix(requestURI, vdir.MatchingPath) {
|
||||
return vdir
|
||||
thisVdir := vdir
|
||||
return thisVdir
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -80,7 +77,8 @@ func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI str
|
||||
func (ep *ProxyEndpoint) GetVirtualDirectoryRuleByMatchingPath(matchingPath string) *VirtualDirectoryEndpoint {
|
||||
for _, vdir := range ep.VirtualDirectories {
|
||||
if vdir.MatchingPath == matchingPath {
|
||||
return vdir
|
||||
thisVdir := vdir
|
||||
return thisVdir
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
60
src/mod/dynamicproxy/loadbalance/loadbalance.go
Normal file
60
src/mod/dynamicproxy/loadbalance/loadbalance.go
Normal file
@ -0,0 +1,60 @@
|
||||
package loadbalance
|
||||
|
||||
import (
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
)
|
||||
|
||||
/*
|
||||
Load Balancer
|
||||
|
||||
Handleing load balance request for upstream destinations
|
||||
*/
|
||||
|
||||
type BalancePolicy int
|
||||
|
||||
const (
|
||||
BalancePolicy_RoundRobin BalancePolicy = 0 //Round robin, will ignore upstream if down
|
||||
BalancePolicy_Fallback BalancePolicy = 1 //Fallback only. Will only switch to next node if the first one failed
|
||||
BalancePolicy_Random BalancePolicy = 2 //Random, randomly pick one from the list that is online
|
||||
BalancePolicy_GeoRegion BalancePolicy = 3 //Use the one defined for this geo-location, when down, pick the next avaible node
|
||||
)
|
||||
|
||||
type LoadBalanceRule struct {
|
||||
Upstreams []string //Reverse proxy upstream servers
|
||||
LoadBalancePolicy BalancePolicy //Policy in deciding which target IP to proxy
|
||||
UseRegionLock bool //If this is enabled with BalancePolicy_Geo, when the main site failed, it will not pick another node
|
||||
UseStickySession bool //Use sticky session, if you are serving EU countries, make sure to add the "Do you want cookie" warning
|
||||
|
||||
parent *RouteManager
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Geodb *geodb.Store //GeoIP resolver for checking incoming request origin country
|
||||
UptimeMonitor *uptime.Monitor //For checking if the target is online, this might be nil when the module starts
|
||||
}
|
||||
|
||||
type RouteManager struct {
|
||||
Options Options
|
||||
Logger *logger.Logger
|
||||
}
|
||||
|
||||
// Create a new load balance route manager
|
||||
func NewRouteManager(options *Options, logger *logger.Logger) *RouteManager {
|
||||
newManager := RouteManager{
|
||||
Options: *options,
|
||||
Logger: logger,
|
||||
}
|
||||
logger.PrintAndLog("INFO", "Load Balance Route Manager started", nil)
|
||||
return &newManager
|
||||
}
|
||||
|
||||
func (b *LoadBalanceRule) GetProxyTargetIP() {
|
||||
//TODO: Implement get proxy target IP logic here
|
||||
}
|
||||
|
||||
// Print debug message
|
||||
func (m *RouteManager) debugPrint(message string, err error) {
|
||||
m.Logger.PrintAndLog("LB", message, err)
|
||||
}
|
197
src/mod/dynamicproxy/permissionpolicy/permissionpolicy.go
Normal file
197
src/mod/dynamicproxy/permissionpolicy/permissionpolicy.go
Normal file
@ -0,0 +1,197 @@
|
||||
package permissionpolicy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Permisson Policy
|
||||
|
||||
This is a permission policy header modifier that changes
|
||||
the request permission related policy fields
|
||||
|
||||
author: tobychui
|
||||
*/
|
||||
|
||||
type PermissionsPolicy struct {
|
||||
Accelerometer []string `json:"accelerometer"`
|
||||
AmbientLightSensor []string `json:"ambient_light_sensor"`
|
||||
Autoplay []string `json:"autoplay"`
|
||||
Battery []string `json:"battery"`
|
||||
Camera []string `json:"camera"`
|
||||
CrossOriginIsolated []string `json:"cross_origin_isolated"`
|
||||
DisplayCapture []string `json:"display_capture"`
|
||||
DocumentDomain []string `json:"document_domain"`
|
||||
EncryptedMedia []string `json:"encrypted_media"`
|
||||
ExecutionWhileNotRendered []string `json:"execution_while_not_rendered"`
|
||||
ExecutionWhileOutOfView []string `json:"execution_while_out_of_viewport"`
|
||||
Fullscreen []string `json:"fullscreen"`
|
||||
Geolocation []string `json:"geolocation"`
|
||||
Gyroscope []string `json:"gyroscope"`
|
||||
KeyboardMap []string `json:"keyboard_map"`
|
||||
Magnetometer []string `json:"magnetometer"`
|
||||
Microphone []string `json:"microphone"`
|
||||
Midi []string `json:"midi"`
|
||||
NavigationOverride []string `json:"navigation_override"`
|
||||
Payment []string `json:"payment"`
|
||||
PictureInPicture []string `json:"picture_in_picture"`
|
||||
PublicKeyCredentialsGet []string `json:"publickey_credentials_get"`
|
||||
ScreenWakeLock []string `json:"screen_wake_lock"`
|
||||
SyncXHR []string `json:"sync_xhr"`
|
||||
USB []string `json:"usb"`
|
||||
WebShare []string `json:"web_share"`
|
||||
XRSpatialTracking []string `json:"xr_spatial_tracking"`
|
||||
ClipboardRead []string `json:"clipboard_read"`
|
||||
ClipboardWrite []string `json:"clipboard_write"`
|
||||
Gamepad []string `json:"gamepad"`
|
||||
SpeakerSelection []string `json:"speaker_selection"`
|
||||
ConversionMeasurement []string `json:"conversion_measurement"`
|
||||
FocusWithoutUserActivation []string `json:"focus_without_user_activation"`
|
||||
HID []string `json:"hid"`
|
||||
IdleDetection []string `json:"idle_detection"`
|
||||
InterestCohort []string `json:"interest_cohort"`
|
||||
Serial []string `json:"serial"`
|
||||
SyncScript []string `json:"sync_script"`
|
||||
TrustTokenRedemption []string `json:"trust_token_redemption"`
|
||||
Unload []string `json:"unload"`
|
||||
WindowPlacement []string `json:"window_placement"`
|
||||
VerticalScroll []string `json:"vertical_scroll"`
|
||||
}
|
||||
|
||||
// GetDefaultPermissionPolicy returns a PermissionsPolicy struct with all policies set to *
|
||||
func GetDefaultPermissionPolicy() *PermissionsPolicy {
|
||||
return &PermissionsPolicy{
|
||||
Accelerometer: []string{"*"},
|
||||
AmbientLightSensor: []string{"*"},
|
||||
Autoplay: []string{"*"},
|
||||
Battery: []string{"*"},
|
||||
Camera: []string{"*"},
|
||||
CrossOriginIsolated: []string{"*"},
|
||||
DisplayCapture: []string{"*"},
|
||||
DocumentDomain: []string{"*"},
|
||||
EncryptedMedia: []string{"*"},
|
||||
ExecutionWhileNotRendered: []string{"*"},
|
||||
ExecutionWhileOutOfView: []string{"*"},
|
||||
Fullscreen: []string{"*"},
|
||||
Geolocation: []string{"*"},
|
||||
Gyroscope: []string{"*"},
|
||||
KeyboardMap: []string{"*"},
|
||||
Magnetometer: []string{"*"},
|
||||
Microphone: []string{"*"},
|
||||
Midi: []string{"*"},
|
||||
NavigationOverride: []string{"*"},
|
||||
Payment: []string{"*"},
|
||||
PictureInPicture: []string{"*"},
|
||||
PublicKeyCredentialsGet: []string{"*"},
|
||||
ScreenWakeLock: []string{"*"},
|
||||
SyncXHR: []string{"*"},
|
||||
USB: []string{"*"},
|
||||
WebShare: []string{"*"},
|
||||
XRSpatialTracking: []string{"*"},
|
||||
ClipboardRead: []string{"*"},
|
||||
ClipboardWrite: []string{"*"},
|
||||
Gamepad: []string{"*"},
|
||||
SpeakerSelection: []string{"*"},
|
||||
ConversionMeasurement: []string{"*"},
|
||||
FocusWithoutUserActivation: []string{"*"},
|
||||
HID: []string{"*"},
|
||||
IdleDetection: []string{"*"},
|
||||
InterestCohort: []string{"*"},
|
||||
Serial: []string{"*"},
|
||||
SyncScript: []string{"*"},
|
||||
TrustTokenRedemption: []string{"*"},
|
||||
Unload: []string{"*"},
|
||||
WindowPlacement: []string{"*"},
|
||||
VerticalScroll: []string{"*"},
|
||||
}
|
||||
}
|
||||
|
||||
// ToKeyValueHeader convert a permission policy struct into a key value string header
|
||||
func (policy *PermissionsPolicy) ToKeyValueHeader() []string {
|
||||
policyHeader := []string{}
|
||||
|
||||
// Helper function to add policy directives
|
||||
addDirective := func(name string, sources []string) {
|
||||
if len(sources) > 0 {
|
||||
if sources[0] == "*" {
|
||||
//Allow all
|
||||
policyHeader = append(policyHeader, fmt.Sprintf("%s=%s", name, "*"))
|
||||
} else {
|
||||
//Other than "self" which do not need double quote, others domain need double quote in place
|
||||
formatedSources := []string{}
|
||||
for _, source := range sources {
|
||||
if source == "self" {
|
||||
formatedSources = append(formatedSources, "self")
|
||||
} else {
|
||||
formatedSources = append(formatedSources, "\""+source+"\"")
|
||||
}
|
||||
}
|
||||
policyHeader = append(policyHeader, fmt.Sprintf("%s=(%s)", name, strings.Join(formatedSources, " ")))
|
||||
}
|
||||
} else {
|
||||
//There are no setting for this field. Assume no permission
|
||||
policyHeader = append(policyHeader, fmt.Sprintf("%s=()", name))
|
||||
}
|
||||
}
|
||||
|
||||
// Add each policy directive to the header
|
||||
addDirective("accelerometer", policy.Accelerometer)
|
||||
addDirective("ambient-light-sensor", policy.AmbientLightSensor)
|
||||
addDirective("autoplay", policy.Autoplay)
|
||||
addDirective("battery", policy.Battery)
|
||||
addDirective("camera", policy.Camera)
|
||||
addDirective("cross-origin-isolated", policy.CrossOriginIsolated)
|
||||
addDirective("display-capture", policy.DisplayCapture)
|
||||
addDirective("document-domain", policy.DocumentDomain)
|
||||
addDirective("encrypted-media", policy.EncryptedMedia)
|
||||
addDirective("execution-while-not-rendered", policy.ExecutionWhileNotRendered)
|
||||
addDirective("execution-while-out-of-viewport", policy.ExecutionWhileOutOfView)
|
||||
addDirective("fullscreen", policy.Fullscreen)
|
||||
addDirective("geolocation", policy.Geolocation)
|
||||
addDirective("gyroscope", policy.Gyroscope)
|
||||
addDirective("keyboard-map", policy.KeyboardMap)
|
||||
addDirective("magnetometer", policy.Magnetometer)
|
||||
addDirective("microphone", policy.Microphone)
|
||||
addDirective("midi", policy.Midi)
|
||||
addDirective("navigation-override", policy.NavigationOverride)
|
||||
addDirective("payment", policy.Payment)
|
||||
addDirective("picture-in-picture", policy.PictureInPicture)
|
||||
addDirective("publickey-credentials-get", policy.PublicKeyCredentialsGet)
|
||||
addDirective("screen-wake-lock", policy.ScreenWakeLock)
|
||||
addDirective("sync-xhr", policy.SyncXHR)
|
||||
addDirective("usb", policy.USB)
|
||||
addDirective("web-share", policy.WebShare)
|
||||
addDirective("xr-spatial-tracking", policy.XRSpatialTracking)
|
||||
addDirective("clipboard-read", policy.ClipboardRead)
|
||||
addDirective("clipboard-write", policy.ClipboardWrite)
|
||||
addDirective("gamepad", policy.Gamepad)
|
||||
addDirective("speaker-selection", policy.SpeakerSelection)
|
||||
addDirective("conversion-measurement", policy.ConversionMeasurement)
|
||||
addDirective("focus-without-user-activation", policy.FocusWithoutUserActivation)
|
||||
addDirective("hid", policy.HID)
|
||||
addDirective("idle-detection", policy.IdleDetection)
|
||||
addDirective("interest-cohort", policy.InterestCohort)
|
||||
addDirective("serial", policy.Serial)
|
||||
addDirective("sync-script", policy.SyncScript)
|
||||
addDirective("trust-token-redemption", policy.TrustTokenRedemption)
|
||||
addDirective("unload", policy.Unload)
|
||||
addDirective("window-placement", policy.WindowPlacement)
|
||||
addDirective("vertical-scroll", policy.VerticalScroll)
|
||||
|
||||
// Join the directives and set the header
|
||||
policyHeaderValue := strings.Join(policyHeader, ", ")
|
||||
return []string{"Permissions-Policy", policyHeaderValue}
|
||||
}
|
||||
|
||||
// InjectPermissionPolicyHeader inject the permission policy into headers
|
||||
func InjectPermissionPolicyHeader(w http.ResponseWriter, policy *PermissionsPolicy) {
|
||||
//Keep the original Permission Policy if exists, or there are no policy given
|
||||
if policy == nil || w.Header().Get("Permissions-Policy") != "" {
|
||||
return
|
||||
}
|
||||
headerKV := policy.ToKeyValueHeader()
|
||||
//Inject the new policy into the header
|
||||
w.Header().Set(headerKV[0], headerKV[1])
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package permissionpolicy_test
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
)
|
||||
|
||||
func TestInjectPermissionPolicyHeader(t *testing.T) {
|
||||
//Prepare the data for permission policy
|
||||
testPermissionPolicy := permissionpolicy.GetDefaultPermissionPolicy()
|
||||
testPermissionPolicy.Geolocation = []string{"self"}
|
||||
testPermissionPolicy.Microphone = []string{"self", "https://example.com"}
|
||||
testPermissionPolicy.Camera = []string{"*"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
existingHeader string
|
||||
policy *permissionpolicy.PermissionsPolicy
|
||||
expectedHeader string
|
||||
}{
|
||||
{
|
||||
name: "Default policy with a few limitations",
|
||||
existingHeader: "",
|
||||
policy: testPermissionPolicy,
|
||||
expectedHeader: `accelerometer=*, ambient-light-sensor=*, autoplay=*, battery=*, camera=*, cross-origin-isolated=*, display-capture=*, document-domain=*, encrypted-media=*, execution-while-not-rendered=*, execution-while-out-of-viewport=*, fullscreen=*, geolocation=(self), gyroscope=*, keyboard-map=*, magnetometer=*, microphone=(self "https://example.com"), midi=*, navigation-override=*, payment=*, picture-in-picture=*, publickey-credentials-get=*, screen-wake-lock=*, sync-xhr=*, usb=*, web-share=*, xr-spatial-tracking=*, clipboard-read=*, clipboard-write=*, gamepad=*, speaker-selection=*, conversion-measurement=*, focus-without-user-activation=*, hid=*, idle-detection=*, interest-cohort=*, serial=*, sync-script=*, trust-token-redemption=*, unload=*, window-placement=*, vertical-scroll=*`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
if tt.existingHeader != "" {
|
||||
rr.Header().Set("Permissions-Policy", tt.existingHeader)
|
||||
}
|
||||
|
||||
permissionpolicy.InjectPermissionPolicyHeader(rr, tt.policy)
|
||||
|
||||
gotHeader := rr.Header().Get("Permissions-Policy")
|
||||
if !strings.Contains(gotHeader, tt.expectedHeader) {
|
||||
t.Errorf("got header %s, want %s", gotHeader, tt.expectedHeader)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/websocketproxy"
|
||||
)
|
||||
@ -34,23 +34,45 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
|
||||
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
||||
ep, ok := router.ProxyEndpoints.Load(hostname)
|
||||
if ok {
|
||||
//Exact hit
|
||||
targetSubdomainEndpoint = ep.(*ProxyEndpoint)
|
||||
if !targetSubdomainEndpoint.Disabled {
|
||||
return targetSubdomainEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
//No hit. Try with wildcard
|
||||
//No hit. Try with wildcard and alias
|
||||
matchProxyEndpoints := []*ProxyEndpoint{}
|
||||
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
|
||||
ep := v.(*ProxyEndpoint)
|
||||
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
|
||||
if err != nil {
|
||||
//Continue
|
||||
//Bad pattern. Skip this rule
|
||||
return true
|
||||
}
|
||||
|
||||
if match {
|
||||
//targetSubdomainEndpoint = ep
|
||||
//Wildcard matches. Skip checking alias
|
||||
matchProxyEndpoints = append(matchProxyEndpoints, ep)
|
||||
return true
|
||||
}
|
||||
|
||||
//Wildcard not match. Check for alias
|
||||
if ep.MatchingDomainAlias != nil && len(ep.MatchingDomainAlias) > 0 {
|
||||
for _, aliasDomain := range ep.MatchingDomainAlias {
|
||||
match, err := filepath.Match(aliasDomain, hostname)
|
||||
if err != nil {
|
||||
//Bad pattern. Skip this alias
|
||||
continue
|
||||
}
|
||||
|
||||
if match {
|
||||
//This alias match
|
||||
matchProxyEndpoints = append(matchProxyEndpoints, ep)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
@ -89,13 +111,6 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
|
||||
//Inject custom headers
|
||||
if len(target.UserDefinedHeaders) > 0 {
|
||||
for _, customHeader := range target.UserDefinedHeaders {
|
||||
r.Header.Set(customHeader.Key, customHeader.Value)
|
||||
}
|
||||
}
|
||||
|
||||
requestURL := r.URL.String()
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
@ -130,12 +145,18 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
//Build downstream and upstream header rules
|
||||
upstreamHeaders, downstreamHeaders := target.SplitInboundOutboundHeaders()
|
||||
|
||||
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
NoCache: h.Parent.Option.NoCache,
|
||||
PathPrefix: "",
|
||||
UpstreamHeaders: upstreamHeaders,
|
||||
DownstreamHeaders: downstreamHeaders,
|
||||
Version: target.parent.Option.HostVersion,
|
||||
})
|
||||
|
||||
var dnsError *net.DNSError
|
||||
@ -162,13 +183,6 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
|
||||
|
||||
//Inject custom headers
|
||||
if len(target.parent.UserDefinedHeaders) > 0 {
|
||||
for _, customHeader := range target.parent.UserDefinedHeaders {
|
||||
r.Header.Set(customHeader.Key, customHeader.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("Zr-Origin-Upgrade", "websocket")
|
||||
@ -197,11 +211,17 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
//Build downstream and upstream header rules
|
||||
upstreamHeaders, downstreamHeaders := target.parent.SplitInboundOutboundHeaders()
|
||||
|
||||
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
PathPrefix: target.MatchingPath,
|
||||
UpstreamHeaders: upstreamHeaders,
|
||||
DownstreamHeaders: downstreamHeaders,
|
||||
Version: target.parent.parent.Option.HostVersion,
|
||||
})
|
||||
|
||||
var dnsError *net.DNSError
|
||||
@ -224,7 +244,7 @@ func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, fo
|
||||
if h.Parent.Option.StatisticCollector != nil {
|
||||
go func() {
|
||||
requestInfo := statistic.RequestInfo{
|
||||
IpAddr: geodb.GetRequesterIP(r),
|
||||
IpAddr: netutils.GetRequesterIP(r),
|
||||
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
|
||||
Succ: succ,
|
||||
StatusCode: statusCode,
|
||||
|
119
src/mod/dynamicproxy/ratelimit.go
Normal file
119
src/mod/dynamicproxy/ratelimit.go
Normal file
@ -0,0 +1,119 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// IpTable is a rate limiter implementation using sync.Map with atomic int64
|
||||
type RequestCountPerIpTable struct {
|
||||
table sync.Map
|
||||
}
|
||||
|
||||
// Increment the count of requests for a given IP
|
||||
func (t *RequestCountPerIpTable) Increment(ip string) {
|
||||
v, _ := t.table.LoadOrStore(ip, new(int64))
|
||||
atomic.AddInt64(v.(*int64), 1)
|
||||
}
|
||||
|
||||
// Check if the IP is in the table and if it is, check if the count is less than the limit
|
||||
func (t *RequestCountPerIpTable) Exceeded(ip string, limit int64) bool {
|
||||
v, ok := t.table.Load(ip)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
count := atomic.LoadInt64(v.(*int64))
|
||||
return count >= limit
|
||||
}
|
||||
|
||||
// Get the count of requests for a given IP
|
||||
func (t *RequestCountPerIpTable) GetCount(ip string) int64 {
|
||||
v, ok := t.table.Load(ip)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return atomic.LoadInt64(v.(*int64))
|
||||
}
|
||||
|
||||
// Clear the IP table
|
||||
func (t *RequestCountPerIpTable) Clear() {
|
||||
t.table.Range(func(key, value interface{}) bool {
|
||||
t.table.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ProxyHandler) handleRateLimitRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
|
||||
err := h.Parent.handleRateLimit(w, r, pe)
|
||||
if err != nil {
|
||||
h.logRequest(r, false, 429, "ratelimit", pe.Domain)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (router *Router) handleRateLimit(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
|
||||
//Get the real client-ip from request header
|
||||
clientIP := r.RemoteAddr
|
||||
if r.Header.Get("X-Real-Ip") == "" {
|
||||
CF_Connecting_IP := r.Header.Get("CF-Connecting-IP")
|
||||
Fastly_Client_IP := r.Header.Get("Fastly-Client-IP")
|
||||
if CF_Connecting_IP != "" {
|
||||
//Use CF Connecting IP
|
||||
clientIP = CF_Connecting_IP
|
||||
} else if Fastly_Client_IP != "" {
|
||||
//Use Fastly Client IP
|
||||
clientIP = Fastly_Client_IP
|
||||
} else {
|
||||
ips := strings.Split(clientIP, ",")
|
||||
if len(ips) > 0 {
|
||||
clientIP = strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ip, _, err := net.SplitHostPort(clientIP)
|
||||
if err != nil {
|
||||
//Default allow passthrough on error
|
||||
return nil
|
||||
}
|
||||
|
||||
router.rateLimitCounter.Increment(ip)
|
||||
|
||||
if router.rateLimitCounter.Exceeded(ip, int64(pe.RateLimit)) {
|
||||
w.WriteHeader(429)
|
||||
return errors.New("rate limit exceeded")
|
||||
}
|
||||
|
||||
// log.Println("Rate limit check", ip, ipTable.GetCount(ip))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start the ticker routine for reseting the rate limit counter every seconds
|
||||
func (r *Router) startRateLimterCounterResetTicker() error {
|
||||
if r.rateLimterStop != nil {
|
||||
return errors.New("another rate limiter ticker already running")
|
||||
}
|
||||
tickerStopChan := make(chan bool)
|
||||
r.rateLimterStop = tickerStopChan
|
||||
|
||||
counterResetTicker := time.NewTicker(1 * time.Second)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-tickerStopChan:
|
||||
r.rateLimterStop = nil
|
||||
return
|
||||
case <-counterResetTicker.C:
|
||||
r.rateLimitCounter.Clear()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package redirection
|
||||
|
||||
import (
|
||||
"log"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
@ -52,7 +52,7 @@ func (t *RuleTable) HandleRedirect(w http.ResponseWriter, r *http.Request) int {
|
||||
//Invalid usage
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("500 - Internal Server Error"))
|
||||
log.Println("Target request URL do not have matching redirect rule. Check with IsRedirectable before calling HandleRedirect!")
|
||||
t.log("Target request URL do not have matching redirect rule. Check with IsRedirectable before calling HandleRedirect!", errors.New("invalid usage"))
|
||||
return 500
|
||||
}
|
||||
}
|
||||
|
@ -30,11 +30,12 @@ type RedirectRules struct {
|
||||
StatusCode int //Status Code for redirection
|
||||
}
|
||||
|
||||
func NewRuleTable(configPath string, allowRegex bool) (*RuleTable, error) {
|
||||
func NewRuleTable(configPath string, allowRegex bool, logger *logger.Logger) (*RuleTable, error) {
|
||||
thisRuleTable := RuleTable{
|
||||
rules: sync.Map{},
|
||||
configPath: configPath,
|
||||
AllowRegex: allowRegex,
|
||||
Logger: logger,
|
||||
}
|
||||
//Load all the rules from the config path
|
||||
if !utils.FileExists(configPath) {
|
||||
@ -67,7 +68,7 @@ func NewRuleTable(configPath string, allowRegex bool) (*RuleTable, error) {
|
||||
|
||||
//Map the rules into the sync map
|
||||
for _, rule := range rules {
|
||||
log.Println("Redirection rule added: " + rule.RedirectURL + " -> " + rule.TargetURL)
|
||||
thisRuleTable.log("Redirection rule added: "+rule.RedirectURL+" -> "+rule.TargetURL, nil)
|
||||
thisRuleTable.rules.Store(rule.RedirectURL, rule)
|
||||
}
|
||||
|
||||
@ -92,7 +93,7 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP
|
||||
// Create a new file for writing the JSON data
|
||||
file, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
log.Printf("Error creating file %s: %s", filepath, err)
|
||||
t.log("Error creating file "+filepath, err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
@ -100,7 +101,7 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP
|
||||
// Encode the RedirectRules object to JSON and write it to the file
|
||||
err = json.NewEncoder(file).Encode(newRule)
|
||||
if err != nil {
|
||||
log.Printf("Error encoding JSON to file %s: %s", filepath, err)
|
||||
t.log("Error encoding JSON to file "+filepath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -125,7 +126,7 @@ func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
|
||||
|
||||
// Delete the file
|
||||
if err := os.Remove(filepath); err != nil {
|
||||
log.Printf("Error deleting file %s: %s", filepath, err)
|
||||
t.log("Error deleting file "+filepath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,9 @@ import (
|
||||
func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
|
||||
//Filter the tailing slash if any
|
||||
domain := endpoint.Domain
|
||||
if len(domain) == 0 {
|
||||
return nil, errors.New("invalid endpoint config")
|
||||
}
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
@ -51,6 +54,10 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
|
||||
//Prepare proxy routing hjandler for each of the virtual directories
|
||||
for _, vdir := range endpoint.VirtualDirectories {
|
||||
domain := vdir.Domain
|
||||
if len(domain) == 0 {
|
||||
//invalid vdir
|
||||
continue
|
||||
}
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
@ -39,7 +39,7 @@
|
||||
<h3 style="margin-top: 1em;">403 - Forbidden</h3>
|
||||
<div class="ui divider"></div>
|
||||
<p>You do not have permission to view this directory or page. <br>
|
||||
This might cause by the region limit setting of this site.</p>
|
||||
This might be caused by the region limit setting of this site.</p>
|
||||
<div class="ui divider"></div>
|
||||
<div style="text-align: left;">
|
||||
<small>Request time: <span id="reqtime"></span></small><br>
|
||||
|
@ -14,7 +14,7 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script>
|
||||
<title>404 - Host Not Found</title>
|
||||
<style>
|
||||
h1, h2, h3, h4, h5, p, a, span{
|
||||
h1, h2, h3, h4, h5, p, a, span, .ui.list .item{
|
||||
font-family: 'Noto Sans TC', sans-serif;
|
||||
font-weight: 300;
|
||||
color: rgb(88, 88, 88)
|
||||
@ -22,9 +22,6 @@
|
||||
|
||||
.diagram{
|
||||
background-color: #ebebeb;
|
||||
box-shadow:
|
||||
inset 0px 11px 8px -10px #CCC,
|
||||
inset 0px -11px 8px -10px #CCC;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,9 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
@ -34,7 +36,8 @@ type RouterOption struct {
|
||||
ForceHttpsRedirect bool //Force redirection of http to https endpoint
|
||||
TlsManager *tlscert.Manager
|
||||
RedirectRuleTable *redirection.RuleTable
|
||||
GeodbStore *geodb.Store //GeoIP blacklist and whitelist
|
||||
GeodbStore *geodb.Store //GeoIP resolver
|
||||
AccessController *access.Controller //Blacklist / whitelist controller
|
||||
StatisticCollector *statistic.Collector
|
||||
WebDirectory string //The static web server directory containing the templates folder
|
||||
}
|
||||
@ -50,7 +53,8 @@ type Router struct {
|
||||
routingRules []*RoutingRule
|
||||
|
||||
tlsRedirectStop chan bool //Stop channel for tls redirection server
|
||||
tldMap map[string]int //Top level domain map, see tld.json
|
||||
rateLimterStop chan bool //Stop channel for rate limiter
|
||||
rateLimitCounter RequestCountPerIpTable //Request counter for rate limter
|
||||
}
|
||||
|
||||
// Auth credential for basic auth on certain endpoints
|
||||
@ -70,10 +74,20 @@ type BasicAuthExceptionRule struct {
|
||||
PathPrefix string
|
||||
}
|
||||
|
||||
// Header injection direction type
|
||||
type HeaderDirection int
|
||||
|
||||
const (
|
||||
HeaderDirection_ZoraxyToUpstream HeaderDirection = 0 //Inject (or remove) header to request out-going from Zoraxy to backend server
|
||||
HeaderDirection_ZoraxyToDownstream HeaderDirection = 1 //Inject (or remove) header to request out-going from Zoraxy to client (e.g. browser)
|
||||
)
|
||||
|
||||
// User defined headers to add into a proxy endpoint
|
||||
type UserDefinedHeader struct {
|
||||
Direction HeaderDirection
|
||||
Key string
|
||||
Value string
|
||||
IsRemove bool //Instead of set, remove this key instead
|
||||
}
|
||||
|
||||
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
|
||||
@ -92,6 +106,7 @@ type VirtualDirectoryEndpoint struct {
|
||||
type ProxyEndpoint struct {
|
||||
ProxyType int //The type of this proxy, see const def
|
||||
RootOrMatchingDomain string //Matching domain for host, also act as key
|
||||
MatchingDomainAlias []string //A list of domains that alias to this rule
|
||||
Domain string //Domain or IP to proxy to
|
||||
|
||||
//TLS/SSL Related
|
||||
@ -105,20 +120,30 @@ type ProxyEndpoint struct {
|
||||
|
||||
//Custom Headers
|
||||
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
|
||||
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
|
||||
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
|
||||
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
|
||||
|
||||
//Authentication
|
||||
RequireBasicAuth bool //Set to true to request basic auth before proxy
|
||||
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
|
||||
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
|
||||
|
||||
//Fallback routing logic
|
||||
DefaultSiteOption int //Fallback routing logic options
|
||||
DefaultSiteValue string //Fallback routing target, optional
|
||||
// Rate Limiting
|
||||
RequireRateLimit bool
|
||||
RateLimit int64 // Rate limit in requests per second
|
||||
|
||||
//Access Control
|
||||
AccessFilterUUID string //Access filter ID
|
||||
|
||||
Disabled bool //If the rule is disabled
|
||||
|
||||
//Fallback routing logic (Special Rule Sets Only)
|
||||
DefaultSiteOption int //Fallback routing logic options
|
||||
DefaultSiteValue string //Fallback routing target, optional
|
||||
|
||||
//Internal Logic Elements
|
||||
parent *Router
|
||||
parent *Router `json:"-"`
|
||||
proxy *dpcore.ReverseProxy `json:"-"`
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
|
||||
type Sender struct {
|
||||
Hostname string //E.g. mail.gandi.net
|
||||
Domain string //E.g. arozos.com
|
||||
Port int //E.g. 587
|
||||
Username string //Username of the email account
|
||||
Password string //Password of the email account
|
||||
@ -21,10 +20,9 @@ type Sender struct {
|
||||
}
|
||||
|
||||
// Create a new email sender object
|
||||
func NewEmailSender(hostname string, domain string, port int, username string, password string, senderAddr string) *Sender {
|
||||
func NewEmailSender(hostname string, port int, username string, password string, senderAddr string) *Sender {
|
||||
return &Sender{
|
||||
Hostname: hostname,
|
||||
Domain: domain,
|
||||
Port: port,
|
||||
Username: username,
|
||||
Password: password,
|
||||
@ -36,9 +34,11 @@ func NewEmailSender(hostname string, domain string, port int, username string, p
|
||||
Send a email to a reciving addr
|
||||
Example Usage:
|
||||
SendEmail(
|
||||
|
||||
test@example.com,
|
||||
"Free donuts",
|
||||
"Come get your free donuts on this Sunday!"
|
||||
|
||||
)
|
||||
*/
|
||||
func (s *Sender) SendEmail(to string, subject string, content string) error {
|
||||
@ -49,8 +49,15 @@ func (s *Sender) SendEmail(to string, subject string, content string) error {
|
||||
"MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" +
|
||||
content + "\n\n")
|
||||
|
||||
// Initialize the auth variable
|
||||
var auth smtp.Auth
|
||||
if s.Password != "" {
|
||||
// Login to the SMTP server
|
||||
auth := smtp.PlainAuth("", s.Username+"@"+s.Domain, s.Password, s.Hostname)
|
||||
// Username can be username (e.g. admin) or email (e.g. admin@example.com), depending on SMTP service provider
|
||||
auth = smtp.PlainAuth("", s.Username, s.Password, s.Hostname)
|
||||
}
|
||||
|
||||
// Send the email
|
||||
err := smtp.SendMail(s.Hostname+":"+strconv.Itoa(s.Port), auth, s.SenderAddr, []string{to}, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
|
8
src/mod/geodb/README.txt
Normal file
8
src/mod/geodb/README.txt
Normal file
@ -0,0 +1,8 @@
|
||||
The data source for geoip is licensed under CC0
|
||||
If you want to build your own version of geodb with updated whitelist,
|
||||
you can go to this repo and get the "GeoFeed + Whois + ASN" version of
|
||||
both the IPV4 and IPV6 mapping (the one without -num)
|
||||
|
||||
https://github.com/sapics/ip-location-db/tree/main/geolite2-country
|
||||
|
||||
And rename it to "gepipv4.csv" and "gepipv6.csv"
|
@ -1,91 +0,0 @@
|
||||
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
|
||||
}
|
@ -2,11 +2,10 @@ package geodb
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
)
|
||||
|
||||
//go:embed geoipv4.csv
|
||||
@ -16,8 +15,6 @@ var geoipv4 []byte //Geodb dataset for ipv4
|
||||
var geoipv6 []byte //Geodb dataset for ipv6
|
||||
|
||||
type Store struct {
|
||||
BlacklistEnabled bool
|
||||
WhitelistEnabled bool
|
||||
geodb [][]string //Parsed geodb list
|
||||
geodbIpv6 [][]string //Parsed geodb list for ipv6
|
||||
geotrie *trie
|
||||
@ -48,40 +45,6 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blacklistEnabled := false
|
||||
whitelistEnabled := false
|
||||
if sysdb != nil {
|
||||
err = sysdb.NewTable("blacklist-cn")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sysdb.NewTable("blacklist-ip")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sysdb.NewTable("whitelist-cn")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sysdb.NewTable("whitelist-ip")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sysdb.NewTable("blackwhitelist")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sysdb.Read("blackwhitelist", "blacklistEnabled", &blacklistEnabled)
|
||||
sysdb.Read("blackwhitelist", "whitelistEnabled", &whitelistEnabled)
|
||||
} else {
|
||||
log.Println("Database pointer set to nil: Entering debug mode")
|
||||
}
|
||||
|
||||
var ipv4Trie *trie
|
||||
if !option.AllowSlowIpv4LookUp {
|
||||
ipv4Trie = constrctTrieTree(parsedGeoData)
|
||||
@ -93,8 +56,6 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
|
||||
}
|
||||
|
||||
return &Store{
|
||||
BlacklistEnabled: blacklistEnabled,
|
||||
WhitelistEnabled: whitelistEnabled,
|
||||
geodb: parsedGeoData,
|
||||
geotrie: ipv4Trie,
|
||||
geodbIpv6: parsedGeoDataIpv6,
|
||||
@ -104,16 +65,6 @@ func NewGeoDb(sysdb *database.Database, option *StoreOptions) (*Store, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) ToggleBlacklist(enabled bool) {
|
||||
s.sysdb.Write("blackwhitelist", "blacklistEnabled", enabled)
|
||||
s.BlacklistEnabled = enabled
|
||||
}
|
||||
|
||||
func (s *Store) ToggleWhitelist(enabled bool) {
|
||||
s.sysdb.Write("blackwhitelist", "whitelistEnabled", enabled)
|
||||
s.WhitelistEnabled = enabled
|
||||
}
|
||||
|
||||
func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
|
||||
cc := s.search(ipstring)
|
||||
return &CountryInfo{
|
||||
@ -127,93 +78,16 @@ func (s *Store) Close() {
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
Check if a IP address is blacklisted, in either country or IP blacklist
|
||||
IsBlacklisted default return is false (allow access)
|
||||
*/
|
||||
func (s *Store) IsBlacklisted(ipAddr string) bool {
|
||||
if !s.BlacklistEnabled {
|
||||
//Blacklist not enabled. Always return false
|
||||
return false
|
||||
}
|
||||
|
||||
if ipAddr == "" {
|
||||
//Unable to get the target IP address
|
||||
return false
|
||||
}
|
||||
|
||||
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.IsIPBlacklisted(ipAddr) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
IsWhitelisted check if a given IP address is in the current
|
||||
server's white list.
|
||||
|
||||
Note that the Whitelist default result is true even
|
||||
when encountered error
|
||||
*/
|
||||
func (s *Store) IsWhitelisted(ipAddr string) bool {
|
||||
if !s.WhitelistEnabled {
|
||||
//Whitelist not enabled. Always return true (allow access)
|
||||
return true
|
||||
}
|
||||
|
||||
if ipAddr == "" {
|
||||
//Unable to get the target IP address, assume ok
|
||||
return true
|
||||
}
|
||||
|
||||
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.IsCountryCodeWhitelisted(countryCode.CountryIsoCode) {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.IsIPWhitelisted(ipAddr) {
|
||||
return true
|
||||
}
|
||||
|
||||
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)
|
||||
ipAddr := netutils.GetRequesterIP(r)
|
||||
if ipAddr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if netutils.IsPrivateIP(ipAddr) {
|
||||
return "LAN"
|
||||
}
|
||||
|
||||
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
|
||||
if err != nil {
|
||||
return ""
|
||||
|
@ -51,8 +51,16 @@ func TestResolveCountryCodeFromIP(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test an IP address that should return a valid country code
|
||||
ip := "8.8.8.8"
|
||||
expected := "US"
|
||||
knownIpCountryMap := [][]string{
|
||||
{"3.224.220.101", "US"},
|
||||
{"176.113.115.113", "RU"},
|
||||
{"65.21.233.213", "FI"},
|
||||
{"94.23.207.193", "FR"},
|
||||
}
|
||||
|
||||
for _, testcase := range knownIpCountryMap {
|
||||
ip := testcase[0]
|
||||
expected := testcase[1]
|
||||
info, err := store.ResolveCountryCodeFromIP(ip)
|
||||
if err != nil {
|
||||
t.Errorf("error resolving country code for IP %s: %v", ip, err)
|
||||
@ -61,11 +69,12 @@ func TestResolveCountryCodeFromIP(t *testing.T) {
|
||||
if info.CountryIsoCode != expected {
|
||||
t.Errorf("expected country code %s, but got %s for IP %s", expected, info.CountryIsoCode, ip)
|
||||
}
|
||||
}
|
||||
|
||||
// Test an IP address that should return an empty country code
|
||||
ip = "127.0.0.1"
|
||||
expected = ""
|
||||
info, err = store.ResolveCountryCodeFromIP(ip)
|
||||
ip := "127.0.0.1"
|
||||
expected := ""
|
||||
info, err := store.ResolveCountryCodeFromIP(ip)
|
||||
if err != nil {
|
||||
t.Errorf("error resolving country code for IP %s: %v", ip, err)
|
||||
return
|
||||
|
133488
src/mod/geodb/geoipv4.csv
133488
src/mod/geodb/geoipv4.csv
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,8 @@ import (
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
)
|
||||
|
||||
func (s *Store) search(ip string) string {
|
||||
@ -24,7 +26,7 @@ func (s *Store) search(ip string) string {
|
||||
|
||||
//Search in geotrie tree
|
||||
cc := ""
|
||||
if IsIPv6(ip) {
|
||||
if netutils.IsIPv6(ip) {
|
||||
if s.geotrieIpv6 == nil {
|
||||
cc = s.slowSearchIpv6(ip)
|
||||
} else {
|
||||
|
@ -53,6 +53,9 @@ func isIPv6InRange(startIP, endIP, testIP string) (bool, error) {
|
||||
|
||||
// Slow country code lookup for
|
||||
func (s *Store) slowSearchIpv4(ipAddr string) string {
|
||||
if isReservedIP(ipAddr) {
|
||||
return ""
|
||||
}
|
||||
for _, ipRange := range s.geodb {
|
||||
startIp := ipRange[0]
|
||||
endIp := ipRange[1]
|
||||
@ -67,6 +70,9 @@ func (s *Store) slowSearchIpv4(ipAddr string) string {
|
||||
}
|
||||
|
||||
func (s *Store) slowSearchIpv6(ipAddr string) string {
|
||||
if isReservedIP(ipAddr) {
|
||||
return ""
|
||||
}
|
||||
for _, ipRange := range s.geodbIpv6 {
|
||||
startIp := ipRange[0]
|
||||
endIp := ipRange[1]
|
||||
|
@ -1,129 +0,0 @@
|
||||
package geodb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Whitelist.go
|
||||
|
||||
This script handles whitelist related functions
|
||||
*/
|
||||
|
||||
const (
|
||||
EntryType_CountryCode int = 0
|
||||
EntryType_IP int = 1
|
||||
)
|
||||
|
||||
type WhitelistEntry struct {
|
||||
EntryType int //Entry type of whitelist, Country Code or IP
|
||||
CC string //ISO Country Code
|
||||
IP string //IP address or range
|
||||
Comment string //Comment for this entry
|
||||
}
|
||||
|
||||
//Geo Whitelist
|
||||
|
||||
func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
entry := WhitelistEntry{
|
||||
EntryType: EntryType_CountryCode,
|
||||
CC: countryCode,
|
||||
Comment: comment,
|
||||
}
|
||||
|
||||
s.sysdb.Write("whitelist-cn", countryCode, entry)
|
||||
}
|
||||
|
||||
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)
|
||||
return s.sysdb.KeyExists("whitelist-cn", countryCode)
|
||||
}
|
||||
|
||||
func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry {
|
||||
whitelistedCountryCode := []*WhitelistEntry{}
|
||||
entries, err := s.sysdb.ListTable("whitelist-cn")
|
||||
if err != nil {
|
||||
return whitelistedCountryCode
|
||||
}
|
||||
for _, keypairs := range entries {
|
||||
thisWhitelistEntry := WhitelistEntry{}
|
||||
json.Unmarshal(keypairs[1], &thisWhitelistEntry)
|
||||
whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry)
|
||||
}
|
||||
|
||||
return whitelistedCountryCode
|
||||
}
|
||||
|
||||
//IP Whitelist
|
||||
|
||||
func (s *Store) AddIPToWhiteList(ipAddr string, comment string) {
|
||||
thisIpEntry := WhitelistEntry{
|
||||
EntryType: EntryType_IP,
|
||||
IP: ipAddr,
|
||||
Comment: comment,
|
||||
}
|
||||
|
||||
s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry)
|
||||
}
|
||||
|
||||
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
|
||||
s.sysdb.Delete("whitelist-ip", ipAddr)
|
||||
}
|
||||
|
||||
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
|
||||
isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr)
|
||||
if isWhitelisted {
|
||||
//single IP whitelist entry
|
||||
return true
|
||||
}
|
||||
|
||||
//Check for IP wildcard and CIRD rules
|
||||
AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice()
|
||||
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() []*WhitelistEntry {
|
||||
whitelistedIp := []*WhitelistEntry{}
|
||||
entries, err := s.sysdb.ListTable("whitelist-ip")
|
||||
if err != nil {
|
||||
return whitelistedIp
|
||||
}
|
||||
|
||||
for _, keypairs := range entries {
|
||||
//ip := string(keypairs[0])
|
||||
thisEntry := WhitelistEntry{}
|
||||
json.Unmarshal(keypairs[1], &thisEntry)
|
||||
whitelistedIp = append(whitelistedIp, &thisEntry)
|
||||
}
|
||||
|
||||
return whitelistedIp
|
||||
}
|
||||
|
||||
func (s *Store) GetAllWhitelistedIpAsStringSlice() []string {
|
||||
allWhitelistedIPs := []string{}
|
||||
entries := s.GetAllWhitelistedIp()
|
||||
for _, entry := range entries {
|
||||
allWhitelistedIPs = append(allWhitelistedIPs, entry.IP)
|
||||
}
|
||||
|
||||
return allWhitelistedIPs
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package geodb
|
||||
package netutils
|
||||
|
||||
import (
|
||||
"net"
|
||||
@ -6,7 +6,13 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Utilities function
|
||||
/*
|
||||
MatchIP.go
|
||||
|
||||
This script contains function for matching IP address, comparing
|
||||
CIDR and IPv4 / v6 validations
|
||||
*/
|
||||
|
||||
func GetRequesterIP(r *http.Request) string {
|
||||
ip := r.Header.Get("X-Real-Ip")
|
||||
if ip == "" {
|
||||
@ -87,6 +93,10 @@ func MatchIpCIDR(ip string, cidr string) bool {
|
||||
|
||||
// Check if a ip is private IP range
|
||||
func IsPrivateIP(ipStr string) bool {
|
||||
if ipStr == "127.0.0.1" || ipStr == "::1" {
|
||||
//local loopback
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return false
|
@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
//Rewrite url based on proxy root
|
||||
// Rewrite url based on proxy root (default site)
|
||||
func RewriteURL(rooturl string, requestURL string) (*url.URL, error) {
|
||||
rewrittenURL := strings.TrimPrefix(requestURL, rooturl)
|
||||
return url.Parse(rewrittenURL)
|
||||
|
@ -1,9 +1,10 @@
|
||||
package tcpprox
|
||||
package streamproxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
@ -22,13 +23,13 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
portA, err := utils.PostPara(r, "porta")
|
||||
listenAddr, err := utils.PostPara(r, "listenAddr")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "first address cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
portB, err := utils.PostPara(r, "portb")
|
||||
proxyAddr, err := utils.PostPara(r, "proxyAddr")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "second address cannot be empty")
|
||||
return
|
||||
@ -44,27 +45,17 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
modeValue := ProxyMode_Transport
|
||||
mode, err := utils.PostPara(r, "mode")
|
||||
if err != nil || mode == "" {
|
||||
utils.SendErrorResponse(w, "no mode given")
|
||||
} else if mode == "listen" {
|
||||
modeValue = ProxyMode_Listen
|
||||
} else if mode == "transport" {
|
||||
modeValue = ProxyMode_Transport
|
||||
} else if mode == "starter" {
|
||||
modeValue = ProxyMode_Starter
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid mode given. Only support listen / transport / starter")
|
||||
}
|
||||
useTCP, _ := utils.PostBool(r, "useTCP")
|
||||
useUDP, _ := utils.PostBool(r, "useUDP")
|
||||
|
||||
//Create the target config
|
||||
newConfigUUID := m.NewConfig(&ProxyRelayOptions{
|
||||
Name: name,
|
||||
PortA: portA,
|
||||
PortB: portB,
|
||||
ListeningAddr: strings.TrimSpace(listenAddr),
|
||||
ProxyAddr: strings.TrimSpace(proxyAddr),
|
||||
Timeout: timeout,
|
||||
Mode: modeValue,
|
||||
UseTCP: useTCP,
|
||||
UseUDP: useUDP,
|
||||
})
|
||||
|
||||
js, _ := json.Marshal(newConfigUUID)
|
||||
@ -80,22 +71,10 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
newName, _ := utils.PostPara(r, "name")
|
||||
newPortA, _ := utils.PostPara(r, "porta")
|
||||
newPortB, _ := utils.PostPara(r, "portb")
|
||||
newModeStr, _ := utils.PostPara(r, "mode")
|
||||
newMode := -1
|
||||
if newModeStr != "" {
|
||||
if newModeStr == "listen" {
|
||||
newMode = 0
|
||||
} else if newModeStr == "transport" {
|
||||
newMode = 1
|
||||
} else if newModeStr == "starter" {
|
||||
newMode = 2
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid new mode value")
|
||||
return
|
||||
}
|
||||
}
|
||||
listenAddr, _ := utils.PostPara(r, "listenAddr")
|
||||
proxyAddr, _ := utils.PostPara(r, "proxyAddr")
|
||||
useTCP, _ := utils.PostBool(r, "useTCP")
|
||||
useUDP, _ := utils.PostBool(r, "useUDP")
|
||||
|
||||
newTimeoutStr, _ := utils.PostPara(r, "timeout")
|
||||
newTimeout := -1
|
||||
@ -108,7 +87,7 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Call the EditConfig method to modify the configuration
|
||||
err = m.EditConfig(configUUID, newName, newPortA, newPortB, newMode, newTimeout)
|
||||
err = m.EditConfig(configUUID, newName, listenAddr, proxyAddr, useTCP, useUDP, newTimeout)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
@ -158,6 +137,7 @@ func (m *Manager) HandleStopProxy(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if !targetProxyConfig.IsRunning() {
|
||||
targetProxyConfig.Running = false
|
||||
utils.SendErrorResponse(w, "target proxy service is not running")
|
||||
return
|
||||
}
|
||||
@ -180,6 +160,7 @@ func (m *Manager) HandleRemoveProxy(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if targetProxyConfig.IsRunning() {
|
||||
targetProxyConfig.Running = false
|
||||
utils.SendErrorResponse(w, "Service is running")
|
||||
return
|
||||
}
|
||||
@ -209,25 +190,3 @@ func (m *Manager) HandleGetProxyStatus(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(targetConfig)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func (m *Manager) HandleConfigValidate(w http.ResponseWriter, r *http.Request) {
|
||||
uuid, err := utils.GetPara(r, "uuid")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid uuid given")
|
||||
return
|
||||
}
|
||||
|
||||
targetConfig, err := m.GetConfigByUUID(uuid)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = targetConfig.ValidateConfigs()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
281
src/mod/streamproxy/streamproxy.go
Normal file
281
src/mod/streamproxy/streamproxy.go
Normal file
@ -0,0 +1,281 @@
|
||||
package streamproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
)
|
||||
|
||||
/*
|
||||
TCP Proxy
|
||||
|
||||
Forward port from one port to another
|
||||
Also accept active connection and passive
|
||||
connection
|
||||
*/
|
||||
|
||||
type ProxyRelayOptions struct {
|
||||
Name string
|
||||
ListeningAddr string
|
||||
ProxyAddr string
|
||||
Timeout int
|
||||
UseTCP bool
|
||||
UseUDP bool
|
||||
}
|
||||
|
||||
type ProxyRelayConfig struct {
|
||||
UUID string //A UUIDv4 representing this config
|
||||
Name string //Name of the config
|
||||
Running bool //Status, read only
|
||||
AutoStart bool //If the service suppose to started automatically
|
||||
ListeningAddress string //Listening Address, usually 127.0.0.1:port
|
||||
ProxyTargetAddr string //Proxy target address
|
||||
UseTCP bool //Enable TCP proxy
|
||||
UseUDP bool //Enable UDP proxy
|
||||
Timeout int //Timeout for connection in sec
|
||||
tcpStopChan chan bool //Stop channel for TCP listener
|
||||
udpStopChan chan bool //Stop channel for UDP listener
|
||||
aTobAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from A to B
|
||||
bToaAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from B to A
|
||||
udpClientMap sync.Map //map storing the UDP client-server connections
|
||||
parent *Manager `json:"-"`
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Database *database.Database
|
||||
DefaultTimeout int
|
||||
AccessControlHandler func(net.Conn) bool
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
//Config and stores
|
||||
Options *Options
|
||||
Configs []*ProxyRelayConfig
|
||||
|
||||
//Realtime Statistics
|
||||
Connections int //currently connected connect counts
|
||||
|
||||
}
|
||||
|
||||
func NewStreamProxy(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)
|
||||
}
|
||||
|
||||
//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,
|
||||
Connections: 0,
|
||||
}
|
||||
|
||||
//Inject manager into the rules
|
||||
for _, rule := range previousRules {
|
||||
rule.parent = &thisManager
|
||||
if rule.Running {
|
||||
//This was previously running. Start it again
|
||||
log.Println("[Stream Proxy] Resuming stream proxy rule " + rule.Name)
|
||||
rule.Start()
|
||||
}
|
||||
}
|
||||
|
||||
thisManager.Configs = previousRules
|
||||
|
||||
return &thisManager
|
||||
}
|
||||
|
||||
func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
|
||||
//Generate two zero value for atomic int64
|
||||
aAcc := atomic.Int64{}
|
||||
bAcc := atomic.Int64{}
|
||||
aAcc.Store(0)
|
||||
bAcc.Store(0)
|
||||
//Generate a new config from options
|
||||
configUUID := uuid.New().String()
|
||||
thisConfig := ProxyRelayConfig{
|
||||
UUID: configUUID,
|
||||
Name: config.Name,
|
||||
ListeningAddress: config.ListeningAddr,
|
||||
ProxyTargetAddr: config.ProxyAddr,
|
||||
UseTCP: config.UseTCP,
|
||||
UseUDP: config.UseUDP,
|
||||
Timeout: config.Timeout,
|
||||
tcpStopChan: nil,
|
||||
udpStopChan: nil,
|
||||
aTobAccumulatedByteTransfer: aAcc,
|
||||
bToaAccumulatedByteTransfer: bAcc,
|
||||
udpClientMap: sync.Map{},
|
||||
parent: m,
|
||||
}
|
||||
m.Configs = append(m.Configs, &thisConfig)
|
||||
m.SaveConfigToDatabase()
|
||||
return configUUID
|
||||
}
|
||||
|
||||
func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayConfig, error) {
|
||||
// Find and return the config with the specified UUID
|
||||
for _, config := range m.Configs {
|
||||
if config.UUID == configUUID {
|
||||
return config, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("config not found")
|
||||
}
|
||||
|
||||
// Edit the config based on config UUID, leave empty for unchange fields
|
||||
func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr string, newProxyAddr string, useTCP bool, useUDP bool, newTimeout int) error {
|
||||
// Find the config with the specified UUID
|
||||
foundConfig, err := m.GetConfigByUUID(configUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate and update the fields
|
||||
if newName != "" {
|
||||
foundConfig.Name = newName
|
||||
}
|
||||
if newListeningAddr != "" {
|
||||
foundConfig.ListeningAddress = newListeningAddr
|
||||
}
|
||||
if newProxyAddr != "" {
|
||||
foundConfig.ProxyTargetAddr = newProxyAddr
|
||||
}
|
||||
|
||||
foundConfig.UseTCP = useTCP
|
||||
foundConfig.UseUDP = useUDP
|
||||
|
||||
if newTimeout != -1 {
|
||||
if newTimeout < 0 {
|
||||
return errors.New("invalid timeout value given")
|
||||
}
|
||||
foundConfig.Timeout = newTimeout
|
||||
}
|
||||
|
||||
m.SaveConfigToDatabase()
|
||||
|
||||
//Check if config is running. If yes, restart it
|
||||
if foundConfig.IsRunning() {
|
||||
foundConfig.Restart()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) RemoveConfig(configUUID string) error {
|
||||
// Find and remove the config with the specified UUID
|
||||
for i, config := range m.Configs {
|
||||
if config.UUID == configUUID {
|
||||
m.Configs = append(m.Configs[:i], m.Configs[i+1:]...)
|
||||
m.SaveConfigToDatabase()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("config not found")
|
||||
}
|
||||
|
||||
func (m *Manager) SaveConfigToDatabase() {
|
||||
m.Options.Database.Write("tcprox", "rules", m.Configs)
|
||||
}
|
||||
|
||||
/*
|
||||
Config Functions
|
||||
*/
|
||||
|
||||
// Start a proxy if stopped
|
||||
func (c *ProxyRelayConfig) Start() error {
|
||||
if c.IsRunning() {
|
||||
c.Running = true
|
||||
return errors.New("proxy already running")
|
||||
}
|
||||
|
||||
// Create a stopChan to control the loop
|
||||
tcpStopChan := make(chan bool)
|
||||
udpStopChan := make(chan bool)
|
||||
|
||||
//Start the proxy service
|
||||
if c.UseUDP {
|
||||
c.udpStopChan = udpStopChan
|
||||
go func() {
|
||||
err := c.ForwardUDP(c.ListeningAddress, c.ProxyTargetAddr, udpStopChan)
|
||||
if err != nil {
|
||||
if !c.UseTCP {
|
||||
c.Running = false
|
||||
c.parent.SaveConfigToDatabase()
|
||||
}
|
||||
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if c.UseTCP {
|
||||
c.tcpStopChan = tcpStopChan
|
||||
go func() {
|
||||
//Default to transport mode
|
||||
err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
|
||||
if err != nil {
|
||||
c.Running = false
|
||||
c.parent.SaveConfigToDatabase()
|
||||
log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//Successfully spawned off the proxy routine
|
||||
c.Running = true
|
||||
c.parent.SaveConfigToDatabase()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return if a proxy config is running
|
||||
func (c *ProxyRelayConfig) IsRunning() bool {
|
||||
return c.tcpStopChan != nil || c.udpStopChan != nil
|
||||
}
|
||||
|
||||
// Restart a proxy config
|
||||
func (c *ProxyRelayConfig) Restart() {
|
||||
if c.IsRunning() {
|
||||
c.Stop()
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
c.Start()
|
||||
}
|
||||
|
||||
// Stop a running proxy if running
|
||||
func (c *ProxyRelayConfig) Stop() {
|
||||
log.Println("[STREAM PROXY] Stopping Stream Proxy " + c.Name)
|
||||
|
||||
if c.udpStopChan != nil {
|
||||
log.Println("[STREAM PROXY] Stopping UDP for " + c.Name)
|
||||
c.udpStopChan <- true
|
||||
c.udpStopChan = nil
|
||||
}
|
||||
|
||||
if c.tcpStopChan != nil {
|
||||
log.Println("[STREAM PROXY] Stopping TCP for " + c.Name)
|
||||
c.tcpStopChan <- true
|
||||
c.tcpStopChan = nil
|
||||
}
|
||||
|
||||
log.Println("[STREAM PROXY] Stopped Stream Proxy " + c.Name)
|
||||
c.Running = false
|
||||
|
||||
//Update the running status
|
||||
c.parent.SaveConfigToDatabase()
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package tcpprox_test
|
||||
package streamproxy_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/tcpprox"
|
||||
"imuslab.com/zoraxy/mod/streamproxy"
|
||||
)
|
||||
|
||||
func TestPort2Port(t *testing.T) {
|
||||
@ -12,7 +12,7 @@ func TestPort2Port(t *testing.T) {
|
||||
stopChan := make(chan bool)
|
||||
|
||||
// Create a ProxyRelayConfig with dummy values
|
||||
config := &tcpprox.ProxyRelayConfig{
|
||||
config := &streamproxy.ProxyRelayConfig{
|
||||
Timeout: 1,
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ func TestPort2Port(t *testing.T) {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// If the goroutine is still running, it means it did not stop as expected
|
||||
if config.Running {
|
||||
if config.IsRunning() {
|
||||
t.Errorf("port2port did not stop as expected")
|
||||
}
|
||||
|
146
src/mod/streamproxy/tcpprox.go
Normal file
146
src/mod/streamproxy/tcpprox.go
Normal file
@ -0,0 +1,146 @@
|
||||
package streamproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
func isValidIP(ip string) bool {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
return parsedIP != nil
|
||||
}
|
||||
|
||||
func isValidPort(port string) bool {
|
||||
portInt, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if portInt < 1 || portInt > 65535 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup, accumulator *atomic.Int64) {
|
||||
n, err := io.Copy(conn1, conn2)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
accumulator.Add(n) //Add to accumulator
|
||||
conn1.Close()
|
||||
log.Println("[←]", "close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]")
|
||||
//conn2.Close()
|
||||
//log.Println("[←]", "close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]")
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
func forward(conn1 net.Conn, conn2 net.Conn, aTob *atomic.Int64, bToa *atomic.Int64) {
|
||||
log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String())
|
||||
var wg sync.WaitGroup
|
||||
// wait tow goroutines
|
||||
wg.Add(2)
|
||||
go connCopy(conn1, conn2, &wg, aTob)
|
||||
go connCopy(conn2, conn1, &wg, bToa)
|
||||
//blocking when the wg is locked
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func startListener(address string) (net.Listener, error) {
|
||||
log.Println("[+]", "try to start server on:["+address+"]")
|
||||
server, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return nil, errors.New("listen address [" + address + "] faild")
|
||||
}
|
||||
log.Println("[√]", "start listen at address:["+address+"]")
|
||||
return server, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Forwarder Functions
|
||||
*/
|
||||
|
||||
/*
|
||||
portA -> server
|
||||
server -> portB
|
||||
*/
|
||||
func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, stopChan chan bool) error {
|
||||
listenerStartingAddr := allowPort
|
||||
if isValidPort(allowPort) {
|
||||
//number only, e.g. 8080
|
||||
listenerStartingAddr = "0.0.0.0:" + allowPort
|
||||
} else if strings.HasPrefix(allowPort, ":") && isValidPort(allowPort[1:]) {
|
||||
//port number starting with :, e.g. :8080
|
||||
listenerStartingAddr = "0.0.0.0" + allowPort
|
||||
}
|
||||
|
||||
server, err := startListener(listenerStartingAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetAddress = strings.TrimSpace(targetAddress)
|
||||
|
||||
//Start stop handler
|
||||
go func() {
|
||||
<-stopChan
|
||||
log.Println("[x]", "Received stop signal. Exiting Port to Host forwarder")
|
||||
server.Close()
|
||||
}()
|
||||
|
||||
//Start blocking loop for accepting connections
|
||||
for {
|
||||
conn, err := c.accept(server)
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
//Terminate by stop chan. Exit listener loop
|
||||
return nil
|
||||
}
|
||||
//Connection error. Retry
|
||||
continue
|
||||
}
|
||||
|
||||
go func(targetAddress string) {
|
||||
log.Println("[+]", "start connect host:["+targetAddress+"]")
|
||||
target, err := net.Dial("tcp", targetAddress)
|
||||
if err != nil {
|
||||
// temporarily unavailable, don't use fatal.
|
||||
log.Println("[x]", "connect target address ["+targetAddress+"] faild. retry in ", c.Timeout, "seconds. ")
|
||||
conn.Close()
|
||||
log.Println("[←]", "close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]")
|
||||
time.Sleep(time.Duration(c.Timeout) * time.Second)
|
||||
return
|
||||
}
|
||||
log.Println("[→]", "connect target address ["+targetAddress+"] success.")
|
||||
forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
|
||||
}(targetAddress)
|
||||
}
|
||||
}
|
157
src/mod/streamproxy/udpprox.go
Normal file
157
src/mod/streamproxy/udpprox.go
Normal file
@ -0,0 +1,157 @@
|
||||
package streamproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
UDP Proxy Module
|
||||
*/
|
||||
|
||||
// Information maintained for each client/server connection
|
||||
type udpClientServerConn struct {
|
||||
ClientAddr *net.UDPAddr // Address of the client
|
||||
ServerConn *net.UDPConn // UDP connection to server
|
||||
}
|
||||
|
||||
// Generate a new connection by opening a UDP connection to the server
|
||||
func createNewUDPConn(srvAddr, cliAddr *net.UDPAddr) *udpClientServerConn {
|
||||
conn := new(udpClientServerConn)
|
||||
conn.ClientAddr = cliAddr
|
||||
srvudp, err := net.DialUDP("udp", nil, srvAddr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
conn.ServerConn = srvudp
|
||||
return conn
|
||||
}
|
||||
|
||||
// Start listener, return inbound lisener and proxy target UDP address
|
||||
func initUDPConnections(listenAddr string, targetAddress string) (*net.UDPConn, *net.UDPAddr, error) {
|
||||
// Set up Proxy
|
||||
saddr, err := net.ResolveUDPAddr("udp", listenAddr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
inboundConn, err := net.ListenUDP("udp", saddr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
log.Println("[UDP] Proxy listening on " + listenAddr)
|
||||
|
||||
outboundConn, err := net.ResolveUDPAddr("udp", targetAddress)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return inboundConn, outboundConn, nil
|
||||
}
|
||||
|
||||
// Go routine which manages connection from server to single client
|
||||
func (c *ProxyRelayConfig) RunUDPConnectionRelay(conn *udpClientServerConn, lisenter *net.UDPConn) {
|
||||
var buffer [1500]byte
|
||||
for {
|
||||
// Read from server
|
||||
n, err := conn.ServerConn.Read(buffer[0:])
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Relay it to client
|
||||
_, err = lisenter.WriteToUDP(buffer[0:n], conn.ClientAddr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Close all connections that waiting for read from server
|
||||
func (c *ProxyRelayConfig) CloseAllUDPConnections() {
|
||||
c.udpClientMap.Range(func(clientAddr, clientServerConn interface{}) bool {
|
||||
conn := clientServerConn.(*udpClientServerConn)
|
||||
conn.ServerConn.Close()
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan bool) error {
|
||||
//By default the incoming listen Address is int
|
||||
//We need to add the loopback address into it
|
||||
if isValidPort(address1) {
|
||||
//Port number only. Missing the : in front
|
||||
address1 = ":" + address1
|
||||
}
|
||||
if strings.HasPrefix(address1, ":") {
|
||||
//Prepend 127.0.0.1 to the address
|
||||
address1 = "127.0.0.1" + address1
|
||||
}
|
||||
|
||||
lisener, targetAddr, err := initUDPConnections(address1, address2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
//Stop channel receiver
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
//Stop signal received
|
||||
//Stop server -> client forwarder
|
||||
c.CloseAllUDPConnections()
|
||||
//Stop client -> server forwarder
|
||||
//Force close, will terminate ReadFromUDP for inbound listener
|
||||
lisener.Close()
|
||||
return
|
||||
default:
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
var buffer [1500]byte
|
||||
for {
|
||||
n, cliaddr, err := lisener.ReadFromUDP(buffer[0:])
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
//Proxy stopped
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
c.aTobAccumulatedByteTransfer.Add(int64(n))
|
||||
saddr := cliaddr.String()
|
||||
rawConn, found := c.udpClientMap.Load(saddr)
|
||||
var conn *udpClientServerConn
|
||||
if !found {
|
||||
conn = createNewUDPConn(targetAddr, cliaddr)
|
||||
if conn == nil {
|
||||
continue
|
||||
}
|
||||
c.udpClientMap.Store(saddr, conn)
|
||||
log.Println("[UDP] Created new connection for client " + saddr)
|
||||
// Fire up routine to manage new connection
|
||||
go c.RunUDPConnectionRelay(conn, lisener)
|
||||
|
||||
} else {
|
||||
log.Println("[UDP] Found connection for client " + saddr)
|
||||
conn = rawConn.(*udpClientServerConn)
|
||||
}
|
||||
|
||||
// Relay to server
|
||||
_, err = conn.ServerConn.Write(buffer[0:n])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,341 +0,0 @@
|
||||
package tcpprox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func isValidIP(ip string) bool {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
return parsedIP != nil
|
||||
}
|
||||
|
||||
func isValidPort(port string) bool {
|
||||
portInt, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if portInt < 1 || portInt > 65535 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isReachable(target string) bool {
|
||||
timeout := time.Duration(2 * time.Second) // Set the timeout value as per your requirement
|
||||
conn, err := net.DialTimeout("tcp", target, timeout)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
func connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup, accumulator *int64) {
|
||||
io.Copy(conn1, conn2)
|
||||
conn1.Close()
|
||||
log.Println("[←]", "close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]")
|
||||
//conn2.Close()
|
||||
//log.Println("[←]", "close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]")
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
func forward(conn1 net.Conn, conn2 net.Conn, aTob *int64, bToa *int64) {
|
||||
log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String())
|
||||
var wg sync.WaitGroup
|
||||
// wait tow goroutines
|
||||
wg.Add(2)
|
||||
go connCopy(conn1, conn2, &wg, aTob)
|
||||
go connCopy(conn2, conn1, &wg, bToa)
|
||||
//blocking when the wg is locked
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func startListener(address string) (net.Listener, error) {
|
||||
log.Println("[+]", "try to start server on:["+address+"]")
|
||||
server, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return nil, errors.New("listen address [" + address + "] faild")
|
||||
}
|
||||
log.Println("[√]", "start listen at address:["+address+"]")
|
||||
return server, nil
|
||||
}
|
||||
|
||||
/*
|
||||
Config Functions
|
||||
*/
|
||||
|
||||
// Config validator
|
||||
func (c *ProxyRelayConfig) ValidateConfigs() error {
|
||||
if c.Mode == ProxyMode_Transport {
|
||||
//Port2Host: PortA int, PortB string
|
||||
if !isValidPort(c.PortA) {
|
||||
return errors.New("first address must be a valid port number")
|
||||
}
|
||||
|
||||
if !isReachable(c.PortB) {
|
||||
return errors.New("second address is unreachable")
|
||||
}
|
||||
return nil
|
||||
|
||||
} else if c.Mode == ProxyMode_Listen {
|
||||
//Port2Port: Both port are port number
|
||||
if !isValidPort(c.PortA) {
|
||||
return errors.New("first address is not a valid port number")
|
||||
}
|
||||
|
||||
if !isValidPort(c.PortB) {
|
||||
return errors.New("second address is not a valid port number")
|
||||
}
|
||||
|
||||
return nil
|
||||
} else if c.Mode == ProxyMode_Starter {
|
||||
//Host2Host: Both have to be hosts
|
||||
if !isReachable(c.PortA) {
|
||||
return errors.New("first address is unreachable")
|
||||
}
|
||||
|
||||
if !isReachable(c.PortB) {
|
||||
return errors.New("second address is unreachable")
|
||||
}
|
||||
|
||||
return nil
|
||||
} else {
|
||||
return errors.New("invalid mode given")
|
||||
}
|
||||
}
|
||||
|
||||
// Start a proxy if stopped
|
||||
func (c *ProxyRelayConfig) Start() error {
|
||||
if c.Running {
|
||||
return errors.New("proxy already running")
|
||||
}
|
||||
|
||||
// Create a stopChan to control the loop
|
||||
stopChan := make(chan bool)
|
||||
c.stopChan = stopChan
|
||||
|
||||
//Validate configs
|
||||
err := c.ValidateConfigs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Start the proxy service
|
||||
go func() {
|
||||
c.Running = true
|
||||
if c.Mode == ProxyMode_Transport {
|
||||
err = c.Port2host(c.PortA, c.PortB, stopChan)
|
||||
} else if c.Mode == ProxyMode_Listen {
|
||||
err = c.Port2port(c.PortA, c.PortB, stopChan)
|
||||
} else if c.Mode == ProxyMode_Starter {
|
||||
err = c.Host2host(c.PortA, c.PortB, stopChan)
|
||||
}
|
||||
if err != nil {
|
||||
c.Running = false
|
||||
log.Println("Error starting proxy service " + c.Name + "(" + c.UUID + "): " + err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
//Successfully spawned off the proxy routine
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop a running proxy if running
|
||||
func (c *ProxyRelayConfig) IsRunning() bool {
|
||||
return c.Running || c.stopChan != nil
|
||||
}
|
||||
|
||||
// Stop a running proxy if running
|
||||
func (c *ProxyRelayConfig) Stop() {
|
||||
if c.Running || c.stopChan != nil {
|
||||
c.stopChan <- true
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
c.stopChan = nil
|
||||
c.Running = false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Forwarder Functions
|
||||
*/
|
||||
|
||||
/*
|
||||
portA -> server
|
||||
portB -> server
|
||||
*/
|
||||
func (c *ProxyRelayConfig) Port2port(port1 string, port2 string, stopChan chan bool) error {
|
||||
//Trim the Prefix of : if exists
|
||||
listen1, err := startListener("0.0.0.0:" + port1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listen2, err := startListener("0.0.0.0:" + port2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("[√]", "listen port:", port1, "and", port2, "success. waiting for client...")
|
||||
c.Running = true
|
||||
|
||||
go func() {
|
||||
<-stopChan
|
||||
log.Println("[x]", "Received stop signal. Exiting Port to Port forwarder")
|
||||
c.Running = false
|
||||
listen1.Close()
|
||||
listen2.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
conn1, err := c.accept(listen1)
|
||||
if err != nil {
|
||||
if !c.Running {
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
conn2, err := c.accept(listen2)
|
||||
if err != nil {
|
||||
if !c.Running {
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if conn1 == nil || conn2 == nil {
|
||||
log.Println("[x]", "accept client faild. retry in ", c.Timeout, " seconds. ")
|
||||
time.Sleep(time.Duration(c.Timeout) * time.Second)
|
||||
continue
|
||||
}
|
||||
go forward(conn1, conn2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
portA -> server
|
||||
server -> portB
|
||||
*/
|
||||
func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, stopChan chan bool) error {
|
||||
server, err := startListener("0.0.0.0:" + allowPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Start stop handler
|
||||
go func() {
|
||||
<-stopChan
|
||||
log.Println("[x]", "Received stop signal. Exiting Port to Host forwarder")
|
||||
c.Running = false
|
||||
server.Close()
|
||||
}()
|
||||
|
||||
//Start blocking loop for accepting connections
|
||||
for {
|
||||
conn, err := c.accept(server)
|
||||
if conn == nil || err != nil {
|
||||
if !c.Running {
|
||||
//Terminate by stop chan. Exit listener loop
|
||||
return nil
|
||||
}
|
||||
|
||||
//Connection error. Retry
|
||||
continue
|
||||
}
|
||||
|
||||
go func(targetAddress string) {
|
||||
log.Println("[+]", "start connect host:["+targetAddress+"]")
|
||||
target, err := net.Dial("tcp", targetAddress)
|
||||
if err != nil {
|
||||
// temporarily unavailable, don't use fatal.
|
||||
log.Println("[x]", "connect target address ["+targetAddress+"] faild. retry in ", c.Timeout, "seconds. ")
|
||||
conn.Close()
|
||||
log.Println("[←]", "close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]")
|
||||
time.Sleep(time.Duration(c.Timeout) * time.Second)
|
||||
return
|
||||
}
|
||||
log.Println("[→]", "connect target address ["+targetAddress+"] success.")
|
||||
forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
|
||||
}(targetAddress)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
server -> portA
|
||||
server -> portB
|
||||
*/
|
||||
func (c *ProxyRelayConfig) Host2host(address1, address2 string, stopChan chan bool) error {
|
||||
c.Running = true
|
||||
go func() {
|
||||
<-stopChan
|
||||
log.Println("[x]", "Received stop signal. Exiting Host to Host forwarder")
|
||||
c.Running = false
|
||||
}()
|
||||
|
||||
for c.Running {
|
||||
log.Println("[+]", "try to connect host:["+address1+"] and ["+address2+"]")
|
||||
var host1, host2 net.Conn
|
||||
var err error
|
||||
for {
|
||||
d := net.Dialer{Timeout: time.Duration(c.Timeout)}
|
||||
host1, err = d.Dial("tcp", address1)
|
||||
if err == nil {
|
||||
log.Println("[→]", "connect ["+address1+"] success.")
|
||||
break
|
||||
} else {
|
||||
log.Println("[x]", "connect target address ["+address1+"] faild. retry in ", c.Timeout, " seconds. ")
|
||||
time.Sleep(time.Duration(c.Timeout) * time.Second)
|
||||
}
|
||||
|
||||
if !c.Running {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
for {
|
||||
d := net.Dialer{Timeout: time.Duration(c.Timeout)}
|
||||
host2, err = d.Dial("tcp", address2)
|
||||
if err == nil {
|
||||
log.Println("[→]", "connect ["+address2+"] success.")
|
||||
break
|
||||
} else {
|
||||
log.Println("[x]", "connect target address ["+address2+"] faild. retry in ", c.Timeout, " seconds. ")
|
||||
time.Sleep(time.Duration(c.Timeout) * time.Second)
|
||||
}
|
||||
|
||||
if !c.Running {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
go forward(host1, host2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,289 +0,0 @@
|
||||
package tcpprox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const timeout = 5
|
||||
|
||||
func main() {
|
||||
//log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
|
||||
log.SetFlags(log.Ldate | log.Lmicroseconds)
|
||||
|
||||
printWelcome()
|
||||
|
||||
args := os.Args
|
||||
argc := len(os.Args)
|
||||
if argc <= 2 {
|
||||
printHelp()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
//TODO:support UDP protocol
|
||||
|
||||
/*var logFileError error
|
||||
if argc > 5 && args[4] == "-log" {
|
||||
logPath := args[5] + "/" + time.Now().Format("2006_01_02_15_04_05") // "2006-01-02 15:04:05"
|
||||
logPath += args[1] + "-" + strings.Replace(args[2], ":", "_", -1) + "-" + args[3] + ".log"
|
||||
logPath = strings.Replace(logPath, `\`, "/", -1)
|
||||
logPath = strings.Replace(logPath, "//", "/", -1)
|
||||
logFile, logFileError = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE, 0666)
|
||||
if logFileError != nil {
|
||||
log.Fatalln("[x]", "log file path error.", logFileError.Error())
|
||||
}
|
||||
log.Println("[√]", "open test log file success. path:", logPath)
|
||||
}*/
|
||||
|
||||
switch args[1] {
|
||||
case "-listen":
|
||||
if argc < 3 {
|
||||
log.Fatalln(`-listen need two arguments, like "nb -listen 1997 2017".`)
|
||||
}
|
||||
port1 := checkPort(args[2])
|
||||
port2 := checkPort(args[3])
|
||||
log.Println("[√]", "start to listen port:", port1, "and port:", port2)
|
||||
port2port(port1, port2)
|
||||
break
|
||||
case "-tran":
|
||||
if argc < 3 {
|
||||
log.Fatalln(`-tran need two arguments, like "nb -tran 1997 192.168.1.2:3389".`)
|
||||
}
|
||||
port := checkPort(args[2])
|
||||
var remoteAddress string
|
||||
if checkIp(args[3]) {
|
||||
remoteAddress = args[3]
|
||||
}
|
||||
split := strings.SplitN(remoteAddress, ":", 2)
|
||||
log.Println("[√]", "start to transmit address:", remoteAddress, "to address:", split[0]+":"+port)
|
||||
port2host(port, remoteAddress)
|
||||
break
|
||||
case "-slave":
|
||||
if argc < 3 {
|
||||
log.Fatalln(`-slave need two arguments, like "nb -slave 127.0.0.1:3389 8.8.8.8:1997".`)
|
||||
}
|
||||
var address1, address2 string
|
||||
checkIp(args[2])
|
||||
if checkIp(args[2]) {
|
||||
address1 = args[2]
|
||||
}
|
||||
checkIp(args[3])
|
||||
if checkIp(args[3]) {
|
||||
address2 = args[3]
|
||||
}
|
||||
log.Println("[√]", "start to connect address:", address1, "and address:", address2)
|
||||
host2host(address1, address2)
|
||||
break
|
||||
default:
|
||||
printHelp()
|
||||
}
|
||||
}
|
||||
|
||||
func printWelcome() {
|
||||
fmt.Println("+----------------------------------------------------------------+")
|
||||
fmt.Println("| Welcome to use NATBypass Ver1.0.0 . |")
|
||||
fmt.Println("| Code by cw1997 at 2017-10-19 03:59:51 |")
|
||||
fmt.Println("| If you have some problem when you use the tool, |")
|
||||
fmt.Println("| please submit issue at : https://github.com/cw1997/NATBypass . |")
|
||||
fmt.Println("+----------------------------------------------------------------+")
|
||||
fmt.Println()
|
||||
// sleep one second because the fmt is not thread-safety.
|
||||
// if not to do this, fmt.Print will print after the log.Print.
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
func printHelp() {
|
||||
fmt.Println(`usage: "-listen port1 port2" example: "nb -listen 1997 2017" `)
|
||||
fmt.Println(` "-tran port1 ip:port2" example: "nb -tran 1997 192.168.1.2:3389" `)
|
||||
fmt.Println(` "-slave ip1:port1 ip2:port2" example: "nb -slave 127.0.0.1:3389 8.8.8.8:1997" `)
|
||||
fmt.Println(`============================================================`)
|
||||
fmt.Println(`optional argument: "-log logpath" . example: "nb -listen 1997 2017 -log d:/nb" `)
|
||||
fmt.Println(`log filename format: Y_m_d_H_i_s-agrs1-args2-args3.log`)
|
||||
fmt.Println(`============================================================`)
|
||||
fmt.Println(`if you want more help, please read "README.md". `)
|
||||
}
|
||||
|
||||
func checkPort(port string) string {
|
||||
PortNum, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
log.Fatalln("[x]", "port should be a number")
|
||||
}
|
||||
if PortNum < 1 || PortNum > 65535 {
|
||||
log.Fatalln("[x]", "port should be a number and the range is [1,65536)")
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
func checkIp(address string) bool {
|
||||
ipAndPort := strings.Split(address, ":")
|
||||
if len(ipAndPort) != 2 {
|
||||
log.Fatalln("[x]", "address error. should be a string like [ip:port]. ")
|
||||
}
|
||||
ip := ipAndPort[0]
|
||||
port := ipAndPort[1]
|
||||
checkPort(port)
|
||||
pattern := `^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$`
|
||||
ok, err := regexp.MatchString(pattern, ip)
|
||||
if err != nil || !ok {
|
||||
log.Fatalln("[x]", "ip error. ")
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func port2port(port1 string, port2 string) {
|
||||
listen1 := start_server("0.0.0.0:" + port1)
|
||||
listen2 := start_server("0.0.0.0:" + port2)
|
||||
log.Println("[√]", "listen port:", port1, "and", port2, "success. waiting for client...")
|
||||
for {
|
||||
conn1 := accept(listen1)
|
||||
conn2 := accept(listen2)
|
||||
if conn1 == nil || conn2 == nil {
|
||||
log.Println("[x]", "accept client faild. retry in ", timeout, " seconds. ")
|
||||
time.Sleep(timeout * time.Second)
|
||||
continue
|
||||
}
|
||||
forward(conn1, conn2)
|
||||
}
|
||||
}
|
||||
|
||||
func port2host(allowPort string, targetAddress string) {
|
||||
server := start_server("0.0.0.0:" + allowPort)
|
||||
for {
|
||||
conn := accept(server)
|
||||
if conn == nil {
|
||||
continue
|
||||
}
|
||||
//println(targetAddress)
|
||||
go func(targetAddress string) {
|
||||
log.Println("[+]", "start connect host:["+targetAddress+"]")
|
||||
target, err := net.Dial("tcp", targetAddress)
|
||||
if err != nil {
|
||||
// temporarily unavailable, don't use fatal.
|
||||
log.Println("[x]", "connect target address ["+targetAddress+"] faild. retry in ", timeout, "seconds. ")
|
||||
conn.Close()
|
||||
log.Println("[←]", "close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]")
|
||||
time.Sleep(timeout * time.Second)
|
||||
return
|
||||
}
|
||||
log.Println("[→]", "connect target address ["+targetAddress+"] success.")
|
||||
forward(target, conn)
|
||||
}(targetAddress)
|
||||
}
|
||||
}
|
||||
|
||||
func host2host(address1, address2 string) {
|
||||
for {
|
||||
log.Println("[+]", "try to connect host:["+address1+"] and ["+address2+"]")
|
||||
var host1, host2 net.Conn
|
||||
var err error
|
||||
for {
|
||||
host1, err = net.Dial("tcp", address1)
|
||||
if err == nil {
|
||||
log.Println("[→]", "connect ["+address1+"] success.")
|
||||
break
|
||||
} else {
|
||||
log.Println("[x]", "connect target address ["+address1+"] faild. retry in ", timeout, " seconds. ")
|
||||
time.Sleep(timeout * time.Second)
|
||||
}
|
||||
}
|
||||
for {
|
||||
host2, err = net.Dial("tcp", address2)
|
||||
if err == nil {
|
||||
log.Println("[→]", "connect ["+address2+"] success.")
|
||||
break
|
||||
} else {
|
||||
log.Println("[x]", "connect target address ["+address2+"] faild. retry in ", timeout, " seconds. ")
|
||||
time.Sleep(timeout * time.Second)
|
||||
}
|
||||
}
|
||||
forward(host1, host2)
|
||||
}
|
||||
}
|
||||
|
||||
func start_server(address string) net.Listener {
|
||||
log.Println("[+]", "try to start server on:["+address+"]")
|
||||
server, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
log.Fatalln("[x]", "listen address ["+address+"] faild.")
|
||||
}
|
||||
log.Println("[√]", "start listen at address:["+address+"]")
|
||||
return server
|
||||
/*defer server.Close()
|
||||
|
||||
for {
|
||||
conn, err := server.Accept()
|
||||
log.Println("accept a new client. remote address:[" + conn.RemoteAddr().String() +
|
||||
"], local address:[" + conn.LocalAddr().String() + "]")
|
||||
if err != nil {
|
||||
log.Println("accept a new client faild.", err.Error())
|
||||
continue
|
||||
}
|
||||
//go recvConnMsg(conn)
|
||||
}*/
|
||||
}
|
||||
|
||||
func accept(listener net.Listener) net.Conn {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
log.Println("[x]", "accept connect ["+conn.RemoteAddr().String()+"] faild.", err.Error())
|
||||
return nil
|
||||
}
|
||||
log.Println("[√]", "accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]")
|
||||
return conn
|
||||
}
|
||||
|
||||
func forward(conn1 net.Conn, conn2 net.Conn) {
|
||||
log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String())
|
||||
var wg sync.WaitGroup
|
||||
// wait tow goroutines
|
||||
wg.Add(2)
|
||||
go connCopy(conn1, conn2, &wg)
|
||||
go connCopy(conn2, conn1, &wg)
|
||||
//blocking when the wg is locked
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup) {
|
||||
//TODO:log, record the data from conn1 and conn2.
|
||||
logFile := openLog(conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String())
|
||||
if logFile != nil {
|
||||
w := io.MultiWriter(conn1, logFile)
|
||||
io.Copy(w, conn2)
|
||||
} else {
|
||||
io.Copy(conn1, conn2)
|
||||
}
|
||||
conn1.Close()
|
||||
log.Println("[←]", "close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]")
|
||||
//conn2.Close()
|
||||
//log.Println("[←]", "close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]")
|
||||
wg.Done()
|
||||
}
|
||||
func openLog(address1, address2, address3, address4 string) *os.File {
|
||||
args := os.Args
|
||||
argc := len(os.Args)
|
||||
var logFileError error
|
||||
var logFile *os.File
|
||||
if argc > 5 && args[4] == "-log" {
|
||||
address1 = strings.Replace(address1, ":", "_", -1)
|
||||
address2 = strings.Replace(address2, ":", "_", -1)
|
||||
address3 = strings.Replace(address3, ":", "_", -1)
|
||||
address4 = strings.Replace(address4, ":", "_", -1)
|
||||
timeStr := time.Now().Format("2006_01_02_15_04_05") // "2006-01-02 15:04:05"
|
||||
logPath := args[5] + "/" + timeStr + args[1] + "-" + address1 + "_" + address2 + "-" + address3 + "_" + address4 + ".log"
|
||||
logPath = strings.Replace(logPath, `\`, "/", -1)
|
||||
logPath = strings.Replace(logPath, "//", "/", -1)
|
||||
logFile, logFileError = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE, 0666)
|
||||
if logFileError != nil {
|
||||
log.Fatalln("[x]", "log file path error.", logFileError.Error())
|
||||
}
|
||||
log.Println("[√]", "open test log file success. path:", logPath)
|
||||
}
|
||||
return logFile
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
package tcpprox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
)
|
||||
|
||||
/*
|
||||
TCP Proxy
|
||||
|
||||
Forward port from one port to another
|
||||
Also accept active connection and passive
|
||||
connection
|
||||
*/
|
||||
|
||||
const (
|
||||
ProxyMode_Listen = 0
|
||||
ProxyMode_Transport = 1
|
||||
ProxyMode_Starter = 2
|
||||
)
|
||||
|
||||
type ProxyRelayOptions struct {
|
||||
Name string
|
||||
PortA string
|
||||
PortB string
|
||||
Timeout int
|
||||
Mode int
|
||||
}
|
||||
|
||||
type ProxyRelayConfig struct {
|
||||
UUID string //A UUIDv4 representing this config
|
||||
Name string //Name of the config
|
||||
Running bool //If the service is running
|
||||
PortA string //Ports A (config depends on mode)
|
||||
PortB string //Ports B (config depends on mode)
|
||||
Mode int //Operation Mode
|
||||
Timeout int //Timeout for connection in sec
|
||||
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
|
||||
AccessControlHandler func(net.Conn) bool
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
//Config and stores
|
||||
Options *Options
|
||||
Configs []*ProxyRelayConfig
|
||||
|
||||
//Realtime Statistics
|
||||
Connections int //currently connected connect counts
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
//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,
|
||||
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.New().String()
|
||||
thisConfig := ProxyRelayConfig{
|
||||
UUID: configUUID,
|
||||
Name: config.Name,
|
||||
Running: false,
|
||||
PortA: config.PortA,
|
||||
PortB: config.PortB,
|
||||
Mode: config.Mode,
|
||||
Timeout: config.Timeout,
|
||||
stopChan: nil,
|
||||
aTobAccumulatedByteTransfer: 0,
|
||||
bToaAccumulatedByteTransfer: 0,
|
||||
|
||||
parent: m,
|
||||
}
|
||||
m.Configs = append(m.Configs, &thisConfig)
|
||||
m.SaveConfigToDatabase()
|
||||
return configUUID
|
||||
}
|
||||
|
||||
func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayConfig, error) {
|
||||
// Find and return the config with the specified UUID
|
||||
for _, config := range m.Configs {
|
||||
if config.UUID == configUUID {
|
||||
return config, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("config not found")
|
||||
}
|
||||
|
||||
// Edit the config based on config UUID, leave empty for unchange fields
|
||||
func (m *Manager) EditConfig(configUUID string, newName string, newPortA string, newPortB string, newMode int, newTimeout int) error {
|
||||
// Find the config with the specified UUID
|
||||
foundConfig, err := m.GetConfigByUUID(configUUID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate and update the fields
|
||||
if newName != "" {
|
||||
foundConfig.Name = newName
|
||||
}
|
||||
if newPortA != "" {
|
||||
foundConfig.PortA = newPortA
|
||||
}
|
||||
if newPortB != "" {
|
||||
foundConfig.PortB = newPortB
|
||||
}
|
||||
if newMode != -1 {
|
||||
if newMode > 2 || newMode < 0 {
|
||||
return errors.New("invalid mode given")
|
||||
}
|
||||
foundConfig.Mode = newMode
|
||||
}
|
||||
if newTimeout != -1 {
|
||||
if newTimeout < 0 {
|
||||
return errors.New("invalid timeout value given")
|
||||
}
|
||||
foundConfig.Timeout = newTimeout
|
||||
}
|
||||
|
||||
/*
|
||||
err = foundConfig.ValidateConfigs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*/
|
||||
|
||||
m.SaveConfigToDatabase()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) RemoveConfig(configUUID string) error {
|
||||
// Find and remove the config with the specified UUID
|
||||
for i, config := range m.Configs {
|
||||
if config.UUID == configUUID {
|
||||
m.Configs = append(m.Configs[:i], m.Configs[i+1:]...)
|
||||
m.SaveConfigToDatabase()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("config not found")
|
||||
}
|
||||
|
||||
func (m *Manager) SaveConfigToDatabase() {
|
||||
m.Options.Database.Write("tcprox", "rules", m.Configs)
|
||||
}
|
@ -4,9 +4,11 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -217,11 +219,24 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
|
||||
}
|
||||
|
||||
func getWebsiteStatus(url string) (int, error) {
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
// Create a one-time use cookie jar to store cookies
|
||||
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
client := http.Client{
|
||||
Jar: jar,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header = http.Header{
|
||||
"User-Agent": {"zoraxy-uptime/1.1"},
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
//resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
//Try replace the http with https and vise versa
|
||||
rewriteURL := ""
|
||||
@ -231,7 +246,12 @@ func getWebsiteStatus(url string) (int, error) {
|
||||
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
|
||||
}
|
||||
|
||||
resp, err = client.Get(rewriteURL)
|
||||
req, _ := http.NewRequest("GET", rewriteURL, nil)
|
||||
req.Header = http.Header{
|
||||
"User-Agent": {"zoraxy-uptime/1.1"},
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
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
|
||||
|
@ -68,9 +68,9 @@ func PostBool(r *http.Request, key string) (bool, error) {
|
||||
|
||||
x = strings.TrimSpace(x)
|
||||
|
||||
if x == "1" || strings.ToLower(x) == "true" {
|
||||
if x == "1" || strings.ToLower(x) == "true" || strings.ToLower(x) == "on" {
|
||||
return true, nil
|
||||
} else if x == "0" || strings.ToLower(x) == "false" {
|
||||
} else if x == "0" || strings.ToLower(x) == "false" || strings.ToLower(x) == "off" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
@ -91,7 +91,7 @@ func handleToggleRedirectRegexpSupport(w http.ResponseWriter, r *http.Request) {
|
||||
//Update the current regex support rule enable state
|
||||
enableRegexSupport := strings.EqualFold(strings.TrimSpace(enabled), "true")
|
||||
redirectTable.AllowRegex = enableRegexSupport
|
||||
err = sysdb.Write("Redirect", "regex", enableRegexSupport)
|
||||
err = sysdb.Write("redirect", "regex", enableRegexSupport)
|
||||
|
||||
if enableRegexSupport {
|
||||
SystemWideLogger.PrintAndLog("redirect", "Regex redirect rule enabled", nil)
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
@ -94,6 +95,7 @@ func ReverseProxtInit() {
|
||||
GeodbStore: geodbStore,
|
||||
StatisticCollector: statisticCollector,
|
||||
WebDirectory: *staticWebServerRoot,
|
||||
AccessController: accessController,
|
||||
})
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err)
|
||||
@ -142,9 +144,12 @@ func ReverseProxtInit() {
|
||||
Interval: 300, //5 minutes
|
||||
MaxRecordsStore: 288, //1 day
|
||||
})
|
||||
|
||||
//Pass the pointer of this uptime monitor into the load balancer
|
||||
loadbalancer.Options.UptimeMonitor = uptimeMonitor
|
||||
|
||||
SystemWideLogger.Println("Uptime Monitor background service started")
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) {
|
||||
@ -194,6 +199,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
useTLS := (tls == "true")
|
||||
|
||||
//Bypass global TLS value / allow direct access from port 80?
|
||||
bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS")
|
||||
if bypassGlobalTLS == "" {
|
||||
bypassGlobalTLS = "false"
|
||||
@ -201,6 +207,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
useBypassGlobalTLS := bypassGlobalTLS == "true"
|
||||
|
||||
//Enable TLS validation?
|
||||
stv, _ := utils.PostPara(r, "tlsval")
|
||||
if stv == "" {
|
||||
stv = "false"
|
||||
@ -208,6 +215,17 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
skipTlsValidation := (stv == "true")
|
||||
|
||||
//Get access rule ID
|
||||
accessRuleID, _ := utils.PostPara(r, "access")
|
||||
if accessRuleID == "" {
|
||||
accessRuleID = "default"
|
||||
}
|
||||
if !accessController.AccessRuleExists(accessRuleID) {
|
||||
utils.SendErrorResponse(w, "invalid access rule ID selected")
|
||||
return
|
||||
}
|
||||
|
||||
// Require basic auth?
|
||||
rba, _ := utils.PostPara(r, "bauth")
|
||||
if rba == "" {
|
||||
rba = "false"
|
||||
@ -215,6 +233,29 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
requireBasicAuth := (rba == "true")
|
||||
|
||||
// Require Rate Limiting?
|
||||
requireRateLimit := false
|
||||
proxyRateLimit := 1000
|
||||
|
||||
requireRateLimit, err = utils.PostBool(r, "rate")
|
||||
if err != nil {
|
||||
requireRateLimit = false
|
||||
}
|
||||
if requireRateLimit {
|
||||
proxyRateLimit, err = utils.PostInt(r, "ratenum")
|
||||
if err != nil {
|
||||
proxyRateLimit = 0
|
||||
}
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rate limit number")
|
||||
return
|
||||
}
|
||||
if proxyRateLimit <= 0 {
|
||||
utils.SendErrorResponse(w, "rate limit number must be greater than 0")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Bypass WebSocket Origin Check
|
||||
strbpwsorg, _ := utils.PostPara(r, "bpwsorg")
|
||||
if strbpwsorg == "" {
|
||||
@ -254,19 +295,37 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if eptype == "host" {
|
||||
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "subdomain not defined")
|
||||
utils.SendErrorResponse(w, "hostname not defined")
|
||||
return
|
||||
}
|
||||
rootOrMatchingDomain = strings.TrimSpace(rootOrMatchingDomain)
|
||||
|
||||
//Check if it contains ",", if yes, split the remainings as alias
|
||||
aliasHostnames := []string{}
|
||||
if strings.Contains(rootOrMatchingDomain, ",") {
|
||||
matchingDomains := strings.Split(rootOrMatchingDomain, ",")
|
||||
if len(matchingDomains) > 1 {
|
||||
rootOrMatchingDomain = matchingDomains[0]
|
||||
for _, aliasHostname := range matchingDomains[1:] {
|
||||
//Filter out any space
|
||||
aliasHostnames = append(aliasHostnames, strings.TrimSpace(aliasHostname))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Generate a proxy endpoint object
|
||||
thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
|
||||
//I/O
|
||||
ProxyType: dynamicproxy.ProxyType_Host,
|
||||
RootOrMatchingDomain: rootOrMatchingDomain,
|
||||
MatchingDomainAlias: aliasHostnames,
|
||||
Domain: endpoint,
|
||||
//TLS
|
||||
RequireTLS: useTLS,
|
||||
BypassGlobalTLS: useBypassGlobalTLS,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
SkipWebSocketOriginCheck: bypassWebsocketOriginCheck,
|
||||
AccessFilterUUID: accessRuleID,
|
||||
//VDir
|
||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||
//Custom headers
|
||||
@ -277,6 +336,9 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
|
||||
DefaultSiteOption: 0,
|
||||
DefaultSiteValue: "",
|
||||
// Rate Limit
|
||||
RequireRateLimit: requireRateLimit,
|
||||
RateLimit: int64(proxyRateLimit),
|
||||
}
|
||||
|
||||
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint)
|
||||
@ -398,6 +460,26 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
requireBasicAuth := (rba == "true")
|
||||
|
||||
// Rate Limiting?
|
||||
rl, _ := utils.PostPara(r, "rate")
|
||||
if rl == "" {
|
||||
rl = "false"
|
||||
}
|
||||
requireRateLimit := (rl == "true")
|
||||
rlnum, _ := utils.PostPara(r, "ratenum")
|
||||
if rlnum == "" {
|
||||
rlnum = "0"
|
||||
}
|
||||
proxyRateLimit, err := strconv.ParseInt(rlnum, 10, 64)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid rate limit number")
|
||||
return
|
||||
}
|
||||
if proxyRateLimit <= 0 {
|
||||
utils.SendErrorResponse(w, "rate limit number must be greater than 0")
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass WebSocket Origin Check
|
||||
strbpwsorg, _ := utils.PostPara(r, "bpwsorg")
|
||||
if strbpwsorg == "" {
|
||||
@ -419,6 +501,8 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
|
||||
newProxyEndpoint.SkipCertValidations = skipTlsValidation
|
||||
newProxyEndpoint.RequireBasicAuth = requireBasicAuth
|
||||
newProxyEndpoint.RequireRateLimit = requireRateLimit
|
||||
newProxyEndpoint.RateLimit = proxyRateLimit
|
||||
newProxyEndpoint.SkipWebSocketOriginCheck = bypassWebsocketOriginCheck
|
||||
|
||||
//Prepare to replace the current routing rule
|
||||
@ -439,6 +523,62 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
|
||||
rootNameOrMatchingDomain, err := utils.PostPara(r, "ep")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ep given")
|
||||
return
|
||||
}
|
||||
|
||||
//No need to check for type as root (/) can be set to default route
|
||||
//and hence, you will not need alias
|
||||
|
||||
//Load the previous alias from current proxy rules
|
||||
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Target proxy config not found or could not be loaded")
|
||||
return
|
||||
}
|
||||
|
||||
newAliasJSON, err := utils.PostPara(r, "alias")
|
||||
if err != nil {
|
||||
//No new set of alias given
|
||||
utils.SendErrorResponse(w, "new alias not given")
|
||||
return
|
||||
}
|
||||
|
||||
//Write new alias to runtime and file
|
||||
newAlias := []string{}
|
||||
err = json.Unmarshal([]byte(newAliasJSON), &newAlias)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to parse new alias list", err)
|
||||
utils.SendErrorResponse(w, "Invalid alias list given")
|
||||
return
|
||||
}
|
||||
|
||||
//Set the current alias
|
||||
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
|
||||
newProxyEndpoint.MatchingDomainAlias = newAlias
|
||||
|
||||
// Prepare to replace the current routing rule
|
||||
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
targetProxyEntry.Remove()
|
||||
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
|
||||
|
||||
// Save it to file
|
||||
err = SaveReverseProxyConfig(newProxyEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Alias update failed")
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to save alias update", err)
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
ep, err := utils.GetPara(r, "ep")
|
||||
if err != nil {
|
||||
@ -740,6 +880,35 @@ func ReverseProxyToggleRuleSet(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func ReverseProxyListDetail(w http.ResponseWriter, r *http.Request) {
|
||||
eptype, err := utils.PostPara(r, "type") //Support root and host
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "type not defined")
|
||||
return
|
||||
}
|
||||
|
||||
if eptype == "host" {
|
||||
epname, err := utils.PostPara(r, "epname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "epname not defined")
|
||||
return
|
||||
}
|
||||
endpointRaw, ok := dynamicProxyRouter.ProxyEndpoints.Load(epname)
|
||||
if !ok {
|
||||
utils.SendErrorResponse(w, "proxy rule not found")
|
||||
return
|
||||
}
|
||||
targetEndpoint := dynamicproxy.CopyEndpoint(endpointRaw.(*dynamicproxy.ProxyEndpoint))
|
||||
js, _ := json.Marshal(targetEndpoint)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else if eptype == "root" {
|
||||
js, _ := json.Marshal(dynamicProxyRouter.Root)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "Invalid type given")
|
||||
}
|
||||
}
|
||||
|
||||
func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
||||
eptype, err := utils.PostPara(r, "type") //Support root and host
|
||||
if err != nil {
|
||||
@ -959,9 +1128,9 @@ func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Add a new header to the target endpoint
|
||||
func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
epType, err := utils.PostPara(r, "type")
|
||||
rewriteType, err := utils.PostPara(r, "type")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "endpoint type not defined")
|
||||
utils.SendErrorResponse(w, "rewriteType not defined")
|
||||
return
|
||||
}
|
||||
|
||||
@ -971,6 +1140,12 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
direction, err := utils.PostPara(r, "direction")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "HTTP modifiy direction not set")
|
||||
return
|
||||
}
|
||||
|
||||
name, err := utils.PostPara(r, "name")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "HTTP header name not set")
|
||||
@ -978,26 +1153,46 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
value, err := utils.PostPara(r, "value")
|
||||
if err != nil {
|
||||
if err != nil && rewriteType == "add" {
|
||||
utils.SendErrorResponse(w, "HTTP header value not set")
|
||||
return
|
||||
}
|
||||
|
||||
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
|
||||
if epType == "root" {
|
||||
targetProxyEndpoint = dynamicProxyRouter.Root
|
||||
} else {
|
||||
ep, err := dynamicProxyRouter.LoadProxy(domain)
|
||||
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "target endpoint not exists")
|
||||
return
|
||||
}
|
||||
|
||||
targetProxyEndpoint = ep
|
||||
//Create a Custom Header Defination type
|
||||
var rewriteDirection dynamicproxy.HeaderDirection
|
||||
if direction == "toOrigin" {
|
||||
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToUpstream
|
||||
} else if direction == "toClient" {
|
||||
rewriteDirection = dynamicproxy.HeaderDirection_ZoraxyToDownstream
|
||||
} else {
|
||||
//Unknown direction
|
||||
utils.SendErrorResponse(w, "header rewrite direction not supported")
|
||||
return
|
||||
}
|
||||
|
||||
isRemove := false
|
||||
if rewriteType == "remove" {
|
||||
isRemove = true
|
||||
}
|
||||
headerRewriteDefination := dynamicproxy.UserDefinedHeader{
|
||||
Key: name,
|
||||
Value: value,
|
||||
Direction: rewriteDirection,
|
||||
IsRemove: isRemove,
|
||||
}
|
||||
|
||||
//Create a new custom header object
|
||||
targetProxyEndpoint.AddUserDefinedHeader(name, value)
|
||||
err = targetProxyEndpoint.AddUserDefinedHeader(&headerRewriteDefination)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to add header rewrite rule: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Save it (no need reload as header are not handled by dpcore)
|
||||
err = SaveReverseProxyConfig(targetProxyEndpoint)
|
||||
@ -1011,12 +1206,6 @@ func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Remove a header from the target endpoint
|
||||
func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) {
|
||||
epType, err := utils.PostPara(r, "type")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "endpoint type not defined")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "domain or matching rule not defined")
|
||||
@ -1029,21 +1218,18 @@ func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
|
||||
if epType == "root" {
|
||||
targetProxyEndpoint = dynamicProxyRouter.Root
|
||||
} else {
|
||||
ep, err := dynamicProxyRouter.LoadProxy(domain)
|
||||
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "target endpoint not exists")
|
||||
return
|
||||
}
|
||||
|
||||
targetProxyEndpoint = ep
|
||||
err = targetProxyEndpoint.RemoveUserDefinedHeader(name)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to remove header rewrite rule: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
targetProxyEndpoint.RemoveUserDefinedHeader(name)
|
||||
|
||||
err = SaveReverseProxyConfig(targetProxyEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to save update")
|
||||
@ -1053,3 +1239,123 @@ func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendOK(w)
|
||||
|
||||
}
|
||||
|
||||
// Handle view or edit HSTS states
|
||||
func HandleHSTSState(w http.ResponseWriter, r *http.Request) {
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
domain, err = utils.GetPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "domain or matching rule not defined")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "target endpoint not exists")
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
//Return current HSTS enable state
|
||||
hstsAge := targetProxyEndpoint.HSTSMaxAge
|
||||
js, _ := json.Marshal(hstsAge)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
} else if r.Method == http.MethodPost {
|
||||
newMaxAge, err := utils.PostInt(r, "maxage")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "maxage not defeined")
|
||||
return
|
||||
}
|
||||
|
||||
if newMaxAge == 0 || newMaxAge >= 31536000 {
|
||||
targetProxyEndpoint.HSTSMaxAge = int64(newMaxAge)
|
||||
SaveReverseProxyConfig(targetProxyEndpoint)
|
||||
targetProxyEndpoint.UpdateToRuntime()
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid max age given")
|
||||
return
|
||||
}
|
||||
utils.SendOK(w)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
// HandlePermissionPolicy handle read or write to permission policy
|
||||
func HandlePermissionPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
domain, err = utils.GetPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "domain or matching rule not defined")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
targetProxyEndpoint, err := dynamicProxyRouter.LoadProxy(domain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "target endpoint not exists")
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
type CurrentPolicyState struct {
|
||||
PPEnabled bool
|
||||
CurrentPolicy *permissionpolicy.PermissionsPolicy
|
||||
}
|
||||
|
||||
currentPolicy := permissionpolicy.GetDefaultPermissionPolicy()
|
||||
if targetProxyEndpoint.PermissionPolicy != nil {
|
||||
currentPolicy = targetProxyEndpoint.PermissionPolicy
|
||||
}
|
||||
result := CurrentPolicyState{
|
||||
PPEnabled: targetProxyEndpoint.EnablePermissionPolicyHeader,
|
||||
CurrentPolicy: currentPolicy,
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(result)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
} else if r.Method == http.MethodPost {
|
||||
//Update the enable state of permission policy
|
||||
enableState, err := utils.PostBool(r, "enable")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid enable state given")
|
||||
return
|
||||
}
|
||||
|
||||
targetProxyEndpoint.EnablePermissionPolicyHeader = enableState
|
||||
SaveReverseProxyConfig(targetProxyEndpoint)
|
||||
targetProxyEndpoint.UpdateToRuntime()
|
||||
utils.SendOK(w)
|
||||
return
|
||||
} else if r.Method == http.MethodPut {
|
||||
//Store the new permission policy
|
||||
newPermissionPolicyJSONString, err := utils.PostPara(r, "pp")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "missing pp (permission policy) paramter")
|
||||
return
|
||||
}
|
||||
|
||||
//Parse the permission policy from JSON string
|
||||
newPermissionPolicy := permissionpolicy.GetDefaultPermissionPolicy()
|
||||
err = json.Unmarshal([]byte(newPermissionPolicyJSONString), &newPermissionPolicy)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "permission policy parse error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Save it to file
|
||||
targetProxyEndpoint.PermissionPolicy = newPermissionPolicy
|
||||
SaveReverseProxyConfig(targetProxyEndpoint)
|
||||
targetProxyEndpoint.UpdateToRuntime()
|
||||
utils.SendOK(w)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "405 - Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
59
src/start.go
59
src/start.go
@ -4,13 +4,17 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dockerux"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/forwardproxy"
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
@ -22,7 +26,7 @@ import (
|
||||
"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/streamproxy"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/webserv"
|
||||
)
|
||||
@ -72,15 +76,22 @@ 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 redirection rule table
|
||||
db.NewTable("redirect")
|
||||
redirectAllowRegexp := false
|
||||
db.Read("redirect", "regex", &redirectAllowRegexp)
|
||||
redirectTable, err = redirection.NewRuleTable("./conf/redirect", redirectAllowRegexp)
|
||||
redirectTable, err = redirection.NewRuleTable("./conf/redirect", redirectAllowRegexp, SystemWideLogger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
redirectTable.Logger = SystemWideLogger
|
||||
|
||||
//Create a geodb store
|
||||
geodbStore, err = geodb.NewGeoDb(sysdb, &geodb.StoreOptions{
|
||||
@ -91,19 +102,26 @@ func startupSequence() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a statistic collector
|
||||
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
||||
//Create a load balance route manager
|
||||
loadbalancer = loadbalance.NewRouteManager(&loadbalance.Options{
|
||||
Geodb: geodbStore,
|
||||
}, SystemWideLogger)
|
||||
|
||||
//Create the access controller
|
||||
accessController, err = access.NewAccessController(&access.Options{
|
||||
Database: sysdb,
|
||||
GeoDB: geodbStore,
|
||||
ConfigFolder: "./conf/access",
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a system wide logger
|
||||
l, err := logger.NewLogger("zr", "./log", *logOutputToFile)
|
||||
if err == nil {
|
||||
SystemWideLogger = l
|
||||
} else {
|
||||
//Create a statistic collector
|
||||
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
||||
Database: sysdb,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@ -149,8 +167,17 @@ func startupSequence() {
|
||||
if err != nil {
|
||||
portInt = 8000
|
||||
}
|
||||
|
||||
hostName := *mdnsName
|
||||
if hostName == "" {
|
||||
hostName = "zoraxy_" + nodeUUID
|
||||
} else {
|
||||
//Trim off the suffix
|
||||
hostName = strings.TrimSuffix(hostName, ".local")
|
||||
}
|
||||
|
||||
mdnsScanner, err = mdns.NewMDNS(mdns.NetworkHost{
|
||||
HostName: "zoraxy_" + nodeUUID,
|
||||
HostName: hostName,
|
||||
Port: portInt,
|
||||
Domain: "zoraxy.arozos.com",
|
||||
Model: "Network Gateway",
|
||||
@ -209,9 +236,9 @@ func startupSequence() {
|
||||
webSshManager = sshprox.NewSSHProxyManager()
|
||||
|
||||
//Create TCP Proxy Manager
|
||||
tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{
|
||||
streamProxyManager = streamproxy.NewStreamProxy(&streamproxy.Options{
|
||||
Database: sysdb,
|
||||
AccessControlHandler: geodbStore.AllowConnectionAccess,
|
||||
AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
|
||||
})
|
||||
|
||||
//Create WoL MAC storage table
|
||||
@ -249,6 +276,12 @@ func startupSequence() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
/* Docker UX Optimizer */
|
||||
if runtime.GOOS == "windows" && *runningInDocker {
|
||||
SystemWideLogger.PrintAndLog("WARNING", "Invalid start flag combination: docker=true && runtime.GOOS == windows. Running in docker UX development mode.", nil)
|
||||
}
|
||||
DockerUXOptimizer = dockerux.NewDockerOptimizer(*runningInDocker, SystemWideLogger)
|
||||
|
||||
}
|
||||
|
||||
// This sequence start after everything is initialized
|
||||
|
2
src/start.sh
Normal file
2
src/start.sh
Normal file
@ -0,0 +1,2 @@
|
||||
#/bin/bash
|
||||
sudo ./zoraxy
|
@ -1,9 +1,44 @@
|
||||
<style>
|
||||
#currentEditingAccessRule {
|
||||
background: var(--theme_background);
|
||||
color: white;
|
||||
border-radius: 1em;
|
||||
font-weight: bolder;
|
||||
}
|
||||
</style>
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Access Control</h2>
|
||||
<p>Setup blacklist or whitelist based on estimated IP geographic location or IP address</p>
|
||||
<p>Setup blacklist or whitelist based on estimated IP geographic location or IP address. <br>
|
||||
To apply access control to a proxy hosts, create a "Access Rule" below and apply it to the proxy hosts in the HTTP Proxy tab.</p>
|
||||
</div>
|
||||
<div id="currentEditingAccessRule" class="ui basic segment">
|
||||
Select an access rule to start editing
|
||||
</div>
|
||||
<div class="ui stackable grid">
|
||||
<div class="four wide column">
|
||||
<h4>Select a rule to configure</h4>
|
||||
<p>All proxy hosts uses the "default" access rule if no rule is set.</p>
|
||||
<div class="ui selection fluid dropdown" id="accessRuleSelector">
|
||||
<input type="hidden" name="targetAccessRule" value="default" onchange="handleSelectedAccessRuleChange(this.value);">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text"></div>
|
||||
<div class="menu" id="accessRuleList">
|
||||
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<p id="accessRuleDesc"></p>
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<div style="width: 100%;" align="center">
|
||||
<button class="ui basic circular button" onclick="openAccessRuleEditor();" title="Edit Access Rules"><i class="ui edit icon"></i> Access Rule Editor</button>
|
||||
<button class="ui basic circular icon button" onclick="reloadAccessRulesWithFeedback();"><i class="ui green refresh icon"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="twelve wide column">
|
||||
<!-- Black / White list menu-->
|
||||
<div class="ui top attached tabular menu">
|
||||
<a class="accesscontrol item active" data-tab="tab_blacklist"><i class="ui red circle times icon"></i> Blacklist</a>
|
||||
<a class="accesscontrol item" data-tab="tab_whitelist"><i class="ui green check circle icon"></i> Whitelist</a>
|
||||
@ -30,7 +65,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Select Country</label>
|
||||
<div id="countrySelector" class="ui fluid search selection dropdown">
|
||||
<div id="countrySelector" class="ui fluid search multiple selection dropdown">
|
||||
<input type="hidden" name="country">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">Select Country</div>
|
||||
@ -347,7 +382,7 @@
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Select Country</label>
|
||||
<div id="countrySelectorWhitelist" class="ui fluid search selection dropdown">
|
||||
<div id="countrySelectorWhitelist" class="ui fluid search multiple selection dropdown">
|
||||
<input type="hidden" name="country">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">Select Country</div>
|
||||
@ -667,26 +702,144 @@
|
||||
</table>
|
||||
<div class="pagination"></div>
|
||||
</div>
|
||||
<div class="ui yellow message">
|
||||
<i class="info circle icon"></i> Access checking and validation will slightly increase proxy latency. <br>
|
||||
Enabling this feature on servers with low end hardware is not recommended.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
/*
|
||||
Access Control
|
||||
*/
|
||||
var currentEditingAccessRule = "default"; //ID of the current editing access rule
|
||||
var currentListOfAccessRules = [];
|
||||
|
||||
$(".dropdown").dropdown();
|
||||
$('.menu .accesscontrol.item').tab();
|
||||
$('.menu .accesscontrol.item').addClass("activated");
|
||||
|
||||
/* Access Rule Selector */
|
||||
function openAccessRuleEditor(){
|
||||
showSideWrapper('snippet/accessRuleEditor.html');
|
||||
}
|
||||
|
||||
function reloadAccessRulesWithFeedback(){
|
||||
reloadAccessRules(function(){
|
||||
msgbox("Access Rules Reloaded", true);
|
||||
});
|
||||
}
|
||||
|
||||
function reloadAccessRules(callback=undefined){
|
||||
initAccessRuleList(function(){
|
||||
//Check if the previous access rule is still there
|
||||
let stillExists = false;
|
||||
for (var i = 0; i < currentListOfAccessRules.length; i++){
|
||||
let thisAccessRule = currentListOfAccessRules[i];
|
||||
if (thisAccessRule.ID == currentEditingAccessRule){
|
||||
stillExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stillExists){
|
||||
handleSelectedAccessRuleChange(currentEditingAccessRule);
|
||||
$("#accessRuleSelector").dropdown("set selected", currentEditingAccessRule);
|
||||
}else{
|
||||
handleSelectedAccessRuleChange("default");
|
||||
$("#accessRuleSelector").dropdown("set selected", "default");
|
||||
}
|
||||
|
||||
if (callback != undefined){
|
||||
callback();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function initAccessRuleList(callback=undefined){
|
||||
$.get("/api/access/list", function(data){
|
||||
if (data.error == undefined){
|
||||
$("#accessRuleList").html("");
|
||||
data.forEach(function(rule){
|
||||
let icon = `<i class="ui grey filter icon"></i>`;
|
||||
if (rule.ID == "default"){
|
||||
icon = `<i class="ui yellow star icon"></i>`;
|
||||
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
|
||||
//This is a blacklist filter
|
||||
icon = `<i class="ui red filter icon"></i>`;
|
||||
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
|
||||
//This is a whitelist filter
|
||||
icon = `<i class="ui green filter icon"></i>`;
|
||||
}
|
||||
$("#accessRuleList").append(`<div class="item" data-value="${rule.ID}">${icon} ${rule.Name}</div>`);
|
||||
});
|
||||
currentListOfAccessRules = data;
|
||||
$(".dropdown").dropdown();
|
||||
if (callback != undefined){
|
||||
callback();
|
||||
}
|
||||
}else{
|
||||
msgbox("Access rule load failed: " + data.error, false);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//Load default when the page is first loaded
|
||||
initAccessRuleList(function(){
|
||||
$("#accessRuleSelector").dropdown("set selected", "default");
|
||||
for (var i = 0; i < currentListOfAccessRules.length; i++){
|
||||
let thisAccessRule = currentListOfAccessRules[i];
|
||||
if (thisAccessRule.ID == "default"){
|
||||
$("#currentEditingAccessRule").html(`Editing Access Rule: ${thisAccessRule.Name} <span style="font-weight: 300;">[${thisAccessRule.ID}]</span>`);
|
||||
$("#accessRuleDesc").text(thisAccessRule.Desc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
initBlackWhitelistTables();
|
||||
});
|
||||
|
||||
//On dropdown change on access rule selection
|
||||
function handleSelectedAccessRuleChange(newRuleUUID){
|
||||
currentEditingAccessRule = newRuleUUID;
|
||||
//Load the name and desc of this acess rule
|
||||
let name = "";
|
||||
let desc = "";
|
||||
for (var i = 0; i < currentListOfAccessRules.length; i++){
|
||||
let thisAccessRule = currentListOfAccessRules[i];
|
||||
if (thisAccessRule.ID == newRuleUUID){
|
||||
name = thisAccessRule.Name;
|
||||
desc = thisAccessRule.Desc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$("#currentEditingAccessRule").html(`Editing Access Rule: ${name} <span style="font-weight: 300;">[${newRuleUUID}]</span>`);
|
||||
$("#accessRuleDesc").text(desc);
|
||||
|
||||
initBlackWhitelistTables();
|
||||
}
|
||||
|
||||
//Load all the black white lists
|
||||
function initBlackWhitelistTables(){
|
||||
//Remove event listener on both toggle buttons
|
||||
$("#enableWhitelist").off("change");
|
||||
$("#enableBlacklist").off("change");
|
||||
|
||||
//Initialize the button states
|
||||
initBlacklistEnableState();
|
||||
initWhitelistEnableState();
|
||||
|
||||
//Update the lists
|
||||
initBannedCountryList();
|
||||
initIpBanTable()
|
||||
initWhitelistCountryList();
|
||||
initIpWhitelistTable();
|
||||
}
|
||||
|
||||
/*
|
||||
Table Init
|
||||
*/
|
||||
|
||||
//Blacklist country table
|
||||
function initBannedCountryList(){
|
||||
$.get("/api/blacklist/list?type=country", function(data) {
|
||||
$.get("/api/blacklist/list?type=country&id=" + currentEditingAccessRule, function(data) {
|
||||
let bannedListHtml = '';
|
||||
data.forEach((countryCode) => {
|
||||
bannedListHtml += `
|
||||
@ -711,11 +864,10 @@
|
||||
|
||||
});
|
||||
}
|
||||
initBannedCountryList();
|
||||
|
||||
//Blacklist ip table
|
||||
function initIpBanTable(){
|
||||
$.get('/api/blacklist/list?type=ip', function(data) {
|
||||
$.get('/api/blacklist/list?type=ip&id=' + currentEditingAccessRule, function(data) {
|
||||
$('#blacklistIpTable').html("");
|
||||
if (data.length === 0) {
|
||||
$('#blacklistIpTable').append(`
|
||||
@ -743,28 +895,30 @@
|
||||
initBlacklistQuickBanTable();
|
||||
});
|
||||
}
|
||||
initIpBanTable();
|
||||
|
||||
|
||||
//Init blacklist state
|
||||
function initBlacklistEnableState(){
|
||||
$.get('/api/blacklist/enable', function(data){
|
||||
$.get('/api/blacklist/enable?id=' + currentEditingAccessRule, function(data){
|
||||
if (data == true){
|
||||
$('#enableBlacklist').parent().checkbox("set checked");
|
||||
$("#ipTable").removeClass("disabled");
|
||||
}else{
|
||||
$('#enableBlacklist').parent().checkbox("set unchecked");
|
||||
$("#ipTable").addClass("disabled");
|
||||
}
|
||||
|
||||
//Register on change event
|
||||
$("#enableBlacklist").on("change", function(){
|
||||
$("#enableBlacklist").off("change").on("change", function(){
|
||||
enableBlacklist();
|
||||
})
|
||||
});
|
||||
}
|
||||
initBlacklistEnableState();
|
||||
|
||||
|
||||
//Whitelist country table
|
||||
function initWhitelistCountryList(){
|
||||
$.get("/api/whitelist/list?type=country", function(data) {
|
||||
$.get("/api/whitelist/list?type=country&id=" + currentEditingAccessRule, function(data) {
|
||||
let bannedListHtml = '';
|
||||
data.forEach((countryWhitelistEntry) => {
|
||||
let countryCode = countryWhitelistEntry.CC;
|
||||
@ -790,11 +944,11 @@
|
||||
|
||||
});
|
||||
}
|
||||
initWhitelistCountryList();
|
||||
|
||||
|
||||
//Whitelist ip table
|
||||
function initIpWhitelistTable(){
|
||||
$.get('/api/whitelist/list?type=ip', function(data) {
|
||||
$.get('/api/whitelist/list?type=ip&id=' + currentEditingAccessRule, function(data) {
|
||||
$('#whitelistIpTable').html("");
|
||||
if (data.length === 0) {
|
||||
$('#whitelistIpTable').append(`
|
||||
@ -823,22 +977,23 @@
|
||||
|
||||
});
|
||||
}
|
||||
initIpWhitelistTable();
|
||||
|
||||
|
||||
//Init whitelist state
|
||||
function initWhitelistEnableState(){
|
||||
$.get('/api/whitelist/enable', function(data){
|
||||
$.get('/api/whitelist/enable?id=' + currentEditingAccessRule, function(data){
|
||||
if (data == true){
|
||||
$('#enableWhitelist').parent().checkbox("set checked");
|
||||
}else{
|
||||
$('#enableWhitelist').parent().checkbox("set unchecked");
|
||||
}
|
||||
|
||||
//Register on change event
|
||||
$("#enableWhitelist").on("change", function(){
|
||||
$("#enableWhitelist").off("change").on("change", function(){
|
||||
enableWhitelist();
|
||||
})
|
||||
});
|
||||
}
|
||||
initWhitelistEnableState();
|
||||
|
||||
/*
|
||||
Blacklist API Calls
|
||||
@ -848,7 +1003,7 @@
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/api/blacklist/enable',
|
||||
data: { enable: isChecked },
|
||||
data: { enable: isChecked, id: currentEditingAccessRule},
|
||||
success: function(data){
|
||||
if (isChecked){
|
||||
$("#ipTable").removeClass("disabled");
|
||||
@ -863,16 +1018,39 @@
|
||||
|
||||
function addCountryToBlacklist() {
|
||||
var countryCode = $("#countrySelector").dropdown("get value").toLowerCase();
|
||||
$('#countrySelector').dropdown('clear');
|
||||
let ccs = [countryCode];
|
||||
if (countryCode.includes(",")){
|
||||
//Multiple country codes selected
|
||||
//Usually just a few countries a for loop will get the job done
|
||||
ccs = countryCode.split(",");
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
for(var i = 0; i < ccs.length; i++){
|
||||
let thisCountryCode = ccs[i];
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/blacklist/country/add",
|
||||
data: { cc: countryCode },
|
||||
data: { cc: thisCountryCode, id: currentEditingAccessRule},
|
||||
success: function(response) {
|
||||
if (response.error != undefined){
|
||||
alert(response.error);
|
||||
msgbox(response.error, false);
|
||||
}
|
||||
|
||||
if (counter == (ccs.length - 1)){
|
||||
//Last item
|
||||
setTimeout(function(){
|
||||
initBannedCountryList();
|
||||
if (ccs.length == 1){
|
||||
//Single country
|
||||
msgbox(`Added ${getCountryName(ccs[0])} to blacklist`);
|
||||
}else{
|
||||
msgbox(ccs.length + " countries added to blacklist");
|
||||
}
|
||||
|
||||
}, (ccs.length==1)?0:100);
|
||||
}
|
||||
counter++;
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
// handle error response
|
||||
@ -880,16 +1058,23 @@
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$('#countrySelector').dropdown('clear');
|
||||
|
||||
}
|
||||
|
||||
function removeFromBannedList(countryCode){
|
||||
if (confirm("Confirm removing " + getCountryName(countryCode) + " from blacklist?")){
|
||||
countryCode = countryCode.toLowerCase();
|
||||
let countryName = getCountryName(countryCode);
|
||||
$.ajax({
|
||||
url: "/api/blacklist/country/remove",
|
||||
method: "POST",
|
||||
data: { cc: countryCode },
|
||||
data: { cc: countryCode, id: currentEditingAccessRule},
|
||||
success: function(response) {
|
||||
if (response.error != undefined){
|
||||
alert(response.error);
|
||||
msgbox(response.error, false);
|
||||
}else{
|
||||
msgbox(countryName + " removed from blacklist");
|
||||
}
|
||||
initBannedCountryList();
|
||||
},
|
||||
@ -899,7 +1084,6 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addIpBlacklist(){
|
||||
let targetIp = $("#ipAddressInput").val().trim();
|
||||
@ -916,7 +1100,7 @@
|
||||
$.ajax({
|
||||
url: "/api/blacklist/ip/add",
|
||||
type: "POST",
|
||||
data: {ip: targetIp.toLowerCase()},
|
||||
data: {ip: targetIp.toLowerCase(), id: currentEditingAccessRule},
|
||||
success: function(response) {
|
||||
if (response.error !== undefined) {
|
||||
msgbox(response.error, false, 6000);
|
||||
@ -938,7 +1122,7 @@
|
||||
$.ajax({
|
||||
url: "/api/blacklist/ip/remove",
|
||||
type: "POST",
|
||||
data: {ip: ipaddr.toLowerCase()},
|
||||
data: {ip: ipaddr.toLowerCase(), id: currentEditingAccessRule},
|
||||
success: function(response) {
|
||||
if (response.error !== undefined) {
|
||||
msgbox(response.error, false, 6000);
|
||||
@ -962,7 +1146,7 @@
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/api/whitelist/enable',
|
||||
data: { enable: isChecked },
|
||||
data: { enable: isChecked , id: currentEditingAccessRule},
|
||||
success: function(data){
|
||||
$(".toggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
|
||||
}
|
||||
@ -971,16 +1155,37 @@
|
||||
|
||||
function addCountryToWhitelist() {
|
||||
var countryCode = $("#countrySelectorWhitelist").dropdown("get value").toLowerCase();
|
||||
$('#countrySelectorWhitelist').dropdown('clear');
|
||||
let ccs = [countryCode];
|
||||
if (countryCode.includes(",")){
|
||||
//Multiple country codes selected
|
||||
//Usually just a few countries a for loop will get the job done
|
||||
ccs = countryCode.split(",");
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
for(var i = 0; i < ccs.length; i++){
|
||||
let thisCountryCode = ccs[i];
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/whitelist/country/add",
|
||||
data: { cc: countryCode },
|
||||
data: { cc: thisCountryCode , id: currentEditingAccessRule},
|
||||
success: function(response) {
|
||||
if (response.error != undefined){
|
||||
alert(response.error);
|
||||
msgbox(response.error, false);
|
||||
}
|
||||
|
||||
if (counter == (ccs.length - 1)){
|
||||
setTimeout(function(){
|
||||
initWhitelistCountryList();
|
||||
if (ccs.length == 1){
|
||||
//Single country
|
||||
msgbox(`Added ${getCountryName(ccs[0])} to whitelist`);
|
||||
}else{
|
||||
msgbox(ccs.length + " countries added to whitelist");
|
||||
}
|
||||
}, (ccs.length==1)?0:100);
|
||||
}
|
||||
counter++;
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
// handle error response
|
||||
@ -988,16 +1193,19 @@
|
||||
});
|
||||
}
|
||||
|
||||
$('#countrySelectorWhitelist').dropdown('clear');
|
||||
}
|
||||
|
||||
function removeFromWhiteList(countryCode){
|
||||
if (confirm("Confirm removing " + getCountryName(countryCode) + " from whitelist?")){
|
||||
countryCode = countryCode.toLowerCase();
|
||||
$.ajax({
|
||||
url: "/api/whitelist/country/remove",
|
||||
method: "POST",
|
||||
data: { cc: countryCode },
|
||||
data: { cc: countryCode , id: currentEditingAccessRule},
|
||||
success: function(response) {
|
||||
if (response.error != undefined){
|
||||
alert(response.error);
|
||||
msgbox(response.error, false);
|
||||
}
|
||||
initWhitelistCountryList();
|
||||
},
|
||||
@ -1025,7 +1233,7 @@
|
||||
$.ajax({
|
||||
url: "/api/whitelist/ip/add",
|
||||
type: "POST",
|
||||
data: {ip: targetIp.toLowerCase(), "comment": remarks},
|
||||
data: {ip: targetIp.toLowerCase(), "comment": remarks, id: currentEditingAccessRule},
|
||||
success: function(response) {
|
||||
if (response.error !== undefined) {
|
||||
msgbox(response.error, false, 6000);
|
||||
@ -1048,7 +1256,7 @@
|
||||
$.ajax({
|
||||
url: "/api/whitelist/ip/remove",
|
||||
type: "POST",
|
||||
data: {ip: ipaddr.toLowerCase()},
|
||||
data: {ip: ipaddr.toLowerCase(), id: currentEditingAccessRule},
|
||||
success: function(response) {
|
||||
if (response.error !== undefined) {
|
||||
msgbox(response.error, false, 6000);
|
||||
|
@ -66,6 +66,7 @@
|
||||
<tr><th>Domain</th>
|
||||
<th>Last Update</th>
|
||||
<th>Expire At</th>
|
||||
<th>DNS Challenge</th>
|
||||
<th class="no-sort">Renew</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
</tr></thead>
|
||||
@ -147,7 +148,7 @@
|
||||
|
||||
|
||||
//Renew certificate by button press
|
||||
function renewCertificate(domain, btn=undefined){
|
||||
function renewCertificate(domain, dns, btn=undefined){
|
||||
let defaultCA = $("#defaultCA").dropdown("get value");
|
||||
if (defaultCA.trim() == ""){
|
||||
defaultCA = "Let's Encrypt";
|
||||
@ -160,7 +161,7 @@
|
||||
$(btn).addClass('disabled');
|
||||
$(btn).html(`<i class="ui loading spinner icon"></i>`);
|
||||
}
|
||||
obtainCertificate(domain, defaultCA.trim(), function(succ){
|
||||
obtainCertificate(domain, dns, defaultCA.trim(), function(succ){
|
||||
if (btn != undefined){
|
||||
$(btn).removeClass('disabled');
|
||||
if (succ){
|
||||
@ -181,7 +182,7 @@
|
||||
*/
|
||||
|
||||
// Obtain certificate from API, only support one domain
|
||||
function obtainCertificate(domains, usingCa = "Let's Encrypt", callback=undefined) {
|
||||
function obtainCertificate(domains, dns, usingCa = "Let's Encrypt", callback=undefined) {
|
||||
//Load the ACME email from server side
|
||||
let acmeEmail = "";
|
||||
$.get("/api/acme/autoRenew/email", function(data){
|
||||
@ -213,6 +214,8 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
//Filename cannot contain wildcards, and wildcards are possible with DNS challenges
|
||||
filename = filename.replace("*", "_");
|
||||
|
||||
$.ajax({
|
||||
url: "/api/acme/obtainCert",
|
||||
@ -222,6 +225,7 @@
|
||||
filename: filename,
|
||||
email: email,
|
||||
ca: usingCa,
|
||||
dns: dns
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.error) {
|
||||
@ -237,7 +241,7 @@
|
||||
msgbox("Certificate installed successfully");
|
||||
|
||||
if (callback != undefined){
|
||||
callback(false);
|
||||
callback(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -357,7 +361,8 @@
|
||||
<td>${entry.Domain}</td>
|
||||
<td>${entry.LastModifiedDate}</td>
|
||||
<td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
|
||||
<td><button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entry.Domain}', this);"><i class="ui green refresh icon"></i></button></td>
|
||||
<td><i class="${entry.UseDNS?"green check": "red times"} circle outline icon"></i></td>
|
||||
<td><button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entry.Domain}', '${entry.UseDNS}', this);"><i class="ui green refresh icon"></i></button></td>
|
||||
<td><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>`);
|
||||
});
|
||||
|
@ -7,16 +7,20 @@
|
||||
#httpProxyList .ui.toggle.checkbox input:checked ~ label::before{
|
||||
background-color: #00ca52 !important;
|
||||
}
|
||||
|
||||
.subdEntry td:not(.ignoremw){
|
||||
min-width: 200px;
|
||||
}
|
||||
</style>
|
||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em; min-height: 300px;">
|
||||
<table class="ui celled sortable unstackable compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Host</th>
|
||||
<th>Destination</th>
|
||||
<th>Virtual Directory</th>
|
||||
<th>Basic Auth</th>
|
||||
<th class="no-sort" style="min-width:100px;">Actions</th>
|
||||
<th style="max-width: 300px;">Advanced Settings</th>
|
||||
<th class="no-sort" style="min-width:150px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="httpProxyList">
|
||||
@ -30,6 +34,8 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
/* List all proxy endpoints */
|
||||
function listProxyEndpoints(){
|
||||
$.get("/api/proxy/list?type=host", function(data){
|
||||
$("#httpProxyList").html(``);
|
||||
@ -42,6 +48,8 @@
|
||||
<td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td>
|
||||
</tr>`);
|
||||
}else{
|
||||
//Sort by RootOrMatchingDomain field
|
||||
data.sort((a,b) => (a.RootOrMatchingDomain > b.RootOrMatchingDomain) ? 1 : ((b.RootOrMatchingDomain > a.RootOrMatchingDomain) ? -1 : 0))
|
||||
data.forEach(subd => {
|
||||
let tlsIcon = "";
|
||||
let subdData = encodeURIComponent(JSON.stringify(subd));
|
||||
@ -70,20 +78,39 @@
|
||||
vdList += `</div>`;
|
||||
|
||||
if (subd.VirtualDirectories.length == 0){
|
||||
vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;"><i class="check icon"></i> No Virtual Directory</small>`;
|
||||
vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Virtual Directory</small>`;
|
||||
}
|
||||
|
||||
var enableChecked = "checked";
|
||||
let enableChecked = "checked";
|
||||
if (subd.Disabled){
|
||||
enableChecked = "";
|
||||
}
|
||||
|
||||
let aliasDomains = ``;
|
||||
if (subd.MatchingDomainAlias != undefined && subd.MatchingDomainAlias.length > 0){
|
||||
aliasDomains = `<small class="aliasDomains" eptuuid="${subd.RootOrMatchingDomain}" style="color: #636363;">Alias: `;
|
||||
subd.MatchingDomainAlias.forEach(alias => {
|
||||
aliasDomains += `<a href="//${alias}" target="_blank">${alias}</a>, `;
|
||||
});
|
||||
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
|
||||
aliasDomains += `</small><br>`;
|
||||
}
|
||||
|
||||
$("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
|
||||
<td data-label="" editable="true" datatype="inbound"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}</td>
|
||||
<td data-label="" editable="true" datatype="inbound">
|
||||
<a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}<br>
|
||||
${aliasDomains}
|
||||
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
|
||||
</td>
|
||||
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
|
||||
<td data-label="" editable="true" datatype="vdir">${vdList}</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="">
|
||||
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
|
||||
${subd.RequireBasicAuth?`<i class="ui green check icon"></i> Basic Auth`:``}
|
||||
${subd.RequireBasicAuth && subd.RequireRateLimit?"<br>":""}
|
||||
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
|
||||
${!subd.RequireBasicAuth && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
|
||||
</td>
|
||||
<td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
|
||||
<div class="ui toggle tiny fitted checkbox" style="margin-bottom: -0.5em; margin-right: 0.4em;" title="Enable / Disable Rule">
|
||||
<input type="checkbox" class="enableToggle" name="active" ${enableChecked} eptuuid="${subd.RootOrMatchingDomain}" onchange="handleProxyRuleToggle(this);">
|
||||
<label></label>
|
||||
@ -94,9 +121,87 @@
|
||||
</tr>`);
|
||||
});
|
||||
}
|
||||
|
||||
resolveAccessRuleNameOnHostRPlist();
|
||||
});
|
||||
}
|
||||
|
||||
//Perform realtime alias update without refreshing the whole page
|
||||
function updateAliasListForEndpoint(endpointName, newAliasDomainList){
|
||||
let targetEle = $(`.aliasDomains[eptuuid='${endpointName}']`);
|
||||
console.log(targetEle);
|
||||
if (targetEle.length == 0){
|
||||
return;
|
||||
}
|
||||
|
||||
let aliasDomains = ``;
|
||||
if (newAliasDomainList != undefined && newAliasDomainList.length > 0){
|
||||
aliasDomains = `Alias: `;
|
||||
newAliasDomainList.forEach(alias => {
|
||||
aliasDomains += `<a href="//${alias}" target="_blank">${alias}</a>, `;
|
||||
});
|
||||
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
|
||||
$(targetEle).html(aliasDomains);
|
||||
$(targetEle).show();
|
||||
}else{
|
||||
$(targetEle).hide();
|
||||
}
|
||||
}
|
||||
|
||||
//Resolve & Update all rule names on host PR list
|
||||
function resolveAccessRuleNameOnHostRPlist(){
|
||||
//Resolve the access filters
|
||||
$.get("/api/access/list", function(data){
|
||||
console.log(data);
|
||||
if (data.error == undefined){
|
||||
//Build a map base on the data
|
||||
let accessRuleMap = {};
|
||||
for (var i = 0; i < data.length; i++){
|
||||
accessRuleMap[data[i].ID] = data[i];
|
||||
}
|
||||
|
||||
|
||||
$(".accessRuleNameUnderHost").each(function(){
|
||||
let thisAccessRuleID = $(this).attr("ruleid");
|
||||
if (thisAccessRuleID== ""){
|
||||
thisAccessRuleID = "default"
|
||||
}
|
||||
|
||||
if (thisAccessRuleID == "default"){
|
||||
//No need to label default access rules
|
||||
$(this).html("");
|
||||
return;
|
||||
}
|
||||
|
||||
let rule = accessRuleMap[thisAccessRuleID];
|
||||
let icon = `<i class="ui grey filter icon"></i>`;
|
||||
if (rule.ID == "default"){
|
||||
icon = `<i class="ui yellow star icon"></i>`;
|
||||
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
|
||||
//This is a blacklist filter
|
||||
icon = `<i class="ui red filter icon"></i>`;
|
||||
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
|
||||
//This is a whitelist filter
|
||||
icon = `<i class="ui green filter icon"></i>`;
|
||||
}else if (rule.WhitelistEnabled && rule.BlacklistEnabled){
|
||||
//Whitelist and blacklist filter
|
||||
icon = `<i class="ui yellow filter icon"></i>`;
|
||||
}
|
||||
|
||||
if (rule != undefined){
|
||||
$(this).html(`${icon} ${rule.Name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//Update the access rule name on given epuuid, call by hostAccessEditor.html
|
||||
function updateAccessRuleNameUnderHost(epuuid, newruleUID){
|
||||
$(`tr[eptuuid='${epuuid}'].subdEntry`).find(".accessRuleNameUnderHost").attr("ruleid", newruleUID);
|
||||
resolveAccessRuleNameOnHostRPlist();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Inline editor for httprp.html
|
||||
@ -141,9 +246,9 @@
|
||||
|
||||
input = `
|
||||
<div class="ui mini fluid input">
|
||||
<input type="text" class="Domain" value="${domain}">
|
||||
<input type="text" class="Domain" onchange="cleanProxyTargetValue(this)" value="${domain}">
|
||||
</div>
|
||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<div class="ui checkbox" style="margin-top: 0.6em;">
|
||||
<input type="checkbox" class="RequireTLS" ${tls}>
|
||||
<label>Require TLS<br>
|
||||
<small>Proxy target require HTTPS connection</small></label>
|
||||
@ -152,7 +257,8 @@
|
||||
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
|
||||
<label>Skip Verification<br>
|
||||
<small>Check this if proxy target is using self signed certificates</small></label>
|
||||
</div>
|
||||
</div><br>
|
||||
<!-- <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editLoadBalanceOptions('${uuid}');"><i class="purple server icon"></i> Load Balance</button> -->
|
||||
`;
|
||||
column.empty().append(input);
|
||||
}else if (datatype == "vdir"){
|
||||
@ -161,11 +267,11 @@
|
||||
<i class="ui yellow folder icon"></i> Edit Virtual Directories
|
||||
</button>`);
|
||||
|
||||
}else if (datatype == "basicauth"){
|
||||
}else if (datatype == "advanced"){
|
||||
let requireBasicAuth = payload.RequireBasicAuth;
|
||||
let checkstate = "";
|
||||
let basicAuthCheckstate = "";
|
||||
if (requireBasicAuth){
|
||||
checkstate = "checked";
|
||||
basicAuthCheckstate = "checked";
|
||||
}
|
||||
|
||||
let skipWebSocketOriginCheck = payload.SkipWebSocketOriginCheck;
|
||||
@ -174,16 +280,35 @@
|
||||
wsCheckstate = "checked";
|
||||
}
|
||||
|
||||
let requireRateLimit = payload.RequireRateLimit;
|
||||
let rateLimitCheckState = "";
|
||||
if (requireRateLimit){
|
||||
rateLimitCheckState = "checked";
|
||||
}
|
||||
let rateLimit = payload.RateLimit;
|
||||
if (rateLimit == 0){
|
||||
//This value is not set. Make it default to 100
|
||||
rateLimit = 100;
|
||||
}
|
||||
let rateLimitDisableState = "";
|
||||
if (!payload.RequireRateLimit){
|
||||
rateLimitDisableState = "disabled";
|
||||
}
|
||||
|
||||
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<input type="checkbox" class="RequireBasicAuth" ${checkstate}>
|
||||
<input type="checkbox" class="RequireBasicAuth" ${basicAuthCheckstate}>
|
||||
<label>Require Basic Auth</label>
|
||||
</div>
|
||||
<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
|
||||
<br>
|
||||
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
|
||||
<br>
|
||||
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
|
||||
|
||||
<div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;">
|
||||
<div class="ui endpointAdvanceConfig accordion" style="padding-right: 0.6em;">
|
||||
<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
Advance Configs
|
||||
Security Options
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
@ -191,13 +316,35 @@
|
||||
<label>Skip WebSocket Origin Check<br>
|
||||
<small>Check this to allow cross-origin websocket requests</small></label>
|
||||
</div>
|
||||
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
|
||||
<!-- <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editLoadBalanceOptions('${uuid}');"><i class="blue server icon"></i> Load Balance</button> -->
|
||||
<br>
|
||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<input type="checkbox" onchange="handleToggleRateLimitInput();" class="RequireRateLimit" ${rateLimitCheckState}>
|
||||
<label>Require Rate Limit<br>
|
||||
<small>Check this to enable rate limit on this inbound hostname</small></label>
|
||||
</div><br>
|
||||
<div class="ui mini right labeled fluid input ${rateLimitDisableState}" style="margin-top: 0.4em;">
|
||||
<input type="number" class="RateLimit" value="${rateLimit}" min="1" >
|
||||
<label class="ui basic label">
|
||||
req / sec / IP
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
`);
|
||||
|
||||
} else if (datatype == "ratelimit"){
|
||||
|
||||
column.empty().append(`
|
||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<input type="checkbox" class="RequireRateLimit" ${checkstate}>
|
||||
<label>Require Rate Limit</label>
|
||||
</div>
|
||||
<div class="ui mini fluid input">
|
||||
<input type="number" class="RateLimit" value="${rateLimit}" placeholder="100" min="1" max="1000" >
|
||||
</div>
|
||||
`);
|
||||
|
||||
}else if (datatype == 'action'){
|
||||
column.empty().append(`
|
||||
<button title="Save" onclick="saveProxyInlineEdit('${uuid.hexEncode()}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui green save icon"></i></button>
|
||||
@ -213,7 +360,12 @@
|
||||
<label>Allow plain HTTP access<br>
|
||||
<small>Allow inbound connections without TLS/SSL</small></label>
|
||||
</div><br>
|
||||
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
|
||||
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
|
||||
|
||||
`);
|
||||
|
||||
$(".hostAccessRuleSelector").dropdown();
|
||||
}else{
|
||||
//Unknown field. Leave it untouched
|
||||
}
|
||||
@ -223,6 +375,17 @@
|
||||
$("#httpProxyList").find(".editBtn").addClass("disabled");
|
||||
}
|
||||
|
||||
//handleToggleRateLimitInput will get trigger if the "require rate limit" checkbox
|
||||
// is changed and toggle the disable state of the rate limit input field
|
||||
function handleToggleRateLimitInput(){
|
||||
let isRateLimitEnabled = $("#httpProxyList input.RequireRateLimit")[0].checked;
|
||||
if (isRateLimitEnabled){
|
||||
$("#httpProxyList input.RateLimit").parent().removeClass("disabled");
|
||||
}else{
|
||||
$("#httpProxyList input.RateLimit").parent().addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
function exitProxyInlineEdit(){
|
||||
listProxyEndpoints();
|
||||
$("#httpProxyList").find(".editBtn").removeClass("disabled");
|
||||
@ -240,6 +403,8 @@
|
||||
let requireTLS = $(row).find(".RequireTLS")[0].checked;
|
||||
let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
|
||||
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
|
||||
let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked;
|
||||
let rateLimit = $(row).find(".RateLimit").val();
|
||||
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
|
||||
let bypassWebsocketOrigin = $(row).find(".SkipWebSocketOriginCheck")[0].checked;
|
||||
console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
|
||||
@ -256,6 +421,8 @@
|
||||
"tlsval": skipCertValidations,
|
||||
"bpwsorg" : bypassWebsocketOrigin,
|
||||
"bauth" :requireBasicAuth,
|
||||
"rate" :requireRateLimit,
|
||||
"ratenum" :rateLimit,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error !== undefined){
|
||||
@ -268,6 +435,21 @@
|
||||
})
|
||||
}
|
||||
|
||||
//Clearn the proxy target value, make sure user do not enter http:// or https://
|
||||
//and auto select TLS checkbox if https:// exists
|
||||
function cleanProxyTargetValue(input){
|
||||
let targetDomain = $(input).val().trim();
|
||||
if (targetDomain.startsWith("http://")){
|
||||
targetDomain = targetDomain.substr(7);
|
||||
$(input).val(targetDomain);
|
||||
$("#httpProxyList input.RequireTLS").parent().checkbox("set unchecked");
|
||||
}else if (targetDomain.startsWith("https://")){
|
||||
targetDomain = targetDomain.substr(8);
|
||||
$(input).val(targetDomain);
|
||||
$("#httpProxyList input.RequireTLS").parent().checkbox("set checked");
|
||||
}
|
||||
}
|
||||
|
||||
/* button events */
|
||||
function editBasicAuthCredentials(uuid){
|
||||
let payload = encodeURIComponent(JSON.stringify({
|
||||
@ -277,11 +459,28 @@
|
||||
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
|
||||
}
|
||||
|
||||
function editAccessRule(uuid){
|
||||
let payload = encodeURIComponent(JSON.stringify({
|
||||
ept: "host",
|
||||
ep: uuid
|
||||
}));
|
||||
showSideWrapper("snippet/hostAccessEditor.html?t=" + Date.now() + "#" + payload);
|
||||
}
|
||||
|
||||
function editAliasHostnames(uuid){
|
||||
let payload = encodeURIComponent(JSON.stringify({
|
||||
ept: "host",
|
||||
ep: uuid
|
||||
}));
|
||||
showSideWrapper("snippet/aliasEditor.html?t=" + Date.now() + "#" + payload);
|
||||
}
|
||||
|
||||
function quickEditVdir(uuid){
|
||||
openTabById("vdir");
|
||||
$("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
|
||||
}
|
||||
|
||||
//Open the custom header editor
|
||||
function editCustomHeaders(uuid){
|
||||
let payload = encodeURIComponent(JSON.stringify({
|
||||
ept: "host",
|
||||
@ -290,6 +489,15 @@
|
||||
showSideWrapper("snippet/customHeaders.html?t=" + Date.now() + "#" + payload);
|
||||
}
|
||||
|
||||
//Open the load balance option
|
||||
function editLoadBalanceOptions(uuid){
|
||||
let payload = encodeURIComponent(JSON.stringify({
|
||||
ept: "host",
|
||||
ep: uuid
|
||||
}));
|
||||
showSideWrapper("snippet/loadBalancer.html?t=" + Date.now() + "#" + payload);
|
||||
}
|
||||
|
||||
function handleProxyRuleToggle(object){
|
||||
let endpointUUID = $(object).attr("eptuuid");
|
||||
let isChecked = object.checked;
|
||||
@ -314,7 +522,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//Bind on tab switch events
|
||||
tabSwitchEventBind["httprp"] = function(){
|
||||
listProxyEndpoints();
|
||||
|
@ -258,7 +258,7 @@
|
||||
|
||||
setTimeout(function(){
|
||||
//Update the checkbox
|
||||
msgbox("Proxy Root Updated");
|
||||
msgbox("Default Site Updated");
|
||||
}, 100);
|
||||
|
||||
})
|
||||
|
@ -5,6 +5,12 @@
|
||||
color: var(--theme_lgrey);
|
||||
border-radius: 1em !important;
|
||||
}
|
||||
|
||||
.ui.form .sub.field{
|
||||
background-color: var(--theme_advance);
|
||||
border-radius: 0.6em;
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
||||
<div class="standardContainer">
|
||||
<div class="ui stackable grid">
|
||||
@ -16,13 +22,16 @@
|
||||
<div class="field">
|
||||
<label>Matching Keyword / Domain</label>
|
||||
<input type="text" id="rootname" placeholder="mydomain.com">
|
||||
<small>Support subdomain and wildcard, e.g. s1.mydomain.com or *.test.mydomain.com</small>
|
||||
<small>Support subdomain and wildcard, e.g. s1.mydomain.com or *.test.mydomain.com. Use comma (,) for alias hostnames. </small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<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>
|
||||
<div class="field dockerOptimizations" style="display:none;">
|
||||
<button style="margin-top: -2em;" class="ui basic small button" onclick="openDockerContainersList();"><i class="blue docker icon"></i> Pick from Docker Containers</button>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="reqTls">
|
||||
@ -37,7 +46,18 @@
|
||||
Advance Settings
|
||||
</div>
|
||||
<div class="content">
|
||||
<p></p>
|
||||
<div class="field">
|
||||
<label>Access Rule</label>
|
||||
<div class="ui selection dropdown">
|
||||
<input type="hidden" id="newProxyRuleAccessFilter" value="default">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">Default</div>
|
||||
<div class="menu" id="newProxyRuleAccessList">
|
||||
<div class="item" data-value="default"><i class="ui yellow star icon"></i> Default</div>
|
||||
</div>
|
||||
</div>
|
||||
<small>Allow regional access control using blacklist or whitelist. Use "default" for "allow all".</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="skipTLSValidation">
|
||||
@ -56,6 +76,22 @@
|
||||
<label>Allow plain HTTP access<br><small>Allow this subdomain to be connected without TLS (Require HTTP server enabled on port 80)</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="requireRateLimit">
|
||||
<label>Require Rate Limit<br><small>This proxy endpoint will be rate limited.</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Rate Limit</label>
|
||||
<div class="ui fluid right labeled input">
|
||||
<input type="number" id="proxyRateLimit" placeholder="100" min="1" max="1000" value="100">
|
||||
<div class="ui basic label">
|
||||
req / sec / IP
|
||||
</div>
|
||||
</div>
|
||||
<small>Return a 429 error code if request rate exceed the rate limit.</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="requireBasicAuth">
|
||||
@ -121,8 +157,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$("#advanceProxyRules").accordion();
|
||||
|
||||
|
||||
//New Proxy Endpoint
|
||||
function newProxyEndpoint(){
|
||||
@ -132,7 +166,10 @@
|
||||
var skipTLSValidation = $("#skipTLSValidation")[0].checked;
|
||||
var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
|
||||
var requireBasicAuth = $("#requireBasicAuth")[0].checked;
|
||||
var proxyRateLimit = $("#proxyRateLimit").val();
|
||||
var requireRateLimit = $("#requireRateLimit")[0].checked;
|
||||
var skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked;
|
||||
var accessRuleToUse = $("#newProxyRuleAccessFilter").val();
|
||||
|
||||
if (rootname.trim() == ""){
|
||||
$("#rootname").parent().addClass("error");
|
||||
@ -160,8 +197,10 @@
|
||||
bpwsorg: skipWebSocketOriginCheck,
|
||||
bypassGlobalTLS: bypassGlobalTLS,
|
||||
bauth: requireBasicAuth,
|
||||
rate: requireRateLimit,
|
||||
ratenum: proxyRateLimit,
|
||||
cred: JSON.stringify(credentials),
|
||||
|
||||
access: accessRuleToUse,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
@ -249,6 +288,16 @@
|
||||
$("#requireBasicAuth").on('change', toggleBasicAuth);
|
||||
toggleBasicAuth();
|
||||
|
||||
function toggleRateLimit() {
|
||||
if ($("#requireRateLimit").parent().checkbox("is checked")) {
|
||||
$("#proxyRateLimit").parent().parent().removeClass("disabled");
|
||||
} else {
|
||||
$("#proxyRateLimit").parent().parent().addClass("disabled");
|
||||
}
|
||||
}
|
||||
$("#requireRateLimit").on('change', toggleRateLimit);
|
||||
toggleRateLimit();
|
||||
|
||||
|
||||
/*
|
||||
Credential Managements
|
||||
@ -343,4 +392,68 @@
|
||||
return back;
|
||||
}
|
||||
|
||||
/*
|
||||
Access Rule dropdown Initialization
|
||||
*/
|
||||
|
||||
function initNewProxyRuleAccessDropdownList(callback=undefined){
|
||||
$.get("/api/access/list", function(data){
|
||||
if (data.error == undefined){
|
||||
$("#newProxyRuleAccessList").html("");
|
||||
data.forEach(function(rule){
|
||||
let icon = `<i class="ui grey filter icon"></i>`;
|
||||
if (rule.ID == "default"){
|
||||
icon = `<i class="ui yellow star icon"></i>`;
|
||||
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
|
||||
//This is a blacklist filter
|
||||
icon = `<i class="ui red filter icon"></i>`;
|
||||
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
|
||||
//This is a whitelist filter
|
||||
icon = `<i class="ui green filter icon"></i>`;
|
||||
}
|
||||
$("#newProxyRuleAccessList").append(`<div class="item" data-value="${rule.ID}">${icon} ${rule.Name}</div>`);
|
||||
});
|
||||
$("#newProxyRuleAccessFilter").parent().dropdown();
|
||||
if (callback != undefined){
|
||||
callback();
|
||||
}
|
||||
}else{
|
||||
msgbox("Access rule load failed: " + data.error, false);
|
||||
}
|
||||
})
|
||||
}
|
||||
initNewProxyRuleAccessDropdownList();
|
||||
|
||||
//Bind on tab switch events
|
||||
tabSwitchEventBind["rules"] = function(){
|
||||
//Update the access rule list
|
||||
initNewProxyRuleAccessDropdownList();
|
||||
}
|
||||
|
||||
/* Docker Optimizations */
|
||||
function initDockerUXOptimizations(){
|
||||
$.get("/api/docker/available", function(dockerAvailable){
|
||||
if (dockerAvailable){
|
||||
$(".dockerOptimizations").show();
|
||||
}else{
|
||||
$(".dockerOptimizations").hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
initDockerUXOptimizations();
|
||||
|
||||
function openDockerContainersList(){
|
||||
showSideWrapper('snippet/dockerContainersList.html');
|
||||
}
|
||||
|
||||
function addContainerItem(item) {
|
||||
$('#rootname').val(item.name);
|
||||
$('#proxyDomain').val(`${item.ip}:${item.port}`)
|
||||
hideSideWrapper(true);
|
||||
}
|
||||
|
||||
/* UI Element Initialization */
|
||||
$("#advanceProxyRules").accordion();
|
||||
$("#newProxyRuleAccessFilter").parent().dropdown();
|
||||
|
||||
</script>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user