mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-21 15:03:05 +02:00
Merge branch 'v3.2.3' into main
This commit is contained in:
commit
b01a21f318
6
.gitignore
vendored
6
.gitignore
vendored
@ -50,4 +50,10 @@ src/log/
|
|||||||
example/plugins/ztnc/ztnc.db
|
example/plugins/ztnc/ztnc.db
|
||||||
example/plugins/ztnc/authtoken.secret
|
example/plugins/ztnc/authtoken.secret
|
||||||
example/plugins/ztnc/ztnc.db.lock
|
example/plugins/ztnc/ztnc.db.lock
|
||||||
|
.idea
|
||||||
|
conf
|
||||||
|
log
|
||||||
|
tmp
|
||||||
|
sys.*
|
||||||
|
www/html/index.html
|
||||||
*.exe
|
*.exe
|
@ -83,6 +83,7 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
|||||||
// Register the APIs for Authentication handlers like Forward Auth and OAUTH2
|
// Register the APIs for Authentication handlers like Forward Auth and OAUTH2
|
||||||
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
|
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
|
||||||
authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions)
|
authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions)
|
||||||
|
authRouter.HandleFunc("/api/sso/OAuth2", oauth2Router.HandleSetOAuth2Settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the APIs for redirection rules management functions
|
// Register the APIs for redirection rules management functions
|
||||||
|
@ -13,6 +13,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/access"
|
"imuslab.com/zoraxy/mod/access"
|
||||||
"imuslab.com/zoraxy/mod/acme"
|
"imuslab.com/zoraxy/mod/acme"
|
||||||
"imuslab.com/zoraxy/mod/auth"
|
"imuslab.com/zoraxy/mod/auth"
|
||||||
@ -42,7 +44,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
/* Build Constants */
|
/* Build Constants */
|
||||||
SYSTEM_NAME = "Zoraxy"
|
SYSTEM_NAME = "Zoraxy"
|
||||||
SYSTEM_VERSION = "3.2.2"
|
SYSTEM_VERSION = "3.2.3"
|
||||||
DEVELOPMENT_BUILD = false
|
DEVELOPMENT_BUILD = false
|
||||||
|
|
||||||
/* System Constants */
|
/* System Constants */
|
||||||
@ -143,7 +145,8 @@ var (
|
|||||||
pluginManager *plugins.Manager //Plugin manager for managing plugins
|
pluginManager *plugins.Manager //Plugin manager for managing plugins
|
||||||
|
|
||||||
//Authentication Provider
|
//Authentication Provider
|
||||||
forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication
|
forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication
|
||||||
|
oauth2Router *oauth2.OAuth2Router //OAuth2Router router for OAuth2Router authentication
|
||||||
|
|
||||||
//Helper modules
|
//Helper modules
|
||||||
EmailSender *email.Sender //Email sender that handle email sending
|
EmailSender *email.Sender //Email sender that handle email sending
|
||||||
|
@ -16,6 +16,7 @@ require (
|
|||||||
github.com/grandcat/zeroconf v1.0.0
|
github.com/grandcat/zeroconf v1.0.0
|
||||||
github.com/likexian/whois v1.15.1
|
github.com/likexian/whois v1.15.1
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
|
github.com/monperrus/crawler-user-agents v1.1.0
|
||||||
github.com/shirou/gopsutil/v4 v4.25.1
|
github.com/shirou/gopsutil/v4 v4.25.1
|
||||||
github.com/syndtr/goleveldb v1.0.0
|
github.com/syndtr/goleveldb v1.0.0
|
||||||
golang.org/x/net v0.33.0
|
golang.org/x/net v0.33.0
|
||||||
@ -32,7 +33,6 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||||
github.com/golang/snappy v0.0.1 // indirect
|
github.com/golang/snappy v0.0.1 // indirect
|
||||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.128 // indirect
|
||||||
github.com/monperrus/crawler-user-agents v1.1.0 // indirect
|
|
||||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
github.com/peterhellberg/link v1.2.0 // indirect
|
github.com/peterhellberg/link v1.2.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
|
@ -56,7 +56,7 @@ func (ar *AuthentikRouter) HandleSetAuthentikURLAndHTTPS(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
useHTTPS, err := utils.PostBool(r, "useHTTPS")
|
useHTTPS, err := utils.PostBool(r, "authentikUseHttps")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
useHTTPS = false
|
useHTTPS = false
|
||||||
}
|
}
|
||||||
|
286
src/mod/auth/sso/oauth2/oauth2.go
Normal file
286
src/mod/auth/sso/oauth2/oauth2.go
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
package oauth2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"imuslab.com/zoraxy/mod/database"
|
||||||
|
"imuslab.com/zoraxy/mod/info/logger"
|
||||||
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OAuth2RouterOptions struct {
|
||||||
|
OAuth2ServerURL string //The URL of the OAuth 2.0 server server
|
||||||
|
OAuth2TokenURL string //The URL of the OAuth 2.0 token server
|
||||||
|
OAuth2ClientId string //The client id for OAuth 2.0 Application
|
||||||
|
OAuth2ClientSecret string //The client secret for OAuth 2.0 Application
|
||||||
|
OAuth2WellKnownUrl string //The well-known url for OAuth 2.0 server
|
||||||
|
OAuth2UserInfoUrl string //The URL of the OAuth 2.0 user info endpoint
|
||||||
|
OAuth2Scopes string //The scopes for OAuth 2.0 Application
|
||||||
|
Logger *logger.Logger
|
||||||
|
Database *database.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
type OIDCDiscoveryDocument struct {
|
||||||
|
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||||
|
ClaimsSupported []string `json:"claims_supported"`
|
||||||
|
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
|
||||||
|
GrantTypesSupported []string `json:"grant_types_supported"`
|
||||||
|
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
JwksURI string `json:"jwks_uri"`
|
||||||
|
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||||
|
ScopesSupported []string `json:"scopes_supported"`
|
||||||
|
SubjectTypesSupported []string `json:"subject_types_supported"`
|
||||||
|
TokenEndpoint string `json:"token_endpoint"`
|
||||||
|
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
|
||||||
|
UserinfoEndpoint string `json:"userinfo_endpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OAuth2Router struct {
|
||||||
|
options *OAuth2RouterOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOAuth2Router creates a new OAuth2Router object
|
||||||
|
func NewOAuth2Router(options *OAuth2RouterOptions) *OAuth2Router {
|
||||||
|
options.Database.NewTable("oauth2")
|
||||||
|
|
||||||
|
//Read settings from database, if exists
|
||||||
|
options.Database.Read("oauth2", "oauth2WellKnownUrl", &options.OAuth2WellKnownUrl)
|
||||||
|
options.Database.Read("oauth2", "oauth2ServerUrl", &options.OAuth2ServerURL)
|
||||||
|
options.Database.Read("oauth2", "oauth2TokenUrl", &options.OAuth2TokenURL)
|
||||||
|
options.Database.Read("oauth2", "oauth2ClientId", &options.OAuth2ClientId)
|
||||||
|
options.Database.Read("oauth2", "oauth2ClientSecret", &options.OAuth2ClientSecret)
|
||||||
|
options.Database.Read("oauth2", "oauth2UserInfoUrl", &options.OAuth2UserInfoUrl)
|
||||||
|
options.Database.Read("oauth2", "oauth2Scopes", &options.OAuth2Scopes)
|
||||||
|
|
||||||
|
return &OAuth2Router{
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleSetOAuth2Settings is the internal handler for setting the OAuth URL and HTTPS
|
||||||
|
func (ar *OAuth2Router) HandleSetOAuth2Settings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
//Return the current settings
|
||||||
|
js, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"oauth2WellKnownUrl": ar.options.OAuth2WellKnownUrl,
|
||||||
|
"oauth2ServerUrl": ar.options.OAuth2ServerURL,
|
||||||
|
"oauth2TokenUrl": ar.options.OAuth2TokenURL,
|
||||||
|
"oauth2UserInfoUrl": ar.options.OAuth2UserInfoUrl,
|
||||||
|
"oauth2Scopes": ar.options.OAuth2Scopes,
|
||||||
|
"oauth2ClientSecret": ar.options.OAuth2ClientSecret,
|
||||||
|
"oauth2ClientId": ar.options.OAuth2ClientId,
|
||||||
|
})
|
||||||
|
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
return
|
||||||
|
} else if r.Method == http.MethodPost {
|
||||||
|
//Update the settings
|
||||||
|
var oauth2ServerUrl, oauth2TokenURL, oauth2Scopes, oauth2UserInfoUrl string
|
||||||
|
oauth2WellKnownUrl, err := utils.PostPara(r, "oauth2WellKnownUrl")
|
||||||
|
if err != nil {
|
||||||
|
oauth2ServerUrl, err = utils.PostPara(r, "oauth2ServerUrl")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "oauth2ServerUrl not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2TokenURL, err = utils.PostPara(r, "oauth2TokenUrl")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "oauth2TokenUrl not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2Scopes, err = utils.PostPara(r, "oauth2Scopes")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "oauth2Scopes not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2UserInfoUrl, err = utils.PostPara(r, "oauth2UserInfoUrl")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "oauth2UserInfoUrl not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2ClientId, err := utils.PostPara(r, "oauth2ClientId")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "oauth2ClientId not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2ClientSecret, err := utils.PostPara(r, "oauth2ClientSecret")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "oauth2ClientSecret not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Write changes to runtime
|
||||||
|
ar.options.OAuth2WellKnownUrl = oauth2WellKnownUrl
|
||||||
|
ar.options.OAuth2ServerURL = oauth2ServerUrl
|
||||||
|
ar.options.OAuth2TokenURL = oauth2TokenURL
|
||||||
|
ar.options.OAuth2UserInfoUrl = oauth2UserInfoUrl
|
||||||
|
ar.options.OAuth2ClientId = oauth2ClientId
|
||||||
|
ar.options.OAuth2ClientSecret = oauth2ClientSecret
|
||||||
|
ar.options.OAuth2Scopes = oauth2Scopes
|
||||||
|
|
||||||
|
//Write changes to database
|
||||||
|
ar.options.Database.Write("oauth2", "oauth2WellKnownUrl", oauth2WellKnownUrl)
|
||||||
|
ar.options.Database.Write("oauth2", "oauth2ServerUrl", oauth2ServerUrl)
|
||||||
|
ar.options.Database.Write("oauth2", "oauth2TokenUrl", oauth2TokenURL)
|
||||||
|
ar.options.Database.Write("oauth2", "oauth2UserInfoUrl", oauth2UserInfoUrl)
|
||||||
|
ar.options.Database.Write("oauth2", "oauth2ClientId", oauth2ClientId)
|
||||||
|
ar.options.Database.Write("oauth2", "oauth2ClientSecret", oauth2ClientSecret)
|
||||||
|
ar.options.Database.Write("oauth2", "oauth2Scopes", oauth2Scopes)
|
||||||
|
|
||||||
|
utils.SendOK(w)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *OAuth2Router) fetchOAuth2Configuration(config *oauth2.Config) (*oauth2.Config, error) {
|
||||||
|
req, err := http.NewRequest("GET", ar.options.OAuth2WellKnownUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client := &http.Client{}
|
||||||
|
if resp, err := client.Do(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
oidcDiscoveryDocument := OIDCDiscoveryDocument{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&oidcDiscoveryDocument); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Scopes) == 0 {
|
||||||
|
config.Scopes = oidcDiscoveryDocument.ScopesSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Endpoint.AuthURL == "" {
|
||||||
|
config.Endpoint.AuthURL = oidcDiscoveryDocument.AuthorizationEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Endpoint.TokenURL == "" {
|
||||||
|
config.Endpoint.TokenURL = oidcDiscoveryDocument.TokenEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
if ar.options.OAuth2UserInfoUrl == "" {
|
||||||
|
ar.options.OAuth2UserInfoUrl = oidcDiscoveryDocument.UserinfoEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *OAuth2Router) newOAuth2Conf(redirectUrl string) (*oauth2.Config, error) {
|
||||||
|
config := &oauth2.Config{
|
||||||
|
ClientID: ar.options.OAuth2ClientId,
|
||||||
|
ClientSecret: ar.options.OAuth2ClientSecret,
|
||||||
|
RedirectURL: redirectUrl,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: ar.options.OAuth2ServerURL,
|
||||||
|
TokenURL: ar.options.OAuth2TokenURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if ar.options.OAuth2Scopes != "" {
|
||||||
|
config.Scopes = strings.Split(ar.options.OAuth2Scopes, ",")
|
||||||
|
}
|
||||||
|
if ar.options.OAuth2WellKnownUrl != "" && (config.Endpoint.AuthURL == "" || config.Endpoint.TokenURL == "" ||
|
||||||
|
ar.options.OAuth2UserInfoUrl == "") {
|
||||||
|
return ar.fetchOAuth2Configuration(config)
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleOAuth2Auth is the internal handler for OAuth authentication
|
||||||
|
// Set useHTTPS to true if your OAuth server is using HTTPS
|
||||||
|
// Set OAuthURL to the URL of the OAuth server, e.g. OAuth.example.com
|
||||||
|
func (ar *OAuth2Router) HandleOAuth2Auth(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
const callbackPrefix = "/internal/oauth2"
|
||||||
|
const tokenCookie = "z-token"
|
||||||
|
scheme := "http"
|
||||||
|
if r.TLS != nil {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
reqUrl := scheme + "://" + r.Host + r.RequestURI
|
||||||
|
oauthConfig, err := ar.newOAuth2Conf(scheme + "://" + r.Host + callbackPrefix)
|
||||||
|
if err != nil {
|
||||||
|
ar.options.Logger.PrintAndLog("OAuth2Router", "Failed to fetch OIDC configuration:", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return errors.New("failed to fetch OIDC configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauthConfig.Endpoint.AuthURL == "" || oauthConfig.Endpoint.TokenURL == "" || ar.options.OAuth2UserInfoUrl == "" {
|
||||||
|
ar.options.Logger.PrintAndLog("OAuth2Router", "Invalid OAuth2 configuration", nil)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return errors.New("invalid OAuth2 configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
code := r.URL.Query().Get("code")
|
||||||
|
state := r.URL.Query().Get("state")
|
||||||
|
if r.Method == http.MethodGet && strings.HasPrefix(r.RequestURI, callbackPrefix) && code != "" && state != "" {
|
||||||
|
ctx := context.Background()
|
||||||
|
token, err := oauthConfig.Exchange(ctx, code)
|
||||||
|
if err != nil {
|
||||||
|
ar.options.Logger.PrintAndLog("OAuth2", "Token exchange failed", err)
|
||||||
|
w.WriteHeader(401)
|
||||||
|
return errors.New("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid() {
|
||||||
|
ar.options.Logger.PrintAndLog("OAuth2", "Invalid token", err)
|
||||||
|
w.WriteHeader(401)
|
||||||
|
return errors.New("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie := http.Cookie{Name: tokenCookie, Value: token.AccessToken, Path: "/"}
|
||||||
|
if scheme == "https" {
|
||||||
|
cookie.Secure = true
|
||||||
|
cookie.SameSite = http.SameSiteLaxMode
|
||||||
|
}
|
||||||
|
w.Header().Add("Set-Cookie", cookie.String())
|
||||||
|
http.Redirect(w, r, state, http.StatusTemporaryRedirect)
|
||||||
|
return errors.New("authorized")
|
||||||
|
}
|
||||||
|
unauthorized := false
|
||||||
|
cookie, err := r.Cookie(tokenCookie)
|
||||||
|
if err == nil {
|
||||||
|
if cookie.Value == "" {
|
||||||
|
unauthorized = true
|
||||||
|
} else {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := oauthConfig.Client(ctx, &oauth2.Token{AccessToken: cookie.Value})
|
||||||
|
req, err := client.Get(ar.options.OAuth2UserInfoUrl)
|
||||||
|
if err != nil {
|
||||||
|
ar.options.Logger.PrintAndLog("OAuth2", "Failed to get user info", err)
|
||||||
|
unauthorized = true
|
||||||
|
}
|
||||||
|
defer req.Body.Close()
|
||||||
|
if req.StatusCode != http.StatusOK {
|
||||||
|
ar.options.Logger.PrintAndLog("OAuth2", "Failed to get user info", err)
|
||||||
|
unauthorized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unauthorized = true
|
||||||
|
}
|
||||||
|
if unauthorized {
|
||||||
|
state := url.QueryEscape(reqUrl)
|
||||||
|
url := oauthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline)
|
||||||
|
http.Redirect(w, r, url, http.StatusFound)
|
||||||
|
|
||||||
|
return errors.New("unauthorized")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -46,6 +46,12 @@ func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *htt
|
|||||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
|
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
case AuthMethodOauth2:
|
||||||
|
err := h.handleOAuth2Auth(w, r)
|
||||||
|
if err != nil {
|
||||||
|
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//No authentication provider, do not need to handle
|
//No authentication provider, do not need to handle
|
||||||
@ -108,3 +114,7 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
|
|||||||
func (h *ProxyHandler) handleForwardAuth(w http.ResponseWriter, r *http.Request) error {
|
func (h *ProxyHandler) handleForwardAuth(w http.ResponseWriter, r *http.Request) error {
|
||||||
return h.Parent.Option.ForwardAuthRouter.HandleAuthProviderRouting(w, r)
|
return h.Parent.Option.ForwardAuthRouter.HandleAuthProviderRouting(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *ProxyHandler) handleOAuth2Auth(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
return h.Parent.Option.OAuth2Router.HandleOAuth2Auth(w, r)
|
||||||
|
}
|
||||||
|
@ -13,6 +13,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
||||||
|
|
||||||
"imuslab.com/zoraxy/mod/access"
|
"imuslab.com/zoraxy/mod/access"
|
||||||
"imuslab.com/zoraxy/mod/auth/sso/forward"
|
"imuslab.com/zoraxy/mod/auth/sso/forward"
|
||||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||||
@ -64,6 +66,7 @@ type RouterOption struct {
|
|||||||
|
|
||||||
/* Authentication Providers */
|
/* Authentication Providers */
|
||||||
ForwardAuthRouter *forward.AuthRouter
|
ForwardAuthRouter *forward.AuthRouter
|
||||||
|
OAuth2Router *oauth2.OAuth2Router //OAuth2Router router for OAuth2Router authentication
|
||||||
|
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
Logger *logger.Logger //Logger for reverse proxy requets
|
Logger *logger.Logger //Logger for reverse proxy requets
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
//go:build (windows && amd64) || (linux && mipsle) || (linux && riscv64) || (freebsd && amd64)
|
//go:build (windows && amd64) || (linux && mipsle) || (linux && riscv64) || (freebsd && amd64) || (darwin && arm64)
|
||||||
// +build windows,amd64 linux,mipsle linux,riscv64 freebsd,amd64
|
// +build windows,amd64 linux,mipsle linux,riscv64 freebsd,amd64 darwin,arm64
|
||||||
|
|
||||||
package sshprox
|
package sshprox
|
||||||
|
|
||||||
import "embed"
|
import "embed"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Binary embedding
|
Binary embedding
|
||||||
|
|
||||||
Make sure when compile, gotty binary exists in static.gotty
|
Make sure when compile, gotty binary exists in static.gotty
|
||||||
*/
|
*/
|
||||||
var (
|
var (
|
||||||
//go:embed gotty/LICENSE
|
//go:embed gotty/LICENSE
|
||||||
|
@ -116,6 +116,7 @@ func ReverseProxtInit() {
|
|||||||
WebDirectory: *path_webserver,
|
WebDirectory: *path_webserver,
|
||||||
AccessController: accessController,
|
AccessController: accessController,
|
||||||
ForwardAuthRouter: forwardAuthRouter,
|
ForwardAuthRouter: forwardAuthRouter,
|
||||||
|
OAuth2Router: oauth2Router,
|
||||||
LoadBalancer: loadBalancer,
|
LoadBalancer: loadBalancer,
|
||||||
PluginManager: pluginManager,
|
PluginManager: pluginManager,
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -147,6 +148,11 @@ func startupSequence() {
|
|||||||
Database: sysdb,
|
Database: sysdb,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
oauth2Router = oauth2.NewOAuth2Router(&oauth2.OAuth2RouterOptions{
|
||||||
|
Logger: SystemWideLogger,
|
||||||
|
Database: sysdb,
|
||||||
|
})
|
||||||
|
|
||||||
//Create a statistic collector
|
//Create a statistic collector
|
||||||
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
||||||
Database: sysdb,
|
Database: sysdb,
|
||||||
|
File diff suppressed because it is too large
Load Diff
797
src/web/components/httprp.html.bak
Normal file
797
src/web/components/httprp.html.bak
Normal file
@ -0,0 +1,797 @@
|
|||||||
|
<div class="standardContainer">
|
||||||
|
<div class="ui basic segment">
|
||||||
|
<h2>HTTP Proxy</h2>
|
||||||
|
<p>Proxy HTTP server with HTTP or HTTPS for multiple hosts. If you are only proxying for one host / domain, use Default Site instead.</p>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
#httpProxyList .ui.toggle.checkbox input:checked ~ label::before{
|
||||||
|
background-color: #00ca52 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;">
|
||||||
|
<!-- Prevent the browser from filling the saved Zoraxy login account into the input searchInput below -->
|
||||||
|
<input type="password" autocomplete="off" hidden/>
|
||||||
|
<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>
|
||||||
|
<tr>
|
||||||
|
<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>
|
||||||
|
</thead>
|
||||||
|
<tbody id="httpProxyList">
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="ui icon right floated basic button" onclick="listProxyEndpoints();"><i class="green refresh icon"></i> Refresh</button>
|
||||||
|
<br><br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
/* List all proxy endpoints */
|
||||||
|
function listProxyEndpoints(){
|
||||||
|
$.get("/api/proxy/list?type=host", function(data){
|
||||||
|
$("#httpProxyList").html(``);
|
||||||
|
if (data.error !== undefined){
|
||||||
|
$("#httpProxyList").append(`<tr>
|
||||||
|
<td data-label="" colspan="5"><i class="remove icon"></i> ${data.error}</td>
|
||||||
|
</tr>`);
|
||||||
|
}else if (data.length == 0){
|
||||||
|
$("#httpProxyList").append(`<tr>
|
||||||
|
<td data-label="" colspan="5"><i class="green check circle icon"></i> No HTTP Proxy Record</td>
|
||||||
|
</tr>`);
|
||||||
|
}else{
|
||||||
|
//Sort by RootOrMatchingDomain field
|
||||||
|
data.sort((a,b) => (a.RootOrMatchingDomain > b.RootOrMatchingDomain) ? 1 : ((b.RootOrMatchingDomain > a.RootOrMatchingDomain) ? -1 : 0))
|
||||||
|
data.forEach(subd => {
|
||||||
|
let subdData = encodeURIComponent(JSON.stringify(subd));
|
||||||
|
|
||||||
|
//Build the upstream list
|
||||||
|
let upstreams = "";
|
||||||
|
if (subd.ActiveOrigins.length == 0){
|
||||||
|
//Invalid config
|
||||||
|
upstreams = `<i class="ui yellow exclamation triangle icon"></i> No Active Upstream Origin<br>`;
|
||||||
|
}else{
|
||||||
|
subd.ActiveOrigins.forEach(upstream => {
|
||||||
|
console.log(upstream);
|
||||||
|
//Check if the upstreams require TLS connections
|
||||||
|
let tlsIcon = "";
|
||||||
|
if (upstream.RequireTLS){
|
||||||
|
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
||||||
|
if (upstream.SkipCertValidations){
|
||||||
|
tlsIcon = `<i class="yellow lock icon" title="TLS/SSL mode without verification"></i>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let upstreamLink = `${upstream.RequireTLS?"https://":"http://"}${upstream.OriginIpOrDomain}`;
|
||||||
|
|
||||||
|
upstreams += `<a href="${upstreamLink}" target="_blank">${upstream.OriginIpOrDomain} ${tlsIcon}</a><br>`;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let inboundTlsIcon = "";
|
||||||
|
if ($("#tls").checkbox("is checked")){
|
||||||
|
inboundTlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
|
||||||
|
if (subd.BypassGlobalTLS){
|
||||||
|
inboundTlsIcon = `<i class="grey lock icon" title="TLS Bypass Enabled"></i>`;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
inboundTlsIcon = `<i class="yellow lock open icon" title="Plain Text Mode"></i>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Build the virtual directory list
|
||||||
|
var vdList = `<div class="ui list">`;
|
||||||
|
subd.VirtualDirectories.forEach(vdir => {
|
||||||
|
vdList += `<div class="item">${vdir.MatchingPath} <i class="green angle double right icon"></i> ${vdir.Domain}</div>`;
|
||||||
|
});
|
||||||
|
vdList += `</div>`;
|
||||||
|
|
||||||
|
if (subd.VirtualDirectories.length == 0){
|
||||||
|
vdList = `<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Virtual Directory</small>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let enableChecked = "checked";
|
||||||
|
if (subd.Disabled){
|
||||||
|
enableChecked = "";
|
||||||
|
}
|
||||||
|
let httpProto = "http://";
|
||||||
|
if ($("#tls").checkbox("is checked")) {
|
||||||
|
httpProto = "https://";
|
||||||
|
} else {
|
||||||
|
httpProto = "http://";
|
||||||
|
}
|
||||||
|
let hostnameRedirectPort = currentListeningPort;
|
||||||
|
if (hostnameRedirectPort == 80 || hostnameRedirectPort == 443){
|
||||||
|
hostnameRedirectPort = "";
|
||||||
|
}else{
|
||||||
|
hostnameRedirectPort = ":" + hostnameRedirectPort;
|
||||||
|
}
|
||||||
|
let aliasDomains = ``;
|
||||||
|
if (subd.MatchingDomainAlias != undefined && subd.MatchingDomainAlias.length > 0){
|
||||||
|
aliasDomains = `<small class="aliasDomains" eptuuid="${subd.RootOrMatchingDomain}" style="color: #636363;">Alias: `;
|
||||||
|
subd.MatchingDomainAlias.forEach(alias => {
|
||||||
|
aliasDomains += `<a href="${httpProto}${alias}${hostnameRedirectPort}" target="_blank">${alias}</a>, `;
|
||||||
|
});
|
||||||
|
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
|
||||||
|
aliasDomains += `</small><br>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#httpProxyList").append(`<tr eptuuid="${subd.RootOrMatchingDomain}" payload="${subdData}" class="subdEntry">
|
||||||
|
<td data-label="" editable="true" datatype="inbound">
|
||||||
|
<a href="${httpProto}${subd.RootOrMatchingDomain}${hostnameRedirectPort}" target="_blank">${subd.RootOrMatchingDomain}</a> ${inboundTlsIcon}<br>
|
||||||
|
${aliasDomains}
|
||||||
|
<small class="accessRuleNameUnderHost" ruleid="${subd.AccessFilterUUID}"></small>
|
||||||
|
</td>
|
||||||
|
<td data-label="" editable="true" datatype="domain">
|
||||||
|
<div class="upstreamList">
|
||||||
|
${upstreams}
|
||||||
|
</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> Forward Auth`:``}
|
||||||
|
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> OAuth2`:``}
|
||||||
|
${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"<br>":""}
|
||||||
|
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
|
||||||
|
${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
|
||||||
|
</td>
|
||||||
|
<td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
|
||||||
|
<div class="ui toggle tiny fitted checkbox" style="margin-bottom: -0.5em; margin-right: 0.4em;" title="Enable / Disable Rule">
|
||||||
|
<input type="checkbox" class="enableToggle" name="active" ${enableChecked} eptuuid="${subd.RootOrMatchingDomain}" onchange="handleProxyRuleToggle(this);">
|
||||||
|
<label></label>
|
||||||
|
</div>
|
||||||
|
<button title="Edit Proxy Rule" class="ui circular mini basic icon button editBtn inlineEditActionBtn" onclick='editEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="edit icon"></i></button>
|
||||||
|
<button title="Remove Proxy Rule" class="ui circular mini red basic icon button inlineEditActionBtn" onclick='deleteEndpoint("${(subd.RootOrMatchingDomain).hexEncode()}")'><i class="trash icon"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>`);
|
||||||
|
});
|
||||||
|
populateTagFilterDropdown(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveAccessRuleNameOnHostRPlist();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Perform realtime alias update without refreshing the whole page
|
||||||
|
function updateAliasListForEndpoint(endpointName, newAliasDomainList){
|
||||||
|
let targetEle = $(`.aliasDomains[eptuuid='${endpointName}']`);
|
||||||
|
console.log(targetEle);
|
||||||
|
if (targetEle.length == 0){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let aliasDomains = ``;
|
||||||
|
if (newAliasDomainList != undefined && newAliasDomainList.length > 0){
|
||||||
|
aliasDomains = `Alias: `;
|
||||||
|
newAliasDomainList.forEach(alias => {
|
||||||
|
aliasDomains += `<a href="//${alias}" target="_blank">${alias}</a>, `;
|
||||||
|
});
|
||||||
|
aliasDomains = aliasDomains.substr(0, aliasDomains.length - 2); //Remove the last tailing seperator
|
||||||
|
$(targetEle).html(aliasDomains);
|
||||||
|
$(targetEle).show();
|
||||||
|
}else{
|
||||||
|
$(targetEle).hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Resolve & Update all rule names on host PR list
|
||||||
|
function resolveAccessRuleNameOnHostRPlist(){
|
||||||
|
//Resolve the access filters
|
||||||
|
$.get("/api/access/list", function(data){
|
||||||
|
console.log(data);
|
||||||
|
if (data.error == undefined){
|
||||||
|
//Build a map base on the data
|
||||||
|
let accessRuleMap = {};
|
||||||
|
for (var i = 0; i < data.length; i++){
|
||||||
|
accessRuleMap[data[i].ID] = data[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$(".accessRuleNameUnderHost").each(function(){
|
||||||
|
let thisAccessRuleID = $(this).attr("ruleid");
|
||||||
|
if (thisAccessRuleID== ""){
|
||||||
|
thisAccessRuleID = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thisAccessRuleID == "default"){
|
||||||
|
//No need to label default access rules
|
||||||
|
$(this).html("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rule = accessRuleMap[thisAccessRuleID];
|
||||||
|
if (rule == undefined){
|
||||||
|
//Missing config or config too old
|
||||||
|
$(this).html(`<i class="ui red exclamation triangle icon"></i> <b style="color: #db2828;">Access Rule Error</b>`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let icon = `<i class="ui grey filter icon"></i>`;
|
||||||
|
if (rule.ID == "default"){
|
||||||
|
icon = `<i class="ui yellow star icon"></i>`;
|
||||||
|
}else if (rule.BlacklistEnabled && !rule.WhitelistEnabled){
|
||||||
|
//This is a blacklist filter
|
||||||
|
icon = `<i class="ui red filter icon"></i>`;
|
||||||
|
}else if (rule.WhitelistEnabled && !rule.BlacklistEnabled){
|
||||||
|
//This is a whitelist filter
|
||||||
|
icon = `<i class="ui green filter icon"></i>`;
|
||||||
|
}else if (rule.WhitelistEnabled && rule.BlacklistEnabled){
|
||||||
|
//Whitelist and blacklist filter
|
||||||
|
icon = `<i class="ui yellow filter icon"></i>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule != undefined){
|
||||||
|
$(this).html(`${icon} ${rule.Name}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//Update the access rule name on given epuuid, call by hostAccessEditor.html
|
||||||
|
function updateAccessRuleNameUnderHost(epuuid, newruleUID){
|
||||||
|
$(`tr[eptuuid='${epuuid}'].subdEntry`).find(".accessRuleNameUnderHost").attr("ruleid", newruleUID);
|
||||||
|
resolveAccessRuleNameOnHostRPlist();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Inline editor for httprp.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
function editEndpoint(uuid) {
|
||||||
|
uuid = uuid.hexDecode();
|
||||||
|
var row = $('tr[eptuuid="' + uuid + '"]');
|
||||||
|
var columns = row.find('td[data-label]');
|
||||||
|
var payload = $(row).attr("payload");
|
||||||
|
payload = JSON.parse(decodeURIComponent(payload));
|
||||||
|
console.log(payload);
|
||||||
|
columns.each(function(index) {
|
||||||
|
var column = $(this);
|
||||||
|
var oldValue = column.text().trim();
|
||||||
|
|
||||||
|
if ($(this).attr("editable") == "false"){
|
||||||
|
//This col do not allow edit. Skip
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an input element based on the column content
|
||||||
|
var input;
|
||||||
|
var datatype = $(this).attr("datatype");
|
||||||
|
if (datatype == "domain"){
|
||||||
|
let useStickySessionChecked = "";
|
||||||
|
if (payload.UseStickySession){
|
||||||
|
useStickySessionChecked = "checked";
|
||||||
|
}
|
||||||
|
|
||||||
|
let enableUptimeMonitor = "";
|
||||||
|
//Note the config file store the uptime monitor as disable, so we need to reverse the logic
|
||||||
|
if (!payload.DisableUptimeMonitor){
|
||||||
|
enableUptimeMonitor = "checked";
|
||||||
|
}
|
||||||
|
|
||||||
|
input = `<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 1em;" onclick="editUpstreams('${uuid}');"><i class="grey server icon"></i> Edit Upstreams</button>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="UseStickySession" ${useStickySessionChecked}>
|
||||||
|
<label>Use Sticky Session<br>
|
||||||
|
<small>Enable stick session on load balancing</small></label>
|
||||||
|
</div>
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="EnableUptimeMonitor" ${enableUptimeMonitor}>
|
||||||
|
<label>Monitor Uptime<br>
|
||||||
|
<small>Enable active uptime monitor</small></label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
column.append(input);
|
||||||
|
$(column).find(".upstreamList").addClass("editing");
|
||||||
|
}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 == "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;
|
||||||
|
|
||||||
|
let skipWebSocketOriginCheck = payload.SkipWebSocketOriginCheck;
|
||||||
|
let wsCheckstate = "";
|
||||||
|
if (skipWebSocketOriginCheck){
|
||||||
|
wsCheckstate = "checked";
|
||||||
|
}
|
||||||
|
|
||||||
|
let requireRateLimit = payload.RequireRateLimit;
|
||||||
|
let rateLimitCheckState = "";
|
||||||
|
if (requireRateLimit){
|
||||||
|
rateLimitCheckState = "checked";
|
||||||
|
}
|
||||||
|
let rateLimit = payload.RateLimit;
|
||||||
|
if (rateLimit == 0){
|
||||||
|
//This value is not set. Make it default to 100
|
||||||
|
rateLimit = 100;
|
||||||
|
}
|
||||||
|
let rateLimitDisableState = "";
|
||||||
|
if (!payload.RequireRateLimit){
|
||||||
|
rateLimitDisableState = "disabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
column.empty().append(`
|
||||||
|
<div class="grouped fields authProviderPicker">
|
||||||
|
<label><b>Authentication Provider</b></label>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui radio checkbox">
|
||||||
|
<input type="radio" value="0" name="authProviderType" ${authProvider==0x0?"checked":""}>
|
||||||
|
<label>None (Anyone can access)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui radio checkbox">
|
||||||
|
<input type="radio" value="1" name="authProviderType" ${authProvider==0x1?"checked":""}>
|
||||||
|
<label>Basic Auth</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui radio checkbox">
|
||||||
|
<input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}>
|
||||||
|
<label>Forward Auth</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui radio checkbox">
|
||||||
|
<input type="radio" value="3" name="authProviderType" ${authProvider==0x3?"checked":""}>
|
||||||
|
<label>OAuth2</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="ui basic compact 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>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
Security Options
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" onchange="handleToggleRateLimitInput();" class="RequireRateLimit" ${rateLimitCheckState}>
|
||||||
|
<label>Require Rate Limit<br>
|
||||||
|
<small>Check this to enable rate limit on this inbound hostname</small></label>
|
||||||
|
</div><br>
|
||||||
|
<div class="ui mini right labeled fluid input ${rateLimitDisableState}" style="margin-top: 0.4em;">
|
||||||
|
<input type="number" class="RateLimit" value="${rateLimit}" min="1" >
|
||||||
|
<label class="ui basic label">
|
||||||
|
req / sec / IP
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
$('.authProviderPicker .ui.checkbox').checkbox();
|
||||||
|
} else if (datatype == "ratelimit"){
|
||||||
|
|
||||||
|
column.empty().append(`
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="RequireRateLimit" ${checkstate}>
|
||||||
|
<label>Require Rate Limit</label>
|
||||||
|
</div>
|
||||||
|
<div class="ui mini fluid input">
|
||||||
|
<input type="number" class="RateLimit" value="${rateLimit}" placeholder="100" min="1" max="1000" >
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
}else if (datatype == 'action'){
|
||||||
|
column.empty().append(`
|
||||||
|
<button title="Save" onclick="saveProxyInlineEdit('${uuid.hexEncode()}');" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui green save icon"></i></button>
|
||||||
|
<button title="Cancel" onclick="exitProxyInlineEdit();" class="ui basic small icon circular button inlineEditActionBtn"><i class="ui remove icon"></i></button>
|
||||||
|
|
||||||
|
`);
|
||||||
|
}else if (datatype == "inbound"){
|
||||||
|
let originalContent = $(column).html();
|
||||||
|
|
||||||
|
//Check if this host is covered within one of the certificates. If not, show the icon
|
||||||
|
let enableQuickRequestButton = true;
|
||||||
|
let domains = [payload.RootOrMatchingDomain]; //Domain for getting certificate if needed
|
||||||
|
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
|
||||||
|
let thisAliasName = payload.MatchingDomainAlias[i];
|
||||||
|
domains.push(thisAliasName);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the domain or alias contains wildcard, if yes, disabled the get certificate button
|
||||||
|
if (payload.RootOrMatchingDomain.indexOf("*") > -1){
|
||||||
|
enableQuickRequestButton = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.MatchingDomainAlias != undefined){
|
||||||
|
for (var i = 0; i < payload.MatchingDomainAlias.length; i++){
|
||||||
|
if (payload.MatchingDomainAlias[i].indexOf("*") > -1){
|
||||||
|
enableQuickRequestButton = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//encode the domain to DOM
|
||||||
|
let certificateDomains = encodeURIComponent(JSON.stringify(domains));
|
||||||
|
|
||||||
|
column.empty().append(`${originalContent}
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui checkbox" style="margin-top: 0.4em;">
|
||||||
|
<input type="checkbox" class="BypassGlobalTLS" ${payload.BypassGlobalTLS?"checked":""}>
|
||||||
|
<label>Allow plain HTTP access<br>
|
||||||
|
<small>Allow inbound connections without TLS/SSL</small></label>
|
||||||
|
</div><br>
|
||||||
|
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAliasHostnames('${uuid}');"><i class=" blue at icon"></i> Alias</button>
|
||||||
|
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editAccessRule('${uuid}');"><i class="ui filter icon"></i> Access Rule</button>
|
||||||
|
<button class="ui basic compact tiny ${enableQuickRequestButton?"":"disabled"} button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="requestCertificateForExistingHost('${uuid}', '${certificateDomains}', this);"><i class="green lock icon"></i> Get Certificate</button>
|
||||||
|
`);
|
||||||
|
|
||||||
|
$(".hostAccessRuleSelector").dropdown();
|
||||||
|
}else{
|
||||||
|
//Unknown field. Leave it untouched
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".endpointAdvanceConfig").accordion();
|
||||||
|
$("#httpProxyList").find(".editBtn").addClass("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
//handleToggleRateLimitInput will get trigger if the "require rate limit" checkbox
|
||||||
|
// is changed and toggle the disable state of the rate limit input field
|
||||||
|
function handleToggleRateLimitInput(){
|
||||||
|
let isRateLimitEnabled = $("#httpProxyList input.RequireRateLimit")[0].checked;
|
||||||
|
if (isRateLimitEnabled){
|
||||||
|
$("#httpProxyList input.RateLimit").parent().removeClass("disabled");
|
||||||
|
}else{
|
||||||
|
$("#httpProxyList input.RateLimit").parent().addClass("disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitProxyInlineEdit(){
|
||||||
|
listProxyEndpoints();
|
||||||
|
$("#httpProxyList").find(".editBtn").removeClass("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveProxyInlineEdit(uuid){
|
||||||
|
uuid = uuid.hexDecode();
|
||||||
|
var row = $('tr[eptuuid="' + uuid + '"]');
|
||||||
|
if (row.length == 0){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var epttype = "host";
|
||||||
|
let useStickySession = $(row).find(".UseStickySession")[0].checked;
|
||||||
|
let DisableUptimeMonitor = !$(row).find(".EnableUptimeMonitor")[0].checked;
|
||||||
|
let authProviderType = $(row).find(".authProviderPicker input[type='radio']:checked").val();
|
||||||
|
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",
|
||||||
|
data: {
|
||||||
|
"type": epttype,
|
||||||
|
"rootname": uuid,
|
||||||
|
"ss":useStickySession,
|
||||||
|
"dutm": DisableUptimeMonitor,
|
||||||
|
"bpgtls": bypassGlobalTLS,
|
||||||
|
"authprovider" :authProviderType,
|
||||||
|
"rate" :requireRateLimit,
|
||||||
|
"ratenum" :rateLimit,
|
||||||
|
"tags": tags,
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error !== undefined){
|
||||||
|
msgbox(data.error, false, 6000);
|
||||||
|
}else{
|
||||||
|
msgbox("Proxy endpoint updated");
|
||||||
|
listProxyEndpoints();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//Generic functions for delete rp endpoints
|
||||||
|
function deleteEndpoint(epoint){
|
||||||
|
epoint = decodeURIComponent(epoint).hexDecode();
|
||||||
|
if (confirm("Confirm remove proxy for :" + epoint + "?")){
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/proxy/del",
|
||||||
|
method: "POST",
|
||||||
|
data: {ep: epoint},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error == undefined){
|
||||||
|
listProxyEndpoints();
|
||||||
|
msgbox("Proxy Rule Deleted", true);
|
||||||
|
reloadUptimeList();
|
||||||
|
}else{
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* button events */
|
||||||
|
function editBasicAuthCredentials(uuid){
|
||||||
|
let payload = encodeURIComponent(JSON.stringify({
|
||||||
|
ept: "host",
|
||||||
|
ep: uuid
|
||||||
|
}));
|
||||||
|
showSideWrapper("snippet/basicAuthEditor.html?t=" + Date.now() + "#" + payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editAccessRule(uuid){
|
||||||
|
let payload = encodeURIComponent(JSON.stringify({
|
||||||
|
ept: "host",
|
||||||
|
ep: uuid
|
||||||
|
}));
|
||||||
|
showSideWrapper("snippet/hostAccessEditor.html?t=" + Date.now() + "#" + payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editAliasHostnames(uuid){
|
||||||
|
let payload = encodeURIComponent(JSON.stringify({
|
||||||
|
ept: "host",
|
||||||
|
ep: uuid
|
||||||
|
}));
|
||||||
|
showSideWrapper("snippet/aliasEditor.html?t=" + Date.now() + "#" + payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickEditVdir(uuid){
|
||||||
|
openTabById("vdir");
|
||||||
|
$("#vdirBaseRoutingRule").parent().dropdown("set selected", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Open the custom header editor
|
||||||
|
function editCustomHeaders(uuid){
|
||||||
|
let payload = encodeURIComponent(JSON.stringify({
|
||||||
|
ept: "host",
|
||||||
|
ep: uuid
|
||||||
|
}));
|
||||||
|
showSideWrapper("snippet/customHeaders.html?t=" + Date.now() + "#" + payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Open the load balance option
|
||||||
|
function editUpstreams(uuid){
|
||||||
|
let payload = encodeURIComponent(JSON.stringify({
|
||||||
|
ept: "host",
|
||||||
|
ep: uuid
|
||||||
|
}));
|
||||||
|
showSideWrapper("snippet/upstreams.html?t=" + Date.now() + "#" + payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProxyRuleToggle(object){
|
||||||
|
let endpointUUID = $(object).attr("eptuuid");
|
||||||
|
let isChecked = object.checked;
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/proxy/toggle",
|
||||||
|
data: {
|
||||||
|
"ep": endpointUUID,
|
||||||
|
"enable": isChecked
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
if (isChecked){
|
||||||
|
msgbox("Proxy Rule Enabled");
|
||||||
|
}else{
|
||||||
|
msgbox("Proxy Rule Disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Certificate Shortcut
|
||||||
|
*/
|
||||||
|
|
||||||
|
function requestCertificateForExistingHost(hostUUID, RootAndAliasDomains, btn=undefined){
|
||||||
|
RootAndAliasDomains = JSON.parse(decodeURIComponent(RootAndAliasDomains))
|
||||||
|
let renewDomainKey = RootAndAliasDomains.join(",");
|
||||||
|
let preferedACMEEmail = $("#prefACMEEmail").val();
|
||||||
|
if (preferedACMEEmail == ""){
|
||||||
|
msgbox("Preferred email for ACME registration not set", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let defaultCA = $("#defaultCA").dropdown("get value");
|
||||||
|
if (defaultCA == ""){
|
||||||
|
defaultCA = "Let's Encrypt";
|
||||||
|
}
|
||||||
|
|
||||||
|
//Check if the root or the alias domain contain wildcard character, if yes, return error
|
||||||
|
for (var i = 0; i < RootAndAliasDomains.length; i++){
|
||||||
|
if (RootAndAliasDomains[i].indexOf("*") != -1){
|
||||||
|
msgbox("Wildcard domain can only be setup via ACME tool", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Renew the certificate
|
||||||
|
renewCertificate(renewDomainKey, false, btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Bind on tab switch events
|
||||||
|
tabSwitchEventBind["httprp"] = function(){
|
||||||
|
listProxyEndpoints();
|
||||||
|
|
||||||
|
//Reset the tag filter
|
||||||
|
$("#tagFilterDropdown").dropdown('set selected', "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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>
|
@ -1,4 +1,4 @@
|
|||||||
<div class="standardContainer">
|
<div class="sso standardContainer">
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
<h2>SSO</h2>
|
<h2>SSO</h2>
|
||||||
<p>Single Sign-On (SSO) and authentication providers settings </p>
|
<p>Single Sign-On (SSO) and authentication providers settings </p>
|
||||||
@ -26,7 +26,7 @@
|
|||||||
<li><a href="https://www.authelia.com" rel=”noopener noreferrer” target="_blank">Authelia</a></li>
|
<li><a href="https://www.authelia.com" rel=”noopener noreferrer” target="_blank">Authelia</a></li>
|
||||||
<li><a href="https://goauthentik.io/" rel=”noopener noreferrer” target="_blank">Authentik</a></li>
|
<li><a href="https://goauthentik.io/" rel=”noopener noreferrer” target="_blank">Authentik</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<form class="ui form">
|
<form class="ui form" action="#" id="forwardAuthSettings">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="forwardAuthAddress">Address</label>
|
<label for="forwardAuthAddress">Address</label>
|
||||||
<input type="text" id="forwardAuthAddress" name="forwardAuthAddress" placeholder="Enter Forward Auth Address">
|
<input type="text" id="forwardAuthAddress" name="forwardAuthAddress" placeholder="Enter Forward Auth Address">
|
||||||
@ -66,7 +66,55 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="ui basic button" onclick="event.preventDefault(); updateForwardAuthSettings();"><i class="green check icon"></i> Apply Change</button>
|
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui basic segment">
|
||||||
|
<h3>OAuth 2.0</h3>
|
||||||
|
<p>Configuration settings for OAuth 2.0 authentication provider.</p>
|
||||||
|
|
||||||
|
<form class="ui form" action="#" id="oauth2Settings">
|
||||||
|
<div class="field">
|
||||||
|
<label for="oauth2ClientId">Client ID</label>
|
||||||
|
<input type="text" id="oauth2ClientId" name="oauth2ClientId" placeholder="Enter Client ID">
|
||||||
|
<small>Public identifier of the OAuth2 application</small>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="oauth2ClientId">Client Secret</label>
|
||||||
|
<input type="password" id="oauth2ClientSecret" name="oauth2ClientSecret" placeholder="Enter Client Secret">
|
||||||
|
<small>Secret key of the OAuth2 application</small>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="oauth2WellKnownUrl">OIDC well-known URL</label>
|
||||||
|
<input type="text" id="oauth2WellKnownUrl" name="oauth2WellKnownUrl" placeholder="Enter Well-Known URL">
|
||||||
|
<small>URL to the OIDC discovery document (usually ending with /.well-known/openid-configuration). Used to automatically fetch provider settings.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="oauth2ServerUrl">Authorization URL</label>
|
||||||
|
<input type="text" id="oauth2ServerUrl" name="oauth2ServerUrl" placeholder="Enter Authorization URL">
|
||||||
|
<small>URL used to authenticate against the OAuth2 provider. Will redirect the user to the OAuth2 provider login view. Optional if Well-Known url is configured.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="oauth2TokenUrl">Token URL</label>
|
||||||
|
<input type="text" id="oauth2TokenUrl" name="oauth2TokenUrl" placeholder="Enter Token URL">
|
||||||
|
<small>URL used by Zoraxy to exchange a valid OAuth2 authentication code for an access token. Optional if Well-Known url is configured.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="oauth2UserInfoURL">User Info URL</label>
|
||||||
|
<input type="text" id="oauth2UserInfoURL" name="oauth2UserInfoURL" placeholder="Enter User Info URL">
|
||||||
|
<small>URL used by the OAuth2 provider to validate generated token. Optional if Well-Known url is configured.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="oauth2Scopes">Scopes</label>
|
||||||
|
<input type="text" id="oauth2Scopes" name="oauth2Scopes" placeholder="Enter Scopes">
|
||||||
|
<small>Scopes required by the OAuth2 provider to retrieve information about the authenticated user. Refer to your OAuth2 provider documentation for more information about this. Optional if Well-Known url is configured.</small>
|
||||||
|
</div>
|
||||||
|
<button class="ui basic button" type="submit"><i class="green check icon"></i> Apply Change</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
@ -74,6 +122,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
/* Load forward-auth settings from backend */
|
||||||
$.cjax({
|
$.cjax({
|
||||||
url: '/api/sso/forward-auth',
|
url: '/api/sso/forward-auth',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -89,8 +138,33 @@
|
|||||||
console.error('Error fetching SSO settings:', textStatus, errorThrown);
|
console.error('Error fetching SSO settings:', textStatus, errorThrown);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Load Oauth2 settings from backend */
|
||||||
|
$.cjax({
|
||||||
|
url: '/api/sso/OAuth2',
|
||||||
|
method: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data) {
|
||||||
|
$('#oauth2WellKnownUrl').val(data.oauth2WellKnownUrl);
|
||||||
|
$('#oauth2ServerUrl').val(data.oauth2ServerUrl);
|
||||||
|
$('#oauth2TokenUrl').val(data.oauth2TokenUrl);
|
||||||
|
$('#oauth2UserInfoUrl').val(data.oauth2UserInfoUrl);
|
||||||
|
$('#oauth2ClientId').val(data.oauth2ClientId);
|
||||||
|
$('#oauth2ClientSecret').val(data.oauth2ClientSecret);
|
||||||
|
$('#oauth2Scopes').val(data.oauth2Scopes);
|
||||||
|
},
|
||||||
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
|
console.error('Error fetching SSO settings:', textStatus, errorThrown);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Add more initialization code here if needed */
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Function to update Forward Auth settings.
|
||||||
|
*/
|
||||||
|
|
||||||
function updateForwardAuthSettings() {
|
function updateForwardAuthSettings() {
|
||||||
const address = $('#forwardAuthAddress').val();
|
const address = $('#forwardAuthAddress').val();
|
||||||
const responseHeaders = $('#forwardAuthResponseHeaders').val();
|
const responseHeaders = $('#forwardAuthResponseHeaders').val();
|
||||||
@ -123,4 +197,62 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$("#forwardAuthSettings").on("submit", function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
updateForwardAuthSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Oauth2 settings update handler.
|
||||||
|
*/
|
||||||
|
$( "#authentikSettings" ).on( "submit", function( event ) {
|
||||||
|
event.preventDefault();
|
||||||
|
$.cjax({
|
||||||
|
url: '/api/sso/forward-auth',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
address: address,
|
||||||
|
responseHeaders: responseHeaders,
|
||||||
|
responseClientHeaders: responseClientHeaders,
|
||||||
|
requestHeaders: requestHeaders,
|
||||||
|
requestExcludedCookies: requestExcludedCookies
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
if (data.error !== undefined) {
|
||||||
|
msgbox(data.error, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
msgbox('Forward Auth settings updated', true);
|
||||||
|
console.log('Forward Auth settings updated:', data);
|
||||||
|
},
|
||||||
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
|
console.error('Error updating Forward Auth settings:', textStatus, errorThrown);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$( "#oauth2Settings" ).on( "submit", function( event ) {
|
||||||
|
event.preventDefault();
|
||||||
|
$.cjax({
|
||||||
|
url: '/api/sso/OAuth2',
|
||||||
|
method: 'POST',
|
||||||
|
data: $( this ).serialize(),
|
||||||
|
success: function(data) {
|
||||||
|
if (data.error != undefined) {
|
||||||
|
msgbox(data.error, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
msgbox('OAuth2 settings updated', true);
|
||||||
|
console.log('OAuth2 settings updated:', data);
|
||||||
|
},
|
||||||
|
error: function(jqXHR, textStatus, errorThrown) {
|
||||||
|
console.error('Error updating OAuth2 settings:', textStatus, errorThrown);
|
||||||
|
msgbox('Error updating OAuth2 settings, check console', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Bind UI events */
|
||||||
|
$(".sso .advanceSettings").accordion();
|
||||||
</script>
|
</script>
|
@ -72,7 +72,7 @@
|
|||||||
<i class="simplistic lock icon"></i> TLS / SSL certificates
|
<i class="simplistic lock icon"></i> TLS / SSL certificates
|
||||||
</a>
|
</a>
|
||||||
<a class="item" tag="sso">
|
<a class="item" tag="sso">
|
||||||
<i class="simplistic user circle icon"></i> SSO / Oauth
|
<i class="simplistic user circle icon"></i> SSO / OAuth2
|
||||||
</a>
|
</a>
|
||||||
<div class="ui divider menudivider">Others</div>
|
<div class="ui divider menudivider">Others</div>
|
||||||
<a class="item" tag="webserv">
|
<a class="item" tag="webserv">
|
||||||
@ -334,6 +334,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleTheme(){
|
function toggleTheme(){
|
||||||
|
let editorSideWrapper = $("#httprpEditModal .wrapper_frame");
|
||||||
if ($("body").hasClass("darkTheme")){
|
if ($("body").hasClass("darkTheme")){
|
||||||
setDarkTheme(false);
|
setDarkTheme(false);
|
||||||
//Check if the snippet iframe is opened. If yes, set the dark theme to the iframe
|
//Check if the snippet iframe is opened. If yes, set the dark theme to the iframe
|
||||||
@ -341,6 +342,10 @@
|
|||||||
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(false);
|
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$(editorSideWrapper).each(function(){
|
||||||
|
$(this)[0].contentWindow.setDarkTheme(false);
|
||||||
|
})
|
||||||
|
|
||||||
if ($("#pluginContextLoader").is(":visible")){
|
if ($("#pluginContextLoader").is(":visible")){
|
||||||
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(false);
|
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(false);
|
||||||
}
|
}
|
||||||
@ -350,6 +355,9 @@
|
|||||||
if ($(".sideWrapper").is(":visible")){
|
if ($(".sideWrapper").is(":visible")){
|
||||||
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
|
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
|
||||||
}
|
}
|
||||||
|
$(editorSideWrapper).each(function(){
|
||||||
|
$(this)[0].contentWindow.setDarkTheme(true);
|
||||||
|
})
|
||||||
if ($("#pluginContextLoader").is(":visible")){
|
if ($("#pluginContextLoader").is(":visible")){
|
||||||
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(true);
|
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(true);
|
||||||
}
|
}
|
||||||
@ -515,6 +523,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hideSideWrapper(discardFrameContent = false){
|
function hideSideWrapper(discardFrameContent = false){
|
||||||
|
if ($("#httprpEditModal").length && $("#httprpEditModal").is(":visible")) {
|
||||||
|
//HTTP Proxy Rule editor side wrapper implementation
|
||||||
|
$("#httprpEditModal .editor_side_wrapper").hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Original side wrapper implementation
|
||||||
if (discardFrameContent){
|
if (discardFrameContent){
|
||||||
$(".sideWrapper iframe").attr("src", "snippet/placeholder.html");
|
$(".sideWrapper iframe").attr("src", "snippet/placeholder.html");
|
||||||
}
|
}
|
||||||
|
@ -264,7 +264,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('accessRuleSelector').addEventListener('change', handleSelectEditingAccessRule);
|
document.getElementById('accessRuleSelector').addEventListener('change', handleSelectEditingAccessRule);
|
||||||
document.getElementById('accessRuleForm').addEventListener('submit', handleCreateNewAccessRule);
|
document.getElementById('accessRuleForm').addEventListener('submit', handleCreateNewAccessRule);
|
||||||
|
|
||||||
|
@ -14,13 +14,14 @@
|
|||||||
<script src="../script/darktheme.js"></script>
|
<script src="../script/darktheme.js"></script>
|
||||||
<br>
|
<br>
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
|
<!--
|
||||||
<div class="ui header">
|
<div class="ui header">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
Alias Hostname
|
Alias Hostname
|
||||||
<div class="sub header epname"></div>
|
<div class="sub header epname"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>-->
|
||||||
<div class="scrolling content ui form">
|
<div class="scrolling content ui form">
|
||||||
<div id="inlineEditBasicAuthCredentials" class="field">
|
<div id="inlineEditBasicAuthCredentials" class="field">
|
||||||
<p>Enter alias hostname or wildcard matching keywords for <code class="epname"></code></p>
|
<p>Enter alias hostname or wildcard matching keywords for <code class="epname"></code></p>
|
||||||
@ -50,10 +51,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<br><br><br><br>
|
<br><br><br><br>
|
||||||
|
@ -27,6 +27,11 @@
|
|||||||
body.darkTheme #permissionPolicyEditor .experimental{
|
body.darkTheme #permissionPolicyEditor .experimental{
|
||||||
background-color: rgb(41, 41, 41);
|
background-color: rgb(41, 41, 41);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.advanceoptions{
|
||||||
|
background: var(--theme_advance) !important;
|
||||||
|
border-radius: 0.4em !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -34,13 +39,6 @@
|
|||||||
<script src="../script/darktheme.js"></script>
|
<script src="../script/darktheme.js"></script>
|
||||||
<br>
|
<br>
|
||||||
<div class="ui container">
|
<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>
|
|
||||||
<div class="ui small pointing secondary menu">
|
<div class="ui small pointing secondary menu">
|
||||||
<a class="item active narrowpadding" data-tab="customheaders">Custom Headers</a>
|
<a class="item active narrowpadding" data-tab="customheaders">Custom Headers</a>
|
||||||
<a class="item narrowpadding" data-tab="security">Security Headers</a>
|
<a class="item narrowpadding" data-tab="security">Security Headers</a>
|
||||||
@ -171,10 +169,6 @@
|
|||||||
<br><br>
|
<br><br>
|
||||||
<button class="ui basic button" onclick="savePermissionPolicy();"><i class="green save icon"></i> Save</button>
|
<button class="ui basic button" onclick="savePermissionPolicy();"><i class="green save icon"></i> Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field" >
|
|
||||||
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br><br><br><br>
|
<br><br><br><br>
|
||||||
@ -189,7 +183,7 @@
|
|||||||
let payloadHash = window.location.hash.substr(1);
|
let payloadHash = window.location.hash.substr(1);
|
||||||
try{
|
try{
|
||||||
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
|
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
|
||||||
$("#epname").text(payloadHash.ep);
|
//$("#epname").text(payloadHash.ep);
|
||||||
editingEndpoint = payloadHash;
|
editingEndpoint = payloadHash;
|
||||||
}catch(ex){
|
}catch(ex){
|
||||||
console.log("Unable to load endpoint data from hash")
|
console.log("Unable to load endpoint data from hash")
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
#accessRuleList{
|
#accessRuleList{
|
||||||
padding: 0.6em;
|
padding: 0.6em;
|
||||||
border: 1px solid rgb(228, 228, 228);
|
/* border: 1px solid rgb(228, 228, 228); */
|
||||||
border-radius: 0.4em !important;
|
border-radius: 0.4em !important;
|
||||||
max-height: calc(100vh - 15em);
|
max-height: calc(100vh - 15em);
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
@ -65,13 +65,6 @@
|
|||||||
<script src="../script/darktheme.js"></script>
|
<script src="../script/darktheme.js"></script>
|
||||||
<br>
|
<br>
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="ui header">
|
|
||||||
<div class="content">
|
|
||||||
Host Access Settings
|
|
||||||
<div class="sub header" id="epname"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<p>Select an access rule to apply blacklist / whitelist filtering</p>
|
<p>Select an access rule to apply blacklist / whitelist filtering</p>
|
||||||
<div id="accessRuleList">
|
<div id="accessRuleList">
|
||||||
<div class="ui segment accessRule">
|
<div class="ui segment accessRule">
|
||||||
@ -85,9 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<button class="ui basic button" onclick="applyChangeAndClose()"><i class="ui green check icon"></i> Apply Change</button>
|
<!-- <button class="ui basic button" onclick="applyChange()"><i class="ui green check icon"></i> Apply Change</button> -->
|
||||||
|
|
||||||
<button class="ui basic button" style="float: right;" onclick="parent.hideSideWrapper();"><i class="remove icon"></i> Close</button>
|
|
||||||
<br><br><br>
|
<br><br><br>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -174,6 +165,35 @@
|
|||||||
let accessRuleID = $(accessRuleObject).attr("ruleid");
|
let accessRuleID = $(accessRuleObject).attr("ruleid");
|
||||||
$(".accessRule").removeClass('active');
|
$(".accessRule").removeClass('active');
|
||||||
$(accessRuleObject).addClass('active');
|
$(accessRuleObject).addClass('active');
|
||||||
|
|
||||||
|
//Updates 2025-06-10: Added auto save on change feature
|
||||||
|
applyChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function applyChange(){
|
||||||
|
let newAccessRuleID = $(".accessRule.active").attr("ruleid");
|
||||||
|
let targetEndpoint = editingEndpoint.ep;
|
||||||
|
$.cjax({
|
||||||
|
url: "/api/access/attach",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
id: newAccessRuleID,
|
||||||
|
host: targetEndpoint
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
parent.msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
parent.msgbox("Access Rule Updated");
|
||||||
|
|
||||||
|
//Modify the parent list if exists
|
||||||
|
if (parent != undefined && parent.updateAccessRuleNameUnderHost){
|
||||||
|
parent.updateAccessRuleNameUnderHost(targetEndpoint, newAccessRuleID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyChangeAndClose(){
|
function applyChangeAndClose(){
|
||||||
|
@ -21,13 +21,6 @@
|
|||||||
<script src="../script/darktheme.js"></script>
|
<script src="../script/darktheme.js"></script>
|
||||||
<br>
|
<br>
|
||||||
<div class="ui container">
|
<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>
|
<p>Tags currently applied to this host name / proxy rule</p>
|
||||||
<div style="max-height: 300px; overflow-y: scroll;">
|
<div style="max-height: 300px; overflow-y: scroll;">
|
||||||
<table class="ui compact basic unstackable celled table">
|
<table class="ui compact basic unstackable celled table">
|
||||||
@ -68,9 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="ui basic button" onclick="joinSelectedTagGroups();"><i class="ui blue plus icon"></i> Join tag group(s)</button>
|
<button class="ui basic button" onclick="joinSelectedTagGroups();"><i class="ui blue plus icon"></i> Join tag group(s)</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<br><br>
|
||||||
<!-- <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>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
let editingEndpoint = {};
|
let editingEndpoint = {};
|
||||||
|
@ -75,13 +75,6 @@
|
|||||||
<script src="../script/darktheme.js"></script>
|
<script src="../script/darktheme.js"></script>
|
||||||
<br>
|
<br>
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="ui header">
|
|
||||||
<div class="content">
|
|
||||||
Upstreams / Load Balance
|
|
||||||
<div class="sub header epname"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<div class="ui small pointing secondary menu">
|
<div class="ui small pointing secondary menu">
|
||||||
<a class="item active narrowpadding" data-tab="upstreamlist">Upstreams</a>
|
<a class="item active narrowpadding" data-tab="upstreamlist">Upstreams</a>
|
||||||
<a class="item narrowpadding" data-tab="newupstream">Add Upstream</a>
|
<a class="item narrowpadding" data-tab="newupstream">Add Upstream</a>
|
||||||
@ -159,10 +152,6 @@
|
|||||||
<br><br>
|
<br><br>
|
||||||
<button class="ui basic button" onclick="addNewUpstream();"><i class="ui green circle add icon"></i> Create</button>
|
<button class="ui basic button" onclick="addNewUpstream();"><i class="ui green circle add icon"></i> Create</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
|
||||||
<div class="field" >
|
|
||||||
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<br><br><br><br>
|
<br><br><br><br>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user