Completed
Push — 2.0 ( 070a8d...a8cc12 )
by Marco
07:08
created

RpcServer::getErrors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 0
cts 2
cp 0
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php namespace Comodojo\RpcServer;
2
3
use \Comodojo\RpcServer\Component\Capabilities;
4
use \Comodojo\RpcServer\Component\Methods;
5
use \Comodojo\RpcServer\Component\Errors;
6
use \Comodojo\RpcServer\Request\Parameters;
7
use \Comodojo\RpcServer\Request\XmlProcessor;
8
use \Comodojo\RpcServer\Request\JsonProcessor;
9
use \Comodojo\Foundation\Logging\Manager as LogManager;
10
use \Comodojo\Xmlrpc\XmlrpcEncoder;
11
use \Comodojo\Xmlrpc\XmlrpcDecoder;
12
use \phpseclib\Crypt\AES;
13
use \Psr\Log\LoggerInterface;
14
use \Comodojo\Exception\RpcException;
15
use \Comodojo\Exception\XmlrpcException;
16
use \InvalidArgumentException;
17
use \Exception;
18
19
20
/**
21
 * The RpcServer main class.
22
 *
23
 * @package     Comodojo Spare Parts
24
 * @author      Marco Giovinazzi <[email protected]>
25
 * @license     MIT
26
 *
27
 * LICENSE:
28
 *
29
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35
 * THE SOFTWARE.
36
 */
