VarnishSocket::calculateAuthToken()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
/**
4
 * Based on the Varnish Admin Socket class used in the Terpentine extension for Magento.
5
 * @link https://github.com/nexcess/magento-turpentine
6
 *
7
 * This was in turn based on Tim Whitlock's VarnishAdminSocket.php from php-varnish
8
 * @link https://github.com/timwhitlock/php-varnish
9
 *
10
 * Pieces from both resources above were used to fit our needs.
11
 */
12
13
/**
14
 * Nexcess.net Turpentine Extension for Magento
15
 * Copyright (C) 2012  Nexcess.net L.L.C.
16
 *
17
 * This program is free software; you can redistribute it and/or modify
18
 * it under the terms of the GNU General Public License as published by
19
 * the Free Software Foundation; either version 2 of the License, or
20
 * (at your option) any later version.
21
 *
22
 * This program is distributed in the hope that it will be useful,
23
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25
 * GNU General Public License for more details.
26
 *
27
 * You should have received a copy of the GNU General Public License along
28
 * with this program; if not, write to the Free Software Foundation, Inc.,
29
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
30
 */
31
32
/**
33
 * Copyright (c) 2010 Tim Whitlock.
34
 *
35
 * Permission is hereby granted, free of charge, to any person obtaining a copy
36
 * of this software and associated documentation files (the "Software"), to deal
37
 * in the Software without restriction, including without limitation the rights
38
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
39
 * copies of the Software, and to permit persons to whom the Software is
40
 * furnished to do so, subject to the following conditions:
41
 *
42
 * The above copyright notice and this permission notice shall be included in
43
 * all copies or substantial portions of the Software.
44
 *
45
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
46
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
47
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
48
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
49
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
50
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
51
 * THE SOFTWARE.
52
 */
