You are here

varnish.module in Varnish 7

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

Common functions used for the module.

File

varnish.module
View source
<?php

/**
 * @file
 * Common functions used for the module.
 */
define('VARNISH_NO_CLEAR', 0);
define('VARNISH_DEFAULT_CLEAR', 1);

// Requires Expire.module to be enabled.
define('VARNISH_SELECTIVE_CLEAR', 2);

// Timeout in milliseconds.
define('VARNISH_DEFAULT_TIMEOUT', 100);
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);

/**
 * Implements hook_menu().
 *
 * Set up admin settings callbacks, etc.
 */
function varnish_menu() {
  $items = array();
  $items['admin/config/development/varnish'] = array(
    'title' => 'Varnish',
    '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/config/development/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;
}

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

/**
 * Implements hook_permisson().
 *
 * Adds administer Varnish permission.
 */
function varnish_permission() {
  return array(
    'administer varnish' => array(
      'title' => t('Administer Varnish'),
      'description' => t('Perform administration tasks for varnish.'),
      'restrict access' => TRUE,
    ),
  );
}

/**
 * Implements hook_flush_caches().
 *
 * Adds a varnish cache to the list of caches.
 */
function varnish_flush_caches() {
  if (variable_get('cache_class_external_varnish_page', FALSE)) {
    return array(
      'external_varnish_page',
    );
  }
}

/**
 * Implements 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.
 *
 * @param $paths
 *   Associative array of paths to be purged, where both keys and values are
 *   paths.
 * @param $wildcards
 *   (Optional) Array of wildcards to be applied, as an associative array where
 *   paths are keys and booleans indicating if the path should have a wildcard
 *   at the end are values.
 */
function varnish_expire_cache($paths, $wildcards = array()) {
  if (count($wildcards)) {
    foreach ($wildcards as $path => $wildcard) {
      if ($wildcard) {
        $paths[$path] .= '([/?].*)?';
      }
    }
  }
  if (module_exists('expire') && variable_get('expire_include_base_url', constant('EXPIRE_INCLUDE_BASE_URL'))) {

    // Sort the URLs by domain and batch by domain.
    $host_buckets = array();
    foreach ($paths as $url) {
      $parts = parse_url($url);

      // Strip base path from the path because it will be added later.
      $path = substr($parts['path'], strlen(base_path()));
      $host_buckets[$parts['host']][] = $path . (!empty($parts['query']) ? '?' . $parts['query'] : '');
    }
    foreach ($host_buckets as $host => $purges) {
      varnish_purge_paths($host, $purges);
    }
  }
  else {
    $host = _varnish_get_host();
    varnish_purge_paths($host, $paths);
  }
}

/**
 * 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.
 *
 * Take care to limit the length of $pattern to params.cli_buffer on your
 * Varnish server, otherwise Varnish will truncate the command. Use
 * varnish_purge_paths() to protect you from this, if applicable.
 *
 * @param string $host
 *   The host to purge.
 * @param string $pattern
 *   The pattern to look for and purge.
 */
function varnish_purge($host, $pattern) {
  global $base_path, $base_root;

  // 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);

  // Modify the patterns to remove base url and base path.
  $patterns = explode('|', $pattern);
  foreach ($patterns as $num => $single_pattern) {
    if (substr($single_pattern, 1, strlen($base_root)) == $base_root) {
      $single_pattern = substr_replace($single_pattern, '', 1, strlen($base_root));
    }
    $patterns[$num] = $single_pattern;
  }
  $pattern = implode('|', $patterns);

  // Get list of domains to ban
  $front_domains = variable_get('varnish_front_domains', '');

  // Trigger hook_varnish_front_domains_alter().
  drupal_alter('varnish_front_domains', $front_domains);
  $fronts = explode(' ', $front_domains);
  switch ($bantype) {
    case VARNISH_BANTYPE_NORMAL:
      _varnish_terminal_run(array(
        "{$command} req.http.host ~ {$host} && req.url ~ \"{$pattern}\"",
      ));
      if ($front_domains) {
        foreach ($fronts as $value) {
          _varnish_terminal_run(array(
            "{$command} req.http.host ~ {$value} && req.url ~ \"{$pattern}\"",
          ));
        }
      }
      break;
    case VARNISH_BANTYPE_BANLURKER:
      _varnish_terminal_run(array(
        "{$command} obj.http.x-host ~ {$host} && obj.http.x-url  ~ \"{$pattern}\"",
      ));
      if ($front_domains) {
        foreach ($fronts as $value) {
          _varnish_terminal_run(array(
            "{$command} obj.http.x-host ~ {$value} && 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);
  }
}

/**
 * Helper function that wraps around varnish_purge().
 *
 * Compiles a regular expression of all paths supplied to it. This function
 * takes care to chunk commands into no more than 7500 bytes each, to avoid
 * hitting params.cli_buffer.
 *
 * @param string $host
 *   The host to purge.
 * @param array $paths
 *   The paths (no leading slash) to purge for this host.
 */
function varnish_purge_paths($host, array $paths) {

  // Subtract the hostname length from the global length limit.
  // Note we use strlen() because we're counting bytes, not characters.
  $length_limit = variable_get('varnish_cmdlength_limit', 7500) - strlen($host);
  $base_path = base_path();
  while (!empty($paths)) {

    // Construct patterns and send them to the server when they're full.
    $purge_pattern = '^';
    while (strlen($purge_pattern) < $length_limit && !empty($paths)) {
      $purge_pattern .= $base_path . array_shift($paths) . '$|^';
    }

    // Chop the final "|^" off the string, leaving "$".
    $purge_pattern = substr($purge_pattern, 0, -2);

    // Remove extra slashes in beginning of URL.
    $purge_pattern = preg_replace('#/+#', '/', $purge_pattern);

    // Submit this purge chunk.
    varnish_purge($host, $purge_pattern);
  }
}

/**
 * Get the status (up/down) of each of the varnish servers.
 *
 * @return array
 *   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($variables) {
  $items = array();
  $status = $variables['status'];
  foreach ($status as $terminal => $state) {
    list($server, $port) = explode(':', $terminal);
    if ($state == VARNISH_SERVER_STATUS_UP) {
      $icon = theme('image', array(
        'path' => 'misc/watchdog-ok.png',
        'alt' => t("Server OK: @server:@port", array(
          '@server' => $server,
          '@port' => $port,
        )),
        'title' => "{$server}:{$port}",
      ));
      $version = $variables['version'];
      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', array(
        'path' => 'misc/watchdog-error.png',
        'alt' => t("Server down: @server:@port", array(
          '@server' => $server,
          '@port' => $port,
        )),
        'title' => "{$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', array(
    'items' => $items,
  ));
}

/**
 * Helper 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'));

  // The variable varnish_socket_timeout defines the timeout in milliseconds.
  $timeout = variable_get('varnish_socket_timeout', VARNISH_DEFAULT_TIMEOUT);
  $seconds = (int) ($timeout / 1000);
  $microseconds = (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' => $microseconds,
    ));
    socket_set_option($client, SOL_SOCKET, SO_RCVTIMEO, array(
      'sec' => $seconds,
      'usec' => $microseconds,
    ));
    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[$terminal] = 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?
      // Require authentication.
      if ($status['code'] == 107) {
        $secret = variable_get('varnish_control_key', '');
        $challenge = substr($status['msg'], 0, 32);
        if (variable_get('varnish_control_key_appendnewline', TRUE)) {
          $pack = $challenge . "\n" . $secret . "\n" . $challenge . "\n";
        }
        else {
          $pack = $challenge . "\n" . $secret . $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;
}

/**
 * Send command to varnish.
 */
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;
}

Functions

Namesort descending Description
theme_varnish_status Theme handler for theme('varnish_status').
varnish_expire_cache Implements hook_expire_cache().
varnish_flush_caches Implements hook_flush_caches().
varnish_get_status Get the status (up/down) of each of the varnish servers.
varnish_menu Implements hook_menu().
varnish_permission Implements hook_permisson().
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_purge_paths Helper function that wraps around varnish_purge().
varnish_theme Implements hook_theme().
_varnish_execute_command Send command to varnish.
_varnish_get_host Helper 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