Skip to content

Commit

Permalink
feat(app sec): Add AppSecRequest class (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
julienloizelet authored Sep 5, 2024
1 parent fe3afd8 commit 85f25f5
Show file tree
Hide file tree
Showing 11 changed files with 385 additions and 60 deletions.
48 changes: 39 additions & 9 deletions src/Client/AbstractClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

namespace CrowdSec\Common\Client;

use CrowdSec\Common\Client\HttpMessage\AppSecRequest;
use CrowdSec\Common\Client\HttpMessage\Request;
use CrowdSec\Common\Client\HttpMessage\Response;
use CrowdSec\Common\Client\RequestHandler\Curl;
use CrowdSec\Common\Client\RequestHandler\RequestHandlerInterface;
use CrowdSec\Common\Constants;
use Monolog\Handler\NullHandler;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
Expand All @@ -33,6 +33,10 @@ abstract class AbstractClient
* @var string[]
*/
private $allowedMethods = ['POST', 'GET', 'DELETE'];
/**
* @var string[]
*/
private $allowedAppSecMethods = ['POST', 'GET'];
/**
* @var LoggerInterface
*/
Expand All @@ -53,7 +57,7 @@ abstract class AbstractClient
public function __construct(
array $configs,
?RequestHandlerInterface $requestHandler = null,
?LoggerInterface $logger = null
?LoggerInterface $logger = null,
) {
$this->configs = $configs;
$this->requestHandler = ($requestHandler) ?: new Curl($this->configs);
Expand Down Expand Up @@ -88,11 +92,14 @@ public function getRequestHandler(): RequestHandlerInterface
return $this->requestHandler;
}

public function getUrl(string $type = Constants::TYPE_REST): string
public function getUrl(): string
{
$url = Constants::TYPE_APPSEC === $type ? $this->appSecUrl : $this->url;
return rtrim($this->url, '/') . '/';
}

return rtrim($url, '/') . '/';
public function getAppSecUrl(): string
{
return rtrim($this->appSecUrl, '/') . '/';
}

/**
Expand All @@ -105,7 +112,6 @@ protected function request(
string $endpoint,
array $parameters = [],
array $headers = [],
string $type = Constants::TYPE_REST
): array {
$method = strtoupper($method);
if (!in_array($method, $this->allowedMethods)) {
Expand All @@ -115,7 +121,31 @@ protected function request(
}

$response = $this->sendRequest(
new Request($this->getFullUrl($endpoint, $type), $method, $headers, $parameters)
new Request($this->getFullUrl($endpoint), $method, $headers, $parameters)
);

return $this->formatResponseBody($response);
}

/**
* Performs an HTTP request (POST, GET) to AppSec and returns its response body as an array.
*
* @throws ClientException
*/
protected function requestAppSec(
string $method,
array $headers = [],
string $rawBody = '',
): array {
$method = strtoupper($method);
if (!in_array($method, $this->allowedAppSecMethods)) {
$message = "Method ($method) is not allowed.";
$this->logger->error($message, ['type' => 'CLIENT_APPSEC_REQUEST']);
throw new ClientException($message);
}

$response = $this->sendRequest(
new AppSecRequest($this->getAppSecUrl(), $method, $headers, $rawBody)
);

return $this->formatResponseBody($response);
Expand Down Expand Up @@ -160,8 +190,8 @@ private function formatResponseBody(Response $response): array
return $decoded;
}

private function getFullUrl(string $endpoint, string $type = Constants::TYPE_REST): string
private function getFullUrl(string $endpoint): string
{
return $this->getUrl($type) . ltrim($endpoint, '/');
return $this->getUrl() . ltrim($endpoint, '/');
}
}
42 changes: 42 additions & 0 deletions src/Client/HttpMessage/AppSecRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace CrowdSec\Common\Client\HttpMessage;

