Completed
Push — master ( d1f44d...2f7083 )
by Kamil
02:59
created

ProtocolParser::configure()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
ccs 0
cts 10
cp 0
rs 9.4285
cc 3
eloc 4
nc 3
nop 1
crap 12
1
<?php
2
3
namespace Dazzle\MySQL\Protocol;
4
5
use BinPHP\BinSupport;
6
use Dazzle\Event\BaseEventEmitter;
7
use Dazzle\MySQL\Protocol\Command;
8
use Dazzle\MySQL\Protocol\CommandInterface;
9
use Dazzle\MySQL\Support\Queue\Queue;
10
use Dazzle\MySQL\Support\Queue\QueueInterface;
11
use Dazzle\Socket\SocketInterface;
12
use Dazzle\Stream\StreamInterface;
13
use Exception;
14
use SplQueue;
15
16
class ProtocolParser extends BaseEventEmitter
17
{
18
    /**
19
     * @var int
20
     */
21
    const PHASE_INIT       = 1;
22
23
    /**
24
     * @var int
25
     */
26
    const PHASE_AUTH_SENT  = 2;
27
28
    /**
29
     * @var int
30
     */
31
    const PHASE_AUTH_ERR   = 3;
32
33
    /**
34
     * @var int
35
     */
36
    const PHASE_HANDSHAKED = 4;
37
38
    /**
39
     * @var int
40
     */
41
    const RS_STATE_HEADER  = 0;
42
43
    /**
44
     * @var int
45
     */
46
    const RS_STATE_FIELD   = 1;
47
48
    /**
49
     * @var int
50
     */
51
    const RS_STATE_ROW     = 2;
52
53
    /**
54
     * @var int
55
     */
56
    const STATE_STANDBY = 0;
57
58
    /**
59
     * @var int
60
     */
61
    const STATE_BODY    = 1;
62
63
    /**
64
     * @var string
65
     */
66
    protected $user     = 'root';
67
68
    /**
69
     * @var string
70
     */
71
    protected $pass     = '';
72
73
    /**
74
     * @var string
75
     */
76
    protected $dbname   = '';
77
78
    /**
79
     * @var CommandInterface
80
     */
81
    protected $currCommand;
82
83
    protected $debug = false;
84
85
    protected $state = 0;
86
87
    protected $phase = 0;
88
89
    public $seq = 0;
90
    public $clientFlags = 239237;
91
92
    public $warnCount;
93
    public $message;
94
95
    protected $maxPacketSize = 0x1000000;
96
97
    protected $charsetNumber = 0x21;
98
99
    protected $serverVersion;
100
    protected $threadId;
101
    protected $scramble;
102
103
    protected $serverCaps;
104
    protected $serverLang;
105
    protected $serverStatus;
106
107
    protected $rsState = 0;
108
    protected $pctSize = 0;
109
    protected $resultRows = [];
110
    protected $resultFields = [];
111
112
    protected $insertId;
113
    protected $affectedRows;
114
115
    public $protocalVersion = 0;
116
117
    protected $errno = 0;
118
    protected $errmsg = '';
119
120
    protected $buffer = '';
121
    protected $bufferPos = 0;
122
123
    protected $connectOptions;
124
125
    /**
126
     * @var StreamInterface
127
     */
128
    protected $stream;
129
130
    /**
131
     * @var QueueInterface
132
     */
133
    protected $executor;
134
135
    /**
136
     * @var SplQueue
137
     */
138
    protected $queue;
139
140
    /**
141
     * @param SocketInterface $stream
142
     * @param QueueInterface $executor
143
     * @param mixed[] $config
144
     */
145
    public function __construct(SocketInterface $stream, QueueInterface $executor, $config = [])
146
    {
147
        $this->stream   = $stream;
148
        $this->executor = $executor;
149
        $this->queue    = new SplQueue($this);
0 ignored issues
show
Unused Code introduced by
The call to SplQueue::__construct() has too many arguments starting with $this.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
150
        $this->configure($config);
151
        $executor->on('new', [ $this, 'handleNewCommand' ]);
152
    }
153
154
    public function start()
155
    {
156
        $this->stream->on('data', [ $this, 'handleData' ]);
157
        $this->stream->on('close', [ $this, 'handleClose' ]);
158
    }
159
160
    public function handleNewCommand()
161
    {
162
        if ($this->queue->isEmpty())
163
        {
164
            $this->nextRequest();
165
        }
166
    }
167
168
    public function debug($message)
169
    {
170
        if ($this->debug)
171
        {
172
            $bt = debug_backtrace();
173
            $caller = array_shift($bt);
174
            printf("[DEBUG] <%s:%d> %s\n", $caller['class'], $caller['line'], $message);
175
        }
176
    }
177
178
    public function handleData($stream, $data)
179
    {
180
        $this->append($data);
181
packet:
182
        if ($this->state === self::STATE_STANDBY)
183
        {
184
            if ($this->length() < 4)
185
            {
186
                return;
187
            }
188
189
            $this->pctSize = BinSupport::bytes2int($this->read(3), true);
0 ignored issues
show
Documentation Bug introduced by
It seems like \BinPHP\BinSupport::byte...t($this->read(3), true) can also be of type double. However, the property $pctSize is declared as type integer. 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...
190
            $this->state = self::STATE_BODY;
191
            $this->seq = ord($this->read(1)) + 1;
192
        }
193
194
        $len = $this->length();
195
196
        if ($len < $this->pctSize)
197
        {
198
            $this->debug('Buffer not enouth, return');
199
            return;
200
        }
201
202
        $this->state = self::STATE_STANDBY;
203
204
        if ($this->phase === 0)
205
        {
206
            $this->phase = self::PHASE_INIT;
207
            $this->protocalVersion = ord($this->read(1));
208
            $this->debug(sprintf("Protocol Version: %d", $this->protocalVersion));
209
            if ($this->protocalVersion === 0xFF)
210
            {
211
                $fieldCount = $this->protocalVersion;
212
                $this->protocalVersion = 0;
213
                printf("Error:\n");
214
215
                $this->rsState = self::RS_STATE_HEADER;
216
                $this->resultFields = [];
217
                $this->resultRows = [];
218
                if ($this->phase === self::PHASE_AUTH_SENT || $this->phase === self::PHASE_INIT)
219
                {
220
                    $this->phase = self::PHASE_AUTH_ERR;
221
                }
222
223
                goto field;
224
            }
225
            if (($p = $this->search("\x00")) === false)
226
            {
227
                return;
228
            }
229
230
            $options = &$this->connectOptions;
231
232
            $options['serverVersion'] = $this->read($p, 1);
233
            $options['threadId']      = BinSupport::bytes2int($this->read(4), true);
234
            $this->scramble           = $this->read(8, 1);
235
            $options['ServerCaps']    = BinSupport::bytes2int($this->read(2), true);
236
            $options['serverLang']    = ord($this->read(1));
237
            $options['serverStatus']  = BinSupport::bytes2int($this->read(2, 13), true);
238
            $restScramble             = $this->read(12, 1);
239
            $this->scramble          .= $restScramble;
240
241
            $this->nextRequest(true);
242
        }
243
        else
244
        {
245
            $fieldCount = ord($this->read(1));
246
field:
247
            if ($fieldCount === 0xFF)
248
            {
249
                //error packet
250
                $u             = unpack('v', $this->read(2));
251
                $this->errno   = $u[1];
252
                $state = $this->read(6);
0 ignored issues
show
Unused Code introduced by
$state is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
253
                $this->errmsg  = $this->read($this->pctSize - $len + $this->length());
254
                $this->debug(sprintf("Error Packet:%d %s\n", $this->errno, $this->errmsg));
255
256
                $this->nextRequest();
257
                $this->onError();
258
            }
259
            else if ($fieldCount === 0x00)
260
            {
261
                $this->debug('Ok Packet');
262
263
                $isAuthenticated = false;
264
                if ($this->phase === self::PHASE_AUTH_SENT)
265
                {
266
                    $this->phase = self::PHASE_HANDSHAKED;
267
                    $isAuthenticated = true;
268
                }
269
270
                $this->affectedRows = $this->parseEncodedBinSupport();
271
                $this->insertId     = $this->parseEncodedBinSupport();
272
273
                $u                  = unpack('v', $this->read(2));
274
                $this->serverStatus = $u[1];
275
276
                $u                  = unpack('v', $this->read(2));
277
                $this->warnCount    = $u[1];
278
279
                $this->message      = $this->read($this->pctSize - $len + $this->length());
280
281
                if ($isAuthenticated)
282
                {
283
                    $this->onAuthenticated();
284
                }
285
                else
286
                {
287
                    $this->onSuccess();
288
                }
289
290
                $this->debug(sprintf("AffectedRows: %d, InsertId: %d, WarnCount:%d", $this->affectedRows, $this->insertId, $this->warnCount));
291
                $this->nextRequest();
292
293
            }
294
            // EOF
295
            else if ($fieldCount === 0xFE)
296
            {
297
                $this->debug('EOF Packet');
298
                if ($this->rsState === self::RS_STATE_ROW)
299
                {
300
                    $this->debug('result done');
301
302
                    $this->nextRequest();
303
                    $this->onResultDone();
304
                }
305
                else
306
                {
307
                    ++ $this->rsState;
308
                }
309
            }
310
            //Data packet
311
            else
312
            {
313
                $this->debug('Data Packet');
314
                $this->prepend(chr($fieldCount));
315
316
                if ($this->rsState === self::RS_STATE_HEADER)
317
                {
318
                    $this->debug('Header packet of Data packet');
319
                    $extra = $this->parseEncodedBinSupport();
0 ignored issues
show
Unused Code introduced by
$extra is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
320
                    //var_dump($extra);
321
                    $this->rsState = self::RS_STATE_FIELD;
322
                }
323
                else if ($this->rsState === self::RS_STATE_FIELD)
324
                {
325
                    $this->debug('Field packet of Data packet');
326
                    $field = [
327
                        'catalog'   => $this->parseEncodedString(),
328
                        'db'        => $this->parseEncodedString(),
329
                        'table'     => $this->parseEncodedString(),
330
                        'org_table' => $this->parseEncodedString(),
331
                        'name'      => $this->parseEncodedString(),
332
                        'org_name'  => $this->parseEncodedString()
333
                    ];
334
335
                    $this->skip(1);
336
                    $u                    = unpack('v', $this->read(2));
337
                    $field['charset']     = $u[1];
338
339
                    $u                    = unpack('v', $this->read(4));
340
                    $field['length']      = $u[1];
341
342
                    $field['type']        = ord($this->read(1));
343
344
                    $u                    = unpack('v', $this->read(2));
345
                    $field['flags']       = $u[1];
346
                    $field['decimals']    = ord($this->read(1));
347
                    //var_dump($field);
348
                    $this->resultFields[] = $field;
349
350
                }
351
                else if ($this->rsState === self::RS_STATE_ROW)
352
                {
353
                    $this->debug('Row packet of Data packet');
354
                    $row = [];
355
                    for ($i = 0, $nf = sizeof($this->resultFields); $i < $nf; ++$i)
356
                    {
357
                        $row[$this->resultFields[$i]['name']] = $this->parseEncodedString();
358
                    }
359
                    $this->resultRows[] = $row;
360
                    $command = $this->queue->dequeue();
361
                    //$command->emit('success', [ $command, [ $row ] ]);
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% 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...
362
                    $this->queue->unshift($command);
363
                }
364
            }
365
        }
366
        $this->restBuffer($this->pctSize - $len + $this->length());
367
        goto packet;
368
    }
369
370
    protected function onError()
371
    {
372
        $command = $this->queue->dequeue();
373
        $error = new Exception($this->errmsg, $this->errno);
374
        $command->setError($error);
375
        $command->emit('error', [ $command, $error ]);
376
        $this->errmsg = '';
377
        $this->errno  = 0;
378
    }
379
380
    protected function onResultDone()
381
    {
382
        $command = $this->queue->dequeue();
383
384
        $command->resultRows   = $this->resultRows;
385
        $command->resultFields = $this->resultFields;
386
387
        $command->emit('success', [ $command, $this->resultRows ]);
388
389
        $this->rsState      = self::RS_STATE_HEADER;
390
        $this->resultRows   = $this->resultFields = [];
391
    }
392
393
    protected function onSuccess()
394
    {
395
        $command = $this->queue->dequeue();
396
397
        $command->affectedRows = $this->affectedRows;
398
        $command->insertId     = $this->insertId;
399
        $command->warnCount    = $this->warnCount;
400
        $command->message      = $this->message;
401
402
        $command->emit('success', [ $command ]);
403
    }
404
405
    protected function onAuthenticated()
406
    {
407
        $command = $this->queue->dequeue();
408
        $command->emit('success', [ $command, $this->connectOptions ]);
409
    }
410
411
    protected function handleClose()
412
    {
413
        $this->emit('close');
414
        if ($this->queue->count())
415
        {
416
            $command = $this->queue->dequeue();
417
            if ($command->equals(Command::QUIT))
418
            {
419
                $command->emit('success', [ $command ]);
420
            }
421
        }
422
    }
423
424
    public function append($str)
425
    {
426
        $this->buffer .= $str;
427
    }
428
429
    public function prepend($str)
430
    {
431
        $this->buffer = $str . substr($this->buffer, $this->bufferPos);
432
        $this->bufferPos = 0;
433
    }
434
435
    public function read($len, $skiplen = 0)
436
    {
437
        if (strlen($this->buffer) - $this->bufferPos - $len - $skiplen < 0)
438
        {
439
            throw new \LogicException('Logic Error');
440
        }
441
        $buffer = substr($this->buffer, $this->bufferPos, $len);
442
        $this->bufferPos += $len;
443
        if ($skiplen)
444
        {
445
            $this->bufferPos += $skiplen;
446
        }
447
448
        return $buffer;
449
    }
450
451
    public function skip($len)
452
    {
453
        $this->bufferPos += $len;
454
    }
455
456
    public function restBuffer($len)
457
    {
458
        if (strlen($this->buffer) === ($this->bufferPos+$len))
459
        {
460
            $this->buffer = '';
461
        }
462
        else
463
        {
464
            $this->buffer = substr($this->buffer,$this->bufferPos+$len);
465
        }
466
        $this->bufferPos = 0;
467
    }
468
469
    public function length()
470
    {
471
        return strlen($this->buffer) - $this->bufferPos;
472
    }
473
474
    public function search($what)
475
    {
476
        if (($p = strpos($this->buffer, $what, $this->bufferPos)) !== false)
477
        {
478
            return $p - $this->bufferPos;
479
        }
480
481
        return false;
482
    }
483
    /* end of buffer operation APIs */
484
485
    public function authenticate()
486
    {
487
        if ($this->phase !== self::PHASE_INIT)
488
        {
489
            return;
490
        }
491
        $this->phase = self::PHASE_AUTH_SENT;
492
493
        $clientFlags =
494
            Protocol::CLIENT_LONG_PASSWORD |
495
            Protocol::CLIENT_LONG_FLAG |
496
            Protocol::CLIENT_LOCAL_FILES |
497
            Protocol::CLIENT_PROTOCOL_41 |
498
            Protocol::CLIENT_INTERACTIVE |
499
            Protocol::CLIENT_TRANSACTIONS |
500
            Protocol::CLIENT_SECURE_CONNECTION |
501
            Protocol::CLIENT_MULTI_RESULTS |
502
            Protocol::CLIENT_MULTI_STATEMENTS |
503
            Protocol::CLIENT_CONNECT_WITH_DB;
504
505
        $packet = pack('VVc', $clientFlags, $this->maxPacketSize, $this->charsetNumber)
506
            . "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
507
            . $this->user . "\x00"
508
            . $this->getAuthToken($this->scramble, $this->pass)
509
            . ($this->dbname ? $this->dbname . "\x00" : '');
510
511
        $this->sendPacket($packet);
512
        $this->debug('Auth packet sent');
513
    }
514
515
    public function getAuthToken($scramble, $password = '')
516
    {
517
        if ($password === '')
518
        {
519
            return "\x00";
520
        }
521
        $token = sha1($scramble . sha1($hash1 = sha1($password, true), true), true) ^ $hash1;
522
523
        return $this->buildLenEncodedBinSupport($token);
524
    }
525
526
    /**
527
     * Builds length-encoded BinSupport string
528
     * @param string String
529
     * @return string Resulting BinSupport string
530
     */
531
    public function buildLenEncodedBinSupport($s)
532
    {
533
        if ($s === null)
534
        {
535
            return "\251";
536
        }
537
538
        $l = strlen($s);
539
540
        if ($l <= 250)
541
        {
542
            return chr($l) . $s;
543
        }
544
545
        if ($l <= 0xFFFF)
546
        {
547
            return "\252" . BinSupport::int2bytes(2, true) . $s;
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
548
        }
549
550
        if ($l <= 0xFFFFFF)
551
        {
552
            return "\254" . BinSupport::int2bytes(3, true) . $s;
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
553
        }
554
555
        return BinSupport::int2bytes(8, $l, true) . $s;
556
    }
557
558
    /**
559
     * Parses length-encoded BinSupport integer
560
     * @return integer Result
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|null|false|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
561
     */
562
    public function parseEncodedBinSupport()
563
    {
564
        $f = ord($this->read(1));
565
        if ($f <= 250)
566
        {
567
            return $f;
568
        }
569
        if ($f === 251)
570
        {
571
            return null;
572
        }
573
        if ($f === 255)
574
        {
575
            return false;
576
        }
577
        if ($f === 252)
578
        {
579
            return BinSupport::bytes2int($this->read(2), true);
580
        }
581
        if ($f === 253)
582
        {
583
            return BinSupport::bytes2int($this->read(3), true);
584
        }
585
586
        return BinSupport::bytes2int($this->read(8), true);
587
    }
588
589
    /**
590
     * Parse length-encoded string
591
     * @return integer Result
0 ignored issues
show
Documentation introduced by
Should the return type not be null|false|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
592
     */
593
    public function parseEncodedString()
594
    {
595
        $l = $this->parseEncodedBinSupport();
596
        if (($l === null) || ($l === false))
597
        {
598
            return $l;
599
        }
600
601
        return $this->read($l);
602
    }
603
604
    /**
605
     * Send packet to the server.
606
     *
607
     * @param string $packet
608
     * @return bool
609
     */
610
    protected function sendPacket($packet)
611
    {
612
        return $this->stream->write(BinSupport::int2bytes(3, strlen($packet), true) . chr($this->seq++) . $packet);
613
    }
614
615
    /**
616
     * Parse next request.
617
     *
618
     * @param bool $isHandshake
619
     * @return bool
620
     */
621
    protected function nextRequest($isHandshake = false)
622
    {
623
        if (!$isHandshake && $this->phase != self::PHASE_HANDSHAKED)
624
        {
625
            return false;
626
        }
627
        if (!$this->executor->isEmpty())
628
        {
629
            $command = $this->executor->dequeue();
630
            $this->queue->enqueue($command);
631
632
            if ($command->equals(Command::INIT_AUTHENTICATE))
633
            {
634
                $this->authenticate();
635
            }
636
            else
637
            {
638
                $this->seq = 0;
639
                $this->sendPacket(chr($command->getID()) . $command->getSQL());
640
            }
641
        }
642
643
        return true;
644
    }
645
646
    /**
647
     * Configure protocol parser.
648
     *
649
     * @param mixed[] $options
650
     */
651
    protected function configure($options)
652
    {
653
        foreach ($options as $option => $value)
654
        {
655
            if (property_exists($this, $option))
656
            {
657
                $this->$option = $value;
658
            }
659
        }
660
    }
661
}
662