mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-08 06:08:30 +02:00
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:
7
src/mod/auth/sso/deprecated/authelia/README.txt
Normal file
7
src/mod/auth/sso/deprecated/authelia/README.txt
Normal 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
|
7
src/mod/auth/sso/deprecated/authentik/README.txt
Normal file
7
src/mod/auth/sso/deprecated/authentik/README.txt
Normal 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
|
46
src/mod/auth/sso/forward/const.go
Normal file
46
src/mod/auth/sso/forward/const.go
Normal 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,
|
||||
}
|
||||
)
|
334
src/mod/auth/sso/forward/forward.go
Normal file
334
src/mod/auth/sso/forward/forward.go
Normal 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)
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
@@ -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{},
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
214
src/mod/plugins/development.go
Normal file
214
src/mod/plugins/development.go
Normal 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)
|
||||
}
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
141
src/mod/update/v322/typedef321.go
Normal file
141
src/mod/update/v322/typedef321.go
Normal 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
|
||||
}
|
93
src/mod/update/v322/typedef322.go
Normal file
93
src/mod/update/v322/typedef322.go
Normal 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
191
src/mod/update/v322/v322.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user