mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-03 06:07:20 +02:00
Merge branch 'main' into DockerMerge
This commit is contained in:
commit
aed703e260
4
.gitignore
vendored
4
.gitignore
vendored
@ -29,6 +29,6 @@ src/Zoraxy_*_*
|
|||||||
src/certs/*
|
src/certs/*
|
||||||
src/rules/*
|
src/rules/*
|
||||||
src/README.md
|
src/README.md
|
||||||
|
|
||||||
docker/ContainerTester.sh
|
docker/ContainerTester.sh
|
||||||
docker/ImagePublisher.sh
|
docker/ImagePublisher.sh
|
||||||
|
src/mod/acme/test/stackoverflow.pem
|
@ -1,3 +1,11 @@
|
|||||||
|
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
|
# v2.6.4 Jun 15 2023
|
||||||
|
|
||||||
+ Added force TLS v1.2 above toggle
|
+ Added force TLS v1.2 above toggle
|
||||||
|
@ -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.
|
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
|
#### Docker
|
||||||
Thanks for cyb3rdoc and PassiveLemon for providing support over the Docker installation. You can check out their repo over here.
|
See the [/docker](https://github.com/tobychui/zoraxy/tree/main/docker) folder for more details
|
||||||
|
|
||||||
[https://github.com/cyb3rdoc/zoraxy-docker](https://github.com/cyb3rdoc/zoraxy-docker)
|
|
||||||
|
|
||||||
[https://github.com/PassiveLemon/zoraxy-docker](https://github.com/PassiveLemon/zoraxy-docker)
|
|
||||||
|
|
||||||
### External Permission Management Mode
|
### External Permission Management Mode
|
||||||
|
|
||||||
|
@ -63,3 +63,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
ARGS: '-port=:8000 -noauth=false'
|
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>
|
||||||
|
|
||||||
|
94
src/acme.go
94
src/acme.go
@ -1,10 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -13,23 +21,95 @@ import (
|
|||||||
This script handle special routing required for acme auto cert renew functions
|
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() {
|
func acmeRegisterSpecialRoutingRule() {
|
||||||
|
log.Println("Assigned temporary port:" + acmeHandler.Getport())
|
||||||
|
|
||||||
err := dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
|
err := dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
|
||||||
ID: "acme-autorenew",
|
ID: "acme-autorenew",
|
||||||
MatchRule: func(r *http.Request) bool {
|
MatchRule: func(r *http.Request) bool {
|
||||||
if r.RequestURI == "/.well-known/" {
|
found, _ := regexp.MatchString("/.well-known/acme-challenge/*", r.RequestURI)
|
||||||
return true
|
return found
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
},
|
||||||
RoutingHandler: func(w http.ResponseWriter, r *http.Request) {
|
RoutingHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte("HELLO WORLD, THIS IS ACME REQUEST HANDLER"))
|
|
||||||
|
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,
|
Enabled: true,
|
||||||
|
UseSystemAccessControl: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("[Err] " + err.Error())
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
16
src/api.go
16
src/api.go
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/acme/acmewizard"
|
||||||
"imuslab.com/zoraxy/mod/auth"
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
"imuslab.com/zoraxy/mod/netstat"
|
"imuslab.com/zoraxy/mod/netstat"
|
||||||
"imuslab.com/zoraxy/mod/netutils"
|
"imuslab.com/zoraxy/mod/netutils"
|
||||||
@ -59,6 +60,7 @@ func initAPIs() {
|
|||||||
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
authRouter.HandleFunc("/api/cert/tlsRequireLatest", handleSetTlsRequireLatest)
|
||||||
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
|
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
|
||||||
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
|
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
|
||||||
|
authRouter.HandleFunc("/api/cert/listdomains", handleListDomains)
|
||||||
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
|
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
|
||||||
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
||||||
|
|
||||||
@ -135,6 +137,7 @@ func initAPIs() {
|
|||||||
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
|
authRouter.HandleFunc("/api/tools/ipscan", HandleIpScan)
|
||||||
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
|
authRouter.HandleFunc("/api/tools/traceroute", netutils.HandleTraceRoute)
|
||||||
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
|
authRouter.HandleFunc("/api/tools/ping", netutils.HandlePing)
|
||||||
|
authRouter.HandleFunc("/api/tools/whois", netutils.HandleWhois)
|
||||||
authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession)
|
authRouter.HandleFunc("/api/tools/webssh", HandleCreateProxySession)
|
||||||
authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck)
|
authRouter.HandleFunc("/api/tools/websshSupported", HandleWebSshSupportCheck)
|
||||||
authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan)
|
authRouter.HandleFunc("/api/tools/wol", HandleWakeOnLan)
|
||||||
@ -147,8 +150,21 @@ func initAPIs() {
|
|||||||
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
||||||
http.HandleFunc("/api/account/new", HandleNewPasswordSetup)
|
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
|
//Others
|
||||||
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
|
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
|
//If you got APIs to add, append them here
|
||||||
}
|
}
|
||||||
|
72
src/cert.go
72
src/cert.go
@ -10,6 +10,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
@ -44,6 +46,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
|||||||
Domain string
|
Domain string
|
||||||
LastModifiedDate string
|
LastModifiedDate string
|
||||||
ExpireDate string
|
ExpireDate string
|
||||||
|
RemainingDays int
|
||||||
}
|
}
|
||||||
|
|
||||||
results := []*CertInfo{}
|
results := []*CertInfo{}
|
||||||
@ -60,6 +63,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
certExpireTime := "Unknown"
|
certExpireTime := "Unknown"
|
||||||
certBtyes, err := os.ReadFile(certFilepath)
|
certBtyes, err := os.ReadFile(certFilepath)
|
||||||
|
expiredIn := 0
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//Unable to load this file
|
//Unable to load this file
|
||||||
continue
|
continue
|
||||||
@ -70,6 +74,11 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
|||||||
cert, err := x509.ParseCertificate(block.Bytes)
|
cert, err := x509.ParseCertificate(block.Bytes)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
|
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,
|
Domain: filename,
|
||||||
LastModifiedDate: modifiedTime,
|
LastModifiedDate: modifiedTime,
|
||||||
ExpireDate: certExpireTime,
|
ExpireDate: certExpireTime,
|
||||||
|
RemainingDays: expiredIn,
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, &thisCertInfo)
|
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
|
// Handle front-end toggling TLS mode
|
||||||
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
currentTlsSetting := false
|
currentTlsSetting := false
|
||||||
@ -205,8 +273,8 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
// create file in upload directory
|
// create file in upload directory
|
||||||
os.MkdirAll("./certs", 0775)
|
os.MkdirAll("./conf/certs", 0775)
|
||||||
f, err := os.Create(filepath.Join("./certs", overWriteFilename))
|
f, err := os.Create(filepath.Join("./conf/certs", overWriteFilename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
211
src/config.go
211
src/config.go
@ -1,12 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
@ -31,7 +37,7 @@ type Record struct {
|
|||||||
|
|
||||||
func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
|
func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
|
||||||
//TODO: Make this accept new def types
|
//TODO: Make this accept new def types
|
||||||
os.MkdirAll("conf", 0775)
|
os.MkdirAll("./conf/proxy/", 0775)
|
||||||
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
|
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
|
||||||
|
|
||||||
//Generate record
|
//Generate record
|
||||||
@ -39,12 +45,12 @@ func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
|
|||||||
|
|
||||||
//Write to file
|
//Write to file
|
||||||
js, _ := json.MarshalIndent(thisRecord, "", " ")
|
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 {
|
func RemoveReverseProxyConfig(rootname string) error {
|
||||||
filename := getFilenameFromRootName(rootname)
|
filename := getFilenameFromRootName(rootname)
|
||||||
removePendingFile := strings.ReplaceAll(filepath.Join("conf", filename), "\\", "/")
|
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
|
||||||
log.Println("Config Removed: ", removePendingFile)
|
log.Println("Config Removed: ", removePendingFile)
|
||||||
if utils.FileExists(removePendingFile) {
|
if utils.FileExists(removePendingFile) {
|
||||||
err := os.Remove(removePendingFile)
|
err := os.Remove(removePendingFile)
|
||||||
@ -83,3 +89,202 @@ func getFilenameFromRootName(rootname string) string {
|
|||||||
filename = filename + ".config"
|
filename = filename + ".config"
|
||||||
return filename
|
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,14 +4,16 @@ go 1.16
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/boltdb/bolt v1.3.1
|
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/go-ping/ping v1.1.0
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/gorilla/sessions v1.2.1
|
github.com/gorilla/sessions v1.2.1
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/grandcat/zeroconf v1.0.0
|
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/microcosm-cc/bluemonday v1.0.24
|
||||||
github.com/oschwald/geoip2-golang v1.8.0
|
github.com/oschwald/geoip2-golang v1.8.0
|
||||||
github.com/satori/go.uuid v1.2.0
|
github.com/satori/go.uuid v1.2.0
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/net v0.11.0
|
||||||
golang.org/x/sys v0.8.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
49
src/main.go
49
src/main.go
@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
"imuslab.com/zoraxy/mod/aroz"
|
"imuslab.com/zoraxy/mod/aroz"
|
||||||
"imuslab.com/zoraxy/mod/auth"
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
@ -37,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 allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
|
||||||
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
|
var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local node")
|
||||||
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
|
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 (
|
var (
|
||||||
name = "Zoraxy"
|
name = "Zoraxy"
|
||||||
version = "2.6.4"
|
version = "2.6.5"
|
||||||
nodeUUID = "generic"
|
nodeUUID = "generic"
|
||||||
development = false //Set this to false to use embedded web fs
|
development = false //Set this to false to use embedded web fs
|
||||||
bootTime = time.Now().Unix()
|
bootTime = time.Now().Unix()
|
||||||
@ -67,6 +69,8 @@ var (
|
|||||||
ganManager *ganserv.NetworkManager //Global Area Network Manager
|
ganManager *ganserv.NetworkManager //Global Area Network Manager
|
||||||
webSshManager *sshprox.Manager //Web SSH connection service
|
webSshManager *sshprox.Manager //Web SSH connection service
|
||||||
tcpProxyManager *tcpprox.Manager //TCP Proxy Manager
|
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
|
//Helper modules
|
||||||
EmailSender *email.Sender //Email sender that handle email sending
|
EmailSender *email.Sender //Email sender that handle email sending
|
||||||
@ -79,29 +83,34 @@ func SetupCloseHandler() {
|
|||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
go func() {
|
go func() {
|
||||||
<-c
|
<-c
|
||||||
fmt.Println("- Shutting down " + name)
|
ShutdownSeq()
|
||||||
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()
|
|
||||||
os.Exit(0)
|
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() {
|
func main() {
|
||||||
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
|
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
|
||||||
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
|
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
|
||||||
|
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) {
|
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
|
//Check if there are external routing rule matches.
|
||||||
clientIpAddr := geodb.GetRequesterIP(r)
|
//If yes, route them via external rr
|
||||||
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
|
matchedRoutingRule := h.Parent.GetMatchingRoutingRule(r)
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
if matchedRoutingRule != nil {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
//Matching routing rule found. Let the sub-router handle it
|
||||||
template, err := os.ReadFile("./web/forbidden.html")
|
if matchedRoutingRule.UseSystemAccessControl {
|
||||||
if err != nil {
|
//This matching rule request system access control.
|
||||||
w.Write([]byte("403 - Forbidden"))
|
//check access logic
|
||||||
} else {
|
respWritten := h.handleAccessRouting(w, r)
|
||||||
w.Write(template)
|
if respWritten {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
h.logRequest(r, false, 403, "blacklist", "")
|
matchedRoutingRule.Route(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check if this ip is in whitelist
|
/*
|
||||||
if !h.Parent.Option.GeodbStore.IsWhitelisted(clientIpAddr) {
|
General Access Check
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
*/
|
||||||
w.WriteHeader(http.StatusForbidden)
|
|
||||||
template, err := os.ReadFile("./web/forbidden.html")
|
respWritten := h.handleAccessRouting(w, r)
|
||||||
if err != nil {
|
if respWritten {
|
||||||
w.Write([]byte("403 - Forbidden"))
|
|
||||||
} else {
|
|
||||||
w.Write(template)
|
|
||||||
}
|
|
||||||
h.logRequest(r, false, 403, "whitelist", "")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,15 +62,6 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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
|
//Extract request host to see if it is virtual directory or subdomain
|
||||||
domainOnly := r.Host
|
domainOnly := r.Host
|
||||||
if strings.Contains(r.Host, ":") {
|
if strings.Contains(r.Host, ":") {
|
||||||
@ -127,3 +115,38 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.proxyRequest(w, r, h.Parent.Root)
|
h.proxyRequest(w, r, h.Parent.Root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle access routing logic. Return true if the request is handled or blocked by the access control logic
|
||||||
|
// if the return value is false, you can continue process the response writer
|
||||||
|
func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
//Check if this ip is in blacklist
|
||||||
|
clientIpAddr := geodb.GetRequesterIP(r)
|
||||||
|
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
template, err := os.ReadFile("./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
|
//Hack the default transporter to handle more connections
|
||||||
thisTransporter := http.DefaultTransport
|
thisTransporter := http.DefaultTransport
|
||||||
thisTransporter.(*http.Transport).MaxIdleConns = 3000
|
optimalConcurrentConnection := 32
|
||||||
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = 3000
|
thisTransporter.(*http.Transport).MaxIdleConns = optimalConcurrentConnection * 2
|
||||||
thisTransporter.(*http.Transport).IdleConnTimeout = 10 * time.Second
|
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = optimalConcurrentConnection
|
||||||
thisTransporter.(*http.Transport).MaxConnsPerHost = 0
|
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
|
||||||
//thisTransporter.(*http.Transport).DisableCompression = true
|
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
|
||||||
|
thisTransporter.(*http.Transport).DisableCompression = true
|
||||||
|
|
||||||
if ignoreTLSVerification {
|
if ignoreTLSVerification {
|
||||||
//Ignore TLS certificate validation error
|
//Ignore TLS certificate validation error
|
||||||
@ -357,11 +358,6 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
|||||||
|
|
||||||
//Custom header rewriter functions
|
//Custom header rewriter functions
|
||||||
if res.Header.Get("Location") != "" {
|
if res.Header.Get("Location") != "" {
|
||||||
/*
|
|
||||||
fmt.Println(">>> REQ", req)
|
|
||||||
fmt.Println(">>> OUTR", outreq)
|
|
||||||
fmt.Println(">>> RESP", res)
|
|
||||||
*/
|
|
||||||
locationRewrite := res.Header.Get("Location")
|
locationRewrite := res.Header.Get("Location")
|
||||||
originLocation := res.Header.Get("Location")
|
originLocation := res.Header.Get("Location")
|
||||||
res.Header.Set("zr-origin-location", originLocation)
|
res.Header.Set("zr-origin-location", originLocation)
|
||||||
@ -369,12 +365,10 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
|||||||
if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") {
|
if strings.HasPrefix(originLocation, "http://") || strings.HasPrefix(originLocation, "https://") {
|
||||||
//Full path
|
//Full path
|
||||||
//Replace the forwarded target with expected Host
|
//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 {
|
if err == nil {
|
||||||
locationRewrite = lr
|
locationRewrite = lr
|
||||||
}
|
}
|
||||||
//locationRewrite = strings.ReplaceAll(locationRewrite, rrr.ProxyDomain, rrr.OriginalHost)
|
|
||||||
//locationRewrite = strings.ReplaceAll(locationRewrite, domainWithoutPort, rrr.OriginalHost)
|
|
||||||
} else if strings.HasPrefix(originLocation, "/") && rrr.PathPrefix != "" {
|
} else if strings.HasPrefix(originLocation, "/") && rrr.PathPrefix != "" {
|
||||||
//Back to the root of this proxy object
|
//Back to the root of this proxy object
|
||||||
//fmt.Println(rrr.ProxyDomain, rrr.OriginalHost)
|
//fmt.Println(rrr.ProxyDomain, rrr.OriginalHost)
|
||||||
@ -387,6 +381,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
|
|||||||
//Custom redirection to this rproxy relative path
|
//Custom redirection to this rproxy relative path
|
||||||
res.Header.Set("Location", locationRewrite)
|
res.Header.Set("Location", locationRewrite)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy header from response to client.
|
// Copy header from response to client.
|
||||||
copyHeader(rw.Header(), res.Header)
|
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 (
|
import (
|
||||||
"net/url"
|
"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)
|
u, err := url.Parse(urlString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Update the schemetic if the proxying target is http
|
||||||
|
//but exposed as https to the internet via Zoraxy
|
||||||
if useTLS {
|
if useTLS {
|
||||||
u.Scheme = "https"
|
u.Scheme = "https"
|
||||||
} else {
|
} else {
|
||||||
u.Scheme = "http"
|
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
|
return u.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug functions
|
||||||
|
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
|
||||||
|
return replaceLocationHost(urlString, rrr, useTLS)
|
||||||
|
}
|
||||||
|
@ -95,6 +95,7 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
|||||||
UseTLS: target.RequireTLS,
|
UseTLS: target.RequireTLS,
|
||||||
PathPrefix: "",
|
PathPrefix: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
var dnsError *net.DNSError
|
var dnsError *net.DNSError
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.As(err, &dnsError) {
|
if errors.As(err, &dnsError) {
|
||||||
|
@ -28,13 +28,15 @@ func (t *RuleTable) HandleRedirect(w http.ResponseWriter, r *http.Request) int {
|
|||||||
rr := t.MatchRedirectRule(requestPath)
|
rr := t.MatchRedirectRule(requestPath)
|
||||||
if rr != nil {
|
if rr != nil {
|
||||||
redirectTarget := rr.TargetURL
|
redirectTarget := rr.TargetURL
|
||||||
//Always pad a / at the back of the target URL
|
|
||||||
if redirectTarget[len(redirectTarget)-1:] != "/" {
|
|
||||||
redirectTarget += "/"
|
|
||||||
}
|
|
||||||
if rr.ForwardChildpath {
|
if rr.ForwardChildpath {
|
||||||
//Remove the first / in the path
|
//Remove the first / in the path if the redirect target already have tailing slash
|
||||||
redirectTarget += strings.TrimPrefix(r.URL.Path, "/")
|
if strings.HasSuffix(redirectTarget, "/") {
|
||||||
|
redirectTarget += strings.TrimPrefix(r.URL.Path, "/")
|
||||||
|
} else {
|
||||||
|
redirectTarget += r.URL.Path
|
||||||
|
}
|
||||||
|
|
||||||
if r.URL.RawQuery != "" {
|
if r.URL.RawQuery != "" {
|
||||||
redirectTarget += "?" + r.URL.RawQuery
|
redirectTarget += "?" + r.URL.RawQuery
|
||||||
}
|
}
|
||||||
|
@ -13,10 +13,11 @@ import (
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
type RoutingRule struct {
|
type RoutingRule struct {
|
||||||
ID string
|
ID string //ID of the routing rule
|
||||||
MatchRule func(r *http.Request) bool
|
Enabled bool //If the routing rule enabled
|
||||||
RoutingHandler func(http.ResponseWriter, *http.Request)
|
UseSystemAccessControl bool //Pass access control check to system white/black list, set this to false to bypass white/black list
|
||||||
Enabled bool
|
MatchRule func(r *http.Request) bool
|
||||||
|
RoutingHandler func(http.ResponseWriter, *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Router functions
|
// Router functions
|
||||||
|
@ -13,8 +13,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func readAuthTokenAsAdmin() (string, error) {
|
func readAuthTokenAsAdmin() (string, error) {
|
||||||
if utils.FileExists("./authtoken.secret") {
|
if utils.FileExists("./conf/authtoken.secret") {
|
||||||
authKey, err := os.ReadFile("./authtoken.secret")
|
authKey, err := os.ReadFile("./conf/authtoken.secret")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return strings.TrimSpace(string(authKey)), nil
|
return strings.TrimSpace(string(authKey)), nil
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,8 @@ import (
|
|||||||
// Use admin permission to read auth token on Windows
|
// Use admin permission to read auth token on Windows
|
||||||
func readAuthTokenAsAdmin() (string, error) {
|
func readAuthTokenAsAdmin() (string, error) {
|
||||||
//Check if the previous startup already extracted the authkey
|
//Check if the previous startup already extracted the authkey
|
||||||
if utils.FileExists("./authtoken.secret") {
|
if utils.FileExists("./conf/authtoken.secret") {
|
||||||
authKey, err := os.ReadFile("./authtoken.secret")
|
authKey, err := os.ReadFile("./conf/authtoken.secret")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return strings.TrimSpace(string(authKey)), nil
|
return strings.TrimSpace(string(authKey)), nil
|
||||||
}
|
}
|
||||||
@ -30,7 +30,7 @@ func readAuthTokenAsAdmin() (string, error) {
|
|||||||
exe := "cmd.exe"
|
exe := "cmd.exe"
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
|
|
||||||
output, _ := filepath.Abs(filepath.Join("./", "authtoken.secret"))
|
output, _ := filepath.Abs(filepath.Join("./conf/", "authtoken.secret"))
|
||||||
os.WriteFile(output, []byte(""), 0775)
|
os.WriteFile(output, []byte(""), 0775)
|
||||||
args := fmt.Sprintf("/C type \"C:\\ProgramData\\ZeroTier\\One\\authtoken.secret\" > \"" + output + "\"")
|
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")
|
log.Println("Please click agree to allow access to ZeroTier authtoken from ProgramData")
|
||||||
retry := 0
|
retry := 0
|
||||||
time.Sleep(3 * time.Second)
|
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)
|
time.Sleep(3 * time.Second)
|
||||||
log.Println("Waiting for ZeroTier authtoken extraction...")
|
log.Println("Waiting for ZeroTier authtoken extraction...")
|
||||||
retry++
|
retry++
|
||||||
}
|
}
|
||||||
|
|
||||||
authKey, err := os.ReadFile("./authtoken.secret")
|
authKey, err := os.ReadFile("./conf/authtoken.secret")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,11 @@ package netutils
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/likexian/whois"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -46,6 +48,50 @@ func TraceRoute(targetIpOrDomain string, maxHops int) ([]string, error) {
|
|||||||
return traceroute(targetIpOrDomain, maxHops)
|
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) {
|
func HandlePing(w http.ResponseWriter, r *http.Request) {
|
||||||
targetIpOrDomain, err := utils.GetPara(r, "target")
|
targetIpOrDomain, err := utils.GetPara(r, "target")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -53,13 +99,44 @@ func HandlePing(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
results := []string{}
|
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++ {
|
for i := 0; i < 4; i++ {
|
||||||
realIP, pingTime, ttl, err := PingIP(targetIpOrDomain)
|
realIP, pingTime, ttl, err := PingIP(targetIpOrDomain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
results = append(results, "Reply from "+realIP+": "+err.Error())
|
results.ICMP = append(results.ICMP, "Reply from "+realIP+": "+err.Error())
|
||||||
} else {
|
} else {
|
||||||
results = append(results, fmt.Sprintf("Reply from %s: Time=%dms TTL=%d", realIP, pingTime.Milliseconds(), ttl))
|
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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,3 +144,16 @@ func HandlePing(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendJSONResponse(w, string(js))
|
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
|
||||||
|
}
|
||||||
|
@ -6,6 +6,39 @@ import (
|
|||||||
"time"
|
"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) {
|
func PingIP(ipOrDomain string) (string, time.Duration, int, error) {
|
||||||
ipAddr, err := net.ResolveIPAddr("ip", ipOrDomain)
|
ipAddr, err := net.ResolveIPAddr("ip", ipOrDomain)
|
||||||
if err != nil {
|
if err != 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
|
||||||
|
}
|
@ -12,15 +12,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Pathblock.go
|
Pathrules.go
|
||||||
|
|
||||||
This script block off some of the specific pathname in access
|
This script handle advance path settings and rules on particular
|
||||||
For example, this module can help you block request for a particular
|
paths of the incoming requests
|
||||||
apache directory or functional endpoints like /.well-known/ when you
|
|
||||||
are not using it
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
Enabled bool //If the pathrule is enabled.
|
||||||
ConfigFolder string //The folder to store the path blocking config files
|
ConfigFolder string //The folder to store the path blocking config files
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +40,7 @@ type Handler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new path blocker handler
|
// Create a new path blocker handler
|
||||||
func NewPathBlocker(options *Options) *Handler {
|
func NewPathRuleHandler(options *Options) *Handler {
|
||||||
//Create folder if not exists
|
//Create folder if not exists
|
||||||
if !utils.FileExists(options.ConfigFolder) {
|
if !utils.FileExists(options.ConfigFolder) {
|
||||||
os.Mkdir(options.ConfigFolder, 0775)
|
os.Mkdir(options.ConfigFolder, 0775)
|
||||||
|
@ -73,7 +73,7 @@ func ReverseProxtInit() {
|
|||||||
dynamicProxyRouter = dprouter
|
dynamicProxyRouter = dprouter
|
||||||
|
|
||||||
//Load all conf from files
|
//Load all conf from files
|
||||||
confs, _ := filepath.Glob("./conf/*.config")
|
confs, _ := filepath.Glob("./conf/proxy/*.config")
|
||||||
for _, conf := range confs {
|
for _, conf := range confs {
|
||||||
record, err := LoadReverseProxyConfig(conf)
|
record, err := LoadReverseProxyConfig(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
30
src/start.go
30
src/start.go
@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
"imuslab.com/zoraxy/mod/auth"
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||||
@ -48,8 +49,9 @@ func startupSequence() {
|
|||||||
//Create tables for the database
|
//Create tables for the database
|
||||||
sysdb.NewTable("settings")
|
sysdb.NewTable("settings")
|
||||||
|
|
||||||
//Create tmp folder
|
//Create tmp folder and conf folder
|
||||||
os.MkdirAll("./tmp", 0775)
|
os.MkdirAll("./tmp", 0775)
|
||||||
|
os.MkdirAll("./conf/proxy/", 0775)
|
||||||
|
|
||||||
//Create an auth agent
|
//Create an auth agent
|
||||||
sessionKey, err := auth.GetSessionKey(sysdb)
|
sessionKey, err := auth.GetSessionKey(sysdb)
|
||||||
@ -62,13 +64,13 @@ func startupSequence() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
//Create a TLS certificate manager
|
//Create a TLS certificate manager
|
||||||
tlsCertManager, err = tlscert.NewManager("./certs", development)
|
tlsCertManager, err = tlscert.NewManager("./conf/certs", development)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Create a redirection rule table
|
//Create a redirection rule table
|
||||||
redirectTable, err = redirection.NewRuleTable("./rules/redirect")
|
redirectTable, err = redirection.NewRuleTable("./conf/redirect")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -95,14 +97,15 @@ func startupSequence() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Path Blocker
|
Path Rules
|
||||||
|
|
||||||
This section of starutp script start the pathblocker
|
This section of starutp script start the path rules where
|
||||||
from file.
|
user can define their own routing logics
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pathRuleHandler = pathrule.NewPathBlocker(&pathrule.Options{
|
pathRuleHandler = pathrule.NewPathRuleHandler(&pathrule.Options{
|
||||||
ConfigFolder: "./rules/pathrules",
|
Enabled: false,
|
||||||
|
ConfigFolder: "./conf/rules/pathrules",
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -188,6 +191,17 @@ func startupSequence() {
|
|||||||
|
|
||||||
//Create an analytic loader
|
//Create an analytic loader
|
||||||
AnalyticLoader = analytic.NewDataLoader(sysdb, statisticCollector)
|
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
|
// This sequence start after everything is initialized
|
||||||
|
@ -67,6 +67,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
|
<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>
|
||||||
<div class="ui message">
|
<div class="ui message">
|
||||||
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
|
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
|
||||||
@ -106,11 +107,14 @@
|
|||||||
msgbox(data.error, false, 5000);
|
msgbox(data.error, false, 5000);
|
||||||
}else{
|
}else{
|
||||||
$("#certifiedDomainList").html("");
|
$("#certifiedDomainList").html("");
|
||||||
|
data.sort((a,b) => {
|
||||||
|
return a.Domain > b.Domain
|
||||||
|
});
|
||||||
data.forEach(entry => {
|
data.forEach(entry => {
|
||||||
$("#certifiedDomainList").append(`<tr>
|
$("#certifiedDomainList").append(`<tr>
|
||||||
<td>${entry.Domain}</td>
|
<td>${entry.Domain}</td>
|
||||||
<td>${entry.LastModifiedDate}</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>
|
<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>`);
|
</tr>`);
|
||||||
});
|
});
|
||||||
@ -125,6 +129,10 @@
|
|||||||
}
|
}
|
||||||
initManagedDomainCertificateList();
|
initManagedDomainCertificateList();
|
||||||
|
|
||||||
|
function openACMEManager(){
|
||||||
|
showSideWrapper('snippet/acme.html');
|
||||||
|
}
|
||||||
|
|
||||||
function handleDomainUploadByKeypress(){
|
function handleDomainUploadByKeypress(){
|
||||||
handleDomainKeysUpload(function(){
|
handleDomainKeysUpload(function(){
|
||||||
$("#certUploadingDomain").text($("#certdomain").val().trim());
|
$("#certUploadingDomain").text($("#certdomain").val().trim());
|
||||||
|
@ -45,8 +45,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class=""></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>
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment nettoolstab" data-tab="tab2">
|
<div class="ui bottom attached tab segment nettoolstab" data-tab="tab2">
|
||||||
@ -485,10 +494,70 @@ function ping(){
|
|||||||
$("#traceroute_results").val("");
|
$("#traceroute_results").val("");
|
||||||
msgbox(data.error, false, 6000);
|
msgbox(data.error, false, 6000);
|
||||||
}else{
|
}else{
|
||||||
$("#traceroute_results").val(data.join("\n"));
|
$("#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>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Destination URL (To)</label>
|
<label>Destination URL (To)</label>
|
||||||
<input type="text" name="destination-url" placeholder="Destination URL">
|
<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>
|
<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="field">
|
<div class="field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
@ -72,7 +72,7 @@
|
|||||||
<i class="ui green checkmark icon"></i> Redirection Rules Added
|
<i class="ui green checkmark icon"></i> Redirection Rules Added
|
||||||
</div>
|
</div>
|
||||||
<br><br>
|
<br><br>
|
||||||
<!--
|
|
||||||
<div class="advancezone ui basic segment">
|
<div class="advancezone ui basic segment">
|
||||||
<div class="ui accordion advanceSettings">
|
<div class="ui accordion advanceSettings">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
@ -85,7 +85,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,10 +43,17 @@
|
|||||||
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
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">
|
$("#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="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="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 data-label="" editable="true" datatype="basicauth">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
|
||||||
<td class="center aligned" editable="true" datatype="action" data-label="">
|
<td 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>
|
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
|
||||||
|
@ -116,7 +116,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>Results: <div id="ipRangeOutput">N/A</div></p>
|
<p>Results: <div id="ipRangeOutput">N/A</div></p>
|
||||||
</div>
|
</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 -->
|
<!-- System Information -->
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<div id="zoraxyinfo">
|
<div id="zoraxyinfo">
|
||||||
|
@ -44,10 +44,18 @@
|
|||||||
if (vdir.RequireTLS){
|
if (vdir.RequireTLS){
|
||||||
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
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">
|
$("#vdirList").append(`<tr eptuuid="${vdir.RootOrMatchingDomain}" payload="${vdirData}" class="vdirEntry">
|
||||||
<td data-label="" editable="false">${vdir.RootOrMatchingDomain}</td>
|
<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="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 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="">
|
<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>
|
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
|
||||||
|
@ -364,6 +364,7 @@
|
|||||||
$(".sideWrapper").show();
|
$(".sideWrapper").show();
|
||||||
$(".sideWrapper .fadingBackground").fadeIn("fast");
|
$(".sideWrapper .fadingBackground").fadeIn("fast");
|
||||||
$(".sideWrapper .content").transition('slide left in', 300);
|
$(".sideWrapper .content").transition('slide left in', 300);
|
||||||
|
$("body").css("overflow", "hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideSideWrapper(discardFrameContent = false){
|
function hideSideWrapper(discardFrameContent = false){
|
||||||
@ -378,6 +379,7 @@
|
|||||||
$(".sideWrapper").hide();
|
$(".sideWrapper").hide();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
$("body").css("overflow", "auto");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -179,7 +179,7 @@ body{
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sideWrapper iframe{
|
.sideWrapper iframe{
|
||||||
height: 100%;
|
height: calc(100% - 55px);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0px solid transparent;
|
border: 0px solid transparent;
|
||||||
}
|
}
|
||||||
|
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>
|
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>
|
Loading…
x
Reference in New Issue
Block a user