Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
20cf290d37 | |||
4ca0fcc6d1 | |||
ce4ce72820 | |||
e363d55899 | |||
172479e4fb | |||
156fa5dace | |||
50f222cced | |||
640e1adf96 | |||
d4bb84180c | |||
bda47fc36b | |||
fd6ba56143 | |||
b63a0fc246 | |||
ed92cccf0e | |||
95892802fd | |||
8a5004e828 | |||
c6c523e005 | |||
a692ec818d | |||
c65f780613 | |||
507c2ab468 | |||
1180da8d11 | |||
83f574e3ab | |||
60837f307d |
22
CHANGELOG.md
@ -1,4 +1,24 @@
|
||||
# 2.6.6 Jul 26 2023
|
||||
# v2.6.7 Sep 26 2023
|
||||
|
||||
+ Added Static Web Server function [#56](https://github.com/tobychui/zoraxy/issues/56)
|
||||
+ Web Directory Manager (see static webserver tab)
|
||||
+ Added static web server and black / whitelist template [#38](https://github.com/tobychui/zoraxy/issues/38)
|
||||
+ Added default / preferred CA features for ACME [#47](https://github.com/tobychui/zoraxy/issues/47)
|
||||
+ Optimized TLS/SSL page and added dedicated section for ACME related features
|
||||
+ Bugfixes [#61](https://github.com/tobychui/zoraxy/issues/61) [#58](https://github.com/tobychui/zoraxy/issues/58)
|
||||
|
||||
# v2.6.6 Aug 30 2023
|
||||
|
||||
+ Added basic auth editor custom exception rules
|
||||
+ Fixed redirection bug under another reverse proxy and Apache location headers [#39](https://github.com/tobychui/zoraxy/issues/39)
|
||||
+ Optimized memory usage (from 1.2GB to 61MB for low speed geoip lookup) [#52](https://github.com/tobychui/zoraxy/issues/52)
|
||||
+ Added unset subdomain custom redirection feature [#46](https://github.com/tobychui/zoraxy/issues/46)
|
||||
+ Fixed potential security issue in satori/go.uuid [#55](https://github.com/tobychui/zoraxy/issues/55)
|
||||
+ Added custom acme feature in back-end, thx [@daluntw](https://github.com/daluntw)
|
||||
+ Added bypass TLS check for custom acme server, thx [@daluntw](https://github.com/daluntw)
|
||||
+ Introduce new startparameter `-fastgeoip=true`, see [Releases](https://github.com/tobychui/zoraxy/releases/tag/2.6.6)
|
||||
|
||||
# v2.6.5.1 Jul 26 2023
|
||||
|
||||
+ Patch on memory leaking for Windows netstat module (do not effect any of the previous non Windows builds)
|
||||
+ Fixed potential memory leak in acme handler logic
|
||||
|
@ -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:
|
||||
|
38
src/acme.go
@ -1,9 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@ -28,7 +28,7 @@ func getRandomPort(minPort int) int {
|
||||
|
||||
// init the new ACME instance
|
||||
func initACME() *acme.ACMEHandler {
|
||||
log.Println("Starting ACME handler")
|
||||
SystemWideLogger.Println("Starting ACME handler")
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
// Generate a random port above 30000
|
||||
port := getRandomPort(30000)
|
||||
@ -38,12 +38,12 @@ 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
|
||||
func acmeRegisterSpecialRoutingRule() {
|
||||
log.Println("Assigned temporary port:" + acmeHandler.Getport())
|
||||
SystemWideLogger.Println("Assigned temporary port:" + acmeHandler.Getport())
|
||||
|
||||
err := dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
|
||||
ID: "acme-autorenew",
|
||||
@ -65,7 +65,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)
|
||||
@ -78,7 +78,7 @@ func acmeRegisterSpecialRoutingRule() {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Println("[Err] " + err.Error())
|
||||
SystemWideLogger.PrintAndLog("ACME", "Unable register temp port for DNS resolver", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +88,7 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
//Enable port 80 to 443 redirect
|
||||
if !dynamicProxyRouter.Option.ForceHttpsRedirect {
|
||||
log.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
|
||||
SystemWideLogger.Println("Temporary enabling HTTP to HTTPS redirect for ACME certificate renew requests")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
|
||||
} else {
|
||||
//Set this to true, so after renew, do not turn it off
|
||||
@ -109,8 +109,28 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
|
||||
if dynamicProxyRouter.Option.Port == 443 {
|
||||
if !isForceHttpsRedirectEnabledOriginally {
|
||||
//Default is off. Turn the redirection off
|
||||
log.Println("Restoring HTTP to HTTPS redirect settings")
|
||||
SystemWideLogger.PrintAndLog("ACME", "Restoring HTTP to HTTPS redirect settings", nil)
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
SystemWideLogger.Println("Updating prefered ACME CA to " + ca)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
}
|
||||
|
20
src/api.go
@ -54,6 +54,7 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/proxy/tlscheck", HandleCheckSiteSupportTLS)
|
||||
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
|
||||
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
||||
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
|
||||
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
||||
//Reverse proxy root related APIs
|
||||
authRouter.HandleFunc("/api/proxy/root/listOptions", HandleRootRouteOptionList)
|
||||
@ -162,6 +163,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 +171,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)
|
||||
|
11
src/cert.go
@ -6,7 +6,6 @@ import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -128,7 +127,7 @@ func handleListDomains(w http.ResponseWriter, r *http.Request) {
|
||||
certBtyes, err := os.ReadFile(certFilepath)
|
||||
if err != nil {
|
||||
// Unable to load this file
|
||||
log.Println("Unable to load certificate: " + certFilepath)
|
||||
SystemWideLogger.PrintAndLog("TLS", "Unable to load certificate: "+certFilepath, err)
|
||||
continue
|
||||
} else {
|
||||
// Cert loaded. Check its expiry time
|
||||
@ -182,11 +181,11 @@ func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
if newState == "true" {
|
||||
sysdb.Write("settings", "usetls", true)
|
||||
log.Println("Enabling TLS mode on reverse proxy")
|
||||
SystemWideLogger.Println("Enabling TLS mode on reverse proxy")
|
||||
dynamicProxyRouter.UpdateTLSSetting(true)
|
||||
} else if newState == "false" {
|
||||
sysdb.Write("settings", "usetls", false)
|
||||
log.Println("Disabling TLS mode on reverse proxy")
|
||||
SystemWideLogger.Println("Disabling TLS mode on reverse proxy")
|
||||
dynamicProxyRouter.UpdateTLSSetting(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid state given. Only support true or false")
|
||||
@ -213,11 +212,11 @@ func handleSetTlsRequireLatest(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
if newState == "true" {
|
||||
sysdb.Write("settings", "forceLatestTLS", true)
|
||||
log.Println("Updating minimum TLS version to v1.2 or above")
|
||||
SystemWideLogger.Println("Updating minimum TLS version to v1.2 or above")
|
||||
dynamicProxyRouter.UpdateTLSVersion(true)
|
||||
} else if newState == "false" {
|
||||
sysdb.Write("settings", "forceLatestTLS", false)
|
||||
log.Println("Updating minimum TLS version to v1.0 or above")
|
||||
SystemWideLogger.Println("Updating minimum TLS version to v1.0 or above")
|
||||
dynamicProxyRouter.UpdateTLSVersion(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid state given")
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -29,6 +28,7 @@ type Record struct {
|
||||
Rootname string
|
||||
ProxyTarget string
|
||||
UseTLS bool
|
||||
BypassGlobalTLS bool
|
||||
SkipTlsValidation bool
|
||||
RequireBasicAuth bool
|
||||
BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials
|
||||
@ -61,11 +61,11 @@ func SaveReverseProxyEndpointToFile(proxyEndpoint *dynamicproxy.ProxyEndpoint) e
|
||||
func RemoveReverseProxyConfigFile(rootname string) error {
|
||||
filename := getFilenameFromRootName(rootname)
|
||||
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
|
||||
log.Println("Config Removed: ", removePendingFile)
|
||||
SystemWideLogger.Println("Config Removed: ", removePendingFile)
|
||||
if utils.FileExists(removePendingFile) {
|
||||
err := os.Remove(removePendingFile)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unabel to remove config file", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -81,6 +81,7 @@ func LoadReverseProxyConfig(filename string) (*Record, error) {
|
||||
Rootname: "",
|
||||
ProxyTarget: "",
|
||||
UseTLS: false,
|
||||
BypassGlobalTLS: false,
|
||||
SkipTlsValidation: false,
|
||||
RequireBasicAuth: false,
|
||||
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
|
||||
@ -109,6 +110,7 @@ func ConvertProxyEndpointToRecord(targetProxyEndpoint *dynamicproxy.ProxyEndpoin
|
||||
Rootname: targetProxyEndpoint.RootOrMatchingDomain,
|
||||
ProxyTarget: targetProxyEndpoint.Domain,
|
||||
UseTLS: targetProxyEndpoint.RequireTLS,
|
||||
BypassGlobalTLS: targetProxyEndpoint.BypassGlobalTLS,
|
||||
SkipTlsValidation: targetProxyEndpoint.SkipCertValidations,
|
||||
RequireBasicAuth: targetProxyEndpoint.RequireBasicAuth,
|
||||
BasicAuthCredentials: targetProxyEndpoint.BasicAuthCredentials,
|
||||
@ -191,14 +193,14 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
//Also zip in the sysdb
|
||||
zipFile, err := zipWriter.Create("sys.db")
|
||||
if err != nil {
|
||||
log.Println("[Backup] Unable to zip sysdb: " + err.Error())
|
||||
SystemWideLogger.PrintAndLog("Backup", "Unable to zip sysdb", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Open the file on disk
|
||||
file, err := os.Open("sys.db")
|
||||
if err != nil {
|
||||
log.Println("[Backup] Unable to open sysdb: " + err.Error())
|
||||
SystemWideLogger.PrintAndLog("Backup", "Unable to open sysdb", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
@ -206,7 +208,7 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
// Copy the file contents to the zip file
|
||||
_, err = io.Copy(zipFile, file)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
SystemWideLogger.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -311,12 +313,12 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Send a success response
|
||||
w.WriteHeader(http.StatusOK)
|
||||
log.Println("Configuration restored")
|
||||
SystemWideLogger.Println("Configuration restored")
|
||||
fmt.Fprintln(w, "Configuration restored")
|
||||
|
||||
if restoreDatabase {
|
||||
go func() {
|
||||
log.Println("Database altered. Restarting in 3 seconds...")
|
||||
SystemWideLogger.Println("Database altered. Restarting in 3 seconds...")
|
||||
time.Sleep(3 * time.Second)
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
21
src/main.go
@ -20,6 +20,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/email"
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/pathrule"
|
||||
@ -30,6 +31,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 +43,13 @@ 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 logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
|
||||
|
||||
var (
|
||||
name = "Zoraxy"
|
||||
version = "2.6.6"
|
||||
version = "2.6.8"
|
||||
nodeUUID = "generic"
|
||||
development = false //Set this to false to use embedded web fs
|
||||
bootTime = time.Now().Unix()
|
||||
@ -73,10 +79,12 @@ 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
|
||||
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
|
||||
SystemWideLogger *logger.Logger //Logger for Zoraxy
|
||||
)
|
||||
|
||||
// Kill signal handler. Do something before the system the core terminate.
|
||||
@ -111,6 +119,9 @@ func ShutdownSeq() {
|
||||
fmt.Println("- Cleaning up tmp files")
|
||||
os.RemoveAll("./tmp")
|
||||
|
||||
fmt.Println("- Closing system wide logger")
|
||||
SystemWideLogger.Close()
|
||||
|
||||
//Close database, final
|
||||
fmt.Println("- Stopping system database")
|
||||
sysdb.Close()
|
||||
@ -146,7 +157,7 @@ func main() {
|
||||
}
|
||||
uuidBytes, err := os.ReadFile(uuidRecord)
|
||||
if err != nil {
|
||||
log.Println("Unable to read system uuid from file system")
|
||||
SystemWideLogger.PrintAndLog("ZeroTier", "Unable to read system uuid from file system", nil)
|
||||
panic(err)
|
||||
}
|
||||
nodeUUID = string(uuidBytes)
|
||||
@ -168,7 +179,7 @@ func main() {
|
||||
//Start the finalize sequences
|
||||
finalSequence()
|
||||
|
||||
log.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
|
||||
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
|
||||
err = http.ListenAndServe(handler.Port, nil)
|
||||
|
||||
if err != nil {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -60,6 +60,12 @@ func (router *Router) UpdateTLSVersion(requireLatest bool) {
|
||||
router.Restart()
|
||||
}
|
||||
|
||||
// Update port 80 listener state
|
||||
func (router *Router) UpdatePort80ListenerState(useRedirect bool) {
|
||||
router.Option.ListenOnPort80 = useRedirect
|
||||
router.Restart()
|
||||
}
|
||||
|
||||
// Update https redirect, which will require updates
|
||||
func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
|
||||
router.Option.ForceHttpsRedirect = useRedirect
|
||||
@ -95,27 +101,73 @@ func (router *Router) StartProxyService() error {
|
||||
}
|
||||
|
||||
if router.Option.UseTls {
|
||||
//Serve with TLS mode
|
||||
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
router.Running = false
|
||||
return err
|
||||
/*
|
||||
//Serve with TLS mode
|
||||
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
router.Running = false
|
||||
return err
|
||||
}
|
||||
router.tlsListener = ln
|
||||
*/
|
||||
router.server = &http.Server{
|
||||
Addr: ":" + strconv.Itoa(router.Option.Port),
|
||||
Handler: router.mux,
|
||||
TLSConfig: config,
|
||||
}
|
||||
router.tlsListener = ln
|
||||
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
|
||||
router.Running = true
|
||||
|
||||
if router.Option.Port != 80 && router.Option.ForceHttpsRedirect {
|
||||
if router.Option.Port != 80 && router.Option.ListenOnPort80 {
|
||||
//Add a 80 to 443 redirector
|
||||
httpServer := &http.Server{
|
||||
Addr: ":80",
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
protocol := "https://"
|
||||
if router.Option.Port == 443 {
|
||||
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
|
||||
//Check if the domain requesting allow non TLS mode
|
||||
domainOnly := r.Host
|
||||
if strings.Contains(r.Host, ":") {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
sep := router.getSubdomainProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && sep.BypassGlobalTLS {
|
||||
//Allow routing via non-TLS handler
|
||||
originalHostHeader := r.Host
|
||||
if r.URL != nil {
|
||||
r.Host = r.URL.Host
|
||||
} else {
|
||||
//Fallback when the upstream proxy screw something up in the header
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
sep.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: sep.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: sep.RequireTLS,
|
||||
PathPrefix: "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if router.Option.ForceHttpsRedirect {
|
||||
//Redirect to https is enabled
|
||||
protocol := "https://"
|
||||
if router.Option.Port == 443 {
|
||||
http.Redirect(w, r, protocol+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
|
||||
} else {
|
||||
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
|
||||
}
|
||||
} else {
|
||||
http.Redirect(w, r, protocol+r.Host+":"+strconv.Itoa(router.Option.Port)+r.RequestURI, http.StatusTemporaryRedirect)
|
||||
//Do not do redirection
|
||||
if sep != nil {
|
||||
//Sub-domain exists but not allow non-TLS access
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("400 - Bad Request"))
|
||||
} else {
|
||||
//No defined sub-domain
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}),
|
||||
@ -143,7 +195,7 @@ func (router *Router) StartProxyService() error {
|
||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
//Unable to startup port 80 listener. Handle shutdown process gracefully
|
||||
stopChan <- true
|
||||
log.Fatalf("Could not start server: %v\n", err)
|
||||
log.Fatalf("Could not start redirection server: %v\n", err)
|
||||
}
|
||||
}()
|
||||
router.tlsRedirectStop = stopChan
|
||||
@ -152,8 +204,8 @@ func (router *Router) StartProxyService() error {
|
||||
//Start the TLS server
|
||||
log.Println("Reverse proxy service started in the background (TLS mode)")
|
||||
go func() {
|
||||
if err := router.server.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Could not start server: %v\n", err)
|
||||
if err := router.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Could not start proxy server: %v\n", err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
|
@ -38,6 +38,7 @@ func (router *Router) AddSubdomainRoutingService(options *SubdOptions) error {
|
||||
Domain: domain,
|
||||
RequireTLS: options.RequireTLS,
|
||||
Proxy: proxy,
|
||||
BypassGlobalTLS: options.BypassGlobalTLS,
|
||||
SkipCertValidations: options.SkipCertValidations,
|
||||
RequireBasicAuth: options.RequireBasicAuth,
|
||||
BasicAuthCredentials: options.BasicAuthCredentials,
|
||||
|
55
src/mod/dynamicproxy/templates/forbidden.html
Normal file
@ -0,0 +1,55 @@
|
||||
<html>
|
||||
<head>
|
||||
<!-- Zoraxy Forbidden Template -->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css">
|
||||
<script type="text/javascript" src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js"></script>
|
||||
<title>Forbidden</title>
|
||||
<style>
|
||||
#msg{
|
||||
position: absolute;
|
||||
top: calc(50% - 150px);
|
||||
left: calc(50% - 250px);
|
||||
width: 500px;
|
||||
height: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#footer{
|
||||
position: fixed;
|
||||
padding: 2em;
|
||||
padding-left: 5em;
|
||||
padding-right: 5em;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
small{
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="msg">
|
||||
<h1 style="font-size: 6em; margin-bottom: 0px;"><i class="red ban icon"></i></h1>
|
||||
<div>
|
||||
<h3 style="margin-top: 1em;">403 - Forbidden</h3>
|
||||
<div class="ui divider"></div>
|
||||
<p>You do not have permission to view this directory or page. <br>
|
||||
This might cause by the region limit setting of this site.</p>
|
||||
<div class="ui divider"></div>
|
||||
<div style="text-align: left;">
|
||||
<small>Request time: <span id="reqtime"></span></small><br>
|
||||
<small id="reqURLDisplay">Request URI: <span id="requrl"></span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$("#reqtime").text(new Date().toLocaleString(undefined, {year: 'numeric', month: '2-digit', day: '2-digit', weekday:"long", hour: '2-digit', hour12: false, minute:'2-digit', second:'2-digit'}));
|
||||
$("#requrl").text(window.location.href);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,6 +1,7 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
@ -26,11 +27,13 @@ type RouterOption struct {
|
||||
Port int //Incoming port
|
||||
UseTls bool //Use TLS to serve incoming requsts
|
||||
ForceTLSLatest bool //Force TLS1.2 or above
|
||||
ListenOnPort80 bool //Enable port 80 http listener
|
||||
ForceHttpsRedirect bool //Force redirection of http to https endpoint
|
||||
TlsManager *tlscert.Manager
|
||||
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 +126,11 @@ type SubdOptions struct {
|
||||
BasicAuthCredentials []*BasicAuthCredentials
|
||||
BasicAuthExceptionRules []*BasicAuthExceptionRule
|
||||
}
|
||||
|
||||
/*
|
||||
Web Templates
|
||||
*/
|
||||
var (
|
||||
//go:embed templates/forbidden.html
|
||||
page_forbidden []byte
|
||||
)
|
||||
|
103
src/mod/info/logger/logger.go
Normal file
@ -0,0 +1,103 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
Zoraxy Logger
|
||||
|
||||
This script is designed to make a managed log for the Zoraxy
|
||||
and replace the ton of log.Println in the system core
|
||||
*/
|
||||
|
||||
type Logger struct {
|
||||
LogToFile bool //Set enable write to file
|
||||
Prefix string //Prefix for log files
|
||||
LogFolder string //Folder to store the log file
|
||||
CurrentLogFile string //Current writing filename
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func NewLogger(logFilePrefix string, logFolder string, logToFile bool) (*Logger, error) {
|
||||
err := os.MkdirAll(logFolder, 0775)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thisLogger := Logger{
|
||||
LogToFile: logToFile,
|
||||
Prefix: logFilePrefix,
|
||||
LogFolder: logFolder,
|
||||
}
|
||||
|
||||
logFilePath := thisLogger.getLogFilepath()
|
||||
f, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
thisLogger.CurrentLogFile = logFilePath
|
||||
thisLogger.file = f
|
||||
return &thisLogger, nil
|
||||
}
|
||||
|
||||
func (l *Logger) getLogFilepath() string {
|
||||
year, month, _ := time.Now().Date()
|
||||
return filepath.Join(l.LogFolder, l.Prefix+"_"+strconv.Itoa(year)+"-"+strconv.Itoa(int(month))+".log")
|
||||
}
|
||||
|
||||
// PrintAndLog will log the message to file and print the log to STDOUT
|
||||
func (l *Logger) PrintAndLog(title string, message string, originalError error) {
|
||||
go func() {
|
||||
l.Log(title, message, originalError)
|
||||
}()
|
||||
log.Println("[" + title + "] " + message)
|
||||
}
|
||||
|
||||
// Println is a fast snap-in replacement for log.Println
|
||||
func (l *Logger) Println(v ...interface{}) {
|
||||
//Convert the array of interfaces into string
|
||||
message := fmt.Sprint(v...)
|
||||
go func() {
|
||||
l.Log("info", string(message), nil)
|
||||
}()
|
||||
log.Println("[INFO] " + string(message))
|
||||
}
|
||||
|
||||
func (l *Logger) Log(title string, errorMessage string, originalError error) {
|
||||
l.ValidateAndUpdateLogFilepath()
|
||||
if l.LogToFile {
|
||||
if originalError == nil {
|
||||
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [INFO]" + errorMessage + "\n")
|
||||
} else {
|
||||
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [ERROR]" + errorMessage + " " + originalError.Error() + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Validate if the logging target is still valid (detect any months change)
|
||||
func (l *Logger) ValidateAndUpdateLogFilepath() {
|
||||
expectedCurrentLogFilepath := l.getLogFilepath()
|
||||
if l.CurrentLogFile != expectedCurrentLogFilepath {
|
||||
//Change of month. Update to a new log file
|
||||
l.file.Close()
|
||||
f, err := os.OpenFile(expectedCurrentLogFilepath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
|
||||
if err != nil {
|
||||
log.Println("[Logger] Unable to create new log. Logging to file disabled.")
|
||||
l.LogToFile = false
|
||||
return
|
||||
}
|
||||
l.CurrentLogFile = expectedCurrentLogFilepath
|
||||
l.file = f
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Close() {
|
||||
l.file.Close()
|
||||
}
|
122
src/mod/info/logviewer/logviewer.go
Normal file
@ -0,0 +1,122 @@
|
||||
package logviewer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type ViewerOption struct {
|
||||
RootFolder string //The root folder to scan for log
|
||||
Extension string //The extension the root files use, include the . in your ext (e.g. .log)
|
||||
}
|
||||
|
||||
type Viewer struct {
|
||||
option *ViewerOption
|
||||
}
|
||||
|
||||
type LogFile struct {
|
||||
Title string
|
||||
Filename string
|
||||
Fullpath string
|
||||
Filesize int64
|
||||
}
|
||||
|
||||
func NewLogViewer(option *ViewerOption) *Viewer {
|
||||
return &Viewer{option: option}
|
||||
}
|
||||
|
||||
/*
|
||||
Log Request Handlers
|
||||
*/
|
||||
//List all the log files in the log folder. Return in map[string]LogFile format
|
||||
func (v *Viewer) HandleListLog(w http.ResponseWriter, r *http.Request) {
|
||||
logFiles := v.ListLogFiles(false)
|
||||
js, _ := json.Marshal(logFiles)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Read log of a given catergory and filename
|
||||
// Require GET varaible: file and catergory
|
||||
func (v *Viewer) HandleReadLog(w http.ResponseWriter, r *http.Request) {
|
||||
filename, err := utils.GetPara(r, "file")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid filename given")
|
||||
return
|
||||
}
|
||||
|
||||
catergory, err := utils.GetPara(r, "catergory")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid catergory given")
|
||||
return
|
||||
}
|
||||
|
||||
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(catergory)), strings.TrimSpace(filepath.Base(filename)))
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendTextResponse(w, content)
|
||||
}
|
||||
|
||||
/*
|
||||
Log Access Functions
|
||||
*/
|
||||
|
||||
func (v *Viewer) ListLogFiles(showFullpath bool) map[string][]*LogFile {
|
||||
result := map[string][]*LogFile{}
|
||||
filepath.WalkDir(v.option.RootFolder, func(path string, di fs.DirEntry, err error) error {
|
||||
if filepath.Ext(path) == v.option.Extension {
|
||||
catergory := filepath.Base(filepath.Dir(path))
|
||||
logList, ok := result[catergory]
|
||||
if !ok {
|
||||
//this catergory hasn't been scanned before.
|
||||
logList = []*LogFile{}
|
||||
}
|
||||
|
||||
fullpath := filepath.ToSlash(path)
|
||||
if !showFullpath {
|
||||
fullpath = ""
|
||||
}
|
||||
|
||||
st, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
logList = append(logList, &LogFile{
|
||||
Title: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)),
|
||||
Filename: filepath.Base(path),
|
||||
Fullpath: fullpath,
|
||||
Filesize: st.Size(),
|
||||
})
|
||||
|
||||
result[catergory] = logList
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *Viewer) LoadLogFile(catergory string, filename string) (string, error) {
|
||||
logFilepath := filepath.Join(v.option.RootFolder, catergory, filename)
|
||||
if utils.FileExists(logFilepath) {
|
||||
//Load it
|
||||
content, err := os.ReadFile(logFilepath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
} else {
|
||||
return "", errors.New("log file not found")
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
406
src/mod/webserv/filemanager/filemanager.go
Normal file
@ -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)
|
||||
}
|
156
src/mod/webserv/filemanager/utils.go
Normal file
@ -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
|
||||
}
|
88
src/mod/webserv/handler.go
Normal file
@ -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)
|
||||
}
|
41
src/mod/webserv/middleware.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
61
src/mod/webserv/templates/index.html
Normal file
@ -0,0 +1,61 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Hello Zoraxy</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Tahoma, sans-serif;
|
||||
background-color: #f6f6f6;
|
||||
color: #2d2e30;
|
||||
}
|
||||
.sectionHeader{
|
||||
background-color: #c4d0d9;
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
.sectionHeader h3{
|
||||
text-align: center;
|
||||
}
|
||||
.container{
|
||||
margin: 4em;
|
||||
margin-left: 10em;
|
||||
margin-right: 10em;
|
||||
background-color: #fefefe;
|
||||
}
|
||||
|
||||
@media (max-width:960px) {
|
||||
.container{
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.sectionHeader{
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.textcontainer{
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="sectionHeader">
|
||||
<h3>Welcome to Zoraxy Static Web Server!</h3>
|
||||
</div>
|
||||
<div class="textcontainer">
|
||||
<p>If you see this page, that means your static web server is running.<br>
|
||||
By default, all the html files are stored under <code>./web/html/</code>
|
||||
relative to the zoraxy runtime directory.<br>
|
||||
You can upload your html files to your web directory via the <b>Web Directory Manager</b>.
|
||||
</p>
|
||||
<p>
|
||||
For online documentation, please refer to <a href="//zoraxy.arozos.com">zoraxy.arozos.com</a> or the <a href="https://github.com/tobychui/zoraxy/wiki">project wiki</a>.<br>
|
||||
Thank you for using Zoraxy!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
18
src/mod/webserv/utils.go
Normal file
@ -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
|
||||
}
|
195
src/mod/webserv/webserv.go
Normal file
@ -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()
|
||||
}
|
@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@ -25,33 +24,43 @@ func ReverseProxtInit() {
|
||||
inboundPort := 80
|
||||
if sysdb.KeyExists("settings", "inbound") {
|
||||
sysdb.Read("settings", "inbound", &inboundPort)
|
||||
log.Println("Serving inbound port ", inboundPort)
|
||||
SystemWideLogger.Println("Serving inbound port ", inboundPort)
|
||||
} else {
|
||||
log.Println("Inbound port not set. Using default (80)")
|
||||
SystemWideLogger.Println("Inbound port not set. Using default (80)")
|
||||
}
|
||||
|
||||
useTls := false
|
||||
sysdb.Read("settings", "usetls", &useTls)
|
||||
if useTls {
|
||||
log.Println("TLS mode enabled. Serving proxxy request with TLS")
|
||||
SystemWideLogger.Println("TLS mode enabled. Serving proxxy request with TLS")
|
||||
} else {
|
||||
log.Println("TLS mode disabled. Serving proxy request with plain http")
|
||||
SystemWideLogger.Println("TLS mode disabled. Serving proxy request with plain http")
|
||||
}
|
||||
|
||||
forceLatestTLSVersion := false
|
||||
sysdb.Read("settings", "forceLatestTLS", &forceLatestTLSVersion)
|
||||
if forceLatestTLSVersion {
|
||||
log.Println("Force latest TLS mode enabled. Minimum TLS LS version is set to v1.2")
|
||||
SystemWideLogger.Println("Force latest TLS mode enabled. Minimum TLS LS version is set to v1.2")
|
||||
} else {
|
||||
log.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
|
||||
SystemWideLogger.Println("Force latest TLS mode disabled. Minimum TLS version is set to v1.0")
|
||||
}
|
||||
|
||||
listenOnPort80 := false
|
||||
sysdb.Read("settings", "listenP80", &listenOnPort80)
|
||||
if listenOnPort80 {
|
||||
SystemWideLogger.Println("Port 80 listener enabled")
|
||||
} else {
|
||||
SystemWideLogger.Println("Port 80 listener disabled")
|
||||
}
|
||||
|
||||
forceHttpsRedirect := false
|
||||
sysdb.Read("settings", "redirect", &forceHttpsRedirect)
|
||||
if forceHttpsRedirect {
|
||||
log.Println("Force HTTPS mode enabled")
|
||||
SystemWideLogger.Println("Force HTTPS mode enabled")
|
||||
//Port 80 listener must be enabled to perform http -> https redirect
|
||||
listenOnPort80 = true
|
||||
} else {
|
||||
log.Println("Force HTTPS mode disabled")
|
||||
SystemWideLogger.Println("Force HTTPS mode disabled")
|
||||
}
|
||||
|
||||
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
|
||||
@ -59,14 +68,16 @@ func ReverseProxtInit() {
|
||||
Port: inboundPort,
|
||||
UseTls: useTls,
|
||||
ForceTLSLatest: forceLatestTLSVersion,
|
||||
ListenOnPort80: listenOnPort80,
|
||||
ForceHttpsRedirect: forceHttpsRedirect,
|
||||
TlsManager: tlsCertManager,
|
||||
RedirectRuleTable: redirectTable,
|
||||
GeodbStore: geodbStore,
|
||||
StatisticCollector: statisticCollector,
|
||||
WebDirectory: *staticWebServerRoot,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -77,7 +88,7 @@ func ReverseProxtInit() {
|
||||
for _, conf := range confs {
|
||||
record, err := LoadReverseProxyConfig(conf)
|
||||
if err != nil {
|
||||
log.Println("Failed to load "+filepath.Base(conf), err.Error())
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Failed to load config file: "+filepath.Base(conf), err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -91,6 +102,7 @@ func ReverseProxtInit() {
|
||||
MatchingDomain: record.Rootname,
|
||||
Domain: record.ProxyTarget,
|
||||
RequireTLS: record.UseTLS,
|
||||
BypassGlobalTLS: record.BypassGlobalTLS,
|
||||
SkipCertValidations: record.SkipTlsValidation,
|
||||
RequireBasicAuth: record.RequireBasicAuth,
|
||||
BasicAuthCredentials: record.BasicAuthCredentials,
|
||||
@ -101,13 +113,14 @@ func ReverseProxtInit() {
|
||||
RootName: record.Rootname,
|
||||
Domain: record.ProxyTarget,
|
||||
RequireTLS: record.UseTLS,
|
||||
BypassGlobalTLS: record.BypassGlobalTLS,
|
||||
SkipCertValidations: record.SkipTlsValidation,
|
||||
RequireBasicAuth: record.RequireBasicAuth,
|
||||
BasicAuthCredentials: record.BasicAuthCredentials,
|
||||
BasicAuthExceptionRules: record.BasicAuthExceptionRules,
|
||||
})
|
||||
} else {
|
||||
log.Println("Unsupported endpoint type: " + record.ProxyType + ". Skipping " + filepath.Base(conf))
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unsupported endpoint type: "+record.ProxyType+". Skipping "+filepath.Base(conf), nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,7 +129,7 @@ func ReverseProxtInit() {
|
||||
//reverse proxy server in front of this service
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
dynamicProxyRouter.StartProxyService()
|
||||
log.Println("Dynamic Reverse Proxy service started")
|
||||
SystemWideLogger.Println("Dynamic Reverse Proxy service started")
|
||||
|
||||
//Add all proxy services to uptime monitor
|
||||
//Create a uptime monitor service
|
||||
@ -127,7 +140,7 @@ func ReverseProxtInit() {
|
||||
Interval: 300, //5 minutes
|
||||
MaxRecordsStore: 288, //1 day
|
||||
})
|
||||
log.Println("Uptime Monitor background service started")
|
||||
SystemWideLogger.Println("Uptime Monitor background service started")
|
||||
}()
|
||||
|
||||
}
|
||||
@ -179,6 +192,13 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
useTLS := (tls == "true")
|
||||
|
||||
bypassGlobalTLS, _ := utils.PostPara(r, "bypassGlobalTLS")
|
||||
if bypassGlobalTLS == "" {
|
||||
bypassGlobalTLS = "false"
|
||||
}
|
||||
|
||||
useBypassGlobalTLS := bypassGlobalTLS == "true"
|
||||
|
||||
stv, _ := utils.PostPara(r, "tlsval")
|
||||
if stv == "" {
|
||||
stv = "false"
|
||||
@ -239,6 +259,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
RootName: vdir,
|
||||
Domain: endpoint,
|
||||
RequireTLS: useTLS,
|
||||
BypassGlobalTLS: useBypassGlobalTLS,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
@ -256,6 +277,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
MatchingDomain: subdomain,
|
||||
Domain: endpoint,
|
||||
RequireTLS: useTLS,
|
||||
BypassGlobalTLS: useBypassGlobalTLS,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
@ -280,6 +302,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
Rootname: rootname,
|
||||
ProxyTarget: endpoint,
|
||||
UseTLS: useTLS,
|
||||
BypassGlobalTLS: useBypassGlobalTLS,
|
||||
SkipTlsValidation: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
@ -331,9 +354,15 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if stv == "" {
|
||||
stv = "false"
|
||||
}
|
||||
|
||||
skipTlsValidation := (stv == "true")
|
||||
|
||||
//Load bypass TLS option
|
||||
bpgtls, _ := utils.PostPara(r, "bpgtls")
|
||||
if bpgtls == "" {
|
||||
bpgtls = "false"
|
||||
}
|
||||
bypassGlobalTLS := (bpgtls == "true")
|
||||
|
||||
rba, _ := utils.PostPara(r, "bauth")
|
||||
if rba == "" {
|
||||
rba = "false"
|
||||
@ -353,6 +382,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
RootName: targetProxyEntry.RootOrMatchingDomain,
|
||||
Domain: endpoint,
|
||||
RequireTLS: useTLS,
|
||||
BypassGlobalTLS: false,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
||||
@ -365,6 +395,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
MatchingDomain: targetProxyEntry.RootOrMatchingDomain,
|
||||
Domain: endpoint,
|
||||
RequireTLS: useTLS,
|
||||
BypassGlobalTLS: bypassGlobalTLS,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
||||
@ -384,6 +415,10 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
||||
}
|
||||
SaveReverseProxyConfigToFile(&thisProxyConfigRecord)
|
||||
|
||||
//Update uptime monitor
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
@ -416,6 +451,9 @@ func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
uptimeMonitor.CleanRecords()
|
||||
}
|
||||
|
||||
//Update uptime monitor
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
@ -730,6 +768,35 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle port 80 incoming traffics
|
||||
func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
|
||||
enabled, err := utils.GetPara(r, "enable")
|
||||
if err != nil {
|
||||
//Load the current status
|
||||
currentEnabled := false
|
||||
err = sysdb.Read("settings", "listenP80", ¤tEnabled)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
js, _ := json.Marshal(currentEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enabled == "true" {
|
||||
sysdb.Write("settings", "listenP80", true)
|
||||
SystemWideLogger.Println("Enabling port 80 listener")
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(true)
|
||||
} else if enabled == "false" {
|
||||
sysdb.Write("settings", "listenP80", false)
|
||||
SystemWideLogger.Println("Disabling port 80 listener")
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(true)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid mode given: "+enabled)
|
||||
}
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle https redirect
|
||||
func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
useRedirect, err := utils.GetPara(r, "set")
|
||||
@ -750,11 +817,11 @@ func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
if useRedirect == "true" {
|
||||
sysdb.Write("settings", "redirect", true)
|
||||
log.Println("Updating force HTTPS redirection to true")
|
||||
SystemWideLogger.Println("Updating force HTTPS redirection to true")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
|
||||
} else if useRedirect == "false" {
|
||||
sysdb.Write("settings", "redirect", false)
|
||||
log.Println("Updating force HTTPS redirection to false")
|
||||
SystemWideLogger.Println("Updating force HTTPS redirection to false")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
|
||||
|
37
src/start.go
@ -14,6 +14,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/pathrule"
|
||||
@ -22,6 +23,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/statistic/analytic"
|
||||
"imuslab.com/zoraxy/mod/tcpprox"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/webserv"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -92,10 +94,17 @@ func startupSequence() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a system wide logger
|
||||
l, err := logger.NewLogger("zr", "./log", *logOutputToFile)
|
||||
if err == nil {
|
||||
SystemWideLogger = l
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
//Create a netstat buffer
|
||||
netstatBuffers, err = netstat.NewNetStatBuffer(300)
|
||||
if err != nil {
|
||||
log.Println("Failed to load network statistic info")
|
||||
SystemWideLogger.PrintAndLog("Network", "Failed to load network statistic info", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@ -133,13 +142,13 @@ func startupSequence() {
|
||||
BuildVersion: version,
|
||||
}, "")
|
||||
if err != nil {
|
||||
log.Println("Unable to startup mDNS service. Disabling mDNS services")
|
||||
SystemWideLogger.Println("Unable to startup mDNS service. Disabling mDNS services")
|
||||
} else {
|
||||
//Start initial scanning
|
||||
go func() {
|
||||
hosts := mdnsScanner.Scan(30, "")
|
||||
previousmdnsScanResults = hosts
|
||||
log.Println("mDNS Startup scan completed")
|
||||
SystemWideLogger.Println("mDNS Startup scan completed")
|
||||
}()
|
||||
|
||||
//Create a ticker to update mDNS results every 5 minutes
|
||||
@ -153,7 +162,7 @@ func startupSequence() {
|
||||
case <-ticker.C:
|
||||
hosts := mdnsScanner.Scan(30, "")
|
||||
previousmdnsScanResults = hosts
|
||||
log.Println("mDNS scan result updated")
|
||||
SystemWideLogger.Println("mDNS scan result updated")
|
||||
}
|
||||
}
|
||||
}()
|
||||
@ -170,7 +179,7 @@ func startupSequence() {
|
||||
if usingZtAuthToken == "" {
|
||||
usingZtAuthToken, err = ganserv.TryLoadorAskUserForAuthkey()
|
||||
if err != nil {
|
||||
log.Println("Failed to load ZeroTier controller API authtoken")
|
||||
SystemWideLogger.Println("Failed to load ZeroTier controller API authtoken")
|
||||
}
|
||||
}
|
||||
ganManager = ganserv.NewNetworkManager(&ganserv.NetworkManagerOptions{
|
||||
@ -203,11 +212,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: "5487", //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
|
||||
|
@ -65,19 +65,21 @@
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<table class="ui sortable unstackable celled table">
|
||||
<thead>
|
||||
<tr><th>Domain</th>
|
||||
<th>Last Update</th>
|
||||
<th>Expire At</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
</tr></thead>
|
||||
<tbody id="certifiedDomainList">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
|
||||
<table class="ui sortable unstackable celled table">
|
||||
<thead>
|
||||
<tr><th>Domain</th>
|
||||
<th>Last Update</th>
|
||||
<th>Expire At</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
</tr></thead>
|
||||
<tbody id="certifiedDomainList">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
|
||||
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow refresh icon"></i> Auto Renew (ACME) Settings</button>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
|
||||
@ -85,11 +87,49 @@
|
||||
depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for <code>a.example.com</code> and <code>b.example.com</code>).<br>
|
||||
If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<h4>Certificate Authority (CA) and Auto Renew (ACME)</h4>
|
||||
<p>Management features regarding CA and ACME</p>
|
||||
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
|
||||
<div class="ui fluid form">
|
||||
<div class="field">
|
||||
<label>Preferred CA</label>
|
||||
<div class="ui selection dropdown" id="defaultCA">
|
||||
<input type="hidden" name="defaultCA">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">Let's Encrypt</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
|
||||
<div class="item" data-value="Buypass">Buypass</div>
|
||||
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>ACME Email</label>
|
||||
<input id="prefACMEEmail" type="text" placeholder="ACME Email">
|
||||
</div>
|
||||
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
|
||||
</div><br>
|
||||
<h5>Certificate Renew / Generation (ACME) Settings</h5>
|
||||
<div class="ui basic segment">
|
||||
<h4 class="ui header" id="acmeAutoRenewer">
|
||||
<i class="red circle icon"></i>
|
||||
<div class="content">
|
||||
<span id="acmeAutoRenewerStatus">Disabled</span>
|
||||
<div class="sub header">Auto-Renewer Status</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<p>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.</p>
|
||||
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
|
||||
</div>
|
||||
<script>
|
||||
var uploadPendingPublicKey = undefined;
|
||||
var uploadPendingPrivateKey = undefined;
|
||||
|
||||
$("#defaultCA").dropdown();
|
||||
|
||||
//Delete the certificate by its domain
|
||||
function deleteCertificate(domain){
|
||||
if (confirm("Confirm delete certificate for " + domain + " ?")){
|
||||
@ -110,6 +150,62 @@
|
||||
|
||||
}
|
||||
|
||||
function initAcmeStatus(){
|
||||
//Initialize the current default CA options
|
||||
$.get("/api/acme/autoRenew/email", function(data){
|
||||
$("#prefACMEEmail").val(data);
|
||||
});
|
||||
|
||||
$.get("/api/acme/autoRenew/ca", function(data){
|
||||
$("#defaultCA").dropdown("set value", data);
|
||||
});
|
||||
|
||||
$.get("/api/acme/autoRenew/enable", function(data){
|
||||
setACMEEnableStates(data);
|
||||
})
|
||||
}
|
||||
//Set the status of the acme enable icon
|
||||
function setACMEEnableStates(enabled){
|
||||
$("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
|
||||
$("#acmeAutoRenewer").find("i").attr("class", enabled?"green circle icon":"red circle icon");
|
||||
}
|
||||
initAcmeStatus();
|
||||
|
||||
function saveDefaultCA(){
|
||||
let newDefaultEmail = $("#prefACMEEmail").val().trim();
|
||||
let newDefaultCA = $("#defaultCA").dropdown("get value");
|
||||
|
||||
if (newDefaultEmail == ""){
|
||||
msgbox("Invalid acme email given", false);
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/api/acme/autoRenew/email",
|
||||
method: "POST",
|
||||
data: {"set": newDefaultEmail},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: "/api/acme/autoRenew/ca",
|
||||
data: {"set": newDefaultCA},
|
||||
method: "POST",
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
msgbox("Settings updated");
|
||||
|
||||
}
|
||||
|
||||
//List the stored certificates
|
||||
function initManagedDomainCertificateList(){
|
||||
$.get("/api/cert/list?date=true", function(data){
|
||||
|
@ -14,6 +14,13 @@
|
||||
<label>Root require TLS connection <br><small>Check this if your proxy root URL starts with https://</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui horizontal divider">OR</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="useStaticWebServer" onchange="handleUseStaticWebServerAsRoot()">
|
||||
<label>Use Static Web Server as Root <br><small>Check this if you prefer a more Apache Web Server like experience</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<button class="ui basic button" onclick="setProxyRoot()"><i class="teal home icon" ></i> Update Proxy Root</button>
|
||||
<div class="ui divider"></div>
|
||||
@ -58,7 +65,31 @@
|
||||
<script>
|
||||
$("#advanceRootSettings").accordion();
|
||||
|
||||
function initRootInfo(){
|
||||
function handleUseStaticWebServerAsRoot(){
|
||||
let useStaticWebServer = $("#useStaticWebServer")[0].checked;
|
||||
if (useStaticWebServer){
|
||||
let staticWebServerURL = "127.0.0.1:" + $("#webserv_listenPort").val();
|
||||
$("#proxyRoot").val(staticWebServerURL);
|
||||
$("#proxyRoot").parent().addClass("disabled");
|
||||
$("#rootReqTLS").parent().checkbox("set unchecked");
|
||||
$("#rootReqTLS").parent().addClass("disabled");
|
||||
|
||||
//Check if web server is enabled. If not, ask if the user want to enable it
|
||||
/*if (!$("#webserv_enable").parent().checkbox("is checked")){
|
||||
confirmBox("Enable static web server now?", function(choice){
|
||||
if (choice == true){
|
||||
$("#webserv_enable").parent().checkbox("set checked");
|
||||
}
|
||||
});
|
||||
}*/
|
||||
}else{
|
||||
$("#rootReqTLS").parent().removeClass("disabled");
|
||||
$("#proxyRoot").parent().removeClass("disabled");
|
||||
initRootInfo();
|
||||
}
|
||||
}
|
||||
|
||||
function initRootInfo(callback=undefined){
|
||||
$.get("/api/proxy/list?type=root", function(data){
|
||||
if (data == null){
|
||||
|
||||
@ -66,12 +97,40 @@
|
||||
$("#proxyRoot").val(data.Domain);
|
||||
checkRootRequireTLS(data.Domain);
|
||||
}
|
||||
});
|
||||
|
||||
if (callback != undefined){
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
initRootInfo(function(){
|
||||
updateWebServerLinkSettings();
|
||||
});
|
||||
|
||||
//Update the current web server port settings
|
||||
function updateWebServerLinkSettings(){
|
||||
isUsingStaticWebServerAsRoot(function(isUsingWebServ){
|
||||
if (isUsingWebServ){
|
||||
$(".webservRootDisabled").addClass("disabled");
|
||||
$("#useStaticWebServer").parent().checkbox("set checked");
|
||||
}else{
|
||||
$(".webservRootDisabled").removeClass("disabled");
|
||||
$("#useStaticWebServer").parent().checkbox("set unchecked");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function isUsingStaticWebServerAsRoot(callback){
|
||||
let currentProxyRoot = $("#proxyRoot").val().trim();
|
||||
$.get("/api/webserv/status", function(webservStatus){
|
||||
if (currentProxyRoot == "127.0.0.1:" + webservStatus.ListeningPort || currentProxyRoot == "localhost:" + webservStatus.ListeningPort){
|
||||
return callback(true);
|
||||
}
|
||||
return callback(false);
|
||||
});
|
||||
|
||||
}
|
||||
initRootInfo();
|
||||
|
||||
|
||||
function updateRootSettingStates(){
|
||||
$.get("/api/cert/tls", function(data){
|
||||
if (data == true){
|
||||
@ -81,6 +140,7 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Bind event to tab switch
|
||||
tabSwitchEventBind["setroot"] = function(){
|
||||
//On switch over to this page, update root info
|
||||
@ -107,11 +167,12 @@
|
||||
let useRedirect = $("#unsetRedirect")[0].checked;
|
||||
updateRedirectionDomainSettingInputBox(useRedirect);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
checkCustomRedirectForUnsetSubd();
|
||||
|
||||
//Check if the given domain will redirect to https
|
||||
function checkRootRequireTLS(targetDomain){
|
||||
//Trim off the http or https from the origin
|
||||
if (targetDomain.startsWith("http://")){
|
||||
@ -138,7 +199,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//Set the new proxy root option
|
||||
function setProxyRoot(){
|
||||
var newpr = $("#proxyRoot").val();
|
||||
if (newpr.trim() == ""){
|
||||
@ -159,8 +220,24 @@
|
||||
msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
//OK
|
||||
initRootInfo();
|
||||
msgbox("Proxy Root Updated")
|
||||
initRootInfo(function(){
|
||||
//Check if WebServ is enabled
|
||||
isUsingStaticWebServerAsRoot(function(isUsingWebServ){
|
||||
if (isUsingWebServ){
|
||||
//Force enable static web server
|
||||
//See webserv.html for details
|
||||
setWebServerRunningState(true);
|
||||
}
|
||||
|
||||
setTimeout(function(){
|
||||
//Update the checkbox
|
||||
updateWebServerLinkSettings();
|
||||
msgbox("Proxy Root Updated");
|
||||
}, 1000);
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -8,7 +8,7 @@
|
||||
<div class="field">
|
||||
<label>Proxy Type</label>
|
||||
<div class="ui selection dropdown">
|
||||
<input type="hidden" id="ptype" value="subd">
|
||||
<input type="hidden" id="ptype" value="subd" onchange="handleProxyTypeOptionChange(this.value)">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">Proxy Type</div>
|
||||
<div class="menu">
|
||||
@ -22,7 +22,7 @@
|
||||
<input type="text" id="rootname" placeholder="s1.mydomain.com">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>IP Address or Domain Name with port</label>
|
||||
<label>Target IP Address or Domain Name with port</label>
|
||||
<input type="text" id="proxyDomain" onchange="autoCheckTls(this.value);">
|
||||
<small>E.g. 192.168.0.101:8000 or example.com</small>
|
||||
</div>
|
||||
@ -44,7 +44,13 @@
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="skipTLSValidation">
|
||||
<label>Ignore TLS/SSL Verification Error<br><small>E.g. self-signed, expired certificate (Not Recommended)</small></label>
|
||||
<label>Ignore TLS/SSL Verification Error<br><small>For targets that is using self-signed, expired certificate (Not Recommended)</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="bypassGlobalTLS">
|
||||
<label>Allow plain HTTP access<br><small>Allow this subdomain to be connected without TLS (Require HTTP server enabled on port 80)</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
@ -123,6 +129,7 @@
|
||||
var proxyDomain = $("#proxyDomain").val();
|
||||
var useTLS = $("#reqTls")[0].checked;
|
||||
var skipTLSValidation = $("#skipTLSValidation")[0].checked;
|
||||
var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
|
||||
var requireBasicAuth = $("#requireBasicAuth")[0].checked;
|
||||
|
||||
if (type === "vdir") {
|
||||
@ -162,6 +169,7 @@
|
||||
tls: useTLS,
|
||||
ep: proxyDomain,
|
||||
tlsval: skipTLSValidation,
|
||||
bypassGlobalTLS: bypassGlobalTLS,
|
||||
bauth: requireBasicAuth,
|
||||
cred: JSON.stringify(credentials),
|
||||
},
|
||||
@ -184,10 +192,17 @@
|
||||
if (type == "subd" && $("#tls").checkbox("is checked")){
|
||||
confirmBox("Request new SSL Cert for this subdomain?", function(choice){
|
||||
if (choice == true){
|
||||
//Load the prefer CA from TLS page
|
||||
let defaultCA = $("#defaultCA").dropdown("get value");
|
||||
if (defaultCA.trim() == ""){
|
||||
defaultCA = "Let's Encrypt";
|
||||
}
|
||||
//Get a new cert using ACME
|
||||
msgbox("Requesting certificate via Let's Encrypt...");
|
||||
msgbox("Requesting certificate via " + defaultCA +"...");
|
||||
console.log("Trying to get a new certificate via ACME");
|
||||
obtainCertificate(rootname);
|
||||
obtainCertificate(rootname, defaultCA.trim());
|
||||
}else{
|
||||
msgbox("Proxy Endpoint Added");
|
||||
}
|
||||
});
|
||||
}else{
|
||||
@ -199,6 +214,14 @@
|
||||
|
||||
}
|
||||
|
||||
function handleProxyTypeOptionChange(newType){
|
||||
if (newType == "subd"){
|
||||
$("#bypassGlobalTLS").parent().removeClass("disabled");
|
||||
}else if (newType == "vdir"){
|
||||
$("#bypassGlobalTLS").parent().addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
//Generic functions for delete rp endpoints
|
||||
function deleteEndpoint(ptype, epoint){
|
||||
if (confirm("Confirm remove proxy for :" + epoint + " (type: " + ptype + ")?")){
|
||||
@ -324,7 +347,7 @@
|
||||
var columns = row.find('td[data-label]');
|
||||
var payload = $(row).attr("payload");
|
||||
payload = JSON.parse(decodeURIComponent(payload));
|
||||
|
||||
console.log(payload);
|
||||
//console.log(payload);
|
||||
columns.each(function(index) {
|
||||
var column = $(this);
|
||||
@ -340,34 +363,37 @@
|
||||
var datatype = $(this).attr("datatype");
|
||||
if (datatype == "domain"){
|
||||
let domain = payload.Domain;
|
||||
//Target require TLS for proxying
|
||||
let tls = payload.RequireTLS;
|
||||
if (tls){
|
||||
tls = "checked";
|
||||
}else{
|
||||
tls = "";
|
||||
}
|
||||
|
||||
//Require TLS validation
|
||||
let skipTLSValidation = payload.SkipCertValidations;
|
||||
let checkstate = "";
|
||||
if (skipTLSValidation){
|
||||
checkstate = "checked";
|
||||
}
|
||||
|
||||
input = `
|
||||
<div class="ui mini fluid input">
|
||||
<input type="text" class="Domain" value="${domain}">
|
||||
</div>
|
||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<input type="checkbox" class="RequireTLS" ${tls}>
|
||||
<label>Require TLS</label>
|
||||
<label>Require TLS<br>
|
||||
<small>Proxy target require HTTPS connection</small></label>
|
||||
</div><br>
|
||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
|
||||
<label>Skip Verification<br>
|
||||
<small>Check this if proxy target is using self signed certificates</small></label>
|
||||
</div>
|
||||
`;
|
||||
column.empty().append(input);
|
||||
|
||||
}else if (datatype == "skipver"){
|
||||
let skipTLSValidation = payload.SkipCertValidations;
|
||||
let checkstate = "";
|
||||
if (skipTLSValidation){
|
||||
checkstate = "checked";
|
||||
}
|
||||
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<input type="checkbox" class="SkipCertValidations" ${checkstate}>
|
||||
<label>Skip Verification</label>
|
||||
<small>Check this if you are using self signed certificates</small>
|
||||
</div>`);
|
||||
}else if (datatype == "basicauth"){
|
||||
let requireBasicAuth = payload.RequireBasicAuth;
|
||||
let checkstate = "";
|
||||
@ -385,6 +411,16 @@
|
||||
<button title="Cancel" onclick="exitProxyInlineEdit('${endpointType}');" class="ui basic small circular icon button"><i class="ui remove icon"></i></button>
|
||||
<button title="Save" onclick="saveProxyInlineEdit('${uuid}');" class="ui basic small circular icon button"><i class="ui green save icon"></i></button>
|
||||
`);
|
||||
}else if (datatype == "inbound" && payload.ProxyType == 0){
|
||||
let originalContent = $(column).html();
|
||||
column.empty().append(`${originalContent}
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||
<input type="checkbox" class="BypassGlobalTLS" ${payload.BypassGlobalTLS?"checked":""}>
|
||||
<label>Allow plain HTTP access<br>
|
||||
<small>Allow inbound connections without TLS/SSL</small></label>
|
||||
</div><br>
|
||||
`);
|
||||
}else{
|
||||
//Unknown field. Leave it untouched
|
||||
}
|
||||
@ -416,6 +452,7 @@
|
||||
let requireTLS = $(row).find(".RequireTLS")[0].checked;
|
||||
let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked;
|
||||
let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked;
|
||||
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
|
||||
|
||||
console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth)
|
||||
|
||||
@ -426,6 +463,7 @@
|
||||
"type": epttype,
|
||||
"rootname": uuid,
|
||||
"ep":newDomain,
|
||||
"bpgtls": bypassGlobalTLS,
|
||||
"tls" :requireTLS,
|
||||
"tlsval": skipCertValidations,
|
||||
"bauth" :requireBasicAuth,
|
||||
@ -467,7 +505,7 @@
|
||||
});
|
||||
|
||||
// Obtain certificate from API, only support one domain
|
||||
function obtainCertificate(domains) {
|
||||
function obtainCertificate(domains, usingCa = "Let's Encrypt") {
|
||||
let filename = "";
|
||||
let email = acmeEmail;
|
||||
if (acmeEmail == ""){
|
||||
@ -494,7 +532,7 @@
|
||||
domains: domains,
|
||||
filename: filename,
|
||||
email: email,
|
||||
ca: "Let's Encrypt",
|
||||
ca: usingCa,
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.error) {
|
||||
|
@ -72,10 +72,15 @@
|
||||
<label>Use TLS to serve proxy request</label>
|
||||
</div>
|
||||
<br>
|
||||
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
|
||||
<div id="listenP80" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;" >
|
||||
<input type="checkbox">
|
||||
<label>Force redirect HTTP request to HTTPS<br>
|
||||
<small>(Only apply when listening port is not 80)</small></label>
|
||||
<label>Enable HTTP server on port 80<br>
|
||||
<small>(Only apply when TLS enabled and not using port 80)</small></label>
|
||||
</div>
|
||||
<br>
|
||||
<div id="redirect" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em; padding-left: 2em;">
|
||||
<input type="checkbox">
|
||||
<label>Force redirect HTTP request to HTTPS</label>
|
||||
</div>
|
||||
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
|
||||
<div class="ui accordion advanceSettings">
|
||||
@ -181,6 +186,7 @@
|
||||
$("#serverstatus").removeClass("green");
|
||||
}
|
||||
$("#incomingPort").val(data.Option.Port);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@ -305,6 +311,27 @@
|
||||
});
|
||||
}
|
||||
|
||||
function handleP80ListenerStateChange(enabled){
|
||||
$.ajax({
|
||||
url: "/api/proxy/listenPort80",
|
||||
data: {"enable": enabled},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
console.log(data.error);
|
||||
return;
|
||||
}
|
||||
if (enabled){
|
||||
$("#redirect").show();
|
||||
msgbox("Port 80 listener enabled");
|
||||
}else{
|
||||
$("#redirect").hide();
|
||||
msgbox("Port 80 listener disabled");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
function handlePortChange(){
|
||||
var newPortValue = $("#incomingPort").val();
|
||||
@ -323,6 +350,25 @@
|
||||
});
|
||||
}
|
||||
|
||||
function initPort80ListenerSetting(){
|
||||
$.get("/api/proxy/listenPort80", function(data){
|
||||
if (data){
|
||||
$("#listenP80").checkbox("set checked");
|
||||
$("#redirect").show();
|
||||
}else{
|
||||
$("#listenP80").checkbox("set unchecked");
|
||||
$("#redirect").hide();
|
||||
}
|
||||
|
||||
$("#listenP80").find("input").on("change", function(){
|
||||
let enabled = $(this)[0].checked;
|
||||
handleP80ListenerStateChange(enabled);
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
initPort80ListenerSetting();
|
||||
|
||||
function initHTTPtoHTTPSRedirectSetting(){
|
||||
$.get("/api/proxy/useHttpsRedirect", function(data){
|
||||
if (data == true){
|
||||
@ -356,8 +402,6 @@
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
initHTTPtoHTTPSRedirectSetting();
|
||||
|
||||
|
@ -9,7 +9,6 @@
|
||||
<tr>
|
||||
<th>Matching Domain</th>
|
||||
<th>Proxy To</th>
|
||||
<th>TLS/SSL Verification</th>
|
||||
<th>Basic Auth</th>
|
||||
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
|
||||
</tr>
|
||||
@ -41,19 +40,14 @@
|
||||
let subdData = encodeURIComponent(JSON.stringify(subd));
|
||||
if (subd.RequireTLS){
|
||||
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"
|
||||
if (subd.SkipCertValidations){
|
||||
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
|
||||
}
|
||||
}
|
||||
|
||||
$("#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="inbound"><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">${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>
|
||||
|
@ -86,7 +86,7 @@
|
||||
|
||||
let id = value[0].ID;
|
||||
let name = value[0].Name;
|
||||
let url = value[0].URL;
|
||||
let url = value[value.length - 1].URL;
|
||||
let protocol = value[0].Protocol;
|
||||
|
||||
//Generate the status dot
|
||||
@ -112,6 +112,9 @@
|
||||
if (thisStatus.StatusCode >= 500 && thisStatus.StatusCode < 600){
|
||||
//Special type of error, cause by downstream reverse proxy
|
||||
dotType = "error";
|
||||
}else if (thisStatus.StatusCode == 401){
|
||||
//Unauthorized error
|
||||
dotType = "error";
|
||||
}else{
|
||||
dotType = "offline";
|
||||
}
|
||||
@ -141,6 +144,28 @@
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Misconfigured`;
|
||||
onlineStatusCss = `color: #f38020;`;
|
||||
reminderEle = `<small style="${onlineStatusCss}">Downstream proxy server is online with misconfigured settings</small>`;
|
||||
}else if (value[value.length - 1].StatusCode >= 400 && value[value.length - 1].StatusCode <= 405){
|
||||
switch(value[value.length - 1].StatusCode){
|
||||
case 400:
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Bad Request`;
|
||||
break;
|
||||
case 401:
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Unauthorized`;
|
||||
break;
|
||||
case 403:
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Forbidden`;
|
||||
break;
|
||||
case 404:
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Not Found`;
|
||||
break;
|
||||
case 405:
|
||||
currentOnlineStatus = `<i class="exclamation circle icon"></i> Method Not Allowed`;
|
||||
break;
|
||||
}
|
||||
|
||||
onlineStatusCss = `color: #f38020;`;
|
||||
reminderEle = `<small style="${onlineStatusCss}">Target online but not accessible</small>`;
|
||||
|
||||
}else{
|
||||
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
|
||||
onlineStatusCss = `color: #df484a;`;
|
||||
|
@ -9,7 +9,6 @@
|
||||
<tr>
|
||||
<th>Virtual Directory</th>
|
||||
<th>Proxy To</th>
|
||||
<th>TLS/SSL Verification</th>
|
||||
<th>Basic Auth</th>
|
||||
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
|
||||
</tr>
|
||||
@ -43,6 +42,9 @@
|
||||
let vdirData = encodeURIComponent(JSON.stringify(vdir));
|
||||
if (vdir.RequireTLS){
|
||||
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
||||
if (vdir.SkipCertValidations){
|
||||
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
|
||||
}
|
||||
}
|
||||
|
||||
let tlsVerificationField = "";
|
||||
@ -55,7 +57,6 @@
|
||||
$("#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">${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>
|
||||
|
229
src/web/components/webserv.html
Normal file
@ -0,0 +1,229 @@
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Static Web Server</h2>
|
||||
<p>A simple static web server that serve html css and js files</p>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui basic segment">
|
||||
<h4 class="ui header" id="webservRunningState">
|
||||
<i class="green circle icon"></i>
|
||||
<div class="content">
|
||||
<span class="webserv_status">Running</span>
|
||||
<div class="sub header">Listen port :<span class="webserv_port">8081</span></div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<h3>Web Server Settings</h3>
|
||||
<div class="ui form">
|
||||
<div class="inline field">
|
||||
<div class="ui toggle checkbox webservRootDisabled">
|
||||
<input id="webserv_enable" type="checkbox" class="hidden">
|
||||
<label>Enable Static Web Server</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline field">
|
||||
<div class="ui toggle checkbox">
|
||||
<input id="webserv_enableDirList" type="checkbox" class="hidden">
|
||||
<label>Enable Directory Listing</label>
|
||||
<small>If this folder do not contains any index files, list the directory of this folder.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Document Root Folder</label>
|
||||
<input id="webserv_docRoot" type="text" readonly="true">
|
||||
<small>
|
||||
The web server root folder can only be changed via startup flags of zoraxy for security reasons.
|
||||
See the -webserv flag for more details.
|
||||
</small>
|
||||
</div>
|
||||
<div class="field webservRootDisabled">
|
||||
<label>Port Number</label>
|
||||
<input id="webserv_listenPort" type="number" step="1" min="0" max="65535" value="8081" onchange="updateWebServLinkExample(this.value);">
|
||||
<small>Use <code>http://127.0.0.1:<span class="webserv_port">8081</span></code> in proxy rules to access the web server</small>
|
||||
</div>
|
||||
</div>
|
||||
<small><i class="ui blue save icon"></i> Changes are saved automatically</small>
|
||||
<br>
|
||||
<div class="ui message">
|
||||
<div class="ui accordion webservhelp">
|
||||
<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
How to access the static web server?
|
||||
</div>
|
||||
<div class="content">
|
||||
There are three ways to access the static web server. <br>
|
||||
<div class="ui ordered list">
|
||||
<div class="item">
|
||||
If you are using Zoraxy as your gateway reverse proxy server,
|
||||
you can add a new subdomain proxy rule that points to
|
||||
<a>http://127.0.0.1:<span class="webserv_port">8081</span></a>
|
||||
</div>
|
||||
<div class="item">
|
||||
If you are using Zoraxy under another reverse proxy server,
|
||||
add <a>http://127.0.0.1:<span class="webserv_port">8081</span></a> to the config of your upper layer reverse proxy server's config file.
|
||||
</div>
|
||||
<div class="item">
|
||||
Directly access the web server via <a>http://{zoraxy_host_ip}:<span class="webserv_port">8081</span></a> (Not recommended)
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui basic segment">
|
||||
<h2>Web Directory Manager</h2>
|
||||
<p>Manage your files inside your web directory</p>
|
||||
</div>
|
||||
<div class="ui basic segment" style="display:none;" id="webdirManDisabledNotice">
|
||||
<h4 class="ui header">
|
||||
<i class="ui red times icon"></i>
|
||||
<div class="content">
|
||||
Web Directory Manager Disabled
|
||||
<div class="sub header">Web Directory Manager has been disabled by the system administrator</div>
|
||||
</div>
|
||||
|
||||
</h4>
|
||||
</div>
|
||||
<iframe id="webserv_dirManager" src="tools/fs.html" style="width: 100%; height: 800px; border: 0px; overflow-y: hidden;">
|
||||
|
||||
</iframe>
|
||||
<small>If you do not want to enable web access to your web directory, you can disable this feature with <code>-webfm=false</code> startup paramter</small>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
$(".webservhelp").accordion();
|
||||
$(".ui.checkbox").checkbox();
|
||||
|
||||
function setWebServerRunningState(running){
|
||||
if (running){
|
||||
$("#webserv_enable").parent().checkbox("set checked");
|
||||
$("#webservRunningState").find("i").attr("class", "green circle icon");
|
||||
$("#webservRunningState").find(".webserv_status").text("Running");
|
||||
}else{
|
||||
$("#webserv_enable").parent().checkbox("set unchecked");
|
||||
$("#webservRunningState").find("i").attr("class", "red circle icon");
|
||||
$("#webservRunningState").find(".webserv_status").text("Stopped");
|
||||
}
|
||||
}
|
||||
|
||||
function updateWebServState(){
|
||||
$.get("/api/webserv/status", function(data){
|
||||
//Clear all event listeners
|
||||
$("#webserv_enableDirList").off("change");
|
||||
$("#webserv_enable").off("change");
|
||||
$("#webserv_listenPort").off("change");
|
||||
|
||||
setWebServerRunningState(data.Running);
|
||||
|
||||
if (data.EnableDirectoryListing){
|
||||
$("#webserv_enableDirList").parent().checkbox("set checked");
|
||||
}else{
|
||||
$("#webserv_enableDirList").parent().checkbox("set unchecked");
|
||||
}
|
||||
|
||||
$("#webserv_docRoot").val(data.WebRoot + "/html/");
|
||||
|
||||
if (!data.EnableWebDirManager){
|
||||
$("#webdirManDisabledNotice").show();
|
||||
$("#webserv_dirManager").remove();
|
||||
}
|
||||
|
||||
$("#webserv_listenPort").val(data.ListeningPort);
|
||||
updateWebServLinkExample(data.ListeningPort);
|
||||
|
||||
//Bind checkbox events
|
||||
$("#webserv_enable").off("change").on("change", function(){
|
||||
let enable = $(this)[0].checked;
|
||||
if (enable){
|
||||
$.get("/api/webserv/start", function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
msgbox("Static web server started");
|
||||
setWebServerRunningState(true);
|
||||
}
|
||||
});
|
||||
}else{
|
||||
$.get("/api/webserv/stop", function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
msgbox("Static web server stopped");
|
||||
setWebServerRunningState(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$("#webserv_enableDirList").off("change").on("change", function(){
|
||||
let enable = $(this)[0].checked;
|
||||
$.ajax({
|
||||
url: "/api/webserv/setDirList",
|
||||
method: "POST",
|
||||
data: {"enable": enable},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
msgbox("Directory listing setting updated");
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
$("#webserv_listenPort").off("change").on("change", function(){
|
||||
let newPort = $(this).val();
|
||||
|
||||
//Check if the new value is same as listening port
|
||||
let rpListeningPort = $("#incomingPort").val();
|
||||
if (rpListeningPort == newPort){
|
||||
confirmBox("This setting might cause port conflict. Continue Anyway?", function(choice){
|
||||
if (choice == true){
|
||||
//Continue anyway
|
||||
$.ajax({
|
||||
url: "/api/webserv/setPort",
|
||||
method: "POST",
|
||||
data: {"port": newPort},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
msgbox("Listening port updated");
|
||||
}
|
||||
updateWebServState();
|
||||
}
|
||||
});
|
||||
}else{
|
||||
//Cancel. Restore to previous value
|
||||
updateWebServState();
|
||||
msgbox("Setting restored");
|
||||
}
|
||||
});
|
||||
}else{
|
||||
$.ajax({
|
||||
url: "/api/webserv/setPort",
|
||||
method: "POST",
|
||||
data: {"port": newPort},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
msgbox("Listening port updated");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
updateWebServState();
|
||||
|
||||
function updateWebServLinkExample(newport){
|
||||
$(".webserv_port").text(newport);
|
||||
}
|
||||
</script>
|
||||
</div>
|
10
src/web/components/zgrok.html
Normal file
@ -0,0 +1,10 @@
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Service Expose Proxy</h2>
|
||||
<p>Expose your local test-site on the internet with single command</p>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<h4>Work In Progress</h4>
|
||||
We are looking for someone to help with implementing this feature in Zoraxy. <br>If you know how to write Golang and want to contribute, feel free to create a pull request to this feature!
|
||||
</div>
|
||||
</div>
|
@ -62,13 +62,16 @@
|
||||
<a class="item" tag="gan">
|
||||
<i class="simplistic globe icon"></i> Global Area Network
|
||||
</a>
|
||||
<a class="item" tag="">
|
||||
<a class="item" tag="zgrok">
|
||||
<i class="simplistic podcast icon"></i> Service Expose Proxy
|
||||
</a>
|
||||
<a class="item" tag="tcpprox">
|
||||
<i class="simplistic exchange icon"></i> TCP Proxy
|
||||
</a>
|
||||
<div class="ui divider menudivider">Others</div>
|
||||
<a class="item" tag="webserv">
|
||||
<i class="simplistic globe icon"></i> Static Web Server
|
||||
</a>
|
||||
<a class="item" tag="utm">
|
||||
<i class="simplistic time icon"></i> Uptime Monitor
|
||||
</a>
|
||||
@ -114,9 +117,15 @@
|
||||
<!-- Global Area Networking -->
|
||||
<div id="gan" class="functiontab" target="gan.html"></div>
|
||||
|
||||
<!-- Service Expose Proxy -->
|
||||
<div id="zgrok" class="functiontab" target="zgrok.html"></div>
|
||||
|
||||
<!-- TCP Proxy -->
|
||||
<div id="tcpprox" class="functiontab" target="tcpprox.html"></div>
|
||||
|
||||
<!-- Web Server -->
|
||||
<div id="webserv" class="functiontab" target="webserv.html"></div>
|
||||
|
||||
<!-- Up Time Monitor -->
|
||||
<div id="utm" class="functiontab" target="uptime.html"></div>
|
||||
|
||||
|
@ -124,7 +124,7 @@
|
||||
<label>Ignore TLS/SSL Verification Error<br><small>E.g. self-signed, expired certificate (Not Recommended)</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Renew Certificate</button>
|
||||
<button id="obtainButton" class="ui basic button" type="submit"><i class="yellow refresh icon"></i> Get Certificate</button>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<small>First time setting up HTTPS?<br>Try out our <a href="../tools/https.html" target="_blank">wizard</a></small>
|
||||
@ -218,6 +218,11 @@
|
||||
$("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
|
||||
}
|
||||
});
|
||||
|
||||
if (parent && parent.setACMEEnableStates){
|
||||
parent.setACMEEnableStates(enabled);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Render the domains table that exists in this zoraxy host
|
||||
@ -323,7 +328,8 @@
|
||||
var filename = $("#filenameInput").val();
|
||||
var email = $("#caRegisterEmail").val();
|
||||
if (email == ""){
|
||||
parent.msgbox("ACME renew email is not set")
|
||||
parent.msgbox("ACME renew email is not set", false)
|
||||
$("#obtainButton").removeClass("loading").removeClass("disabled");
|
||||
return;
|
||||
}
|
||||
if (filename.trim() == "" && !domains.includes(",")){
|
||||
@ -334,8 +340,9 @@
|
||||
//Invalid settings. Force the filename to be same as domain
|
||||
//if there are only 1 domain
|
||||
filename = domains;
|
||||
}else{
|
||||
parent.msgbox("Filename cannot be empty for certs containing multiple domains.")
|
||||
}else if (filename == "" && domains.includes(",")){
|
||||
parent.msgbox("Filename cannot be empty for certs containing multiple domains.", false, 5000);
|
||||
$("#obtainButton").removeClass("loading").removeClass("disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
|
1388
src/web/tools/fs.css
Normal file
1057
src/web/tools/fs.html
Normal file
8
src/web/tools/img/arrow-left.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<polygon fill="#FFFFFF" stroke="#231815" stroke-width="3" stroke-miterlimit="10" points="105.518,113.641 20.981,64.833
|
||||
105.518,16.027 "/>
|
||||
</svg>
|
After Width: | Height: | Size: 618 B |
8
src/web/tools/img/arrow-right.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<polygon fill="#FFFFFF" stroke="#231815" stroke-width="3" stroke-miterlimit="10" points="20.981,16.026 105.518,64.833
|
||||
20.981,113.64 "/>
|
||||
</svg>
|
After Width: | Height: | Size: 616 B |
8
src/web/tools/img/eq.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="48px" height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
|
||||
<path fill="#4f4f4f" d="M13.95,36.15v-24.3h3.3v24.3H13.95z M22.35,44.15V3.85h3.3v40.3H22.35z M5.6,28.15v-8.3h3.25v8.3H5.6z
|
||||
M30.75,36.15v-24.3h3.3v24.3H30.75z M39.15,28.15v-8.3h3.25v8.3H39.15z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 669 B |
3442
src/web/tools/img/file.svg
Normal file
After Width: | Height: | Size: 251 KiB |
12
src/web/tools/img/folder.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="128px"
|
||||
height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<g id="圖層_2">
|
||||
<polygon fill="#DBAC50" points="104.03,38.089 104.03,101.074 22.388,101.074 22.388,27.94 49.702,27.75 55.666,38.345 "/>
|
||||
</g>
|
||||
<g id="圖層_3">
|
||||
<polygon fill="#E5BD64" points="104.328,101.97 22.836,101.97 38.209,52.716 118.806,52.716 "/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 728 B |
11
src/web/tools/img/icon.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="200px" height="50px" viewBox="0 0 200 50" enable-background="new 0 0 200 50" xml:space="preserve">
|
||||
<polygon fill="#DBDCDC" points="18.325,42.875 37.912,6.593 57.5,42.875 "/>
|
||||
<polygon fill="#EEEEEF" points="66.771,6.594 94.125,24.744 66.771,42.895 "/>
|
||||
<polygon fill="#9E9E9F" points="180.347,6.594 165.384,25 150.421,6.594 "/>
|
||||
<polygon fill="#9E9E9F" points="150.422,42.875 165.384,24.469 180.347,42.875 "/>
|
||||
<circle fill="#DBDCDC" cx="122.75" cy="25.156" r="17.875"/>
|
||||
</svg>
|
After Width: | Height: | Size: 843 B |
18
src/web/tools/img/network.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="128px"
|
||||
height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<g id="圖層_2">
|
||||
<polygon fill="#95D5F4" points="110.998,23.424 110.998,84.064 17.001,84.064 17.001,13.652 48.449,13.469 55.315,23.669 "/>
|
||||
</g>
|
||||
<g id="圖層_3">
|
||||
<polygon fill="#6BC2EC" points="110.998,84.064 17.001,84.064 17.087,31.401 110.57,31.401 "/>
|
||||
</g>
|
||||
<g id="圖層_4">
|
||||
<rect x="17.001" y="103.51" fill="#B5B5B6" width="93.997" height="4.691"/>
|
||||
<rect x="60.985" y="84.064" fill="#B5B5B6" width="6.029" height="19.445"/>
|
||||
<path fill="#C9CACA" d="M72.935,110.512c0,2.221-1.8,4.02-4.021,4.02h-9.827c-2.221,0-4.021-1.799-4.021-4.02v-9.828
|
||||
c0-2.221,1.8-4.021,4.021-4.021h9.827c2.221,0,4.021,1.801,4.021,4.021V110.512z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
24
src/web/tools/img/opr/copy.svg
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<polygon fill="#FFFFFF" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="78.125,32.875 78.125,83.25
|
||||
30.125,83.25 30.125,22 68.625,22 "/>
|
||||
<polyline fill="none" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="78.125,35.75 65.125,35.75 65.125,22 "/>
|
||||
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.75" y1="38.667" x2="60.417" y2="38.667"/>
|
||||
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.75" y1="45.167" x2="73.25" y2="45.167"/>
|
||||
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.75" y1="51.25" x2="73.25" y2="51.25"/>
|
||||
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.75" y1="57.833" x2="73.25" y2="57.833"/>
|
||||
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.417" y1="64" x2="73.25" y2="64"/>
|
||||
<line fill="none" stroke="#65D0FF" stroke-width="3" stroke-miterlimit="10" x1="35.417" y1="70.75" x2="73.25" y2="70.75"/>
|
||||
<polygon fill="#FFFFFF" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="104.5,57.916 104.5,108.291
|
||||
56.5,108.291 56.5,47.041 95,47.041 "/>
|
||||
<polyline fill="none" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="104.5,60.791 91.5,60.791 91.5,47.041 "/>
|
||||
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="62.125" y1="63.708" x2="86.791" y2="63.708"/>
|
||||
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="62.125" y1="70.207" x2="99.625" y2="70.207"/>
|
||||
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="62.125" y1="76.291" x2="99.625" y2="76.291"/>
|
||||
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="62.125" y1="82.875" x2="99.625" y2="82.875"/>
|
||||
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="61.792" y1="89.041" x2="99.625" y2="89.041"/>
|
||||
<line fill="none" stroke="#2EA7E0" stroke-width="3" stroke-miterlimit="10" x1="61.792" y1="95.791" x2="99.625" y2="95.791"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
8
src/web/tools/img/opr/delete.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<polygon fill="#EC0013" stroke="#C30D23" stroke-miterlimit="10" points="95.338,37.37 88.63,30.662 64,55.292 39.37,30.662
|
||||
32.662,37.37 57.292,62 32.662,86.63 39.369,93.338 64,68.707 88.63,93.338 95.338,86.631 70.707,62 "/>
|
||||
</svg>
|
After Width: | Height: | Size: 702 B |
154
src/web/tools/img/opr/download.svg
Normal file
@ -0,0 +1,154 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<image display="none" overflow="visible" width="347" height="333" xlink:href="data:image/jpeg;base64,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" transform="matrix(0.3272 0 0 0.3272 5.9824 3.0469)">
|
||||
</image>
|
||||
<rect x="19.833" y="28.5" fill="#322912" width="92.667" height="52.5"/>
|
||||
<rect x="19.833" y="81" fill="#DCDDDD" width="92.667" height="10.667"/>
|
||||
<rect x="50.167" y="101.167" fill="#DCDDDD" width="32.5" height="3.666"/>
|
||||
<rect x="59.833" y="91.667" fill="#C9CACA" width="13.5" height="9.5"/>
|
||||
<rect x="23.333" y="31.667" fill="#00A0E9" width="85.833" height="46"/>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="55.591,51.929 66.042,63.104
|
||||
77.577,51.929 "/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" x1="66.042" y1="63.104" x2="66.043" y2="28.5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
15
src/web/tools/img/opr/new_folder.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<rect x="26.2" y="17.403" fill="#F5D370" width="71.08" height="88.553"/>
|
||||
<g>
|
||||
<g>
|
||||
<polygon fill="#FFE79C" points="59.98,72.606 59.98,22.099 26.2,17.403 26.2,105.956 49.486,109.192 49.486,76.212 "/>
|
||||
</g>
|
||||
</g>
|
||||
<polygon fill="#FFFFFF" stroke="#727171" stroke-miterlimit="10" points="109.001,90.244 94.813,90.244 94.813,76.057
|
||||
87.313,76.057 87.313,90.244 73.126,90.244 73.126,97.744 87.313,97.744 87.313,111.932 94.813,111.932 94.813,97.744
|
||||
109.001,97.744 "/>
|
||||
</svg>
|
After Width: | Height: | Size: 943 B |
16
src/web/tools/img/opr/open.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<g id="圖層_2">
|
||||
<g>
|
||||
<polygon fill="#FADA79" points="104.029,38.089 104.029,101.074 22.388,101.074 22.388,27.94 49.702,27.75 55.666,38.345 "/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="圖層_3">
|
||||
<g>
|
||||
<polygon fill="#FFE79E" points="104.328,101.971 22.836,101.971 38.209,52.716 118.807,52.716 "/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 772 B |
102
src/web/tools/img/opr/paste.svg
Normal file
@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<image display="none" overflow="visible" width="256" height="256" xlink:href="data:image/jpeg;base64,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
|
||||
b+Ybb2/mG29v5htvb+Ybb2/mG29v5htj9P8A2MonrshldT23PxBTq4t8lKvkWRJ2fhHFxHs1RL/6
|
||||
4j2aoy0mb0Es5EPSXSP8K5LTE1zmkYappnEPQx2lykU1tVDvoJxl0slSTvXQbjcrkE4f7a/tQf4V
|
||||
WiJrnNIw1TTOIehjtLlIprbL2BTL6CW04Uikn8wakka4Nw/2nOz+lXzs4muc0jDVNM4h6GO0uUim
|
||||
txF8guHiEE404UikmJSlcg3D/ad7P6VfOyia5zSMNU0ziHoY7S5SKa3F9OhbD6CW04UhkYMjI1wq
|
||||
z/ad/wAVfOxia5zSMNU0ziHoY7S5SKa2yDOPCLi1YTH3VqwmFwsa2TrSykOUrv1ISHKuFcP9l3/F
|
||||
XzsImuc0jDVNM4h6GO0uUim+BuiJafQSkfpqOTsMilIysImuc0jDVNM4h6GO0uUim+CiTO8TS5rC
|
||||
JrnNIw1TTOIehjtLlIpunJUZkV+4M5WEvIZysJeQzlYS8hnKwl5DOVhLyGcrCXkM5WEvIZysJeQz
|
||||
lYS8hnKwl5DOVhLyBqSZmZlJdk8umLqlzWETXOaRhqmmcQ9DHaXKRTfBRdUuawia5zSMNU0ziHoY
|
||||
7S5SxdOUlBrPsLqY4dXjqjh1eOqOHV46o4dXjqjh1eOqOHV46o4dXjqjh1eOqOHV46o4dXjqjh1e
|
||||
OqDJTRtkRSynL5F0xdUuawia5zSMNU0ziHoY7S5SxfBRdUuawia5zSMNU0ziGoY7S5SxfBRdUuaw
|
||||
ia5zSMNU0ziHoY7S5SxdOQZmRX5SGeoZ6hnqGeoZ6hnqGeoZ6hnqGeoZ6hlEozlKS70xdUuawia5
|
||||
zSMNU0ziGoY7S5SxfBRdUuawia5zSMNU0ziGoY7S5SxdOUSTUfYQ2KuvcNirr3DYq69w2KuvcNir
|
||||
r3DYq69w2KuvcNirr3DYq69w2KuvcNirr3A5UGiTt6YuqXNYRNc5pGGqaZxDUMdpcpYvgouqXNYR
|
||||
Nc5pGGqaZxD0MdpcpYvgouqXNYRNc5pGELv5KiPAcoZUh1KFoKT7xyENuzvCHEM7whxDO8IcQzvC
|
||||
HEM7whxDO8IcQzvCHEM7wg4RKSssq+g8or3b05BmZfMhnqGeoZ6hnqGeoZ6hnqGeoZ6hnqGeoHIo
|
||||
zl7emLqlzWETXOaR9B/pOKRLfyTkF19fqMbZeExtl4TG2XhMbZeExtl4TG2XhMbZeEwpTijUr9VV
|
||||
07p/BRdUuawia5zSO1nXK6cokmr5ENirr3DYq69w2KuvcNirr3DYq69w2KuvcNirr3DYq69w2Kuv
|
||||
cNirr3DYq69wOVBok7emLqlzWETXOaR2s65XwUXVLmsImuc0jtZ1yvgouqXMdhE1zmkdrOuX8FF1
|
||||
K5rCJrnNI7WdcoXheF4XheF4XheF4XheF4XheF4XheEXUrmsImuc0jtf6TEQ403LLkpUZFKf0HGP
|
||||
esxxj3rMcY96zHGPesxxj3rMcY96zHGPesxxj3rMcY96zHGPesxxj3rMcY96zHGPesxxj3rMcY96
|
||||
zHGPesxxj3rMGRxbpkdwyNZ2ETXOaR/8D//Z" transform="matrix(0.459 0 0 0.459 5.25 4.5)">
|
||||
</image>
|
||||
<path fill="#D0B065" stroke="#B28247" stroke-width="2" stroke-miterlimit="10" d="M81.61,90.593c0,0.923-0.748,1.671-1.671,1.671
|
||||
H24.671c-0.923,0-1.672-0.748-1.672-1.671V20.698c0-0.924,0.749-1.672,1.672-1.672h55.268c0.923,0,1.671,0.749,1.671,1.672V90.593z"
|
||||
/>
|
||||
<rect x="41.492" y="18.085" fill="#DCDDDD" stroke="#9FA0A0" stroke-width="2" stroke-miterlimit="10" width="22.985" height="10.552"/>
|
||||
<polygon fill="#FFFFFF" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="105.714,53.116 105.714,106.334
|
||||
55.004,106.334 55.004,41.627 95.679,41.627 "/>
|
||||
<polyline fill="none" stroke="#9FA0A0" stroke-width="3" stroke-miterlimit="10" points="105.714,56.153 91.98,56.153 91.98,41.627
|
||||
"/>
|
||||
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.947" y1="59.234" x2="87.006" y2="59.234"/>
|
||||
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.947" y1="66.101" x2="100.563" y2="66.101"/>
|
||||
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.947" y1="72.527" x2="100.563" y2="72.527"/>
|
||||
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.947" y1="79.484" x2="100.563" y2="79.484"/>
|
||||
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.595" y1="85.998" x2="100.563" y2="85.998"/>
|
||||
<line fill="none" stroke="#DCDDDD" stroke-width="3" stroke-miterlimit="10" x1="60.595" y1="93.129" x2="100.563" y2="93.129"/>
|
||||
</svg>
|
After Width: | Height: | Size: 8.1 KiB |
11
src/web/tools/img/opr/share.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<circle fill="#24CC29" cx="84.5" cy="42.833" r="13.833"/>
|
||||
<circle fill="#24CC29" cx="36.5" cy="67.334" r="13.833"/>
|
||||
<circle fill="#24CC29" cx="84.5" cy="92.167" r="13.833"/>
|
||||
<line fill="none" stroke="#24CC29" stroke-width="9" stroke-miterlimit="10" x1="36.5" y1="67.334" x2="84.5" y2="42.833"/>
|
||||
<line fill="none" stroke="#24CC29" stroke-width="9" stroke-miterlimit="10" x1="36.5" y1="67.334" x2="84.5" y2="92.166"/>
|
||||
</svg>
|
After Width: | Height: | Size: 894 B |
171
src/web/tools/img/opr/upload.svg
Normal file
@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<g>
|
||||
<circle fill="#86D1EF" cx="37.667" cy="71.896" r="25.083"/>
|
||||
<circle fill="#86D1EF" cx="64.917" cy="58.895" r="36.249"/>
|
||||
<circle fill="#86D1EF" cx="94.999" cy="76.729" r="20.417"/>
|
||||
<path fill="#86D1EF" d="M96.416,95.146c0,1.104-0.777,2-1.738,2H37.156c-0.961,0-1.739-0.896-1.739-2v-31c0-1.106,0.778-2,1.739-2
|
||||
h57.521c0.961,0,1.738,0.895,1.738,2V95.146z"/>
|
||||
</g>
|
||||
<image display="none" overflow="visible" width="512" height="512" xlink:href="data:image/jpeg;base64,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
|
||||
V0ycYLD+6+/cNRw9ran8dujZyQ29LgPfXb+qpferqLCu9mlt8WUQbYt8vD2tqfx26NnJAx2/rXb+
|
||||
qpferuNDuBxIvGzy8Pa2p/Hbo2ckE9D2/rXb+qpfertr/wDkYPdY5eHtbU/jt0bORruggprhacAc
|
||||
NdOb0ixUvPSKK7l33aCOXh7W1P47dGzll4lvuAf0934V0Hi5bvIOaaQaj7TTYHzX67l755eHtbU/
|
||||
jt0bOV8E24Tvc6u+7ZbdaV3qWnCvmpvBdmGOyOm7Xkuz2E+/l4e1tT+O3Rs5ftONDYw7PXbHprsC
|
||||
1DaBh5eHtbU/jt0bOVsRpocwgg3lDjttPFJ9huj0wSbQtqNFuOcaLwscvD2tqfx26NlQ6SiGw7vQ
|
||||
790emIr6aHOHZbfNRw9ran8dujZUNiwzQ9hBBvJkdts2Ht6HXfS7ZVhpZB+bGNRw9ran8dujZU94
|
||||
0wIliIPig9hpa4Ugi6D6VfFJ75sMHS4pz3mlzjST7TUcPa2p/Hbo2VQlJl34XfI4+E9F5Ui16TL3
|
||||
mhrRSSehEt8lliGPjU8Pa2p/Hbo2VbZWbdTDtMiHw+w+xBzTSDZBHpGk2ALZRlZY/hae+4eI/tVc
|
||||
Pa2p/Hbo2cwIUWmJL9F1t5CLAeHtPRbF/wBHl8Rwa1tkk2kZeVJbAtOfdf8A6VfD2tqfx26NnMiJ
|
||||
AeWG6LhvhCHNj7MT6vAf2QcwhzTaIs+jPyO7US5DbZJVDz2IQ+WGLXX08xw9ran8dujZzdMCIQPo
|
||||
NluBBs3CLT9bLIwIfajNJPhJoOAqkWfQ1LiGjpNhEfc+68eFln32kWQB9hhuiy7Ci5xLnG2TZPM8
|
||||
Pa2p/Hbo2c9+KM9o6KSRgKsxA/GaPhQu9Dhm9SF5LMJXkMwleQzCV5DMJXkMwleQzCV5DMJXkMwl
|
||||
eQzCV5DMJXkMwleQzCV5DMJXkMwleQzCV5DMJXkMwleQzCV5DMJXkMwleQzCV5DMJXkMwleQzCV5
|
||||
DMJXkMwleQzCV5DMJXkMwleQzCV5DMJXkMwleQzCV3YcNt8ErzQwfxAH60qmNFc/GJI5zh7W1P47
|
||||
dGz1A4e1tT+O3Rs9QOHtbU/jt0bPUDh7W1P47dGz1A4e1tT+O3Rs9QOHtbU/jt0bPUDh7W1P47dG
|
||||
z1A4e1tT+O3Rs9QOHtbU/jt0bPUDh7W1P47dGz1A4e1tRti2x/l+faHmLd5bvLd5bvLd5bvLd5bv
|
||||
Ld5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvL
|
||||
d5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5bvLd5f/AJX+H/t8zQf+
|
||||
S//Z" transform="matrix(0.2174 0 0 0.2174 8.354 3.207)">
|
||||
</image>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="10" stroke-miterlimit="10" points="78.459,66.957 64.917,52.479
|
||||
49.973,66.957 "/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="10" stroke-miterlimit="10" x1="64.917" y1="52.479" x2="64.917" y2="97.313"/>
|
||||
</svg>
|
After Width: | Height: | Size: 13 KiB |
13
src/web/tools/img/wifi.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="128px" height="128px" viewBox="0 0 128 128" enable-background="new 0 0 128 128" xml:space="preserve">
|
||||
<circle fill="#231815" stroke="#231815" stroke-miterlimit="10" cx="64" cy="98.551" r="8.801"/>
|
||||
<path fill="none" stroke="#231815" stroke-width="14" stroke-miterlimit="10" d="M10.296,49.66
|
||||
c29.139-29.141,76.297-29.141,105.436,0"/>
|
||||
<path fill="none" stroke="#231815" stroke-width="14" stroke-miterlimit="10" d="M28.269,65.352
|
||||
c19.917-19.917,52.152-19.917,72.068,0"/>
|
||||
<path fill="none" stroke="#231815" stroke-width="14" stroke-miterlimit="10" d="M45.548,81.838
|
||||
c10.367-10.367,27.145-10.367,37.509,0"/>
|
||||
</svg>
|
After Width: | Height: | Size: 981 B |
1
src/web/tools/img/zoom-in.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#FFFFFF"><path d="M0 0h24v24H0V0z" fill="none"/><path stroke="#231815" stroke-width="0.4px" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zm.5-7H9v2H7v1h2v2h1v-2h2V9h-2z"/></svg>
|
After Width: | Height: | Size: 462 B |
1
src/web/tools/img/zoom-out.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FFFFFF"><path d="M0 0h24v24H0V0z" fill="none"/><path stroke="#231815" stroke-width="0.4px" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z"/></svg>
|
After Width: | Height: | Size: 442 B |
@ -19,7 +19,6 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@ -104,7 +103,21 @@ func HandleCountryDistrSummary(w http.ResponseWriter, r *http.Request) {
|
||||
/*
|
||||
Up Time Monitor
|
||||
*/
|
||||
//Generate uptime monitor targets from reverse proxy rules
|
||||
|
||||
// Update uptime monitor targets after rules updated
|
||||
// See https://github.com/tobychui/zoraxy/issues/77
|
||||
func UpdateUptimeMonitorTargets() {
|
||||
if uptimeMonitor != nil {
|
||||
uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter)
|
||||
go func() {
|
||||
uptimeMonitor.ExecuteUptimeCheck()
|
||||
}()
|
||||
|
||||
SystemWideLogger.PrintAndLog("Uptime", "Uptime monitor config updated", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate uptime monitor targets from reverse proxy rules
|
||||
func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Target {
|
||||
subds := dp.GetSDProxyEndpointsAsMap()
|
||||
vdirs := dp.GetVDProxyEndpointsAsMap()
|
||||
@ -263,7 +276,7 @@ func HandleWakeOnLan(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[WoL] Sending Wake on LAN magic packet to " + wake)
|
||||
SystemWideLogger.PrintAndLog("WoL", "Sending Wake on LAN magic packet to "+wake, nil)
|
||||
err := wakeonlan.WakeTarget(wake)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
|