3 * Squid and Varnish cache purging.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
24 * An HTTP 1.0 client built for the purposes of purging Squid and Varnish.
25 * Uses asynchronous I/O, allowing purges to be done in a highly parallel
28 * Could be replaced by curl_multi_exec() or some such.
30 class SquidPurgeClient
{
37 /** @var string|bool */
41 protected $readState = 'idle';
44 protected $writeBuffer = '';
47 protected $requests = [];
50 protected $currentRequestIndex;
54 const EINPROGRESS
= 115;
55 const BUFFER_SIZE
= 8192;
58 * @var resource|null The socket resource, or null for unconnected, or false
59 * for disabled due to error.
64 protected $readBuffer;
67 protected $bodyRemaining;
70 * @param string $server
71 * @param array $options
73 public function __construct( $server, $options = [] ) {
74 $parts = explode( ':', $server, 2 );
75 $this->host
= $parts[0];
76 $this->port
= isset( $parts[1] ) ?
$parts[1] : 80;
80 * Open a socket if there isn't one open already, return it.
81 * Returns false on error.
83 * @return bool|resource
85 protected function getSocket() {
86 if ( $this->socket
!== null ) {
92 $this->log( "DNS error" );
96 $this->socket
= socket_create( AF_INET
, SOCK_STREAM
, SOL_TCP
);
97 socket_set_nonblock( $this->socket
);
98 MediaWiki\
suppressWarnings();
99 $ok = socket_connect( $this->socket
, $ip, $this->port
);
100 MediaWiki\restoreWarnings
();
102 $error = socket_last_error( $this->socket
);
103 if ( $error !== self
::EINPROGRESS
) {
104 $this->log( "connection error: " . socket_strerror( $error ) );
110 return $this->socket
;
114 * Get read socket array for select()
117 public function getReadSocketsForSelect() {
118 if ( $this->readState
== 'idle' ) {
121 $socket = $this->getSocket();
122 if ( $socket === false ) {
129 * Get write socket array for select()
132 public function getWriteSocketsForSelect() {
133 if ( !strlen( $this->writeBuffer
) ) {
136 $socket = $this->getSocket();
137 if ( $socket === false ) {
144 * Get the host's IP address.
145 * Does not support IPv6 at present due to the lack of a convenient interface in PHP.
146 * @throws MWException
149 protected function getIP() {
150 if ( $this->ip
=== null ) {
151 if ( IP
::isIPv4( $this->host
) ) {
152 $this->ip
= $this->host
;
153 } elseif ( IP
::isIPv6( $this->host
) ) {
154 throw new MWException( '$wgSquidServers does not support IPv6' );
156 MediaWiki\
suppressWarnings();
157 $this->ip
= gethostbyname( $this->host
);
158 if ( $this->ip
=== $this->host
) {
161 MediaWiki\restoreWarnings
();
168 * Close the socket and ignore any future purge requests.
169 * This is called if there is a protocol error.
171 protected function markDown() {
173 $this->socket
= false;
177 * Close the socket but allow it to be reopened for future purge requests
179 public function close() {
180 if ( $this->socket
) {
181 MediaWiki\
suppressWarnings();
182 socket_set_block( $this->socket
);
183 socket_shutdown( $this->socket
);
184 socket_close( $this->socket
);
185 MediaWiki\restoreWarnings
();
187 $this->socket
= null;
188 $this->readBuffer
= '';
189 // Write buffer is kept since it may contain a request for the next socket
193 * Queue a purge operation
197 public function queuePurge( $url ) {
198 global $wgSquidPurgeUseHostHeader;
199 $url = CdnCacheUpdate
::expand( str_replace( "\n", '', $url ) );
201 if ( $wgSquidPurgeUseHostHeader ) {
202 $url = wfParseUrl( $url );
203 $host = $url['host'];
204 if ( isset( $url['port'] ) && strlen( $url['port'] ) > 0 ) {
205 $host .= ":" . $url['port'];
207 $path = $url['path'];
208 if ( isset( $url['query'] ) && is_string( $url['query'] ) ) {
209 $path = wfAppendQuery( $path, $url['query'] );
211 $request[] = "PURGE $path HTTP/1.1";
212 $request[] = "Host: $host";
214 $request[] = "PURGE $url HTTP/1.0";
216 $request[] = "Connection: Keep-Alive";
217 $request[] = "Proxy-Connection: Keep-Alive";
218 $request[] = "User-Agent: " . Http
::userAgent() . ' ' . __CLASS__
;
219 // Two ''s to create \r\n\r\n
223 $this->requests
[] = implode( "\r\n", $request );
224 if ( $this->currentRequestIndex
=== null ) {
225 $this->nextRequest();
232 public function isIdle() {
233 return strlen( $this->writeBuffer
) == 0 && $this->readState
== 'idle';
237 * Perform pending writes. Call this when socket_select() indicates that writing will not block.
239 public function doWrites() {
240 if ( !strlen( $this->writeBuffer
) ) {
243 $socket = $this->getSocket();
248 if ( strlen( $this->writeBuffer
) <= self
::BUFFER_SIZE
) {
249 $buf = $this->writeBuffer
;
252 $buf = substr( $this->writeBuffer
, 0, self
::BUFFER_SIZE
);
255 MediaWiki\
suppressWarnings();
256 $bytesSent = socket_send( $socket, $buf, strlen( $buf ), $flags );
257 MediaWiki\restoreWarnings
();
259 if ( $bytesSent === false ) {
260 $error = socket_last_error( $socket );
261 if ( $error != self
::EAGAIN
&& $error != self
::EINTR
) {
262 $this->log( 'write error: ' . socket_strerror( $error ) );
268 $this->writeBuffer
= substr( $this->writeBuffer
, $bytesSent );
272 * Read some data. Call this when socket_select() indicates that the read buffer is non-empty.
274 public function doReads() {
275 $socket = $this->getSocket();
281 MediaWiki\
suppressWarnings();
282 $bytesRead = socket_recv( $socket, $buf, self
::BUFFER_SIZE
, 0 );
283 MediaWiki\restoreWarnings
();
284 if ( $bytesRead === false ) {
285 $error = socket_last_error( $socket );
286 if ( $error != self
::EAGAIN
&& $error != self
::EINTR
) {
287 $this->log( 'read error: ' . socket_strerror( $error ) );
291 } elseif ( $bytesRead === 0 ) {
297 $this->readBuffer
.= $buf;
298 while ( $this->socket
&& $this->processReadBuffer() === 'continue' );
302 * @throws MWException
305 protected function processReadBuffer() {
306 switch ( $this->readState
) {
311 $lines = explode( "\r\n", $this->readBuffer
, 2 );
312 if ( count( $lines ) < 2 ) {
315 if ( $this->readState
== 'status' ) {
316 $this->processStatusLine( $lines[0] );
318 $this->processHeaderLine( $lines[0] );
320 $this->readBuffer
= $lines[1];
323 if ( $this->bodyRemaining
!== null ) {
324 if ( $this->bodyRemaining
> strlen( $this->readBuffer
) ) {
325 $this->bodyRemaining
-= strlen( $this->readBuffer
);
326 $this->readBuffer
= '';
329 $this->readBuffer
= substr( $this->readBuffer
, $this->bodyRemaining
);
330 $this->bodyRemaining
= 0;
331 $this->nextRequest();
335 // No content length, read all data to EOF
336 $this->readBuffer
= '';
340 throw new MWException( __METHOD__
. ': unexpected state' );
345 * @param string $line
347 protected function processStatusLine( $line ) {
348 if ( !preg_match( '!^HTTP/(\d+)\.(\d+) (\d{3}) (.*)$!', $line, $m ) ) {
349 $this->log( 'invalid status line' );
353 list( , , , $status, $reason ) = $m;
354 $status = intval( $status );
355 if ( $status !== 200 && $status !== 404 ) {
356 $this->log( "unexpected status code: $status $reason" );
360 $this->readState
= 'header';
364 * @param string $line
366 protected function processHeaderLine( $line ) {
367 if ( preg_match( '/^Content-Length: (\d+)$/i', $line, $m ) ) {
368 $this->bodyRemaining
= intval( $m[1] );
369 } elseif ( $line === '' ) {
370 $this->readState
= 'body';
374 protected function nextRequest() {
375 if ( $this->currentRequestIndex
!== null ) {
376 unset( $this->requests
[$this->currentRequestIndex
] );
378 if ( count( $this->requests
) ) {
379 $this->readState
= 'status';
380 $this->currentRequestIndex
= key( $this->requests
);
381 $this->writeBuffer
= $this->requests
[$this->currentRequestIndex
];
383 $this->readState
= 'idle';
384 $this->currentRequestIndex
= null;
385 $this->writeBuffer
= '';
387 $this->bodyRemaining
= null;
393 protected function log( $msg ) {
394 wfDebugLog( 'squid', __CLASS__
. " ($this->host): $msg" );