28 Commits
2.6.2 ... 2.6.5

Author SHA1 Message Date
7eda6ba501 Merge pull request #29 from Morethanevil/patch-4
Updated change log
2023-07-19 11:42:13 +08:00
2da5ef048f Update CHANGELOG.md 2023-07-19 05:14:42 +02:00
6c48939316 Update CHANGELOG.md 2023-07-19 05:10:08 +02:00
544894bbba Merge pull request #28 from tobychui/autorenew_experimental
Auto-renew with ACME
2023-07-19 09:43:23 +08:00
153d056bdf ACME compatibility fix for /.well-known/
+ Updated acme well known take-over regrex
+ Added experimental config export and import
+ Added unit test for location rewrite in dpcore
+ Moved all config files to ./conf and original proxy files to ./conf/proxy
+ Minor optimization on UI regarding TLS verification logo on subdomain and vdir list
2023-07-12 21:42:09 +08:00
12c1118af9 Merge pull request #27 from tobychui/main
Update auto-renew branch to catch up with main
2023-07-09 14:43:07 +08:00
67ba143999 Merge branch 'autorenew_experimental' into main 2023-07-09 14:41:34 +08:00
0a8a821394 Update main.go
Switched dev mode to false (Note: The advance routing rule system is still work in progress, do not use them)
2023-07-06 11:03:41 +08:00
36b17ce4cf acme and redirection patch
+ Added experimental fix for redirection tailing problem
+ Added acme widget for first time users to setup https
2023-07-06 11:01:33 +08:00
519372069f Update README.md 2023-07-04 07:58:40 -04:00
2f14d6f271 Fixed minor bugs in renew policy toggle 2023-06-29 18:59:24 +08:00
44ac7144ec Added access control bypass for /.well-known router 2023-06-23 23:45:49 +08:00
741d3f8de1 Minor fix to renew now function 2023-06-23 23:22:19 +08:00
23eca5afae Added experimental acme renew from Let's Encrypt 2023-06-23 23:09:10 +08:00
050fab9481 Merge pull request #24 from tobychui/DockerMerge
Updates + Changes
2023-06-22 11:12:26 +08:00
3fc92bac27 Updates + Changes
Some code improvements. If the version ever changes, it will download the new binary if it doesn't already exist. Old ones will be kept however.

Some general changes to documentation.
2023-06-21 13:04:32 -04:00
594f75da97 Update README.md 2023-06-21 23:59:24 +08:00
3fbf246fb4 Added docker file (Fork from PassiveLemon) 2023-06-21 19:45:58 +08:00
828af6263d Merge pull request #23 from Morethanevil/patch-3
Update CHANGELOG.md
2023-06-16 21:46:32 +08:00
ab42cec31f Update CHANGELOG.md 2023-06-15 19:23:45 +02:00
a8bf07dbba Update start.go
Patched the rule folder name
2023-06-16 00:54:55 +08:00
48dc85ea3e Updates 2.6.4
+ Added force TLS v1.2 above toggle
+ Added trace route
+ Added ICMP ping
+ Added special routing rules module for up-coming acme integration
+ Fixed IPv6 check bug in black/whitelist
+ Optimized UI for TCP Proxy
+
2023-06-16 00:48:39 +08:00
a73a7944ec Merge pull request #19 from Morethanevil/patch-2
Update CHANGELOG.md
2023-06-09 00:22:33 +08:00
d187124db6 Quick fix on the edit not working bug 2023-06-09 00:01:11 +08:00
0dd9e5d73c Update CHANGELOG.md 2023-06-08 17:05:41 +02:00
5e7599756f Updates 2.6.3
+ Added X-Forwarded-Proto for automatic proxy detector
+ Split blacklist and whitelist from geodb script file
+ Optimized compile binary size
+ Added access control to TCP proxy
+ Added "invalid config detect" in up time monitor for isse #7
+ Fixed minor bugs in advance stats panel
+ Reduced file size of embedded materials
2023-06-08 21:42:03 +08:00
5db50c1ca2 Merge pull request #16 from Morethanevil/main-1
Create CHANGELOG.md
2023-06-06 14:38:03 +08:00
884507b45a Create CHANGELOG.md
Create Changelog.md for users to see progress of development
2023-06-05 19:18:28 +02:00
79 changed files with 6512 additions and 475 deletions

3
.gitignore vendored
View File

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

53
CHANGELOG.md Normal file
View File

