You are here

varnish.module in Varnish 6

Same filename and directory in other branches
  1. 8 varnish.module
  2. 5 varnish.module
  3. 7 varnish.module

varnish.module Provide drupal hooks for integration with the Varnish control layer.

File

varnish.module
View source
<?php

define('VARNISH_NO_CLEAR', 0);
define('VARNISH_DEFAULT_CLEAR', 1);
define('VARNISH_SELECTIVE_CLEAR', 2);

// Requires Expire.module to be enabled.
define('VARNISH_DEFAULT_TIMETOUT', 100);

// 100ms
define('VARNISH_SERVER_STATUS_DOWN', 0);
define('VARNISH_SERVER_STATUS_UP', 1);
define('VARNISH_BANTYPE_NORMAL', 0);
define('VARNISH_BANTYPE_BANLURKER', 1);
define('VARNISH_DEFAULT_BANTYPE', VARNISH_BANTYPE_NORMAL);

/**
 * @file
 * varnish.module
 * Provide drupal hooks for integration with the Varnish control layer.
 */

/**
 * Implementation of hook_menu()
 *
 * Set up admin settings callbacks, etc.
 */
function varnish_menu() {
  $items = array();
  $items['admin/settings/varnish'] = array(
    'title' => 'Varnish settings',
    'description' => 'Configure your varnish integration.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'varnish_admin_settings_form',
    ),
    'access arguments' => array(
      'administer varnish',
    ),
    'file' => 'varnish.admin.inc',
  );
  $items['admin/settings/varnish/general'] = array(
    'title' => 'General',
    'description' => 'Configure Varnish servers and cache invalidation settings',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -5,
  );

  // Varnish 3 has removed the stats command from the terminal, so
  // we can't provide this functionality for varnish 3 users
  // currently.
  if (floatval(variable_get('varnish_version', 2.1)) < 3) {
    $items['admin/reports/varnish'] = array(
      'title' => 'Varnish status',
      'description' => 'Get the output of varnishstat.',
      'page callback' => 'varnish_admin_reports_page',
      'access arguments' => array(
        'administer varnish',
      ),
      'file' => 'varnish.admin.inc',
    );
  }
  return $items;
}

/**
 * Implementation of hook_theme().
 */
function varnish_theme() {
  return array(
    'varnish_status' => array(
      'arguments' => array(
        'status' => array(),
        'version' => 2.1,
      ),
    ),
  );
}

/**
 * Implemetation of hook_perm()
 *
 * Allows admins to control access to varnish settings.
 */
function varnish_perm() {
  return array(
    'administer varnish',
  );
}

/**
 * Implementation of hook_requirements()
 *
 * Ensure that varnish's connection is good.
 */
function varnish_requirements($phase) {
  if ($phase == 'runtime') {
    $requirements = array(
      'varnish' => array(),
    );
    $requirements['varnish']['title'] = t('Varnish status');

    // try a varnish admin connect, report results
    $status = _varnish_terminal_run(array(
      'status',
    ));
    $terminals = explode(' ', variable_get('varnish_control_terminal', '127.0.0.1:6082'));
    foreach ($terminals as $term) {
      list($server, $port) = explode(':', $term);
      $stat = array_shift($status);
      if ($stat['status']['code'] === FALSE) {
        $requirements['varnish']['value'] = t('Varnish connection broken');
        $requirements['varnish']['severity'] = REQUIREMENT_ERROR;
        $requirements['varnish']['description'] = t('The Varnish control terminal is not responding at %server on port %port.', array(
          '%server' => $server,
          '%port' => $port,
        ));
        return $requirements;
      }
      else {
        $requirements['varnish']['value'] = t('Varnish running. Observe more detailed statistics !link.', array(
          '!link' => l(t('here'), 'admin/reports/varnish'),
        ));
      }
    }
    return $requirements;
  }
}

/**
 * Implementation of hook_nodeapi()
 *
 * Used to pick up cache_clearing events
 */
