Custom header support

+ Added custom header
+ Removed unused files
This commit is contained in:
Toby Chui 2024-02-17 20:28:19 +08:00
parent 216b53f224
commit 33c7c5fa00
30 changed files with 488 additions and 9436 deletions

View File

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

View File

@ -62,7 +62,10 @@ func initAPIs() {
authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir) authRouter.HandleFunc("/api/proxy/vdir/add", ReverseProxyAddVdir)
authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir) authRouter.HandleFunc("/api/proxy/vdir/del", ReverseProxyDeleteVdir)
authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir) authRouter.HandleFunc("/api/proxy/vdir/edit", ReverseProxyEditVdir)
//Reverse proxy user define header apis
authRouter.HandleFunc("/api/proxy/header/list", HandleCustomHeaderList)
authRouter.HandleFunc("/api/proxy/header/add", HandleCustomHeaderAdd)
authRouter.HandleFunc("/api/proxy/header/remove", HandleCustomHeaderRemove)
//Reverse proxy auth related APIs //Reverse proxy auth related APIs
authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths) authRouter.HandleFunc("/api/proxy/auth/exceptions/list", ListProxyBasicAuthExceptionPaths)
authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths) authRouter.HandleFunc("/api/proxy/auth/exceptions/add", AddProxyBasicAuthExceptionPaths)

View File

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

View File

@ -45,7 +45,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
//Inject debug headers //Inject headers
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion) w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
/* /*

View File

@ -228,7 +228,6 @@ func (router *Router) StopProxyService() error {
// Restart the current router if it is running. // Restart the current router if it is running.
func (router *Router) Restart() error { func (router *Router) Restart() error {
//Stop the router if it is already running //Stop the router if it is already running
var err error = nil
if router.Running { if router.Running {
err := router.StopProxyService() err := router.StopProxyService()
if err != nil { if err != nil {
@ -243,7 +242,7 @@ func (router *Router) Restart() error {
} }
} }
return err return nil
} }
/* /*

View File

@ -4,6 +4,9 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"strings" "strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
) )
/* /*
@ -16,6 +19,53 @@ import (
Most of the functions are implemented in dynamicproxy.go 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 // Get virtual directory handler from given URI
func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI string) *VirtualDirectoryEndpoint { func (ep *ProxyEndpoint) GetVirtualDirectoryHandlerFromRequestURI(requestURI string) *VirtualDirectoryEndpoint {
for _, vdir := range ep.VirtualDirectories { for _, vdir := range ep.VirtualDirectories {

View File

@ -1,32 +0,0 @@
package dynamicproxy
/*
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
*/
// Update change in the current running proxy endpoint config
func (ep *ProxyEndpoint) UpdateToRuntime() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
}
// Remove this proxy endpoint from running proxy endpoint list
func (ep *ProxyEndpoint) Remove() error {
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
return nil
}
// ProxyEndpoint remove provide global access by key
func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain string) error {
targetEpt, err := router.LoadProxy(rootnameOrMatchingDomain)
if err != nil {
return err
}
return targetEpt.Remove()
}

View File

@ -88,6 +88,14 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) { func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
r.Header.Set("X-Forwarded-Host", r.Host) r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
//Inject custom headers
if len(target.UserDefinedHeaders) > 0 {
for _, customHeader := range target.UserDefinedHeaders {
r.Header.Set(customHeader.Key, customHeader.Value)
}
}
requestURL := r.URL.String() requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
@ -150,6 +158,14 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
r.Header.Set("X-Forwarded-Host", r.Host) r.Header.Set("X-Forwarded-Host", r.Host)
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID) r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
//Inject custom headers
if len(target.parent.UserDefinedHeaders) > 0 {
for _, customHeader := range target.parent.UserDefinedHeaders {
r.Header.Set(customHeader.Key, customHeader.Value)
}
}
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" { if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("Zr-Origin-Upgrade", "websocket") r.Header.Set("Zr-Origin-Upgrade", "websocket")

View File

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

View File