@ -0,0 +1,53 @@
v2.6.5 Jul 19 2023
+ Added Import / Export-Feature
+ Moved configurationfiles to a separate folder [#26](https://github.com/tobychui/zoraxy/issues/26)
+ Added auto-renew with ACME [#6](https://github.com/tobychui/zoraxy/issues/6)
+ Fixed Whitelistbug [#18](https://github.com/tobychui/zoraxy/issues/18)
+ Added Whois
# v2.6.4 Jun 15 2023
+ Added force TLS v1.2 above toggle
+ Added trace route
+ Added ICMP ping
+ Added special routing rules module for up-coming acme integration
+ Fixed IPv6 check bug in black/whitelist
+ Optimized UI for TCP Proxy
# v2.6.3 Jun 8 2023
+ Added X-Forwarded-Proto for automatic proxy detector
+ Split blacklist and whitelist from geodb script file
+ Optimized compile binary size
+ Added access control to TCP proxy
+ Added "invalid config detect" in up time monitor for isse [#7](https://github.com/tobychui/zoraxy/issues/7)
+ Fixed minor bugs in advance stats panel
+ Reduced file size of embedded materials
# v2.6.2 Jun 4 2023
+ Added advance stats operation tab
+ Added statistic reset [#13](https://github.com/tobychui/zoraxy/issues/13)
+ Added statistic export to csv and json (please use json)
+ Make subdomain clickable (not vdir) [#12](https://github.com/tobychui/zoraxy/issues/12)
+ Added TCP Proxy
+ Updates SMTP setup UI to make it more straight forward to setup
# v2.6.1 May 31 2023
+ Added reverse proxy TLS skip verification
+ Added basic auth
+ Edit proxy settings
+ Whitelist
+ TCP Proxy (experimental)
+ Info (Utilities page)
# v2.6 May 27 2023
+ Basic auth
+ Support TLS verification skip (for self signed certs)
+ Added trend analysis
+ Added referer and file type analysis
+ Added cert expire day display
+ Moved subdomain proxy logic to dpcore

View File

@ -67,11 +67,7 @@ The installation method is same as Linux. If you are using Raspberry Pi 4 or new
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
#### Docker
Thanks for cyb3rdoc and PassiveLemon for providing support over the Docker installation. You can check out their repo over here.
[https://github.com/cyb3rdoc/zoraxy-docker](https://github.com/cyb3rdoc/zoraxy-docker)
[https://github.com/PassiveLemon/zoraxy-docker](https://github.com/PassiveLemon/zoraxy-docker)
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details
### External Permission Management Mode

19
docker/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM alpine:latest
RUN apk update && apk upgrade &&\
apk add bash curl jq sudo &&\
mkdir -p /zoraxy/data/
VOLUME [ "/zoraxy/data/" ]
COPY entrypoint.sh /zoraxy/
RUN chmod +x /zoraxy/entrypoint.sh
ENV ARGS="-port=:8000 -noauth=false"
EXPOSE 80
EXPOSE 443
EXPOSE 8000
ENTRYPOINT ["/zoraxy/entrypoint.sh"]

21
docker/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 PassiveLemon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

67
docker/README.md Normal file
View File

@ -0,0 +1,67 @@
[![Repo](https://img.shields.io/badge/Docker-Repo-007EC6?labelColor-555555&color-007EC6&logo=docker&logoColor=fff&style=flat-square)](https://hub.docker.com/r/passivelemon/zoraxy-docker)
[![Version](https://img.shields.io/docker/v/passivelemon/zoraxy-docker/latest?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/passivelemon/zoraxy-docker)
[![Size](https://img.shields.io/docker/image-size/passivelemon/zoraxy-docker/latest?sort=semver&labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/passivelemon/zoraxy-docker)
[![Pulls](https://img.shields.io/docker/pulls/passivelemon/zoraxy-docker?labelColor-555555&color-007EC6&style=flat-square)](https://hub.docker.com/r/passivelemon/zoraxy-docker)
## Setup: </br>
Although not required, it is recommended to give Zoraxy a dedicated location on the host to mount the container. That way, the host/user can access them whenever needed. A volume will be created automatically within Docker if a location is not specified. </br>
You may also need to portforward your 80/443 to allow http and https traffic. If you are accessing the interface from outside of the local network, you may also need to forward your management port. If you know how to do this, great! If not, find the manufacturer of your router and search on how to do that. There are too many to be listed here. </br>
### Using Docker run </br>
```
docker run -d --name (container name) -p (ports) -v (path to storage directory):/zoraxy/data/ -e ARGS=(your arguments) -e VERSION=(version) passivelemon/zoraxy-docker:latest
```
### Using Docker Compose </br>
```
version: '3.3'
services:
zoraxy-docker:
image: passivelemon/zoraxy-docker:latest
container_name: (container name)
ports:
- 80:80 # Http port
- 443:443 # Https port
- (external):8000 # Management portal port
volumes:
- (path to storage directory):/zoraxy/data/ # Host directory for Zoraxy file storage
environment:
ARGS: '(your arguments)' # The arguments to run with Zoraxy. Enter them as they would be entered normally.
```
| Operator | Need | Details |
|:-|:-|:-|
| `-d` | Yes | will run the container in the background. |
| `--name (container name)` | No | Sets the name of the container to the following word. You can change this to whatever you want. |
| `-p (ports)` | Yes | Depending on how your network is setup, you may need to portforward 80, 443, and the management port. |
| `-v (path to storage directory):/zoraxy/data/` | Recommend | Sets the folder that holds your files. This should be the place you just chose. By default, it will create a Docker volume for the files for persistency but they will not be accessible. |
| `-e ARGS=(your arguments)` | No | Sets the arguments to run Zoraxy with. Enter them as you would normally. By default, it is ran with `-port=:8000 -noauth=false` |
| `-e VERSION=(version)` | No | Sets the version of Zoraxy that the container will download. Must be a supported version found on the Zoraxy Github. Defaults to the latest if not set. |
| `passivelemon/zoraxy-docker:latest` | Yes | The repository on Docker hub. By default, it is the latest version that I have published. |
## Examples: </br>
### Docker Run </br>
```
docker run -d --name zoraxy -p 80:80 -p 443:443 -p 8005:8000/tcp -v /home/docker/Containers/Zoraxy:/zoraxy/data/ -e ARGS="-port=:8000 -noauth=false" passivelemon/zoraxy-docker:latest
```
### Docker Compose </br>
```
version: '3.3'
services:
zoraxy-docker:
image: passivelemon/zoraxy-docker:latest
container_name: zoraxy
ports:
- 80:80
- 443:443
- 8005:8000/tcp
volumes:
- /home/docker/Containers/Zoraxy:/zoraxy/data/
environment:
ARGS: '-port=:8000 -noauth=false'
```
### Other </br>
If the container doesn't start properly, you might be rate limited from GitHub for some amount of time. You can check this by running `curl -s https://api.github.com/repos/tobychui/zoraxy/releases` on the host. If you are, you will just have to wait for a little while or use a VPN. </br>

20
docker/entrypoint.sh Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
cd /zoraxy/data/
if [ "$VERSION" != "" ]; then
echo "|| Using release ${VERSION} ||"
release=${VERSION}
else
echo "|| Using latest release ||"
# Gets the latest pre-release version tag. Will be updated when official release comes out.
release=$(curl -s https://api.github.com/repos/tobychui/zoraxy/releases | jq -r 'map(select(.prerelease)) | .[0].tag_name')
fi
if [ ! -e "/zoraxy/data/zoraxy_linux_amd64-${release}" ]; then
echo "|| Downloading version ${release} ||"
curl -Lso /zoraxy/data/zoraxy_linux_amd64-${release} https://github.com/tobychui/zoraxy/releases/download/${release}/zoraxy_linux_amd64
chmod u+x /zoraxy/data/zoraxy_linux_amd64-${release}
fi
./zoraxy_linux_amd64-${release} ${ARGS}

115
src/acme.go Normal file
View File

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

View File

@ -4,8 +4,10 @@ import (
"encoding/json"
"net/http"
"imuslab.com/zoraxy/mod/acme/acmewizard"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/utils"
)
@ -55,8 +57,10 @@ func initAPIs() {
//TLS / SSL config
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
@ -81,6 +85,11 @@ func initAPIs() {
authRouter.HandleFunc("/api/whitelist/ip/remove", handleIpWhitelistRemove)
authRouter.HandleFunc("/api/whitelist/enable", handleWhitelistEnable)
//Path Blocker APIs
authRouter.HandleFunc("/api/pathrule/add", pathRuleHandler.HandleAddBlockingPath)
authRouter.HandleFunc("/api/pathrule/list", pathRuleHandler.HandleListBlockingPath)
authRouter.HandleFunc("/api/pathrule/remove", pathRuleHandler.HandleRemoveBlockingPath)
//Statistic & uptime monitoring API
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
authRouter.HandleFunc("/api/stats/countries", HandleCountryDistrSummary)
@ -126,6 +135,9 @@ func initAPIs() {
//Network utilities
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession)
authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck)
authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan)
@ -138,8 +150,21 @@ func initAPIs() {
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
http.HandleFunc("/api/account/new", HandleNewPasswordSetup)
//ACME & Auto Renewer
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
authRouter.HandleFunc("/api/acme/autoRenew/enable", acmeAutoRenewer.HandleAutoRenewEnable)
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/renewPolicy", acmeAutoRenewer.HandleRenewPolicy)
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
//Others
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
http.HandleFunc("/api/conf/export", ExportConfigAsZip)
http.HandleFunc("/api/conf/import", ImportConfigFromZip)
//If you got APIs to add, append them here
}

View File

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

View File

@ -1,12 +1,18 @@
package main
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/utils"
@ -31,7 +37,7 @@ type Record struct {
func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
//TODO: Make this accept new def types
os.MkdirAll("conf", 0775)
os.MkdirAll("./conf/proxy/", 0775)
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
//Generate record
@ -39,12 +45,12 @@ func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
//Write to file
js, _ := json.MarshalIndent(thisRecord, "", " ")
return ioutil.WriteFile(filepath.Join("conf", filename), js, 0775)
return ioutil.WriteFile(filepath.Join("./conf/proxy/", filename), js, 0775)
}
func RemoveReverseProxyConfig(rootname string) error {
filename := getFilenameFromRootName(rootname)
removePendingFile := strings.ReplaceAll(filepath.Join("conf", filename), "\\", "/")
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
log.Println("Config Removed: ", removePendingFile)
if utils.FileExists(removePendingFile) {
err := os.Remove(removePendingFile)
@ -83,3 +89,202 @@ func getFilenameFromRootName(rootname string) string {
filename = filename + ".config"
return filename
}
/*
Importer and Exporter of Zoraxy proxy config
*/
func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
includeSysDBRaw, err := utils.GetPara(r, "includeDB")
includeSysDB := false
if includeSysDBRaw == "true" {
//Include the system database in backup snapshot
//Temporary set it to read only
sysdb.ReadOnly = true
includeSysDB = true
}
// Specify the folder path to be zipped
folderPath := "./conf/"
// Set the Content-Type header to indicate it's a zip file
w.Header().Set("Content-Type", "application/zip")
// Set the Content-Disposition header to specify the file name
w.Header().Set("Content-Disposition", "attachment; filename=\"config.zip\"")
// Create a zip writer
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
// Walk through the folder and add files to the zip
err = filepath.Walk(folderPath, func(filePath string, fileInfo os.FileInfo, err error) error {
if err != nil {
return err
}
if folderPath == filePath {
//Skip root folder
return nil
}
// Create a new file in the zip
if !utils.IsDir(filePath) {
zipFile, err := zipWriter.Create(filePath)
if err != nil {
return err
}
// Open the file on disk
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
// Copy the file contents to the zip file
_, err = io.Copy(zipFile, file)
if err != nil {
return err
}
}
return nil
})
if includeSysDB {
//Also zip in the sysdb
zipFile, err := zipWriter.Create("sys.db")
if err != nil {
log.Println("[Backup] Unable to zip sysdb: " + err.Error())
return
}
// Open the file on disk
file, err := os.Open("sys.db")
if err != nil {
log.Println("[Backup] Unable to open sysdb: " + err.Error())
return
}
defer file.Close()
// Copy the file contents to the zip file
_, err = io.Copy(zipFile, file)
if err != nil {
log.Println(err)
return
}
//Restore sysdb state
sysdb.ReadOnly = false
}
if err != nil {
// Handle the error and send an HTTP response with the error message
http.Error(w, fmt.Sprintf("Failed to zip folder: %v", err), http.StatusInternalServerError)
return
}
}
func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
// Check if the request is a POST with a file upload
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusBadRequest)
return
}
// Max file size limit (10 MB in this example)
r.ParseMultipartForm(10 << 20)
// Get the uploaded file
file, handler, err := r.FormFile("file")
if err != nil {
http.Error(w, "Failed to retrieve uploaded file", http.StatusInternalServerError)
return
}
defer file.Close()
if filepath.Ext(handler.Filename) != ".zip" {
http.Error(w, "Upload file is not a zip file", http.StatusInternalServerError)
return
}
// Create the target directory to unzip the files
targetDir := "./conf"
if utils.FileExists(targetDir) {
//Backup the old config to old
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
}
err = os.MkdirAll(targetDir, os.ModePerm)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to create target directory: %v", err), http.StatusInternalServerError)
return
}
// Open the zip file
zipReader, err := zip.NewReader(file, handler.Size)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to open zip file: %v", err), http.StatusInternalServerError)
return
}
restoreDatabase := false
// Extract each file from the zip archive
for _, zipFile := range zipReader.File {
// Open the file in the zip archive
rc, err := zipFile.Open()
if err != nil {
http.Error(w, fmt.Sprintf("Failed to open file in zip: %v", err), http.StatusInternalServerError)
return
}
defer rc.Close()
// Create the corresponding file on disk
zipFile.Name = strings.ReplaceAll(zipFile.Name, "../", "")
fmt.Println("Restoring: " + strings.ReplaceAll(zipFile.Name, "\\", "/"))
if zipFile.Name == "sys.db" {
//Sysdb replacement. Close the database and restore
sysdb.Close()
restoreDatabase = true
} else if !strings.HasPrefix(strings.ReplaceAll(zipFile.Name, "\\", "/"), "conf/") {
//Malformed zip file.
http.Error(w, fmt.Sprintf("Invalid zip file structure or version too old"), http.StatusInternalServerError)
return
}
//Check if parent dir exists
if !utils.FileExists(filepath.Dir(zipFile.Name)) {
os.MkdirAll(filepath.Dir(zipFile.Name), 0775)
}
//Create the file
newFile, err := os.Create(zipFile.Name)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to create file: %v", err), http.StatusInternalServerError)
return
}
defer newFile.Close()
// Copy the file contents from the zip to the new file
_, err = io.Copy(newFile, rc)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to extract file from zip: %v", err), http.StatusInternalServerError)
return
}
}
// Send a success response
w.WriteHeader(http.StatusOK)
log.Println("Configuration restored")
fmt.Fprintln(w, "Configuration restored")
if restoreDatabase {
go func() {
log.Println("Database altered. Restarting in 3 seconds...")
time.Sleep(3 * time.Second)
os.Exit(0)
}()
}
}

View File

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

1597
src/go.sum

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ import (
"time"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/aroz"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/database"
@ -21,6 +22,7 @@ import (
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/pathrule"
"imuslab.com/zoraxy/mod/sshprox"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/statistic/analytic"
@ -36,9 +38,10 @@ var showver = flag.Bool("version", false, "Show version of this server")
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL certificate renew check interval (seconds)")
var (
name = "Zoraxy"
version = "2.6.2"
version = "2.6.5"
nodeUUID = "generic"
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()
@ -57,6 +60,7 @@ var (
authAgent *auth.AuthAgent //Authentication agent
tlsCertManager *tlscert.Manager //TLS / SSL management
redirectTable *redirection.RuleTable //Handle special redirection rule sets
pathRuleHandler *pathrule.Handler //Handle specific path blocking or custom headers
geodbStore *geodb.Store //GeoIP database, also handle black list and whitelist features
netstatBuffers *netstat.NetStatBuffers //Realtime graph buffers
statisticCollector *statistic.Collector //Collecting statistic from visitors
@ -65,6 +69,8 @@ var (
ganManager *ganserv.NetworkManager //Global Area Network Manager
webSshManager *sshprox.Manager //Web SSH connection service
tcpProxyManager *tcpprox.Manager //TCP Proxy Manager
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending
@ -77,29 +83,34 @@ func SetupCloseHandler() {
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
fmt.Println("- Shutting down " + name)
fmt.Println("- Closing GeoDB ")
geodbStore.Close()
fmt.Println("- Closing Netstats Listener")
netstatBuffers.Close()
fmt.Println("- Closing Statistic Collector")
statisticCollector.Close()
fmt.Println("- Stopping mDNS Discoverer")
//Stop the mdns service
mdnsTickerStop <- true
mdnsScanner.Close()
//Remove the tmp folder
fmt.Println("- Cleaning up tmp files")
os.RemoveAll("./tmp")
//Close database, final
fmt.Println("- Stopping system database")
sysdb.Close()
ShutdownSeq()
os.Exit(0)
}()
}
func ShutdownSeq() {
fmt.Println("- Shutting down " + name)
fmt.Println("- Closing GeoDB ")
geodbStore.Close()
fmt.Println("- Closing Netstats Listener")
netstatBuffers.Close()
fmt.Println("- Closing Statistic Collector")
statisticCollector.Close()
fmt.Println("- Stopping mDNS Discoverer")
//Stop the mdns service
mdnsTickerStop <- true
mdnsScanner.Close()
fmt.Println("- Closing Certificates Auto Renewer")
acmeAutoRenewer.Close()
//Remove the tmp folder
fmt.Println("- Cleaning up tmp files")
os.RemoveAll("./tmp")
//Close database, final
fmt.Println("- Stopping system database")
sysdb.Close()
}
func main() {
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
@ -149,6 +160,9 @@ func main() {
time.Sleep(500 * time.Millisecond)
//Start the finalize sequences
finalSequence()
log.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
err = http.ListenAndServe(handler.Port, nil)

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

@ -0,0 +1,288 @@
package acme
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"imuslab.com/zoraxy/mod/utils"
)
// ACMEUser represents a user in the ACME system.
type ACMEUser struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
// GetEmail returns the email of the ACMEUser.
func (u *ACMEUser) GetEmail() string {
return u.Email
}
// GetRegistration returns the registration resource of the ACMEUser.
func (u ACMEUser) GetRegistration() *registration.Resource {
return u.Registration
}
// GetPrivateKey returns the private key of the ACMEUser.
func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
return u.key
}
// ACMEHandler handles ACME-related operations.
type ACMEHandler struct {
DefaultAcmeServer string
Port string
}
// NewACME creates a new ACMEHandler instance.
func NewACME(acmeServer string, port string) *ACMEHandler {
return &ACMEHandler{
DefaultAcmeServer: acmeServer,
Port: port,
}
}
// ObtainCert obtains a certificate for the specified domains.
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, ca string) (bool, error) {
log.Println("[ACME] Obtaining certificate...")
// generate private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Println(err)
return false, err
}
// create a admin user for our new generation
adminUser := ACMEUser{
Email: email,
key: privateKey,
}
// create config
config := lego.NewConfig(&adminUser)
// setup who is the issuer and the key type
config.CADirURL = a.DefaultAcmeServer
//Overwrite the CADir URL if set
if ca != "" {
caLinkOverwrite, err := loadCAApiServerFromName(ca)
if err == nil {
config.CADirURL = caLinkOverwrite
log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
} else {
return false, errors.New("CA " + ca + " is not supported. Please contribute to the source code and add this CA's directory link.")
}
}
config.Certificate.KeyType = certcrypto.RSA2048
client, err := lego.NewClient(config)
if err != nil {
log.Println(err)
return false, err
}
// setup how to receive challenge
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
if err != nil {
log.Println(err)
return false, err
}
// New users will need to register
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Println(err)
return false, err
}
adminUser.Registration = reg
// obtain the certificate
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
}
certificates, err := client.Certificate.Obtain(request)
if err != nil {
log.Println(err)
return false, err
}
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL.
err = ioutil.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
if err != nil {
log.Println(err)
return false, err
}
err = ioutil.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
if err != nil {
log.Println(err)
return false, err
}
return true, nil
}
// CheckCertificate returns a list of domains that are in expired certificates.
// It will return all domains that is in expired certificates
// *** if there is a vaild certificate contains the domain and there is a expired certificate contains the same domain
// it will said expired as well!
func (a *ACMEHandler) CheckCertificate() []string {
// read from dir
filenames, err := os.ReadDir("./conf/certs/")
expiredCerts := []string{}
if err != nil {
log.Println(err)
return []string{}
}
for _, filename := range filenames {
certFilepath := filepath.Join("./conf/certs/", filename.Name())
certBytes, err := os.ReadFile(certFilepath)
if err != nil {
// Unable to load this file
continue
} else {
// Cert loaded. Check its expiry time
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
elapsed := time.Since(cert.NotAfter)
if elapsed > 0 {
// if it is expired then add it in
// make sure it's uniqueless
for _, dnsName := range cert.DNSNames {
if !contains(expiredCerts, dnsName) {
expiredCerts = append(expiredCerts, dnsName)
}
}
if !contains(expiredCerts, cert.Subject.CommonName) {
expiredCerts = append(expiredCerts, cert.Subject.CommonName)
}
}
}
}
}
}
return expiredCerts
}
// return the current port number
func (a *ACMEHandler) Getport() string {
return a.Port
}
// contains checks if a string is present in a slice.
func contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
// HandleGetExpiredDomains handles the HTTP GET request to retrieve the list of expired domains.
// It calls the CheckCertificate method to obtain the expired domains and sends a JSON response
// containing the list of expired domains.
func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Request) {
type ExpiredDomains struct {
Domain []string `json:"domain"`
}
info := ExpiredDomains{
Domain: a.CheckCertificate(),
}
js, _ := json.MarshalIndent(info, "", " ")
utils.SendJSONResponse(w, string(js))
}
// HandleRenewCertificate handles the HTTP GET request to renew a certificate for the provided domains.
// It retrieves the domains and filename parameters from the request, calls the ObtainCert method
// to renew the certificate, and sends a JSON response indicating the result of the renewal process.
func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
domainPara, err := utils.PostPara(r, "domains")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
filename, err := utils.PostPara(r, "filename")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
email, err := utils.PostPara(r, "email")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
ca, err := utils.PostPara(r, "ca")
if err != nil {
log.Println("CA not set. Using default (Let's Encrypt)")
ca = "Let's Encrypt"
}
domains := strings.Split(domainPara, ",")
result, err := a.ObtainCert(domains, filename, email, ca)
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
utils.SendJSONResponse(w, strconv.FormatBool(result))
}
// Escape JSON string
func jsonEscape(i string) string {
b, err := json.Marshal(i)
if err != nil {
log.Println("Unable to escape json data: " + err.Error())
return i
}
s := string(b)
return s[1 : len(s)-1]
}
// Helper function to check if a port is in use
func IsPortInUse(port int) bool {
address := fmt.Sprintf(":%d", port)
listener, err := net.Listen("tcp", address)
if err != nil {
return true // Port is in use
}
defer listener.Close()
return false // Port is not in use
}

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

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

View File

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

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

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

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

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

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

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

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

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

View File

@ -23,35 +23,32 @@ import (
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
General Access Check
Special Routing Rules, bypass most of the limitations
*/
//Check if this ip is in blacklist
clientIpAddr := geodb.GetRequesterIP(r)
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile("./web/forbidden.html")
if err != nil {
w.Write([]byte("403 - Forbidden"))
} else {
w.Write(template)
//Check if there are external routing rule matches.
//If yes, route them via external rr
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil {
//Matching routing rule found. Let the sub-router handle it
if matchedRoutingRule.UseSystemAccessControl {
//This matching rule request system access control.
//check access logic
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
}
h.logRequest(r, false, 403, "blacklist", "")
matchedRoutingRule.Route(w, r)
return
}
//Check if this ip is in whitelist
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile("./web/forbidden.html")
if err != nil {
w.Write([]byte("403 - Forbidden"))
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "whitelist", "")
/*
General Access Check
*/
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
}
@ -65,15 +62,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
//Check if there are external routing rule matches.
//If yes, route them via external rr
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
if matchedRoutingRule != nil {
//Matching routing rule found. Let the sub-router handle it
matchedRoutingRule.Route(w, r)
return
}
//Extract request host to see if it is virtual directory or subdomain
domainOnly := r.Host
if strings.Contains(r.Host, ":") {
@ -115,7 +103,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.proxyRequest(w, r, targetProxyEndpoint)
} else if !strings.HasSuffix(proxyingPath, "/") {
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil {
//Missing tailing slash. Redirect to target proxy endpoint
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
@ -128,3 +115,38 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.proxyRequest(w, r, h.Parent.Root)
}
}
// Handle access routing logic. Return true if the request is handled or blocked by the access control logic
// if the return value is false, you can continue process the response writer
func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Request) bool {
//Check if this ip is in blacklist
clientIpAddr := geodb.GetRequesterIP(r)
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
template, err := os.ReadFile("./web/forbidden.html")
if err != nil {
w.Write([]byte("403 - 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("./web/forbidden.html")
if err != nil {
w.Write([]byte("403 - Forbidden"))
} else {
w.Write(template)
}
h.logRequest(r, false, 403, "whitelist", "")
return true
}
return false
}

View File

@ -91,11 +91,12 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
//Hack the default transporter to handle more connections
thisTransporter := http.DefaultTransport
thisTransporter.(*http.Transport).MaxIdleConns = 3000
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = 3000
thisTransporter.(*http.Transport).IdleConnTimeout = 10 * time.Second
thisTransporter.(*http.Transport).MaxConnsPerHost = 0
//thisTransporter.(*http.Transport).DisableCompression = true
optimalConcurrentConnection := 32
thisTransporter.(*http.Transport).MaxIdleConns = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = optimalConcurrentConnection
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).DisableCompression = true
if ignoreTLSVerification {
//Ignore TLS certificate validation error
@ -278,6 +279,12 @@ func addXForwardedForHeader(req *http.Request) {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
req.Header.Set("X-Forwarded-For", clientIP)
if req.TLS != nil {
req.Header.Set("X-Forwarded-Proto", "https")
} else {
req.Header.Set("X-Forwarded-Proto", "http")
}
}
}
@ -351,11 +358,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
//Custom header rewriter functions
if res.Header.Get("Location") != "" {
/*
fmt.Println(">>> REQ", req)
fmt.Println(">>> OUTR", outreq)
fmt.Println(">>> RESP", res)
*/
locationRewrite := res.Header.Get("Location")
originLocation := res.Header.Get("Location")
res.Header.Set("zr-origin-location", originLocation)
@ -363,12 +365,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") {
//Full path
//Replace the forwarded target with expected Host
lr, err := replaceLocationHost(locationRewrite, rrr.OriginalHost, req.TLS != nil)
lr, err := replaceLocationHost(locationRewrite, rrr, req.TLS != nil)
if err == nil {
locationRewrite = lr
}
//locationRewrite = strings.ReplaceAll(locationRewrite, rrr.ProxyDomain, rrr.OriginalHost)
//locationRewrite = strings.ReplaceAll(locationRewrite, domainWithoutPort, rrr.OriginalHost)
} else if strings.HasPrefix(originLocation, "/") && rrr.PathPrefix != "" {
//Back to the root of this proxy object
//fmt.Println(rrr.ProxyDomain, rrr.OriginalHost)
@ -381,6 +381,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
//Custom redirection to this rproxy relative path
res.Header.Set("Location", locationRewrite)
}
// Copy header from response to client.
copyHeader(rw.Header(), res.Header)

View File

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

View File

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

View File

@ -45,6 +45,13 @@ func (router *Router) UpdateTLSSetting(tlsEnabled bool) {
router.Restart()
}
// Update TLS Version in runtime. Will restart proxy server if running.
// Set this to true to force TLS 1.2 or above
func (router *Router) UpdateTLSVersion(requireLatest bool) {
router.Option.ForceTLSLatest = requireLatest
router.Restart()
}
// Update https redirect, which will require updates
func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
router.Option.ForceHttpsRedirect = useRedirect
@ -62,8 +69,13 @@ func (router *Router) StartProxyService() error {
return errors.New("Reverse proxy router root not set")
}
minVersion := tls.VersionTLS10
if router.Option.ForceTLSLatest {
minVersion = tls.VersionTLS12
}
config := &tls.Config{
GetCertificate: router.Option.TlsManager.GetCert,
MinVersion: uint16(minVersion),
}
if router.Option.UseTls {
@ -171,18 +183,22 @@ func (router *Router) StopProxyService() error {
}
// Restart the current router if it is running.
// Startup the server if it is not running initially
func (router *Router) Restart() error {
//Stop the router if it is already running
var err error = nil
if router.Running {
err := router.StopProxyService()
if err != nil {
return err
}
// Start the server
err = router.StartProxyService()
if err != nil {
return err
}
}
//Start the server
err := router.StartProxyService()
return err
}

View File

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

View File

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

View File

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

View File

@ -22,12 +22,14 @@ type ProxyHandler struct {
}
type RouterOption struct {
Port int
UseTls bool
ForceHttpsRedirect bool
HostUUID string //The UUID of Zoraxy, use for heading mod
Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above
ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager
RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store
GeodbStore *geodb.Store //GeoIP blacklist and whitelist
StatisticCollector *statistic.Collector
}

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

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

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

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,8 @@ package geodb
import (
_ "embed"
"log"
"net"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/database"
)
@ -112,170 +112,6 @@ func (s *Store) Close() {
}
/*
Country code based black / white list
*/
func (s *Store) AddCountryCodeToBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("blacklist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromBlackList(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("blacklist-cn", countryCode)
}
func (s *Store) AddCountryCodeToWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("whitelist-cn", countryCode, true)
}
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Delete("whitelist-cn", countryCode)
}
func (s *Store) IsCountryCodeBlacklisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isBlacklisted bool = false
s.sysdb.Read("blacklist-cn", countryCode, &isBlacklisted)
return isBlacklisted
}
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isWhitelisted bool = false
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
return isWhitelisted
}
func (s *Store) GetAllBlacklistedCountryCode() []string {
bannedCountryCodes := []string{}
entries, err := s.sysdb.ListTable("blacklist-cn")
if err != nil {
return bannedCountryCodes
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedCountryCodes = append(bannedCountryCodes, ip)
}
return bannedCountryCodes
}
func (s *Store) GetAllWhitelistedCountryCode() []string {
whitelistedCountryCode := []string{}
entries, err := s.sysdb.ListTable("whitelist-cn")
if err != nil {
return whitelistedCountryCode
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedCountryCode = append(whitelistedCountryCode, ip)
}
return whitelistedCountryCode
}
/*
IP based black / whitelist
*/
func (s *Store) AddIPToBlackList(ipAddr string) {
s.sysdb.Write("blacklist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromBlackList(ipAddr string) {
s.sysdb.Delete("blacklist-ip", ipAddr)
}
func (s *Store) AddIPToWhiteList(ipAddr string) {
s.sysdb.Write("whitelist-ip", ipAddr, true)
}
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
s.sysdb.Delete("whitelist-ip", ipAddr)
}
func (s *Store) IsIPBlacklisted(ipAddr string) bool {
var isBlacklisted bool = false
s.sysdb.Read("blacklist-ip", ipAddr, &isBlacklisted)
if isBlacklisted {
return true
}
//Check for IP wildcard and CIRD rules
AllBlacklistedIps := s.GetAllBlacklistedIp()
for _, blacklistRule := range AllBlacklistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
if cidrMatch {
return true
}
}
return false
}
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
var isBlacklisted bool = false
s.sysdb.Read("whitelist-ip", ipAddr, &isBlacklisted)
if isBlacklisted {
return true
}
//Check for IP wildcard and CIRD rules
AllBlacklistedIps := s.GetAllBlacklistedIp()
for _, blacklistRule := range AllBlacklistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
if wildcardMatch {
return true
}
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
if cidrMatch {
return true
}
}
return false
}
func (s *Store) GetAllBlacklistedIp() []string {
bannedIps := []string{}
entries, err := s.sysdb.ListTable("blacklist-ip")
if err != nil {
return bannedIps
}
for _, keypairs := range entries {
ip := string(keypairs[0])
bannedIps = append(bannedIps, ip)
}
return bannedIps
}
func (s *Store) GetAllWhitelistedIp() []string {
whitelistedIp := []string{}
entries, err := s.sysdb.ListTable("whitelist-ip")
if err != nil {
return whitelistedIp
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedIp = append(whitelistedIp, ip)
}
return whitelistedIp
}
/*
Check if a IP address is blacklisted, in either country or IP blacklist
IsBlacklisted default return is false (allow access)
@ -341,6 +177,23 @@ func (s *Store) IsWhitelisted(ipAddr string) bool {
return false
}
// A helper function that check both blacklist and whitelist for access
// for both geoIP and ip / CIDR ranges
func (s *Store) AllowIpAccess(ipaddr string) bool {
if s.IsBlacklisted(ipaddr) {
return false
}
return s.IsWhitelisted(ipaddr)
}
func (s *Store) AllowConnectionAccess(conn net.Conn) bool {
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
return s.AllowIpAccess(addr.IP.String())
}
return true
}
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
ipAddr := GetRequesterIP(r)
if ipAddr == "" {

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"log"
"net/http"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
@ -220,7 +221,24 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
func getWebsiteStatus(url string) (int, error) {
resp, err := http.Get(url)
if err != nil {
return 0, err
//Try replace the http with https and vise versa
rewriteURL := ""
if strings.Contains(url, "https://") {
rewriteURL = strings.ReplaceAll(url, "https://", "http://")
} else if strings.Contains(url, "http://") {
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
}
resp, err = http.Get(rewriteURL)
if err != nil {
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
//Invalid downstream reverse proxy settings, but it is online
//return SSL handshake failed
return 525, nil
}
return 0, err
}
}
status_code := resp.StatusCode
resp.Body.Close()

View File

@ -38,6 +38,14 @@ func ReverseProxtInit() {
log.Println("TLS mode disabled. Serving proxy request with plain http")
}
forceLatestTLSVersion := false
sysdb.Read("settings", "forceLatestTLS", &forceLatestTLSVersion)
if forceLatestTLSVersion {
log.Println("Force latest TLS mode enabled. Minimum TLS LS version is set to v1.2")
} else {
log.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
}
forceHttpsRedirect := false
sysdb.Read("settings", "redirect", &forceHttpsRedirect)
if forceHttpsRedirect {
@ -47,8 +55,10 @@ func ReverseProxtInit() {
}
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
HostUUID: nodeUUID,
Port: inboundPort,
UseTls: useTls,
ForceTLSLatest: forceLatestTLSVersion,
ForceHttpsRedirect: forceHttpsRedirect,
TlsManager: tlsCertManager,
RedirectRuleTable: redirectTable,
@ -63,7 +73,7 @@ func ReverseProxtInit() {
dynamicProxyRouter = dprouter
//Load all conf from files
confs, _ := filepath.Glob("./conf/*.config")
confs, _ := filepath.Glob("./conf/proxy/*.config")
for _, conf := range confs {
record, err := LoadReverseProxyConfig(conf)
if err != nil {
@ -345,6 +355,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
dynamicProxyRouter.RemoveProxy("vdir", thisOption.RootName)
dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)
} else if eptype == "subd" {
@ -356,6 +367,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
dynamicProxyRouter.RemoveProxy("subd", thisOption.MatchingDomain)
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
}
@ -370,14 +382,6 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
SaveReverseProxyConfig(&thisProxyConfigRecord)
//Update the current running config
targetProxyEntry.Domain = endpoint
targetProxyEntry.RequireTLS = useTLS
targetProxyEntry.SkipCertValidations = skipTlsValidation
targetProxyEntry.RequireBasicAuth = requireBasicAuth
dynamicProxyRouter.SaveProxy(eptype, targetProxyEntry.RootOrMatchingDomain, targetProxyEntry)
utils.SendOK(w)
}

View File

@ -8,6 +8,7 @@ import (
"strings"
"time"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
@ -15,6 +16,7 @@ import (
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/mdns"
"imuslab.com/zoraxy/mod/netstat"
"imuslab.com/zoraxy/mod/pathrule"
"imuslab.com/zoraxy/mod/sshprox"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/statistic/analytic"
@ -47,8 +49,9 @@ func startupSequence() {
//Create tables for the database
sysdb.NewTable("settings")
//Create tmp folder
//Create tmp folder and conf folder
os.MkdirAll("./tmp", 0775)
os.MkdirAll("./conf/proxy/", 0775)
//Create an auth agent
sessionKey, err := auth.GetSessionKey(sysdb)
@ -61,13 +64,13 @@ func startupSequence() {
})
//Create a TLS certificate manager
tlsCertManager, err = tlscert.NewManager("./certs", development)
tlsCertManager, err = tlscert.NewManager("./conf/certs", development)
if err != nil {
panic(err)
}
//Create a redirection rule table
redirectTable, err = redirection.NewRuleTable("./rules")
redirectTable, err = redirection.NewRuleTable("./conf/redirect")
if err != nil {
panic(err)
}
@ -93,6 +96,18 @@ func startupSequence() {
panic(err)
}
/*
Path Rules
This section of starutp script start the path rules where
user can define their own routing logics
*/
pathRuleHandler = pathrule.NewPathRuleHandler(&pathrule.Options{
Enabled: false,
ConfigFolder: "./conf/rules/pathrules",
})
/*
MDNS Discovery Service
@ -163,7 +178,8 @@ func startupSequence() {
//Create TCP Proxy Manager
tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{
Database: sysdb,
Database: sysdb,
AccessControlHandler: geodbStore.AllowConnectionAccess,
})
//Create WoL MAC storage table
@ -175,4 +191,21 @@ func startupSequence() {
//Create an analytic loader
AnalyticLoader = analytic.NewDataLoader(sysdb, statisticCollector)
/*
ACME API
Obtaining certificates from ACME Server
*/
acmeHandler = initACME()
acmeAutoRenewer, err = acme.NewAutoRenewer("./conf/acme_conf.json", "./conf/certs/", int64(*acmeAutoRenewInterval), acmeHandler)
if err != nil {
log.Fatal(err)
}
}
// This sequence start after everything is initialized
func finalSequence() {
//Start ACME renew agent
acmeRegisterSpecialRoutingRule()
}

View File

@ -1082,24 +1082,51 @@
//Check if a input is a valid IP address, wildcard of a IP address or a CIDR string
function isValidIpFilter(input) {
// Check if input is a valid IP address
const isValidIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(input);
// Check if input is a valid IPv4 address
const isValidIPv4 = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(input);
if (isValidIp) {
if (isValidIPv4) {
return true;
}
// Check if input is a wildcard IP address
const isValidWildcardIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])$/.test(input);
// Check if input is a valid IPv4 wildcard address
const isValidIPv4Wildcard = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])$/.test(input);
if (isValidWildcardIp) {
if (isValidIPv4Wildcard) {
return true;
}
// Check if input is a valid CIDR address string
const isValidCidr = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2][0-9]|3[0-2])$/.test(input);
// Check if input is a valid IPv4 CIDR address
const isValidIPv4CIDR = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2][0-9]|3[0-2])$/.test(input);
if (isValidCidr) {
if (isValidIPv4CIDR) {
return true;
}
// Check if input is loopback ipv6
if (input == "::1"){
return true;
}
// Check if input is a valid IPv6 address
const isValidIPv6 = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/.test(input);
if (isValidIPv6) {
return true;
}
//Pure magic, I have no idea how this works
//src: https://stackoverflow.com/questions/70348674/alternate-solution-validate-ipv4-and-ipv6-with-wildcard-characters-using-r
function evalIp6(t){var e=t.split(":"),n=t.split("::").length-1;if(8<e.length&&(9!=t.split(":").length||""!=e[e.length-1]||1!=n))return!1;if(1<n)return!1;if(-1!=t.indexOf("::*")||-1!=t.indexOf("*::"))return!1;var r=!1;for(let t=0;t<e.length;t++){if(!isIPV6Group(e[t]))return!1;"*"==e[t]&&(r=!0)}return!(!r&&0==n&&8!=e.length)}function isIPV6Group(t){var e="^(([0-9A-Fa-f]{1,4})|\\*|)$";return(e=new RegExp(e)).test(t)}
if (evalIp6(input)){
return true;
}
// Check if input is a valid IPv6 CIDR address
const isValidIPv6CIDR = /^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/.test(input);
if (isValidIPv6CIDR) {
return true;
}

View File

@ -67,6 +67,7 @@
</tbody>
</table>
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow refresh icon"></i> Auto Renew (ACME) Settings</button>
</div>
<div class="ui message">
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
@ -106,11 +107,14 @@
msgbox(data.error, false, 5000);
}else{
$("#certifiedDomainList").html("");
data.sort((a,b) => {
return a.Domain > b.Domain
});
data.forEach(entry => {
$("#certifiedDomainList").append(`<tr>
<td>${entry.Domain}</td>
<td>${entry.LastModifiedDate}</td>
<td>${entry.ExpireDate}</td>
<td>${entry.ExpireDate} (${entry.RemainingDays} days left)</td>
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
</tr>`);
});
@ -125,6 +129,10 @@
}
initManagedDomainCertificateList();
function openACMEManager(){
showSideWrapper('snippet/acme.html');
}
function handleDomainUploadByKeypress(){
handleDomainKeysUpload(function(){
$("#certUploadingDomain").text($("#certdomain").val().trim());

View File

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

View File

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

View File

@ -72,11 +72,30 @@
<label>Use TLS to serve proxy request</label>
</div>
<br>
<div id="redirect" class="ui toggle notloopbackOnly checkbox" style="margin-top: 0.6em;">
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
<input type="checkbox">
<label>Force redirect HTTP request to HTTPS<br>
<small>(Only apply when listening port is not 80)</small></label>
</div>
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<p>If you have no idea what are these, you can leave them as default :)</p>
<div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
<input type="checkbox">
<label>Force TLS v1.2 or above<br>
<small>(Enhance security, but not compatible with legacy browsers)</small></label>
</div>
<br>
</div>
</div>
</div>
<br><br>
<button id="startbtn" class="ui teal button" onclick="startService();">Start Service</button>
<button id="stopbtn" class="ui red notloopbackOnly disabled button" onclick="stopService();">Stop Service</button>
@ -128,6 +147,8 @@
</div>
<script>
let loopbackProxiedInterface = false;
$(".advanceSettings").accordion();
//Initial the start stop button if this is reverse proxied
$.get("/api/proxy/requestIsProxied", function(data){
if (data == true){
@ -340,21 +361,50 @@
}
initHTTPtoHTTPSRedirectSetting();
function initTlsVersionSetting(){
$.get("/api/cert/tlsRequireLatest", function(data){
if (data == true){
$("#tlsMinVer").checkbox("set checked");
}else{
$("#tlsMinVer").checkbox("set unchecked");
}
//Bind events to the checkbox
$("#tlsMinVer").find("input").on("change", function(){
let thisValue = $("#tlsMinVer").checkbox("is checked");
$.ajax({
url: "/api/cert/tlsRequireLatest",
data: {"set": thisValue},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
}else{
msgbox("TLS Version Setting Updated");
}
}
})
});
});
}
initTlsVersionSetting();
function initTlsSetting(){
$.get("/api/cert/tls", function(data){
if (data == true){
$("#tls").checkbox("set checked");
}else{
$("#redirect").addClass('disabled');
$(".tlsEnabledOnly").addClass('disabled');
$(".tlsEnabledOnly").addClass('disabled');
}
//Initiate the input listener on the checkbox
$("#tls").find("input").on("change", function(){
let thisValue = $("#tls").checkbox("is checked");
if (thisValue){
$("#redirect").removeClass('disabled');
$(".tlsEnabledOnly").removeClass('disabled');
}else{
$("#redirect").addClass('disabled');
$(".tlsEnabledOnly").addClass('disabled');
}
$.ajax({
url: "/api/cert/tls",
@ -364,7 +414,27 @@
alert(data.error);
}else{
//Updated
msgbox("Setting Updated");
//Check for case if the port is invalid default ports
if ($("#incomingPort").val() == "80" && thisValue == true){
confirmBox("Change listen port to :443?", function(choice){
if (choice == true){
$("#incomingPort").val("443");
handlePortChange();
}
});
}else if ($("#incomingPort").val() == "443" && thisValue == false){
confirmBox("Change listen port to :80?", function(choice){
if (choice == true){
$("#incomingPort").val("80");
handlePortChange();
}
});
}else{
msgbox("Setting Updated");
}
initRPStaste();
}
}

View File

@ -43,10 +43,17 @@
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
}
let tlsVerificationField = "";
if (subd.RequireTLS){
tlsVerificationField = !subd.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`
}else{
tlsVerificationField = "N/A"
}
$("#subdList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
<td data-label="" editable="false"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a></td>
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="skipver">${!subd.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`}</td>
<td data-label="" editable="true" datatype="skipver">${tlsVerificationField}</td>
<td data-label="" editable="true" datatype="basicauth">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
<td class="center aligned" editable="true" datatype="action" data-label="">
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="edit icon"></i></button>

View File

@ -42,11 +42,75 @@
<button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui blue add icon"></i> Create</button>
<button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);"><i class="ui blue save icon"></i> Update</button>
<button class="ui basic red button" onclick="event.preventDefault(); cancelTCPProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
<div class="ui basic inverted segment" style="background-color: #414141; border-radius: 0.6em;">
<h3>Proxy Mode Instructions</h3>
<p>TCP Proxy support the following TCP sockets proxy modes</p>
<table class="ui celled padded inverted basic table">
<thead>
<tr><th class="single line">Mode</th>
<th>Public-IP</th>
<th>Concurrent Access</th>
<th>Flow Diagram</th>
</tr></thead>
<tbody>
<tr>
<td>
<h4 class="ui center aligned inverted header">Transport</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui green check icon"></i> (or same LAN)<br>
</td>
<td>
<i class="ui green check icon"></i>
</td>
<td>Port A (e.g. 25565) <i class="arrow right icon"></i> Server<br>
Server <i class="arrow right icon"></i> Port B (e.g. 192.168.0.2:25565)<br>
<small>Traffic from Port A will be forward to Port B's (IP if provided and) Port</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Listen</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui remove icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Port A (e.g. 8080) <i class="arrow right icon"></i> Server<br>
Port B (e.g. 8081) <i class="arrow right icon"></i> Server<br>
<small>Server will act as a bridge to proxy traffic between Port A and B</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Starter</h4>
</td>
<td class="single line">
Server: <i class="ui times icon"></i><br>
A: <i class="ui green check icon"></i><br>
B: <i class="ui green check icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Server <i class="arrow right icon"></i> Port A (e.g. remote.local.:8080) <br>
Server <i class="arrow right icon"></i> Port B (e.g. recv.local.:8081) <br>
<small>Port A and B will be actively bridged</small>
</td>
</tr>
</tbody>
</table>
</div>
</form>
<div class="ui divider"></div>
</div>
<div class="ui basic segment">
<div class="ui basic segment" style="margin-top: 0;">
<h3>TCP Proxy Configs</h3>
<p>A list of TCP proxy configs created on this host. To enable them, use the toggle button on the right.</p>
<div style="overflow-x: auto; min-height: 400px;">
@ -67,72 +131,7 @@
</table>
</div>
</div>
<div class="ui basic inverted segment" style="background-color: #414141; border-radius: 0.6em;">
<h3>Proxy Mode</h3>
<p>TCP Proxy support the following TCP sockets proxy modes</p>
<table class="ui celled padded inverted basic table">
<thead>
<tr><th class="single line">Mode</th>
<th>Public-IP</th>
<th>Concurrent Access</th>
<th>Flow Diagram</th>
</tr></thead>
<tbody>
<tr>
<td>
<h4 class="ui center aligned inverted header">Transport</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui green check icon"></i> (or same LAN)<br>
</td>
<td>
<i class="ui green check icon"></i>
</td>
<td>Port A (e.g. 25565) <i class="arrow right icon"></i> Server<br>
Server <i class="arrow right icon"></i> Port B (e.g. 192.168.0.2:25565)<br>
<small>Traffic from Port A will be forward to Port B's (IP if provided and) Port</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Listen</h4>
</td>
<td class="single line">
Server: <i class="ui green check icon"></i><br>
A: <i class="ui remove icon"></i><br>
B: <i class="ui remove icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Port A (e.g. 8080) <i class="arrow right icon"></i> Server<br>
Port B (e.g. 8081) <i class="arrow right icon"></i> Server<br>
<small>Server will act as a bridge to proxy traffic between Port A and B</small>
</td>
</tr>
<tr>
<td>
<h4 class="ui center aligned inverted header">Starter</h4>
</td>
<td class="single line">
Server: <i class="ui times icon"></i><br>
A: <i class="ui green check icon"></i><br>
B: <i class="ui green check icon"></i><br>
</td>
<td>
<i class="ui red times icon"></i>
</td>
<td>Server <i class="arrow right icon"></i> Port A (e.g. remote.local.:8080) <br>
Server <i class="arrow right icon"></i> Port B (e.g. recv.local.:8081) <br>
<small>Port A and B will be actively bridged</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
let editingTCPProxyConfigUUID = ""; //The current editing TCP Proxy config UUID

View File

@ -109,7 +109,13 @@
}
ontimeRate++;
}else{
dotType = "offline";
if (thisStatus.StatusCode >= 500 && thisStatus.StatusCode < 600){
//Special type of error, cause by downstream reverse proxy
dotType = "error";
}else{
dotType = "offline";
}
}
let datetime = format_time(thisStatus.Timestamp);
@ -126,12 +132,20 @@
//Check of online status now
let currentOnlineStatus = "Unknown";
let onlineStatusCss = ``;
let reminderEle = ``;
if (value[value.length - 1].Online){
currentOnlineStatus = `<i class="circle icon"></i> Online`;
onlineStatusCss = `color: #3bd671;`;
}else{
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
onlineStatusCss = `color: #df484a;`;
if (value[value.length - 1].StatusCode >= 500 && value[value.length - 1].StatusCode < 600){
currentOnlineStatus = `<i class="exclamation circle icon"></i> Misconfigured`;
onlineStatusCss = `color: #f38020;`;
reminderEle = `<small style="${onlineStatusCss}">Downstream proxy server is online with misconfigured settings</small>`;
}else{
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
onlineStatusCss = `color: #df484a;`;
}
}
//Generate the html
@ -151,6 +165,7 @@
<div class="status" style="marign-top: 1em;">
${statusDotList}
</div>
${reminderEle}
<div class="ui divider"></div>
</div>`);
}

View File

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

View File

@ -44,10 +44,18 @@
if (vdir.RequireTLS){
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
}
let tlsVerificationField = "";
if (vdir.RequireTLS){
tlsVerificationField = !vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`
}else{
tlsVerificationField = "N/A"
}
$("#vdirList").append(`<tr eptuuid="${vdir.RootOrMatchingDomain}" payload="${vdirData}" class="vdirEntry">
<td data-label="" editable="false">${vdir.RootOrMatchingDomain}</td>
<td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="skipver">${!vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`}</td>
<td data-label="" editable="true" datatype="skipver">${tlsVerificationField}</td>
<td data-label="" editable="true" datatype="basicauth">${vdir.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
<td class="center aligned" editable="true" datatype="action" data-label="">
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="edit icon"></i></button>

BIN
src/web/img/public/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

BIN
src/web/img/public/bg2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 MiB

View File

@ -148,6 +148,17 @@
<p><i class="green check circle icon"></i> There are no message</p>
</div>
<div id="confirmBox" style="display:none;">
<div class="ui top attached progress">
<div class="bar" style="width: 100%; min-width: 0px;"></div>
</div>
<div class="confirmBoxBody">
<button class="ui red basic mini circular icon right floated button" style="margin-left: 0.4em;"><i class="ui times icon"></i></button>
<button class="ui green basic mini circular icon right floated button"><i class="ui check icon"></i></button>
<div class="questionToConfirm">Confirm Exit?</div>
</div>
</div>
<br><br>
<script>
$(".year").text(new Date().getFullYear());
@ -280,6 +291,64 @@
$("#messageBox").stop().finish().fadeIn("fast").delay(delayDuration).fadeOut("fast");
}
function confirmBox(question_to_confirm, callback, delaytime=15) {
var progressBar = $("#confirmBox .bar");
var questionElement = $("#confirmBox .questionToConfirm");
//Just to make sure there are no animation runnings
progressBar.stop();
// Update the question to confirm
questionElement.text(question_to_confirm);
// Start the progress bar animation
progressBar.css("width", "100%");
progressBar.animate({ width: "0%", easing: "linear" }, delaytime * 1000, function() {
// Animation complete, invoke the callback with undefined
callback(undefined);
//Unset the event listener
$("#confirmBox .ui.green.button").off("click");
// Hide the confirm box
$("#confirmBox").hide();
});
// Bind click event to "Yes" button
$("#confirmBox .ui.green.button").on("click", function() {
// Stop the progress bar animation
progressBar.stop();
// Invoke the callback with true
callback(true);
// Hide the confirm box
$("#confirmBox").hide();
//Unset the event listener
$("#confirmBox .ui.green.button").off("click");
});
// Bind click event to "No" button
$("#confirmBox .ui.red.button").on("click", function() {
// Stop the progress bar animation
progressBar.stop();
// Invoke the callback with false
callback(false);
// Hide the confirm box
$("#confirmBox").hide();
//Unset the event listener
$("#confirmBox .ui.red.button").off("click");
});
// Show the confirm box
$("#confirmBox").show().transition('jiggle');
}
/*
Toggles for side wrapper
*/
@ -295,6 +364,7 @@
$(".sideWrapper").show();
$(".sideWrapper .fadingBackground").fadeIn("fast");
$(".sideWrapper .content").transition('slide left in', 300);
$("body").css("overflow", "hidden");
}
function hideSideWrapper(discardFrameContent = false){
@ -309,6 +379,7 @@
$(".sideWrapper").hide();
});
});
$("body").css("overflow", "auto");
}
</script>
</body>

