mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-01 13:17:21 +02:00
Merge pull request #522 from tobychui/v3.1.7
- Merged and added new tagging system for HTTP Proxy rules - Added inline editing for redirection rules - Added uptime monitor status dot detail info (now clickable) - Added close connection support to port 80 listener - Optimized port collision check on startup - Optimized dark theme color scheme (Free consultation by [3S Design studio](https://www.3sdesign.io/)) - Fixed capital letter rule unable to delete bug
This commit is contained in:
commit
a7285438af
7
.gitignore
vendored
7
.gitignore
vendored
@ -39,4 +39,9 @@ src/tmp/localhost.pem
|
||||
src/www/html/index.html
|
||||
src/sys.uuid
|
||||
src/zoraxy
|
||||
src/log/
|
||||
src/log/
|
||||
|
||||
|
||||
# dev-tags
|
||||
/Dockerfile
|
||||
/Entrypoint.sh
|
@ -1,5 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
trap cleanup TERM INT
|
||||
|
||||
cleanup() {
|
||||
echo "Shutting down..."
|
||||
kill -TERM "$(pidof zoraxy)" &> /dev/null && echo "Zoraxy stopped."
|
||||
kill -TERM "$(pidof zerotier-one)" &> /dev/null && echo "ZeroTier-One stopped."
|
||||
exit 0
|
||||
}
|
||||
|
||||
update-ca-certificates
|
||||
echo "CA certificates updated."
|
||||
|
||||
@ -11,12 +20,13 @@ if [ "$ZEROTIER" = "true" ]; then
|
||||
mkdir -p /opt/zoraxy/config/zerotier/
|
||||
fi
|
||||
ln -s /opt/zoraxy/config/zerotier/ /var/lib/zerotier-one
|
||||
zerotier-one -d
|
||||
zerotier-one -d &
|
||||
zerotierpid=$!
|
||||
echo "ZeroTier daemon started."
|
||||
fi
|
||||
|
||||
echo "Starting Zoraxy..."
|
||||
exec zoraxy \
|
||||
zoraxy \
|
||||
-autorenew="$AUTORENEW" \
|
||||
-cfgupgrade="$CFGUPGRADE" \
|
||||
-db="$DB" \
|
||||
@ -33,5 +43,10 @@ exec zoraxy \
|
||||
-webfm="$WEBFM" \
|
||||
-webroot="$WEBROOT" \
|
||||
-ztauth="$ZTAUTH" \
|
||||
-ztport="$ZTPORT"
|
||||
-ztport="$ZTPORT" \
|
||||
&
|
||||
|
||||
zoraxypid=$!
|
||||
wait $zoraxypid
|
||||
wait $zerotierpid
|
||||
|
||||
|
@ -88,6 +88,7 @@ func RegisterRedirectionAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/edit", handleEditRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/regex", handleToggleRedirectRegexpSupport)
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,11 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
//Make sure the tags are not nil
|
||||
if thisConfigEndpoint.Tags == nil {
|
||||
thisConfigEndpoint.Tags = []string{}
|
||||
}
|
||||
|
||||
//Matching domain not set. Assume root
|
||||
if thisConfigEndpoint.RootOrMatchingDomain == "" {
|
||||
thisConfigEndpoint.RootOrMatchingDomain = "/"
|
||||
@ -175,8 +180,8 @@ func ExportConfigAsZip(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Set the Content-Type header to indicate it's a zip file
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
// Set the Content-Disposition header to specify the file name
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\"config.zip\"")
|
||||
// Set the Content-Disposition header to specify the file name, add timestamp to the filename
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\"zoraxy-config-"+time.Now().Format("2006-01-02-15-04-05")+".zip\"")
|
||||
|
||||
// Create a zip writer
|
||||
zipWriter := zip.NewWriter(w)
|
||||
|
@ -42,7 +42,7 @@ import (
|
||||
const (
|
||||
/* Build Constants */
|
||||
SYSTEM_NAME = "Zoraxy"
|
||||
SYSTEM_VERSION = "3.1.6"
|
||||
SYSTEM_VERSION = "3.1.7"
|
||||
DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */
|
||||
|
||||
/* System Constants */
|
||||
@ -87,6 +87,10 @@ var (
|
||||
allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
|
||||
enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
|
||||
|
||||
/* Default Configuration Flags */
|
||||
defaultInboundPort = flag.Int("default_inbound_port", 443, "Default web server listening port")
|
||||
defaultEnableInboundTraffic = flag.Bool("default_inbound_enabled", true, "If web server is enabled by default")
|
||||
|
||||
/* Path Configuration Flags */
|
||||
//path_database = flag.String("dbpath", "./sys.db", "Database path")
|
||||
//path_conf = flag.String("conf", "./conf", "Configuration folder path")
|
||||
|
@ -209,25 +209,18 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
http.Redirect(w, r, redirectTarget, http.StatusTemporaryRedirect)
|
||||
case DefaultSite_NotFoundPage:
|
||||
//Serve the not found page, use template if exists
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
|
||||
if err != nil {
|
||||
w.Write(page_hosterror)
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
h.serve404PageWithTemplate(w, r)
|
||||
case DefaultSite_NoResponse:
|
||||
//No response. Just close the connection
|
||||
h.Parent.logRequest(r, false, 444, "root-noresponse", domainOnly)
|
||||
h.Parent.logRequest(r, false, 444, "root-no_resp", domainOnly)
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
w.Header().Set("Connection", "close")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
conn, _, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
w.Header().Set("Connection", "close")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
@ -241,3 +234,15 @@ func (h *ProxyHandler) handleRootRouting(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, "544 - No Route Defined", 544)
|
||||
}
|
||||
}
|
||||
|
||||
// Serve 404 page with template if exists
|
||||
func (h *ProxyHandler) serve404PageWithTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
template, err := os.ReadFile(filepath.Join(h.Parent.Option.WebDirectory, "templates/notfound.html"))
|
||||
if err != nil {
|
||||
w.Write(page_hosterror)
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
}
|
||||
|
@ -17,5 +17,6 @@ func IsProxmox(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package dpcore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
@ -11,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/domainsniff"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
)
|
||||
@ -84,6 +86,7 @@ type requestCanceler interface {
|
||||
type DpcoreOptions struct {
|
||||
IgnoreTLSVerification bool //Disable all TLS verification when request pass through this proxy router
|
||||
FlushInterval time.Duration //Duration to flush in normal requests. Stream request or keep-alive request will always flush with interval of -1 (immediately)
|
||||
UseH2CRoundTripper bool //Use H2C RoundTripper for HTTP/2.0 connection
|
||||
}
|
||||
|
||||
func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOptions) *ReverseProxy {
|
||||
@ -100,8 +103,17 @@ func NewDynamicProxyCore(target *url.URL, prepender string, dpcOptions *DpcoreOp
|
||||
|
||||
}
|
||||
|
||||
//Hack the default transporter to handle more connections
|
||||
thisTransporter := http.DefaultTransport
|
||||
if dpcOptions.UseH2CRoundTripper {
|
||||
thisTransporter = &http2.Transport{
|
||||
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||
return net.Dial(network, addr)
|
||||
},
|
||||
AllowHTTP: true,
|
||||
}
|
||||
}
|
||||
|
||||
//Hack the default transporter to handle more connections
|
||||
optimalConcurrentConnection := 32
|
||||
thisTransporter.(*http.Transport).MaxIdleConns = optimalConcurrentConnection * 2
|
||||
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = optimalConcurrentConnection
|
||||
|
@ -191,7 +191,24 @@ func (router *Router) StartProxyService() error {
|
||||
w.Write([]byte("400 - Bad Request"))
|
||||
} else {
|
||||
//No defined sub-domain
|
||||
http.NotFound(w, r)
|
||||
if router.Root.DefaultSiteOption == DefaultSite_NoResponse {
|
||||
//No response. Just close the connection
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
w.Header().Set("Connection", "close")
|
||||
return
|
||||
}
|
||||
conn, _, err := hijacker.Hijack()
|
||||
if err != nil {
|
||||
w.Header().Set("Connection", "close")
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
} else {
|
||||
//Default behavior
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -337,7 +354,7 @@ func (router *Router) LoadProxy(matchingDomain string) (*ProxyEndpoint, error) {
|
||||
return true
|
||||
}
|
||||
|
||||
if key == matchingDomain {
|
||||
if key == strings.ToLower(matchingDomain) {
|
||||
targetProxyEndpoint = v
|
||||
}
|
||||
return true
|
||||
|
@ -267,7 +267,8 @@ func (ep *ProxyEndpoint) Clone() *ProxyEndpoint {
|
||||
|
||||
// Remove this proxy endpoint from running proxy endpoint list
|
||||
func (ep *ProxyEndpoint) Remove() error {
|
||||
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
|
||||
lookupHostname := strings.ToLower(ep.RootOrMatchingDomain)
|
||||
ep.parent.ProxyEndpoints.Delete(lookupHostname)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ package redirection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
@ -111,6 +110,42 @@ func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardP
|
||||
return nil
|
||||
}
|
||||
|
||||
// Edit an existing redirection rule, the oldRedirectURL is used to find the rule to be edited
|
||||
func (t *RuleTable) EditRedirectRule(oldRedirectURL string, newRedirectURL string, destURL string, forwardPathname bool, statusCode int) error {
|
||||
newRule := &RedirectRules{
|
||||
RedirectURL: newRedirectURL,
|
||||
TargetURL: destURL,
|
||||
ForwardChildpath: forwardPathname,
|
||||
StatusCode: statusCode,
|
||||
}
|
||||
|
||||
//Remove the old rule
|
||||
t.DeleteRedirectRule(oldRedirectURL)
|
||||
|
||||
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
||||
filename := utils.ReplaceSpecialCharacters(newRedirectURL) + ".json"
|
||||
filepath := path.Join(t.configPath, filename)
|
||||
|
||||
// Create a new file for writing the JSON data
|
||||
file, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
t.log("Error creating file "+filepath, err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
err = json.NewEncoder(file).Encode(newRule)
|
||||
if err != nil {
|
||||
t.log("Error encoding JSON to file "+filepath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the runtime map
|
||||
t.rules.Store(newRedirectURL, newRule)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
|
||||
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
||||
filename := utils.ReplaceSpecialCharacters(redirectURL) + ".json"
|
||||
@ -118,7 +153,6 @@ func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
|
||||
// Create the full file path by joining the t.configPath with the filename
|
||||
filepath := path.Join(t.configPath, filename)
|
||||
|
||||
fmt.Println(redirectURL, filename, filepath)
|
||||
// Check if the file exists
|
||||
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||
return nil // File doesn't exist, nothing to delete
|
||||
|
@ -194,6 +194,7 @@ type ProxyEndpoint struct {
|
||||
|
||||
//Internal Logic Elements
|
||||
parent *Router `json:"-"`
|
||||
Tags []string // Tags for the proxy endpoint
|
||||
}
|
||||
|
||||
/*
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -157,3 +157,13 @@ func resolveIpFromDomain(targetIpOrDomain string) string {
|
||||
|
||||
return targetIpAddrString
|
||||
}
|
||||
|
||||
// Check if the given port is already used by another process
|
||||
func CheckIfPortOccupied(portNumber int) bool {
|
||||
listener, err := net.Listen("tcp", ":"+strconv.Itoa(portNumber))
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
listener.Close()
|
||||
return false
|
||||
}
|
||||
|
@ -78,6 +78,49 @@ func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleEditRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
||||
originalRedirectUrl, err := utils.PostPara(r, "originalRedirectUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "original redirect url cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
newRedirectUrl, err := utils.PostPara(r, "newRedirectUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "redirect url cannot be empty")
|
||||
return
|
||||
}
|
||||
destUrl, err := utils.PostPara(r, "destUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "destination url cannot be empty")
|
||||
}
|
||||
|
||||
forwardChildpath, err := utils.PostPara(r, "forwardChildpath")
|
||||
if err != nil {
|
||||
//Assume true
|
||||
forwardChildpath = "true"
|
||||
}
|
||||
|
||||
redirectTypeString, err := utils.PostPara(r, "redirectType")
|
||||
if err != nil {
|
||||
redirectTypeString = "307"
|
||||
}
|
||||
|
||||
redirectionStatusCode, err := strconv.Atoi(redirectTypeString)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid status code number")
|
||||
return
|
||||
}
|
||||
|
||||
err = redirectTable.EditRedirectRule(originalRedirectUrl, newRedirectUrl, destUrl, forwardChildpath == "true", redirectionStatusCode)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
// Toggle redirection regex support. Note that this cost another O(n) time complexity to each page load
|
||||
func handleToggleRedirectRegexpSupport(w http.ResponseWriter, r *http.Request) {
|
||||
enabled, err := utils.PostPara(r, "enable")
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
"imuslab.com/zoraxy/mod/uptime"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
@ -27,11 +28,23 @@ func ReverseProxtInit() {
|
||||
/*
|
||||
Load Reverse Proxy Global Settings
|
||||
*/
|
||||
inboundPort := 443
|
||||
inboundPort := *defaultInboundPort
|
||||
autoStartReverseProxy := *defaultEnableInboundTraffic
|
||||
if sysdb.KeyExists("settings", "inbound") {
|
||||
//Read settings from database
|
||||
sysdb.Read("settings", "inbound", &inboundPort)
|
||||
SystemWideLogger.Println("Serving inbound port ", inboundPort)
|
||||
if netutils.CheckIfPortOccupied(inboundPort) {
|
||||
autoStartReverseProxy = false
|
||||
SystemWideLogger.Println("Inbound port ", inboundPort, " is occupied. Change the listening port in the webmin panel and press \"Start Service\" to start reverse proxy service")
|
||||
} else {
|
||||
SystemWideLogger.Println("Serving inbound port ", inboundPort)
|
||||
}
|
||||
} else {
|
||||
//Default port
|
||||
if netutils.CheckIfPortOccupied(inboundPort) {
|
||||
autoStartReverseProxy = false
|
||||
SystemWideLogger.Println("Port 443 is occupied. Change the listening port in the webmin panel and press \"Start Service\" to start reverse proxy service")
|
||||
}
|
||||
SystemWideLogger.Println("Inbound port not set. Using default (443)")
|
||||
}
|
||||
|
||||
@ -60,6 +73,9 @@ func ReverseProxtInit() {
|
||||
}
|
||||
|
||||
listenOnPort80 := true
|
||||
if netutils.CheckIfPortOccupied(80) {
|
||||
listenOnPort80 = false
|
||||
}
|
||||
sysdb.Read("settings", "listenP80", &listenOnPort80)
|
||||
if listenOnPort80 {
|
||||
SystemWideLogger.Println("Port 80 listener enabled")
|
||||
@ -136,9 +152,11 @@ func ReverseProxtInit() {
|
||||
//Start Service
|
||||
//Not sure why but delay must be added if you have another
|
||||
//reverse proxy server in front of this service
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
dynamicProxyRouter.StartProxyService()
|
||||
SystemWideLogger.Println("Dynamic Reverse Proxy service started")
|
||||
if autoStartReverseProxy {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
dynamicProxyRouter.StartProxyService()
|
||||
SystemWideLogger.Println("Dynamic Reverse Proxy service started")
|
||||
}
|
||||
|
||||
//Add all proxy services to uptime monitor
|
||||
//Create a uptime monitor service
|
||||
@ -287,6 +305,23 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
tagStr, _ := utils.PostPara(r, "tags")
|
||||
tags := []string{}
|
||||
if tagStr != "" {
|
||||
tags = strings.Split(tagStr, ",")
|
||||
for i := range tags {
|
||||
tags[i] = strings.TrimSpace(tags[i])
|
||||
}
|
||||
}
|
||||
// Remove empty tags
|
||||
filteredTags := []string{}
|
||||
for _, tag := range tags {
|
||||
if tag != "" {
|
||||
filteredTags = append(filteredTags, tag)
|
||||
}
|
||||
}
|
||||
tags = filteredTags
|
||||
|
||||
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
|
||||
if eptype == "host" {
|
||||
rootOrMatchingDomain, err := utils.PostPara(r, "rootname")
|
||||
@ -357,6 +392,8 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
// Rate Limit
|
||||
RequireRateLimit: requireRateLimit,
|
||||
RateLimit: int64(proxyRateLimit),
|
||||
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint)
|
||||
@ -515,6 +552,15 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
tagStr, _ := utils.PostPara(r, "tags")
|
||||
tags := []string{}
|
||||
if tagStr != "" {
|
||||
tags = strings.Split(tagStr, ",")
|
||||
for i := range tags {
|
||||
tags[i] = strings.TrimSpace(tags[i])
|
||||
}
|
||||
}
|
||||
|
||||
//Generate a new proxyEndpoint from the new config
|
||||
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
|
||||
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
|
||||
@ -539,6 +585,7 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
newProxyEndpoint.RateLimit = proxyRateLimit
|
||||
newProxyEndpoint.UseStickySession = useStickySession
|
||||
newProxyEndpoint.DisableUptimeMonitor = disbleUtm
|
||||
newProxyEndpoint.Tags = tags
|
||||
|
||||
//Prepare to replace the current routing rule
|
||||
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newProxyEndpoint)
|
||||
|
@ -1174,7 +1174,7 @@
|
||||
}
|
||||
|
||||
function removeIpBlacklist(ipaddr){
|
||||
if (confirm("Confirm remove blacklist for " + ipaddr + " ?")){
|
||||
//if (confirm("Confirm remove blacklist for " + ipaddr + " ?")){
|
||||
$.cjax({
|
||||
url: "/api/blacklist/ip/remove",
|
||||
type: "POST",
|
||||
@ -1191,7 +1191,7 @@
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -1318,7 +1318,7 @@
|
||||
}
|
||||
|
||||
function removeIpWhitelist(ipaddr){
|
||||
if (confirm("Confirm remove whitelist for " + ipaddr + " ?")){
|
||||
//if (confirm("Confirm remove whitelist for " + ipaddr + " ?")){
|
||||
$.cjax({
|
||||
url: "/api/whitelist/ip/remove",
|
||||
type: "POST",
|
||||
@ -1335,7 +1335,7 @@
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -11,7 +11,47 @@
|
||||
.subdEntry td:not(.ignoremw){
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.httpProxyListTools{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tag-select{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tag-select:hover{
|
||||
text-decoration: underline;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
<div class="httpProxyListTools" style="margin-bottom: 1em;">
|
||||
<div id="tagFilterDropdown" class="ui floating basic dropdown labeled icon button" style="min-width: 150px;">
|
||||
<i class="filter icon"></i>
|
||||
<span class="text">Filter by tags</span>
|
||||
<div class="menu">
|
||||
<div class="ui icon search input">
|
||||
<i class="search icon"></i>
|
||||
<input type="text" placeholder="Search tags...">
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="scrolling menu tagList">
|
||||
<!--
|
||||
Example:
|
||||
<div class="item">
|
||||
<div class="ui red empty circular label"></div>
|
||||
Important
|
||||
</div>
|
||||
-->
|
||||
<!-- Add more tag options dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui small input" style="width: 300px; height: 38px;">
|
||||
<input type="text" id="searchInput" placeholder="Quick Search" onkeydown="handleSearchInput(event);" onchange="handleSearchInput(event);" onblur="handleSearchInput(event);">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="width: 100%; overflow-x: auto; margin-bottom: 1em; min-height: 300px;">
|
||||
<table class="ui celled sortable unstackable compact table">
|
||||
<thead>
|
||||
@ -19,6 +59,7 @@
|
||||
<th>Host</th>
|
||||
<th>Destination</th>
|
||||
<th>Virtual Directory</th>
|
||||
<th>Tags</th>
|
||||
<th style="max-width: 300px;">Advanced Settings</th>
|
||||
<th class="no-sort" style="min-width:150px;">Actions</th>
|
||||
</tr>
|
||||
@ -124,6 +165,11 @@
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="" editable="true" datatype="vdir">${vdList}</td>
|
||||
<td data-label="tags" payload="${encodeURIComponent(JSON.stringify(subd.Tags))}" datatype="tags">
|
||||
<div class="tags-list">
|
||||
${subd.Tags.length >0 ? subd.Tags.map(tag => `<span class="ui tiny label tag-select" style="background-color: ${getTagColorByName(tag)}; color: ${getTagTextColor(tag)}">${tag}</span>`).join(""):"<small style='opacity: 0.3; pointer-events: none; user-select: none;'>No Tags</small>"}
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
|
||||
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
|
||||
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Authelia`:``}
|
||||
@ -142,6 +188,7 @@
|
||||
</td>
|
||||
</tr>`);
|
||||
});
|
||||
populateTagFilterDropdown(data);
|
||||
}
|
||||
|
||||
resolveAccessRuleNameOnHostRPlist();
|
||||
@ -285,7 +332,11 @@
|
||||
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 == "tags"){
|
||||
column.append(`
|
||||
<div class="ui divider"></div>
|
||||
<button class="ui basic compact fluid tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editTags('${uuid}');"><i class="ui purple tag icon"></i> Edit tags</button>
|
||||
`);
|
||||
}else if (datatype == "advanced"){
|
||||
let authProvider = payload.AuthenticationProvider.AuthMethod;
|
||||
|
||||
@ -457,7 +508,12 @@
|
||||
let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked;
|
||||
let rateLimit = $(row).find(".RateLimit").val();
|
||||
let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked;
|
||||
|
||||
let tags = getTagsArrayFromEndpoint(uuid);
|
||||
if (tags.length > 0){
|
||||
tags = tags.join(",");
|
||||
}else{
|
||||
tags = "";
|
||||
}
|
||||
$.cjax({
|
||||
url: "/api/proxy/edit",
|
||||
method: "POST",
|
||||
@ -470,6 +526,7 @@
|
||||
"authprovider" :authProviderType,
|
||||
"rate" :requireRateLimit,
|
||||
"ratenum" :rateLimit,
|
||||
"tags": tags,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error !== undefined){
|
||||
@ -609,4 +666,110 @@
|
||||
tabSwitchEventBind["httprp"] = function(){
|
||||
listProxyEndpoints();
|
||||
}
|
||||
|
||||
/* Tags & Search */
|
||||
function handleSearchInput(event){
|
||||
if (event.key == "Escape"){
|
||||
$("#searchInput").val("");
|
||||
}
|
||||
filterProxyList();
|
||||
}
|
||||
|
||||
// Function to filter the proxy list
|
||||
function filterProxyList() {
|
||||
let searchInput = $("#searchInput").val().toLowerCase();
|
||||
let selectedTag = $("#tagFilterDropdown").dropdown('get value');
|
||||
$("#httpProxyList tr").each(function() {
|
||||
let host = $(this).find("td[data-label='']").text().toLowerCase();
|
||||
let tagElements = $(this).find("td[data-label='tags']");
|
||||
let tags = tagElements.attr("payload");
|
||||
tags = JSON.parse(decodeURIComponent(tags));
|
||||
if ((host.includes(searchInput) || searchInput === "") && (tags.includes(selectedTag) || selectedTag === "")) {
|
||||
$(this).show();
|
||||
} else {
|
||||
$(this).hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to generate a color based on a tag name
|
||||
function getTagColorByName(tagName) {
|
||||
function hashCode(str) {
|
||||
return str.split('').reduce((prevHash, currVal) =>
|
||||
((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0);
|
||||
}
|
||||
let hash = hashCode(tagName);
|
||||
let color = '#' + ((hash >> 24) & 0xFF).toString(16).padStart(2, '0') +
|
||||
((hash >> 16) & 0xFF).toString(16).padStart(2, '0') +
|
||||
((hash >> 8) & 0xFF).toString(16).padStart(2, '0');
|
||||
return color;
|
||||
}
|
||||
|
||||
function getTagTextColor(tagName){
|
||||
let color = getTagColorByName(tagName);
|
||||
let r = parseInt(color.substr(1, 2), 16);
|
||||
let g = parseInt(color.substr(3, 2), 16);
|
||||
let b = parseInt(color.substr(5, 2), 16);
|
||||
let brightness = Math.round(((r * 299) + (g * 587) + (b * 114)) / 1000);
|
||||
return brightness > 125 ? "#000000" : "#ffffff";
|
||||
}
|
||||
|
||||
// Populate the tag filter dropdown
|
||||
function populateTagFilterDropdown(data) {
|
||||
let tags = new Set();
|
||||
data.forEach(subd => {
|
||||
subd.Tags.forEach(tag => tags.add(tag));
|
||||
});
|
||||
tags = Array.from(tags).sort((a, b) => a.localeCompare(b));
|
||||
let dropdownMenu = $("#tagFilterDropdown .tagList");
|
||||
dropdownMenu.html(`<div class="item tag-select" data-value="">
|
||||
<div class="ui grey empty circular label"></div>
|
||||
Show all
|
||||
</div>`);
|
||||
tags.forEach(tag => {
|
||||
let thisTagColor = getTagColorByName(tag);
|
||||
dropdownMenu.append(`<div class="item tag-select" data-value="${tag}">
|
||||
<div class="ui empty circular label" style="background-color: ${thisTagColor}; border-color: ${thisTagColor};" ></div>
|
||||
${tag}
|
||||
</div>`);
|
||||
});
|
||||
}
|
||||
|
||||
// Edit tags for a specific endpoint
|
||||
function editTags(uuid){
|
||||
let payload = encodeURIComponent(JSON.stringify({
|
||||
ept: "host",
|
||||
ep: uuid
|
||||
}));
|
||||
showSideWrapper("snippet/tagEditor.html?t=" + Date.now() + "#" + payload);
|
||||
}
|
||||
|
||||
// Render the tags preview from tag editing snippet
|
||||
function renderTagsPreview(endpoint, tags){
|
||||
let targetProxyRuleEle = $(".subdEntry[eptuuid='" + endpoint + "'] td[data-label='tags']");
|
||||
//Update the tag DOM
|
||||
let newTagDOM = tags.map(tag => `<span class="ui tiny label tag-select" style="background-color: ${getTagColorByName(tag)}; color: ${getTagTextColor(tag)}">${tag}</span>`).join("");
|
||||
$(targetProxyRuleEle).find(".tags-list").html(newTagDOM);
|
||||
|
||||
//Update the tag payload
|
||||
$(targetProxyRuleEle).attr("payload", encodeURIComponent(JSON.stringify(tags)));
|
||||
}
|
||||
|
||||
function getTagsArrayFromEndpoint(endpoint){
|
||||
let targetProxyRuleEle = $(".subdEntry[eptuuid='" + endpoint + "'] td[data-label='tags']");
|
||||
let tags = $(targetProxyRuleEle).attr("payload");
|
||||
return JSON.parse(decodeURIComponent(tags));
|
||||
}
|
||||
|
||||
// Initialize the proxy list on page load
|
||||
$(document).ready(function() {
|
||||
listProxyEndpoints();
|
||||
|
||||
// Event listener for clicking on tags
|
||||
$(document).on('click', '.tag-select', function() {
|
||||
let tag = $(this).text().trim();
|
||||
$('#tagFilterDropdown').dropdown('set selected', tag);
|
||||
filterProxyList();
|
||||
});
|
||||
});
|
||||
</script>
|
@ -13,7 +13,7 @@
|
||||
<th>Destination URL</th>
|
||||
<th class="no-sort">Copy Pathname</th>
|
||||
<th class="no-sort">Status Code</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
<th class="no-sort">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="redirectionRuleList">
|
||||
@ -163,13 +163,21 @@
|
||||
$("#redirectionRuleList").html("");
|
||||
$.get("/api/redirect/list", function(data){
|
||||
data.forEach(function(entry){
|
||||
$("#redirectionRuleList").append(`<tr>
|
||||
<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>
|
||||
<td><button onclick="deleteRule(this);" rurl="${encodeURIComponent(JSON.stringify(entry.RedirectURL))}" title="Delete redirection rule" class="ui mini red icon basic button"><i class="trash icon"></i></button></td>
|
||||
</tr>`);
|
||||
let encodedEntry = encodeURIComponent(JSON.stringify(entry));
|
||||
let hrefURL = entry.RedirectURL;
|
||||
if (!hrefURL.startsWith("http")){
|
||||
hrefURL = "https://" + hrefURL;
|
||||
}
|
||||
$("#redirectionRuleList").append(`<tr>
|
||||
<td><a href="${hrefURL}" 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>
|
||||
<td>
|
||||
<button onclick="editRule(this);" payload="${encodedEntry}" title="Edit redirection rule" class="ui mini circular icon basic button redirectEditBtn"><i class="edit icon"></i></button>
|
||||
<button onclick="deleteRule(this);" rurl="${encodeURIComponent(JSON.stringify(entry.RedirectURL))}" title="Delete redirection rule" class="ui mini red circular icon basic button"><i class="trash icon"></i></button>
|
||||
</td>
|
||||
</tr>`);
|
||||
});
|
||||
|
||||
if (data.length == 0){
|
||||
@ -180,6 +188,68 @@
|
||||
}
|
||||
initRedirectionRuleList();
|
||||
|
||||
function editRule(obj){
|
||||
$(".redirectEditBtn").addClass("disabled");
|
||||
let payload = JSON.parse(decodeURIComponent($(obj).attr("payload")));
|
||||
let row = $(obj).closest("tr");
|
||||
let redirectUrl = payload.RedirectURL;
|
||||
let destUrl = payload.TargetURL;
|
||||
let forwardChildpath = payload.ForwardChildpath;
|
||||
let statusCode = payload.StatusCode;
|
||||
|
||||
row.html(`
|
||||
<td>
|
||||
<div class="ui small input">
|
||||
<input type="text" value="${redirectUrl}" id="editRedirectUrl">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="ui small input">
|
||||
<input type="text" value="${destUrl}" id="editDestUrl">
|
||||
</div>
|
||||
</td>
|
||||
<td><div class="ui toggle checkbox"><input type="checkbox" ${forwardChildpath ? "checked" : ""} id="editForwardChildpath"><label></label></div></td>
|
||||
<td>
|
||||
<div class="ui radio checkbox"><input type="radio" name="editStatusCode" value="307" ${statusCode == 307 ? "checked" : ""}><label>Temporary Redirect (307)</label></div><br>
|
||||
<div class="ui radio checkbox"><input type="radio" name="editStatusCode" value="301" ${statusCode == 301 ? "checked" : ""}><label>Moved Permanently (301)</label></div>
|
||||
</td>
|
||||
<td>
|
||||
<button onclick="saveEditRule(this);" payload="${encodeURIComponent(JSON.stringify(payload))}" class="ui small circular green icon basic button"><i class="save icon"></i></button>
|
||||
<button onclick="initRedirectionRuleList();" class="ui small circular icon basic button"><i class="cancel icon"></i></button>
|
||||
</td>
|
||||
`);
|
||||
|
||||
$(".checkbox").checkbox();
|
||||
}
|
||||
|
||||
function saveEditRule(obj){
|
||||
let payload = JSON.parse(decodeURIComponent($(obj).attr("payload")));
|
||||
let redirectUrl = $("#editRedirectUrl").val();
|
||||
let destUrl = $("#editDestUrl").val();
|
||||
let forwardChildpath = $("#editForwardChildpath").is(":checked");
|
||||
let statusCode = parseInt($("input[name='editStatusCode']:checked").val());
|
||||
|
||||
$.cjax({
|
||||
url: "/api/redirect/edit",
|
||||
method: "POST",
|
||||
data: {
|
||||
originalRedirectUrl: payload.RedirectURL,
|
||||
newRedirectUrl: redirectUrl,
|
||||
destUrl: destUrl,
|
||||
forwardChildpath: forwardChildpath,
|
||||
redirectType: statusCode,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
msgbox(data.error, false);
|
||||
}else{
|
||||
msgbox("Redirection rule updated", true);
|
||||
initRedirectionRuleList();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initRegexpSupportToggle(){
|
||||
$.get("/api/redirect/regex", function(data){
|
||||
//Set the checkbox initial state
|
||||
|
@ -63,6 +63,11 @@
|
||||
<label>Sticky Session<br><small>Enable stick session on upstream load balancing</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Tags</label>
|
||||
<input type="text" id="proxyTags" placeholder="e.g. mediaserver, management">
|
||||
<small>Comma-separated list of tags for this proxy host.</small>
|
||||
</div>
|
||||
<div class="ui horizontal divider">
|
||||
<i class="ui green lock icon"></i>
|
||||
Security
|
||||
@ -198,6 +203,7 @@
|
||||
let skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked;
|
||||
let accessRuleToUse = $("#newProxyRuleAccessFilter").val();
|
||||
let useStickySessionLB = $("#useStickySessionLB")[0].checked;
|
||||
let tags = $("#proxyTags").val().trim();
|
||||
|
||||
if (rootname.trim() == ""){
|
||||
$("#rootname").parent().addClass("error");
|
||||
@ -231,6 +237,7 @@
|
||||
cred: JSON.stringify(credentials),
|
||||
access: accessRuleToUse,
|
||||
stickysess: useStickySessionLB,
|
||||
tags: tags,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
@ -239,6 +246,7 @@
|
||||
//Clear old data
|
||||
$("#rootname").val("");
|
||||
$("#proxyDomain").val("");
|
||||
$("#proxyTags").val("");
|
||||
credentials = [];
|
||||
updateTable();
|
||||
reloadUptimeList();
|
||||
|
@ -108,6 +108,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
function showStatusDotInfo(targetDot){
|
||||
$(".statusbar .selectedDotInfo").hide();
|
||||
let payload = $(targetDot).attr("payload");
|
||||
let statusData = JSON.parse(decodeURIComponent(payload));
|
||||
let statusDotInfoEle = $(targetDot).parent().parent().find(".selectedDotInfo");
|
||||
let statusInfoEle = $(statusDotInfoEle).find(".status_dot_status_info");
|
||||
//Fill in the data to the info box
|
||||
$(statusDotInfoEle).find(".status_dot_timestamp").text(format_time(statusData.Timestamp));
|
||||
$(statusDotInfoEle).find(".status_dot_latency").text(statusData.Latency + "ms");
|
||||
$(statusDotInfoEle).find(".status_dot_status_code").text(statusData.StatusCode);
|
||||
|
||||
|
||||
//Set the class of the info box if status code is 5xx
|
||||
$(statusDotInfoEle).removeClass("yellow");
|
||||
$(statusDotInfoEle).removeClass("red");
|
||||
$(statusDotInfoEle).removeClass("green");
|
||||
if (statusData.StatusCode >= 500 && statusData.StatusCode < 600){
|
||||
$(statusDotInfoEle).addClass("yellow");
|
||||
$(statusInfoEle).text(httpErrorStatusCodeToText(statusData.StatusCode));
|
||||
}else if (statusData.StatusCode == 0 && !statusData.Online){
|
||||
$(statusDotInfoEle).addClass("red");
|
||||
$(statusInfoEle).text("Upstream is offline");
|
||||
}else{
|
||||
$(statusDotInfoEle).addClass("green");
|
||||
$(statusInfoEle).text("Upstream Online");
|
||||
}
|
||||
|
||||
$(statusDotInfoEle).show();
|
||||
}
|
||||
|
||||
|
||||
function renderUptimeData(key, value){
|
||||
if (value.length == 0){
|
||||
@ -132,6 +162,7 @@
|
||||
let thisStatus = value[i];
|
||||
let dotType = "";
|
||||
let statusCode = thisStatus.StatusCode;
|
||||
let statusDotPayload = encodeURIComponent(JSON.stringify(thisStatus));
|
||||
|
||||
if (!thisStatus.Online && statusCode == 0){
|
||||
dotType = "offline";
|
||||
@ -159,7 +190,7 @@
|
||||
}
|
||||
|
||||
let datetime = format_time(thisStatus.Timestamp);
|
||||
statusDotList += `<div title="${datetime}" class="${dotType} statusDot"></div>`
|
||||
statusDotList += `<div title="${datetime}" class="${dotType} statusDot" payload="${statusDotPayload}" onclick="showStatusDotInfo(this);"></div>`
|
||||
}
|
||||
|
||||
ontimeRate = ontimeRate / value.length * 100;
|
||||
@ -207,7 +238,7 @@
|
||||
|
||||
onlineStatusCss = `color: #f38020;`;
|
||||
reminderEle = `<small style="${onlineStatusCss}">Target online but not accessible</small>`;
|
||||
|
||||
|
||||
}else{
|
||||
currentOnlineStatus = `<i class="circle icon"></i> Offline`;
|
||||
onlineStatusCss = `color: #df484a;`;
|
||||
@ -233,8 +264,71 @@
|
||||
${statusDotList}
|
||||
</div>
|
||||
${reminderEle}
|
||||
<div class="ui basic segment selectedDotInfo" style="display:none; border: 0.4em;">
|
||||
<div class="ui list">
|
||||
<div class="item"><b>Timestamp</b>: <span class="status_dot_timestamp"></span></div>
|
||||
<div class="item"><b>Latency</b>: <span class="status_dot_latency"></span></div>
|
||||
<div class="item"><b>Status Code</b>: <span class="status_dot_status_code"></span></div>
|
||||
<div class="item"><b>Status Info</b>: <span class="status_dot_status_info"></span></div>
|
||||
</div>
|
||||
<button onclick="$(this).parent().hide();" style="position: absolute; right: 0.4em; top: 0.6em;" class="ui basic tiny circular icon button"><i class="ui times icon"></i></button>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function httpErrorStatusCodeToText(statusCode){
|
||||
switch(statusCode){
|
||||
case 400:
|
||||
return "Bad Request";
|
||||
case 401:
|
||||
return "Unauthorized";
|
||||
case 403:
|
||||
return "Forbidden";
|
||||
case 404:
|
||||
return "Not Found";
|
||||
case 405:
|
||||
return "Method Not Allowed";
|
||||
case 500:
|
||||
return "Internal Server Error";
|
||||
case 501:
|
||||
return "Not Implemented";
|
||||
case 502:
|
||||
return "Bad Gateway";
|
||||
case 503:
|
||||
return "Service Unavailable";
|
||||
case 504:
|
||||
return "Gateway Timeout";
|
||||
case 505:
|
||||
return "HTTP Version Not Supported";
|
||||
case 506:
|
||||
return "Variant Also Negotiates";
|
||||
case 507:
|
||||
return "Insufficient Storage";
|
||||
case 508:
|
||||
return "Loop Detected";
|
||||
case 510:
|
||||
return "Not Extended";
|
||||
case 511:
|
||||
return "Network Authentication Required";
|
||||
case 520:
|
||||
return "Web Server Returned an Unknown Error (Cloudflare)";
|
||||
case 521:
|
||||
return "Web Server is Down (Cloudflare)";
|
||||
case 522:
|
||||
return "Connection Timed Out (Cloudflare)";
|
||||
case 523:
|
||||
return "Origin is Unreachable (Cloudflare)";
|
||||
case 524:
|
||||
return "A Timeout Occurred (Cloudflare)";
|
||||
case 525:
|
||||
return "SSL Handshake Failed (Cloudflare)";
|
||||
case 526:
|
||||
return "Invalid SSL Certificate (Cloudflare)";
|
||||
case 527:
|
||||
return "Railgun Error (Cloudflare)";
|
||||
default:
|
||||
return "Unknown Error";
|
||||
}
|
||||
}
|
||||
</script>
|
@ -23,6 +23,10 @@ body:not(.darkTheme){
|
||||
--text_color_inverted: #fcfcfc;
|
||||
--button_text_color: #878787;
|
||||
--button_border_color: #dedede;
|
||||
--buttom_toggle_active: #01dc64;
|
||||
--buttom_toggle_disabled: #f2f2f2;
|
||||
--table_bg_default: transparent;
|
||||
--status_dot_bg: #e8e8e8;
|
||||
|
||||
--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%);
|
||||
@ -31,10 +35,10 @@ body:not(.darkTheme){
|
||||
}
|
||||
|
||||
body.darkTheme{
|
||||
--theme_bg: #0a090e;
|
||||
--theme_bg_primary: #060912;
|
||||
--theme_bg_secondary:#172a41;
|
||||
--theme_highlight: #4380b0;
|
||||
--theme_bg: #1e1e1e;
|
||||
--theme_bg_primary: #151517;
|
||||
--theme_bg_secondary:#1b3572;
|
||||
--theme_highlight: #6a7792;
|
||||
--theme_bg_active: #020101;
|
||||
--theme_bg_inverted: #f8f8f9;
|
||||
--theme_advance: #000000;
|
||||
@ -47,8 +51,12 @@ body.darkTheme{
|
||||
--text_color_inverted: #414141;
|
||||
--button_text_color: #e9e9e9;
|
||||
--button_border_color: #646464;
|
||||
--buttom_toggle_active: #01dc64;
|
||||
--buttom_toggle_disabled: #2b2b2b;
|
||||
--table_bg_default: #121214;
|
||||
--status_dot_bg: #232323;
|
||||
|
||||
--theme_background: linear-gradient(214deg, rgba(3,1,70,1) 17%, rgb(1, 55, 80) 78%);
|
||||
--theme_background: linear-gradient(23deg, rgba(2,74,106,1) 17%, rgba(46,12,136,1) 86%);
|
||||
--theme_background_inverted: linear-gradient(215deg, rgba(38,60,71,1) 13%, rgba(2,3,42,1) 84%);
|
||||
--theme_green: linear-gradient(214deg, rgba(25,128,94,1) 17%, rgba(62,76,111,1) 78%);
|
||||
--theme_red: linear-gradient(203deg, rgba(250,172,38,1) 17%, rgba(202,0,37,1) 78%);
|
||||
@ -113,6 +121,9 @@ body.darkTheme .ui.basic.button:not(.red) {
|
||||
body.darkTheme .ui.basic.button:not(.red):hover {
|
||||
border: 1px solid var(--button_border_color) !important;
|
||||
background-color: var(--theme_bg) !important;
|
||||
}
|
||||
|
||||
body.darkTheme .ui.basic.button:not(.red):not(.dropdown):hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@ -195,10 +206,14 @@ body.darkTheme textarea:focus {
|
||||
}
|
||||
|
||||
body.darkTheme .ui.toggle.checkbox input ~ label::before{
|
||||
background-color: var(--theme_bg_secondary) !important;
|
||||
background-color: var(--buttom_toggle_disabled) !important;
|
||||
}
|
||||
body.darkTheme .ui.toggle.checkbox input:checked ~ label::before{
|
||||
background-color: var(--theme_highlight) !important;
|
||||
background-color: var(--buttom_toggle_active) !important;
|
||||
}
|
||||
|
||||
body.darkTheme .ui.checkbox:not(.toggle) input[type="checkbox"]{
|
||||
opacity: 100% !important;
|
||||
}
|
||||
|
||||
#sidemenuBtn{
|
||||
@ -444,7 +459,7 @@ body.darkTheme .ui.table{
|
||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table thead th,
|
||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td,
|
||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table tfoot td {
|
||||
background-color: var(--theme_bg) !important;
|
||||
background-color: var(--table_bg_default) !important;
|
||||
color: var(--text_color) !important;
|
||||
border-color: var(--divider_color) !important;
|
||||
}
|
||||
@ -476,11 +491,11 @@ body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td .ui.toggle
|
||||
}
|
||||
|
||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td .ui.toggle.checkbox input ~ label::before {
|
||||
background-color: var(--theme_bg_secondary) !important;
|
||||
background-color: var(--buttom_toggle_disabled) !important;
|
||||
}
|
||||
|
||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td .ui.toggle.checkbox input:checked ~ label::before {
|
||||
background-color: var(--theme_highlight) !important;
|
||||
background-color: var(--buttom_toggle_active) !important;
|
||||
}
|
||||
|
||||
body.darkTheme .ui.celled.sortable.unstackable.compact.table tbody td .ui.circular.mini.basic.icon.button {
|
||||
@ -537,6 +552,18 @@ body.darkTheme .RateLimit input {
|
||||
border-color: var(--theme_highlight) !important;
|
||||
}
|
||||
|
||||
body.darkTheme .menu.transition{
|
||||
background-color: var(--theme_bg) !important;
|
||||
color: var(--text_color) !important;
|
||||
}
|
||||
|
||||
body.darkTheme .ui.dropdown .menu{
|
||||
background: var(--theme_bg_primary) !important;
|
||||
}
|
||||
|
||||
body.darkTheme .ui.dropdown .menu .item{
|
||||
color: var(--text_color) !important;
|
||||
}
|
||||
/*
|
||||
Virtual Directorie Table
|
||||
*/
|
||||
@ -714,7 +741,7 @@ body.darkTheme #redirectset .ui.sortable.unstackable.celled.table thead th {
|
||||
}
|
||||
|
||||
body.darkTheme #redirectset .ui.sortable.unstackable.celled.table tbody tr td {
|
||||
background-color: var(--theme_bg) !important;
|
||||
background-color: var(--table_bg_default) !important;
|
||||
color: var(--text_color) !important;
|
||||
border-color: var(--divider_color) !important;
|
||||
}
|
||||
@ -833,7 +860,7 @@ body.darkTheme #access .ui.unstackable.basic.celled.table thead th {
|
||||
}
|
||||
|
||||
body.darkTheme #access .ui.unstackable.basic.celled.table tbody tr td {
|
||||
background-color: var(--theme_bg) !important;
|
||||
background-color: var(--table_bg_default) !important;
|
||||
color: var(--text_color) !important;
|
||||
border-color: var(--divider_color) !important;
|
||||
}
|
||||
@ -985,8 +1012,8 @@ body.darkTheme #utm .standardContainer {
|
||||
}
|
||||
|
||||
body.darkTheme #utm .standardContainer .padding.statusDot {
|
||||
background-color: var(--theme_bg) !important;
|
||||
border: 0.2px solid var(--text_color_inverted) !important;
|
||||
background-color: var(--status_dot_bg) !important;
|
||||
|
||||
}
|
||||
|
||||
body.darkTheme .ui.utmloading.segment {
|
||||
@ -1116,7 +1143,7 @@ body.darkTheme .statistic .label {
|
||||
/* Other Tables */
|
||||
|
||||
body.darkTheme .ui.celled.compact.table {
|
||||
background-color: var(--theme_bg) !important;
|
||||
background-color: var(--table_bg_default) !important;
|
||||
color: var(--text_color) !important;
|
||||
border-color: var(--divider_color) !important;
|
||||
}
|
||||
|
260
src/web/snippet/tagEditor.html
Normal file
260
src/web/snippet/tagEditor.html
Normal file
@ -0,0 +1,260 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
|
||||
<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>
|
||||
<script src="../script/utils.js"></script>
|
||||
<style>
|
||||
.ui.circular.label.tag-color{
|
||||
min-width: 5px !important;
|
||||
min-height: 5px !important;
|
||||
margin-right: .4em;
|
||||
margin-bottom: -0.2em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<link rel="stylesheet" href="../darktheme.css">
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui header">
|
||||
<div class="content">
|
||||
Edit Tags
|
||||
<div class="sub header" id="epname"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<p>Tags currently applied to this host name / proxy rule</p>
|
||||
<div style="max-height: 300px; overflow-y: scroll;">
|
||||
<table class="ui compact basic unstackable celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag Name</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tagsTableBody">
|
||||
<!-- Rows will be dynamically added here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<h4>Add New Tags</h4>
|
||||
<p>Create new tag or add this proxy rule to an existing tag</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>New Tags</label>
|
||||
<input type="text" id="tagsInput" placeholder="e.g. mediaserver, management">
|
||||
</div>
|
||||
<button class="ui basic button" onclick="addSelectedTags();"><i class="ui blue plus icon"></i> Add tag</button>
|
||||
<div class="ui horizontal divider">
|
||||
Or
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Join Existing Tag Groups</label>
|
||||
<div class="ui fluid multiple search selection dropdown" id="existingTagsDropdown">
|
||||
<input type="hidden" id="existingTagsInput">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">Select Tags</div>
|
||||
<div class="menu" id="existingTagsMenu">
|
||||
<!-- Options will be dynamically added here -->
|
||||
</div>
|
||||
</div>
|
||||
<small id="notagwarning" style="display:none;"><i class="ui green circle check icon"></i> All tags has already been included in this host</small>
|
||||
</div>
|
||||
<button class="ui basic button" onclick="joinSelectedTagGroups();"><i class="ui blue plus icon"></i> Join tag group(s)</button>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<!-- <button class="ui basic button" onclick="saveTags();"><i class="ui green save icon"></i> Save Changes</button> -->
|
||||
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
|
||||
</div>
|
||||
<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;
|
||||
loadTags();
|
||||
}catch(ex){
|
||||
console.log("Unable to load endpoint data from hash")
|
||||
}
|
||||
}
|
||||
|
||||
function loadTags(){
|
||||
$.get("/api/proxy/detail", { type: "host", epname: editingEndpoint.ep }, function(data){
|
||||
if (data.error == undefined){
|
||||
//Render the tags to the table
|
||||
$("#tagsTableBody").empty();
|
||||
data.Tags.forEach(tag => {
|
||||
addTagRow(tag);
|
||||
});
|
||||
|
||||
if (data.Tags.length == 0){
|
||||
appendNoTagNotice();
|
||||
}
|
||||
} else {
|
||||
parent.msgbox(data.error, false);
|
||||
}
|
||||
|
||||
//Populate the dropdown with all tags created in the system
|
||||
populateTagsDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
//Append or remove a notice to the table when no tags are applied
|
||||
function appendNoTagNotice(){
|
||||
$("#tagsTableBody").append(`<tr class="notagNotice" style="opacity: 0.5; pointer-events: none; user-select: none;"><td colspan="2"><i class="ui green circle check icon"></i> No tags applied to this host</td></tr>`);
|
||||
}
|
||||
|
||||
function removeNoTagNotice(){
|
||||
$("#tagsTableBody .notagNotice").remove();
|
||||
}
|
||||
|
||||
//Load all tags created in this system
|
||||
function loadAllCreatedTags(callback){
|
||||
$.get("/api/proxy/list?type=host", function(data){
|
||||
if (data.error !== undefined){
|
||||
//No existsing rule created yet. Fresh install?
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
let tags = {};
|
||||
data.forEach(host => {
|
||||
host.Tags.forEach(tag => {
|
||||
tags[tag] = true;
|
||||
});
|
||||
});
|
||||
|
||||
let uniqueTags = Object.keys(tags);
|
||||
callback(uniqueTags);
|
||||
});
|
||||
}
|
||||
|
||||
//Populate the dropdown with all tags created in the system
|
||||
function populateTagsDropdown(){
|
||||
loadAllCreatedTags(function(tags) {
|
||||
let existingTags = new Set();
|
||||
$('#tagsTableBody tr').each(function() {
|
||||
existingTags.add($(this).attr('value'));
|
||||
});
|
||||
tags = tags.filter(tag => !existingTags.has(tag));
|
||||
$('#existingTagsMenu').empty();
|
||||
tags.forEach(tag => {
|
||||
$('#existingTagsMenu').append(`<div class="item" data-value="${tag}">${tag}</div>`);
|
||||
});
|
||||
$('#existingTagsDropdown').dropdown();
|
||||
|
||||
if (tags.length == 0){
|
||||
$('#notagwarning').show();
|
||||
}else{
|
||||
$('#notagwarning').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tagAlreadyExistsInTable(tag) {
|
||||
return $(`#tagsTableBody .tagEntry[value="${tag}"]`).length > 0;
|
||||
}
|
||||
|
||||
function addSelectedTags() {
|
||||
let tags = $('#tagsInput').val().split(',').map(tag => tag.trim());
|
||||
tags.forEach(tag => {
|
||||
if (tag && !tagAlreadyExistsInTable(tag)) {
|
||||
addTagRow(tag);
|
||||
}
|
||||
});
|
||||
console.log(tags);
|
||||
populateTagsDropdown();
|
||||
$('#tagsInput').val('');
|
||||
saveTags();
|
||||
}
|
||||
|
||||
function joinSelectedTagGroups() {
|
||||
if ($('#existingTagsInput').val() == ""){
|
||||
parent.msgbox("Please select at least one tag group to join", false);
|
||||
return;
|
||||
}
|
||||
let selectedTags = $('#existingTagsInput').val().split(',');
|
||||
selectedTags.forEach(tag => {
|
||||
if (tag && !tagAlreadyExistsInTable(tag)) {
|
||||
addTagRow(tag);
|
||||
}
|
||||
});
|
||||
populateTagsDropdown();
|
||||
$('#existingTagsDropdown').dropdown('clear');
|
||||
saveTags();
|
||||
}
|
||||
|
||||
|
||||
// Function to generate a color based on a tag name
|
||||
function getTagColorByName(tagName) {
|
||||
function hashCode(str) {
|
||||
return str.split('').reduce((prevHash, currVal) =>
|
||||
((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0);
|
||||
}
|
||||
let hash = hashCode(tagName);
|
||||
let color = '#' + ((hash >> 24) & 0xFF).toString(16).padStart(2, '0') +
|
||||
((hash >> 16) & 0xFF).toString(16).padStart(2, '0') +
|
||||
((hash >> 8) & 0xFF).toString(16).padStart(2, '0');
|
||||
return color;
|
||||
}
|
||||
|
||||
//Add a tag row to the table
|
||||
function addTagRow(tag) {
|
||||
const row = `<tr class="tagEntry" value="${tag}">
|
||||
<td><div class="ui circular label tag-color" style="background-color: ${getTagColorByName(tag)};"></div> ${tag}</td>
|
||||
<td>
|
||||
<button title="Delete Tag" class="ui circular mini red basic icon button" onclick="removeTag('${tag}')">
|
||||
<i class="trash icon"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
$("#tagsTableBody").append(row);
|
||||
removeNoTagNotice();
|
||||
}
|
||||
|
||||
function removeTag(tag) {
|
||||
$(`#tagsTableBody .tagEntry[value="${tag}"]`).remove();
|
||||
populateTagsDropdown();
|
||||
saveTags();
|
||||
|
||||
if ($('#tagsTableBody tr').length == 0){
|
||||
appendNoTagNotice();
|
||||
}
|
||||
}
|
||||
|
||||
function saveTags(){
|
||||
let tags = [];
|
||||
$('#tagsTableBody tr').each(function() {
|
||||
tags.push($(this).attr('value'));
|
||||
});
|
||||
console.log(tags);
|
||||
$.cjax({
|
||||
url: "/api/proxy/edit",
|
||||
method: "POST",
|
||||
data: {
|
||||
type: "host",
|
||||
rootname: editingEndpoint.ep,
|
||||
tags: tags.join(",")
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
parent.msgbox(data.error, false);
|
||||
} else {
|
||||
parent.msgbox("Tags updated");
|
||||
//Update the preview on parent page
|
||||
parent.renderTagsPreview(editingEndpoint.ep, tags);
|
||||
//parent.hideSideWrapper();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -36,6 +36,10 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
body.darkTheme #upstreamTable{
|
||||
border: 1px solid var(--button_border_color);
|
||||
}
|
||||
|
||||
.upstreamEntry.inactive{
|
||||
background-color: #f3f3f3 !important;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user