Skip to content

Commit 75066c2

Browse files
committed
Add Varnishadm CLI client
Fix #61.
1 parent 6c755b3 commit 75066c2

File tree

11 files changed

+371
-61
lines changed

11 files changed

+371
-61
lines changed

Diff for: src/ProxyClient/AbstractVarnishClient.php

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCache package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\HttpCache\ProxyClient;
13+
14+
use FOS\HttpCache\Exception\InvalidArgumentException;
15+
16+
abstract class AbstractVarnishClient extends AbstractProxyClient
17+
{
18+
const HTTP_HEADER_HOST = 'X-Host';
19+
const HTTP_HEADER_URL = 'X-Url';
20+
const HTTP_HEADER_CONTENT_TYPE = 'X-Content-Type';
21+
22+
protected function createHostsRegex(array $hosts)
23+
{
24+
if (!count($hosts)) {
25+
throw new InvalidArgumentException('Either supply a list of hosts or null, but not an empty array.');
26+
}
27+
28+
return '^('.join('|', $hosts).')$';
29+
}
30+
}

Diff for: src/ProxyClient/Varnish.php

+2-10
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,23 @@
1111

1212
namespace FOS\HttpCache\ProxyClient;
1313

14-
use FOS\HttpCache\Exception\InvalidArgumentException;
1514
use FOS\HttpCache\Exception\MissingHostException;
1615
use FOS\HttpCache\ProxyClient\Invalidation\BanInterface;
1716
use FOS\HttpCache\ProxyClient\Invalidation\PurgeInterface;
1817
use FOS\HttpCache\ProxyClient\Invalidation\RefreshInterface;
1918
use FOS\HttpCache\ProxyClient\Request\InvalidationRequest;
20-
use FOS\HttpCache\ProxyClient\Request\RequestQueue;
2119
use Http\Adapter\HttpAdapter;
2220

