Shopware HTTP Cache mit Varnish

Für die Konfiguration von Varnish stellt Shopware eine vollständige Konfiguration in den Shopware Developer Docs bereit:

https://developer.shopware.com/docs/guides/hosting/infrastructure/reverse-http-cache.html#configure-shopware



Trusted Proxies

Die Verwendung von Symfony und Varnish stellt in den meisten Fällen kein Problem dar. Wenn jedoch eine Anfrage durch einen Proxy läuft, werden bestimmte Anfragedaten entweder mit dem Standard-Header Forwarded oder X-Forwarded gesendet. Anstatt beispielsweise den Header REMOTE_ADDR zu lesen (der jetzt die IP-Adresse Ihres Reverse-Proxys ist), wird die tatsächliche IP-Adresse des Benutzers gespeichert.

Wenn Sie Symfony nicht so konfigurieren, dass es nach diesen Headern sucht, werden Sie falsche Informationen über die IP-Adresse des Clients erhalten. Unabhängig davon, ob sich der Client über https verbindet oder nicht, werden der Port des Clients und der Hostname abgefragt.


Beispiel Konfiguration über .env:

TRUSTED_PROXIES=127.0.0.1,127.0.0.2,10.20.1.1/32,10.20.0.1/32,10.20.0.2/32


In dieser Beispiel-Konfiguration werden die beiden Varnish-Instanzen 10.20.0.1 und 10.20.0.2 als Proxy autorisiert.

Mehr Informationen hierzu finden Sie unter:

Symfony Proxy Docs

creoline VPC-Netzwerke



Cache Purging

Zunächst muss die Reverse-Proxy-Unterstützung in Shopware aktiviert werden. Um diese zu aktivieren, muss folgende Datei erzeugt werden:

config/packages/storefront.yaml
storefront:
    reverse_proxy:
        enabled: true
        ban_method: "BAN"
        # Varnish hosts (IP:Port) [Default Port: 80]
        hosts: [ "http://10.20.X.X", "http://10.20.X.X" ]

        # Additional Headers for BAN Requests (E.G. Overwrite Host Header)
        ban_headers:
            Host: "www.creoline-demo.com"

        # Max parallel invalidations at same time for a single worker
        max_parallel_invalidations: 5


Bitte stellen Sie sicher, dass Sie die korrekten IP-Adressen der Varnish-Instanzen inklusive Angabe der HTTP Ports angeben, sofern dieser vom Standard Port 80 abweicht. In der Standard-Konfiguration des creoline Varnish-Servers ist hier der HTTP Port 80 konfiguriert. Sofern Sie Varnish eigenständig installiert haben, ist der Standard Port 6081.


Beachten Sie, dass der HTTP Host Header im BAN Request im Standard den IP-Adressen bzw. den Hostnamen der angegebenen hosts entspricht. Mithilfe der Angabe von ban_headers kann der Host Header entsprechend überschrieben werden, sodass der korrekte Cache-Key in Varnish ermittelt werden kann.



Varnish Konfiguration

Die Varnish-Konfiguration kann direkt über das Konfigurationsmodul im Kundencenter editiert und ausgerollt werden. Navigieren Sie zu Server → Ihr Server → Konfigurationsdateien → Varnish Config, um die Varnish Konfiguration anzupassen.


Bitte beachten Sie, dass das Speichern der Änderungen der Varnish-Konfiguration umgehend einen Configtest mit einem anschließenden Reload der Varnish-Instanz auslöst. Sollten Sie Varnish inital konfigurieren, empfehlen wir eine Test-Instanz zur Evaluierung der korrekten Konfiguration.


Beispiel Konfiguration mit Soft-Purge:

vcl 4.0;

import std;
import purge;

# You should specify here all your app nodes and use round robin to select a backend
backend default {
    .host = "<app-host>";
    .port = "80";
}

# ACL for purgers IP. (This needs to contain app server ips)
acl purgers {
    "127.0.0.1";
    "localhost";
    "::1";
}

