00001 <?php
00009 class SquidPurgeClient {
00010 var $host, $port, $ip;
00011
00012 var $readState = 'idle';
00013 var $writeBuffer = '';
00014 var $requests = array();
00015 var $currentRequestIndex;
00016
00017 const EINTR = 4;
00018 const EAGAIN = 11;
00019 const EINPROGRESS = 115;
00020 const BUFFER_SIZE = 8192;
00021
00025 var $socket;
00026
00027 public function __construct( $server, $options = array() ) {
00028 $parts = explode( ':', $server, 2 );
00029 $this->host = $parts[0];
00030 $this->port = isset( $parts[1] ) ? $parts[1] : 80;
00031 }
00032
00037 protected function getSocket() {
00038 if ( $this->socket !== null ) {
00039 return $this->socket;
00040 }
00041
00042 $ip = $this->getIP();
00043 if ( !$ip ) {
00044 $this->log( "DNS error" );
00045 $this->markDown();
00046 return false;
00047 }
00048 $this->socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
00049 socket_set_nonblock( $this->socket );
00050 wfSuppressWarnings();
00051 $ok = socket_connect( $this->socket, $ip, $this->port );
00052 wfRestoreWarnings();
00053 if ( !$ok ) {
00054 $error = socket_last_error( $this->socket );
00055 if ( $error !== self::EINPROGRESS ) {
00056 $this->log( "connection error: " . socket_strerror( $error ) );
00057 $this->markDown();
00058 return false;
00059 }
00060 }
00061
00062 return $this->socket;
00063 }
00064
00068 public function getReadSocketsForSelect() {
00069 if ( $this->readState == 'idle' ) {
00070 return array();
00071 }
00072 $socket = $this->getSocket();
00073 if ( $socket === false ) {
00074 return array();
00075 }
00076 return array( $socket );
00077 }
00078
00082 public function getWriteSocketsForSelect() {
00083 if ( !strlen( $this->writeBuffer ) ) {
00084 return array();
00085 }
00086 $socket = $this->getSocket();
00087 if ( $socket === false ) {
00088 return array();
00089 }
00090 return array( $socket );
00091 }
00092
00097 protected function getIP() {
00098 if ( $this->ip === null ) {
00099 if ( IP::isIPv4( $this->host ) ) {
00100 $this->ip = $this->host;
00101 } elseif ( IP::isIPv6( $this->host ) ) {
00102 throw new MWException( '$wgSquidServers does not support IPv6' );
00103 } else {
00104 wfSuppressWarnings();
00105 $this->ip = gethostbyname( $this->host );
00106 if ( $this->ip === $this->host ) {
00107 $this->ip = false;
00108 }
00109 wfRestoreWarnings();
00110 }
00111 }
00112 return $this->ip;
00113 }
00114
00119 protected function markDown() {
00120 $this->close();
00121 $this->socket = false;
00122 }
00123
00127 public function close() {
00128 if ( $this->socket ) {
00129 wfSuppressWarnings();
00130 socket_set_block( $this->socket );
00131 socket_shutdown( $this->socket );
00132 socket_close( $this->socket );
00133 wfRestoreWarnings();
00134 }
00135 $this->socket = null;
00136 $this->readBuffer = '';
00137
00138 }
00139
00143 public function queuePurge( $url ) {
00144 $url = str_replace( "\n", '', $url );
00145 $this->requests[] = "PURGE $url HTTP/1.0\r\n" .
00146 "Connection: Keep-Alive\r\n" .
00147 "Proxy-Connection: Keep-Alive\r\n" .
00148 "User-Agent: " . Http::userAgent() . ' ' . __CLASS__ . "\r\n\r\n";
00149 if ( $this->currentRequestIndex === null ) {
00150 $this->nextRequest();
00151 }
00152 }
00153
00154 public function isIdle() {
00155 return strlen( $this->writeBuffer ) == 0 && $this->readState == 'idle';
00156 }
00157
00161 public function doWrites() {
00162 if ( !strlen( $this->writeBuffer ) ) {
00163 return;
00164 }
00165 $socket = $this->getSocket();
00166 if ( !$socket ) {
00167 return;
00168 }
00169
00170 if ( strlen( $this->writeBuffer ) <= self::BUFFER_SIZE ) {
00171 $buf = $this->writeBuffer;
00172 $flags = MSG_EOR;
00173 } else {
00174 $buf = substr( $this->writeBuffer, 0, self::BUFFER_SIZE );
00175 $flags = 0;
00176 }
00177 wfSuppressWarnings();
00178 $bytesSent = socket_send( $socket, $buf, strlen( $buf ), $flags );
00179 wfRestoreWarnings();
00180
00181 if ( $bytesSent === false ) {
00182 $error = socket_last_error( $socket );
00183 if ( $error != self::EAGAIN && $error != self::EINTR ) {
00184 $this->log( 'write error: ' . socket_strerror( $error ) );
00185 $this->markDown();
00186 }
00187 return;
00188 }
00189
00190 $this->writeBuffer = substr( $this->writeBuffer, $bytesSent );
00191 }
00192
00196 public function doReads() {
00197 $socket = $this->getSocket();
00198 if ( !$socket ) {
00199 return;
00200 }
00201
00202 $buf = '';
00203 wfSuppressWarnings();
00204 $bytesRead = socket_recv( $socket, $buf, self::BUFFER_SIZE, 0 );
00205 wfRestoreWarnings();
00206 if ( $bytesRead === false ) {
00207 $error = socket_last_error( $socket );
00208 if ( $error != self::EAGAIN && $error != self::EINTR ) {
00209 $this->log( 'read error: ' . socket_strerror( $error ) );
00210 $this->markDown();
00211 return;
00212 }
00213 } elseif ( $bytesRead === 0 ) {
00214
00215 $this->close();
00216 return;
00217 }
00218
00219 $this->readBuffer .= $buf;
00220 while ( $this->socket && $this->processReadBuffer() === 'continue' );
00221 }
00222
00223 protected function processReadBuffer() {
00224 switch ( $this->readState ) {
00225 case 'idle':
00226 return 'done';
00227 case 'status':
00228 case 'header':
00229 $lines = explode( "\r\n", $this->readBuffer, 2 );
00230 if ( count( $lines ) < 2 ) {
00231 return 'done';
00232 }
00233 if ( $this->readState == 'status' ) {
00234 $this->processStatusLine( $lines[0] );
00235 } else {
00236 $this->processHeaderLine( $lines[0] );
00237 }
00238 $this->readBuffer = $lines[1];
00239 return 'continue';
00240 case 'body':
00241 if ( $this->bodyRemaining !== null ) {
00242 if ( $this->bodyRemaining > strlen( $this->readBuffer ) ) {
00243 $this->bodyRemaining -= strlen( $this->readBuffer );
00244 $this->readBuffer = '';
00245 return 'done';
00246 } else {
00247 $this->readBuffer = substr( $this->readBuffer, $this->bodyRemaining );
00248 $this->bodyRemaining = 0;
00249 $this->nextRequest();
00250 return 'continue';
00251 }
00252 } else {
00253
00254 $this->readBuffer = '';
00255 return 'done';
00256 }
00257 default:
00258 throw new MWException( __METHOD__.': unexpected state' );
00259 }
00260 }
00261
00262 protected function processStatusLine( $line ) {
00263 if ( !preg_match( '!^HTTP/(\d+)\.(\d+) (\d{3}) (.*)$!', $line, $m ) ) {
00264 $this->log( 'invalid status line' );
00265 $this->markDown();
00266 return;
00267 }
00268 list( $all, $major, $minor, $status, $reason ) = $m;
00269 $status = intval( $status );
00270 if ( $status !== 200 && $status !== 404 ) {
00271 $this->log( "unexpected status code: $status $reason" );
00272 $this->markDown();
00273 return;
00274 }
00275 $this->readState = 'header';
00276 }
00277
00278 protected function processHeaderLine( $line ) {
00279 if ( preg_match( '/^Content-Length: (\d+)$/i', $line, $m ) ) {
00280 $this->bodyRemaining = intval( $m[1] );
00281 } elseif ( $line === '' ) {
00282 $this->readState = 'body';
00283 }
00284 }
00285
00286 protected function nextRequest() {
00287 if ( $this->currentRequestIndex !== null ) {
00288 unset( $this->requests[$this->currentRequestIndex] );
00289 }
00290 if ( count( $this->requests ) ) {
00291 $this->readState = 'status';
00292 $this->currentRequestIndex = key( $this->requests );
00293 $this->writeBuffer = $this->requests[$this->currentRequestIndex];
00294 } else {
00295 $this->readState = 'idle';
00296 $this->currentRequestIndex = null;
00297 $this->writeBuffer = '';
00298 }
00299 $this->bodyRemaining = null;
00300 }
00301
00302 protected function log( $msg ) {
00303 wfDebugLog( 'squid', __CLASS__." ($this->host): $msg\n" );
00304 }
00305 }
00306
00307 class SquidPurgeClientPool {
00308 var $clients = array();
00309 var $timeout = 5;
00310
00311 function __construct( $options = array() ) {
00312 if ( isset( $options['timeout'] ) ) {
00313 $this->timeout = $options['timeout'];
00314 }
00315 }
00316
00317 public function addClient( $client ) {
00318 $this->clients[] = $client;
00319 }
00320
00321 public function run() {
00322 $done = false;
00323 $startTime = microtime( true );
00324 while ( !$done ) {
00325 $readSockets = $writeSockets = array();
00326 foreach ( $this->clients as $clientIndex => $client ) {
00327 $sockets = $client->getReadSocketsForSelect();
00328 foreach ( $sockets as $i => $socket ) {
00329 $readSockets["$clientIndex/$i"] = $socket;
00330 }
00331 $sockets = $client->getWriteSocketsForSelect();
00332 foreach ( $sockets as $i => $socket ) {
00333 $writeSockets["$clientIndex/$i"] = $socket;
00334 }
00335 }
00336 if ( !count( $readSockets ) && !count( $writeSockets ) ) {
00337 break;
00338 }
00339 $exceptSockets = null;
00340 $timeout = min( $startTime + $this->timeout - microtime( true ), 1 );
00341 wfSuppressWarnings();
00342 $numReady = socket_select( $readSockets, $writeSockets, $exceptSockets, $timeout );
00343 wfRestoreWarnings();
00344 if ( $numReady === false ) {
00345 wfDebugLog( 'squid', __METHOD__.': Error in stream_select: ' .
00346 socket_strerror( socket_last_error() ) . "\n" );
00347 break;
00348 }
00349
00350
00351 if ( microtime( true ) - $startTime > $this->timeout * 0.99 ) {
00352 wfDebugLog( 'squid', __CLASS__.": timeout ({$this->timeout}s)\n" );
00353 break;
00354 } elseif ( !$numReady ) {
00355 continue;
00356 }
00357
00358 foreach ( $readSockets as $key => $socket ) {
00359 list( $clientIndex, $i ) = explode( '/', $key );
00360 $client = $this->clients[$clientIndex];
00361 $client->doReads();
00362 }
00363 foreach ( $writeSockets as $key => $socket ) {
00364 list( $clientIndex, $i ) = explode( '/', $key );
00365 $client = $this->clients[$clientIndex];
00366 $client->doWrites();
00367 }
00368
00369 $done = true;
00370 foreach ( $this->clients as $client ) {
00371 if ( !$client->isIdle() ) {
00372 $done = false;
00373 }
00374 }
00375 }
00376 foreach ( $this->clients as $client ) {
00377 $client->close();
00378 }
00379 }
00380 }