/**
* Request that will be sent to CrowdSec AppSec component.
*
* @author CrowdSec team
*
* @see https://crowdsec.net CrowdSec Official Website
*
* @copyright Copyright (c) 2024+ CrowdSec
* @license MIT License
*/
class AppSecRequest extends Request
{
/**
* @var array
*/
protected $headers = [];
/**
* @var string
*/
private $rawBody = '';

public function __construct(
string $uri,
string $method,
array $headers = [],
string $rawBody = '',
) {
$this->rawBody = $rawBody;
parent::__construct($uri, $method, $headers);
}

public function getRawBody(): string
{
return $this->rawBody;
}
}
5 changes: 1 addition & 4 deletions src/Client/HttpMessage/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace CrowdSec\Common\Client\HttpMessage;

use CrowdSec\Common\Constants;

/**
* Request that will be sent to CrowdSec.
*
Expand Down Expand Up @@ -43,11 +41,10 @@ public function __construct(
string $method,
array $headers = [],
array $parameters = [],
string $type = Constants::TYPE_REST
) {
$this->uri = $uri;
$this->method = $method;
$this->headers = Constants::TYPE_REST === $type ? array_merge($this->headers, $headers) : $headers;
$this->headers = array_merge($this->headers, $headers);
$this->parameters = $parameters;
}

Expand Down
64 changes: 43 additions & 21 deletions src/Client/RequestHandler/Curl.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace CrowdSec\Common\Client\RequestHandler;

use CrowdSec\Common\Client\ClientException;
use CrowdSec\Common\Client\HttpMessage\AppSecRequest;
use CrowdSec\Common\Client\HttpMessage\Request;
use CrowdSec\Common\Client\HttpMessage\Response;
use CrowdSec\Common\Constants;
Expand Down Expand Up @@ -62,22 +63,9 @@ protected function getResponseHttpCode($handle)
return curl_getinfo($handle, \CURLINFO_HTTP_CODE);
}

private function handleConfigs(): array
private function handleTimeout(): array
{
$result = [\CURLOPT_SSL_VERIFYPEER => false];
$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') ?? '';
}
}
$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).
Expand All @@ -100,13 +88,38 @@ private function handleConfigs(): array
return $result;
}

private function handleMethod(string $method, string $url, array $parameters = []): array
private function handleSSL(Request $request): array
{
$result = [\CURLOPT_SSL_VERIFYPEER => false];
if ($request instanceof AppSecRequest) {
// AppSec does not require SSL verification
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 $result;
}

private function handleMethod(string $method, string $url, array $parameters = [], $rawBody = ''): array
{
$result = [];
if ('POST' === strtoupper($method)) {
$result[\CURLOPT_POST] = true;
$result[\CURLOPT_CUSTOMREQUEST] = 'POST';
$result[\CURLOPT_POSTFIELDS] = json_encode($parameters);
$result[\CURLOPT_POSTFIELDS] = $rawBody ?: json_encode($parameters);
} elseif ('GET' === strtoupper($method)) {
$result[\CURLOPT_POST] = false;
$result[\CURLOPT_CUSTOMREQUEST] = 'GET';
Expand All @@ -132,19 +145,27 @@ private function handleMethod(string $method, string $url, array $parameters = [
*/
private function createOptions(Request $request): array
{
$isAppSec = $request instanceof AppSecRequest;
$headers = $request->getHeaders();
$method = $request->getMethod();
$url = $request->getUri();
$parameters = $request->getParams();
if (!isset($headers['User-Agent'])) {
if (!isset($headers['User-Agent']) && !$isAppSec) {
throw new ClientException('User agent is required', 400);
}
$rawBody = '';
if ($isAppSec) {
/** @var AppSecRequest $request */
$rawBody = $request->getRawBody();
}
$options = [
\CURLOPT_HEADER => false,
\CURLOPT_RETURNTRANSFER => true,
\CURLOPT_USERAGENT => $headers['User-Agent'],
\CURLOPT_ENCODING => '',
];
if (isset($headers['User-Agent'])) {
$options[\CURLOPT_USERAGENT] = $headers['User-Agent'];
}

$options[\CURLOPT_HTTPHEADER] = [];
foreach ($headers as $key => $values) {
Expand All @@ -153,8 +174,9 @@ private function createOptions(Request $request): array
}
}
// We need to keep keys indexes (array_merge not keeping indexes)
$options += $this->handleConfigs();
$options += $this->handleMethod($method, $url, $parameters);
$options += $this->handleSSL($request);
$options += $this->handleTimeout();
$options += $this->handleMethod($method, $url, $parameters, $rawBody);

return $options;
}
Expand Down
37 changes: 28 additions & 9 deletions src/Client/RequestHandler/FileGetContents.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace CrowdSec\Common\Client\RequestHandler;

