start()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * Nexcess.net Turpentine Extension for Magento
5
 * Copyright (C) 2012  Nexcess.net L.L.C.
6
 *
7
 * This program is free software; you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation; either version 2 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License along
18
 * with this program; if not, write to the Free Software Foundation, Inc.,
19
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
 */
21
22
/**
23
 * Based heavily on Tim Whitlock's VarnishAdminSocket.php from php-varnish
24
 * @link https://github.com/timwhitlock/php-varnish
25
 *
26
 * Copyright (c) 2010 Tim Whitlock
27
 *
28
 * Permission is hereby granted, free of charge, to any person obtaining a copy
29
 * of this software and associated documentation files (the "Software"), to deal
30
 * in the Software without restriction, including without limitation the rights
31
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
32
 * copies of the Software, and to permit persons to whom the Software is
33
 * furnished to do so, subject to the following conditions:
34
 *
35
 * The above copyright notice and this permission notice shall be included in
36
 * all copies or substantial portions of the Software.
37
 *
38
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
44
 * THE SOFTWARE.
45
 */
46
47
/**
48
 * @method array help()
49
 * @method array ping()
50
 * @method array auth()
51
 * @method array banner()
52
 * @method array vcl_load()
53
 * @method array vcl_inline()
54
 * @method array vcl_use()
55
 * @method array vcl_discard()
56
 * @method array vcl_list()
57
 * @method array vcl_show()
58
 * @method array param_show()
59
 * @method array param_set()
60
 * Warning: ban_url does a non-lurker-friendly ban. This means it is not cleaned
61
 *          up from the ban list. A long ban list will slow down Varnish.
62
 * @method array ban_url()
63
 * @method array ban()
64
 * @method array ban_list()
65
 */
