Merge pull request #101 from tobychui/v3.0.0

V3.0.0 Updates
This commit is contained in:
Toby Chui 2024-02-18 20:00:20 +08:00 committed by GitHub
commit 676a45c222
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 3921 additions and 11311 deletions

View File

@ -2,18 +2,25 @@
# Zoraxy # Zoraxy
General purpose request (reverse) proxy and forwarding tool for low power devices. Now written in Go! General purpose request (reverse) proxy and forwarding tool for networking noobs. Now written in Go!
*Zoraxy v3 HTTP proxy config is not compatible with the older v2. If you are looking for the legacy version of Zoraxy, take a look at the [v2 branch](https://github.com/tobychui/zoraxy/tree/v2)*
### Features ### Features
- Simple to use interface with detail in-system instructions - Simple to use interface with detail in-system instructions
- Reverse Proxy - Reverse Proxy
- Subdomain Reverse Proxy - Virtual Directory
- Virtual Directory Reverse Proxy - Basic Auth
- Custom Headers
- Redirection Rules - Redirection Rules
- TLS / SSL setup and deploy - TLS / SSL setup and deploy
- Blacklist by country or IP address (single IP, CIDR or wildcard for beginners) - ACME features like auto-renew to serve your sites in http**s**
- SNI support (one certificate contains multiple host names)
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
- Global Area Network Controller Web UI (ZeroTier not included) - Global Area Network Controller Web UI (ZeroTier not included)
- TCP Tunneling / Proxy
- Integrated Up-time Monitor - Integrated Up-time Monitor
- Web-SSH Terminal - Web-SSH Terminal
- Utilities - Utilities
@ -83,8 +90,6 @@ Usage of zoraxy:
Disable authentication for management interface Disable authentication for management interface
-port string -port string
Management web interface listening port (default ":8000") Management web interface listening port (default ":8000")
-rpt string
Reserved
-sshlb -sshlb
Allow loopback web ssh connection (DANGER) Allow loopback web ssh connection (DANGER)
-version -version
@ -109,45 +114,12 @@ If you already have an upstream reverse proxy server in place with permission ma
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.* *Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
#### Use with ArozOS
The [ArozOS](https://arozos.com) subservice is a built-in, permission-managed, reverse proxy server. To use Zoraxy with ArozOS, connect to your ArozOS host via SSH and use the following command to install Zoraxy:
```bash
# cd into your ArozOS subservice folder. Sometimes it is under ~/arozos/src/subservice.
cd ~/arozos/subservices
mkdir zoraxy
cd ./zoraxy
# Download the release binary from Github release.
wget {binary executable link from release page}
# Set permission. Change this if required.
sudo chmod 775 -R ./
# Start zoraxy to see if the downloaded arch is correct.
./zoraxy
# After unzipping, press Ctrl + C to kill it.
# Rename it to validate the ArozOS subservice binary format.
mv ./zoraxy zoraxy_linux_amd64
# If you are using SBCs with a different CPU arch, use the following names:
# mv ./zoraxy zoraxy_linux_arm
# mv ./zoraxy zoraxy_linux_arm64
# Restart ArozOS
sudo systemctl restart arozos
```
To start the module, go to System Settings > Modules > Subservice and enable it in the menu. You should be able to see a new module named "Zoraxy" pop up in the start menu.
## Screenshots ## Screenshots
![](img/screenshots/0_1.png)
![](img/screenshots/1.png) ![](img/screenshots/1.png)
![](img/screenshots/2.png)
More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)! More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)!
## FAQ ## FAQ
@ -194,5 +166,5 @@ If you like the project and want to support us, please consider a donation. You
## License ## License
This project is open-sourced under AGPL. I open-sourced this project so everyone can check for security issues and benefit all users. **If you plan to use this project in a commercial environment (which violate the AGPL terms), please contact toby@imuslab.com for an alternative commercial license.** This project is open-sourced under AGPL. I open-sourced this project so everyone can check for security issues and benefit all users. **If you plan to use this project in a commercial environment (which violate the AGPL terms), please contact toby@imuslab.com for an alternative license.**

View File

@ -3,8 +3,6 @@ FROM docker.io/golang:alpine
ARG VERSION ARG VERSION
RUN apk add --no-cache bash netcat-openbsd sudo RUN apk add --no-cache bash netcat-openbsd sudo
# Alternatives for security
RUN apk add --no-cache openssl=3.1.4-r1
RUN mkdir -p /opt/zoraxy/source/ &&\ RUN mkdir -p /opt/zoraxy/source/ &&\
mkdir -p /opt/zoraxy/config/ &&\ mkdir -p /opt/zoraxy/config/ &&\

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

View File

@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
strip "github.com/grokify/html-strip-tags-go"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
@ -115,7 +117,7 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
bltype = "country" bltype = "country"
} }
resulst := []string{} resulst := []*geodb.WhitelistEntry{}
if bltype == "country" { if bltype == "country" {
resulst = geodbStore.GetAllWhitelistedCountryCode() resulst = geodbStore.GetAllWhitelistedCountryCode()
} else if bltype == "ip" { } else if bltype == "ip" {
@ -134,7 +136,10 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.AddCountryCodeToWhitelist(countryCode) comment, _ := utils.PostPara(r, "comment")
comment = strip.StripTags(comment)
geodbStore.AddCountryCodeToWhitelist(countryCode, comment)
utils.SendOK(w) utils.SendOK(w)
} }
@ -158,7 +163,10 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
return return
} }
geodbStore.AddIPToWhiteList(ipAddr) comment, _ := utils.PostPara(r, "comment")
comment = strip.StripTags(comment)
geodbStore.AddIPToWhiteList(ipAddr, comment)
} }
func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) { func handleIpWhitelistRemove(w http.ResponseWriter, r *http.Request) {

View File

@ -103,6 +103,9 @@ func AcmeCheckAndHandleRenewCertificate(w http.ResponseWriter, r *http.Request)
utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)") utils.SendErrorResponse(w, "ACME renew only support web server listening on port 80 (http) or 443 (https)")
} }
//Add a 3 second delay to make sure everything is settle down
time.Sleep(3 * time.Second)
// Pass over to the acmeHandler to deal with the communication // Pass over to the acmeHandler to deal with the communication
acmeHandler.HandleRenewCertificate(w, r) acmeHandler.HandleRenewCertificate(w, r)

View File

@ -56,9 +56,16 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect) authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener) authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck) authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
//Reverse proxy root related APIs authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
authRouter.HandleFunc("/api/proxy/root/listOptions", HandleRootRouteOptionList) //Reverse proxy virtual directory APIs
authRouter.HandleFunc("/api/proxy/root/updateOptions", HandleRootRouteOptionsUpdate) authRouter.HandleFunc("/api/proxy/vdir/list", ReverseProxyListVdir)
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
//Reverse proxy user define header apis
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
//Reverse proxy auth related APIs //Reverse proxy auth related APIs
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths) authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths) authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)
@ -115,6 +122,8 @@ func initAPIs() {
authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming) authRouter.HandleFunc("/api/gan/network/name", ganManager.HandleNetworkNaming)
//authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails) //authRouter.HandleFunc("/api/gan/network/detail", ganManager.HandleNetworkDetails)
authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges) authRouter.HandleFunc("/api/gan/network/setRange", ganManager.HandleSetRanges)
authRouter.HandleFunc("/api/gan/network/join", ganManager.HandleServerJoinNetwork)
authRouter.HandleFunc("/api/gan/network/leave", ganManager.HandleServerLeaveNetwork)
authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList) authRouter.HandleFunc("/api/gan/members/list", ganManager.HandleMemberList)
authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP) authRouter.HandleFunc("/api/gan/members/ip", ganManager.HandleMemberIP)
authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming) authRouter.HandleFunc("/api/gan/members/name", ganManager.HandleMemberNaming)
@ -175,7 +184,7 @@ func initAPIs() {
authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus) authRouter.HandleFunc("/api/webserv/status", staticWebServer.HandleGetStatus)
authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer) authRouter.HandleFunc("/api/webserv/start", staticWebServer.HandleStartServer)
authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer) authRouter.HandleFunc("/api/webserv/stop", staticWebServer.HandleStopServer)
authRouter.HandleFunc("/api/webserv/setPort", staticWebServer.HandlePortChange) authRouter.HandleFunc("/api/webserv/setPort", HandleStaticWebServerPortChange)
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing) authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
if *allowWebFileManager { if *allowWebFileManager {
//Web Directory Manager file operation functions //Web Directory Manager file operation functions

View File

@ -51,7 +51,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
results := []*CertInfo{} results := []*CertInfo{}
for _, filename := range filenames { for _, filename := range filenames {
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".crt") certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".pem")
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key") //keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
fileInfo, err := os.Stat(certFilepath) fileInfo, err := os.Stat(certFilepath)
if err != nil { if err != nil {
@ -248,7 +248,7 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
} }
if keytype == "pub" { if keytype == "pub" {
overWriteFilename = domain + ".crt" overWriteFilename = domain + ".pem"
} else if keytype == "pri" { } else if keytype == "pri" {
overWriteFilename = domain + ".key" overWriteFilename = domain + ".key"
} else { } else {
@ -287,6 +287,9 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
return return
} }
//Update cert list
tlsCertManager.UpdateLoadedCertList()
// send response // send response
fmt.Fprintln(w, "File upload successful!") fmt.Fprintln(w, "File upload successful!")
} }

View File

@ -3,6 +3,7 @@ package main
import ( import (
"archive/zip" "archive/zip"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -35,97 +36,118 @@ type Record struct {
BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule
} }
// Save a reverse proxy config record to file /*
func SaveReverseProxyConfigToFile(proxyConfigRecord *Record) error { Load Reverse Proxy Config from file and append it to current runtime proxy router
//TODO: Make this accept new def types */
os.MkdirAll("./conf/proxy/", 0775) func LoadReverseProxyConfig(configFilepath string) error {
filename := getFilenameFromRootName(proxyConfigRecord.Rootname) //Load the config file from disk
endpointConfig, err := os.ReadFile(configFilepath)
//Generate record
thisRecord := proxyConfigRecord
//Write to file
js, _ := json.MarshalIndent(thisRecord, "", " ")
return os.WriteFile(filepath.Join("./conf/proxy/", filename), js, 0775)
}
// Save a running reverse proxy endpoint to file (with automatic endpoint to record conversion)
func SaveReverseProxyEndpointToFile(proxyEndpoint *dynamicproxy.ProxyEndpoint) error {
recordToSave, err := ConvertProxyEndpointToRecord(proxyEndpoint)
if err != nil { if err != nil {
return err return err
} }
return SaveReverseProxyConfigToFile(recordToSave)
}
func RemoveReverseProxyConfigFile(rootname string) error { //Parse it into dynamic proxy endpoint
filename := getFilenameFromRootName(rootname) thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/") err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
SystemWideLogger.Println("Config Removed: ", removePendingFile) if err != nil {
if utils.FileExists(removePendingFile) { return err
err := os.Remove(removePendingFile)
if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Unabel to remove config file", err)
return err
}
} }
//File already gone //Matching domain not set. Assume root
if thisConfigEndpoint.RootOrMatchingDomain == "" {
thisConfigEndpoint.RootOrMatchingDomain = "/"
}
if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Root {
//This is a root config file
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
return err
}
dynamicProxyRouter.SetProxyRouteAsRoot(rootProxyEndpoint)
} else if thisConfigEndpoint.ProxyType == dynamicproxy.ProxyType_Host {
//This is a host config file
readyProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisConfigEndpoint)
if err != nil {
return err
}
dynamicProxyRouter.AddProxyRouteToRuntime(readyProxyEndpoint)
} else {
return errors.New("not supported proxy type")
}
SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+thisConfigEndpoint.Domain+" routing rule loaded", nil)
return nil return nil
} }
// Return ptype, rootname and proxyTarget, error if any func filterProxyConfigFilename(filename string) string {
func LoadReverseProxyConfig(filename string) (*Record, error) { //Filter out wildcard characters
thisRecord := Record{ filename = strings.ReplaceAll(filename, "*", "(ST)")
ProxyType: "", filename = strings.ReplaceAll(filename, "?", "(QM)")
Rootname: "", filename = strings.ReplaceAll(filename, "[", "(OB)")
ProxyTarget: "", filename = strings.ReplaceAll(filename, "]", "(CB)")
UseTLS: false, filename = strings.ReplaceAll(filename, "#", "(HT)")
return filepath.ToSlash(filename)
}
func SaveReverseProxyConfig(endpoint *dynamicproxy.ProxyEndpoint) error {
//Get filename for saving
filename := filepath.Join("./conf/proxy/", endpoint.RootOrMatchingDomain+".config")
if endpoint.ProxyType == dynamicproxy.ProxyType_Root {
filename = "./conf/proxy/root.config"
}
filename = filterProxyConfigFilename(filename)
//Save config to file
js, err := json.MarshalIndent(endpoint, "", " ")
if err != nil {
return err
}
return os.WriteFile(filename, js, 0775)
}
func RemoveReverseProxyConfig(endpoint string) error {
filename := filepath.Join("./conf/proxy/", endpoint+".config")
if endpoint == "/" {
filename = "./conf/proxy/root.config"
}
filename = filterProxyConfigFilename(filename)
if !utils.FileExists(filename) {
return errors.New("target endpoint not exists")
}
return os.Remove(filename)
}
// Get the default root config that point to the internal static web server
// this will be used if root config is not found (new deployment / missing root.config file)
func GetDefaultRootConfig() (*dynamicproxy.ProxyEndpoint, error) {
//Default settings
rootProxyEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyType_Root,
RootOrMatchingDomain: "/",
Domain: "127.0.0.1:" + staticWebServer.GetListeningPort(),
RequireTLS: false,
BypassGlobalTLS: false, BypassGlobalTLS: false,
SkipTlsValidation: false, SkipCertValidations: false,
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
RequireBasicAuth: false, RequireBasicAuth: false,
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{}, BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{}, BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
} DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
DefaultSiteValue: "",
configContent, err := os.ReadFile(filename) })
if err != nil { if err != nil {
return &thisRecord, err return nil, err
} }
//Unmarshal the content into config return rootProxyEndpoint, nil
err = json.Unmarshal(configContent, &thisRecord)
if err != nil {
return &thisRecord, err
}
//Return it
return &thisRecord, nil
}
// Convert a running proxy endpoint object into a save-able record struct
func ConvertProxyEndpointToRecord(targetProxyEndpoint *dynamicproxy.ProxyEndpoint) (*Record, error) {
thisProxyConfigRecord := Record{
ProxyType: targetProxyEndpoint.GetProxyTypeString(),
Rootname: targetProxyEndpoint.RootOrMatchingDomain,
ProxyTarget: targetProxyEndpoint.Domain,
UseTLS: targetProxyEndpoint.RequireTLS,
BypassGlobalTLS: targetProxyEndpoint.BypassGlobalTLS,
SkipTlsValidation: targetProxyEndpoint.SkipCertValidations,
RequireBasicAuth: targetProxyEndpoint.RequireBasicAuth,
BasicAuthCredentials: targetProxyEndpoint.BasicAuthCredentials,
BasicAuthExceptionRules: targetProxyEndpoint.BasicAuthExceptionRules,
}
return &thisProxyConfigRecord, nil
}
func getFilenameFromRootName(rootname string) string {
//Generate a filename for this rootname
filename := strings.ReplaceAll(rootname, ".", "_")
filename = strings.ReplaceAll(filename, "/", "-")
filename = filename + ".config"
return filename
} }
/* /*

View File

@ -10,9 +10,11 @@ require (
github.com/gorilla/sessions v1.2.1 github.com/gorilla/sessions v1.2.1
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/grandcat/zeroconf v1.0.0 github.com/grandcat/zeroconf v1.0.0
github.com/grokify/html-strip-tags-go v0.1.0
github.com/likexian/whois v1.15.1 github.com/likexian/whois v1.15.1
github.com/microcosm-cc/bluemonday v1.0.25 github.com/microcosm-cc/bluemonday v1.0.25
golang.org/x/net v0.14.0 golang.org/x/net v0.14.0
golang.org/x/sys v0.11.0 golang.org/x/sys v0.11.0
golang.org/x/text v0.12.0
golang.org/x/tools v0.12.0 // indirect golang.org/x/tools v0.12.0 // indirect
) )

View File

@ -740,6 +740,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=

View File

@ -13,7 +13,6 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/aroz"
"imuslab.com/zoraxy/mod/auth" "imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection" "imuslab.com/zoraxy/mod/dynamicproxy/redirection"
@ -35,6 +34,7 @@ import (
) )
// General flags // General flags
var webUIPort = flag.String("port", ":8000", "Management web interface listening port")
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface") var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
var showver = flag.Bool("version", false, "Show version of this server") var showver = flag.Bool("version", false, "Show version of this server")
var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)") var allowSshLoopback = flag.Bool("sshlb", false, "Allow loopback web ssh connection (DANGER)")
@ -49,7 +49,7 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
var ( var (
name = "Zoraxy" name = "Zoraxy"
version = "2.6.8" version = "3.0.0"
nodeUUID = "generic" nodeUUID = "generic"
development = false //Set this to false to use embedded web fs development = false //Set this to false to use embedded web fs
bootTime = time.Now().Unix() bootTime = time.Now().Unix()
@ -63,7 +63,6 @@ var (
/* /*
Handler Modules Handler Modules
*/ */
handler *aroz.ArozHandler //Handle arozos managed permission system
sysdb *database.Database //System database sysdb *database.Database //System database
authAgent *auth.AuthAgent //Authentication agent authAgent *auth.AuthAgent //Authentication agent
tlsCertManager *tlscert.Manager //TLS / SSL management tlsCertManager *tlscert.Manager //TLS / SSL management
@ -128,20 +127,8 @@ func ShutdownSeq() {
} }
func main() { func main() {
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information //Parse startup flags
handler = aroz.HandleFlagParse(aroz.ServiceInfo{ flag.Parse()
Name: name,
Desc: "Dynamic Reverse Proxy Server",
Group: "Network",
IconPath: "zoraxy/img/small_icon.png",
Version: version,
StartDir: "zoraxy/index.html",
SupportFW: true,
LaunchFWDir: "zoraxy/index.html",
SupportEmb: false,
InitFWSize: []int{1080, 580},
})
if *showver { if *showver {
fmt.Println(name + " - Version " + version) fmt.Println(name + " - Version " + version)
os.Exit(0) os.Exit(0)
@ -166,7 +153,7 @@ func main() {
startupSequence() startupSequence()
//Initiate management interface APIs //Initiate management interface APIs
requireAuth = !(*noauth || handler.IsUsingExternalPermissionManager()) requireAuth = !(*noauth)
initAPIs() initAPIs()
//Start the reverse proxy server in go routine //Start the reverse proxy server in go routine
@ -179,8 +166,8 @@ func main() {
//Start the finalize sequences //Start the finalize sequences
finalSequence() finalSequence()
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port) SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
err = http.ListenAndServe(handler.Port, nil) err = http.ListenAndServe(*webUIPort, nil)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -163,7 +163,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// Each certificate comes back with the cert bytes, the bytes of the client's // Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL. // private key, and a certificate URL.
err = os.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777) err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return false, err return false, err

View File

@ -1,9 +1,6 @@
package dynamicproxy package dynamicproxy
import ( import (
_ "embed"
"errors"
"log"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
@ -26,11 +23,6 @@ import (
- Vitrual Directory Routing - Vitrual Directory Routing
*/ */
var (
//go:embed tld.json
rawTldMap []byte
)
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/* /*
Special Routing Rules, bypass most of the limitations Special Routing Rules, bypass most of the limitations
@ -53,10 +45,12 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
//Inject headers
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
/* /*
General Access Check General Access Check
*/ */
respWritten := h.handleAccessRouting(w, r) respWritten := h.handleAccessRouting(w, r)
if respWritten { if respWritten {
return return
@ -80,38 +74,45 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
/* /*
Subdomain Routing Host Routing
*/ */
if strings.Contains(r.Host, ".") {
//This might be a subdomain. See if there are any subdomain proxy router for this
sep := h.Parent.getSubdomainProxyEndpointFromHostname(domainOnly)
if sep != nil {
if sep.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
return
}
}
h.subdomainRequest(w, r, sep)
return
}
}
/* sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
Virtual Directory Routing if sep != nil && !sep.Disabled {
*/ if sep.RequireBasicAuth {
//Clean up the request URI err := h.handleBasicAuthRouting(w, r, sep)
proxyingPath := strings.TrimSpace(r.RequestURI)
targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath)
if targetProxyEndpoint != nil {
if targetProxyEndpoint.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, targetProxyEndpoint)
if err != nil { if err != nil {
return return
} }
} }
h.proxyRequest(w, r, targetProxyEndpoint)
} else if !strings.HasSuffix(proxyingPath, "/") { //Check if any virtual directory rules matches
proxyingPath := strings.TrimSpace(r.RequestURI)
targetProxyEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
//Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint)
return
} else if !strings.HasSuffix(proxyingPath, "/") && sep.ProxyType != ProxyType_Root {
potentialProxtEndpoint := sep.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
//Missing tailing slash. Redirect to target proxy endpoint
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
return
}
}
//Fallback to handle by the host proxy forwarder
h.hostRequest(w, r, sep)
return
}
/*
Root Router Handling
*/
//Clean up the request URI
proxyingPath := strings.TrimSpace(r.RequestURI)
if !strings.HasSuffix(proxyingPath, "/") {
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/") potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
if potentialProxtEndpoint != nil { if potentialProxtEndpoint != nil {
//Missing tailing slash. Redirect to target proxy endpoint //Missing tailing slash. Redirect to target proxy endpoint
@ -136,52 +137,63 @@ Once entered this routing segment, the root routing options will take over
for the routing logic. for the routing logic.
*/ */
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) { func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
domainOnly := r.Host domainOnly := r.Host
if strings.Contains(r.Host, ":") { if strings.Contains(r.Host, ":") {
hostPath := strings.Split(r.Host, ":") hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0] domainOnly = hostPath[0]
} }
if h.Parent.RootRoutingOptions.EnableRedirectForUnsetRules { //Get the proxy root config
//Route to custom domain proot := h.Parent.Root
if h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget == "" { switch proot.DefaultSiteOption {
//Not set. Redirect to first level of domain redirectable case DefaultSite_InternalStaticWebServer:
fld, err := h.getTopLevelRedirectableDomain(domainOnly) fallthrough
if err != nil { case DefaultSite_ReverseProxy:
//Redirect to proxy root //They both share the same behavior
h.proxyRequest(w, r, h.Parent.Root)
} else { //Check if any virtual directory rules matches
log.Println("[Router] Redirecting request from " + domainOnly + " to " + fld) proxyingPath := strings.TrimSpace(r.RequestURI)
h.logRequest(r, false, 307, "root-redirect", domainOnly) targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
http.Redirect(w, r, fld, http.StatusTemporaryRedirect) if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
} //Virtual directory routing rule found. Route via vdir mode
h.vdirRequest(w, r, targetProxyEndpoint)
return return
} else if h.isTopLevelRedirectableDomain(domainOnly) { } else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root {
//This is requesting a top level private domain that should be serving root potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
h.proxyRequest(w, r, h.Parent.Root) if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
} else { //Missing tailing slash. Redirect to target proxy endpoint
//Validate the redirection target URL http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
parsedURL, err := url.Parse(h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget)
if err != nil {
//Error when parsing target. Send to root
h.proxyRequest(w, r, h.Parent.Root)
return return
} }
hostname := parsedURL.Hostname()
if domainOnly != hostname {
//Redirect to target
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget, http.StatusTemporaryRedirect)
return
} else {
//Loopback request due to bad settings (Shd leave it empty)
//Forward it to root proxy
h.proxyRequest(w, r, h.Parent.Root)
}
} }
} else {
//Route to root //No vdir match. Route via root router
h.proxyRequest(w, r, h.Parent.Root) h.hostRequest(w, r, h.Parent.Root)
case DefaultSite_Redirect:
redirectTarget := strings.TrimSpace(proot.DefaultSiteValue)
if redirectTarget == "" {
redirectTarget = "about:blank"
}
//Check if it is an infinite loopback redirect
parsedURL, err := url.Parse(proot.DefaultSiteValue)
if err != nil {
//Error when parsing target. Send to root
h.hostRequest(w, r, h.Parent.Root)
return
}
hostname := parsedURL.Hostname()
if hostname == domainOnly {
h.logRequest(r, false, 500, "root-redirect", domainOnly)
http.Error(w, "Loopback redirects due to invalid settings", 500)
return
}
h.logRequest(r, false, 307, "root-redirect", domainOnly)
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
case DefaultSite_NotFoundPage:
http.NotFound(w, r)
} }
} }
@ -219,44 +231,3 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
return false return false
} }
// Return if the given host is already topped (e.g. example.com or example.co.uk) instead of
// a host with subdomain (e.g. test.example.com)
func (h *ProxyHandler) isTopLevelRedirectableDomain(requestHost string) bool {
parts := strings.Split(requestHost, ".")
if len(parts) > 2 {
//Cases where strange tld is used like .co.uk or .com.hk
_, ok := h.Parent.tldMap[strings.Join(parts[1:], ".")]
if ok {
//Already topped
return true
}
} else {
//Already topped
return true
}
return false
}
// GetTopLevelRedirectableDomain returns the toppest level of domain
// that is redirectable. E.g. a.b.c.example.co.uk will return example.co.uk
func (h *ProxyHandler) getTopLevelRedirectableDomain(unsetSubdomainHost string) (string, error) {
parts := strings.Split(unsetSubdomainHost, ".")
if h.isTopLevelRedirectableDomain(unsetSubdomainHost) {
//Already topped
return "", errors.New("already at top level domain")
}
for i := 0; i < len(parts); i++ {
possibleTld := parts[i:]
_, ok := h.Parent.tldMap[strings.Join(possibleTld, ".")]
if ok {
//This is tld length
tld := strings.Join(parts[i-1:], ".")
return "//" + tld, nil
}
}
return "", errors.New("unsupported top level domain given")
}

View File