use CrowdSec\Common\Client\ClientException;
use CrowdSec\Common\Client\HttpMessage\AppSecRequest;
use CrowdSec\Common\Client\HttpMessage\Request;
use CrowdSec\Common\Client\HttpMessage\Response;
use CrowdSec\Common\Constants;
Expand Down Expand Up @@ -95,9 +96,15 @@ protected function convertHeadersToString(array $headers): string
private function createContextConfig(Request $request): array
{
$headers = $request->getHeaders();
if (!isset($headers['User-Agent'])) {
$isAppSec = $request instanceof AppSecRequest;
if (!isset($headers['User-Agent']) && !$isAppSec) {
throw new ClientException('User agent is required', 400);
}
$rawBody = '';
if ($isAppSec) {
/** @var AppSecRequest $request */
$rawBody = $request->getRawBody();
}
$header = $this->convertHeadersToString($headers);
$method = $request->getMethod();
$timeout = $this->getConfig('api_timeout');
Expand All @@ -112,24 +119,36 @@ private function createContextConfig(Request $request): array
],
];

$config['ssl'] = ['verify_peer' => false];
$config += $this->handleSSL($request);

if ('POST' === strtoupper($method)) {
$config['http']['content'] =
$isAppSec ? $rawBody : json_encode($request->getParams());
}

return $config;
}

private function handleSSL(Request $request): array
{
$result = ['ssl' => ['verify_peer' => false]];
if ($request instanceof AppSecRequest) {
// AppSec does not require SSL verification
return $result;
}
$authType = $this->getConfig('auth_type');
if ($authType && Constants::AUTH_TLS === $authType) {
$verifyPeer = $this->getConfig('tls_verify_peer') ?? true;
$config['ssl'] = [
$result['ssl'] = [
'verify_peer' => $verifyPeer,
'local_cert' => $this->getConfig('tls_cert_path') ?? '',
'local_pk' => $this->getConfig('tls_key_path') ?? '',
];
if ($verifyPeer) {
$config['ssl']['cafile'] = $this->getConfig('tls_ca_cert_path') ?? '';
$result['ssl']['cafile'] = $this->getConfig('tls_ca_cert_path') ?? '';
}
}

if ('POST' === strtoupper($method)) {
$config['http']['content'] = json_encode($request->getParams());
}

return $config;
return $result;
}
}
4 changes: 4 additions & 0 deletions src/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ class Constants
* @var string The LISTS origin for decisions
*/
public const ORIGIN_LISTS = 'lists';
/**
* @var string The CrowdSec App Sec raw body param
*/
public const APPSEC_RAW_BODY_PARAM = 'app_sec_raw_body';
/**
* @var string The ban remediation
*/
Expand Down
4 changes: 4 additions & 0 deletions tests/MockedData.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,9 @@ class MockedData

public const UNAUTHORIZED = <<<EOT
{"message":"Unauthorized"}
EOT;

public const APPSEC_ALLOWED = <<<EOT
{"action":"allow"}
EOT;
}
Loading

0 comments on commit 85f25f5

Please sign in to comment.