function varnish_nodeapi(&$node, $op) {

  // We've probably just run through node_save, and normally this is where
  // Drupal calls a cache_clear_all().
  if (in_array($op, array(
    'insert',
    'update',
    'delete',
    'delete revision',
  )) && variable_get('varnish_cache_clear', VARNISH_DEFAULT_CLEAR) == VARNISH_DEFAULT_CLEAR) {
    varnish_purge_all_pages();
  }
}

/**
 * Implementation of hook_comment()
 *
 * Used to pick up cache_clearing events
 */
function varnish_comment($comment, $op) {
  if (in_array($op, array(
    'insert',
    'update',
    'publish',
    'unpublish',
    'delete',
  )) && variable_get('varnish_cache_clear', VARNISH_DEFAULT_CLEAR) == VARNISH_DEFAULT_CLEAR) {
    varnish_purge_all_pages();
  }
}

/**
 * Implements hook_user().
 *
 * Flush cache on user modifications.
 */
function varnish_user($op, &$edit, &$account, $category = NULL) {
  if (in_array($op, array(
    'insert',
    'delete',
    'after_update',
  )) && variable_get('varnish_cache_clear', VARNISH_DEFAULT_CLEAR) == VARNISH_DEFAULT_CLEAR) {
    varnish_purge_all_pages();
  }
}

/**
 * Implementation of hook_expire_cache
 *
 * Takes an array from expire.module and issue purges.
 *
 * You may also safely call this function directly with an array of local urls to purge.
 */
function varnish_expire_cache($paths) {
  $host = _varnish_get_host();
  $base = base_path();
  $purge = implode('$|^' . $base, $paths);
  $purge = '^' . $base . $purge . '$';
  varnish_purge($host, $purge);
}

/**
 * Implementation of hook_form_alter()
 *
 * Add our submit callback to the "clear caches" button.
 */
function varnish_form_alter(&$form, $form_state, $form_id) {
  if ($form_id == 'system_performance_settings') {
    $form['clear_cache']['clear']['#submit'][] = 'varnish_purge_all_pages';
  }
}

/**
 * Implementation of hook_flush_caches()
 *
 * Flush caches on events like cron.
 *
 * This borrows logic from cache_clear_all() to respect cache_lifetime.
 */
function varnish_flush_caches() {
  if (variable_get('varnish_flush_cron', 0)) {
    if (variable_get('cache_lifetime', 0)) {
      $cache_flush = variable_get('cache_flush_varnish', 0);
      if ($cache_flush == 0) {

        // This is the first request to clear the cache, start a timer.
        variable_set('cache_flush_varnish', time());
      }
      elseif (time() > $cache_flush + variable_get('cache_lifetime', 0)) {
        varnish_purge_all_pages();
      }
    }
    else {
      varnish_purge_all_pages();
    }
  }
}

/**
 * Helper function to quickly flush all caches for the current site.
 */
function varnish_purge_all_pages() {
  $path = base_path();
  $host = _varnish_get_host();
  varnish_purge($host, $path);
}

/**
 * Helper function to purge items for a host that matches the provided pattern.
 * @param string $host the host to purge.
 * @param string $pattern the pattern to look for and purge.
 */
function varnish_purge($host, $pattern) {

  // Get the current varnish version, if we are using Varnish 3.x, then we can
  // need to use ban instead of purge.
  $version = floatval(variable_get('varnish_version', 2.1));
  $command = $version >= 3 ? "ban" : "purge";
  $bantype = variable_get('varnish_bantype', VARNISH_DEFAULT_BANTYPE);
  switch ($bantype) {
    case VARNISH_BANTYPE_NORMAL:
      _varnish_terminal_run(array(
        "{$command} req.http.host ~ {$host} && req.url ~ \"{$pattern}\"",
      ));
      break;
    case VARNISH_BANTYPE_BANLURKER:
      _varnish_terminal_run(array(
        "{$command} obj.http.x-host ~ {$host} && obj.http.x-url  ~ \"{$pattern}\"",
      ));
      break;
    default:

      // We really should NEVER get here. Log WATCHDOG_ERROR. I can only see this happening if a user switches between different versions of the module where we remove a ban type.
      watchdog('varnish', 'Varnish ban type is out of range.', array(), WATCHDOG_ERROR);
  }
}

