Skip to content

Commit 85f25f5

Browse files
feat(app sec): Add AppSecRequest class (#13)
1 parent fe3afd8 commit 85f25f5

File tree

11 files changed

+385
-60
lines changed

11 files changed

+385
-60
lines changed

src/Client/AbstractClient.php

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
namespace CrowdSec\Common\Client;
66

7+
use CrowdSec\Common\Client\HttpMessage\AppSecRequest;
78
use CrowdSec\Common\Client\HttpMessage\Request;
89
use CrowdSec\Common\Client\HttpMessage\Response;
910
use CrowdSec\Common\Client\RequestHandler\Curl;
1011
use CrowdSec\Common\Client\RequestHandler\RequestHandlerInterface;
11-
use CrowdSec\Common\Constants;
1212
use Monolog\Handler\NullHandler;
1313
use Monolog\Logger;
1414
use Psr\Log\LoggerInterface;
@@ -33,6 +33,10 @@ abstract class AbstractClient
3333
* @var string[]
3434
*/
3535
private $allowedMethods = ['POST', 'GET', 'DELETE'];
36+
/**
37+
* @var string[]
38+
*/
39+
private $allowedAppSecMethods = ['POST', 'GET'];
3640
/**
3741
* @var LoggerInterface
3842
*/
@@ -53,7 +57,7 @@ abstract class AbstractClient
5357
public function __construct(
5458
array $configs,
5559
?RequestHandlerInterface $requestHandler = null,
56-
?LoggerInterface $logger = null
60+
?LoggerInterface $logger = null,
5761
) {
5862
$this->configs = $configs;
5963
$this->requestHandler = ($requestHandler) ?: new Curl($this->configs);
@@ -88,11 +92,14 @@ public function getRequestHandler(): RequestHandlerInterface
8892
return $this->requestHandler;
8993
}
9094

91-
public function getUrl(string $type = Constants::TYPE_REST): string
95+
public function getUrl(): string
9296
{
93-
$url = Constants::TYPE_APPSEC === $type ? $this->appSecUrl : $this->url;
97+
return rtrim($this->url, '/') . '/';
98+
}
9499

95-
return rtrim($url, '/') . '/';
100+
public function getAppSecUrl(): string
101+
{
102+
return rtrim($this->appSecUrl, '/') . '/';
96103
}
97104

98105
/**
@@ -105,7 +112,6 @@ protected function request(
105112
string $endpoint,
106113
array $parameters = [],
107114
array $headers = [],
108-
string $type = Constants::TYPE_REST
109115
): array {
110116
$method = strtoupper($method);
111117
if (!in_array($method, $this->allowedMethods)) {
@@ -115,7 +121,31 @@ protected function request(
115121
}
116122

117123
$response = $this->sendRequest(
118-
new Request($this->getFullUrl($endpoint, $type), $method, $headers, $parameters)
124+
new Request($this->getFullUrl($endpoint), $method, $headers, $parameters)
125+
);
126+
127+
return $this->formatResponseBody($response);
128+
}
129+
130+
/**
131+
* Performs an HTTP request (POST, GET) to AppSec and returns its response body as an array.
132+
*
133+
* @throws ClientException
134+
*/
135+
protected function requestAppSec(
136+
string $method,
137+
array $headers = [],
138+
string $rawBody = '',
139+
): array {
140+
$method = strtoupper($method);
141+
if (!in_array($method, $this->allowedAppSecMethods)) {
142+
$message = "Method ($method) is not allowed.";
143+
$this->logger->error($message, ['type' => 'CLIENT_APPSEC_REQUEST']);
144+
throw new ClientException($message);
145+
}
146+
147+
$response = $this->sendRequest(
148+
new AppSecRequest($this->getAppSecUrl(), $method, $headers, $rawBody)
119149
);
120150

121151
return $this->formatResponseBody($response);
@@ -160,8 +190,8 @@ private function formatResponseBody(Response $response): array
160190
return $decoded;
161191
}
162192

163-
private function getFullUrl(string $endpoint, string $type = Constants::TYPE_REST): string
193+
private function getFullUrl(string $endpoint): string
164194
{
165-
return $this->getUrl($type) . ltrim($endpoint, '/');
195+
return $this->getUrl() . ltrim($endpoint, '/');
166196
}
167197
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CrowdSec\Common\Client\HttpMessage;
6+
7+
/**
8+
* Request that will be sent to CrowdSec AppSec component.
9+
*
10+
* @author CrowdSec team
11+
*
12+
* @see https://crowdsec.net CrowdSec Official Website
13+
*
14+
* @copyright Copyright (c) 2024+ CrowdSec
15+
* @license MIT License
16+
*/
17+
class AppSecRequest extends Request
18+
{
19+
/**
20+
* @var array
21+
*/
22+
protected $headers = [];
23+
/**
24+
* @var string
25+
*/
26+
private $rawBody = '';
27+
28+
public function __construct(
29+
string $uri,
30+
string $method,
31+
array $headers = [],
32+
string $rawBody = '',
33+
) {
34+
$this->rawBody = $rawBody;
35+
parent::__construct($uri, $method, $headers);
36+
}
37+
38+
public function getRawBody(): string
39+
{
40+
return $this->rawBody;
41+
}
42+
}

src/Client/HttpMessage/Request.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
namespace CrowdSec\Common\Client\HttpMessage;
66

7-
use CrowdSec\Common\Constants;
8-
97
/**
108
* Request that will be sent to CrowdSec.
119
*
@@ -43,11 +41,10 @@ public function __construct(
4341
string $method,
4442
array $headers = [],
4543
array $parameters = [],
46-
string $type = Constants::TYPE_REST
4744
) {
4845
$this->uri = $uri;
4946
$this->method = $method;
50-
$this->headers = Constants::TYPE_REST === $type ? array_merge($this->headers, $headers) : $headers;
47+
$this->headers = array_merge($this->headers, $headers);
5148
$this->parameters = $parameters;
5249
}
5350

src/Client/RequestHandler/Curl.php

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace CrowdSec\Common\Client\RequestHandler;
66

77
use CrowdSec\Common\Client\ClientException;
8+
use CrowdSec\Common\Client\HttpMessage\AppSecRequest;
89
use CrowdSec\Common\Client\HttpMessage\Request;
910
use CrowdSec\Common\Client\HttpMessage\Response;
1011
use CrowdSec\Common\Constants;
@@ -62,22 +63,9 @@ protected function getResponseHttpCode($handle)
6263
return curl_getinfo($handle, \CURLINFO_HTTP_CODE);
6364
}
6465

65-
private function handleConfigs(): array
66+
private function handleTimeout(): array
6667
{
67-
$result = [\CURLOPT_SSL_VERIFYPEER => false];
68-
$authType = $this->getConfig('auth_type');
69-
if ($authType && Constants::AUTH_TLS === $authType) {
70-
$verifyPeer = $this->getConfig('tls_verify_peer') ?? true;
71-
$result[\CURLOPT_SSL_VERIFYPEER] = $verifyPeer;
72-
// The --cert option
73-
$result[\CURLOPT_SSLCERT] = $this->getConfig('tls_cert_path') ?? '';
74-
// The --key option
75-
$result[\CURLOPT_SSLKEY] = $this->getConfig('tls_key_path') ?? '';
76-
if ($verifyPeer) {
77-
// The --cacert option
78-
$result[\CURLOPT_CAINFO] = $this->getConfig('tls_ca_cert_path') ?? '';
79-
}
80-
}
68+
$result = [];
8169
$timeout = $this->getConfig('api_timeout') ?? Constants::API_TIMEOUT;
8270
/**
8371
* To obtain an unlimited timeout, we don't pass the option (as it is the default behavior).
@@ -100,13 +88,38 @@ private function handleConfigs(): array
10088
return $result;
10189
}
10290

103-
private function handleMethod(string $method, string $url, array $parameters = []): array
91+
private function handleSSL(Request $request): array
92+
{
93+
$result = [\CURLOPT_SSL_VERIFYPEER => false];
94+
if ($request instanceof AppSecRequest) {
95+
// AppSec does not require SSL verification
96+
return $result;
97+
}
98+
99+
$authType = $this->getConfig('auth_type');
100+
if ($authType && Constants::AUTH_TLS === $authType) {
101+
$verifyPeer = $this->getConfig('tls_verify_peer') ?? true;
102+
$result[\CURLOPT_SSL_VERIFYPEER] = $verifyPeer;
103+
// The --cert option
104+
$result[\CURLOPT_SSLCERT] = $this->getConfig('tls_cert_path') ?? '';
105+
// The --key option
106+
$result[\CURLOPT_SSLKEY] = $this->getConfig('tls_key_path') ?? '';
107+
if ($verifyPeer) {
108+
// The --cacert option
109+
$result[\CURLOPT_CAINFO] = $this->getConfig('tls_ca_cert_path') ?? '';
110+
}
111+
}
112+
113+
return $result;
114+
}
115+
116+
private function handleMethod(string $method, string $url, array $parameters = [], $rawBody = ''): array
104117
{
105118
$result = [];
106119
if ('POST' === strtoupper($method)) {
107120
$result[\CURLOPT_POST] = true;
108121
$result[\CURLOPT_CUSTOMREQUEST] = 'POST';
109-
$result[\CURLOPT_POSTFIELDS] = json_encode($parameters);
122+
$result[\CURLOPT_POSTFIELDS] = $rawBody ?: json_encode($parameters);
110123
} elseif ('GET' === strtoupper($method)) {
111124
$result[\CURLOPT_POST] = false;
112125
$result[\CURLOPT_CUSTOMREQUEST] = 'GET';
@@ -132,19 +145,27 @@ private function handleMethod(string $method, string $url, array $parameters = [
132145
*/
133146
private function createOptions(Request $request): array
134147
{
148+
$isAppSec = $request instanceof AppSecRequest;
135149
$headers = $request->getHeaders();
136150
$method = $request->getMethod();
137151
$url = $request->getUri();
138152
$parameters = $request->getParams();
139-
if (!isset($headers['User-Agent'])) {
153+
if (!isset($headers['User-Agent']) && !$isAppSec) {
140154
throw new ClientException('User agent is required', 400);
141155
}
156+
$rawBody = '';
157+
if ($isAppSec) {
158+
/** @var AppSecRequest $request */
159+
$rawBody = $request->getRawBody();
160+
}
142161
$options = [
143162
\CURLOPT_HEADER => false,
144163
\CURLOPT_RETURNTRANSFER => true,
145-
\CURLOPT_USERAGENT => $headers['User-Agent'],
146164
\CURLOPT_ENCODING => '',
147165
];
166+
if (isset($headers['User-Agent'])) {
167+
$options[\CURLOPT_USERAGENT] = $headers['User-Agent'];
168+
}
148169

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

159181
return $options;
160182
}

src/Client/RequestHandler/FileGetContents.php

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace CrowdSec\Common\Client\RequestHandler;
66

77
use CrowdSec\Common\Client\ClientException;
8+
use CrowdSec\Common\Client\HttpMessage\AppSecRequest;
89
use CrowdSec\Common\Client\HttpMessage\Request;
910
use CrowdSec\Common\Client\HttpMessage\Response;
1011
use CrowdSec\Common\Constants;
@@ -95,9 +96,15 @@ protected function convertHeadersToString(array $headers): string
9596
private function createContextConfig(Request $request): array
9697
{
9798
$headers = $request->getHeaders();
98-
if (!isset($headers['User-Agent'])) {
99+
$isAppSec = $request instanceof AppSecRequest;
100+
if (!isset($headers['User-Agent']) && !$isAppSec) {
99101
throw new ClientException('User agent is required', 400);
100102
}
103+
$rawBody = '';
104+
if ($isAppSec) {
105+
/** @var AppSecRequest $request */
106+
$rawBody = $request->getRawBody();
107+
}
101108
$header = $this->convertHeadersToString($headers);
102109
$method = $request->getMethod();
103110
$timeout = $this->getConfig('api_timeout');
@@ -112,24 +119,36 @@ private function createContextConfig(Request $request): array
112119
],
113120
];
114121

115-
$config['ssl'] = ['verify_peer' => false];
122+
$config += $this->handleSSL($request);
123+
124+
if ('POST' === strtoupper($method)) {
125+
$config['http']['content'] =
126+
$isAppSec ? $rawBody : json_encode($request->getParams());
127+
}
128+
129+
return $config;
130+
}
131+
132+
private function handleSSL(Request $request): array
133+
{
134+
$result = ['ssl' => ['verify_peer' => false]];
135+
if ($request instanceof AppSecRequest) {
136+
// AppSec does not require SSL verification
137+
return $result;
138+
}
116139
$authType = $this->getConfig('auth_type');
117140
if ($authType && Constants::AUTH_TLS === $authType) {
118141
$verifyPeer = $this->getConfig('tls_verify_peer') ?? true;
119-
$config['ssl'] = [
142+
$result['ssl'] = [
120143
'verify_peer' => $verifyPeer,
121144
'local_cert' => $this->getConfig('tls_cert_path') ?? '',
122145
'local_pk' => $this->getConfig('tls_key_path') ?? '',
123146
];
124147
if ($verifyPeer) {
125-
$config['ssl']['cafile'] = $this->getConfig('tls_ca_cert_path') ?? '';
148+
$result['ssl']['cafile'] = $this->getConfig('tls_ca_cert_path') ?? '';
126149
}
127150
}
128151

129-
if ('POST' === strtoupper($method)) {
130-
$config['http']['content'] = json_encode($request->getParams());
131-
}
132-
133-
return $config;
152+
return $result;
134153
}
135154
}

src/Constants.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ class Constants
5656
* @var string The LISTS origin for decisions
5757
*/
5858
public const ORIGIN_LISTS = 'lists';
59+
/**
60+
* @var string The CrowdSec App Sec raw body param
61+
*/
62+
public const APPSEC_RAW_BODY_PARAM = 'app_sec_raw_body';
5963
/**
6064
* @var string The ban remediation
6165
*/

tests/MockedData.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,9 @@ class MockedData
3232

3333
public const UNAUTHORIZED = <<<EOT
3434
{"message":"Unauthorized"}
35+
EOT;
36+
37+
public const APPSEC_ALLOWED = <<<EOT
38+
{"action":"allow"}
3539
EOT;
3640
}

0 commit comments

Comments
 (0)