37
38
class RpcServer {
39
40
    /**
41
     * Capabilities collector
42
     *
43
     * @const string
44
     */
45
    const XMLRPC = 'xml';
46
47
    /**
48
     * Capabilities collector
49
     *
50
     * @const string
51
     */
52
    const JSONRPC = 'json';
53
54
    /**
55
     * Capabilities collector
56
     *
57
     * @var Capabilities
58
     */
59
    private $capabilities;
60
61
    /**
62
     * RpcMethods collector
63
     *
64
     * @var Methods
65
     */
66
    private $methods;
67
68
    /**
69
     * Standard Rpc Errors collector
70
     *
71
     * @var Errors
72
     */
73
    private $errors;
74
75
    /**
76
     * The request payload, better the RAW export of 'php://input'
77
     *
78
     * @var string
79
     */
80
    private $payload;
81
82
    /**
83
     * Encryption key, in case of encrypted transport
84
     *
85
     * @var string
86
     */
87
    private $encrypt;
88
89
    /**
90
     * Current encoding
91
     *
92
     * @var string
93
     */
94
    private $encoding = 'utf-8';
95
96
    /**
97
     * Current protocol
98
     *
99
     * @var string
100
     */
101
    private $protocol;
102
103
    /**
104
     * Supported RPC protocols
105
     *
106
     * @var array
107
     */
108
    private $supported_protocols = ['xml', 'json'];
109
110
    /**
111
     * Internal marker (encryption)
112
     *
113
     * @var bool
114
     */
115
    private $request_is_encrypted = false;
116
117
    /**
118
     * Current logger
119
     *
120
     * @var LoggerInterface
121
     */
122
    private $logger;
123
124
    /**
125
     * Class constructor
126
     *
127
     * @param string $protocol
128
     *
129
     * @throws Exception
130
     * @throws InvalidArgumentException
131
     */
132 90
    public function __construct($protocol, LoggerInterface $logger = null) {
133
134 90
        $this->logger = is_null($logger) ? LogManager::create('rpcserver', false)->getLogger() : $logger;
0 ignored issues
show
Documentation Bug introduced by
It seems like is_null($logger) ? \Como...->getLogger() : $logger can also be of type object<Monolog\Logger>. However, the property $logger is declared as type object<Psr\Log\LoggerInterface>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
135
136
        try {
137
138
            // setup protocol
139
140 90
            $this->setProtocol($protocol);
141
142
            // init components
143
144 87
            $this->capabilities = new Capabilities($this->logger);
145
146 87
            $this->methods = new Methods($this->logger);
147
148 87
            $this->errors = new Errors($this->logger);
149
150
            // populate components
151
152 87
            self::setIntrospectionMethods($this->methods);
153
154 87
            self::setCapabilities($this->capabilities);
155
156 87
            self::setErrors($this->errors);
157
158 32
        } catch (Exception $e) {
159
160 3
            throw $e;
161
162
        }
163
164 87
        $this->logger->debug("RpcServer init complete, protocol $protocol");
165
166 87
    }
167
168
    /**
169
     * Set RPC protocol (json or xml)
170
     *
171
     * @param string $protocol
172
     *
173
     * @return self
174
     * @throws InvalidArgumentException
175
     */
176 90
    public function setProtocol($protocol) {
177
178 90
        if ( empty($protocol) || !in_array($protocol, $this->supported_protocols) ) throw new InvalidArgumentException('Invalid or unsupported RPC protocol');
179
180 87
        $this->protocol = $protocol;
181
182 87
        return $this;
183
184
    }
185
186
    /**
187
     * Get RPC protocol
188
     *
189
     * @return string
190
     */
191 15
    public function getProtocol() {
192
193 15
        return $this->protocol;
194
195
    }
196
197
    /**
198
     * Set request payload, raw format
199
     *
200
     * @return self
201
     */
202 87
    public function setPayload($payload) {
203
204 87
        $this->payload = $payload;
205
206 87
        return $this;
207
208
    }
209
210
    /**
211
     * Get request payload
212
     *
213
     * @return string
214
     */
215 3
    public function getPayload() {
216
217 3
        return $this->payload;
218
219
    }
220
221 3
    public function setEncoding($encoding) {
222
223 3
        $this->encoding = $encoding;
224
225 3
        return $this;
226
227
    }
228
229 3
    public function getEncoding() {
230
231 3
        return $this->encoding;
232
233
    }
234
235
    /**
236
     * Set encryption key; this will enable the NOT-STANDARD payload encryption
237
     *
238
     * @param string $key The encryption key
239
     *
240
     * @return self
241
     * @throws InvalidArgumentException
242
     */
243 6
    public function setEncryption($key) {
244
245 6
        if ( empty($key) ) throw new InvalidArgumentException("Shared key cannot be empty");
246
247 6
        $this->encrypt = $key;
248
249 6
        return $this;
250
251
    }
252
253
    /**
254
     * Get the ecryption key or null if no encryption is selected
255
     *
256
     * @return string
257
     */
258 3
    public function getEncryption() {
259
260 3
        return $this->encrypt;
261
262
    }
263
264
    /**
265
     * Get capabilities object
266
     *
267
     * @deprecated
268
     * @see Parameters::getCapabilities()
269
     * @return Capabilities
270
     */
271 6
    public function capabilities() {
272
273 6
        return $this->getCapabilities();
0 ignored issues
show
Deprecated Code introduced by
The method Comodojo\RpcServer\RpcServer::getCapabilities() has been deprecated.

This method has been deprecated.

Loading history...
274
275
    }
276
277
    /**
278
     * Get capabilities object
279
     *
280
     * @deprecated
281
     * @see Parameters::getCapabilities()
282
     * @return Capabilities
283
     */
284 6
    public function getCapabilities() {
285
286 6
        return $this->capabilities;
287
288
    }
289
290
    /**
291
     * Get methods object
292
     *
293
     * @deprecated
294
     * @see Parameters::getMethods()
295
     * @return Methods
296
     */
297 36
    public function methods() {
298
299 36
        return $this->getMethods();
300
301
    }
302
303
    /**
304
     * Get methods object
305
     *
306
     * @return Methods
307
     */
308 36
    public function getMethods() {
309
310 36
        return $this->methods;
311
312
    }
313
314
    /**
315
     * Get errors object
316
     *
317
     * @deprecated
318
     * @see Parameters::getErrors()
319
     * @return Errors
320
     */
321
    public function errors() {
322
323
        return $this->getErrors();
324
325
    }
326
327
    /**
328
     * Get errors object
329
     *
330
     * @return Errors
331
     */
332
    public function getErrors() {
333
334
        return $this->errors;
335
336
    }
337
338
    /**
339
     * Serve request
340
     *
341
     * @return string
342
     * @throws Exception
343
     */
344 84
    public function serve() {
345
346 84
        $this->logger->notice("Start serving request");
347
348 84
        $parameters_object = new Parameters($this->capabilities, $this->methods, $this->errors, $this->logger, $this->protocol);
349
350
        try {
351
352 84
            $this->logger->debug("Received payload: ".$this->payload);
353
354 84
            $payload = $this->uncan($this->payload);
355
356 84
            $this->logger->debug("Decoded payload", (array) $payload);
357
358 84
            if ( $this->protocol == self::XMLRPC ) $result = XmlProcessor::process($payload, $parameters_object, $this->logger);
359
360 45
            else if ( $this->protocol == self::JSONRPC ) $result = JsonProcessor::process($payload, $parameters_object, $this->logger);
361
362 52
            else throw new Exception('Invalid or unsupported RPC protocol');
363
364 32
        } catch (RpcException $re) {
365
366 6
            return $this->can($re, true);
367
368
        } catch (Exception $e) {
369
370
            throw $e;
371
372
        }
373
374 78
        return $this->can($result, false);
375
376
    }
377
378
    /**
379
     * Uncan the provided payload
380
     *
381
     * @param string $payload
382
     *
383
     * @return mixed
384
     * @throws RpcException
385
     */
386 84
    private function uncan($payload) {
387
388 84
        $decoded = null;
389
390 84
        if ( empty($payload) || !is_string($payload) ) throw new RpcException("Invalid Request", -32600);
391
392 84
        if ( substr($payload, 0, 27) == 'comodojo_encrypted_request-' ) {
393
394 3
            if ( empty($this->encrypt) ) throw new RpcException("Transport error", -32300);
395
396 3
            $this->request_is_encrypted = true;
397
398 3
            $aes = new AES();
399
400 3
            $aes->setKey($this->encrypt);
401
402 3
            $payload = $aes->decrypt(base64_decode(substr($payload, 27)));
403
404 3
            if ( $payload == false ) throw new RpcException("Transport error", -32300);
405
406 1
        }
407
408 84
        if ( $this->protocol == 'xml' ) {
409
410 39
            $decoder = new XmlrpcDecoder();
411
412
            try {
413
414 39
                $decoded = $decoder->decodeCall($payload);
415
416 13
            } catch (XmlrpcException $xe) {
417
418 26
                throw new RpcException("Parse error", -32700);
419
420
            }
421
422 58
        } else if ( $this->protocol == 'json' ) {
423
424 45
            if ( strtolower($this->encoding) != 'utf-8' ) {
425
426
                $payload = mb_convert_encoding($payload, "UTF-8", strtoupper($this->encoding));
427
428
            }
429
430 45
            $decoded = json_decode($payload, false /*DO RAW conversion*/);
431
432 45
            if ( is_null($decoded) ) throw new RpcException("Parse error", -32700);
433
434 15
        } else {
435
436
            throw new RpcException("Transport error", -32300);
437
438
        }
439
440 84
        return $decoded;
441
442
    }
443
444
    /**
445
     * Can the RPC response
446
     *
447
     * @param mixed   $response
448
     * @param boolean $error
449
     *
450
     * @return string
451
     * @throws RpcException
452
     */
453 84
    private function can($response, $error) {
454
455 84
        $encoded = null;
456
457 84
        if ( $this->protocol == 'xml' ) {
458
459 39
            $encoder = new XmlrpcEncoder();
460
461 39
            $encoder->setEncoding($this->encoding);
462
463
            try {
464
465 39
                $encoded = $error ? $encoder->encodeError($response->getCode(), $response->getMessage()) : $encoder->encodeResponse($response);
466
467 13
            } catch (XmlrpcException $xe) {
468
469
                $this->logger->error($xe->getMessage());
470
471 26
                $encoded = $encoder->encodeError(-32500, "Application error");
472
473
            }
474
475 13
        } else {
476
477 45
            if ( strtolower($this->encoding) != 'utf-8' && !is_null($response) ) {
478
479
                array_walk_recursive($response, function(&$entry) {
480
481
                    if ( is_string($entry) ) {
482
483
                        $entry = mb_convert_encoding($entry, strtoupper($this->encoding), "UTF-8");
484
485
                    }
486
487
                });
488
489
            }
490
491
            // json will not return any RpcException; errors (in case) are handled directly by processor
492
493 45
            $encoded = is_null($response) ? null : json_encode($response/*, JSON_NUMERIC_CHECK*/);
494
495
        }
496
497 84
        $this->logger->debug("Plain response: $encoded");
498
499 84
        if ( $this->request_is_encrypted /* && !empty($encoded) */ ) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
75% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
500
501 3
            $aes = new AES();
502
503 3
            $aes->setKey($this->encrypt);
504
505 3
            $encoded = 'comodojo_encrypted_response-'.base64_encode($aes->encrypt($encoded));
506
507 3
            $this->logger->debug("Encrypted response: $encoded");
508
509 1
        }
510
511 84
        return $encoded;
512
513
    }