@ -71,6 +71,7 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
proxy := dpcore.NewDynamicProxyCore(path, vdir.MatchingPath, vdir.SkipCertValidations) proxy := dpcore.NewDynamicProxyCore(path, vdir.MatchingPath, vdir.SkipCertValidations)
vdir.proxy = proxy vdir.proxy = proxy
vdir.parent = endpoint
} }
return endpoint, nil return endpoint, nil

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -70,6 +70,12 @@ type BasicAuthExceptionRule struct {
PathPrefix string PathPrefix string
} }
// 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 // A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
// program structure than directly using ProxyEndpoint // program structure than directly using ProxyEndpoint
type VirtualDirectoryEndpoint struct { type VirtualDirectoryEndpoint struct {
@ -79,6 +85,7 @@ type VirtualDirectoryEndpoint struct {
SkipCertValidations bool //Set to true to accept self signed certs SkipCertValidations bool //Set to true to accept self signed certs
Disabled bool //If the rule is enabled Disabled bool //If the rule is enabled
proxy *dpcore.ReverseProxy `json:"-"` proxy *dpcore.ReverseProxy `json:"-"`
parent *ProxyEndpoint `json:"-"`
} }
// A proxy endpoint record, a general interface for handling inbound routing // A proxy endpoint record, a general interface for handling inbound routing
@ -95,6 +102,9 @@ type ProxyEndpoint struct {
//Virtual Directories //Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
UserDefinedHeaders []*UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
//Authentication //Authentication
RequireBasicAuth bool //Set to true to request basic auth before proxy RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials

View File

@ -1,22 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDuTCCAqCgAwIBAgIBADANBgkqhkiG9w0BAQ0FADB2MQswCQYDVQQGEwJoazES
MBAGA1UECAwJSG9uZyBLb25nMRQwEgYDVQQKDAtpbXVzbGFiLmNvbTEZMBcGA1UE
AwwQWm9yYXh5IFNlbGYtaG9zdDEQMA4GA1UEBwwHSU1VU0xBQjEQMA4GA1UECwwH
SU1VU0xBQjAeFw0yMzA1MjcxMDQyNDJaFw0zODA1MjgxMDQyNDJaMHYxCzAJBgNV
BAYTAmhrMRIwEAYDVQQIDAlIb25nIEtvbmcxFDASBgNVBAoMC2ltdXNsYWIuY29t
MRkwFwYDVQQDDBBab3JheHkgU2VsZi1ob3N0MRAwDgYDVQQHDAdJTVVTTEFCMRAw
DgYDVQQLDAdJTVVTTEFCMIIBIzANBgkqhkiG9w0BAQEFAAOCARAAMIIBCwKCAQIA
xav3Qq4DBooHsGW9m+r0dgjI832grX2c0Z6MJQQoE7B6wfpUI0OyfRugTXyXoiRZ
gLxuROgiCUmp8FaLbl7RsvbImMbCPo3D/RbCT1aJCNXLZ0a7yvcDYc6woQW4nUyk
ohHfT2otcu+OYS6aYRZuXGsKTAqPSwEXRMtr89wkPgZPsrCD27LFHBOmIcVABDvF
KRuiwHWSHhFfU5n1AZLyYeYoLNQ9fZPvzPpkMD+HMKi4MMwr/vLE0DwU5jSfVFq+
cd68zVihp9N/T77yah5EIH9CYm4m8Acs4bfL8DALxnaSN3KmGw6J35rOXrJvJLdh
t42PDROmQrXN8uG8wGkBiBkCAwEAAaNQME4wHQYDVR0OBBYEFLhXihE+1K6MoL0P
Nx5htfuSatpiMB8GA1UdIwQYMBaAFLhXihE+1K6MoL0PNx5htfuSatpiMAwGA1Ud
EwQFMAMBAf8wDQYJKoZIhvcNAQENBQADggECAMCn0ed1bfLefGvoQJV/q+X9p61U
HunSFJAAhp0N2Q3tq/zjIu0kJX7N0JBciEw2c0ZmqJIqR8V8Im/h/4XuuOR+53hg
opOSPo39ww7mpxyBlQm63v1nXcNQcvw4U0JqXQ4Kyv8cgX7DIuyjRWHQpc5+6joy
L5Nz5hzQbgpnPdHQEMorfnm8q6bWg/291IAV3ZA9Z6T5gn4YuyjeUdDczQtpT6nu
1iTNPqtO6R3aeTVT+OSJT9sH2MHfDAsf371HBM6MzM/5QBc/62Bgau7NUjNKeSEA
EtUBil8wBHwT7vOtqbyNk5FHEfoCpYsQtP7AtEo10izKCQpDXPftfiJefkOY
-----END CERTIFICATE-----

View File

@ -261,6 +261,8 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
SkipCertValidations: skipTlsValidation, SkipCertValidations: skipTlsValidation,
//VDir //VDir
VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{}, VirtualDirectories: []*dynamicproxy.VirtualDirectoryEndpoint{},
//Custom headers
UserDefinedHeaders: []*dynamicproxy.UserDefinedHeader{},
//Auth //Auth
RequireBasicAuth: requireBasicAuth, RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials, BasicAuthCredentials: basicAuthCredentials,
@ -864,3 +866,139 @@ func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
utils.SendOK(w) utils.SendOK(w)
} }
/* Handle Custom Header Rules */
//List all the custom header defined in this proxy rule
func HandleCustomHeaderList(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
}
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, "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)
}

