Merge pull request #671 from tobychui/v3.2.2

- Merged in forward-auth implementation
- Added upgrader for v3.2.1 to v3.2.2
This commit is contained in:
Toby Chui 2025-05-20 20:01:30 +08:00 committed by GitHub
commit 9d2b8f224c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1362 additions and 209 deletions

View File

@ -80,10 +80,9 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
}
// Register the APIs for Authentication handlers like Authelia and OAUTH2
// Register the APIs for Authentication handlers like Forward Auth and OAUTH2
func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS)
authRouter.HandleFunc("/api/sso/Authentik", authentikRouter.HandleSetAuthentikURLAndHTTPS)
authRouter.HandleFunc("/api/sso/forward-auth", forwardAuthRouter.HandleAPIOptions)
}
// Register the APIs for redirection rules management functions
@ -239,6 +238,10 @@ func RegisterPluginAPIs(authRouter *auth.RouterDef) {
authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList)
authRouter.HandleFunc("/api/plugins/store/install", pluginManager.HandleInstallPlugin)
authRouter.HandleFunc("/api/plugins/store/uninstall", pluginManager.HandleUninstallPlugin)
// Developer options
authRouter.HandleFunc("/api/plugins/developer/enableAutoReload", pluginManager.HandleEnableHotReload)
authRouter.HandleFunc("/api/plugins/developer/setAutoReloadInterval", pluginManager.HandleSetHotReloadInterval)
}
// Register the APIs for Auth functions, due to scoping issue some functions are defined here

View File

@ -13,12 +13,10 @@ import (
"net/http"
"time"
"imuslab.com/zoraxy/mod/auth/sso/authentik"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/auth/sso/forward"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dockerux"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
@ -43,8 +41,9 @@ import (
const (
/* Build Constants */
SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.2.1"
SYSTEM_NAME = "Zoraxy"
SYSTEM_VERSION = "3.2.2"
DEVELOPMENT_BUILD = false
/* System Constants */
TMP_FOLDER = "./tmp"
@ -144,8 +143,7 @@ var (
pluginManager *plugins.Manager //Plugin manager for managing plugins
//Authentication Provider
autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
authentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication
forwardAuthRouter *forward.AuthRouter // Forward Auth router for Authelia/Authentik/etc authentication
//Helper modules
EmailSender *email.Sender //Email sender that handle email sending

View File

@ -0,0 +1,7 @@
Module: authelia
Notice:
This module is **deprecated** and is no longer in use. It has been retained here for reference purposes only.
Consider using the updated implementation or alternative solutions as this module may be removed in future updates.
Original implementation: https://github.com/tobychui/zoraxy/pull/421

View File

@ -0,0 +1,7 @@
Module: authentik
Notice:
This module is **deprecated** and is no longer in use. It has been retained here for reference purposes only.
Consider using the updated implementation or alternative solutions as this module may be removed in future updates.
Original implementation: https://github.com/tobychui/zoraxy/pull/568

View File

@ -0,0 +1,46 @@
package forward
import "errors"
const (
LogTitle = "Forward Auth"
DatabaseTable = "auth_sso_forward"
DatabaseKeyAddress = "address"
DatabaseKeyResponseHeaders = "responseHeaders"
DatabaseKeyResponseClientHeaders = "responseClientHeaders"
DatabaseKeyRequestHeaders = "requestHeaders"
DatabaseKeyRequestExcludedCookies = "requestExcludedCookies"
HeaderXForwardedProto = "X-Forwarded-Proto"
HeaderXForwardedHost = "X-Forwarded-Host"
HeaderXForwardedFor = "X-Forwarded-For"
HeaderXForwardedURI = "X-Forwarded-URI"
HeaderXForwardedMethod = "X-Forwarded-Method"
HeaderCookie = "Cookie"
HeaderUpgrade = "Upgrade"
HeaderConnection = "Connection"
HeaderTransferEncoding = "Transfer-Encoding"
HeaderTE = "TE"
HeaderTrailers = "Trailers"
HeaderKeepAlive = "Keep-Alive"
)
var (
ErrInternalServerError = errors.New("internal server error")
ErrUnauthorized = errors.New("unauthorized")
)
var (
doNotCopyHeaders = []string{
HeaderUpgrade,
HeaderConnection,
HeaderTransferEncoding,
HeaderTE,
HeaderTrailers,
HeaderKeepAlive,
}
)

View File

@ -0,0 +1,334 @@
package forward
import (
"encoding/json"
"io"
"net"
"net/http"
"strings"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/utils"
)
type AuthRouterOptions struct {
// Address of the forward auth endpoint.
Address string
// ResponseHeaders is a list of headers to be copied from the response if provided by the forward auth endpoint to
// the request.
ResponseHeaders []string
// ResponseClientHeaders is a list of headers to be copied from the response if provided by the forward auth
// endpoint to the response to the client.
ResponseClientHeaders []string
// RequestHeaders is a list of headers to be copied from the request to the authorization server. If empty all
// headers are copied.
RequestHeaders []string
// RequestExcludedCookies is a list of cookie keys that should be removed from every request sent to the upstream.
RequestExcludedCookies []string
Logger *logger.Logger
Database *database.Database
}
type AuthRouter struct {
client *http.Client
options *AuthRouterOptions
}
// NewAuthRouter creates a new AuthRouter object
func NewAuthRouter(options *AuthRouterOptions) *AuthRouter {
options.Database.NewTable(DatabaseTable)
//Read settings from database if available.
options.Database.Read(DatabaseTable, DatabaseKeyAddress, &options.Address)
responseHeaders, responseClientHeaders, requestHeaders, requestExcludedCookies := "", "", "", ""
options.Database.Read(DatabaseTable, DatabaseKeyResponseHeaders, &responseHeaders)
options.Database.Read(DatabaseTable, DatabaseKeyResponseClientHeaders, &responseClientHeaders)
options.Database.Read(DatabaseTable, DatabaseKeyRequestHeaders, &requestHeaders)
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, &requestExcludedCookies)
options.ResponseHeaders = strings.Split(responseHeaders, ",")
options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",")
options.RequestHeaders = strings.Split(requestHeaders, ",")
options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
return &AuthRouter{
client: &http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) (err error) {
return http.ErrUseLastResponse
},
},
options: options,
}
}
// HandleAPIOptions is the internal handler for setting the options.
func (ar *AuthRouter) HandleAPIOptions(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
ar.handleOptionsGET(w, r)
case http.MethodPost:
ar.handleOptionsPOST(w, r)
default:
ar.handleOptionsMethodNotAllowed(w, r)
}
}
func (ar *AuthRouter) handleOptionsGET(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(map[string]interface{}{
DatabaseKeyAddress: ar.options.Address,
DatabaseKeyResponseHeaders: ar.options.ResponseHeaders,
DatabaseKeyResponseClientHeaders: ar.options.ResponseClientHeaders,
DatabaseKeyRequestHeaders: ar.options.RequestHeaders,
DatabaseKeyRequestExcludedCookies: ar.options.RequestExcludedCookies,
})
utils.SendJSONResponse(w, string(js))
return
}
func (ar *AuthRouter) handleOptionsPOST(w http.ResponseWriter, r *http.Request) {
// Update the settings
address, err := utils.PostPara(r, DatabaseKeyAddress)
if err != nil {
utils.SendErrorResponse(w, "address not found")
return
}
// These are optional fields and can be empty strings.
responseHeaders, _ := utils.PostPara(r, DatabaseKeyResponseHeaders)
responseClientHeaders, _ := utils.PostPara(r, DatabaseKeyResponseClientHeaders)
requestHeaders, _ := utils.PostPara(r, DatabaseKeyRequestHeaders)
requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies)
// Write changes to runtime
ar.options.Address = address
ar.options.ResponseHeaders = strings.Split(responseHeaders, ",")
ar.options.ResponseClientHeaders = strings.Split(responseClientHeaders, ",")
ar.options.RequestHeaders = strings.Split(requestHeaders, ",")
ar.options.RequestExcludedCookies = strings.Split(requestExcludedCookies, ",")
// Write changes to database
ar.options.Database.Write(DatabaseTable, DatabaseKeyAddress, address)
ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseHeaders, responseHeaders)
ar.options.Database.Write(DatabaseTable, DatabaseKeyResponseClientHeaders, responseClientHeaders)
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestHeaders, requestHeaders)
ar.options.Database.Write(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies)
utils.SendOK(w)
}
func (ar *AuthRouter) handleOptionsMethodNotAllowed(w http.ResponseWriter, r *http.Request) {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
// HandleAuthProviderRouting is the internal handler for Forward Auth authentication.
func (ar *AuthRouter) HandleAuthProviderRouting(w http.ResponseWriter, r *http.Request) error {
if ar.options.Address == "" {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Address not set", nil)
return ErrInternalServerError
}
// Make a request to Authz Server to verify the request
req, err := http.NewRequest(http.MethodGet, ar.options.Address, nil)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to create request", err)
return ErrInternalServerError
}
// TODO: Add opt-in support for copying the request body to the forward auth request.
headerCopyIncluded(r.Header, req.Header, ar.options.RequestHeaders, true)
// TODO: Add support for upstream headers.
rSetForwardedHeaders(r, req)
// Make the Authz Request.
respForwarded, err := ar.client.Do(req)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to perform forwarded auth due to a request error", err)
return ErrInternalServerError
}
defer respForwarded.Body.Close()
body, err := io.ReadAll(respForwarded.Body)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to read response to forward auth request", err)
return ErrInternalServerError
}
// Responses within the 200-299 range are considered successful and allow the proxy to handle the request.
if respForwarded.StatusCode >= http.StatusOK && respForwarded.StatusCode < http.StatusMultipleChoices {
if len(ar.options.ResponseClientHeaders) != 0 {
headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseClientHeaders, false)
}
if len(ar.options.RequestExcludedCookies) != 0 {
// If the user has specified a list of cookies to be removed from the request, deterministically remove them.
headerCookieRedact(r, ar.options.RequestExcludedCookies)
}
if len(ar.options.ResponseHeaders) != 0 {
// Copy specific user-specified headers from the response of the forward auth request to the request sent to the
// upstream server/next hop.
headerCopyIncluded(respForwarded.Header, w.Header(), ar.options.ResponseHeaders, false)
}
return nil
}
// Copy the response.
headerCopyExcluded(respForwarded.Header, w.Header(), nil)
w.WriteHeader(respForwarded.StatusCode)
if _, err = w.Write(body); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
ar.options.Logger.PrintAndLog(LogTitle, "Unable to write response", err)
return ErrInternalServerError
}
return ErrUnauthorized
}
func scheme(r *http.Request) string {
if r.TLS != nil {
return "https"
}
return "http"
}
func headerCookieRedact(r *http.Request, excluded []string) {
original := r.Cookies()
if len(original) == 0 {
return
}
var cookies []string
for _, cookie := range original {
if stringInSlice(cookie.Name, excluded) {
continue
}
cookies = append(cookies, cookie.String())
}
r.Header.Set(HeaderCookie, strings.Join(cookies, "; "))
}
func headerCopyExcluded(original, destination http.Header, excludedHeaders []string) {
for key, values := range original {
// We should never copy the headers in the below list.
if stringInSliceFold(key, doNotCopyHeaders) {
continue
}
if stringInSliceFold(key, excludedHeaders) {
continue
}
destination[key] = append(destination[key], values...)
}
}
func headerCopyIncluded(original, destination http.Header, includedHeaders []string, allIfEmpty bool) {
if allIfEmpty && len(includedHeaders) == 0 {
headerCopyAll(original, destination)
} else {
headerCopyIncludedExact(original, destination, includedHeaders)
}
}
func headerCopyAll(original, destination http.Header) {
for key, values := range original {
// We should never copy the headers in the below list, even if they're in the list provided by a user.
if stringInSliceFold(key, doNotCopyHeaders) {
continue
}
destination[key] = append(destination[key], values...)
}
}
func headerCopyIncludedExact(original, destination http.Header, keys []string) {
for _, key := range keys {
// We should never copy the headers in the below list, even if they're in the list provided by a user.
if stringInSliceFold(key, doNotCopyHeaders) {
continue
}
if values, ok := original[key]; ok {
destination[key] = append(destination[key], values...)
}
}
}
func stringInSlice(needle string, haystack []string) bool {
if len(haystack) == 0 {
return false
}
for _, v := range haystack {
if needle == v {
return true
}
}
return false
}
func stringInSliceFold(needle string, haystack []string) bool {
if len(haystack) == 0 {
return false
}
for _, v := range haystack {
if strings.EqualFold(needle, v) {
return true
}
}
return false
}
func rSetForwardedHeaders(r, req *http.Request) {
if r.RemoteAddr != "" {
before, _, _ := strings.Cut(r.RemoteAddr, ":")
if ip := net.ParseIP(before); ip != nil {
req.Header.Set(HeaderXForwardedFor, ip.String())
}
}
req.Header.Set(HeaderXForwardedMethod, r.Method)
req.Header.Set(HeaderXForwardedProto, scheme(r))
req.Header.Set(HeaderXForwardedHost, r.Host)
req.Header.Set(HeaderXForwardedURI, r.URL.Path)
}