/**
 * Get the status (up/down) of each of the varnish servers.
 *
 * @return An array of server statuses, keyed by varnish terminal addresses.
 * The status will be a numeric constant, either:
 * - VARNISH_SERVER_STATUS_UP
 * - VARNISH_SERVER_STATUS_DOWN
 */
function varnish_get_status() {

  // use a static-cache so this can be called repeatedly without incurring
  // socket-connects for each call.
  static $results = NULL;
  if (is_null($results)) {
    $results = array();
    $status = _varnish_terminal_run(array(
      'status',
    ));
    $terminals = explode(' ', variable_get('varnish_control_terminal', '127.0.0.1:6082'));
    foreach ($terminals as $terminal) {
      $stat = array_shift($status);
      $results[$terminal] = $stat['status']['code'] == 200 ? VARNISH_SERVER_STATUS_UP : VARNISH_SERVER_STATUS_DOWN;
    }
  }
  return $results;
}

/**
 * Theme handler for theme('varnish_status').
 */
function theme_varnish_status($status, $version = 2.1) {
  $items = array();
  foreach ($status as $terminal => $state) {
    list($server, $port) = explode(':', $terminal);
    if ($state == VARNISH_SERVER_STATUS_UP) {
      $icon = theme('image', 'misc/watchdog-ok.png', t("Server OK: @server:@port", array(
        '@server' => $server,
        '@port' => $port,
      )), "{$server}:{$port}");
      if ($version < 3) {
        $items[] = t('!status_icon Varnish running. Observe more detailed statistics !link.', array(
          '!status_icon' => $icon,
          '!link' => l(t('here'), 'admin/reports/varnish'),
        ));
      }
      else {
        $items[] = t('!status_icon Varnish running.', array(
          '!status_icon' => $icon,
        ));
      }
    }
    else {
      $icon = theme('image', 'misc/watchdog-error.png', t('Server down: @server:@port', array(
        '@server' => $server,
        '@port' => $port,
      )), "{$server}:{$port}");
      $items[] = t('!status_icon The Varnish control terminal is not responding at @server on port @port.', array(
        '!status_icon' => $icon,
        '@server' => $server,
        '@port' => $port,
      ));
    }
  }
  return theme('item_list', $items);
}

/**
 * Help[er function to parse the host from the global $base_url
 */
function _varnish_get_host() {
  global $base_url;
  $parts = parse_url($base_url);
  return $parts['host'];
}

/**
 * Helper function that sends commands to Varnish
 *
 * Utilizes sockets to talk to varnish terminal.
 */
function _varnish_terminal_run($commands) {
  if (!extension_loaded('sockets')) {

    // Prevent fatal errors if people don't have requirements.
    return FALSE;
  }

  // Convert single commands to an array so we can handle everything in the same way.
  if (!is_array($commands)) {
    $commands = array(
      $commands,
    );
  }
  $ret = array();
  $terminals = explode(' ', variable_get('varnish_control_terminal', '127.0.0.1:6082'));
  $timeout = variable_get('varnish_socket_timeout', VARNISH_DEFAULT_TIMETOUT);
  $seconds = (int) ($timeout / 1000);
  $milliseconds = (int) ($timeout % 1000 * 1000);
  foreach ($terminals as $terminal) {
    list($server, $port) = explode(':', $terminal);
    $client = socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp'));
    socket_set_option($client, SOL_SOCKET, SO_SNDTIMEO, array(
      'sec' => $seconds,
      'usec' => $milliseconds,
    ));
    socket_set_option($client, SOL_SOCKET, SO_RCVTIMEO, array(
      'sec' => $seconds,
      'usec' => $milliseconds,
    ));
    if (@(!socket_connect($client, $server, $port))) {
      watchdog('varnish', 'Unable to connect to server socket @server: @port: %error', array(
        '@server' => $server,
        '@port' => $port,
        '%error' => socket_strerror(socket_last_error($client)),
      ), WATCHDOG_ERROR);
      $ret[] = FALSE;

      // If a varnish server is unavailable, move on to the next in the list.
      continue;
    }

    // If there is a CLI banner message (varnish >= 2.1.x), try to read it and move on.
    if (floatval(variable_get('varnish_version', 2.1)) > 2.0) {
      $status = _varnish_read_socket($client);

      // Do we need to authenticate?
      if ($status['code'] == 107) {

        // Require authentication
        $secret = variable_get('varnish_control_key', '');
        $challenge = substr($status['msg'], 0, 32);
        $pack = $challenge . "\n" . $secret . "\n" . $challenge . "\n";
        $key = hash('sha256', $pack);
        socket_write($client, "auth {$key}\n");
        $status = _varnish_read_socket($client);
        if ($status['code'] != 200) {
          watchdog('varnish', 'Authentication to server failed!', array(), WATCHDOG_ERROR);
        }
      }
    }
    foreach ($commands as $command) {
      if ($status = _varnish_execute_command($client, $command)) {
        $ret[$terminal][$command] = $status;
      }
    }
  }
  return $ret;
}
function _varnish_execute_command($client, $command) {

  // Send command and get response.
  $result = socket_write($client, "{$command}\n");
  $status = _varnish_read_socket($client);
  if ($status['code'] != 200) {
    watchdog('varnish', 'Received status code @code running %command. Full response text: %error', array(
      '@code' => $status['code'],
      '%command' => $command,
      '%error' => $status['msg'],
    ), WATCHDOG_ERROR);
    return FALSE;
  }
  else {

    // successful connection
    return $status;
  }
}

