go-dev-zprox-0.01/internal/config/config.go
2025-03-22 08:57:23 +00:00

739 lines
18 KiB
Go

package config
import (
"context"
"errors"
"log"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"github.com/google/uuid"
"github.com/gookit/goutil/dump"
"github.com/joho/godotenv"
"github.com/spf13/viper"
)
// var routeRulesNames = []string{"Host", "Path", "PathPrefix"}
const DEFAULT_CONFIG_TYPE = "yaml"
var DEFAULT_CONFIG_PATH string
var DEFAULT_CONFIG_NAME string
var Data *Config
func init() {
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
DEFAULT_CONFIG_PATH = os.Getenv("CONFIG_PATH")
DEFAULT_CONFIG_NAME = os.Getenv("CONFIG_NAME")
Data = ConfigBuild()
}
type Config struct {
Routers Routers
Services Services
EntryPoints EntryPoints
TLS TLS
AuthMap AuthMap
Health *Health
Logs *Logs
Context context.Context
}
type Logs struct {
ConfigBuild []string
}
type Health struct {
ConfigHealth *ConfigHealth
}
type ConfigHealth struct {
Routers bool
Services bool
EntryPoints bool
AuthBuild bool
}
type ConfigLog map[string][]string
type AuthMap map[string]*Auth
type Auth struct {
Domain string
SessionSecret string
Paths *Paths
OpenID *OpenID
}
type OpenID struct {
EndPoints EndPoints
Realm string
ClientID string
ClientSecret string
}
type EndPoints struct {
Issuer string
AuthURL string
TokenURL string
UserURL string
LogoutURL string
Config string
RedirectURI string
PostLogoutRedirectUri string
JwksURI string
}
type Paths struct {
Prefix string
Login string
Logout string
Callback string
PostLogout string
}
type EntryPoints map[string]*EntryPoint
type EntryPoint struct {
Address string
TLS *EntryPointTLS
}
type EntryPointTLS struct {
Enabled bool
}
type TLS struct {
CertProviders CertProviders
}
type CertProviders map[string]*certProvider
type certProvider struct {
Cert string
Key string
}
type Routers map[string]*Router
type Router struct {
IsAuthRouter bool
StripPrefix bool
Priority int
Name string
Service string
EntryPoint string
Routes Routes
Auth RouterAuth
}
type RouterAuth struct {
JWT *JWT
Enabled bool
Provider string
}
type JWT struct {
AllowLists map[string][]string
}
type Routes map[string]*Route
type Route struct {
ID string
Router string
Rule map[string]string
}
type Services map[string]string
func (c *Config) InitContext(ctx context.Context) {
c.Context = ctx
}
func (e EntryPoints) ForEach(fn func(name string, data *EntryPoint)) {
for name, data := range e {
fn(name, data)
}
}
func (r Routers) ForEach(fn func(name string, data *Router) string) {
for name, data := range r {
msg := fn(name, data)
if msg == "break" {
break
}
if msg == "continue" {
continue
}
}
}
func (r Routers) ForEachByPriority(fn func(name string, data *Router)) {
slc := r.getSlicesByPriority()
for _, n := range slc {
d := r[n]
fn(n, d)
}
}
func (r Routers) getSlicesByPriority() []string {
routersSlice := []string{}
r.ForEach(func(routerName string, routerData *Router) string {
routersSlice = append(routersSlice, routerName)
return ""
})
slices.SortFunc(routersSlice, func(a, b string) int {
if r[a].Priority == r[b].Priority {
return 0
}
if r[a].Priority > r[b].Priority {
return -1
}
if r[a].Priority < r[b].Priority {
return 1
}
return r[a].Priority - r[b].Priority
})
return routersSlice
}
func (r *Router) GetRouteByID(routeID string) *Route {
return r.Routes[routeID]
}
func (r Routes) ForEach(fn func(routeID string, v *Route) string) {
for routeID, v := range r {
msg := fn(routeID, v)
if msg == "break" {
break
}
if msg == "continue" {
continue
}
}
}
func (r Route) ForEachRule(fn func(routeID string, ruleName string, ruleValue string) string) {
for k, v := range r.Rule {
msg := fn(r.ID, k, v)
if msg == "break" {
break
}
}
}
func BuildAuthPaths(authData any) *Paths {
pathsData := getMap(authData, "paths")
return &Paths{
Prefix: getMapString(pathsData, "prefix"),
Login: getMapString(pathsData, "login"),
Logout: getMapString(pathsData, "logout"),
Callback: getMapString(pathsData, "callback"),
PostLogout: getMapString(pathsData, "postlogout"),
}
}
func InitConfigStruct() *Config {
config := &Config{
Routers: Routers{},
Services: Services{},
EntryPoints: EntryPoints{},
TLS: TLS{
CertProviders: CertProviders{},
},
AuthMap: AuthMap{},
Health: &Health{
ConfigHealth: &ConfigHealth{
Routers: false,
Services: false,
EntryPoints: false,
AuthBuild: false,
},
},
Logs: &Logs{
ConfigBuild: []string{},
},
}
return config
}
func ConfigBuild() *Config {
vc := NewViperConfig()
c := InitConfigStruct()
// //Routers
// //Services
BuildRoutersConfig(vc, c, "routers")
BuildServicesConfig(vc, c, "services")
BuildCertProvidersConfig(vc, c, "tls.certproviders")
BuildEntryPointsConfig(vc, c, "entrypoints")
BuildAuthConfig(vc, c, "auth")
// if !BuildServicesConfig(viperConfig, config, "services") {
// err = errors.Join(errors.New("build services config error ="))
// }
// if !BuildRoutersConfig(viperConfig, config, "routers") {
// err = errors.Join(errors.New("build routers config error"))
// }
// // //Entrypoints
// if !BuildEntryPointsConfig(viperConfig, config, "entryPoints") {
// err = errors.Join(errors.New("build entry points config error"))
// }
// // //TLS Certificate Providers
// if !BuildCertProvidersConfig(viperConfig, config, "tls.certproviders") {
// err = errors.Join(errors.New("build cert providers config error"))
// }
// if !BuildAuthConfig(viperConfig, config, "auth") {
// // err = errors.Join(errors.New("build auth config error"))
// }
return c
}
func BuildCertProvidersConfig(viperConfig *viper.Viper, config *Config, key string) bool {
if viperConfig.Sub(key) == nil {
config.Logs.ConfigBuild = append(config.Logs.ConfigBuild, "Missing \""+key+"\" in Config")
return false
} else {
certProviders := viperConfig.Sub(key).AllSettings()
// config.TLS.CertProviders = make(CertProviders)
for k, v := range certProviders {
v := v.(map[string]any)
cert := v["cert"].(string)
key := v["key"].(string)
config.TLS.CertProviders[k] = &certProvider{
Cert: cert,
Key: key,
}
}
return true
}
}
func BuildServicesConfig(viperConfig *viper.Viper, config *Config, key string) bool {
if viperConfig.Sub(key) == nil {
config.Logs.ConfigBuild = append(config.Logs.ConfigBuild, "Missing \""+key+"\" in Config")
dump.P("Missing \"" + key + "\" in Config")
return false
} else {
config.Services = make(Services)
services := viperConfig.Sub(key).AllSettings()
for k, v := range services {
config.Services[k] = v.(string)
}
return true
}
}
func BuildEntryPointsConfig(viperConfig *viper.Viper, config *Config, key string) bool {
if viperConfig.Sub(key) == nil {
config.Logs.ConfigBuild = append(config.Logs.ConfigBuild, "Missing \""+key+"\" in Config")
dump.P("Missing \"" + key + "\" in Config")
return false
} else {
entryPoints := viperConfig.Sub(key).AllSettings()
for k, v := range entryPoints {
entryPoint := &EntryPoint{
Address: "",
TLS: &EntryPointTLS{
Enabled: false,
},
}
v := v.(map[string]any)
if _, ok := v["address"]; ok {
entryPoint.Address = v["address"].(string)
}
if _, ok := v["tls"]; ok {
tls := v["tls"].(map[string]any)
enabled := tls["enabled"].(bool)
entryPoint.TLS = &EntryPointTLS{
Enabled: enabled,
}
}
config.EntryPoints[k] = entryPoint
}
return true
}
}
func getMap(mp any, key string) map[string]any {
if val, ok := mp.(map[string]any)[key]; ok {
return val.(map[string]any)
} else {
return val.(map[string]any)
}
}
func getMapString(mp any, key string) string {
if val, ok := mp.(map[string]any)[key]; ok {
return val.(string)
} else {
return ""
}
}
func markWrapper(str string, mark []string) string {
return mark[0] + str + mark[1]
}
func BuildAuthConfig(viperConfig *viper.Viper, config *Config, key string) bool {
if viperConfig.Sub(key) == nil {
config.Logs.ConfigBuild = append(config.Logs.ConfigBuild, "Missing \""+key+"\" in Config")
dump.P("Missing \"" + key + "\" in Config")
return false
} else {
authData := viperConfig.Sub(key).AllSettings()
mark := []string{"<{{", "}}>"}
realmUrlMark := markWrapper("realm", mark)
authRootUrlMark := markWrapper("auth_root_url", mark)
targetRootUrlMark := markWrapper("target_root_url", mark)
authLocalRootUrlMark := markWrapper("auth_local_root_url", mark)
for authName, v := range authData {
auth := &Auth{}
openid := &OpenID{}
// dump.P(auth)
auth.Paths = BuildAuthPaths(v)
// endPoints := &EndPoints{}
authRootURL := getMapString(v, "auth_root_url")
targetRootURL := getMapString(v, "target_root_url")
authLocalRootURL := getMapString(v, "auth_local_root_url")
openidData := getMap(v, "openid")
endPointsData := getMap(openidData, "end_points")
// for k, v := range openidData {
// if k != "end_points" {
// v := v.(string)
// field := helper.CapitalizeFirstLetter(k)
// field = helper.CapitalizeAfterCharMulti(field, '_')
// field = strings.ReplaceAll(field, "_", "")
// if strings.HasSuffix(field, "Id") {
// field, _ = strings.CutSuffix(field, "Id")
// field += "ID"
// }
// helper.InsertStringValueIntoField(openid, field, v)
// dump.P(field, v)
// }
// }
openid.Realm = openidData["realm"].(string)
openid.ClientID = openidData["client_id"].(string)
openid.ClientSecret = openidData["client_secret"].(string)
endPointsMapStringString := make(map[string]string)
for k, v := range endPointsData {
val := v.(string)
if strings.Contains(val, realmUrlMark) {
val = strings.ReplaceAll(val, realmUrlMark, openid.Realm)
}
if strings.Contains(val, authRootUrlMark) {
val = strings.ReplaceAll(val, authRootUrlMark, authRootURL)
}
if strings.Contains(val, targetRootUrlMark) {
val = strings.ReplaceAll(val, targetRootUrlMark, targetRootURL)
}
if strings.Contains(val, authLocalRootUrlMark) {
val = strings.ReplaceAll(val, authLocalRootUrlMark, authLocalRootURL)
}
endPointsMapStringString[k] = val
}
if epData, ok := endPointsMapStringString["issuer"]; ok {
openid.EndPoints.Issuer = epData
}
if epData, ok := endPointsMapStringString["redirect_uri"]; ok {
openid.EndPoints.RedirectURI = epData
}
if epData, ok := endPointsMapStringString["post_logout_redirect_uri"]; ok {
openid.EndPoints.PostLogoutRedirectUri = epData
}
if epData, ok := endPointsMapStringString["config"]; ok {
openid.EndPoints.Config = epData
}
if epData, ok := endPointsMapStringString["authurl"]; ok {
openid.EndPoints.AuthURL = epData
}
if epData, ok := endPointsMapStringString["tokenurl"]; ok {
openid.EndPoints.TokenURL = epData
}
if epData, ok := endPointsMapStringString["userurl"]; ok {
openid.EndPoints.UserURL = epData
}
if epData, ok := endPointsMapStringString["logouturl"]; ok {
openid.EndPoints.LogoutURL = epData
}
if epData, ok := endPointsMapStringString["jwksuri"]; ok {
openid.EndPoints.JwksURI = epData
}
auth.OpenID = openid
config.AuthMap[authName] = auth
}
return true
}
}
func splitStatementByRegex(str string, regx string) []string {
input := str
re := regexp.MustCompile(regx)
matches := re.FindAllStringSubmatch(input, -1)
if len(matches) == 0 {
return []string{str} // Return original if no backticks found
}
result := []string{}
lastIndex := 0
for _, match := range matches {
startIndex := strings.Index(input, match[0])
if startIndex > lastIndex {
str := input[lastIndex:startIndex]
str, _ = strings.CutPrefix(str, ".")
result = append(result, str)
}
str := match[1]
str, _ = strings.CutPrefix(str, ".")
result = append(result, str) // Append the content within backticks
lastIndex = startIndex + len(match[0])
}
if lastIndex < len(input) {
str := input[lastIndex:]
str, _ = strings.CutPrefix(str, ".")
result = append(result, str)
}
return result
}
func BuildRoutersConfig(viperConfig *viper.Viper, config *Config, key string) bool {
if viperConfig.Sub(key) == nil {
config.Logs.ConfigBuild = append(config.Logs.ConfigBuild, "Missing \""+key+"\" in Config")
dump.P("Missing \"" + key + "\" in Config")
return false
} else {
routers := viperConfig.Sub(key).AllSettings()
for routerName, d := range routers {
data := d.(map[string]any)
// var (
// routes []any
// service string
// entryPoint string
// priority int
// stripPrefix bool
// )
if _, ok := data["routes"]; !ok {
continue
}
if _, ok := data["service"]; !ok {
continue
}
if _, ok := data["entrypoint"]; !ok {
continue
}
if _, ok := data["priority"]; !ok {
data["priority"] = 1000
}
if _, ok := data["routes"]; !ok {
continue
}
if _, ok := data["stripprefix"]; !ok {
data["stripprefix"] = false
}
routes := data["routes"].([]any)
service := data["service"].(string)
entryPoint := data["entrypoint"].(string)
priority := data["priority"].(int)
stripPrefix := data["stripprefix"].(bool)
isAuthRouter := false
if _, ok := data["is_auth_router"]; ok {
isAuthRouter = data["is_auth_router"].(bool)
}
//AUTH
authEnabled := false
provider := ""
jwt := &JWT{
AllowLists: map[string][]string{},
}
if d, ok := data["auth"]; ok {
if _, ok := d.(map[string]any)["enabled"]; ok {
authEnabled = d.(map[string]any)["enabled"].(bool)
}
provider = d.(map[string]any)["provider"].(string)
if jwtData, ok := d.(map[string]any)["jwt"]; ok {
if allow, ok := jwtData.(map[string]any)["allow"]; ok {
a := allow.(map[string]any)
for f, s := range a {
field := f
slice := []string{}
for _, v := range s.([]any) {
slice = append(slice, v.(string))
}
jwt.AllowLists[field] = slice
dump.P(slice)
}
}
}
// if _, ok := d.(map[string]any)["emails"]; ok {
// for _, e := range d.(map[string]any)["emails"].([]any) {
// email := e.(string)
// emails = append(emails, email)
// }
// }
}
router := &Router{
Name: routerName,
IsAuthRouter: isAuthRouter,
Priority: priority,
Service: service,
EntryPoint: entryPoint,
StripPrefix: stripPrefix,
Routes: Routes{},
Auth: RouterAuth{
Enabled: authEnabled,
Provider: provider,
JWT: jwt,
// Emails: emails,
},
}
for _, v := range routes {
routeID := uuid.New().String()
route := &Route{
ID: routeID,
Router: routerName,
Rule: map[string]string{},
}
rawRoute := v.(string)
route.Rule["raw"] = rawRoute
routeStatmentsLogicSplit := strings.Split(rawRoute, "&&")
for _, logicPart := range routeStatmentsLogicSplit {
logicPart = strings.TrimSpace(logicPart)
routeStatmentsSlice := splitStatementByRegex(logicPart, "\\(`(.*?)`\\)")
for i, v := range routeStatmentsSlice {
if i&1 == 0 {
if i+1 < len(routeStatmentsSlice) {
route.Rule[v] = routeStatmentsSlice[i+1]
}
}
}
}
// routeStatmentsSlice := splitStatementByRegex(rawRoute, "\\(`(.*?)`\\)")
// for i, v := range routeStatmentsSlice {
// if i&1 == 0 {
// if i+1 < len(routeStatmentsSlice) {
// route.Rule[v] = routeStatmentsSlice[i+1]
// }
// }
// }
router.Routes[routeID] = route
// router.Routes = append(router.Routes, route)
}
config.Routers[routerName] = router
}
return true
}
}
// func NewViperConfig() *viper.Viper {
// vc := viper.New()
// vc.AddConfigPath(DEFAULT_CONFIG_PATH)
// vc.SetConfigType(DEFAULT_CONFIG_TYPE)
// vc.SetConfigName(DEFAULT_CONFIG_NAME)
// vc.ReadInConfig()
// fileNames, err := getFileNames(DEFAULT_CONFIG_PATH, DEFAULT_CONFIG_NAME)
// if err != nil {
// dump.Println("No extra config files" + err.Error())
// } else {
// for _, fileName := range fileNames {
// vc.SetConfigName(fileName)
// vc.MergeInConfig()
// }
// }
// return vc
// }
func NewViperConfig() *viper.Viper {
vc := viper.New()
vc.AddConfigPath(DEFAULT_CONFIG_PATH)
vc.SetConfigType(DEFAULT_CONFIG_TYPE)
vc.SetConfigName(DEFAULT_CONFIG_NAME)
err := vc.ReadInConfig()
if err != nil {
dump.P(err.Error())
}
// vc1 := viper.New()
// vc1.AddConfigPath(DEFAULT_CONFIG_PATH)
// vc1.SetConfigType(DEFAULT_CONFIG_TYPE)
// vc1.SetConfigName("auth")
// vc1.ReadInConfig()
// vc.MergeConfigMap(vc1.AllSettings())
// vc.MergeConfigMap(vc1.AllSettings())
fileNames, err := getFileNames(DEFAULT_CONFIG_PATH, DEFAULT_CONFIG_NAME)
if err != nil {
dump.Println("No extra config files" + err.Error())
} else {
for _, fileName := range fileNames {
vc.SetConfigName(fileName)
vc.MergeInConfig()
}
}
return vc
}
func getYamlFileNameExcExt(fileName string, exts ...string) (string, error) {
var fileExt string
for _, ext := range exts {
if strings.HasSuffix(fileName, ext) {
fileExt = ext
break
}
}
if name, ok := strings.CutSuffix(fileName, fileExt); ok && fileExt != "" {
return name, nil
} else {
return "", errors.New("error: no file extension found ")
}
}
func getFileNames(folderPath string, skipFileName string) ([]string, error) {
var fileNames []string
err := filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() { // Only add files, not directories
filename := info.Name()
filesize := info.Size()
filenameNoExt, _ := getYamlFileNameExcExt(filename, ".yaml", ".yml")
if skipFileName == filenameNoExt || filesize == 0 {
return nil
}
fileNames = append(fileNames, filenameNoExt)
}
return nil
})
if err != nil {
return nil, err
}
return fileNames, nil
}