@ -26,10 +26,6 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
} }
} }
proxyType := "vdir-auth"
if pe.ProxyType == ProxyType_Subdomain {
proxyType = "subd-auth"
}
u, p, ok := r.BasicAuth() u, p, ok := r.BasicAuth()
if !ok { if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
@ -48,7 +44,7 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
} }
if !matchingFound { if !matchingFound {
h.logRequest(r, false, 401, proxyType, pe.Domain) h.logRequest(r, false, 401, "host", pe.Domain)
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401) w.WriteHeader(401)
return errors.New("unauthorized") return errors.New("unauthorized")

View File

@ -60,6 +60,7 @@ type ResponseRewriteRuleSet struct {
ProxyDomain string ProxyDomain string
OriginalHost string OriginalHost string
UseTLS bool UseTLS bool
NoCache bool
PathPrefix string //Vdir prefix for root, / will be rewrite to this PathPrefix string //Vdir prefix for root, / will be rewrite to this
} }
@ -243,7 +244,7 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) {
} }
} }
func removeHeaders(header http.Header) { func removeHeaders(header http.Header, noCache bool) {
// Remove hop-by-hop headers listed in the "Connection" header. // Remove hop-by-hop headers listed in the "Connection" header.
if c := header.Get("Connection"); c != "" { if c := header.Get("Connection"); c != "" {
for _, f := range strings.Split(c, ",") { for _, f := range strings.Split(c, ",") {
@ -260,9 +261,16 @@ func removeHeaders(header http.Header) {
} }
} }
if header.Get("A-Upgrade") != "" { //Restore the Upgrade header if any
header.Set("Upgrade", header.Get("A-Upgrade")) if header.Get("Zr-Origin-Upgrade") != "" {
header.Del("A-Upgrade") header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
header.Del("Zr-Origin-Upgrade")
}
//Disable cache if nocache is set
if noCache {
header.Del("Cache-Control")
header.Set("Cache-Control", "no-store")
} }
} }
@ -281,6 +289,11 @@ func addXForwardedForHeader(req *http.Request) {
req.Header.Set("X-Forwarded-Proto", "http") req.Header.Set("X-Forwarded-Proto", "http")
} }
if req.Header.Get("X-Real-Ip") == "" {
//Not exists. Fill it in with client IP
req.Header.Set("X-Real-Ip", clientIP)
}
} }
} }
@ -323,7 +336,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
copyHeader(outreq.Header, req.Header) copyHeader(outreq.Header, req.Header)
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers. // Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
removeHeaders(outreq.Header) removeHeaders(outreq.Header, rrr.NoCache)
// Add X-Forwarded-For Header. // Add X-Forwarded-For Header.
addXForwardedForHeader(outreq) addXForwardedForHeader(outreq)
@ -339,7 +352,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
} }
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers. // Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
removeHeaders(res.Header) removeHeaders(res.Header, rrr.NoCache)
if p.ModifyResponse != nil { if p.ModifyResponse != nil {
if err := p.ModifyResponse(res); err != nil { if err := p.ModifyResponse(res); err != nil {

View File

@ -22,27 +22,19 @@ import (
func NewDynamicProxy(option RouterOption) (*Router, error) { func NewDynamicProxy(option RouterOption) (*Router, error) {
proxyMap := sync.Map{} proxyMap := sync.Map{}
domainMap := sync.Map{}
thisRouter := Router{ thisRouter := Router{
Option: &option, Option: &option,
ProxyEndpoints: &proxyMap, ProxyEndpoints: &proxyMap,
SubdomainEndpoint: &domainMap, Running: false,
Running: false, server: nil,
server: nil, routingRules: []*RoutingRule{},
routingRules: []*RoutingRule{}, tldMap: map[string]int{},
tldMap: map[string]int{},
} }
thisRouter.mux = &ProxyHandler{ thisRouter.mux = &ProxyHandler{
Parent: &thisRouter, Parent: &thisRouter,
} }
//Prase the tld map for tld redirection in main router
//See Server.go declarations
if len(rawTldMap) > 0 {
json.Unmarshal(rawTldMap, &thisRouter.tldMap)
}
return &thisRouter, nil return &thisRouter, nil
} }
@ -76,21 +68,14 @@ func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
func (router *Router) StartProxyService() error { func (router *Router) StartProxyService() error {
//Create a new server object //Create a new server object
if router.server != nil { if router.server != nil {
return errors.New("Reverse proxy server already running") return errors.New("reverse proxy server already running")
} }
//Check if root route is set //Check if root route is set
if router.Root == nil { if router.Root == nil {
return errors.New("Reverse proxy router root not set") return errors.New("reverse proxy router root not set")
} }
//Load root options from file
loadedRootOption, err := loadRootRoutingOptionsFromFile()
if err != nil {
return err
}
router.RootRoutingOptions = loadedRootOption
minVersion := tls.VersionTLS10 minVersion := tls.VersionTLS10
if router.Option.ForceTLSLatest { if router.Option.ForceTLSLatest {
minVersion = tls.VersionTLS12 minVersion = tls.VersionTLS12
@ -101,16 +86,6 @@ func (router *Router) StartProxyService() error {
} }
if router.Option.UseTls { 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
}
router.tlsListener = ln
*/
router.server = &http.Server{ router.server = &http.Server{
Addr: ":" + strconv.Itoa(router.Option.Port), Addr: ":" + strconv.Itoa(router.Option.Port),
Handler: router.mux, Handler: router.mux,
@ -129,7 +104,7 @@ func (router *Router) StartProxyService() error {
hostPath := strings.Split(r.Host, ":") hostPath := strings.Split(r.Host, ":")
domainOnly = hostPath[0] domainOnly = hostPath[0]
} }
sep := router.getSubdomainProxyEndpointFromHostname(domainOnly) sep := router.getProxyEndpointFromHostname(domainOnly)
if sep != nil && sep.BypassGlobalTLS { if sep != nil && sep.BypassGlobalTLS {
//Allow routing via non-TLS handler //Allow routing via non-TLS handler
originalHostHeader := r.Host originalHostHeader := r.Host
@ -140,7 +115,7 @@ func (router *Router) StartProxyService() error {
r.URL, _ = url.Parse(originalHostHeader) r.URL, _ = url.Parse(originalHostHeader)
} }
sep.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: sep.Domain, ProxyDomain: sep.Domain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
UseTLS: sep.RequireTLS, UseTLS: sep.RequireTLS,
@ -225,7 +200,7 @@ func (router *Router) StartProxyService() error {
func (router *Router) StopProxyService() error { func (router *Router) StopProxyService() error {
if router.server == nil { if router.server == nil {
return errors.New("Reverse proxy server already stopped") return errors.New("reverse proxy server already stopped")
} }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
@ -253,13 +228,13 @@ func (router *Router) StopProxyService() error {
// Restart the current router if it is running. // Restart the current router if it is running.
func (router *Router) Restart() error { func (router *Router) Restart() error {
//Stop the router if it is already running //Stop the router if it is already running
var err error = nil
if router.Running { if router.Running {
err := router.StopProxyService() err := router.StopProxyService()
if err != nil { if err != nil {
return err return err
} }
time.Sleep(300 * time.Millisecond)
// Start the server // Start the server
err = router.StartProxyService() err = router.StartProxyService()
if err != nil { if err != nil {
@ -267,7 +242,7 @@ func (router *Router) Restart() error {
} }
} }
return err return nil
} }
/* /*
@ -280,128 +255,17 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
hostname = r.Host hostname = r.Host
} }
hostname = strings.Split(hostname, ":")[0] hostname = strings.Split(hostname, ":")[0]
subdEndpoint := router.getSubdomainProxyEndpointFromHostname(hostname) subdEndpoint := router.getProxyEndpointFromHostname(hostname)
return subdEndpoint != nil return subdEndpoint != nil
} }
/*
Add an URL into a custom proxy services
*/
func (router *Router) AddVirtualDirectoryProxyService(options *VdirOptions) error {
domain := options.Domain
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
/*
if rootname[len(rootname)-1:] == "/" {
rootname = rootname[:len(rootname)-1]
}
*/
webProxyEndpoint := domain
if options.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
}
//Create a new proxy agent for this root
path, err := url.Parse(webProxyEndpoint)
if err != nil {
return err
}
proxy := dpcore.NewDynamicProxyCore(path, options.RootName, options.SkipCertValidations)
endpointObject := ProxyEndpoint{
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: options.RootName,
Domain: domain,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
Proxy: proxy,
}
router.ProxyEndpoints.Store(options.RootName, &endpointObject)
log.Println("Registered Proxy Rule: ", options.RootName+" to "+domain)
return nil
}
/* /*
Load routing from RP Load routing from RP
*/ */
func (router *Router) LoadProxy(ptype string, key string) (*ProxyEndpoint, error) { func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
if ptype == "vdir" { var targetProxyEndpoint *ProxyEndpoint
proxy, ok := router.ProxyEndpoints.Load(key) router.ProxyEndpoints.Range(func(key, value interface{}) bool {
if !ok { key, ok := key.(string)
return nil, errors.New("target proxy not found")
}
targetProxy := proxy.(*ProxyEndpoint)
targetProxy.parent = router
return targetProxy, nil
} else if ptype == "subd" {
proxy, ok := router.SubdomainEndpoint.Load(key)
if !ok {
return nil, errors.New("target proxy not found")
}
targetProxy := proxy.(*ProxyEndpoint)
targetProxy.parent = router
return targetProxy, nil
}
return nil, errors.New("unsupported ptype")
}
/*
Add an default router for the proxy server
*/
func (router *Router) SetRootProxy(options *RootOptions) error {
proxyLocation := options.ProxyLocation
if proxyLocation[len(proxyLocation)-1:] == "/" {
proxyLocation = proxyLocation[:len(proxyLocation)-1]
}
webProxyEndpoint := proxyLocation
if options.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
}
//Create a new proxy agent for this root
path, err := url.Parse(webProxyEndpoint)
if err != nil {
return err
}
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
rootEndpoint := ProxyEndpoint{
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: "/",
Domain: proxyLocation,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
Proxy: proxy,
}
router.Root = &rootEndpoint
return nil
}
// Helpers to export the syncmap for easier processing
func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
m := make(map[string]*ProxyEndpoint)
r.SubdomainEndpoint.Range(func(key, value interface{}) bool {
k, ok := key.(string)
if !ok { if !ok {
return true return true
} }
@ -409,13 +273,32 @@ func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
if !ok { if !ok {
return true return true
} }
m[k] = v
if key == matchingDomain {
targetProxyEndpoint = v
}
return true return true
}) })
return m
if targetProxyEndpoint == nil {
return nil, errors.New("target routing rule not found")
}
return targetProxyEndpoint, nil
} }
func (r *Router) GetVDProxyEndpointsAsMap() map[string]*ProxyEndpoint { // Deep copy a proxy endpoint, excluding runtime paramters
func CopyEndpoint(endpoint *ProxyEndpoint) *ProxyEndpoint {
js, _ := json.Marshal(endpoint)
newProxyEndpoint := ProxyEndpoint{}
err := json.Unmarshal(js, &newProxyEndpoint)
if err != nil {
return nil
}
return &newProxyEndpoint
}
func (r *Router) GetProxyEndpointsAsMap() map[string]*ProxyEndpoint {
m := make(map[string]*ProxyEndpoint) m := make(map[string]*ProxyEndpoint)
r.ProxyEndpoints.Range(func(key, value interface{}) bool { r.ProxyEndpoints.Range(func(key, value interface{}) bool {
k, ok := key.(string) k, ok := key.(string)

View File

@ -0,0 +1,158 @@
package dynamicproxy
import (
"encoding/json"
"errors"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
/*
endpoint.go
author: tobychui
This script handle the proxy endpoint object actions
so proxyEndpoint can be handled like a proper oop object
Most of the functions are implemented in dynamicproxy.go
*/
/*
User Defined Header Functions
*/
// Check if a user define header exists in this endpoint, ignore case
func (ep *ProxyEndpoint) UserDefinedHeaderExists(key string) bool {
for _, header := range ep.UserDefinedHeaders {
if strings.EqualFold(header.Key, key) {
return true
}
}
return false
}
// Remvoe a user defined header from the list
func (ep *ProxyEndpoint) RemoveUserDefinedHeader(key string) error {
newHeaderList := []*UserDefinedHeader{}
for _, header := range ep.UserDefinedHeaders {
if !strings.EqualFold(header.Key, key) {
newHeaderList = append(newHeaderList, header)
}
}
ep.UserDefinedHeaders = newHeaderList
return nil
}
// Add a user defined header to the list, duplicates will be automatically removed
func (ep *ProxyEndpoint) AddUserDefinedHeader(key string, value string) error {
if ep.UserDefinedHeaderExists(key) {
ep.RemoveUserDefinedHeader(key)
}
ep.UserDefinedHeaders = append(ep.UserDefinedHeaders, &UserDefinedHeader{
Key: cases.Title(language.Und, cases.NoLower).String(key), //e.g. x-proxy-by -> X-Proxy-By
Value: value,
})
return nil
}
/*
Virtual Directory Functions
*/
// Get virtual directory handler from given URI
func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI string) *VirtualDirectoryEndpoint {
for _, vdir := range ep.VirtualDirectories {
if strings.HasPrefix(requestURI, vdir.MatchingPath) {
return vdir
}
}
return nil
}
// Get virtual directory handler by matching path (exact match required)
func (ep *ProxyEndpoint) GetVirtualDirectoryRuleByMatchingPath(matchingPath string) *VirtualDirectoryEndpoint {
for _, vdir := range ep.VirtualDirectories {
if vdir.MatchingPath == matchingPath {
return vdir
}
}
return nil
}
// Delete a vdir rule by its matching path
func (ep *ProxyEndpoint) RemoveVirtualDirectoryRuleByMatchingPath(matchingPath string) error {
entryFound := false
newVirtualDirectoryList := []*VirtualDirectoryEndpoint{}
for _, vdir := range ep.VirtualDirectories {
if vdir.MatchingPath == matchingPath {
entryFound = true
} else {
newVirtualDirectoryList = append(newVirtualDirectoryList, vdir)
}
}
if entryFound {
//Update the list of vdirs
ep.VirtualDirectories = newVirtualDirectoryList
return nil
}
return errors.New("target virtual directory routing rule not found")
}
// Delete a vdir rule by its matching path
func (ep *ProxyEndpoint) AddVirtualDirectoryRule(vdir *VirtualDirectoryEndpoint) (*ProxyEndpoint, error) {
//Check for matching path duplicate
if ep.GetVirtualDirectoryRuleByMatchingPath(vdir.MatchingPath) != nil {
return nil, errors.New("rule with same matching path already exists")
}
//Append it to the list of virtual directory
ep.VirtualDirectories = append(ep.VirtualDirectories, vdir)
//Prepare to replace the current routing rule
parentRouter := ep.parent
readyRoutingRule, err := parentRouter.PrepareProxyRoute(ep)
if err != nil {
return nil, err
}
if ep.ProxyType == ProxyType_Root {
parentRouter.Root = readyRoutingRule
} else if ep.ProxyType == ProxyType_Host {
ep.Remove()
parentRouter.AddProxyRouteToRuntime(readyRoutingRule)
} else {
return nil, errors.New("unsupported proxy type")
}
return readyRoutingRule, nil
}
// Create a deep clone object of the proxy endpoint
// Note the returned object is not activated. Call to prepare function before pushing into runtime
func (ep *ProxyEndpoint) Clone() *ProxyEndpoint {
clonedProxyEndpoint := ProxyEndpoint{}
js, _ := json.Marshal(ep)
json.Unmarshal(js, &clonedProxyEndpoint)
return &clonedProxyEndpoint
}
// Remove this proxy endpoint from running proxy endpoint list
func (ep *ProxyEndpoint) Remove() error {
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
return nil
}
// Write changes to runtime without respawning the proxy handler
// use prepare -> remove -> add if you change anything in the endpoint
// that effects the proxy routing src / dest
func (ep *ProxyEndpoint) UpdateToRuntime() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
}

View File

@ -1,68 +0,0 @@
package dynamicproxy
import "errors"
/*
ProxyEndpoint.go
author: tobychui
This script handle the proxy endpoint object actions
so proxyEndpoint can be handled like a proper oop object
Most of the functions are implemented in dynamicproxy.go
*/
//Get the string version of proxy type
func (ep *ProxyEndpoint) GetProxyTypeString() string {
if ep.ProxyType == ProxyType_Subdomain {
return "subd"
} else if ep.ProxyType == ProxyType_Vdir {
return "vdir"
}
return "unknown"
}
//Update change in the current running proxy endpoint config
func (ep *ProxyEndpoint) UpdateToRuntime() {
if ep.IsVdir() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
} else if ep.IsSubDomain() {
ep.parent.SubdomainEndpoint.Store(ep.RootOrMatchingDomain, ep)
}
}
//Return true if the endpoint type is virtual directory
func (ep *ProxyEndpoint) IsVdir() bool {
return ep.ProxyType == ProxyType_Vdir
}
//Return true if the endpoint type is subdomain
func (ep *ProxyEndpoint) IsSubDomain() bool {
return ep.ProxyType == ProxyType_Subdomain
}
//Remove this proxy endpoint from running proxy endpoint list
func (ep *ProxyEndpoint) Remove() error {
//fmt.Println(ptype, key)
if ep.IsVdir() {
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
return nil
} else if ep.IsSubDomain() {
ep.parent.SubdomainEndpoint.Delete(ep.RootOrMatchingDomain)
return nil
}
return errors.New("invalid or unsupported type")
}
//ProxyEndpoint remove provide global access by key
func (router *Router) RemoveProxyEndpointByRootname(proxyType string, rootnameOrMatchingDomain string) error {
targetEpt, err := router.LoadProxy(proxyType, rootnameOrMatchingDomain)
if err != nil {
return err
}
return targetEpt.Remove()
}

View File

@ -6,6 +6,8 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"path/filepath"
"sort"
"strings" "strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
@ -28,13 +30,41 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
return targetProxyEndpoint return targetProxyEndpoint
} }
func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *ProxyEndpoint { func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
var targetSubdomainEndpoint *ProxyEndpoint = nil var targetSubdomainEndpoint *ProxyEndpoint = nil
ep, ok := router.SubdomainEndpoint.Load(hostname) ep, ok := router.ProxyEndpoints.Load(hostname)
if ok { if ok {
targetSubdomainEndpoint = ep.(*ProxyEndpoint) targetSubdomainEndpoint = ep.(*ProxyEndpoint)
} }
//No hit. Try with wildcard
matchProxyEndpoints := []*ProxyEndpoint{}
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
ep := v.(*ProxyEndpoint)
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
if err != nil {
//Continue
return true
}
if match {
//targetSubdomainEndpoint = ep
matchProxyEndpoints = append(matchProxyEndpoints, ep)
return true
}
return true
})
if len(matchProxyEndpoints) == 1 {
//Only 1 match
return matchProxyEndpoints[0]
} else if len(matchProxyEndpoints) > 1 {
//More than one match. Get the best match one
sort.Slice(matchProxyEndpoints, func(i, j int) bool {
return matchProxyEndpoints[i].RootOrMatchingDomain < matchProxyEndpoints[j].RootOrMatchingDomain
})
return matchProxyEndpoints[0]
}
return targetSubdomainEndpoint return targetSubdomainEndpoint
} }
@ -54,14 +84,22 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
return rewrittenURL return rewrittenURL
} }
// Handle subdomain request // Handle host request
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) { func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
r.Header.Set("X-Forwarded-Host", r.Host) r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
//Inject custom headers
if len(target.UserDefinedHeaders) > 0 {
for _, customHeader := range target.UserDefinedHeaders {
r.Header.Set(customHeader.Key, customHeader.Value)
}
}
requestURL := r.URL.String() requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket") r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" { if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
//Append / to the end of the redirection endpoint if not exists //Append / to the end of the redirection endpoint if not exists
@ -89,10 +127,11 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
r.URL, _ = url.Parse(originalHostHeader) r.URL, _ = url.Parse(originalHostHeader)
} }
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain, ProxyDomain: target.Domain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS, UseTLS: target.RequireTLS,
NoCache: h.Parent.Option.NoCache,
PathPrefix: "", PathPrefix: "",
}) })
@ -113,15 +152,23 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
} }
// Handle vdir type request // Handle vdir type request
func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) { func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, target *VirtualDirectoryEndpoint) {
rewriteURL := h.Parent.rewriteURL(target.RootOrMatchingDomain, r.RequestURI) rewriteURL := h.Parent.rewriteURL(target.MatchingPath, r.RequestURI)
r.URL, _ = url.Parse(rewriteURL) r.URL, _ = url.Parse(rewriteURL)
r.Header.Set("X-Forwarded-Host", r.Host) r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
//Inject custom headers
if len(target.parent.UserDefinedHeaders) > 0 {
for _, customHeader := range target.parent.UserDefinedHeaders {
r.Header.Set(customHeader.Key, customHeader.Value)
}
}
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket") r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" { if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
wsRedirectionEndpoint = wsRedirectionEndpoint + "/" wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
@ -144,11 +191,11 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
r.URL, _ = url.Parse(originalHostHeader) r.URL, _ = url.Parse(originalHostHeader)
} }
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{ err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain, ProxyDomain: target.Domain,
OriginalHost: originalHostHeader, OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS, UseTLS: target.RequireTLS,
PathPrefix: target.RootOrMatchingDomain, PathPrefix: target.MatchingPath,
}) })
var dnsError *net.DNSError var dnsError *net.DNSError

View File

@ -1,51 +0,0 @@
package dynamicproxy
import (
"encoding/json"
"errors"
"log"
"os"
"imuslab.com/zoraxy/mod/utils"
)
/*
rootRoute.go
This script handle special case in routing where the root proxy
entity is involved. This also include its setting object
RootRoutingOptions
*/
var rootConfigFilepath string = "conf/root_config.json"
func loadRootRoutingOptionsFromFile() (*RootRoutingOptions, error) {
if !utils.FileExists(rootConfigFilepath) {
//Not found. Create a root option
js, _ := json.MarshalIndent(RootRoutingOptions{}, "", " ")
err := os.WriteFile(rootConfigFilepath, js, 0775)
if err != nil {
return nil, errors.New("Unable to write root config to file: " + err.Error())
}
}
newRootOption := RootRoutingOptions{}
rootOptionsBytes, err := os.ReadFile(rootConfigFilepath)
if err != nil {
log.Println("[Error] Unable to read root config file at " + rootConfigFilepath + ": " + err.Error())
return nil, err
}
err = json.Unmarshal(rootOptionsBytes, &newRootOption)
if err != nil {
log.Println("[Error] Unable to parse root config file: " + err.Error())
return nil, err
}
return &newRootOption, nil
}
// Save the new config to file. Note that this will not overwrite the runtime one
func (opt *RootRoutingOptions) SaveToFile() error {
js, _ := json.MarshalIndent(opt, "", " ")
err := os.WriteFile(rootConfigFilepath, js, 0775)
return err
}

View File

@ -0,0 +1,110 @@
package dynamicproxy
import (
"errors"
"net/url"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
)
/*
Dynamic Proxy Router Functions
This script handle the proxy rules router spawning
and preparation
*/
// Prepare proxy route generate a proxy handler service object for your endpoint
func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint, error) {
//Filter the tailing slash if any
domain := endpoint.Domain
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
endpoint.Domain = domain
//Parse the web proxy endpoint
webProxyEndpoint := domain
if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
//TLS is not hardcoded in proxy target domain
if endpoint.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
}
}
//Create a new proxy agent for this root
path, err := url.Parse(webProxyEndpoint)
if err != nil {
return nil, err
}
//Create the proxy routing handler
proxy := dpcore.NewDynamicProxyCore(path, "", endpoint.SkipCertValidations)
endpoint.proxy = proxy
endpoint.parent = router
//Prepare proxy routing hjandler for each of the virtual directories
for _, vdir := range endpoint.VirtualDirectories {
domain := vdir.Domain
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
//Parse the web proxy endpoint
webProxyEndpoint = domain
if !strings.HasPrefix("http://", domain) && !strings.HasPrefix("https://", domain) {
//TLS is not hardcoded in proxy target domain
if vdir.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
}
}
path, err := url.Parse(webProxyEndpoint)
if err != nil {
return nil, err
}
proxy := dpcore.NewDynamicProxyCore(path, vdir.MatchingPath, vdir.SkipCertValidations)
vdir.proxy = proxy
vdir.parent = endpoint
}
return endpoint, nil
}
// Add Proxy Route to current runtime. Call to PrepareProxyRoute before adding to runtime
func (router *Router) AddProxyRouteToRuntime(endpoint *ProxyEndpoint) error {
if endpoint.proxy == nil {
//This endpoint is not prepared
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
}
// Push record into running subdomain endpoints
router.ProxyEndpoints.Store(endpoint.RootOrMatchingDomain, endpoint)
return nil
}
// Set given Proxy Route as Root. Call to PrepareProxyRoute before adding to runtime
func (router *Router) SetProxyRouteAsRoot(endpoint *ProxyEndpoint) error {
if endpoint.proxy == nil {
//This endpoint is not prepared
return errors.New("proxy endpoint not ready. Use PrepareProxyRoute before adding to runtime")
}
// Push record into running root endpoints
router.Root = endpoint
return nil
}
// ProxyEndpoint remove provide global access by key
func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain string) error {
targetEpt, err := router.LoadProxy(rootnameOrMatchingDomain)
if err != nil {
return err
}
return targetEpt.Remove()
}

View File

@ -1,50 +0,0 @@
package dynamicproxy
import (
"log"
"net/url"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
)
/*
Add an URL intoa custom subdomain service
*/
func (router *Router) AddSubdomainRoutingService(options *SubdOptions) error {
domain := options.Domain
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
webProxyEndpoint := domain
if options.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
}
//Create a new proxy agent for this root
path, err := url.Parse(webProxyEndpoint)
if err != nil {
return err
}
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
router.SubdomainEndpoint.Store(options.MatchingDomain, &ProxyEndpoint{
RootOrMatchingDomain: options.MatchingDomain,
Domain: domain,
RequireTLS: options.RequireTLS,
Proxy: proxy,
BypassGlobalTLS: options.BypassGlobalTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
BasicAuthExceptionRules: options.BasicAuthExceptionRules,
})
log.Println("Adding Subdomain Rule: ", options.MatchingDomain+" to "+domain)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -14,8 +14,9 @@ import (
) )
const ( const (
ProxyType_Subdomain = 0 ProxyType_Root = 0
ProxyType_Vdir = 1 ProxyType_Host = 1
ProxyType_Vdir = 2
) )
type ProxyHandler struct { type ProxyHandler struct {
@ -24,9 +25,11 @@ type ProxyHandler struct {
type RouterOption struct { type RouterOption struct {
HostUUID string //The UUID of Zoraxy, use for heading mod HostUUID string //The UUID of Zoraxy, use for heading mod
HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above ForceTLSLatest bool //Force TLS1.2 or above
NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager TlsManager *tlscert.Manager
@ -37,16 +40,14 @@ type RouterOption struct {
} }
type Router struct { type Router struct {
Option *RouterOption Option *RouterOption
ProxyEndpoints *sync.Map ProxyEndpoints *sync.Map
SubdomainEndpoint *sync.Map Running bool
Running bool Root *ProxyEndpoint
Root *ProxyEndpoint mux http.Handler
RootRoutingOptions *RootRoutingOptions server *http.Server
mux http.Handler tlsListener net.Listener
server *http.Server routingRules []*RoutingRule
tlsListener net.Listener
routingRules []*RoutingRule
tlsRedirectStop chan bool //Stop channel for tls redirection server tlsRedirectStop chan bool //Stop channel for tls redirection server
tldMap map[string]int //Top level domain map, see tld.json tldMap map[string]int //Top level domain map, see tld.json
@ -69,63 +70,70 @@ type BasicAuthExceptionRule struct {
PathPrefix string PathPrefix string
} }
// A proxy endpoint record // User defined headers to add into a proxy endpoint
type ProxyEndpoint struct { type UserDefinedHeader struct {
ProxyType int //The type of this proxy, see const def Key string
RootOrMatchingDomain string //Root for vdir or Matching domain for subd, also act as key Value string
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
SkipCertValidations bool //Set to true to accept self signed certs
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials `json:"-"` //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
Proxy *dpcore.ReverseProxy `json:"-"`
parent *Router
} }
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
// program structure than directly using ProxyEndpoint
type VirtualDirectoryEndpoint struct {
MatchingPath string //Matching prefix of the request path, also act as key
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
Disabled bool //If the rule is enabled
proxy *dpcore.ReverseProxy `json:"-"`
parent *ProxyEndpoint `json:"-"`
}
// A proxy endpoint record, a general interface for handling inbound routing
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
Domain string //Domain or IP to proxy to
//TLS/SSL Related
RequireTLS bool //Target domain require TLS
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
SkipCertValidations bool //Set to true to accept self signed certs
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
//Authentication
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
//Fallback routing logic
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
Disabled bool //If the rule is disabled
//Internal Logic Elements
parent *Router
proxy *dpcore.ReverseProxy `json:"-"`
}
/*
Routing type specific interface
These are options that only avaible for a specific interface
when running, these are converted into "ProxyEndpoint" objects
for more generic routing logic
*/
// Root options are those that are required for reverse proxy handler to work // Root options are those that are required for reverse proxy handler to work
type RootOptions struct { const (
ProxyLocation string //Proxy Root target, all unset traffic will be forward to here DefaultSite_InternalStaticWebServer = 0
RequireTLS bool //Proxy root target require TLS connection (not recommended) DefaultSite_ReverseProxy = 1
BypassGlobalTLS bool //Bypass global TLS setting and make root http only (not recommended) DefaultSite_Redirect = 2
SkipCertValidations bool //Skip cert validation, suitable for self-signed certs, CURRENTLY NOT USED DefaultSite_NotFoundPage = 3
)
//Basic Auth Related
RequireBasicAuth bool //Require basic auth, CURRENTLY NOT USED
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
// Additional options are here for letting router knows how to route exception cases for root
type RootRoutingOptions struct {
//Root only configs
EnableRedirectForUnsetRules bool //Force unset rules to redirect to custom domain
UnsetRuleRedirectTarget string //Custom domain to redirect to for unset rules
}
type VdirOptions struct {
RootName string
Domain string
RequireTLS bool
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
type SubdOptions struct {
MatchingDomain string
Domain string
RequireTLS bool
BypassGlobalTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
BasicAuthExceptionRules []*BasicAuthExceptionRule
}
/* /*
Web Templates Web Templates

View File

@ -207,7 +207,7 @@ func (m *NetworkManager) HandleSetRanges(w http.ResponseWriter, r *http.Request)
utils.SendOK(w) utils.SendOK(w)
} }
//Handle listing of network members. Set details=true for listing all details // Handle listing of network members. Set details=true for listing all details
func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) { func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request) {
netid, err := utils.GetPara(r, "netid") netid, err := utils.GetPara(r, "netid")
if err != nil { if err != nil {
@ -241,7 +241,7 @@ func (m *NetworkManager) HandleMemberList(w http.ResponseWriter, r *http.Request
} }
} }
//Handle Authorization of members // Handle Authorization of members
func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) { func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid") netid, err := utils.PostPara(r, "netid")
if err != nil { if err != nil {
@ -281,7 +281,7 @@ func (m *NetworkManager) HandleMemberAuthorization(w http.ResponseWriter, r *htt
} }
} }
//Handle Delete or Add IP for a member in a network // Handle Delete or Add IP for a member in a network
func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request) { func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid") netid, err := utils.PostPara(r, "netid")
if err != nil { if err != nil {
@ -356,7 +356,7 @@ func (m *NetworkManager) HandleMemberIP(w http.ResponseWriter, r *http.Request)
} }
} }
//Handle naming for members // Handle naming for members
func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Request) { func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid") netid, err := utils.PostPara(r, "netid")
if err != nil { if err != nil {
@ -391,7 +391,7 @@ func (m *NetworkManager) HandleMemberNaming(w http.ResponseWriter, r *http.Reque
} }
} }
//Handle delete of a given memver // Handle delete of a given memver
func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) { func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid") netid, err := utils.PostPara(r, "netid")
if err != nil { if err != nil {
@ -426,3 +426,79 @@ func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Reque
utils.SendOK(w) utils.SendOK(w)
} }
// Check if a given network id is a network hosted on this zoraxy node
func (m *NetworkManager) IsLocalGAN(networkId string) bool {
networks, err := m.listNetworkIds()
if err != nil {
return false
}
for _, network := range networks {
if network == networkId {
return true
}
}
return false
}
// Handle server instant joining a given network
func (m *NetworkManager) HandleServerJoinNetwork(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
//Check if the target network is a network hosted on this server
if !m.IsLocalGAN(netid) {
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
return
}
if m.memberExistsInNetwork(netid, m.ControllerID) {
utils.SendErrorResponse(w, "controller already inside network")
return
}
//Join the network
err = m.joinNetwork(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
// Handle server instant leaving a given network
func (m *NetworkManager) HandleServerLeaveNetwork(w http.ResponseWriter, r *http.Request) {
netid, err := utils.PostPara(r, "netid")
if err != nil {
utils.SendErrorResponse(w, "net id not set")
return
}
//Check if the target network is a network hosted on this server
if !m.IsLocalGAN(netid) {
utils.SendErrorResponse(w, "given network is not a GAN hosted on this node")
return
}
//Leave the network
err = m.leaveNetwork(netid)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Remove it from target network if it is authorized
err = m.deleteMember(netid, m.ControllerID)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}

View File

@ -117,7 +117,7 @@ type MemberInfo struct {
VRev int `json:"vRev"` VRev int `json:"vRev"`
} }
//Get the zerotier node info from local service // Get the zerotier node info from local service
func getControllerInfo(token string, apiPort int) (*NodeInfo, error) { func getControllerInfo(token string, apiPort int) (*NodeInfo, error) {
url := "http://localhost:" + strconv.Itoa(apiPort) + "/status" url := "http://localhost:" + strconv.Itoa(apiPort) + "/status"
@ -187,7 +187,7 @@ func (m *NetworkManager) createNetwork() (*NetworkInfo, error) {
return &networkInfo, nil return &networkInfo, nil
} }
//List network details // List network details
func (m *NetworkManager) getNetworkInfoById(networkId string) (*NetworkInfo, error) { func (m *NetworkManager) getNetworkInfoById(networkId string) (*NetworkInfo, error) {
req, err := http.NewRequest("GET", os.ExpandEnv("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+networkId+"/"), nil) req, err := http.NewRequest("GET", os.ExpandEnv("http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+networkId+"/"), nil)
if err != nil { if err != nil {
@ -249,7 +249,7 @@ func (m *NetworkManager) setNetworkInfoByID(networkId string, newNetworkInfo *Ne
return nil return nil
} }
//List network IDs // List network IDs
func (m *NetworkManager) listNetworkIds() ([]string, error) { func (m *NetworkManager) listNetworkIds() ([]string, error) {
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil) req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil)
if err != nil { if err != nil {
@ -281,7 +281,7 @@ func (m *NetworkManager) listNetworkIds() ([]string, error) {
return networkIds, nil return networkIds, nil
} }
//wrapper for checking if a network id exists // wrapper for checking if a network id exists
func (m *NetworkManager) networkExists(networkId string) bool { func (m *NetworkManager) networkExists(networkId string) bool {
networkIds, err := m.listNetworkIds() networkIds, err := m.listNetworkIds()
if err != nil { if err != nil {
@ -297,7 +297,7 @@ func (m *NetworkManager) networkExists(networkId string) bool {
return false return false
} }
//delete a network // delete a network
func (m *NetworkManager) deleteNetwork(networkID string) error { func (m *NetworkManager) deleteNetwork(networkID string) error {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/" url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
client := &http.Client{} client := &http.Client{}
@ -330,8 +330,8 @@ func (m *NetworkManager) deleteNetwork(networkID string) error {
return nil return nil
} }
//Configure network // Configure network
//Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24") // Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
func (m *NetworkManager) configureNetwork(networkID string, ipRangeStart string, ipRangeEnd string, routeTarget string) error { func (m *NetworkManager) configureNetwork(networkID string, ipRangeStart string, ipRangeEnd string, routeTarget string) error {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/" url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
data := map[string]interface{}{ data := map[string]interface{}{
@ -545,7 +545,7 @@ func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool
return false return false
} }
//Get a network memeber info by netid and memberid // Get a network memeber info by netid and memberid
func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*MemberInfo, error) { func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*MemberInfo, error) {
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil) req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil)
if err != nil { if err != nil {
@ -573,7 +573,7 @@ func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*M
return thisMemeberInfo, nil return thisMemeberInfo, nil
} }
//Set the authorization state of a member // Set the authorization state of a member
func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error { func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAuthorized bool) error {
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid
payload := []byte(`{"authorized": true}`) payload := []byte(`{"authorized": true}`)
@ -600,7 +600,7 @@ func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAutho
return nil return nil
} }
//Delete a member from the network // Delete a member from the network
func (m *NetworkManager) deleteMember(netid string, memid string) error { func (m *NetworkManager) deleteMember(netid string, memid string) error {
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil) req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memid, nil)
if err != nil { if err != nil {
@ -620,3 +620,45 @@ func (m *NetworkManager) deleteMember(netid string, memid string) error {
return nil return nil
} }
// Make the host to join a given network
func (m *NetworkManager) joinNetwork(netid string) error {
req, err := http.NewRequest("POST", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
if err != nil {
return err
}
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}
// Make the host to leave a given network
func (m *NetworkManager) leaveNetwork(netid string) error {
req, err := http.NewRequest("DELETE", "http://localhost:"+strconv.Itoa(m.apiPort)+"/network/"+netid, nil)
if err != nil {
return err
}
req.Header.Set("X-Zt1-Auth", os.ExpandEnv(m.authToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("network error. Status code: " + strconv.Itoa(resp.StatusCode))
}
return nil
}

View File

@ -1,6 +1,9 @@
package geodb package geodb
import "strings" import (
"encoding/json"
"strings"
)
/* /*
Whitelist.go Whitelist.go
@ -8,11 +11,29 @@ import "strings"
This script handles whitelist related functions This script handles whitelist related functions
*/ */
const (
EntryType_CountryCode int = 0
EntryType_IP int = 1
)
type WhitelistEntry struct {
EntryType int //Entry type of whitelist, Country Code or IP
CC string //ISO Country Code
IP string //IP address or range
Comment string //Comment for this entry
}
//Geo Whitelist //Geo Whitelist
func (s *Store) AddCountryCodeToWhitelist(countryCode string) { func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode) countryCode = strings.ToLower(countryCode)
s.sysdb.Write("whitelist-cn", countryCode, true) entry := WhitelistEntry{
EntryType: EntryType_CountryCode,
CC: countryCode,
Comment: comment,
}
s.sysdb.Write("whitelist-cn", countryCode, entry)
} }
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) { func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
@ -22,20 +43,19 @@ func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool { func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode) countryCode = strings.ToLower(countryCode)
var isWhitelisted bool = false return s.sysdb.KeyExists("whitelist-cn", countryCode)
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
return isWhitelisted
} }
func (s *Store) GetAllWhitelistedCountryCode() []string { func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry {
whitelistedCountryCode := []string{} whitelistedCountryCode := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-cn") entries, err := s.sysdb.ListTable("whitelist-cn")
if err != nil { if err != nil {
return whitelistedCountryCode return whitelistedCountryCode
} }
for _, keypairs := range entries { for _, keypairs := range entries {
ip := string(keypairs[0]) thisWhitelistEntry := WhitelistEntry{}
whitelistedCountryCode = append(whitelistedCountryCode, ip) json.Unmarshal(keypairs[1], &thisWhitelistEntry)
whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry)
} }
return whitelistedCountryCode return whitelistedCountryCode
@ -43,8 +63,14 @@ func (s *Store) GetAllWhitelistedCountryCode() []string {
//IP Whitelist //IP Whitelist
func (s *Store) AddIPToWhiteList(ipAddr string) { func (s *Store) AddIPToWhiteList(ipAddr string, comment string) {
s.sysdb.Write("whitelist-ip", ipAddr, true) thisIpEntry := WhitelistEntry{
EntryType: EntryType_IP,
IP: ipAddr,
Comment: comment,
}
s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry)
} }
func (s *Store) RemoveIPFromWhiteList(ipAddr string) { func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
@ -52,14 +78,14 @@ func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
} }
func (s *Store) IsIPWhitelisted(ipAddr string) bool { func (s *Store) IsIPWhitelisted(ipAddr string) bool {
var isWhitelisted bool = false isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr)
s.sysdb.Read("whitelist-ip", ipAddr, &isWhitelisted)
if isWhitelisted { if isWhitelisted {
//single IP whitelist entry
return true return true
} }
//Check for IP wildcard and CIRD rules //Check for IP wildcard and CIRD rules
AllWhitelistedIps := s.GetAllWhitelistedIp() AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice()
for _, whitelistRules := range AllWhitelistedIps { for _, whitelistRules := range AllWhitelistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules) wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
if wildcardMatch { if wildcardMatch {
@ -75,17 +101,29 @@ func (s *Store) IsIPWhitelisted(ipAddr string) bool {
return false return false
} }
func (s *Store) GetAllWhitelistedIp() []string { func (s *Store) GetAllWhitelistedIp() []*WhitelistEntry {
whitelistedIp := []string{} whitelistedIp := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-ip") entries, err := s.sysdb.ListTable("whitelist-ip")
if err != nil { if err != nil {
return whitelistedIp return whitelistedIp
} }
for _, keypairs := range entries { for _, keypairs := range entries {
ip := string(keypairs[0]) //ip := string(keypairs[0])
whitelistedIp = append(whitelistedIp, ip) thisEntry := WhitelistEntry{}
json.Unmarshal(keypairs[1], &thisEntry)
whitelistedIp = append(whitelistedIp, &thisEntry)
} }
return whitelistedIp return whitelistedIp
} }
func (s *Store) GetAllWhitelistedIpAsStringSlice() []string {
allWhitelistedIPs := []string{}
entries := s.GetAllWhitelistedIp()
for _, entry := range entries {
allWhitelistedIPs = append(allWhitelistedIPs, entry.IP)
}
return allWhitelistedIPs
}

View File

@ -211,9 +211,9 @@ func removeHeaders(header http.Header) {
} }
} }
if header.Get("A-Upgrade") != "" { if header.Get("Zr-Origin-Upgrade") != "" {
header.Set("Upgrade", header.Get("A-Upgrade")) header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
header.Del("A-Upgrade") header.Del("Zr-Origin-Upgrade")
} }
} }