53
54
namespace DeltaBlue\Varnish;
55
56
class VarnishSocket
57
{
58
    /**
59
     * The socket used to connect to Varnish and a timeout in seconds.
60
     */
61
    protected $varnishSocket = null;
62
    protected $socketTimeout = 10;
63
64
    /**
65
     * Limits for reading and writing to and from the socket.
66
     */
67
    const CHUNK_SIZE = 1024;
68
    const WRITE_MAX_SIZE = 16 * self::CHUNK_SIZE;
69
70
    /**
71
     * Connect to the Varnish socket and authenticate when needed.
72
     * @param string $host
73
     * @param int $port
74
     * @param string $secret
75
     *
76
     * @return bool
77
     *
78
     * @throws \Exception
79
     */
80
    public function connect($host, $port, $secret = '')
81
    {
82
        // Open socket connection
83
        self::socketConnect($host, $port);
84
        self::authenticate($secret);
85
86
        return $this->isConnected();
87
    }
88
89
    /**
90
     * @param string $host
91
     * @param integer $port
92
     * @throws \Exception
93
     */
94
    private function socketConnect($host, $port)
95
    {
96
        $this->varnishSocket = fsockopen(
97
            $host, $port,
98
            $errno, $errstr,
99
            $this->socketTimeout
100
        );
101
102
        if (! self::isConnected()) {
103
            throw new \Exception(sprintf(
104
                'Failed to connect to Varnish on %s:%d, error %d: %s',
105
                $host, $port, $errno, $errstr
106
            ));
107
        }
108
109
        // Set stream options
110
        stream_set_blocking($this->varnishSocket, true);
111
        stream_set_timeout($this->varnishSocket, $this->socketTimeout);
112
    }
113
114
    /**
115
     * @param string $secret
116
     * @throws \Exception
117
     */
118
    private function authenticate($secret)
119
    {
120
        // Read first response from socket
121
        $response = $this->read();
122
123
        // Authenticate using secret if authentication is required
124
        // https://varnish-cache.org/docs/trunk/reference/varnish-cli.html#authentication-with-s
125
        if ($response->isAuthRequest()) {
126
            // Generate the authentication token based on the challenge and secret
127
            $token = $this->calculateAuthToken($response->getAuthChallenge(), $secret);
128
129
            // Authenticate using token
130
            $response = $this->command(
131
                sprintf('auth %s', $token)
132
            );
133
134
            if ($response->getCode() !== VarnishResponse::VARN_OK) {
135
                throw new \Exception(sprintf(
136
                    'Varnish admin authentication failed: %s',
137
                    $response->getContent()
138
                ));
139
            }
140
        }
141
    }
142
143
    /**
144
     * Check if we're connected to Varnish socket.
145
     *
146
     * @return bool
147
     */
148
    public function isConnected()
149
    {
150
        if (is_resource($this->varnishSocket)) {
151
            $meta = stream_get_meta_data($this->varnishSocket);
152
153
            return ! ($meta['eof'] || $meta['timed_out']);
154
        }
155
156
        return false;
157
    }
158
159
    /**
160
     * @param string $challenge
161
     * @param $secret
162
     * @return string
163
     */
164
    private function calculateAuthToken($challenge, $secret)
165
    {
166
        // Ensure challenge ends with a newline
167
        $challenge = $this->ensureNewline($challenge);
168
        return hash('sha256',
169
            sprintf('%s%s%s',
170
                $challenge,
171
                $secret,
172
                $challenge
173
            ));
174
    }
175
176
    /**
177
     * @param $data
178
     * @return string
179
     */
180
    private function ensureNewline($data)
181
    {
182
        if (! preg_match('/\n$/', $data)) {
183
            $data .= "\n";
184
        }
185
        return $data;
186
    }
187
188
    /**
189
     * @return VarnishResponse
190
     *
191
     * @throws \Exception
192
     */
193
    private function read()
194
    {
195
        if (! $this->isConnected()) {
196
            throw new \Exception('Cannot read from Varnish socket because it\'s not connected');
197
        }
198
199
        // Read data from socket
200
        $response = self::readChunks();
201
202
        // Failed to get code from socket
203
        if ($response->getCode() === null) {
204
            throw new \Exception(
205
                'Failed to read response code from Varnish socket'
206
            );
207
        }
208
209
        return $response;
210
    }
211
212
    /**
213
     * @param VarnishResponse $response
214
     * @return VarnishResponse
215
     * @throws \Exception
216
     */
217
    private function readChunks(VarnishResponse $response = null)
218
    {
219
        if ($response === null) {
220
            $response = new VarnishResponse();
221
        }
222
223
        while (self::continueReading($response)) {
224
            $chunk = self::readSingleChunk();
225
226
            // Given content length
227
            if ($response->hasLength()) {
228
                $response->appendContent($chunk);
229
                continue;
230
            }
231
232
            // No content length given, expecting code + content length response
233
            if ($response->parseControlCommand($chunk)) {
234
                // Read actual content with given length
235
                return self::readChunks($response);
236
            }
237
        }
238
239
        return $response;
240
    }
241
242
    /**
243
     * Determine whether we should continue to read from the Varnish socket
244
     * - There is still data on the socket to read
245
     * - We have not reached the given content length
246
     *
247
     * @param VarnishResponse $response
248
     * @return bool
249
     */
250
    private function continueReading(VarnishResponse $response)
251
    {
252
        return ! feof($this->varnishSocket) && ! $response->finishedReading();
253
    }
254
255
    /**
256
     * Check if Varnish socket has timed out
257
     *
258
     * @throws \Exception
259
     */
260
    private function checkSocketTimeout()
261
    {
262
        $meta = stream_get_meta_data($this->varnishSocket);
263
        if ($meta['timed_out']) {
264
            throw new \Exception(
265
                'Varnish socket connection timed out'
266
            );
267
        }
268
    }
269
270
    /**
271
     * Read a single chunk from the Varnish socket
272
     *
273
     * @return string
274
     * @throws \Exception
275
     */
276
    private function readSingleChunk()
277
    {
278
        $chunk = fgets($this->varnishSocket, self::CHUNK_SIZE);
279
280
        // fgets returns false when an error occurs
281
        if ($chunk === false) {
282
            $chunk = '';
283
        }
284
285
        // Check for socket timeout when an empty chunk is returned
286
        if (empty($chunk)) {
287
            self::checkSocketTimeout();
288
        }
289
290
        return $chunk;
291
    }
292
293
    /**
294
     * Write data to the socket input stream.
295
     *
296
     * @param string $data
297
     *
298
     * @return VarnishSocket
299
     *
300
     * @throws \Exception
301
     */
302
    private function write($data)
303
    {
304
        if (! $this->isConnected()) {
305
            throw new \Exception('Cannot write to Varnish socket because it\'s not connected');
306
        }
307
        $data = $this->ensureNewline($data);
308
        if (strlen($data) >= self::WRITE_MAX_SIZE) {
309
            throw new \Exception(sprintf(
310
                'Data to write to Varnish socket is too large (max %d chars)',
311
                self::WRITE_MAX_SIZE
312
            ));
313
        }
314
315
        // Write data to socket
316
        $bytes = fwrite($this->varnishSocket, $data);
317
        if ($bytes !== strlen($data)) {
318
            throw new \Exception('Failed to write to Varnish socket');
319
        }
320
321
        return $this;
322
    }
323
324
    /**
325
     * Write a command to the socket with a trailing line break and get response straight away.
326
     *
327
     * @param string $cmd
328
     * @param int $ok
329
     *
330
     * @return VarnishResponse
331
     *
332
     * @throws \Exception
333
     */
334
    public function command($cmd, $ok = VarnishResponse::VARN_OK)
335
    {
336
        $response = $this->write($cmd)->read();
337
        if ($response->getCode() !== $ok) {
338
            throw new \Exception(
339
                sprintf(
340
                    "Command '%s' responded %d: '%s'",
341
                    $cmd, $response->getCode(), $response->getContent()
342
                ),
343
                $response->getCode()
344
            );
345
        }
346
347
        return $response;
348
    }
349
350
    /**
351
     * Brutal close, doesn't send quit command to varnishadm.
352
     *
353
     * @return void
354
     */
355
    public function close()
356
    {
357
        if (self::isConnected()) {
358
            fclose($this->varnishSocket);
359
        }
360
        $this->varnishSocket = null;
361
    }
362
363
    /**
364
     * Graceful close, sends quit command.
365
     *
366
     * @return void
367
     *
368
     * @throws \Exception
369
     */
370
    public function quit()
371
    {
372
        try {
373
            $this->command('quit', VarnishResponse::VARN_CLOSE);
374
        } finally {
375
            $this->close();
376
        }
377
    }
378
}
379