View File

@ -23,7 +23,7 @@
width: 100%;
opacity: 0.8;
z-index: -99;
background-image: url("img/public/bg.png");
background-image: url("img/public/bg.jpg");
background-size: auto 100%;
background-position: right top;
background-repeat: no-repeat;

View File

@ -6,6 +6,7 @@
--theme_lgrey: #f6f6f6;
--theme_green: #3c9c63;
--theme_fcolor: #979797;
--theme_advance: #f8f8f9;
}
body{
background-color:#f6f6f6;
@ -16,6 +17,15 @@ body{
display:none;
}
.advance{
background: var(--theme_advance) !important;
}
.advancezone{
background: var(--theme_advance) !important;
border-radius: 1em !important;
}
.menubar{
width: 100%;
padding: 0.4em;
@ -89,6 +99,34 @@ body{
z-index: 999;
}
/* Confirm Box */
#confirmBox{
position: fixed;
z-index: 999;
bottom: 1em;
right: 1em;
min-width: 300px;
background-color: #ffffff;
color: rgb(65, 65, 65);
box-shadow: 10px 10px 5px -2px rgba(0,0,0,0.13);
}
#confirmBox .confirmBoxBody{
padding: 1em;
}
#confirmBox .ui.progress .bar{
background: #ffe32b !important;
}
#confirmBox .confirmBoxBody .button{
margin-top: -0.4em;
}
#confirmBox .questionToConfirm{
margin-top: -0.2em;
}
/* Standard containers */
.standardContainer{
position: relative;
@ -141,7 +179,7 @@ body{
}
.sideWrapper iframe{
height: 100%;
height: calc(100% - 55px);
width: 100%;
border: 0px solid transparent;
}
@ -552,4 +590,18 @@ body{
.GANetMember.unauthorized{
border-left: 6px solid #9c3c3c !important;
}
}
/*
Network Utilities
*/
#traceroute_results{
resize: none;
background-color: #202020;
color: white;
}
#traceroute_results::selection {
background: #a9d1f3;
}

