This commit is contained in:
Zeev Diukman 2025-03-03 14:27:20 +00:00
parent 5a6eed8c57
commit 5724939500
17 changed files with 576 additions and 356 deletions

View file

@ -5,16 +5,16 @@ tmp_dir = "tmp"
[build] [build]
args_bin = [] args_bin = []
bin = "./tmp/main" bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/server/." cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/server/"
delay = 1000 delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata","docker"] exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = [] exclude_file = []
exclude_regex = ["_test.go"] exclude_regex = ["_test.go"]
exclude_unchanged = false exclude_unchanged = false
follow_symlink = false follow_symlink = false
full_bin = "" full_bin = ""
include_dir = [] include_dir = []
include_ext = ["go", "tpl", "tmpl", "html","yml","yaml"] include_ext = ["go", "tpl", "tmpl", "html","yml"]
include_file = [] include_file = []
kill_delay = "0s" kill_delay = "0s"
log = "build-errors.log" log = "build-errors.log"

52
.air.toml.bkup Normal file
View file

@ -0,0 +1,52 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/server/."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata","docker"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html","yml","yaml"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

15
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Process",
"type": "go",
"request": "attach",
"mode": "local",
"processId": 0
}
]
}

View file

@ -10,87 +10,85 @@ import (
) )
func CallbackHandler(w http.ResponseWriter, r *http.Request) { func CallbackHandler(w http.ResponseWriter, r *http.Request) {
config.Wrapper(func(c *config.Config) { configData := config.Get()
// ctx := context.Background() // ctx := context.Background()
query := r.URL.Query() query := r.URL.Query()
code := query.Get("code") code := query.Get("code")
state := query.Get("state") state := query.Get("state")
verifier := app.SessionManager.GetString(r.Context(), "code_verifier") verifier := appData.SessionManager.GetString(r.Context(), "code_verifier")
if verifier == "" { if verifier == "" {
http.Error(w, "Code verifier not found in session", http.StatusBadRequest) http.Error(w, "Code verifier not found in session", http.StatusBadRequest)
return return
} }
expectedState := app.SessionManager.GetString(r.Context(), "state") expectedState := appData.SessionManager.GetString(r.Context(), "state")
if state != expectedState { if state != expectedState {
http.Error(w, "Invalid state parameter", http.StatusBadRequest) http.Error(w, "Invalid state parameter", http.StatusBadRequest)
return return
} }
// originalURL, err := decodeState(state) originalURL, err := decodeState(state)
// if err != nil { if err != nil {
// dump.P(err.Error()) dump.P(err.Error())
// http.Error(w, "Invalid state", http.StatusBadRequest) http.Error(w, "Invalid state", http.StatusBadRequest)
// return return
// } }
originalPath := app.SessionManager.GetString(r.Context(), "original_path") dump.P("Original_Path: " + originalURL)
// originalPath := appData.SessionManager.GetString(r.Context(), "original_path")
authName := c.GetAuthNameByDomain(r.Host) authName := configData.GetAuthNameByDomain(r.Host)
token, fullResponse, e := exchangeCode(code, verifier, c, authName) token, fullResponse, e := exchangeCode(code, verifier, authName)
if e != nil { if e != nil {
dump.Println("exchangeCode: " + e.Error()) dump.Println("exchangeCode: " + e.Error())
} }
app.SessionManager.Put(r.Context(), "access_token", token.AccessToken) appData.SessionManager.Put(r.Context(), "access_token", token.AccessToken)
app.SessionManager.Put(r.Context(), "full_token", fullResponse) appData.SessionManager.Put(r.Context(), "full_token", fullResponse)
// SetAuthHeader(w, token.AccessToken) // SetAuthHeader(w, token.AccessToken)
http.Redirect(w, r, originalPath, http.StatusFound) // http.Redirect(w, r, originalPath, http.StatusFound)
// http.Redirect(w, r, originalURL, http.StatusFound) http.Redirect(w, r, originalURL, http.StatusFound)
})
} }
func LogoutHandler(w http.ResponseWriter, r *http.Request) { func LogoutHandler(w http.ResponseWriter, r *http.Request) {
config.Wrapper(func(c *config.Config) { configData := config.Get()
//TODO: only after returninig, delete the session! //TODO: only after returninig, delete the session!
app.SessionManager.Remove(r.Context(), "access_token") appData.SessionManager.Remove(r.Context(), "access_token")
app.SessionManager.Remove(r.Context(), "full_token") appData.SessionManager.Remove(r.Context(), "full_token")
authName := c.DataMaps.DomainToAuth[r.Host] authName := configData.DataMaps.DomainToAuth[r.Host]
a := c.Auth[authName] a := configData.AuthMap[authName]
u := a.OpenID.EndPoints.Logout u := a.OpenID.EndPoints.Logout
http.Redirect(w, r, u, http.StatusFound) http.Redirect(w, r, u, http.StatusFound)
})
} }
func LoginHandler(w http.ResponseWriter, r *http.Request) { func LoginHandler(w http.ResponseWriter, r *http.Request) {
config.Wrapper(func(c *config.Config) { configData := config.Get()
authName := c.DataMaps.DomainToAuth[r.Host] authName := configData.DataMaps.DomainToAuth[r.Host]
// state := helper.RandStringByBits(64) // state := helper.RandStringByBits(64)
nonce := helper.RandStringByBits(64) nonce := helper.RandStringByBits(64)
authURL, _ := url.Parse(c.Auth[authName].OpenID.EndPoints.Auth) authURL, _ := url.Parse(configData.AuthMap[authName].OpenID.EndPoints.Auth)
query := authURL.Query() query := authURL.Query()
codeVerifier, _ := generateCodeVerifier() codeVerifier, _ := generateCodeVerifier()
codeChallenge := generateCodeChallenge(codeVerifier) codeChallenge := generateCodeChallenge(codeVerifier)
originalPath := app.SessionManager.GetString(r.Context(), "original_path") originalPath := appData.SessionManager.GetString(r.Context(), "original_path")
state := generateState(url.QueryEscape(originalPath)) state := generateState(url.QueryEscape(originalPath))
query.Set("client_id", c.Auth[authName].OpenID.ClientID) query.Set("client_id", configData.AuthMap[authName].OpenID.ClientID)
query.Set("response_type", "code") query.Set("response_type", "code")
query.Set("scope", "openid") query.Set("scope", "openid")
query.Set("redirect_uri", c.Auth[authName].OpenID.RedirectURI) query.Set("redirect_uri", configData.AuthMap[authName].OpenID.RedirectURI)
query.Set("code_challenge", codeChallenge) query.Set("code_challenge", codeChallenge)
query.Set("code_challenge_method", "S256") query.Set("code_challenge_method", "S256")
query.Set("state", state) query.Set("state", state)
query.Set("nonce", nonce) query.Set("nonce", nonce)
authURL.RawQuery = query.Encode() authURL.RawQuery = query.Encode()
app.SessionManager.Put(r.Context(), "state", state) appData.SessionManager.Put(r.Context(), "state", state)
app.SessionManager.Put(r.Context(), "code_verifier", codeVerifier) appData.SessionManager.Put(r.Context(), "code_verifier", codeVerifier)
http.Redirect(w, r, authURL.String(), http.StatusFound) http.Redirect(w, r, authURL.String(), http.StatusFound)
})
} }