514
515
    /**
516
     * Inject introspection and reserved RPC methods
517
     *
518
     * @param Methods $methods
519
     */
520 87
    private static function setIntrospectionMethods($methods) {
521
522 87
        $methods->add(RpcMethod::create("system.getCapabilities", '\Comodojo\RpcServer\Reserved\GetCapabilities::execute')
523 87
            ->setDescription("This method lists all the capabilites that the RPC server has: the (more or less standard) extensions to the RPC spec that it adheres to")
524 87
            ->setReturnType('struct')
525 29
        );
526
527 87
        $methods->add(RpcMethod::create("system.listMethods", '\Comodojo\RpcServer\Introspection\ListMethods::execute')
528 87
            ->setDescription("This method lists all the methods that the RPC server knows how to dispatch")
529 87
            ->setReturnType('array')
530 29
        );
531
532 87
        $methods->add(RpcMethod::create("system.methodHelp", '\Comodojo\RpcServer\Introspection\MethodHelp::execute')
533 87
            ->setDescription("Returns help text if defined for the method passed, otherwise returns an empty string")
534 87
            ->setReturnType('string')
535 87
            ->addParameter('string', 'method')
536 29
        );
537
538 87
        $methods->add(RpcMethod::create("system.methodSignature", '\Comodojo\RpcServer\Introspection\MethodSignature::execute')
539 87
            ->setDescription("Returns an array of known signatures (an array of arrays) for the method name passed.".
540 87
                "If no signatures are known, returns a none-array (test for type != array to detect missing signature)")
541 87
            ->setReturnType('array')
542 87
            ->addParameter('string', 'method')
543 29
        );
544
545 87
        $methods->add(RpcMethod::create("system.multicall", '\Comodojo\RpcServer\Reserved\Multicall::execute')
546 87
            ->setDescription("Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader\$1208 for details")
547 87
            ->setReturnType('array')
548 87
            ->addParameter('array', 'requests')
549 29
        );
550
551 87
    }
