Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add Varnishadm CLI client #231

Open
wants to merge 1 commit into
base: 3.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/ProxyClient/AbstractVarnishClient.php
@@ -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).')$';
}
}
11 changes: 2 additions & 9 deletions src/ProxyClient/Varnish.php
Expand Up @@ -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;
Expand All @@ -27,14 +26,11 @@
*
* @author David de Boer <david@driebit.nl>
*/
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.
Expand Down Expand Up @@ -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 = [
Expand Down
195 changes: 195 additions & 0 deletions src/ProxyClient/VarnishAdmin.php
@@ -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
@@ -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
@@ -0,0 +1,11 @@
<?php

namespace FOS\HttpCache\ProxyClient;

class VarnishAdminMultiple
{
public function __construct($servers)
{

}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure yet whether we should support multiple servers in the VarnishAdmin class itself, or wrap them in a separate class such as this VarnishAdminMultiple.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at VarnishAdmin, i wonder if we should have a VarnishCli class that wraps the socket connection. then VarnishAdmin would become a lot simpler, and having multiple Cli instances would become simpler. if we keep it the way we currently have it, having a separate wrapper to multiplex seems better to me.

2 changes: 1 addition & 1 deletion src/Test/Proxy/VarnishProxy.php
Expand Up @@ -47,7 +47,7 @@ public function start()
'-f', $this->getConfigFile(),
'-n', $this->getCacheDir(),
'-p', 'vcl_dir=' . $this->getConfigDir(),

'-S', realpath('./tests/Functional/Fixtures/secret'),
'-P', $this->pid,
];
if ($this->getAllowInlineC()) {
Expand Down
58 changes: 58 additions & 0 deletions tests/Functional/Fixtures/BanTest.php
@@ -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
@@ -0,0 +1 @@
fos