22 Commits
2.6.6 ... 2.6.8

Author SHA1 Message Date
20cf290d37 Merge pull request #84 from tobychui/2.6.8
Update v2.6.8
2023-11-25 22:25:22 +08:00
4ca0fcc6d1 Update main.go
Swap to use embedded web fs
2023-11-25 16:38:15 +08:00
ce4ce72820 Added optional TLS bypass mechanism
+ Added opt-out for subdomains for global TLS settings #44
+ Optimized subdomain / vdir editing interface
2023-11-25 15:54:28 +08:00
e363d55899 Fixed #77 and added systemwide logger
+ Added system wide logger (wip)
+ Fixed issue #77 uptime monitor bug
+ Added backend options for bypass global TLS (or allow per rule TLS settings, wip)
+
2023-11-25 12:50:30 +08:00
172479e4fb Added default HTTP/2 Mode
+ Added automatic HTTP/2 switch to TLS mode (Related to #40)
2023-10-20 13:44:47 +08:00
156fa5dace Bug fix
+ Added potential fix for #67
+ Update version number
+ Changed default static web port to 5487 so it is even more unlikely to be used
+
2023-10-20 11:01:28 +08:00
50f222cced Merge pull request #68 from Morethanevil/patch-6
Update CHANGELOG.md
2023-09-26 21:30:23 +08:00
640e1adf96 Update CHANGELOG.md
Updated to latest version
2023-09-26 09:28:57 +02:00
d4bb84180c Merge pull request #66 from tobychui/2.6.7
v2.6.7 Update
2023-09-26 11:24:09 +08:00
bda47fc36b Added default Ca features
+ Added default CA feature
+ Fixed RWD issue in TLS cert table
+ Optimized ACME UI in the TLS page
2023-09-25 20:54:50 +08:00
fd6ba56143 Added web directory manager
+ Added web directory manager
+ Added dummy service expose proxy page
+ Moved ACME and renew to a new section in TLS management page
2023-09-24 23:44:48 +08:00
b63a0fc246 Added static web server and black / whitelist template
- Added static web server
- Added static web server default index
- Added embeded templates to blacklist / whitelist
- Added wip Web Directory Manager

Place the templates at ./www/templates/blacklist.html or whitelist.html to replace the build in embedded template for access control 403 error
-
2023-09-14 16:15:40 +08:00
ed92cccf0e Merge pull request #65 from daluntw/2.6.7
Fix the out of range problem when certificate auto renew
2023-09-13 17:21:29 +08:00
95892802fd use issuer org as failover for json file not exist 2023-09-13 04:28:33 +00:00
8a5004e828 handle buypass issuer not match 2023-09-13 04:27:11 +00:00
c6c523e005 prevent out of range when check issuer exist 2023-09-13 00:39:29 +00:00
a692ec818d Static web server
- Fixed arm64 build bug in Make file
- Added wip static web server
2023-09-12 16:41:52 +08:00
c65f780613 Merge pull request #64 from daluntw/multidomain_fix
Fix multidomain UI handle incorrectly
2023-09-10 17:42:07 +08:00
507c2ab468 Updated 2.6.7 init 2023-09-09 12:28:24 +08:00
1180da8d11 fix multidomain missing logic 2023-09-08 23:18:47 +00:00
83f574e3ab Merge pull request #60 from Morethanevil/main
Update CHANGELOG.md
2023-08-30 22:47:42 +08:00
60837f307d Update CHANGELOG.md 2023-08-30 16:34:43 +02:00
60 changed files with 8679 additions and 163 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View 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()
}

View 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")
}
}

View File

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

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

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

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

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

View 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
View 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
View 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()
}

View File

@ -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", &currentEnabled)
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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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();

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

1057
src/web/tools/fs.html Normal file

File diff suppressed because it is too large Load Diff

View 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

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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 251 KiB

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

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

View 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

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

View 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

View 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

View 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

View File

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