mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-30 11:21:44 +02:00
Compare commits
28 Commits
Author | SHA1 | Date | |
---|---|---|---|
7eda6ba501 | |||
2da5ef048f | |||
6c48939316 | |||
544894bbba | |||
153d056bdf | |||
12c1118af9 | |||
67ba143999 | |||
0a8a821394 | |||
36b17ce4cf | |||
519372069f | |||
2f14d6f271 | |||
44ac7144ec | |||
741d3f8de1 | |||
23eca5afae | |||
050fab9481 | |||
3fc92bac27 | |||
594f75da97 | |||
3fbf246fb4 | |||
828af6263d | |||
ab42cec31f | |||
a8bf07dbba | |||
48dc85ea3e | |||
a73a7944ec | |||
d187124db6 | |||
0dd9e5d73c | |||
5e7599756f | |||
5db50c1ca2 | |||
884507b45a |
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
53
CHANGELOG.md
Normal 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
|
@ -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
19
docker/Dockerfile
Normal 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
21
docker/LICENSE
Normal 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
67
docker/README.md
Normal file
@ -0,0 +1,67 @@
|
||||
[](https://hub.docker.com/r/passivelemon/zoraxy-docker)
|
||||
[](https://hub.docker.com/r/passivelemon/zoraxy-docker)
|
||||
[](https://hub.docker.com/r/passivelemon/zoraxy-docker)
|
||||
[](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
20
docker/entrypoint.sh
Normal 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
115
src/acme.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
25
src/api.go
25
src/api.go
@ -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
|
||||
}
|
||||
|
99
src/cert.go
99
src/cert.go
@ -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
|
||||
|
211
src/config.go
211
src/config.go
@ -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)
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
1597
src/go.sum
File diff suppressed because it is too large
Load Diff
54
src/main.go
54
src/main.go
@ -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
288
src/mod/acme/acme.go
Normal 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
24
src/mod/acme/acme_test.go
Normal 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)
|
||||
}
|
||||
}
|
163
src/mod/acme/acmewizard/acmewizard.go
Normal file
163
src/mod/acme/acmewizard/acmewizard.go
Normal 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
374
src/mod/acme/autorenew.go
Normal 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
45
src/mod/acme/ca.go
Normal 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
15
src/mod/acme/ca.json
Normal 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
94
src/mod/acme/utils.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
49
src/mod/dynamicproxy/dpcore/dpcore_test.go
Normal file
49
src/mod/dynamicproxy/dpcore/dpcore_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
16
src/mod/expose/expose.go
Normal 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
111
src/mod/expose/security.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
91
src/mod/geodb/blacklist.go
Normal file
91
src/mod/geodb/blacklist.go
Normal 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
|
||||
}
|
@ -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 == "" {
|
||||
|
91
src/mod/geodb/whitelist.go
Normal file
91
src/mod/geodb/whitelist.go
Normal 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
|
||||
}
|
159
src/mod/netutils/netutils.go
Normal file
159
src/mod/netutils/netutils.go
Normal 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
|
||||
}
|
28
src/mod/netutils/netutils_test.go
Normal file
28
src/mod/netutils/netutils_test.go
Normal 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)
|
||||
}
|
81
src/mod/netutils/pingip.go
Normal file
81
src/mod/netutils/pingip.go
Normal 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
|
||||
}
|
212
src/mod/netutils/traceroute.go
Normal file
212
src/mod/netutils/traceroute.go
Normal 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
199
src/mod/netutils/whois.go
Normal 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
100
src/mod/pathrule/handler.go
Normal 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)
|
||||
}
|
174
src/mod/pathrule/pathrule.go
Normal file
174
src/mod/pathrule/pathrule.go
Normal 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
16
src/mod/sshprox/embed.go
Normal 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
|
||||
)
|
18
src/mod/sshprox/embed_linux_386.go
Normal file
18
src/mod/sshprox/embed_linux_386.go
Normal 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
|
||||
)
|
18
src/mod/sshprox/embed_linux_amd64.go
Normal file
18
src/mod/sshprox/embed_linux_amd64.go
Normal 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
|
||||
)
|
18
src/mod/sshprox/embed_linux_arm.go
Normal file
18
src/mod/sshprox/embed_linux_arm.go
Normal 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
|
||||
)
|
18
src/mod/sshprox/embed_linux_arm64.go
Normal file
18
src/mod/sshprox/embed_linux_arm64.go
Normal 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
|
||||
)
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
41
src/start.go
41
src/start.go
@ -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()
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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){
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>`);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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
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
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 |
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
@ -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
475
src/web/snippet/acme.html
Normal 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>
|
76
src/web/snippet/advancePathRules.html
Normal file
76
src/web/snippet/advancePathRules.html
Normal 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>
|
@ -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}$/;
|
||||
|
||||
|
100
src/web/snippet/configTools.html
Normal file
100
src/web/snippet/configTools.html
Normal 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
327
src/web/tools/https.html
Normal 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>
|
@ -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,
|
||||
|
Reference in New Issue
Block a user