View File

@ -32,20 +32,16 @@ and return a boolean indicate if the request is written to http.ResponseWriter
*/
func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool {
requestHostname := r.Host
if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic {
switch sep.AuthenticationProvider.AuthMethod {
case AuthMethodBasic:
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
return true
}
} else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthelia {
err := h.handleAutheliaAuth(w, r)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
return true
}
} else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthentik {
err := h.handleAuthentikAuth(w, r)
case AuthMethodForward:
err := h.handleForwardAuth(w, r)
if err != nil {
h.Parent.Option.Logger.LogHTTPRequest(r, "host-http", 401, requestHostname, "")
return true
@ -106,13 +102,9 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint)
return nil
}
/* Authelia */
/* Forward Auth */
// Handle authelia auth routing
func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r)
}
func (h *ProxyHandler) handleAuthentikAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.AuthentikRouter.HandleAuthentikAuth(w, r)
// Handle forward auth routing
func (h *ProxyHandler) handleForwardAuth(w http.ResponseWriter, r *http.Request) error {
return h.Parent.Option.ForwardAuthRouter.HandleAuthProviderRouting(w, r)
}

View File

@ -17,12 +17,15 @@ import (
// GetDefaultAuthenticationProvider return a default authentication provider
func GetDefaultAuthenticationProvider() *AuthenticationProvider {
return &AuthenticationProvider{
AuthMethod: AuthMethodNone,
BasicAuthCredentials: []*BasicAuthCredentials{},
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
BasicAuthGroupIDs: []string{},
AutheliaURL: "",
UseHTTPS: false,
AuthMethod: AuthMethodNone,
BasicAuthCredentials: []*BasicAuthCredentials{},
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
BasicAuthGroupIDs: []string{},
ForwardAuthURL: "",
ForwardAuthResponseHeaders: []string{},
ForwardAuthResponseClientHeaders: []string{},
ForwardAuthRequestHeaders: []string{},
ForwardAuthRequestExcludedCookies: []string{},
}
}

View File

@ -9,13 +9,12 @@ package dynamicproxy
*/
import (
_ "embed"
"imuslab.com/zoraxy/mod/auth/sso/authentik"
"net"
"net/http"
"sync"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/auth/sso/forward"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
@ -64,8 +63,7 @@ type RouterOption struct {
PluginManager *plugins.Manager //Plugin manager for handling plugin routing
/* Authentication Providers */
AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication
AuthentikRouter *authentik.AuthentikRouter //Authentik router for Authentik authentication
ForwardAuthRouter *forward.AuthRouter
/* Utilities */
Logger *logger.Logger //Logger for reverse proxy requets
@ -141,11 +139,10 @@ type HeaderRewriteRules struct {
type AuthMethod int
const (
AuthMethodNone AuthMethod = iota //No authentication required
AuthMethodBasic //Basic Auth
AuthMethodAuthelia //Authelia
AuthMethodOauth2 //Oauth2
AuthMethodAuthentik
AuthMethodNone AuthMethod = iota //No authentication required
AuthMethodBasic //Basic Auth
AuthMethodForward //Forward
AuthMethodOauth2 //Oauth2
)
type AuthenticationProvider struct {
@ -155,9 +152,12 @@ type AuthenticationProvider struct {
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint
/* Authelia Settings */
AutheliaURL string //URL of the Authelia server, leave empty to use global settings e.g. authelia.example.com
UseHTTPS bool //Whether to use HTTPS for the Authelia server
/* Forward Auth Settings */
ForwardAuthURL string // Full URL of the Forward Auth endpoint. Example: https://auth.example.com/api/authz/forward-auth
ForwardAuthResponseHeaders []string // List of headers to copy from the forward auth server response to the request.
ForwardAuthResponseClientHeaders []string // List of headers to copy from the forward auth server response to the client response.
ForwardAuthRequestHeaders []string // List of headers to copy from the original request to the auth server. If empty all are copied.
ForwardAuthRequestExcludedCookies []string // List of cookies to exclude from the request after sending it to the forward auth server.
}
// A proxy endpoint record, a general interface for handling inbound routing

View File

@ -0,0 +1,214 @@
package plugins
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net/http"
"os"
"strconv"
"time"
"imuslab.com/zoraxy/mod/utils"
)
// StartHotReloadTicker starts the hot reload ticker
func (m *Manager) StartHotReloadTicker() error {
if m.pluginReloadTicker != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker already started", nil)
return errors.New("hot reload ticker already started")
}
m.pluginReloadTicker = time.NewTicker(time.Duration(m.Options.HotReloadInterval) * time.Second)
m.pluginReloadStop = make(chan bool)
go func() {
for {
select {
case <-m.pluginReloadTicker.C:
err := m.UpdatePluginHashList(false)
if err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to update plugin hash list", err)
}
case <-m.pluginReloadStop:
return
}
}
}()
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker started", nil)
return nil
}
// StopHotReloadTicker stops the hot reload ticker
func (m *Manager) StopHotReloadTicker() error {
if m.pluginReloadTicker != nil {
m.pluginReloadStop <- true
m.pluginReloadTicker.Stop()
m.pluginReloadTicker = nil
m.pluginReloadStop = nil
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker stopped", nil)
} else {
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload ticker already stopped", nil)
}
return nil
}
func (m *Manager) InitPluginHashList() error {
return m.UpdatePluginHashList(true)
}
// Update the plugin hash list and if there are change, reload the plugin
func (m *Manager) UpdatePluginHashList(noReload bool) error {
for pluginId, plugin := range m.LoadedPlugins {
//Get the plugin Entry point
pluginEntryPoint, err := m.GetPluginEntryPoint(plugin.RootDir)
if err != nil {
//Unable to get the entry point of the plugin
return err
}
file, err := os.Open(pluginEntryPoint)
if err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to open plugin entry point: "+pluginEntryPoint, err)
return err
}
defer file.Close()
//Calculate the hash of the file
hasher := sha256.New()
if _, err := file.Seek(0, 0); err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to seek plugin entry point: "+pluginEntryPoint, err)
return err
}
if _, err := io.Copy(hasher, file); err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to copy plugin entry point: "+pluginEntryPoint, err)
return err
}
hash := hex.EncodeToString(hasher.Sum(nil))
m.pluginCheckMutex.Lock()
if m.PluginHash[pluginId] != hash {
m.PluginHash[pluginId] = hash
m.pluginCheckMutex.Unlock()
if !noReload {
//Plugin file changed, reload the plugin
m.Options.Logger.PrintAndLog("plugin-manager", "Plugin file changed, reloading plugin: "+pluginId, nil)
err := m.HotReloadPlugin(pluginId)
if err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to reload plugin: "+pluginId, err)
return err
} else {
m.Options.Logger.PrintAndLog("plugin-manager", "Plugin reloaded: "+pluginId, nil)
}
} else {
m.Options.Logger.PrintAndLog("plugin-manager", "Plugin hash generated for: "+pluginId, nil)
}
} else {
m.pluginCheckMutex.Unlock()
}
}
return nil
}
// Reload the plugin from file system
func (m *Manager) HotReloadPlugin(pluginId string) error {
//Check if the plugin is currently running
thisPlugin, err := m.GetPluginByID(pluginId)
if err != nil {
return err
}
if thisPlugin.IsRunning() {
err = m.StopPlugin(pluginId)
if err != nil {
return err
}
}
//Remove the plugin from the loaded plugins list
m.loadedPluginsMutex.Lock()
if _, ok := m.LoadedPlugins[pluginId]; ok {
delete(m.LoadedPlugins, pluginId)
} else {
m.loadedPluginsMutex.Unlock()
return nil
}
m.loadedPluginsMutex.Unlock()
//Reload the plugin from disk, it should reload the plugin from latest version
m.ReloadPluginFromDisk()
return nil
}
/*
Request handlers for developer options
*/
func (m *Manager) HandleEnableHotReload(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current status of hot reload
js, _ := json.Marshal(m.Options.EnableHotReload)
utils.SendJSONResponse(w, string(js))
return
}
enabled, err := utils.PostBool(r, "enabled")
if err != nil {
utils.SendErrorResponse(w, "enabled not found")
return
}
m.Options.EnableHotReload = enabled
if enabled {
//Start the hot reload ticker
err := m.StartHotReloadTicker()
if err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to start hot reload ticker", err)
utils.SendErrorResponse(w, "Failed to start hot reload ticker")
return
}
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload enabled", nil)
} else {
//Stop the hot reload ticker
err := m.StopHotReloadTicker()
if err != nil {
m.Options.Logger.PrintAndLog("plugin-manager", "Failed to stop hot reload ticker", err)
utils.SendErrorResponse(w, "Failed to stop hot reload ticker")
return
}
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload disabled", nil)
}
utils.SendOK(w)
}
func (m *Manager) HandleSetHotReloadInterval(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
//Return the current status of hot reload
js, _ := json.Marshal(m.Options.HotReloadInterval)
utils.SendJSONResponse(w, string(js))
return
}
interval, err := utils.PostInt(r, "interval")
if err != nil {
utils.SendErrorResponse(w, "interval not found")
return
}
if interval < 1 {
utils.SendErrorResponse(w, "interval must be at least 1 second")
return
}
m.Options.HotReloadInterval = interval
//Restart the hot reload ticker
if m.pluginReloadTicker != nil {
m.StopHotReloadTicker()
time.Sleep(1 * time.Second)
//Start the hot reload ticker again
m.StartHotReloadTicker()
}
m.Options.Logger.PrintAndLog("plugin-manager", "Hot reload interval set to "+strconv.Itoa(interval)+" sec", nil)
utils.SendOK(w)
}

