From 002e7f0dfe147db7ed68d0dca5d8fba55f2de13f Mon Sep 17 00:00:00 2001 From: David de Boer Date: Sun, 5 Jul 2015 13:51:06 +0200 Subject: [PATCH] Add Varnishadm CLI client Fix #61. --- src/ProxyClient/AbstractVarnishClient.php | 30 +++ src/ProxyClient/Varnish.php | 11 +- src/ProxyClient/VarnishAdmin.php | 195 ++++++++++++++++++ src/ProxyClient/VarnishAdmin/Response.php | 31 +++ src/ProxyClient/VarnishAdminMultiple.php | 11 + src/Test/Proxy/VarnishProxy.php | 2 +- tests/Functional/Fixtures/BanTest.php | 58 ++++++ tests/Functional/Fixtures/secret | 1 + .../ProxyClient/VarnishAdminTest.php | 39 ++++ .../Varnish/VarnishProxyClientTest.php | 53 +---- tests/Unit/Test/Proxy/VarnishProxyTest.php | 1 + 11 files changed, 371 insertions(+), 61 deletions(-) create mode 100644 src/ProxyClient/AbstractVarnishClient.php create mode 100644 src/ProxyClient/VarnishAdmin.php create mode 100644 src/ProxyClient/VarnishAdmin/Response.php create mode 100644 src/ProxyClient/VarnishAdminMultiple.php create mode 100644 tests/Functional/Fixtures/BanTest.php create mode 100755 tests/Functional/Fixtures/secret create mode 100644 tests/Functional/ProxyClient/VarnishAdminTest.php diff --git a/src/ProxyClient/AbstractVarnishClient.php b/src/ProxyClient/AbstractVarnishClient.php new file mode 100644 index 00000000..f38c5f5f --- /dev/null +++ b/src/ProxyClient/AbstractVarnishClient.php @@ -0,0 +1,30 @@ + + * + * 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).')$'; + } +} diff --git a/src/ProxyClient/Varnish.php b/src/ProxyClient/Varnish.php index b66e4a33..6d891190 100644 --- a/src/ProxyClient/Varnish.php +++ b/src/ProxyClient/Varnish.php @@ -11,7 +11,6 @@ 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; @@ -27,14 +26,11 @@ * * @author David de Boer */ -class Varnish extends AbstractProxyClient implements BanInterface, PurgeInterface, RefreshInterface, TagsInterface +class Varnish extends AbstractVarnishClient implements BanInterface, PurgeInterface, RefreshInterface, TagsInterface { 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. @@ -118,10 +114,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 = [ diff --git a/src/ProxyClient/VarnishAdmin.php b/src/ProxyClient/VarnishAdmin.php new file mode 100644 index 00000000..0addbc5a --- /dev/null +++ b/src/ProxyClient/VarnishAdmin.php @@ -0,0 +1,195 @@ + + * + * 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\d{3}) (?P\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; + } +} diff --git a/src/ProxyClient/VarnishAdmin/Response.php b/src/ProxyClient/VarnishAdmin/Response.php new file mode 100644 index 00000000..d16f843b --- /dev/null +++ b/src/ProxyClient/VarnishAdmin/Response.php @@ -0,0 +1,31 @@ +statusCode = (int) $statusCode; + $this->response = $response; + } + + /** + * @return int + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * @return mixed + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/src/ProxyClient/VarnishAdminMultiple.php b/src/ProxyClient/VarnishAdminMultiple.php new file mode 100644 index 00000000..4ed962e3 --- /dev/null +++ b/src/ProxyClient/VarnishAdminMultiple.php @@ -0,0 +1,11 @@ +getConfigFile(), '-n', $this->getCacheDir(), '-p', 'vcl_dir=' . $this->getConfigDir(), - + '-S', realpath('./tests/Functional/Fixtures/secret'), '-P', $this->pid, ]; if ($this->getAllowInlineC()) { diff --git a/tests/Functional/Fixtures/BanTest.php b/tests/Functional/Fixtures/BanTest.php new file mode 100644 index 00000000..f58838d1 --- /dev/null +++ b/tests/Functional/Fixtures/BanTest.php @@ -0,0 +1,58 @@ +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')); + } +} diff --git a/tests/Functional/Fixtures/secret b/tests/Functional/Fixtures/secret new file mode 100755 index 00000000..366bbcc1 --- /dev/null +++ b/tests/Functional/Fixtures/secret @@ -0,0 +1 @@ +fos diff --git a/tests/Functional/ProxyClient/VarnishAdminTest.php b/tests/Functional/ProxyClient/VarnishAdminTest.php new file mode 100644 index 00000000..1cd8f93c --- /dev/null +++ b/tests/Functional/ProxyClient/VarnishAdminTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\HttpCache\Tests\Functional\ProxyClient; + +use FOS\HttpCache\ProxyClient\VarnishAdmin; +use FOS\HttpCache\Test\VarnishTestCase; +use FOS\HttpCache\Tests\Functional\Fixtures\BanTest; + +class VarnishAdminTest extends VarnishTestCase +{ + use BanTest; + + /** + * Get Varnish admin proxy client + * + * @return VarnishAdmin + */ + protected function getProxyClient() + { + if (null === $this->proxyClient) { + $this->proxyClient = new VarnishAdmin( + '127.0.0.1', + $this->getProxy()->getManagementPort(), + 'fos' + ); + } + + return $this->proxyClient; + } +} diff --git a/tests/Functional/Varnish/VarnishProxyClientTest.php b/tests/Functional/Varnish/VarnishProxyClientTest.php index fc68261e..705842b4 100644 --- a/tests/Functional/Varnish/VarnishProxyClientTest.php +++ b/tests/Functional/Varnish/VarnishProxyClientTest.php @@ -13,6 +13,7 @@ use FOS\HttpCache\ProxyClient\Varnish; use FOS\HttpCache\Test\VarnishTestCase; +use FOS\HttpCache\Tests\Functional\Fixtures\BanTest; /** * @group webserver @@ -20,57 +21,7 @@ */ class VarnishProxyClientTest extends VarnishTestCase { - 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([Varnish::HTTP_HEADER_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([Varnish::HTTP_HEADER_HOST => 'wrong-host.lo'])->flush(); - $this->assertHit($this->getResponse('/cache.php')); - - $this->getProxyClient()->ban([Varnish::HTTP_HEADER_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')); - } + use BanTest; public function testPurge() { diff --git a/tests/Unit/Test/Proxy/VarnishProxyTest.php b/tests/Unit/Test/Proxy/VarnishProxyTest.php index 461b50c6..415599bf 100644 --- a/tests/Unit/Test/Proxy/VarnishProxyTest.php +++ b/tests/Unit/Test/Proxy/VarnishProxyTest.php @@ -42,6 +42,7 @@ public function testStart() '-f', 'config.vcl', '-n', '/tmp/cache/dir', '-p', 'vcl_dir=/my/varnish/dir', + '-S', realpath('./tests/Functional/Fixtures/secret'), '-P', '/tmp/foshttpcache-varnish.pid', ], $proxy->arguments