mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-03 06:07:20 +02:00
Merge pull request #28 from tobychui/autorenew_experimental
Auto-renew with ACME
This commit is contained in:
commit
544894bbba
@ -48,7 +48,7 @@ func acmeRegisterSpecialRoutingRule() {
|
||||
err := dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
|
||||
ID: "acme-autorenew",
|
||||
MatchRule: func(r *http.Request) bool {
|
||||
found, _ := regexp.MatchString("/.well-known/*", r.RequestURI)
|
||||
found, _ := regexp.MatchString("/.well-known/acme-challenge/*", r.RequestURI)
|
||||
return found
|
||||
},
|
||||
RoutingHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -150,9 +150,6 @@ func initAPIs() {
|
||||
http.HandleFunc("/api/account/reset", HandleAdminAccountResetEmail)
|
||||
http.HandleFunc("/api/account/new", HandleNewPasswordSetup)
|
||||
|
||||
//Others
|
||||
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
|
||||
|
||||
//ACME & Auto Renewer
|
||||
authRouter.HandleFunc("/api/acme/listExpiredDomains", acmeHandler.HandleGetExpiredDomains)
|
||||
authRouter.HandleFunc("/api/acme/obtainCert", AcmeCheckAndHandleRenewCertificate)
|
||||
@ -164,6 +161,11 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
|
||||
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
|
||||
|
||||
//Others
|
||||
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
|
||||
http.HandleFunc("/api/conf/export", ExportConfigAsZip)
|
||||
http.HandleFunc("/api/conf/import", ImportConfigFromZip)
|
||||
|
||||
//If you got APIs to add, append them here
|
||||
}
|
||||
|
||||
|
@ -111,7 +111,7 @@ 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("./certs/")
|
||||
filenames, err := os.ReadDir("./conf/certs/")
|
||||
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
@ -123,7 +123,7 @@ func handleListDomains(w http.ResponseWriter, r *http.Request) {
|
||||
if filename.IsDir() {
|
||||
continue
|
||||
}
|
||||
certFilepath := filepath.Join("./certs/", filename.Name())
|
||||
certFilepath := filepath.Join("./conf/certs/", filename.Name())
|
||||
|
||||
certBtyes, err := os.ReadFile(certFilepath)
|
||||
if err != nil {
|
||||
@ -273,8 +273,8 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
defer file.Close()
|
||||
|
||||
// create file in upload directory
|
||||
os.MkdirAll("./certs", 0775)
|
||||
f, err := os.Create(filepath.Join("./certs", overWriteFilename))
|
||||
os.MkdirAll("./conf/certs", 0775)
|
||||
f, err := os.Create(filepath.Join("./conf/certs", overWriteFilename))
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
||||
return
|
||||
|
211
src/config.go
211
src/config.go
@ -1,12 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
@ -31,7 +37,7 @@ type Record struct {
|
||||
|
||||
func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
|
||||
//TODO: Make this accept new def types
|
||||
os.MkdirAll("conf", 0775)
|
||||
os.MkdirAll("./conf/proxy/", 0775)
|
||||
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
|
||||
|
||||
//Generate record
|
||||
@ -39,12 +45,12 @@ func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
|
||||
|
||||
//Write to file
|
||||
js, _ := json.MarshalIndent(thisRecord, "", " ")
|
||||
return ioutil.WriteFile(filepath.Join("conf", filename), js, 0775)
|
||||
return ioutil.WriteFile(filepath.Join("./conf/proxy/", filename), js, 0775)
|
||||
}
|
||||
|
||||
func RemoveReverseProxyConfig(rootname string) error {
|
||||
filename := getFilenameFromRootName(rootname)
|
||||
removePendingFile := strings.ReplaceAll(filepath.Join("conf", filename), "\\", "/")
|
||||
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
|
||||
log.Println("Config Removed: ", removePendingFile)
|
||||
if utils.FileExists(removePendingFile) {
|
||||
err := os.Remove(removePendingFile)
|
||||
@ -83,3 +89,202 @@ func getFilenameFromRootName(rootname string) string {
|
||||
filename = filename + ".config"
|
||||
return filename
|
||||
}
|
||||
|
||||
/*
|
||||
Importer and Exporter of Zoraxy proxy config
|
||||
*/
|
||||
|
||||
func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
includeSysDBRaw, err := utils.GetPara(r, "includeDB")
|
||||
includeSysDB := false
|
||||
if includeSysDBRaw == "true" {
|
||||
//Include the system database in backup snapshot
|
||||
//Temporary set it to read only
|
||||
sysdb.ReadOnly = true
|
||||
includeSysDB = true
|
||||
}
|
||||
|
||||
// Specify the folder path to be zipped
|
||||
folderPath := "./conf/"
|
||||
|
||||
// Set the Content-Type header to indicate it's a zip file
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
// Set the Content-Disposition header to specify the file name
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\"config.zip\"")
|
||||
|
||||
// Create a zip writer
|
||||
zipWriter := zip.NewWriter(w)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// Walk through the folder and add files to the zip
|
||||
err = filepath.Walk(folderPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if folderPath == filePath {
|
||||
//Skip root folder
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new file in the zip
|
||||
if !utils.IsDir(filePath) {
|
||||
zipFile, err := zipWriter.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Open the file on disk
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy the file contents to the zip file
|
||||
_, err = io.Copy(zipFile, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if includeSysDB {
|
||||
//Also zip in the sysdb
|
||||
zipFile, err := zipWriter.Create("sys.db")
|
||||
if err != nil {
|
||||
log.Println("[Backup] Unable to zip sysdb: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Open the file on disk
|
||||
file, err := os.Open("sys.db")
|
||||
if err != nil {
|
||||
log.Println("[Backup] Unable to open sysdb: " + err.Error())
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Copy the file contents to the zip file
|
||||
_, err = io.Copy(zipFile, file)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
//Restore sysdb state
|
||||
sysdb.ReadOnly = false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Handle the error and send an HTTP response with the error message
|
||||
http.Error(w, fmt.Sprintf("Failed to zip folder: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if the request is a POST with a file upload
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Invalid request method", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Max file size limit (10 MB in this example)
|
||||
r.ParseMultipartForm(10 << 20)
|
||||
|
||||
// Get the uploaded file
|
||||
file, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to retrieve uploaded file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if filepath.Ext(handler.Filename) != ".zip" {
|
||||
http.Error(w, "Upload file is not a zip file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Create the target directory to unzip the files
|
||||
targetDir := "./conf"
|
||||
if utils.FileExists(targetDir) {
|
||||
//Backup the old config to old
|
||||
os.Rename("./conf", "./conf.old_"+strconv.Itoa(int(time.Now().Unix())))
|
||||
}
|
||||
|
||||
err = os.MkdirAll(targetDir, os.ModePerm)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to create target directory: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Open the zip file
|
||||
zipReader, err := zip.NewReader(file, handler.Size)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to open zip file: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
restoreDatabase := false
|
||||
|
||||
// Extract each file from the zip archive
|
||||
for _, zipFile := range zipReader.File {
|
||||
// Open the file in the zip archive
|
||||
rc, err := zipFile.Open()
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to open file in zip: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// Create the corresponding file on disk
|
||||
zipFile.Name = strings.ReplaceAll(zipFile.Name, "../", "")
|
||||
fmt.Println("Restoring: " + strings.ReplaceAll(zipFile.Name, "\\", "/"))
|
||||
if zipFile.Name == "sys.db" {
|
||||
//Sysdb replacement. Close the database and restore
|
||||
sysdb.Close()
|
||||
restoreDatabase = true
|
||||
} else if !strings.HasPrefix(strings.ReplaceAll(zipFile.Name, "\\", "/"), "conf/") {
|
||||
//Malformed zip file.
|
||||
http.Error(w, fmt.Sprintf("Invalid zip file structure or version too old"), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
//Check if parent dir exists
|
||||
if !utils.FileExists(filepath.Dir(zipFile.Name)) {
|
||||
os.MkdirAll(filepath.Dir(zipFile.Name), 0775)
|
||||
}
|
||||
|
||||
//Create the file
|
||||
newFile, err := os.Create(zipFile.Name)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to create file: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer newFile.Close()
|
||||
|
||||
// Copy the file contents from the zip to the new file
|
||||
_, err = io.Copy(newFile, rc)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to extract file from zip: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Send a success response
|
||||
w.WriteHeader(http.StatusOK)
|
||||
log.Println("Configuration restored")
|
||||
fmt.Fprintln(w, "Configuration restored")
|
||||
|
||||
if restoreDatabase {
|
||||
go func() {
|
||||
log.Println("Database altered. Restarting in 3 seconds...")
|
||||
time.Sleep(3 * time.Second)
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
43
src/main.go
43
src/main.go
@ -83,29 +83,34 @@ func SetupCloseHandler() {
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
fmt.Println("- Shutting down " + name)
|
||||
fmt.Println("- Closing GeoDB ")
|
||||
geodbStore.Close()
|
||||
fmt.Println("- Closing Netstats Listener")
|
||||
netstatBuffers.Close()
|
||||
fmt.Println("- Closing Statistic Collector")
|
||||
statisticCollector.Close()
|
||||
fmt.Println("- Stopping mDNS Discoverer")
|
||||
//Stop the mdns service
|
||||
mdnsTickerStop <- true
|
||||
mdnsScanner.Close()
|
||||
|
||||
//Remove the tmp folder
|
||||
fmt.Println("- Cleaning up tmp files")
|
||||
os.RemoveAll("./tmp")
|
||||
|
||||
//Close database, final
|
||||
fmt.Println("- Stopping system database")
|
||||
sysdb.Close()
|
||||
ShutdownSeq()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
func ShutdownSeq() {
|
||||
fmt.Println("- Shutting down " + name)
|
||||
fmt.Println("- Closing GeoDB ")
|
||||
geodbStore.Close()
|
||||
fmt.Println("- Closing Netstats Listener")
|
||||
netstatBuffers.Close()
|
||||
fmt.Println("- Closing Statistic Collector")
|
||||
statisticCollector.Close()
|
||||
fmt.Println("- Stopping mDNS Discoverer")
|
||||
//Stop the mdns service
|
||||
mdnsTickerStop <- true
|
||||
mdnsScanner.Close()
|
||||
fmt.Println("- Closing Certificates Auto Renewer")
|
||||
acmeAutoRenewer.Close()
|
||||
//Remove the tmp folder
|
||||
fmt.Println("- Cleaning up tmp files")
|
||||
os.RemoveAll("./tmp")
|
||||
|
||||
//Close database, final
|
||||
fmt.Println("- Stopping system database")
|
||||
sysdb.Close()
|
||||
}
|
||||
|
||||
func main() {
|
||||
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
|
||||
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
|
||||
|
@ -134,12 +134,12 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
|
||||
// Each certificate comes back with the cert bytes, the bytes of the client's
|
||||
// private key, and a certificate URL.
|
||||
err = ioutil.WriteFile("./certs/"+certificateName+".crt", certificates.Certificate, 0777)
|
||||
err = ioutil.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
}
|
||||
err = ioutil.WriteFile("./certs/"+certificateName+".key", certificates.PrivateKey, 0777)
|
||||
err = ioutil.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
@ -154,7 +154,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
|
||||
// it will said expired as well!
|
||||
func (a *ACMEHandler) CheckCertificate() []string {
|
||||
// read from dir
|
||||
filenames, err := os.ReadDir("./certs/")
|
||||
filenames, err := os.ReadDir("./conf/certs/")
|
||||
|
||||
expiredCerts := []string{}
|
||||
|
||||
@ -164,7 +164,7 @@ func (a *ACMEHandler) CheckCertificate() []string {
|
||||
}
|
||||
|
||||
for _, filename := range filenames {
|
||||
certFilepath := filepath.Join("./certs/", filename.Name())
|
||||
certFilepath := filepath.Join("./conf/certs/", filename.Name())
|
||||
|
||||
certBytes, err := os.ReadFile(certFilepath)
|
||||
if err != nil {
|
||||
|
@ -341,6 +341,12 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
|
||||
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) {
|
||||
|
@ -91,11 +91,12 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
|
||||
|
||||
//Hack the default transporter to handle more connections
|
||||
thisTransporter := http.DefaultTransport
|
||||
thisTransporter.(*http.Transport).MaxIdleConns = 3000
|
||||
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = 3000
|
||||
thisTransporter.(*http.Transport).IdleConnTimeout = 10 * time.Second
|
||||
thisTransporter.(*http.Transport).MaxConnsPerHost = 0
|
||||
//thisTransporter.(*http.Transport).DisableCompression = true
|
||||
optimalConcurrentConnection := 32
|
||||
thisTransporter.(*http.Transport).MaxIdleConns = optimalConcurrentConnection * 2
|
||||
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = optimalConcurrentConnection
|
||||
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
|
||||
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
|
||||
thisTransporter.(*http.Transport).DisableCompression = true
|
||||
|
||||
if ignoreTLSVerification {
|
||||
//Ignore TLS certificate validation error
|
||||
|
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)
|
||||
}
|
||||
}
|
@ -44,3 +44,8 @@ func replaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS b
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// Debug functions
|
||||
func ReplaceLocationHost(urlString string, rrr *ResponseRewriteRuleSet, useTLS bool) (string, error) {
|
||||
return replaceLocationHost(urlString, rrr, useTLS)
|
||||
}
|
||||
|
@ -30,8 +30,13 @@ func (t *RuleTable) HandleRedirect(w http.ResponseWriter, r *http.Request) int {
|
||||
redirectTarget := rr.TargetURL
|
||||
|
||||
if rr.ForwardChildpath {
|
||||
//Remove the first / in the path
|
||||
redirectTarget += strings.TrimPrefix(r.URL.Path, "/")
|
||||
//Remove the first / in the path if the redirect target already have tailing slash
|
||||
if strings.HasSuffix(redirectTarget, "/") {
|
||||
redirectTarget += strings.TrimPrefix(r.URL.Path, "/")
|
||||
} else {
|
||||
redirectTarget += r.URL.Path
|
||||
}
|
||||
|
||||
if r.URL.RawQuery != "" {
|
||||
redirectTarget += "?" + r.URL.RawQuery
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ import (
|
||||
)
|
||||
|
||||
func readAuthTokenAsAdmin() (string, error) {
|
||||
if utils.FileExists("./authtoken.secret") {
|
||||
authKey, err := os.ReadFile("./authtoken.secret")
|
||||
if utils.FileExists("./conf/authtoken.secret") {
|
||||
authKey, err := os.ReadFile("./conf/authtoken.secret")
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(authKey)), nil
|
||||
}
|
||||
|
@ -19,8 +19,8 @@ import (
|
||||
// Use admin permission to read auth token on Windows
|
||||
func readAuthTokenAsAdmin() (string, error) {
|
||||
//Check if the previous startup already extracted the authkey
|
||||
if utils.FileExists("./authtoken.secret") {
|
||||
authKey, err := os.ReadFile("./authtoken.secret")
|
||||
if utils.FileExists("./conf/authtoken.secret") {
|
||||
authKey, err := os.ReadFile("./conf/authtoken.secret")
|
||||
if err == nil {
|
||||
return strings.TrimSpace(string(authKey)), nil
|
||||
}
|
||||
@ -30,7 +30,7 @@ func readAuthTokenAsAdmin() (string, error) {
|
||||
exe := "cmd.exe"
|
||||
cwd, _ := os.Getwd()
|
||||
|
||||
output, _ := filepath.Abs(filepath.Join("./", "authtoken.secret"))
|
||||
output, _ := filepath.Abs(filepath.Join("./conf/", "authtoken.secret"))
|
||||
os.WriteFile(output, []byte(""), 0775)
|
||||
args := fmt.Sprintf("/C type \"C:\\ProgramData\\ZeroTier\\One\\authtoken.secret\" > \"" + output + "\"")
|
||||
|
||||
@ -49,13 +49,13 @@ func readAuthTokenAsAdmin() (string, error) {
|
||||
log.Println("Please click agree to allow access to ZeroTier authtoken from ProgramData")
|
||||
retry := 0
|
||||
time.Sleep(3 * time.Second)
|
||||
for !utils.FileExists("./authtoken.secret") && retry < 10 {
|
||||
for !utils.FileExists("./conf/authtoken.secret") && retry < 10 {
|
||||
time.Sleep(3 * time.Second)
|
||||
log.Println("Waiting for ZeroTier authtoken extraction...")
|
||||
retry++
|
||||
}
|
||||
|
||||
authKey, err := os.ReadFile("./authtoken.secret")
|
||||
authKey, err := os.ReadFile("./conf/authtoken.secret")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ func ReverseProxtInit() {
|
||||
dynamicProxyRouter = dprouter
|
||||
|
||||
//Load all conf from files
|
||||
confs, _ := filepath.Glob("./conf/*.config")
|
||||
confs, _ := filepath.Glob("./conf/proxy/*.config")
|
||||
for _, conf := range confs {
|
||||
record, err := LoadReverseProxyConfig(conf)
|
||||
if err != nil {
|
||||
|
11
src/start.go
11
src/start.go
@ -49,8 +49,9 @@ func startupSequence() {
|
||||
//Create tables for the database
|
||||
sysdb.NewTable("settings")
|
||||
|
||||
//Create tmp folder
|
||||
//Create tmp folder and conf folder
|
||||
os.MkdirAll("./tmp", 0775)
|
||||
os.MkdirAll("./conf/proxy/", 0775)
|
||||
|
||||
//Create an auth agent
|
||||
sessionKey, err := auth.GetSessionKey(sysdb)
|
||||
@ -63,13 +64,13 @@ func startupSequence() {
|
||||
})
|
||||
|
||||
//Create a TLS certificate manager
|
||||
tlsCertManager, err = tlscert.NewManager("./certs", development)
|
||||
tlsCertManager, err = tlscert.NewManager("./conf/certs", development)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a redirection rule table
|
||||
redirectTable, err = redirection.NewRuleTable("./rules/redirect")
|
||||
redirectTable, err = redirection.NewRuleTable("./conf/redirect")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -104,7 +105,7 @@ func startupSequence() {
|
||||
|
||||
pathRuleHandler = pathrule.NewPathRuleHandler(&pathrule.Options{
|
||||
Enabled: false,
|
||||
ConfigFolder: "./rules/pathrules",
|
||||
ConfigFolder: "./conf/rules/pathrules",
|
||||
})
|
||||
|
||||
/*
|
||||
@ -197,7 +198,7 @@ func startupSequence() {
|
||||
Obtaining certificates from ACME Server
|
||||
*/
|
||||
acmeHandler = initACME()
|
||||
acmeAutoRenewer, err = acme.NewAutoRenewer("./rules/acme_conf.json", "./certs/", int64(*acmeAutoRenewInterval), acmeHandler)
|
||||
acmeAutoRenewer, err = acme.NewAutoRenewer("./conf/acme_conf.json", "./conf/certs/", int64(*acmeAutoRenewInterval), acmeHandler)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -43,10 +43,17 @@
|
||||
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
||||
}
|
||||
|
||||
let tlsVerificationField = "";
|
||||
if (subd.RequireTLS){
|
||||
tlsVerificationField = !subd.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`
|
||||
}else{
|
||||
tlsVerificationField = "N/A"
|
||||
}
|
||||
|
||||
$("#subdList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
|
||||
<td data-label="" editable="false"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a></td>
|
||||
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
|
||||
<td data-label="" editable="true" datatype="skipver">${!subd.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`}</td>
|
||||
<td data-label="" editable="true" datatype="skipver">${tlsVerificationField}</td>
|
||||
<td data-label="" editable="true" datatype="basicauth">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
|
||||
<td class="center aligned" editable="true" datatype="action" data-label="">
|
||||
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
|
||||
|
@ -116,7 +116,11 @@
|
||||
</div>
|
||||
<p>Results: <div id="ipRangeOutput">N/A</div></p>
|
||||
</div>
|
||||
|
||||
<!-- Config Tools -->
|
||||
<div class="ui divider"></div>
|
||||
<h3>System Backup & Restore</h3>
|
||||
<p>Options related to system backup, migrate and restore.</p>
|
||||
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
|
||||
<!-- System Information -->
|
||||
<div class="ui divider"></div>
|
||||
<div id="zoraxyinfo">
|
||||
|
@ -44,10 +44,18 @@
|
||||
if (vdir.RequireTLS){
|
||||
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
||||
}
|
||||
|
||||
let tlsVerificationField = "";
|
||||
if (vdir.RequireTLS){
|
||||
tlsVerificationField = !vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`
|
||||
}else{
|
||||
tlsVerificationField = "N/A"
|
||||
}
|
||||
|
||||
$("#vdirList").append(`<tr eptuuid="${vdir.RootOrMatchingDomain}" payload="${vdirData}" class="vdirEntry">
|
||||
<td data-label="" editable="false">${vdir.RootOrMatchingDomain}</td>
|
||||
<td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
|
||||
<td data-label="" editable="true" datatype="skipver">${!vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`}</td>
|
||||
<td data-label="" editable="true" datatype="skipver">${tlsVerificationField}</td>
|
||||
<td data-label="" editable="true" datatype="basicauth">${vdir.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
|
||||
<td class="center aligned" editable="true" datatype="action" data-label="">
|
||||
<button class="ui circular mini basic icon button editBtn" onclick='editEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
|
||||
|
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>
|
Loading…
x
Reference in New Issue
Block a user