View File

@ -11,11 +11,11 @@ import (
// ListPluginGroups returns a map of plugin groups
func (m *Manager) ListPluginGroups() map[string][]string {
pluginGroup := map[string][]string{}
m.Options.pluginGroupsMutex.RLock()
m.pluginGroupsMutex.RLock()
for k, v := range m.Options.PluginGroups {
pluginGroup[k] = append([]string{}, v...)
}
m.Options.pluginGroupsMutex.RUnlock()
m.pluginGroupsMutex.RUnlock()
return pluginGroup
}
@ -32,26 +32,26 @@ func (m *Manager) AddPluginToGroup(tag, pluginID string) error {
return errors.New("plugin is not a router type plugin")
}
m.Options.pluginGroupsMutex.Lock()
m.pluginGroupsMutex.Lock()
//Check if the tag exists
_, ok = m.Options.PluginGroups[tag]
if !ok {
m.Options.PluginGroups[tag] = []string{pluginID}
m.Options.pluginGroupsMutex.Unlock()
m.pluginGroupsMutex.Unlock()
return nil
}
//Add the plugin to the group
m.Options.PluginGroups[tag] = append(m.Options.PluginGroups[tag], pluginID)
m.Options.pluginGroupsMutex.Unlock()
m.pluginGroupsMutex.Unlock()
return nil
}
// RemovePluginFromGroup removes a plugin from a group
func (m *Manager) RemovePluginFromGroup(tag, pluginID string) error {
m.Options.pluginGroupsMutex.Lock()
defer m.Options.pluginGroupsMutex.Unlock()
m.pluginGroupsMutex.Lock()
defer m.pluginGroupsMutex.Unlock()
//Check if the tag exists
_, ok := m.Options.PluginGroups[tag]
if !ok {
@ -72,8 +72,8 @@ func (m *Manager) RemovePluginFromGroup(tag, pluginID string) error {
// RemovePluginGroup removes a plugin group
func (m *Manager) RemovePluginGroup(tag string) error {
m.Options.pluginGroupsMutex.Lock()
defer m.Options.pluginGroupsMutex.Unlock()
m.pluginGroupsMutex.Lock()
defer m.pluginGroupsMutex.Unlock()
_, ok := m.Options.PluginGroups[tag]
if !ok {
return errors.New("tag not found")
@ -84,12 +84,12 @@ func (m *Manager) RemovePluginGroup(tag string) error {
// SavePluginGroupsFromFile loads plugin groups from a file
func (m *Manager) SavePluginGroupsToFile() error {
m.Options.pluginGroupsMutex.RLock()
m.pluginGroupsMutex.RLock()
pluginGroupsCopy := make(map[string][]string)
for k, v := range m.Options.PluginGroups {
pluginGroupsCopy[k] = append([]string{}, v...)
}
m.Options.pluginGroupsMutex.RUnlock()
m.pluginGroupsMutex.RUnlock()
//Write to file
js, _ := json.Marshal(pluginGroupsCopy)

View File

@ -47,15 +47,26 @@ func NewPluginManager(options *ManagerOptions) *Manager {
//Create database table
options.Database.NewTable("plugins")
return &Manager{
thisManager := &Manager{
LoadedPlugins: make(map[string]*Plugin),
tagPluginMap: sync.Map{},
tagPluginListMutex: sync.RWMutex{},
tagPluginList: make(map[string][]*Plugin),
Options: options,
PluginHash: make(map[string]string),
/* Internal */
loadedPluginsMutex: sync.RWMutex{},
}
//Check if hot reload is enabled
if options.EnableHotReload {
err := thisManager.StartHotReloadTicker()
if err != nil {
options.Logger.PrintAndLog("plugin-manager", "Failed to start hot reload ticker", err)
}
}
return thisManager
}
// Reload all plugins from disk
@ -104,11 +115,16 @@ func (m *Manager) ReloadPluginFromDisk() {
m.loadedPluginsMutex.Lock()
m.LoadedPlugins[thisPlugin.Spec.ID] = thisPlugin
m.loadedPluginsMutex.Unlock()
m.Log("Added new plugin: "+thisPlugin.Spec.Name, nil)
versionNumber := strconv.Itoa(thisPlugin.Spec.VersionMajor) + "." + strconv.Itoa(thisPlugin.Spec.VersionMinor) + "." + strconv.Itoa(thisPlugin.Spec.VersionPatch)
//Check if the plugin is enabled
m.Log("Found plugin: "+thisPlugin.Spec.Name+" (v"+versionNumber+")", nil)
// The default state of the plugin is disabled, so no need to start it
}
}
//Generate a hash list for plugins
m.InitPluginHashList()
}
// LoadPluginsFromDisk loads all plugins from the plugin directory
@ -156,6 +172,8 @@ func (m *Manager) LoadPluginsFromDisk() error {
//Generate the static forwarder radix tree
m.UpdateTagsToPluginMaps()
//Generate a hash list for plugins
m.InitPluginHashList()
return nil
}

View File

@ -17,8 +17,8 @@ import (
// This will only load the plugin tags to option.PluginGroups map
// to push the changes to runtime, call UpdateTagsToPluginMaps()
func (m *Manager) LoadPluginGroupsFromConfig() error {
m.Options.pluginGroupsMutex.RLock()
defer m.Options.pluginGroupsMutex.RUnlock()
m.pluginGroupsMutex.RLock()
defer m.pluginGroupsMutex.RUnlock()
//Read the config file
rawConfig, err := os.ReadFile(m.Options.PluginGroupsConfig)
@ -39,8 +39,8 @@ func (m *Manager) LoadPluginGroupsFromConfig() error {
// AddPluginToTag adds a plugin to a tag
func (m *Manager) AddPluginToTag(tag string, pluginID string) error {
m.Options.pluginGroupsMutex.RLock()
defer m.Options.pluginGroupsMutex.RUnlock()
m.pluginGroupsMutex.RLock()
defer m.pluginGroupsMutex.RUnlock()
//Check if the plugin exists
_, err := m.GetPluginByID(pluginID)
@ -66,8 +66,8 @@ func (m *Manager) AddPluginToTag(tag string, pluginID string) error {
// RemovePluginFromTag removes a plugin from a tag
func (m *Manager) RemovePluginFromTag(tag string, pluginID string) error {
// Check if the plugin exists in Options.PluginGroups
m.Options.pluginGroupsMutex.RLock()
defer m.Options.pluginGroupsMutex.RUnlock()
m.pluginGroupsMutex.RLock()
defer m.pluginGroupsMutex.RUnlock()
pluginList, ok := m.Options.PluginGroups[tag]
if !ok {
return nil
@ -91,8 +91,8 @@ func (m *Manager) RemovePluginFromTag(tag string, pluginID string) error {
// savePluginTagMap saves the plugin tag map to the config file
func (m *Manager) savePluginTagMap() error {
m.Options.pluginGroupsMutex.RLock()
defer m.Options.pluginGroupsMutex.RUnlock()
m.pluginGroupsMutex.RLock()
defer m.pluginGroupsMutex.RUnlock()
js, _ := json.Marshal(m.Options.PluginGroups)
return os.WriteFile(m.Options.PluginGroupsConfig, js, 0644)

View File

@ -5,6 +5,7 @@ import (
"net/http"
"os/exec"
"sync"
"time"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
@ -45,8 +46,9 @@ type ManagerOptions struct {
Database *database.Database `json:"-"`
Logger *logger.Logger `json:"-"`
/* Internal */
pluginGroupsMutex sync.RWMutex //Mutex for the pluginGroups
/* Development */
EnableHotReload bool //Check if the plugin file is changed and reload the plugin automatically
HotReloadInterval int //The interval for checking the plugin file change, in seconds
}
type Manager struct {
@ -56,6 +58,12 @@ type Manager struct {
tagPluginList map[string][]*Plugin //Storing the plugin list for each tag, only concurrent READ is allowed
Options *ManagerOptions
PluginHash map[string]string //The hash of the plugin file, used to check if the plugin file is changed
/* Internal */
loadedPluginsMutex sync.RWMutex //Mutex for the loadedPlugins
pluginGroupsMutex sync.RWMutex //Mutex for the pluginGroups
pluginCheckMutex sync.RWMutex //Mutex for the plugin hash
pluginReloadTicker *time.Ticker //Ticker for the plugin reload
pluginReloadStop chan bool //Channel to stop the plugin reload ticker
}

View File

@ -3,6 +3,7 @@ package update
import (
v308 "imuslab.com/zoraxy/mod/update/v308"
v315 "imuslab.com/zoraxy/mod/update/v315"
v322 "imuslab.com/zoraxy/mod/update/v322"
)
// Updater Core logic
@ -19,6 +20,12 @@ func runUpdateRoutineWithVersion(fromVersion int, toVersion int) {
if err != nil {
panic(err)
}
} else if fromVersion == 321 && toVersion == 322 {
//Updating from v3.2.1 to v3.2.2
err := v322.UpdateFrom321To322()
if err != nil {
panic(err)
}
}
//ADD MORE VERSIONS HERE

View File

@ -0,0 +1,141 @@
package v322
import (
"net/http"
"sync"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
)
type ProxyType int
// Pull from ratelimit.go
type RequestCountPerIpTable struct {
table sync.Map
}
// Pull from special.go
type RoutingRule struct {
ID string //ID of the routing rule
Enabled bool //If the routing rule enabled
UseSystemAccessControl bool //Pass access control check to system white/black list, set this to false to bypass white/black list
MatchRule func(r *http.Request) bool
RoutingHandler func(http.ResponseWriter, *http.Request)
}
const (
ProxyTypeRoot ProxyType = iota //Root Proxy, everything not matching will be routed here
ProxyTypeHost //Host Proxy, match by host (domain) name
ProxyTypeVdir //Virtual Directory Proxy, match by path prefix
)
/* Basic Auth Related Data structure*/
// Auth credential for basic auth on certain endpoints
type BasicAuthCredentials struct {
Username string
PasswordHash string
}
// Auth credential for basic auth on certain endpoints
type BasicAuthUnhashedCredentials struct {
Username string
Password string
}
// Paths to exclude in basic auth enabled proxy handler
type BasicAuthExceptionRule struct {
PathPrefix string
}
/* Routing Rule Data Structures */
// A Virtual Directory endpoint, provide a subset of ProxyEndpoint for better
// program structure than directly using ProxyEndpoint
type VirtualDirectoryEndpoint struct {
MatchingPath string //Matching prefix of the request path, also act as key
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
Disabled bool //If the rule is enabled
}
// Rules and settings for header rewriting
type HeaderRewriteRules struct {
UserDefinedHeaders []*rewrite.UserDefinedHeader //Custom headers to append when proxying requests from this endpoint
RequestHostOverwrite string //If not empty, this domain will be used to overwrite the Host field in request header
HSTSMaxAge int64 //HSTS max age, set to 0 for disable HSTS headers
EnablePermissionPolicyHeader bool //Enable injection of permission policy header
PermissionPolicy *permissionpolicy.PermissionsPolicy //Permission policy header
DisableHopByHopHeaderRemoval bool //Do not remove hop-by-hop headers
}
/*
Authentication Providers
*/
const (
AuthMethodNone AuthMethod = iota //No authentication required
AuthMethodBasic //Basic Auth
AuthMethodAuthelia //Authelia
AuthMethodOauth2 //Oauth2
AuthMethodAuthentik
)
type AuthenticationProvider struct {
AuthMethod AuthMethod //The authentication method to use
/* Basic Auth Settings */
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint
/* Authelia Settings */
AutheliaURL string //URL of the Authelia server, leave empty to use global settings e.g. authelia.example.com
UseHTTPS bool //Whether to use HTTPS for the Authelia server
}
// A proxy endpoint record, a general interface for handling inbound routing
type ProxyEndpointv321 struct {
ProxyType ProxyType //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
InactiveOrigins []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
UseStickySession bool //Use stick session for load balancing
UseActiveLoadBalance bool //Use active loadbalancing, default passive
Disabled bool //If the rule is disabled
//Inbound TLS/SSL Related
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
HeaderRewriteRules *HeaderRewriteRules
EnableWebsocketCustomHeaders bool //Enable custom headers for websocket connections as well (default only http reqiests)
//Authentication
AuthenticationProvider *AuthenticationProvider
// Rate Limiting
RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second
//Uptime Monitor
DisableUptimeMonitor bool //Disable uptime monitor for this endpoint
//Access Control
AccessFilterUUID string //Access filter ID
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
//Internal Logic Elements
Tags []string // Tags for the proxy endpoint
}

View File

@ -0,0 +1,93 @@
package v322
import "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
/*
Authentication Provider in v3.2.2
The only change is the removal of the deprecated Authelia and Authentik SSO
provider, and the addition of the new Forward Auth provider.
Need to map all provider with ID = 4 into 2 and remove the old provider configs
*/
type AuthMethod int
/*
v3.2.1 Authentication Provider
const (
AuthMethodNone AuthMethod = iota //No authentication required
AuthMethodBasic //Basic Auth
AuthMethodAuthelia //Authelia => 2
AuthMethodOauth2 //Oauth2
AuthMethodAuthentik //Authentik => 4
)
v3.2.2 Authentication Provider
const (
AuthMethodNone AuthMethod = iota //No authentication required
AuthMethodBasic //Basic Auth
AuthMethodForward //Forward => 2
AuthMethodOauth2 //Oauth2
)
We need to merge both Authelia and Authentik into the Forward Auth provider, and remove
*/
//The updated structure of the authentication provider
type AuthenticationProviderV322 struct {
AuthMethod AuthMethod //The authentication method to use
/* Basic Auth Settings */
BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials
BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint
/* Forward Auth Settings */
ForwardAuthURL string // Full URL of the Forward Auth endpoint. Example: https://auth.example.com/api/authz/forward-auth
ForwardAuthResponseHeaders []string // List of headers to copy from the forward auth server response to the request.
ForwardAuthResponseClientHeaders []string // List of headers to copy from the forward auth server response to the client response.
ForwardAuthRequestHeaders []string // List of headers to copy from the original request to the auth server. If empty all are copied.
ForwardAuthRequestExcludedCookies []string // List of cookies to exclude from the request after sending it to the forward auth server.
}
// A proxy endpoint record, a general interface for handling inbound routing
type ProxyEndpointv322 struct {
ProxyType ProxyType //The type of this proxy, see const def
RootOrMatchingDomain string //Matching domain for host, also act as key
MatchingDomainAlias []string //A list of domains that alias to this rule
ActiveOrigins []*loadbalance.Upstream //Activated Upstream or origin servers IP or domain to proxy to
InactiveOrigins []*loadbalance.Upstream //Disabled Upstream or origin servers IP or domain to proxy to
UseStickySession bool //Use stick session for load balancing
UseActiveLoadBalance bool //Use active loadbalancing, default passive
Disabled bool //If the rule is disabled
//Inbound TLS/SSL Related
BypassGlobalTLS bool //Bypass global TLS setting options if TLS Listener enabled (parent.tlsListener != nil)
//Virtual Directories
VirtualDirectories []*VirtualDirectoryEndpoint
//Custom Headers
HeaderRewriteRules *HeaderRewriteRules
EnableWebsocketCustomHeaders bool //Enable custom headers for websocket connections as well (default only http reqiests)
//Authentication
AuthenticationProvider *AuthenticationProviderV322
// Rate Limiting
RequireRateLimit bool
RateLimit int64 // Rate limit in requests per second
//Uptime Monitor
DisableUptimeMonitor bool //Disable uptime monitor for this endpoint
//Access Control
AccessFilterUUID string //Access filter ID
//Fallback routing logic (Special Rule Sets Only)
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
//Internal Logic Elements
Tags []string // Tags for the proxy endpoint
}

191
src/mod/update/v322/v322.go Normal file
View File

@ -0,0 +1,191 @@
package v322
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/update/updateutil"
)
// UpdateFrom321To322 updates proxy config files from v3.2.1 to v3.2.2
func UpdateFrom321To322() error {
// Load the configs
oldConfigFiles, err := filepath.Glob("./conf/proxy/*.config")
if err != nil {
return err
}
// Backup all the files
err = os.MkdirAll("./conf/proxy-321.old/", 0775)
if err != nil {
return err
}
for _, oldConfigFile := range oldConfigFiles {
// Extract the file name from the path
fileName := filepath.Base(oldConfigFile)
// Construct the backup file path
backupFile := filepath.Join("./conf/proxy-321.old/", fileName)
// Copy the file to the backup directory
err := updateutil.CopyFile(oldConfigFile, backupFile)
if err != nil {
return err
}
}
// Read the config into the old struct
for _, oldConfigFile := range oldConfigFiles {
configContent, err := os.ReadFile(oldConfigFile)
if err != nil {
log.Println("Unable to read config file "+filepath.Base(oldConfigFile), err.Error())
continue
}
thisOldConfigStruct := ProxyEndpointv321{}
err = json.Unmarshal(configContent, &thisOldConfigStruct)
if err != nil {
log.Println("Unable to parse file "+filepath.Base(oldConfigFile), err.Error())
continue
}
// Convert the old struct to the new struct
thisNewConfigStruct := convertV321ToV322(thisOldConfigStruct)
// Write the new config to file
newConfigContent, err := json.MarshalIndent(thisNewConfigStruct, "", " ")
if err != nil {
log.Println("Unable to marshal new config "+filepath.Base(oldConfigFile), err.Error())
continue
}
err = os.WriteFile(oldConfigFile, newConfigContent, 0664)
if err != nil {
log.Println("Unable to write new config "+filepath.Base(oldConfigFile), err.Error())
continue
}
}
return nil
}
func convertV321ToV322(thisOldConfigStruct ProxyEndpointv321) ProxyEndpointv322 {
// Merge both Authelia and Authentik into the Forward Auth provider, and remove the old provider configs
if thisOldConfigStruct.AuthenticationProvider == nil {
//Configs before v3.1.7 with no authentication provider
// Set the default authentication provider
thisOldConfigStruct.AuthenticationProvider = &AuthenticationProvider{
AuthMethod: AuthMethodNone, // Default to no authentication
BasicAuthCredentials: []*BasicAuthCredentials{},
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
BasicAuthGroupIDs: []string{},
AutheliaURL: "",
UseHTTPS: false,
}
} else {
//Override the old authentication provider with the new one
if thisOldConfigStruct.AuthenticationProvider.AuthMethod == AuthMethodAuthelia {
thisOldConfigStruct.AuthenticationProvider.AuthMethod = 2
} else if thisOldConfigStruct.AuthenticationProvider.AuthMethod == AuthMethodAuthentik {
thisOldConfigStruct.AuthenticationProvider.AuthMethod = 2
}
}
if thisOldConfigStruct.AuthenticationProvider.BasicAuthGroupIDs == nil {
//Create an empty basic auth group IDs array if it does not exist
thisOldConfigStruct.AuthenticationProvider.BasicAuthGroupIDs = []string{}
}
newAuthenticationProvider := AuthenticationProviderV322{
AuthMethod: AuthMethodNone, // Default to no authentication
//Fill in the empty arrays
BasicAuthCredentials: []*BasicAuthCredentials{},
BasicAuthExceptionRules: []*BasicAuthExceptionRule{},
BasicAuthGroupIDs: []string{},
ForwardAuthURL: "",
ForwardAuthResponseHeaders: []string{},
ForwardAuthResponseClientHeaders: []string{},
ForwardAuthRequestHeaders: []string{},
ForwardAuthRequestExcludedCookies: []string{},
}
// In theory the old config should have a matching itoa value that
// can be converted to the new config
js, err := json.Marshal(thisOldConfigStruct.AuthenticationProvider)
if err != nil {
fmt.Println("Unable to marshal authentication provider "+thisOldConfigStruct.RootOrMatchingDomain, err.Error())
fmt.Println("Using default authentication provider")
}
err = json.Unmarshal(js, &newAuthenticationProvider)
if err != nil {
fmt.Println("Unable to unmarshal authentication provider "+thisOldConfigStruct.RootOrMatchingDomain, err.Error())
fmt.Println("Using default authentication provider")
} else {
fmt.Println("Authentication provider for " + thisOldConfigStruct.RootOrMatchingDomain + " updated")
}
// Fill in any null values in the old config struct
// these are non-upgrader requires values that updates between v3.1.5 to v3.2.1
// will be in null state if not set by the user
if thisOldConfigStruct.VirtualDirectories == nil {
//Create an empty virtual directories array if it does not exist
thisOldConfigStruct.VirtualDirectories = []*VirtualDirectoryEndpoint{}
}
if thisOldConfigStruct.HeaderRewriteRules == nil {
//Create an empty header rewrite rules array if it does not exist
thisOldConfigStruct.HeaderRewriteRules = &HeaderRewriteRules{
UserDefinedHeaders: []*rewrite.UserDefinedHeader{},
RequestHostOverwrite: "",
HSTSMaxAge: 0,
EnablePermissionPolicyHeader: false,
PermissionPolicy: permissionpolicy.GetDefaultPermissionPolicy(),
DisableHopByHopHeaderRemoval: false,
}
}
if thisOldConfigStruct.Tags == nil {
//Create an empty tags array if it does not exist
thisOldConfigStruct.Tags = []string{}
}
if thisOldConfigStruct.MatchingDomainAlias == nil {
//Create an empty matching domain alias array if it does not exist
thisOldConfigStruct.MatchingDomainAlias = []string{}
}
// Update the config struct
thisNewConfigStruct := ProxyEndpointv322{
ProxyType: thisOldConfigStruct.ProxyType,
RootOrMatchingDomain: thisOldConfigStruct.RootOrMatchingDomain,
MatchingDomainAlias: thisOldConfigStruct.MatchingDomainAlias,
ActiveOrigins: thisOldConfigStruct.ActiveOrigins,
InactiveOrigins: thisOldConfigStruct.InactiveOrigins,
UseStickySession: thisOldConfigStruct.UseStickySession,
UseActiveLoadBalance: thisOldConfigStruct.UseActiveLoadBalance,
Disabled: thisOldConfigStruct.Disabled,
BypassGlobalTLS: thisOldConfigStruct.BypassGlobalTLS,
VirtualDirectories: thisOldConfigStruct.VirtualDirectories,
HeaderRewriteRules: thisOldConfigStruct.HeaderRewriteRules,
EnableWebsocketCustomHeaders: thisOldConfigStruct.EnableWebsocketCustomHeaders,
RequireRateLimit: thisOldConfigStruct.RequireRateLimit,
RateLimit: thisOldConfigStruct.RateLimit,
DisableUptimeMonitor: thisOldConfigStruct.DisableUptimeMonitor,
AccessFilterUUID: thisOldConfigStruct.AccessFilterUUID,
DefaultSiteOption: thisOldConfigStruct.DefaultSiteOption,
DefaultSiteValue: thisOldConfigStruct.DefaultSiteValue,
Tags: thisOldConfigStruct.Tags,
}
// Set the new authentication provider
thisNewConfigStruct.AuthenticationProvider = &newAuthenticationProvider
return thisNewConfigStruct
}

View File

@ -115,8 +115,7 @@ func ReverseProxtInit() {
StatisticCollector: statisticCollector,
WebDirectory: *path_webserver,
AccessController: accessController,
AutheliaRouter: autheliaRouter,
AuthentikRouter: authentikRouter,
ForwardAuthRouter: forwardAuthRouter,
LoadBalancer: loadBalancer,
PluginManager: pluginManager,
/* Utilities */
@ -585,11 +584,9 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
if authProviderType == 1 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic
} else if authProviderType == 2 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodForward
} else if authProviderType == 3 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2
} else if authProviderType == 4 {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthentik
} else {
newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone
}

View File

@ -9,13 +9,11 @@ import (
"strings"
"time"
"imuslab.com/zoraxy/mod/auth/sso/authentik"
"github.com/gorilla/csrf"
"imuslab.com/zoraxy/mod/access"
"imuslab.com/zoraxy/mod/acme"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/auth/sso/authelia"
"imuslab.com/zoraxy/mod/auth/sso/forward"
"imuslab.com/zoraxy/mod/database"
"imuslab.com/zoraxy/mod/database/dbinc"
"imuslab.com/zoraxy/mod/dockerux"
@ -143,18 +141,10 @@ func startupSequence() {
}
//Create authentication providers
autheliaRouter = authelia.NewAutheliaRouter(&authelia.AutheliaRouterOptions{
UseHTTPS: false, // Automatic populate in router initiation
AutheliaURL: "", // Automatic populate in router initiation
Logger: SystemWideLogger,
Database: sysdb,
})
authentikRouter = authentik.NewAuthentikRouter(&authentik.AuthentikRouterOptions{
UseHTTPS: false, // Automatic populate in router initiation
AuthentikURL: "", // Automatic populate in router initiation
Logger: SystemWideLogger,
Database: sysdb,
forwardAuthRouter = forward.NewAuthRouter(&forward.AuthRouterOptions{
Address: "",
Logger: SystemWideLogger,
Database: sysdb,
})
//Create a statistic collector
@ -317,21 +307,26 @@ func startupSequence() {
pluginFolder := *path_plugin
pluginFolder = strings.TrimSuffix(pluginFolder, "/")
pluginManager = plugins.NewPluginManager(&plugins.ManagerOptions{
PluginDir: pluginFolder,
SystemConst: &zoraxy_plugin.RuntimeConstantValue{
ZoraxyVersion: SYSTEM_VERSION,
ZoraxyUUID: nodeUUID,
DevelopmentBuild: *development_build,
},
PluginStoreURLs: []string{
"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json",
},
PluginDir: pluginFolder,
Database: sysdb,
Logger: SystemWideLogger,
PluginGroupsConfig: CONF_PLUGIN_GROUPS,
CSRFTokenGen: func(r *http.Request) string {
return csrf.Token(r)
},
SystemConst: &zoraxy_plugin.RuntimeConstantValue{
ZoraxyVersion: SYSTEM_VERSION,
ZoraxyUUID: nodeUUID,
DevelopmentBuild: *development_build,
},
/* Plugin Store URLs */
PluginStoreURLs: []string{
"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json",
//TO BE ADDED
},
/* Developer Options */
EnableHotReload: *development_build, //Default to true if development build
HotReloadInterval: 5, //seconds
})
//Sync latest plugin list from the plugin store

View File

@ -185,9 +185,8 @@
</td>
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Authelia`:``}
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> Oauth2`:``}
${subd.AuthenticationProvider.AuthMethod == 0x4?`<i class="ui blue key icon"></i> Authentik`:``}
${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>`:""}
@ -393,13 +392,7 @@
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}>
<label>Authelia</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="4" name="authProviderType" ${authProvider==0x4?"checked":""}>
<label>Authentik</label>
<label>Forward Auth</label>
</div>
</div>
</div>

View File

@ -185,6 +185,33 @@
</tbody>
</table>
<div class="ui basic segment advanceoptions">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Developer Settings
</div>
<div class="content ui form">
<div class="ui inverted message" style="margin-top: 0.6em;">
<div class="header">Developer Only</div>
<p>These functions are intended for developers only. Enabling them may add latency to plugin loading & routing. Proceed with caution.<br>
<b>Tips: You can start zoraxy with -dev=true to enable auto-reload when start</b></p>
</div>
<div id="enablePluginAutoReload" class="ui toggle notloopbackOnly tlsEnabledOnly checkbox" style="margin-top: 0.6em;">
<input id="enable_plugin_auto_reload" type="checkbox">
<label>Enable Plugin Auto Reload<br>
<small>Automatic reload plugin when the plugin binary changed</small></label>
</div>
<br><br>
<div class="field" style="max-width: 50%;margin-bottom: 0px;">
<label>Check Interval</label>
<input type="number" id="autoreload-interval" placeholder="Check Interval" min="1" max="60" step="1" value="1">
</div>
<small>Specify the interval (in seconds) for checking plugin changes. <br>Minimum is 1 second, maximum is 60 seconds.</small>
</div>
</div>
</div>
<br>
<button class="ui basic violet button" onclick="openPluginStore();"><i class="download icon"></i>Plugin Store (Experimental)</button>
</div>
@ -592,6 +619,95 @@ function uninstallPlugin(pluginId, pluginName, btn=undefined) {
}
}
/* Developer Settings */
function initDeveloperSettings() {
// Fetch the auto reload status
$.get('/api/plugins/developer/enableAutoReload', function(data) {
if (data.error != undefined) {
msgbox(data.error, false);
return;
}
// Set the checkbox for Plugin Auto Reload
if (data == true) {
$("#enablePluginAutoReload").checkbox('set checked');
} else {
$("#enablePluginAutoReload").checkbox('set unchecked');
}
// Fetch the auto reload interval
$.get('/api/plugins/developer/setAutoReloadInterval', function(data) {
if (data.error != undefined) {
msgbox(data.error, false);
return;
}
// Set the input value for Auto Reload Interval
if (data) {
$("#autoreload-interval").val(data);
}
bindEventsToDeveloperSettings();
});
});
}
function bindEventsToDeveloperSettings(){
$("#enablePluginAutoReload").checkbox({
onChecked: function() {
$.cjax({
url: '/api/plugins/developer/enableAutoReload',
type: 'POST',
data: { "enabled": true },
success: function(data) {
if (data.error != undefined) {
msgbox(data.error, false);
} else {
msgbox("Plugin Auto Reload enabled", true);
}
}
});
},
onUnchecked: function() {
$.cjax({
url: '/api/plugins/developer/enableAutoReload',
type: 'POST',
data: { "enabled": false },
success: function(data) {
if (data.error != undefined) {
msgbox(data.error, false);
} else {
msgbox("Plugin Auto Reload disabled", true);
}
}
});
}
});
$("#autoreload-interval").on("change", function() {
const interval = $(this).val();
if (interval < 1 || interval > 60) {
msgbox("Interval must be between 1 and 60 seconds", false);
return;
}
$.cjax({
url: '/api/plugins/developer/setAutoReloadInterval',
type: 'POST',
data: { "interval": interval },
success: function(data) {
if (data.error != undefined) {
msgbox(data.error, false);
} else {
msgbox("Auto Reload Interval updated to " + interval + " seconds", true);
}
}
});
});
}
initDeveloperSettings();
</script>

View File

@ -14,44 +14,59 @@
</div>
<div class="ui divider"></div>
<div class="ui basic segment">
<h3>Authelia</h3>
<p>Configuration settings for Authelia authentication provider.</p>
<h3>Forward Auth</h3>
<p>Configuration settings for the Forward Auth provider.</p>
<p>The Forward Auth provider makes a subrequest to an authorization server that supports Forward Auth, then either:</p>
<ul>
<li>Allows the request to flow through to the backend when the authorization server responds with a 200-299 status code.</li>
<li>Responds with the response from the authorization server.</li>
</ul>
<p>Example authorization servers that support this:</p>
<ul>
<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">
<div class="field">
<label for="autheliaServerUrl">Authelia Server URL</label>
<input type="text" id="autheliaServerUrl" name="autheliaServerUrl" placeholder="Enter Authelia Server URL">
<small>Example: auth.example.com</small>
<label for="forwardAuthAddress">Address</label>
<input type="text" id="forwardAuthAddress" name="forwardAuthAddress" placeholder="Enter Forward Auth Address">
<small>The full remote address or URL of the authorization servers forward auth endpoint. <strong>Example:</strong> https://auth.example.com/authz/forward-auth</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="useHttps" name="useHttps">
<label for="useHttps">Use HTTPS</label>
<small>Check this if your authelia server uses HTTPS</small>
<div class="ui basic segment advanceoptions" style="margin-top:0.6em;">
<div class="ui advancedSSOForwardAuthOptions accordion">
<div class="title">
<i class="dropdown icon"></i>
Advanced Options
</div>
<div class="content">
<div class="field">
<label for="forwardAuthResponseHeaders">Response Headers</label>
<input type="text" id="forwardAuthResponseHeaders" name="forwardAuthResponseHeaders" placeholder="Enter Forward Auth Response Headers">
<small>Comma separated list of case-insensitive headers to copy from the authorization servers response to the request sent to the backend. If not set no headers are copied. <br>
<strong>Example:</strong> <code>Remote-User,Remote-Groups,Remote-Email,Remote-Name</code></small>
</div>
<div class="field">
<label for="forwardAuthResponseClientHeaders">Response Client Headers</label>
<input type="text" id="forwardAuthResponseClientHeaders" name="forwardAuthResponseClientHeaders" placeholder="Enter Forward Auth Response Client Headers">
<small>Comma separated list of case-insensitive headers to copy from the authorization servers response to the response sent to the client. If not set no headers are copied. <br>
<strong>Example:</strong> <code>Set-Cookie,WWW-Authenticate</code></small>
</div>
<div class="field">
<label for="forwardAuthRequestHeaders">Request Headers</label>
<input type="text" id="forwardAuthRequestHeaders" name="forwardAuthRequestHeaders" placeholder="Enter Forward Auth Request Headers">
<small>Comma separated list of case-insensitive headers to copy from the original request to the request made to the authorization server. If not set all headers are copied. <br>
<strong>Example:</strong> <code>Cookie,Authorization</code></small>
</div>
<div class="field">
<label for="forwardAuthRequestExcludedCookies">Request Excluded Cookies</label>
<input type="text" id="forwardAuthRequestExcludedCookies" name="forwardAuthRequestExcludedCookies" placeholder="Enter Forward Auth Request Excluded Cookies">
<small>Comma separated list of case-sensitive cookie names to exclude from the request to the backend. If not set no cookies are excluded. <br>
<strong>Example:</strong> <code>authelia_session,another_session</code></small>
</div>
</div>
</div>
</div>
<button class="ui basic button" onclick="event.preventDefault(); updateAutheliaSettings();"><i class="green check icon"></i> Apply Change</button>
</form>
</div>
<div class="ui divider"></div>
<div class="ui basic segment">
<h3>Authentik</h3>
<p>Configuration settings for Authentik authentication provider.</p>
<form class="ui form">
<div class="field">
<label for="authentikServerUrl">Authentik Server URL</label>
<input type="text" id="authentikServerUrl" name="authentikServerUrl" placeholder="Enter Authentik Server URL">
<small>Example: auth.example.com</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="authentikUseHttps" name="useHttps">
<label for="authentikUseHttps">Use HTTPS</label>
<small>Check this if your Authentik server uses HTTPS</small>
</div>
</div>
<button class="ui basic button" onclick="event.preventDefault(); updateAuthentikSettings();"><i class="green check icon"></i> Apply Change</button>
<button class="ui basic button" onclick="event.preventDefault(); updateForwardAuthSettings();"><i class="green check icon"></i> Apply Change</button>
</form>
</div>
<div class="ui divider"></div>
@ -60,24 +75,15 @@
<script>
$(document).ready(function() {
$.cjax({
url: '/api/sso/Authelia',
url: '/api/sso/forward-auth',
method: 'GET',
dataType: 'json',
success: function(data) {
$('#autheliaServerUrl').val(data.autheliaURL);
$('#useHttps').prop('checked', data.useHTTPS);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown);
}
});
$.cjax({
url: '/api/sso/Authentik',
method: 'GET',
dataType: 'json',
success: function(data) {
$('#authentikServerUrl').val(data.authentikURL);
$('#authentikUseHttps').prop('checked', data.useHTTPS);
$('#forwardAuthAddress').val(data.address);
$('#forwardAuthResponseHeaders').val(data.responseHeaders.join(","));
$('#forwardAuthResponseClientHeaders').val(data.responseClientHeaders.join(","));
$('#forwardAuthRequestHeaders').val(data.requestHeaders.join(","));
$('#forwardAuthRequestExcludedCookies').val(data.requestExcludedCookies.join(","));
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown);
@ -85,51 +91,35 @@
});
});
function updateAutheliaSettings(){
var autheliaServerUrl = $('#autheliaServerUrl').val();
var useHttps = $('#useHttps').prop('checked');
function updateForwardAuthSettings() {
const address = $('#forwardAuthAddress').val();
const responseHeaders = $('#forwardAuthResponseHeaders').val();
const responseClientHeaders = $('#forwardAuthResponseClientHeaders').val();
const requestHeaders = $('#forwardAuthRequestHeaders').val();
const requestExcludedCookies = $('#forwardAuthRequestExcludedCookies').val();
console.log(`Updating Forward Auth settings. Address: ${address}. Response Headers: ${responseHeaders}. Response Client Headers: ${responseClientHeaders}. Request Headers: ${requestHeaders}. Request Excluded Cookies: ${requestExcludedCookies}.`);
$.cjax({
url: '/api/sso/Authelia',
url: '/api/sso/forward-auth',
method: 'POST',
data: {
autheliaURL: autheliaServerUrl,
useHTTPS: useHttps
address: address,
responseHeaders: responseHeaders,
responseClientHeaders: responseClientHeaders,
requestHeaders: requestHeaders,
requestExcludedCookies: requestExcludedCookies
},
success: function(data) {
if (data.error != undefined) {
$.msgbox(data.error, false);
if (data.error !== undefined) {
msgbox(data.error, false);
return;
}
msgbox('Authelia settings updated', true);
console.log('Authelia settings updated:', data);
msgbox('Forward Auth settings updated', true);
console.log('Forward Auth settings updated:', data);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error updating Authelia settings:', textStatus, errorThrown);
}
});
}
function updateAuthentikSettings(){
var authentikServerUrl = $('#authentikServerUrl').val();
var useHttps = $('#authentikUseHttps').prop('checked');
$.cjax({
url: '/api/sso/Authentik',
method: 'POST',
data: {
authentikURL: authentikServerUrl,
useHTTPS: useHttps
},
success: function(data) {
if (data.error != undefined) {
$.msgbox(data.error, false);
return;
}
msgbox('Authentik settings updated', true);
console.log('Authentik settings updated:', data);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error updating Authentik settings:', textStatus, errorThrown);
console.error('Error updating Forward Auth settings:', textStatus, errorThrown);
}
});
}

View File

@ -58,28 +58,28 @@
</div>
<button class="ui basic button" onclick="forceResyncPlugins();"><i class="ui green refresh icon"></i> Update Plugin List</button>
<!-- <div class="ui divider"></div>
<div class="ui basic segment advanceoptions">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<p>Plugin Store URLs</p>
<div class="ui form">
<div class="field">
<textarea id="pluginStoreURLs" rows="5"></textarea>
<label>Enter plugin store URLs, separating each URL with a new line</label>
</div>
<button class="ui basic button" onclick="savePluginStoreURLs()">
<i class="ui green save icon"></i>Save
</button>
<div class="ui basic segment advanceoptions">
<div class="ui accordion advanceSettings">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<p>Plugin Store URLs</p>
<div class="ui form">
<div class="field">
<textarea id="pluginStoreURLs" rows="5"></textarea>
<label>Enter plugin store URLs, separating each URL with a new line</label>
</div>
<button class="ui basic button" onclick="savePluginStoreURLs()">
<i class="ui green save icon"></i>Save
</button>
</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>