MediaWiki  master
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 = SOCKET_EINTR;
53  const EAGAIN = SOCKET_EAGAIN;
54  const EINPROGRESS = SOCKET_EINPROGRESS;
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 = [];
200  if ( $wgSquidPurgeUseHostHeader ) {
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  $flags = 0;
249 
250  if ( strlen( $this->writeBuffer ) <= self::BUFFER_SIZE ) {
251  $buf = $this->writeBuffer;
252  } else {
253  $buf = substr( $this->writeBuffer, 0, self::BUFFER_SIZE );
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 }
getSocket()
Open a socket if there isn&#39;t one open already, return it.
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
getWriteSocketsForSelect()
Get write socket array for select()
queuePurge( $url)
Queue a purge operation.
static userAgent()
A standard user-agent we can use for external requests.
Definition: Http.php:98
getReadSocketsForSelect()
Get read socket array for select()
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
$wgSquidPurgeUseHostHeader
Whether to use a Host header in purge requests sent to the proxy servers configured in $wgCdnServers...
doWrites()
Perform pending writes.
An HTTP 1.0 client built for the purposes of purging Squid and Varnish.
markDown()
Close the socket and ignore any future purge requests.
static isIPv6( $ip)
Given a string, determine if it as valid IP in IPv6 only.
Definition: IP.php:88
$lines
Definition: router.php:61
getIP()
Get the host&#39;s IP address.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
static isIPv4( $ip)
Given a string, determine if it as valid IP in IPv4 only.
Definition: IP.php:99
close()
Close the socket but allow it to be reopened for future purge requests.
$line
Definition: mcc.php:119
resource null $socket
The socket resource, or null for unconnected, or false for disabled due to error. ...
doReads()
Read some data.