Skip to content

Commit

Permalink
Add Varnishadm CLI client
Browse files Browse the repository at this point in the history
Fix #61.
  • Loading branch information
ddeboer committed Jul 5, 2015
1 parent 6c755b3 commit 75066c2
Show file tree
Hide file tree
Showing 11 changed files with 371 additions and 61 deletions.
30 changes: 30 additions & 0 deletions src/ProxyClient/AbstractVarnishClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the FOSHttpCache package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\HttpCache\ProxyClient;

use FOS\HttpCache\Exception\InvalidArgumentException;

abstract class AbstractVarnishClient extends AbstractProxyClient
{
const HTTP_HEADER_HOST = 'X-Host';
const HTTP_HEADER_URL = 'X-Url';
const HTTP_HEADER_CONTENT_TYPE = 'X-Content-Type';

protected function createHostsRegex(array $hosts)
{
if (!count($hosts)) {
throw new InvalidArgumentException('Either supply a list of hosts or null, but not an empty array.');
}

return '^('.join('|', $hosts).')$';
}
}
12 changes: 2 additions & 10 deletions src/ProxyClient/Varnish.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,23 @@

namespace FOS\HttpCache\ProxyClient;

use FOS\HttpCache\Exception\InvalidArgumentException;
use FOS\HttpCache\Exception\MissingHostException;
use FOS\HttpCache\ProxyClient\Invalidation\BanInterface;
use FOS\HttpCache\ProxyClient\Invalidation\PurgeInterface;
use FOS\HttpCache\ProxyClient\Invalidation\RefreshInterface;
use FOS\HttpCache\ProxyClient\Request\InvalidationRequest;
use FOS\HttpCache\ProxyClient\Request\RequestQueue;
use Http\Adapter\HttpAdapter;

/**
* Varnish HTTP cache invalidator.
*
* @author David de Boer <[email protected]>
*/
class Varnish extends AbstractProxyClient implements BanInterface, PurgeInterface, RefreshInterface
class Varnish extends AbstractVarnishClient implements BanInterface, PurgeInterface, RefreshInterface
{
const HTTP_METHOD_BAN = 'BAN';
const HTTP_METHOD_PURGE = 'PURGE';
const HTTP_METHOD_REFRESH = 'GET';
const HTTP_HEADER_HOST = 'X-Host';
const HTTP_HEADER_URL = 'X-Url';
const HTTP_HEADER_CONTENT_TYPE = 'X-Content-Type';

/**
* Map of default headers for ban requests with their default values.
Expand Down Expand Up @@ -107,10 +102,7 @@ public function ban(array $headers)
public function banPath($path, $contentType = null, $hosts = null)
{
if (is_array($hosts)) {
if (!count($hosts)) {
throw new InvalidArgumentException('Either supply a list of hosts or null, but not an empty array.');
}
$hosts = '^('.join('|', $hosts).')$';
$hosts = $this->createHostsRegex($hosts);
}

$headers = [
Expand Down
195 changes: 195 additions & 0 deletions src/ProxyClient/VarnishAdmin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<?php

/*
* This file is part of the FOSHttpCache package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\HttpCache\ProxyClient;

use FOS\HttpCache\Exception\ProxyUnreachableException;
use FOS\HttpCache\ProxyClient\Invalidation\BanInterface;
use FOS\HttpCache\ProxyClient\VarnishAdmin\Response;

/**
* Varnish Admin CLI (also known as Management Port) client
*/
class VarnishAdmin extends AbstractVarnishClient implements BanInterface
{
const CLIS_CLOSE = 50;
const CLIS_SYNTAX = 100;
const CLIS_UNKNOWN = 101;
const CLIS_UNIMPL = 102;
const CLIS_TOOFEW = 104;
const CLIS_TOOMANY = 105;
const CLIS_PARAM = 106;
const CLIS_AUTH = 107;
const CLIS_OK = 200;
const CLIS_TRUNCATED = 201;
const CLIS_CANT = 300;
const CLIS_COMMS = 400;

const TIMEOUT = 3;

/**
* @var string
*/
private $host;

/**
* @var int
*/
private $port;

private $connection;

/**
* @var string[]
*/
private $queuedBans = [];

/**
* @var string
*/
private $secret;

public function __construct($host, $port, $secret = null)
{
$this->host = $host;
$this->port = $port;
$this->secret = $secret;
}

/**
* {@inheritdoc}
*/
public function ban(array $headers)
{
$mappedHeaders = array_map(
function ($name, $value) {
return sprintf('obj.http.%s ~ "%s"', $name, $value);
},
array_keys($headers),
$headers
);

$this->queuedBans[] = implode('&&', $mappedHeaders);

return $this;
}

/**
* {@inheritdoc}
*/
public function banPath($path, $contentType = null, $hosts = null)
{
$ban = sprintf('obj.http.%s ~ "%s"', self::HTTP_HEADER_URL, $path);

if ($contentType) {
$ban .= sprintf(
' && obj.http.content-type ~ "%s"',
$contentType
);
}

if ($hosts) {
$ban .= sprintf(
' && obj.http.%s ~ "%s"',
self::HTTP_HEADER_HOST,
$this->createHostsRegex($hosts)
);
}

$this->queuedBans[] = $ban;

return $this;
}

/**
* {@inheritdoc}
*/
public function flush()
{
foreach ($this->queuedBans as $ban) {
$this->executeCommand('ban', $ban);
}
}

private function getConnection()
{
if ($this->connection === null) {
$connection = fsockopen($this->host, $this->port, $errno, $errstr, self::TIMEOUT);
if ($connection === false) {
throw new ProxyUnreachableException('Unreachable');
}

stream_set_timeout($connection, self::TIMEOUT);
$response = $this->read($connection);

switch ($response->getStatusCode()) {
case self::CLIS_AUTH:
$this->authenticate(substr($response->getResponse(), 0, 32), $connection);
break;
}

$this->connection = $connection;
}

return $this->connection;
}

private function read($connection)
{
while (!feof($connection)) {
$line = fgets($connection, 1024);
if ($line === false) {
throw new ProxyUnreachableException('bla');
}
if (strlen($line) === 13
&& preg_match('/^(?P<status>\d{3}) (?P<length>\d+)/', $line, $matches)
) {
$response = '';
while (!feof($connection) && strlen($response) < $matches['length']) {
$response .= fread($connection, $matches['length']);
}

return new Response($matches['status'], $response);
}
}
}

private function authenticate($challenge, $connection = null)
{
$data = sprintf("%1\$s\n%2\$s\n%1\$s\n", $challenge, $this->secret);
$hash = hash('sha256', $data);

$this->executeCommand('auth', $hash, $connection);
}

/**
* Execute a command
*
* @param string $command
* @param string $param
* @param \resource $connection
*
* @return Response
*/
private function executeCommand($command, $param = null, $connection = null)
{
$connection = $connection ?: $this->getConnection();
$all = sprintf("%s %s\n", $command, $param);
fwrite($connection, $all);

$response = $this->read($connection);
if ($response->getStatusCode() !== 200) {
throw new \RuntimeException($response->getResponse());
}

return $response;
}
}
31 changes: 31 additions & 0 deletions src/ProxyClient/VarnishAdmin/Response.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace FOS\HttpCache\ProxyClient\VarnishAdmin;

class Response
{
private $statusCode;
private $response;

public function __construct($statusCode, $response)
{
$this->statusCode = (int) $statusCode;
$this->response = $response;
}

/**
* @return int
*/
public function getStatusCode()
{
return $this->statusCode;
}

/**
* @return mixed
*/
public function getResponse()
{
return $this->response;
}
}
11 changes: 11 additions & 0 deletions src/ProxyClient/VarnishAdminMultiple.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace FOS\HttpCache\ProxyClient;

class VarnishAdminMultiple
{
public function __construct($servers)
{

}
}
1 change: 1 addition & 0 deletions src/Test/Proxy/VarnishProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public function start()
'-f', $this->getConfigFile(),
'-n', $this->getCacheDir(),
'-p', 'vcl_dir=' . $this->getConfigDir(),
'-S', realpath('./tests/Functional/Fixtures/secret'),
'-P', $this->pid,
]
);
Expand Down
58 changes: 58 additions & 0 deletions tests/Functional/Fixtures/BanTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace FOS\HttpCache\Tests\Functional\Fixtures;

