Varnish at the front of WordPress + Apache and Plone CMS virtual hosts

When moving some sites to a new server I upgraded the Varnish cache server configuration serving this setup. Here are my notes how one can use Varnish at the front of virtual hosting.

The setup is following

  • Multiple sites are hosted on the same server. The sites are mix of PHP of Plone sites.
  • Varnish accepts HTTP requests in port 80 and forwards request to the corresponding backend through HTTP proxying
  • Our virtual host rules capture domain names with or without www-prefix, or with any subdomain name prefix
  • Apache runs in non-standard port localhost:81, serving PHP and WordPress. WordPress caching rules are defined in Apache <virtualhost> config file.
  • Every Plone site runs in its own port and process. Plone uses VirtualHostMonster to rewrite publicly facing site URLS. Plone caching HTTP headers are set by plone.app.caching addon.
  • We do extensive cookie sanitization for PHP (generic), WordPress and Plone.  Google Analytics etc. cookies don’t bust the cache and we can still login to WordPress and Plone as admin
  • As a special trick, there is cleaned cookie debugging trick through HTTP response headers

1-IMG_2677

Don’t worry, Varnish can handle little load

 

Pros

  • Blazingly fast, as Varnish is
  • With Plone’s plone.app.caching, one does not need to touch configuration files but Plone caching HTTP headers can be configured through-the-web

Cons

  • Varnish does not have Nginx or Apache style virtual host configuration file facilities by default and making includes is little bit tricky: With many virtualhost the default.vcl config file grows long.
  • Because WordPress cannot do static resource serving as smartly as Plone, which has unique URLs for all static media revisions, you need to purge Varnish manually from command line if you update any static media files like CSS, JS or images.

Varnish /etc/varnish/default.vcl example for Varnish 3.0:

#
# This backend never responds... we get hit in the case of bad virtualhost name
#
backend default {
    .host = "127.0.0.1";
    .port = "55555";
}

backend myplonesite {
    .host = "127.0.0.1";
    .port = "6699";
}

#
# Apache running on server port 81
#
backend apache {
    .host = "127.0.0.1";
    .port = "81";
}

#
# Gues which site / virtualhost we are diving into.
# Apache, Nginx or Plone directly
#
sub choose_backend {

    # WordPress site
    if (req.http.host ~ "^(.*\.)?opensourcehacker\.com(:[0-9]+)?$") {
        set req.backend = apache;
    }

    # Example Plone site
    if (req.http.host ~ "^(.*\.)?myplonesite\.fi(:[0-9]+)?$") {
        set req.backend = myplonesite;

        # Zope VirtualHostMonster
        set req.url = "/VirtualHostBase/http/" + req.http.host + ":80/Plone/VirtualHostRoot" + req.url;

    }

}

sub vcl_recv {

    #
    # Do Plone cookie sanitization, so cookies do not destroy cacheable anonymous pages.
    # Also, make sure we do not destroy WordPress admin and login cookies in the proces
    #
    if (req.http.Cookie && !(req.url ~ "wp-(login|admin)")) {
        set req.http.Cookie = ";" + req.http.Cookie;
        set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";");
        set req.http.Cookie = regsuball(req.http.Cookie, ";(statusmessages|__ac|_ZopeId|__cp|php|PHP|wordpress_(.*))=", "; \1=");
        set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", "");

        if (req.http.Cookie == "") {
            remove req.http.Cookie;
        }
    }

    call choose_backend;

    if (req.request != "GET" &&
      req.request != "HEAD" &&
      req.request != "PUT" &&
      req.request != "POST" &&
      req.request != "TRACE" &&
      req.request != "OPTIONS" &&
      req.request != "DELETE") {
        /* Non-RFC2616 or CONNECT which is weird. */
        return (pipe);
    }
    if (req.request != "GET" && req.request != "HEAD") {
        /* We only deal with GET and HEAD by default */
        return (pass);
    }
    if (req.http.Authorization || req.http.Cookie) {
        /* Not cacheable by default */
        return (pass);
    }
    return (lookup);
}

