PluginProbe
OPcache Reset / 2.4.1
OPcache Reset v2.4.1
trunk 1.0.0 2.0.0 2.0.1 2.1.0 2.1.1 2.1.2 2.1.3 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.3.0 2.4.0 2.4.1 2.4.2 2.4.3
opcache-reset / opcache-fastcgi.php

opcache-fastcgi.php in OPcache Reset 2.4.1, at opcache-fastcgi.php

317 lines 9.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Direct FastCGI client for OPcache reset.
4 *
5 * Implements FastCGI protocol to communicate with PHP-FPM directly,
6 * eliminating the need for cachetool binary.
7 *
8 * Note: This file uses low-level PHP socket functions intentionally.
9 * WP_Filesystem is not suitable for socket communication.
10 *
11 * @package opcache-reset
12 */
13
14 // Prevent direct access.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 // FastCGI protocol constants.
20 define( 'GPS_FCGI_VERSION_1', 1 );
21 define( 'GPS_FCGI_BEGIN_REQUEST', 1 );
22 define( 'GPS_FCGI_END_REQUEST', 3 );
23 define( 'GPS_FCGI_PARAMS', 4 );
24 define( 'GPS_FCGI_STDIN', 5 );
25 define( 'GPS_FCGI_STDOUT', 6 );
26 define( 'GPS_FCGI_RESPONDER', 1 );
27 define( 'GPS_FCGI_REQUEST_COMPLETE', 0 );
28
29 /**
30 * Build a FastCGI record.
31 *
32 * @param int $type Record type.
33 * @param string $content Record content.
34 * @param int $request_id Request ID.
35 * @return string Binary record data.
36 */
37 function gps_fcgi_build_record( $type, $content, $request_id = 1 ) {
38 $content_length = strlen( $content );
39 $padding_length = ( 8 - ( $content_length % 8 ) ) % 8;
40
41 return pack(
42 'CCnnCx',
43 GPS_FCGI_VERSION_1,
44 $type,
45 $request_id,
46 $content_length,
47 $padding_length
48 ) . $content . str_repeat( "\0", $padding_length );
49 }
50
51 /**
52 * Build FastCGI name-value pair.
53 *
54 * @param string $name Parameter name.
55 * @param string $value Parameter value.
56 * @return string Binary name-value pair.
57 */
58 function gps_fcgi_build_nvpair( $name, $value ) {
59 $name_len = strlen( $name );
60 $value_len = strlen( $value );
61
62 $result = '';
63 if ( $name_len < 128 ) {
64 $result .= chr( $name_len );
65 } else {
66 $result .= pack( 'N', $name_len | 0x80000000 );
67 }
68 if ( $value_len < 128 ) {
69 $result .= chr( $value_len );
70 } else {
71 $result .= pack( 'N', $value_len | 0x80000000 );
72 }
73
74 return $result . $name . $value;
75 }
76
77 /**
78 * Find cachetool configuration file.
79 *
80 * Searches from the WordPress root and current directory upward, then checks
81 * the home directory and system-wide CacheTool locations.
82 *
83 * @return string|null Path to config file or null if not found.
84 */
85 function gps_find_cachetool_config() {
86 $locations = array();
87 $roots = array( ABSPATH, getcwd() );
88
89 foreach ( $roots as $root ) {
90 if ( ! $root ) {
91 continue;
92 }
93
94 $directory = realpath( $root );
95 while ( $directory ) {
96 $locations[] = $directory . '/.cachetool.yml';
97 $locations[] = $directory . '/.cachetool.yaml';
98 $parent = dirname( $directory );
99 if ( $parent === $directory ) {
100 break;
101 }
102 $directory = $parent;
103 }
104 }
105
106 $home = getenv( 'HOME' );
107 if ( $home ) {
108 $locations[] = $home . '/.cachetool.yml';
109 $locations[] = $home . '/.cachetool.yaml';
110 }
111 $locations[] = '/etc/cachetool.yml';
112 $locations[] = '/etc/cachetool.yaml';
113
114 foreach ( array_unique( $locations ) as $path ) {
115 if ( file_exists( $path ) && is_readable( $path ) ) {
116 return $path;
117 }
118 }
119
120 return null;
121 }
122
123 /**
124 * Parse cachetool.yml configuration file.
125 *
126 * Simple YAML parser for the adapter, fastcgi, and fastcgiChroot keys.
127 *
128 * @param string $path Path to the config file.
129 * @return array{fastcgi: string, fastcgi_chroot: string|null}|null FastCGI configuration or null if invalid.
130 */
131 function gps_parse_cachetool_yml( $path ) {
132 $contents = file_get_contents( $path );
133 if ( false === $contents ) {
134 return null;
135 }
136
137 $adapter = 'fastcgi';
138 $fastcgi = null;
139 $fastcgi_chroot = null;
140
141 $lines = explode( "\n", $contents );
142 foreach ( $lines as $line ) {
143 $line = trim( $line );
144
145 // Skip comments and empty lines.
146 if ( empty( $line ) || '#' === $line[0] ) {
147 continue;
148 }
149
150 // Parse key: value format.
151 if ( preg_match( '/^(\w+)\s*:\s*(.+)$/', $line, $matches ) ) {
152 $key = strtolower( $matches[1] );
153 $value = trim( $matches[2], " \t\n\r\0\x0B\"'" );
154
155 if ( 'adapter' === $key ) {
156 $adapter = strtolower( $value );
157 } elseif ( 'fastcgi' === $key ) {
158 $fastcgi = $value;
159 } elseif ( 'fastcgichroot' === $key ) {
160 $fastcgi_chroot = rtrim( $value, '/\\' );
161 }
162 }
163 }
164
165 // Only return socket if adapter is fastcgi.
166 if ( 'fastcgi' === $adapter && $fastcgi ) {
167 return array(
168 'fastcgi' => $fastcgi,
169 'fastcgi_chroot' => $fastcgi_chroot,
170 );
171 }
172
173 return null;
174 }
175
176 /**
177 * Reset OPcache via direct FastCGI communication.
178 *
179 * Sends a FastCGI request directly to PHP-FPM with a custom parameter
180 * that triggers the reset handler in opcache-reset.php.
181 * No temporary files needed - completely inline.
182 *
183 * Security: The GPS_OPCACHE_RESET_INTERNAL param can only be set via
184 * direct FastCGI connection. nginx/Apache never forward arbitrary
185 * custom CGI params - only HTTP_* headers from HTTP requests.
186 *
187 * @param string $socket Socket path (Unix) or address (TCP host:port).
188 * @param string|null $chroot Optional PHP-FPM chroot prefix to remove from the script path.
189 * @return bool True on success, false on failure.
190 */
191 function gps_fastcgi_opcache_reset( $socket, $chroot = null ) {
192 // Determine connection string.
193 if ( strpos( $socket, '/' ) === 0 ) {
194 // Unix socket.
195 $connect_string = 'unix://' . $socket;
196 } elseif ( strpos( $socket, ':' ) !== false ) {
197 // TCP socket (host:port).
198 $connect_string = 'tcp://' . $socket;
199 } else {
200 // Assume TCP with default port.
201 $connect_string = 'tcp://' . $socket . ':9000';
202 }
203
204 // Connect to PHP-FPM. Silence errors - we check $fp instead.
205 $fp = @stream_socket_client( $connect_string, $errno, $errstr, 5 );
206 if ( ! $fp ) {
207 return false;
208 }
209 stream_set_timeout( $fp, 5 );
210
211 // Use the plugin's own file as the script.
212 $script_filename = __DIR__ . '/opcache-reset.php';
213 if ( $chroot ) {
214 $chroot = rtrim( $chroot, '/\\' );
215 if ( strpos( $script_filename, $chroot . '/' ) !== 0 ) {
216 fclose( $fp );
217 return false;
218 }
219 $script_filename = substr( $script_filename, strlen( $chroot ) );
220 }
221
222 // Build the FastCGI BEGIN_REQUEST record.
223 $begin_request = pack( 'nCCCCCC', GPS_FCGI_RESPONDER, 0, 0, 0, 0, 0, 0, 0 );
224 $request = gps_fcgi_build_record( GPS_FCGI_BEGIN_REQUEST, $begin_request );
225
226 // Build params.
227 // GPS_OPCACHE_RESET_INTERNAL is a custom param that triggers the reset handler.
228 // This param can ONLY be set via direct FastCGI - nginx/Apache never forward it.
229 $params = '';
230 $params .= gps_fcgi_build_nvpair( 'SCRIPT_FILENAME', $script_filename );
231 $params .= gps_fcgi_build_nvpair( 'SCRIPT_NAME', '/opcache-reset.php' );
232 $params .= gps_fcgi_build_nvpair( 'REQUEST_METHOD', 'GET' );
233 $params .= gps_fcgi_build_nvpair( 'QUERY_STRING', '' );
234 $params .= gps_fcgi_build_nvpair( 'REQUEST_URI', '/opcache-reset.php' );
235 $params .= gps_fcgi_build_nvpair( 'DOCUMENT_ROOT', __DIR__ );
236 $params .= gps_fcgi_build_nvpair( 'SERVER_PROTOCOL', 'HTTP/1.1' );
237 $params .= gps_fcgi_build_nvpair( 'GATEWAY_INTERFACE', 'CGI/1.1' );
238 $params .= gps_fcgi_build_nvpair( 'SERVER_SOFTWARE', 'opcache-reset' );
239 $params .= gps_fcgi_build_nvpair( 'REMOTE_ADDR', '127.0.0.1' );
240 $params .= gps_fcgi_build_nvpair( 'REMOTE_PORT', '0' );
241 $params .= gps_fcgi_build_nvpair( 'SERVER_ADDR', '127.0.0.1' );
242 $params .= gps_fcgi_build_nvpair( 'SERVER_PORT', '80' );
243 $params .= gps_fcgi_build_nvpair( 'SERVER_NAME', 'localhost' );
244 // Custom param to trigger reset - not forwardable via HTTP.
245 $params .= gps_fcgi_build_nvpair( 'GPS_OPCACHE_RESET_INTERNAL', '1' );
246
247 $request .= gps_fcgi_build_record( GPS_FCGI_PARAMS, $params );
248 $request .= gps_fcgi_build_record( GPS_FCGI_PARAMS, '' ); // Empty params to end.
249 $request .= gps_fcgi_build_record( GPS_FCGI_STDIN, '' ); // Empty stdin.
250
251 // Send the complete request, accounting for partial socket writes.
252 $request_length = strlen( $request );
253 $bytes_written = 0;
254 while ( $bytes_written < $request_length ) {
255 $written = fwrite( $fp, substr( $request, $bytes_written ) );
256 if ( false === $written || 0 === $written ) {
257 fclose( $fp );
258 return false;
259 }
260 $bytes_written += $written;
261 }
262
263 // Parse FastCGI records until END_REQUEST and verify the handler body.
264 $buffer = '';
265 $stdout = '';
266 while ( ! feof( $fp ) ) {
267 $data = fread( $fp, 8192 );
268 if ( false === $data ) {
269 break;
270 }
271 if ( '' === $data ) {
272 $metadata = stream_get_meta_data( $fp );
273 if ( ! empty( $metadata['timed_out'] ) ) {
274 break;
275 }
276 continue;
277 }
278
279 $buffer .= $data;
280
281 $buffer_length = strlen( $buffer );
282 while ( $buffer_length >= 8 ) {
283 $header = unpack( 'Cversion/Ctype/nrequestId/ncontentLength/CpaddingLength', substr( $buffer, 0, 8 ) );
284 if ( ! is_array( $header ) || GPS_FCGI_VERSION_1 !== $header['version'] ) {
285 fclose( $fp );
286 return false;
287 }
288
289 $record_length = 8 + $header['contentLength'] + $header['paddingLength'];
290 if ( strlen( $buffer ) < $record_length ) {
291 break;
292 }
293
294 $content = substr( $buffer, 8, $header['contentLength'] );
295 $buffer = substr( $buffer, $record_length );
296 $buffer_length = strlen( $buffer );
297
298 if ( GPS_FCGI_STDOUT === $header['type'] ) {
299 $stdout .= $content;
300 } elseif ( GPS_FCGI_END_REQUEST === $header['type'] ) {
301 $end_status = unpack( 'Napp_status/Cprotocol_status', $content );
302 fclose( $fp );
303 if ( ! is_array( $end_status ) || 0 !== $end_status['app_status'] || GPS_FCGI_REQUEST_COMPLETE !== $end_status['protocol_status'] ) {
304 return false;
305 }
306
307 $body_separator = strpos( $stdout, "\r\n\r\n" );
308 $body = false === $body_separator ? $stdout : substr( $stdout, $body_separator + 4 );
309 return 'OK' === trim( $body );
310 }
311 }
312 }
313
314 fclose( $fp );
315 return false;
316 }
317