MediaWiki REL1_35
SquidPurgeClient.php
Go to the documentation of this file.
1<?php
23use 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 // @phan-suppress-next-line PhanPluginDuplicateAdjacentStatement
224 $request[] = '';
225
226 $this->requests[] = implode( "\r\n", $request );
227 if ( $this->currentRequestIndex === null ) {
228 $this->nextRequest();
229 }
230 }
231
235 public function isIdle() {
236 return strlen( $this->writeBuffer ) == 0 && $this->readState == 'idle';
237 }
238
242 public function doWrites() {
243 if ( !strlen( $this->writeBuffer ) ) {
244 return;
245 }
246 $socket = $this->getSocket();
247 if ( !$socket ) {
248 return;
249 }
250
251 $flags = 0;
252
253 if ( strlen( $this->writeBuffer ) <= self::BUFFER_SIZE ) {
254 $buf = $this->writeBuffer;
255 } else {
256 $buf = substr( $this->writeBuffer, 0, self::BUFFER_SIZE );
257 }
258 Wikimedia\suppressWarnings();
259 $bytesSent = socket_send( $socket, $buf, strlen( $buf ), $flags );
260 Wikimedia\restoreWarnings();
261
262 if ( $bytesSent === false ) {
263 $error = socket_last_error( $socket );
264 if ( $error != self::EAGAIN && $error != self::EINTR ) {
265 $this->log( 'write error: ' . socket_strerror( $error ) );
266 $this->markDown();
267 }
268 return;
269 }
270
271 $this->writeBuffer = substr( $this->writeBuffer, $bytesSent );
272 }
273
277 public function doReads() {
278 $socket = $this->getSocket();
279 if ( !$socket ) {
280 return;
281 }
282
283 $buf = '';
284 Wikimedia\suppressWarnings();
285 $bytesRead = socket_recv( $socket, $buf, self::BUFFER_SIZE, 0 );
286 Wikimedia\restoreWarnings();
287 if ( $bytesRead === false ) {
288 $error = socket_last_error( $socket );
289 if ( $error != self::EAGAIN && $error != self::EINTR ) {
290 $this->log( 'read error: ' . socket_strerror( $error ) );
291 $this->markDown();
292 return;
293 }
294 } elseif ( $bytesRead === 0 ) {
295 // Assume EOF
296 $this->close();
297 return;
298 }
299
300 $this->readBuffer .= $buf;
301 while ( $this->socket && $this->processReadBuffer() === 'continue' );
302 }
303
308 protected function processReadBuffer() {
309 switch ( $this->readState ) {
310 case 'idle':
311 return 'done';
312 case 'status':
313 case 'header':
314 $lines = explode( "\r\n", $this->readBuffer, 2 );
315 if ( count( $lines ) < 2 ) {
316 return 'done';
317 }
318 if ( $this->readState == 'status' ) {
319 $this->processStatusLine( $lines[0] );
320 } else {
321 $this->processHeaderLine( $lines[0] );
322 }
323 $this->readBuffer = $lines[1];
324 return 'continue';
325 case 'body':
326 if ( $this->bodyRemaining !== null ) {
327 if ( $this->bodyRemaining > strlen( $this->readBuffer ) ) {
328 $this->bodyRemaining -= strlen( $this->readBuffer );
329 $this->readBuffer = '';
330 return 'done';
331 } else {
332 $this->readBuffer = substr( $this->readBuffer, $this->bodyRemaining );
333 $this->bodyRemaining = 0;
334 $this->nextRequest();
335 return 'continue';
336 }
337 } else {
338 // No content length, read all data to EOF
339 $this->readBuffer = '';
340 return 'done';
341 }
342 default:
343 throw new MWException( __METHOD__ . ': unexpected state' );
344 }
345 }
346
350 protected function processStatusLine( $line ) {
351 if ( !preg_match( '!^HTTP/(\d+)\.(\d+) (\d{3}) (.*)$!', $line, $m ) ) {
352 $this->log( 'invalid status line' );
353 $this->markDown();
354 return;
355 }
356 list( , , , $status, $reason ) = $m;
357 $status = intval( $status );
358 if ( $status !== 200 && $status !== 404 ) {
359 $this->log( "unexpected status code: $status $reason" );
360 $this->markDown();
361 return;
362 }
363 $this->readState = 'header';
364 }
365
369 protected function processHeaderLine( $line ) {
370 if ( preg_match( '/^Content-Length: (\d+)$/i', $line, $m ) ) {
371 $this->bodyRemaining = intval( $m[1] );
372 } elseif ( $line === '' ) {
373 $this->readState = 'body';
374 }
375 }
376
377 protected function nextRequest() {
378 if ( $this->currentRequestIndex !== null ) {
379 unset( $this->requests[$this->currentRequestIndex] );
380 }
381 if ( count( $this->requests ) ) {
382 $this->readState = 'status';
383 $this->currentRequestIndex = key( $this->requests );
384 $this->writeBuffer = $this->requests[$this->currentRequestIndex];
385 } else {
386 $this->readState = 'idle';
387 $this->currentRequestIndex = null;
388 $this->writeBuffer = '';
389 }
390 $this->bodyRemaining = null;
391 }
392
396 protected function log( $msg ) {
397 wfDebugLog( 'squid', __CLASS__ . " ($this->host): $msg" );
398 }
399}
$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)
Logs a warning that $function is deprecated.
static userAgent()
A standard user-agent we can use for external requests.
Definition Http.php:97
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 false 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.
$line
Definition mcc.php:119
if(!file_exists( $CREDITS)) $lines