Completed
Push — master ( a55444...d1f44d )
by Kamil
03:40
created

ProtocolParser::parseEncodedBinSupport()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 0
cts 25
cp 0
rs 8.439
c 0
b 0
f 0
cc 6
eloc 13
nc 6
nop 0
crap 42
1
<?php
2
3
namespace Dazzle\MySQL\Protocol;
4
5
use BinPHP\BinSupport;
6
use Dazzle\Event\BaseEventEmitter;
7
use Dazzle\MySQL\Command\Command;
8
use Dazzle\Stream\StreamInterface;
9
use Exception;
10
11
class ProtocolParser extends BaseEventEmitter
12
{
13
    const PHASE_GOT_INIT   = 1;
14
    const PHASE_AUTH_SENT  = 2;
15
    const PHASE_AUTH_ERR   = 3;
16
    const PHASE_HANDSHAKED = 4;
17
18
    const RS_STATE_HEADER = 0;
19
    const RS_STATE_FIELD  = 1;
20
    const RS_STATE_ROW    = 2;
21
22
    const STATE_STANDBY = 0;
23
    const STATE_BODY    = 1;
24
25
    protected $user     = 'root';
26
    protected $pass     = '';
27
    protected $dbname   = '';
28
29
    /**
30
     * @var \Dazzle\MySQL\Command\Command
31
     */
32
    protected $currCommand;
33
34
    protected $debug = false;
35
36
    protected $state = 0;
37
38
    protected $phase = 0;
39
40
    public $seq = 0;
41
    public $clientFlags = 239237;
42
43
    public $warnCount;
44
    public $message;
45
46
    protected $maxPacketSize = 0x1000000;
47
48
    public $charsetNumber = 0x21;
49
50
    protected $serverVersion;
51
    protected $threadId;
52
    protected $scramble;
53
54
    protected $serverCaps;
55
    protected $serverLang;
56
    protected $serverStatus;
57
58
    protected $rsState = 0;
59
    protected $pctSize = 0;
60
    protected $resultRows = [];
61
    protected $resultFields = [];
62
63
    protected $insertId;
64
    protected $affectedRows;
65
66
    public $protocalVersion = 0;
67
68
    protected $errno = 0;
69
    protected $errmsg = '';
70
71
    protected $buffer = '';
72
    protected $bufferPos = 0;
73
74
    protected $connectOptions;
75
76
    /**
77
     * @var StreamInterface
78
     */
79
    protected $stream;
80
    /**
81
     * @var \Dazzle\MySQL\Executor
82
     */
83
    protected $executor;
84
85
    protected $queue;
86
87
    public function __construct($stream, $executor)
88
    {
89
        $this->stream   = $stream;
90
        $this->executor = $executor;
91
        $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...
92
        $executor->on('new', array($this, 'handleNewCommand'));
93
    }
94
95
    public function start()
96
    {
97
        $this->stream->on('data', [ $this, 'handleData' ]);
98
        $this->stream->on('close', [ $this, 'handleClose' ]);
99
    }
100
101
    public function handleNewCommand()
102
    {
103
        if ($this->queue->count() <= 0)
104
        {
105
            $this->nextRequest();
106
        }
107
    }
108
109
    public function debug($message)
110
    {
111
        if ($this->debug)
112
        {
113
            $bt = debug_backtrace();
114
            $caller = array_shift($bt);
115
            printf("[DEBUG] <%s:%d> %s\n", $caller['class'], $caller['line'], $message);
116
        }
117
    }
118
119
    public function setOptions($options)
120
    {
121
        foreach ($options as $option => $value)
122
        {
123
            if (property_exists($this, $option))
124
            {
125
                $this->$option = $value;
126
            }
127
        }
128
    }
129
130
    public function handleData($stream, $data)
131
    {
132
        $this->append($data);
133
packet:
134
        if ($this->state === self::STATE_STANDBY)
135
        {
136
            if ($this->length() < 4)
137
            {
138
                return;
139
            }
140
141
            $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...
142
            $this->state = self::STATE_BODY;
143
            $this->seq = ord($this->read(1)) + 1;
144
        }
145
146
        $len = $this->length();
147
148
        if ($len < $this->pctSize)
149
        {
150
            $this->debug('Buffer not enouth, return');
151
            return;
152
        }
153
154
        $this->state = self::STATE_STANDBY;
155
156
        if ($this->phase === 0)
157
        {
158
            $this->phase = self::PHASE_GOT_INIT;
159
            $this->protocalVersion = ord($this->read(1));
160
            $this->debug(sprintf("Protocol Version: %d", $this->protocalVersion));
161
            if ($this->protocalVersion === 0xFF)
162
            {
163
                $fieldCount = $this->protocalVersion;
164
                $this->protocalVersion = 0;
165
                printf("Error:\n");
166
167
                $this->rsState = self::RS_STATE_HEADER;
168
                $this->resultFields = [];
169
                $this->resultRows = [];
170
                if ($this->phase === self::PHASE_AUTH_SENT || $this->phase === self::PHASE_GOT_INIT)
171
                {
172
                    $this->phase = self::PHASE_AUTH_ERR;
173
                }
174
175
                goto field;
176
            }
177
            if (($p = $this->search("\x00")) === false)
178
            {
179
                return;
180
            }
181
182
            $options = &$this->connectOptions;
183
184
            $options['serverVersion'] = $this->read($p, 1);
185
            $options['threadId']      = BinSupport::bytes2int($this->read(4), true);
186
            $this->scramble           = $this->read(8, 1);
187
            $options['ServerCaps']    = BinSupport::bytes2int($this->read(2), true);
188
            $options['serverLang']    = ord($this->read(1));
189
            $options['serverStatus']  = BinSupport::bytes2int($this->read(2, 13), true);
190
            $restScramble             = $this->read(12, 1);
191
            $this->scramble          .= $restScramble;
192
193
            $this->nextRequest(true);
194
        }
195
        else
196
        {
197
            $fieldCount = ord($this->read(1));
198
field:
199
            if ($fieldCount === 0xFF)
200
            {
201
                //error packet
202
                $u             = unpack('v', $this->read(2));
203
                $this->errno   = $u[1];
204
                $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...
205
                $this->errmsg  = $this->read($this->pctSize - $len + $this->length());
206
                $this->debug(sprintf("Error Packet:%d %s\n", $this->errno, $this->errmsg));
207
208
                $this->nextRequest();
209
                $this->onError();
210
            }
211
            else if ($fieldCount === 0x00)
212
            {
213
                $this->debug('Ok Packet');
214
215
                $isAuthenticated = false;
216
                if ($this->phase === self::PHASE_AUTH_SENT)
217
                {
218
                    $this->phase = self::PHASE_HANDSHAKED;
219
                    $isAuthenticated = true;
220
                }
221
222
                $this->affectedRows = $this->parseEncodedBinSupport();
223
                $this->insertId     = $this->parseEncodedBinSupport();
224
225
                $u                  = unpack('v', $this->read(2));
226
                $this->serverStatus = $u[1];
227
228
                $u                  = unpack('v', $this->read(2));
229
                $this->warnCount    = $u[1];
230
231
                $this->message      = $this->read($this->pctSize - $len + $this->length());
232
233
                if ($isAuthenticated)
234
                {
235
                    $this->onAuthenticated();
236
                }
237
                else
238
                {
239
                    $this->onSuccess();
240
                }
241
242
                $this->debug(sprintf("AffectedRows: %d, InsertId: %d, WarnCount:%d", $this->affectedRows, $this->insertId, $this->warnCount));
243
                $this->nextRequest();
244
245
            }
246
            // EOF
247
            else if ($fieldCount === 0xFE)
248
            {
249
                $this->debug('EOF Packet');
250
                if ($this->rsState === self::RS_STATE_ROW)
251
                {
252
                    $this->debug('result done');
253
254
                    $this->nextRequest();
255
                    $this->onResultDone();
256
                }
257
                else
258
                {
259
                    ++ $this->rsState;
260
                }
261
            }
262
            //Data packet
263
            else
264
            {
265
                $this->debug('Data Packet');
266
                $this->prepend(chr($fieldCount));
267
268
                if ($this->rsState === self::RS_STATE_HEADER)
269
                {
270
                    $this->debug('Header packet of Data packet');
271
                    $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...
272
                    //var_dump($extra);
273
                    $this->rsState = self::RS_STATE_FIELD;
274
                }
275
                else if ($this->rsState === self::RS_STATE_FIELD)
276
                {
277
                    $this->debug('Field packet of Data packet');
278
                    $field = [
279
                        'catalog'   => $this->parseEncodedString(),
280
                        'db'        => $this->parseEncodedString(),
281
                        'table'     => $this->parseEncodedString(),
282
                        'org_table' => $this->parseEncodedString(),
283
                        'name'      => $this->parseEncodedString(),
284
                        'org_name'  => $this->parseEncodedString()
285
                    ];
286
287
                    $this->skip(1);
288
                    $u                    = unpack('v', $this->read(2));
289
                    $field['charset']     = $u[1];
290
291
                    $u                    = unpack('v', $this->read(4));
292
                    $field['length']      = $u[1];
293
294
                    $field['type']        = ord($this->read(1));
295
296
                    $u                    = unpack('v', $this->read(2));
297
                    $field['flags']       = $u[1];
298
                    $field['decimals']    = ord($this->read(1));
299
                    //var_dump($field);
300
                    $this->resultFields[] = $field;
301
302
                }
303
                else if ($this->rsState === self::RS_STATE_ROW)
304
                {
305
                    $this->debug('Row packet of Data packet');
306
                    $row = [];
307
                    for ($i = 0, $nf = sizeof($this->resultFields); $i < $nf; ++$i)
308
                    {
309
                        $row[$this->resultFields[$i]['name']] = $this->parseEncodedString();
310
                    }
311
                    $this->resultRows[] = $row;
312
                    $command = $this->queue->dequeue();
313
                    $command->emit('result', array($row, $command, $command->getConnection()));
314
                    $this->queue->unshift($command);
315
                }
316
            }
317
        }
318
        $this->restBuffer($this->pctSize - $len + $this->length());
319
        goto packet;
320
    }
321
322
    protected function onError()
323
    {
324
        $command = $this->queue->dequeue();
325
        $error = new Exception($this->errmsg, $this->errno);
326
        $command->setError($error);
327
        $command->emit('error', array($error, $command, $command->getConnection()));
328
        $this->errmsg = '';
329
        $this->errno  = 0;
330
    }
331
332
    protected function onResultDone()
333
    {
334
        $command = $this->queue->dequeue();
335
        $command->resultRows   = $this->resultRows;
336
        $command->resultFields = $this->resultFields;
337
        $command->emit('results', array($this->resultRows, $command, $command->getConnection()));
338
        $command->emit('end', array($command, $command->getConnection()));
339
340
        $this->rsState      = self::RS_STATE_HEADER;
341
        $this->resultRows   = $this->resultFields = [];
342
    }
343
344
    protected function onSuccess()
345
    {
346
        $command = $this->queue->dequeue();
347
        if ($command->equals(Command::QUERY))
348
        {
349
            $command->affectedRows = $this->affectedRows;
350
            $command->insertId     = $this->insertId;
351
            $command->warnCount    = $this->warnCount;
352
            $command->message      = $this->message;
353
        }
354
        $command->emit('success', array($command, $command->getConnection()));
355
    }
356
357
    protected function onAuthenticated()
358
    {
359
        $command = $this->queue->dequeue();
360
        $command->emit('authenticated', array($this->connectOptions));
361
    }
362
363
    protected function handleClose()
364
    {
365
        $this->emit('close');
366
        if ($this->queue->count())
367
        {
368
            $command = $this->queue->dequeue();
369
            if ($command->equals(Command::QUIT))
370
            {
371
                $command->emit('success');
372
            }
373
        }
374
    }
375
376
    /* begin of buffer operation APIs */
377
378
    public function append($str)
379
    {
380
        $this->buffer .= $str;
381
    }
382
383
    public function prepend($str)
384
    {
385
        $this->buffer = $str . substr($this->buffer, $this->bufferPos);
386
        $this->bufferPos = 0;
387
    }
388
389
    public function read($len, $skiplen = 0)
390
    {
391
        if (strlen($this->buffer) - $this->bufferPos - $len - $skiplen < 0)
392
        {
393
            throw new \LogicException('Logic Error');
394
        }
395
        $buffer = substr($this->buffer, $this->bufferPos, $len);
396
        $this->bufferPos += $len;
397
        if ($skiplen)
398
        {
399
            $this->bufferPos += $skiplen;
400
        }
401
402
        return $buffer;
403
    }
404
405
    public function skip($len)
406
    {
407
        $this->bufferPos += $len;
408
    }
409
410
    public function restBuffer($len)
411
    {
412
        if(strlen($this->buffer) === ($this->bufferPos+$len))
413
        {
414
            $this->buffer = '';
415
        }
416
        else
417
        {
418
            $this->buffer = substr($this->buffer,$this->bufferPos+$len);
419
        }
420
        $this->bufferPos = 0;
421
    }
422
423
    public function length()
424
    {
425
        return strlen($this->buffer) - $this->bufferPos;
426
    }
427
428
    public function search($what)
429
    {
430
        if (($p = strpos($this->buffer, $what, $this->bufferPos)) !== false)
431
        {
432
            return $p - $this->bufferPos;
433
        }
434
435
        return false;
436
    }
437
    /* end of buffer operation APIs */
438
439
    public function authenticate()
440
    {
441
        if ($this->phase !== self::PHASE_GOT_INIT)
442
        {
443
            return;
444
        }
445
        $this->phase = self::PHASE_AUTH_SENT;
446
447
        $clientFlags =
448
            Protocol::CLIENT_LONG_PASSWORD |
449
            Protocol::CLIENT_LONG_FLAG |
450
            Protocol::CLIENT_LOCAL_FILES |
451
            Protocol::CLIENT_PROTOCOL_41 |
452
            Protocol::CLIENT_INTERACTIVE |
453
            Protocol::CLIENT_TRANSACTIONS |
454
            Protocol::CLIENT_SECURE_CONNECTION |
455
            Protocol::CLIENT_MULTI_RESULTS |
456
            Protocol::CLIENT_MULTI_STATEMENTS |
457
            Protocol::CLIENT_CONNECT_WITH_DB;
458
459
        $packet = pack('VVc', $clientFlags, $this->maxPacketSize, $this->charsetNumber)
460
            . "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
461
            . $this->user . "\x00"
462
            . $this->getAuthToken($this->scramble, $this->pass)
463
            . ($this->dbname ? $this->dbname . "\x00" : '');
464
465
        $this->sendPacket($packet);
466
        $this->debug('Auth packet sent');
467
    }
468
469
    public function getAuthToken($scramble, $password = '')
470
    {
471
        if ($password === '')
472
        {
473
            return "\x00";
474
        }
475
        $token = sha1($scramble . sha1($hash1 = sha1($password, true), true), true) ^ $hash1;
476
477
        return $this->buildLenEncodedBinSupport($token);
478
    }
479
480
    /**
481
     * Builds length-encoded BinSupport string
482
     * @param string String
483
     * @return string Resulting BinSupport string
484
     */
485
    public function buildLenEncodedBinSupport($s)
486
    {
487
        if ($s === null)
488
        {
489
            return "\251";
490
        }
491
492
        $l = strlen($s);
493
494
        if ($l <= 250)
495
        {
496
            return chr($l) . $s;
497
        }
498
499
        if ($l <= 0xFFFF)
500
        {
501
            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...
502
        }
503
504
        if ($l <= 0xFFFFFF)
505
        {
506
            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...
507
        }
508
509
        return BinSupport::int2bytes(8, $l, true) . $s;
510
    }
511
512
    /**
513
     * Parses length-encoded BinSupport integer
514
     * @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...
515
     */
516
    public function parseEncodedBinSupport()
517
    {
518
        $f = ord($this->read(1));
519
        if ($f <= 250)
520
        {
521
            return $f;
522
        }
523
        if ($f === 251)
524
        {
525
            return null;
526
        }
527
        if ($f === 255)
528
        {
529
            return false;
530
        }
531
        if ($f === 252)
532
        {
533
            return BinSupport::bytes2int($this->read(2), true);
534
        }
535
        if ($f === 253)
536
        {
537
            return BinSupport::bytes2int($this->read(3), true);
538
        }
539
540
        return BinSupport::bytes2int($this->read(8), true);
541
    }
542
543
    /**
544
     * Parse length-encoded string
545
     * @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...
546
     */
547
    public function parseEncodedString()
548
    {
549
        $l = $this->parseEncodedBinSupport();
550
        if (($l === null) || ($l === false))
551
        {
552
            return $l;
553
        }
554
555
        return $this->read($l);
556
    }
557
558
    public function sendPacket($packet)
559
    {
560
        return $this->stream->write(BinSupport::int2bytes(3, strlen($packet), true) . chr($this->seq++) . $packet);
561
    }
562
563
    protected function nextRequest($isHandshake = false)
564
    {
565
        if (!$isHandshake && $this->phase != self::PHASE_HANDSHAKED)
566
        {
567
            return false;
568
        }
569
        if (!$this->executor->isIdle())
570
        {
571
            $command = $this->executor->dequeue();
572
            $this->queue->enqueue($command);
573
            if ($command->equals(Command::INIT_AUTHENTICATE))
574
            {
575
                $this->authenticate();
576
            }
577
            else
578
            {
579
                $this->seq = 0;
580
                $this->sendPacket(chr($command->getID()) . $command->getSql());
581
            }
582
        }
583
584
        return true;
585
    }
586
}
587