sub vcl_recv {
    # Mitigate httpoxy application vulnerability, see: https://httpoxy.org/
    unset req.http.Proxy;

    # Strip query strings only needed by browser javascript. Customize to used tags.
    if (req.url ~ "(\?|&)(pk_campaign|piwik_campaign|pk_kwd|piwik_kwd|pk_keyword|pixelId|kwid|kw|adid|chl|dv|nk|pa|camid|adgid|cx|ie|cof|siteurl|utm_[a-z]+|_ga|gclid)=") {
        # see rfc3986#section-2.3 "Unreserved Characters" for regex
        set req.url = regsuball(req.url, "(pk_campaign|piwik_campaign|pk_kwd|piwik_kwd|pk_keyword|pixelId|kwid|kw|adid|chl|dv|nk|pa|camid|adgid|cx|ie|cof|siteurl|utm_[a-z]+|_ga|gclid)=[A-Za-z0-9\-\_\.\~]+&?", "");
    }
    set req.url = regsub(req.url, "(\?|\?&|&)$", "");

    # Normalize query arguments
    set req.url = std.querysort(req.url);

    # Make sure that the client ip is forward to the client.
    if (req.http.x-forwarded-for) {
        set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip;
    } else {
        set req.http.X-Forwarded-For = client.ip;
    }

    # Handle BAN
    if (req.method == "BAN") {
        if (!client.ip ~ purgers) {
            return (synth(405, "Method not allowed"));
        }

        return (hash);
    }

    # Normalize Accept-Encoding header
    # straight from the manual: https://www.varnish-cache.org/docs/3.0/tutorial/vary.html
    if (req.http.Accept-Encoding) {
        if (req.url ~ "\.(jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg)$") {
            # No point in compressing these
            unset req.http.Accept-Encoding;
        } elsif (req.http.Accept-Encoding ~ "gzip") {
            set req.http.Accept-Encoding = "gzip";
        } elsif (req.http.Accept-Encoding ~ "deflate") {
            set req.http.Accept-Encoding = "deflate";
        } else {
            # unknown algorithm
            unset req.http.Accept-Encoding;
        }
    }

    if (req.method != "GET" &&
        req.method != "HEAD" &&
        req.method != "PUT" &&
        req.method != "POST" &&
        req.method != "TRACE" &&
        req.method != "OPTIONS" &&
        req.method != "PATCH" &&
        req.method != "DELETE") {
        /* Non-RFC2616 or CONNECT which is weird. */
        return (pipe);
    }

    # We only deal with GET and HEAD by default
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    # Don't cache Authenticate & Authorization
    if (req.http.Authenticate || req.http.Authorization) {
        return (pass);
    }

    # Always pass these paths directly to php without caching
    # Note: virtual URLs might bypass this rule (e.g. /en/checkout)
    if (req.url ~ "^/(checkout|account|admin|api)(/.*)?$") {
        return (pass);
    }

    return (hash);
}

sub vcl_hash {
    # Consider Shopware HTTP cache cookies
    if (req.http.cookie ~ "sw-cache-hash=") {
        hash_data("+context=" + regsub(req.http.cookie, "^.*?sw-cache-hash=([^;]*);*.*$", "\1"));
    } elseif (req.http.cookie ~ "sw-currency=") {
        hash_data("+currency=" + regsub(req.http.cookie, "^.*?sw-currency=([^;]*);*.*$", "\1"));
    }
}

sub vcl_hit {
    if (req.method == "BAN") {
        call soft_purge_page;
    }

    # Consider client states for response headers
    if (req.http.cookie ~ "sw-states=") {
        set req.http.states = regsub(req.http.cookie, "^.*?sw-states=([^;]*);*.*$", "\1");

        if (req.http.states ~ "logged-in" && obj.http.sw-invalidation-states ~ "logged-in" ) {
            return (pass);
        }

        if (req.http.states ~ "cart-filled" && obj.http.sw-invalidation-states ~ "cart-filled" ) {
            return (pass);
        }
    }
}

sub vcl_miss {
    if (req.method == "BAN") {
        call soft_purge_page;
    }
}

sub vcl_backend_response {
    # Fix Vary Header in some cases
    # https://www.varnish-cache.org/trac/wiki/VCLExampleFixupVary
    if (beresp.http.Vary ~ "User-Agent") {
        set beresp.http.Vary = regsub(beresp.http.Vary, ",? *User-Agent *", "");
        set beresp.http.Vary = regsub(beresp.http.Vary, "^, *", "");
        if (beresp.http.Vary == "") {
            unset beresp.http.Vary;
        }
    }

    # Respect the Cache-Control=private header from the backend
    if (
        beresp.http.Pragma        ~ "no-cache" ||
        beresp.http.Cache-Control ~ "no-cache" ||
        beresp.http.Cache-Control ~ "private"
    ) {
        set beresp.ttl = 0s;
        set beresp.http.X-Cacheable = "NO:Cache-Control=private";
        set beresp.uncacheable = true;
        return (deliver);
    }

    # strip the cookie before the image is inserted into cache.
    if (bereq.url ~ "\.(png|gif|jpg|swf|css|js|webp)$") {
        unset beresp.http.set-cookie;
    }

    # Allow items to be stale if needed.
    set beresp.grace = 6h;

    # Save the bereq.url so bans work efficiently
    set beresp.http.x-url = bereq.url;
    set beresp.http.X-Cacheable = "YES";

    # Remove the exact PHP Version from the response for more security
    unset beresp.http.x-powered-by;

    return (deliver);
}

sub vcl_deliver {
    ## we don't want the client to cache
    set resp.http.Cache-Control = "max-age=0, private";

    # remove link header, if session is already started to save client resources
    if (req.http.cookie ~ "session-") {
        unset resp.http.Link;
    }

    # Set a cache header to allow us to inspect the response headers during testing
    if (obj.hits > 0) {
        unset resp.http.set-cookie;
        set resp.http.X-Cache = "HIT";
    }  else {
        set resp.http.X-Cache = "MISS";
    }

    # Remove the exact PHP Version from the response for more security (e.g. 404 pages)
    unset resp.http.x-powered-by;

    # Remove additional information about Varnish
    unset resp.http.via;
    unset resp.http.x-varnish;

    # invalidation headers are only for internal use
    unset resp.http.sw-invalidation-states;

    set resp.http.X-Cache-Hits = obj.hits;
}

sub soft_purge_page {
    # See https://docs.varnish-software.com/varnish-cache-plus/vmods/purge/ for all possible options
    set req.http.purged = purge.soft(ttl = 0s, grace = 300s, keep = 3600s);
    return (synth(200));
}