Compare commits

...

3 Commits

Author SHA1 Message Date
Clemens Schwaighofer
d8379a10d9 URL Request phpunit test added 2024-11-06 10:33:05 +09:00
Clemens Schwaighofer
30e2f33620 Test calls update for admin area 2024-11-06 10:03:33 +09:00
Clemens Schwaighofer
a4f16f4ca9 Various updates and fixes during testing
Move the build auth content to dedicated variables
Add a default User-Agent that is always sent
Default headers like Authorization and User-Agent are always set, even when
request is sent with headers null
Fix timeout, was sent as is and not converted to milliseconds
Fix headers not correctly set to null if array entry was set to null
2024-11-06 10:03:14 +09:00
6 changed files with 1267 additions and 90 deletions

View File

@@ -1,4 +1,4 @@
<?php
<?php // phpcs:ignore PSR1.Files.SideEffects
/**
* AUTHOR: Clemens Schwaighofer
@@ -9,20 +9,40 @@
declare(strict_types=1);
/**
* build return json
*
* @param array $http_headers
* @param string $body
* @return string
*/
function buildContent(array $http_headers, string $body): string
{
return json_encode([
'HEADERS' => $http_headers,
"REQUEST_TYPE" => $_SERVER['REQUEST_METHOD'],
"PARAMS" => $_GET,
"BODY" => json_decode($body, true)
]);
}
$http_headers = array_filter($_SERVER, function ($value, $key) {
if (str_starts_with($key, 'HTTP_')) {
return true;
}
}, ARRAY_FILTER_USE_BOTH);
$file_get = file_get_contents('php://input') ?: '["code": 500, "content": {"Error" => "file_get_contents failed"}]';
header("Content-Type: application/json; charset=UTF-8");
print json_encode([
'HEADERS' => $http_headers,
"PARAMS" => $_GET,
"BODY" => json_decode($file_get, true),
]);
if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) {
header("HTTP/1.1 401 Unauthorized");
print buildContent($http_headers, '["code": 401, "content": {"Error" => "Not Authorized"}]');
exit;
}
print buildContent(
$http_headers,
file_get_contents('php://input') ?: '["code": 500, "content": {"Error" => "file_get_contents failed"}]'
);
// __END__

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<?php
<?php // phpcs:ignore PSR1.Files.SideEffects
declare(strict_types=1);
@@ -12,28 +12,49 @@ $log = new CoreLibs\Logging\Logging([
'log_per_date' => true,
]);
/**
* build return json
*
* @param array<string,mixed> $http_headers
* @param string $body
* @return string
*/
function buildContent(array $http_headers, string $body): string
{
return Json::jsonConvertArrayTo([
'HEADERS' => $http_headers,
"REQUEST_TYPE" => $_SERVER['REQUEST_METHOD'],
"PARAMS" => $_GET,
"BODY" => Json::jsonConvertToArray($body)
]);
}
$http_headers = array_filter($_SERVER, function ($value, $key) {
if (str_starts_with($key, 'HTTP_')) {
return true;
}
}, ARRAY_FILTER_USE_BOTH);
header("Content-Type: application/json; charset=UTF-8");
// if the header has Authorization and RunAuthTest then exit with 401
if (!empty($http_headers['HTTP_AUTHORIZATION']) && !empty($http_headers['HTTP_RUNAUTHTEST'])) {
header("HTTP/1.1 401 Unauthorized");
print buildContent($http_headers, '["code": 401, "content": {"Error" => "Not Authorized"}]');
exit;
}
$file_get = file_get_contents('php://input') ?: '{"Error" => "file_get_contents failed"}';
// str_replace('\"', '"', trim($file_get, '"'));
$log->debug('SERVER', $log->prAr($_SERVER));
$log->debug('HEADERS', $log->prAr($http_headers));
$log->debug('REQUEST TYPE', $_SERVER['REQUEST_METHOD']);
$log->debug('GET', $log->prAr($_GET));
$log->debug('POST', $log->prAr($_POST));
$log->debug('PHP-INPUT', $log->prAr($file_get));
header("Content-Type: application/json; charset=UTF-8");
print Json::jsonConvertArrayTo([
'HEADERS' => $http_headers,
"PARAMS" => $_GET,
"BODY" => Json::jsonConvertToArray($file_get),
]);
print buildContent($http_headers, $file_get);
$log->debug('[END]', '=========================================>');

View File