/**
 * Low-level socket read function.
 *
 * @params
 *   $client an initialized socket client
 *
 *   $retty how many times to retry on "temporarily unavalble" errors
 */
function _varnish_read_socket($client, $retry = 2) {

  // status and length info is always 13 characters.
  $header = socket_read($client, 13, PHP_BINARY_READ);
  if ($header == FALSE) {
    $error = socket_last_error();

    // 35 = socket-unavailable, so it might be blocked from our write.
    // This is an acceptable place to retry.
    if ($error == 35 && $retry > 0) {
      return _varnish_read_socket($client, $retry - 1);
    }
    else {
      watchdog('varnish', 'Socket error: %error', array(
        '%error' => socket_strerror($error),
      ), WATCHDOG_ERROR);
      return array(
        'code' => $error,
        'msg' => socket_strerror($error),
      );
    }
  }
  $msg_len = (int) substr($header, 4, 6) + 1;
  $status = array(
    'code' => substr($header, 0, 3),
    'msg' => socket_read($client, $msg_len, PHP_BINARY_READ),
  );
  return $status;
}

/**
 * Implements hook_preprocess_page().
 */
function varnish_preprocess_page(&$variables) {
  global $conf;

  // Set some special headers if the cache is disabled so that varnish will not
  // cache the pages (for example when using captcha on them).
  if (isset($conf['cache']) && empty($conf['cache'])) {
    drupal_set_header('Pragma: no-cache');
    drupal_set_header('Cache-Control: s-maxage=0, max-age=0, no-store, no-cache, must-revalidate');
  }
}

Functions

Namesort descending Description
theme_varnish_status Theme handler for theme('varnish_status').
varnish_comment Implementation of hook_comment()
varnish_expire_cache Implementation of hook_expire_cache
varnish_flush_caches Implementation of hook_flush_caches()
varnish_form_alter Implementation of hook_form_alter()
varnish_get_status Get the status (up/down) of each of the varnish servers.
varnish_menu Implementation of hook_menu()
varnish_nodeapi Implementation of hook_nodeapi()
varnish_perm Implemetation of hook_perm()
varnish_preprocess_page Implements hook_preprocess_page().
varnish_purge Helper function to purge items for a host that matches the provided pattern.
varnish_purge_all_pages Helper function to quickly flush all caches for the current site.
varnish_requirements Implementation of hook_requirements()
varnish_theme Implementation of hook_theme().
varnish_user Implements hook_user().
_varnish_execute_command
_varnish_get_host Help[er function to parse the host from the global $base_url
_varnish_read_socket Low-level socket read function.
_varnish_terminal_run Helper function that sends commands to Varnish

Constants