I recently had a Linux client that was, for whatever odd reason, making infinite recursive HTTP calls to a single script, which was making the server process count skyrocket. I decided to use the same module as I did in my Painless migration from PHP MySQL to MySQLi post, which is to say, overriding base functions for fun and profit using the PHP runkit extension. I did this so I could gather, for debugging, logs of when and where the calls that were causing this to occur.
The below code overrides all functions listed on the line that says “List of functions to intercept” [Line 9]. It works by first renaming these built in functions to “OVERRIDE_$FuncName” [Line 12], and replacing them with a call to “GlobalRunFunc()” [Line 13], which receives the original function name and argument list. The GlobalRunFunc():
- Checks to see if it is interested in logging the call
- In the case of this example, it will log the call if [Line 20]:
- Line 21: curl_setopt is called with the CURLOPT_URL parameter (enum=10002)
- Line 22: curl_init is called with a first parameter, which would be a URL
- Line 23: file_get_contents or fopen is called and is not an absolute path
(Wordpress calls everything absolutely. Normally I would have only checked for http[s] calls).
- If it does want to log the call, it stores it in a global array (which holds all the calls we will want to log).
The logged data includes [Line 25]:
- The function name
- The function parameters
- 2 functions back of backtrace (which can often get quite large when stored in the log file)
- It then calls the original function, with parameters intact, and passes through the return [Line 27].
The “GlobalShutdown()” [Line 30] is then called when the script is closing [Line 38] and saves all the logs, if any exist, to “$GlobalLogDir/$DATETIME.srl”.
I have it using “serialize()” to encode the log data [Line 25], as opposed to “json_encode()” or “print_r()” calls, as the latter were getting too large for the logs. You may want to have it use one of these other encoding functions for easier log perusal, if running out of space is not a concern.
<?
//The log data to save is stored here
global $GlobalLogArr, $GlobalLogDir;
$GlobalLogArr=Array();
$GlobalLogDir='./LOG_DIRECTORY_NAME';
//Override the functions here to instead have them call to GlobalRunFunc, which will in turn call the original functions
foreach(Array(
'fopen', 'file_get_contents', 'curl_init', 'curl_setopt', //List of functions to intercept
) as $FuncName)
{
runkit_function_rename($FuncName, "OVERRIDE_$FuncName");
runkit_function_add($FuncName, '', "return GlobalRunFunc('$FuncName', func_get_args());");
}
//This optionally
function GlobalRunFunc($FuncName, $Args)
{
global $GlobalLogArr;
if(
($FuncName=='curl_setopt' && $Args[1]==10002) || //CURLOPT enumeration can be found at https://curl.haxx.se/mail/archive-2004-07/0100.html
($FuncName=='curl_init' && isset($Args[0])) ||
(($FuncName=='file_get_contents' || $FuncName=='fopen') && $Args[0][0]!='/')
)
$GlobalLogArr[]=serialize(Array('FuncName'=>$FuncName, 'Args'=>$Args, 'Trace'=>array_slice(debug_backtrace(), 1, 2)));
return call_user_func_array("OVERRIDE_$FuncName", $Args);
}
function GlobalShutdown()
{
global $GlobalLogArr, $GlobalLogDir;
$Time=microtime(true);
if(count($GlobalLogArr))
file_put_contents($GlobalLogDir.date('Y-m-d_H:i:s.'.substr($Time-floor($Time), 2, 3), floor($Time)).'.srl', implode("\n", $GlobalLogArr));
}
register_shutdown_function('GlobalShutdown');
?>