2321
/**
2422
* Varnish HTTP cache invalidator.
2523
*
2624
* @author David de Boer <david@driebit.nl>
2725
*/
28-
class Varnish extends AbstractProxyClient implements BanInterface, PurgeInterface, RefreshInterface
26+
class Varnish extends AbstractVarnishClient implements BanInterface, PurgeInterface, RefreshInterface
2927
{
3028
const HTTP_METHOD_BAN = 'BAN';
3129
const HTTP_METHOD_PURGE = 'PURGE';
3230
const HTTP_METHOD_REFRESH = 'GET';
33-
const HTTP_HEADER_HOST = 'X-Host';
34-
const HTTP_HEADER_URL = 'X-Url';
35-
const HTTP_HEADER_CONTENT_TYPE = 'X-Content-Type';
3631

3732
/**
3833
* Map of default headers for ban requests with their default values.
@@ -107,10 +102,7 @@ public function ban(array $headers)
107102
public function banPath($path, $contentType = null, $hosts = null)
108103
{
109104
if (is_array($hosts)) {
110-
if (!count($hosts)) {
111-
throw new InvalidArgumentException('Either supply a list of hosts or null, but not an empty array.');
112-
}
113-
$hosts = '^('.join('|', $hosts).')$';
105+
$hosts = $this->createHostsRegex($hosts);
114106
}
115107

116108
$headers = [

Diff for: src/ProxyClient/VarnishAdmin.php

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSHttpCache package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\HttpCache\ProxyClient;
13+
14+
use FOS\HttpCache\Exception\ProxyUnreachableException;
15+
use FOS\HttpCache\ProxyClient\Invalidation\BanInterface;
16+
use FOS\HttpCache\ProxyClient\VarnishAdmin\Response;
17+
18+
/**
19+
* Varnish Admin CLI (also known as Management Port) client
20+
*/
21+
class VarnishAdmin extends AbstractVarnishClient implements BanInterface
22+
{
23+
const CLIS_CLOSE = 50;
24+
const CLIS_SYNTAX = 100;
25+
const CLIS_UNKNOWN = 101;
26+
const CLIS_UNIMPL = 102;
27+
const CLIS_TOOFEW = 104;
28+
const CLIS_TOOMANY = 105;
29+
const CLIS_PARAM = 106;
30+
const CLIS_AUTH = 107;
31+
const CLIS_OK = 200;
32+
const CLIS_TRUNCATED = 201;
33+
const CLIS_CANT = 300;
34+
const CLIS_COMMS = 400;
35+
36+
const TIMEOUT = 3;
37+
38+
/**
39+
* @var string
40+
*/
41+
private $host;
42+
43+
/**
44+
* @var int
45+
*/
46+
private $port;
47+
48+
private $connection;
49+
50+
/**
51+
* @var string[]
52+
*/
53+
private $queuedBans = [];
54+
55+
/**
56+
* @var string
57+
*/
58+
private $secret;
59+
60+
public function __construct($host, $port, $secret = null)
61+
{
62+
$this->host = $host;
63+
$this->port = $port;
64+
$this->secret = $secret;
65+
}
66+
67+
/**
68+
* {@inheritdoc}
69+
*/
70+
public function ban(array $headers)
71+
{
72+
$mappedHeaders = array_map(
73+
function ($name, $value) {
74+
return sprintf('obj.http.%s ~ "%s"', $name, $value);
75+
},
76+
array_keys($headers),
77+
$headers
78+
);
79+
80+
$this->queuedBans[] = implode('&&', $mappedHeaders);
81+
82+
return $this;
83+
}
84+
85+
/**
86+
* {@inheritdoc}
87+
*/
88+
public function banPath($path, $contentType = null, $hosts = null)
89+
{
90+
$ban = sprintf('obj.http.%s ~ "%s"', self::HTTP_HEADER_URL, $path);
91+
92+
if ($contentType) {
93+
$ban .= sprintf(
94+
' && obj.http.content-type ~ "%s"',
95+
$contentType
96+
);
97+
}
98+
99+
if ($hosts) {
100+
$ban .= sprintf(
101+
' && obj.http.%s ~ "%s"',
102+
self::HTTP_HEADER_HOST,
103+
$this->createHostsRegex($hosts)
104+
);
105+
}
106+
107+
$this->queuedBans[] = $ban;
108+
109+
return $this;
110+
}
111+
112+
/**
113+
* {@inheritdoc}
114+
*/
115+
public function flush()
116+
{
117+
foreach ($this->queuedBans as $ban) {
118+
$this->executeCommand('ban', $ban);
119+
}
120+
}
121+
122+
private function getConnection()
123+
{
124+
if ($this->connection === null) {
125+
$connection = fsockopen($this->host, $this->port, $errno, $errstr, self::TIMEOUT);
126+
if ($connection === false) {
127+
throw new ProxyUnreachableException('Unreachable');
128+
}
129+
130+
stream_set_timeout($connection, self::TIMEOUT);
131+
$response = $this->read($connection);
132+
133+
switch ($response->getStatusCode()) {
134+
case self::CLIS_AUTH:
135+
$this->authenticate(substr($response->getResponse(), 0, 32), $connection);
136+
break;
137+
}
138+
139+
$this->connection = $connection;
140+
}
141+
142+
return $this->connection;
143+
}
144+
145+
private function read($connection)
146+
{
147+
while (!feof($connection)) {
148+
$line = fgets($connection, 1024);
149+
if ($line === false) {
150+
throw new ProxyUnreachableException('bla');
151+
}
152+
if (strlen($line) === 13
153+
&& preg_match('/^(?P<status>\d{3}) (?P<length>\d+)/', $line, $matches)
154+
) {
155+
$response = '';
156+
while (!feof($connection) && strlen($response) < $matches['length']) {
157+
$response .= fread($connection, $matches['length']);
158+
}
159+
160+
return new Response($matches['status'], $response);
161+
}
162+
}
163+
}
164+
165+
private function authenticate($challenge, $connection = null)
166+
{
167+
$data = sprintf("%1\$s\n%2\$s\n%1\$s\n", $challenge, $this->secret);
168+
$hash = hash('sha256', $data);
169+
170+
$this->executeCommand('auth', $hash, $connection);
171+
}
172+
173+
/**
174+
* Execute a command
175+
*
176+
* @param string $command
177+
* @param string $param
178+
* @param \resource $connection
179+
*
180+
* @return Response
181+
*/
182+
private function executeCommand($command, $param = null, $connection = null)
183+
{
184+
$connection = $connection ?: $this->getConnection();
185+
$all = sprintf("%s %s\n", $command, $param);
186+
fwrite($connection, $all);
187+
188+
$response = $this->read($connection);
189+
if ($response->getStatusCode() !== 200) {
190+
throw new \RuntimeException($response->getResponse());
191+
}
192+
193+
return $response;
194+
}
195+
}

Diff for: src/ProxyClient/VarnishAdmin/Response.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace FOS\HttpCache\ProxyClient\VarnishAdmin;
4+
5+
class Response
6+
{
7+
private $statusCode;
8+
private $response;
9+
10+
public function __construct($statusCode, $response)
11+
{
12+
$this->statusCode = (int) $statusCode;
13+
$this->response = $response;
14+
}
15+
16+
/**
17+
* @return int
18+
*/
19+
public function getStatusCode()
20+
{
21+
return $this->statusCode;
22+
}
23+
24+
/**
25+
* @return mixed
26+
*/
27+
public function getResponse()
28+
{
29+
return $this->response;
30+
}
31+
}

Diff for: src/ProxyClient/VarnishAdminMultiple.php

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace FOS\HttpCache\ProxyClient;
4+
5+
class VarnishAdminMultiple
6+
{
7+
public function __construct($servers)
8+
{
9+
10+
}
11+
}

Diff for: src/Test/Proxy/VarnishProxy.php

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public function start()
4848
'-f', $this->getConfigFile(),
4949
'-n', $this->getCacheDir(),
5050
'-p', 'vcl_dir=' . $this->getConfigDir(),
51+
'-S', realpath('./tests/Functional/Fixtures/secret'),
5152
'-P', $this->pid,
5253
]
5354
);