trait BanTest
{
public function testBanAll()
{
$this->assertMiss($this->getResponse('/cache.php'));
$this->assertHit($this->getResponse('/cache.php'));

$this->assertMiss($this->getResponse('/json.php'));
$this->assertHit($this->getResponse('/json.php'));

$this->getProxyClient()->ban(['X-Url' => '.*'])->flush();

$this->assertMiss($this->getResponse('/cache.php'));
$this->assertMiss($this->getResponse('/json.php'));
}

public function testBanHost()
{
$this->assertMiss($this->getResponse('/cache.php'));
$this->assertHit($this->getResponse('/cache.php'));

$this->getProxyClient()->ban(['X-Host' => 'wrong-host.lo'])->flush();
$this->assertHit($this->getResponse('/cache.php'));

$this->getProxyClient()->ban(['X-Host' => $this->getHostname()])->flush();
$this->assertMiss($this->getResponse('/cache.php'));
}

public function testBanPathAll()
{
$this->assertMiss($this->getResponse('/cache.php'));
$this->assertHit($this->getResponse('/cache.php'));

$this->assertMiss($this->getResponse('/json.php'));
$this->assertHit($this->getResponse('/json.php'));

$this->getProxyClient()->banPath('.*')->flush();
$this->assertMiss($this->getResponse('/cache.php'));
$this->assertMiss($this->getResponse('/json.php'));
}

public function testBanPathContentType()
{
$this->assertMiss($this->getResponse('/cache.php'));
$this->assertHit($this->getResponse('/cache.php'));

$this->assertMiss($this->getResponse('/json.php'));
$this->assertHit($this->getResponse('/json.php'));

$this->getProxyClient()->banPath('.*', 'text/html')->flush();
$this->assertMiss($this->getResponse('/cache.php'));
$this->assertHit($this->getResponse('/json.php'));
}
}
1 change: 1 addition & 0 deletions tests/Functional/Fixtures/secret
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fos
Loading

0 comments on commit 75066c2

Please sign in to comment.