Skip to content

Commit ceb92d1

Browse files
authored
Merge pull request #178 from dotkernel/issue-146
Issue #146: Implemented domain/ip whitelist based error reporting.
2 parents fae0b4e + 1508e3a commit ceb92d1

File tree

6 files changed

+192
-24
lines changed

6 files changed

+192
-24
lines changed

config/autoload/error-handling.global.php

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use Api\App\Service\ErrorReportServiceInterface;
34
use Laminas\Log\Logger;
45
use Laminas\Log\Formatter\Json;
56

@@ -36,7 +37,40 @@
3637
],
3738
],
3839
],
39-
'error-report' => [
40-
'filePath' => sprintf('%s/../../log/error-report-endpoint-log.log', __DIR__)
40+
41+
/**
42+
* Messages will be stored only if all the below conditions are met:
43+
* enabled = true
44+
* at least one of the domain_whitelist OR ip_whitelist checks passes
45+
*/
46+
ErrorReportServiceInterface::class => [
47+
/**
48+
* Usage:
49+
* If enabled is set to true, messages will be stored and an info message is returned.
50+
* If enabled is set to false, no message is stored and an error message is returned.
51+
*/
52+
'enabled' => true,
53+
54+
/**
55+
* Path to the file where messages will be stored.
56+
* If it does not exist, it will be created.
57+
*/
58+
'path' => __DIR__ . '/../../log/error-report-endpoint-log.log',
59+
60+
/**
61+
* Usage:
62+
* 1. Missing/empty domain_whitelist => no domain is allowed to store messages.
63+
* 2. Add '*' to allow any domain to store messages.
64+
* 3. If you want to whitelist only specific domains, add them to domain_whitelist.
65+
*/
66+
'domain_whitelist' => [],
67+
68+
/**
69+
* Usage:
70+
* 1. Missing/empty ip_whitelist => no IP address is allowed to store messages.
71+
* 2. Add '*' to allow any IP address to store messages.
72+
* 3. If you want to whitelist only specific IP addresses, add them to ip_whitelist.
73+
*/
74+
'ip_whitelist' => [],
4175
]
4276
];

src/App/src/ConfigProvider.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use Api\App\Middleware\AuthenticationMiddleware;
1212
use Api\App\Middleware\AuthorizationMiddleware;
1313
use Api\App\Middleware\ErrorResponseMiddleware;
14+
use Api\App\Service\ErrorReportService;
15+
use Api\App\Service\ErrorReportServiceInterface;
1416
use Doctrine\ORM\EntityManager;
1517
use Doctrine\ORM\EntityManagerInterface;
1618
use Dot\AnnotatedServices\Factory\AbstractAnnotatedFactory;
@@ -74,14 +76,16 @@ public function getDependencies(): array
7476
TwigExtension::class => TwigExtensionFactory::class,
7577
TwigRenderer::class => TwigRendererFactory::class,
7678
ErrorResponseMiddleware::class => AnnotatedServiceFactory::class,
77-
RouteListCommand::class => RouteListCommandFactory::class
79+
RouteListCommand::class => RouteListCommandFactory::class,
80+
ErrorReportService::class => AnnotatedServiceFactory::class,
7881
],
7982
'aliases' => [
8083
Authentication\AuthenticationInterface::class => Authentication\OAuth2\OAuth2Adapter::class,
8184
MailService::class => 'dot-mail.service.default',
8285
EntityManager::class => 'doctrine.entity_manager.orm_default',
8386
EntityManagerInterface::class => 'doctrine.entity_manager.orm_default',
8487
TemplateRendererInterface::class => TwigRenderer::class,
88+
ErrorReportServiceInterface::class => ErrorReportService::class,
8589
]
8690
];
8791
}

src/App/src/Log/Handler/ErrorReportHandler.php

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@
66