66
class Nexcessnet_Turpentine_Model_Varnish_Admin_Socket {
67
68
    // possible command return codes, from vcli.h
69
    const CODE_SYNTAX       = 100;
70
    const CODE_UNKNOWN      = 101;
71
    const CODE_UNIMPL       = 102;
72
    const CODE_TOOFEW       = 104;
73
    const CODE_TOOMANY      = 105;
74
    const CODE_PARAM        = 106;
75
    const CODE_AUTH         = 107;
76
    const CODE_OK           = 200;
77
    const CODE_CANT         = 300;
78
    const CODE_COMMS        = 400;
79
    const CODE_CLOSE        = 500;
80
81
    const READ_CHUNK_SIZE   = 1024;
82
    // varnish default, can only be changed at Varnish startup time
83
    // if data to write is over this limit the actual run-time limit is checked
84
    // and used
85
    const CLI_CMD_LENGTH_LIMIT = 16384;
86
87
    /**
88
     * Regexp to detect the varnish version number
89
     * @var string
90
     */
91
    const REGEXP_VARNISH_VERSION = '/^varnish\-(?P<vmajor>\d+)\.(?P<vminor>\d+)\.(?P<vsub>\d+) revision (?P<vhash>[0-9a-f]+)$/';
92
93
    /**
94
     * VCL config versions, should match config select values
95
     */
96
    static protected $_VERSIONS = array('2.1', '3.0', '4.0', '4.1');
97
98
    /**
99
     * Varnish socket connection
100
     *
101
     * @var resource
102
     */
103
    protected $_varnishConn = null;
104
    protected $_host = '127.0.0.1';
105
    protected $_port = 6082;
106
    protected $_private = null;
107
    protected $_authSecret = null;
108
    protected $_timeout = 5;
109
    protected $_version = null; //auto-detect
110
111
    public function __construct(array $options = array()) {
112
        foreach ($options as $key => $value) {
113
            switch ($key) {
114
                case 'host':
115
                    $this->setHost($value);
116
                    break;
117
                case 'port':
118
                    $this->setPort($value);
119
                    break;
120
                case 'auth_secret':
121
                    $this->setAuthSecret($value);
122
                    break;
123
                case 'timeout':
124
                    $this->setTimeout($value);
125
                    break;
126
                case 'version':
127
                    $this->setVersion($value);
128
                    break;
129
                default:
130
                    break;
131
            }
132
        }
133
    }
134
135
    /**
136
     * Provide simple Varnish methods
137
     *
138
     * Methods provided:
139
            help [command]
140
            ping [timestamp]
141
            auth response
142
            banner
143
            stats
144
            vcl.load <configname> <filename>
145
            vcl.inline <configname> <quoted_VCLstring>
146
            vcl.use <configname>
147
            vcl.discard <configname>
148
            vcl.list
149
            vcl.show <configname>
150
            param.show [-l] [<param>]
151
            param.set <param> <value>
152
            purge.url <regexp>
153
            purge <field> <operator> <arg> [&& <field> <oper> <arg>]...
154
            purge.list
155
     *
156
     * @param  string $name method name
157
     * @param  array $args method args
158
     * @return array
159
     */
160
    public function __call($name, $args) {
161
        array_unshift($args, self::CODE_OK);
162
        array_unshift($args, $this->_translateCommandMethod($name));
163
        return call_user_func_array(array($this, '_command'), $args);
164
    }
165
166
    /**
167
     * Get the connection string for this socket (<host>:<port>)
168
     *
169
     * @return string
170
     */
171
    public function getConnectionString() {
172
        return sprintf('%s:%d', $this->getHost(), $this->getPort());
173
    }
174
175
    /**
176
     * Get the set host for this instance
177
     *
178
     * @return string
179
     */
180
    public function getHost() {
181
        return $this->_host;
182
    }
183
184
    /**
185
     * Set the Varnish host name/ip to connect to
186
     *
187
     * @param string $host hostname or ip
188
     */
189
    public function setHost($host) {
190
        $this->_close();
191
        $this->_host = $host;
192
        return $this;
193
    }
194
195
    /**
196
     * Get the port set for this instance
197
     *
198
     * @return int
199
     */
200
    public function getPort() {
201
        return $this->_port;
202
    }
203
204
    /**
205
     * Set the Varnish admin port
206
     *
207
     * @param int $port
208
     */
209
    public function setPort($port) {
210
        $this->_close();
211
        $this->_port = (int) $port;
212
        return $this;
213
    }
214
215
    /**
216
     * Set the Varnish admin auth secret, use null to indicate there isn't one
217
     *
218
     * @param string $authSecret
219
     */
220
    public function setAuthSecret($authSecret = null) {
221
        $this->_authSecret = $authSecret;
222
        return $this;
223
    }
224
225
    /**
226
     * Set the timeout to connect to the varnish instance
227
     *
228
     * @param int $timeout
229
     */
230
    public function setTimeout($timeout) {
231
        $this->_timeout = (int) $timeout;
232
        if ( ! is_null($this->_varnishConn)) {
233
            stream_set_timeout($this->_varnishConn, $this->_timeout);
234
        }
235
        return $this;
236
    }
237
238
    /**
239
     * Explicitly set the version of the varnish instance we're connecting to
240
     *
241
     * @param string $version version from $_VERSIONS
242
     */
243
    public function setVersion($version) {
244
        if (in_array($version, self::$_VERSIONS)) {
245
            $this->_version = $version;
246
        } else {
247
            Mage::throwException('Unsupported Varnish version: '.$version);
248
        }
249
    }
250
251
    /**
252
     * Check if we're connected to Varnish
253
     *
254
     * @return boolean
255
     */
256
    public function isConnected() {
257
        return ! is_null($this->_varnishConn);
258
    }
259
260
    /**
261
     * Find out what version mode we're running in
262
     *
263
     * @return string
264
     */
265
    public function getVersion() {
266
        if ( ! $this->isConnected()) {
267
            $this->_connect();
268
        }
269
        return $this->_version;
270
    }
271
272
    /**
273
     * Stop the Varnish instance
274
     */
275
    public function quit() {
276
        $this->_command('quit', self::CODE_CLOSE);
277
        $this->_close();
278
    }
279
280
    /**
281
     * Check if Varnish has a child running or not
282
     *
283
     * @return boolean
284
     */
285
    public function status() {
286
        $response = $this->_command('status');
287
        if ( ! preg_match('~Child in state (\w+)~', $response['text'], $match)) {
288
            return false;
289
        } else {
290
            return $match[1] === 'running';
291
        }
292
    }
293
294
    /**
295
     * Stop the running child (if it is running)
296
     *
297
     * @return $this
298
     */
299
    public function stop() {
300
        if ($this->status()) {
301
            $this->_command('stop');
302
        }
303
        return $this;
304
    }
305
306
    /**
307
     * Start running the Varnish child
308
     *
309
     * @return $this
310
     */
311
    public function start() {
312
        $this->_command('start');
313
        return $this;
314
    }
315
316
    /**
317
     * Establish a connection to the configured Varnish instance
318
     *
319
     * @return boolean
320
     */
321
    protected function _connect() {
322
        $this->_varnishConn = fsockopen($this->_host, $this->_port, $errno,
323
            $errstr, $this->_timeout);
324
        if ( ! is_resource($this->_varnishConn)) {
325
            Mage::throwException(sprintf(
326
                'Failed to connect to Varnish on [%s:%d]: (%d) %s',
327
                $this->_host, $this->_port, $errno, $errstr ));
328
        }
329
330
        stream_set_blocking($this->_varnishConn, 1);
331
        stream_set_timeout($this->_varnishConn, $this->_timeout);
332
333
        //varnish 2.0 doesn't spit out a banner on connection, this will need
334
        //to be changed if 2.0 support is ever added
335
        $banner = $this->_read();
336
        if ($banner['code'] === self::CODE_AUTH) {
337
            $challenge = substr($banner['text'], 0, 32);
338
            $response = hash('sha256', sprintf("%s\n%s%s\n", $challenge,
339
                $this->_authSecret, $challenge));
340
            $banner = $this->_command('auth', self::CODE_OK, $response);
341
        }
342
343
        if ($banner['code'] !== self::CODE_OK) {
344
            Mage::throwException('Varnish admin authentication failed: '.
345
                $banner['text']);
346
        }
347
348
        if ($this->_version == null) { // If autodetecting
349
            $this->_version = $this->_determineVersion($banner['text']);
350
        }
351
352
        return $this->isConnected();
353
    }
354
355
    /**
356
     * @param string $bannerText
357
     */
358
    protected function _determineVersion($bannerText) {
359
        $bannerText = array_filter(explode("\n", $bannerText));
360
        if (count($bannerText) < 6) {
361
            // Varnish 2.0 does not spit out a banner on connect
362
            Mage::throwException('Varnish versions before 2.1 are not supported');
363
        }
364
        if (count($bannerText) < 7) {
365
            // Varnish before 3.0.4 does not spit out a version number
366
            $resp = $this->_write('help')->_read();
367
            if (strpos($resp['text'], 'ban.url') !== false) {
368
                // Varnish versions 3.0 through 3.0.3 do not return a version banner. 
369
                // To differentiate between 2.1 and 3.0, we check the existence of the ban.url command.
370
                return '3.0';
371
            }
372
            return '2.1';
373
        } elseif (preg_match(self::REGEXP_VARNISH_VERSION, $bannerText[4], $matches) === 1) {
374
            return $matches['vmajor'].'.'.$matches['vminor'];
375
        } else {
376
            Mage::throwException('Unable to detect varnish version');
377
        }
378
    }
379
380
    /**
381
     * Close the connection (if we're connected)
382
     *
383
     * @return $this
384
     */
385
    protected function _close() {
386
        if ($this->isConnected()) {
387
            fclose($this->_varnishConn);
388
            $this->_varnishConn = null;
389
        }
390
        return $this;
391
    }
392
393
    /**
394
     * Write data to the Varnish instance, a newline is automatically appended
395
     *
396
     * @param  string $data data to write
397
     * @return $this
398
     */
399
    protected function _write($data) {
400
        if (is_null($this->_varnishConn)) {
401
            $this->_connect();
402
        }
403
        $data = rtrim($data).PHP_EOL;
404
        $dataLength = strlen($data);
405
        if ($dataLength >= self::CLI_CMD_LENGTH_LIMIT) {
406
            $cliBufferResponse = $this->param_show('cli_buffer');
0 ignored issues
show
Unused Code introduced by
The call to Nexcessnet_Turpentine_Mo...in_Socket::param_show() has too many arguments starting with 'cli_buffer'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
407
            $regexp = '~^cli_buffer\s+(\d+)\s+\[bytes\]~';
408
            if ($this->getVersion() === '4.0' || $this->getVersion() === '4.1') {
409
                // Varnish4 supports "16k" style notation
410
                $regexp = '~^cli_buffer\s+Value is:\s+(\d+)([k|m|g|b]{1})?\s+\[bytes\]~';
411
            }
412
            if (preg_match($regexp, $cliBufferResponse['text'], $match)) {
413
                $realLimit = (int) $match[1];
414
                if (isset($match[2])) {
415
                    $factors = array('b'=>0, 'k'=>1, 'm'=>2, 'g'=>3);
416
                    $realLimit *= pow(1024, $factors[$match[2]]);
417
                }
418
            } else {
419
                Mage::helper('turpentine/debug')->logWarn(
420
                    'Failed to determine Varnish cli_buffer limit, using default' );
421
                $realLimit = self::CLI_CMD_LENGTH_LIMIT;
422
            }
423
            if ($dataLength >= $realLimit) {
424
                Mage::throwException(sprintf(
425
                    'Varnish data to write over length limit by %d characters',
426
                    $dataLength - $realLimit ));
427
            }
428
        }
429
        if (($byteCount = fwrite($this->_varnishConn, $data)) !== $dataLength) {
430
            Mage::throwException(sprintf('Varnish socket write error: %d != %d',
431
                $byteCount, $dataLength));
432
        }
433
        return $this;
434
    }
435
436
    /**
437
     * Read a response from Varnish instance
438
     *
439
     * @return array tuple of the response (code, text)
440
     */
441
    protected function _read() {
442
        $code = null;
443
        $len = -1;
444
        while ( ! feof($this->_varnishConn)) {
445
            $response = fgets($this->_varnishConn, self::READ_CHUNK_SIZE);
446
            if (empty($response)) {
447
                $streamMeta = stream_get_meta_data($this->_varnishConn);
448
                if ($streamMeta['timed_out']) {
449
                    Mage::throwException('Varnish admin socket timeout');
450
                }
451
            }
452
            if (preg_match('~^(\d{3}) (\d+)~', $response, $match)) {
453
                $code = (int) $match[1];
454
                $len = (int) $match[2];
455
                break;
456
            }
457
        }
458
459
        if (is_null($code)) {
460
            Mage::throwException('Failed to read response code from Varnish');
461
        } else {
462
            $response = array('code' => $code, 'text' => '');
463
            while ( ! feof($this->_varnishConn) &&
464
                    strlen($response['text']) < $len) {
465
                $response['text'] .= fgets($this->_varnishConn,
466
                    self::READ_CHUNK_SIZE);
467
            }
468
            return $response;
469
        }
470
    }
471
472
    /**
473
     * [_command description]
474
     * @param  string  $verb       command name
475
     * @param  integer $okCode code that indicates command was successful
476
     * @param  string  ...         command args
477
     * @return array
478
     */
479
    protected function _command($verb, $okCode = 200) {
480
        $params = func_get_args();
481
        //remove $verb
482
        array_shift($params);
483
        //remove $okCode (if it exists)
484
        array_shift($params);
485
        $cleanedParams = array();
486
        foreach ($params as $param) {
487
            $cp = addcslashes($param, "\"\\");
488
            $cp = str_replace(PHP_EOL, '\n', $cp);
489
            $cleanedParams[] = sprintf('"%s"', $cp);
490
        }
491
        $data = implode(' ', array_merge(
492
            array(sprintf('"%s"', $verb)),
493
            $cleanedParams ));
494
        $response = $this->_write($data)->_read();
495
        if ($response['code'] !== $okCode && ! is_null($okCode)) {
496
            Mage::helper('turpentine/debug')->logDebug(
497
                'Error on Varnish command: %s', $data );
498
            Mage::throwException(sprintf(
499
                "Got unexpected response code from Varnish: %d\n%s",
500
                $response['code'], $response['text'] ));
501
        } else {
502
            if (Mage::getStoreConfig('turpentine_varnish/general/varnish_log_commands')) { 
503
                Mage::helper('turpentine/debug')->logDebug('VARNISH command sent: '.$data);
504
            }
505
            return $response;
506
        }
507
    }
508
509
    /**
510
     * Handle v2.1 <> v3.0 command compatibility
511
     *
512
     * @param  string $verb command to check
513
     * @return string
514
     */
515
    protected function _translateCommandMethod($verb) {
516
        $command = str_replace('_', '.', $verb);
517
        switch ($this->getVersion()) {
518
            case '2.1':
519
                $command = str_replace('ban', 'purge', $command);
520
                break;
521
            case '4.1':
522
            case '4.0':
523
            case '3.0':
524
                $command = str_replace('purge', 'ban', $command);
525
                break;
526
            default:
527
                Mage::throwException('Unrecognized Varnish version: '.
528
                    $this->_version);
529
        }
530
        return $command;
531
    }
532
}
533