MediaWiki REL1_34
SquidPurgeClient.php
Go to the documentation of this file.
1<?php
32 protected $host;
33
35 protected $port;
36
38 protected $ip;
39
41 protected $readState = 'idle';
42
44 protected $writeBuffer = '';
45
47 protected $requests = [];
48
51
52 const EINTR = 4;
53 const EAGAIN = 11;
54 const EINPROGRESS = 115;
55 const BUFFER_SIZE = 8192;
56
61 protected $socket;
62
64 protected $readBuffer;
65
67 protected $bodyRemaining;
68
72 public function __construct( $server ) {
73 $parts = explode( ':', $server, 2 );
74 $this->host = $parts[0];
75 $this->port = $parts[1] ?? 80;
76 }
77
84 protected function getSocket() {
85 if ( $this->socket !== null ) {
86 return $this->socket;
87 }
88
89 $ip = $this->getIP();
90 if ( !$ip ) {
91 $this->log( "DNS error" );
92 $this->markDown();
93 return false;
94 }
95 $this->socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
96 socket_set_nonblock( $this->socket );
97 Wikimedia\suppressWarnings();
98 $ok = socket_connect( $this->socket, $ip, $this->port );
99 Wikimedia\restoreWarnings();
100 if ( !$ok ) {
101 $error = socket_last_error( $this->socket );
102 if ( $error !== self::EINPROGRESS ) {
103 $this->log( "connection error: " . socket_strerror( $error ) );
104 $this->markDown();
105 return false;
106 }
107 }
108
109 return $this->socket;
110 }
111
116 public function getReadSocketsForSelect() {
117 if ( $this->readState == 'idle' ) {
118 return [];
119 }
120 $socket = $this->getSocket();
121 if ( $socket === false ) {
122 return [];
123 }
124 return [ $socket ];
125 }
126
131 public function getWriteSocketsForSelect() {
132 if ( !strlen( $this->writeBuffer ) ) {
133 return [];
134 }
135 $socket = $this->getSocket();
136 if ( $socket === false ) {
137 return [];
138 }
139 return [ $socket ];
140 }
141
148 protected function getIP() {
149 if ( $this->ip === null ) {
150 if ( IP::isIPv4( $this->host ) ) {
151 $this->ip = $this->host;
152 } elseif ( IP::isIPv6( $this->host ) ) {
153 throw new MWException( '$wgCdnServers does not support IPv6' );
154 } else {
155 Wikimedia\suppressWarnings();
156 $this->ip = gethostbyname( $this->host );
157 if ( $this->ip === $this->host ) {
158 $this->ip = false;
159 }
160 Wikimedia\restoreWarnings();
161 }
162 }
163 return $this->ip;
164 }
165
170 protected function markDown() {
171 $this->close();
172 $this->socket = false;
173 }
174
178 public function close() {
179 if ( $this->socket ) {
180 Wikimedia\suppressWarnings();
181 socket_set_block( $this->socket );
182 socket_shutdown( $this->socket );
183 socket_close( $this->socket );
184 Wikimedia\restoreWarnings();
185 }
186 $this->socket = null;
187 $this->readBuffer = '';
188 // Write buffer is kept since it may contain a request for the next socket
189 }
190
196 public function queuePurge( $url ) {
198 $url = str_replace( "\n", '', $url ); // sanity
199 $request = [];
201 $url = wfParseUrl( $url );
202 $host = $url['host'];
203 if ( isset( $url['port'] ) && strlen( $url['port'] ) > 0 ) {
204 $host .= ":" . $url['port'];
205 }
206 $path = $url['path'];
207 if ( isset( $url['query'] ) && is_string( $url['query'] ) ) {
208 $path = wfAppendQuery( $path, $url['query'] );
209 }
210 $request[] = "PURGE $path HTTP/1.1";
211 $request[] = "Host: $host";
212 } else {
213 wfDeprecated( '$wgSquidPurgeUseHostHeader = false', '1.33' );
214 $request[] = "PURGE $url HTTP/1.0";
215 }
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
220 $request[] = '';
221 $request[] = '';
222
223 $this->requests[] = implode( "\r\n", $request );
224 if ( $this->currentRequestIndex === null ) {
225 $this->nextRequest();
226 }
227 }
228
232 public function isIdle() {
233 return strlen( $this->writeBuffer ) == 0 && $this->readState == 'idle';
234 }
235
239 public function doWrites() {
240 if ( !strlen( $this->writeBuffer ) ) {
241 return;
242 }
243 $socket = $this->getSocket();
244 if ( !$socket ) {
245 return;
246 }
247
248 if ( strlen( $this->writeBuffer ) <= self::BUFFER_SIZE ) {
249 $buf = $this->writeBuffer;
250 $flags = MSG_EOR;
251 } else {
252 $buf = substr( $this->writeBuffer, 0, self::BUFFER_SIZE );
253 $flags = 0;
254 }
255 Wikimedia\suppressWarnings();
256 $bytesSent = socket_send( $socket, $buf, strlen( $buf ), $flags );
257 Wikimedia\restoreWarnings();
258
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 ) );
263 $this->markDown();
264 }
265 return;
266 }
267
268 $this->writeBuffer = substr( $this->writeBuffer, $bytesSent );
269 }
270
274 public function doReads() {
275 $socket = $this->getSocket();
276 if ( !$socket ) {
277 return;
278 }
279
280 $buf = '';
281 Wikimedia\suppressWarnings();
282 $bytesRead = socket_recv( $socket, $buf, self::BUFFER_SIZE, 0 );
283 Wikimedia\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 ) );
288 $this->markDown();
289 return;
290 }
291 } elseif ( $bytesRead === 0 ) {
292 // Assume EOF
293 $this->close();
294 return;
295 }
296
297 $this->readBuffer .= $buf;
298 while ( $this->socket && $this->processReadBuffer() === 'continue' );
299 }
300
305 protected function processReadBuffer() {
306 switch ( $this->readState ) {
307 case 'idle':
308 return 'done';
309 case 'status':
310 case 'header':
311 $lines = explode( "\r\n", $this->readBuffer, 2 );
312 if ( count( $lines ) < 2 ) {
313 return 'done';
314 }
315 if ( $this->readState == 'status' ) {
316 $this->processStatusLine( $lines[0] );
317 } else {
318 $this->processHeaderLine( $lines[0] );
319 }
320 $this->readBuffer = $lines[1];
321 return 'continue';
322 case 'body':
323 if ( $this->bodyRemaining !== null ) {
324 if ( $this->bodyRemaining > strlen( $this->readBuffer ) ) {
325 $this->bodyRemaining -= strlen( $this->readBuffer );
326 $this->readBuffer = '';
327 return 'done';
328 } else {
329 $this->readBuffer = substr( $this->readBuffer, $this->bodyRemaining );
330 $this->bodyRemaining = 0;
331 $this->nextRequest();
332 return 'continue';
333 }
334 } else {
335 // No content length, read all data to EOF
336 $this->readBuffer = '';
337 return 'done';
338 }
339 default:
340 throw new MWException( __METHOD__ . ': unexpected state' );
341 }
342 }
343
347 protected function processStatusLine( $line ) {
348 if ( !preg_match( '!^HTTP/(\d+)\.(\d+) (\d{3}) (.*)$!', $line, $m ) ) {
349 $this->log( 'invalid status line' );
350 $this->markDown();
351 return;
352 }
353 list( , , , $status, $reason ) = $m;
354 $status = intval( $status );
355 if ( $status !== 200 && $status !== 404 ) {
356 $this->log( "unexpected status code: $status $reason" );
357 $this->markDown();
358 return;
359 }
360 $this->readState = 'header';
361 }
362
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';
371 }
372 }
373
374 protected function nextRequest() {
375 if ( $this->currentRequestIndex !== null ) {
376 unset( $this->requests[$this->currentRequestIndex] );
377 }
378 if ( count( $this->requests ) ) {
379 $this->readState = 'status';
380 $this->currentRequestIndex = key( $this->requests );
381 $this->writeBuffer = $this->requests[$this->currentRequestIndex];
382 } else {
383 $this->readState = 'idle';
384 $this->currentRequestIndex = null;
385 $this->writeBuffer = '';
386 }
387 $this->bodyRemaining = null;
388 }
389
393 protected function log( $msg ) {
394 wfDebugLog( 'squid', __CLASS__ . " ($this->host): $msg" );
395 }
396}
$wgSquidPurgeUseHostHeader
Whether to use a Host header in purge requests sent to the proxy servers configured in $wgCdnServers.
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
$line
Definition cdb.php:59
static userAgent()
A standard user-agent we can use for external requests.
Definition Http.php:98
MediaWiki exception.
An HTTP 1.0 client built for the purposes of purging Squid and Varnish.
markDown()
Close the socket and ignore any future purge requests.
getReadSocketsForSelect()
Get read socket array for select()
getIP()
Get the host's IP address.
doReads()
Read some data.
getSocket()
Open a socket if there isn't one open already, return it.
close()
Close the socket but allow it to be reopened for future purge requests.
getWriteSocketsForSelect()
Get write socket array for select()
resource null $socket
The socket resource, or null for unconnected, or false for disabled due to error.
queuePurge( $url)
Queue a purge operation.
doWrites()
Perform pending writes.
$lines
Definition router.php:61