MediaWiki  master
SquidPurgeClient.php
Go to the documentation of this file.
1 <?php
23 use Wikimedia\IPUtils;
24 
34  protected $host;
35 
37  protected $port;
38 
40  protected $ip;
41 
43  protected $readState = 'idle';
44 
46  protected $writeBuffer = '';
47 
49  protected $requests = [];
50 
53 
54  public const EINTR = SOCKET_EINTR;
55  public const EAGAIN = SOCKET_EAGAIN;
56  public const EINPROGRESS = SOCKET_EINPROGRESS;
57  public const BUFFER_SIZE = 8192;
58 
63  protected $socket;
64 
66  protected $readBuffer;
67 
69  protected $bodyRemaining;
70 
74  public function __construct( $server ) {
75  $parts = explode( ':', $server, 2 );
76  $this->host = $parts[0];
77  $this->port = $parts[1] ?? 80;
78  }
79 
86  protected function getSocket() {
87  if ( $this->socket !== null ) {
88  return $this->socket;
89  }
90 
91  $ip = $this->getIP();
92  if ( !$ip ) {
93  $this->log( "DNS error" );
94  $this->markDown();
95  return false;
96  }
97  $this->socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
98  socket_set_nonblock( $this->socket );
99  Wikimedia\suppressWarnings();
100  $ok = socket_connect( $this->socket, $ip, $this->port );
101  Wikimedia\restoreWarnings();
102  if ( !$ok ) {
103  $error = socket_last_error( $this->socket );
104  if ( $error !== self::EINPROGRESS ) {
105  $this->log( "connection error: " . socket_strerror( $error ) );
106  $this->markDown();
107  return false;
108  }
109  }
110 
111  return $this->socket;
112  }
113 
118  public function getReadSocketsForSelect() {
119  if ( $this->readState == 'idle' ) {
120  return [];
121  }
122  $socket = $this->getSocket();
123  if ( $socket === false ) {
124  return [];
125  }
126  return [ $socket ];
127  }
128 
133  public function getWriteSocketsForSelect() {
134  if ( !strlen( $this->writeBuffer ) ) {
135  return [];
136  }
137  $socket = $this->getSocket();
138  if ( $socket === false ) {
139  return [];
140  }
141  return [ $socket ];
142  }
143 
150  protected function getIP() {
151  if ( $this->ip === null ) {
152  if ( IPUtils::isIPv4( $this->host ) ) {
153  $this->ip = $this->host;
154  } elseif ( IPUtils::isIPv6( $this->host ) ) {
155  throw new MWException( '$wgCdnServers does not support IPv6' );
156  } else {
157  Wikimedia\suppressWarnings();
158  $this->ip = gethostbyname( $this->host );
159  if ( $this->ip === $this->host ) {
160  $this->ip = false;
161  }
162  Wikimedia\restoreWarnings();
163  }
164  }
165  return $this->ip;
166  }
167 
172  protected function markDown() {
173  $this->close();
174  $this->socket = false;
175  }
176 
180  public function close() {
181  if ( $this->socket ) {
182  Wikimedia\suppressWarnings();
183  socket_set_block( $this->socket );
184  socket_shutdown( $this->socket );
185  socket_close( $this->socket );
186  Wikimedia\restoreWarnings();
187  }
188  $this->socket = null;
189  $this->readBuffer = '';
190  // Write buffer is kept since it may contain a request for the next socket
191  }
192 
198  public function queuePurge( $url ) {
200  $url = str_replace( "\n", '', $url ); // sanity
201  $request = [];
203  $url = wfParseUrl( $url );
204  $host = $url['host'];
205  if ( isset( $url['port'] ) && strlen( $url['port'] ) > 0 ) {
206  $host .= ":" . $url['port'];
207  }
208  $path = $url['path'];
209  if ( isset( $url['query'] ) && is_string( $url['query'] ) ) {
210  $path = wfAppendQuery( $path, $url['query'] );
211  }
212  $request[] = "PURGE $path HTTP/1.1";
213  $request[] = "Host: $host";
214  } else {
215  wfDeprecated( '$wgSquidPurgeUseHostHeader = false', '1.33' );
216  $request[] = "PURGE $url HTTP/1.0";
217  }
218  $request[] = "Connection: Keep-Alive";
219  $request[] = "Proxy-Connection: Keep-Alive";
220  $request[] = "User-Agent: " . Http::userAgent() . ' ' . __CLASS__;
221  // Two ''s to create \r\n\r\n
222  $request[] = '';
223  $request[] = '';
224 
225  $this->requests[] = implode( "\r\n", $request );
226  if ( $this->currentRequestIndex === null ) {
227  $this->nextRequest();
228  }
229  }
230 
234  public function isIdle() {
235  return strlen( $this->writeBuffer ) == 0 && $this->readState == 'idle';
236  }
237 
241  public function doWrites() {
242  if ( !strlen( $this->writeBuffer ) ) {
243  return;
244  }
245  $socket = $this->getSocket();
246  if ( !$socket ) {
247  return;
248  }
249 
250  $flags = 0;
251 
252  if ( strlen( $this->writeBuffer ) <= self::BUFFER_SIZE ) {
253  $buf = $this->writeBuffer;
254  } else {
255  $buf = substr( $this->writeBuffer, 0, self::BUFFER_SIZE );
256  }
257  Wikimedia\suppressWarnings();
258  $bytesSent = socket_send( $socket, $buf, strlen( $buf ), $flags );
259  Wikimedia\restoreWarnings();
260 
261  if ( $bytesSent === false ) {
262  $error = socket_last_error( $socket );
263  if ( $error != self::EAGAIN && $error != self::EINTR ) {
264  $this->log( 'write error: ' . socket_strerror( $error ) );
265  $this->markDown();
266  }
267  return;
268  }
269 
270  $this->writeBuffer = substr( $this->writeBuffer, $bytesSent );
271  }
272 
276  public function doReads() {
277  $socket = $this->getSocket();
278  if ( !$socket ) {
279  return;
280  }
281 
282  $buf = '';
283  Wikimedia\suppressWarnings();
284  $bytesRead = socket_recv( $socket, $buf, self::BUFFER_SIZE, 0 );
285  Wikimedia\restoreWarnings();
286  if ( $bytesRead === false ) {
287  $error = socket_last_error( $socket );
288  if ( $error != self::EAGAIN && $error != self::EINTR ) {
289  $this->log( 'read error: ' . socket_strerror( $error ) );
290  $this->markDown();
291  return;
292  }
293  } elseif ( $bytesRead === 0 ) {
294  // Assume EOF
295  $this->close();
296  return;
297  }
298 
299  $this->readBuffer .= $buf;
300  while ( $this->socket && $this->processReadBuffer() === 'continue' );
301  }
302 
307  protected function processReadBuffer() {
308  switch ( $this->readState ) {
309  case 'idle':
310  return 'done';
311  case 'status':
312  case 'header':
313  $lines = explode( "\r\n", $this->readBuffer, 2 );
314  if ( count( $lines ) < 2 ) {
315  return 'done';
316  }
317  if ( $this->readState == 'status' ) {
318  $this->processStatusLine( $lines[0] );
319  } else {
320  $this->processHeaderLine( $lines[0] );
321  }
322  $this->readBuffer = $lines[1];
323  return 'continue';
324  case 'body':
325  if ( $this->bodyRemaining !== null ) {
326  if ( $this->bodyRemaining > strlen( $this->readBuffer ) ) {
327  $this->bodyRemaining -= strlen( $this->readBuffer );
328  $this->readBuffer = '';
329  return 'done';
330  } else {
331  $this->readBuffer = substr( $this->readBuffer, $this->bodyRemaining );
332  $this->bodyRemaining = 0;
333  $this->nextRequest();
334  return 'continue';
335  }
336  } else {
337  // No content length, read all data to EOF
338  $this->readBuffer = '';
339  return 'done';
340  }
341  default:
342  throw new MWException( __METHOD__ . ': unexpected state' );
343  }
344  }
345 
349  protected function processStatusLine( $line ) {
350  if ( !preg_match( '!^HTTP/(\d+)\.(\d+) (\d{3}) (.*)$!', $line, $m ) ) {
351  $this->log( 'invalid status line' );
352  $this->markDown();
353  return;
354  }
355  list( , , , $status, $reason ) = $m;
356  $status = intval( $status );
357  if ( $status !== 200 && $status !== 404 ) {
358  $this->log( "unexpected status code: $status $reason" );
359  $this->markDown();
360  return;
361  }
362  $this->readState = 'header';
363  }
364 
368  protected function processHeaderLine( $line ) {
369  if ( preg_match( '/^Content-Length: (\d+)$/i', $line, $m ) ) {
370  $this->bodyRemaining = intval( $m[1] );
371  } elseif ( $line === '' ) {
372  $this->readState = 'body';
373  }
374  }
375 
376  protected function nextRequest() {
377  if ( $this->currentRequestIndex !== null ) {
378  unset( $this->requests[$this->currentRequestIndex] );
379  }
380  if ( count( $this->requests ) ) {
381  $this->readState = 'status';
382  $this->currentRequestIndex = key( $this->requests );
383  $this->writeBuffer = $this->requests[$this->currentRequestIndex];
384  } else {
385  $this->readState = 'idle';
386  $this->currentRequestIndex = null;
387  $this->writeBuffer = '';
388  }
389  $this->bodyRemaining = null;
390  }
391 
395  protected function log( $msg ) {
396  wfDebugLog( 'squid', __CLASS__ . " ($this->host): $msg" );
397  }
398 }
SquidPurgeClient\EINTR
const EINTR
Definition: SquidPurgeClient.php:54
SquidPurgeClient\$writeBuffer
string $writeBuffer
Definition: SquidPurgeClient.php:46
SquidPurgeClient\markDown
markDown()
Close the socket and ignore any future purge requests.
Definition: SquidPurgeClient.php:172
SquidPurgeClient\__construct
__construct( $server)
Definition: SquidPurgeClient.php:74
SquidPurgeClient\EAGAIN
const EAGAIN
Definition: SquidPurgeClient.php:55
SquidPurgeClient\close
close()
Close the socket but allow it to be reopened for future purge requests.
Definition: SquidPurgeClient.php:180
Http\userAgent
static userAgent()
A standard user-agent we can use for external requests.
Definition: Http.php:97
SquidPurgeClient\$port
int $port
Definition: SquidPurgeClient.php:37
SquidPurgeClient\$host
string $host
Definition: SquidPurgeClient.php:34
SquidPurgeClient\$ip
string bool $ip
Definition: SquidPurgeClient.php:40
SquidPurgeClient
An HTTP 1.0 client built for the purposes of purging Squid and Varnish.
Definition: SquidPurgeClient.php:32
wfDebugLog
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Definition: GlobalFunctions.php:992
SquidPurgeClient\$readState
string $readState
Definition: SquidPurgeClient.php:43
wfAppendQuery
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
Definition: GlobalFunctions.php:439
SquidPurgeClient\processHeaderLine
processHeaderLine( $line)
Definition: SquidPurgeClient.php:368
SquidPurgeClient\getIP
getIP()
Get the host's IP address.
Definition: SquidPurgeClient.php:150
wfParseUrl
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
Definition: GlobalFunctions.php:793
MWException
MediaWiki exception.
Definition: MWException.php:26
SquidPurgeClient\BUFFER_SIZE
const BUFFER_SIZE
Definition: SquidPurgeClient.php:57
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1030
SquidPurgeClient\$currentRequestIndex
mixed $currentRequestIndex
Definition: SquidPurgeClient.php:52
SquidPurgeClient\getSocket
getSocket()
Open a socket if there isn't one open already, return it.
Definition: SquidPurgeClient.php:86
SquidPurgeClient\isIdle
isIdle()
Definition: SquidPurgeClient.php:234
SquidPurgeClient\nextRequest
nextRequest()
Definition: SquidPurgeClient.php:376
SquidPurgeClient\$requests
array $requests
Definition: SquidPurgeClient.php:49
SquidPurgeClient\doReads
doReads()
Read some data.
Definition: SquidPurgeClient.php:276
SquidPurgeClient\$readBuffer
string $readBuffer
Definition: SquidPurgeClient.php:66
$line
$line
Definition: mcc.php:119
$lines
if(!file_exists( $CREDITS)) $lines
Definition: updateCredits.php:49
SquidPurgeClient\$socket
resource false null $socket
The socket resource, or null for unconnected, or false for disabled due to error.
Definition: SquidPurgeClient.php:63
SquidPurgeClient\EINPROGRESS
const EINPROGRESS
Definition: SquidPurgeClient.php:56
SquidPurgeClient\doWrites
doWrites()
Perform pending writes.
Definition: SquidPurgeClient.php:241
$wgSquidPurgeUseHostHeader
$wgSquidPurgeUseHostHeader
Whether to use a Host header in purge requests sent to the proxy servers configured in $wgCdnServers.
Definition: DefaultSettings.php:2980
SquidPurgeClient\getWriteSocketsForSelect
getWriteSocketsForSelect()
Get write socket array for select()
Definition: SquidPurgeClient.php:133
SquidPurgeClient\processStatusLine
processStatusLine( $line)
Definition: SquidPurgeClient.php:349
SquidPurgeClient\queuePurge
queuePurge( $url)
Queue a purge operation.
Definition: SquidPurgeClient.php:198
SquidPurgeClient\getReadSocketsForSelect
getReadSocketsForSelect()
Get read socket array for select()
Definition: SquidPurgeClient.php:118
$path
$path
Definition: NoLocalSettings.php:25
SquidPurgeClient\processReadBuffer
processReadBuffer()
Definition: SquidPurgeClient.php:307
SquidPurgeClient\log
log( $msg)
Definition: SquidPurgeClient.php:395
SquidPurgeClient\$bodyRemaining
int $bodyRemaining
Definition: SquidPurgeClient.php:69