Observium_CE/includes/http.inc.php

1014 lines
37 KiB
PHP

<?php
/**
* Observium
*
* This file is part of Observium.
*
* @package observium
* @subpackage functions
* @copyright (C) Adam Armstrong
*
*/
/**
* Common functions for http requests
*/
function is_http_curl() {
// Detect available curl module
if (!defined('OBS_HTTP')) {
$tmp = get_versions(); // cached versions
if ($tmp['curl_old']) {
define('OBS_HTTP', 'php');
} else {
define('OBS_HTTP', 'curl');
}
print_debug("HTTP '" . OBS_HTTP . "' library used.");
}
return OBS_HTTP === 'curl';
}
/**
* Request an http (s) url.
* Note. If the first runtime request exits with timeout,
* then will be set constant OBS_HTTP_REQUEST as FALSE
* and all other requests will skip with FALSE response!
*
* @param string $request Requested URL
* @param array $context Set additional HTTP context options, see http://php.net/manual/en/context.http.php
* @param int|boolean $rate_limit Rate limit per day for specified domain (in url). If FALSE no limits
*
* @return string|boolean Return response content or FALSE
* @global array $response_headers Response headers with keys:
* code (HTTP code status), status (HTTP status description) and all other
* @global boolean $request_status TRUE if response code is 2xx or 3xx
*
* @global array $config
*/
function get_http_request($request, $context = [], $rate_limit = FALSE) {
global $config;
$ok = !safe_empty($request);
if (!$ok) {
print_debug("HTTP request url is empty");
$GLOBALS['response_headers'] = [ 'code' => 400, 'descr' => 'Bad Request' ];
}
if (defined('OBS_HTTP_REQUEST') && OBS_HTTP_REQUEST === FALSE) {
print_debug("HTTP requests skipped since previous request exit with timeout");
$ok = FALSE;
$GLOBALS['response_headers'] = [ 'code' => 408, 'descr' => 'Previous Request Timeout' ];
}
if ($ok && !is_http_curl()) {
if (!ini_get('allow_url_fopen')) {
print_debug('HTTP requests disabled, since PHP config option "allow_url_fopen" set to off. Please enable this option in your PHP config.');
$ok = FALSE;
$GLOBALS['response_headers'] = ['code' => 400, 'descr' => 'HTTP Method Disabled'];
} elseif (str_istarts($request, 'https') && !check_extension_exists('openssl')) {
// Check if Secure requests allowed, but ssl extension not exists
print_debug(__FUNCTION__ . '() wants to connect with https but https is not enabled on this server. Please check your PHP settings, the openssl extension must exist and be enabled.');
logfile(__FUNCTION__ . '() wants to connect with https but https is not enabled on this server. Please check your PHP settings, the openssl extension must exist and be enabled.');
$ok = FALSE;
$GLOBALS['response_headers'] = ['code' => 400, 'descr' => 'HTTPS Method Disabled'];
}
}
if ($ok && process_http_ratelimit($request, $rate_limit)) {
// request exceeded rate limit
$GLOBALS['response_headers'] = ['code' => 429, 'descr' => 'Too Many Requests'];
$ok = FALSE;
}
if (OBS_DEBUG > 0) {
$debug_request = $request;
if (OBS_DEBUG < 2 && strpos($request, 'update.observium.org')) {
$debug_request = preg_replace('/&stats=.+/', '&stats=***', $debug_request);
}
$debug_msg = PHP_EOL . 'REQUEST[%y' . $debug_request . '%n]';
}
if (!$ok) {
if (OBS_DEBUG > 0) {
print_message($debug_msg . PHP_EOL .
'REQUEST STATUS[%rFALSE%n]' . PHP_EOL .
'RESPONSE CODE[' . $GLOBALS['response_headers']['code'] . ' ' . $GLOBALS['response_headers']['descr'] . ']', 'console');
}
// Set GLOBAL var $request_status for use as validate status of last response
$GLOBALS['request_status'] = FALSE;
return FALSE;
}
if (!isset($GLOBALS['http_stats'])) {
// Init stats
$GLOBALS['http_stats'] = [ 'sec' => 0, 'requests' => 0,
'ok' => 0, 'error' => 0, 'timeout' => 0 ];
}
if (is_http_curl()) {
$response_array = process_http_curl($request, $context);
} else {
$response_array = process_http_php($request, $context);
}
$GLOBALS['http_stats']['sec'] += $response_array['request_runtime'];
$GLOBALS['http_stats']['requests']++;
// Request response
$response = $response_array['response'];
$runtime = $response_array['request_runtime'];
// Request end unixtime and runtime
$GLOBALS['request_unixtime'] = $response_array['request_unixtime'];
$GLOBALS['request_runtime'] = $response_array['request_runtime'];
// Headers
$head = $response_array['response_headers'];
$GLOBALS['response_headers'] = $response_array['response_headers'];
// Set GLOBAL var $request_status for use as validate status of last response
if (isset($head['code']) && ($head['code'] < 200 || $head['code'] >= 400) && $head['code'] !== 28) {
$GLOBALS['request_status'] = FALSE;
$GLOBALS['http_stats']['error']++;
bdump($response_array);
} elseif ($response === FALSE) {
// An error in get response
$GLOBALS['response_headers'] = ['code' => 408, 'descr' => 'Request Timeout'];
$GLOBALS['request_status'] = FALSE;
$GLOBALS['http_stats']['timeout']++;
} else {
// Valid statuses: 2xx Success, 3xx Redirection or head code not set (ie response not correctly parsed)
$GLOBALS['request_status'] = TRUE;
$GLOBALS['http_stats']['ok']++;
}
// if (str_contains($request, 'somewrong')) {
// print_vars($head);
// var_dump($response);
// }
// Set OBS_HTTP_REQUEST for skip all other requests (FALSE for skip all other requests)
if (!defined('OBS_HTTP_REQUEST')) {
if ($response === FALSE && empty($head)) {
// Derp, no way for get proxy headers
if ($runtime < 1 &&
isset($config['http_proxy']) && $config['http_proxy'] &&
!(isset($config['proxy_user']) || isset($config['proxy_password']))) {
$GLOBALS['response_headers'] = ['code' => 407, 'descr' => 'Proxy Authentication Required'];
} else {
$GLOBALS['response_headers'] = ['code' => 408, 'descr' => 'Request Timeout'];
}
$GLOBALS['request_status'] = FALSE;
// Validate host from request and check if it timeout request
if (OBS_PROCESS_NAME === 'poller' && gethostbyname6(parse_url($request, PHP_URL_HOST))) {
// Timeout error, only if not received response headers
define('OBS_HTTP_REQUEST', FALSE);
print_debug(__FUNCTION__ . '() exit with timeout. Access to outside localnet is blocked by firewall or network problems. Check proxy settings.');
logfile(__FUNCTION__ . '() exit with timeout. Access to outside localnet is blocked by firewall or network problems. Check proxy settings.');
}
} else {
define('OBS_HTTP_REQUEST', TRUE);
}
}
// FIXME. what if first request fine, but second broken?
//else if ($response === FALSE)
//{
// if (function_exists('runkit_constant_redefine')) { runkit_constant_redefine('OBS_HTTP_REQUEST', FALSE); }
//}
if (defined('OBS_DEBUG') && OBS_DEBUG) {
// Hide extended stats in normal debug level = 1
if (OBS_DEBUG < 2 && strpos($request, 'update.observium.org')) {
$request = preg_replace('/&stats=.+/', '&stats=***', $request);
}
// Show debug info
print_message($debug_msg . PHP_EOL .
'REQUEST STATUS[' . ($GLOBALS['request_status'] ? '%gTRUE' : '%rFALSE') . '%n]' . PHP_EOL .
'REQUEST RUNTIME[' . ($runtime > 3 ? '%r' : '%g') . round($runtime, 4) . 's%n]' . PHP_EOL .
'RESPONSE CODE[' . $GLOBALS['response_headers']['code'] . ' ' . $GLOBALS['response_headers']['descr'] . ']', 'console');
if (OBS_DEBUG > 1) {
echo "RESPONSE[\n" . $response . "\n]";
if (function_exists('http_get_last_response_headers')) {
// PHP 8.4+
$http_response_header = http_get_last_response_headers();
}
print_vars($http_response_header);
//print_vars($opts);
}
}
return $response;
}
/**
* @param string $request
* @param mixed $rate_limit
* @return bool
*/
function process_http_ratelimit($request, $rate_limit = FALSE) {
if ($rate_limit && is_numeric($rate_limit) && $rate_limit >= 0) {
// Check limit rates to this domain (per/day)
if (preg_match('/^https?:\/\/([\w\.]+[\w\-\.]*(:\d+)?)/i', $request, $matches)) {
$date = format_unixtime(get_time(), 'Y-m-d');
$domain = $matches[0]; // base domain (with http(s)): https://test-me.com/ -> https://test-me.com
$rate_db = safe_json_decode(get_obs_attrib('http_rate_' . $domain));
//print_vars($date); print_vars($rate_db);
if (is_array($rate_db) && isset($rate_db[$date])) {
$rate_count = $rate_db[$date];
} else {
$rate_count = 0;
}
$rate_count++;
set_obs_attrib('http_rate_' . $domain, safe_json_encode([$date => $rate_count]));
if ($rate_count > $rate_limit) {
print_debug("HTTP requests skipped because the rate limit $rate_limit/day for domain '$domain' is exceeded (count: $rate_count)");
//$GLOBALS['response_headers'] = [ 'code' => 429, 'descr' => 'Too Many Requests' ];
//$ok = FALSE;
return TRUE;
}
if (OBS_DEBUG > 1) {
print_debug("HTTP rate count for domain '$domain': $rate_count ($rate_limit/day)");
}
}
}
return FALSE;
}
function print_debug_curl_cmd($request, $opts_http) {
global $config;
// DEBUG, generate curl cmd for testing:
if (!defined('OBS_DEBUG') || !OBS_DEBUG) {
return;
}
$curl_cmd = 'curl';
if (OBS_DEBUG > 1) {
// Show response headers
$curl_cmd .= ' -i';
}
if (isset($config['http_ip_version'])) {
$curl_cmd .= str_contains($config['http_ip_version'], '6') ? ' -6' : ' -4';
}
if (isset($opts_http['timeout'])) {
$curl_cmd .= ' --connect-timeout ' . $opts_http['timeout'];
}
if (isset($opts_http['method'])) {
$curl_cmd .= ' -X ' . $opts_http['method'];
}
if (isset($opts_http['header'])) {
foreach (explode("\r\n", $opts_http['header']) as $curl_header) {
if (safe_empty($curl_header)) {
continue;
}
$curl_cmd .= ' -H \'' . $curl_header . '\'';
}
}
if (isset($opts_http['content'])) {
$curl_cmd .= ' -d \'' . $opts_http['content'] . '\'';
}
// Proxy
// -x, --proxy <[protocol://][user:password@]proxyhost[:port]>
// -U, --proxy-user <user:password>
if (isset($config['http_proxy']) && $config['http_proxy']) {
$http_proxy = $config['http_proxy'];
// Basic proxy auth
if (isset($config['proxy_user'], $config['proxy_password']) && $config['proxy_user']) {
$http_proxy = $config['proxy_user'] . ':' . $config['proxy_password'] . '@' . $http_proxy;
}
$curl_cmd .= ' -x ' . $http_proxy;
}
print_cli("HTTP CURL cmd:\n$curl_cmd $request", FALSE);
}
/**
* Http query by native php function, compat when curl not installed.
*
* @param string $request
* @param array $context
* @return array
*/
function process_http_php($request, $context = []) {
global $config;
// Add common http context
$opts = [ 'http' => generate_http_context_defaults($context) ];
// Force IPv4 or IPv6
if (isset($config['http_ip_version'])) {
// Bind to IPv4 -> 0:0
// Bind to IPv6 -> [::]:0
$bindto = str_contains($config['http_ip_version'], '6') ? '[::]:0' : '0:0';
$opts['socket'] = [ 'bindto' => $bindto ];
}
// HTTPS
// if ($parse_url = parse_url($request))
// {
// if ($parse_url['scheme'] == 'https')
// {
// $opts['ssl'] = [ 'SNI_enabled' => TRUE, 'SNI_server_name' => $parse_url['host'] ];
// }
// }
// DEBUG, generate curl cmd for testing:
print_debug_curl_cmd($request, $opts['http']);
// Process http request and calculate runtime
$start = utime();
$context = stream_context_create($opts);
$response = file_get_contents($request, FALSE, $context);
$end = utime();
$runtime = $end - $start;
// Request end unixtime and runtime
$GLOBALS['request_unixtime'] = $end;
$GLOBALS['request_runtime'] = $runtime;
// Parse response headers
// Note: $http_response_header - see: http://php.net/manual/en/reserved.variables.httpresponseheader.php
if (function_exists('http_get_last_response_headers')) {
// PHP 8.4+
$http_response_header = http_get_last_response_headers();
}
$head = [];
foreach ($http_response_header as $k => $v) {
$t = explode(':', $v, 2);
if (isset($t[1])) {
// Date: Sat, 12 Apr 2008 17:30:38 GMT
$head[trim($t[0])] = trim($t[1]);
} elseif (preg_match("!HTTP/([\d\.]+)\s+(\d+)(.*)!", $v, $matches)) {
// HTTP/1.1 200 OK
$head['http'] = $matches[1];
$head['code'] = (int)$matches[2];
$head['descr'] = trim($matches[3]);
} else {
$head[] = $v;
}
}
return [
'response' => $response,
'request_unixtime' => $end,
'request_runtime' => $runtime,
'response_headers' => $head
];
}
function process_http_curl($request, $context = []) {
global $config;
$c = curl_init($request);
$options = [
CURLOPT_RETURNTRANSFER => TRUE, // return response instead print
CURLOPT_HTTP_CONTENT_DECODING => FALSE, // return raw response
//CURLOPT_HEADER => TRUE,
CURLINFO_HEADER_OUT => TRUE, // request headers
CURLOPT_USERAGENT => OBSERVIUM_PRODUCT . '/' . OBSERVIUM_VERSION,
//CURLOPT_CONNECTTIMEOUT => 0,
//CURLOPT_TIMEOUT => 5,
//CURLOPT_HTTPHEADER => $header,
//CURLOPT_CUSTOMREQUEST => 'POST',
//CURLOPT_POSTFIELDS => $fields,
//CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
//CURLOPT_ENCODING => '',
//CURLOPT_DNS_USE_GLOBAL_CACHE => false,
CURLOPT_SSL_VERIFYHOST => (bool)$config['http_ssl_verify'], // Disabled SSL Host check
CURLOPT_SSL_VERIFYPEER => (bool)$config['http_ssl_verify'], // Disabled SSL Cert check
];
// Append options based on defaults from generate_http_context_defaults($context)
$options[CURLOPT_TIMEOUT] = isset($context['timeout']) ? (int)$context['timeout'] : 15;
// Proxy
if (isset($config['http_proxy']) && $config['http_proxy']) {
$options[CURLOPT_PROXY] = $config['http_proxy'];
//$options[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP; // CURLPROXY_SOCKS4, CURLPROXY_SOCKS5, CURLPROXY_SOCKS4A, CURLPROXY_SOCKS5_HOSTNAME
}
// Basic proxy auth
if (isset($config['proxy_user'], $config['proxy_password']) && $config['proxy_user']) {
$options[CURLOPT_PROXYAUTH] = CURLAUTH_BASIC; // CURLAUTH_NTLM
$options[CURLOPT_PROXYUSERPWD] = $config['proxy_user'] . ':' . $config['proxy_password'];
}
// Headers
if (isset($context['header'])) {
$options[CURLOPT_HTTPHEADER] = explode("\r\n", trim($context['header']));
}
if (!safe_empty($context['content'])) {
$options[CURLOPT_POSTFIELDS] = $context['content'];
print_debug_vars($context['content']);
}
if ($context['method']) {
switch ($context['method']) {
case 'GET':
$options[CURLOPT_HTTPGET] = TRUE;
break;
case 'POST':
$options[CURLOPT_POST] = TRUE;
$options[CURLOPT_POSTREDIR] = 1; // follow 301 redir
break;
case 'PUT':
$options[CURLOPT_PUT] = TRUE;
$options[CURLOPT_POSTREDIR] = 1; // follow 301 redir
break;
default:
$options[CURLOPT_CUSTOMREQUEST] = $context['method'];
}
}
// Follow up to 3 redirects
$options[CURLOPT_MAXREDIRS] = 3;
$options[CURLOPT_FOLLOWLOCATION] = TRUE;
// Force IPv4 or IPv6
if (isset($config['http_ip_version'])) {
$options[CURLOPT_IPRESOLVE] = str_contains($config['http_ip_version'], '6') ? CURL_IPRESOLVE_V6 : CURL_IPRESOLVE_V4;
}
if (OBS_DEBUG) {
$options[CURLOPT_VERBOSE] = TRUE;
// DEBUG, generate curl cmd for testing:
//print_debug_curl_cmd($request, $context);
}
curl_setopt_array($c, $options);
$response = curl_exec($c);
$end = utime();
//r($response);
if (!curl_errno($c)) {
// Normal response
$http_info = curl_getinfo($c);
print_debug_vars($http_info);
//r($http_info);
$head = [
'http' => $http_info['http_version'],
'code' => $http_info['http_code'],
//'descr' => curl_error($c)
];
if (preg_match("!HTTP/([\d\.]+)!", $http_info['request_header'], $matches)) {
$head['http'] = $matches[1];
}
// HTTP code descriptions
switch ($http_info['http_code']) {
case 100:
$head['descr'] = 'Continue';
break;
case 101:
$head['descr'] = 'Switching Protocols';
break;
case 200:
$head['descr'] = 'OK';
break;
case 201:
$head['descr'] = 'Created';
break;
case 202:
$head['descr'] = 'Accepted';
break;
case 203:
$head['descr'] = 'Non-Authoritative Information';
break;
case 204:
$head['descr'] = 'No Content';
break;
case 205:
$head['descr'] = 'Reset Content';
break;
case 206:
$head['descr'] = 'Partial Content';
break;
case 300:
$head['descr'] = 'Multiple Choices';
break;
case 301:
$head['descr'] = 'Moved Permanently';
break;
case 302:
$head['descr'] = 'Moved Temporarily';
break;
case 303:
$head['descr'] = 'See Other';
break;
case 304:
$head['descr'] = 'Not Modified';
break;
case 305:
$head['descr'] = 'Use Proxy';
break;
case 400:
$head['descr'] = 'Bad Request';
break;
case 401:
$head['descr'] = 'Unauthorized';
break;
case 402:
$head['descr'] = 'Payment Required';
break;
case 403:
$head['descr'] = 'Forbidden';
break;
case 404:
$head['descr'] = 'Not Found';
break;
case 405:
$head['descr'] = 'Method Not Allowed';
break;
case 406:
$head['descr'] = 'Not Acceptable';
break;
case 407:
$head['descr'] = 'Proxy Authentication Required';
break;
case 408:
$head['descr'] = 'Request Time-out';
break;
case 409:
$head['descr'] = 'Conflict';
break;
case 410:
$head['descr'] = 'Gone';
break;
case 411:
$head['descr'] = 'Length Required';
break;
case 412:
$head['descr'] = 'Precondition Failed';
break;
case 413:
$head['descr'] = 'Request Entity Too Large';
break;
case 414:
$head['descr'] = 'Request-URI Too Large';
break;
case 415:
$head['descr'] = 'Unsupported Media Type';
break;
case 500:
$head['descr'] = 'Internal Server Error';
break;
case 501:
$head['descr'] = 'Not Implemented';
break;
case 502:
$head['descr'] = 'Bad Gateway';
break;
case 503:
$head['descr'] = 'Service Unavailable';
break;
case 504:
$head['descr'] = 'Gateway Time-out';
break;
case 505:
$head['descr'] = 'HTTP Version not supported';
break;
default:
$head['descr'] = 'Unknown HTTP status';
}
$runtime = $http_info['total_time'];
} else {
$head = [
//'http' => $http_info['http_version'],
'code' => curl_errno($c),
'descr' => curl_error($c)
];
$runtime = curl_getinfo($c, CURLINFO_TOTAL_TIME);
print_debug_vars(curl_getinfo($c));
}
curl_close($c);
return [
'response' => $response,
'request_unixtime' => $end,
'request_runtime' => $runtime,
'response_headers' => $head
];
}
/**
* Process HTTP request by definition array and process it for valid status.
* Used definition params in response key.
*
* @param string|array $def Definition array or alert transport key (see transports definitions)
* @param string $response Response from get_http_request()
*
* @return boolean Return TRUE if request processed with valid HTTP code (2xx, 3xx) and API response return valid param
*/
function test_http_request($def, $response) {
$response = trim($response);
if (is_string($def)) {
// Get transport definition for responses
$def = $GLOBALS['config']['transports'][$def]['notification'];
}
// Response is array (or xml)?
$is_response_array = strtolower($def['response_format']) === 'json';
// Set status by response status
$success = get_http_last_status();
// If response return valid code and content, additional parse for specific defined tests
if ($success) {
// Decode if request OK
if ($is_response_array) {
$response = safe_json_decode($response);
}
// else additional formats?
// Check if call succeeded
if (isset($def['response_test'])) {
// Convert single test condition to multi-level condition
if (isset($def['response_test']['operator'])) {
$def['response_test'] = [$def['response_test']];
}
// Compare all definition fields with response,
// if response param not equals to expected, set not success
// multilevel keys should written with '->' separator, ie: $a[key][some][0] - key->some->0
foreach ($def['response_test'] as $test) {
if ($is_response_array) {
$field = array_get_nested($response, $test['field']);
} else {
// RAW response
$field = $response;
}
if (test_condition($field, $test['operator'], $test['value']) === FALSE) {
print_debug("Response [" . $field . "] not valid: [" . $test['field'] . "] " . $test['operator'] . " [" . implode(', ', (array)$test['value']) . "]");
$success = FALSE;
break;
} else {
print_debug("Response [" . $field . "] valid: [" . $test['field'] . "] " . $test['operator'] . " [" . implode(', ', (array)$test['value']) . "]");
}
}
}
} elseif ($is_response_array && isset($def['response_fields']['message'], $def['response_fields']['status'])) {
// Decode response for useful error reports also for bad statuses
$response = safe_json_decode($response);
}
if (!$success) {
if (isset($def['response_fields']['message'], $def['response_fields']['status']) && is_array($response)) {
echo PHP_EOL;
if (isset($def['response_fields']['status'])) {
if ($def['response_fields']['status'] === 'raw') {
$status = get_http_last_code();
} else {
$status = array_get_nested($response, $def['response_fields']['status']);
}
if (OBS_DEBUG) {
print_message("%WRESPONSE STATUS%n[%r" . $status . "%n]", 'console');
}
}
$msg = array_get_nested($response, $def['response_fields']['message']);
if (isset($def['response_fields']['info']) &&
$info = array_get_nested($response, $def['response_fields']['info'])) {
$msg .= " ($info)";
}
if (OBS_DEBUG) {
print_message("%WRESPONSE ERROR%n[%y" . $msg . "%n]\n", 'console');
}
$GLOBALS['last_message'] = $msg;
} elseif (is_string($response) && $response && !get_http_last_status()) {
if (OBS_DEBUG) {
echo PHP_EOL;
print_message("%WRESPONSE STATUS%n[%r" . get_http_last_code() . "%n]", 'console');
print_message("%WRESPONSE ERROR%n[%y" . $response . "%n]\n", 'console');
}
$GLOBALS['last_message'] = $response;
}
}
print_debug_vars($response, 1);
return $success;
}
function process_http_request($def, $url, $options, &$response = NULL, $request_retry = 1) {
if (is_string($def)) {
// Get transport definition for responses
$def = $GLOBALS['config']['transports'][$def]['notification'];
}
// Rate limit
if (isset($def['ratelimit_key']) && !safe_empty($def['key'])) {
// Ratelimit if used an api key
$ratelimit = $def['ratelimit_key'];
} elseif (isset($def['ratelimit'])) {
$ratelimit = $def['ratelimit'];
} else {
$ratelimit = FALSE;
}
// Retry count (default 1, max 10). See discord definition
if (isset($def['request_retry']) && is_intnum($def['request_retry']) &&
$def['request_retry'] > 1 && $def['request_retry'] <= 10) {
$request_retry = $def['request_retry'];
}
$request_sleep = $request_retry > 1 ? 1 : 0; // sleep for 1s when retry count more than 1
$request_status = FALSE;
// Send out API call and parse response
for ($retry = 1; $retry <= (int)$request_retry; $retry++) {
print_debug("Request [$url] #$retry:");
$response = get_http_request($url, $options, $ratelimit);
if ($request_status = test_http_request($def, $response)) {
// stop for on success
return $request_status;
}
// wait little time
sleep($request_sleep);
}
return $request_status;
}
/**
* Return HTTP return code for last request by get_http_request()
*
* @return integer HTTP code
*/
function get_http_last_code() {
return $GLOBALS['response_headers']['code'];
}
/**
* Return HTTP return code for last request by get_http_request()
*
* @return boolean HTTP status TRUE if response code 2xx or 3xx
*/
function get_http_last_status() {
return $GLOBALS['request_status'];
}
/**
* Generate HTTP specific context with some defaults for proxy, timeout, user-agent.
* Used in get_http_request().
*
* @param array $context HTTP specified context, see http://php.net/manual/ru/function.stream-context-create.php
*
* @return array HTTP context array
*/
function generate_http_context_defaults($context = []) {
global $config;
if (!is_array($context)) {
$context = [];
} // Fix context if not array passed
// Defaults
if (!isset($context['timeout'])) {
$context['timeout'] = '15';
}
// HTTP/1.1
$context['protocol_version'] = 1.1;
// get the entire body of the response in case of error (HTTP/1.1 400, for example)
if (OBS_DEBUG) {
$context['ignore_errors'] = TRUE;
}
// User agent (required for some type of queries, ie geocoding)
if (!isset($context['header'])) {
$context['header'] = ''; // Avoid 'undefined index' when concatting below
}
$context['header'] .= 'User-Agent: ' . OBSERVIUM_PRODUCT . '/' . OBSERVIUM_VERSION . "\r\n";
if (isset($config['http_proxy']) && $config['http_proxy']) {
$context['proxy'] = 'tcp://' . $config['http_proxy'];
$context['request_fulluri'] = !isset($config['proxy_fulluri']) || (bool)$config['proxy_fulluri'];
}
// Basic proxy auth
if (isset($config['proxy_user'], $config['proxy_password']) && $config['proxy_user']) {
$auth = base64_encode($config['proxy_user'] . ':' . $config['proxy_password']);
$context['header'] .= 'Proxy-Authorization: Basic ' . $auth . "\r\n";
}
print_debug_vars($context);
return $context;
}
function generate_http_data($def, $tags = [], &$params = []) {
if (isset($def['request_params_key'])) {
// Key based link to request params, see google-chat notification
$key = 'request_params_' . strtolower(array_tag_replace($tags, $def['request_params_key']));
$request_params = $def[$key] ?? $def['request_params'];
} else {
// Common default request_params
$request_params = $def['request_params'];
}
// Generate request params
foreach ((array)$request_params as $param => $entry) {
// Param based on expression (only true/false)
// See: pushover notification def
if (str_contains($param, '?')) {
[ $param, $param_if ] = explode('?', $param, 2);
//print_vars($tags);
//print_vars($params);
if (!isset($tags[$param_if]) || !get_var_true($tags[$param_if])) {
print_debug("Request param '$param' skipped, because other param '$param_if' false or unset.");
continue;
}
}
// Try to find all keys in header like %bot_hash% matched with same key in $endpoint array
if (is_array($entry)) {
// i.e., teams and pagerduty
$params[$param] = array_merge((array)$params[$param], array_tag_replace($tags, $entry));
} elseif (!isset($params[$param]) || $params[$param] === '') {
$params[$param] = array_tag_replace($tags, $entry);
}
// Clean empty params
if (safe_empty($params[$param])) {
unset($params[$param]);
}
}
if (($def['method'] === 'POST' || $def['method'] === 'PUT') &&
strtolower($def['request_format']) === 'json') {
if (OBS_DEBUG) {
print_debug("\nJSON embedded params for method: ${def['method']}\n");
print_vars(safe_json_encode($params));
}
// Encode params as json string
return safe_json_encode($params);
}
// Encode params as url encoded string
return http_build_query($params);
}
/**
* Generate HTTP context based on passed params, tags and definition.
* This context will be used in get_http_request()
*
* @param string|array $def Definition array or alert transport key (see transports definitions)
* @param array $tags (optional) Contact array and other tags
* @param array $params (optional) Array of requested params with key => value entries (used with request method POST)
*
* @return array HTTP Context which can used in get_http_request()
* @global array $config
*/
function generate_http_context($def, $tags = [], $params = []) {
global $config;
if (is_string($def)) {
// Get transport definition for requests
$def = $config['transports'][$def]['notification'];
}
$context = []; // Init
// Request method POST/GET
if ($def['method']) {
$def['method'] = strtoupper($def['method']);
$context['method'] = $def['method'];
}
// Request timeout
if (is_intnum($def['timeout']) || $def['timeout'] >= 1 || $def['timeout'] <= 300) {
$context['timeout'] = $def['timeout'];
}
// Content and headers
$header = "Connection: close\r\n";
// Add encode $params for POST request inside http headers
if ($def['method'] === 'POST' || $def['method'] === 'PUT') {
$data = generate_http_data($def, $tags, $params);
if (strtolower($def['request_format']) === 'json') {
// Encode params as json string
//$data = safe_json_encode($params);
$header .= "Content-Type: application/json; charset=utf-8\r\n";
} else {
// Encode params as url encoded string
//$data = http_build_query($params);
// https://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data
//$header .= "Content-Type: multipart/form-data\r\n";
$header .= "Content-Type: application/x-www-form-urlencoded; charset=utf-8\r\n";
}
$header .= "Content-Length: " . strlen($data) . "\r\n";
// Encoded content data
$context['content'] = $data;
}
// Basic auth
if (isset($def['request_user'])) {
$basic_auth = $def['request_user'];
if (isset($def['request_password'])) {
$basic_auth .= ':' . $def['request_password'];
}
$basic_auth = array_tag_replace($tags, $basic_auth);
$header .= 'Authorization: Basic ' . base64_encode($basic_auth) . "\r\n";
}
// Additional headers with contact params
foreach ($def['request_header'] as $key => $entries) {
[ $head, $tag ] = explode('?', $key, 2);
if ($tag && (!isset($tags[$tag]) || !$tags[$tag])) {
// see webhook json transport
continue;
}
// Try to find all keys in header like %bot_hash% matched with same key in $endpoint array
foreach ((array)$entries as $entry) {
// multiple same headers can be exist at same time.
$header .= $head . ': ' . array_tag_replace($tags, $entry) . "\r\n";
}
}
$context['header'] = $header;
return $context;
}
/**
* Generate URL based on passed params, tags and definition.
* This context will be used in get_http_request() or process_http_request()
*
* @param string|array $def Definition array or alert transport key (see transports definitions)
* @param array $tags (optional) Contact array, used only if transport required additional headers (ie hipchat)
* @param array $params (optional) Array of requested params with key => value entries (used with request method GET)
* @param string $url_key Definition key which used for get url, default is $def['url']
*
* @return string URL which can used in get_http_request() or process_http_request()
* @global array $config
*/
function generate_http_url($def, $tags = [], $params = [], $url_key = 'url') {
global $config;
if (is_string($def)) {
// Get definition for transport API
$def = $config['transports'][$def]['notification'];
}
$url = ''; // Init
// Append (if set $def['url']) or set hardcoded url for transport
if (isset($def[$url_key])) {
// Try to find all keys in URL like %bot_hash% and %%url_encoded%% matched with the same key in $endpoint array
$url .= array_tag_replace_encode($tags, $def[$url_key]);
}
// Add GET params to url
if (($def['method'] === 'GET' || $def['method'] === 'DELETE')) {
$data = generate_http_data($def, $tags, $params);
if (safe_count($params)) {
if (str_contains($url, '?')) {
// Append additional params to url string
$url .= '&' . $data;
} else {
// Add get params as first time
$url .= '?' . $data;
}
}
}
//print_debug_vars($def);
//print_debug_vars($params);
//print_debug_vars($data);
print_debug_vars($url);
return $url;
}
function get_http_def($http_def, $tags) {
global $config;
if (!is_array($http_def)) {
$http_def = $config['http_api'][$http_def];
}
// Generate context/options with encoded data
$options = generate_http_context($http_def, $tags);
// API URL
$url = generate_http_url($http_def, $tags);
// Request
if (process_http_request($http_def, $url, $options, $response)) {
return $http_def['response_format'] === 'json' ? safe_json_decode($response) : $response;
}
return FALSE;
}
// EOF