Skip to content

Commit

Permalink
feat(appsec): Add appsec timeout (#14)
Browse files Browse the repository at this point in the history
* feat(timeout): Handle appsec timeout and throw specific error for timeout issue

* feat(file_get_contents): Fix http_response_header undefined in case of failure

* feat(file_get_contents): Exclude SSL timeout from timeout exceptions

* feat(appsec): Rename all app_sec in appsec

* docs(*): Update docs

* feat(appsec): Use milliseconds for AppSec timeout

* feat(appsec): Rename appsec timeout configurations

* style(phpstan): Ignore phpstan false positive
  • Loading branch information
julienloizelet authored Oct 4, 2024
1 parent 396d64f commit ccbdfe5
Show file tree
Hide file tree
Showing 15 changed files with 386 additions and 97 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ As far as possible, we try to adhere to [Symfony guidelines](https://symfony.com
---

## [2.3.0](https://github.com/crowdsecurity/php-common/releases/tag/v2.3.0) - 2024-??-??
[_Compare with previous release_](https://github.com/crowdsecurity/php-common/compare/v2.2.0...v2.3.0)
[_Compare with previous release_](https://github.com/crowdsecurity/php-common/compare/v2.2.0...HEAD)


### Added

- Add AppSec requests support

### Changed

- Throws a `CrowdSec\Common\Client\TimeoutException` for `curl` and `file_get_contents` request handlers when a
timeout is detected

---

Expand Down
8 changes: 6 additions & 2 deletions docs/DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@

- [Local development](#local-development)
- [DDEV setup](#ddev-setup)
- [DDEV installation](#ddev-installation)
- [Prepare DDEV PHP environment](#prepare-ddev-php-environment)
- [DDEV Usage](#ddev-usage)
- [Use composer to update or install the lib](#use-composer-to-update-or-install-the-lib)
- [Unit test](#unit-test)
- [Coding standards](#coding-standards)
- [Commit message](#commit-message)
- [Allowed message `type` values](#allowed-message-type-values)
- [Update documentation table of contents](#update-documentation-table-of-contents)
Expand Down Expand Up @@ -148,7 +153,6 @@ To use the [PHPMD](https://github.com/phpmd/phpmd) tool, you can run:

```bash
ddev phpmd ./my-code/common/tools/coding-standards phpmd/rulesets.xml ../../src

```

##### PHPCS and PHPCBF
Expand Down Expand Up @@ -247,7 +251,7 @@ npm install -g doctoc
Then, run it in the documentation folder:

```bash
doctoc docs/* --maxlevel 3
doctoc docs/* --maxlevel 4
```


Expand Down
2 changes: 1 addition & 1 deletion src/Client/AbstractClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public function __construct(
$this->configs = $configs;
$this->requestHandler = ($requestHandler) ?: new Curl($this->configs);
$this->url = $this->getConfig('api_url');
$this->appSecUrl = $this->getConfig('app_sec_url');
$this->appSecUrl = $this->getConfig('appsec_url');
if (!$logger) {
$logger = new Logger('null');
$logger->pushHandler(new NullHandler());
Expand Down
2 changes: 1 addition & 1 deletion src/Client/ClientException.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use CrowdSec\Common\Exception;

/**
* Class for all exceptions thrown by CrowdSec client.
* Class for generic exceptions thrown by CrowdSec client.
*
* @author CrowdSec team
*
Expand Down
29 changes: 29 additions & 0 deletions src/Client/RequestHandler/AbstractRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace CrowdSec\Common\Client\RequestHandler;

use CrowdSec\Common\Client\HttpMessage\AppSecRequest;
use CrowdSec\Common\Client\HttpMessage\Request;
use CrowdSec\Common\Constants;

/**
* Request handler abstraction.
*
Expand Down Expand Up @@ -33,4 +37,29 @@ public function getConfig(string $name)
{
return (isset($this->configs[$name])) ? $this->configs[$name] : null;
}

/**
* Retrieve the appropriate timeout value for the current request.
* The returned value will be used as milliseconds for AppSec requests and as seconds for API requests.
*/
protected function getTimeout(Request $request): int
{
if ($request instanceof AppSecRequest) {
return $this->getConfig('appsec_timeout_ms') ?? Constants::APPSEC_TIMEOUT_MS;
}

return $this->getConfig('api_timeout') ?? Constants::API_TIMEOUT;
}

/**
* Retrieve the appropriate connect timeout value for the current request.
*/
protected function getConnectTimeout(Request $request): int
{
if ($request instanceof AppSecRequest) {
return $this->getConfig('appsec_connect_timeout_ms') ?? Constants::APPSEC_CONNECT_TIMEOUT_MS;
}

return $this->getConfig('api_connect_timeout') ?? Constants::API_CONNECT_TIMEOUT;
}
}
181 changes: 109 additions & 72 deletions src/Client/RequestHandler/Curl.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use CrowdSec\Common\Client\HttpMessage\AppSecRequest;
use CrowdSec\Common\Client\HttpMessage\Request;
use CrowdSec\Common\Client\HttpMessage\Response;
use CrowdSec\Common\Client\TimeoutException;
use CrowdSec\Common\Constants;

/**
Expand All @@ -22,6 +23,10 @@
*/
class Curl extends AbstractRequestHandler
{
/**
* @throws ClientException
* @throws TimeoutException
*/
public function handle(Request $request): Response
{
$handle = curl_init();
Expand All @@ -32,7 +37,12 @@ public function handle(Request $request): Response
$response = $this->exec($handle);

if (false === $response) {
throw new ClientException('Unexpected CURL call failure: ' . curl_error($handle), 500);
$errorCode = $this->errno($handle);
$errorMessage = $this->error($handle);
if (\CURLE_OPERATION_TIMEOUTED === $errorCode) {
throw new TimeoutException('CURL call timeout: ' . $errorMessage, 500);
}
throw new ClientException('Unexpected CURL call failure: ' . $errorMessage, 500);
}

$statusCode = $this->getResponseHttpCode($handle);
Expand All @@ -45,6 +55,22 @@ public function handle(Request $request): Response
return new Response((string) $response, $statusCode);
}

/**
* @codeCoverageIgnore
*/
protected function errno($handle): int
{
return curl_errno($handle);
}

/**
* @codeCoverageIgnore
*/
protected function error($handle): string
{
return curl_error($handle);
}

/**
* @codeCoverageIgnore
*
Expand All @@ -63,58 +89,49 @@ protected function getResponseHttpCode($handle)
return curl_getinfo($handle, \CURLINFO_HTTP_CODE);
}

private function handleTimeout(): array
/**
* Retrieve Curl options.
*
* @throws ClientException
*/
private function createOptions(Request $request): array
{
$result = [];
$timeout = $this->getConfig('api_timeout') ?? Constants::API_TIMEOUT;
/**
* To obtain an unlimited timeout, we don't pass the option (as it is the default behavior).
*
* @see https://curl.se/libcurl/c/CURLOPT_TIMEOUT.html
*/
if ($timeout > 0) {
$result[\CURLOPT_TIMEOUT] = $timeout;
$headers = $request->getValidatedHeaders();
$method = $request->getMethod();
$url = $request->getUri();
$parameters = $request->getParams();
$rawBody = $request instanceof AppSecRequest ? $request->getRawBody() : '';
$options = [
\CURLOPT_HEADER => false,
\CURLOPT_RETURNTRANSFER => true,
\CURLOPT_ENCODING => '',
];
if (isset($headers['User-Agent'])) {
$options[\CURLOPT_USERAGENT] = $headers['User-Agent'];
}
$connectTimeout = $this->getConfig('api_connect_timeout') ?? Constants::API_CONNECT_TIMEOUT;
if ($connectTimeout >= 0) {
/**
* 0 means infinite timeout (@see https://www.php.net/manual/en/function.curl-setopt.php.
*
* @see https://curl.se/libcurl/c/CURLOPT_CONNECTTIMEOUT.html
*/
$result[\CURLOPT_CONNECTTIMEOUT] = $connectTimeout;

$options[\CURLOPT_HTTPHEADER] = [];
foreach ($headers as $key => $values) {
foreach (\is_array($values) ? $values : [$values] as $value) {
$options[\CURLOPT_HTTPHEADER][] = sprintf('%s:%s', $key, $value);
}
}
// We need to keep keys indexes (array_merge not keeping indexes)
$options += $this->handleSSL($request);
$options += $this->handleTimeout($request);
$options += $this->handleMethod($method, $url, $parameters, $rawBody);

return $result;
return $options;
}

private function handleSSL(Request $request): array
private function getConnectTimeoutOption(Request $request): int
{
$result = [\CURLOPT_SSL_VERIFYPEER => false];
if ($request instanceof AppSecRequest) {
/**
* AppSec does not currently support TLS authentication.
*
* @see https://github.com/crowdsecurity/crowdsec/issues/3172
*/
return $result;
}

$authType = $this->getConfig('auth_type');
if ($authType && Constants::AUTH_TLS === $authType) {
$verifyPeer = $this->getConfig('tls_verify_peer') ?? true;
$result[\CURLOPT_SSL_VERIFYPEER] = $verifyPeer;
// The --cert option
$result[\CURLOPT_SSLCERT] = $this->getConfig('tls_cert_path') ?? '';
// The --key option
$result[\CURLOPT_SSLKEY] = $this->getConfig('tls_key_path') ?? '';
if ($verifyPeer) {
// The --cacert option
$result[\CURLOPT_CAINFO] = $this->getConfig('tls_ca_cert_path') ?? '';
}
}
return $request instanceof AppSecRequest ? \CURLOPT_CONNECTTIMEOUT_MS : \CURLOPT_CONNECTTIMEOUT;
}

return $result;
private function getTimeoutOption(Request $request): int
{
return $request instanceof AppSecRequest ? \CURLOPT_TIMEOUT_MS : \CURLOPT_TIMEOUT;
}

private function handleMethod(string $method, string $url, array $parameters = [], string $rawBody = ''): array
Expand Down Expand Up @@ -142,38 +159,58 @@ private function handleMethod(string $method, string $url, array $parameters = [
return $result;
}

/**
* Retrieve Curl options.
*
* @throws ClientException
*/
private function createOptions(Request $request): array
private function handleSSL(Request $request): array
{
$headers = $request->getValidatedHeaders();
$method = $request->getMethod();
$url = $request->getUri();
$parameters = $request->getParams();
$rawBody = $request instanceof AppSecRequest ? $request->getRawBody() : '';
$options = [
\CURLOPT_HEADER => false,
\CURLOPT_RETURNTRANSFER => true,
\CURLOPT_ENCODING => '',
];
if (isset($headers['User-Agent'])) {
$options[\CURLOPT_USERAGENT] = $headers['User-Agent'];
$result = [\CURLOPT_SSL_VERIFYPEER => false];
if ($request instanceof AppSecRequest) {
/**
* AppSec does not currently support TLS authentication.
*
* @see https://github.com/crowdsecurity/crowdsec/issues/3172
*/
return $result;
}

$options[\CURLOPT_HTTPHEADER] = [];
foreach ($headers as $key => $values) {
foreach (\is_array($values) ? $values : [$values] as $value) {
$options[\CURLOPT_HTTPHEADER][] = sprintf('%s:%s', $key, $value);
$authType = $this->getConfig('auth_type');
if ($authType && Constants::AUTH_TLS === $authType) {
$verifyPeer = $this->getConfig('tls_verify_peer') ?? true;
$result[\CURLOPT_SSL_VERIFYPEER] = $verifyPeer;
// The --cert option
$result[\CURLOPT_SSLCERT] = $this->getConfig('tls_cert_path') ?? '';
// The --key option
$result[\CURLOPT_SSLKEY] = $this->getConfig('tls_key_path') ?? '';
if ($verifyPeer) {
// The --cacert option
$result[\CURLOPT_CAINFO] = $this->getConfig('tls_ca_cert_path') ?? '';
}
}
// We need to keep keys indexes (array_merge not keeping indexes)
$options += $this->handleSSL($request);
$options += $this->handleTimeout();
$options += $this->handleMethod($method, $url, $parameters, $rawBody);

return $options;
return $result;
}

private function handleTimeout(Request $request): array
{
$result = [];
$timeout = $this->getTimeout($request);
/**
* To obtain an unlimited timeout (with non-positive value),
* we don't pass the option (as unlimited timeout is the default behavior).
*
* @see https://curl.se/libcurl/c/CURLOPT_TIMEOUT.html
*/
if ($timeout > 0) {
$result[$this->getTimeoutOption($request)] = $timeout;
}
$connectTimeout = $this->getConnectTimeout($request);
if ($connectTimeout >= 0) {
/**
* 0 means infinite timeout (@see https://www.php.net/manual/en/function.curl-setopt.php.
*
* @see https://curl.se/libcurl/c/CURLOPT_CONNECTTIMEOUT.html
*/
$result[$this->getConnectTimeoutOption($request)] = $connectTimeout;
}

return $result;
}
}
Loading

0 comments on commit ccbdfe5

Please sign in to comment.