View File

@ -82,7 +82,7 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
requestURL := r.URL.String() requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket") r.Header.Set("Zr-Origin-Upgrade", "websocket")
requestURL = strings.TrimPrefix(requestURL, "/") requestURL = strings.TrimPrefix(requestURL, "/")
u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL) u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL)
wspHandler := websocketproxy.NewProxy(u, false) wspHandler := websocketproxy.NewProxy(u, false)

View File

@ -5,22 +5,22 @@ import (
"strings" "strings"
) )
//This remove the certificates in the list where either the // This remove the certificates in the list where either the
//public key or the private key is missing // public key or the private key is missing
func getCertPairs(certFiles []string) []string { func getCertPairs(certFiles []string) []string {
crtMap := make(map[string]bool) pemMap := make(map[string]bool)
keyMap := make(map[string]bool) keyMap := make(map[string]bool)
for _, filename := range certFiles { for _, filename := range certFiles {
if filepath.Ext(filename) == ".crt" { if filepath.Ext(filename) == ".pem" {
crtMap[strings.TrimSuffix(filename, ".crt")] = true pemMap[strings.TrimSuffix(filename, ".pem")] = true
} else if filepath.Ext(filename) == ".key" { } else if filepath.Ext(filename) == ".key" {
keyMap[strings.TrimSuffix(filename, ".key")] = true keyMap[strings.TrimSuffix(filename, ".key")] = true
} }
} }
var result []string var result []string
for domain := range crtMap { for domain := range pemMap {
if keyMap[domain] { if keyMap[domain] {
result = append(result, domain) result = append(result, domain)
} }
@ -29,7 +29,7 @@ func getCertPairs(certFiles []string) []string {
return result return result
} }
//Get the cloest subdomain certificate from a list of domains // Get the cloest subdomain certificate from a list of domains
func matchClosestDomainCertificate(subdomain string, domains []string) string { func matchClosestDomainCertificate(subdomain string, domains []string) string {
var matchingDomain string = "" var matchingDomain string = ""
maxLength := 0 maxLength := 0
@ -43,18 +43,3 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
return matchingDomain return matchingDomain
} }
//Check if a requesting domain is a subdomain of a given domain
func isSubdomain(subdomain, domain string) bool {
subdomainParts := strings.Split(subdomain, ".")
domainParts := strings.Split(domain, ".")
if len(subdomainParts) < len(domainParts) {
return false
}
for i := range domainParts {
if subdomainParts[len(subdomainParts)-1-i] != domainParts[len(domainParts)-1-i] {
return false
}
}
return true
}

View File

@ -6,7 +6,6 @@ import (
"embed" "embed"
"encoding/pem" "encoding/pem"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@ -15,12 +14,19 @@ import (
"imuslab.com/zoraxy/mod/utils" "imuslab.com/zoraxy/mod/utils"
) )
type Manager struct { type CertCache struct {
CertStore string Cert *x509.Certificate
verbal bool PubKey string
PriKey string
} }
//go:embed localhost.crt localhost.key type Manager struct {
CertStore string //Path where all the certs are stored
LoadedCerts []*CertCache //A list of loaded certs
verbal bool
}
//go:embed localhost.pem localhost.key
var buildinCertStore embed.FS var buildinCertStore embed.FS
func NewManager(certStore string, verbal bool) (*Manager, error) { func NewManager(certStore string, verbal bool) (*Manager, error) {
@ -28,14 +34,99 @@ func NewManager(certStore string, verbal bool) (*Manager, error) {
os.MkdirAll(certStore, 0775) os.MkdirAll(certStore, 0775)
} }
pubKey := "./tmp/localhost.pem"
priKey := "./tmp/localhost.key"
//Check if this is initial setup
if !utils.FileExists(pubKey) {
buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey))
os.WriteFile(pubKey, buildInPubKey, 0775)
}
if !utils.FileExists(priKey) {
buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
os.WriteFile(priKey, buildInPriKey, 0775)
}
thisManager := Manager{ thisManager := Manager{
CertStore: certStore, CertStore: certStore,
verbal: verbal, LoadedCerts: []*CertCache{},
verbal: verbal,
}
err := thisManager.UpdateLoadedCertList()
if err != nil {
return nil, err
} }
return &thisManager, nil return &thisManager, nil
} }
// Update domain mapping from file
func (m *Manager) UpdateLoadedCertList() error {
//Get a list of certificates from file
domainList, err := m.ListCertDomains()
if err != nil {
return err
}
//Load each of the certificates into memory
certList := []*CertCache{}
for _, certname := range domainList {
//Read their certificate into memory
pubKey := filepath.Join(m.CertStore, certname+".pem")
priKey := filepath.Join(m.CertStore, certname+".key")
certificate, err := tls.LoadX509KeyPair(pubKey, priKey)
if err != nil {
log.Println("Certificate loaded failed: " + certname)
continue
}
for _, thisCert := range certificate.Certificate {
loadedCert, err := x509.ParseCertificate(thisCert)
if err != nil {
//Error pasring cert, skip this byte segment
continue
}
thisCacheEntry := CertCache{
Cert: loadedCert,
PubKey: pubKey,
PriKey: priKey,
}
certList = append(certList, &thisCacheEntry)
}
}
//Replace runtime cert array
m.LoadedCerts = certList
return nil
}
// Match cert by CN
func (m *Manager) CertMatchExists(serverName string) bool {
for _, certCacheEntry := range m.LoadedCerts {
if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
return true
}
}
return false
}
// Get cert entry by matching server name, return pubKey and priKey if found
// check with CertMatchExists before calling to the load function
func (m *Manager) GetCertByX509CNHostname(serverName string) (string, string) {
for _, certCacheEntry := range m.LoadedCerts {
if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
return certCacheEntry.PubKey, certCacheEntry.PriKey
}
}
return "", ""
}
// Return a list of domains by filename
func (m *Manager) ListCertDomains() ([]string, error) { func (m *Manager) ListCertDomains() ([]string, error) {
filenames, err := m.ListCerts() filenames, err := m.ListCerts()
if err != nil { if err != nil {
@ -48,8 +139,9 @@ func (m *Manager) ListCertDomains() ([]string, error) {
return filenames, nil return filenames, nil
} }
// Return a list of cert files (public and private keys)
func (m *Manager) ListCerts() ([]string, error) { func (m *Manager) ListCerts() ([]string, error) {
certs, err := ioutil.ReadDir(m.CertStore) certs, err := os.ReadDir(m.CertStore)
if err != nil { if err != nil {
return []string{}, err return []string{}, err
} }
@ -64,44 +156,52 @@ func (m *Manager) ListCerts() ([]string, error) {
return filenames, nil return filenames, nil
} }
// Get a certificate from disk where its certificate matches with the helloinfo
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) { func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
//Check if the domain corrisponding cert exists //Check if the domain corrisponding cert exists
pubKey := "./tmp/localhost.crt" pubKey := "./tmp/localhost.pem"
priKey := "./tmp/localhost.key" priKey := "./tmp/localhost.key"
//Check if this is initial setup if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
if !utils.FileExists(pubKey) { //Direct hit
buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey)) pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".pem")
os.WriteFile(pubKey, buildInPubKey, 0775)
}
if !utils.FileExists(priKey) {
buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
os.WriteFile(priKey, buildInPriKey, 0775)
}
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".crt")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".crt")
priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key") priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
} else if m.CertMatchExists(helloInfo.ServerName) {
//Use x509
pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName)
} else { } else {
domainCerts, _ := m.ListCertDomains() //Fallback to legacy method of matching certificates
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts) /*
if cloestDomainCert != "" { domainCerts, _ := m.ListCertDomains()
//There is a matching parent domain for this subdomain. Use this instead. cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".crt") if cloestDomainCert != "" {
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key") //There is a matching parent domain for this subdomain. Use this instead.
} else if m.DefaultCertExists() { pubKey = filepath.Join(m.CertStore, cloestDomainCert+".pem")
//Use default.crt and default.key priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
pubKey = filepath.Join(m.CertStore, "default.crt") } else if m.DefaultCertExists() {
//Use default.pem and default.key
pubKey = filepath.Join(m.CertStore, "default.pem")
priKey = filepath.Join(m.CertStore, "default.key")
if m.verbal {
log.Println("No matching certificate found. Serving with default")
}
} else {
if m.verbal {
log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
}
}*/
if m.DefaultCertExists() {
//Use default.pem and default.key
pubKey = filepath.Join(m.CertStore, "default.pem")
priKey = filepath.Join(m.CertStore, "default.key") priKey = filepath.Join(m.CertStore, "default.key")
if m.verbal { //if m.verbal {
log.Println("No matching certificate found. Serving with default") // log.Println("No matching certificate found. Serving with default")
} //}
} else { } else {
if m.verbal { //if m.verbal {
log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName) // log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
} //}
} }
} }
@ -117,17 +217,17 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
// Check if both the default cert public key and private key exists // Check if both the default cert public key and private key exists
func (m *Manager) DefaultCertExists() bool { func (m *Manager) DefaultCertExists() bool {
return utils.FileExists(filepath.Join(m.CertStore, "default.crt")) && utils.FileExists(filepath.Join(m.CertStore, "default.key")) return utils.FileExists(filepath.Join(m.CertStore, "default.pem")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
} }
// Check if the default cert exists returning seperate results for pubkey and prikey // Check if the default cert exists returning seperate results for pubkey and prikey
func (m *Manager) DefaultCertExistsSep() (bool, bool) { func (m *Manager) DefaultCertExistsSep() (bool, bool) {
return utils.FileExists(filepath.Join(m.CertStore, "default.crt")), utils.FileExists(filepath.Join(m.CertStore, "default.key")) return utils.FileExists(filepath.Join(m.CertStore, "default.pem")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
} }
// Delete the cert if exists // Delete the cert if exists
func (m *Manager) RemoveCert(domain string) error { func (m *Manager) RemoveCert(domain string) error {
pubKey := filepath.Join(m.CertStore, domain+".crt") pubKey := filepath.Join(m.CertStore, domain+".pem")
priKey := filepath.Join(m.CertStore, domain+".key") priKey := filepath.Join(m.CertStore, domain+".key")
if utils.FileExists(pubKey) { if utils.FileExists(pubKey) {
err := os.Remove(pubKey) err := os.Remove(pubKey)
@ -143,6 +243,9 @@ func (m *Manager) RemoveCert(domain string) error {
} }
} }
//Update the cert list
m.UpdateLoadedCertList()
return nil return nil
} }
@ -171,15 +274,11 @@ func IsValidTLSFile(file io.Reader) bool {
return false return false
} }
// Check if the certificate is a valid TLS/SSL certificate // Check if the certificate is a valid TLS/SSL certificate
return cert.IsCA == false && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 return !cert.IsCA && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0
} else if strings.Contains(block.Type, "PRIVATE KEY") { } else if strings.Contains(block.Type, "PRIVATE KEY") {
// The file contains a private key // The file contains a private key
_, err := x509.ParsePKCS1PrivateKey(block.Bytes) _, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil { return err == nil
// Handle the error
return false
}
return true
} else { } else {
return false return false
} }

View File

@ -217,7 +217,11 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
} }
func getWebsiteStatus(url string) (int, error) { func getWebsiteStatus(url string) (int, error) {
resp, err := http.Get(url) client := http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get(url)
if err != nil { if err != nil {
//Try replace the http with https and vise versa //Try replace the http with https and vise versa
rewriteURL := "" rewriteURL := ""
@ -227,7 +231,7 @@ func getWebsiteStatus(url string) (int, error) {
rewriteURL = strings.ReplaceAll(url, "http://", "https://") rewriteURL = strings.ReplaceAll(url, "http://", "https://")
} }
resp, err = http.Get(rewriteURL) resp, err = client.Get(rewriteURL)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") { if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
//Invalid downstream reverse proxy settings, but it is online //Invalid downstream reverse proxy settings, but it is online

View File

@ -72,6 +72,7 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return
} }
utils.SendOK(w) utils.SendOK(w)
} }

View File

@ -89,7 +89,7 @@ func (ws *WebServer) RestorePreviousState() {
ws.option.EnableDirectoryListing = enableDirList ws.option.EnableDirectoryListing = enableDirList
//Check the running state //Check the running state
webservRunning := false webservRunning := true
ws.option.Sysdb.Read("webserv", "enabled", &webservRunning) ws.option.Sysdb.Read("webserv", "enabled", &webservRunning)
if webservRunning { if webservRunning {
ws.Start() ws.Start()
@ -124,6 +124,11 @@ func (ws *WebServer) ChangePort(port string) error {
return nil return nil
} }
// Get current using port in options
func (ws *WebServer) GetListeningPort() string {
return ws.option.Port
}
// Start starts the web server. // Start starts the web server.
func (ws *WebServer) Start() error { func (ws *WebServer) Start() error {
ws.mu.Lock() ws.mu.Lock()

View File

@ -21,6 +21,9 @@ var (
// Add user customizable reverse proxy // Add user customizable reverse proxy
func ReverseProxtInit() { func ReverseProxtInit() {
/*
Load Reverse Proxy Global Settings
*/
inboundPort := 80 inboundPort := 80
if sysdb.KeyExists("settings", "inbound") { if sysdb.KeyExists("settings", "inbound") {
sysdb.Read("settings", "inbound", &inboundPort) sysdb.Read("settings", "inbound", &inboundPort)
@ -45,6 +48,14 @@ func ReverseProxtInit() {
SystemWideLogger.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")
} }
developmentMode := false
sysdb.Read("settings", "devMode", &developmentMode)
if useTls {
SystemWideLogger.Println("Development mode enabled. Using no-store Cache Control policy")
} else {
SystemWideLogger.Println("Development mode disabled. Proxying with default Cache Control policy")
}
listenOnPort80 := false listenOnPort80 := false
sysdb.Read("settings", "listenP80", &listenOnPort80) sysdb.Read("settings", "listenP80", &listenOnPort80)
if listenOnPort80 { if listenOnPort80 {
@ -63,11 +74,19 @@ func ReverseProxtInit() {
SystemWideLogger.Println("Force HTTPS mode disabled") SystemWideLogger.Println("Force HTTPS mode disabled")
} }
/*
Create a new proxy object
The DynamicProxy is the parent of all reverse proxy handlers,
use for managemening and provide functions to access proxy handlers
*/
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{ dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
HostUUID: nodeUUID, HostUUID: nodeUUID,
HostVersion: version,
Port: inboundPort, Port: inboundPort,
UseTls: useTls, UseTls: useTls,
ForceTLSLatest: forceLatestTLSVersion, ForceTLSLatest: forceLatestTLSVersion,
NoCache: developmentMode,
ListenOnPort80: listenOnPort80, ListenOnPort80: listenOnPort80,
ForceHttpsRedirect: forceHttpsRedirect, ForceHttpsRedirect: forceHttpsRedirect,
TlsManager: tlsCertManager, TlsManager: tlsCertManager,
@ -83,45 +102,28 @@ func ReverseProxtInit() {
dynamicProxyRouter = dprouter dynamicProxyRouter = dprouter
//Load all conf from files /*
Load all conf from files
*/
confs, _ := filepath.Glob("./conf/proxy/*.config") confs, _ := filepath.Glob("./conf/proxy/*.config")
for _, conf := range confs { for _, conf := range confs {
record, err := LoadReverseProxyConfig(conf) err := LoadReverseProxyConfig(conf)
if err != nil { if err != nil {
SystemWideLogger.PrintAndLog("Proxy", "Failed to load config file: "+filepath.Base(conf), err) SystemWideLogger.PrintAndLog("Proxy", "Failed to load config file: "+filepath.Base(conf), err)
return return
} }
}
if record.ProxyType == "root" { if dynamicProxyRouter.Root == nil {
dynamicProxyRouter.SetRootProxy(&dynamicproxy.RootOptions{ //Root config not set (new deployment?), use internal static web server as root
ProxyLocation: record.ProxyTarget, defaultRootRouter, err := GetDefaultRootConfig()
RequireTLS: record.UseTLS, if err != nil {
}) SystemWideLogger.PrintAndLog("Proxy", "Failed to generate default root routing", err)
} else if record.ProxyType == "subd" { return
dynamicProxyRouter.AddSubdomainRoutingService(&dynamicproxy.SubdOptions{
MatchingDomain: record.Rootname,
Domain: record.ProxyTarget,
RequireTLS: record.UseTLS,
BypassGlobalTLS: record.BypassGlobalTLS,
SkipCertValidations: record.SkipTlsValidation,
RequireBasicAuth: record.RequireBasicAuth,
BasicAuthCredentials: record.BasicAuthCredentials,
BasicAuthExceptionRules: record.BasicAuthExceptionRules,
})
} else if record.ProxyType == "vdir" {
dynamicProxyRouter.AddVirtualDirectoryProxyService(&dynamicproxy.VdirOptions{
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 {
SystemWideLogger.PrintAndLog("Proxy", "Unsupported endpoint type: "+record.ProxyType+". Skipping "+filepath.Base(conf), nil)
} }
dynamicProxyRouter.SetProxyRouteAsRoot(defaultRootRouter)
} }
//Start Service //Start Service
@ -173,7 +175,7 @@ func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) {
} }
func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd eptype, err := utils.PostPara(r, "type") //Support root and host
if err != nil { if err != nil {
utils.SendErrorResponse(w, "type not defined") utils.SendErrorResponse(w, "type not defined")
return return
@ -241,96 +243,112 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
} }
} }
rootname := "" var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
if eptype == "vdir" { if eptype == "host" {
vdir, err := utils.PostPara(r, "rootname") rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
if err != nil {
utils.SendErrorResponse(w, "vdir not defined")
return
}
//Vdir must start with /
if !strings.HasPrefix(vdir, "/") {
vdir = "/" + vdir
}
rootname = vdir
thisOption := dynamicproxy.VdirOptions{
RootName: vdir,
Domain: endpoint,
RequireTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
}
dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)
} else if eptype == "subd" {
subdomain, err := utils.PostPara(r, "rootname")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "subdomain not defined") utils.SendErrorResponse(w, "subdomain not defined")
return return
} }
rootname = subdomain thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
thisOption := dynamicproxy.SubdOptions{ //I/O
MatchingDomain: subdomain, ProxyType: dynamicproxy.ProxyType_Host,
RootOrMatchingDomain: rootOrMatchingDomain,
Domain: endpoint,
//TLS
RequireTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
//VDir
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
//Custom headers
UserDefinedHeaders: []*dynamicproxy.UserDefinedHeader{},
//Auth
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
DefaultSiteOption: 0,
DefaultSiteValue: "",
}
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, "unable to prepare proxy route to target endpoint: "+err.Error())
return
}
dynamicProxyRouter.AddProxyRouteToRuntime(preparedEndpoint)
proxyEndpointCreated = &thisProxyEndpoint
} else if eptype == "root" {
//Get the default site options and target
dsOptString, err := utils.PostPara(r, "defaultSiteOpt")
if err != nil {
utils.SendErrorResponse(w, "default site action not defined")
return
}
var defaultSiteOption int = 1
opt, err := strconv.Atoi(dsOptString)
if err != nil {
utils.SendErrorResponse(w, "invalid default site option")
return
}
defaultSiteOption = opt
dsVal, err := utils.PostPara(r, "defaultSiteVal")
if err != nil && (defaultSiteOption == 1 || defaultSiteOption == 2) {
//Reverse proxy or redirect, must require value to be set
utils.SendErrorResponse(w, "target not defined")
return
}
//Write the root options to file
rootRoutingEndpoint := dynamicproxy.ProxyEndpoint{
ProxyType: dynamicproxy.ProxyType_Root,
RootOrMatchingDomain: "/",
Domain: endpoint, Domain: endpoint,
RequireTLS: useTLS, RequireTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS, BypassGlobalTLS: false,
SkipCertValidations: skipTlsValidation, SkipCertValidations: false,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials, DefaultSiteOption: defaultSiteOption,
DefaultSiteValue: dsVal,
} }
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption) preparedRootProxyRoute, err := dynamicProxyRouter.PrepareProxyRoute(&rootRoutingEndpoint)
} else if eptype == "root" { if err != nil {
rootname = "root" utils.SendErrorResponse(w, "unable to prepare root routing: "+err.Error())
thisOption := dynamicproxy.RootOptions{ return
ProxyLocation: endpoint,
RequireTLS: useTLS,
} }
dynamicProxyRouter.SetRootProxy(&thisOption)
dynamicProxyRouter.SetProxyRouteAsRoot(preparedRootProxyRoute)
proxyEndpointCreated = &rootRoutingEndpoint
} else { } else {
//Invalid eptype //Invalid eptype
utils.SendErrorResponse(w, "Invalid endpoint type") utils.SendErrorResponse(w, "invalid endpoint type")
return return
} }
//Save it //Save the config to file
thisProxyConfigRecord := Record{ err = SaveReverseProxyConfig(proxyEndpointCreated)
ProxyType: eptype, if err != nil {
Rootname: rootname, SystemWideLogger.PrintAndLog("Proxy", "Unable to save new proxy rule to file", err)
ProxyTarget: endpoint, return
UseTLS: useTLS,
BypassGlobalTLS: useBypassGlobalTLS,
SkipTlsValidation: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
} }
SaveReverseProxyConfigToFile(&thisProxyConfigRecord)
//Update utm if exists //Update utm if exists
if uptimeMonitor != nil { UpdateUptimeMonitorTargets()
uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter)
uptimeMonitor.CleanRecords()
}
utils.SendOK(w) utils.SendOK(w)
} }
/* /*
ReverseProxyHandleEditEndpoint handles proxy endpoint edit ReverseProxyHandleEditEndpoint handles proxy endpoint edit
This endpoint do not handle (host only, for root use Default Site page to edit)
basic auth credential update. The credential This endpoint do not handle basic auth credential update.
will be loaded from old config and reused The credential will be loaded from old config and reused
*/ */
func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd
if err != nil {
utils.SendErrorResponse(w, "type not defined")
return
}
rootNameOrMatchingDomain, err := utils.PostPara(r, "rootname") rootNameOrMatchingDomain, err := utils.PostPara(r, "rootname")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "Target proxy rule not defined") utils.SendErrorResponse(w, "Target proxy rule not defined")
@ -371,50 +389,31 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
requireBasicAuth := (rba == "true") requireBasicAuth := (rba == "true")
//Load the previous basic auth credentials from current proxy rules //Load the previous basic auth credentials from current proxy rules
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(eptype, rootNameOrMatchingDomain) targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
if err != nil { if err != nil {
utils.SendErrorResponse(w, "Target proxy config not found or could not be loaded") utils.SendErrorResponse(w, "Target proxy config not found or could not be loaded")
return return
} }
if eptype == "vdir" { //Generate a new proxyEndpoint from the new config
thisOption := dynamicproxy.VdirOptions{ newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
RootName: targetProxyEntry.RootOrMatchingDomain, newProxyEndpoint.Domain = endpoint
Domain: endpoint, newProxyEndpoint.RequireTLS = useTLS
RequireTLS: useTLS, newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
BypassGlobalTLS: false, newProxyEndpoint.SkipCertValidations = skipTlsValidation
SkipCertValidations: skipTlsValidation, newProxyEndpoint.RequireBasicAuth = requireBasicAuth
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
targetProxyEntry.Remove()
dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)
} else if eptype == "subd" { //Prepare to replace the current routing rule
thisOption := dynamicproxy.SubdOptions{ readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
MatchingDomain: targetProxyEntry.RootOrMatchingDomain, if err != nil {
Domain: endpoint, utils.SendErrorResponse(w, err.Error())
RequireTLS: useTLS, return
BypassGlobalTLS: bypassGlobalTLS,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
targetProxyEntry.Remove()
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
} }
targetProxyEntry.Remove()
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
//Save it to file //Save it to file
thisProxyConfigRecord := Record{ SaveReverseProxyConfig(newProxyEndpoint)
ProxyType: eptype,
Rootname: targetProxyEntry.RootOrMatchingDomain,
ProxyTarget: endpoint,
UseTLS: useTLS,
SkipTlsValidation: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
}
SaveReverseProxyConfigToFile(&thisProxyConfigRecord)
//Update uptime monitor //Update uptime monitor
UpdateUptimeMonitorTargets() UpdateUptimeMonitorTargets()
@ -429,21 +428,19 @@ func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
return return
} }
ptype, err := utils.PostPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
//Remove the config from runtime //Remove the config from runtime
err = dynamicProxyRouter.RemoveProxyEndpointByRootname(ptype, ep) err = dynamicProxyRouter.RemoveProxyEndpointByRootname(ep)
if err != nil { if err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return
} }
//Remove the config from file //Remove the config from file
RemoveReverseProxyConfigFile(ep) err = RemoveReverseProxyConfig(ep)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Update utm if exists //Update utm if exists
if uptimeMonitor != nil { if uptimeMonitor != nil {
@ -473,14 +470,8 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
return return
} }
ptype, err := utils.GetPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
//Load the target proxy object from router //Load the target proxy object from router
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep) targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
if err != nil { if err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return
@ -502,17 +493,6 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
return return
} }
ptype, err := utils.PostPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
if ptype != "vdir" && ptype != "subd" {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
creds, err := utils.PostPara(r, "creds") creds, err := utils.PostPara(r, "creds")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given") utils.SendErrorResponse(w, "Invalid ptype given")
@ -520,7 +500,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
} }
//Load the target proxy object from router //Load the target proxy object from router
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep) targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
if err != nil { if err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return
@ -570,7 +550,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
targetProxy.BasicAuthCredentials = mergedCredentials targetProxy.BasicAuthCredentials = mergedCredentials
//Save it to file //Save it to file
SaveReverseProxyEndpointToFile(targetProxy) SaveReverseProxyConfig(targetProxy)
//Replace runtime configuration //Replace runtime configuration
targetProxy.UpdateToRuntime() targetProxy.UpdateToRuntime()
@ -593,14 +573,8 @@ func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
return return
} }
ptype, err := utils.GetPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
//Load the target proxy object from router //Load the target proxy object from router
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep) targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
if err != nil { if err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return
@ -624,12 +598,6 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
return return
} }
ptype, err := utils.PostPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
matchingPrefix, err := utils.PostPara(r, "prefix") matchingPrefix, err := utils.PostPara(r, "prefix")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "Invalid matching prefix given") utils.SendErrorResponse(w, "Invalid matching prefix given")
@ -637,7 +605,7 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
} }
//Load the target proxy object from router //Load the target proxy object from router
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep) targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
if err != nil { if err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return
@ -666,7 +634,7 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
//Save configs to runtime and file //Save configs to runtime and file
targetProxy.UpdateToRuntime() targetProxy.UpdateToRuntime()
SaveReverseProxyEndpointToFile(targetProxy) SaveReverseProxyConfig(targetProxy)
utils.SendOK(w) utils.SendOK(w)
} }
@ -679,12 +647,6 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
return return
} }
ptype, err := utils.PostPara(r, "ptype")
if err != nil {
utils.SendErrorResponse(w, "Invalid ptype given")
return
}
matchingPrefix, err := utils.PostPara(r, "prefix") matchingPrefix, err := utils.PostPara(r, "prefix")
if err != nil { if err != nil {
utils.SendErrorResponse(w, "Invalid matching prefix given") utils.SendErrorResponse(w, "Invalid matching prefix given")
@ -692,7 +654,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
} }
// Load the target proxy object from router // Load the target proxy object from router
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep) targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
if err != nil { if err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, err.Error())
return return
@ -717,7 +679,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
// Save configs to runtime and file // Save configs to runtime and file
targetProxy.UpdateToRuntime() targetProxy.UpdateToRuntime()
SaveReverseProxyEndpointToFile(targetProxy) SaveReverseProxyConfig(targetProxy)
utils.SendOK(w) utils.SendOK(w)
} }
@ -728,16 +690,28 @@ func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
} }
func ReverseProxyList(w http.ResponseWriter, r *http.Request) { func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd eptype, err := utils.PostPara(r, "type") //Support root and host
if err != nil { if err != nil {
utils.SendErrorResponse(w, "type not defined") utils.SendErrorResponse(w, "type not defined")
return return
} }
if eptype == "vdir" { if eptype == "host" {
results := []*dynamicproxy.ProxyEndpoint{} results := []*dynamicproxy.ProxyEndpoint{}
dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool { dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
results = append(results, value.(*dynamicproxy.ProxyEndpoint)) thisEndpoint := dynamicproxy.CopyEndpoint(value.(*dynamicproxy.ProxyEndpoint))
//Clear the auth passwords before showing to front-end
cleanedCredentials := []*dynamicproxy.BasicAuthCredentials{}
for _, user := range thisEndpoint.BasicAuthCredentials {
cleanedCredentials = append(cleanedCredentials, &dynamicproxy.BasicAuthCredentials{
Username: user.Username,
PasswordHash: "",
})
}
thisEndpoint.BasicAuthCredentials = cleanedCredentials
results = append(results, thisEndpoint)
return true return true
}) })
@ -745,19 +719,6 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
return results[i].Domain < results[j].Domain return results[i].Domain < results[j].Domain
}) })
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
} else if eptype == "subd" {
results := []*dynamicproxy.ProxyEndpoint{}
dynamicProxyRouter.SubdomainEndpoint.Range(func(key, value interface{}) bool {
results = append(results, value.(*dynamicproxy.ProxyEndpoint))
return true
})
sort.Slice(results, func(i, j int) bool {
return results[i].RootOrMatchingDomain < results[j].RootOrMatchingDomain
})
js, _ := json.Marshal(results) js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} else if eptype == "root" { } else if eptype == "root" {
@ -789,7 +750,7 @@ func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
} else if enabled == "false" { } else if enabled == "false" {
sysdb.Write("settings", "listenP80", false) sysdb.Write("settings", "listenP80", false)
SystemWideLogger.Println("Disabling port 80 listener") SystemWideLogger.Println("Disabling port 80 listener")
dynamicProxyRouter.UpdatePort80ListenerState(true) dynamicProxyRouter.UpdatePort80ListenerState(false)
} else { } else {
utils.SendErrorResponse(w, "invalid mode given: "+enabled) utils.SendErrorResponse(w, "invalid mode given: "+enabled)
} }
@ -837,6 +798,30 @@ func HandleManagementProxyCheck(w http.ResponseWriter, r *http.Request) {
utils.SendJSONResponse(w, string(js)) utils.SendJSONResponse(w, string(js))
} }
func HandleDevelopmentModeChange(w http.ResponseWriter, r *http.Request) {
enableDevelopmentModeStr, err := utils.GetPara(r, "enable")
if err != nil {
//Load the current development mode toggle state
js, _ := json.Marshal(dynamicProxyRouter.Option.NoCache)
utils.SendJSONResponse(w, string(js))
} else {
//Write changes to runtime
enableDevelopmentMode := false
if enableDevelopmentModeStr == "true" {
enableDevelopmentMode = true
}
//Write changes to runtime
dynamicProxyRouter.Option.NoCache = enableDevelopmentMode
//Write changes to database
sysdb.Write("settings", "devMode", enableDevelopmentMode)
utils.SendOK(w)
}
}
// Handle incoming port set. Change the current proxy incoming port // Handle incoming port set. Change the current proxy incoming port
func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) { func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
newIncomingPort, err := utils.PostPara(r, "incoming") newIncomingPort, err := utils.PostPara(r, "incoming")
@ -882,33 +867,138 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w) utils.SendOK(w)
} }
// Handle list of root route options /* Handle Custom Header Rules */
func HandleRootRouteOptionList(w http.ResponseWriter, r *http.Request) { //List all the custom header defined in this proxy rule
js, _ := json.Marshal(dynamicProxyRouter.RootRoutingOptions)
utils.SendJSONResponse(w, string(js))
}
// Handle update of the root route edge case options. See dynamicproxy/rootRoute.go func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
func HandleRootRouteOptionsUpdate(w http.ResponseWriter, r *http.Request) { epType, err := utils.PostPara(r, "type")
enableUnsetSubdomainRedirect, err := utils.PostBool(r, "unsetRedirect")
if err != nil { if err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, "endpoint type not defined")
return return
} }
unsetRedirectTarget, _ := utils.PostPara(r, "unsetRedirectTarget") domain, err := utils.PostPara(r, "domain")
if err != nil {
newRootOption := dynamicproxy.RootRoutingOptions{ utils.SendErrorResponse(w, "domain or matching rule not defined")
EnableRedirectForUnsetRules: enableUnsetSubdomainRedirect, return
UnsetRuleRedirectTarget: unsetRedirectTarget,
} }
dynamicProxyRouter.RootRoutingOptions = &newRootOption var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
err = newRootOption.SaveToFile() if epType == "root" {
targetProxyEndpoint = dynamicProxyRouter.Root
} else {
ep, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
targetProxyEndpoint = ep
}
//List all custom headers
customHeaderList := targetProxyEndpoint.UserDefinedHeaders
if customHeaderList == nil {
customHeaderList = []*dynamicproxy.UserDefinedHeader{}
}
js, _ := json.Marshal(customHeaderList)
utils.SendJSONResponse(w, string(js))
}
// Add a new header to the target endpoint
func HandleCustomHeaderAdd(w http.ResponseWriter, r *http.Request) {
epType, err := utils.PostPara(r, "type")
if err != nil { if err != nil {
utils.SendErrorResponse(w, err.Error()) utils.SendErrorResponse(w, "endpoint type not defined")
return
}
domain, err := utils.PostPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "domain or matching rule not defined")
return
}
name, err := utils.PostPara(r, "name")
if err != nil {
utils.SendErrorResponse(w, "HTTP header name not set")
return
}
value, err := utils.PostPara(r, "value")
if err != nil {
utils.SendErrorResponse(w, "HTTP header value not set")
return
}
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
if epType == "root" {
targetProxyEndpoint = dynamicProxyRouter.Root
} else {
ep, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
targetProxyEndpoint = ep
}
//Create a new custom header object
targetProxyEndpoint.AddUserDefinedHeader(name, value)
//Save it (no need reload as header are not handled by dpcore)
err = SaveReverseProxyConfig(targetProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, "unable to save update")
return return
} }
utils.SendOK(w) utils.SendOK(w)
} }
// Remove a header from the target endpoint
func HandleCustomHeaderRemove(w http.ResponseWriter, r *http.Request) {
epType, err := utils.PostPara(r, "type")
if err != nil {
utils.SendErrorResponse(w, "endpoint type not defined")
return
}
domain, err := utils.PostPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "domain or matching rule not defined")
return
}
name, err := utils.PostPara(r, "name")
if err != nil {
utils.SendErrorResponse(w, "HTTP header name not set")
return
}
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
if epType == "root" {
targetProxyEndpoint = dynamicProxyRouter.Root
} else {
ep, err := dynamicProxyRouter.LoadProxy(domain)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not exists")
return
}
targetProxyEndpoint = ep
}
targetProxyEndpoint.RemoveUserDefinedHeader(name)
err = SaveReverseProxyConfig(targetProxyEndpoint)
if err != nil {
utils.SendErrorResponse(w, "unable to save update")
return
}
utils.SendOK(w)
}

