Let's say we want to provide a unique CSRF token for each response cached by Varnish.
Is there a way hook Varnish output so that we can change a specific part of the content programmatically?
Varnish Cache solution with ESI & vmod_digest
In the open source version of Varnish, you can use Edge Side Includes for this.
You can put the following place holder in your output:
<esi:include src="/csrf-token-esi" />
This ESI tag will be parsed by Varnish and the /csrf-token-esi endpoint can be intercepted by Varnish and changed.
Be sure to return a Surrogate-Control: Varnish=ESI/1.0 response header on the page that contains the ESI tag. This header will force Varnish to parse the ESI tag.
Via synthetic responses, we can change the response body of /csrf-token-esi.
Here's an example where we use vmod_digest to create an HMAC signature as the CSRF token:
vcl 4.1;
import digest;
backend default {
.host = "localhost";
.port = "8080";
}
sub vcl_recv {
set req.http.Surrogate-Capability = "Varnish=ESI/1.0";
if(req.url == "/csrf-token-esi") {
return(synth(777,digest.hmac_sha256("secret-key", now)));
}
}
sub vcl_backend_response {
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}
}
sub vcl_synth {
if(resp.status == 777) {
set resp.body = resp.reason;
set resp.http.csrf-token = resp.reason;
set resp.status = 200;
return(deliver);
}
}
FYI: you can download the vmod_digest source from https://github.com/varnish/libvmod-digest. You need to compile this from source or use another mechanism to create a unique value.
The value of the CSRF token is generated by the following line of code:
return(synth(777,digest.hmac_sha256("secret-key", now)));
The secret-key is the HMAC's signing key and now returns the current timestamp in Varnish which is the value that will be signed.
There are plenty of other ways to generate a CSRF token. vmod_digest also has other ways to generate unique output. Just remember that the 2nd argument of the synth() function is the response body and effectively the value of the CSRF token.
Varnish Enterprise solution with vmod_edgestash & vmod_crypto
In Varnish Enterprise all the necessary VMODs are packaged, so no need to compile them from source. The vmod_edgestash and vmod_crypto VMODs are enterprise VMODs.
The solution consists of adding a {{csrf}} placeholder in your response. The format of this placeholder is Mustache syntax and is parsed by vmod_edgestash.
Unlike the Varnish Cache solution, no extra roundtrips and request interception are required. Varnish Enterprise will cache the page that includes the placeholder and will customize the CSRF token upon delivery.
Here's the VCL code:
vcl 4.1;
import crypto;
import edgestash;
backend default {
.host = "localhost";
.port = "8080";
}
sub vcl_backend_response{
if (beresp.http.Content-Type ~ "html") {
edgestash.parse_response();
}
}
sub vcl_deliver {
if (edgestash.is_edgestash()) {
set req.http.csrf = crypto.hex_encode(crypto.urandom(16));
edgestash.add_json({"
{"csrf":""} + req.http.csrf + {""
}
"});
edgestash.execute();
}
}
In this case we're using crypto.hex_encode(crypto.urandom(16)) to generate a unique value. But again, you can use any function that vmod_crypto provides to generate this unique value.
See https://docs.varnish-software.com/varnish-cache-plus/vmods/edgestash/ for more information about vmod_edgestash and https://docs.varnish-software.com/varnish-cache-plus/vmods/total-encryption/ for more information about vmod_crypto.
Related
Using varnish 4 to cache different content of same request from multiple servers. It looks like it caches the first request from one server and keeps giving the same content for every subsequent request.
doing curl gives response with two caches and different age.
Are there any factors like load or anything else for stickiness behaviour?
Used Jmeter and apache benchmark with load but still got the same behaviour.
Is my vcl_hash is good? Want to save the object with hash combination of url and ip of backend server.
Atleast in my case, looks like after the ttl of the cache object, varnish is caching from second server and returns the same until ttl is done. But this is not what we expect it to behave?
am I missing anything?
using round robin and hash_data. below is my config.vcl
backend s1{
.host = "190.120.90.1";
}
backend s2{
.host = "190.120.90.2";
}
sub vcl_init {
new vms = directors.round_robin();
vms.add_backend(s1);
vms.add_backend(s2);
}
sub vcl_recv {
set req.backend_hint = vms.backend();
}
sub vcl_hash {
hash_data(req.url);
if (req.http.host) {
hash_data(req.http.host);
} else {
hash_data(server.ip);
}
return(lookup);
}
First thing to consider is that you are going to have the backend ip only after the object has been fetched from it. So you are not able to use that ip on your hash method because vcl_hash occurs before fetch.
Second is about the round robin. It takes place only when Varnish is fetching the objects, hence it will not take place when an object has already been cached.
To answer your question precisely it is needed to know why your application delivers different content for the same request. How do you indicate which backend is being requested if the request is always the same? There must be something like a cookie, a header or the origin IP of the request that should dictate which one must respond to that request.
Knowing that it is possible to set the specific backend and use it in your vcl_hash. For example purposes, let's assume that you want to set your backends based on the presence of a header named backend_choice:
sub vcl_recv {
if (req.http.backends_choice == "s1") {
set req.backend_hint = s1;
# If the header is not "s1" or does not exist
} else {
set req.backend_hint = s2;
}
...
}
sub vcl_hash {
hash_data(req.url);
if (req.http.host) {
hash_data(req.http.host);
} else {
hash_data(server.ip);
}
# We use the selected backend to hash the object
hash_data(req.backend_hint);
return(lookup);
}
Hope this answer address your needs. If there was something that I missed, feel free to comment or add to your question and I'll be glad to add some info to my answer.
We are serving javascript resources (and others) via wro in our webapp.
On the PROD environment, the browser gets (for example) the app.js angular webapp's content with an 'expires' headers one year in the future.
Meaning that for subsequent requests the browser takes it from cache without a request to the server.
If we deploy a new version of the webapp, the browser does not get the new version, as it takes it from the local cache.
The goal is to configure wro or/and spring so that the headers will be correctly set to have the browser perform the request each time, and the server return a 304 not modified. So we would have the clients automatically "updated" uppon new deployment.
Did someone already achieve this?
We use Spring's Java Configuration:
#Configuration
public class Wro4jConfiguration {
#Value("${app.webapp.web.minimize}")
private String minimize;
#Value("${app.webapp.web.disableCache}")
private String disableCache;
#Autowired
private Environment env;
#Bean(name = "wroFilter")
public WroFilter wroFilter() {
ConfigurableWroFilter filter = new ConfigurableWroFilter();
filter.setWroManagerFactory(new Wro4jManagerFactory());
filter.setWroConfigurationFactory(createProperties());
return filter;
}
private PropertyWroConfigurationFactory createProperties() {
Properties props = new Properties();
props.setProperty("jmxEnabled", "false");
props.setProperty("debug", String.valueOf(!env.acceptsProfiles(EnvConstants.PROD)));
props.setProperty("gzipResources", "false");
props.setProperty("ignoreMissingResources", "true");
props.setProperty("minimizeEnabled", minimize);
props.setProperty("resourceWatcherUpdatePeriod", "0");
props.setProperty("modelUpdatePeriod", "0");
props.setProperty("cacheGzippedContent", "false");
// let's see if server-side cache is disabled (DEV only)
if (Boolean.valueOf(disableCache)) {
props.setProperty("resourceWatcherUpdatePeriod", "1");
props.setProperty("modelUpdatePeriod", "5");
}
return new PropertyWroConfigurationFactory(props);
}
}
By default, WroFilter set the following headers: ETag (md5 checksum of the resource), Cache-Control (public, max-age=315360000), Expires (1 year since resource creation).
There are plenty of details about the significance of those headers. The short explanation is this:
When the server reads the ETag from the client request, the server can determine whether to send the file (HTTP 200) or tell the client to just use their local copy (HTTP 304). An ETag is basically just a checksum for a file that semantically changes when the content of the file changes. If only ETag is sent, the client will always have to make a request.
The Expires and Cache-Control headers are very similar and are used by the client (and proxies/caches) to determine whether or not it even needs to make a request to the server at all.
So really what you want to do is use BOTH headers - set the Expires header to a reasonable value based on how often the content changes. Then configure ETags to be sent so that when clients DO send a request to the server, it can more easily determine whether or not to send the file back.
If you want the client always to check for the latest resource version, you should not send the expires & cache-control headers.
Alternatively, there is a more aggressive caching technique: encode the checksum of the resource into its path. As result, every time a resource is changed, the path to that resource is changed. This approach guarantee that the client would always request the most recent version. For this approach, in theory the resources should never expire, since the checksum change every time a resource is changed.
Based on Alex's information and documentation reference, I ended up overriding WroFilter.setResponseHeaders to put appropriate expire values.
This is working fine. Wro already takes care of setting ETag, Date and others, so I only overwrite the expiration delay and date.
#Configuration
public class Wro4jConfiguration {
#Value("${app.webapp.web.browserCache.maxAgeInHours}")
private String maxAgeInHours;
#Bean(name = "wroFilter")
public WroFilter wroFilter() {
ConfigurableWroFilter filter = createFilter();
filter.setWroManagerFactory(new Wro4jManagerFactory());
filter.setWroConfigurationFactory(createProperties());
return filter;
}
private ConfigurableWroFilter createFilter() {
return new ConfigurableWroFilter() {
private final int BROWSER_CACHE_HOURS = Integer.parseInt(maxAgeInHours);
private final int BROWSER_CACHE_SECONDS = BROWSER_CACHE_HOURS * 60 * 60;
#Override
protected void setResponseHeaders(final HttpServletResponse response){
super.setResponseHeaders(response);
if (!getConfiguration().isDebug()) {
ZonedDateTime cacheExpires = ZonedDateTime.of(LocalDateTime.now(), ZoneId.of("GMT")).plusHours(BROWSER_CACHE_HOURS);
String cacheExpiresStr = cacheExpires.format(DateTimeFormatter.RFC_1123_DATE_TIME);
response.setHeader(HttpHeader.EXPIRES.toString(), cacheExpiresStr);
response.setHeader(HttpHeader.CACHE_CONTROL.toString(), "public, max-age=" + BROWSER_CACHE_SECONDS);
}
}
};
}
// Other config methods
}
I read several post talking about some similar problems, but I don't get yet to do this to work.
I'm doing ajax to "Account/ExternalLogin" which generates the ChallengeResult and starts the flow for the authentication with OWIN.
This is my Startup class :
public partial class Startup
{
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login")
});
// Use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseCors(CorsOptions.AllowAll);
var goath2 = new Microsoft.Owin.Security.Google.GoogleOAuth2AuthenticationOptions
{
ClientId = "myclientid",
ClientSecret = "mysecret",
Provider = new Microsoft.Owin.Security.Google.GoogleOAuth2AuthenticationProvider
{
OnApplyRedirect = context =>
{
string redirect = context.RedirectUri;
const string Origin = "Origin";
const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
// origin is https://localhost:44301
var origin = context.Request.Headers.GetValues(Origin).First();
// header is present
var headerIsPresent = context.Response.Headers.ContainsKey(AccessControlAllowOrigin);
context.Response.Redirect(redirect);
}
}
};
app.UseGoogleAuthentication(goath2);
}
}
I'm enabling CORS support whith the line app.UserCors(CorsOptinos.AllowAll);
And I know the header is being added to the response because I intercept the OnApplyRedirectevent and when I look for the origin it is setted to 'localhost:443001' and the header 'Access-Control-Allow-Origin' is setted also to this value.
Nevertheless when the response is sent to the client I have the following error:
XMLHttpRequest cannot load
https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=xxxxxxxxxxxxxxxxxxxxxxxxxxx
No 'Access-Control-Allow-Origin' header is present on the requested
resource. Origin https://localhost:44301 is therefore not allowed
access.
What I'm missing here.
I could get a work around doing all this "manually" (requesting directly google from the client...) but I really want to use the OWIN middleware.
You are making request to google from https://localhost:44301 domain. In order that to work 'Access-Control-Allow-Origin' should have 'https://localhost:44301' origin domain in the list. So in this case it's google who need to set this domain in the 'Access-Control-Allow-Origin'.
Looking at the response you are getting it seems that google doesn't allow Cros origin request from your domain. To solve this I believe you need to register your domain on google https://developers.google.com.
I am working on this Sitecore project and am using WebApi to perform some service calls. My methods are decorated with CacheOutput information like this:
[HttpGet]
[CacheOutput(ClientTimeSpan = 3600, ServerTimeSpan = 3600)]
I am testing these calls using DHC app on Google Chrome. I am sure that the ClientTimespan is set correctly but the response headers that i am getting back are not what i am expecting. I would expect that Cache-Control would have a max-age of 1hour as set by the ClientTimespan attribute but instead it is set to private.
I have been debugging everything possible and t turns out that Sitecore may be intercepting the response and setting this header value to private. I have also added the service url to the sitecore ignored url prefixes configuration but no help .
Does anyone have an idea how I can make Sitecore NOT change my Cache-Control headers?
This is default MVC behaviour and not directly related to Sitecore / Web API.
You can create a custom attribute that sets the Cache-Control header:
public class CacheControl : System.Web.Http.Filters.ActionFilterAttribute
{
public int MaxAge { get; set; }
public CacheControl()
{
MaxAge = 3600;
}
public override void OnActionExecuted(HttpActionExecutedContext context)
{
context.Response.Headers.CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(MaxAge)
};
base.OnActionExecuted(context);
}
}
Which enables you to add the [CacheControl(MaxAge = n)] attribute to your methods.
Code taken from: Setting HTTP cache control headers in WebAPI (answer #2)
Or you can apply it globally throughout the application, as explained here: http://juristr.com/blog/2012/10/output-caching-in-aspnet-mvc/
How Multi-Language Magento store work with Varnish.
Is there any configuration available in varnish,so we can create cache base on cookies?
If you don't mind the languages being at different urls, Turpentine can handle this for you: https://github.com/nexcess/magento-turpentine/issues/36
If you want them to behave as they do out of the box, lets keep going.
You have to modify how varnish generates the has in your VCL
Reference: https://www.varnish-cache.org/trac/wiki/VCLExampleCachingLoggedInUsers
We would modify this to also take into account the store cookie that Magento sets based on the language selector. (Following the behavior here: http://demo.magentocommerce.com) Unfortunately this gets tricky as Varnish tends to either not pass cookies back to the server or not cache things when it sees cookies flying around
This would have Varnish cache based on the value of the cookie as well as the default url and host:
sub vcl_hash {
hash_data(req.url);
hash_data(req.http.host);
if (req.http.Cookie ~ "(?:^|;\s*)(?:store=(.*?))(?:;|$)"){
hash_data(regsub(req.http.Cookie, "(?:^|;\s*)(?:store=(.*?))(?:;|$)"));
}
return (hash);
}
But, with this method you might have to tweak the rest of your VCL to cache the page properly AND send the cookies back to the server
Another option is to use the cookie to vary caching on an arbitrary header, lets call it X-Mage-Lang:
sub vcl_fetch {
#can do this better with regex
if (req.http.Cookie ~ "(?:^|;\s*)(?:store=(.*?))(?:;|$)"){
if (!beresp.http.Vary) { # no Vary at all
set beresp.http.Vary = "X-Mage-Lang";
} elseif (beresp.http.Vary !~ "X-Mage-Lang") { # add to existing Vary
set beresp.http.Vary = beresp.http.Vary + ", X-Mage-Lang";
}
}
# comment this out if you don't want the client to know your classification
set beresp.http.X-Mage-Lang = regsub(req.http.Cookie, "(?:^|;\s*)(?:store=(.*?))(?:;|$)");
}
This pattern is also used for device detection with varnish: https://github.com/varnish/varnish-devicedetect/blob/master/INSTALL.rst
Then, you would have to extend Mage_Core_Model_App to use this header instead of the 'store' cookie. In Magento CE 1.7 its _checkCookieStore :
protected function _checkCookieStore($type)
{
if (!$this->getCookie()->get()) {
return $this;
}
$store = $this->getCookie()->get(Mage_Core_Model_Store::COOKIE_NAME);
if ($store && isset($this->_stores[$store])
&& $this->_stores[$store]->getId()
&& $this->_stores[$store]->getIsActive()) {
if ($type == 'website'
&& $this->_stores[$store]->getWebsiteId() == $this->_stores[$this->_currentStore]->getWebsiteId()) {
$this->_currentStore = $store;
}
if ($type == 'group'
&& $this->_stores[$store]->getGroupId() == $this->_stores[$this->_currentStore]->getGroupId()) {
$this->_currentStore = $store;
}
if ($type == 'store') {
$this->_currentStore = $store;
}
}
return $this;
}
You would set the current store on $_SERVER['X-Mage-Lang'] instead of the cookie
Add Following lines in Varnish Config,
if(beresp.http.Set-Cookie) {
return (hit_for_pass);
}