View File

@ -23,7 +23,7 @@
width: 100%;
opacity: 0.8;
z-index: -99;
background-image: url("img/public/bg2.png");
background-image: url("img/public/bg2.jpg");
background-size: auto 100%;
background-position: right top;
background-repeat: no-repeat;

475
src/web/snippet/acme.html Normal file
View File

@ -0,0 +1,475 @@
<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
<style>
.disabled.table{
opacity: 0.5;
pointer-events: none;
}
.expiredDomain{
color: rgb(238, 31, 31);
}
.validDomain{
color: rgb(49, 192, 113);
}
</style>
</head>
<body>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Certificates Auto Renew Settings
<div class="sub header">Fetch and renew your certificates with Automated Certificate Management Environment (ACME) protocol</div>
</div>
</div>
<div class="ui basic segment">
<p style="float: right; color: #21ba45; display:none;" id="enableToggleSucc"><i class="green checkmark icon"></i> Setting Updated</p>
<div class="ui toggle checkbox">
<input type="checkbox" id="enableCertAutoRenew">
<label>Enable Certificate Auto Renew</label>
</div>
<br>
<h3>ACME Email</h3>
<p>Email is required by many CAs for renewing via ACME protocol</p>
<div class="ui fluid action input">
<input id="caRegisterEmail" type="text" placeholder="webmaster@example.com">
<button class="ui icon basic button" onclick="saveEmailToConfig(this);">
<i class="blue save icon"></i>
</button>
</div>
<small>If you don't want to share your private email address, you can also fill in an email address that point to a mailbox not exists on your domain.</small>
</div>
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Renew Policy
</div>
<div class="content">
<p>Renew all certificates with ACME supported CAs</p>
<div class="ui toggle checkbox">
<input type="checkbox" id="renewAllSupported" onchange="setAutoRenewIfCASupportMode(this.checked);">
<label>Renew All Certs</label>
</div><br>
<button id="renewNowBtn" onclick="renewNow();" class="ui basic right floated button" style="margin-top: -2em;"><i class="yellow refresh icon"></i> Renew Now</button>
<div class="ui horizontal divider"> OR </div>
<p>Select the certificates to automatic renew in the list below</p>
<table id="domainCertFileTable" class="ui very compact unstackable basic disabled table">
<thead>
<tr>
<th>Domain Name</th>
<th>Match Rule</th>
<th>Auto-Renew</th>
</tr>
</thead>
<tbody id="domainTableBody"></tbody>
</table>
<small><i class="ui red info circle icon"></i> Domain in red are expired</small><br>
<div class="ui yellow message">
Certificate Renew only works on the certification authority (CA) supported by Zoraxy. Check Zoraxy wiki for more information on supported list of CAs.
</div>
<button class="ui basic right floated button" onclick="saveAutoRenewPolicy();"><i class="blue save icon"></i> Save Changes</button>
<button id="renewSelectedButton" onclick="renewNow();" class="ui basic right floated disabled button"><i class="yellow refresh icon"></i> Renew Selected</button>
<br><br>
</div>
</div>
</div>
<div class="ui divider"></div>
<h3>Manual Renew</h3>
<p>Pick a certificate below to force renew</p>
<div class="ui form">
<div class="field">
<label>Domain(s)</label>
<input id="domainsInput" type="text" placeholder="example.com" onkeyup="checkIfInputDomainIsMultiple();">
<small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. s1.dev.example.com,s2.dev.example.com)</small>
</div>
<div class="field multiDomainOnly" style="display:none;">
<label>Matching Rule</label>
<input id="filenameInput" type="text" placeholder="Enter filename (no file extension)">
<small>Matching rule to let Zoraxy pick which certificate to use (Also be used as filename). Usually is the longest common suffix of the entered addresses. (e.g. dev.example.com)</small>
</div>
<div class="field multiDomainOnly" style="display:none;">
<button class="ui basic fluid button" onclick="autoDetectMatchingRules();">Auto Detect Matching Rule</button>
</div>
<div class="field">
<label>Certificate Authority (CA)</label>
<div class="ui selection dropdown" id="ca">
<input type="hidden" name="ca">
<i class="dropdown icon"></i>
<div class="default text">Let's Encrypt</div>
<div class="menu">
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
<!-- <div class="item" data-value="Google">Google</div> -->
</div>
</div>
</div>
<button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Renew Certificate</button>
</div>
<div class="ui divider"></div>
<small>First time setting up HTTPS?<br>Try out our <a href="../tools/https.html" target="_blank">wizard</a></small>
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
<br><br><br><br>
</div>
<script>
let expiredDomains = [];
let enableTrigerOnChangeEvent = true;
$(".accordion").accordion();
$(".dropdown").dropdown();
function setAutoRenewIfCASupportMode(useAutoMode = true){
if (useAutoMode){
$("#domainCertFileTable").addClass("disabled");
$("#renewNowBtn").removeClass("disabled");
$("#renewSelectedButton").addClass("disabled");
}else{
$("#domainCertFileTable").removeClass("disabled");
$("#renewNowBtn").addClass("disabled");
$("#renewSelectedButton").removeClass("disabled");
}
}
function initRenewerConfigFromFile(){
//Set the renew switch state
$.get("/api/acme/autoRenew/enable", function(data){
if (data == true){
$("#enableCertAutoRenew").parent().checkbox("set checked");
}
$("#enableCertAutoRenew").on("change", function(){
if (!enableTrigerOnChangeEvent){
return;
}
toggleAutoRenew();
})
});
//Load the email from server side
$.get("/api/acme/autoRenew/email", function(data){
if (data != "" && data != undefined && data != null){
$("#caRegisterEmail").val(data);
}
});
//Load the domain selection options
$.get("/api/acme/autoRenew/renewPolicy", function(data){
if (data == true){
$("#renewAllSupported").parent().checkbox("set checked");
}else{
$("#renewAllSupported").parent().checkbox("set unchecked");
}
});
}
initRenewerConfigFromFile();
function saveEmailToConfig(btn){
$.ajax({
url: "/api/acme/autoRenew/email",
data: {set: $("#caRegisterEmail").val()},
success: function(data){
if (data.error != undefined){
parent.msgbox(data.error, false, 5000);
}else{
parent.msgbox("Email updated");
$(btn).html(`<i class="green check icon"></i>`);
$(btn).addClass("disabled");
setTimeout(function(){
$(btn).html(`<i class="blue save icon"></i>`);
$(btn).removeClass("disabled");
}, 3000);
}
}
});
}
function toggleAutoRenew(){
var enabled = $("#enableCertAutoRenew").parent().checkbox("is checked");
$.post("/api/acme/autoRenew/enable?enable=" + enabled, function(data){
if (data.error){
parent.msgbox(data.error, false, 5000);
if (enabled){
enableTrigerOnChangeEvent = false;
$("#enableCertAutoRenew").parent().checkbox("set unchecked");
enableTrigerOnChangeEvent = true;
}
}else{
$("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
}
});
}
//Render the domains table that exists in this zoraxy host
function renderDomainTable(domainFileList) {
// Get the table body element
var tableBody = $('#domainTableBody');
// Clear the table body
tableBody.empty();
// Iterate over the domain names
var counter = 0;
for (const [srcfile, domains] of Object.entries(domainFileList)) {
// Create a table row
var row = $('<tr>');
// Create the domain name cell
var domainClass = "validDomain";
for (var i = 0; i < domains.length; i++){
let thisDomain = domains[i];
if (expiredDomains.includes(thisDomain)){
domainClass = "expiredDomain";
}
}
var domainCell = $('<td class="' + domainClass +'">').html(domains.join("<br>"));
row.append(domainCell);
var srcFileCell = $('<td>').text(srcfile);
row.append(srcFileCell);
// Create the auto-renew checkbox cell
let domainsEncoded = encodeURIComponent(JSON.stringify(domains));
var checkboxCell = $(`<td domain="${domainsEncoded}" srcfile="${srcfile}">`);
var checkbox = $(`<input name="${srcfile}">`).attr('type', 'checkbox');
checkboxCell.append(checkbox);
row.append(checkboxCell);
// Add the row to the table body
tableBody.append(row);
counter++;
}
if (Object.keys(domainFileList).length == 0){
//No certificate in this system
tableBody.append(`<tr>
<td colspan="3"><i class="ui green circle check icon"></i> No certificate in use</td>
</tr>`);
}
}
//Initiate domain table. If you needs to update the expired domain as well
//call from initDomainFileList() instead
function initDomainTable(){
$.get("/api/cert/listdomains?compact=true", function(data){
if (data.error != undefined){
parent.msgbox(data.error, false);
}else{
renderDomainTable(data);
}
initAutoRenewPolicy();
})
}
function initDomainFileList() {
$.ajax({
url: "/api/acme/listExpiredDomains",
method: "GET",
success: function(response) {
// Render domain table
expiredDomains = response.domain;
initDomainTable();
//renderDomainTable(response.domain);
},
error: function(error) {
console.log("Failed to fetch expired domains:", error);
}
});
}
initDomainFileList();
// Button click event handler for obtaining certificate
$("#obtainButton").click(function() {
$("#obtainButton").addClass("loading").addClass("disabled");
obtainCertificate();
});
// Obtain certificate from API
function obtainCertificate() {
var domains = $("#domainsInput").val();
var filename = $("#filenameInput").val();
var email = $("#caRegisterEmail").val();
if (email == ""){
parent.msgbox("ACME renew email is not set")
return;
}
if (filename.trim() == "" && !domains.includes(",")){
//Zoraxy filename are the matching name for domains.
//Use the same as domains
filename = domains;
}else if (filename != "" && !domains.includes(",")){
//Invalid settings. Force the filename to be same as domain
//if there are only 1 domain
filename = domains;
}else{
parent.msgbox("Filename cannot be empty for certs containing multiple domains.")
return;
}
var ca = $("#ca").dropdown("get value");
$.ajax({
url: "/api/acme/obtainCert",
method: "GET",
data: {
domains: domains,
filename: filename,
email: email,
ca: ca,
},
success: function(response) {
$("#obtainButton").removeClass("loading").removeClass("disabled");
if (response.error) {
console.log("Error:", response.error);
// Show error message
parent.msgbox(response.error, false, 12000);
} else {
console.log("Certificate renewed successfully");
// Show success message
parent.msgbox("Certificate renewed successfully");
// Renew the parent certificate list
parent.initManagedDomainCertificateList();
}
},
error: function(error) {
$("#obtainButton").removeClass("loading").removeClass("disabled");
console.log("Failed to renewed certificate:", error);
}
});
}
function checkIfInputDomainIsMultiple(){
var inputDomains = $("#domainsInput").val();
if (inputDomains.includes(",")){
$(".multiDomainOnly").show();
}else{
$(".multiDomainOnly").hide();
}
}
//Grab the longest common suffix of all domains
//not that smart technically
function autoDetectMatchingRules(){
var domainsString = $("#domainsInput").val();
if (!domainsString.includes(",")){
return domainsString;
}
let domains = domainsString.split(",");
//Clean out any spacing between commas
for (var i = 0; i < domains.length; i++){
domains[i] = domains[i].trim();
}
function getLongestCommonSuffix(strings) {
if (strings.length === 0) {
return ''; // Return an empty string if the array is empty
}
var sortedStrings = strings.slice().sort(); // Create a sorted copy of the array
var firstString = sortedStrings[0];
var lastString = sortedStrings[sortedStrings.length - 1];
var suffix = '';
var minLength = Math.min(firstString.length, lastString.length);
for (var i = 0; i < minLength; i++) {
if (firstString[firstString.length - 1 - i] !== lastString[lastString.length - 1 - i]) {
break; // Stop iterating if characters don't match
}
suffix = firstString[firstString.length - 1 - i] + suffix;
}
return suffix;
}
let longestSuffix = getLongestCommonSuffix(domains);
//Check if the suffix is a valid domain
if (longestSuffix.substr(0,1) == "."){
//Trim off the first dot
longestSuffix = longestSuffix.substr(1);
}
if (!longestSuffix.includes(".")){
parent.msgbox("Auto Detect failed: Multiple Domains", false, 5000);
return;
}
$("#filenameInput").val(longestSuffix);
}
//Handle the renew now btn click
function renewNow(){
$.get("/api/acme/autoRenew/renewNow", function(data){
if (data.error != undefined){
parent.msgbox(data.error, false, 6000);
}else{
parent.msgbox(data)
}
})
}
function initAutoRenewPolicy(){
$.get("/api/acme/autoRenew/listDomains", function(data){
if (data.error != undefined){
parent.msgbox(data.error, false)
}else{
if (data[0] == "*"){
//Auto select and renew is enabled
$("#renewAllSupported").parent().checkbox("set checked");
}else{
//This is a list of domain files
data.forEach(function(name) {
$('#domainTableBody input[type="checkbox"][name="' + name + '"]').prop('checked', true);
});
$("#domainCertFileTable").removeClass("disabled");
$("#renewNowBtn").addClass("disabled");
$("#renewSelectedButton").removeClass("disabled");
}
}
})
}
function saveAutoRenewPolicy(){
let autoRenewAll = $("#renewAllSupported").parent().checkbox("is checked");
if (autoRenewAll == true){
$.ajax({
url: "/api/acme/autoRenew/setDomains",
data: {opr: "setAuto"},
success: function(data){
parent.msgbox("Renew policy rule updated")
}
});
}else{
let checkedNames = [];
$('#domainTableBody input[type="checkbox"]:checked').each(function() {
checkedNames.push($(this).attr('name'));
});
$.ajax({
url: "/api/acme/autoRenew/setDomains",
data: {opr: "setSelected", domains: JSON.stringify(checkedNames)},
success: function(data){
parent.msgbox("Renew policy rule updated")
}
});
}
}
//Clear up the input field when page load
$("#filenameInput").val("");
</script>
</body>
</html>

View File

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
</head>
<body>
<br>
<div class="ui container">
<!-- Path Rules -->
<div class="ui header">
<div class="content">
Special Path Rules
<div class="sub header">Advanced customization for response on particular matching path or URL</div>
</div>
</div>
<h4>Current list of special path rules.</h4>
<div style="width: 100%; overflow-x: auto;">
<table class="ui sortable unstackable celled table" >
<thead>
<tr>
<th>Matching Path</th>
<th>Status Code</th>
<th class="no-sort">Exact Match</th>
<th class="no-sort">Case Sensitive</th>
<th class="no-sort">Enabled</th>
<th class="no-sort">Actions</th>
</tr>
</thead>
<tbody id="specialPathRules">
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="ui divider"></div>
<h4>Add Special Path Rule</h4>
<div class="ui form">
<div class="field">
<label>Matching URI</label>
<input type="text" name="matchingPath" placeholder="Matching URL">
<small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be handled by this rule, e.g. example.com/secret</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="exactMatch" tabindex="0" class="hidden">
<label>Require Exact Match</label>
</div>
<div class="ui message">
<p>Require exactly match but not prefix match (default). Enable this if you only want to block access to a directory but not the resources inside the directory. Assume you have entered a matching URI of <b>example.com/secret/</b> and set it to return 401</p>
<i class="check square outline icon"></i> example.com/secret<b>/image.png</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> (content of image.png)<br>
<i class="square outline icon"></i> example.com/secret<b>/image.png</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> HTTP 401
</div>
</div>
<div class="field">
<label>Response Status Code</label>
<input type="text"name="statusCode" placeholder="500">
<small><i class="ui circle info icon"></i> HTTP Status Code to be served by this rule</small>
</div>
</div>
<br><br>
<button class="ui basic button iframeOnly" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
</div>
<script>
</script>
</body>
</html>

View File

@ -95,6 +95,10 @@
loadDateRange();
function isValidDateFormat(dateString) {
if (dateString.indexOf("_") >= 0){
//Replace all the _ to -
dateString = dateString.split("_").join("-");
}
// Create a regular expression pattern for the yyyy-mm-dd format
const pattern = /^\d{4}-\d{2}-\d{2}$/;

View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
</head>
<body>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Config Export and Import Tool
<div class="sub header">Painless migration with one click</div>
</div>
</div>
<h3>Backup Current Configs</h3>
<p>This will download all your configuration on zoraxy in a zip file. This includes all the proxy configs and certificates. Please keep it somewhere safe and after migration, delete this if possible.</p>
<div class="ui form">
<div class="grouped fields">
<label>Backup Mode</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="rules" name="backupmode" checked="checked">
<label>Proxy Settings, Redirect Rules and Certificates Only</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="full" name="backupmode">
<label>Full System Snapshot</label>
</div>
</div>
</div>
</div>
<br>
<button class="ui basic button" onclick="downloadConfig();"><i class="ui blue download icon"></i> Download</button>
<div class="ui divider"></div>
<h3>Restore from Config</h3>
<p>You can restore your previous settings and database from a zip file config backup.
<br><b style="color: rgba(255, 0, 0, 0.644);">RESTORE FULL SYSTEM SNAPSHOT WILL CAUSE THE SYSTEM TO SHUTDOWN AFTER COMPLETED. Make sure your Zoraxy is configured to work with systemd to automatic restart Zoraxy after system restore completed.<br>
</b></p>
<form class="ui form" id="uploadForm" action="/api/conf/import" method="POST" enctype="multipart/form-data">
<input type="file" name="file" id="fileInput" accept=".zip">
<button style="margin-top: 0.6em;" class="ui basic button" type="submit"><i class="ui green upload icon"></i> Upload</button>
</form>
<small>The current config will be backup to ./conf_old{timestamp}. If you screw something up, you can always restore it by ssh to your host and restore the configs from the old folder.</small>
<br><br>
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
</div>
<script>
$(".checkbox").checkbox();
function getCheckedRadioValue() {
var checkedValue = $("input[name='backupmode']:checked").val();
return checkedValue;
}
function downloadConfig(){
let backupMode = getCheckedRadioValue();
if (backupMode == "full"){
window.open("/api/conf/export?includeDB=true");
}else{
window.open("/api/conf/export");
}
}
document.getElementById("uploadForm").addEventListener("submit", function(event) {
event.preventDefault(); // Prevent the form from submitting normally
var fileInput = document.getElementById("fileInput");
var file = fileInput.files[0];
if (!file) {
alert("Missing file.");
return;
}
var formData = new FormData();
formData.append("file", file);
var xhr = new XMLHttpRequest();
xhr.open("POST", "/api/conf/import", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
parent.msgbox("Config restore succeed. Restart Zoraxy to apply changes.")
} else {
parent.msgbox("Restore failed: " + xhr.responseText, false, 5000);
}
}
};
xhr.send(formData);
});
</script>
</body>
</html>