View file

@ -36,39 +36,46 @@ type EntryPoint struct {
type ReverseProxies map[string]ReverseProxy type ReverseProxies map[string]ReverseProxy
type ReverseProxy *httputil.ReverseProxy type ReverseProxy *httputil.ReverseProxy
var app = logic.NewApp() var (
appData *logic.App = logic.NewApp()
// configData *config.Config
)
func main() { func main() {
helper.AppRunner(func() { helper.AppRunner(func() {
config.Wrapper(func(c *config.Config) { configData := config.Get()
//////////TESTING AREA//////////////////
////////////////////////////////////////
groups := logic.NewGroups() groups := logic.NewGroups()
mainRouter := router.New() mainRouter := router.New()
groups.ForEach(func(k string, g *logic.Group) { groups.ForEach(func(groupName string, g *logic.Group) {
groupSubRouter := mainRouter.Mux.NewRoute().Subrouter() mainRouter.AddGroupRouter(groupName)
groupSubRouter.Use(Domain_Middleware) mainRouter.GetGroupRouter(groupName).Mux.Use(Domain_Middleware)
for k := range g.ReverseProxies { for k := range g.ReverseProxies {
rpConfig := c.ReverseProxies[k] rpConfig := configData.ReverseProxies[k]
domain := rpConfig.Domain domain := rpConfig.Domain
proxy := reverse_proxy.New(rpConfig.Host) proxy := reverse_proxy.New(rpConfig.Host)
proxy.Name = domain proxy.Name = domain
newRoute := groupSubRouter.NewRoute() mainRouter.AddHostSubRouter(groupName, domain)
subRouter := newRoute.Host(domain).Subrouter()
if rpConfig.Auth != "" { if rpConfig.Auth != "" {
if _, ok := c.Auth[rpConfig.Auth]; !ok { if _, ok := configData.AuthMap[rpConfig.Auth]; !ok {
err := errors.New("Error: Auth " + rpConfig.Auth + " not exist!") err := errors.New("Error: Auth " + rpConfig.Auth + " not exist!")
panic(err.Error()) panic(err.Error())
} }
pths := c.Auth[rpConfig.Auth].Paths pths := configData.AuthMap[rpConfig.Auth].Paths
authRoute := subRouter.NewRoute() hostRouterMux := mainRouter.GetHostSubRouter(groupName, domain).Mux
subRouter.Use(Middleware_SetHeaders) hostRouterMux.Use(Middleware_SetHeaders)
hostRouterMux.Use(authMiddleware)
authRoute := hostRouterMux.NewRoute()
authSubRouter := authRoute.PathPrefix(pths.Prefix).Subrouter() authSubRouter := authRoute.PathPrefix(pths.Prefix).Subrouter()
authSubRouter.Path(pths.Login).Handler(http.HandlerFunc(LoginHandler)) authSubRouter.Path(pths.Login).Handler(http.HandlerFunc(LoginHandler))
authSubRouter.Path(pths.Logout).Handler(http.HandlerFunc(LogoutHandler)) authSubRouter.Path(pths.Logout).Handler(http.HandlerFunc(LogoutHandler))
authSubRouter.Path(pths.Callback).Handler(http.HandlerFunc(CallbackHandler)) authSubRouter.Path(pths.Callback).Handler(http.HandlerFunc(CallbackHandler))
subRouter.Use(authMiddleware)
} }
subRouter.PathPrefix("/").Handler(proxy.Httputil) mainRouter.GetHostSubRouter(groupName, domain).Mux.PathPrefix("/").Handler(proxy.Httputil)
// Filter out static file requests first // Filter out static file requests first
} }
@ -79,11 +86,11 @@ func main() {
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
// crt, key := "", "" // crt, key := "", ""
crt, key := c.GetCertsPairByDomain(info.ServerName) crt, key := configData.GetCertsPairByDomain(info.ServerName)
if crt == "" && key == "" { if crt == "" && key == "" {
// crt = c.TLS.Certs["default"].Cert // crt = configData.TLS.Certs["default"].Cert
// key = c.TLS.Certs["default"].Key // key = configData.TLS.Certs["default"].Key
// panic("Error: TLS cert and key not found!") // panic("Error: TLS cert and key not found!")
} }
@ -96,7 +103,7 @@ func main() {
} }
server := &http.Server{ server := &http.Server{
Addr: ":" + g.Port, Addr: ":" + g.Port,
Handler: app.SessionManager.LoadAndSave(groupSubRouter), Handler: appData.SessionManager.LoadAndSave(mainRouter.GetGroupRouter(groupName).Mux),
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
} }
var err error var err error
@ -115,8 +122,8 @@ func main() {
} }
}) })
helper.StartTestHTTPServer(3000) helper.StartTestHTTPServer(3000, "app")
}) helper.StartTestHTTPServer(3001, "oded")
}) })
} }
@ -130,18 +137,19 @@ func main() {
func authMiddleware(next http.Handler) http.Handler { func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
configData := config.Get()
config.Wrapper(func(c *config.Config) {
currentPath := r.URL.Path currentPath := r.URL.Path
authName := c.GetAuthNameByDomain(r.Host) authName := configData.GetAuthNameByDomain(r.Host)
loginPath := c.Auth[authName].Paths.Prefix + c.Auth[authName].Paths.Login loginPath := configData.AuthMap[authName].Paths.Prefix + configData.AuthMap[authName].Paths.Login
logoutPath := c.Auth[authName].Paths.Prefix + c.Auth[authName].Paths.Logout logoutPath := configData.AuthMap[authName].Paths.Prefix + configData.AuthMap[authName].Paths.Logout
callbackPath := c.Auth[authName].Paths.Prefix + c.Auth[authName].Paths.Callback callbackPath := configData.AuthMap[authName].Paths.Prefix + configData.AuthMap[authName].Paths.Callback
// TODO: mark auth reverse proxy in yaml // TODO: mark auth reverse proxy in yaml
proxyName := configData.GetProxyNameByDomain(r.Host)
if r.Host == "keycloak.z.com" { if configData.ReverseProxies[proxyName].IsAuthServer {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return
} }
switch currentPath { switch currentPath {
case loginPath: case loginPath:
{ {
@ -163,14 +171,15 @@ func authMiddleware(next http.Handler) http.Handler {
default: default:
{ {
accessToken := app.SessionManager.GetString(r.Context(), "access_token") accessToken := appData.SessionManager.GetString(r.Context(), "access_token")
if accessToken == "" { if accessToken == "" {
authName := c.DataMaps.DomainToAuth[r.Host]
http.Redirect(w, r, c.Auth[authName].Paths.Prefix+c.Auth[authName].Paths.Login, http.StatusFound) authName := configData.DataMaps.DomainToAuth[r.Host]
http.Redirect(w, r, configData.AuthMap[authName].Paths.Prefix+configData.AuthMap[authName].Paths.Login, http.StatusFound)
return return
} }
// auth.SetAuthHeader(w, accessToken) // auth.SetAuthHeader(w, accessToken)
a := c.Auth[authName] a := configData.AuthMap[authName]
pths := a.Paths pths := a.Paths
prefix := pths.Prefix prefix := pths.Prefix
login := pths.Login login := pths.Login
@ -182,7 +191,8 @@ func authMiddleware(next http.Handler) http.Handler {
// return // return
} }
tokenOk := IsAuthorizedJWT(accessToken, c, "default") tokenOk := IsAuthorizedJWT(accessToken, authName)
if !tokenOk { if !tokenOk {
http.Redirect(w, r, loginPath, http.StatusFound) http.Redirect(w, r, loginPath, http.StatusFound)
return return
@ -191,7 +201,6 @@ func authMiddleware(next http.Handler) http.Handler {
} }
} }
}) })
})
} }
@ -200,8 +209,8 @@ func Domain_Middleware(next http.Handler) http.Handler {
// c := config.Get() // c := config.Get()
// requestedPath := r.URL.Path // requestedPath := r.URL.Path
// authName := c.GetAuthNameByDomain(r.Host) // authName := configData.GetAuthNameByDomain(r.Host)
// auth := c.Auth[authName] // auth := configData.AuthMap[authName]
// excludedPaths := []string{ // excludedPaths := []string{
// auth.Paths.Prefix + auth.Paths.Login, // auth.Paths.Prefix + auth.Paths.Login,
// auth.Paths.Prefix + auth.Paths.Callback, // auth.Paths.Prefix + auth.Paths.Callback,
@ -210,7 +219,7 @@ func Domain_Middleware(next http.Handler) http.Handler {
// contains := helper.IsSliceContains(excludedPaths, requestedPath) // contains := helper.IsSliceContains(excludedPaths, requestedPath)
// contains := slices.Contains(excludedPaths, requestedPath) // contains := slices.Contains(excludedPaths, requestedPath)
// if !contains { // if !contains {
// app.SessionManager.Put(r.Context(), "original_path", requestedPath) // appData.SessionManager.Put(r.Context(), "original_path", requestedPath)
// } // }
// dump.P(requestedPath) // dump.P(requestedPath)
@ -303,12 +312,22 @@ func decodeState(encodedState string) (string, error) {
} }
return stateData["redirect_uri"], nil return stateData["redirect_uri"], nil
} }
func exchangeCode(code string, verifier string, c *config.Config, authName string) (*TokenResponse, string, error) {
func exchangeCode(code string, verifier string, authName string) (*TokenResponse, string, error) {
configData := config.Get()
clientID := configData.AuthMap[authName].OpenID.ClientID
clientSecert := configData.AuthMap[authName].OpenID.ClientSecert
redirectURI := configData.AuthMap[authName].OpenID.RedirectURI
// if strings.Contains(redirectURI,"<{{dynamic}}>") {
// configData.GenerateDynamicRedirectUri()
// }
// GenerateDynamicRedirectUri
data := url.Values{} data := url.Values{}
data.Set("grant_type", "authorization_code") data.Set("grant_type", "authorization_code")
data.Set("client_id", c.Auth[authName].OpenID.ClientID) data.Set("client_id", clientID)
data.Set("client_secret", c.Auth[authName].OpenID.ClientSecert) data.Set("client_secret", clientSecert)
data.Set("redirect_uri", c.Auth[authName].OpenID.RedirectURI) data.Set("redirect_uri", redirectURI)
data.Set("code", code) data.Set("code", code)
data.Set("scope", "openid zapp") data.Set("scope", "openid zapp")
if verifier != "" { if verifier != "" {
@ -318,7 +337,7 @@ func exchangeCode(code string, verifier string, c *config.Config, authName strin
TLSClientConfig: &tls.Config{InsecureSkipVerify: DEVELOPMENT}, TLSClientConfig: &tls.Config{InsecureSkipVerify: DEVELOPMENT},
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
u := c.Auth[authName].OpenID.EndPoints.Token u := configData.AuthMap[authName].OpenID.EndPoints.Token
r, _ := http.NewRequest(http.MethodPost, u, strings.NewReader(data.Encode())) r, _ := http.NewRequest(http.MethodPost, u, strings.NewReader(data.Encode()))
r.Header.Add("Content-Type", "application/x-www-form-urlencoded") r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(r) resp, err := client.Do(r)
@ -358,7 +377,9 @@ func generateCodeChallenge(verifier string) string {
return base64.RawURLEncoding.EncodeToString(hash[:]) return base64.RawURLEncoding.EncodeToString(hash[:])
} }
func IsAuthorizedJWT(rawAccessToken string, c *config.Config, authName string) bool { func IsAuthorizedJWT(rawAccessToken string, authName string) bool {
configData := config.Get()
tr := &http.Transport{ tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
} }
@ -367,14 +388,14 @@ func IsAuthorizedJWT(rawAccessToken string, c *config.Config, authName string) b
Transport: tr, Transport: tr,
} }
ctx := oidc.ClientContext(context.Background(), client) ctx := oidc.ClientContext(context.Background(), client)
provider, err := oidc.NewProvider(ctx, c.Auth[authName].OpenID.EndPoints.Issuer) provider, err := oidc.NewProvider(ctx, configData.AuthMap[authName].OpenID.EndPoints.Issuer)
if err != nil { if err != nil {
dump.Println("authorisation failed while getting the provider: " + err.Error()) dump.Println("authorisation failed while getting the provider: " + err.Error())
return false return false
} }
oidcConfig := &oidc.Config{ oidcConfig := &oidc.Config{
ClientID: c.Auth[authName].OpenID.ClientID, ClientID: configData.AuthMap[authName].OpenID.ClientID,
} }
verifier := provider.Verifier(oidcConfig) verifier := provider.Verifier(oidcConfig)
idToken, err := verifier.Verify(ctx, rawAccessToken) idToken, err := verifier.Verify(ctx, rawAccessToken)
@ -394,22 +415,22 @@ func IsAuthorizedJWT(rawAccessToken string, c *config.Config, authName string) b
/////////////////////////////////// ///////////////////////////////////
/////////////////////////////////// ///////////////////////////////////
func isStaticFileRequest(path string) bool { // func isStaticFileRequest(path string) bool {
// Check for common static file prefixes // // Check for common static file prefixes
if strings.HasPrefix(path, "/static/") || strings.HasPrefix(path, "/assets/") { // if strings.HasPrefix(path, "/static/") || strings.HasPrefix(path, "/assets/") {
return true // return true
} // }
// Check for common static file extensions // // Check for common static file extensions
staticExtensions := []string{ // staticExtensions := []string{
".css", ".js", ".jpg", ".jpeg", ".png", ".gif", ".svg", ".ico", // ".css", ".js", ".jpg", ".jpeg", ".png", ".gif", ".svg", ".ico",
} // }
for _, ext := range staticExtensions { // for _, ext := range staticExtensions {
if strings.HasSuffix(path, ext) { // if strings.HasSuffix(path, ext) {
return true // return true
} // }
} // }
return false // return false
} // }

View file

@ -8,7 +8,6 @@ reverse_proxies:
enabled: true enabled: true
certs: default certs: default
auth_server: true auth_server: true
app: app:
domain: app.z.com domain: app.z.com
host: http://127.0.0.1:3000 host: http://127.0.0.1:3000
@ -16,7 +15,16 @@ reverse_proxies:
tls: tls:
enabled: true enabled: true
certs: default certs: default
auth: default auth: app_auth
oded:
domain: oded.z.com
host: http://127.0.0.1:3001
entry_point: https
tls:
enabled: true
certs: default
auth: oded_auth
tls: tls:
certs: certs:
@ -32,7 +40,7 @@ entry_points:
port: 80 port: 80
auth: auth:
default: app_auth:
paths: paths:
prefix: /auth prefix: /auth
login: /login login: /login
@ -43,19 +51,25 @@ auth:
realm: dev realm: dev
client_id: dev_client client_id: dev_client
client_secret: dWhSJgARBAuBAXN7sUTpqpIq2sKQdugs client_secret: dWhSJgARBAuBAXN7sUTpqpIq2sKQdugs
redirect_uri: https://app.z.com/auth/callback redirect_uri: <{{dynamic}}>/auth/callback
post_logout_redirect_uri: https://app.z.com/auth/logout post_logout_redirect_uri: https://app.z.com/auth/logout
config_path: /realms/{{realm}}/.well-known/openid-configuration config_path: /realms/<{{realm}}>/.well-known/openid-configuration
# config_fields: oded_auth:
# - issuer paths:
# - authorization_endpoint prefix: /auth
# - token_endpoint login: /login
# - introspection_endpoint logout: /logout
# - userinfo_endpoint callback: /callback
# - end_session_endpoint open_id:
# - jwks_uri host: http://127.0.0.1:8080
# issuer: http://127.0.0.1:8080/realms/dev realm: dev
client_id: dev_client
client_secret: dWhSJgARBAuBAXN7sUTpqpIq2sKQdugs
# redirect_uri: https://oded.z.com/auth/callback
redirect_uri: <{{dynamic}}>/auth/callback
post_logout_redirect_uri: https://oded.z.com/auth/logout
# post_logout_redirect_uri: <{{dynamic}}>/auth/logout
config_path: /realms/<{{realm}}>/.well-known/openid-configuration
# scope: openid profile email # scope: openid profile email

View file

@ -15,20 +15,28 @@ const Viper_File_Name string = "config"
const Viper_File_Type string = "yaml" const Viper_File_Type string = "yaml"
const Viper_File_Path string = "." const Viper_File_Path string = "."
var c *Config var (
configData *Config
)
func init() { func init() {
helper.New().Screen.Clear() h := helper.New()
c = createConfig() h.Screen.Clear()
configData = createConfig()
} }
func Wrapper(fn func(c *Config)) { func Wrapper(fn func(c *Config)) {
fn(c) fn(configData)
} }
func Get() *Config { func Get() *Config {
return c
return configData
}
func Get2() *Config {
return configData
} }
func createConfig() *Config { func createConfig() *Config {
c := &Config{} c := &Config{}
c.initViper() c.initViper()
c.setViperOptions(Viper_File_Name, Viper_File_Type, Viper_File_Path) c.setViperOptions(Viper_File_Name, Viper_File_Type, Viper_File_Path)
@ -76,6 +84,19 @@ func (rp *ReverseProxies) ForEach(fn func(rpName string, rpConfig *ReverseProxy)
} }
} }
func (rp *ReverseProxies) ForEachReturn(fn func(rpName string, rpConfig *ReverseProxy) (res ForEachReturn)) *ForEachReturn {
res := ForEachReturn{}
for rpName, rpData := range *rp {
res = append(fn(rpName, &rpData), res)
}
return &res
}
// func (rp AuthMap) ForEach(fn func(authName string, authConfig Auth)) {
// for authName, authConfig := range rp {
// fn(authName, authConfig)
// }
// }
func (c *Config) fetchEndPointsByAuthName(authName string) map[string]any { func (c *Config) fetchEndPointsByAuthName(authName string) map[string]any {
configURL := c.KeycloakWellknownURL(authName) configURL := c.KeycloakWellknownURL(authName)
resp := helper.IsFetchOK[EndPoints](configURL, "", http.Get) resp := helper.IsFetchOK[EndPoints](configURL, "", http.Get)
@ -89,10 +110,10 @@ func (c *Config) fetchEndPointsByAuthName(authName string) map[string]any {
} }
func (c *Config) initOpenIDEndPoints() { func (c *Config) initOpenIDEndPoints() {
for authName := range c.Auth { for authName := range c.AuthMap {
data := c.fetchEndPointsByAuthName(authName) data := c.fetchEndPointsByAuthName(authName)
c.Auth[authName].OpenID.EndPoints = &EndPoints{ c.AuthMap[authName].OpenID.EndPoints = &EndPoints{
Issuer: data["issuer"].(string), Issuer: data["issuer"].(string),
Auth: data["authorization_endpoint"].(string), Auth: data["authorization_endpoint"].(string),
Introspection: data["introspection_endpoint"].(string), Introspection: data["introspection_endpoint"].(string),
@ -107,17 +128,35 @@ func (c *Config) initOpenIDEndPoints() {
} }
func (c *Config) KeycloakWellknownURL(authName string) string { func (c *Config) KeycloakWellknownURL(authName string) string {
if _, ok := c.Auth[authName]; !ok { if _, ok := c.AuthMap[authName]; !ok {
return "" return ""
} }
hostUrl := c.Auth[authName].OpenID.Host hostUrl := c.AuthMap[authName].OpenID.Host
realm := c.Auth[authName].OpenID.Realm realm := c.AuthMap[authName].OpenID.Realm
configPath := c.Auth[authName].OpenID.ConfigPath configPath := c.AuthMap[authName].OpenID.ConfigPath
u := hostUrl u := hostUrl
u += strings.ReplaceAll(configPath, "{{realm}}", realm) u += strings.ReplaceAll(configPath, "<{{realm}}>", realm)
return u return u
} }
func (c *Config) GenerateDynamicRedirectUri(proxyName string, authName string) string {
if _, ok := c.AuthMap[authName]; !ok {
return ""
}
proto := "http"
domain := c.ReverseProxies[proxyName].Domain
tls := c.ReverseProxies[proxyName].TLS.Enabled
if tls {
proto = "https"
}
redirectURI := c.AuthMap[authName].OpenID.RedirectURI
u := redirectURI
u = strings.ReplaceAll(redirectURI, "<{{dynamic}}>", proto+"://"+domain)
return u
}
func (c *Config) initDomainToProxyNameMap() { func (c *Config) initDomainToProxyNameMap() {
mp := make(map[string]string) mp := make(map[string]string)
c.ReverseProxies.ForEach(func(rpName string, rpConfig *ReverseProxy) { c.ReverseProxies.ForEach(func(rpName string, rpConfig *ReverseProxy) {
@ -126,17 +165,22 @@ func (c *Config) initDomainToProxyNameMap() {
c.DataMaps.DomainToProxy = mp c.DataMaps.DomainToProxy = mp
} }
func (c *Config) initDomainToProxyAuthName() { func (c *Config) initDomainToProxyAuthName() {
authMap := map[string]int{} authMap := map[string]int{}
for authName := range c.Auth { for authName := range c.AuthMap {
authMap[authName] = 1 authMap[authName] = 1
} }
mp := make(map[string]string) mp := make(map[string]string)
c.ReverseProxies.ForEach(func(rpName string, rpConfig *ReverseProxy) { c.ReverseProxies.ForEach(func(rpName string, rpConfig *ReverseProxy) {
if v, ok := authMap[rpConfig.Auth]; ok && v == 1 { if v, ok := authMap[rpConfig.Auth]; ok && v == 1 {
mp[rpConfig.Domain] = rpConfig.Auth mp[rpConfig.Domain] = rpConfig.Auth
} }
}) })
c.DataMaps.DomainToAuth = mp c.DataMaps.DomainToAuth = mp
} }
func (c *Config) initDomainToCertName() { func (c *Config) initDomainToCertName() {
crtMap := map[string]int{} crtMap := map[string]int{}
@ -155,6 +199,7 @@ func (c *Config) initDomainToCertName() {
func (c *Config) GetAuthNameByDomain(domain string) string { func (c *Config) GetAuthNameByDomain(domain string) string {
return c.DataMaps.DomainToAuth[domain] return c.DataMaps.DomainToAuth[domain]
} }
func (c *Config) GetProxyNameByDomain(domain string) string { func (c *Config) GetProxyNameByDomain(domain string) string {
return c.DataMaps.DomainToProxy[domain] return c.DataMaps.DomainToProxy[domain]
} }
@ -174,5 +219,25 @@ func (c *Config) GetCertsPairByDomain(domain string) (string, string) {
} }
return crt, key return crt, key
}
func (c *Config) GetProxiesSliceByAuthName(authName string) []string {
slc := &[]string{}
c.ReverseProxies.ForEach(func(rpName string, rpConfig *ReverseProxy) {
isOK := true
if authName != rpConfig.Auth {
isOK = false
}
if _, ok := c.AuthMap[authName]; !ok {
isOK = false
}
if isOK {
helper.AppendToPointer(slc, rpName)
}
})
return *slc
}
func (c *Config) GetAuthNameByProxyName(proxyName string) string {
return c.ReverseProxies[proxyName].Auth
} }

View file

@ -1,93 +0,0 @@
package config
import "github.com/spf13/viper"
type Config struct {
Viper *viper.Viper
DataMaps DataMaps
ReverseProxies ReverseProxies `mapstructure:"reverse_proxies"`
TLS TLS `mapstructure:"tls"`
EntryPoints EntryPoints `mapstructure:"entry_points"`
Auth map[string]*AuthInstance `mapstructure:"auth"`
}
type DataMaps struct {
DomainToProxy map[string]string
DomainToAuth map[string]string
DomainToCert map[string]string
}
// AUTH
// type Auth map[string]AuthInstance
type AuthInstance struct {
Paths Paths `mapstructure:"paths"`
OpenID OpenID `mapstructure:"open_id"`
}
type Paths struct {
Prefix string `mapstructure:"prefix"`
Login string `mapstructure:"login"`
Logout string `mapstructure:"logout"`
Callback string `mapstructure:"callback"`
}
type OpenID struct {
Host string `mapstructure:"host"`
Realm string `mapstructure:"realm"`
ClientID string `mapstructure:"client_id"`
ClientSecert string `mapstructure:"client_secret"`
RedirectURI string `mapstructure:"redirect_uri"`
PostLogoutRedirectURI string `mapstructure:"post_logout_redirect_uri"`
ConfigPath string `mapstructure:"config_path"`
EndPoints *EndPoints
}
type EndPoints struct {
Issuer string
Auth string
Introspection string
Token string
UserInfo string
Logout string
JwksUri string
}
// type OpenIdEndPoints struct {
// Issuer string
// Authorization string
// Token string
// Introspection string
// UserInfo string
// EndSession string
// }
// ReverseProxies
type ReverseProxies map[string]ReverseProxy
type ReverseProxy struct {
Domain string `mapstructure:"domain"`
Host string `mapstructure:"host"`
EntryPoint string `mapstructure:"entry_point"`
TLS TLS_RP `mapstructure:"tls"`
Auth string `mapstructure:"auth"`
}
type TLS_RP struct {
Enabled bool `mapstructure:"enabled"`
Certs string `mapstructure:"certs"`
}
// TLS
type TLS struct {
Certs map[string]Certs `mapstructure:"certs"`
}
type Certs struct {
Cert string `mapstructure:"cert"`
Key string `mapstructure:"key"`
}
// EntryPoints
type EntryPoints map[string]EntryPoint
type EntryPoint struct {
Port string `mapstructure:"port"`
TLS bool `mapstructure:"tls"`
}

View file

@ -0,0 +1,36 @@
package config
type AuthMap map[string]*Auth
type Auth struct {
Paths Paths `mapstructure:"paths"`
OpenID OpenID `mapstructure:"open_id"`
}
type Paths struct {
Prefix string `mapstructure:"prefix"`
Login string `mapstructure:"login"`
Logout string `mapstructure:"logout"`
Callback string `mapstructure:"callback"`
}
type OpenID struct {
Host string `mapstructure:"host"`
Realm string `mapstructure:"realm"`
ClientID string `mapstructure:"client_id"`
ClientSecert string `mapstructure:"client_secret"`
RedirectURI string `mapstructure:"redirect_uri"`
PostLogoutRedirectURI string `mapstructure:"post_logout_redirect_uri"`
ConfigPath string `mapstructure:"config_path"`
EndPoints *EndPoints
}
type EndPoints struct {
Issuer string
Auth string
Introspection string
Token string
UserInfo string
Logout string
JwksUri string
}

View file

@ -0,0 +1,20 @@
package config
import "github.com/spf13/viper"
type Config struct {
Viper *viper.Viper
DataMaps DataMaps
ReverseProxies ReverseProxies `mapstructure:"reverse_proxies"`
TLS TLS `mapstructure:"tls"`
EntryPoints EntryPoints `mapstructure:"entry_points"`
AuthMap AuthMap `mapstructure:"auth"`
}
type DataMaps struct {
DomainToProxy map[string]string
DomainToAuth map[string]string
DomainToCert map[string]string
}
type ForEachReturn []any

View file

@ -0,0 +1,8 @@
package config
type EntryPoints map[string]EntryPoint
type EntryPoint struct {
Port string `mapstructure:"port"`
TLS bool `mapstructure:"tls"`
}

View file

@ -0,0 +1,12 @@
package config
type ReverseProxies map[string]ReverseProxy
type ReverseProxy struct {
Domain string `mapstructure:"domain"`
Host string `mapstructure:"host"`
EntryPoint string `mapstructure:"entry_point"`
TLS TLS_RP `mapstructure:"tls"`
Auth string `mapstructure:"auth"`
IsAuthServer bool `mapstructure:"auth_server"`
}

View file

@ -0,0 +1,15 @@
package config
type TLS struct {
Certs map[string]Certs `mapstructure:"certs"`
}
type Certs struct {
Cert string `mapstructure:"cert"`
Key string `mapstructure:"key"`
}
type TLS_RP struct {
Enabled bool `mapstructure:"enabled"`
Certs string `mapstructure:"certs"`
}

View file

@ -1,24 +1,77 @@
package router package router
import "github.com/gorilla/mux" import (
"github.com/gorilla/mux"
"zeevdiukman.com/zprox/pkg/helper"
)
func New() *MainRouter { func New() *MainRouter {
r := &MainRouter{} r := &MainRouter{}
r.SetMux() r.setMux()
return r return r
} }
func (r *MainRouter) SetMux() { func (r *MainRouter) setMux() {
r.Mux = mux.NewRouter() r.Mux = mux.NewRouter()
} }
func (mainRouter *MainRouter) AddGroupRouter(groupName string) *GroupRouter {
s := mainRouter.Mux.NewRoute().Subrouter()
groupRouter := &GroupRouter{
Mux: s,
}
newMap := make(GroupRouters)
helper.MapIter(mainRouter.GroupRouters, func(grName string, gr *GroupRouter) {
newMap[grName] = gr
})
newMap[groupName] = groupRouter
mainRouter.GroupRouters = newMap
return groupRouter
}
func (mainRouter *MainRouter) GetGroupRouter(groupName string) *GroupRouter {
return mainRouter.GroupRouters[groupName]
}
func (mainRouter *MainRouter) GetHostSubRouter(groupName string, domain string) *SubRouter {
return mainRouter.GroupRouters[groupName].SubRouters[domain]
}
func (mainRouter *MainRouter) AddHostSubRouter(groupName string, domain string) *SubRouter {
groupSubRouter := mainRouter.GroupRouters[groupName]
newRoute := groupSubRouter.Mux.NewRoute()
hostSubRouter := &SubRouter{
Mux: newRoute.Host(domain).Subrouter(),
}
newMap := make(SubRouters)
helper.MapIter(mainRouter.GroupRouters[groupName].SubRouters, func(grName string, gr *SubRouter) {
newMap[grName] = gr
})
newMap[domain] = hostSubRouter
mainRouter.GroupRouters[groupName].SubRouters = newMap
return hostSubRouter
}
func (mainRouter *MainRouter) AddGroupSubRouter(groupName string, subRouterName string) *SubRouter {
gsr := mainRouter.GetGroupRouter(groupName).Mux.NewRoute().Subrouter()
subRouter := &SubRouter{
Mux: gsr,
}
mainRouter.GroupRouters[groupName].SubRouters[subRouterName] = subRouter
return subRouter
}
type MainRouter struct { type MainRouter struct {
Subrouters Subrouters GroupRouters GroupRouters
Mux *mux.Router Mux *mux.Router
} }
type Subrouters map[string]SubRouter
type SubRouter struct { type GroupRouters map[string]*GroupRouter
Name string
Group string type GroupRouter struct {
*mux.Router SubRouters SubRouters
Mux *mux.Router
}
type SubRouters map[string]*SubRouter
type SubRouter struct {
Mux *mux.Router
} }

View file

@ -275,7 +275,7 @@ func CapitalizeFirstLetter(input string) string {
return string(runes) // Convert rune slice back to string return string(runes) // Convert rune slice back to string
} }
func MapIter[K comparable, V comparable](m map[K]V, fn func(K, V)) { func MapIter[K string, V any](m map[K]V, fn func(K, V)) {
for p, d := range m { for p, d := range m {
fn(p, d) fn(p, d)
} }
@ -456,25 +456,25 @@ func AppRunner(runApp func()) {
<-ctx.Done() <-ctx.Done()
//do stuff after ending //do stuff after ending
wg.Wait() wg.Wait()
fmt.Println("BYE BYE!") log.Println("AppRunner stopped")
os.Exit(0) os.Exit(0)
} }
func StartTestHTTPServer(port int) { func StartTestHTTPServer(port int, name string) {
p := strconv.Itoa(port) p := strconv.Itoa(port)
go func() { go func() {
log.Println("Test server is running at http://" + GetIP() + ":" + p) log.Println("Test server is running at http://" + GetIP() + ":" + p)
r := mux.NewRouter() r := mux.NewRouter()
r.Path("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r.Path("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "/ OK") fmt.Fprintln(w, r.Host+"/ OK")
}) })
r.Path("/test1").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r.Path("/test1").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "/test1 OK") fmt.Fprintln(w, r.Host+"/test1 OK")
}) })
r.Path("/test2").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r.Path("/test2").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "/test2 OK") fmt.Fprintln(w, r.Host+"/test2 OK")
}) })
err := http.ListenAndServe(":3000", r) err := http.ListenAndServe(":"+p, r)
if err != nil { if err != nil {
log.Println(err.Error()) log.Println(err.Error())
} }
@ -510,3 +510,7 @@ func GetIP(prefix ...string) string {
} }
return prfx return prfx
} }
func AppendToPointer[T any](slc *[]T, val T) {
*slc = append(*slc, val)
}

File diff suppressed because one or more lines are too long

BIN
tmp/main

Binary file not shown.