56
README.md
@ -2,18 +2,25 @@
|
||||
|
||||
# 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
|
||||
|
||||
- Simple to use interface with detail in-system instructions
|
||||
- Reverse Proxy
|
||||
- Subdomain Reverse Proxy
|
||||
- Virtual Directory Reverse Proxy
|
||||
- Virtual Directory
|
||||
- Basic Auth
|
||||
- Custom Headers
|
||||
- Redirection Rules
|
||||
- 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)
|
||||
- TCP Tunneling / Proxy
|
||||
- Integrated Up-time Monitor
|
||||
- Web-SSH Terminal
|
||||
- Utilities
|
||||
@ -83,8 +90,6 @@ Usage of zoraxy:
|
||||
Disable authentication for management interface
|
||||
-port string
|
||||
Management web interface listening port (default ":8000")
|
||||
-rpt string
|
||||
Reserved
|
||||
-sshlb
|
||||
Allow loopback web ssh connection (DANGER)
|
||||
-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.*
|
||||
|
||||
#### 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
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
More screenshots on the wikipage [Screenshots](https://github.com/tobychui/zoraxy/wiki/Screenshots)!
|
||||
|
||||
## FAQ
|
||||
@ -194,5 +166,5 @@ If you like the project and want to support us, please consider a donation. You
|
||||
|
||||
## 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.**
|
||||
|
||||
|
@ -3,8 +3,6 @@ FROM docker.io/golang:alpine
|
||||
ARG VERSION
|
||||
|
||||
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/ &&\
|
||||
mkdir -p /opt/zoraxy/config/ &&\
|
||||
|
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 161 KiB |
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 72 KiB |
@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
strip "github.com/grokify/html-strip-tags-go"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -115,7 +117,7 @@ func handleListWhitelisted(w http.ResponseWriter, r *http.Request) {
|
||||
bltype = "country"
|
||||
}
|
||||
|
||||
resulst := []string{}
|
||||
resulst := []*geodb.WhitelistEntry{}
|
||||
if bltype == "country" {
|
||||
resulst = geodbStore.GetAllWhitelistedCountryCode()
|
||||
} else if bltype == "ip" {
|
||||
@ -134,7 +136,10 @@ func handleCountryWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddCountryCodeToWhitelist(countryCode)
|
||||
comment, _ := utils.PostPara(r, "comment")
|
||||
comment = strip.StripTags(comment)
|
||||
|
||||
geodbStore.AddCountryCodeToWhitelist(countryCode, comment)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -158,7 +163,10 @@ func handleIpWhitelistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
|
@ -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)")
|
||||
}
|
||||
|
||||
//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
|
||||
acmeHandler.HandleRenewCertificate(w, r)
|
||||
|
||||
|
17
src/api.go
@ -56,9 +56,16 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
||||
authRouter.HandleFunc("/api/proxy/listenPort80", HandleUpdatePort80Listener)
|
||||
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
||||
//Reverse proxy root related APIs
|
||||
authRouter.HandleFunc("/api/proxy/root/listOptions", HandleRootRouteOptionList)
|
||||
authRouter.HandleFunc("/api/proxy/root/updateOptions", HandleRootRouteOptionsUpdate)
|
||||
authRouter.HandleFunc("/api/proxy/developmentMode", HandleDevelopmentModeChange)
|
||||
//Reverse proxy virtual directory APIs
|
||||
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
|
||||
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
|
||||
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/detail", ganManager.HandleNetworkDetails)
|
||||
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/ip", ganManager.HandleMemberIP)
|
||||
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/start", staticWebServer.HandleStartServer)
|
||||
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)
|
||||
if *allowWebFileManager {
|
||||
//Web Directory Manager file operation functions
|
||||
|
@ -51,7 +51,7 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
results := []*CertInfo{}
|
||||
|
||||
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")
|
||||
fileInfo, err := os.Stat(certFilepath)
|
||||
if err != nil {
|
||||
@ -248,7 +248,7 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if keytype == "pub" {
|
||||
overWriteFilename = domain + ".crt"
|
||||
overWriteFilename = domain + ".pem"
|
||||
} else if keytype == "pri" {
|
||||
overWriteFilename = domain + ".key"
|
||||
} else {
|
||||
@ -287,6 +287,9 @@ func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Update cert list
|
||||
tlsCertManager.UpdateLoadedCertList()
|
||||
|
||||
// send response
|
||||
fmt.Fprintln(w, "File upload successful!")
|
||||
}
|
||||
|
172
src/config.go
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -35,97 +36,118 @@ type Record struct {
|
||||
BasicAuthExceptionRules []*dynamicproxy.BasicAuthExceptionRule
|
||||
}
|
||||
|
||||
// Save a reverse proxy config record to file
|
||||
func SaveReverseProxyConfigToFile(proxyConfigRecord *Record) error {
|
||||
//TODO: Make this accept new def types
|
||||
os.MkdirAll("./conf/proxy/", 0775)
|
||||
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
|
||||
|
||||
//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)
|
||||
/*
|
||||
Load Reverse Proxy Config from file and append it to current runtime proxy router
|
||||
*/
|
||||
func LoadReverseProxyConfig(configFilepath string) error {
|
||||
//Load the config file from disk
|
||||
endpointConfig, err := os.ReadFile(configFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return SaveReverseProxyConfigToFile(recordToSave)
|
||||
}
|
||||
|
||||
func RemoveReverseProxyConfigFile(rootname string) error {
|
||||
filename := getFilenameFromRootName(rootname)
|
||||
removePendingFile := strings.ReplaceAll(filepath.Join("./conf/proxy/", filename), "\\", "/")
|
||||
SystemWideLogger.Println("Config Removed: ", removePendingFile)
|
||||
if utils.FileExists(removePendingFile) {
|
||||
err := os.Remove(removePendingFile)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unabel to remove config file", err)
|
||||
return err
|
||||
}
|
||||
//Parse it into dynamic proxy endpoint
|
||||
thisConfigEndpoint := dynamicproxy.ProxyEndpoint{}
|
||||
err = json.Unmarshal(endpointConfig, &thisConfigEndpoint)
|
||||
if err != nil {
|
||||
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 ptype, rootname and proxyTarget, error if any
|
||||
func LoadReverseProxyConfig(filename string) (*Record, error) {
|
||||
thisRecord := Record{
|
||||
ProxyType: "",
|
||||
Rootname: "",
|
||||
ProxyTarget: "",
|
||||
UseTLS: false,
|
||||
func filterProxyConfigFilename(filename string) string {
|
||||
//Filter out wildcard characters
|
||||
filename = strings.ReplaceAll(filename, "*", "(ST)")
|
||||
filename = strings.ReplaceAll(filename, "?", "(QM)")
|
||||
filename = strings.ReplaceAll(filename, "[", "(OB)")
|
||||
filename = strings.ReplaceAll(filename, "]", "(CB)")
|
||||
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,
|
||||
SkipTlsValidation: false,
|
||||
SkipCertValidations: false,
|
||||
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
|
||||
RequireBasicAuth: false,
|
||||
BasicAuthCredentials: []*dynamicproxy.BasicAuthCredentials{},
|
||||
BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{},
|
||||
}
|
||||
|
||||
configContent, err := os.ReadFile(filename)
|
||||
DefaultSiteOption: dynamicproxy.DefaultSite_InternalStaticWebServer,
|
||||
DefaultSiteValue: "",
|
||||
})
|
||||
if err != nil {
|
||||
return &thisRecord, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Unmarshal the content into config
|
||||
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
|
||||
return rootProxyEndpoint, nil
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -10,9 +10,11 @@ require (
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.5.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/microcosm-cc/bluemonday v1.0.25
|
||||
golang.org/x/net v0.14.0
|
||||
golang.org/x/sys v0.11.0
|
||||
golang.org/x/text v0.12.0
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
)
|
||||
|
@ -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/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
|
||||
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-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
|
27
src/main.go
@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/aroz"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
@ -35,6 +34,7 @@ import (
|
||||
)
|
||||
|
||||
// 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 showver = flag.Bool("version", false, "Show version of this server")
|
||||
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 (
|
||||
name = "Zoraxy"
|
||||
version = "2.6.8"
|
||||
version = "3.0.0"
|
||||
nodeUUID = "generic"
|
||||
development = false //Set this to false to use embedded web fs
|
||||
bootTime = time.Now().Unix()
|
||||
@ -63,7 +63,6 @@ var (
|
||||
/*
|
||||
Handler Modules
|
||||
*/
|
||||
handler *aroz.ArozHandler //Handle arozos managed permission system
|
||||
sysdb *database.Database //System database
|
||||
authAgent *auth.AuthAgent //Authentication agent
|
||||
tlsCertManager *tlscert.Manager //TLS / SSL management
|
||||
@ -128,20 +127,8 @@ func ShutdownSeq() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
|
||||
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
|
||||
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},
|
||||
})
|
||||
|
||||
//Parse startup flags
|
||||
flag.Parse()
|
||||
if *showver {
|
||||
fmt.Println(name + " - Version " + version)
|
||||
os.Exit(0)
|
||||
@ -166,7 +153,7 @@ func main() {
|
||||
startupSequence()
|
||||
|
||||
//Initiate management interface APIs
|
||||
requireAuth = !(*noauth || handler.IsUsingExternalPermissionManager())
|
||||
requireAuth = !(*noauth)
|
||||
initAPIs()
|
||||
|
||||
//Start the reverse proxy server in go routine
|
||||
@ -179,8 +166,8 @@ func main() {
|
||||
//Start the finalize sequences
|
||||
finalSequence()
|
||||
|
||||
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + handler.Port)
|
||||
err = http.ListenAndServe(handler.Port, nil)
|
||||
SystemWideLogger.Println("Zoraxy started. Visit control panel at http://localhost" + *webUIPort)
|
||||
err = http.ListenAndServe(*webUIPort, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -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
|
||||
// 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 {
|
||||
log.Println(err)
|
||||
return false, err
|
||||
|
@ -1,9 +1,6 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -26,11 +23,6 @@ import (
|
||||
- Vitrual Directory Routing
|
||||
*/
|
||||
|
||||
var (
|
||||
//go:embed tld.json
|
||||
rawTldMap []byte
|
||||
)
|
||||
|
||||
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
/*
|
||||
Special Routing Rules, bypass most of the limitations
|
||||
@ -53,10 +45,12 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Inject headers
|
||||
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
|
||||
|
||||
/*
|
||||
General Access Check
|
||||
*/
|
||||
|
||||
respWritten := h.handleAccessRouting(w, r)
|
||||
if respWritten {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Virtual Directory Routing
|
||||
*/
|
||||
//Clean up the request URI
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath)
|
||||
if targetProxyEndpoint != nil {
|
||||
if targetProxyEndpoint.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, targetProxyEndpoint)
|
||||
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && !sep.Disabled {
|
||||
if sep.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, sep)
|
||||
if err != nil {
|
||||
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 + "/")
|
||||
if potentialProxtEndpoint != nil {
|
||||
//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.
|
||||
*/
|
||||
func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
domainOnly := r.Host
|
||||
if strings.Contains(r.Host, ":") {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
|
||||
if h.Parent.RootRoutingOptions.EnableRedirectForUnsetRules {
|
||||
//Route to custom domain
|
||||
if h.Parent.RootRoutingOptions.UnsetRuleRedirectTarget == "" {
|
||||
//Not set. Redirect to first level of domain redirectable
|
||||
fld, err := h.getTopLevelRedirectableDomain(domainOnly)
|
||||
if err != nil {
|
||||
//Redirect to proxy root
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
} else {
|
||||
log.Println("[Router] Redirecting request from " + domainOnly + " to " + fld)
|
||||
h.logRequest(r, false, 307, "root-redirect", domainOnly)
|
||||
http.Redirect(w, r, fld, http.StatusTemporaryRedirect)
|
||||
}
|
||||
//Get the proxy root config
|
||||
proot := h.Parent.Root
|
||||
switch proot.DefaultSiteOption {
|
||||
case DefaultSite_InternalStaticWebServer:
|
||||
fallthrough
|
||||
case DefaultSite_ReverseProxy:
|
||||
//They both share the same behavior
|
||||
|
||||
//Check if any virtual directory rules matches
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
targetProxyEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath)
|
||||
if targetProxyEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
//Virtual directory routing rule found. Route via vdir mode
|
||||
h.vdirRequest(w, r, targetProxyEndpoint)
|
||||
return
|
||||
} else if h.isTopLevelRedirectableDomain(domainOnly) {
|
||||
//This is requesting a top level private domain that should be serving root
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
} else {
|
||||
//Validate the redirection target URL
|
||||
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)
|
||||
} else if !strings.HasSuffix(proxyingPath, "/") && proot.ProxyType != ProxyType_Root {
|
||||
potentialProxtEndpoint := proot.GetVirtualDirectoryHandlerFromRequestURI(proxyingPath + "/")
|
||||
if potentialProxtEndpoint != nil && !targetProxyEndpoint.Disabled {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||
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
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
|
||||
//No vdir match. Route via root router
|
||||
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 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")
|
||||
}
|
||||
|
@ -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()
|
||||
if !ok {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
@ -48,7 +44,7 @@ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
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.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
|
@ -60,6 +60,7 @@ type ResponseRewriteRuleSet struct {
|
||||
ProxyDomain string
|
||||
OriginalHost string
|
||||
UseTLS bool
|
||||
NoCache bool
|
||||
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.
|
||||
if c := header.Get("Connection"); c != "" {
|
||||
for _, f := range strings.Split(c, ",") {
|
||||
@ -260,9 +261,16 @@ func removeHeaders(header http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
if header.Get("A-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("A-Upgrade"))
|
||||
header.Del("A-Upgrade")
|
||||
//Restore the Upgrade header if any
|
||||
if header.Get("Zr-Origin-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")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 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.
|
||||
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.
|
||||
removeHeaders(res.Header)
|
||||
removeHeaders(res.Header, rrr.NoCache)
|
||||
|
||||
if p.ModifyResponse != nil {
|
||||
if err := p.ModifyResponse(res); err != nil {
|
||||
|
@ -22,27 +22,19 @@ import (
|
||||
|
||||
func NewDynamicProxy(option RouterOption) (*Router, error) {
|
||||
proxyMap := sync.Map{}
|
||||
domainMap := sync.Map{}
|
||||
thisRouter := Router{
|
||||
Option: &option,
|
||||
ProxyEndpoints: &proxyMap,
|
||||
SubdomainEndpoint: &domainMap,
|
||||
Running: false,
|
||||
server: nil,
|
||||
routingRules: []*RoutingRule{},
|
||||
tldMap: map[string]int{},
|
||||
Option: &option,
|
||||
ProxyEndpoints: &proxyMap,
|
||||
Running: false,
|
||||
server: nil,
|
||||
routingRules: []*RoutingRule{},
|
||||
tldMap: map[string]int{},
|
||||
}
|
||||
|
||||
thisRouter.mux = &ProxyHandler{
|
||||
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
|
||||
}
|
||||
|
||||
@ -76,21 +68,14 @@ func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
|
||||
func (router *Router) StartProxyService() error {
|
||||
//Create a new server object
|
||||
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
|
||||
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
|
||||
if router.Option.ForceTLSLatest {
|
||||
minVersion = tls.VersionTLS12
|
||||
@ -101,16 +86,6 @@ func (router *Router) StartProxyService() error {
|
||||
}
|
||||
|
||||
if router.Option.UseTls {
|
||||
/*
|
||||
//Serve with TLS mode
|
||||
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
router.Running = false
|
||||
return err
|
||||
}
|
||||
router.tlsListener = ln
|
||||
*/
|
||||
router.server = &http.Server{
|
||||
Addr: ":" + strconv.Itoa(router.Option.Port),
|
||||
Handler: router.mux,
|
||||
@ -129,7 +104,7 @@ func (router *Router) StartProxyService() error {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
sep := router.getSubdomainProxyEndpointFromHostname(domainOnly)
|
||||
sep := router.getProxyEndpointFromHostname(domainOnly)
|
||||
if sep != nil && sep.BypassGlobalTLS {
|
||||
//Allow routing via non-TLS handler
|
||||
originalHostHeader := r.Host
|
||||
@ -140,7 +115,7 @@ func (router *Router) StartProxyService() error {
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
sep.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
sep.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: sep.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: sep.RequireTLS,
|
||||
@ -225,7 +200,7 @@ func (router *Router) StartProxyService() error {
|
||||
|
||||
func (router *Router) StopProxyService() error {
|
||||
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)
|
||||
defer cancel()
|
||||
@ -253,13 +228,13 @@ func (router *Router) StopProxyService() error {
|
||||
// Restart the current router if it is running.
|
||||
func (router *Router) Restart() error {
|
||||
//Stop the router if it is already running
|
||||
var err error = nil
|
||||
if router.Running {
|
||||
err := router.StopProxyService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
// Start the server
|
||||
err = router.StartProxyService()
|
||||
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 = strings.Split(hostname, ":")[0]
|
||||
subdEndpoint := router.getSubdomainProxyEndpointFromHostname(hostname)
|
||||
subdEndpoint := router.getProxyEndpointFromHostname(hostname)
|
||||
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
|
||||
*/
|
||||
func (router *Router) LoadProxy(ptype string, key string) (*ProxyEndpoint, error) {
|
||||
if ptype == "vdir" {
|
||||
proxy, ok := router.ProxyEndpoints.Load(key)
|
||||
if !ok {
|
||||
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)
|
||||
func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
|
||||
var targetProxyEndpoint *ProxyEndpoint
|
||||
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
key, ok := key.(string)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
@ -409,13 +273,32 @@ func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
m[k] = v
|
||||
|
||||
if key == matchingDomain {
|
||||
targetProxyEndpoint = v
|
||||
}
|
||||
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)
|
||||
r.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
k, ok := key.(string)
|
||||
|
158
src/mod/dynamicproxy/endpoints.go
Normal 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)
|
||||
}
|
@ -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()
|
||||
}
|
@ -6,6 +6,8 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
@ -28,13 +30,41 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
|
||||
return targetProxyEndpoint
|
||||
}
|
||||
|
||||
func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||
func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
|
||||
var targetSubdomainEndpoint *ProxyEndpoint = nil
|
||||
ep, ok := router.SubdomainEndpoint.Load(hostname)
|
||||
ep, ok := router.ProxyEndpoints.Load(hostname)
|
||||
if ok {
|
||||
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
|
||||
}
|
||||
|
||||
@ -54,14 +84,22 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
|
||||
return rewrittenURL
|
||||
}
|
||||
|
||||
// Handle subdomain request
|
||||
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
// Handle host request
|
||||
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-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()
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//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
|
||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||
//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)
|
||||
}
|
||||
|
||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
NoCache: h.Parent.Option.NoCache,
|
||||
PathPrefix: "",
|
||||
})
|
||||
|
||||
@ -113,15 +152,23 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
// Handle vdir type request
|
||||
func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
rewriteURL := h.Parent.rewriteURL(target.RootOrMatchingDomain, r.RequestURI)
|
||||
func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, target *VirtualDirectoryEndpoint) {
|
||||
rewriteURL := h.Parent.rewriteURL(target.MatchingPath, r.RequestURI)
|
||||
r.URL, _ = url.Parse(rewriteURL)
|
||||
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
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" {
|
||||
//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
|
||||
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
|
||||
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
|
||||
@ -144,11 +191,11 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
|
||||
r.URL, _ = url.Parse(originalHostHeader)
|
||||
}
|
||||
|
||||
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
err := target.proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
ProxyDomain: target.Domain,
|
||||
OriginalHost: originalHostHeader,
|
||||
UseTLS: target.RequireTLS,
|
||||
PathPrefix: target.RootOrMatchingDomain,
|
||||
PathPrefix: target.MatchingPath,
|
||||
})
|
||||
|
||||
var dnsError *net.DNSError
|
||||
|
@ -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
|
||||
}
|
110
src/mod/dynamicproxy/router.go
Normal 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()
|
||||
}
|
@ -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
|
||||
}
|
@ -14,8 +14,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ProxyType_Subdomain = 0
|
||||
ProxyType_Vdir = 1
|
||||
ProxyType_Root = 0
|
||||
ProxyType_Host = 1
|
||||
ProxyType_Vdir = 2
|
||||
)
|
||||
|
||||
type ProxyHandler struct {
|
||||
@ -24,9 +25,11 @@ type ProxyHandler struct {
|
||||
|
||||
type RouterOption struct {
|
||||
HostUUID string //The UUID of Zoraxy, use for heading mod
|
||||
HostVersion string //The version of Zoraxy, use for heading mod
|
||||
Port int //Incoming port
|
||||
UseTls bool //Use TLS to serve incoming requsts
|
||||
ForceTLSLatest bool //Force TLS1.2 or above
|
||||
NoCache bool //Force set Cache-Control: no-store
|
||||
ListenOnPort80 bool //Enable port 80 http listener
|
||||
ForceHttpsRedirect bool //Force redirection of http to https endpoint
|
||||
TlsManager *tlscert.Manager
|
||||
@ -37,16 +40,14 @@ type RouterOption struct {
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
Option *RouterOption
|
||||
ProxyEndpoints *sync.Map
|
||||
SubdomainEndpoint *sync.Map
|
||||
Running bool
|
||||
Root *ProxyEndpoint
|
||||
RootRoutingOptions *RootRoutingOptions
|
||||
mux http.Handler
|
||||
server *http.Server
|
||||
tlsListener net.Listener
|
||||
routingRules []*RoutingRule
|
||||
Option *RouterOption
|
||||
ProxyEndpoints *sync.Map
|
||||
Running bool
|
||||
Root *ProxyEndpoint
|
||||
mux http.Handler
|
||||
server *http.Server
|
||||
tlsListener net.Listener
|
||||
routingRules []*RoutingRule
|
||||
|
||||
tlsRedirectStop chan bool //Stop channel for tls redirection server
|
||||
tldMap map[string]int //Top level domain map, see tld.json
|
||||
@ -69,63 +70,70 @@ type BasicAuthExceptionRule struct {
|
||||
PathPrefix string
|
||||
}
|
||||
|
||||
// A proxy endpoint record
|
||||
type ProxyEndpoint struct {
|
||||
ProxyType int //The type of this proxy, see const def
|
||||
RootOrMatchingDomain string //Root for vdir or Matching domain for subd, also act as key
|
||||
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
|
||||
// User defined headers to add into a proxy endpoint
|
||||
type UserDefinedHeader struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
// 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
|
||||
type RootOptions struct {
|
||||
ProxyLocation string //Proxy Root target, all unset traffic will be forward to here
|
||||
RequireTLS bool //Proxy root target require TLS connection (not recommended)
|
||||
BypassGlobalTLS bool //Bypass global TLS setting and make root http only (not recommended)
|
||||
SkipCertValidations bool //Skip cert validation, suitable for self-signed certs, CURRENTLY NOT USED
|
||||
|
||||
//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
|
||||
}
|
||||
const (
|
||||
DefaultSite_InternalStaticWebServer = 0
|
||||
DefaultSite_ReverseProxy = 1
|
||||
DefaultSite_Redirect = 2
|
||||
DefaultSite_NotFoundPage = 3
|
||||
)
|
||||
|
||||
/*
|
||||
Web Templates
|
||||
|
@ -207,7 +207,7 @@ func (m *NetworkManager) HandleSetRanges(w http.ResponseWriter, r *http.Request)
|
||||
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) {
|
||||
netid, err := utils.GetPara(r, "netid")
|
||||
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) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
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) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
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) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
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) {
|
||||
netid, err := utils.PostPara(r, "netid")
|
||||
if err != nil {
|
||||
@ -426,3 +426,79 @@ func (m *NetworkManager) HandleMemberDelete(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ type MemberInfo struct {
|
||||
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) {
|
||||
url := "http://localhost:" + strconv.Itoa(apiPort) + "/status"
|
||||
|
||||
@ -187,7 +187,7 @@ func (m *NetworkManager) createNetwork() (*NetworkInfo, error) {
|
||||
return &networkInfo, nil
|
||||
}
|
||||
|
||||
//List network details
|
||||
// List network details
|
||||
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)
|
||||
if err != nil {
|
||||
@ -249,7 +249,7 @@ func (m *NetworkManager) setNetworkInfoByID(networkId string, newNetworkInfo *Ne
|
||||
return nil
|
||||
}
|
||||
|
||||
//List network IDs
|
||||
// List network IDs
|
||||
func (m *NetworkManager) listNetworkIds() ([]string, error) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/", nil)
|
||||
if err != nil {
|
||||
@ -281,7 +281,7 @@ func (m *NetworkManager) listNetworkIds() ([]string, error) {
|
||||
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 {
|
||||
networkIds, err := m.listNetworkIds()
|
||||
if err != nil {
|
||||
@ -297,7 +297,7 @@ func (m *NetworkManager) networkExists(networkId string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//delete a network
|
||||
// delete a network
|
||||
func (m *NetworkManager) deleteNetwork(networkID string) error {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
||||
client := &http.Client{}
|
||||
@ -330,8 +330,8 @@ func (m *NetworkManager) deleteNetwork(networkID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Configure network
|
||||
//Example: configureNetwork(netid, "192.168.192.1", "192.168.192.254", "192.168.192.0/24")
|
||||
// Configure network
|
||||
// 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 {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + networkID + "/"
|
||||
data := map[string]interface{}{
|
||||
@ -545,7 +545,7 @@ func (m *NetworkManager) memberExistsInNetwork(netid string, memid string) bool
|
||||
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) {
|
||||
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(m.apiPort)+"/controller/network/"+netid+"/member/"+memberid, nil)
|
||||
if err != nil {
|
||||
@ -573,7 +573,7 @@ func (m *NetworkManager) getNetworkMemberInfo(netid string, memberid string) (*M
|
||||
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 {
|
||||
url := "http://localhost:" + strconv.Itoa(m.apiPort) + "/controller/network/" + netid + "/member/" + memberid
|
||||
payload := []byte(`{"authorized": true}`)
|
||||
@ -600,7 +600,7 @@ func (m *NetworkManager) AuthorizeMember(netid string, memberid string, setAutho
|
||||
return nil
|
||||
}
|
||||
|
||||
//Delete a member from the network
|
||||
// Delete a member from the network
|
||||
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)
|
||||
if err != nil {
|
||||
@ -620,3 +620,45 @@ func (m *NetworkManager) deleteMember(netid string, memid string) error {
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package geodb
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
Whitelist.go
|
||||
@ -8,11 +11,29 @@ import "strings"
|
||||
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
|
||||
|
||||
func (s *Store) AddCountryCodeToWhitelist(countryCode string) {
|
||||
func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) {
|
||||
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) {
|
||||
@ -22,20 +43,19 @@ func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
|
||||
|
||||
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
var isWhitelisted bool = false
|
||||
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
|
||||
return isWhitelisted
|
||||
return s.sysdb.KeyExists("whitelist-cn", countryCode)
|
||||
}
|
||||
|
||||
func (s *Store) GetAllWhitelistedCountryCode() []string {
|
||||
whitelistedCountryCode := []string{}
|
||||
func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry {
|
||||
whitelistedCountryCode := []*WhitelistEntry{}
|
||||
entries, err := s.sysdb.ListTable("whitelist-cn")
|
||||
if err != nil {
|
||||
return whitelistedCountryCode
|
||||
}
|
||||
for _, keypairs := range entries {
|
||||
ip := string(keypairs[0])
|
||||
whitelistedCountryCode = append(whitelistedCountryCode, ip)
|
||||
thisWhitelistEntry := WhitelistEntry{}
|
||||
json.Unmarshal(keypairs[1], &thisWhitelistEntry)
|
||||
whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry)
|
||||
}
|
||||
|
||||
return whitelistedCountryCode
|
||||
@ -43,8 +63,14 @@ func (s *Store) GetAllWhitelistedCountryCode() []string {
|
||||
|
||||
//IP Whitelist
|
||||
|
||||
func (s *Store) AddIPToWhiteList(ipAddr string) {
|
||||
s.sysdb.Write("whitelist-ip", ipAddr, true)
|
||||
func (s *Store) AddIPToWhiteList(ipAddr string, comment string) {
|
||||
thisIpEntry := WhitelistEntry{
|
||||
EntryType: EntryType_IP,
|
||||
IP: ipAddr,
|
||||
Comment: comment,
|
||||
}
|
||||
|
||||
s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry)
|
||||
}
|
||||
|
||||
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
|
||||
@ -52,14 +78,14 @@ func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
|
||||
}
|
||||
|
||||
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
|
||||
var isWhitelisted bool = false
|
||||
s.sysdb.Read("whitelist-ip", ipAddr, &isWhitelisted)
|
||||
isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr)
|
||||
if isWhitelisted {
|
||||
//single IP whitelist entry
|
||||
return true
|
||||
}
|
||||
|
||||
//Check for IP wildcard and CIRD rules
|
||||
AllWhitelistedIps := s.GetAllWhitelistedIp()
|
||||
AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice()
|
||||
for _, whitelistRules := range AllWhitelistedIps {
|
||||
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
|
||||
if wildcardMatch {
|
||||
@ -75,17 +101,29 @@ func (s *Store) IsIPWhitelisted(ipAddr string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Store) GetAllWhitelistedIp() []string {
|
||||
whitelistedIp := []string{}
|
||||
func (s *Store) GetAllWhitelistedIp() []*WhitelistEntry {
|
||||
whitelistedIp := []*WhitelistEntry{}
|
||||
entries, err := s.sysdb.ListTable("whitelist-ip")
|
||||
if err != nil {
|
||||
return whitelistedIp
|
||||
}
|
||||
|
||||
for _, keypairs := range entries {
|
||||
ip := string(keypairs[0])
|
||||
whitelistedIp = append(whitelistedIp, ip)
|
||||
//ip := string(keypairs[0])
|
||||
thisEntry := WhitelistEntry{}
|
||||
json.Unmarshal(keypairs[1], &thisEntry)
|
||||
whitelistedIp = append(whitelistedIp, &thisEntry)
|
||||
}
|
||||
|
||||
return whitelistedIp
|
||||
}
|
||||
|
||||
func (s *Store) GetAllWhitelistedIpAsStringSlice() []string {
|
||||
allWhitelistedIPs := []string{}
|
||||
entries := s.GetAllWhitelistedIp()
|
||||
for _, entry := range entries {
|
||||
allWhitelistedIPs = append(allWhitelistedIPs, entry.IP)
|
||||
}
|
||||
|
||||
return allWhitelistedIPs
|
||||
}
|
||||
|
@ -211,9 +211,9 @@ func removeHeaders(header http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
if header.Get("A-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("A-Upgrade"))
|
||||
header.Del("A-Upgrade")
|
||||
if header.Get("Zr-Origin-Upgrade") != "" {
|
||||
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
|
||||
header.Del("Zr-Origin-Upgrade")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
|
||||
requestURL := r.URL.String()
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//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, "/")
|
||||
u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL)
|
||||
wspHandler := websocketproxy.NewProxy(u, false)
|
||||
|
@ -5,22 +5,22 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
//This remove the certificates in the list where either the
|
||||
//public key or the private key is missing
|
||||
// This remove the certificates in the list where either the
|
||||
// public key or the private key is missing
|
||||
func getCertPairs(certFiles []string) []string {
|
||||
crtMap := make(map[string]bool)
|
||||
pemMap := make(map[string]bool)
|
||||
keyMap := make(map[string]bool)
|
||||
|
||||
for _, filename := range certFiles {
|
||||
if filepath.Ext(filename) == ".crt" {
|
||||
crtMap[strings.TrimSuffix(filename, ".crt")] = true
|
||||
if filepath.Ext(filename) == ".pem" {
|
||||
pemMap[strings.TrimSuffix(filename, ".pem")] = true
|
||||
} else if filepath.Ext(filename) == ".key" {
|
||||
keyMap[strings.TrimSuffix(filename, ".key")] = true
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for domain := range crtMap {
|
||||
for domain := range pemMap {
|
||||
if keyMap[domain] {
|
||||
result = append(result, domain)
|
||||
}
|
||||
@ -29,7 +29,7 @@ func getCertPairs(certFiles []string) []string {
|
||||
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 {
|
||||
var matchingDomain string = ""
|
||||
maxLength := 0
|
||||
@ -43,18 +43,3 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
"embed"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -15,12 +14,19 @@ import (
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
CertStore string
|
||||
verbal bool
|
||||
type CertCache struct {
|
||||
Cert *x509.Certificate
|
||||
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
|
||||
|
||||
func NewManager(certStore string, verbal bool) (*Manager, error) {
|
||||
@ -28,14 +34,99 @@ func NewManager(certStore string, verbal bool) (*Manager, error) {
|
||||
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{
|
||||
CertStore: certStore,
|
||||
verbal: verbal,
|
||||
CertStore: certStore,
|
||||
LoadedCerts: []*CertCache{},
|
||||
verbal: verbal,
|
||||
}
|
||||
|
||||
err := thisManager.UpdateLoadedCertList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) {
|
||||
filenames, err := m.ListCerts()
|
||||
if err != nil {
|
||||
@ -48,8 +139,9 @@ func (m *Manager) ListCertDomains() ([]string, error) {
|
||||
return filenames, nil
|
||||
}
|
||||
|
||||
// Return a list of cert files (public and private keys)
|
||||
func (m *Manager) ListCerts() ([]string, error) {
|
||||
certs, err := ioutil.ReadDir(m.CertStore)
|
||||
certs, err := os.ReadDir(m.CertStore)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
@ -64,44 +156,52 @@ func (m *Manager) ListCerts() ([]string, error) {
|
||||
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) {
|
||||
//Check if the domain corrisponding cert exists
|
||||
pubKey := "./tmp/localhost.crt"
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
|
||||
//Direct hit
|
||||
pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".pem")
|
||||
priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
|
||||
|
||||
} else if m.CertMatchExists(helloInfo.ServerName) {
|
||||
//Use x509
|
||||
pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName)
|
||||
} else {
|
||||
domainCerts, _ := m.ListCertDomains()
|
||||
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
|
||||
if cloestDomainCert != "" {
|
||||
//There is a matching parent domain for this subdomain. Use this instead.
|
||||
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".crt")
|
||||
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
|
||||
} else if m.DefaultCertExists() {
|
||||
//Use default.crt and default.key
|
||||
pubKey = filepath.Join(m.CertStore, "default.crt")
|
||||
//Fallback to legacy method of matching certificates
|
||||
/*
|
||||
domainCerts, _ := m.ListCertDomains()
|
||||
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
|
||||
if cloestDomainCert != "" {
|
||||
//There is a matching parent domain for this subdomain. Use this instead.
|
||||
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".pem")
|
||||
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
|
||||
} 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")
|
||||
if m.verbal {
|
||||
log.Println("No matching certificate found. Serving with default")
|
||||
}
|
||||
//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.verbal {
|
||||
// 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
|
||||
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
|
||||
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
|
||||
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")
|
||||
if utils.FileExists(pubKey) {
|
||||
err := os.Remove(pubKey)
|
||||
@ -143,6 +243,9 @@ func (m *Manager) RemoveCert(domain string) error {
|
||||
}
|
||||
}
|
||||
|
||||
//Update the cert list
|
||||
m.UpdateLoadedCertList()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -171,15 +274,11 @@ func IsValidTLSFile(file io.Reader) bool {
|
||||
return false
|
||||
}
|
||||
// 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") {
|
||||
// The file contains a private key
|
||||
_, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return err == nil
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
@ -217,7 +217,11 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
|
||||
}
|
||||
|
||||
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 {
|
||||
//Try replace the http with https and vise versa
|
||||
rewriteURL := ""
|
||||
@ -227,7 +231,7 @@ func getWebsiteStatus(url string) (int, error) {
|
||||
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
|
||||
}
|
||||
|
||||
resp, err = http.Get(rewriteURL)
|
||||
resp, err = client.Get(rewriteURL)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
|
||||
//Invalid downstream reverse proxy settings, but it is online
|
||||
|
@ -72,6 +72,7 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ func (ws *WebServer) RestorePreviousState() {
|
||||
ws.option.EnableDirectoryListing = enableDirList
|
||||
|
||||
//Check the running state
|
||||
webservRunning := false
|
||||
webservRunning := true
|
||||
ws.option.Sysdb.Read("webserv", "enabled", &webservRunning)
|
||||
if webservRunning {
|
||||
ws.Start()
|
||||
@ -124,6 +124,11 @@ func (ws *WebServer) ChangePort(port string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get current using port in options
|
||||
func (ws *WebServer) GetListeningPort() string {
|
||||
return ws.option.Port
|
||||
}
|
||||
|
||||
// Start starts the web server.
|
||||
func (ws *WebServer) Start() error {
|
||||
ws.mu.Lock()
|
||||
|
@ -21,6 +21,9 @@ var (
|
||||
|
||||
// Add user customizable reverse proxy
|
||||
func ReverseProxtInit() {
|
||||
/*
|
||||
Load Reverse Proxy Global Settings
|
||||
*/
|
||||
inboundPort := 80
|
||||
if sysdb.KeyExists("settings", "inbound") {
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
sysdb.Read("settings", "listenP80", &listenOnPort80)
|
||||
if listenOnPort80 {
|
||||
@ -63,11 +74,19 @@ func ReverseProxtInit() {
|
||||
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{
|
||||
HostUUID: nodeUUID,
|
||||
HostVersion: version,
|
||||
Port: inboundPort,
|
||||
UseTls: useTls,
|
||||
ForceTLSLatest: forceLatestTLSVersion,
|
||||
NoCache: developmentMode,
|
||||
ListenOnPort80: listenOnPort80,
|
||||
ForceHttpsRedirect: forceHttpsRedirect,
|
||||
TlsManager: tlsCertManager,
|
||||
@ -83,45 +102,28 @@ func ReverseProxtInit() {
|
||||
|
||||
dynamicProxyRouter = dprouter
|
||||
|
||||
//Load all conf from files
|
||||
/*
|
||||
|
||||
Load all conf from files
|
||||
|
||||
*/
|
||||
confs, _ := filepath.Glob("./conf/proxy/*.config")
|
||||
for _, conf := range confs {
|
||||
record, err := LoadReverseProxyConfig(conf)
|
||||
err := LoadReverseProxyConfig(conf)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Failed to load config file: "+filepath.Base(conf), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if record.ProxyType == "root" {
|
||||
dynamicProxyRouter.SetRootProxy(&dynamicproxy.RootOptions{
|
||||
ProxyLocation: record.ProxyTarget,
|
||||
RequireTLS: record.UseTLS,
|
||||
})
|
||||
} else if record.ProxyType == "subd" {
|
||||
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)
|
||||
if dynamicProxyRouter.Root == nil {
|
||||
//Root config not set (new deployment?), use internal static web server as root
|
||||
defaultRootRouter, err := GetDefaultRootConfig()
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Failed to generate default root routing", err)
|
||||
return
|
||||
}
|
||||
dynamicProxyRouter.SetProxyRouteAsRoot(defaultRootRouter)
|
||||
}
|
||||
|
||||
//Start Service
|
||||
@ -173,7 +175,7 @@ func ReverseProxyHandleOnOff(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 {
|
||||
utils.SendErrorResponse(w, "type not defined")
|
||||
return
|
||||
@ -241,96 +243,112 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
rootname := ""
|
||||
if eptype == "vdir" {
|
||||
vdir, 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")
|
||||
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
|
||||
if eptype == "host" {
|
||||
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "subdomain not defined")
|
||||
return
|
||||
}
|
||||
rootname = subdomain
|
||||
thisOption := dynamicproxy.SubdOptions{
|
||||
MatchingDomain: subdomain,
|
||||
thisProxyEndpoint := dynamicproxy.ProxyEndpoint{
|
||||
//I/O
|
||||
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,
|
||||
RequireTLS: useTLS,
|
||||
BypassGlobalTLS: useBypassGlobalTLS,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
BypassGlobalTLS: false,
|
||||
SkipCertValidations: false,
|
||||
|
||||
DefaultSiteOption: defaultSiteOption,
|
||||
DefaultSiteValue: dsVal,
|
||||
}
|
||||
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
|
||||
} else if eptype == "root" {
|
||||
rootname = "root"
|
||||
thisOption := dynamicproxy.RootOptions{
|
||||
ProxyLocation: endpoint,
|
||||
RequireTLS: useTLS,
|
||||
preparedRootProxyRoute, err := dynamicProxyRouter.PrepareProxyRoute(&rootRoutingEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "unable to prepare root routing: "+err.Error())
|
||||
return
|
||||
}
|
||||
dynamicProxyRouter.SetRootProxy(&thisOption)
|
||||
|
||||
dynamicProxyRouter.SetProxyRouteAsRoot(preparedRootProxyRoute)
|
||||
proxyEndpointCreated = &rootRoutingEndpoint
|
||||
} else {
|
||||
//Invalid eptype
|
||||
utils.SendErrorResponse(w, "Invalid endpoint type")
|
||||
utils.SendErrorResponse(w, "invalid endpoint type")
|
||||
return
|
||||
}
|
||||
|
||||
//Save it
|
||||
thisProxyConfigRecord := Record{
|
||||
ProxyType: eptype,
|
||||
Rootname: rootname,
|
||||
ProxyTarget: endpoint,
|
||||
UseTLS: useTLS,
|
||||
BypassGlobalTLS: useBypassGlobalTLS,
|
||||
SkipTlsValidation: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: basicAuthCredentials,
|
||||
//Save the config to file
|
||||
err = SaveReverseProxyConfig(proxyEndpointCreated)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to save new proxy rule to file", err)
|
||||
return
|
||||
}
|
||||
SaveReverseProxyConfigToFile(&thisProxyConfigRecord)
|
||||
|
||||
//Update utm if exists
|
||||
if uptimeMonitor != nil {
|
||||
uptimeMonitor.Config.Targets = GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter)
|
||||
uptimeMonitor.CleanRecords()
|
||||
}
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
/*
|
||||
ReverseProxyHandleEditEndpoint handles proxy endpoint edit
|
||||
This endpoint do not handle
|
||||
basic auth credential update. The credential
|
||||
will be loaded from old config and reused
|
||||
(host only, for root use Default Site page to edit)
|
||||
This endpoint do not handle basic auth credential update.
|
||||
The credential will be loaded from old config and reused
|
||||
*/
|
||||
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")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Target proxy rule not defined")
|
||||
@ -371,50 +389,31 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
requireBasicAuth := (rba == "true")
|
||||
|
||||
//Load the previous basic auth credentials from current proxy rules
|
||||
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(eptype, rootNameOrMatchingDomain)
|
||||
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Target proxy config not found or could not be loaded")
|
||||
return
|
||||
}
|
||||
|
||||
if eptype == "vdir" {
|
||||
thisOption := dynamicproxy.VdirOptions{
|
||||
RootName: targetProxyEntry.RootOrMatchingDomain,
|
||||
Domain: endpoint,
|
||||
RequireTLS: useTLS,
|
||||
BypassGlobalTLS: false,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
||||
}
|
||||
targetProxyEntry.Remove()
|
||||
dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)
|
||||
//Generate a new proxyEndpoint from the new config
|
||||
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
|
||||
newProxyEndpoint.Domain = endpoint
|
||||
newProxyEndpoint.RequireTLS = useTLS
|
||||
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
|
||||
newProxyEndpoint.SkipCertValidations = skipTlsValidation
|
||||
newProxyEndpoint.RequireBasicAuth = requireBasicAuth
|
||||
|
||||
} else if eptype == "subd" {
|
||||
thisOption := dynamicproxy.SubdOptions{
|
||||
MatchingDomain: targetProxyEntry.RootOrMatchingDomain,
|
||||
Domain: endpoint,
|
||||
RequireTLS: useTLS,
|
||||
BypassGlobalTLS: bypassGlobalTLS,
|
||||
SkipCertValidations: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
||||
}
|
||||
targetProxyEntry.Remove()
|
||||
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
|
||||
//Prepare to replace the current routing rule
|
||||
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
targetProxyEntry.Remove()
|
||||
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
|
||||
|
||||
//Save it to file
|
||||
thisProxyConfigRecord := Record{
|
||||
ProxyType: eptype,
|
||||
Rootname: targetProxyEntry.RootOrMatchingDomain,
|
||||
ProxyTarget: endpoint,
|
||||
UseTLS: useTLS,
|
||||
SkipTlsValidation: skipTlsValidation,
|
||||
RequireBasicAuth: requireBasicAuth,
|
||||
BasicAuthCredentials: targetProxyEntry.BasicAuthCredentials,
|
||||
}
|
||||
SaveReverseProxyConfigToFile(&thisProxyConfigRecord)
|
||||
SaveReverseProxyConfig(newProxyEndpoint)
|
||||
|
||||
//Update uptime monitor
|
||||
UpdateUptimeMonitorTargets()
|
||||
@ -429,21 +428,19 @@ func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ptype, err := utils.PostPara(r, "ptype")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
||||
return
|
||||
}
|
||||
|
||||
//Remove the config from runtime
|
||||
err = dynamicProxyRouter.RemoveProxyEndpointByRootname(ptype, ep)
|
||||
err = dynamicProxyRouter.RemoveProxyEndpointByRootname(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Remove the config from file
|
||||
RemoveReverseProxyConfigFile(ep)
|
||||
err = RemoveReverseProxyConfig(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Update utm if exists
|
||||
if uptimeMonitor != nil {
|
||||
@ -473,14 +470,8 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ptype, err := utils.GetPara(r, "ptype")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
||||
return
|
||||
}
|
||||
|
||||
//Load the target proxy object from router
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
@ -502,17 +493,6 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
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")
|
||||
if err != nil {
|
||||
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
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
@ -570,7 +550,7 @@ func UpdateProxyBasicAuthCredentials(w http.ResponseWriter, r *http.Request) {
|
||||
targetProxy.BasicAuthCredentials = mergedCredentials
|
||||
|
||||
//Save it to file
|
||||
SaveReverseProxyEndpointToFile(targetProxy)
|
||||
SaveReverseProxyConfig(targetProxy)
|
||||
|
||||
//Replace runtime configuration
|
||||
targetProxy.UpdateToRuntime()
|
||||
@ -593,14 +573,8 @@ func ListProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ptype, err := utils.GetPara(r, "ptype")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
||||
return
|
||||
}
|
||||
|
||||
//Load the target proxy object from router
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
@ -624,12 +598,6 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ptype, err := utils.PostPara(r, "ptype")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
||||
return
|
||||
}
|
||||
|
||||
matchingPrefix, err := utils.PostPara(r, "prefix")
|
||||
if err != nil {
|
||||
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
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
@ -666,7 +634,7 @@ func AddProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
//Save configs to runtime and file
|
||||
targetProxy.UpdateToRuntime()
|
||||
SaveReverseProxyEndpointToFile(targetProxy)
|
||||
SaveReverseProxyConfig(targetProxy)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -679,12 +647,6 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
ptype, err := utils.PostPara(r, "ptype")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
||||
return
|
||||
}
|
||||
|
||||
matchingPrefix, err := utils.PostPara(r, "prefix")
|
||||
if err != nil {
|
||||
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
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ptype, ep)
|
||||
targetProxy, err := dynamicProxyRouter.LoadProxy(ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
@ -717,7 +679,7 @@ func RemoveProxyBasicAuthExceptionPaths(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Save configs to runtime and file
|
||||
targetProxy.UpdateToRuntime()
|
||||
SaveReverseProxyEndpointToFile(targetProxy)
|
||||
SaveReverseProxyConfig(targetProxy)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
@ -728,16 +690,28 @@ func ReverseProxyStatus(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 {
|
||||
utils.SendErrorResponse(w, "type not defined")
|
||||
return
|
||||
}
|
||||
|
||||
if eptype == "vdir" {
|
||||
if eptype == "host" {
|
||||
results := []*dynamicproxy.ProxyEndpoint{}
|
||||
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
|
||||
})
|
||||
|
||||
@ -745,19 +719,6 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else if eptype == "root" {
|
||||
@ -789,7 +750,7 @@ func HandleUpdatePort80Listener(w http.ResponseWriter, r *http.Request) {
|
||||
} else if enabled == "false" {
|
||||
sysdb.Write("settings", "listenP80", false)
|
||||
SystemWideLogger.Println("Disabling port 80 listener")
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(true)
|
||||
dynamicProxyRouter.UpdatePort80ListenerState(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid mode given: "+enabled)
|
||||
}
|
||||
@ -837,6 +798,30 @@ func HandleManagementProxyCheck(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
||||
newIncomingPort, err := utils.PostPara(r, "incoming")
|
||||
@ -882,33 +867,138 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Handle list of root route options
|
||||
func HandleRootRouteOptionList(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(dynamicProxyRouter.RootRoutingOptions)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
/* Handle Custom Header Rules */
|
||||
//List all the custom header defined in this proxy rule
|
||||
|
||||
// Handle update of the root route edge case options. See dynamicproxy/rootRoute.go
|
||||
func HandleRootRouteOptionsUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
enableUnsetSubdomainRedirect, err := utils.PostBool(r, "unsetRedirect")
|
||||
func HandleCustomHeaderList(w http.ResponseWriter, r *http.Request) {
|
||||
epType, err := utils.PostPara(r, "type")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
utils.SendErrorResponse(w, "endpoint type not defined")
|
||||
return
|
||||
}
|
||||
|
||||
unsetRedirectTarget, _ := utils.PostPara(r, "unsetRedirectTarget")
|
||||
|
||||
newRootOption := dynamicproxy.RootRoutingOptions{
|
||||
EnableRedirectForUnsetRules: enableUnsetSubdomainRedirect,
|
||||
UnsetRuleRedirectTarget: unsetRedirectTarget,
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "domain or matching rule not defined")
|
||||
return
|
||||
}
|
||||
|
||||
dynamicProxyRouter.RootRoutingOptions = &newRootOption
|
||||
err = newRootOption.SaveToFile()
|
||||
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
|
||||
}
|
||||
|
||||
//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 {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ import (
|
||||
|
||||
This script holds the static resources router
|
||||
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 {
|
||||
|
35
src/routingrule.go
Normal 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,
|
||||
})
|
||||
|
||||
}
|
32
src/start.go
@ -101,6 +101,18 @@ func startupSequence() {
|
||||
} else {
|
||||
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
|
||||
netstatBuffers, err = netstat.NewNetStatBuffer(300)
|
||||
if err != nil {
|
||||
@ -128,7 +140,7 @@ func startupSequence() {
|
||||
*/
|
||||
|
||||
if *allowMdnsScanning {
|
||||
portInt, err := strconv.Atoi(strings.Split(handler.Port, ":")[1])
|
||||
portInt, err := strconv.Atoi(strings.Split(*webUIPort, ":")[1])
|
||||
if err != nil {
|
||||
portInt = 8000
|
||||
}
|
||||
@ -220,25 +232,13 @@ func startupSequence() {
|
||||
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
|
||||
func finalSequence() {
|
||||
//Start ACME renew agent
|
||||
acmeRegisterSpecialRoutingRule()
|
||||
|
||||
//Inject routing rules
|
||||
registerBuildInRoutingRules()
|
||||
}
|
||||
|
290
src/vdir.go
Normal 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)
|
||||
}
|
@ -615,8 +615,12 @@
|
||||
<p>Whitelist a certain IP or IP range</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>IP Address</label>
|
||||
<input id="ipAddressInputWhitelist" type="text" placeholder="IP Address">
|
||||
<label>IP Address</label>
|
||||
<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>
|
||||
<button id="addIpButton" onclick="addIpWhitelist();" class="ui basic green button">
|
||||
<i class="green add icon"></i> Whitelist IP
|
||||
@ -634,6 +638,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Remarks</th>
|
||||
<th>Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -793,11 +798,12 @@
|
||||
if (data.length === 0) {
|
||||
$('#whitelistIpTable').append(`
|
||||
<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>
|
||||
`);
|
||||
} else {
|
||||
$.each(data, function(index, ip) {
|
||||
$.each(data, function(index, ipEntry) {
|
||||
let ip = ipEntry.IP;
|
||||
let icon = "globe icon";
|
||||
if (isLAN(ip)){
|
||||
icon = "desktop icon";
|
||||
@ -807,6 +813,7 @@
|
||||
$('#whitelistIpTable').append(`
|
||||
<tr class="whitelistItem" ip="${encodeURIComponent(ip)}">
|
||||
<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>
|
||||
</tr>
|
||||
`);
|
||||
@ -1003,6 +1010,7 @@
|
||||
|
||||
function addIpWhitelist(){
|
||||
let targetIp = $("#ipAddressInputWhitelist").val().trim();
|
||||
let remarks = $("#ipAddressCommentsWhitelist").val().trim();
|
||||
if (targetIp == ""){
|
||||
alert("IP address is empty")
|
||||
return
|
||||
@ -1016,7 +1024,7 @@
|
||||
$.ajax({
|
||||
url: "/api/whitelist/ip/add",
|
||||
type: "POST",
|
||||
data: {ip: targetIp.toLowerCase()},
|
||||
data: {ip: targetIp.toLowerCase(), "comment": remarks},
|
||||
success: function(response) {
|
||||
if (response.error !== undefined) {
|
||||
msgbox(response.error, false, 6000);
|
||||
@ -1025,6 +1033,7 @@
|
||||
}
|
||||
|
||||
$("#ipAddressInputWhitelist").val("");
|
||||
$("#ipAddressCommentsWhitelist").val("");
|
||||
$("#ipAddressInputWhitelist").parent().remvoeClass("error");
|
||||
},
|
||||
error: function() {
|
||||
|
@ -15,42 +15,19 @@
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<h4>Default Certificates</h4>
|
||||
<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>
|
||||
<h3>Hosts Certificates</h3>
|
||||
<p>Provide certificates for multiple domains reverse proxy</p>
|
||||
<div class="ui fluid form">
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>Server Name (Domain)</label>
|
||||
<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 class="field">
|
||||
<label>Public Key (.pem)</label>
|
||||
<input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
|
||||
<small>or .crt files in order systems</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Private Key (.key)</label>
|
||||
@ -63,14 +40,33 @@
|
||||
<div id="certUploadSuccMsg" class="ui green message" style="display:none;">
|
||||
<i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
|
||||
</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 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>
|
||||
<tr><th>Domain</th>
|
||||
<th>Last Update</th>
|
||||
<th>Expire At</th>
|
||||
<th class="no-sort">Renew</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
</tr></thead>
|
||||
<tbody id="certifiedDomainList">
|
||||
@ -81,15 +77,34 @@
|
||||
|
||||
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
|
||||
</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>
|
||||
<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>
|
||||
<h4>Prefered Certificate Authority</h4>
|
||||
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
|
||||
<div class="ui fluid form">
|
||||
<div class="field">
|
||||
@ -112,12 +127,12 @@
|
||||
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
|
||||
</div><br>
|
||||
<h5>Certificate Renew / Generation (ACME) Settings</h5>
|
||||
<div class="ui basic segment">
|
||||
<div class="ui basic segment acmeRenewStateWrapper">
|
||||
<h4 class="ui header" id="acmeAutoRenewer">
|
||||
<i class="red circle icon"></i>
|
||||
<i class="white remove icon"></i>
|
||||
<div class="content">
|
||||
<span id="acmeAutoRenewerStatus">Disabled</span>
|
||||
<div class="sub header">Auto-Renewer Status</div>
|
||||
<div class="sub header">ACME Auto-Renewer</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
@ -130,6 +145,110 @@
|
||||
|
||||
$("#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
|
||||
function deleteCertificate(domain){
|
||||
if (confirm("Confirm delete certificate for " + domain + " ?")){
|
||||
@ -154,6 +273,12 @@
|
||||
//Initialize the current default CA options
|
||||
$.get("/api/acme/autoRenew/email", function(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){
|
||||
@ -167,7 +292,13 @@
|
||||
//Set the status of the acme enable icon
|
||||
function setACMEEnableStates(enabled){
|
||||
$("#acmeAutoRenewerStatus").text(enabled?"Enabled":"Disabled");
|
||||
$("#acmeAutoRenewer").find("i").attr("class", enabled?"green circle icon":"red circle icon");
|
||||
if (enabled){
|
||||
$(".acmeRenewStateWrapper").addClass("enabled");
|
||||
}else{
|
||||
$(".acmeRenewStateWrapper").removeClass("enabled");
|
||||
}
|
||||
|
||||
$("#acmeAutoRenewer").find("i").attr("class", enabled?"white circle check icon":"white circle times icon");
|
||||
}
|
||||
initAcmeStatus();
|
||||
|
||||
@ -187,6 +318,9 @@
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
//Update the renew button states
|
||||
$(".renewButton").removeClass('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -223,13 +357,14 @@
|
||||
<td>${entry.Domain}</td>
|
||||
<td>${entry.LastModifiedDate}</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>
|
||||
</tr>`);
|
||||
});
|
||||
|
||||
if (data.length == 0){
|
||||
$("#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>`);
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@
|
||||
<div class="content">
|
||||
<div class="header" style="font-size: 1.2em;" id="ganodeCount">0</div>
|
||||
<div class="description" id="connectedNodes" count="0">Connected Nodes</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -219,8 +218,8 @@
|
||||
|
||||
}
|
||||
|
||||
//Bind event to tab switch
|
||||
tabSwitchEventBind["gan"] = function(){
|
||||
//Bind event to tab switch
|
||||
tabSwitchEventBind["gan"] = function(){
|
||||
//On switch over to this page, load info
|
||||
listGANet();
|
||||
initGANetID();
|
||||
|
@ -61,7 +61,7 @@
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="" style="overflow-x: auto;">
|
||||
@ -84,6 +84,11 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</div>
|
||||
<script>
|
||||
@ -355,7 +360,10 @@
|
||||
url: '/api/gan/members/list?netid=' + currentGANetID + '&detail=true',
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
const tableBody = $('#networkMemeberTable');
|
||||
let tableBody = $('#networkMemeberTable');
|
||||
if (tableBody.length == 0){
|
||||
return;
|
||||
}
|
||||
data.sort((a, b) => a.address.localeCompare(b.address));
|
||||
//Check if the new object equal to the old one
|
||||
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
|
||||
function initGanetDetails(ganetId){
|
||||
currentGANetID = ganetId;
|
||||
@ -612,6 +669,11 @@
|
||||
|
||||
}
|
||||
|
||||
//Switch from other tabs back to this, exit to GAN list
|
||||
tabSwitchEventBind["gan"] = function(){
|
||||
exitToGanList();
|
||||
}
|
||||
|
||||
//Exit point
|
||||
function exitToGanList(){
|
||||
$("#gan").load("./components/gan.html", function(){
|
||||
|
275
src/web/components/httprp.html
Normal 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>
|
@ -72,25 +72,10 @@
|
||||
<i class="ui green checkmark icon"></i> Redirection Rules Added
|
||||
</div>
|
||||
<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>
|
||||
<script>
|
||||
$(".advanceSettings").accordion();
|
||||
|
||||
/*
|
||||
Redirection functions
|
||||
@ -125,6 +110,7 @@
|
||||
$("#ruleAddSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
||||
}
|
||||
initRedirectionRuleList();
|
||||
resetForm();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -151,16 +137,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function createAdvanceRules(){
|
||||
showSideWrapper("snippet/advancePathRules.html?t=" + Date.now(), true);
|
||||
}
|
||||
|
||||
function initRedirectionRuleList(){
|
||||
$("#redirectionRuleList").html("");
|
||||
$.get("/api/redirect/list", function(data){
|
||||
data.forEach(function(entry){
|
||||
$("#redirectionRuleList").append(`<tr>
|
||||
<td>${entry.RedirectURL} </td>
|
||||
<td><a href="${entry.RedirectURL}" target="_blank">${entry.RedirectURL}</a></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.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td>
|
||||
@ -169,7 +151,7 @@
|
||||
});
|
||||
|
||||
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>`);
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -1,92 +1,122 @@
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Set Proxy Root</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>
|
||||
<h2>Default Site</h2>
|
||||
<p>Default routing options for inbound traffic (previously called Proxy Root)</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Proxy Root</label>
|
||||
<input type="text" id="proxyRoot" onchange="checkRootRequireTLS(this.value);">
|
||||
<small>E.g. localhost:8080</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="rootReqTLS">
|
||||
<label>Root require TLS connection <br><small>Check this if your proxy root URL starts with https://</small></label>
|
||||
<div class="grouped fields">
|
||||
<label>What to show when Zoraxy is hit with an unknown Host?</label>
|
||||
<div class="field">
|
||||
<div class="ui radio defaultsite checkbox">
|
||||
<input type="radio" name="defaultsiteOption" checked="checked" value="webserver">
|
||||
<label>Internal Static Web Server<br>
|
||||
<small>Check this if you prefer a more Apache / Nginx like experience</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 class="ui horizontal divider">OR</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="useStaticWebServer" onchange="handleUseStaticWebServerAsRoot()">
|
||||
<label>Use Static Web Server as Root <br><small>Check this if you prefer a more Apache Web Server like experience</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<button class="ui basic button" onclick="setProxyRoot()"><i class="teal home icon" ></i> Update Proxy Root</button>
|
||||
<div class="ui divider"></div>
|
||||
<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;">
|
||||
</div>
|
||||
|
||||
<!-- 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 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">
|
||||
<label>Redirect target domain</label>
|
||||
<div class="ui input">
|
||||
<input id="unsetRedirectDomain" type="text" placeholder="http://example.com">
|
||||
<input id="redirectDomain" type="text" placeholder="http://example.com">
|
||||
</div>
|
||||
<small>Unset subdomain will be redirected to the link above. Remember to include the protocol (e.g. http:// or https://)<br>
|
||||
Leave empty for redirecting to upper level domain (e.g. notfound.example.com <i class="right arrow icon"></i> example.com)</small>
|
||||
<small>Unset subdomain will be redirected to the link above. Remember to include the protocol (e.g. http:// or https://)</small>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<button class="ui basic button" onclick="updateRootOptions()"><i class="blue save icon" ></i> Save Root Options</button>
|
||||
</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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var currentDefaultSiteOption = 0; //For enum see typedef.go
|
||||
$("#advanceRootSettings").accordion();
|
||||
|
||||
function handleUseStaticWebServerAsRoot(){
|
||||
let useStaticWebServer = $("#useStaticWebServer")[0].checked;
|
||||
if (useStaticWebServer){
|
||||
//Handle toggle events of option radio boxes
|
||||
function updateAvaibleDefaultSiteOptions(){
|
||||
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();
|
||||
$("#proxyRoot").val(staticWebServerURL);
|
||||
$("#proxyRoot").parent().addClass("disabled");
|
||||
$("#rootReqTLS").parent().checkbox("set unchecked");
|
||||
$("#rootReqTLS").parent().addClass("disabled");
|
||||
|
||||
//Check if web server is enabled. If not, ask if the user want to enable it
|
||||
/*if (!$("#webserv_enable").parent().checkbox("is checked")){
|
||||
confirmBox("Enable static web server now?", function(choice){
|
||||
if (choice == true){
|
||||
$("#webserv_enable").parent().checkbox("set checked");
|
||||
}
|
||||
});
|
||||
}*/
|
||||
}else{
|
||||
$("#useRootProxyRouterForVdir").parent().removeClass("disabled");
|
||||
currentDefaultSiteOption = 0;
|
||||
}else if (selectedDefaultSite == "proxy"){
|
||||
$("#defaultSiteProxyOptions").show();
|
||||
$("#rootReqTLS").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){
|
||||
@ -94,6 +124,22 @@
|
||||
if (data == null){
|
||||
|
||||
}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);
|
||||
checkRootRequireTLS(data.Domain);
|
||||
}
|
||||
@ -104,21 +150,9 @@
|
||||
});
|
||||
}
|
||||
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){
|
||||
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
|
||||
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
|
||||
function checkRootRequireTLS(targetDomain){
|
||||
//Trim off the http or https from the origin
|
||||
@ -193,28 +192,56 @@
|
||||
}else if (data == "http"){
|
||||
$("#rootReqTLS").parent().checkbox("set unchecked");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//Set the new proxy root option
|
||||
function setProxyRoot(){
|
||||
function setProxyRoot(btn=undefined){
|
||||
var newpr = $("#proxyRoot").val();
|
||||
if (newpr.trim() == ""){
|
||||
$("#proxyRoot").parent().addClass('error');
|
||||
return
|
||||
}else{
|
||||
$("#proxyRoot").parent().removeClass('error');
|
||||
if (newpr.trim() == "" && currentDefaultSiteOption == 0){
|
||||
//Fill in the web server info
|
||||
newpr = "127.0.0.1:" + $("#webserv_listenPort").val();
|
||||
$("#proxyRoot").val(newpr);
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
$.ajax({
|
||||
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){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000);
|
||||
@ -231,37 +258,20 @@
|
||||
|
||||
setTimeout(function(){
|
||||
//Update the checkbox
|
||||
updateWebServerLinkSettings();
|
||||
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>
|
@ -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="standardContainer">
|
||||
<div class="ui basic segment" style="margin-top: 1em;">
|
||||
<h2>New Proxy Rule</h2>
|
||||
<p>You can create a proxy endpoing by subdomain or virtual directories</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Proxy Type</label>
|
||||
<div class="ui selection dropdown">
|
||||
<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 class="ui basic segment" style="border-radius: 1em; padding: 1em !important;">
|
||||
<h2>New Proxy Rule</h2>
|
||||
<p>You can add more proxy rules to support more site via domain / subdomains</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Matching Keyword / Domain</label>
|
||||
<input type="text" id="rootname" placeholder="mydomain.com">
|
||||
<small>Support subdomain and wildcard, e.g. s1.mydomain.com or *.test.mydomain.com</small>
|
||||
</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 class="six wide column">
|
||||
<div class="ui basic segment" style="height: 100%; background-color: var(--theme_grey); color: var(--theme_lgrey);">
|
||||
<br>
|
||||
<span style="font-size: 1.2em; font-weight: 300;">Subdomain</span><br>
|
||||
Example of subdomain matching keyword:<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 basic segment rulesInstructions">
|
||||
<span style="font-size: 1.2em; font-weight: 300;"><i class="ui yellow star icon"></i> Domain</span><br>
|
||||
Example of domain matching keyword:<br>
|
||||
<code>arozos.com</code> <br>Any acess requesting arozos.com will be proxy to the IP address below<br>
|
||||
<div class="ui divider"></div>
|
||||
<span style="font-size: 1.2em; font-weight: 300;">Virtual Directory</span><br>
|
||||
Example of virtual directory name: <br>
|
||||
<code>/s1/home/</code> <br>(Any access to {this_server}/s1/home/ will be proxy to the IP address below)<br>
|
||||
You can also ignore the tailing slash for wildcard like usage.<br>
|
||||
<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> Subdomain</span><br>
|
||||
Example of subdomain matching keyword:<br>
|
||||
<code>s1.arozos.com</code> <br>Any request starting with s1.arozos.com will be proxy to the IP address below<br>
|
||||
<div class="ui divider"></div>
|
||||
<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="item"><code>/s1/room-101</code></div>
|
||||
<div class="item"><code>/s1/room-102/</code></div>
|
||||
<div class="item"><code>/s1/room-103/map.txt</code></div>
|
||||
</div><br>
|
||||
|
||||
<div class="item"><code>www.arozos.com</code></div>
|
||||
<div class="item"><code>foo.bar.arozos.com</code></div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
@ -122,9 +117,9 @@
|
||||
<script>
|
||||
$("#advanceProxyRules").accordion();
|
||||
|
||||
|
||||
//New Proxy Endpoint
|
||||
function newProxyEndpoint(){
|
||||
var type = $("#ptype").val();
|
||||
var rootname = $("#rootname").val();
|
||||
var proxyDomain = $("#proxyDomain").val();
|
||||
var useTLS = $("#reqTls")[0].checked;
|
||||
@ -132,20 +127,6 @@
|
||||
var bypassGlobalTLS = $("#bypassGlobalTLS")[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() == ""){
|
||||
$("#rootname").parent().addClass("error");
|
||||
return
|
||||
@ -164,7 +145,7 @@
|
||||
$.ajax({
|
||||
url: "/api/proxy/add",
|
||||
data: {
|
||||
type: type,
|
||||
type: "host",
|
||||
rootname: rootname,
|
||||
tls: useTLS,
|
||||
ep: proxyDomain,
|
||||
@ -177,19 +158,14 @@
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false, 5000);
|
||||
}else{
|
||||
//OK
|
||||
listVdirs();
|
||||
listSubd();
|
||||
|
||||
|
||||
//Clear old data
|
||||
$("#rootname").val("");
|
||||
$("#proxyDomain").val("");
|
||||
credentials = [];
|
||||
updateTable();
|
||||
|
||||
reloadUptimeList();
|
||||
//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){
|
||||
if (choice == true){
|
||||
//Load the prefer CA from TLS page
|
||||
@ -200,7 +176,12 @@
|
||||
//Get a new cert using ACME
|
||||
msgbox("Requesting certificate via " + defaultCA +"...");
|
||||
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{
|
||||
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
|
||||
function deleteEndpoint(ptype, epoint){
|
||||
if (confirm("Confirm remove proxy for :" + epoint + " (type: " + ptype + ")?")){
|
||||
function deleteEndpoint(epoint){
|
||||
epoint = decodeURIComponent(epoint).hexDecode();
|
||||
if (confirm("Confirm remove proxy for :" + epoint + "?")){
|
||||
$.ajax({
|
||||
url: "/api/proxy/del",
|
||||
data: {ep: epoint, ptype: ptype},
|
||||
data: {ep: epoint, },
|
||||
success: function(){
|
||||
listVdirs();
|
||||
listSubd();
|
||||
listProxyEndpoints();
|
||||
msgbox("Proxy Rule Deleted", true);
|
||||
reloadUptimeList();
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -330,227 +305,33 @@
|
||||
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({
|
||||
ept: endpointType,
|
||||
ep: uuid
|
||||
}));
|
||||
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
|
||||
|
||||
//Update v3.0.0
|
||||
//Since some proxy rules now contains wildcard characters
|
||||
//all uuid are converted to hex code before use in DOM selector
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Obtain Certificate via ACME
|
||||
*/
|
||||
|
||||
//Load the ACME email from server side
|
||||
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;
|
||||
String.prototype.hexDecode = function(){
|
||||
var j;
|
||||
var hexes = this.match(/.{1,4}/g) || [];
|
||||
var back = "";
|
||||
for(j = 0; j<hexes.length; j++) {
|
||||
back += String.fromCharCode(parseInt(hexes[j], 16));
|
||||
}
|
||||
|
||||
$.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);
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
return back;
|
||||
}
|
||||
|
||||
</script>
|
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="six wide column statisticWrapper">
|
||||
<div class="ui greybackground statustab segment">
|
||||
<div class="ui statustab segment">
|
||||
<h5 class="ui header">
|
||||
<i class="exchange icon"></i>
|
||||
<div class="content">
|
||||
@ -29,7 +29,7 @@
|
||||
</div>
|
||||
</h5>
|
||||
<div class="ui divider"></div>
|
||||
<h5 class="ui header">
|
||||
<h5 class="ui header" style="margin-top: 0px;">
|
||||
<i class="arrows alternate horizontal icon"></i>
|
||||
<div class="content">
|
||||
<span id="forwardtype"></span>
|
||||
@ -39,32 +39,39 @@
|
||||
</div>
|
||||
</h5>
|
||||
<div class="ui divider"></div>
|
||||
<h5 class="ui header">
|
||||
<h5 class="ui header" style="margin-top: 0px;">
|
||||
<i class="map marker alternate icon"></i>
|
||||
<div class="content">
|
||||
<span id="country"></span>
|
||||
<div class="sub header" id="countryList">
|
||||
|
||||
<i class="ui loading circle notch icon"></i> Resolving GeoIP
|
||||
</div>
|
||||
</div>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="networkActWrapper" class="standardContainer" style="position: relative; margin-top: 1em;">
|
||||
<div class="standardContainer" style="padding-bottom: 0 !important;">
|
||||
<!-- 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>
|
||||
</div>
|
||||
<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>
|
||||
<br>
|
||||
<div class="standardContainer">
|
||||
<h4>Basic Settings</h4>
|
||||
<div class="ui divider"></div>
|
||||
<h4>Global Settings</h4>
|
||||
<p>Inbound Port (Port to be proxied)</p>
|
||||
<div class="ui action fluid notloopbackOnly input">
|
||||
<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>
|
||||
<br>
|
||||
<div id="tls" class="ui toggle notloopbackOnly checkbox">
|
||||
@ -89,21 +96,22 @@
|
||||
Advance Settings
|
||||
</div>
|
||||
<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;">
|
||||
<input type="checkbox">
|
||||
<label>Force TLS v1.2 or above<br>
|
||||
<small>(Enhance security, but not compatible with legacy browsers)</small></label>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
|
||||
<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;">
|
||||
<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>
|
||||
@ -114,7 +122,7 @@
|
||||
<div class="ui two column stackable grid">
|
||||
<div class="column">
|
||||
<p>Visitor Counts</p>
|
||||
<table class="ui unstackable inverted celled table">
|
||||
<table class="ui unstackable very basic celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Country ISO Code</th>
|
||||
@ -130,7 +138,7 @@
|
||||
</div>
|
||||
<div class="column">
|
||||
<p>Proxy Request Types</p>
|
||||
<table class="ui unstackable inverted celled table">
|
||||
<table class="ui unstackable very basic celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Proxy Type</th>
|
||||
@ -175,13 +183,13 @@
|
||||
}
|
||||
$("#serverstatus").addClass("green");
|
||||
$("#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);
|
||||
}else{
|
||||
$("#startbtn").removeClass("disabled");
|
||||
$("#stopbtn").addClass("disabled");
|
||||
$("#statusTitle").text("Offline");
|
||||
$("#rpStatusIcon").attr("class", "black circle times icon")
|
||||
$("#rpStatusIcon").attr("class", "yellow moon icon")
|
||||
$("#statusText").text("Reverse proxy server is offline");
|
||||
$("#serverstatus").removeClass("green");
|
||||
}
|
||||
@ -433,6 +441,30 @@
|
||||
}
|
||||
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(){
|
||||
$.get("/api/cert/tls", function(data){
|
||||
if (data == true){
|
||||
@ -561,10 +593,11 @@
|
||||
{
|
||||
type: 'line',
|
||||
responsive: true,
|
||||
resizeDelay: 100,
|
||||
resizeDelay: 300,
|
||||
options: {
|
||||
animation: false,
|
||||
maintainAspectRatio: false,
|
||||
bezierCurve: true,
|
||||
tooltips: {enabled: false},
|
||||
hover: {mode: null},
|
||||
//stepped: 'middle',
|
||||
@ -606,18 +639,18 @@
|
||||
{
|
||||
label: 'Inbound',
|
||||
data: rxValues,
|
||||
borderColor: "#4d9dd9",
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'rgba(77, 157, 217, 0.2)',
|
||||
borderColor: "#484bb8",
|
||||
borderWidth: 1,
|
||||
backgroundColor: 'rgba(72, 75, 184, 0.2)',
|
||||
fill: true,
|
||||
pointStyle: false,
|
||||
},
|
||||
{
|
||||
label: 'Outbound',
|
||||
data: txValues,
|
||||
borderColor: '#ffe32b',
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'rgba(255, 227, 43, 0.2)',
|
||||
borderColor: '#02a9c1',
|
||||
borderWidth: 1,
|
||||
backgroundColor: 'rgba(2, 169, 193, 0.2)',
|
||||
fill: true,
|
||||
pointStyle: false,
|
||||
}
|
||||
|
@ -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>
|
@ -3,11 +3,33 @@
|
||||
<h2>TCP Proxy</h2>
|
||||
<p>Proxy traffic flow on layer 3 via TCP/IP</p>
|
||||
</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 basic segment" id="addproxyConfig" style="display:none;">
|
||||
<h3>TCP Proxy Config</h3>
|
||||
<div class="ui basic segment" style="margin-top: 0;">
|
||||
<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>
|
||||
<form id="tcpProxyForm" class="ui form">
|
||||
<div class="field" style="display:none;">
|
||||
@ -39,11 +61,10 @@
|
||||
<option value="starter">Starter</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui blue 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="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);" 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>
|
||||
<div class="ui basic inverted segment" style="background-color: #414141; border-radius: 0.6em;">
|
||||
<h3>Proxy Mode Instructions</h3>
|
||||
<div class="ui basic inverted segment" style="background: var(--theme_background_inverted); border-radius: 0.6em;">
|
||||
<p>TCP Proxy support the following TCP sockets proxy modes</p>
|
||||
<table class="ui celled padded inverted basic table">
|
||||
<thead>
|
||||
@ -108,28 +129,6 @@
|
||||
</table>
|
||||
</div>
|
||||
</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>
|
||||
<script>
|
||||
@ -138,6 +137,13 @@
|
||||
$("#tcpProxyForm .dropdown").dropdown();
|
||||
$('#tcpProxyForm').on('submit', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
//Check if update mode
|
||||
if ($("#editTcpProxyButton").is(":visible")){
|
||||
confirmEditTCPProxyConfig(event);
|
||||
return;
|
||||
}
|
||||
|
||||
var form = $(this);
|
||||
|
||||
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(){
|
||||
$('#tcpProxyForm input, #tcpProxyForm select').val('');
|
||||
$('#tcpProxyForm select').dropdown('clear');
|
||||
}
|
||||
|
||||
function cancelTCPProxyEdit(event) {
|
||||
function cancelTCPProxyEdit(event=undefined) {
|
||||
clearTCPProxyAddEditForm();
|
||||
$('#addproxyConfig').slideUp('fast');
|
||||
$("#addTcpProxyButton").show();
|
||||
$("#editTcpProxyButton").hide();
|
||||
}
|
||||
|
||||
function validateTCPProxyConfig(form){
|
||||
@ -231,7 +230,7 @@
|
||||
proxyConfigs.forEach(function(config) {
|
||||
var runningLogo = '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){
|
||||
runningLogo = 'Running';
|
||||
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");
|
||||
}
|
||||
initProxyConfigList();
|
||||
clearTCPProxyAddEditForm();
|
||||
$("#addproxyConfig").slideUp("fast");
|
||||
cancelTCPProxyEdit();
|
||||
|
||||
},
|
||||
error: function() {
|
||||
msgbox('An error occurred while processing the request', false);
|
||||
|
@ -177,7 +177,7 @@
|
||||
$("#utmrender").append(`<div class="ui basic segment statusbar">
|
||||
<div class="domain">
|
||||
<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>
|
||||
<h3 class="ui header" style="margin-bottom: 0.2em;">${name}</h3>
|
||||
|
@ -3,166 +3,179 @@
|
||||
<h2>Utilities</h2>
|
||||
<p>You might find these tools or information helpful when setting up your gateway server</p>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
|
||||
<div class="selfauthOnly">
|
||||
<h3>Account Management</h3>
|
||||
<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 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>
|
||||
<a class="nettools item" data-tab="tab3">System</a>
|
||||
</div>
|
||||
|
||||
<div id="passwordChangeSuccMsg" class="ui green message" style="display:none;">
|
||||
<i class="ui circle checkmark green icon "></i> Password Updated
|
||||
<div class="ui bottom attached tab segment nettoolstab active" data-tab="tab1">
|
||||
<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 class="ui divider"></div>
|
||||
<h3>Forget Password Email</h3>
|
||||
<p>The following SMTP settings help you to reset your password in case you have lost your management account.</p>
|
||||
<form id="email-form" class="ui form">
|
||||
<div class="field">
|
||||
<label>Sender Address</label>
|
||||
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.arozos.com">
|
||||
</div>
|
||||
<div class="field">
|
||||
<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 class="selfauthOnly">
|
||||
<h3>Change Password</h3>
|
||||
<p>Update the current account credentials</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="four wide field">
|
||||
<label>Port</label>
|
||||
<input type="number" name="port" placeholder="E.g. 587" value="587">
|
||||
<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;">
|
||||
<i class="ui circle checkmark green icon "></i> Password Updated
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<p><i class="caret down icon"></i> Credentials for SMTP server authentications</p>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>Sender Username</label>
|
||||
<input type="text" name="username" placeholder="E.g. admin">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Sender Domain</label>
|
||||
<div class="ui labeled input">
|
||||
<div class="ui basic label">
|
||||
@
|
||||
</div>
|
||||
<input type="text" name="domain" min="1" max="65534" placeholder="E.g. arozos.com">
|
||||
<div class="ui divider"></div>
|
||||
<h3>Forget Password Email</h3>
|
||||
<p>The following SMTP settings help you to reset your password in case you have lost your management account.</p>
|
||||
<form id="email-form" class="ui form">
|
||||
<div class="field">
|
||||
<label>Sender Address</label>
|
||||
<input type="text" name="senderAddr" placeholder="E.g. noreply@zoraxy.arozos.com">
|
||||
</div>
|
||||
<div class="field">
|
||||
<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 class="four wide field">
|
||||
<label>Port</label>
|
||||
<input type="number" name="port" placeholder="E.g. 587" value="587">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Sender Password</label>
|
||||
<input type="password" name="password" placeholder="Password of the email account">
|
||||
<small>Leave empty to use the old password</small>
|
||||
</div>
|
||||
|
||||
<p> <i class="caret down icon"></i> Email for sending account reset link</p>
|
||||
<div class="field">
|
||||
<label>Admin / Receiver Address</label>
|
||||
<input type="text" name="recvAddr" placeholder="E.g. personalEmail@gmail.com">
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<h3> IP Address to CIDR</h3>
|
||||
<p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p>
|
||||
<div class="ui basic segment">
|
||||
<h5><i class="chevron down icon"></i> IP Range to CIDR Conversion</h5>
|
||||
<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>
|
||||
<!-- 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>
|
||||
|
||||
<div class="field">
|
||||
<p><i class="caret down icon"></i> Credentials for SMTP server authentications</p>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>Sender Username</label>
|
||||
<input type="text" name="username" placeholder="E.g. admin">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Sender Domain</label>
|
||||
<div class="ui labeled input">
|
||||
<div class="ui basic label">
|
||||
@
|
||||
</div>
|
||||
<input type="text" name="domain" min="1" max="65534" placeholder="E.g. arozos.com">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Sender Password</label>
|
||||
<input type="password" name="password" placeholder="Password of the email account">
|
||||
<small>Leave empty to use the old password</small>
|
||||
</div>
|
||||
|
||||
<p> <i class="caret down icon"></i> Email for sending account reset link</p>
|
||||
<div class="field">
|
||||
<label>Admin / Receiver Address</label>
|
||||
<input type="text" name="recvAddr" placeholder="E.g. personalEmail@gmail.com">
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment nettoolstab" data-tab="tab2">
|
||||
<h3> IP Address to CIDR</h3>
|
||||
<p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p>
|
||||
<div class="ui basic segment">
|
||||
<h5><i class="chevron down icon"></i> IP Range to CIDR Conversion</h5>
|
||||
<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>
|
||||
<br>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
$('.menu .nettools.item').tab();
|
||||
/*
|
||||
Account Password utilities
|
||||
*/
|
||||
@ -171,6 +184,7 @@
|
||||
if (data == 0){
|
||||
//Using external auth manager. Hide options
|
||||
$(".selfauthOnly").hide();
|
||||
$(".extAuthOnly").show();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -3,73 +3,420 @@
|
||||
<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>
|
||||
</div>
|
||||
<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>Proxy To</th>
|
||||
<th>Basic Auth</th>
|
||||
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="vdirList">
|
||||
<tr>
|
||||
<td data-label=""><button class="ui circular mini red basic button"><i class="remove icon"></i> Remove Proxy</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="currentVirtualDirectoryAttachingHost" class="ui basic segment">
|
||||
Select a host / routing rule to start editing Virtual Directory
|
||||
</div>
|
||||
<div class="ui stackable grid">
|
||||
<div class="six wide column">
|
||||
<h4>Select a Target Host / Site</h4>
|
||||
<p>Attach Virtual Directory routing rule to root proxy router</p>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="useRootProxyRouterForVdir" onchange="handleVdirAttachTargetChange(this);">
|
||||
<label>Root Proxy Router<br>
|
||||
<small>Only applicable when Default Site is set to "Reverse Proxy" mode</small></label>
|
||||
</div>
|
||||
<div class="ui horizontal divider">OR</div>
|
||||
<p>Create Virtual Directory routing in existing host / routing rule entries</p>
|
||||
<div class="ui fluid search selection dropdown">
|
||||
<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>
|
||||
<button class="ui icon right floated basic button" onclick="listVdirs();"><i class="green refresh icon"></i> Refresh</button>
|
||||
<br><br>
|
||||
</div>
|
||||
<script>
|
||||
//Virtual directories functions
|
||||
function listVdirs(){
|
||||
$.get("/api/proxy/list?type=vdir", function(data){
|
||||
$("#vdirList").html(``);
|
||||
if (data.error !== undefined){
|
||||
$("#vdirList").append(`<tr>
|
||||
<td data-label="" colspan="3"><i class="remove icon"></i> ${data.error}</td>
|
||||
</tr>`);
|
||||
}else if (data.length == 0){
|
||||
$("#vdirList").append(`<tr>
|
||||
<td data-label="" colspan="3"><i class="checkmark icon"></i> No Virtual Directory Record</td>
|
||||
</tr>`);
|
||||
|
||||
//Initialize the list of hosts that can be used to attach vdirs config
|
||||
function initVdirList(){
|
||||
//Load the hosts into the dropdown
|
||||
$("#hostDomainList").html("");
|
||||
$.get("/api/proxy/list?type=host", function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
data.forEach(vdir => {
|
||||
let tlsIcon = "";
|
||||
let vdirData = encodeURIComponent(JSON.stringify(vdir));
|
||||
if (vdir.RequireTLS){
|
||||
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
||||
if (vdir.SkipCertValidations){
|
||||
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
|
||||
}
|
||||
}
|
||||
if (data.length == 0){
|
||||
//No hosts found
|
||||
}else{
|
||||
data.forEach(proxyEndpoint => {
|
||||
let domain = proxyEndpoint.RootOrMatchingDomain;
|
||||
$("#hostDomainList").append(`<div class="item" data-value="${domain}">${domain}</div>`);
|
||||
});
|
||||
|
||||
let tlsVerificationField = "";
|
||||
if (vdir.RequireTLS){
|
||||
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>`);
|
||||
});
|
||||
//Update the dropdown events
|
||||
$("#vdirBaseRoutingRule").parent().dropdown();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
tabSwitchEventBind["vdir"] = function(){
|
||||
listVdirs();
|
||||
initVdirList();
|
||||
}
|
||||
|
||||
initVdirList();
|
||||
</script>
|
@ -4,7 +4,7 @@
|
||||
<p>A simple static web server that serve html css and js files</p>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui basic segment">
|
||||
<div class="ui basic segment webservRunningStateWrapper">
|
||||
<h4 class="ui header" id="webservRunningState">
|
||||
<i class="green circle icon"></i>
|
||||
<div class="content">
|
||||
@ -102,12 +102,14 @@
|
||||
function setWebServerRunningState(running){
|
||||
if (running){
|
||||
$("#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");
|
||||
$(".webservRunningStateWrapper").addClass("enabled")
|
||||
}else{
|
||||
$("#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");
|
||||
$(".webservRunningStateWrapper").removeClass("enabled")
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 694 KiB |
Before Width: | Height: | Size: 818 KiB |
Before Width: | Height: | Size: 1.1 MiB |
@ -26,9 +26,12 @@
|
||||
<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>
|
||||
</div>
|
||||
<div class="ui right floated buttons" style="padding-top: 2px;">
|
||||
<button class="ui basic icon button" onclick="logout();"><i class="sign-out icon"></i></button>
|
||||
<div class="ui right floated buttons" style="padding-top: 2px; padding-right: 0.4em;">
|
||||
<button class="ui basic white icon button" onclick="logout();"><i class="sign-out icon"></i></button>
|
||||
</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 class="wrapper">
|
||||
<div class="toolbar">
|
||||
@ -36,17 +39,21 @@
|
||||
<a class="item active" tag="status">
|
||||
<i class="simplistic info circle icon"></i>Status
|
||||
</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">
|
||||
<i class="simplistic folder icon"></i> Virtual Directory
|
||||
</a>
|
||||
<a class="item" tag="subd">
|
||||
<i class="simplistic sitemap icon"></i> Subdomain Proxy
|
||||
</a>
|
||||
<a class="item" tag="rules">
|
||||
<i class="simplistic plus square icon"></i> Create Proxy Rules
|
||||
</a>
|
||||
<a class="item" tag="setroot">
|
||||
<i class="simplistic home icon"></i> Set Proxy Root
|
||||
<a class="item" tag="tcpprox">
|
||||
<i class="simplistic exchange icon"></i> TCP Proxy
|
||||
</a>
|
||||
<div class="ui divider menudivider">Access & Connections</div>
|
||||
<a class="item" tag="cert">
|
||||
@ -65,9 +72,6 @@
|
||||
<a class="item" tag="zgrok">
|
||||
<i class="simplistic podcast icon"></i> Service Expose Proxy
|
||||
</a>
|
||||
<a class="item" tag="tcpprox">
|
||||
<i class="simplistic exchange icon"></i> TCP Proxy
|
||||
</a>
|
||||
<div class="ui divider menudivider">Others</div>
|
||||
<a class="item" tag="webserv">
|
||||
<i class="simplistic globe icon"></i> Static Web Server
|
||||
@ -97,7 +101,7 @@
|
||||
<div id="vdir" class="functiontab" target="vdir.html"></div>
|
||||
|
||||
<!-- Subdomain Proxy -->
|
||||
<div id="subd" class="functiontab" target="subd.html"></div>
|
||||
<div id="httprp" class="functiontab" target="httprp.html"></div>
|
||||
|
||||
<!-- Create Rules -->
|
||||
<div id="rules" class="functiontab" target="rules.html"></div>
|
||||
@ -235,12 +239,22 @@
|
||||
$.get("/api/auth/logout", function(response) {
|
||||
if (response === "OK") {
|
||||
setTimeout(function(){
|
||||
window.location.href = "/";
|
||||
window.location.href = "/login.html";
|
||||
}, 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){
|
||||
let targetTabBtn = undefined;
|
||||
$("#mainmenu").find(".item").each(function(){
|
||||
@ -271,9 +285,11 @@
|
||||
$(targetBtn).addClass("active");
|
||||
$(".functiontab").hide();
|
||||
$("#" + tabID).fadeIn('fast', function(){
|
||||
if (tabSwitchEventBind[tabID]){
|
||||
tabSwitchEventBind[tabID]();
|
||||
}
|
||||
setTimeout(function(){
|
||||
if (tabSwitchEventBind[tabID]){
|
||||
tabSwitchEventBind[tabID]();
|
||||
}
|
||||
},100)
|
||||
});
|
||||
$('html,body').animate({scrollTop: 0}, 'fast');
|
||||
window.location.hash = tabID;
|
||||
|
@ -7,48 +7,13 @@
|
||||
<link rel="icon" type="image/png" href="./favicon.png" />
|
||||
<title>Login | Zoraxy</title>
|
||||
<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/semantic/semantic.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background: rgb(245,245,245);
|
||||
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;
|
||||
}
|
||||
background: linear-gradient(60deg, rgba(84,58,183,1) 0%, rgba(0,172,193,1) 100%);
|
||||
}
|
||||
|
||||
#errmsg{
|
||||
@ -65,13 +30,97 @@
|
||||
.ui.fluid.button.registerOnly{
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
<div class="background"></div>
|
||||
<div id="loginForm" class="ui middle aligned center aligned grid">
|
||||
<div class="column">
|
||||
<form class="ui large form">
|
||||
<div id="loginForm" class="ui middle aligned center aligned grid" data-aos="fade-up">
|
||||
<div class="column" style="padding-top: 0 !important;">
|
||||
<form class="ui large form content">
|
||||
<div class="ui basic segment">
|
||||
<img class="ui fluid image" src="img/public/logo.svg" style="pointer-events:none;">
|
||||
<p class="registerOnly">Account Setup</p>
|
||||
@ -99,19 +148,37 @@
|
||||
<label>Remember Me</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loginbtn" class="ui fluid basic blue button loginOnly">Login</div>
|
||||
<div id="regsiterbtn" class="ui fluid basic blue button registerOnly">Create</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 button registerOnly"><i class="ui green checkmark icon"></i> Confirm</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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<small>Proudly powered by Zoraxy</small>
|
||||
</form>
|
||||
</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>
|
||||
AOS.init();
|
||||
|
||||
var redirectionAddress = "/";
|
||||
var loginAddress = "/api/auth/login";
|
||||
$(".checkbox").checkbox();
|
||||
|
247
src/web/main.css
@ -2,15 +2,65 @@
|
||||
index.html style overwrite
|
||||
*/
|
||||
:root{
|
||||
--theme_grey: #414141;
|
||||
--theme_lgrey: #f6f6f6;
|
||||
--theme_green: #3c9c63;
|
||||
--theme_fcolor: #979797;
|
||||
--theme_advance: #f8f8f9;
|
||||
|
||||
|
||||
|
||||
--theme_background: linear-gradient(60deg, rgb(84, 58, 183) 0%, rgb(0, 172, 193) 100%);
|
||||
--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{
|
||||
background-color:#f6f6f6;
|
||||
color: #414141;
|
||||
background-color:var(--theme_bg);
|
||||
}
|
||||
|
||||
.functiontab{
|
||||
@ -31,14 +81,14 @@ body{
|
||||
padding: 0.4em;
|
||||
padding-left: 1.2em;
|
||||
padding-right: 1.2em;
|
||||
background-color: #f5f5f5;
|
||||
background-color: var(--theme_bg_secondary);
|
||||
margin-bottom: 1em;
|
||||
|
||||
border-bottom: 1px solid var(--theme_highlight);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
box-shadow: 0px 1px 5px 0px rgba(38,38,38,0.26);
|
||||
|
||||
}
|
||||
|
||||
.menubar .logo{
|
||||
@ -67,7 +117,7 @@ body{
|
||||
display: inline-block;
|
||||
width: calc(100% - 240px);
|
||||
vertical-align: top;
|
||||
background-color: white;
|
||||
background-color: var(--theme_bg_primary);
|
||||
border-radius: 1em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
@ -76,19 +126,30 @@ body{
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.rulesInstructions, .rulesInstructions span{
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.ui.divider{
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.serverstatusWrapper{
|
||||
padding-right: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.statisticWrapper{
|
||||
margin-top: 1em;
|
||||
padding-left: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
padding-right: 1em !important;
|
||||
}
|
||||
|
||||
.statisticWrapper .statustab{
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
|
||||
/* Message Box */
|
||||
#messageBox{
|
||||
position: fixed;
|
||||
@ -204,14 +265,6 @@ body{
|
||||
|
||||
|
||||
@media screen and (min-width: 750px) {
|
||||
#serverstatus{
|
||||
border-top-left-radius: 1em !important;
|
||||
}
|
||||
|
||||
.greybackground.statustab{
|
||||
border-top-right-radius: 1em !important;
|
||||
}
|
||||
|
||||
.standardContainer{
|
||||
padding-left: 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 {
|
||||
position: fixed;
|
||||
display: inline-block;
|
||||
@ -257,19 +321,10 @@ body{
|
||||
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{
|
||||
border-bottom: 0px solid transparent !important;
|
||||
}
|
||||
|
||||
.greybackground.statustab{
|
||||
border-top-right-radius: 0em !important;
|
||||
padding: 2em 2em !important;
|
||||
}
|
||||
|
||||
.standardContainer{
|
||||
padding-left: 1.2em;
|
||||
padding-right: 1.2em;
|
||||
@ -298,11 +353,16 @@ body{
|
||||
}
|
||||
|
||||
.ui.menu .item{
|
||||
color: #5e5d5d;
|
||||
color: var(--item_color);
|
||||
}
|
||||
|
||||
.ui.segment{
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
||||
.ui.secondary.vertical.menu .active.item{
|
||||
background-color: #414141;
|
||||
background: var(--theme_background);
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
@ -311,11 +371,15 @@ body{
|
||||
animation: blinker 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.basic.segment.advanceoptions{
|
||||
background-color: #f7f7f7;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.bluefont{
|
||||
color: #417ac1 !important;
|
||||
}
|
||||
|
||||
|
||||
@keyframes blinker {
|
||||
50% {
|
||||
opacity: 50%;
|
||||
@ -327,52 +391,26 @@ body{
|
||||
*/
|
||||
#serverstatus{
|
||||
height: 100%;
|
||||
border-radius: 1em;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
#statusTitle{
|
||||
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{
|
||||
background-color: #fefefe !important;
|
||||
border-right: 5px solid #3d9c64;
|
||||
background: linear-gradient(60deg, #27e7ff, #00ca52);
|
||||
}
|
||||
#serverstatus.green .sub.header{
|
||||
color: rgb(224, 224, 224);
|
||||
}
|
||||
#serverstatus.green i,
|
||||
#serverstatus.green #statusTitle{
|
||||
color: #3d9c64;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
#serverstatus.green #statusText{
|
||||
color: #2c583d;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
|
||||
@ -381,22 +419,20 @@ body{
|
||||
}
|
||||
|
||||
#serverstatus:not(.green){
|
||||
background-color: white !important;
|
||||
background-image: url("img/plant.png");
|
||||
background-position: right;
|
||||
background-repeat: no-repeat;
|
||||
background-size: auto 100%;
|
||||
background: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
|
||||
}
|
||||
|
||||
#serverstatus:not(.green) #statusTitle,
|
||||
#serverstatus:not(.green) i,
|
||||
#serverstatus:not(.green) .sub.header{
|
||||
color: #4c4c4c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.statustab{
|
||||
min-height: 5.5em;
|
||||
margin: 1em;
|
||||
border-radius: 1em !important;
|
||||
}
|
||||
|
||||
#summaryTotalCount{
|
||||
@ -467,7 +503,7 @@ body{
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
background-color: #3d9c64;
|
||||
background-color: white;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
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
|
||||
*/
|
||||
@ -506,24 +559,70 @@ body{
|
||||
}
|
||||
|
||||
.tcproxConfig.running td:first-child{
|
||||
border-left: 0.6em solid #21ba45 !important;
|
||||
border-left: 0.6em solid #02cb59 !important;
|
||||
}
|
||||
|
||||
.tcproxConfig.stopped td:first-child{
|
||||
border-left: 0.6em solid #414141 !important;
|
||||
border-left: 0.6em solid #02032a !important;
|
||||
}
|
||||
|
||||
.tcproxConfig td:first-child .statusText{
|
||||
position: absolute;
|
||||
bottom: 0.3em;
|
||||
left: 0.2em;
|
||||
font-size: 2em;
|
||||
font-size: 1.4em;
|
||||
color:rgb(224, 224, 224);
|
||||
opacity: 0.7;
|
||||
pointer-events: 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
|
||||
*/
|
||||
|
@ -7,12 +7,14 @@
|
||||
<link rel="icon" type="image/png" href="./favicon.png" />
|
||||
<title>Account Reset | Zoraxy</title>
|
||||
<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/semantic/semantic.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background: rgb(245,245,245);
|
||||
background: linear-gradient(28deg, rgba(245,245,245,1) 63%, rgba(255,255,255,1) 100%);
|
||||
background: rgb(38,60,71);
|
||||
background: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
|
||||
}
|
||||
|
||||
.background{
|
||||
@ -34,23 +36,6 @@
|
||||
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{
|
||||
color: #9f3a38;
|
||||
margin-top: 1em;
|
||||
@ -61,18 +46,102 @@
|
||||
.backBtn{
|
||||
position: absolute;
|
||||
top: 0em;
|
||||
left: 1em;
|
||||
margin-top: -4em;
|
||||
left: 2em;
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
<div class="background"></div>
|
||||
<div id="loginForm" class="ui middle aligned center aligned grid">
|
||||
<div id="loginForm" class="ui middle aligned center aligned grid" data-aos="fade-up">
|
||||
<div class="column">
|
||||
<a class="backBtn" href="/">
|
||||
<i class="huge black chevron circle left icon"></i>
|
||||
</a>
|
||||
<form class="ui large form">
|
||||
<div class="ui basic segment">
|
||||
<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">
|
||||
</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;">
|
||||
<i class="red remove icon"></i> Unknown Error Occured
|
||||
</div>
|
||||
@ -107,12 +176,36 @@
|
||||
<a href="#" id="resendEmailLink" onclick="sendResetAccountEmail();">Resend Email</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<small>Proudly powered by Zoraxy</small>
|
||||
</form>
|
||||
<a class="backBtn" href="/">
|
||||
<i class="big chevron circle left icon" style="color: #121d37;"></i>
|
||||
</a>
|
||||
</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>
|
||||
AOS.init();
|
||||
|
||||
|
||||
var redirectionAddress = "/";
|
||||
var loginAddress = "/api/auth/login";
|
||||
$(".checkbox").checkbox();
|
||||
@ -155,6 +248,12 @@
|
||||
var token = $('#token').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
|
||||
$.post('/api/account/new', { username: username, token: token, newpw: newPassword })
|
||||
.done(function(data) {
|
||||
|
1
src/web/script/aos.css
Normal file
1
src/web/script/aos.js
Normal file
@ -107,6 +107,10 @@
|
||||
asc: 'sorted ascending',
|
||||
desc: 'sorted descending',
|
||||
compare: function(a, b) {
|
||||
if (!isNaN(parseInt(a.trim())) && !isNaN(parseInt(b.trim())) ){
|
||||
a = parseInt(a);
|
||||
b = parseInt(b);
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
} else if (a < b) {
|
||||
|
@ -191,6 +191,7 @@
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
parent.msgbox(data.error, false, 5000);
|
||||
|
||||
}else{
|
||||
parent.msgbox("Email updated");
|
||||
$(btn).html(`<i class="green check icon"></i>`);
|
||||
@ -214,14 +215,18 @@
|
||||
$("#enableCertAutoRenew").parent().checkbox("set unchecked");
|
||||
enableTrigerOnChangeEvent = true;
|
||||
}
|
||||
if (parent && parent.setACMEEnableStates){
|
||||
parent.setACMEEnableStates(!enabled);
|
||||
}
|
||||
}else{
|
||||
$("#enableToggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
|
||||
if (parent && parent.setACMEEnableStates){
|
||||
parent.setACMEEnableStates(enabled);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (parent && parent.setACMEEnableStates){
|
||||
parent.setACMEEnableStates(enabled);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -41,8 +41,7 @@
|
||||
<input id="inlineEditBasicAuthCredPassword" type="password" placeholder="Password" autocomplete="off">
|
||||
</div>
|
||||
<div class="field" >
|
||||
<button class="ui basic button" onclick="addCredentialsToEditingList();"><i class="blue 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>
|
||||
<button class="ui basic button" onclick="addCredentialsToEditingList();"><i class="green add icon"></i> Add Credential</button>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
@ -69,7 +68,7 @@
|
||||
<small>Make sure you add the tailing slash for only selecting the files / folder inside that path.</small>
|
||||
</div>
|
||||
<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 class="field">
|
||||
<div class="ui basic message">
|
||||
@ -99,7 +98,7 @@
|
||||
let payloadHash = window.location.hash.substr(1);
|
||||
try{
|
||||
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
|
||||
loadBasicAuthCredentials(payloadHash.ept, payloadHash.ep);
|
||||
loadBasicAuthCredentials(payloadHash.ep);
|
||||
$("#epname").text(payloadHash.ep);
|
||||
editingEndpoint = payloadHash;
|
||||
}catch(ex){
|
||||
@ -107,13 +106,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
function loadBasicAuthCredentials(endpointType, uuid){
|
||||
function loadBasicAuthCredentials(uuid){
|
||||
$.ajax({
|
||||
url: "/api/proxy/updateCredentials",
|
||||
method: "GET",
|
||||
data: {
|
||||
ep: uuid,
|
||||
ptype: endpointType
|
||||
},
|
||||
success: function(data){
|
||||
//Push the existing account to list
|
||||
@ -163,6 +161,9 @@
|
||||
|
||||
// Update the table body with the credentials
|
||||
updateEditingCredentialList();
|
||||
|
||||
//Save the table
|
||||
saveCredentials();
|
||||
}
|
||||
|
||||
function addExceptionPath(){
|
||||
@ -175,7 +176,6 @@
|
||||
$.ajax({
|
||||
url: "/api/proxy/auth/exceptions/add",
|
||||
data:{
|
||||
ptype: editingEndpoint.ept,
|
||||
ep: editingEndpoint.ep,
|
||||
prefix: newExclusionPathMatchingPrefix
|
||||
},
|
||||
@ -197,7 +197,6 @@
|
||||
$.ajax({
|
||||
url: "/api/proxy/auth/exceptions/delete",
|
||||
data:{
|
||||
ptype: editingEndpoint.ept,
|
||||
ep: editingEndpoint.ep,
|
||||
prefix: matchingPrefix
|
||||
},
|
||||
@ -271,6 +270,8 @@
|
||||
|
||||
// Update the table body
|
||||
updateEditingCredentialList();
|
||||
|
||||
saveCredentials();
|
||||
}
|
||||
|
||||
function alreadyExists(username){
|
||||
@ -293,7 +294,6 @@
|
||||
method: "POST",
|
||||
data: {
|
||||
ep: editingEndpoint.ep,
|
||||
ptype: editingEndpoint.ept,
|
||||
creds: JSON.stringify(editingCredentials)
|
||||
},
|
||||
success: function(data){
|
||||
|
@ -45,7 +45,7 @@
|
||||
</b></p>
|
||||
<form class="ui form" id="uploadForm" action="/api/conf/import" method="POST" enctype="multipart/form-data">
|
||||
<input type="file" name="file" id="fileInput" accept=".zip">
|
||||
<button style="margin-top: 0.6em;" class="ui basic button" type="submit"><i class="ui green upload icon"></i> Upload</button>
|
||||
<button style="margin-top: 0.6em;" class="ui basic button" type="submit"><i class="ui green upload icon"></i> Restore & Exit</button>
|
||||
</form>
|
||||
<small>The current config will be backup to ./conf_old{timestamp}. If you screw something up, you can always restore it by ssh to your host and restore the configs from the old folder.</small>
|
||||
<br><br>
|
||||
|
182
src/web/snippet/customHeaders.html
Normal 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>
|
@ -20,6 +20,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -119,11 +120,10 @@ func UpdateUptimeMonitorTargets() {
|
||||
|
||||
// Generate uptime monitor targets from reverse proxy rules
|
||||
func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Target {
|
||||
subds := dp.GetSDProxyEndpointsAsMap()
|
||||
vdirs := dp.GetVDProxyEndpointsAsMap()
|
||||
hosts := dp.GetProxyEndpointsAsMap()
|
||||
|
||||
UptimeTargets := []*uptime.Target{}
|
||||
for subd, target := range subds {
|
||||
for hostid, target := range hosts {
|
||||
url := "http://" + target.Domain
|
||||
protocol := "http"
|
||||
if target.RequireTLS {
|
||||
@ -131,27 +131,31 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
//Add the root url
|
||||
UptimeTargets = append(UptimeTargets, &uptime.Target{
|
||||
ID: subd,
|
||||
Name: subd,
|
||||
ID: hostid,
|
||||
Name: hostid,
|
||||
URL: url,
|
||||
Protocol: protocol,
|
||||
})
|
||||
}
|
||||
|
||||
for vdir, target := range vdirs {
|
||||
url := "http://" + target.Domain
|
||||
protocol := "http"
|
||||
if target.RequireTLS {
|
||||
url = "https://" + target.Domain
|
||||
protocol = "https"
|
||||
//Add each virtual directory into the list
|
||||
for _, vdir := range target.VirtualDirectories {
|
||||
url := "http://" + vdir.Domain
|
||||
protocol := "http"
|
||||
if target.RequireTLS {
|
||||
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
|
||||
@ -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
|
||||
func HandleMdnsListing(w http.ResponseWriter, r *http.Request) {
|
||||
if mdnsScanner == nil {
|
||||
|