package middleware import ( "context" "fmt" "hash/crc32" "net/http" "net/url" "path" "strconv" "strings" "github.com/labstack/echo/v4" psconfig "prestaproxy/internal/prestashop/config" pscookie "prestaproxy/internal/prestashop/cookie" ) type AnonymousSessionInitializer interface { NewAnonymous(ctx context.Context, req *http.Request, cookieName string) (*pscookie.SessionContext, error) } type LanguageResolver interface { ResolveLanguageID(ctx context.Context, req *http.Request, fallback int64) int64 } type ProductRouteMatcher interface { Owns(path string) bool } func Session(cfg psconfig.Config, codec pscookie.Codec, initializer AnonymousSessionInitializer, languageResolver LanguageResolver, matcher ProductRouteMatcher) echo.MiddlewareFunc { ownership := cfg.ParseRouteOwnership() return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { ownedRoute := ownsProductRoute(ownership.ProductPrefixes, c.Request().URL.Path, matcher) configuredCookieName := cfg.DeriveCookieName(requestCookieHost(c.Request())) cookieName, rawCookie := findPrestaShopCookie(c.Request(), configuredCookieName) if cookieName == "" { cookieName = configuredCookieName } session, err := codec.Decode(rawCookie) if err != nil { if ownedRoute { return echo.NewHTTPError(http.StatusInternalServerError, "prestashop cookie decode failed") } SetSession(c, &pscookie.SessionContext{ CookieName: cookieName, RawCookie: rawCookie, Values: map[string]string{}, ParseStatus: pscookie.ParseStatusInvalid, }) return next(c) } session.CookieName = cookieName if ownedRoute && initializer != nil && shouldBootstrapAnonymousSession(rawCookie, session) { session, err = initializer.NewAnonymous(c.Request().Context(), c.Request(), cookieName) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("prestashop session bootstrap failed: %v", err)) } } if ownedRoute { applyRequestLanguage(session, resolveRequestLanguageID(c.Request().Context(), c.Request(), session, languageResolver)) } if ownedRoute && shouldSetSessionCookie(rawCookie, session) { encoded, err := codec.Encode(session) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "prestashop cookie encode failed") } session.RawCookie = encoded setPrestaShopCookie(c.Request(), c.Response(), ownership.ProductPrefixes, cookieName, encoded) } SetSession(c, session) return next(c) } } } func resolveRequestLanguageID(ctx context.Context, req *http.Request, session *pscookie.SessionContext, resolver LanguageResolver) int64 { if resolver == nil { return 0 } return resolver.ResolveLanguageID(ctx, req, sessionLanguageID(session)) } func findPrestaShopCookie(req *http.Request, configuredName string) (name, value string) { cookies := req.Cookies() for _, cookie := range cookies { if cookie.Name == configuredName { return cookie.Name, cookie.Value } } prefix := cookiePrefix(configuredName) if prefix == "" { return "", "" } for _, cookie := range cookies { if strings.HasPrefix(cookie.Name, prefix) { return cookie.Name, cookie.Value } } return "", "" } func cookiePrefix(configuredName string) string { if configuredName == "" { return "" } if strings.HasPrefix(configuredName, "PrestaShop-") { return "PrestaShop-" } if idx := strings.Index(configuredName, "-"); idx >= 0 { return configuredName[:idx+1] } return "" } func shouldBootstrapAnonymousSession(rawCookie string, session *pscookie.SessionContext) bool { if session == nil { return true } if rawCookie == "" { return true } if session.IsLoggedIn { return false } return session.GuestID == nil || session.CurrencyID == nil || session.LanguageID == nil || session.Values["id_connections"] == "" || session.Values["iso_code_country"] == "" } func shouldSetSessionCookie(rawCookie string, session *pscookie.SessionContext) bool { if session == nil { return false } if rawCookie == "" { return true } return rawCookie != session.RawCookie } func applyRequestLanguage(session *pscookie.SessionContext, languageID int64) { if session == nil || languageID == 0 { return } if current := sessionLanguageID(session); current == languageID { return } if session.Values == nil { session.Values = map[string]string{} } value := strconv.FormatInt(languageID, 10) session.LanguageID = int64Ptr(languageID) session.Values["id_lang"] = value session.Values["id_language"] = value session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "id_lang", 1) session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "id_language", 3) if !session.IsLoggedIn { if checksum := anonymousSessionChecksum(session, languageID); checksum != "" { session.Values["checksum"] = checksum session.OrderedKeys = ensureOrderedKey(session.OrderedKeys, "checksum", len(session.OrderedKeys)) } } session.Plaintext = "" session.RawCookie = "" } func sessionLanguageID(session *pscookie.SessionContext) int64 { if session == nil || session.LanguageID == nil { return 0 } return *session.LanguageID } func anonymousSessionChecksum(session *pscookie.SessionContext, languageID int64) string { if session == nil || session.Values == nil { return "" } guestID, _ := strconv.ParseInt(session.Values["id_guest"], 10, 64) connectionID, _ := strconv.ParseInt(session.Values["id_connections"], 10, 64) currencyID, _ := strconv.ParseInt(session.Values["id_currency"], 10, 64) shopID, _ := strconv.ParseInt(session.Values["id_shop"], 10, 64) if guestID == 0 || connectionID == 0 || currencyID == 0 { return "" } buf := make([]byte, 0, 32) for _, value := range []int64{guestID, connectionID, languageID, currencyID, shopID} { buf = strconv.AppendInt(buf, value, 10) buf = append(buf, '|') } return strconv.FormatUint(uint64(crc32.ChecksumIEEE(buf)), 10) } func ensureOrderedKey(keys []string, key string, index int) []string { for i, existing := range keys { if existing != key { continue } if i == index || index >= len(keys) { return keys } keys = append(keys[:i], keys[i+1:]...) break } if index < 0 { index = 0 } if index >= len(keys) { return append(keys, key) } keys = append(keys, "") copy(keys[index+1:], keys[index:]) keys[index] = key return keys } func int64Ptr(value int64) *int64 { if value == 0 { return nil } v := value return &v } func setPrestaShopCookie(req *http.Request, res *echo.Response, ownedPrefixes []string, name, value string) { http.SetCookie(res.Writer, &http.Cookie{ Name: name, Value: value, Path: requestCookiePath(req.URL.Path, ownedPrefixes), Domain: requestCookieDomain(req), Secure: requestCookieSecure(req), HttpOnly: true, SameSite: http.SameSiteLaxMode, }) } func requestCookiePath(requestPath string, ownedPrefixes []string) string { for _, prefix := range ownedPrefixes { if prefix == "" || !strings.HasPrefix(requestPath, prefix) { continue } base := path.Clean(strings.TrimSuffix(prefix, "/")) if base == "." || base == "/" { return "/" } parent := path.Dir(base) if parent == "." { return "/" } if !strings.HasSuffix(parent, "/") { parent += "/" } return parent } return "/" } func requestCookieDomain(req *http.Request) string { host := requestCookieHost(req) if host == "" { return "" } if parsed, err := url.Parse("http://" + host); err == nil { return parsed.Hostname() } return host } func requestCookieHost(req *http.Request) string { if req == nil { return "" } host := req.Header.Get("X-Forwarded-Host") if host == "" { host = req.Host } if strings.Contains(host, ",") { host = strings.TrimSpace(strings.Split(host, ",")[0]) } return host } func requestCookieSecure(req *http.Request) bool { if req.TLS != nil { return true } if forwarded := req.Header.Get("X-Forwarded-Proto"); forwarded != "" { if strings.Contains(forwarded, ",") { forwarded = strings.TrimSpace(strings.Split(forwarded, ",")[0]) } return strings.EqualFold(forwarded, "https") } return false } func ownsProductRoute(prefixes []string, path string, matcher ProductRouteMatcher) bool { if matcher != nil { return matcher.Owns(path) } for _, prefix := range prefixes { if prefix != "" && len(path) >= len(prefix) && path[:len(prefix)] == prefix { return true } } return false }