mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-08 06:08:30 +02:00
feat: forward auth
This adds basic support for forwarded authentication similar to caddy and traefik. This replaces Authelia SSO as it effectively covers exactly the same use cases.
This commit is contained in:
@@ -1,164 +0,0 @@
|
||||
package authelia
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type AutheliaRouterOptions struct {
|
||||
UseHTTPS bool //If the Authelia server is using HTTPS
|
||||
AutheliaURL string //The URL of the Authelia server
|
||||
Logger *logger.Logger
|
||||
Database *database.Database
|
||||
}
|
||||
|
||||
type AutheliaRouter struct {
|
||||
options *AutheliaRouterOptions
|
||||
}
|
||||
|
||||
// NewAutheliaRouter creates a new AutheliaRouter object
|
||||
func NewAutheliaRouter(options *AutheliaRouterOptions) *AutheliaRouter {
|
||||
options.Database.NewTable("authelia")
|
||||
|
||||
//Read settings from database, if exists
|
||||
options.Database.Read("authelia", "autheliaURL", &options.AutheliaURL)
|
||||
options.Database.Read("authelia", "useHTTPS", &options.UseHTTPS)
|
||||
|
||||
return &AutheliaRouter{
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSetAutheliaURLAndHTTPS is the internal handler for setting the Authelia URL and HTTPS
|
||||
func (ar *AutheliaRouter) HandleSetAutheliaURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
//Return the current settings
|
||||
js, _ := json.Marshal(map[string]interface{}{
|
||||
"useHTTPS": ar.options.UseHTTPS,
|
||||
"autheliaURL": ar.options.AutheliaURL,
|
||||
})
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
} else if r.Method == http.MethodPost {
|
||||
//Update the settings
|
||||
autheliaURL, err := utils.PostPara(r, "autheliaURL")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "autheliaURL not found")
|
||||
return
|
||||
}
|
||||
|
||||
useHTTPS, err := utils.PostBool(r, "useHTTPS")
|
||||
if err != nil {
|
||||
useHTTPS = false
|
||||
}
|
||||
|
||||
//Write changes to runtime
|
||||
ar.options.AutheliaURL = autheliaURL
|
||||
ar.options.UseHTTPS = useHTTPS
|
||||
|
||||
//Write changes to database
|
||||
ar.options.Database.Write("authelia", "autheliaURL", autheliaURL)
|
||||
ar.options.Database.Write("authelia", "useHTTPS", useHTTPS)
|
||||
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// handleAutheliaAuth is the internal handler for Authelia authentication
|
||||
// Set useHTTPS to true if your authelia server is using HTTPS
|
||||
// Set autheliaURL to the URL of the Authelia server, e.g. authelia.example.com
|
||||
func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Request) error {
|
||||
client := &http.Client{}
|
||||
|
||||
if ar.options.AutheliaURL == "" {
|
||||
ar.options.Logger.PrintAndLog("Authelia", "Authelia URL not set", nil)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("500 - Internal Server Error"))
|
||||
return errors.New("authelia URL not set")
|
||||
}
|
||||
protocol := "http"
|
||||
if ar.options.UseHTTPS {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
autheliaURL := &url.URL{
|
||||
Scheme: protocol,
|
||||
Host: ar.options.AutheliaURL,
|
||||
}
|
||||
|
||||
//Make a request to Authelia to verify the request
|
||||
req, err := http.NewRequest("POST", autheliaURL.JoinPath("api", "verify").String(), nil)
|
||||
if err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authelia", "Unable to create request", err)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
originalURL := rOriginalHeaders(r, req)
|
||||
|
||||
// Copy cookies from the incoming request
|
||||
for _, cookie := range r.Cookies() {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
|
||||
// Making the verification request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authelia", "Unable to verify", err)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
redirectURL := autheliaURL.JoinPath()
|
||||
|
||||
query := redirectURL.Query()
|
||||
|
||||
query.Set("rd", originalURL.String())
|
||||
query.Set("rm", r.Method)
|
||||
|
||||
http.Redirect(w, r, redirectURL.String(), http.StatusSeeOther)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rOriginalHeaders(r, req *http.Request) *url.URL {
|
||||
if r.RemoteAddr != "" {
|
||||
before, _, _ := strings.Cut(r.RemoteAddr, ":")
|
||||
|
||||
if ip := net.ParseIP(before); ip != nil {
|
||||
req.Header.Set("X-Forwarded-For", ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
originalURL := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: r.Host,
|
||||
Path: r.URL.Path,
|
||||
RawPath: r.URL.RawPath,
|
||||
}
|
||||
|
||||
if r.TLS != nil {
|
||||
originalURL.Scheme = "https"
|
||||
}
|
||||
|
||||
req.Header.Add("X-Forwarded-Method", r.Method)
|
||||
req.Header.Add("X-Original-URL", originalURL.String())
|
||||
|
||||
return originalURL
|
||||
}
|
@@ -1,169 +0,0 @@
|
||||
package authentik
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type AuthentikRouterOptions struct {
|
||||
UseHTTPS bool //If the Authentik server is using HTTPS
|
||||
AuthentikURL string //The URL of the Authentik server
|
||||
Logger *logger.Logger
|
||||
Database *database.Database
|
||||
}
|
||||
|
||||
type AuthentikRouter struct {
|
||||
options *AuthentikRouterOptions
|
||||
}
|
||||
|
||||
// NewAuthentikRouter creates a new AuthentikRouter object
|
||||
func NewAuthentikRouter(options *AuthentikRouterOptions) *AuthentikRouter {
|
||||
options.Database.NewTable("authentik")
|
||||
|
||||
//Read settings from database, if exists
|
||||
options.Database.Read("authentik", "authentikURL", &options.AuthentikURL)
|
||||
options.Database.Read("authentik", "useHTTPS", &options.UseHTTPS)
|
||||
|
||||
return &AuthentikRouter{
|
||||
options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSetAuthentikURLAndHTTPS is the internal handler for setting the Authentik URL and HTTPS
|
||||
func (ar *AuthentikRouter) HandleSetAuthentikURLAndHTTPS(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
//Return the current settings
|
||||
js, _ := json.Marshal(map[string]interface{}{
|
||||
"useHTTPS": ar.options.UseHTTPS,
|
||||
"authentikURL": ar.options.AuthentikURL,
|
||||
})
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
return
|
||||
} else if r.Method == http.MethodPost {
|
||||
//Update the settings
|
||||
AuthentikURL, err := utils.PostPara(r, "authentikURL")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "authentikURL not found")
|
||||
return
|
||||
}
|
||||
|
||||
useHTTPS, err := utils.PostBool(r, "useHTTPS")
|
||||
if err != nil {
|
||||
useHTTPS = false
|
||||
}
|
||||
|
||||
//Write changes to runtime
|
||||
ar.options.AuthentikURL = AuthentikURL
|
||||
ar.options.UseHTTPS = useHTTPS
|
||||
|
||||
//Write changes to database
|
||||
ar.options.Database.Write("authentik", "authentikURL", AuthentikURL)
|
||||
ar.options.Database.Write("authentik", "useHTTPS", useHTTPS)
|
||||
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// HandleAuthentikAuth is the internal handler for Authentik authentication
|
||||
// Set useHTTPS to true if your Authentik server is using HTTPS
|
||||
// Set AuthentikURL to the URL of the Authentik server, e.g. Authentik.example.com
|
||||
func (ar *AuthentikRouter) HandleAuthentikAuth(w http.ResponseWriter, r *http.Request) error {
|
||||
const outpostPrefix = "outpost.goauthentik.io"
|
||||
client := &http.Client{}
|
||||
|
||||
if ar.options.AuthentikURL == "" {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Authentik URL not set", nil)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("500 - Internal Server Error"))
|
||||
return errors.New("authentik URL not set")
|
||||
}
|
||||
protocol := "http"
|
||||
if ar.options.UseHTTPS {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
authentikBaseURL := protocol + "://" + ar.options.AuthentikURL
|
||||
//Remove tailing slash if any
|
||||
authentikBaseURL = strings.TrimSuffix(authentikBaseURL, "/")
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
reqUrl := scheme + "://" + r.Host + r.RequestURI
|
||||
// Pass request to outpost if path matches outpost prefix
|
||||
if reqPath := strings.TrimPrefix(r.URL.Path, "/"); strings.HasPrefix(reqPath, outpostPrefix) {
|
||||
req, err := http.NewRequest(r.Method, authentikBaseURL+r.RequestURI, r.Body)
|
||||
if err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
req.Header.Set("X-Original-URL", reqUrl)
|
||||
req.Header.Set("Host", r.Host)
|
||||
for _, cookie := range r.Cookies() {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
if resp, err := client.Do(req); err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Unable to pass request to Authentik outpost", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return errors.New("internal server error")
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
for k := range resp.Header {
|
||||
w.Header().Set(k, resp.Header.Get(k))
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
if _, err = io.Copy(w, resp.Body); err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Unable to pass Authentik outpost response to client", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return errors.New("internal server error")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//Make a request to Authentik to verify the request
|
||||
req, err := http.NewRequest(http.MethodGet, authentikBaseURL+"/"+outpostPrefix+"/auth/nginx", nil)
|
||||
if err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Unable to create request", err)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
req.Header.Set("X-Original-URL", reqUrl)
|
||||
|
||||
// Copy cookies from the incoming request
|
||||
for _, cookie := range r.Cookies() {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
|
||||
// Making the verification request
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
ar.options.Logger.PrintAndLog("Authentik", "Unable to verify", err)
|
||||
w.WriteHeader(401)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
redirectURL := authentikBaseURL + "/" + outpostPrefix + "/start?rd=" + url.QueryEscape(scheme+"://"+r.Host+r.URL.String())
|
||||
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
44
src/mod/auth/sso/forward/const.go
Normal file
44
src/mod/auth/sso/forward/const.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package forward
|
||||
|
||||
import "errors"
|
||||
|
||||
const (
|
||||
LogTitle = "Forward Auth"
|
||||
|
||||
DatabaseTable = "auth_sso_forward"
|
||||
|
||||
DatabaseKeyAddress = "address"
|
||||
DatabaseKeyResponseHeaders = "responseHeaders"
|
||||
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,
|
||||
}
|
||||
)
|
294
src/mod/auth/sso/forward/forward.go
Normal file
294
src/mod/auth/sso/forward/forward.go
Normal file
@@ -0,0 +1,294 @@
|
||||
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
|
||||
|
||||
// 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, requestExcludedCookies := "", ""
|
||||
|
||||
options.Database.Read(DatabaseTable, DatabaseKeyResponseHeaders, responseHeaders)
|
||||
options.Database.Read(DatabaseTable, DatabaseKeyRequestExcludedCookies, requestExcludedCookies)
|
||||
|
||||
options.ResponseHeaders = strings.Split(responseHeaders, ",")
|
||||
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,
|
||||
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.
|
||||
responseHeaders, _ := utils.PostPara(r, DatabaseKeyResponseHeaders)
|
||||
requestExcludedCookies, _ := utils.PostPara(r, DatabaseKeyRequestExcludedCookies)
|
||||
|
||||
// Write changes to runtime
|
||||
ar.options.Address = address
|
||||
ar.options.ResponseHeaders = strings.Split(responseHeaders, ",")
|
||||
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, 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.
|
||||
// TODO: Add support for customizing which headers are copied from the request to the forward auth request.
|
||||
headerCopyExcluded(r.Header, req.Header, nil)
|
||||
|
||||
// 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 {
|
||||
// TODO: Add support for copying response headers to the response (in the user agent), not just the request.
|
||||
|
||||
if len(ar.options.ResponseHeaders) != 0 {
|
||||
// If the user has specified a list of cookies to be removed from the request, deterministically remove them.
|
||||
headerCookieRedact(r, ar.options.RequestExcludedCookies)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
if !stringInSliceFold(key, includedHeaders) {
|
||||
continue
|
||||
}
|
||||
|
||||
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,13 @@ 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{},
|
||||
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,10 @@ 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.
|
||||
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
|
||||
|
@@ -59,7 +59,7 @@ type AuthProvider int
|
||||
const (
|
||||
AuthProviderNone AuthProvider = iota
|
||||
AuthProviderBasicAuth
|
||||
AuthProviderAuthelia
|
||||
AuthProviderForward
|
||||
AuthProviderOauth2
|
||||
)
|
||||
|
||||
|
Reference in New Issue
Block a user