123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 |
- <?php
- /**
- * Squid and Varnish cache purging.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
- /**
- * An HTTP 1.0 client built for the purposes of purging Squid and Varnish.
- * Uses asynchronous I/O, allowing purges to be done in a highly parallel
- * manner.
- *
- * @todo Consider using MultiHttpClient.
- */
- class SquidPurgeClient {
- /** @var string */
- protected $host;
- /** @var int */
- protected $port;
- /** @var string|bool */
- protected $ip;
- /** @var string */
- protected $readState = 'idle';
- /** @var string */
- protected $writeBuffer = '';
- /** @var array */
- protected $requests = [];
- /** @var mixed */
- protected $currentRequestIndex;
- const EINTR = 4;
- const EAGAIN = 11;
- const EINPROGRESS = 115;
- const BUFFER_SIZE = 8192;
- /**
- * @var resource|null The socket resource, or null for unconnected, or false
- * for disabled due to error.
- */
- protected $socket;
- /** @var string */
- protected $readBuffer;
- /** @var int */
- protected $bodyRemaining;
- /**
- * @param string $server
- */
- public function __construct( $server ) {
- $parts = explode( ':', $server, 2 );
- $this->host = $parts[0];
- $this->port = $parts[1] ?? 80;
- }
- /**
- * Open a socket if there isn't one open already, return it.
- * Returns false on error.
- *
- * @return bool|resource
- */
- protected function getSocket() {
- if ( $this->socket !== null ) {
- return $this->socket;
- }
- $ip = $this->getIP();
- if ( !$ip ) {
- $this->log( "DNS error" );
- $this->markDown();
- return false;
- }
- $this->socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
- socket_set_nonblock( $this->socket );
- Wikimedia\suppressWarnings();
- $ok = socket_connect( $this->socket, $ip, $this->port );
- Wikimedia\restoreWarnings();
- if ( !$ok ) {
- $error = socket_last_error( $this->socket );
- if ( $error !== self::EINPROGRESS ) {
- $this->log( "connection error: " . socket_strerror( $error ) );
- $this->markDown();
- return false;
- }
- }
- return $this->socket;
- }
- /**
- * Get read socket array for select()
- * @return array
- */
- public function getReadSocketsForSelect() {
- if ( $this->readState == 'idle' ) {
- return [];
- }
- $socket = $this->getSocket();
- if ( $socket === false ) {
- return [];
- }
- return [ $socket ];
- }
- /**
- * Get write socket array for select()
- * @return array
- */
- public function getWriteSocketsForSelect() {
- if ( !strlen( $this->writeBuffer ) ) {
- return [];
- }
- $socket = $this->getSocket();
- if ( $socket === false ) {
- return [];
- }
- return [ $socket ];
- }
- /**
- * Get the host's IP address.
- * Does not support IPv6 at present due to the lack of a convenient interface in PHP.
- * @throws MWException
- * @return string
- */
- protected function getIP() {
- if ( $this->ip === null ) {
- if ( IP::isIPv4( $this->host ) ) {
- $this->ip = $this->host;
- } elseif ( IP::isIPv6( $this->host ) ) {
- throw new MWException( '$wgCdnServers does not support IPv6' );
- } else {
- Wikimedia\suppressWarnings();
- $this->ip = gethostbyname( $this->host );
- if ( $this->ip === $this->host ) {
- $this->ip = false;
- }
- Wikimedia\restoreWarnings();
- }
- }
- return $this->ip;
- }
- /**
- * Close the socket and ignore any future purge requests.
- * This is called if there is a protocol error.
- */
- protected function markDown() {
- $this->close();
- $this->socket = false;
- }
- /**
- * Close the socket but allow it to be reopened for future purge requests
- */
- public function close() {
- if ( $this->socket ) {
- Wikimedia\suppressWarnings();
- socket_set_block( $this->socket );
- socket_shutdown( $this->socket );
- socket_close( $this->socket );
- Wikimedia\restoreWarnings();
- }
- $this->socket = null;
- $this->readBuffer = '';
- // Write buffer is kept since it may contain a request for the next socket
- }
- /**
- * Queue a purge operation
- *
- * @param string $url Fully expanded URL (with host and protocol)
- */
- public function queuePurge( $url ) {
- global $wgSquidPurgeUseHostHeader;
- $url = str_replace( "\n", '', $url ); // sanity
- $request = [];
- if ( $wgSquidPurgeUseHostHeader ) {
- $url = wfParseUrl( $url );
- $host = $url['host'];
- if ( isset( $url['port'] ) && strlen( $url['port'] ) > 0 ) {
- $host .= ":" . $url['port'];
- }
- $path = $url['path'];
- if ( isset( $url['query'] ) && is_string( $url['query'] ) ) {
- $path = wfAppendQuery( $path, $url['query'] );
- }
- $request[] = "PURGE $path HTTP/1.1";
- $request[] = "Host: $host";
- } else {
- wfDeprecated( '$wgSquidPurgeUseHostHeader = false', '1.33' );
- $request[] = "PURGE $url HTTP/1.0";
- }
- $request[] = "Connection: Keep-Alive";
- $request[] = "Proxy-Connection: Keep-Alive";
- $request[] = "User-Agent: " . Http::userAgent() . ' ' . __CLASS__;
- // Two ''s to create \r\n\r\n
- $request[] = '';
- $request[] = '';
- $this->requests[] = implode( "\r\n", $request );
- if ( $this->currentRequestIndex === null ) {
- $this->nextRequest();
- }
- }
- /**
- * @return bool
- */
- public function isIdle() {
- return strlen( $this->writeBuffer ) == 0 && $this->readState == 'idle';
- }
- /**
- * Perform pending writes. Call this when socket_select() indicates that writing will not block.
- */
- public function doWrites() {
- if ( !strlen( $this->writeBuffer ) ) {
- return;
- }
- $socket = $this->getSocket();
- if ( !$socket ) {
- return;
- }
- if ( strlen( $this->writeBuffer ) <= self::BUFFER_SIZE ) {
- $buf = $this->writeBuffer;
- $flags = MSG_EOR;
- } else {
- $buf = substr( $this->writeBuffer, 0, self::BUFFER_SIZE );
- $flags = 0;
- }
- Wikimedia\suppressWarnings();
- $bytesSent = socket_send( $socket, $buf, strlen( $buf ), $flags );
- Wikimedia\restoreWarnings();
- if ( $bytesSent === false ) {
- $error = socket_last_error( $socket );
- if ( $error != self::EAGAIN && $error != self::EINTR ) {
- $this->log( 'write error: ' . socket_strerror( $error ) );
- $this->markDown();
- }
- return;
- }
- $this->writeBuffer = substr( $this->writeBuffer, $bytesSent );
- }
- /**
- * Read some data. Call this when socket_select() indicates that the read buffer is non-empty.
- */
- public function doReads() {
- $socket = $this->getSocket();
- if ( !$socket ) {
- return;
- }
- $buf = '';
- Wikimedia\suppressWarnings();
- $bytesRead = socket_recv( $socket, $buf, self::BUFFER_SIZE, 0 );
- Wikimedia\restoreWarnings();
- if ( $bytesRead === false ) {
- $error = socket_last_error( $socket );
- if ( $error != self::EAGAIN && $error != self::EINTR ) {
- $this->log( 'read error: ' . socket_strerror( $error ) );
- $this->markDown();
- return;
- }
- } elseif ( $bytesRead === 0 ) {
- // Assume EOF
- $this->close();
- return;
- }
- $this->readBuffer .= $buf;
- while ( $this->socket && $this->processReadBuffer() === 'continue' );
- }
- /**
- * @throws MWException
- * @return string
- */
- protected function processReadBuffer() {
- switch ( $this->readState ) {
- case 'idle':
- return 'done';
- case 'status':
- case 'header':
- $lines = explode( "\r\n", $this->readBuffer, 2 );
- if ( count( $lines ) < 2 ) {
- return 'done';
- }
- if ( $this->readState == 'status' ) {
- $this->processStatusLine( $lines[0] );
- } else {
- $this->processHeaderLine( $lines[0] );
- }
- $this->readBuffer = $lines[1];
- return 'continue';
- case 'body':
- if ( $this->bodyRemaining !== null ) {
- if ( $this->bodyRemaining > strlen( $this->readBuffer ) ) {
- $this->bodyRemaining -= strlen( $this->readBuffer );
- $this->readBuffer = '';
- return 'done';
- } else {
- $this->readBuffer = substr( $this->readBuffer, $this->bodyRemaining );
- $this->bodyRemaining = 0;
- $this->nextRequest();
- return 'continue';
- }
- } else {
- // No content length, read all data to EOF
- $this->readBuffer = '';
- return 'done';
- }
- default:
- throw new MWException( __METHOD__ . ': unexpected state' );
- }
- }
- /**
- * @param string $line
- */
- protected function processStatusLine( $line ) {
- if ( !preg_match( '!^HTTP/(\d+)\.(\d+) (\d{3}) (.*)$!', $line, $m ) ) {
- $this->log( 'invalid status line' );
- $this->markDown();
- return;
- }
- list( , , , $status, $reason ) = $m;
- $status = intval( $status );
- if ( $status !== 200 && $status !== 404 ) {
- $this->log( "unexpected status code: $status $reason" );
- $this->markDown();
- return;
- }
- $this->readState = 'header';
- }
- /**
- * @param string $line
- */
- protected function processHeaderLine( $line ) {
- if ( preg_match( '/^Content-Length: (\d+)$/i', $line, $m ) ) {
- $this->bodyRemaining = intval( $m[1] );
- } elseif ( $line === '' ) {
- $this->readState = 'body';
- }
- }
- protected function nextRequest() {
- if ( $this->currentRequestIndex !== null ) {
- unset( $this->requests[$this->currentRequestIndex] );
- }
- if ( count( $this->requests ) ) {
- $this->readState = 'status';
- $this->currentRequestIndex = key( $this->requests );
- $this->writeBuffer = $this->requests[$this->currentRequestIndex];
- } else {
- $this->readState = 'idle';
- $this->currentRequestIndex = null;
- $this->writeBuffer = '';
- }
- $this->bodyRemaining = null;
- }
- /**
- * @param string $msg
- */
- protected function log( $msg ) {
- wfDebugLog( 'squid', __CLASS__ . " ($this->host): $msg" );
- }
- }
|