309 lines
7.3 KiB
PHP
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;
|
|
}
|
|
}
|
|
|
|
?>
|