ACME compatibility fix for /.well-known/

+ Updated acme well known take-over regrex
+ Added experimental config export and import
+ Added unit test for location rewrite in dpcore
+ Moved all config files to ./conf and original proxy files to ./conf/proxy
+ Minor optimization on UI regarding TLS verification logo on subdomain and vdir list
This commit is contained in:
Toby Chui 2023-07-12 21:42:09 +08:00
parent 12c1118af9
commit 153d056bdf
19 changed files with 455 additions and 57 deletions

View File

@ -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) {

View File

@ -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
}

View File

@ -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

View File

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

View File

@ -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{

View File

@ -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 {

View File

@ -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) {

View File

@ -91,11 +91,12 @@ func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerificatio
//Hack the default transporter to handle more connections
thisTransporter := http.DefaultTransport
thisTransporter.(*http.Transport).MaxIdleConns = 3000
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = 3000
thisTransporter.(*http.Transport).IdleConnTimeout = 10 * time.Second
thisTransporter.(*http.Transport).MaxConnsPerHost = 0
//thisTransporter.(*http.Transport).DisableCompression = true
optimalConcurrentConnection := 32
thisTransporter.(*http.Transport).MaxIdleConns = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = optimalConcurrentConnection
thisTransporter.(*http.Transport).IdleConnTimeout = 30 * time.Second
thisTransporter.(*http.Transport).MaxConnsPerHost = optimalConcurrentConnection * 2
thisTransporter.(*http.Transport).DisableCompression = true
if ignoreTLSVerification {
//Ignore TLS certificate validation error

View File

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

View File

@ -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)
}

View File

@ -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
}

View File

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

View File

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

View File

@ -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 {

View File

@ -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)
}

View File

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

View File

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

View File

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

View File

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