mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-05 20:58:28 +02:00
Compare commits
16 Commits
plugin_doc
...
31ba4f20ae
Author | SHA1 | Date | |
---|---|---|---|
![]() |
31ba4f20ae | ||
![]() |
6d0c0be8c2 | ||
![]() |
7164b74d4a | ||
![]() |
b01a21f318 | ||
![]() |
809e1fa815 | ||
![]() |
c7b5e0994e | ||
![]() |
1f8684481a | ||
![]() |
0e74ff69c3 | ||
![]() |
f0fa71c5b4 | ||
![]() |
8cb47e19fa | ||
![]() |
475650de0d | ||
![]() |
b19867865c | ||
![]() |
df636c9f76 | ||
![]() |
e6b2cf09d7 | ||
![]() |
e2882b6436 | ||
![]() |
61b873451f |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -50,5 +50,10 @@ src/log/
|
||||
example/plugins/ztnc/ztnc.db
|
||||
example/plugins/ztnc/authtoken.secret
|
||||
example/plugins/ztnc/ztnc.db.lock
|
||||
docs/plugins/docs.exe
|
||||
.idea
|
||||
conf
|
||||
log
|
||||
tmp
|
||||
sys.*
|
||||
www/html/index.html
|
||||
*.exe
|
36
README.md
36
README.md
@@ -13,22 +13,24 @@ A general purpose HTTP reverse proxy and forwarding tool. Now written in Go!
|
||||
- Basic Auth
|
||||
- Alias Hostnames
|
||||
- Custom Headers
|
||||
- Load Balancing
|
||||
- Redirection Rules
|
||||
- TLS / SSL setup and deploy
|
||||
- ACME features like auto-renew to serve your sites in http**s**
|
||||
- SNI support (and SAN certs)
|
||||
- DNS Challenge for Let's Encrypt and [these DNS providers](https://go-acme.github.io/lego/dns/)
|
||||
- Blacklist / Whitelist by country or IP address (single IP, CIDR or wildcard for beginners)
|
||||
- Global Area Network Controller Web UI (ZeroTier not included)
|
||||
- Stream Proxy (TCP & UDP)
|
||||
- Integrated Up-time Monitor
|
||||
- Web-SSH Terminal
|
||||
- Plugin System
|
||||
- Utilities
|
||||
- CIDR IP converters
|
||||
- mDNS Scanner
|
||||
- Wake-On-Lan
|
||||
- Debug Forward Proxy
|
||||
- IP Scanner
|
||||
- Port Scanner
|
||||
- Others
|
||||
- Basic single-admin management mode
|
||||
- External permission management system for easy system integration
|
||||
@@ -107,6 +109,8 @@ Usage of zoraxy:
|
||||
If web server is enabled by default (default true)
|
||||
-default_inbound_port int
|
||||
Default web server listening port (default 443)
|
||||
-dev
|
||||
Use external web folder for UI development
|
||||
-docker
|
||||
Run Zoraxy in docker compatibility mode
|
||||
-earlyrenew int
|
||||
@@ -164,19 +168,7 @@ There is a wikipage with [Frequently-Asked-Questions](https://github.com/tobychu
|
||||
|
||||
## Global Area Network Controller
|
||||
|
||||
This project also compatible with [ZeroTier](https://www.zerotier.com/). However, due to licensing issues, ZeroTier is not included in the binary.
|
||||
|
||||
To use Zoraxy with ZeroTier, assuming you already have a valid license, install ZeroTier on your host and then run Zoraxy in sudo mode (or Run As Administrator if you are on Windows). The program will automatically grab the authtoken in the correct location on your host.
|
||||
|
||||
If you prefer not to run Zoraxy in sudo mode or you have some weird installation profile, you can also pass in the ZeroTier auth token using the following flags:
|
||||
|
||||
```bash
|
||||
./zoraxy -ztauth="your_zerotier_authtoken" -ztport=9993
|
||||
```
|
||||
|
||||
The ZeroTier auth token can usually be found at ```/var/lib/zerotier-one/authtoken.secret``` or ```C:\ProgramData\ZeroTier\One\authtoken.secret```.
|
||||
|
||||
This allows you to have an infinite number of network members in your Global Area Network controller. For more technical details, see [here](https://docs.zerotier.com/self-hosting/network-controllers/).
|
||||
Moved to official plugin repo, see [ztnc](https://github.com/aroz-online/zoraxy-official-plugins/tree/main/src/ztnc) plugin
|
||||
|
||||
## Web SSH
|
||||
|
||||
@@ -199,10 +191,20 @@ Loopback web SSH connections, by default, are disabled. This means that if you a
|
||||
|
||||
Some section of Zoraxy are contributed by our amazing community and if you have any issues regarding those sections, it would be more efficient if you can tag them directly when creating an issue report.
|
||||
|
||||
- Authelia Support added by [@7brend7](https://github.com/7brend7)
|
||||
- Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
|
||||
- Forward Auth [@james-d-elliott](https://github.com/james-d-elliott)
|
||||
|
||||
- (Legacy) Authelia Support added by [@7brend7](https://github.com/7brend7)
|
||||
|
||||
- (Legacy) Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
|
||||
|
||||
|
||||
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
|
||||
|
||||
### Looking for Maintainer
|
||||
|
||||
- ACME DNS Challenge Module
|
||||
- Logging (including analysis & attack prevention) Module
|
||||
|
||||
Thank you so much for your contributions!
|
||||
|
||||
## Sponsor This Project
|
||||
@@ -210,7 +212,7 @@ Thank you so much for your contributions!
|
||||
If you like the project and want to support us, please consider a donation. You can use the links below
|
||||
|
||||
- [tobychui (Primary author)](https://paypal.me/tobychui)
|
||||
- PassiveLemon (Docker compatibility maintainer)
|
||||
- [PassiveLemon (Docker compatibility maintainer)](https://github.com/PassiveLemon)
|
||||
|
||||
## License
|
||||
|
||||
|
@@ -457,7 +457,7 @@
|
||||
</div>
|
||||
</a>
|
||||
<i class="divider"> </i>
|
||||
<a class="section externallink" href="" target="_blank">
|
||||
<a class="section externallink" href="https://zoraxy.aroz.org/plugins/html/" target="_blank">
|
||||
<div class="ui icon header">
|
||||
<i class="green code icon"></i>
|
||||
<div class="content" i18n>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 61 KiB |
Binary file not shown.
BIN
img/title.png
BIN
img/title.png
Binary file not shown.
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 57 KiB |
BIN
img/title.psd
BIN
img/title.psd
Binary file not shown.
@@ -34,6 +34,7 @@ func RegisterHTTPProxyAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/proxy/detail", ReverseProxyListDetail)
|
||||
authRouter.HandleFunc("/api/proxy/edit", ReverseProxyHandleEditEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/setAlias", ReverseProxyHandleAlias)
|
||||
authRouter.HandleFunc("/api/proxy/setHostname", ReverseProxyHandleSetHostname)
|
||||
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/updateCredentials", UpdateProxyBasicAuthCredentials)
|
||||
authRouter.HandleFunc("/api/proxy/tlscheck", domainsniff.HandleCheckSiteSupportTLS)
|
||||
@@ -83,6 +84,7 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) {
|
||||
// Register the APIs for Authentication handlers like Forward Auth and OAUTH2
|
||||
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
|
||||
authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions)
|
||||
authRouter.HandleFunc("/api/sso/OAuth2", oauth2Router.HandleSetOAuth2Settings)
|
||||
}
|
||||
|
||||
// Register the APIs for redirection rules management functions
|
||||
|
@@ -13,6 +13,8 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/acme"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
@@ -42,7 +44,7 @@ import (
|
||||
const (
|
||||
/* Build Constants */
|
||||
SYSTEM_NAME = "Zoraxy"
|
||||
SYSTEM_VERSION = "3.2.2"
|
||||
SYSTEM_VERSION = "3.2.3"
|
||||
DEVELOPMENT_BUILD = false
|
||||
|
||||
/* System Constants */
|
||||
@@ -143,7 +145,8 @@ var (
|
||||
pluginManager *plugins.Manager //Plugin manager for managing plugins
|
||||
|
||||
//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
|
||||
EmailSender *email.Sender //Email sender that handle email sending
|
||||
|
@@ -16,6 +16,7 @@ require (
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/likexian/whois v1.15.1
|
||||
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/syndtr/goleveldb v1.0.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/snappy v0.0.1 // 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/peterhellberg/link v1.2.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
|
@@ -56,7 +56,7 @@ func (ar *AuthentikRouter) HandleSetAuthentikURLAndHTTPS(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
useHTTPS, err := utils.PostBool(r, "useHTTPS")
|
||||
useHTTPS, err := utils.PostBool(r, "authentikUseHttps")
|
||||
if err != nil {
|
||||
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, "")
|
||||
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
|
||||
@@ -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 {
|
||||
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"
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
||||
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
"imuslab.com/zoraxy/mod/auth/sso/forward"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
|
||||
@@ -64,6 +66,7 @@ type RouterOption struct {
|
||||
|
||||
/* Authentication Providers */
|
||||
ForwardAuthRouter *forward.AuthRouter
|
||||
OAuth2Router *oauth2.OAuth2Router //OAuth2Router router for OAuth2Router authentication
|
||||
|
||||
/* Utilities */
|
||||
Logger *logger.Logger //Logger for reverse proxy requets
|
||||
|
@@ -1,14 +1,14 @@
|
||||
//go:build (windows && amd64) || (linux && mipsle) || (linux && riscv64) || (freebsd && amd64)
|
||||
// +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 darwin,arm64
|
||||
|
||||
package sshprox
|
||||
|
||||
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 (
|
||||
//go:embed gotty/LICENSE
|
||||
|
@@ -116,6 +116,7 @@ func ReverseProxtInit() {
|
||||
WebDirectory: *path_webserver,
|
||||
AccessController: accessController,
|
||||
ForwardAuthRouter: forwardAuthRouter,
|
||||
OAuth2Router: oauth2Router,
|
||||
LoadBalancer: loadBalancer,
|
||||
PluginManager: pluginManager,
|
||||
/* Utilities */
|
||||
@@ -672,6 +673,83 @@ func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func ReverseProxyHandleSetHostname(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
utils.SendErrorResponse(w, "Method not supported")
|
||||
return
|
||||
}
|
||||
|
||||
originalRootnameOrMatchingDomain, err := utils.PostPara(r, "oldHostname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid original hostname given")
|
||||
return
|
||||
}
|
||||
|
||||
newHostname, err := utils.PostPara(r, "newHostname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid new hostname given")
|
||||
return
|
||||
}
|
||||
|
||||
originalRootnameOrMatchingDomain = strings.TrimSpace(originalRootnameOrMatchingDomain)
|
||||
newHostname = strings.TrimSpace(newHostname)
|
||||
if newHostname == "/" {
|
||||
//Reserevd, reutrn error
|
||||
utils.SendErrorResponse(w, "Invalid new hostname: system reserved path")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the endpoint already exists
|
||||
_, err = dynamicProxyRouter.LoadProxy(newHostname)
|
||||
if err == nil {
|
||||
//Endpoint already exists, return error
|
||||
utils.SendErrorResponse(w, "Endpoint with this hostname already exists")
|
||||
return
|
||||
}
|
||||
|
||||
//Clone, edit the endpoint and remove the original one
|
||||
ept, err := dynamicProxyRouter.LoadProxy(originalRootnameOrMatchingDomain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
newEndpoint := ept.Clone()
|
||||
newEndpoint.RootOrMatchingDomain = newHostname
|
||||
|
||||
//Prepare to replace the current routing rule
|
||||
readyRoutingRule, err := dynamicProxyRouter.PrepareProxyRoute(newEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Remove the old endpoint from runtime
|
||||
err = dynamicProxyRouter.RemoveProxyEndpointByRootname(originalRootnameOrMatchingDomain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Remove the config from file
|
||||
err = RemoveReverseProxyConfig(originalRootnameOrMatchingDomain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Add the new endpoint to runtime
|
||||
dynamicProxyRouter.AddProxyRouteToRuntime(readyRoutingRule)
|
||||
|
||||
//Save it to file
|
||||
SaveReverseProxyConfig(newEndpoint)
|
||||
|
||||
//Update uptime monitor targets
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
ep, err := utils.PostPara(r, "ep")
|
||||
if err != nil {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -147,6 +148,11 @@ func startupSequence() {
|
||||
Database: sysdb,
|
||||
})
|
||||
|
||||
oauth2Router = oauth2.NewOAuth2Router(&oauth2.OAuth2RouterOptions{
|
||||
Logger: SystemWideLogger,
|
||||
Database: sysdb,
|
||||
})
|
||||
|
||||
//Create a statistic collector
|
||||
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
||||
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,7 +1,7 @@
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Default Site</h2>
|
||||
<p>Default routing options for inbound traffic (previously called Proxy Root)</p>
|
||||
<p>Default routing options for inbound traffic</p>
|
||||
<div class="ui form">
|
||||
<div class="grouped fields">
|
||||
<label>What to show when Zoraxy is hit with an unknown Host?</label>
|
||||
@@ -209,14 +209,13 @@
|
||||
})
|
||||
}
|
||||
|
||||
//Set the new proxy root option
|
||||
//Set the new proxy root (aka default site) option
|
||||
function setProxyRoot(btn=undefined){
|
||||
var newpr = $("#proxyRoot").val();
|
||||
if (newpr.trim() == "" && currentDefaultSiteOption == 0){
|
||||
//Fill in the web server info
|
||||
newpr = "127.0.0.1:" + $("#webserv_listenPort").val();
|
||||
$("#proxyRoot").val(newpr);
|
||||
|
||||
}
|
||||
|
||||
var rootReqTls = $("#rootReqTLS")[0].checked;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div class="standardContainer">
|
||||
<div class="sso standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>SSO</h2>
|
||||
<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://goauthentik.io/" rel=”noopener noreferrer” target="_blank">Authentik</a></li>
|
||||
</ul>
|
||||
<form class="ui form">
|
||||
<form class="ui form" action="#" id="forwardAuthSettings">
|
||||
<div class="field">
|
||||
<label for="forwardAuthAddress">Address</label>
|
||||
<input type="text" id="forwardAuthAddress" name="forwardAuthAddress" placeholder="Enter Forward Auth Address">
|
||||
@@ -66,7 +66,55 @@
|
||||
</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>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
@@ -74,6 +122,7 @@
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
/* Load forward-auth settings from backend */
|
||||
$.cjax({
|
||||
url: '/api/sso/forward-auth',
|
||||
method: 'GET',
|
||||
@@ -89,8 +138,33 @@
|
||||
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() {
|
||||
const address = $('#forwardAuthAddress').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>
|
@@ -184,7 +184,8 @@ body.darkTheme .ui.input input::placeholder {
|
||||
body.darkTheme .ui.label,
|
||||
body.darkTheme .ui.label .detail,
|
||||
body.darkTheme .ui.label .icon {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--buttom_toggle_disabled);
|
||||
color: var(--text_color) !important;
|
||||
}
|
||||
|
||||
body.darkTheme .advanceoptions .title {
|
||||
|
@@ -72,7 +72,7 @@
|
||||
<i class="simplistic lock icon"></i> TLS / SSL certificates
|
||||
</a>
|
||||
<a class="item" tag="sso">
|
||||
<i class="simplistic user circle icon"></i> SSO / Oauth
|
||||
<i class="simplistic user circle icon"></i> SSO / OAuth2
|
||||
</a>
|
||||
<div class="ui divider menudivider">Others</div>
|
||||
<a class="item" tag="webserv">
|
||||
@@ -120,7 +120,7 @@
|
||||
<!-- Create Rules -->
|
||||
<div id="rules" class="functiontab" target="rules.html"></div>
|
||||
|
||||
<!-- Set proxy root -->
|
||||
<!-- Set default site -->
|
||||
<div id="setroot" class="functiontab" target="rproot.html"></div>
|
||||
|
||||
<!-- Set TLS cert -->
|
||||
@@ -334,6 +334,7 @@
|
||||
}
|
||||
|
||||
function toggleTheme(){
|
||||
let editorSideWrapper = $("#httprpEditModal .wrapper_frame");
|
||||
if ($("body").hasClass("darkTheme")){
|
||||
setDarkTheme(false);
|
||||
//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);
|
||||
}
|
||||
|
||||
$(editorSideWrapper).each(function(){
|
||||
$(this)[0].contentWindow.setDarkTheme(false);
|
||||
})
|
||||
|
||||
if ($("#pluginContextLoader").is(":visible")){
|
||||
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(false);
|
||||
}
|
||||
@@ -350,6 +355,9 @@
|
||||
if ($(".sideWrapper").is(":visible")){
|
||||
$(".sideWrapper iframe")[0].contentWindow.setDarkTheme(true);
|
||||
}
|
||||
$(editorSideWrapper).each(function(){
|
||||
$(this)[0].contentWindow.setDarkTheme(true);
|
||||
})
|
||||
if ($("#pluginContextLoader").is(":visible")){
|
||||
$("#pluginContextLoader")[0].contentWindow.setDarkTheme(true);
|
||||
}
|
||||
@@ -515,6 +523,12 @@
|
||||
}
|
||||
|
||||
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){
|
||||
$(".sideWrapper iframe").attr("src", "snippet/placeholder.html");
|
||||
}
|
||||
|
@@ -264,7 +264,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
document.getElementById('accessRuleSelector').addEventListener('change', handleSelectEditingAccessRule);
|
||||
document.getElementById('accessRuleForm').addEventListener('submit', handleCreateNewAccessRule);
|
||||
|
||||
|
@@ -14,17 +14,18 @@
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<!--
|
||||
<div class="ui header">
|
||||
<div class="content">
|
||||
Alias Hostname
|
||||
<div class="sub header epname"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui divider"></div>-->
|
||||
<div class="scrolling content ui form">
|
||||
<div id="inlineEditBasicAuthCredentials" class="field">
|
||||
<p>Enter alias hostname or wildcard matching keywords for <code class="epname"></code></p>
|
||||
<table class="ui very basic compacted unstackable celled table">
|
||||
<table class="ui basic very compact unstackable celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alias Hostname</th>
|
||||
@@ -50,10 +51,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="field" >
|
||||
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br><br><br><br>
|
||||
@@ -164,7 +161,7 @@
|
||||
}
|
||||
$("#inlineEditTable").append(`<tr>
|
||||
<td>${domainLink}</td>
|
||||
<td><button class="ui basic button" onclick="removeAliasDomain('${aliasDomain}');"><i class="red remove icon"></i> Remove</button></td>
|
||||
<td><button class="ui basic mini circular icon button" onclick="removeAliasDomain('${aliasDomain}');"><i class="red trash icon"></i></button></td>
|
||||
</tr>`);
|
||||
});
|
||||
|
||||
|
@@ -14,18 +14,11 @@
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui header">
|
||||
<div class="content">
|
||||
Basic Auth Settings
|
||||
<div class="sub header" id="epname"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<h3 class="ui header">Basic Auth Credential</h3>
|
||||
<div class="scrolling content ui form">
|
||||
<div id="inlineEditBasicAuthCredentials" class="field">
|
||||
<p>Enter the username and password for allowing them to access this proxy endpoint</p>
|
||||
<table class="ui very basic compacted unstackable celled table">
|
||||
<table class="ui basic very compacted unstackable celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
@@ -56,7 +49,7 @@
|
||||
<h3 class="ui header">Authentication Exclusion Paths</h3>
|
||||
<div class="scrolling content ui form">
|
||||
<p>Exclude specific directories / paths which contains the following subpath prefix from authentication. Useful if you are hosting services require remote API access.</p>
|
||||
<table class="ui very basic compacted unstackable celled table">
|
||||
<table class="ui basic very compacted unstackable celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Path Prefix</th>
|
||||
@@ -86,10 +79,6 @@
|
||||
<code>/public/res/far/boo/</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="field" >
|
||||
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br><br><br><br>
|
||||
@@ -232,7 +221,7 @@
|
||||
data.forEach(function(rule){
|
||||
$("#exclusionPaths").append(` <tr>
|
||||
<td>${rule.PathPrefix}</td>
|
||||
<td><button class="ui red basic mini icon button" onclick="removeExceptionPath(this);" prefix="${rule.PathPrefix}"><i class="ui red times icon"></i></button></td>
|
||||
<td><button class="ui red basic mini circular icon button" onclick="removeExceptionPath(this);" prefix="${rule.PathPrefix}"><i class="ui red times icon"></i></button></td>
|
||||
</tr>`);
|
||||
})
|
||||
}
|
||||
@@ -261,7 +250,7 @@
|
||||
var row = '<tr>' +
|
||||
'<td>' + username + '</td>' +
|
||||
'<td>' + password + '</td>' +
|
||||
'<td><button class="ui basic button" onclick="removeCredentialFromEditingList(' + i + ');"><i class="red remove icon"></i> Remove</button></td>' +
|
||||
'<td><button class="ui basic tiny circular button" onclick="removeCredentialFromEditingList(' + i + ');"><i class="red remove icon"></i> Remove</button></td>' +
|
||||
'</tr>';
|
||||
|
||||
tableBody.append(row);
|
||||
|
@@ -27,6 +27,11 @@
|
||||
body.darkTheme #permissionPolicyEditor .experimental{
|
||||
background-color: rgb(41, 41, 41);
|
||||
}
|
||||
|
||||
.advanceoptions{
|
||||
background: var(--theme_advance) !important;
|
||||
border-radius: 0.4em !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -34,19 +39,12 @@
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui header">
|
||||
<div class="content">
|
||||
Custom Headers
|
||||
<div class="sub header" id="epname"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui small pointing secondary menu">
|
||||
<a class="item active narrowpadding" data-tab="customheaders">Custom Headers</a>
|
||||
<a class="item narrowpadding" data-tab="security">Security Headers</a>
|
||||
</div>
|
||||
<div class="ui tab basic segment active" data-tab="customheaders">
|
||||
<table class="ui very basic compacted unstackable celled table">
|
||||
<table class="ui basic very compacted unstackable celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
@@ -171,10 +169,6 @@
|
||||
<br><br>
|
||||
<button class="ui basic button" onclick="savePermissionPolicy();"><i class="green save icon"></i> Save</button>
|
||||
</div>
|
||||
|
||||
<div class="field" >
|
||||
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br><br><br><br>
|
||||
@@ -189,7 +183,7 @@
|
||||
let payloadHash = window.location.hash.substr(1);
|
||||
try{
|
||||
payloadHash = JSON.parse(decodeURIComponent(payloadHash));
|
||||
$("#epname").text(payloadHash.ep);
|
||||
//$("#epname").text(payloadHash.ep);
|
||||
editingEndpoint = payloadHash;
|
||||
}catch(ex){
|
||||
console.log("Unable to load endpoint data from hash")
|
||||
|
@@ -35,7 +35,7 @@
|
||||
|
||||
#accessRuleList{
|
||||
padding: 0.6em;
|
||||
border: 1px solid rgb(228, 228, 228);
|
||||
/* border: 1px solid rgb(228, 228, 228); */
|
||||
border-radius: 0.4em !important;
|
||||
max-height: calc(100vh - 15em);
|
||||
min-height: 300px;
|
||||
@@ -65,13 +65,6 @@
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<br>
|
||||
<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>
|
||||
<div id="accessRuleList">
|
||||
<div class="ui segment accessRule">
|
||||
@@ -85,9 +78,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<button class="ui basic button" onclick="applyChangeAndClose()"><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>
|
||||
<!-- <button class="ui basic button" onclick="applyChange()"><i class="ui green check icon"></i> Apply Change</button> -->
|
||||
<br><br><br>
|
||||
|
||||
</div>
|
||||
@@ -174,6 +165,35 @@
|
||||
let accessRuleID = $(accessRuleObject).attr("ruleid");
|
||||
$(".accessRule").removeClass('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(){
|
||||
|
@@ -21,13 +21,6 @@
|
||||
<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">
|
||||
@@ -68,9 +61,7 @@
|
||||
</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>
|
||||
<br><br>
|
||||
</div>
|
||||
<script>
|
||||
let editingEndpoint = {};
|
||||
@@ -164,6 +155,10 @@
|
||||
|
||||
function addSelectedTags() {
|
||||
let tags = $('#tagsInput').val().split(',').map(tag => tag.trim());
|
||||
if (tags.length == 0 || (tags.length == 1 && tags[0] == "")){
|
||||
parent.msgbox("Please enter at least one tag", false);
|
||||
return;
|
||||
}
|
||||
tags.forEach(tag => {
|
||||
if (tag && !tagAlreadyExistsInTable(tag)) {
|
||||
addTagRow(tag);
|
||||
@@ -210,8 +205,8 @@
|
||||
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 title="Delete Tag" class="ui circular mini basic button" onclick="removeTag('${tag}')">
|
||||
<i class="red trash icon"></i> Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
@@ -75,13 +75,6 @@
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<br>
|
||||
<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">
|
||||
<a class="item active narrowpadding" data-tab="upstreamlist">Upstreams</a>
|
||||
<a class="item narrowpadding" data-tab="newupstream">Add Upstream</a>
|
||||
@@ -159,10 +152,6 @@
|
||||
<br><br>
|
||||
<button class="ui basic button" onclick="addNewUpstream();"><i class="ui green circle add icon"></i> Create</button>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="field" >
|
||||
<button class="ui basic button" style="float: right;" onclick="closeThisWrapper();">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<br><br><br><br>
|
||||
|
||||
|
Reference in New Issue
Block a user