View File

@ -9,7 +9,7 @@
<tr> <tr>
<th>Host</th> <th>Host</th>
<th>Destination</th> <th>Destination</th>
<!-- <th>Virtual Directory</th> --> <th>Virtual Directory</th>
<th>Basic Auth</th> <th>Basic Auth</th>
<th class="no-sort" style="min-width:100px;">Actions</th> <th class="no-sort" style="min-width:100px;">Actions</th>
</tr> </tr>
@ -30,11 +30,11 @@
$("#httpProxyList").html(``); $("#httpProxyList").html(``);
if (data.error !== undefined){ if (data.error !== undefined){
$("#httpProxyList").append(`<tr> $("#httpProxyList").append(`<tr>
<td data-label="" colspan="4"><i class="remove icon"></i> ${data.error}</td> <td data-label="" colspan="5"><i class="remove icon"></i> ${data.error}</td>
</tr>`); </tr>`);
}else if (data.length == 0){ }else if (data.length == 0){
$("#httpProxyList").append(`<tr> $("#httpProxyList").append(`<tr>
<td data-label="" colspan="4"><i class="green check circle icon"></i> No HTTP Proxy Record</td> <td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td>
</tr>`); </tr>`);
}else{ }else{
data.forEach(subd => { data.forEach(subd => {
@ -65,12 +65,13 @@
vdList += `</div>`; vdList += `</div>`;
if (subd.VirtualDirectories.length == 0){ if (subd.VirtualDirectories.length == 0){
vdList = `<i class="ui green circle check icon"></i> No Virtual Directory Rule`; 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"> $("#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="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="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 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=""> <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 basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="edit icon"></i></button>
@ -140,6 +141,12 @@
</div> </div>
`; `;
column.empty().append(input); 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"){ }else if (datatype == "basicauth"){
let requireBasicAuth = payload.RequireBasicAuth; let requireBasicAuth = payload.RequireBasicAuth;
let checkstate = ""; let checkstate = "";
@ -150,7 +157,20 @@
<input type="checkbox" class="RequireBasicAuth" ${checkstate}> <input type="checkbox" class="RequireBasicAuth" ${checkstate}>
<label>Require Basic Auth</label> <label>Require Basic Auth</label>
</div> </div>
<button class="ui basic tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');">Edit Credentials</button>`); <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'){ }else if (datatype == 'action'){
column.empty().append(` column.empty().append(`
@ -173,6 +193,7 @@
} }
}); });
$(".endpointAdvanceConfig").accordion();
$("#httpProxyList").find(".editBtn").addClass("disabled"); $("#httpProxyList").find(".editBtn").addClass("disabled");
} }
@ -220,6 +241,7 @@
}) })
} }
/* button events */
function editBasicAuthCredentials(uuid){ function editBasicAuthCredentials(uuid){
let payload = encodeURIComponent(JSON.stringify({ let payload = encodeURIComponent(JSON.stringify({
ept: "host", ept: "host",
@ -227,6 +249,23 @@
})); }));
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload); 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 //Bind on tab switch events

View File

@ -593,7 +593,7 @@
{ {
type: 'line', type: 'line',
responsive: true, responsive: true,
resizeDelay: 100, resizeDelay: 300,
options: { options: {
animation: false, animation: false,
maintainAspectRatio: false, maintainAspectRatio: false,

View File

@ -1,79 +0,0 @@
<div class="standardContainer">
<div class="ui basic segment">
<h2>HTTP Proxy</h2>
<p>Proxy HTTP server with HTTP or HTTPS. Sometime subdomain contain 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>Host</th>
<th>Destination</th>
<th>Virtual Directory</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="listProxyEndpoints();"><i class="green refresh icon"></i> Refresh</button>
<br><br>
</div>
<script>
function listProxyEndpoints(){
$.get("/api/proxy/list?type=host", 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="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>`;
}
$("#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> ${inboundTlsIcon}</td>
<td data-label="" editable="true" datatype="domain">${subd.Domain} ${tlsIcon}</td>
<td data-label="" editable="true" datatype="vdir">WIP</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(){
listProxyEndpoints();
}
</script>

View File

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

View File

@ -207,6 +207,7 @@
} }
function reloadVdirList(){ function reloadVdirList(){
$("#vdirList").find(".editBtn").removeClass("disabled");
if ($("#useRootProxyRouterForVdir")[0].checked){ if ($("#useRootProxyRouterForVdir")[0].checked){
loadVdirList("root"); loadVdirList("root");
return; return;
@ -314,7 +315,8 @@
} }
function editVdir(matchingPath, ept){ function editVdir(matchingPath, ept){
let targetDOM = $(".vdirEntry[vdirid='" + matchingPath.hexEncode() + "']") let targetDOM = $(".vdirEntry[vdirid='" + matchingPath.hexEncode() + "']");
$("#vdirList").find(".editBtn").addClass("disabled");
let payload = $(targetDOM).attr("payload").hexDecode(); let payload = $(targetDOM).attr("payload").hexDecode();
payload = JSON.parse(payload); payload = JSON.parse(payload);
console.log(payload); console.log(payload);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 694 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -284,11 +284,12 @@
$("#mainmenu").find(".item").removeClass("active"); $("#mainmenu").find(".item").removeClass("active");
$(targetBtn).addClass("active"); $(targetBtn).addClass("active");
$(".functiontab").hide(); $(".functiontab").hide();
if (tabSwitchEventBind[tabID]){
tabSwitchEventBind[tabID]();
}
$("#" + tabID).fadeIn('fast', function(){ $("#" + tabID).fadeIn('fast', function(){
setTimeout(function(){
if (tabSwitchEventBind[tabID]){
tabSwitchEventBind[tabID]();
}
},100)
}); });
$('html,body').animate({scrollTop: 0}, 'fast'); $('html,body').animate({scrollTop: 0}, 'fast');
window.location.hash = tabID; window.location.hash = tabID;

View File

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

View File

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

View File

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

View File

@ -123,7 +123,7 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
hosts := dp.GetProxyEndpointsAsMap() hosts := dp.GetProxyEndpointsAsMap()
UptimeTargets := []*uptime.Target{} UptimeTargets := []*uptime.Target{}
for subd, target := range hosts { for hostid, target := range hosts {
url := "http://" + target.Domain url := "http://" + target.Domain
protocol := "http" protocol := "http"
if target.RequireTLS { if target.RequireTLS {
@ -131,12 +131,31 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
protocol = "https" protocol = "https"
} }
//Add the root url
UptimeTargets = append(UptimeTargets, &uptime.Target{ UptimeTargets = append(UptimeTargets, &uptime.Target{
ID: subd, ID: hostid,
Name: subd, Name: hostid,
URL: url, URL: url,
Protocol: protocol, Protocol: protocol,
}) })
//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,
})
}
} }
return UptimeTargets return UptimeTargets