View File

@ -15,6 +15,8 @@ import (
This script holds the static resources router This script holds the static resources router
for the reverse proxy service for the reverse proxy service
If you are looking for reverse proxy handler, see Server.go in mod/dynamicproxy/
*/ */
func FSHandler(handler http.Handler) http.Handler { func FSHandler(handler http.Handler) http.Handler {

35
src/routingrule.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"net/http"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy"
)
/*
Routing Rule
This script handle special routing rules for some utilities functions
*/
// Register the system build-in routing rules into the core
func registerBuildInRoutingRules() {
//Cloudflare email decoder
//It decode the email address if you are proxying a cloudflare protected site
//[email-protected] -> real@email.com
dynamicProxyRouter.AddRoutingRules(&dynamicproxy.RoutingRule{
ID: "cloudflare-decoder",
MatchRule: func(r *http.Request) bool {
return strings.HasSuffix(r.RequestURI, "cloudflare-static/email-decode.min.js")
},
RoutingHandler: func(w http.ResponseWriter, r *http.Request) {
decoder := "function fixObfuscatedEmails(){let t=document.getElementsByClassName(\"__cf_email__\");for(let e=0;e<t.length;e++){let r=t[e],l=r.getAttribute(\"data-cfemail\");if(l){let a=decrypt(l);r.setAttribute(\"href\",\"mailto:\"+a),r.innerHTML=a}}}function decrypt(t){let e=\"\",r=parseInt(t.substr(0,2),16);for(let l=2;l<t.length;l+=2){let a=parseInt(t.substr(l,2),16)^r;e+=String.fromCharCode(a)}try{e=decodeURIComponent(escape(e))}catch(f){console.error(f)}return e}fixObfuscatedEmails();"
w.Header().Set("Content-type", "text/javascript")
w.Write([]byte(decoder))
},
Enabled: false,
UseSystemAccessControl: false,
})
}

View File

@ -101,6 +101,18 @@ func startupSequence() {
} else { } else {
panic(err) panic(err)
} }
//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()
//Create a netstat buffer //Create a netstat buffer
netstatBuffers, err = netstat.NewNetStatBuffer(300) netstatBuffers, err = netstat.NewNetStatBuffer(300)
if err != nil { if err != nil {
@ -128,7 +140,7 @@ func startupSequence() {
*/ */
if *allowMdnsScanning { if *allowMdnsScanning {
portInt, err := strconv.Atoi(strings.Split(handler.Port, ":")[1]) portInt, err := strconv.Atoi(strings.Split(*webUIPort, ":")[1])
if err != nil { if err != nil {
portInt = 8000 portInt = 8000
} }
@ -220,25 +232,13 @@ func startupSequence() {
log.Fatal(err) 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 // This sequence start after everything is initialized
func finalSequence() { func finalSequence() {
//Start ACME renew agent //Start ACME renew agent
acmeRegisterSpecialRoutingRule() acmeRegisterSpecialRoutingRule()
//Inject routing rules
registerBuildInRoutingRules()
} }

290
src/vdir.go Normal file
View File

@ -0,0 +1,290 @@
package main
/*
Vdir.go
This script handle virtual directory functions
in global scopes
Author: tobychui
*/
import (
"encoding/json"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/utils"
)
// List the Virtual directory under given proxy rule
func ReverseProxyListVdir(w http.ResponseWriter, r *http.Request) {
eptype, err := utils.PostPara(r, "type") //Support root and host
if err != nil {
utils.SendErrorResponse(w, "type not defined")
return
}
var targetEndpoint *dynamicproxy.ProxyEndpoint
if eptype == "host" {
endpoint, err := utils.PostPara(r, "ep") //Support root and host
if err != nil {
utils.SendErrorResponse(w, "endpoint not defined")
return
}
targetEndpoint, err = dynamicProxyRouter.LoadProxy(endpoint)
if err != nil {
utils.SendErrorResponse(w, "target endpoint not found")
return
}
} else if eptype == "root" {
targetEndpoint = dynamicProxyRouter.Root
} else {
utils.SendErrorResponse(w, "invalid type given")
return
}
//Parse result to json
vdirs := targetEndpoint.VirtualDirectories
if targetEndpoint.VirtualDirectories == nil {
//Avoid returning null to front-end
vdirs = []*dynamicproxy.VirtualDirectoryEndpoint{}
}
js, _ := json.Marshal(vdirs)
utils.SendJSONResponse(w, string(js))
}
// Add Virtual Directory to a host
func ReverseProxyAddVdir(w http.ResponseWriter, r *http.Request) {
eptype, err := utils.PostPara(r, "type") //Support root and host
if err != nil {
utils.SendErrorResponse(w, "type not defined")
return
}
matchingPath, err := utils.PostPara(r, "path")
if err != nil {
utils.SendErrorResponse(w, "matching path not defined")
return
}
//Must start with /
if !strings.HasPrefix(matchingPath, "/") {
matchingPath = "/" + matchingPath
}
domain, err := utils.PostPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "target domain not defined")
return
}
reqTLSStr, err := utils.PostPara(r, "reqTLS")
if err != nil {
//Assume false
reqTLSStr = "false"
}
reqTLS := (reqTLSStr == "true")
skipValidStr, err := utils.PostPara(r, "skipValid")
if err != nil {
//Assume false
skipValidStr = "false"
}
skipValid := (skipValidStr == "true")
//Load the target proxy endpoint from runtime
var targetProxyEndpoint *dynamicproxy.ProxyEndpoint
if eptype == "root" {
//Check if root is running at reverse proxy mode
if dynamicProxyRouter.Root.DefaultSiteOption != dynamicproxy.DefaultSite_ReverseProxy {
utils.SendErrorResponse(w, "virtual directory can only be added to root router under proxy mode")
return
}
targetProxyEndpoint = dynamicProxyRouter.Root
} else if eptype == "host" {
endpointID, err := utils.PostPara(r, "endpoint")
if err != nil {
utils.SendErrorResponse(w, "endpoint not defined")
return
}
loadedEndpoint, err := dynamicProxyRouter.LoadProxy(endpointID)
if err != nil {
utils.SendErrorResponse(w, "selected proxy host not exists")
return
}
targetProxyEndpoint = loadedEndpoint
} else {
utils.SendErrorResponse(w, "invalid proxy type given")
return
}
// Create a virtual directory entry base on the above info
newVirtualDirectoryRouter := dynamicproxy.VirtualDirectoryEndpoint{
MatchingPath: matchingPath,
Domain: domain,
RequireTLS: reqTLS,
SkipCertValidations: skipValid,
}
//Add Virtual Directory Rule to this Proxy Endpoint
activatedProxyEndpoint, err := targetProxyEndpoint.AddVirtualDirectoryRule(&newVirtualDirectoryRouter)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Save it to file
SaveReverseProxyConfig(activatedProxyEndpoint)
// Update uptime monitor
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}
func ReverseProxyDeleteVdir(w http.ResponseWriter, r *http.Request) {
eptype, err := utils.PostPara(r, "type") //Support root and host
if err != nil {
utils.SendErrorResponse(w, "type not defined")
return
}
vdir, err := utils.PostPara(r, "vdir")
if err != nil {
utils.SendErrorResponse(w, "vdir matching key not defined")
return
}
var targetEndpoint *dynamicproxy.ProxyEndpoint
if eptype == "root" {
targetEndpoint = dynamicProxyRouter.Root
} else if eptype == "host" {
//Proxy rule
matchingPath, err := utils.PostPara(r, "path")
if err != nil {
utils.SendErrorResponse(w, "matching path not defined")
return
}
ept, err := dynamicProxyRouter.LoadProxy(matchingPath)
if err != nil {
utils.SendErrorResponse(w, "target proxy rule not found")
return
}
targetEndpoint = ept
} else {
utils.SendErrorResponse(w, "invalid endpoint type")
return
}
//Delete the Vdir from endpoint
err = targetEndpoint.RemoveVirtualDirectoryRuleByMatchingPath(vdir)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
err = SaveReverseProxyConfig(targetEndpoint)
if err != nil {
SystemWideLogger.PrintAndLog("Config", "Fail to write vdir rules update to config file", err)
utils.SendErrorResponse(w, "unable to write changes to file")
return
}
utils.SendOK(w)
}
// Handle update of reverse proxy vdir rules
func ReverseProxyEditVdir(w http.ResponseWriter, r *http.Request) {
eptype, err := utils.PostPara(r, "type") //Support root and host
if err != nil {
utils.SendErrorResponse(w, "type not defined")
return
}
vdir, err := utils.PostPara(r, "vdir")
if err != nil {
utils.SendErrorResponse(w, "vdir matching key not defined")
return
}
domain, err := utils.PostPara(r, "domain")
if err != nil {
utils.SendErrorResponse(w, "target domain not defined")
return
}
reqTLSStr, err := utils.PostPara(r, "reqTLS")
if err != nil {
//Assume false
reqTLSStr = "false"
}
reqTLS := (reqTLSStr == "true")
skipValidStr, err := utils.PostPara(r, "skipValid")
if err != nil {
//Assume false
skipValidStr = "false"
}
skipValid := (skipValidStr == "true")
var targetEndpoint *dynamicproxy.ProxyEndpoint
if eptype == "root" {
targetEndpoint = dynamicProxyRouter.Root
} else if eptype == "host" {
//Proxy rule
matchingPath, err := utils.PostPara(r, "path")
if err != nil {
utils.SendErrorResponse(w, "matching path not defined")
return
}
ept, err := dynamicProxyRouter.LoadProxy(matchingPath)
if err != nil {
utils.SendErrorResponse(w, "target proxy rule not found")
return
}
targetEndpoint = ept
} else {
utils.SendErrorResponse(w, "invalid endpoint type given")
return
}
//Check if the target vdir exists
if targetEndpoint.GetVirtualDirectoryRuleByMatchingPath(vdir) == nil {
utils.SendErrorResponse(w, "target virtual directory rule not exists")
return
}
//Overwrite the target endpoint
newVdirRule := dynamicproxy.VirtualDirectoryEndpoint{
MatchingPath: vdir,
Domain: domain,
RequireTLS: reqTLS,
SkipCertValidations: skipValid,
Disabled: false,
}
targetEndpoint.RemoveVirtualDirectoryRuleByMatchingPath(vdir)
activatedProxyEndpoint, err := targetEndpoint.AddVirtualDirectoryRule(&newVdirRule)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Save changes to file
SaveReverseProxyConfig(activatedProxyEndpoint)
UpdateUptimeMonitorTargets()
utils.SendOK(w)
}

View File

@ -615,8 +615,12 @@
<p>Whitelist a certain IP or IP range</p> <p>Whitelist a certain IP or IP range</p>
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="field">
<label>IP Address</label> <label>IP Address</label>
<input id="ipAddressInputWhitelist" type="text" placeholder="IP Address"> <input id="ipAddressInputWhitelist" type="text" placeholder="IP Address">
</div>
<div class="field">
<label>Remarks (Optional)</label>
<input id="ipAddressCommentsWhitelist" type="text" placeholder="Comments or remarks for this IP range">
</div> </div>
<button id="addIpButton" onclick="addIpWhitelist();" class="ui basic green button"> <button id="addIpButton" onclick="addIpWhitelist();" class="ui basic green button">
<i class="green add icon"></i> Whitelist IP <i class="green add icon"></i> Whitelist IP
@ -634,6 +638,7 @@
<thead> <thead>
<tr> <tr>
<th>IP Address</th> <th>IP Address</th>
<th>Remarks</th>
<th>Remove</th> <th>Remove</th>
</tr> </tr>
</thead> </thead>
@ -793,11 +798,12 @@
if (data.length === 0) { if (data.length === 0) {
$('#whitelistIpTable').append(` $('#whitelistIpTable').append(`
<tr> <tr>
<td colspan="2"><i class="green check circle icon"></i>There are no whitelisted IP addresses</td> <td colspan="3"><i class="green check circle icon"></i>There are no whitelisted IP addresses</td>
</tr> </tr>
`); `);
} else { } else {
$.each(data, function(index, ip) { $.each(data, function(index, ipEntry) {
let ip = ipEntry.IP;
let icon = "globe icon"; let icon = "globe icon";
if (isLAN(ip)){ if (isLAN(ip)){
icon = "desktop icon"; icon = "desktop icon";
@ -807,6 +813,7 @@
$('#whitelistIpTable').append(` $('#whitelistIpTable').append(`
<tr class="whitelistItem" ip="${encodeURIComponent(ip)}"> <tr class="whitelistItem" ip="${encodeURIComponent(ip)}">
<td><i class="${icon}"></i> ${ip}</td> <td><i class="${icon}"></i> ${ip}</td>
<td>${ipEntry.Comment}</td>
<td><button class="ui icon basic mini red button" onclick="removeIpWhitelist('${ip}');"><i class="trash alternate icon"></i></button></td> <td><button class="ui icon basic mini red button" onclick="removeIpWhitelist('${ip}');"><i class="trash alternate icon"></i></button></td>
</tr> </tr>
`); `);
@ -1003,6 +1010,7 @@
function addIpWhitelist(){ function addIpWhitelist(){
let targetIp = $("#ipAddressInputWhitelist").val().trim(); let targetIp = $("#ipAddressInputWhitelist").val().trim();
let remarks = $("#ipAddressCommentsWhitelist").val().trim();
if (targetIp == ""){ if (targetIp == ""){
alert("IP address is empty") alert("IP address is empty")
return return
@ -1016,7 +1024,7 @@
$.ajax({ $.ajax({
url: "/api/whitelist/ip/add", url: "/api/whitelist/ip/add",
type: "POST", type: "POST",
data: {ip: targetIp.toLowerCase()}, data: {ip: targetIp.toLowerCase(), "comment": remarks},
success: function(response) { success: function(response) {
if (response.error !== undefined) { if (response.error !== undefined) {
msgbox(response.error, false, 6000); msgbox(response.error, false, 6000);
@ -1025,6 +1033,7 @@
} }
$("#ipAddressInputWhitelist").val(""); $("#ipAddressInputWhitelist").val("");
$("#ipAddressCommentsWhitelist").val("");
$("#ipAddressInputWhitelist").parent().remvoeClass("error"); $("#ipAddressInputWhitelist").parent().remvoeClass("error");
}, },
error: function() { error: function() {

View File

@ -15,42 +15,19 @@
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
<h4>Default Certificates</h4> <h3>Hosts Certificates</h3>
<small>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</small></p>
<table class="ui very basic unstackable celled table">
<thead>
<tr><th class="no-sort">Key Type</th>
<th class="no-sort">Exists</th>
</tr></thead>
<tbody>
<tr>
<td><i class="globe icon"></i> Default Public Key</td>
<td id="pubkeyExists"></td>
</tr>
<tr>
<td><i class="lock icon"></i> Default Private Key</td>
<td id="prikeyExists"></td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
<div class="ui buttons">
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
</div>
<div class="ui divider"></div>
<h4>Sub-domain Certificates</h4>
<p>Provide certificates for multiple domains reverse proxy</p> <p>Provide certificates for multiple domains reverse proxy</p>
<div class="ui fluid form"> <div class="ui fluid form">
<div class="three fields"> <div class="three fields">
<div class="field"> <div class="field">
<label>Server Name (Domain)</label> <label>Server Name (Domain)</label>
<input type="text" id="certdomain" placeholder="example.com / blog.example.com"> <input type="text" id="certdomain" placeholder="example.com / blog.example.com">
<small><i class="exclamation circle yellow icon"></i> Match the server name with your CN/DNS entry in certificate for faster resolve time</small>
</div> </div>
<div class="field"> <div class="field">
<label>Public Key (.pem)</label> <label>Public Key (.pem)</label>
<input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')"> <input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
<small>or .crt files in order systems</small>
</div> </div>
<div class="field"> <div class="field">
<label>Private Key (.key)</label> <label>Private Key (.key)</label>
@ -63,14 +40,33 @@
<div id="certUploadSuccMsg" class="ui green message" style="display:none;"> <div id="certUploadSuccMsg" class="ui green message" style="display:none;">
<i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded. <i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
</div> </div>
<br> <div class="ui message">
<h4>Tips about Server Names & SNI</h4>
<div class="ui bulleted list">
<div class="item">
If you have two subdomains like <code>a.example.com</code> and <code>b.example.com</code> ,
for faster response speed, you might want to setup them one by one (i.e. having two seperate certificate for
<code>a.example.com</code> and <code>b.example.com</code>).
</div>
<div class="item">
If you have a wildcard certificate that covers <code>*.example.com</code>,
you can just enter <code>example.com</code> as server name to add a certificate.
</div>
<div class="item">
If you have a certificate contain multiple host, you can enter the first domain in your certificate
and Zoraxy will try to match the remaining CN/DNS for you.
</div>
</div>
</div>
<p>Current list of loaded certificates</p>
<div> <div>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;"> <div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui sortable unstackable celled table"> <table class="ui sortable unstackable basic celled table">
<thead> <thead>
<tr><th>Domain</th> <tr><th>Domain</th>
<th>Last Update</th> <th>Last Update</th>
<th>Expire At</th> <th>Expire At</th>
<th class="no-sort">Renew</th>
<th class="no-sort">Remove</th> <th class="no-sort">Remove</th>
</tr></thead> </tr></thead>
<tbody id="certifiedDomainList"> <tbody id="certifiedDomainList">
@ -81,15 +77,34 @@
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button> <button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
</div> </div>
<div class="ui message">
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
If you have 3rd or even 4th level subdomains like <code>blog.example.com</code> or <code>en.blog.example.com</code> ,
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> <div class="ui divider"></div>
<h4>Certificate Authority (CA) and Auto Renew (ACME)</h4> <h3>Fallback Certificate</h3>
<p>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</p>
<table class="ui very basic unstackable celled table">
<thead>
<tr><th class="no-sort">Key Type</th>
<th class="no-sort">Found</th>
</tr></thead>
<tbody>
<tr>
<td><i class="globe icon"></i> Fallback Public Key</td>
<td id="pubkeyExists"></td>
</tr>
<tr>
<td><i class="lock icon"></i> Fallback Private Key</td>
<td id="prikeyExists"></td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 0.4em;"><i class="ui upload icon"></i> Upload Default Keypairs</p>
<div class="ui buttons">
<button class="ui basic grey button" onclick="uploadPublicKey();"><i class="globe icon"></i> Public Key</button>
<button class="ui basic black button" onclick="uploadPrivateKey();"><i class="black lock icon"></i> Private Key</button>
</div>
<div class="ui divider"></div>
<h3>Certificate Authority (CA) and Auto Renew (ACME)</h3>
<p>Management features regarding CA and ACME</p> <p>Management features regarding CA and ACME</p>
<h4>Prefered Certificate Authority</h4>
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</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="ui fluid form">
<div class="field"> <div class="field">
@ -112,12 +127,12 @@
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button> <button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
</div><br> </div><br>
<h5>Certificate Renew / Generation (ACME) Settings</h5> <h5>Certificate Renew / Generation (ACME) Settings</h5>
<div class="ui basic segment"> <div class="ui basic segment acmeRenewStateWrapper">
<h4 class="ui header" id="acmeAutoRenewer"> <h4 class="ui header" id="acmeAutoRenewer">
<i class="red circle icon"></i> <i class="white remove icon"></i>
<div class="content"> <div class="content">
<span id="acmeAutoRenewerStatus">Disabled</span> <span id="acmeAutoRenewerStatus">Disabled</span>
<div class="sub header">Auto-Renewer Status</div> <div class="sub header">ACME Auto-Renewer</div>
</div> </div>
</h4> </h4>
</div> </div>
@ -130,6 +145,110 @@
$("#defaultCA").dropdown(); $("#defaultCA").dropdown();
//Renew certificate by button press
function renewCertificate(domain, btn=undefined){
let defaultCA = $("#defaultCA").dropdown("get value");
if (defaultCA.trim() == ""){
defaultCA = "Let's Encrypt";
}
//Get a new cert using ACME
msgbox("Requesting certificate via " + defaultCA +"...");
//Request ACME for certificate
if (btn != undefined){
$(btn).addClass('disabled');
$(btn).html(`<i class="ui loading spinner icon"></i>`);
}
obtainCertificate(domain, defaultCA.trim(), function(succ){
if (btn != undefined){
$(btn).removeClass('disabled');
if (succ){
$(btn).html(`<i class="ui green check icon"></i>`);
}else{
$(btn).html(`<i class="ui red times icon"></i>`);
}
setTimeout(function(){
initManagedDomainCertificateList();
}, 3000);
}
});
}
/*
Obtain Certificate via ACME
*/
// Obtain certificate from API, only support one domain
function obtainCertificate(domains, usingCa = "Let's Encrypt", callback=undefined) {
//Load the ACME email from server side
let acmeEmail = "";
$.get("/api/acme/autoRenew/email", function(data){
if (data != "" && data != undefined && data != null){
acmeEmail = data;
}
let filename = "";
let email = acmeEmail;
if (acmeEmail == ""){
msgbox("Unable to obtain certificate: ACME email not set", false, 8000);
if (callback != undefined){
callback(false);
}
return;
}
if (filename.trim() == "" && !domains.includes(",")){
//Zoraxy filename are the matching name for domains.
//Use the same as domains
filename = domains;
}else if (filename != "" && !domains.includes(",")){
//Invalid settings. Force the filename to be same as domain
//if there are only 1 domain
filename = domains;
}else{
msgbox("Filename cannot be empty for certs containing multiple domains.")
if (callback != undefined){
callback(false);
}
return;
}
$.ajax({
url: "/api/acme/obtainCert",
method: "GET",
data: {
domains: domains,
filename: filename,
email: email,
ca: usingCa,
},
success: function(response) {
if (response.error) {
console.log("Error:", response.error);
// Show error message
msgbox(response.error, false, 12000);
if (callback != undefined){
callback(false);
}
} else {
console.log("Certificate installed successfully");
// Show success message
msgbox("Certificate installed successfully");
if (callback != undefined){
callback(false);
}
}
},
error: function(error) {
console.log("Failed to install certificate:", error);
}
});
});
}
//Delete the certificate by its domain //Delete the certificate by its domain
function deleteCertificate(domain){ function deleteCertificate(domain){
if (confirm("Confirm delete certificate for " + domain + " ?")){ if (confirm("Confirm delete certificate for " + domain + " ?")){
@ -154,6 +273,12 @@
//Initialize the current default CA options //Initialize the current default CA options
$.get("/api/acme/autoRenew/email", function(data){ $.get("/api/acme/autoRenew/email", function(data){
$("#prefACMEEmail").val(data); $("#prefACMEEmail").val(data);
if (data.trim() == ""){
//acme email is not yet set
$(".renewButton").addClass('disabled');
}else{
$(".renewButton").removeClass('disabled');
}
}); });
$.get("/api/acme/autoRenew/ca", function(data){ $.get("/api/acme/autoRenew/ca", function(data){
@ -167,7 +292,13 @@
//Set the status of the acme enable icon //Set the status of the acme enable icon
function setACMEEnableStates(enabled){ function setACMEEnableStates(enabled){
$("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled"); $("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
$("#acmeAutoRenewer").find("i").attr("class", enabled?"green circle icon":"red circle icon"); if (enabled){
$(".acmeRenewStateWrapper").addClass("enabled");
}else{
$(".acmeRenewStateWrapper").removeClass("enabled");
}
$("#acmeAutoRenewer").find("i").attr("class", enabled?"white circle check icon":"white circle times icon");
} }
initAcmeStatus(); initAcmeStatus();
@ -187,6 +318,9 @@
success: function(data){ success: function(data){
if (data.error != undefined){ if (data.error != undefined){
msgbox(data.error, false); msgbox(data.error, false);
}else{
//Update the renew button states
$(".renewButton").removeClass('disabled');
} }
} }
}); });
@ -223,13 +357,14 @@
<td>${entry.Domain}</td> <td>${entry.Domain}</td>
<td>${entry.LastModifiedDate}</td> <td>${entry.LastModifiedDate}</td>
<td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td> <td class="${isExpired?"expired":"valid"} certdate">${entry.ExpireDate} (${!isExpired?entry.RemainingDays+" days left":"Expired"})</td>
<td><button title="Renew Certificate" class="ui mini basic icon button renewButton" onclick="renewCertificate('${entry.Domain}', this);"><i class="ui green refresh icon"></i></button></td>
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td> <td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
</tr>`); </tr>`);
}); });
if (data.length == 0){ if (data.length == 0){
$("#certifiedDomainList").append(`<tr> $("#certifiedDomainList").append(`<tr>
<td colspan="4"><i class="ui times circle icon"></i> No valid keypairs found</td> <td colspan="4"><i class="ui times red circle icon"></i> No valid keypairs found</td>
</tr>`); </tr>`);
} }
} }

View File

@ -24,7 +24,6 @@
<div class="content"> <div class="content">
<div class="header" style="font-size: 1.2em;" id="ganodeCount">0</div> <div class="header" style="font-size: 1.2em;" id="ganodeCount">0</div>
<div class="description" id="connectedNodes" count="0">Connected Nodes</div> <div class="description" id="connectedNodes" count="0">Connected Nodes</div>
</div> </div>
</div> </div>
</div> </div>
@ -219,8 +218,8 @@
} }
//Bind event to tab switch //Bind event to tab switch
tabSwitchEventBind["gan"] = function(){ tabSwitchEventBind["gan"] = function(){
//On switch over to this page, load info //On switch over to this page, load info
listGANet(); listGANet();
initGANetID(); initGANetID();

View File

@ -61,7 +61,7 @@
<h2>Members</h2> <h2>Members</h2>
<p>To join this network using command line, type <code>sudo zerotier-cli join <span class="ganetID"></span></code> on your device terminal</p> <p>To join this network using command line, type <code>sudo zerotier-cli join <span class="ganetID"></span></code> on your device terminal</p>
<div class="ui checkbox" style="margin-bottom: 1em;"> <div class="ui checkbox" style="margin-bottom: 1em;">
<input id="showUnauthorizedMembers" type="checkbox" onchange="changeUnauthorizedVisibility(this.checked);"> <input id="showUnauthorizedMembers" type="checkbox" onchange="changeUnauthorizedVisibility(this.checked);" checked>
<label>Show Unauthorized Members</label> <label>Show Unauthorized Members</label>
</div> </div>
<div class="" style="overflow-x: auto;"> <div class="" style="overflow-x: auto;">
@ -84,6 +84,11 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="ui divider"></div>
<h4>Add Controller as Member</h4>
<p>Optionally you can add the network controller (ZeroTier running on the Zoraxy node) as member for cross GAN reverse proxy to bypass NAT limitations.</p>
<button class="ui basic small button addControllerToNetworkBtn" onclick="ganAddControllerToNetwork(this);"><i class="green add icon"></i> Add Controller as Member</button>
<button class="ui basic small button removeControllerFromNetworkBtn" onclick="ganRemoveControllerFromNetwork(this);"><i class="red sign-out icon"></i> Remove Controller from Member</button>
<br><br> <br><br>
</div> </div>
<script> <script>
@ -355,7 +360,10 @@
url: '/api/gan/members/list?netid=' + currentGANetID + '&detail=true', url: '/api/gan/members/list?netid=' + currentGANetID + '&detail=true',
type: 'GET', type: 'GET',
success: function(data) { success: function(data) {
const tableBody = $('#networkMemeberTable'); let tableBody = $('#networkMemeberTable');
if (tableBody.length == 0){
return;
}
data.sort((a, b) => a.address.localeCompare(b.address)); data.sort((a, b) => a.address.localeCompare(b.address));
//Check if the new object equal to the old one //Check if the new object equal to the old one
if (objectEqual(currentGANMemberList, data) && !forceUpdate){ if (objectEqual(currentGANMemberList, data) && !forceUpdate){
@ -592,6 +600,55 @@
} }
//Add and remove this controller node to network as member
function ganAddControllerToNetwork(){
$(".addControllerToNetworkBtn").addClass("disabled");
$(".addControllerToNetworkBtn").addClass("loading");
$.ajax({
url: "/api/gan/network/join",
method: "POST",
data: {
netid:currentGANetID,
},
success: function(data){
$(".addControllerToNetworkBtn").removeClass("disabled");
$(".addControllerToNetworkBtn").removeClass("loading");
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Controller joint " + currentGANetID);
}
setTimeout(function(){
renderMemeberTable(true);
}, 3000)
}
});
}
function ganRemoveControllerFromNetwork(){
$(".removeControllerFromNetworkBtn").addClass("disabled");
$(".removeControllerFromNetworkBtn").addClass("loading");
$.ajax({
url: "/api/gan/network/leave",
method: "POST",
data: {
netid:currentGANetID,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Controller left " + currentGANetID);
}
renderMemeberTable(true);
$(".removeControllerFromNetworkBtn").removeClass("disabled");
$(".removeControllerFromNetworkBtn").removeClass("loading");
}
});
}
//Entry points //Entry points
function initGanetDetails(ganetId){ function initGanetDetails(ganetId){
currentGANetID = ganetId; currentGANetID = ganetId;
@ -612,6 +669,11 @@
} }
//Switch from other tabs back to this, exit to GAN list
tabSwitchEventBind["gan"] = function(){
exitToGanList();
}
//Exit point //Exit point
function exitToGanList(){ function exitToGanList(){
$("#gan").load("./components/gan.html", function(){ $("#gan").load("./components/gan.html", function(){

View File

@ -0,0 +1,275 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>HTTP Proxy</h2>
<p>Proxy HTTP server with HTTP or HTTPS for multiple hosts. If you are only proxying for one host / domain, use Default Site instead.</p>
</div>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui celled sortable unstackable compact table">
<thead>
<tr>
<th>Host</th>
<th>Destination</th>
<th>Virtual Directory</th>
<th>Basic Auth</th>
<th class="no-sort" style="min-width:100px;">Actions</th>
</tr>
</thead>
<tbody id="httpProxyList">
</tbody>
</table>
</div>
<button class="ui icon right floated basic button" onclick="listProxyEndpoints();"><i class="green refresh icon"></i> Refresh</button>
<br><br>
</div>
<script>
function listProxyEndpoints(){
$.get("/api/proxy/list?type=host", function(data){
$("#httpProxyList").html(``);
if (data.error !== undefined){
$("#httpProxyList").append(`<tr>
<td data-label="" colspan="5"><i class="remove icon"></i> ${data.error}</td>
</tr>`);
}else if (data.length == 0){
$("#httpProxyList").append(`<tr>
<td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td>
</tr>`);
}else{
data.forEach(subd => {
let tlsIcon = "";
let subdData = encodeURIComponent(JSON.stringify(subd));
if (subd.RequireTLS){
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
if (subd.SkipCertValidations){
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
}
}
let inboundTlsIcon = "";
if ($("#tls").checkbox("is checked")){
inboundTlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
if (subd.BypassGlobalTLS){
inboundTlsIcon = `<i class="grey lock icon" title="TLS Bypass Enabled"></i>`;
}
}else{
inboundTlsIcon = `<i class="yellow lock open icon" title="Plain Text Mode"></i>`;
}
//Build the virtual directory list
var vdList = `<div class="ui list">`;
subd.VirtualDirectories.forEach(vdir => {
vdList += `<div class="item">${vdir.MatchingPath} <i class="green angle double right icon"></i> ${vdir.Domain}</div>`;
});
vdList += `</div>`;
if (subd.VirtualDirectories.length == 0){
vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;"><i class="check icon"></i> No Virtual Directory</small>`;
}
$("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
<td data-label="" editable="true" datatype="inbound"><a href="//${subd.RootOrMatchingDomain}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}</td>
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="vdir">${vdList}</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 inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="edit icon"></i></button>
<button class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button>
</td>
</tr>`);
});
}
});
}
/*
Inline editor for httprp.html
*/
function editEndpoint(uuid) {
uuid = uuid.hexDecode();
var row = $('tr[eptuuid="' + uuid + '"]');
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);
var oldValue = column.text().trim();
if ($(this).attr("editable") == "false"){
//This col do not allow edit. Skip
return;
}
// Create an input element based on the column content
var input;
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<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 == "vdir"){
//Append a quick access button for vdir page
column.append(`<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="quickEditVdir('${uuid}');">
<i class="ui yellow folder icon"></i> Edit Virtual Directories
</button>`);
}else if (datatype == "basicauth"){
let requireBasicAuth = payload.RequireBasicAuth;
let checkstate = "";
if (requireBasicAuth){
checkstate = "checked";
}
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="RequireBasicAuth" ${checkstate}>
<label>Require Basic Auth</label>
</div>
<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
<div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;">
<div class="ui endpointAdvanceConfig accordion" style="padding-right: 0.6em;">
<div class="title">
<i class="dropdown icon"></i>
Advance Configs
</div>
<div class="content">
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
<!-- <button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editLoadBalanceOptions('${uuid}');"><i class="blue server icon"></i> Load Balance</button> -->
</div>
</div>
<div>
`);
}else if (datatype == 'action'){
column.empty().append(`
<button title="Save" onclick="saveProxyInlineEdit('${uuid.hexEncode()}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui green save icon"></i></button>
<button title="Cancel" onclick="exitProxyInlineEdit();" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui remove icon"></i></button>
`);
}else if (datatype == "inbound"){
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
}
});
$(".endpointAdvanceConfig").accordion();
$("#httpProxyList").find(".editBtn").addClass("disabled");
}
function exitProxyInlineEdit(){
listProxyEndpoints();
$("#httpProxyList").find(".editBtn").removeClass("disabled");
}
function saveProxyInlineEdit(uuid){
uuid = uuid.hexDecode();
var row = $('tr[eptuuid="' + uuid + '"]');
if (row.length == 0){
return;
}
var epttype = "host";
let newDomain = $(row).find(".Domain").val();
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)
$.ajax({
url: "/api/proxy/edit",
method: "POST",
data: {
"type": epttype,
"rootname": uuid,
"ep":newDomain,
"bpgtls": bypassGlobalTLS,
"tls" :requireTLS,
"tlsval": skipCertValidations,
"bauth" :requireBasicAuth,
},
success: function(data){
if (data.error !== undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Proxy endpoint updated");
listProxyEndpoints();
}
}
})
}
/* button events */
function editBasicAuthCredentials(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
}
function quickEditVdir(uuid){
openTabById("vdir");
$("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
}
function editCustomHeaders(uuid){
let payload = encodeURIComponent(JSON.stringify({
ept: "host",
ep: uuid
}));
showSideWrapper("snippet/customHeaders.html?t=" + Date.now() + "#" + payload);
}
function editLoadBalanceOptions(uuid){
alert(uuid);
}
//Bind on tab switch events
tabSwitchEventBind["httprp"] = function(){
listProxyEndpoints();
}
</script>

View File

@ -72,25 +72,10 @@
<i class="ui green checkmark icon"></i> Redirection Rules Added <i class="ui green checkmark icon"></i> Redirection Rules Added
</div> </div>
<br><br> <br><br>
<div class="advancezone ui basic segment">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Options
</div>
<div class="content">
<p>If you need custom header, content or status code other than basic redirects, you can use the advance path rules editor.</p>
<button class="ui black basic button" onclick="createAdvanceRules();"><i class="ui black external icon"></i> Open Advance Rules Editor</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
$(".advanceSettings").accordion();
/* /*
Redirection functions Redirection functions
@ -125,6 +110,7 @@
$("#ruleAddSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast"); $("#ruleAddSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
} }
initRedirectionRuleList(); initRedirectionRuleList();
resetForm();
} }
}); });
} }
@ -151,16 +137,12 @@
} }
} }
function createAdvanceRules(){
showSideWrapper("snippet/advancePathRules.html?t=" + Date.now(), true);
}
function initRedirectionRuleList(){ function initRedirectionRuleList(){
$("#redirectionRuleList").html(""); $("#redirectionRuleList").html("");
$.get("/api/redirect/list", function(data){ $.get("/api/redirect/list", function(data){
data.forEach(function(entry){ data.forEach(function(entry){
$("#redirectionRuleList").append(`<tr> $("#redirectionRuleList").append(`<tr>
<td>${entry.RedirectURL} </td> <td><a href="${entry.RedirectURL}" target="_blank">${entry.RedirectURL}</a></td>
<td>${entry.TargetURL}</td> <td>${entry.TargetURL}</td>
<td>${entry.ForwardChildpath?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td> <td>${entry.ForwardChildpath?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td>
<td>${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td> <td>${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td>
@ -169,7 +151,7 @@
}); });
if (data.length == 0){ if (data.length == 0){
$("#redirectionRuleList").append(`<tr colspan="4"><td><i class="checkmark icon"></i> No redirection rule</td></tr>`); $("#redirectionRuleList").append(`<tr colspan="4"><td><i class="green check circle icon"></i> No redirection rule</td></tr>`);
} }
}); });

View File

@ -1,92 +1,122 @@
<div class="standardContainer"> <div class="standardContainer">
<div class="ui basic segment"> <div class="ui basic segment">
<h2>Set Proxy Root</h2> <h2>Default Site</h2>
<p>The default routing point for all incoming traffics. For all routing not found in the proxy rules, request will be redirected to the proxy root server.</p> <p>Default routing options for inbound traffic (previously called Proxy Root)</p>
<div class="ui form"> <div class="ui form">
<div class="field"> <div class="grouped fields">
<label>Proxy Root</label> <label>What to show when Zoraxy is hit with an unknown Host?</label>
<input type="text" id="proxyRoot" onchange="checkRootRequireTLS(this.value);"> <div class="field">
<small>E.g. localhost:8080</small> <div class="ui radio defaultsite checkbox">
</div> <input type="radio" name="defaultsiteOption" checked="checked" value="webserver">
<div class="field"> <label>Internal Static Web Server<br>
<div class="ui checkbox"> <small>Check this if you prefer a more Apache / Nginx like experience</small>
<input type="checkbox" id="rootReqTLS"> </label>
<label>Root require TLS connection <br><small>Check this if your proxy root URL starts with https://</small></label> </div>
</div>
<div class="field">
<div class="ui radio defaultsite checkbox">
<input type="radio" name="defaultsiteOption" value="proxy">
<label>Reverse Proxy Target<br>
<small>Proxy the request to a target IP / domain</small>
</label>
</div>
</div>
<div class="field">
<div class="ui radio defaultsite checkbox">
<input type="radio" name="defaultsiteOption" value="redirect">
<label>Redirect<br>
<small>Redirect the user to a new location</small>
</label>
</div>
</div>
<div class="field">
<div class="ui radio defaultsite checkbox">
<input type="radio" name="defaultsiteOption" value="notfound">
<label>Show 404 NOT FOUND<br>
<small>Respond to request with a 404 page</small>
</label>
</div>
</div> </div>
</div> </div>
<div class="ui horizontal divider">OR</div> </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>
<div class="field">
<h4>Root Routing Options</h4>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="unsetRedirect">
<label>Enable redirect for unset subdomains <br><small>Redirect subdomain that is not found to custom domain</small></label>
</div>
</div>
<div class="ui basic segment" id="unsetRedirectDomainWrapper" style="background-color: #f7f7f7; border-radius: 1em; margin-left: 2em; padding-left: 2em; display:none;">
<div style="
position: absolute;
top:0;
left: 1em;
width: 0px;
height: 0px;
margin-top: -10px;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid #f7f7f7;">
<!-- Reverse Proxy as Default Site Options -->
<div id="defaultSiteProxyOptions" class="ui basic segment advanceoptions defaultSiteOptionDetails" style="display:none; ">
<div class="ui form">
<div class="field">
<label>Reverse Proxy Target</label>
<input type="text" id="proxyRoot" onchange="checkRootRequireTLS(this.value);">
<small>e.g. localhost:8080 / 192.168.0.100:80 / example.com</small>
</div> </div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="rootReqTLS">
<label>Reverse proxy target require TLS connection <br><small>Check this if your proxy target URL require connection with https://</small></label>
</div>
</div>
</div>
</div>
<!-- Redirect as default site Options-->
<div id="defaultSiteRedirectOptions" class="ui basic segment advanceoptions defaultSiteOptionDetails" style="display:none;"">
<div class="ui form">
<div class="field"> <div class="field">
<label>Redirect target domain</label> <label>Redirect target domain</label>
<div class="ui input"> <div class="ui input">
<input id="unsetRedirectDomain" type="text" placeholder="http://example.com"> <input id="redirectDomain" type="text" placeholder="http://example.com">
</div> </div>
<small>Unset subdomain will be redirected to the link above. Remember to include the protocol (e.g. http:// or https://)<br> <small>Unset subdomain will be redirected to the link above. Remember to include the protocol (e.g. http:// or https://)</small>
Leave empty for redirecting to upper level domain (e.g. notfound.example.com <i class="right arrow icon"></i> example.com)</small>
</div> </div>
</div> </div>
<br>
<button class="ui basic button" onclick="updateRootOptions()"><i class="blue save icon" ></i> Save Root Options</button>
</div> </div>
<button class="ui basic button" onclick="setProxyRoot(this)"><i class="green checkmark icon" ></i> Apply Changes</button>
<button class="ui basic button" onclick="initRootInfo()"><i class="refresh icon" ></i> Reset</button>
<br> <br>
</div> </div>
</div> </div>
<script> <script>
var currentDefaultSiteOption = 0; //For enum see typedef.go
$("#advanceRootSettings").accordion(); $("#advanceRootSettings").accordion();
function handleUseStaticWebServerAsRoot(){ //Handle toggle events of option radio boxes
let useStaticWebServer = $("#useStaticWebServer")[0].checked; function updateAvaibleDefaultSiteOptions(){
if (useStaticWebServer){ let selectedDefaultSite = $('input[name="defaultsiteOption"]:checked').val();
$(".defaultSiteOptionDetails").hide();
$("#useRootProxyRouterForVdir").parent().addClass("disabled");
if (selectedDefaultSite == "webserver"){
//Use build in web server as target
let staticWebServerURL = "127.0.0.1:" + $("#webserv_listenPort").val(); let staticWebServerURL = "127.0.0.1:" + $("#webserv_listenPort").val();
$("#proxyRoot").val(staticWebServerURL); $("#proxyRoot").val(staticWebServerURL);
$("#proxyRoot").parent().addClass("disabled"); $("#proxyRoot").parent().addClass("disabled");
$("#rootReqTLS").parent().checkbox("set unchecked"); $("#rootReqTLS").parent().checkbox("set unchecked");
$("#rootReqTLS").parent().addClass("disabled"); $("#rootReqTLS").parent().addClass("disabled");
$("#useRootProxyRouterForVdir").parent().removeClass("disabled");
//Check if web server is enabled. If not, ask if the user want to enable it currentDefaultSiteOption = 0;
/*if (!$("#webserv_enable").parent().checkbox("is checked")){ }else if (selectedDefaultSite == "proxy"){
confirmBox("Enable static web server now?", function(choice){ $("#defaultSiteProxyOptions").show();
if (choice == true){
$("#webserv_enable").parent().checkbox("set checked");
}
});
}*/
}else{
$("#rootReqTLS").parent().removeClass("disabled"); $("#rootReqTLS").parent().removeClass("disabled");
$("#proxyRoot").parent().removeClass("disabled"); $("#proxyRoot").parent().removeClass("disabled");
initRootInfo(); $("#useRootProxyRouterForVdir").parent().removeClass("disabled");
currentDefaultSiteOption = 1;
}else if (selectedDefaultSite == "redirect"){
$("#defaultSiteRedirectOptions").show();
currentDefaultSiteOption = 2;
}else if (selectedDefaultSite == "notfound"){
currentDefaultSiteOption = 3;
}else{
//Unknown option
return;
} }
}
//Bind events to the radio boxes
function bindDefaultSiteRadioCheckboxEvents(){
$('input[type=radio][name=defaultsiteOption]').off("change").on("change", function() {
updateAvaibleDefaultSiteOptions();
});
} }
function initRootInfo(callback=undefined){ function initRootInfo(callback=undefined){
@ -94,6 +124,22 @@
if (data == null){ if (data == null){
}else{ }else{
var $radios = $('input:radio[name=defaultsiteOption]');
let proxyType = data.DefaultSiteOption;
//See typedef.go for enum conversion
if (proxyType == 0){
$radios.filter('[value=webserver]').prop('checked', true);
}else if (proxyType == 1){
$radios.filter('[value=proxy]').prop('checked', true);
$("#proxyRoot").val(data.DefaultSiteValue);
}else if (proxyType == 2){
$radios.filter('[value=redirect]').prop('checked', true);
$("#redirectDomain").val(data.DefaultSiteValue);
}else if (proxyType == 3){
$radios.filter('[value=notfound]').prop('checked', true);
}
updateAvaibleDefaultSiteOptions();
$("#proxyRoot").val(data.Domain); $("#proxyRoot").val(data.Domain);
checkRootRequireTLS(data.Domain); checkRootRequireTLS(data.Domain);
} }
@ -104,21 +150,9 @@
}); });
} }
initRootInfo(function(){ initRootInfo(function(){
updateWebServerLinkSettings(); bindDefaultSiteRadioCheckboxEvents();
}); });
//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){ function isUsingStaticWebServerAsRoot(callback){
let currentProxyRoot = $("#proxyRoot").val().trim(); let currentProxyRoot = $("#proxyRoot").val().trim();
@ -131,47 +165,12 @@
} }
function updateRootSettingStates(){
$.get("/api/cert/tls", function(data){
if (data == true){
$("#disableRootTLS").parent().removeClass('disabled').attr("title", "");
}else{
$("#disableRootTLS").parent().addClass('disabled').attr("title", "TLS listener is not enabled");
}
});
}
//Bind event to tab switch //Bind event to tab switch
tabSwitchEventBind["setroot"] = function(){ tabSwitchEventBind["setroot"] = function(){
//On switch over to this page, update root info
updateRootSettingStates();
} }
//Toggle the display status of the input box for domain setting
function updateRedirectionDomainSettingInputBox(useRedirect){
if(useRedirect){
$("#unsetRedirectDomainWrapper").stop().finish().slideDown("fast");
}else{
$("#unsetRedirectDomainWrapper").stop().finish().slideUp("fast");
}
}
function checkCustomRedirectForUnsetSubd(){
$.get("/api/proxy/root/listOptions", function(data){
$("#unsetRedirect")[0].checked = data.EnableRedirectForUnsetRules || false;
$("#unsetRedirectDomain").val(data.UnsetRuleRedirectTarget);
updateRedirectionDomainSettingInputBox(data.EnableRedirectForUnsetRules);
//Bind event to the checkbox
$("#unsetRedirect").off("change").on("change", function(){
let useRedirect = $("#unsetRedirect")[0].checked;
updateRedirectionDomainSettingInputBox(useRedirect);
});
});
}
checkCustomRedirectForUnsetSubd();
//Check if the given domain will redirect to https //Check if the given domain will redirect to https
function checkRootRequireTLS(targetDomain){ function checkRootRequireTLS(targetDomain){
//Trim off the http or https from the origin //Trim off the http or https from the origin
@ -193,28 +192,56 @@
}else if (data == "http"){ }else if (data == "http"){
$("#rootReqTLS").parent().checkbox("set unchecked"); $("#rootReqTLS").parent().checkbox("set unchecked");
} }
} }
}) })
} }
//Set the new proxy root option //Set the new proxy root option
function setProxyRoot(){ function setProxyRoot(btn=undefined){
var newpr = $("#proxyRoot").val(); var newpr = $("#proxyRoot").val();
if (newpr.trim() == ""){ if (newpr.trim() == "" && currentDefaultSiteOption == 0){
$("#proxyRoot").parent().addClass('error'); //Fill in the web server info
return newpr = "127.0.0.1:" + $("#webserv_listenPort").val();
}else{ $("#proxyRoot").val(newpr);
$("#proxyRoot").parent().removeClass('error');
} }
var rootReqTls = $("#rootReqTLS")[0].checked; var rootReqTls = $("#rootReqTLS")[0].checked;
if (btn != undefined){
$(btn).addClass("disabled");
}
//proxy mode or redirect mode, check for input values
var defaultSiteValue = "";
if (currentDefaultSiteOption == 1){
if ($("#proxyRoot").val().trim() == ""){
$("#proxyRoot").parent().addClass("error");
return;
}
defaultSiteValue = $("#proxyRoot").val().trim();
$("#proxyRoot").parent().removeClass("error");
}else if (currentDefaultSiteOption == 2){
if ($("#redirectDomain").val().trim() == ""){
$("#redirectDomain").parent().addClass("error");
return;
}
defaultSiteValue = $("#redirectDomain").val().trim();
$("#redirectDomain").parent().removeClass("error");
}
//Create the endpoint by calling add //Create the endpoint by calling add
$.ajax({ $.ajax({
url: "/api/proxy/add", url: "/api/proxy/add",
data: {"type": "root", tls: rootReqTls, ep: newpr}, data: {
"type": "root",
"tls": rootReqTls,
"ep": newpr,
"defaultSiteOpt": currentDefaultSiteOption,
"defaultSiteVal":defaultSiteValue,
},
method: "POST",
success: function(data){ success: function(data){
if (data.error != undefined){ if (data.error != undefined){
msgbox(data.error, false, 5000); msgbox(data.error, false, 5000);
@ -231,37 +258,20 @@
setTimeout(function(){ setTimeout(function(){
//Update the checkbox //Update the checkbox
updateWebServerLinkSettings();
msgbox("Proxy Root Updated"); msgbox("Proxy Root Updated");
}, 1000); }, 100);
}) })
}); });
} }
if (btn != undefined){
$(btn).removeClass("disabled");
}
} }
}); });
} }
function updateRootOptions(){
$.ajax({
type: "POST",
url: "/api/proxy/root/updateOptions",
data: {
unsetRedirect: $("#unsetRedirect")[0].checked,
unsetRedirectTarget: $("#unsetRedirectDomain").val().trim(),
},
success: function(data) {
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Root Routing Options updated");
}
},
error: function(error) {
console.log("Error:", error);
}
});
}
</script> </script>

View File

@ -1,119 +1,114 @@
<div class="ui stackable grid"> <!-- Proxy Create Form-->
<style>
.rulesInstructions{
background: var(--theme_background) !important;
color: var(--theme_lgrey);
border-radius: 1em !important;
}
</style>
<div class="standardContainer">
<div class="ui stackable grid">
<div class="ten wide column"> <div class="ten wide column">
<div class="standardContainer"> <div class="ui basic segment" style="border-radius: 1em; padding: 1em !important;">
<div class="ui basic segment" style="margin-top: 1em;"> <h2>New Proxy Rule</h2>
<h2>New Proxy Rule</h2> <p>You can add more proxy rules to support more site via domain / subdomains</p>
<p>You can create a proxy endpoing by subdomain or virtual directories</p> <div class="ui form">
<div class="ui form"> <div class="field">
<div class="field"> <label>Matching Keyword / Domain</label>
<label>Proxy Type</label> <input type="text" id="rootname" placeholder="mydomain.com">
<div class="ui selection dropdown"> <small>Support subdomain and wildcard, e.g. s1.mydomain.com or *.test.mydomain.com</small>
<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">
<div class="item" data-value="subd">Sub-domain</div>
<div class="item" data-value="vdir">Virtual Directory</div>
</div>
</div>
</div>
<div class="field">
<label>Subdomain Matching Keyword / Virtual Directory Name</label>
<input type="text" id="rootname" placeholder="s1.mydomain.com">
</div>
<div class="field">
<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>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="reqTls">
<label>Proxy Target require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
</div>
</div>
<!-- Advance configs -->
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div id="advanceProxyRules" class="ui fluid accordion">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<p></p>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="skipTLSValidation">
<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">
<div class="ui checkbox">
<input type="checkbox" id="requireBasicAuth">
<label>Require Basic Auth<br><small>Require client to login in order to view the page</small></label>
</div>
</div>
<div id="basicAuthCredentials" class="field">
<p>Enter the username and password for allowing them to access this proxy endpoint</p>
<table class="ui very basic celled table">
<thead>
<tr>
<th>Username</th>
<th>Password</th>
<th>Remove</th>
</tr></thead>
<tbody id="basicAuthCredentialTable">
<tr>
<td colspan="3"><i class="ui green circle check icon"></i> No Entered Credential</td>
</tr>
</tbody>
</table>
<div class="three small fields credentialEntry">
<div class="field">
<input id="basicAuthCredUsername" type="text" placeholder="Username" autocomplete="off">
</div>
<div class="field">
<input id="basicAuthCredPassword" type="password" placeholder="Password" autocomplete="off">
</div>
<div class="field">
<button class="ui basic button" onclick="addCredentials();"><i class="blue add icon"></i> Add Credential</button>
</div>
</div>
</div>
</div>
</div>
</div>
<br>
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="blue add icon"></i> Create Endpoint</button>
<br><br>
</div> </div>
<div class="field">
<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>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="reqTls">
<label>Proxy Target require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
</div>
</div>
<!-- Advance configs -->
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div id="advanceProxyRules" class="ui fluid accordion">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<p></p>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="skipTLSValidation">
<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">
<div class="ui checkbox">
<input type="checkbox" id="requireBasicAuth">
<label>Require Basic Auth<br><small>Require client to login in order to view the page</small></label>
</div>
</div>
<div id="basicAuthCredentials" class="field">
<p>Enter the username and password for allowing them to access this proxy endpoint</p>
<table class="ui very basic celled table">
<thead>
<tr>
<th>Username</th>
<th>Password</th>
<th>Remove</th>
</tr></thead>
<tbody id="basicAuthCredentialTable">
<tr>
<td colspan="3"><i class="ui green circle check icon"></i> No Entered Credential</td>
</tr>
</tbody>
</table>
<div class="three small fields credentialEntry">
<div class="field">
<input id="basicAuthCredUsername" type="text" placeholder="Username" autocomplete="off">
</div>
<div class="field">
<input id="basicAuthCredPassword" type="password" placeholder="Password" autocomplete="off">
</div>
<div class="field">
<button class="ui basic button" onclick="addCredentials();"><i class="blue add icon"></i> Add Credential</button>
</div>
</div>
</div>
</div>
</div>
</div>
<br>
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="green add icon"></i> Create Endpoint</button>
<br><br>
</div> </div>
</div> </div>
</div> </div>
<div class="six wide column"> <div class="six wide column">
<div class="ui basic segment" style="height: 100%; background-color: var(--theme_grey); color: var(--theme_lgrey);"> <div class="ui basic segment rulesInstructions">
<br> <span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
<span style="font-size: 1.2em; font-weight: 300;">Subdomain</span><br> Example of domain matching keyword:<br>
Example of subdomain matching keyword:<br> <code>arozos.com</code> <br>Any acess requesting arozos.com will be proxy to the IP address below<br>
<code>s1.arozos.com</code> <br>(Any access starting with s1.arozos.com will be proxy to the IP address below)<br>
<div class="ui divider"></div> <div class="ui divider"></div>
<span style="font-size: 1.2em; font-weight: 300;">Virtual Directory</span><br> <span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Subdomain</span><br>
Example of virtual directory name: <br> Example of subdomain matching keyword:<br>
<code>/s1/home/</code> <br>(Any access to {this_server}/s1/home/ will be proxy to the IP address below)<br> <code>s1.arozos.com</code> <br>Any request starting with s1.arozos.com will be proxy to the IP address below<br>
You can also ignore the tailing slash for wildcard like usage.<br> <div class="ui divider"></div>
<code>/s1/room-</code> <br>Any access to {this_server}/s1/classroom_* will be proxied, for example: <br> <span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Wildcard</span><br>
Example of wildcard matching keyword:<br>
<code>*.arozos.com</code> <br>Any request with a host name matching *.arozos.com will be proxy to the IP address below. Here are some examples.<br>
<div class="ui list"> <div class="ui list">
<div class="item"><code>/s1/room-101</code></div> <div class="item"><code>www.arozos.com</code></div>
<div class="item"><code>/s1/room-102/</code></div> <div class="item"><code>foo.bar.arozos.com</code></div>
<div class="item"><code>/s1/room-103/map.txt</code></div> </div>
</div><br>
<br> <br>
</div> </div>
</div> </div>
@ -122,9 +117,9 @@
<script> <script>
$("#advanceProxyRules").accordion(); $("#advanceProxyRules").accordion();
//New Proxy Endpoint //New Proxy Endpoint
function newProxyEndpoint(){ function newProxyEndpoint(){
var type = $("#ptype").val();
var rootname = $("#rootname").val(); var rootname = $("#rootname").val();
var proxyDomain = $("#proxyDomain").val(); var proxyDomain = $("#proxyDomain").val();
var useTLS = $("#reqTls")[0].checked; var useTLS = $("#reqTls")[0].checked;
@ -132,20 +127,6 @@
var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked; var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked;
var requireBasicAuth = $("#requireBasicAuth")[0].checked; var requireBasicAuth = $("#requireBasicAuth")[0].checked;
if (type === "vdir") {
if (!rootname.startsWith("/")) {
rootname = "/" + rootname
$("#rootname").val(rootname);
}
}else{
if (!isSubdomainDomain(rootname)){
//This doesn't seems like a subdomain
if (!confirm(rootname + " does not looks like a subdomain. Continue anyway?")){
return;
}
}
}
if (rootname.trim() == ""){ if (rootname.trim() == ""){
$("#rootname").parent().addClass("error"); $("#rootname").parent().addClass("error");
return return
@ -164,7 +145,7 @@
$.ajax({ $.ajax({
url: "/api/proxy/add", url: "/api/proxy/add",
data: { data: {
type: type, type: "host",
rootname: rootname, rootname: rootname,
tls: useTLS, tls: useTLS,
ep: proxyDomain, ep: proxyDomain,
@ -177,19 +158,14 @@
if (data.error != undefined){ if (data.error != undefined){
msgbox(data.error, false, 5000); msgbox(data.error, false, 5000);
}else{ }else{
//OK
listVdirs();
listSubd();
//Clear old data //Clear old data
$("#rootname").val(""); $("#rootname").val("");
$("#proxyDomain").val(""); $("#proxyDomain").val("");
credentials = []; credentials = [];
updateTable(); updateTable();
reloadUptimeList();
//Check if it is a new subdomain and TLS enabled //Check if it is a new subdomain and TLS enabled
if (type == "subd" && $("#tls").checkbox("is checked")){ if ($("#tls").checkbox("is checked")){
confirmBox("Request new SSL Cert for this subdomain?", function(choice){ confirmBox("Request new SSL Cert for this subdomain?", function(choice){
if (choice == true){ if (choice == true){
//Load the prefer CA from TLS page //Load the prefer CA from TLS page
@ -200,7 +176,12 @@
//Get a new cert using ACME //Get a new cert using ACME
msgbox("Requesting certificate via " + defaultCA +"..."); msgbox("Requesting certificate via " + defaultCA +"...");
console.log("Trying to get a new certificate via ACME"); console.log("Trying to get a new certificate via ACME");
obtainCertificate(rootname, defaultCA.trim());
//Request ACME for certificate, see cert.html component
obtainCertificate(rootname, defaultCA.trim(), function(){
// Renew the parent certificate list
initManagedDomainCertificateList();
});
}else{ }else{
msgbox("Proxy Endpoint Added"); msgbox("Proxy Endpoint Added");
} }
@ -214,23 +195,17 @@
} }
function handleProxyTypeOptionChange(newType){
if (newType == "subd"){
$("#bypassGlobalTLS").parent().removeClass("disabled");
}else if (newType == "vdir"){
$("#bypassGlobalTLS").parent().addClass("disabled");
}
}
//Generic functions for delete rp endpoints //Generic functions for delete rp endpoints
function deleteEndpoint(ptype, epoint){ function deleteEndpoint(epoint){
if (confirm("Confirm remove proxy for :" + epoint + " (type: " + ptype + ")?")){ epoint = decodeURIComponent(epoint).hexDecode();
if (confirm("Confirm remove proxy for :" + epoint + "?")){
$.ajax({ $.ajax({
url: "/api/proxy/del", url: "/api/proxy/del",
data: {ep: epoint, ptype: ptype}, data: {ep: epoint, },
success: function(){ success: function(){
listVdirs(); listProxyEndpoints();
listSubd(); msgbox("Proxy Rule Deleted", true);
reloadUptimeList();
} }
}) })
} }
@ -330,227 +305,33 @@
updateTable(); updateTable();
} }
//Check if a string is a valid subdomain
function isSubdomainDomain(str) {
const regex = /^(localhost|[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,}|[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,}\.)$/i;
return regex.test(str);
}
/*
Inline editor for subd.html and vdir.html
*/
function editEndpoint(endpointType, uuid) {
var row = $('tr[eptuuid="' + uuid + '"]');
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);
var oldValue = column.text().trim();
if ($(this).attr("editable") == "false"){
//This col do not allow edit. Skip
return;
}
// Create an input element based on the column content
var input;
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<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 == "basicauth"){
let requireBasicAuth = payload.RequireBasicAuth;
let checkstate = "";
if (requireBasicAuth){
checkstate = "checked";
}
column.empty().append(`<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="RequireBasicAuth" ${checkstate}>
<label>Require Basic Auth</label>
</div>
<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${endpointType}','${uuid}');"><i class="ui blue lock icon"></i> Edit Settings</button>`);
}else if (datatype == 'action'){
column.empty().append(`
<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
}
});
$("#" + endpointType).find(".editBtn").addClass("disabled");
}
function exitProxyInlineEdit(){
listSubd();
listVdirs();
$("#" + endpointType).find(".editBtn").removeClass("disabled");
}
function saveProxyInlineEdit(uuid){
var row = $('tr[eptuuid="' + uuid + '"]');
if (row.length == 0){
return;
}
var epttype = $(row).attr("class");
if (epttype == "subdEntry"){
epttype = "subd";
}else if (epttype == "vdirEntry"){
epttype = "vdir";
}
let newDomain = $(row).find(".Domain").val();
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)
$.ajax({
url: "/api/proxy/edit",
method: "POST",
data: {
"type": epttype,
"rootname": uuid,
"ep":newDomain,
"bpgtls": bypassGlobalTLS,
"tls" :requireTLS,
"tlsval": skipCertValidations,
"bauth" :requireBasicAuth,
},
success: function(data){
if (data.error !== undefined){
msgbox(data.error, false, 6000);
}else{
msgbox("Proxy endpoint updated");
if (epttype == "subd"){
listSubd();
}else if (epttype == "vdir"){
listVdirs();
}
}
}
})
}
function editBasicAuthCredentials(endpointType, uuid){
let payload = encodeURIComponent(JSON.stringify({ //Update v3.0.0
ept: endpointType, //Since some proxy rules now contains wildcard characters
ep: uuid //all uuid are converted to hex code before use in DOM selector
}));
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload); String.prototype.hexEncode = function(){
var hex, i;
var result = "";
for (i=0; i<this.length; i++) {
hex = this.charCodeAt(i).toString(16);
result += ("000"+hex).slice(-4);
}
return result
} }
String.prototype.hexDecode = function(){
/* var j;
Obtain Certificate via ACME var hexes = this.match(/.{1,4}/g) || [];
*/ var back = "";
for(j = 0; j<hexes.length; j++) {
//Load the ACME email from server side back += String.fromCharCode(parseInt(hexes[j], 16));
let acmeEmail = "";
$.get("/api/acme/autoRenew/email", function(data){
if (data != "" && data != undefined && data != null){
acmeEmail = data;
}
});
// Obtain certificate from API, only support one domain
function obtainCertificate(domains, usingCa = "Let's Encrypt") {
let filename = "";
let email = acmeEmail;
if (acmeEmail == ""){
let rootDomain = domains.split(".").pop();
email = "admin@" + rootDomain;
}
if (filename.trim() == "" && !domains.includes(",")){
//Zoraxy filename are the matching name for domains.
//Use the same as domains
filename = domains;
}else if (filename != "" && !domains.includes(",")){
//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.")
return;
} }
$.ajax({ return back;
url: "/api/acme/obtainCert",
method: "GET",
data: {
domains: domains,
filename: filename,
email: email,
ca: usingCa,
},
success: function(response) {
if (response.error) {
console.log("Error:", response.error);
// Show error message
msgbox(response.error, false, 12000);
} else {
console.log("Certificate installed successfully");
// Show success message
msgbox("Certificate installed successfully");
// Renew the parent certificate list
initManagedDomainCertificateList();
}
},
error: function(error) {
console.log("Failed to install certificate:", error);
}
});
} }
</script> </script>

View File

@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div class="six wide column statisticWrapper"> <div class="six wide column statisticWrapper">
<div class="ui greybackground statustab segment"> <div class="ui statustab segment">
<h5 class="ui header"> <h5 class="ui header">
<i class="exchange icon"></i> <i class="exchange icon"></i>
<div class="content"> <div class="content">
@ -29,7 +29,7 @@
</div> </div>
</h5> </h5>
<div class="ui divider"></div> <div class="ui divider"></div>
<h5 class="ui header"> <h5 class="ui header" style="margin-top: 0px;">
<i class="arrows alternate horizontal icon"></i> <i class="arrows alternate horizontal icon"></i>
<div class="content"> <div class="content">
<span id="forwardtype"></span> <span id="forwardtype"></span>
@ -39,32 +39,39 @@
</div> </div>
</h5> </h5>
<div class="ui divider"></div> <div class="ui divider"></div>
<h5 class="ui header"> <h5 class="ui header" style="margin-top: 0px;">
<i class="map marker alternate icon"></i> <i class="map marker alternate icon"></i>
<div class="content"> <div class="content">
<span id="country"></span> <span id="country"></span>
<div class="sub header" id="countryList"> <div class="sub header" id="countryList">
<i class="ui loading circle notch icon"></i> Resolving GeoIP
</div> </div>
</div> </div>
</h5> </h5>
</div> </div>
</div> </div>
</div> </div>
<div class="standardContainer" style="padding-bottom: 0 !important;">
<div id="networkActWrapper" class="standardContainer" style="position: relative; margin-top: 1em;"> <!-- Power Buttons-->
<button id="startbtn" class="ui basic button" onclick="startService();"><i class="ui green arrow alternate circle up icon"></i> Start Service</button>
<button id="stopbtn" class="ui basic notloopbackOnly disabled button" onclick="stopService();"><i class="ui red minus circle icon"></i> Stop Service</button>
<div class="ui divider"></div>
<h4>Network Status</h4>
<p>Overall Network I/O in Current Host Server</p>
</div>
<div id="networkActWrapper" class="standardContainer" style="position: relative;">
<canvas id="networkActivity"></canvas> <canvas id="networkActivity"></canvas>
</div> </div>
<div id="networkActivityPlaceHolder"> <div id="networkActivityPlaceHolder">
<p style="opacity: 0.5;"><i class="ui pause icon"></i> Graph Render Paused</p> <p style="opacity: 0.5;"> Graph Render Paused</p>
</div> </div>
<br>
<div class="standardContainer"> <div class="standardContainer">
<h4>Basic Settings</h4> <div class="ui divider"></div>
<h4>Global Settings</h4>
<p>Inbound Port (Port to be proxied)</p> <p>Inbound Port (Port to be proxied)</p>
<div class="ui action fluid notloopbackOnly input"> <div class="ui action fluid notloopbackOnly input">
<input type="text" id="incomingPort" placeholder="Incoming Port" value="80"> <input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
<button class="ui basic green notloopbackOnly button" onclick="handlePortChange();">Apply</button> <button class="ui basic notloopbackOnly button" onclick="handlePortChange();"><i class="ui green checkmark icon"></i> Apply</button>
</div> </div>
<br> <br>
<div id="tls" class="ui toggle notloopbackOnly checkbox"> <div id="tls" class="ui toggle notloopbackOnly checkbox">
@ -89,21 +96,22 @@
Advance Settings Advance Settings
</div> </div>
<div class="content"> <div class="content">
<p>If you have no idea what are these, you can leave them as default :)</p>
<div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;"> <div id="tlsMinVer" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
<input type="checkbox"> <input type="checkbox">
<label>Force TLS v1.2 or above<br> <label>Force TLS v1.2 or above<br>
<small>(Enhance security, but not compatible with legacy browsers)</small></label> <small>(Enhance security, but not compatible with legacy browsers)</small></label>
</div> </div>
<br> <br>
<div id="developmentMode" class="ui toggle checkbox" style="margin-top: 0.6em;">
<input type="checkbox">
<label>Development Mode<br>
<small>(Set Cache-Control to no-store so browser will always fetch new contents from your sites)</small></label>
</div>
<br>
</div> </div>
</div> </div>
</div> </div>
<br><br>
<button id="startbtn" class="ui teal button" onclick="startService();">Start Service</button>
<button id="stopbtn" class="ui red notloopbackOnly disabled button" onclick="stopService();">Stop Service</button>
<div id="rploopbackWarning" class="ui segment" style="display:none;"> <div id="rploopbackWarning" class="ui segment" style="display:none;">
<b><i class="yellow warning icon"></i> Loopback Routing Warning</b><br> <b><i class="yellow warning icon"></i> Loopback Routing Warning</b><br>
<small>This management interface is a loopback proxied service. <br>If you want to shutdown the reverse proxy server, please remove the proxy rule for the management interface and refresh.</small> <small>This management interface is a loopback proxied service. <br>If you want to shutdown the reverse proxy server, please remove the proxy rule for the management interface and refresh.</small>
@ -114,7 +122,7 @@
<div class="ui two column stackable grid"> <div class="ui two column stackable grid">
<div class="column"> <div class="column">
<p>Visitor Counts</p> <p>Visitor Counts</p>
<table class="ui unstackable inverted celled table"> <table class="ui unstackable very basic celled table">
<thead> <thead>
<tr> <tr>
<th>Country ISO Code</th> <th>Country ISO Code</th>
@ -130,7 +138,7 @@
</div> </div>
<div class="column"> <div class="column">
<p>Proxy Request Types</p> <p>Proxy Request Types</p>
<table class="ui unstackable inverted celled table"> <table class="ui unstackable very basic celled table">
<thead> <thead>
<tr> <tr>
<th>Proxy Type</th> <th>Proxy Type</th>
@ -175,13 +183,13 @@
} }
$("#serverstatus").addClass("green"); $("#serverstatus").addClass("green");
$("#statusTitle").text("Online"); $("#statusTitle").text("Online");
$("#rpStatusIcon").attr("class", "green circle check icon"); $("#rpStatusIcon").attr("class", "white circle check icon");
$("#statusText").text("Serving request on port: " + data.Option.Port); $("#statusText").text("Serving request on port: " + data.Option.Port);
}else{ }else{
$("#startbtn").removeClass("disabled"); $("#startbtn").removeClass("disabled");
$("#stopbtn").addClass("disabled"); $("#stopbtn").addClass("disabled");
$("#statusTitle").text("Offline"); $("#statusTitle").text("Offline");
$("#rpStatusIcon").attr("class", "black circle times icon") $("#rpStatusIcon").attr("class", "yellow moon icon")
$("#statusText").text("Reverse proxy server is offline"); $("#statusText").text("Reverse proxy server is offline");
$("#serverstatus").removeClass("green"); $("#serverstatus").removeClass("green");
} }
@ -433,6 +441,30 @@
} }
initTlsVersionSetting(); initTlsVersionSetting();
function initDevelopmentMode(){
$.get("/api/proxy/developmentMode", function(data){
if (data === true){
$("#developmentMode").checkbox("set checked")
}else{
$("#developmentMode").checkbox("set unchecked")
}
//Bind change events
$("#developmentMode").off("change").on("change", function(data){
let enableDevMode = ($(this).find("input[type='checkbox']")[0].checked);
$.get("/api/proxy/developmentMode?enable=" + enableDevMode, function(data){
if (enableDevMode){
msgbox("Development mode enabled");
}else{
msgbox("Development mode disabled");
}
});
});
});
}
initDevelopmentMode();
function initTlsSetting(){ function initTlsSetting(){
$.get("/api/cert/tls", function(data){ $.get("/api/cert/tls", function(data){
if (data == true){ if (data == true){
@ -561,10 +593,11 @@
{ {
type: 'line', type: 'line',
responsive: true, responsive: true,
resizeDelay: 100, resizeDelay: 300,
options: { options: {
animation: false, animation: false,
maintainAspectRatio: false, maintainAspectRatio: false,
bezierCurve: true,
tooltips: {enabled: false}, tooltips: {enabled: false},
hover: {mode: null}, hover: {mode: null},
//stepped: 'middle', //stepped: 'middle',
@ -606,18 +639,18 @@
{ {
label: 'Inbound', label: 'Inbound',
data: rxValues, data: rxValues,
borderColor: "#4d9dd9", borderColor: "#484bb8",
borderWidth: 2, borderWidth: 1,
backgroundColor: 'rgba(77, 157, 217, 0.2)', backgroundColor: 'rgba(72, 75, 184, 0.2)',
fill: true, fill: true,
pointStyle: false, pointStyle: false,
}, },
{ {
label: 'Outbound', label: 'Outbound',
data: txValues, data: txValues,
borderColor: '#ffe32b', borderColor: '#02a9c1',
borderWidth: 2, borderWidth: 1,
backgroundColor: 'rgba(255, 227, 43, 0.2)', backgroundColor: 'rgba(2, 169, 193, 0.2)',
fill: true, fill: true,
pointStyle: false, pointStyle: false,
} }

View File

@ -1,66 +0,0 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>Subdomain</h2>
<p>Subdomains are a way to organize and identify different sections of a website or domain. They are essentially a prefix to the main domain name, separated by a dot. <br>For example, in the domain "blog.example.com," "blog" is the subdomain.</p>
</div>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui celled sortable unstackable compact table">
<thead>
<tr>
<th>Matching Domain</th>
<th>Proxy To</th>
<th>Basic Auth</th>
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
</tr>
</thead>
<tbody id="subdList">
</tbody>
</table>
</div>
<button class="ui icon right floated basic button" onclick="listSubd();"><i class="green refresh icon"></i> Refresh</button>
<br><br>
</div>
<script>
function listSubd(){
$.get("/api/proxy/list?type=subd", function(data){
$("#subdList").html(``);
if (data.error !== undefined){
$("#subdList").append(`<tr>
<td data-label="" colspan="3"><i class="remove icon"></i> ${data.error}</td>
</tr>`);
}else if (data.length == 0){
$("#subdList").append(`<tr>
<td data-label="" colspan="3"><i class="checkmark icon"></i> No Subdomain Proxy Record</td>
</tr>`);
}else{
data.forEach(subd => {
let tlsIcon = "";
let subdData = encodeURIComponent(JSON.stringify(subd));
if (subd.RequireTLS){
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
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="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="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>
<button class="ui circular mini red basic icon button" onclick='deleteEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="trash icon"></i></button>
</td>
</tr>`);
});
}
});
}
//Bind on tab switch events
tabSwitchEventBind["subd"] = function(){
listSubd();
}
</script>

View File

@ -3,11 +3,33 @@
<h2>TCP Proxy</h2> <h2>TCP Proxy</h2>
<p>Proxy traffic flow on layer 3 via TCP/IP</p> <p>Proxy traffic flow on layer 3 via TCP/IP</p>
</div> </div>
<button class="ui basic orange button" id="addProxyConfigButton"><i class="ui add icon"></i> Add Proxy Config</button>
<button class="ui basic circular right floated icon button" onclick="initProxyConfigList();" title="Refresh List"><i class="ui green refresh icon"></i></button>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="ui basic segment" id="addproxyConfig" style="display:none;"> <div class="ui basic segment" style="margin-top: 0;">
<h3>TCP Proxy Config</h3> <h4>TCP Proxy Rules</h4>
<p>A list of TCP proxy rules created on this host. To enable them, use the toggle button on the right.</p>
<div style="overflow-x: auto; min-height: 400px;">
<table id="proxyTable" class="ui celled unstackable table">
<thead>
<tr>
<th>Name</th>
<th>Port/Addr A</th>
<th>Port/Addr B</th>
<th>Mode</th>
<th>Timeout (s)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<button class="ui basic right floated button" onclick="initProxyConfigList();" title="Refresh List"><i class="ui green refresh icon"></i>Refresh</button>
<br><br>
</div>
<div class="ui divider"></div>
<div class="ui basic segment" id="addproxyConfig">
<h4>Add or Edit TCP Proxy</h4>
<p>Create or edit a new proxy instance</p> <p>Create or edit a new proxy instance</p>
<form id="tcpProxyForm" class="ui form"> <form id="tcpProxyForm" class="ui form">
<div class="field" style="display:none;"> <div class="field" style="display:none;">
@ -39,11 +61,10 @@
<option value="starter">Starter</option> <option value="starter">Starter</option>
</select> </select>
</div> </div>
<button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui blue add icon"></i> Create</button> <button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>
<button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);"><i class="ui blue save icon"></i> Update</button> <button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);" style="display:none;"><i class="ui green check icon"></i> Update</button>
<button class="ui basic red button" onclick="event.preventDefault(); cancelTCPProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button> <button class="ui basic red button" onclick="event.preventDefault(); cancelTCPProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>
<div class="ui basic inverted segment" style="background-color: #414141; border-radius: 0.6em;"> <div class="ui basic inverted segment" style="background: var(--theme_background_inverted); border-radius: 0.6em;">
<h3>Proxy Mode Instructions</h3>
<p>TCP Proxy support the following TCP sockets proxy modes</p> <p>TCP Proxy support the following TCP sockets proxy modes</p>
<table class="ui celled padded inverted basic table"> <table class="ui celled padded inverted basic table">
<thead> <thead>
@ -108,28 +129,6 @@
</table> </table>
</div> </div>
</form> </form>
<div class="ui divider"></div>
</div>
<div class="ui basic segment" style="margin-top: 0;">
<h3>TCP Proxy Configs</h3>
<p>A list of TCP proxy configs created on this host. To enable them, use the toggle button on the right.</p>
<div style="overflow-x: auto; min-height: 400px;">
<table id="proxyTable" class="ui celled unstackable table">
<thead>
<tr>
<th>Name</th>
<th>Port/Addr A</th>
<th>Port/Addr B</th>
<th>Mode</th>
<th>Timeout (s)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
<script> <script>
@ -138,6 +137,13 @@
$("#tcpProxyForm .dropdown").dropdown(); $("#tcpProxyForm .dropdown").dropdown();
$('#tcpProxyForm').on('submit', function(event) { $('#tcpProxyForm').on('submit', function(event) {
event.preventDefault(); event.preventDefault();
//Check if update mode
if ($("#editTcpProxyButton").is(":visible")){
confirmEditTCPProxyConfig(event);
return;
}
var form = $(this); var form = $(this);
var formValid = validateTCPProxyConfig(form); var formValid = validateTCPProxyConfig(form);
@ -165,23 +171,16 @@
} }
}); });
}); });
//Add proxy button pressed. Show add TCP proxy menu
$("#addProxyConfigButton").on("click", function(){
$('#addproxyConfig').slideToggle('fast');
$("#addTcpProxyButton").show();
$("#editTcpProxyButton").hide();
});
function clearTCPProxyAddEditForm(){ function clearTCPProxyAddEditForm(){
$('#tcpProxyForm input, #tcpProxyForm select').val(''); $('#tcpProxyForm input, #tcpProxyForm select').val('');
$('#tcpProxyForm select').dropdown('clear'); $('#tcpProxyForm select').dropdown('clear');
} }
function cancelTCPProxyEdit(event) { function cancelTCPProxyEdit(event=undefined) {
clearTCPProxyAddEditForm(); clearTCPProxyAddEditForm();
$('#addproxyConfig').slideUp('fast'); $("#addTcpProxyButton").show();
$("#editTcpProxyButton").hide();
} }
function validateTCPProxyConfig(form){ function validateTCPProxyConfig(form){
@ -231,7 +230,7 @@
proxyConfigs.forEach(function(config) { proxyConfigs.forEach(function(config) {
var runningLogo = 'Stopped'; var runningLogo = 'Stopped';
var runningClass = "stopped"; var runningClass = "stopped";
var startButton = `<button onclick="startTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="play icon"></i> Start Proxy</button>`; var startButton = `<button onclick="startTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="green play icon"></i> Start Proxy</button>`;
if (config.Running){ if (config.Running){
runningLogo = 'Running'; runningLogo = 'Running';
startButton = `<button onclick="stopTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="red stop icon"></i> Stop Proxy</button>`; startButton = `<button onclick="stopTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="red stop icon"></i> Stop Proxy</button>`;
@ -354,8 +353,8 @@
msgbox("Config Updated"); msgbox("Config Updated");
} }
initProxyConfigList(); initProxyConfigList();
clearTCPProxyAddEditForm(); cancelTCPProxyEdit();
$("#addproxyConfig").slideUp("fast");
}, },
error: function() { error: function() {
msgbox('An error occurred while processing the request', false); msgbox('An error occurred while processing the request', false);

View File

@ -177,7 +177,7 @@
$("#utmrender").append(`<div class="ui basic segment statusbar"> $("#utmrender").append(`<div class="ui basic segment statusbar">
<div class="domain"> <div class="domain">
<div style="position: absolute; top: 0; right: 0.4em;"> <div style="position: absolute; top: 0; right: 0.4em;">
<p class="onlineStatus" style="display: inline-block; font-size: 1.3em; padding-right: 0.5em; padding-left: 0.3em; ${onlineStatusCss}">${currentOnlineStatus}</p> <p class="onlineStatus" style="display: inline-block; font-size: 1.2em; padding-right: 0.5em; padding-left: 0.3em; ${onlineStatusCss}">${currentOnlineStatus}</p>
</div> </div>
<div> <div>
<h3 class="ui header" style="margin-bottom: 0.2em;">${name}</h3> <h3 class="ui header" style="margin-bottom: 0.2em;">${name}</h3>

View File

@ -3,166 +3,179 @@
<h2>Utilities</h2> <h2>Utilities</h2>
<p>You might find these tools or information helpful when setting up your gateway server</p> <p>You might find these tools or information helpful when setting up your gateway server</p>
</div> </div>
<div class="ui divider"></div> <div class="ui top attached tabular menu">
<a class="nettools item active" data-tab="tab1"><i class="ui user circle blue icon"></i> Accounts</a>
<a class="nettools item" data-tab="tab2">Toolbox</a>
<div class="selfauthOnly"> <a class="nettools item" data-tab="tab3">System</a>
<h3>Account Management</h3> </div>
<p>Functions to help management the current account</p>
<div class="ui basic segment">
<h5><i class="chevron down icon"></i> Change Password</h5>
<div class="ui form">
<div class="field">
<label>Current Password</label>
<input type="password" name="oldPassword" placeholder="Current Password">
</div>
<div class="field">
<label>New Password</label>
<input type="password" name="newPassword" placeholder="New Password">
</div>
<div class="field">
<label>Confirm New Password</label>
<input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
</div>
<button class="ui basic button" onclick="changePassword()"><i class="ui teal key icon"></i> Change Password</button>
</div>
<div id="passwordChangeSuccMsg" class="ui green message" style="display:none;"> <div class="ui bottom attached tab segment nettoolstab active" data-tab="tab1">
<i class="ui circle checkmark green icon "></i> Password Updated <div class="extAuthOnly" style="display:none;">
<div class="ui basic segment">
<i class="ui green circle check icon"></i> Account options are not available due to -noauth flag is set to true.
</div> </div>
</div> </div>
<div class="ui divider"></div> <div class="selfauthOnly">
<h3>Forget Password Email</h3> <h3>Change Password</h3>
<p>The following SMTP settings help you to reset your password in case you have lost your management account.</p> <p>Update the current account credentials</p>
<form id="email-form" class="ui form"> <div class="ui basic segment">
<div class="field"> <h5><i class="chevron down icon"></i> Change Password</h5>
<label>Sender Address</label> <div class="ui form">
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.arozos.com"> <div class="field">
</div> <label>Current Password</label>
<div class="field"> <input type="password" name="oldPassword" placeholder="Current Password">
<p><i class="caret down icon"></i> Connection setup for email service provider</p>
<div class="fields">
<div class="twelve wide field">
<label>SMTP Provider Hostname</label>
<input type="text" name="hostname" placeholder="E.g. mail.gandi.net">
</div> </div>
<div class="field">
<div class="four wide field"> <label>New Password</label>
<label>Port</label> <input type="password" name="newPassword" placeholder="New Password">
<input type="number" name="port" placeholder="E.g. 587" value="587">
</div> </div>
<div class="field">
<label>Confirm New Password</label>
<input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
</div>
<button class="ui basic button" onclick="changePassword()"><i class="ui teal key icon"></i> Change Password</button>
</div>
<div id="passwordChangeSuccMsg" class="ui green message" style="display:none;">
<i class="ui circle checkmark green icon "></i> Password Updated
</div> </div>
</div> </div>
<div class="ui divider"></div>
<div class="field"> <h3>Forget Password Email</h3>
<p><i class="caret down icon"></i> Credentials for SMTP server authentications</p> <p>The following SMTP settings help you to reset your password in case you have lost your management account.</p>
<div class="two fields"> <form id="email-form" class="ui form">
<div class="field"> <div class="field">
<label>Sender Username</label> <label>Sender Address</label>
<input type="text" name="username" placeholder="E.g. admin"> <input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.arozos.com">
</div> </div>
<div class="field">
<div class="field"> <p><i class="caret down icon"></i> Connection setup for email service provider</p>
<label>Sender Domain</label> <div class="fields">
<div class="ui labeled input"> <div class="twelve wide field">
<div class="ui basic label"> <label>SMTP Provider Hostname</label>
@ <input type="text" name="hostname" placeholder="E.g. mail.gandi.net">
</div> </div>
<input type="text" name="domain" min="1" max="65534" placeholder="E.g. arozos.com">
<div class="four wide field">
<label>Port</label>
<input type="number" name="port" placeholder="E.g. 587" value="587">
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="field"> <div class="field">
<label>Sender Password</label> <p><i class="caret down icon"></i> Credentials for SMTP server authentications</p>
<input type="password" name="password" placeholder="Password of the email account"> <div class="two fields">
<small>Leave empty to use the old password</small> <div class="field">
</div> <label>Sender Username</label>
<input type="text" name="username" placeholder="E.g. admin">
<p> <i class="caret down icon"></i> Email for sending account reset link</p> </div>
<div class="field">
<label>Admin / Receiver Address</label> <div class="field">
<input type="text" name="recvAddr" placeholder="E.g. personalEmail@gmail.com"> <label>Sender Domain</label>
</div> <div class="ui labeled input">
<div class="ui basic label">
<button class="ui basic button" type="submit"><i class="blue save icon"></i> Set SMTP Configs</button> @
<button class="ui basic button" onclick="event.preventDefault(); sendTestEmail(this);"><i class="teal mail icon"></i> Send Test Email</button> </div>
</form> <input type="text" name="domain" min="1" max="65534" placeholder="E.g. arozos.com">
</div> </div>
<div class="ui divider"></div> </div>
<h3> IP Address to CIDR</h3> </div>
<p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p> </div>
<div class="ui basic segment"> <div class="field">
<h5><i class="chevron down icon"></i> IP Range to CIDR Conversion</h5> <label>Sender Password</label>
<div class="ui message"> <input type="password" name="password" placeholder="Password of the email account">
<i class="info circle icon"></i> Note that the CIDR generated here covers additional IP address before or after the given range. If you need more details settings, please use CIDR with a smaller range and add additional IPs for detail range adjustment. <small>Leave empty to use the old password</small>
</div> </div>
<div class="ui input">
<input type="text" placeholder="Start IP" id="startIpInput"> <p> <i class="caret down icon"></i> Email for sending account reset link</p>
</div> <div class="field">
<div class="ui input"> <label>Admin / Receiver Address</label>
<input type="text" placeholder="End IP" id="endIpInput"> <input type="text" name="recvAddr" placeholder="E.g. personalEmail@gmail.com">
</div> </div>
<br>
<button style="margin-top: 0.6em;" class="ui basic button" onclick="convertToCIDR()">Convert</button>
<p>Results: <div id="cidrOutput">N/A</div></p>
</div>
<div class="ui basic segment">
<h5><i class="chevron down icon"></i> CIDR to IP Range Conversion</h5>
<div class="ui action input">
<input type="text" placeholder="CIDR" id="cidrInput">
<button class="ui basic button" onclick="convertToIPRange()">Convert</button>
</div>
<p>Results: <div id="ipRangeOutput">N/A</div></p>
</div>
<!-- Config Tools -->
<div class="ui divider"></div>
<h3>System Backup & Restore</h3>
<p>Options related to system backup, migrate and restore.</p>
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
<!-- System Information -->
<div class="ui divider"></div>
<div id="zoraxyinfo">
<h3 class="ui header">
System Information
</h3>
<p>Basic information about this zoraxy host</p>
<table class="ui very basic collapsing celled table">
<tbody>
<tr>
<td>Host UUID</td>
<td class="uuid"></td>
</tr>
<tr>
<td>Version</td>
<td class="version"></td>
</tr>
<tr>
<td>Build</td>
<td class="development"></td>
</tr>
<tr>
<td>Running Since</td>
<td class="boottime"></td>
</tr>
<tr> <button class="ui basic button" type="submit"><i class="blue save icon"></i> Set SMTP Configs</button>
<td>ZeroTier Linked</td> <button class="ui basic button" onclick="event.preventDefault(); sendTestEmail(this);"><i class="teal mail icon"></i> Send Test Email</button>
<td class="zt"></td> </form>
</tr> </div>
<tr> </div>
<td>Enable SSH Loopback</td> <div class="ui bottom attached tab segment nettoolstab" data-tab="tab2">
<td class="sshlb"></td> <h3> IP Address to CIDR</h3>
</tr> <p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p>
</tbody> <div class="ui basic segment">
</table> <h5><i class="chevron down icon"></i> IP Range to CIDR Conversion</h5>
<p>Zoraxy is developed by tobychui for <a href="//imuslab.com" target="_blank">imuslab</a> and open source under <a href="https://www.gnu.org/licenses/agpl-3.0.txt">AGPL</a></p> <div class="ui message">
<i class="info circle icon"></i> Note that the CIDR generated here covers additional IP address before or after the given range. If you need more details settings, please use CIDR with a smaller range and add additional IPs for detail range adjustment.
</div>
<div class="ui input">
<input type="text" placeholder="Start IP" id="startIpInput">
</div>
<div class="ui input">
<input type="text" placeholder="End IP" id="endIpInput">
</div>
<br>
<button style="margin-top: 0.6em;" class="ui basic button" onclick="convertToCIDR()">Convert</button>
<p>Results: <div id="cidrOutput">N/A</div></p>
</div>
<div class="ui basic segment">
<h5><i class="chevron down icon"></i> CIDR to IP Range Conversion</h5>
<div class="ui action input">
<input type="text" placeholder="CIDR" id="cidrInput">
<button class="ui basic button" onclick="convertToIPRange()">Convert</button>
</div>
<p>Results: <div id="ipRangeOutput">N/A</div></p>
</div>
<div class="ui divider"></div>
</div>
<div class="ui bottom attached tab segment nettoolstab" data-tab="tab3">
<!-- Config Tools -->
<h3>System Backup & Restore</h3>
<p>Options related to system backup, migrate and restore.</p>
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
<div class="ui divider"></div>
<!-- System Information -->
<div id="zoraxyinfo">
<h3 class="ui header">
System Information
</h3>
<p>Basic information about this zoraxy host</p>
<table class="ui very basic collapsing celled table">
<tbody>
<tr>
<td>Host UUID</td>
<td class="uuid"></td>
</tr>
<tr>
<td>Version</td>
<td class="version"></td>
</tr>
<tr>
<td>Build</td>
<td class="development"></td>
</tr>
<tr>
<td>Running Since</td>
<td class="boottime"></td>
</tr>
<tr>
<td>ZeroTier Linked</td>
<td class="zt"></td>
</tr>
<tr>
<td>Enable SSH Loopback</td>
<td class="sshlb"></td>
</tr>
</tbody>
</table>
<p>Zoraxy is developed by tobychui for <a href="//imuslab.com" target="_blank">imuslab</a> and open source under <a href="https://www.gnu.org/licenses/agpl-3.0.txt">AGPL</a></p>
</div>
</div> </div>
<br> <br>
</div> </div>
<script> <script>
$('.menu .nettools.item').tab();
/* /*
Account Password utilities Account Password utilities
*/ */
@ -171,6 +184,7 @@
if (data == 0){ if (data == 0){
//Using external auth manager. Hide options //Using external auth manager. Hide options
$(".selfauthOnly").hide(); $(".selfauthOnly").hide();
$(".extAuthOnly").show();
} }
}); });

View File

@ -3,73 +3,420 @@
<h2>Virtual Directory</h2> <h2>Virtual Directory</h2>
<p>A virtual directory is a consolidated view of multiple directories that provides a unified entry point for users to access disparate sources.</p> <p>A virtual directory is a consolidated view of multiple directories that provides a unified entry point for users to access disparate sources.</p>
</div> </div>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;"> <div id="currentVirtualDirectoryAttachingHost" class="ui basic segment">
<table class="ui celled sortable unstackable compact table"> Select a host / routing rule to start editing Virtual Directory
<thead> </div>
<tr> <div class="ui stackable grid">
<th>Virtual Directory</th> <div class="six wide column">
<th>Proxy To</th> <h4>Select a Target Host / Site</h4>
<th>Basic Auth</th> <p>Attach Virtual Directory routing rule to root proxy router</p>
<th class="no-sort" style="min-width: 7.2em;">Actions</th> <div class="ui checkbox">
</tr> <input type="checkbox" id="useRootProxyRouterForVdir" onchange="handleVdirAttachTargetChange(this);">
</thead> <label>Root Proxy Router<br>
<tbody id="vdirList"> <small>Only applicable when Default Site is set to "Reverse Proxy" mode</small></label>
<tr> </div>
<td data-label=""><button class="ui circular mini red basic button"><i class="remove icon"></i> Remove Proxy</button></td> <div class="ui horizontal divider">OR</div>
</tr> <p>Create Virtual Directory routing in existing host / routing rule entries</p>
</tbody> <div class="ui fluid search selection dropdown">
</table> <input type="hidden" id="vdirBaseRoutingRule" name="vdirBaseRoutingRule" onchange="handleVdirAttachTargetChange();">
<i class="dropdown icon"></i>
<div class="default text">Select a host to edit</div>
<div class="menu" id="hostDomainList">
</div>
</div>
</div>
<div class="ten wide column">
<h4>Edit Virtual Directory Routing Rules</h4>
<p>The following are the list of Virtual Directories currently handled by the host router above</p>
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em;">
<table class="ui celled sortable unstackable compact table">
<thead>
<tr>
<th>Virtual Directory</th>
<th>Destination</th>
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
</tr>
</thead>
<tbody id="vdirList">
<tr>
<td data-label="" colspan="3">No Selected Host</td>
</tr>
</tbody>
</table>
</div>
<button class="ui icon right floated basic button" onclick="reloadVdirList();"><i class="green refresh icon"></i> Refresh</button>
<br><br>
<div class="ui divider"></div>
<div id="newVDSection" class="disabled section">
<h4>New Virtual Directory Rule</h4>
<form class="ui form">
<div class="field">
<label>Matching Path Prefix</label>
<input type="text" id="virtualDirectoryPath" placeholder="/mysite/">
<small>Path that follows your select host / domain, e.g. <code>/mysite/</code> as path prefix will forward all request that matches <code>mydomain.com/mysite/*</code></small>
</div>
<div class="field">
<label>Target IP Address or Domain Name with port</label>
<input type="text" id="virtualDirectoryDomain" onchange="updateVDTargetTLSState();">
<small>E.g. 192.168.0.101:8000 or example.com</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="vdReqTls">
<label>Proxy Target require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
</div>
</div>
<!-- Advance configs -->
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div id="advanceProxyRules" class="ui fluid accordion">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<p></p>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="vdSkipTLSValidation">
<label>Ignore TLS/SSL Verification Error<br><small>For targets that is using self-signed, expired certificate (Not Recommended)</small></label>
</div>
</div>
</div>
</div>
</div>
<button class="ui basic button" onclick="addVdirToHost(); event.preventDefault();"><i class="green add icon"></i> Create Virtual Directory</button>
</form>
</div>
</div>
</div> </div>
<button class="ui icon right floated basic button" onclick="listVdirs();"><i class="green refresh icon"></i> Refresh</button>
<br><br> <br><br>
</div> </div>
<script> <script>
//Virtual directories functions //Virtual directories functions
function listVdirs(){
$.get("/api/proxy/list?type=vdir", function(data){ //Initialize the list of hosts that can be used to attach vdirs config
$("#vdirList").html(``); function initVdirList(){
if (data.error !== undefined){ //Load the hosts into the dropdown
$("#vdirList").append(`<tr> $("#hostDomainList").html("");
<td data-label="" colspan="3"><i class="remove icon"></i> ${data.error}</td> $.get("/api/proxy/list?type=host", function(data){
</tr>`); if (data.error != undefined){
}else if (data.length == 0){ msgbox(data.error, false);
$("#vdirList").append(`<tr>
<td data-label="" colspan="3"><i class="checkmark icon"></i> No Virtual Directory Record</td>
</tr>`);
}else{ }else{
data.forEach(vdir => { if (data.length == 0){
let tlsIcon = ""; //No hosts found
let vdirData = encodeURIComponent(JSON.stringify(vdir)); }else{
if (vdir.RequireTLS){ data.forEach(proxyEndpoint => {
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`; let domain = proxyEndpoint.RootOrMatchingDomain;
if (vdir.SkipCertValidations){ $("#hostDomainList").append(`<div class="item" data-value="${domain}">${domain}</div>`);
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>` });
}
}
let tlsVerificationField = ""; //Update the dropdown events
if (vdir.RequireTLS){ $("#vdirBaseRoutingRule").parent().dropdown();
tlsVerificationField = !vdir.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>` }
}else{
tlsVerificationField = "N/A"
}
$("#vdirList").append(`<tr eptuuid="${vdir.RootOrMatchingDomain}" payload="${vdirData}" class="vdirEntry">
<td data-label="" editable="false">${vdir.RootOrMatchingDomain}</td>
<td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="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>
<button class="ui circular mini red basic icon button" onclick='deleteEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="trash icon"></i></button>
</td>
</tr>`);
});
} }
}); });
} }
function handleVdirAttachTargetChange(targetCheckbox=undefined){
if (targetCheckbox != undefined && targetCheckbox.checked){
$("#vdirBaseRoutingRule").parent().addClass("disabled");
//Load the vdir list for root
loadVdirList("root");
$("#newVDSection").removeClass("disabled");
}else{
$("#vdirBaseRoutingRule").parent().removeClass("disabled");
let selectedEndpointRule = $("#vdirBaseRoutingRule").val();
if (selectedEndpointRule != ""){
loadVdirList(selectedEndpointRule);
$("#newVDSection").removeClass("disabled");
}else{
$("#newVDSection").addClass("disabled");
}
}
}
//List the Vdir of the given endpoint, use "root" for root router
function loadVdirList(endpoint){
$("#currentVirtualDirectoryAttachingHost").html(`Editing Host: ${endpoint}`);
let reqURL = "/api/proxy/vdir/list?type=host&ep=" + endpoint;
if (endpoint == "root"){
//Load root endpoint vdir list
reqURL = "/api/proxy/vdir/list?type=root";
}
$.get(reqURL, function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
$("#vdirList").html("");
if (data.length == 0){
//No virtual directory for this host
$("#vdirList").append(`<tr>
<td data-label="" colspan="3"><i class="green check circle icon"></i> No Virtual Directory Routing Rule</td>
</tr>`);
}else{
//List the vdirs
console.log(data);
data.forEach(vdir => {
var tlsIcon = "";
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 payload = JSON.stringify(vdir).hexEncode();
$("#vdirList").append(`<tr vdirid="${vdir.MatchingPath.hexEncode()}" class="vdirEntry" payload="${payload}">
<td data-label="" editable="false" >${vdir.MatchingPath}</td>
<td data-label="" editable="true" datatype="domain">${vdir.Domain} ${tlsIcon}</td>
<td class="center aligned" editable="true" datatype="action" data-label="">
<button class="ui circular mini basic icon button editBtn" onclick='editVdir("${vdir.MatchingPath}", "${endpoint}")'><i class="edit icon"></i></button>
<button class="ui circular mini red basic icon button" onclick='deleteVdir("${vdir.MatchingPath}", "${endpoint}")'><i class="trash icon"></i></button>
</td>
</tr>`);
})
}
}
});
}
//Check if the entered domain require TLS
function updateVDTargetTLSState(){
var targetDomain = $("#virtualDirectoryDomain").val().trim();
if (targetDomain != ""){
$.ajax({
url: "/api/proxy/tlscheck",
data: {url: targetDomain},
success: function(data){
if (data.error != undefined){
}else if (data == "https"){
$("#vdReqTls").parent().checkbox("set checked");
}else if (data == "http"){
$("#vdReqTls").parent().checkbox("set unchecked");
}
}
});
}
}
function reloadVdirList(){
$("#vdirList").find(".editBtn").removeClass("disabled");
if ($("#useRootProxyRouterForVdir")[0].checked){
loadVdirList("root");
return;
}
let endpoint = $("#vdirBaseRoutingRule").val().trim();
if (endpoint != ""){
loadVdirList(endpoint);
}
}
//Create a virtual directory routing rule and attach to this endpoint
function addVdirToHost(){
var matchingPath = $("#virtualDirectoryPath").val().trim();
var targetDomain = $("#virtualDirectoryDomain").val().trim();
var reqTLS = $("#vdReqTls")[0].checked;
var skipTLSValidation = $("#vdSkipTLSValidation")[0].checked;
//Validate the input data
if (matchingPath == ""){
$("#virtualDirectoryPath").parent().addClass('error');
return;
}else{
$("#virtualDirectoryPath").parent().removeClass('error');
}
if (targetDomain == ""){
$("#virtualDirectoryDomain").parent().addClass('error');
return;
}else{
$("#virtualDirectoryDomain").parent().removeClass('error');
}
//Check if we are editing host
let epType = "host";
let endpoint = "root";
if ($("#useRootProxyRouterForVdir")[0].checked){
//Editing root virtual directory
epType = "root";
}else{
//Editing hosts virtual directory
endpoint = $("#vdirBaseRoutingRule").val().trim();
}
//Create a virtual directory endpoint
$.ajax({
url: "/api/proxy/vdir/add",
method: "POST",
data: {
"type": epType,
"endpoint": endpoint,
"path": matchingPath,
"domain":targetDomain,
"reqTLS":reqTLS,
"skipValid":skipTLSValidation,
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("New Virtual Directory rule added");
reloadVdirList();
resetVdirForm();
}
},
error: function(){
msgbox("Add Virtual Directory failed due to unknown reasons", false);
}
})
}
//Reset the vdir form
function resetVdirForm(){
$("#virtualDirectoryPath").val("");
$("#virtualDirectoryDomain").val("");
$("#vdReqTls").parent().checkbox("set unchecked");
$("#vdSkipTLSValidation").parent().checkbox("set unchecked");
}
//Remove Vdir
function deleteVdir(matchingPath, endpoint){
var epType = "host";
var path = $("#vdirBaseRoutingRule").val().trim();
if (endpoint.trim() == "root"){
epType = "root";
path = "";
}
$.ajax({
url: "/api/proxy/vdir/del",
method: "POST",
data: {
"type":epType,
"vdir": matchingPath,
"path": path
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Virtual Directory rule removed", true);
reloadVdirList();
}
}
})
}
function editVdir(matchingPath, ept){
let targetDOM = $(".vdirEntry[vdirid='" + matchingPath.hexEncode() + "']");
$("#vdirList").find(".editBtn").addClass("disabled");
let payload = $(targetDOM).attr("payload").hexDecode();
payload = JSON.parse(payload);
console.log(payload);
$(targetDOM).find("td[editable='true']").each(function(){
let datatype = $(this).attr("datatype");
let column = $(this);
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<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 == 'action'){
column.empty().append(`
<button title="Save" onclick="saveVdirInlineEdit('${payload.MatchingPath.hexEncode()}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui green save icon"></i></button>
<button title="Cancel" onclick="exitVdirInlineEdit();" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui remove icon"></i></button>
`);
}
});
}
function saveVdirInlineEdit(mathingPath){
mathingPath = mathingPath.hexDecode();
var epType = "host";
var path = $("#vdirBaseRoutingRule").val().trim();
if ($("#useRootProxyRouterForVdir")[0].checked){
epType = "root";
path = "";
}
//Load new setting from inline editor
let newDomain = $("#vdirList").find(".Domain").val();
let requireTLS = $("#vdirList").find(".RequireTLS")[0].checked;
let skipValidation = $("#vdirList").find(".SkipCertValidations")[0].checked;
//console.log(mathingPath, newDomain, requireTLS, skipValidation);
$.ajax({
url: "/api/proxy/vdir/edit",
method: "POST",
data: {
"type": epType,
"vdir": mathingPath,
"domain":newDomain,
"path":path,
"reqTLS":requireTLS,
"skipValid": skipValidation
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false);
}else{
msgbox("Virtual Directory rule updated", true);
exitVdirInlineEdit();
}
},
error: function(){
msgbox("unknown error occured", false)
}
})
}
function exitVdirInlineEdit(){
reloadVdirList();
}
//Bind on tab switch events //Bind on tab switch events
tabSwitchEventBind["vdir"] = function(){ tabSwitchEventBind["vdir"] = function(){
listVdirs(); initVdirList();
} }
initVdirList();
</script> </script>

View File

@ -4,7 +4,7 @@
<p>A simple static web server that serve html css and js files</p> <p>A simple static web server that serve html css and js files</p>
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
<div class="ui basic segment"> <div class="ui basic segment webservRunningStateWrapper">
<h4 class="ui header" id="webservRunningState"> <h4 class="ui header" id="webservRunningState">
<i class="green circle icon"></i> <i class="green circle icon"></i>
<div class="content"> <div class="content">
@ -102,12 +102,14 @@
function setWebServerRunningState(running){ function setWebServerRunningState(running){
if (running){ if (running){
$("#webserv_enable").parent().checkbox("set checked"); $("#webserv_enable").parent().checkbox("set checked");
$("#webservRunningState").find("i").attr("class", "green circle icon"); $("#webservRunningState").find("i").attr("class", "white circle check icon");
$("#webservRunningState").find(".webserv_status").text("Running"); $("#webservRunningState").find(".webserv_status").text("Running");
$(".webservRunningStateWrapper").addClass("enabled")
}else{ }else{
$("#webserv_enable").parent().checkbox("set unchecked"); $("#webserv_enable").parent().checkbox("set unchecked");
$("#webservRunningState").find("i").attr("class", "red circle icon"); $("#webservRunningState").find("i").attr("class", "white circle times icon");
$("#webservRunningState").find(".webserv_status").text("Stopped"); $("#webservRunningState").find(".webserv_status").text("Stopped");
$(".webservRunningStateWrapper").removeClass("enabled")
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 694 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -26,9 +26,12 @@
<div class="ui right floated buttons menutoggle" style="padding-top: 2px;"> <div class="ui right floated buttons menutoggle" style="padding-top: 2px;">
<button class="ui basic icon button" onclick="$('.toolbar').fadeToggle('fast');"><i class="content icon"></i></button> <button class="ui basic icon button" onclick="$('.toolbar').fadeToggle('fast');"><i class="content icon"></i></button>
</div> </div>
<div class="ui right floated buttons" style="padding-top: 2px;"> <div class="ui right floated buttons" style="padding-top: 2px; padding-right: 0.4em;">
<button class="ui basic icon button" onclick="logout();"><i class="sign-out icon"></i></button> <button class="ui basic white icon button" onclick="logout();"><i class="sign-out icon"></i></button>
</div> </div>
<!-- <div class="ui right floated buttons" style="padding-top: 2px;">
<button id="themeColorButton" class="ui icon button" onclick="toggleTheme();"><i class="sun icon"></i></button>
</div> -->
</div> </div>
<div class="wrapper"> <div class="wrapper">
<div class="toolbar"> <div class="toolbar">
@ -36,17 +39,21 @@
<a class="item active" tag="status"> <a class="item active" tag="status">
<i class="simplistic info circle icon"></i>Status <i class="simplistic info circle icon"></i>Status
</a> </a>
<a class="item" tag="setroot">
<i class="simplistic home icon"></i> Default Site
</a>
<div class="ui divider menudivider">Reverse Proxy</div>
<a class="item" tag="httprp">
<i class="simplistic sitemap icon"></i> HTTP Proxy
</a>
<a class="item" tag="vdir"> <a class="item" tag="vdir">
<i class="simplistic folder icon"></i> Virtual Directory <i class="simplistic folder icon"></i> Virtual Directory
</a> </a>
<a class="item" tag="subd">
<i class="simplistic sitemap icon"></i> Subdomain Proxy
</a>
<a class="item" tag="rules"> <a class="item" tag="rules">
<i class="simplistic plus square icon"></i> Create Proxy Rules <i class="simplistic plus square icon"></i> Create Proxy Rules
</a> </a>
<a class="item" tag="setroot"> <a class="item" tag="tcpprox">
<i class="simplistic home icon"></i> Set Proxy Root <i class="simplistic exchange icon"></i> TCP Proxy
</a> </a>
<div class="ui divider menudivider">Access & Connections</div> <div class="ui divider menudivider">Access & Connections</div>
<a class="item" tag="cert"> <a class="item" tag="cert">
@ -65,9 +72,6 @@
<a class="item" tag="zgrok"> <a class="item" tag="zgrok">
<i class="simplistic podcast icon"></i> Service Expose Proxy <i class="simplistic podcast icon"></i> Service Expose Proxy
</a> </a>
<a class="item" tag="tcpprox">
<i class="simplistic exchange icon"></i> TCP Proxy
</a>
<div class="ui divider menudivider">Others</div> <div class="ui divider menudivider">Others</div>
<a class="item" tag="webserv"> <a class="item" tag="webserv">
<i class="simplistic globe icon"></i> Static Web Server <i class="simplistic globe icon"></i> Static Web Server
@ -97,7 +101,7 @@
<div id="vdir" class="functiontab" target="vdir.html"></div> <div id="vdir" class="functiontab" target="vdir.html"></div>
<!-- Subdomain Proxy --> <!-- Subdomain Proxy -->
<div id="subd" class="functiontab" target="subd.html"></div> <div id="httprp" class="functiontab" target="httprp.html"></div>
<!-- Create Rules --> <!-- Create Rules -->
<div id="rules" class="functiontab" target="rules.html"></div> <div id="rules" class="functiontab" target="rules.html"></div>
@ -235,12 +239,22 @@
$.get("/api/auth/logout", function(response) { $.get("/api/auth/logout", function(response) {
if (response === "OK") { if (response === "OK") {
setTimeout(function(){ setTimeout(function(){
window.location.href = "/"; window.location.href = "/login.html";
}, 300); }, 300);
} }
}); });
} }
function toggleTheme(){
if ($("body").hasClass("darkTheme")){
$("body").removeClass("darkTheme")
$("#themeColorButton").html(`<i class="ui moon icon"></i>`);
}else{
$("body").addClass("darkTheme");
$("#themeColorButton").html(`<i class="ui sun icon"></i>`);
}
}
function getTabButtonById(targetTabId){ function getTabButtonById(targetTabId){
let targetTabBtn = undefined; let targetTabBtn = undefined;
$("#mainmenu").find(".item").each(function(){ $("#mainmenu").find(".item").each(function(){
@ -271,9 +285,11 @@
$(targetBtn).addClass("active"); $(targetBtn).addClass("active");
$(".functiontab").hide(); $(".functiontab").hide();
$("#" + tabID).fadeIn('fast', function(){ $("#" + tabID).fadeIn('fast', function(){
if (tabSwitchEventBind[tabID]){ setTimeout(function(){
tabSwitchEventBind[tabID](); if (tabSwitchEventBind[tabID]){
} tabSwitchEventBind[tabID]();
}
},100)
}); });
$('html,body').animate({scrollTop: 0}, 'fast'); $('html,body').animate({scrollTop: 0}, 'fast');
window.location.hash = tabID; window.location.hash = tabID;

View File

@ -7,48 +7,13 @@
<link rel="icon" type="image/png" href="./favicon.png" /> <link rel="icon" type="image/png" href="./favicon.png" />
<title>Login | Zoraxy</title> <title>Login | Zoraxy</title>
<link rel="stylesheet" href="script/semantic/semantic.min.css"> <link rel="stylesheet" href="script/semantic/semantic.min.css">
<link href="script/aos.css" rel="stylesheet">
<script src="script/aos.js"></script>
<script type="application/javascript" src="script/jquery-3.6.0.min.js"></script> <script type="application/javascript" src="script/jquery-3.6.0.min.js"></script>
<script type="application/javascript" src="script/semantic/semantic.min.js"></script> <script type="application/javascript" src="script/semantic/semantic.min.js"></script>
<style> <style>
body { body {
background: rgb(245,245,245); background: linear-gradient(60deg, rgba(84,58,183,1) 0%, rgba(0,172,193,1) 100%);
background: linear-gradient(28deg, rgba(245,245,245,1) 63%, rgba(255,255,255,1) 100%);
}
.background{
position: fixed;
top: 0;
right: 0;
height: 100%;
width: 100%;
opacity: 0.8;
z-index: -99;
background-image: url("img/public/bg.jpg");
background-size: auto 100%;
background-position: right top;
background-repeat: no-repeat;
overflow-x: hidden;
}
form {
margin:auto;
}
#loginForm{
height: 100%;
background-color: white;
width: 25em;
margin-left: 10em;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
@media all and (max-width: 550px) {
/* CSS rules here for screens lower than 750px */
#loginForm{
width: calc(100% - 4em);
margin-left: 2em;
}
} }
#errmsg{ #errmsg{
@ -65,13 +30,97 @@
.ui.fluid.button.registerOnly{ .ui.fluid.button.registerOnly{
display:none; display:none;
} }
#loginForm {
border-radius: 1em;
width: 25em;
height: 450px;
position: absolute; /*Can also be `fixed`*/
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
/*Solves a problem in which the content is being cut when the div is smaller than its' wrapper:*/
max-width: 100%;
max-height: 100%;
overflow: auto;
background-color: white;
}
.wavebase {
position:fixed;
bottom: 0;
left: 0;
width: 100%;
height:5vh;
text-align:center;
padding-top: 1em;
background-color: white;
}
/*
Waves CSS
*/
#wavesWrapper{
position: fixed;
bottom: 5vh;
width: 100%;
left: 0;
}
.waves {
position:relative;
width: 100%;
height:15vh;
margin-bottom:-7px; /*Fix for safari gap*/
min-height:100px;
max-height:150px;
}
.parallax > use {
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
}
.parallax > use:nth-child(1) {
animation-delay: -8s;
animation-duration: 28s;
}
.parallax > use:nth-child(2) {
animation-delay: -12s;
animation-duration: 40s;
}
.parallax > use:nth-child(3) {
animation-delay: -16s;
animation-duration: 52s;
}
.parallax > use:nth-child(4) {
animation-delay: -20s;
animation-duration: 80s;
}
@keyframes move-forever {
0% {
transform: translate3d(-90px,0,0);
}
100% {
transform: translate3d(85px,0,0);
}
}
/*Shrinking for mobile*/
@media (max-width: 768px) {
.waves {
height:40px;
min-height:40px;
}
}
</style> </style>
</head> </head>
<body> <body>
<div class="background"></div> <div id="loginForm" class="ui middle aligned center aligned grid" data-aos="fade-up">
<div id="loginForm" class="ui middle aligned center aligned grid"> <div class="column" style="padding-top: 0 !important;">
<div class="column"> <form class="ui large form content">
<form class="ui large form">
<div class="ui basic segment"> <div class="ui basic segment">
<img class="ui fluid image" src="img/public/logo.svg" style="pointer-events:none;"> <img class="ui fluid image" src="img/public/logo.svg" style="pointer-events:none;">
<p class="registerOnly">Account Setup</p> <p class="registerOnly">Account Setup</p>
@ -99,19 +148,37 @@
<label>Remember Me</label> <label>Remember Me</label>
</div> </div>
</div> </div>
<div id="loginbtn" class="ui fluid basic blue button loginOnly">Login</div> <div id="loginbtn" class="ui fluid basic button loginOnly"> <i class="ui blue sign-in icon"></i> Login</div>
<div id="regsiterbtn" class="ui fluid basic blue button registerOnly">Create</div> <div id="regsiterbtn" class="ui fluid basic button registerOnly"><i class="ui green checkmark icon"></i> Confirm</div>
<div id="errmsg"></div> <div id="errmsg"></div>
<div id="forgetPassword" class="field loginOnly" style="text-align: right;"> <div id="forgetPassword" class="field loginOnly" style="text-align: right; margin-top: 2em;">
<a href="#" onclick="sendResetAccountEmail();">Forget Password</a> <a href="#" onclick="sendResetAccountEmail();">Forget Password</a>
</div> </div>
</div> </div>
<div class="ui divider"></div>
<small>Proudly powered by Zoraxy</small>
</form> </form>
</div> </div>
</div> </div>
<div id="wavesWrapper">
<!-- CSS waves-->
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
</g>
</svg>
</div>
<div class="wavebase">
<p>Proudly powered by <a href="https://zoraxy.arozos.com" target="_blank">Zoraxy</a></p>
</div>
<script> <script>
AOS.init();
var redirectionAddress = "/"; var redirectionAddress = "/";
var loginAddress = "/api/auth/login"; var loginAddress = "/api/auth/login";
$(".checkbox").checkbox(); $(".checkbox").checkbox();

View File

@ -2,15 +2,65 @@
index.html style overwrite index.html style overwrite
*/ */
:root{ :root{
--theme_grey: #414141;
--theme_lgrey: #f6f6f6;
--theme_green: #3c9c63;
--theme_fcolor: #979797; --theme_background: linear-gradient(60deg, rgb(84, 58, 183) 0%, rgb(0, 172, 193) 100%);
--theme_advance: #f8f8f9; --theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
--theme_green: linear-gradient(270deg, #27e7ff, #00ca52);
--theme_red: linear-gradient(203deg, rgba(250,172,38,1) 17%, rgba(202,0,37,1) 78%);
} }
/* Theme Color Definations */
body:not(.darkTheme){
--theme_bg: #f6f6f6;
--theme_bg_primary: #ffffff;
--theme_bg_secondary: #ffffff;
--theme_bg_active: #ececec;
--theme_highlight: #a9d1f3;
--theme_bg_inverted: #27292d;
--theme_advance: #f8f8f9;
--item_color: #5e5d5d;
--item_color_select: rgba(0,0,0,.87);
--text_color: #414141;
--input_color: white;
--divider_color: #cacaca;
--text_color_inverted: #fcfcfc;
--button_text_color: #878787;
--button_border_color: #dedede;
}
body.darkTheme{
--theme_bg: #27292d;
--theme_bg_primary: #3d3f47;
--theme_bg_secondary: #373a42;
--theme_highlight: #6682c4;
--theme_bg_active: #292929;
--theme_bg_inverted: #f8f8f9;
--theme_advance: #333333;
--item_color: #cacaca;
--text_color: #fcfcfc;
--text_color_secondary: #dfdfdf;
--input_color: black;
--divider_color: #3b3b3b;
--item_color_select: rgba(255, 255, 255, 0.87);
--text_color_inverted: #414141;
--button_text_color: #e9e9e9;
--button_border_color: #646464;
}
/* Theme Toggle Css */
#themeColorButton{
background-color: black;
color: var(--text_color_inverted);
}
body.darkTheme #themeColorButton{
background-color: white;
}
body{ body{
background-color:#f6f6f6; background-color:var(--theme_bg);
color: #414141;
} }
.functiontab{ .functiontab{
@ -31,14 +81,14 @@ body{
padding: 0.4em; padding: 0.4em;
padding-left: 1.2em; padding-left: 1.2em;
padding-right: 1.2em; padding-right: 1.2em;
background-color: #f5f5f5; background-color: var(--theme_bg_secondary);
margin-bottom: 1em; margin-bottom: 1em;
border-bottom: 1px solid var(--theme_highlight);
position: fixed; position: fixed;
top: 0; top: 0;
width: 100%; width: 100%;
z-index: 10; z-index: 10;
box-shadow: 0px 1px 5px 0px rgba(38,38,38,0.26);
} }
.menubar .logo{ .menubar .logo{
@ -67,7 +117,7 @@ body{
display: inline-block; display: inline-block;
width: calc(100% - 240px); width: calc(100% - 240px);
vertical-align: top; vertical-align: top;
background-color: white; background-color: var(--theme_bg_primary);
border-radius: 1em; border-radius: 1em;
margin-right: 2em; margin-right: 2em;
} }
@ -76,19 +126,30 @@ body{
display: none !important; display: none !important;
} }
.rulesInstructions, .rulesInstructions span{
color: white !important;
}
.ui.divider{ .ui.divider{
font-weight: 300; font-weight: 300;
} }
.serverstatusWrapper{ .serverstatusWrapper{
padding-right: 0 !important; padding-right: 0 !important;
} }
.statisticWrapper{ .statisticWrapper{
margin-top: 1em;
padding-left: 0 !important; padding-left: 0 !important;
padding-bottom: 0 !important;
padding-right: 1em !important;
} }
.statisticWrapper .statustab{
margin-right: 1em;
}
/* Message Box */ /* Message Box */
#messageBox{ #messageBox{
position: fixed; position: fixed;
@ -204,14 +265,6 @@ body{
@media screen and (min-width: 750px) { @media screen and (min-width: 750px) {
#serverstatus{
border-top-left-radius: 1em !important;
}
.greybackground.statustab{
border-top-right-radius: 1em !important;
}
.standardContainer{ .standardContainer{
padding-left: 2.4em; padding-left: 2.4em;
padding-right: 2.4em; padding-right: 2.4em;
@ -221,7 +274,18 @@ body{
} }
@media screen and (max-width: 750px) { @media screen and (max-width: 748px) {
#serverstatus{
margin-left: 0 !important;
margin-right: 0 !important;
}
.statisticWrapper .statustab{
margin-left: 0 !important;
margin-right: 0 !important;
}
.toolbar { .toolbar {
position: fixed; position: fixed;
display: inline-block; display: inline-block;
@ -257,19 +321,10 @@ body{
padding: 0em; padding: 0em;
} }
.ui.grid > .stackable.stackable.row > .column, .ui.stackable.grid > .column.grid > .column, .ui.stackable.grid > .column.row > .column, .ui.stackable.grid > .column:not(.row), .ui.stackable.grid > .row > .column, .ui.stackable.grid > .row > .wide.column, .ui.stackable.grid > .wide.column.serverstatusWrapper {
padding: 0rem 0rem !important;
}
#serverstatus.green{ #serverstatus.green{
border-bottom: 0px solid transparent !important; border-bottom: 0px solid transparent !important;
} }
.greybackground.statustab{
border-top-right-radius: 0em !important;
padding: 2em 2em !important;
}
.standardContainer{ .standardContainer{
padding-left: 1.2em; padding-left: 1.2em;
padding-right: 1.2em; padding-right: 1.2em;
@ -298,11 +353,16 @@ body{
} }
.ui.menu .item{ .ui.menu .item{
color: #5e5d5d; color: var(--item_color);
} }
.ui.segment{
box-shadow: none !important;
}
.ui.secondary.vertical.menu .active.item{ .ui.secondary.vertical.menu .active.item{
background-color: #414141; background: var(--theme_background);
font-weight: 600; font-weight: 600;
color: white; color: white;
} }
@ -311,11 +371,15 @@ body{
animation: blinker 3s ease-in-out infinite; animation: blinker 3s ease-in-out infinite;
} }
.basic.segment.advanceoptions{
background-color: #f7f7f7;
border-radius: 1em;
}
.bluefont{ .bluefont{
color: #417ac1 !important; color: #417ac1 !important;
} }
@keyframes blinker { @keyframes blinker {
50% { 50% {
opacity: 50%; opacity: 50%;
@ -327,52 +391,26 @@ body{
*/ */
#serverstatus{ #serverstatus{
height: 100%; height: 100%;
border-radius: 1em;
margin: 1em;
} }
#statusTitle{ #statusTitle{
font-weight: 300; font-weight: 300;
} }
.statustab{
border-radius: 0 !important;
}
.greybackground.statustab{
background-color: #414141 !important;
color: white;
}
.greybackground.statustab .ui.header:not(:first-child){
margin-top: 1em;
}
.greybackground.statustab span,
.greybackground.statustab h1,
.greybackground.statustab h2,
.greybackground.statustab h3,
.greybackground.statustab h4,
.greybackground.statustab h5 {
color: white !important;
}
.greybackground.statustab .header{
color: #b7b7b7 !important;
}
#serverstatus.green{ #serverstatus.green{
background-color: #fefefe !important; background: linear-gradient(60deg, #27e7ff, #00ca52);
border-right: 5px solid #3d9c64;
} }
#serverstatus.green .sub.header{ #serverstatus.green .sub.header{
color: rgb(224, 224, 224); color: rgb(224, 224, 224);
} }
#serverstatus.green i, #serverstatus.green i,
#serverstatus.green #statusTitle{ #serverstatus.green #statusTitle{
color: #3d9c64; color: rgb(255, 255, 255);
} }
#serverstatus.green #statusText{ #serverstatus.green #statusText{
color: #2c583d; color: rgb(255, 255, 255);
} }
@ -381,22 +419,20 @@ body{
} }
#serverstatus:not(.green){ #serverstatus:not(.green){
background-color: white !important; background: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
background-image: url("img/plant.png");
background-position: right;
background-repeat: no-repeat;
background-size: auto 100%;
} }
#serverstatus:not(.green) #statusTitle, #serverstatus:not(.green) #statusTitle,
#serverstatus:not(.green) i, #serverstatus:not(.green) i,
#serverstatus:not(.green) .sub.header{ #serverstatus:not(.green) .sub.header{
color: #4c4c4c; color: white;
} }
.statustab{ .statustab{
min-height: 5.5em; min-height: 5.5em;
margin: 1em;
border-radius: 1em !important;
} }
#summaryTotalCount{ #summaryTotalCount{
@ -467,7 +503,7 @@ body{
transform: scale(1); transform: scale(1);
} }
50% { 50% {
background-color: #3d9c64; background-color: white;
transform: scale(1.5); transform: scale(1.5);
} }
100% { 100% {
@ -476,6 +512,23 @@ body{
} }
} }
/*
HTTP Proxy & Virtual Directory
*/
#currentVirtualDirectoryAttachingHost{
background: var(--theme_background);
color: white;
border-radius: 1em;
font-weight: bolder;
}
.section.disabled{
opacity: 0.5;
user-select: none;
pointer-events: none;
}
/* /*
Access Control Access Control
*/ */
@ -506,24 +559,70 @@ body{
} }
.tcproxConfig.running td:first-child{ .tcproxConfig.running td:first-child{
border-left: 0.6em solid #21ba45 !important; border-left: 0.6em solid #02cb59 !important;
} }
.tcproxConfig.stopped td:first-child{ .tcproxConfig.stopped td:first-child{
border-left: 0.6em solid #414141 !important; border-left: 0.6em solid #02032a !important;
} }
.tcproxConfig td:first-child .statusText{ .tcproxConfig td:first-child .statusText{
position: absolute; position: absolute;
bottom: 0.3em; bottom: 0.3em;
left: 0.2em; left: 0.2em;
font-size: 2em; font-size: 1.4em;
color:rgb(224, 224, 224); color:rgb(224, 224, 224);
opacity: 0.7; opacity: 0.7;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
} }
/*
ACME Renewer Status Panel
*/
.acmeRenewStateWrapper{
padding: 1em;
border-radius: 1em !important;
}
.acmeRenewStateWrapper .ui.header, .acmeRenewStateWrapper .sub.header{
color: white !important;
}
.acmeRenewStateWrapper:not(.enabled){
background: var(--theme_red) !important;
}
.acmeRenewStateWrapper.enabled{
background: var(--theme_green) !important;
}
/*
Static Web Server
*/
.webservRunningStateWrapper{
padding: 1em;
border-radius: 1em !important;
}
.webservRunningStateWrapper .ui.header, .webservRunningStateWrapper .sub.header{
color: white !important;
}
.webservRunningStateWrapper:not(.enabled){
background: var(--theme_red) !important;
}
.webservRunningStateWrapper.enabled{
background: var(--theme_green) !important;
}
/* /*
Uptime Monitor Uptime Monitor
*/ */