sub vcl_fetch {

    /* Use to see what cookies go through our filtering code to the server */
    /* set beresp.http.X-Varnish-Cookie-Debug = "Cleaned request cookie: " + req.http.Cookie; */

    if (beresp.ttl <= 0s ||
        beresp.http.Set-Cookie ||
        beresp.http.Vary == "*") {
        /*
         * Mark as "Hit-For-Pass" for the next 2 minutes
         */
        set beresp.ttl = 120 s;
        return (hit_for_pass);
    }
    return (deliver);
}

#
# Show custom helpful 500 page when the upstream does not respond
#
sub vcl_error {
  // Let's deliver a friendlier error page.
  // You can customize this as you wish.
  set obj.http.Content-Type = "text/html; charset=utf-8";
  synthetic {"
  <?xml version="1.0" encoding="utf-8"?>
  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  <html>
    <head>
      <title>"} + obj.status + " " + obj.response + {"</title>
      <style type="text/css">
      #page {width: 400px; padding: 10px; margin: 20px auto; border: 1px solid black; background-color: #FFF;}
      p {margin-left:20px;}
      body {background-color: #DDD; margin: auto;}
      </style>
    </head>
    <body>
    <div id="page">
    <h1>Sivu ei ole saatavissa</h1>
    <p>Pahoittelemme, mutta palvelua ei ole saatavilla.</p>
    <hr />
    <h4>Debug Info:</h4>
    <pre>Status: "} + obj.status + {"
Response: "} + obj.response + {"
XID: "} + req.xid + {"</pre>
      </div>
    </body>
   </html>
  "};
  return(deliver);
}

WordPress does not support setting HTTP response headers natively like Plone. We set them in Apache virtual host configuration file in /etc/apache2/sites-enabled:

<VirtualHost 127.0.0.1:81>

    ServerName opensourcehacker.com
    ServerAlias www.opensourcehacker.com
    ServerAdmin mikko@opensourcehacker.com

    LogFormat       combined
    TransferLog     /var/log/apache2/opensourcehacker.com.log

    # Basic WordPress setup

    Options +Indexes FollowSymLinks +ExecCGI

    DocumentRoot /srv/php/opensourcehacker/wordpress

    <Directory /srv/php/opensourcehacker/wordpress>
        Options FollowSymlinks
        AllowOverride All
    </Directory>

    AddType text/css .css
    AddType application/x-httpd-php .php .php3 .php4 .php5
    AddType application/x-httpd-php-source .phps

    #
    # Set expires headers manually
    #
    ExpiresActive On
    ExpiresByType text/html A0
    ExpiresByType image/gif A3600
    ExpiresByType image/png A3600
    ExpiresByType image/image/vnd.microsoft.icon A3600
    ExpiresByType image/jpeg A3600
    ExpiresByType text/css A3600
    ExpiresByType text/javascript A3600
    ExpiresByType application/x-javascript A3600

</VirtualHost>

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

皆さんが大好きです

Instead of technical or depressing blog post I’ll give you these images. It happened somewhere around here.

Image

I accidentally a lot of  new friends. Some may have names  consisting entirely of Unicode characters

Image

An API developer living in my pocket, with an Argentina guy who has a superpower to transform to a Finn when entering  a bar.

Image

A panel discussion how to deal with users opening Feature Requests

Image

Plone girls causing Cute Overload. Take that, Ruby, as we have also handsome tall German gentlemen!

Image

A surprising CSS: float without clear, even after packing 100 web developers on it. Maybe we could drive this thing all the way down to Brazil to make the sundeck more useful next year?

Image

Plone Foundation is one of the oldest software foundations after Apache. Happy 10th Ploneconf.

 

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

HTTP Referer based redirects in Apache

Referer HTTP header is a HTTP request field tellling the page the browser was on prior clicking link on it. It answers the question “where the user is coming from to my site”. Also the browsers use it for telling on which page images and other media was referred or embedded in.  The most common use case using HTTP Referer redicts is prevent image hot linking, but I had a use case where I’d like to migrate some traffic to a new domain, based on where the traffic is coming from. A referer based redirect is a solution for this.

HTTP Referer header is HTTP_REFERER environment variable in Apache mod_rewrite procesing context.

Here is a sample snippet which you can toss into virtualhost conf or .htaccess. It matches domain name in any part of HTTP referer (path part too) so it is not water-proof, but for 99,99% cases it works. If the user is coming to some of the mentioned domains the user will be redirected to the new site.

RewriteEngine On
RewriteCond %{HTTP_REFERER} .*plone.org.*$ [OR]
RewriteCond %{HTTP_REFERER} .*ubuntuforums.org.*$ [OR]
RewriteCond %{HTTP_REFERER} .*stackoverflow.com.*$ [OR]
RewriteCond %{HTTP_REFERER} .*sourceforge.net.*$ [OR]
RewriteCond %{HTTP_REFERER} .*ubuntu.ru.*$ [OR]
RewriteCond %{HTTP_REFERER} .*python.org.*$ [OR]
RewriteCond %{HTTP_REFERER} .*kotelett.no.*$
RewriteRule ^(.*)$ http://opensourcehacker.com/

More info

(I hate it when Apache cannot suggest links to the current version of documentation)

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+

Generating Apache redirects from sitemap.xml for WordPress using Python

Sitemap.xml (Sitemaps) is a protocol exposing website pages for the search engines. Beyond hinting crawler bots you can find some creative uses for it.

I was migrating blog posts from one WordPress site to another. I wanted to have the old site still run the front page and few other static pages and I wanted to move the blog posts only. A best practice for content migrations is set HTTP 301 Moved Permantently redirects on the orignal web server so that users and search engines following old links will automatically find the new content.

WordPress sites were and are hosted on Apache web server. Since writing Apache .htaccess redirects by hand is something no living soul wants to do, I wrote a little Python script using Requests (a.k.a. urllib done right) and lxml libraries.

This script will

  • Read sitemap.xml.gz file
  • Extract page URLs out of it
  • (add optional filtering step here)
  • Output Apache .htaccess redirect rules so that old links will point to the new site

The beauty of scraping the information from sitemap.xml: Even though this script was written for WordPress it is valid for any CMS supporting Sitemaps protocol. (hint: Plone, Drupal, Joomla!, you name it.. they all support it)

In theory one could run this script in cron so that all new WordPress content gets automatically redirected continuously.

In additional for setting up .htaccess WordPress provides easy export and import tool for blog posts through its admin interface.

And the script:

# Python stdlib
from StringIO import StringIO
import urlparse

# 3rd party
import requests
from lxml import etree

# Sitemap of the source site
SITEMAP_URL = "http://mobilizejs.com/wp-content/plugins/simple-google-sitemap-xml/sitemap.xml"

# Where posts will be moved
NEW_DOMAIN="http://opensourcehacker.com"

# Apache .htaccess line template
# http://httpd.apache.org/docs/2.0/mod/mod_alias.html#redirectmatch
REDIRECT_TEMPLATE="RedirectMatch permanent %(path)s$ %(new_domain)s%(path)s"

response = requests.get(SITEMAP_URL)

def create_apache_redirect_to_other_domain(url, new_domain):
    """
    Creates .htaccess redirect entry.

    Create HTTP 301 Permanent Redirect.

    Output is something along lines::

        RedirectMatch permanent /tag/cache-control/$ http://opensourcehacker.com/tag/cache-control/

    """
    parts = urlparse.urlparse(url)

    vars = {
        "new_domain" : new_domain,
        "path" : parts.path          
    }    

    # Simple Python string template substituion
    return REDIRECT_TEMPLATE % vars

# Parse sitemap
tree = etree.parse(StringIO(response.content))
root = tree.getroot()

locs = root.xpath("//*[name()='loc']")

# Iterate through each sitemap <loc> entry and creating corresponding redirect
for loc in locs:
    url = loc.text
    redirect_line = create_apache_redirect_to_other_domain(url, NEW_DOMAIN)
    print redirect_line

 

\"\" Subscribe to RSS feed Image Follow me on Twitter Image Follow me on Facebook Image Follow me Google+