@@ -204,6 +204,9 @@ try {
'default-remove-array-part-alt' => ['c', 'd', 'e'],
'default-overwrite' => 'will be overwritten',
'default-add' => 'will be added',
],
'query' => [
'global-p' => 'glob'
]
]);
print "CONFIG: <pre>" . print_r($uc->getConfig(), true) . "</pre>";
@@ -217,13 +220,16 @@ try {
print "CONFIG: <pre>" . print_r($uc->getConfig(), true) . "</pre>";
$data = $uc->request(
'get',
'UrlRequests.target.php?other=get_a',
'UrlRequests.target.php',
[
'headers' => [
'call-header' => 'call-get',
'default-header' => 'overwrite-uc-get',
'X-Foo' => ['bar', 'baz'],
]
],
'query' => [
'other' => 'get_a',
],
]
);
print "[uc] _GET RESPONSE, nothing set: <pre>" . print_r($data, true) . "</pre>";
@@ -234,6 +240,55 @@ try {
print "Exception: <pre>" . print_r(json_decode($e->getMessage(), true), true) . "</pre><br>";
}
print "<hr>";
try {
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/',
"exception_on_not_authorized" => false,
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
]
]);
$response = $uc->get('UrlRequests.target.php');
print "AUTH REQUEST: <pre>" . print_r($response, true) . "</pre>";
print "[uc] SENT URL: " . $uc->getUrlSent() . "<br>";
print "[uc] SENT URL PARSED: <pre>" . print_r($uc->getUrlParsedSent(), true) . "</pre>";
print "[uc] SENT HEADERS: <pre>" . print_r($uc->getHeadersSent(), true) . "</pre>";
} catch (Exception $e) {
print "Exception: <pre>" . print_r(json_decode($e->getMessage(), true), true) . "</pre><br>";
}
print "AUTH REQUEST WITH EXCEPTION:<br>";
try {
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/',
"exception_on_not_authorized" => true,
"headers" => [
"Authorization" => "schmalztiegel",
"RunAuthTest" => "yes",
]
]);
$response = $uc->get('UrlRequests.target.php');
print "AUTH REQUEST: <pre>" . print_r($response, true) . "</pre>";
print "[uc] SENT URL: " . $uc->getUrlSent() . "<br>";
print "[uc] SENT URL PARSED: <pre>" . print_r($uc->getUrlParsedSent(), true) . "</pre>";
print "[uc] SENT HEADERS: <pre>" . print_r($uc->getHeadersSent(), true) . "</pre>";
} catch (Exception $e) {
print "Exception: <pre>" . print_r(json_decode($e->getMessage(), true), true) . "</pre><br>";
}
print "<hr>";
$uc = new Curl([
"base_uri" => 'https://soba.egplusww.jp/developers/clemens/core_data/php_libraries/trunk/www/admin/',
"headers" => [
"header-one" => "one"
]
]);
$response = $uc->get('UrlRequests.target.php', ["headers" => null, "query" => ["test" => "one-test"]]);
print "AUTH REQUEST: <pre>" . print_r($response, true) . "</pre>";
print "[uc] SENT URL: " . $uc->getUrlSent() . "<br>";
print "[uc] SENT URL PARSED: <pre>" . print_r($uc->getUrlParsedSent(), true) . "</pre>";
print "[uc] SENT HEADERS: <pre>" . print_r($uc->getHeadersSent(), true) . "</pre>";
print "</body></html>";

View File