View File

@ -7,12 +7,14 @@
<link rel="icon" type="image/png" href="./favicon.png" /> <link rel="icon" type="image/png" href="./favicon.png" />
<title>Account Reset | Zoraxy</title> <title>Account Reset | Zoraxy</title>
<link rel="stylesheet" href="script/semantic/semantic.min.css"> <link rel="stylesheet" href="script/semantic/semantic.min.css">
<link href="script/aos.css" rel="stylesheet">
<script src="script/aos.js"></script>
<script type="application/javascript" src="script/jquery-3.6.0.min.js"></script> <script type="application/javascript" src="script/jquery-3.6.0.min.js"></script>
<script type="application/javascript" src="script/semantic/semantic.min.js"></script> <script type="application/javascript" src="script/semantic/semantic.min.js"></script>
<style> <style>
body { body {
background: rgb(245,245,245); background: rgb(38,60,71);
background: linear-gradient(28deg, rgba(245,245,245,1) 63%, rgba(255,255,255,1) 100%); background: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
} }
.background{ .background{
@ -34,23 +36,6 @@
margin:auto; margin:auto;
} }
#loginForm{
height: 100%;
background-color: white;
width: 25em;
margin-left: 10em;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
@media all and (max-width: 550px) {
/* CSS rules here for screens lower than 750px */
#loginForm{
width: calc(100% - 4em);
margin-left: 2em;
}
}
#errmsg{ #errmsg{
color: #9f3a38; color: #9f3a38;
margin-top: 1em; margin-top: 1em;
@ -61,18 +46,102 @@
.backBtn{ .backBtn{
position: absolute; position: absolute;
top: 0em; top: 0em;
left: 1em; left: 2em;
margin-top: -4em; transition: opacity 0.3s linear;
}
.backBtn:hover{
opacity: 0.8;
}
#loginForm {
border-radius: 1em;
width: 25em;
height: 550px;
position: absolute; /*Can also be `fixed`*/
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
/*Solves a problem in which the content is being cut when the div is smaller than its' wrapper:*/
max-width: 100%;
max-height: 100%;
overflow: auto;
background-color: white;
}
.wavebase {
position:fixed;
bottom: 0;
left: 0;
width: 100%;
height:5vh;
text-align:center;
padding-top: 1em;
background-color: white;
}
/*
Waves CSS
*/
#wavesWrapper{
position: fixed;
bottom: 5vh;
width: 100%;
left: 0;
}
.waves {
position:relative;
width: 100%;
height:15vh;
margin-bottom:-7px; /*Fix for safari gap*/
min-height:100px;
max-height:150px;
}
.parallax > use {
animation: move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite;
}
.parallax > use:nth-child(1) {
animation-delay: -8s;
animation-duration: 28s;
}
.parallax > use:nth-child(2) {
animation-delay: -12s;
animation-duration: 40s;
}
.parallax > use:nth-child(3) {
animation-delay: -16s;
animation-duration: 52s;
}
.parallax > use:nth-child(4) {
animation-delay: -20s;
animation-duration: 80s;
}
@keyframes move-forever {
0% {
transform: translate3d(-90px,0,0);
}
100% {
transform: translate3d(85px,0,0);
}
}
/*Shrinking for mobile*/
@media (max-width: 768px) {
.waves {
height:40px;
min-height:40px;
}
} }
</style> </style>
</head> </head>
<body> <body>
<div class="background"></div> <div id="loginForm" class="ui middle aligned center aligned grid" data-aos="fade-up">
<div id="loginForm" class="ui middle aligned center aligned grid">
<div class="column"> <div class="column">
<a class="backBtn" href="/">
<i class="huge black chevron circle left icon"></i>
</a>
<form class="ui large form"> <form class="ui large form">
<div class="ui basic segment"> <div class="ui basic segment">
<img class="ui fluid image" src="img/public/logo.svg" style="pointer-events:none;"> <img class="ui fluid image" src="img/public/logo.svg" style="pointer-events:none;">
@ -95,7 +164,7 @@
<input id="magic" type="password" name="New password" placeholder="New Password"> <input id="magic" type="password" name="New password" placeholder="New Password">
</div> </div>
</div> </div>
<div id="resetBtn" class="ui fluid basic green button">Set New Password</div> <div id="resetBtn" class="ui fluid basic button"><i class="ui green lock open icon"></i> Reset Password</div>
<div id="errmsg" class="ui red message" style="display: none;"> <div id="errmsg" class="ui red message" style="display: none;">
<i class="red remove icon"></i> Unknown Error Occured <i class="red remove icon"></i> Unknown Error Occured
</div> </div>
@ -107,12 +176,36 @@
<a href="#" id="resendEmailLink" onclick="sendResetAccountEmail();">Resend Email</a> <a href="#" id="resendEmailLink" onclick="sendResetAccountEmail();">Resend Email</a>
</div> </div>
</div> </div>
<div class="ui divider"></div>
<small>Proudly powered by Zoraxy</small>
</form> </form>
<a class="backBtn" href="/">
<i class="big chevron circle left icon" style="color: #121d37;"></i>
</a>
</div> </div>
</div>
<div id="wavesWrapper">
<!-- CSS waves-->
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
</g>
</svg>
</div>
<div class="wavebase">
<p>Proudly powered by <a href="https://zoraxy.arozos.com" target="_blank">Zoraxy</a></p>
</div> </div>
<script> <script>
AOS.init();
var redirectionAddress = "/"; var redirectionAddress = "/";
var loginAddress = "/api/auth/login"; var loginAddress = "/api/auth/login";
$(".checkbox").checkbox(); $(".checkbox").checkbox();
@ -155,6 +248,12 @@
var token = $('#token').val(); var token = $('#token').val();
var newPassword = $('#magic').val(); var newPassword = $('#magic').val();
if (token.trim() == ""){
$("#errmsg").html(`<i class="red circle times icon"></i> Token cannot be empty!`);
$("#errmsg").show();
return;
}
// Send POST request with input values as data // Send POST request with input values as data
$.post('/api/account/new', { username: username, token: token, newpw: newPassword }) $.post('/api/account/new', { username: username, token: token, newpw: newPassword })
.done(function(data) { .done(function(data) {

1
src/web/script/aos.css Normal file

File diff suppressed because one or more lines are too long

1
src/web/script/aos.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -107,6 +107,10 @@
asc: 'sorted ascending', asc: 'sorted ascending',
desc: 'sorted descending', desc: 'sorted descending',
compare: function(a, b) { compare: function(a, b) {
if (!isNaN(parseInt(a.trim())) && !isNaN(parseInt(b.trim())) ){
a = parseInt(a);
b = parseInt(b);
}
if (a > b) { if (a > b) {
return 1; return 1;
} else if (a < b) { } else if (a < b) {

View File

@ -191,6 +191,7 @@
success: function(data){ success: function(data){
if (data.error != undefined){ if (data.error != undefined){
parent.msgbox(data.error, false, 5000); parent.msgbox(data.error, false, 5000);
}else{ }else{
parent.msgbox("Email updated"); parent.msgbox("Email updated");
$(btn).html(`<i class="green check icon"></i>`); $(btn).html(`<i class="green check icon"></i>`);
@ -214,14 +215,18 @@
$("#enableCertAutoRenew").parent().checkbox("set unchecked"); $("#enableCertAutoRenew").parent().checkbox("set unchecked");
enableTrigerOnChangeEvent = true; enableTrigerOnChangeEvent = true;
} }
if (parent && parent.setACMEEnableStates){
parent.setACMEEnableStates(!enabled);
}
}else{ }else{
$("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast"); $("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
if (parent && parent.setACMEEnableStates){
parent.setACMEEnableStates(enabled);
}
} }
}); });
if (parent && parent.setACMEEnableStates){
parent.setACMEEnableStates(enabled);
}
} }

View File

@ -1,76 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
</head>
<body>
<br>
<div class="ui container">
<!-- Path Rules -->
<div class="ui header">
<div class="content">
Special Path Rules
<div class="sub header">Advanced customization for response on particular matching path or URL</div>
</div>
</div>
<h4>Current list of special path rules.</h4>
<div style="width: 100%; overflow-x: auto;">
<table class="ui sortable unstackable celled table" >
<thead>
<tr>
<th>Matching Path</th>
<th>Status Code</th>
<th class="no-sort">Exact Match</th>
<th class="no-sort">Case Sensitive</th>
<th class="no-sort">Enabled</th>
<th class="no-sort">Actions</th>
</tr>
</thead>
<tbody id="specialPathRules">
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="ui divider"></div>
<h4>Add Special Path Rule</h4>
<div class="ui form">
<div class="field">
<label>Matching URI</label>
<input type="text" name="matchingPath" placeholder="Matching URL">
<small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be handled by this rule, e.g. example.com/secret</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="exactMatch" tabindex="0" class="hidden">
<label>Require Exact Match</label>
</div>
<div class="ui message">
<p>Require exactly match but not prefix match (default). Enable this if you only want to block access to a directory but not the resources inside the directory. Assume you have entered a matching URI of <b>example.com/secret/</b> and set it to return 401</p>
<i class="check square outline icon"></i> example.com/secret<b>/image.png</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> (content of image.png)<br>
<i class="square outline icon"></i> example.com/secret<b>/image.png</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> HTTP 401
</div>
</div>
<div class="field">
<label>Response Status Code</label>
<input type="text"name="statusCode" placeholder="500">
<small><i class="ui circle info icon"></i> HTTP Status Code to be served by this rule</small>
</div>
</div>
<br><br>
<button class="ui basic button iframeOnly" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Cancel</button>
</div>
<script>
</script>
</body>
</html>

View File

@ -41,8 +41,7 @@
<input id="inlineEditBasicAuthCredPassword" type="password" placeholder="Password" autocomplete="off"> <input id="inlineEditBasicAuthCredPassword" type="password" placeholder="Password" autocomplete="off">
</div> </div>
<div class="field" > <div class="field" >
<button class="ui basic button" onclick="addCredentialsToEditingList();"><i class="blue add icon"></i> Add Credential</button> <button class="ui basic button" onclick="addCredentialsToEditingList();"><i class="green add icon"></i> Add Credential</button>
<button class="ui basic button" style="float: right;" onclick="saveCredentials();"><i class="green save icon"></i> Save Credential</button>
</div> </div>
<div class="ui divider"></div> <div class="ui divider"></div>
</div> </div>
@ -69,7 +68,7 @@
<small>Make sure you add the tailing slash for only selecting the files / folder inside that path.</small> <small>Make sure you add the tailing slash for only selecting the files / folder inside that path.</small>
</div> </div>
<div class="field" > <div class="field" >
<button class="ui basic button" onclick="addExceptionPath();"><i class="blue add icon"></i> Add Exception</button> <button class="ui basic button" onclick="addExceptionPath();"><i class="yellow add icon"></i> Add Exception</button>
</div> </div>
<div class="field"> <div class="field">
<div class="ui basic message"> <div class="ui basic message">
@ -99,7 +98,7 @@
let payloadHash = window.location.hash.substr(1); let payloadHash = window.location.hash.substr(1);
try{ try{
payloadHash = JSON.parse(decodeURIComponent(payloadHash)); payloadHash = JSON.parse(decodeURIComponent(payloadHash));
loadBasicAuthCredentials(payloadHash.ept, payloadHash.ep); loadBasicAuthCredentials(payloadHash.ep);
$("#epname").text(payloadHash.ep); $("#epname").text(payloadHash.ep);
editingEndpoint = payloadHash; editingEndpoint = payloadHash;
}catch(ex){ }catch(ex){
@ -107,13 +106,12 @@
} }
} }
function loadBasicAuthCredentials(endpointType, uuid){ function loadBasicAuthCredentials(uuid){
$.ajax({ $.ajax({
url: "/api/proxy/updateCredentials", url: "/api/proxy/updateCredentials",
method: "GET", method: "GET",
data: { data: {
ep: uuid, ep: uuid,
ptype: endpointType
}, },
success: function(data){ success: function(data){
//Push the existing account to list //Push the existing account to list
@ -163,6 +161,9 @@
// Update the table body with the credentials // Update the table body with the credentials
updateEditingCredentialList(); updateEditingCredentialList();
//Save the table
saveCredentials();
} }
function addExceptionPath(){ function addExceptionPath(){
@ -175,7 +176,6 @@
$.ajax({ $.ajax({
url: "/api/proxy/auth/exceptions/add", url: "/api/proxy/auth/exceptions/add",
data:{ data:{
ptype: editingEndpoint.ept,
ep: editingEndpoint.ep, ep: editingEndpoint.ep,
prefix: newExclusionPathMatchingPrefix prefix: newExclusionPathMatchingPrefix
}, },
@ -197,7 +197,6 @@
$.ajax({ $.ajax({
url: "/api/proxy/auth/exceptions/delete", url: "/api/proxy/auth/exceptions/delete",
data:{ data:{
ptype: editingEndpoint.ept,
ep: editingEndpoint.ep, ep: editingEndpoint.ep,
prefix: matchingPrefix prefix: matchingPrefix
}, },
@ -271,6 +270,8 @@
// Update the table body // Update the table body
updateEditingCredentialList(); updateEditingCredentialList();
saveCredentials();
} }
function alreadyExists(username){ function alreadyExists(username){
@ -293,7 +294,6 @@
method: "POST", method: "POST",
data: { data: {
ep: editingEndpoint.ep, ep: editingEndpoint.ep,
ptype: editingEndpoint.ept,
creds: JSON.stringify(editingCredentials) creds: JSON.stringify(editingCredentials)
}, },
success: function(data){ success: function(data){

View File

@ -45,7 +45,7 @@
</b></p> </b></p>
<form class="ui form" id="uploadForm" action="/api/conf/import" method="POST" enctype="multipart/form-data"> <form class="ui form" id="uploadForm" action="/api/conf/import" method="POST" enctype="multipart/form-data">
<input type="file" name="file" id="fileInput" accept=".zip"> <input type="file" name="file" id="fileInput" accept=".zip">
<button style="margin-top: 0.6em;" class="ui basic button" type="submit"><i class="ui green upload icon"></i> Upload</button> <button style="margin-top: 0.6em;" class="ui basic button" type="submit"><i class="ui green upload icon"></i> Restore & Exit</button>
</form> </form>
<small>The current config will be backup to ./conf_old{timestamp}. If you screw something up, you can always restore it by ssh to your host and restore the configs from the old folder.</small> <small>The current config will be backup to ./conf_old{timestamp}. If you screw something up, you can always restore it by ssh to your host and restore the configs from the old folder.</small>
<br><br> <br><br>

View File

@ -0,0 +1,182 @@
<!DOCTYPE html>
<html>
<head>
<!-- Notes: This should be open in its original path-->
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
</head>
<body>
<br>
<div class="ui container">
<div class="ui header">
<div class="content">
Custom Headers
<div class="sub header" id="epname"></div>
</div>
</div>
<div class="ui divider"></div>
<p>You can define custom headers to be sent
together with the client request to the backend server in
this reverse proxy endpoint / host.</p>
<table class="ui very basic compacted unstackable celled table">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th>Remove</th>
</tr></thead>
<tbody id="headerTable">
<tr>
<td colspan="3"><i class="ui green circle check icon"></i> No Additonal Header</td>
</tr>
</tbody>
</table>
<div class="ui divider"></div>
<h4>Add Custom Header</h4>
<p>Add custom header(s) into this proxy target</p>
<div class="scrolling content ui form">
<div class="three small fields credentialEntry">
<div class="field">
<input id="headerName" type="text" placeholder="X-Custom-Header" autocomplete="off">
</div>
<div class="field">
<input id="headerValue" type="text" placeholder="value1,value2,value3" autocomplete="off">
</div>
<div class="field" >
<button class="ui basic button" onclick="addCustomHeader();"><i class="green add icon"></i> Add Header</button>
</div>
<div class="ui divider"></div>
</div>
</div>
<div class="ui divider"></div>
<div class="field" >
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
</div>
</div>
<br><br><br><br>
<script>
let editingEndpoint = {};
if (window.location.hash.length > 1){
let payloadHash = window.location.hash.substr(1);
try{
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
$("#epname").text(payloadHash.ep);
editingEndpoint = payloadHash;
}catch(ex){
console.log("Unable to load endpoint data from hash")
}
}
function closeThisWrapper(){
parent.hideSideWrapper(true);
}
//$("#debug").text(JSON.stringify(editingEndpoint));
function addCustomHeader(){
let name = $("#headerName").val().trim();
let value = $("#headerValue").val().trim();
if (name == ""){
$("#headerName").parent().addClass("error");
return
}else{
$("#headerName").parent().removeClass("error");
}
if (value == ""){
$("#headerValue").parent().addClass("error");
return
}else{
$("#headerValue").parent().removeClass("error");
}
$.ajax({
url: "/api/proxy/header/add",
data: {
"type": editingEndpoint.ept,
"domain": editingEndpoint.ep,
"name": name,
"value": value
},
success: function(data){
if (data.error != undefined){
if (parent != undefined && parent.msgbox != undefined){
parent.msgbox(data.error,false);
}else{
alert(data.error);
}
}else{
listCustomHeaders();
if (parent != undefined && parent.msgbox != undefined){
parent.msgbox("Custom header added",true);
}
//Clear the form
$("#headerName").val("");
$("#headerValue").val("");
}
}
});
}
function deleteCustomHeader(name){
$.ajax({
url: "/api/proxy/header/remove",
data: {
"type": editingEndpoint.ept,
"domain": editingEndpoint.ep,
"name": name,
},
success: function(data){
listCustomHeaders();
if (parent != undefined && parent.msgbox != undefined){
parent.msgbox("Custom header removed",true);
}
}
});
}
function listCustomHeaders(){
$("#headerTable").html(`<tr><td colspan="3"><i class="ui loading spinner icon"></i> Loading</td></tr>`);
$.ajax({
url: "/api/proxy/header/list",
data: {
"type": editingEndpoint.ept,
"domain": editingEndpoint.ep,
},
success: function(data){
if (data.error != undefined){
alert(data.error);
}else{
$("#headerTable").html("");
data.forEach(header => {
$("#headerTable").append(`
<tr>
<td>${header.Key}</td>
<td>${header.Value}</td>
<td><button class="ui basic circular mini red icon button" onclick="deleteCustomHeader('${header.Key}');"><i class="ui trash icon"></i></button></td>
</tr>
`);
});
if (data.length == 0){
$("#headerTable").html(`<tr>
<td colspan="3"><i class="ui green circle check icon"></i> No Additonal Header</td>
</tr>`);
}
}
},
});
}
listCustomHeaders();
</script>
</body>
</html>

View File

@ -20,6 +20,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
@ -119,11 +120,10 @@ func UpdateUptimeMonitorTargets() {
// Generate uptime monitor targets from reverse proxy rules // Generate uptime monitor targets from reverse proxy rules
func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Target { func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Target {
subds := dp.GetSDProxyEndpointsAsMap() hosts := dp.GetProxyEndpointsAsMap()
vdirs := dp.GetVDProxyEndpointsAsMap()
UptimeTargets := []*uptime.Target{} UptimeTargets := []*uptime.Target{}
for subd, target := range subds { for hostid, target := range hosts {
url := "http://" + target.Domain url := "http://" + target.Domain
protocol := "http" protocol := "http"
if target.RequireTLS { if target.RequireTLS {
@ -131,27 +131,31 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
protocol = "https" protocol = "https"
} }
//Add the root url
UptimeTargets = append(UptimeTargets, &uptime.Target{ UptimeTargets = append(UptimeTargets, &uptime.Target{
ID: subd, ID: hostid,
Name: subd, Name: hostid,
URL: url, URL: url,
Protocol: protocol, Protocol: protocol,
}) })
}
for vdir, target := range vdirs { //Add each virtual directory into the list
url := "http://" + target.Domain for _, vdir := range target.VirtualDirectories {
protocol := "http" url := "http://" + vdir.Domain
if target.RequireTLS { protocol := "http"
url = "https://" + target.Domain if target.RequireTLS {
protocol = "https" url = "https://" + vdir.Domain
protocol = "https"
}
//Add the root url
UptimeTargets = append(UptimeTargets, &uptime.Target{
ID: hostid + vdir.MatchingPath,
Name: hostid + vdir.MatchingPath,
URL: url,
Protocol: protocol,
})
} }
UptimeTargets = append(UptimeTargets, &uptime.Target{
ID: vdir,
Name: "*" + vdir,
URL: url,
Protocol: protocol,
})
} }
return UptimeTargets return UptimeTargets
@ -167,6 +171,48 @@ func HandleUptimeMonitorListing(w http.ResponseWriter, r *http.Request) {
} }
} }
/*
Static Web Server
*/
// Handle port change, if root router is using internal static web server
// update the root router as well
func HandleStaticWebServerPortChange(w http.ResponseWriter, r *http.Request) {
newPort, err := utils.PostInt(r, "port")
if err != nil {
utils.SendErrorResponse(w, "invalid port number given")
return
}
if dynamicProxyRouter.Root.DefaultSiteOption == dynamicproxy.DefaultSite_InternalStaticWebServer {
//Update the root site as well
newDraftingRoot := dynamicProxyRouter.Root.Clone()
newDraftingRoot.Domain = "127.0.0.1:" + strconv.Itoa(newPort)
activatedNewRoot, err := dynamicProxyRouter.PrepareProxyRoute(newDraftingRoot)
if err != nil {
utils.SendErrorResponse(w, "unable to update root routing rule")
return
}
//Replace the root
dynamicProxyRouter.Root = activatedNewRoot
SaveReverseProxyConfig(newDraftingRoot)
}
err = staticWebServer.ChangePort(strconv.Itoa(newPort))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}
/*
mDNS Scanning
*/
// Handle listing current registered mdns nodes // Handle listing current registered mdns nodes
func HandleMdnsListing(w http.ResponseWriter, r *http.Request) { func HandleMdnsListing(w http.ResponseWriter, r *http.Request) {
if mdnsScanner == nil { if mdnsScanner == nil {