package config import ( "bufio" "crypto/md5" "encoding/json" "errors" "fmt" "net" "net/url" "os" "path/filepath" "regexp" "strings" pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie" ) type Config struct { AppAddr string AppEnv string AppDBDSN string AssetManifestPath string PrestaShopBaseURL string PrestaShopProxyTarget string PrestaShopVersion string PrestaShopCookieKey string PrestaShopCookieIV string PrestaShopCookieName string PrestaShopDBDSN string PrestaShopDBDialect string PrestaShopTablePrefix string PrestaShopProjectRoot string PrestaShopBootstrap string RouteOwnershipConfig string } func Load() (Config, error) { if err := loadDotEnv(".env"); err != nil { return Config{}, err } cfg := Config{ AppAddr: envOr("APP_ADDR", ":8080"), AppEnv: envOr("APP_ENV", "development"), AppDBDSN: firstNonEmpty(os.Getenv("APP_DB_DSN"), os.Getenv("PRESTASHOP_DB_DSN"), dbDSNFromEnv()), AssetManifestPath: envOr("ASSET_MANIFEST_PATH", "web/dist/manifest.json"), PrestaShopBaseURL: os.Getenv("PRESTASHOP_BASE_URL"), PrestaShopProxyTarget: os.Getenv("PRESTASHOP_PROXY_TARGET"), PrestaShopVersion: envOr("PRESTASHOP_VERSION", "1.7.3"), PrestaShopCookieKey: os.Getenv("PRESTASHOP_COOKIE_KEY"), PrestaShopCookieIV: os.Getenv("PRESTASHOP_COOKIE_IV"), PrestaShopCookieName: os.Getenv("PRESTASHOP_COOKIE_NAME"), PrestaShopDBDSN: firstNonEmpty(os.Getenv("PRESTASHOP_DB_DSN"), dbDSNFromEnv()), PrestaShopDBDialect: envOr("PRESTASHOP_DB_DIALECT", "mariadb"), PrestaShopTablePrefix: firstNonEmpty(os.Getenv("PRESTASHOP_TABLE_PREFIX"), os.Getenv("DB_PREFIX"), "ps_"), PrestaShopProjectRoot: os.Getenv("PRESTASHOP_PROJECT_ROOT"), PrestaShopBootstrap: os.Getenv("PRESTASHOP_BOOTSTRAP_PATH"), RouteOwnershipConfig: envOr("ROUTE_OWNERSHIP_CONFIG", "/product/"), } if err := cfg.bootstrap(); err != nil { return Config{}, err } if cfg.PrestaShopProxyTarget == "" { return Config{}, errors.New("PRESTASHOP_PROXY_TARGET is required") } if cfg.PrestaShopProjectRoot == "" && cfg.PrestaShopBootstrap == "" && cfg.PrestaShopCookieKey == "" { return Config{}, errors.New("prestashop cookie configuration is incomplete") } if cfg.PrestaShopDBDSN == "" { return Config{}, errors.New("PRESTASHOP_DB_DSN is required") } return cfg, nil } func loadDotEnv(path string) error { file, err := os.Open(path) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil } return fmt.Errorf("open %s: %w", path, err) } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } if strings.HasPrefix(line, "export ") { line = strings.TrimSpace(strings.TrimPrefix(line, "export ")) } key, value, ok := strings.Cut(line, "=") if !ok { continue } key = strings.TrimSpace(key) value = strings.TrimSpace(value) if key == "" { continue } value = strings.Trim(value, `"'`) if _, exists := os.LookupEnv(key); exists { continue } if err := os.Setenv(key, value); err != nil { return fmt.Errorf("set env %s: %w", key, err) } } if err := scanner.Err(); err != nil { return fmt.Errorf("scan %s: %w", path, err) } return nil } func (c Config) CookieConfig() pscookie.Config { return pscookie.Config{ Version: c.PrestaShopVersion, CookieName: c.DeriveCookieName(""), CookieKey: c.PrestaShopCookieKey, CookieIV: c.PrestaShopCookieIV, ProjectRoot: c.PrestaShopProjectRoot, BootstrapPath: c.PrestaShopBootstrap, } } func (c Config) DeriveCookieName(host string) string { if c.PrestaShopCookieName != "" { return c.PrestaShopCookieName } domain := fallbackCookieHashDomain(host) if domain == "" { domain = fallbackCookieHashDomain(c.PrestaShopBaseURL) } if domain == "" { domain = fallbackCookieHashDomain(c.PrestaShopProxyTarget) } sum := md5.Sum([]byte(c.PrestaShopVersion + "ps-s1" + domain)) return fmt.Sprintf("PrestaShop-%x", sum) } func (c *Config) bootstrap() error { if c.PrestaShopProjectRoot == "" && c.PrestaShopBootstrap != "" { c.PrestaShopProjectRoot = filepath.Dir(filepath.Dir(c.PrestaShopBootstrap)) } if c.PrestaShopProjectRoot == "" { return nil } settings := filepath.Join(c.PrestaShopProjectRoot, "config", "settings.inc.php") if data, err := os.ReadFile(settings); err == nil { if c.PrestaShopCookieKey == "" { c.PrestaShopCookieKey = parseDefine(string(data), "_COOKIE_KEY_") } if c.PrestaShopCookieIV == "" { c.PrestaShopCookieIV = parseDefine(string(data), "_COOKIE_IV_") } if c.PrestaShopTablePrefix == "ps_" { if prefix := parseDefine(string(data), "_DB_PREFIX_"); prefix != "" { c.PrestaShopTablePrefix = prefix } } } parameters := filepath.Join(c.PrestaShopProjectRoot, "app", "config", "parameters.php") if data, err := os.ReadFile(parameters); err == nil { values := parsePHPParameters(string(data)) if c.PrestaShopDBDSN == "" { c.PrestaShopDBDSN = mysqlDSN(values["database_host"], values["database_port"], values["database_name"], values["database_user"], values["database_password"]) } if c.PrestaShopCookieKey == "" { c.PrestaShopCookieKey = values["secret"] } } if c.PrestaShopBootstrap == "" { c.PrestaShopBootstrap = filepath.Join(c.PrestaShopProjectRoot, "config", "config.inc.php") } return nil } func parseDefine(input, key string) string { re := regexp.MustCompile(fmt.Sprintf(`define\('%s',\s*'([^']*)'`, regexp.QuoteMeta(key))) matches := re.FindStringSubmatch(input) if len(matches) < 2 { return "" } return matches[1] } func parsePHPParameters(input string) map[string]string { out := map[string]string{} re := regexp.MustCompile(`'([^']+)'\s*=>\s*'([^']*)'`) for _, match := range re.FindAllStringSubmatch(input, -1) { out[match[1]] = match[2] } return out } func mysqlDSN(host, port, db, user, pass string) string { if host == "" || db == "" || user == "" { return "" } if port == "" { port = "3306" } return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=UTC", user, pass, host, port, db) } func envOr(key, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } func firstNonEmpty(values ...string) string { for _, value := range values { if value != "" { return value } } return "" } func normalizedCookieDomain(input string) string { value := strings.TrimSpace(input) if value == "" { return "" } if strings.Contains(value, "://") { if parsed, err := url.Parse(value); err == nil { value = parsed.Hostname() } } if host, _, err := net.SplitHostPort(value); err == nil { value = host } value = strings.TrimPrefix(strings.ToLower(value), ".") value = strings.TrimPrefix(value, "www.") return value } func fallbackCookieHashDomain(input string) string { value := normalizedCookieDomain(input) if value == "" { return "" } if net.ParseIP(value) != nil || !strings.Contains(value, ".") { return "" } return value } func dbDSNFromEnv() string { return mysqlDSN( firstNonEmpty(os.Getenv("PRESTASHOP_DB_HOST"), os.Getenv("DB_HOST")), firstNonEmpty(os.Getenv("PRESTASHOP_DB_PORT"), os.Getenv("DB_PORT")), firstNonEmpty(os.Getenv("PRESTASHOP_DB_NAME"), os.Getenv("DB_NAME")), firstNonEmpty(os.Getenv("PRESTASHOP_DB_USER"), os.Getenv("DB_USER")), firstNonEmpty(os.Getenv("PRESTASHOP_DB_PASS"), os.Getenv("DB_PASS")), ) } type RouteOwnership struct { ProductPrefixes []string `json:"product_prefixes"` } func (c Config) ParseRouteOwnership() RouteOwnership { if strings.HasPrefix(strings.TrimSpace(c.RouteOwnershipConfig), "{") { var parsed RouteOwnership if err := json.Unmarshal([]byte(c.RouteOwnershipConfig), &parsed); err == nil && len(parsed.ProductPrefixes) > 0 { return parsed } } return RouteOwnership{ ProductPrefixes: []string{c.RouteOwnershipConfig}, } }