552
553
    /**
554
     * Inject supported capabilities
555
     *
556
     * @param Capabilities $capabilities
557
     */
558 87
    private static function setCapabilities($capabilities) {
559
560
        $supported_capabilities = array(
561 87
            'xmlrpc' => array('http://www.xmlrpc.com/spec', 1),
562 29
            'system.multicall' => array('http://www.xmlrpc.com/discuss/msgReader$1208', 1),
563 29
            'introspection' => array('http://phpxmlrpc.sourceforge.net/doc-2/ch10.html', 2),
564 29
            'nil' => array('http://www.ontosys.com/xml-rpc/extensions.php', 1),
565 29
            'faults_interop' => array('http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php', 20010516),
566 29
            'json-rpc' => array('http://www.jsonrpc.org/specification', 2)
567 29
        );
568
569 87
        foreach ( $supported_capabilities as $capability => $values ) {
570
571 87
            $capabilities->add($capability, $values[0], $values[1]);
572
573 29
        }
574
575 87
    }
576
577
    /**
578
     * Inject standard and RPC errors
579
     *
580
     * @param Errors $errors
581
     */
582 87
    private static function setErrors($errors) {
583
584
        $std_rpc_errors = array(
585 87
            -32700 => "Parse error",
586 29
            -32701 => "Parse error - Unsupported encoding",
587 29
            -32702 => "Parse error - Invalid character for encoding",
588 29
            -32600 => "Invalid Request",
589 29
            -32601 => "Method not found",
590 29
            -32602 => "Invalid params",
591 29
            -32603 => "Internal error",
592 29
            -32500 => "Application error",
593 29
            -32400 => "System error",
594 29
            -32300 => "Transport error",
595
            // Predefined Comodojo Errors
596 29
            -31000 => "Multicall is available only in XMLRPC",
597
            -31001 => "Recursive system.multicall forbidden"
598 29
        );
599
600 87
        foreach ( $std_rpc_errors as $code => $message ) {
601
602 87
            $errors->add($code, $message);
603
604 29
        }
605
606 87
    }
607
608
 }
609