@@ -53,10 +53,12 @@ class Curl implements Interface\RequestsInterface
public const HTTP_CREATED = 201;
/** @var int http ok no content */
public const HTTP_NO_CONTENT = 204;
/** @var int major version for user agent */
public const MAJOR_VERSION = 1;
// the config is set to be as much compatible to guzzelHttp as possible
// phpcs:disable Generic.Files.LineLength
/** @var array{auth?:array{0:string,1:string,2:string},auth_type?:int|string,auth_userpwd?:string,exception_on_not_authorized:bool,base_uri:string,headers:array<string,string|array<string>>,query:array<string,string>,timeout:float,connection_timeout:float} config settings as
/** @var array{auth?:array{0:string,1:string,2:string},exception_on_not_authorized:bool,base_uri:string,headers:array<string,string|array<string>>,query:array<string,string>,timeout:float,connection_timeout:float} config settings as
*phpcs:enable Generic.Files.LineLength
* auth: [0: user, 1: password, 2: auth type]
* base_uri: base url to set, will prefix all urls given in calls
@@ -78,6 +80,12 @@ class Curl implements Interface\RequestsInterface
private array $parsed_base_uri = [];
/** @var array<string,string> lower key header name matches to given header name */
private array $headers_named = [];
/** @var int auth type from auth array in config */
private int $auth_type = 0;
/** @var string username and password string from auth array in config */
private string $auth_userpwd = '';
/** @var string set if auth type basic is given, will be set as "Authorization: ..." */
private string $auth_basic_header = '';
/** @var array<string,array<string>> received headers per header name, with sub array if there are redirects */
private array $received_headers = [];
@@ -107,7 +115,7 @@ class Curl implements Interface\RequestsInterface
* Set the main configuration
*
* phpcs:disable Generic.Files.LineLength
* @param array{auth?:array{0:string,1:string,2:string},auth_type?:int|string,auth_userpwd?:string,exception_on_not_authorized?:bool,base_uri?:string,headers?:array<string,string|array<string>>,query?:array<string,string>,timeout?:float,connection_timeout?:float} $config
* @param array{auth?:array{0:string,1:string,2:string},exception_on_not_authorized?:bool,base_uri?:string,headers?:array<string,string|array<string>>,query?:array<string,string>,timeout?:float,connection_timeout?:float} $config
* @return void
* phpcs:enable Generic.Files.LineLength
*/
@@ -128,19 +136,22 @@ class Curl implements Interface\RequestsInterface
$userpwd = $config['auth'][0] . ':' . $config['auth'][1];
switch ($type) {
case 'basic':
if (!isset($config['headers']['Authorization'])) {
$config['headers']['Authorization'] = 'Basic ' . base64_encode(
$userpwd
);
}
$this->auth_basic_header = 'Basic ' . base64_encode(
$userpwd
);
// if (!isset($config['headers']['Authorization'])) {
// $config['headers']['Authorization'] = 'Basic ' . base64_encode(
// $userpwd
// );
// }
break;
case 'digest':
$config['auth_type'] = CURLAUTH_DIGEST;
$config['auth_userpwd'] = $userpwd;
$this->auth_type = CURLAUTH_DIGEST;
$this->auth_userpwd = $userpwd;
break;
case 'ntlm':
$config['auth_type'] = CURLAUTH_NTLM;
$config['auth_userpwd'] = $userpwd;
$this->auth_type = CURLAUTH_NTLM;
$this->auth_userpwd = $userpwd;
break;
}
}
@@ -152,8 +163,6 @@ class Curl implements Interface\RequestsInterface
$config['exception_on_not_authorized'] = false;
}
if (!empty($config['base_uri'])) {
// TODO: this should be run through Guzzle\Psr7\Utils in future
// $config['base_uri'] = Psr7\Utils::urlFor($config['base_uri']);
if (($parsed_base_uri = $this->parseUrl($config['base_uri'])) !== false) {
$this->parsed_base_uri = $parsed_base_uri;
$config['base_uri'] = $config['base_uri'];
@@ -303,6 +312,14 @@ class Curl implements Interface\RequestsInterface
$this->parsed_url = $parsed_url;
}
}
// build query with global query
// any query set in the base_url or url_req will be overwritten
if (!empty($this->config['query'])) {
// add current query if set
// for params: if foo[0] then we ADD as php array type
// note that this has to be done on the user side, we just merge and local overrides global
$query = array_merge($this->config['query'], $query ?? []);
}
if (is_array($query)) {
$query = http_build_query($query, '', '&', PHP_QUERY_RFC3986);
}
@@ -382,6 +399,27 @@ class Curl implements Interface\RequestsInterface
return $return_headers;
}
/**
* default headers that are always set
* Authorization
* User-Agent
*
* @return array<string,string>
*/
private function buildDefaultHeaders(): array
{
$headers = [];
// add auth header if set
if (!empty($this->auth_basic_header)) {
$headers['Authorization'] = $this->auth_basic_header;
}
// always add HTTP_HOST and HTTP_USER_AGENT
if (!isset($headers[strtolower('User-Agent')])) {
$headers['User-Agent'] = 'CoreLibsUrlRequestCurl/' . self::MAJOR_VERSION;
}
return $headers;
}
/**
* Build headers, combine with global headers of they are set
*
@@ -390,9 +428,10 @@ class Curl implements Interface\RequestsInterface
*/
private function buildHeaders(null|array $headers): array
{
// if headers is null, return empty headers, do not set default headers
// if headers is null, return empty headers, do not set config default headers
// but the automatic set User-Agent and Authorization headers are always set
if ($headers === null) {
return [];
return $this->buildDefaultHeaders();
}
// merge master headers with sub headers, sub headers overwrite master headers
if (!empty($this->config['headers'])) {
@@ -410,7 +449,7 @@ class Curl implements Interface\RequestsInterface
$headers[$key] = $this->config['headers'][$key];
}
}
// always add HTTP_HOST and HTTP_USER_AGENT
$headers = array_merge($headers, $this->buildDefaultHeaders());
return $headers;
}
@@ -427,6 +466,7 @@ class Curl implements Interface\RequestsInterface
* @param null|array<string,string> $query [default=null] Optinal query parameters
* @param null|string|array<string,mixed> $body [default=null] Data body, converted to JSON
* @return array{code:string,headers:array<string,array<string>>,content:string}
* @throws \RuntimeException if type param is not valid
*/
private function curlRequest(
string $type,
@@ -492,6 +532,7 @@ class Curl implements Interface\RequestsInterface
*
* @param string $url
* @return \CurlHandle
* @throws \RuntimeException if curl could not be initialized
*/
private function handleCurleInit(string $url): \CurlHandle
{
@@ -528,14 +569,15 @@ class Curl implements Interface\RequestsInterface
private function setCurlOptions(\CurlHandle $handle, array $headers): void
{
// for not Basic auth, basic auth sets its own header
if (!empty($this->config['auth_type']) && !empty($this->config['auth_userpwd'])) {
curl_setopt($handle, CURLOPT_HTTPAUTH, $this->config['auth_type']);
curl_setopt($handle, CURLOPT_USERPWD, $this->config['auth_userpwd']);
if (!empty($this->auth_type) && !empty($this->auth_userpwd)) {
curl_setopt($handle, CURLOPT_HTTPAUTH, $this->auth_type);
curl_setopt($handle, CURLOPT_USERPWD, $this->auth_userpwd);
}
if ($headers !== []) {
curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
}
// curl_setopt($handle, CURLOPT_FAILONERROR, true);
// return response as string and not just HTTP_OK
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
// for debug only
curl_setopt($handle, CURLINFO_HEADER_OUT, true);
@@ -547,11 +589,11 @@ class Curl implements Interface\RequestsInterface
// if we have a timeout signal
if (!empty($this->config['timeout'])) {
$timeout_requires_no_signal |= $this->config['timeout'] < 1;
curl_setopt($handle, CURLOPT_TIMEOUT_MS, $this->config['timeout']);
curl_setopt($handle, CURLOPT_TIMEOUT_MS, $this->config['timeout'] * 1000);
}
if (!empty($this->config['connection_timeout'])) {
$timeout_requires_no_signal |= $this->config['connection_timeout'] < 1;
curl_setopt($handle, CURLOPT_CONNECTTIMEOUT_MS, $this->config['connection_timeout']);
curl_setopt($handle, CURLOPT_CONNECTTIMEOUT_MS, $this->config['connection_timeout'] * 1000);
}
if ($timeout_requires_no_signal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
curl_setopt($handle, CURLOPT_NOSIGNAL, true);
@@ -583,14 +625,17 @@ class Curl implements Interface\RequestsInterface
/**
* handles any CURL execute and on error throws a correct error message
*
* @param \CurlHandle $handle
* @return string
* @param \CurlHandle $handle Curl handler
* @return string Return content as string, if False will throw exception
* will only return HTTP_OK if CURLOPT_RETURNTRANSFER is turned off
* @throws \RuntimeException if the connection had an error
*/
private function handleCurlExec(\CurlHandle $handle): string
{
// execute query
$http_result = curl_exec($handle);
if ($http_result === true) {
// only if CURLOPT_RETURNTRANSFER
return (string)self::HTTP_OK;
} elseif ($http_result !== false) {
return $http_result;
@@ -640,7 +685,8 @@ class Curl implements Interface\RequestsInterface
*
* @param string $http_result
* @param \CurlHandle $handle
* @return string
* @return string http response code
* @throws \RuntimeException Auth error
*/
private function handleCurlResponse(
string $http_result,
@@ -658,18 +704,14 @@ class Curl implements Interface\RequestsInterface
$result_ar = json_decode((string)$http_result, true);
$url = curl_getinfo($handle, CURLINFO_EFFECTIVE_URL);
$error_status = 'ERROR';
$error_code = $http_response;
$error_type = 'UnauthorizedRequest';
$message = 'Request could not be finished successfully because of an authorization error';
// throw Error here with all codes
throw new RuntimeException(
json_encode([
'status' => $error_status,
'code' => $error_code,
'type' => $error_type,
'message' => $message,
'status' => 'ERROR',
'code' => $http_response,
'type' => 'UnauthorizedRequest',
'message' => 'Request could not be finished successfully because of an authorization error',
'context' => [
'url' => $url,
'result' => $result_ar,
@@ -823,7 +865,7 @@ class Curl implements Interface\RequestsInterface
* remove header entry
* if key is only set then match only key, if both are set both sides must match
*
* @param array<string,string> $remove_headers
* @param array<string,null|string|array<string>> $remove_headers
* @return void
*/
public function removeHeaders(array $remove_headers): void
@@ -861,10 +903,11 @@ class Curl implements Interface\RequestsInterface
if (!is_array($value)) {
$value = [$value];
}
$this->config['headers'][$header_key] = array_diff(
// array values so we rewrite the key pos
$this->config['headers'][$header_key] = array_values(array_diff(
$this->config['headers'][$header_key],
$value
);
));
}
}
}
@@ -917,7 +960,7 @@ class Curl implements Interface\RequestsInterface
return $this->curlRequest(
$type,
$url,
$options['headers'] ?? [],
!array_key_exists('headers', $options) ? [] : $options['headers'],
$options['query'] ?? null,
$options['body'] ?? null
);

View File

@@ -18,6 +18,32 @@ namespace CoreLibs\UrlRequests;
trait CurlTrait
{
/**
* Set the array block that is sent to the request call
* Make sure that if headers is set as key but null it stays null and set to empty array
* if headers key is missing
* "get" calls do not set any body
*
* @param string $type if set as get do not add body, else add body
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $options Request options
* @return array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>}
*/
private function setOptions(string $type, array $options): array
{
if ($type == "get") {
return [
"headers" => !array_key_exists('headers', $options) ? [] : $options['headers'],
"query" => $options['query'] ?? null,
];
} else {
return [
"headers" => !array_key_exists('headers', $options) ? [] : $options['headers'],
"query" => $options['query'] ?? null,
"body" => $options['body'] ?? null,
];
}
}
/**
* combined set call for any type of request with options type parameters
* The following options can be set:
@@ -28,10 +54,10 @@ trait CurlTrait
* @param string $type What type of request we send, will throw exception if not a valid one
* @param string $url The url to send
* @param array{headers?:null|array<string,string|array<string>>,query?:null|string|array<string,mixed>,body?:null|string|array<string,mixed>} $options Request options
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
* @throws \UnexpectedValueException on missing body data when body data is needed
*/
abstract public function request(string $type, string $url, array $options): array;
abstract public function request(string $type, string $url, array $options = []): array;
/**
* Makes an request to the target url via curl: GET
@@ -40,18 +66,18 @@ trait CurlTrait
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
*/
public function get(string $url, array $options): array
public function get(string $url, array $options = []): array
{
return $this->request(
"get",
$url,
[
"headers" => $options['headers'] ?? [],
"query" => $options['query'] ?? null,
],
$this->setOptions('get', $options),
);
// array{headers?: array<string, array<string>|string>|null, query?: array<string, string>|null, body?: array<string, mixed>|string|null},
// array{headers?: array<string, array<string>|string>|null, query?: array<string, mixed>|string|null, body?: array<string, mixed>|string|null}
}
/**
@@ -68,11 +94,7 @@ trait CurlTrait
return $this->request(
"post",
$url,
[
"headers" => $options['headers'] ?? [],
"query" => $options['query'] ?? null,
"body" => $options['body'] ?? null,
],
$this->setOptions('post', $options),
);
}
@@ -90,11 +112,7 @@ trait CurlTrait
return $this->request(
"put",
$url,
[
"headers" => $options['headers'] ?? [],
"query" => $options['query'] ?? null,
"body" => $options['body'] ?? null,
],
$this->setOptions('put', $options),
);
}
@@ -112,11 +130,7 @@ trait CurlTrait
return $this->request(
"patch",
$url,
[
"headers" => $options['headers'] ?? [],
"query" => $options['query'] ?? null,
"body" => $options['body'] ?? null,
],
$this->setOptions('patch', $options),
);
}
@@ -128,18 +142,14 @@ trait CurlTrait
* @param string $url The URL being requested,
* including domain and protocol
* @param array{headers?:null|array<string,string|array<string>>,query?:null|array<string,string>,body?:null|string|array<mixed>} $options Options to set
* @return array{code:string,headers:array<string,array<string>>,content:string} Result code, headers and content as array, content is json
* @return array{code:string,headers:array<string,array<string>>,content:string} [default=[]] Result code, headers and content as array, content is json
*/
public function delete(string $url, array $options): array
public function delete(string $url, array $options = []): array
{
return $this->request(
"delete",
$url,
[
"headers" => $options['headers'] ?? [],
"query" => $options['query'] ?? null,
"body" => $options['body'] ?? null,
],
$this->setOptions('delete', $options),
);
}
}