Skip to content

default.vcl

Danila Vershinin edited this page Jun 4, 2025 · 3 revisions
# WordPress Varnish VCL

# VCL compiler declaration
vcl 4.1;

# Imports
import std;

# Includes (DO NOT CHANGE ORDER!)
include "lib/cloudflare.vcl";
include "lib/xforward.vcl";
include "lib/purge.vcl";
include "lib/bigfiles_pipe.vcl";
include "lib/static.vcl";

# Default backend definition. Set this to point to your content server.
backend default {
	.host = "MACHINE.IP.ADDRESS";
	.port = "PORT";
}

acl purge {
	"localhost";
	"127.0.0.1";
	"0.0.0.0";
	"::1";
	"IP OF MACHINE";
}

# We just received the request from the site visitor.
sub vcl_recv {

	# LetsEncrypt Certbot passthrough
	if (req.url ~ "^/\.well-known/acme-challenge/") {
		return (pass);
	}

	# httproxy
	unset req.http.proxy;

	# Force HTTPS because of Hitch
	if ((std.port(server.ip) == 443)) {
		set req.http.X-Forwarded-Proto = "https";
	}

	# Normalize the query arguments (but exclude for WordPress' backend)
	if (req.url !~ "wp-admin|wp-login") {
		set req.url = std.querysort(req.url);
	}

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

	# === URL manipulation ===
	# Remove the Google Analytics added parameters
	if (req.url ~ "(\?|&)(utm_source|utm_medium|utm_campaign|utm_content|gclid|cx|ie|cof|siteurl)=") {
		set req.url = regsuball(req.url, "&(utm_source|utm_medium|utm_campaign|utm_content|gclid|cx|ie|cof|siteurl)=([A-z0-9_\-\.%25]+)", "");
		set req.url = regsuball(req.url, "\?(utm_source|utm_medium|utm_campaign|utm_content|gclid|cx|ie|cof|siteurl)=([A-z0-9_\-\.%25]+)", "?");
		set req.url = regsub(req.url, "\?&", "?");
		set req.url = regsub(req.url, "\?$", "");
	}

	# Remove variable for replying to comments, so people still get cache
	if (req.url ~ "\?replytocom") {
		set req.url = regsub(req.url, "\?replytocom=.*$", "");
	}

	# Strip hash, server doesn't need it.
	if (req.url ~ "\#") {
		set req.url = regsub(req.url, "\#.*$", "");
	}

	# Strip a trailing ? if it exists
	if (req.url ~ "\?$") {
		set req.url = regsub(req.url, "\?$", "");
	}

	# === DO NOT CACHE ===

	# never cache anything except GET/HEAD
	if (req.method != "GET" && req.method != "HEAD") {
		return(pass);
	}

	# Don't cache HTTP authorization/authentication pages and pages with certain headers or cookies
	if (
		req.http.Authorization ||
		req.http.Authenticate ||
		req.http.X-Logged-In == "True" ||
		req.http.Cookie ~ "wordpress_(?!test_)[a-zA-Z0-9_]+|wp-postpass|comment_author_[a-zA-Z0-9_]+|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_[a-zA-Z0-9]+|wordpress_logged_in_|comment_author|PHPSESSID"
	) {
		return(pass);
	}

	# Exclude the following paths (e.g. backend admins, user pages or ad URLs that require tracking)
	if (
		req.url ~ "add_to_cart" ||
		req.url ~ "edd_action" ||
		req.url ~ "nocache" ||
		req.url ~ "^/addons" ||
		req.url ~ "^/bb-admin" ||
		req.url ~ "^/bb-login.php" ||
		req.url ~ "^/bb-reset-password.php" ||
		req.url ~ "^/cart" ||
		req.url ~ "^/checkout" ||
		req.url ~ "^/control.php" ||
		req.url ~ "^/login" ||
		req.url ~ "^/logout" ||
		req.url ~ "^/lost-password" ||
		req.url ~ "^/my-account" ||
		req.url ~ "^/product" ||
		req.url ~ "^/register" ||
		req.url ~ "^/register.php" ||
		req.url ~ "^/server-status" ||
		req.url ~ "^/signin" ||
		req.url ~ "^/signup" ||
		req.url ~ "^/stats" ||
		req.url ~ "^/wc-api" ||
		req.url ~ "^/wp-admin" ||
		req.url ~ "^/wp-comments-post.php" ||
		req.url ~ "^/wp-cron.php" ||
		req.url ~ "^/wp-login.php" ||
		req.url ~ "^/wp-activate.php" ||
		req.url ~ "^/wp-mail.php" ||
		req.url ~ "^/wp-login.php" ||
		req.url ~ "^\?add-to-cart=" ||
		req.url ~ "^\?wc-api=" ||
		req.url ~ "^/preview="
	) {
		return(pass);
	}

	# never cache ajax requests
	if (req.http.X-Requested-With == "XMLHttpRequest") {
		return(pass);
	}


	# === Generic cookie manipulation ===
	# Remove the "has_js" cookie
	set req.http.Cookie = regsuball(req.http.Cookie, "has_js=[^;]+(; )?", "");

	# Strip cookies with two underscores first (Cloudflare etc)
	set req.http.Cookie = regsuball(req.http.Cookie, "__[a-z]=[^;]+(; )?", "");

	# Remove any Google Analytics based cookies
	set req.http.Cookie = regsuball(req.http.Cookie, "__utm.=[^;]+(; )?", "");
	set req.http.Cookie = regsuball(req.http.Cookie, "_ga=[^;]+(; )?", "");
	set req.http.Cookie = regsuball(req.http.Cookie, "_gat=[^;]+(; )?", "");
	set req.http.Cookie = regsuball(req.http.Cookie, "utmctr=[^;]+(; )?", "");
	set req.http.Cookie = regsuball(req.http.Cookie, "utmcmd.=[^;]+(; )?", "");
	set req.http.Cookie = regsuball(req.http.Cookie, "utmccn.=[^;]+(; )?", "");

	# Remove DoubleClick offensive cookies
	set req.http.Cookie = regsuball(req.http.Cookie, "__gads=[^;]+(; )?", "");

	# Remove the wp-settings-1 cookie
	set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-1=[^;]+(; )?", "");

	# Remove the wp-settings-time-1 cookie
	set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-time-1=[^;]+(; )?", "");

	# Remove the wp test cookie
	set req.http.Cookie = regsuball(req.http.Cookie, "wordpress_test_cookie=[^;]+(; )?", "");

	# Remove a ";" prefix in the cookie if present
	set req.http.Cookie = regsuball(req.http.Cookie, "^;\s*", "");

	# Remove cookies from common plugins
	# Add This
	set req.http.Cookie = regsuball(req.http.Cookie, "__atuv.=[^;]+(; )?", "");
	# WooCommerce
	set req.http.Cookie = regsuball(req.http.Cookie, "wooTracker=[^;]+(; )?", "");
	# EDD
	set req.http.Cookie = regsuball(req.http.Cookie, "edd=[^;]+(; )?", "");
	# WordFence
	set req.http.Cookie = regsuball(req.http.Cookie, "wfvt_[0-9]+=[^;]+(; )?", "");
	set req.http.Cookie = regsuball(req.http.Cookie, "wordfence_verifiedHuman=[^;]+(; )?", "");
	# Remove the Quant Capital cookies (added by some plugin, all __qca)
	set req.http.Cookie = regsuball(req.http.Cookie, "__qc.=[^;]+(; )?", "");

	# Are there cookies left with only spaces or that are empty?
	if (req.http.cookie ~ "^\s*$") {
		unset req.http.cookie;
	}

	# Check for the custom "X-Logged-In" header (used by K2 and other apps) to identify
	# if the visitor is a guest, then unset any cookie (including session cookies) provided
	# it's not a POST request.
	if(req.http.X-Logged-In == "False" && req.method != "POST") {
		unset req.http.Cookie;
	}

	# === STATIC FILES ===
	# Properly handle different encoding types
	if (req.http.Accept-Encoding) {
		if (req.url ~ "\.(jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|ogg|swf)$") {
			# No point in compressing these
			unset req.http.Accept-Encoding;
		} elseif (req.http.Accept-Encoding ~ "gzip") {
			set req.http.Accept-Encoding = "gzip";
		} elseif (req.http.Accept-Encoding ~ "deflate") {
			set req.http.Accept-Encoding = "deflate";
		} else {
			# unknown algorithm (aka crappy browser)
			unset req.http.Accept-Encoding;
		}
	}

	### looks like we might actually cache it!
	# fix up the request
	set req.grace = 2m;

	return(hash);
}