327
src/web/tools/https.html Normal file
View File

@ -0,0 +1,327 @@
<!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="./favicon.png" />
<title>HTTPS Setup Wizard | Zoraxy</title>
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../../script/ao_module.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
<script src="../script/tablesort.js"></script>
<link rel="stylesheet" href="shepherd.js/dist/css/shepherd.css"/>
<script src="shepherd.js/dist/js/shepherd.min.js"></script>
<link rel="stylesheet" href="../main.css">
</head>
<body>
<br>
<div class="ui container">
<div class="ui yellow message">
This Wizard require both client and server connected to the internet.
<br><b>
As different deployment methods might involve different network environment,
this wizard is only provided for assistant and the correctness of the setup is not guaranteed.
If you need to verify your TLS/SSL certificate installation is valid, please seek help
from IT professionals.</b>
</div>
<div class="ui segment">
<h3 class="ui header">
HTTPS (TLS/SSL Certificate) Setup Wizard
<div class="sub header">This tool help you setup https with your domain / subdomain on your Zoraxy host. <br>
Follow the steps below to get started</div>
</h3>
</div>
<div class="ui segment stepContainer" step="1">
<h4 class="ui header">
1. Setup Zoraxy to listen to port 80 or 443 and start listening
<div class="sub header">ACME can only works on port 80 (or 80 redirected 443). Please make sure Zoarxy is listening to either one of the ports.</div>
</h4>
<button class="ui basic green button" onclick="checkStep(1, step1Callback, this);">Check Port Setup</button>
<div class="checkResult" style="margin-top: 1em;">
</div>
</div>
<div class="ui segment stepContainer" step="2">
<h4 class="ui header">
2. If you are under NAT, setup Port Forward and forward external port 80 (<b style="color: rgb(206, 29, 29); font-weight:bolder;">and</b> 443, if you are using 443) to your Zoraxy's LAN IP address port 80 (and 443)
<div class="sub header">If your Zoraxy server IP address starts with 192.168., you are mostly under a NAT router.</div>
</h4>
<small>The check function below will use public ip to check if port is opened. Make sure your host is reachable from the internet!<br>
<b style="color: rgb(206, 29, 29); font-weight:bolder;">If you are using 443, you still need to forward port 80 for performing 80 to 443 redirect.</b></small><br>
<button style="margin-top: 0.6em;" class="ui basic green button" onclick="checkStep(2, step2Callback, this);">Check Internet Reachable</button>
<div class="checkResult" style="margin-top: 1em;">
</div>
</div>
<div class="ui segment stepContainer" step="3">
<h4 class="ui header">
3. Point your domain (or sub-domain) to your Zoraxy server public IP address
<div class="sub header">DNS records might takes 5 - 10 minutes to take effect. If checking did not poss the first time, wait for a few minutes and retry.</div>
</h4>
<div class="ui fluid input">
<input type="text" name="domain" placeholder="Your Domain / DNS name (e.g. dev.example.com)">
</div>
<br>
<button class="ui basic green button" onclick="checkStep(3, step3Callback, this);">Check Domain Reachable</button>
<div class="checkResult" style="margin-top: 1em;">
</div>
</div>
<div class="ui segment stepContainer" step="4">
<h4 class="ui header">
4. Request a public CA to assign you a certificate
<div class="sub header">This process might take a few minutes and usually fully automated. If there are any error, you can see Zoraxy STDOUT / log for more information.</div>
</h4>
<div class="ui form">
<div class="field">
<label>Renewer Email</label>
<div class="ui fluid input">
<input id="caRegisterEmail" type="text" placeholder="webmaster@example.com">
</div>
<small>Your CA might send expire notification to you via this email.</small>
</div>
<div class="field">
<label>Domain(s)</label>
<input id="domainsInput" type="text" placeholder="example.com" onkeyup="checkIfInputDomainIsMultiple();">
<small>If you have more than one domain in a single certificate, enter the domains separated by commas (e.g. s1.dev.example.com,s2.dev.example.com)</small>
</div>
<div class="field multiDomainOnly" style="display:none;">
<label>Matching Rule</label>
<input id="filenameInput" type="text" placeholder="Enter filename (no file extension)">
<small>Matching rule to let Zoraxy pick which certificate to use (Also be used as filename). Usually is the longest common suffix of the entered addresses. (e.g. dev.example.com)</small>
</div>
<div class="field multiDomainOnly" style="display:none;">
<button class="ui basic fluid button" onclick="autoDetectMatchingRules();">Auto Detect Matching Rule</button>
</div>
<div class="field">
<label>Certificate Authority (CA)</label>
<div class="ui selection dropdown" id="ca">
<input type="hidden" name="ca">
<i class="dropdown icon"></i>
<div class="default text">Let's Encrypt</div>
<div class="menu">
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
<div class="item" data-value="Buypass">Buypass</div>
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
<!-- <div class="item" data-value="Google">Google</div> -->
</div>
</div>
</div>
<button id="obtainButton" class="ui green basic button" type="submit"><i class="green download icon"></i> Get Certificate</button>
</div>
<div class="ui green message" id="installSucc" style="display:none;">
<i class="ui check icon"></i> Certificate for this domain has been installed. Visit the TLS/SSL tab for advance operations.
</div>
</div>
<script>
$(".dropdown").dropdown();
function checkIfInputDomainIsMultiple(){
var inputDomains = $("#domainsInput").val();
if (inputDomains.includes(",")){
$(".multiDomainOnly").show();
}else{
$(".multiDomainOnly").hide();
}
}
//Grab the longest common suffix of all domains
//not that smart technically
function autoDetectMatchingRules(){
var domainsString = $("#domainsInput").val();
if (!domainsString.includes(",")){
return domainsString;
}
let domains = domainsString.split(",");
//Clean out any spacing between commas
for (var i = 0; i < domains.length; i++){
domains[i] = domains[i].trim();
}
function getLongestCommonSuffix(strings) {
if (strings.length === 0) {
return ''; // Return an empty string if the array is empty
}
var sortedStrings = strings.slice().sort(); // Create a sorted copy of the array
var firstString = sortedStrings[0];
var lastString = sortedStrings[sortedStrings.length - 1];
var suffix = '';
var minLength = Math.min(firstString.length, lastString.length);
for (var i = 0; i < minLength; i++) {
if (firstString[firstString.length - 1 - i] !== lastString[lastString.length - 1 - i]) {
break; // Stop iterating if characters don't match
}
suffix = firstString[firstString.length - 1 - i] + suffix;
}
return suffix;
}
let longestSuffix = getLongestCommonSuffix(domains);
//Check if the suffix is a valid domain
if (longestSuffix.substr(0,1) == "."){
//Trim off the first dot
longestSuffix = longestSuffix.substr(1);
}
if (!longestSuffix.includes(".")){
alert("Auto Detect failed: Multiple Domains");
return;
}
$("#filenameInput").val(longestSuffix);
}
$("#obtainButton").click(function() {
$("#obtainButton").addClass("loading").addClass("disabled");
obtainCertificate();
});
// Obtain certificate from API
function obtainCertificate() {
var domains = $("#domainsInput").val();
var filename = $("#filenameInput").val();
var email = $("#caRegisterEmail").val();
if (email == ""){
alert("ACME renew email is not set")
return;
}
if (filename.trim() == "" && !domains.includes(",")){
//Zoraxy filename are the matching name for domains.
//Use the same as domains
filename = domains;
}else if (filename != "" && !domains.includes(",")){
//Invalid settings. Force the filename to be same as domain
//if there are only 1 domain
filename = domains;
}else{
alert("Filename cannot be empty for certs containing multiple domains.")
return;
}
var ca = $("#ca").dropdown("get value");
$.ajax({
url: "/api/acme/obtainCert",
method: "GET",
data: {
domains: domains,
filename: filename,
email: email,
ca: ca,
},
success: function(response) {
$("#obtainButton").removeClass("loading").removeClass("disabled");
if (response.error) {
console.log("Error:", response.error);
// Show error message
alert(response.error);
$("#installSucc").hide();
} else {
console.log("Certificate installed successfully");
// Show success message
//alert("Certificate installed successfully");
$("#installSucc").show();
}
},
error: function(error) {
$("#obtainButton").removeClass("loading").removeClass("disabled");
console.log("Failed to renewed certificate:", error);
}
});
}
function step3Callback(resultContainer, data){
if (data == true){
$(resultContainer).html(`<div class="ui green message">
<i class="ui check icon"></i> Domain is reachable and seems there is a HTTP server listening. Please move on to the next step.
</div>`);
}else{
$(resultContainer).html(`<div class="ui red message">
<i class="ui remove icon"></i> Domain is reachable but there are no HTTP server listening<br>
Make sure you have point to the correct IP address and there are not another proxy server above Zoraxy.
</div>`);
}
}
function step2Callback(resultContainer, data){
if (data == true){
$(resultContainer).html(`<div class="ui green message">
<i class="ui check icon"></i> HTTP Server reachable from public IP address. Please move on to the next step.
</div>`);
}else{
$(resultContainer).html(`<div class="ui red message">
<i class="ui remove icon"></i> Server unreachable from public IP address<br>
Check if you have correct NAT port forward setup in your home router, firewall and make sure network is reachable from the public internet.
</div>`);
}
}
function step1Callback(resultContainer, data){
if (data == true){
$(resultContainer).html(`<div class="ui green message">
<i class="ui check icon"></i> Supported listening port. Please move on to the next step.
</div>`);
}else{
$(resultContainer).html(`<div class="ui red message">
<i class="ui remove icon"></i> Invalid listening port.<br>
Go to Status tab and change the listening port to 80 or 443
</div>`);
}
}
function getStepContainerByNo(stepNo){
let targetStepContainer = undefined;
$(".stepContainer").each(function(){
if ($(this).attr("step") == stepNo){
let thisContainer = $(this);
targetStepContainer = thisContainer;
}
});
return targetStepContainer;
}
function checkStep(stepNo, callback, btn){
let targetContainer = getStepContainerByNo(stepNo);
$(btn).addClass("loading");
//Load all the inputs
data = {};
$(targetContainer).find("input").each(function(){
let key = $(this).attr("name")
if (key != undefined){
data[key] = $(this).val();
}
});
$.ajax({
url: "/api/acme/wizard?step=" + stepNo,
data: data,
success: function(data){
$(btn).removeClass("loading");
if (data.error != undefined){
$(targetContainer).find(".checkResult").html(`
<div class="ui red message">${data.error}</div>`);
}else{
callback($(targetContainer).find(".checkResult"), data);
}
},
error: function(){
$(btn).removeClass("loading");
$(targetContainer).find(".checkResult").html(`
<div class="ui red message">Server return an Unknown Error</div>`);
}
});
}
</script>
</body>
</html>

View File

@ -117,6 +117,7 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
url = "https://" + target.Domain
protocol = "https"
}
UptimeTargets = append(UptimeTargets, &uptime.Target{
ID: subd,
Name: subd,