varnish.module in Varnish 6
Same filename and directory in other branches
varnish.module Provide drupal hooks for integration with the Varnish control layer.
File
varnish.moduleView 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
Name | 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 |