sub vcl_hash {
	# Provide support for SSL termination
	if (req.http.X-Forwarded-Proto ~ "https") {
		hash_data(req.http.X-Forwarded-Proto);
	}

	# Add the browser cookie only when a WordPress cookie found.
	if (req.http.Cookie ~ "wp-postpass_|wordpress_logged_in_|comment_author|PHPSESSID") {
		hash_data(req.http.Cookie);
	}
}

sub vcl_backend_response {

	# Don't cache 50x responses
	if (beresp.status >= 500) {
		return (abandon);
	}

	# === DO NOT CACHE ===
	# Exclude the following paths (e.g. backend admins, user pages or ad URLs that require tracking)
	if(
		bereq.url ~ "add_to_cart" ||
		bereq.url ~ "edd_action" ||
		bereq.url ~ "nocache" ||
		bereq.url ~ "^/addons" ||
		bereq.url ~ "^/bb-admin" ||
		bereq.url ~ "^/bb-login.php" ||
		bereq.url ~ "^/bb-reset-password.php" ||
		bereq.url ~ "^/cart" ||
		bereq.url ~ "^/checkout" ||
		bereq.url ~ "^/control.php" ||
		bereq.url ~ "^/login" ||
		bereq.url ~ "^/logout" ||
		bereq.url ~ "^/lost-password" ||
		bereq.url ~ "^/my-account" ||
		bereq.url ~ "^/product" ||
		bereq.url ~ "^/register" ||
		bereq.url ~ "^/register.php" ||
		bereq.url ~ "^/server-status" ||
		bereq.url ~ "^/signin" ||
		bereq.url ~ "^/signup" ||
		bereq.url ~ "^/stats" ||
		bereq.url ~ "^/wc-api" ||
		bereq.url ~ "^/wp-admin" ||
		bereq.url ~ "^/wp-comments-post.php" ||
		bereq.url ~ "^/wp-cron.php" ||
		bereq.url ~ "^/wp-login.php" ||
		bereq.url ~ "^/wp-activate.php" ||
		bereq.url ~ "^/wp-mail.php" ||
		bereq.url ~ "^/wp-login.php" ||
		bereq.url ~ "^\?add-to-cart=" ||
		bereq.url ~ "^\?wc-api=" ||
		bereq.url ~ "^/preview="
	) {
		set beresp.uncacheable = true;
		return (deliver);
	}

	# Don't cache HTTP authorization/authentication pages and pages with certain headers or cookies
	if (
		bereq.http.Authorization ||
		bereq.http.Authenticate ||
		bereq.http.X-Logged-In == "True" ||
		bereq.http.Cookie ~ "userID" ||
		bereq.http.Cookie ~ "wordpress_(?!test_)[a-zA-Z0-9_]+|wp-postpass|comment_author_[a-zA-Z0-9_]+|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_[a-zA-Z0-9]+|wordpress_logged_in_|comment_author|PHPSESSID"
	) {
		set beresp.http.X-Cacheable = "NO:Logged in/Got Sessions";
		set beresp.uncacheable = true;
		return (deliver);
	}

	# Don't cache ajax requests
	if(beresp.http.X-Requested-With == "XMLHttpRequest" || bereq.url ~ "nocache") {
		set beresp.http.X-Cacheable = "NO:Ajax";
		set beresp.uncacheable = true;
		return (deliver);
	}

	# Don't cache backend response to posted requests
	if (bereq.method == "POST") {
		set beresp.uncacheable = true;
		return (deliver);
	}

	# Ok, we're cool & ready to cache things
	# so let's clean up some headers and cookies
	# to maximize caching.

	# Check for the custom "X-Logged-In" header to identify if the visitor is a guest,
	# then unset any cookie (including session cookies) provided it's not a POST request.
	if(beresp.http.X-Logged-In == "False" && bereq.method != "POST") {
		unset beresp.http.Set-Cookie;
	}

	# If WordFence cookies are found, remove them. They show up here because they get set on the
	# backend of the server.
	if (beresp.http.Set-Cookie && beresp.http.Set-Cookie ~ "wfvt_|wordfence_verifiedHuman") {
		unset beresp.http.Set-Cookie;
	}

	# Catch obvious reasons we cannot cache (i.e. cookies exist)
	if (beresp.http.Set-Cookie) {
		set beresp.http.X-Cacheable = "NO:Got Cookies";
		set beresp.uncacheable = true;
		return (deliver);
	}

	# Varnish determined the object was not cacheable
	if (beresp.ttl <= 0s && beresp.status != 404) {
		set beresp.http.X-Cacheable = "NO:Not Cacheable";
		set beresp.uncacheable = true;
		return (deliver);
	# You are respecting the Cache-Control=private header from the backend
	} else if (beresp.http.Cache-Control ~ "private") {
		set beresp.http.X-Cacheable = "NO:Cache-Control=private";
		set beresp.uncacheable = true;
		return (deliver);
	# You are extending the lifetime of the object artificially
	} else if (beresp.ttl < 60m) {
		set beresp.ttl   = 60m;
		set beresp.grace = 60m;
		set beresp.http.X-Cacheable = "YES:Forced";
	# Varnish determined the object was cacheable
	} else {
		set beresp.http.X-Cacheable = "YES";
	}

	# Unset the "pragma" header (suggested)
	unset beresp.http.Pragma;

	# Unset the "vary" header (suggested)
	unset beresp.http.Vary;

	# Unset the "etag" header (optional)
	#unset beresp.http.etag;

	# Allow stale content, in case the backend goes down
	set beresp.grace = 24h;

	# Enforce your own cache TTL (optional)
	set beresp.ttl = 60m;

	# Modify "expires" header - https://www.varnish-cache.org/trac/wiki/VCLExampleSetExpires (optional)
	if (beresp.http.Expires == "") {
		set beresp.http.Expires = "" + (now + beresp.ttl);
	}

	## Add support for gzip/brotli
	if (beresp.http.content-type ~ "text") {
		set beresp.do_gzip = true;
	}

	# We have content to cache, but it's got no-cache or other Cache-Control values sent
	# The additional parameters specified (stale-while-revalidate & stale-if-error) are used
	# by modern browsers to better control caching. Set these to twice & four times your main
	# cache time respectively.
	# This final setting will normalize cache-control headers that set max-age=0 even when
	# the CMS' cache is enabled.
	if (beresp.http.Cache-Control !~ "max-age" || beresp.http.Cache-Control ~ "max-age=0") {
		set beresp.http.Cache-Control = "public, max-age=3600, stale-while-revalidate=360, stale-if-error=43200";
	}

	# Optionally set a larger TTL for pages with less than the timeout of cache TTL
	if (beresp.ttl < 3600s) {
		set beresp.http.Cache-Control = "public, max-age=3600, stale-while-revalidate=360, stale-if-error=43200";
	}

	return (deliver);

}

sub vcl_deliver {

	# Send a special header for excluded domains only
	if (
		req.http.host ~ ".stage.site" ||
		req.http.host ~ ".dream.website"
	) {
		set resp.http.X-Domain-Status = "EXCLUDED";
	}

	# Send special headers that indicate the cache status of each web page
	if (obj.hits > 0) {
		set resp.http.X-Cache = "HIT";
		set resp.http.X-Cache-Hits = obj.hits;
	} else {
		set resp.http.X-Cache = "MISS";
		# Force revalidate for browser if the cache is missed.
		set resp.http.Cache-Control = "must-revalidate, max-age=0";
	}

	# Remove headers we do not use
	unset resp.http.X-Cache-Hits;

	# Add powered by
	set resp.http.X-Powered-By = "DreamPress";

	return (deliver);

}

Clone this wiki locally