rosavox/lib/alveolata/http/functions.php
2025-05-23 07:33:29 +00:00

309 lines
7.3 KiB
PHP

<?php
namespace alveolata\http;
require_once(DIR_ALVEOLATA . '/http/types.php');
require_once(DIR_ALVEOLATA . '/log/functions.php');
/**
* @author Christian Fraß <frass@greenscale.de>
*/
function method_to_oas(
string $http_method
) : string
{
return \strtolower($http_method);
}
/**
* @param string $input
* @return \alveolata\http\struct_request
* @author Christian Fraß <frass@greenscale.de>
*/
function request_decode(
string $input
) : \alveolata\http\struct_request
{
$input_ = str_replace("\r\n", "\n", $input);
$parts = explode("\n", $input_);
$chunks = [];
$request_method = null;
$request_path = null;
$request_protocol = null;
$request_headers = [];
$request_body = '';
foreach ($parts as $part) {
if (preg_match('/^([A-Z]+) ([\S]*) (.*)$/', $part, $matches) === 1) {
$request_method = strtoupper($matches[1]);
$request_path = $matches[2];
$request_protocol = $matches[3];
}
else if (preg_match('/^([^:]+): (.*)$/', $part, $matches) === 1) {
$request_headers[$matches[1]] = $matches[2];
}
else if ($part !== '') {
array_push($chunks, $part);
}
else {
// ignore
}
}
$request_body .= implode("\n", $chunks);
$request = new \alveolata\http\struct_request(
$request_protocol,
$request_method,
$request_path,
$request_headers,
$request_body
);
return $request;
}
/**
* @param \alveolata\http\struct_response $response
* @return string
* @author Christian Fraß <frass@greenscale.de>
*/
function response_encode(
\alveolata\http\struct_response $response
) : string
{
$code_text_map = [
'100' => 'Continue',
'101' => 'Switching Protocols',
'103' => 'Early Hints',
'200' => 'OK',
'201' => 'Created',
'202' => 'Accepted',
'203' => 'Non-Authoritative Information',
'204' => 'No Content',
'205' => 'Reset Content',
'206' => 'Partial Content',
'300' => 'Multiple Choices',
'301' => 'Moved Permanently',
'302' => 'Found',
'303' => 'See Other',
'304' => 'Not Modified',
'307' => 'Temporary Redirect',
'308' => 'Permanent Redirect',
'400' => 'Bad Request',
'401' => 'Unauthorized',
'402' => 'Payment Required',
'403' => 'Forbidden',
'404' => 'Not Found',
'405' => 'Method Not Allowed',
'406' => 'Not Acceptable',
'407' => 'Proxy Authentication Required',
'408' => 'Request Timeout',
'409' => 'Conflict',
'410' => 'Gone',
'411' => 'Length Required',
'412' => 'Precondition Failed',
'413' => 'Payload Too Large',
'414' => 'URI Too Long',
'415' => 'Unsupported Media Type',
'416' => 'Range Not Satisfiable',
'417' => 'Expectation Failed',
'418' => 'I\'m a teapot',
'422' => 'Unprocessable Entity',
'424' => 'Failed Dependency',
'425' => 'Too Early',
'426' => 'Upgrade Required',
'428' => 'Precondition Required',
'429' => 'Too Many Requests',
'431' => 'Request Header Fields Too Large',
'451' => 'Unavailable For Legal Reasons',
'500' => 'Internal Server Error',
'501' => 'Not Implemented',
'502' => 'Bad Gateway',
'503' => 'Service Unavailable',
'504' => 'Gateway Timeout',
'505' => 'HTTP Version Not Supported',
'506' => 'Variant Also Negotiates',
'507' => 'Insufficient Storage',
'508' => 'Loop Detected',
'510' => 'Not Extended',
'511' => 'Network Authentication Required',
];
$output = implode(
"\r\n",
array_merge(
[
sprintf('HTTP/1.1 %d %s', $response->statuscode, $code_text_map[strval($response->statuscode)]),
],
array_map(
function ($key) use (&$response) {
$value = $response->headers[$key];
return sprintf('%s: %s', $key, $value);
},
array_keys($response->headers)
),
[
'',
$response->body,
]
)
);
return $output;
}
/**
* sends an HTTP request and returns the response
* requires CURL plugin
*
* @param array $options {
* record<
* timeout ?: float,
* >
* }
*/
function call(
\alveolata\http\struct_request $request,
array $options = []
): \alveolata\http\struct_response
{
$options = \array_merge(
[
'timeout' => 5.0
],
$options
);
\alveolata\log\debug(
'http_call_request',
[
'request' => [
'protocol' => $request->protocol,
'method' => $request->method,
'target' => $request->target,
'headers' => $request->headers,
'body' => $request->body,
],
]
);
$request_headers_transformed = [];
foreach ($request->headers as $key => $value) {
\array_push($request_headers_transformed, \sprintf('%s: %s', $key, $value));
}
$curl_object = \curl_init();
// set common options
\curl_setopt($curl_object, \CURLOPT_CONNECTTIMEOUT, $options['timeout']);
\curl_setopt($curl_object, \CURLOPT_TIMEOUT, $options['timeout']);
\curl_setopt($curl_object, \CURLOPT_FOLLOWLOCATION, true);
// set request options
\curl_setopt($curl_object, \CURLOPT_URL, $request->target);
\curl_setopt($curl_object, \CURLOPT_CUSTOMREQUEST, $request->method);
\curl_setopt($curl_object, \CURLINFO_HEADER_OUT, true);
\curl_setopt($curl_object, \CURLOPT_HTTPHEADER, $request_headers_transformed);
\curl_setopt($curl_object, \CURLOPT_SSL_VERIFYPEER, /*$options['sslVerifyPeer']*/1);
\curl_setopt($curl_object, \CURLOPT_SSL_VERIFYHOST, /*$options['sslVerifyHost']*/2);
if (
\in_array(
$request->method,
[
\alveolata\http\enum_method::post,
\alveolata\http\enum_method::put,
\alveolata\http\enum_method::patch,
]
)
) {
\curl_setopt($curl_object, \CURLOPT_POSTFIELDS, $request->body);
}
// set response options
\curl_setopt($curl_object, \CURLOPT_RETURNTRANSFER, true);
\curl_setopt($curl_object, \CURLOPT_HEADER, 1);
$response_raw = \curl_exec($curl_object);
if ($response_raw === false) {
$error_number = \curl_errno($curl_object);
$error_message = \curl_error($curl_object);
\curl_close($curl_object);
throw (new \Exception(
\sprintf(
'%s | %s',
'http_call_failed',
\sprintf(
\json_encode(
[
'error_number' => $error_number,
'error_message' => $error_message,
'method' => $request->method,
'target' => $request->target,
]
)
)
)
));
}
else {
$header_size = \curl_getinfo($curl_object, \CURLINFO_HEADER_SIZE);
// statuscode
{
$statuscode = \intval(\curl_getinfo($curl_object, \CURLINFO_HTTP_CODE));
}
// headers
{
$headers_raw = \substr($response_raw, 0, $header_size);
$header_parts = \explode("\r\n", $headers_raw);
// throw away first part, containing the status code information
\array_shift($header_parts);
$headers = [];
foreach ($header_parts as $part) {
if ($part === '') {
// do nothing
}
else if (\preg_match('/^HTTP\/.* [0-9]{3} .*$/', $part) === 1) {
// e.g. "HTTP\/1.1 200 OK"
// do nothing
}
else {
$funds = null;
$matching = (\preg_match('/([^:]*): (.*)/', $part, $funds) === 1);
if (! $matching) {
\alveolata\log\warning(
'http_call_malformed_header',
[
'header' => $part,
]
);
}
else {
$key = $funds[1];
$value = $funds[2];
$headers[$key] = $value;
}
}
}
}
// body
{
$body = \substr($response_raw, $header_size);
}
\curl_close($curl_object);
$response = new \alveolata\http\struct_response($statuscode, $headers, $body);
\alveolata\log\debug(
'http_call_response',
[
'response' => [
'statuscode' => $response->statuscode,
'headers' => $response->headers,
'body' => $response->body,
],
]
);
return $response;
}
}
?>