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 }