Completed
Push — develop ( 0935d6...5677b3 )
by Fabian
02:21
created

Processor::verifySenderAddress()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 46
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 25
c 2
b 0
f 0
nc 7
nop 1
dl 0
loc 46
rs 8.8977
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cakasim\Payone\Sdk\Notification\Processor;
6
7
use Cakasim\Payone\Sdk\Config\ConfigExceptionInterface;
8
use Cakasim\Payone\Sdk\Config\ConfigInterface;
9
use Cakasim\Payone\Sdk\Notification\Context\Context;
10
use Cakasim\Payone\Sdk\Notification\Handler\HandlerManagerInterface;
11
use Cakasim\Payone\Sdk\Notification\Message\MessageInterface;
12
use Cakasim\Payone\Sdk\Notification\Message\TransactionStatus;
13
use Cakasim\Payone\Sdk\Notification\Message\TransactionStatusInterface;
14
use Psr\Http\Message\ServerRequestInterface;
15
use Psr\Log\LoggerInterface;
16
17
/**
18
 * The implementation of the ProcessorInterface.
19
 *
20
 * @author Fabian Böttcher <[email protected]>
21
 * @since 0.1.0
22
 */
23
class Processor implements ProcessorInterface
24
{
25
    /**
26
     * @var ConfigInterface The SDK config.
27
     */
28
    protected $config;
29
30
    /**
31
     * @var LoggerInterface The SDK logger.
32
     */
33
    protected $logger;
34
35
    /**
36
     * @var HandlerManagerInterface The notification handler manager.
37
     */
38
    protected $handlerManager;
39
40
    /**
41
     * Constructs the processor.
42
     *
43
     * @param ConfigInterface $config The SDK config.
44
     * @param LoggerInterface $logger The SDK logger.
45
     * @param HandlerManagerInterface $handlerManager The notification handler manager.
46
     */
47
    public function __construct(
48
        ConfigInterface $config,
49
        LoggerInterface $logger,
50
        HandlerManagerInterface $handlerManager
51
    ) {
52
        $this->config = $config;
53
        $this->logger = $logger;
54
        $this->handlerManager = $handlerManager;
55
    }
56
57
    /**
58
     * @inheritDoc
59
     */
60
    public function processRequest(ServerRequestInterface $request): void
61
    {
62
        $this->logger->info('Process incoming request as PAYONE notification message.');
63
64
        // Validate and get the sender address from the request.
65
        $senderAddress = $this->validateSenderAddress($request);
66
67
        // Verify that the sender IP address is authorized.
68
        $this->verifySenderAddress($senderAddress);
69
70
        // Validate and get the notification message parameters.
71
        $parameters = $this->validateParameters($request);
72
73
        // Verify API key from parameters.
74
        $this->verifyKey($parameters['key']);
75
76
        // Filter parameter array, remove sensible or unnecessary parameters.
77
        $parameters = $this->filterParameters($parameters);
78
79
        // Create notification message from parameters.
80
        $message = $this->createMessage($parameters);
81
82
        // Create notification context and pass it to the
83
        // registered notification handlers.
84
        $context = new Context($request, $message);
85
        $this->handlerManager->forwardMessage($context);
86
    }
87
88
    /**
89
     * Validates and returns the sender address from
90
     * the provided request.
91
     *
92
     * @param ServerRequestInterface $request The request to get the sender address from.
93
     * @return string The validated sender address.
94
     * @throws ProcessorExceptionInterface If the sender address validation fails.
95
     */
96
    protected function validateSenderAddress(ServerRequestInterface $request): string
97
    {
98
        $senderAddress = $request->getServerParams()['REMOTE_ADDR'] ?? null;
99
100
        if (!is_string($senderAddress)) {
101
            throw new ProcessorException("Failed notification processing, invalid notification sender address.");
102
        }
103
104
        return $senderAddress;
105
    }
106
107
    /**
108
     * Verifies the provided sender IP address by checking
109
     * if the sender IP address is authorized.
110
     *
111
     * @param string $senderAddress The sender IP address to verify.
112
     * @throws ProcessorExceptionInterface If the sender address verification fails.
113
     */
114
    protected function verifySenderAddress(string $senderAddress): void
115
    {
116
        // Get numeric format of sender IP address.
117
        $senderAddress = ip2long($senderAddress);
118
119
        if (!$senderAddress) {
120
            throw new ProcessorException("Failed notification processing, cannot convert sender address '{$senderAddress}' to numeric format.");
121
        }
122
123
        try {
124
            // Get list of allowed sender IP ranges.
125
            $whitelist = $this->config->get('notification.sender_address_whitelist');
126
        } catch (ConfigExceptionInterface $e) {
127
            throw new ProcessorException("Failed notification processing, cannot get sender address whitelist from config.", 0, $e);
128
        }
129
130
        $this->logger->debug(sprintf(
131
            "Verify PAYONE notification message sender address '%s' is in whitelist: %s.",
132
            long2ip($senderAddress),
133
            join(', ', $whitelist)
134
        ));
135
136
        foreach ($whitelist as $range) {
137
            // Add /32 mask if range is single IP.
138
            if (strpos($range, '/') === false) {
139
                $range .= '/32';
140
            }
141
142
            // https://gist.github.com/tott/7684443
143
            [$ip, $mask] = explode('/', $range, 2);
144
            $mask = (int) $mask;
145
            $mask = ~((2 ** (32 - $mask)) - 1);
146
            $ip = ip2long($ip);
147
148
            if (($senderAddress & $mask) === ($ip & $mask)) {
149
                $this->logger->debug(sprintf(
150
                    "PAYONE notification message sender address '%s' matches whitelist entry '%s'.",
151
                    long2ip($senderAddress),
152
                    $range
153
                ));
154
155
                return;
156
            }
157
        }
158
159
        throw new ProcessorException("Failed notification processing, sender address is not in the whitelist.");
160
    }
161
162
    /**
163
     * Validates and returns the notification request parameters.
164
     *
165
     * @param ServerRequestInterface $request The request to get the notification parameters from.
166
     * @return array The validated notification parameters.
167
     * @throws ProcessorExceptionInterface If the notification parameter validation fails.
168
     */
169
    protected function validateParameters(ServerRequestInterface $request): array
170
    {
171
        $parameters = $request->getParsedBody();
172
173
        if (!is_array($parameters)) {
174
            throw new ProcessorException("Failed notification processing, invalid notification parameters.");
175
        }
176
177
        if (!is_string($parameters['key'] ?? null)) {
178
            throw new ProcessorException("Failed notification processing, no API key provided.");
179
        }
180
181
        return $parameters;
182
    }
183
184
    /**
185
     * Verifies the provided API key.
186
     *
187
     * @param string $key The API key to verify.
188
     * @throws ProcessorExceptionInterface If the API key verification fails.
189
     */
190
    protected function verifyKey(string $key): void
191
    {
192
        try {
193
            $validKey = $this->config->get('api.key');
194
        } catch (ConfigExceptionInterface $e) {
195
            throw new ProcessorException("Failed notification processing, cannot get API key from config.", 0, $e);
196
        }
197
198
        // Make MD5 hash from configured API key.
199
        $validKey = md5($validKey);
200
201
        $this->logger->debug("Verify configured API key matches notification API key.");
202
203
        if ($validKey !== $key) {
204
            throw new ProcessorException("Failed notification processing, wrong API key.");
205
        }
206
    }
207
208
    /**
209
     * Filters the notification parameters.
210
     *
211
     * @param array $parameters The notification parameters to filter.
212
     * @return array The filtered notification parameters.
213
     */
214
    protected function filterParameters(array $parameters): array
215
    {
216
        // Remove API key from parameters.
217
        unset($parameters['key']);
218
219
        return $parameters;
220
    }
221
222
    /**
223
     * Creates the notification message from the notification parameters.
224
     *
225
     * @param array $parameters The notification parameters.
226
     * @return MessageInterface The created notification message.
227
     * @throws ProcessorExceptionInterface If the notification message cannot be created.
228
     */
229
    protected function createMessage(array $parameters): MessageInterface
230
    {
231
        // Expect a transaction status message if the txaction parameter is present.
232
        if (isset($parameters['txaction'])) {
233
            return $this->createTransactionStatusMessage($parameters);
234
        }
235
236
        throw new ProcessorException("Failed notification processing, cannot create notification message from parameters.");
237
    }
238
239
    /**
240
     * Creates a transaction status message from the provided notification parameters.
241
     *
242
     * @param array $parameters The notification parameters to create the transaction status message from.
243
     * @return TransactionStatusInterface The created transaction status message.
244
     * @throws ProcessorExceptionInterface If the transaction status message cannot be created.
245
     */
246
    protected function createTransactionStatusMessage(array $parameters): TransactionStatusInterface
247
    {
248
        $requiredParameters = [
249
            'mode',
250
            'portalid',
251
            'aid',
252
            'txaction',
253
            'txtime',
254
            'clearingtype',
255
            'currency',
256
        ];
257
258
        foreach ($requiredParameters as $requiredParameter) {
259
            if (!is_string($parameters[$requiredParameter] ?? null)) {
260
                throw new ProcessorException("Failed notification processing, missing or invalid '{$requiredParameter}' parameter for transaction status message.");
261
            }
262
        }
263
264
        return new TransactionStatus($parameters);
265
    }
266
}
267