routing
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user