77
use Api\App\Handler\DefaultHandler;
88
use Api\App\Message;
9+
use Api\App\Service\ErrorReportServiceInterface;
910
use Dot\AnnotatedServices\Annotation\Inject;
1011
use Dot\AnnotatedServices\Annotation\Service;
1112
use Mezzio\Hal\HalResponseFactory;
1213
use Mezzio\Hal\ResourceGenerator;
1314
use Psr\Http\Message\ResponseInterface;
1415
use Psr\Http\Message\ServerRequestInterface;
15-
use Symfony\Component\Filesystem\Filesystem;
16-
17-
use function date;
18-
use function sprintf;
16+
use Throwable;
1917

2018
/**
2119
* Class ErrorReportHandler
@@ -25,43 +23,47 @@
2523
*/
2624
class ErrorReportHandler extends DefaultHandler
2725
{
28-
protected array $config;
26+
private ErrorReportServiceInterface $errorReportService;
2927

3028
/**
3129
* ErrorReportHandler constructor.
3230
* @param HalResponseFactory $halResponseFactory
3331
* @param ResourceGenerator $resourceGenerator
34-
* @param array $config
32+
* @param ErrorReportServiceInterface $errorReportService
3533
*
36-
* @Inject({HalResponseFactory::class, ResourceGenerator::class, "config.error-report"})
34+
* @Inject({
35+
* HalResponseFactory::class,
36+
* ResourceGenerator::class,
37+
* ErrorReportServiceInterface::class
38+
* })
3739
*/
3840
public function __construct(
3941
HalResponseFactory $halResponseFactory,
4042
ResourceGenerator $resourceGenerator,
41-
array $config
43+
ErrorReportServiceInterface $errorReportService
4244
) {
4345
parent::__construct($halResponseFactory, $resourceGenerator);
4446

45-
$this->config = $config;
47+
$this->errorReportService = $errorReportService;
4648
}
4749

4850
/**
4951
* @param ServerRequestInterface $request
5052
* @return ResponseInterface
53+
* @throws Throwable
5154
*/
5255
public function post(ServerRequestInterface $request): ResponseInterface
5356
{
54-
$data = $request->getParsedBody();
55-
if (empty($data['message'])) {
56-
return $this->errorResponse(Message::ERROR_REPORT_KO);
57+
try {
58+
$this->errorReportService
59+
->checkStatus()
60+
->checkRequest($request)
61+
->appendMessage(
62+
$request->getParsedBody()['message'] ?? ''
63+
);
64+
return $this->infoResponse(Message::ERROR_REPORT_OK);
65+
} catch (Throwable $exception) {
66+
return $this->errorResponse($exception->getMessage());
5767
}
58-
59-
$writer = new Filesystem();
60-
$writer->appendToFile(
61-
$this->config['filePath'],
62-
sprintf('%s => %s' . PHP_EOL, date('Y-m-d H:i:s'), $data['message'])
63-
);
64-
65-
return $this->infoResponse(Message::ERROR_REPORT_OK);
6668
}
6769
}

src/App/src/Message.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ class Message
2020
public const DUPLICATE_EMAIL = 'An account with this email address already exists.';
2121
public const DUPLICATE_IDENTITY = 'An account with this identity already exists.';
2222
public const ERROR_REPORT_OK = 'Error report successfully saved.';
23-
public const ERROR_REPORT_KO = 'Message is empty and nothing was saved.';
23+
public const ERROR_REPORT_NOT_ALLOWED = 'This host is not allowed to report logs.';
24+
public const ERROR_REPORT_NOT_ENABLED = 'Remote error reporting is not enabled.';
2425
public const INVALID_ACTIVATION_CODE = 'Invalid activation code.';
2526
public const INVALID_CLIENT_ID = 'Invalid client_id.';
2627
public const INVALID_EMAIL = 'Invalid email.';
@@ -31,6 +32,7 @@ class Message
3132
public const MAIL_SENT_RESET_PASSWORD = 'If the provided email identifies an account in our system, ' .
3233
'you will receive an email with further instructions on resetting your account\'s password.';
3334
public const MAIL_SENT_USER_ACTIVATION = 'User activation mail has been successfully sent to \'%s\'';
35+
public const MISSING_CONFIG = 'Missing configuration value: \'%s\'';
3436
public const MISSING_PARAMETER = 'Missing parameter: \'%s\'';
3537
public const NOT_FOUND_BY_UUID = 'Unable to find %s identified by uuid: %s';
3638
public const RESET_PASSWORD_EXPIRED = 'Password reset request for hash: \'%s\' is invalid (expired).';
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Api\App\Service;
6+
7+
use Api\App\Message;
8+
use Dot\AnnotatedServices\Annotation\Inject;
9+
use Exception;
10+
use Psr\Http\Message\ServerRequestInterface;
11+
use Symfony\Component\Filesystem\Filesystem;
12+
13+
class ErrorReportService implements ErrorReportServiceInterface
14+
{
15+
private FileSystem $fileSystem;
16+
protected array $config;
17+
18+
/**
19+
* @param array $config
20+
*
21+
* @Inject({
22+
* "config"
23+
* })
24+
*/
25+
public function __construct(array $config)
26+
{
27+
$this->fileSystem = new Filesystem();
28+
$this->config = $config[ErrorReportServiceInterface::class] ?? [];
29+
}
30+
31+
/**
32+
* @throws Exception
33+
*/
34+
public function appendMessage(string $message): void
35+
{
36+
if (empty($this->config['path'])) {
37+
throw new Exception(
38+
sprintf(
39+
Message::MISSING_CONFIG,
40+
sprintf('config.%s.path', ErrorReportServiceInterface::class)
41+
)
42+
);
43+
}
44+
45+
$this->fileSystem->appendToFile(
46+
$this->config['path'],
47+
sprintf('%s => %s' . PHP_EOL, date('Y-m-d H:i:s'), $message)
48+
);
49+
}
50+
51+
/**
52+
* @throws Exception
53+
*/
54+
public function checkStatus(): self
55+
{
56+
if (empty($this->config['enabled'])) {
57+
throw new Exception(Message::ERROR_REPORT_NOT_ENABLED);
58+
}
59+
60+
return $this;
61+
}
62+
63+
/**
64+
* @throws Exception
65+
*/
66+
public function checkRequest(ServerRequestInterface $request): self
67+
{
68+
if ($this->isMatchingDomain($request)) {
69+
return $this;
70+
}
71+
72+
if ($this->isMatchingIpAddress($request)) {
73+
return $this;
74+
}
75+
76+
throw new Exception(Message::ERROR_REPORT_NOT_ALLOWED);
77+
}
78+
79+
private function isMatchingDomain(ServerRequestInterface $request): bool
80+
{
81+
if (in_array('*', $this->config['domain_whitelist'])) {
82+
return true;
83+
}
84+
85+
$domain = parse_url($request->getServerParams()['HTTP_ORIGIN'] ?? '', PHP_URL_HOST);
86+
87+
return in_array($domain, $this->config['domain_whitelist']);
88+
}
89+
90+
private function isMatchingIpAddress(ServerRequestInterface $request): bool
91+
{
92+
if (in_array('*', $this->config['ip_whitelist'])) {
93+
return true;
94+
}
95+
96+
$ipAddress = $request->getServerParams()['REMOTE_ADDR'] ?? null;
97+
98+
return in_array($ipAddress, $this->config['ip_whitelist']);
99+
}
100+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Api\App\Service;
6+
7+
use Exception;
8+
use Psr\Http\Message\ServerRequestInterface;
9+
10+
interface ErrorReportServiceInterface
11+
{
12+
/**
13+
* @throws Exception
14+
*/
15+
public function appendMessage(string $message): void;
16+
17+
/**
18+
* @throws Exception
19+
*/
20+
public function checkRequest(ServerRequestInterface $request): self;
21+
22+
/**
23+
* @throws Exception
24+
*/
25+
public function checkStatus(): self;
26+
}

0 commit comments

Comments
 (0)