Diff for: tests/Functional/Fixtures/BanTest.php

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace FOS\HttpCache\Tests\Functional\Fixtures;
4+
5+
trait BanTest
6+
{
7+
public function testBanAll()
8+
{
9+
$this->assertMiss($this->getResponse('/cache.php'));
10+
$this->assertHit($this->getResponse('/cache.php'));
11+
12+
$this->assertMiss($this->getResponse('/json.php'));
13+
$this->assertHit($this->getResponse('/json.php'));
14+
15+
$this->getProxyClient()->ban(['X-Url' => '.*'])->flush();
16+
17+
$this->assertMiss($this->getResponse('/cache.php'));
18+
$this->assertMiss($this->getResponse('/json.php'));
19+
}
20+
21+
public function testBanHost()
22+
{
23+
$this->assertMiss($this->getResponse('/cache.php'));
24+
$this->assertHit($this->getResponse('/cache.php'));
25+
26+
$this->getProxyClient()->ban(['X-Host' => 'wrong-host.lo'])->flush();
27+
$this->assertHit($this->getResponse('/cache.php'));
28+
29+
$this->getProxyClient()->ban(['X-Host' => $this->getHostname()])->flush();
30+
$this->assertMiss($this->getResponse('/cache.php'));
31+
}
32+
33+
public function testBanPathAll()
34+
{
35+
$this->assertMiss($this->getResponse('/cache.php'));
36+
$this->assertHit($this->getResponse('/cache.php'));
37+
38+
$this->assertMiss($this->getResponse('/json.php'));
39+
$this->assertHit($this->getResponse('/json.php'));
40+
41+
$this->getProxyClient()->banPath('.*')->flush();
42+
$this->assertMiss($this->getResponse('/cache.php'));
43+
$this->assertMiss($this->getResponse('/json.php'));
44+
}
45+
46+
public function testBanPathContentType()
47+
{
48+
$this->assertMiss($this->getResponse('/cache.php'));
49+
$this->assertHit($this->getResponse('/cache.php'));
50+
51+
$this->assertMiss($this->getResponse('/json.php'));
52+
$this->assertHit($this->getResponse('/json.php'));
53+
54+
$this->getProxyClient()->banPath('.*', 'text/html')->flush();
55+
$this->assertMiss($this->getResponse('/cache.php'));
56+
$this->assertHit($this->getResponse('/json.php'));
57+
}
58+
}

Diff for: tests/Functional/Fixtures/secret

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fos

0 commit comments

Comments
 (0)