Browse Source

auto update script executed

Toby Chui 1 year ago
parent
commit
3f74742fd1
11 changed files with 251 additions and 65 deletions
  1. 5 3
      api.go
  2. 2 2
      cert.go
  3. 155 3
      config.go
  4. 0 39
      geoip.go
  5. 4 4
      mod/acme/acme.go
  6. 2 2
      mod/ganserv/authkeyLinux.go
  7. 5 5
      mod/ganserv/authkeyWin.go
  8. 1 1
      reverseproxy.go
  9. 6 5
      start.go
  10. 4 1
      web/components/utils.html
  11. 67 0
      web/snippet/configTools.html

+ 5 - 3
api.go

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

+ 2 - 2
cert.go

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

+ 155 - 3
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,149 @@ 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) {
+	// 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 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
+	}
+
+	// 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 !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)
+	fmt.Fprintln(w, "Configuration restored")
+}

+ 0 - 39
geoip.go

@@ -1,39 +0,0 @@
-package main
-
-import (
-	"net"
-	"net/http"
-	"strings"
-
-	"github.com/oschwald/geoip2-golang"
-)
-
-func getCountryCodeFromRequest(r *http.Request) string {
-	countryCode := ""
-
-	// Get the IP address of the user from the request headers
-	ipAddress := r.Header.Get("X-Forwarded-For")
-	if ipAddress == "" {
-		ipAddress = strings.Split(r.RemoteAddr, ":")[0]
-	}
-
-	// Open the GeoIP database
-	db, err := geoip2.Open("./tmp/GeoIP2-Country.mmdb")
-	if err != nil {
-		// Handle the error
-		return countryCode
-	}
-	defer db.Close()
-
-	// Look up the country code for the IP address
-	record, err := db.Country(net.ParseIP(ipAddress))
-	if err != nil {
-		// Handle the error
-		return countryCode
-	}
-
-	// Get the ISO country code from the record
-	countryCode = record.Country.IsoCode
-
-	return countryCode
-}

+ 4 - 4
mod/acme/acme.go

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

+ 2 - 2
mod/ganserv/authkeyLinux.go

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

+ 5 - 5
mod/ganserv/authkeyWin.go

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

+ 1 - 1
reverseproxy.go

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

+ 6 - 5
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)
 	}

+ 4 - 1
web/components/utils.html

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

+ 67 - 0
web/snippet/configTools.html

@@ -0,0 +1,67 @@
+<!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>
+            <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. <b style="color: rgba(255, 0, 0, 0.644);">DO NOT UPLOAD ZIP FILE FROM UNKNOWN SOURCE</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>
+
+            function downloadConfig(){
+                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>