diff --git a/src/Makefile b/src/Makefile
index c5dbbc2..e82e24e 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -19,7 +19,8 @@ clean:
$(PLATFORMS):
@echo "Building $(os)/$(arch)"
- GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) GOARM=6 go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
+ GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) $(if $(filter linux/arm,$(os)/$(arch)),GOARM=6,) go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
+# GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) GOARM=6 go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
fixwindows:
diff --git a/src/acme.go b/src/acme.go
index 72e5883..a04a257 100644
--- a/src/acme.go
+++ b/src/acme.go
@@ -1,8 +1,9 @@
package main
import (
+ "encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"log"
"math/rand"
"net/http"
@@ -38,7 +39,7 @@ func initACME() *acme.ACMEHandler {
port = getRandomPort(30000)
}
- return acme.NewACME("https://acme-staging-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
+ return acme.NewACME("https://acme-v02.api.letsencrypt.org/directory", strconv.Itoa(port))
}
// create the special routing rule for ACME
@@ -65,7 +66,7 @@ func acmeRegisterSpecialRoutingRule() {
return
}
- resBody, err := ioutil.ReadAll(res.Body)
+ resBody, err := io.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
fmt.Printf("error reading: %s\n", err)
@@ -114,3 +115,23 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
}
}
}
+
+// HandleACMEPreferredCA return the user preferred / default CA for new subdomain auto creation
+func HandleACMEPreferredCA(w http.ResponseWriter, r *http.Request) {
+ ca, err := utils.PostPara(r, "set")
+ if err != nil {
+ //Return the current ca to user
+ prefCA := "Let's Encrypt"
+ sysdb.Read("acmepref", "prefca", &prefCA)
+ js, _ := json.Marshal(prefCA)
+ utils.SendJSONResponse(w, string(js))
+ } else {
+ //Check if the CA is supported
+ acme.IsSupportedCA(ca)
+ //Set the new config
+ sysdb.Write("acmepref", "prefca", ca)
+ log.Println("Updating prefered ACME CA to " + ca)
+ utils.SendOK(w)
+ }
+
+}
diff --git a/src/api.go b/src/api.go
index 8faea65..5b52342 100644
--- a/src/api.go
+++ b/src/api.go
@@ -162,6 +162,7 @@ func initAPIs() {
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/ca", HandleACMEPreferredCA)
authRouter.HandleFunc("/api/acme/autoRenew/email", acmeAutoRenewer.HandleACMEEmail)
authRouter.HandleFunc("/api/acme/autoRenew/setDomains", acmeAutoRenewer.HandleSetAutoRenewDomains)
authRouter.HandleFunc("/api/acme/autoRenew/listDomains", acmeAutoRenewer.HandleLoadAutoRenewDomains)
@@ -169,6 +170,24 @@ func initAPIs() {
authRouter.HandleFunc("/api/acme/autoRenew/renewNow", acmeAutoRenewer.HandleRenewNow)
authRouter.HandleFunc("/api/acme/wizard", acmewizard.HandleGuidedStepCheck) //ACME Wizard
+ //Static Web Server
+ authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
+ authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
+ authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
+ authRouter.HandleFunc("/api/webserv/setPort", staticWebServer.HandlePortChange)
+ authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
+ if *allowWebFileManager {
+ //Web Directory Manager file operation functions
+ authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
+ authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
+ authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
+ authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
+ authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
+ authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
+ authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
+ authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
+ }
+
//Others
http.HandleFunc("/api/info/x", HandleZoraxyInfo)
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
diff --git a/src/main.go b/src/main.go
index edff546..4177a75 100644
--- a/src/main.go
+++ b/src/main.go
@@ -30,6 +30,7 @@ import (
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
+ "imuslab.com/zoraxy/mod/webserv"
)
// General flags
@@ -41,9 +42,12 @@ var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local no
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 enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
+var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
+var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
+
var (
name = "Zoraxy"
- version = "2.6.6"
+ version = "2.6.7"
nodeUUID = "generic"
development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix()
@@ -73,6 +77,7 @@ var (
tcpProxyManager *tcpprox.Manager //TCP Proxy Manager
acmeHandler *acme.ACMEHandler //Handler for ACME Certificate renew
acmeAutoRenewer *acme.AutoRenewer //Handler for ACME auto renew ticking
+ staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending
diff --git a/src/mod/acme/acme.go b/src/mod/acme/acme.go
index d82cf05..efbd325 100644
--- a/src/mod/acme/acme.go
+++ b/src/mod/acme/acme.go
@@ -10,7 +10,6 @@ import (
"encoding/json"
"encoding/pem"
"fmt"
- "io/ioutil"
"log"
"net"
"net/http"
@@ -164,12 +163,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("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
+ err = os.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)
+ err = os.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777)
if err != nil {
log.Println(err)
return false, err
@@ -303,18 +302,23 @@ func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Requ
ca, err := utils.PostPara(r, "ca")
if err != nil {
- log.Println("CA not set. Using default")
+ log.Println("[INFO] CA not set. Using default")
ca, caUrl = "", ""
}
if ca == "custom" {
caUrl, err = utils.PostPara(r, "caURL")
if err != nil {
- log.Println("Custom CA set but no URL provide, Using default")
+ log.Println("[INFO] Custom CA set but no URL provide, Using default")
ca, caUrl = "", ""
}
}
+ if ca == "" {
+ //default. Use Let's Encrypt
+ ca = "Let's Encrypt"
+ }
+
var skipTLS bool
if skipTLSString, err := utils.PostPara(r, "skipTLS"); err != nil {
@@ -357,8 +361,8 @@ func IsPortInUse(port int) bool {
}
+// Load cert information from json file
func loadCertInfoJSON(filename string) (*CertificateInfoJSON, error) {
-
certInfoBytes, err := os.ReadFile(filename)
if err != nil {
return nil, err
diff --git a/src/mod/acme/autorenew.go b/src/mod/acme/autorenew.go
index 13872fd..b5f1596 100644
--- a/src/mod/acme/autorenew.go
+++ b/src/mod/acme/autorenew.go
@@ -40,7 +40,6 @@ type AutoRenewer struct {
type ExpiredCerts struct {
Domains []string
Filepath string
- CA string
}
// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
@@ -280,12 +279,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
}
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 {
@@ -296,7 +289,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
expiredCertList = append(expiredCertList, &ExpiredCerts{
Filepath: filepath.Join(certFolder, file.Name()),
- CA: CAName,
Domains: DNSName,
})
}
@@ -315,12 +307,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
}
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 {
@@ -331,7 +317,6 @@ func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
expiredCertList = append(expiredCertList, &ExpiredCerts{
Filepath: filepath.Join(certFolder, file.Name()),
- CA: CAName,
Domains: DNSName,
})
}
@@ -361,8 +346,14 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro
certInfoFilename := fmt.Sprintf("%s/%s.json", filepath.Dir(expiredCert.Filepath), certName)
certInfo, err := loadCertInfoJSON(certInfoFilename)
if err != nil {
- log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, using default ACME", certName, err)
- certInfo = &CertificateInfoJSON{}
+ log.Printf("Renew %s certificate error, can't get the ACME detail for cert: %v, trying org section as ca", certName, err)
+
+ if CAName, extractErr := ExtractIssuerNameFromPEM(expiredCert.Filepath); extractErr != nil {
+ log.Printf("extract issuer name for cert error: %v, using default ca", extractErr)
+ certInfo = &CertificateInfoJSON{}
+ } else {
+ certInfo = &CertificateInfoJSON{AcmeName: CAName}
+ }
}
_, err = a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, certInfo.AcmeName, certInfo.AcmeUrl, certInfo.SkipTLS)
diff --git a/src/mod/acme/ca.go b/src/mod/acme/ca.go
index e2a9b96..b673170 100644
--- a/src/mod/acme/ca.go
+++ b/src/mod/acme/ca.go
@@ -10,6 +10,7 @@ import (
"encoding/json"
"errors"
"log"
+ "strings"
)
// CA Defination, load from embeded json when startup
@@ -32,14 +33,24 @@ func init() {
}
caDef = runtimeCaDef
-
}
// Get the CA ACME server endpoint and error if not found
func loadCAApiServerFromName(caName string) (string, error) {
+ // handle BuyPass cert org section (Buypass AS-983163327)
+ if strings.HasPrefix(caName, "Buypass AS") {
+ caName = "Buypass"
+ }
+
val, ok := caDef.Production[caName]
if !ok {
return "", errors.New("This CA is not supported")
}
+
return val, nil
}
+
+func IsSupportedCA(caName string) bool {
+ _, err := loadCAApiServerFromName(caName)
+ return err == nil
+}
diff --git a/src/mod/acme/utils.go b/src/mod/acme/utils.go
index 40d873d..1638044 100644
--- a/src/mod/acme/utils.go
+++ b/src/mod/acme/utils.go
@@ -53,6 +53,11 @@ func ExtractIssuerName(certBytes []byte) (string, error) {
return "", fmt.Errorf("failed to parse certificate: %v", err)
}
+ // Check if exist incase some acme server didn't have org section
+ if len(cert.Issuer.Organization) == 0 {
+ return "", fmt.Errorf("cert didn't have org section exist")
+ }
+
// Extract the issuer name
issuer := cert.Issuer.Organization[0]
diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go
index 1fa4793..c446277 100644
--- a/src/mod/dynamicproxy/Server.go
+++ b/src/mod/dynamicproxy/Server.go
@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"os"
+ "path/filepath"
"strings"
"imuslab.com/zoraxy/mod/geodb"
@@ -192,9 +193,9 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
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")
+ template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/blacklist.html"))
if err != nil {
- w.Write([]byte("403 - Forbidden"))
+ w.Write(page_forbidden)
} else {
w.Write(template)
}
@@ -206,9 +207,9 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
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")
+ template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/whitelist.html"))
if err != nil {
- w.Write([]byte("403 - Forbidden"))
+ w.Write(page_forbidden)
} else {
w.Write(template)
}
diff --git a/src/mod/dynamicproxy/templates/forbidden.html b/src/mod/dynamicproxy/templates/forbidden.html
new file mode 100644
index 0000000..5c3f6e2
--- /dev/null
+++ b/src/mod/dynamicproxy/templates/forbidden.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+ Forbidden
+
+
+
+
+
+
+
403 - Forbidden
+
+
You do not have permission to view this directory or page.
+ This might cause by the region limit setting of this site.
+
+
+ Request time:
+ Request URI:
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go
index 218bae5..2372f04 100644
--- a/src/mod/dynamicproxy/typedef.go
+++ b/src/mod/dynamicproxy/typedef.go
@@ -1,6 +1,7 @@
package dynamicproxy
import (
+ _ "embed"
"net"
"net/http"
"sync"
@@ -31,6 +32,7 @@ type RouterOption struct {
RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store //GeoIP blacklist and whitelist
StatisticCollector *statistic.Collector
+ WebDirectory string //The static web server directory containing the templates folder
}
type Router struct {
@@ -123,3 +125,11 @@ type SubdOptions struct {
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
+
+/*
+Web Templates
+*/
+var (
+ //go:embed templates/forbidden.html
+ page_forbidden []byte
+)
diff --git a/src/mod/utils/utils.go b/src/mod/utils/utils.go
index bd21c8b..9af3ebc 100644
--- a/src/mod/utils/utils.go
+++ b/src/mod/utils/utils.go
@@ -5,6 +5,7 @@ import (
"log"
"net/http"
"os"
+ "strconv"
"strings"
"time"
)
@@ -76,6 +77,22 @@ func PostBool(r *http.Request, key string) (bool, error) {
return false, errors.New("invalid boolean given")
}
+// Get POST paramter as int
+func PostInt(r *http.Request, key string) (int, error) {
+ x, err := PostPara(r, key)
+ if err != nil {
+ return 0, err
+ }
+
+ x = strings.TrimSpace(x)
+ rx, err := strconv.Atoi(x)
+ if err != nil {
+ return 0, err
+ }
+
+ return rx, nil
+}
+
func FileExists(filename string) bool {
_, err := os.Stat(filename)
if os.IsNotExist(err) {
diff --git a/src/mod/webserv/filemanager/filemanager.go b/src/mod/webserv/filemanager/filemanager.go
new file mode 100644
index 0000000..8847553
--- /dev/null
+++ b/src/mod/webserv/filemanager/filemanager.go
@@ -0,0 +1,406 @@
+package filemanager
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "imuslab.com/zoraxy/mod/utils"
+)
+
+/*
+ File Manager
+
+ This is a simple package that handles file management
+ under the web server directory
+*/
+
+type FileManager struct {
+ Directory string
+}
+
+// Create a new file manager with directory as root
+func NewFileManager(directory string) *FileManager {
+ return &FileManager{
+ Directory: directory,
+ }
+}
+
+// Handle listing of a given directory
+func (fm *FileManager) HandleList(w http.ResponseWriter, r *http.Request) {
+ directory, err := utils.GetPara(r, "dir")
+ if err != nil {
+ utils.SendErrorResponse(w, "invalid directory given")
+ return
+ }
+
+ // Construct the absolute path to the target directory
+ targetDir := filepath.Join(fm.Directory, directory)
+
+ // Open the target directory
+ dirEntries, err := os.ReadDir(targetDir)
+ if err != nil {
+ utils.SendErrorResponse(w, "unable to open directory")
+ return
+ }
+
+ // Create a slice to hold the file information
+ var files []map[string]interface{} = []map[string]interface{}{}
+
+ // Iterate through the directory entries
+ for _, dirEntry := range dirEntries {
+ fileInfo := make(map[string]interface{})
+ fileInfo["filename"] = dirEntry.Name()
+ fileInfo["filepath"] = filepath.Join(directory, dirEntry.Name())
+ fileInfo["isDir"] = dirEntry.IsDir()
+
+ // Get file size and last modified time
+ finfo, err := dirEntry.Info()
+ if err != nil {
+ //unable to load its info. Skip this file
+ continue
+ }
+ fileInfo["lastModified"] = finfo.ModTime().Unix()
+ if !dirEntry.IsDir() {
+ // If it's a file, get its size
+ fileInfo["size"] = finfo.Size()
+ } else {
+ // If it's a directory, set size to 0
+ fileInfo["size"] = 0
+ }
+
+ // Append file info to the list
+ files = append(files, fileInfo)
+ }
+
+ // Serialize the file info slice to JSON
+ jsonData, err := json.Marshal(files)
+ if err != nil {
+ utils.SendErrorResponse(w, "unable to marshal JSON")
+ return
+ }
+
+ // Set response headers and send the JSON response
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ w.Write(jsonData)
+}
+
+// Handle upload of a file (multi-part), 25MB max
+func (fm *FileManager) HandleUpload(w http.ResponseWriter, r *http.Request) {
+ dir, err := utils.PostPara(r, "dir")
+ if err != nil {
+ log.Println("no dir given")
+ utils.SendErrorResponse(w, "invalid dir given")
+ return
+ }
+
+ // Parse the multi-part form data
+ err = r.ParseMultipartForm(25 << 20)
+ if err != nil {
+ utils.SendErrorResponse(w, "unable to parse form data")
+ return
+ }
+
+ // Get the uploaded file
+ file, fheader, err := r.FormFile("file")
+ if err != nil {
+ log.Println(err.Error())
+ utils.SendErrorResponse(w, "unable to get uploaded file")
+ return
+ }
+ defer file.Close()
+
+ // Specify the directory where you want to save the uploaded file
+ uploadDir := filepath.Join(fm.Directory, dir)
+ if !utils.FileExists(uploadDir) {
+ utils.SendErrorResponse(w, "upload target directory not exists")
+ return
+ }
+
+ filename := sanitizeFilename(fheader.Filename)
+ if !isValidFilename(filename) {
+ utils.SendErrorResponse(w, "filename contain invalid or reserved characters")
+ return
+ }
+
+ // Create the file on the server
+ filePath := filepath.Join(uploadDir, filepath.Base(filename))
+ out, err := os.Create(filePath)
+ if err != nil {
+ utils.SendErrorResponse(w, "unable to create file on the server")
+ return
+ }
+ defer out.Close()
+
+ // Copy the uploaded file to the server
+ _, err = io.Copy(out, file)
+ if err != nil {
+ utils.SendErrorResponse(w, "unable to copy file to server")
+ return
+ }
+
+ // Respond with a success message or appropriate response
+ utils.SendOK(w)
+}
+
+// Handle download of a selected file, serve with content dispose header
+func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) {
+ filename, err := utils.GetPara(r, "file")
+ if err != nil {
+ utils.SendErrorResponse(w, "invalid filepath given")
+ return
+ }
+
+ previewMode, _ := utils.GetPara(r, "preview")
+ if previewMode == "true" {
+ // Serve the file using http.ServeFile
+ filePath := filepath.Join(fm.Directory, filename)
+ http.ServeFile(w, r, filePath)
+ } else {
+ // Trigger a download with content disposition headers
+ filePath := filepath.Join(fm.Directory, filename)
+ w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename))
+ http.ServeFile(w, r, filePath)
+ }
+}
+
+// HandleNewFolder creates a new folder in the specified directory
+func (fm *FileManager) HandleNewFolder(w http.ResponseWriter, r *http.Request) {
+ // Parse the directory name from the request
+ dirName, err := utils.GetPara(r, "path")
+ if err != nil {
+ utils.SendErrorResponse(w, "invalid directory name")
+ return
+ }
+
+ //Prevent path escape
+ dirName = strings.ReplaceAll(dirName, "\\", "/")
+ dirName = strings.ReplaceAll(dirName, "../", "")
+
+ // Specify the directory where you want to create the new folder
+ newFolderPath := filepath.Join(fm.Directory, dirName)
+
+ // Check if the folder already exists
+ if _, err := os.Stat(newFolderPath); os.IsNotExist(err) {
+ // Create the new folder
+ err := os.Mkdir(newFolderPath, os.ModePerm)
+ if err != nil {
+ utils.SendErrorResponse(w, "unable to create the new folder")
+ return
+ }
+
+ // Respond with a success message or appropriate response
+ utils.SendOK(w)
+ } else {
+ // If the folder already exists, respond with an error
+ utils.SendErrorResponse(w, "folder already exists")
+ }
+}
+
+// HandleFileCopy copies a file or directory from the source path to the destination path
+func (fm *FileManager) HandleFileCopy(w http.ResponseWriter, r *http.Request) {
+ // Parse the source and destination paths from the request
+ srcPath, err := utils.PostPara(r, "srcpath")
+ if err != nil {
+ utils.SendErrorResponse(w, "invalid source path")
+ return
+ }
+
+ destPath, err := utils.PostPara(r, "destpath")
+ if err != nil {
+ utils.SendErrorResponse(w, "invalid destination path")
+ return
+ }
+
+ // Validate and sanitize the source and destination paths
+ srcPath = filepath.Clean(srcPath)
+ destPath = filepath.Clean(destPath)
+
+ // Construct the absolute paths
+ absSrcPath := filepath.Join(fm.Directory, srcPath)
+ absDestPath := filepath.Join(fm.Directory, destPath)
+
+ // Check if the source path exists
+ if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
+ utils.SendErrorResponse(w, "source path does not exist")
+ return
+ }
+
+ // Check if the destination path exists
+ if _, err := os.Stat(absDestPath); os.IsNotExist(err) {
+ utils.SendErrorResponse(w, "destination path does not exist")
+ return
+ }
+
+ //Join the name to create final paste filename
+ absDestPath = filepath.Join(absDestPath, filepath.Base(absSrcPath))
+ //Reject opr if already exists
+ if utils.FileExists(absDestPath) {
+ utils.SendErrorResponse(w, "target already exists")
+ return
+ }
+
+ // Perform the copy operation based on whether the source is a file or directory
+ if isDir(absSrcPath) {
+ // Recursive copy for directories
+ err := copyDirectory(absSrcPath, absDestPath)
+ if err != nil {
+ utils.SendErrorResponse(w, fmt.Sprintf("error copying directory: %v", err))
+ return
+ }
+ } else {
+ // Copy a single file
+ err := copyFile(absSrcPath, absDestPath)
+ if err != nil {
+ utils.SendErrorResponse(w, fmt.Sprintf("error copying file: %v", err))
+ return
+ }
+ }
+
+ utils.SendOK(w)
+}
+
+func (fm *FileManager) HandleFileMove(w http.ResponseWriter, r *http.Request) {
+ // Parse the source and destination paths from the request
+ srcPath, err := utils.GetPara(r, "srcpath")
+ if err != nil {
+ utils.SendErrorResponse(w, "invalid source path")
+ return
+ }
+
+ destPath, err := utils.GetPara(r, "destpath")
+ if err != nil {
+ utils.SendErrorResponse(w, "invalid destination path")
+ return
+ }
+
+ // Validate and sanitize the source and destination paths
+ srcPath = filepath.Clean(srcPath)
+ destPath = filepath.Clean(destPath)
+
+ // Construct the absolute paths
+ absSrcPath := filepath.Join(fm.Directory, srcPath)
+ absDestPath := filepath.Join(fm.Directory, destPath)
+
+ // Check if the source path exists
+ if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
+ utils.SendErrorResponse(w, "source path does not exist")
+ return
+ }
+
+ // Check if the destination path exists
+ if _, err := os.Stat(absDestPath); !os.IsNotExist(err) {
+ utils.SendErrorResponse(w, "destination path already exists")
+ return
+ }
+
+ // Rename the source to the destination
+ err = os.Rename(absSrcPath, absDestPath)
+ if err != nil {
+ utils.SendErrorResponse(w, fmt.Sprintf("error moving file/directory: %v", err))
+ return
+ }
+ utils.SendOK(w)
+}
+
+func (fm *FileManager) HandleFileProperties(w http.ResponseWriter, r *http.Request) {
+ // Parse the target file or directory path from the request
+ filePath, err := utils.GetPara(r, "file")
+ if err != nil {
+ utils.SendErrorResponse(w, "invalid file path")
+ return
+ }
+
+ // Construct the absolute path to the target file or directory
+ absPath := filepath.Join(fm.Directory, filePath)
+
+ // Check if the target path exists
+ _, err = os.Stat(absPath)
+ if err != nil {
+ utils.SendErrorResponse(w, "file or directory does not exist")
+ return
+ }
+
+ // Initialize a map to hold file properties
+ fileProps := make(map[string]interface{})
+ fileProps["filename"] = filepath.Base(absPath)
+ fileProps["filepath"] = filePath
+ fileProps["isDir"] = isDir(absPath)
+
+ // Get file size and last modified time
+ finfo, err := os.Stat(absPath)
+ if err != nil {
+ utils.SendErrorResponse(w, "unable to retrieve file properties")
+ return
+ }
+ fileProps["lastModified"] = finfo.ModTime().Unix()
+ if !isDir(absPath) {
+ // If it's a file, get its size
+ fileProps["size"] = finfo.Size()
+ } else {
+ // If it's a directory, calculate its total size containing all child files and folders
+ totalSize, err := calculateDirectorySize(absPath)
+ if err != nil {
+ utils.SendErrorResponse(w, "unable to calculate directory size")
+ return
+ }
+ fileProps["size"] = totalSize
+ }
+
+ // Count the number of sub-files and sub-folders
+ numSubFiles, numSubFolders, err := countSubFilesAndFolders(absPath)
+ if err != nil {
+ utils.SendErrorResponse(w, "unable to count sub-files and sub-folders")
+ return
+ }
+ fileProps["fileCounts"] = numSubFiles
+ fileProps["folderCounts"] = numSubFolders
+
+ // Serialize the file properties to JSON
+ jsonData, err := json.Marshal(fileProps)
+ if err != nil {
+ utils.SendErrorResponse(w, "unable to marshal JSON")
+ return
+ }
+
+ // Set response headers and send the JSON response
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ w.Write(jsonData)
+}
+
+// HandleFileDelete deletes a file or directory
+func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request) {
+ // Parse the target file or directory path from the request
+ filePath, err := utils.PostPara(r, "target")
+ if err != nil {
+ utils.SendErrorResponse(w, "invalid file path")
+ return
+ }
+
+ // Construct the absolute path to the target file or directory
+ absPath := filepath.Join(fm.Directory, filePath)
+
+ // Check if the target path exists
+ _, err = os.Stat(absPath)
+ if err != nil {
+ utils.SendErrorResponse(w, "file or directory does not exist")
+ return
+ }
+
+ // Delete the file or directory
+ err = os.RemoveAll(absPath)
+ if err != nil {
+ utils.SendErrorResponse(w, "error deleting file or directory")
+ return
+ }
+
+ // Respond with a success message or appropriate response
+ utils.SendOK(w)
+}
diff --git a/src/mod/webserv/filemanager/utils.go b/src/mod/webserv/filemanager/utils.go
new file mode 100644
index 0000000..bfa8333
--- /dev/null
+++ b/src/mod/webserv/filemanager/utils.go
@@ -0,0 +1,156 @@
+package filemanager
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// isValidFilename checks if a given filename is safe and valid.
+func isValidFilename(filename string) bool {
+ // Define a list of disallowed characters and reserved names
+ disallowedChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} // Add more if needed
+ reservedNames := []string{"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"} // Add more if needed
+
+ // Check for disallowed characters
+ for _, char := range disallowedChars {
+ if strings.Contains(filename, char) {
+ return false
+ }
+ }
+
+ // Check for reserved names (case-insensitive)
+ lowerFilename := strings.ToUpper(filename)
+ for _, reserved := range reservedNames {
+ if lowerFilename == reserved {
+ return false
+ }
+ }
+
+ // Check for empty filename
+ if filename == "" {
+ return false
+ }
+
+ // The filename is considered valid
+ return true
+}
+
+// sanitizeFilename sanitizes a given filename by removing disallowed characters.
+func sanitizeFilename(filename string) string {
+ disallowedChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} // Add more if needed
+
+ // Replace disallowed characters with underscores
+ for _, char := range disallowedChars {
+ filename = strings.ReplaceAll(filename, char, "_")
+ }
+
+ return filename
+}
+
+// copyFile copies a single file from source to destination
+func copyFile(srcPath, destPath string) error {
+ srcFile, err := os.Open(srcPath)
+ if err != nil {
+ return err
+ }
+ defer srcFile.Close()
+
+ destFile, err := os.Create(destPath)
+ if err != nil {
+ return err
+ }
+ defer destFile.Close()
+
+ _, err = io.Copy(destFile, srcFile)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// copyDirectory recursively copies a directory and its contents from source to destination
+func copyDirectory(srcPath, destPath string) error {
+ // Create the destination directory
+ err := os.MkdirAll(destPath, os.ModePerm)
+ if err != nil {
+ return err
+ }
+
+ entries, err := os.ReadDir(srcPath)
+ if err != nil {
+ return err
+ }
+
+ for _, entry := range entries {
+ srcEntryPath := filepath.Join(srcPath, entry.Name())
+ destEntryPath := filepath.Join(destPath, entry.Name())
+
+ if entry.IsDir() {
+ err := copyDirectory(srcEntryPath, destEntryPath)
+ if err != nil {
+ return err
+ }
+ } else {
+ err := copyFile(srcEntryPath, destEntryPath)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+// isDir checks if the given path is a directory
+func isDir(path string) bool {
+ fileInfo, err := os.Stat(path)
+ if err != nil {
+ return false
+ }
+ return fileInfo.IsDir()
+}
+
+// calculateDirectorySize calculates the total size of a directory and its contents
+func calculateDirectorySize(dirPath string) (int64, error) {
+ var totalSize int64
+ err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ totalSize += info.Size()
+ return nil
+ })
+ if err != nil {
+ return 0, err
+ }
+ return totalSize, nil
+}
+
+// countSubFilesAndFolders counts the number of sub-files and sub-folders within a directory
+func countSubFilesAndFolders(dirPath string) (int, int, error) {
+ var numSubFiles, numSubFolders int
+
+ err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if info.IsDir() {
+ numSubFolders++
+ } else {
+ numSubFiles++
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return 0, 0, err
+ }
+
+ // Subtract 1 from numSubFolders to exclude the root directory itself
+ return numSubFiles, numSubFolders - 1, nil
+}
diff --git a/src/mod/webserv/handler.go b/src/mod/webserv/handler.go
new file mode 100644
index 0000000..5db1837
--- /dev/null
+++ b/src/mod/webserv/handler.go
@@ -0,0 +1,88 @@
+package webserv
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+
+ "imuslab.com/zoraxy/mod/utils"
+)
+
+/*
+ Handler.go
+
+ Handler for web server options change
+ web server is directly listening to the TCP port
+ handlers in this script are for setting change only
+*/
+
+type StaticWebServerStatus struct {
+ ListeningPort int
+ EnableDirectoryListing bool
+ WebRoot string
+ Running bool
+ EnableWebDirManager bool
+}
+
+// Handle getting current static web server status
+func (ws *WebServer) HandleGetStatus(w http.ResponseWriter, r *http.Request) {
+ listeningPortInt, _ := strconv.Atoi(ws.option.Port)
+ currentStatus := StaticWebServerStatus{
+ ListeningPort: listeningPortInt,
+ EnableDirectoryListing: ws.option.EnableDirectoryListing,
+ WebRoot: ws.option.WebRoot,
+ Running: ws.isRunning,
+ EnableWebDirManager: ws.option.EnableWebDirManager,
+ }
+
+ js, _ := json.Marshal(currentStatus)
+ utils.SendJSONResponse(w, string(js))
+}
+
+// Handle request for starting the static web server
+func (ws *WebServer) HandleStartServer(w http.ResponseWriter, r *http.Request) {
+ err := ws.Start()
+ if err != nil {
+ utils.SendErrorResponse(w, err.Error())
+ return
+ }
+ utils.SendOK(w)
+}
+
+// Handle request for stopping the static web server
+func (ws *WebServer) HandleStopServer(w http.ResponseWriter, r *http.Request) {
+ err := ws.Stop()
+ if err != nil {
+ utils.SendErrorResponse(w, err.Error())
+ return
+ }
+ utils.SendOK(w)
+}
+
+// Handle change server listening port request
+func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
+ newPort, err := utils.PostInt(r, "port")
+ if err != nil {
+ utils.SendErrorResponse(w, "invalid port number given")
+ return
+ }
+
+ err = ws.ChangePort(strconv.Itoa(newPort))
+ if err != nil {
+ utils.SendErrorResponse(w, err.Error())
+ return
+ }
+ utils.SendOK(w)
+}
+
+// Change enable directory listing settings
+func (ws *WebServer) SetEnableDirectoryListing(w http.ResponseWriter, r *http.Request) {
+ enableList, err := utils.PostBool(r, "enable")
+ if err != nil {
+ utils.SendErrorResponse(w, "invalid setting given")
+ return
+ }
+
+ ws.option.EnableDirectoryListing = enableList
+ utils.SendOK(w)
+}
diff --git a/src/mod/webserv/middleware.go b/src/mod/webserv/middleware.go
new file mode 100644
index 0000000..ed03037
--- /dev/null
+++ b/src/mod/webserv/middleware.go
@@ -0,0 +1,41 @@
+package webserv
+
+import (
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ "imuslab.com/zoraxy/mod/utils"
+)
+
+// Convert a request path (e.g. /index.html) into physical path on disk
+func (ws *WebServer) resolveFileDiskPath(requestPath string) string {
+ fileDiskpath := filepath.Join(ws.option.WebRoot, "html", requestPath)
+
+ //Force convert it to slash even if the host OS is on Windows
+ fileDiskpath = filepath.Clean(fileDiskpath)
+ fileDiskpath = strings.ReplaceAll(fileDiskpath, "\\", "/")
+ return fileDiskpath
+
+}
+
+// File server middleware to handle directory listing (and future expansion)
+func (ws *WebServer) fsMiddleware(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !ws.option.EnableDirectoryListing {
+ if strings.HasSuffix(r.URL.Path, "/") {
+ //This is a folder. Let check if index exists
+ if utils.FileExists(filepath.Join(ws.resolveFileDiskPath(r.URL.Path), "index.html")) {
+
+ } else if utils.FileExists(filepath.Join(ws.resolveFileDiskPath(r.URL.Path), "index.htm")) {
+
+ } else {
+ http.NotFound(w, r)
+ return
+ }
+ }
+ }
+
+ h.ServeHTTP(w, r)
+ })
+}
diff --git a/src/mod/webserv/templates/index.html b/src/mod/webserv/templates/index.html
new file mode 100644
index 0000000..8716959
--- /dev/null
+++ b/src/mod/webserv/templates/index.html
@@ -0,0 +1,61 @@
+
+
+ Hello Zoraxy
+
+
+
+
+
+
Welcome to Zoraxy Static Web Server!
+
+
+
If you see this page, that means your static web server is running.
+ By default, all the html files are stored under ./web/html/
+ relative to the zoraxy runtime directory.
+ You can upload your html files to your web directory via the Web Directory Manager.
+
+
+
\ No newline at end of file
diff --git a/src/mod/webserv/utils.go b/src/mod/webserv/utils.go
new file mode 100644
index 0000000..856ac1a
--- /dev/null
+++ b/src/mod/webserv/utils.go
@@ -0,0 +1,18 @@
+package webserv
+
+import (
+ "net"
+)
+
+// IsPortInUse checks if a port is in use.
+func IsPortInUse(port string) bool {
+ listener, err := net.Listen("tcp", "localhost:"+port)
+ if err != nil {
+ // If there was an error, the port is in use.
+ return true
+ }
+ defer listener.Close()
+
+ // No error means the port is available.
+ return false
+}
diff --git a/src/mod/webserv/webserv.go b/src/mod/webserv/webserv.go
new file mode 100644
index 0000000..9eb7ee7
--- /dev/null
+++ b/src/mod/webserv/webserv.go
@@ -0,0 +1,195 @@
+package webserv
+
+import (
+ "embed"
+ _ "embed"
+ "errors"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "sync"
+
+ "imuslab.com/zoraxy/mod/database"
+ "imuslab.com/zoraxy/mod/utils"
+ "imuslab.com/zoraxy/mod/webserv/filemanager"
+)
+
+/*
+ Static Web Server package
+
+ This module host a static web server
+*/
+
+//go:embed templates/*
+var templates embed.FS
+
+type WebServerOptions struct {
+ Port string //Port for listening
+ EnableDirectoryListing bool //Enable listing of directory
+ WebRoot string //Folder for stroing the static web folders
+ EnableWebDirManager bool //Enable web file manager to handle files in web directory
+ Sysdb *database.Database //Database for storing configs
+}
+
+type WebServer struct {
+ FileManager *filemanager.FileManager
+
+ mux *http.ServeMux
+ server *http.Server
+ option *WebServerOptions
+ isRunning bool
+ mu sync.Mutex
+}
+
+// NewWebServer creates a new WebServer instance. One instance only
+func NewWebServer(options *WebServerOptions) *WebServer {
+ if !utils.FileExists(options.WebRoot) {
+ //Web root folder not exists. Create one with default templates
+ os.MkdirAll(filepath.Join(options.WebRoot, "html"), 0775)
+ os.MkdirAll(filepath.Join(options.WebRoot, "templates"), 0775)
+ indexTemplate, err := templates.ReadFile("templates/index.html")
+ if err != nil {
+ log.Println("Failed to read static wev server template file: ", err.Error())
+ } else {
+ os.WriteFile(filepath.Join(options.WebRoot, "html", "index.html"), indexTemplate, 0775)
+ }
+
+ }
+
+ //Create a new file manager if it is enabled
+ var newDirManager *filemanager.FileManager
+ if options.EnableWebDirManager {
+ fm := filemanager.NewFileManager(filepath.Join(options.WebRoot, "/html"))
+ newDirManager = fm
+ }
+
+ //Create new table to store the config
+ options.Sysdb.NewTable("webserv")
+ return &WebServer{
+ mux: http.NewServeMux(),
+ FileManager: newDirManager,
+ option: options,
+ isRunning: false,
+ mu: sync.Mutex{},
+ }
+}
+
+// Restore the configuration to previous config
+func (ws *WebServer) RestorePreviousState() {
+ //Set the port
+ port := ws.option.Port
+ ws.option.Sysdb.Read("webserv", "port", &port)
+ ws.option.Port = port
+
+ //Set the enable directory list
+ enableDirList := ws.option.EnableDirectoryListing
+ ws.option.Sysdb.Read("webserv", "dirlist", &enableDirList)
+ ws.option.EnableDirectoryListing = enableDirList
+
+ //Check the running state
+ webservRunning := false
+ ws.option.Sysdb.Read("webserv", "enabled", &webservRunning)
+ if webservRunning {
+ ws.Start()
+ } else {
+ ws.Stop()
+ }
+
+}
+
+// ChangePort changes the server's port.
+func (ws *WebServer) ChangePort(port string) error {
+ if IsPortInUse(port) {
+ return errors.New("Selected port is used by another process")
+ }
+
+ if ws.isRunning {
+ if err := ws.Stop(); err != nil {
+ return err
+ }
+ }
+
+ ws.option.Port = port
+ ws.server.Addr = ":" + port
+
+ err := ws.Start()
+ if err != nil {
+ return err
+ }
+
+ ws.option.Sysdb.Write("webserv", "port", port)
+
+ return nil
+}
+
+// Start starts the web server.
+func (ws *WebServer) Start() error {
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+
+ //Check if server already running
+ if ws.isRunning {
+ return fmt.Errorf("web server is already running")
+ }
+
+ //Check if the port is usable
+ if IsPortInUse(ws.option.Port) {
+ return errors.New("Port already in use or access denied by host OS")
+ }
+
+ //Dispose the old mux and create a new one
+ ws.mux = http.NewServeMux()
+
+ //Create a static web server
+ fs := http.FileServer(http.Dir(filepath.Join(ws.option.WebRoot, "html")))
+ ws.mux.Handle("/", ws.fsMiddleware(fs))
+
+ ws.server = &http.Server{
+ Addr: ":" + ws.option.Port,
+ Handler: ws.mux,
+ }
+
+ go func() {
+ if err := ws.server.ListenAndServe(); err != nil {
+ if err != http.ErrServerClosed {
+ fmt.Printf("Web server error: %v\n", err)
+ }
+ }
+ }()
+
+ log.Println("Static Web Server started. Listeing on :" + ws.option.Port)
+ ws.isRunning = true
+ ws.option.Sysdb.Write("webserv", "enabled", true)
+ return nil
+}
+
+// Stop stops the web server.
+func (ws *WebServer) Stop() error {
+ ws.mu.Lock()
+ defer ws.mu.Unlock()
+
+ if !ws.isRunning {
+ return fmt.Errorf("web server is not running")
+ }
+
+ if err := ws.server.Close(); err != nil {
+ return err
+ }
+
+ ws.isRunning = false
+ ws.option.Sysdb.Write("webserv", "enabled", false)
+ return nil
+}
+
+// UpdateDirectoryListing enables or disables directory listing.
+func (ws *WebServer) UpdateDirectoryListing(enable bool) {
+ ws.option.EnableDirectoryListing = enable
+ ws.option.Sysdb.Write("webserv", "dirlist", enable)
+}
+
+// Close stops the web server without returning an error.
+func (ws *WebServer) Close() {
+ ws.Stop()
+}
diff --git a/src/reverseproxy.go b/src/reverseproxy.go
index e45953a..43c66de 100644
--- a/src/reverseproxy.go
+++ b/src/reverseproxy.go
@@ -64,6 +64,7 @@ func ReverseProxtInit() {
RedirectRuleTable: redirectTable,
GeodbStore: geodbStore,
StatisticCollector: statisticCollector,
+ WebDirectory: *staticWebServerRoot,
})
if err != nil {
log.Println(err.Error())
diff --git a/src/start.go b/src/start.go
index 3da18e6..4dd4aa8 100644
--- a/src/start.go
+++ b/src/start.go
@@ -22,6 +22,7 @@ import (
"imuslab.com/zoraxy/mod/statistic/analytic"
"imuslab.com/zoraxy/mod/tcpprox"
"imuslab.com/zoraxy/mod/tlscert"
+ "imuslab.com/zoraxy/mod/webserv"
)
/*
@@ -203,11 +204,29 @@ func startupSequence() {
Obtaining certificates from ACME Server
*/
+ //Create a table just to store acme related preferences
+ sysdb.NewTable("acmepref")
acmeHandler = initACME()
acmeAutoRenewer, err = acme.NewAutoRenewer("./conf/acme_conf.json", "./conf/certs/", int64(*acmeAutoRenewInterval), acmeHandler)
if err != nil {
log.Fatal(err)
}
+
+ /*
+ Static Web Server
+
+ Start the static web server
+ */
+
+ staticWebServer = webserv.NewWebServer(&webserv.WebServerOptions{
+ Sysdb: sysdb,
+ Port: "8081", //Default Port
+ WebRoot: *staticWebServerRoot,
+ EnableDirectoryListing: true,
+ EnableWebDirManager: *allowWebFileManager,
+ })
+ //Restore the web server to previous shutdown state
+ staticWebServer.RestorePreviousState()
}
// This sequence start after everything is initialized
diff --git a/src/web/components/cert.html b/src/web/components/cert.html
index 5f4b75e..1f2ab4c 100644
--- a/src/web/components/cert.html
+++ b/src/web/components/cert.html
@@ -65,19 +65,21 @@
Sub-domain Certificates
-
-
-
Domain
-
Last Update
-
Expire At
-
Remove
-
-
-
-
-
+
+
+
+
Domain
+
Last Update
+
Expire At
+
Remove
+
+
+
+
+
+
+
-
Sub-domain Certificates
@@ -85,11 +87,49 @@
Sub-domain Certificates
depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for a.example.com and b.example.com).
If you have a wildcard certificate that covers *.example.com, you can just enter example.com as server name in the form below to add a certificate.
+
+
Certificate Authority (CA) and Auto Renew (ACME)
+
Management features regarding CA and ACME
+
The default CA to use when create a new subdomain proxy endpoint with TLS certificate
+
+
+
+
+
+
+
Let's Encrypt
+
+
Let's Encrypt
+
Buypass
+
ZeroSSL
+
+
+
+
+
+
+
+
+
+
Certificate Renew / Generation (ACME) Settings
+
+
+
+
+ Disabled
+
Auto-Renewer Status
+
+
+
+
This tool provide you a graphical interface to setup auto certificate renew on your (sub)domains. You can also manually generate a certificate if one of your domain do not have certificate.
+
+
\ No newline at end of file
diff --git a/src/web/components/zgrok.html b/src/web/components/zgrok.html
new file mode 100644
index 0000000..86200de
--- /dev/null
+++ b/src/web/components/zgrok.html
@@ -0,0 +1,10 @@
+
+
+
Service Expose Proxy
+
Expose your local test-site on the internet with single command
+
+
+
Work In Progress
+ We are looking for someone to help with implementing this feature in Zoraxy. If you know how to write Golang and want to contribute, feel free to create a pull request to this feature!
+
+
\ No newline at end of file
diff --git a/src/web/index.html b/src/web/index.html
index 19760bf..163b44c 100644
--- a/src/web/index.html
+++ b/src/web/index.html
@@ -62,13 +62,16 @@
Global Area Network
-
+
Service Expose Proxy
TCP Proxy