Completed
Push — master ( 21bf70...cc5276 )
by Kamil
05:50
created

ProtocolParser::authenticate()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 27
ccs 0
cts 24
cp 0
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 21
nc 3
nop 0
crap 12
1
<?php
2
3
namespace Dazzle\MySQL\Protocol;
4
5
use Dazzle\Event\BaseEventEmitter;
6
use Dazzle\MySQL\Command\Command;
7
use Dazzle\MySQL\Protocol\Support\BinarySupport;
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 = true;
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
            $this->nextRequest();
105
        }
106
    }
107
108
    public function debug($message)
109
    {
110
        if ($this->debug) {
111
            $bt = debug_backtrace();
112
            $caller = array_shift($bt);
113
            printf("[DEBUG] <%s:%d> %s\n", $caller['class'], $caller['line'], $message);
114
        }
115
    }
116
117
    public function setOptions($options)
118
    {
119
        foreach ($options as $option => $value) {
120
            if (property_exists($this, $option)) {
121
                $this->$option = $value;
122
            }
123
        }
124
    }
125
126
    public function handleData($stream, $data)
127
    {
128
        $this->append($data);
129
packet:
130
        if ($this->state === self::STATE_STANDBY) {
131
            if ($this->length() < 4) {
132
                return;
133
            }
134
135
            $this->pctSize = BinarySupport::bytes2int($this->read(3), true);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Dazzle\MySQL\Protocol\S...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...
136
            //printf("packet size:%d\n", $this->pctSize);
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% 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...
137
            $this->state = self::STATE_BODY;
138
            $this->seq = ord($this->read(1)) + 1;
139
        }
140
141
        $len = $this->length();
142
        if ($len < $this->pctSize) {
143
            $this->debug('Buffer not enouth, return');
144
            return;
145
        }
146
        $this->state = self::STATE_STANDBY;
147
        //$this->stream->bufferSize = 4;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
148
        if ($this->phase === 0) {
149
            $this->phase = self::PHASE_GOT_INIT;
150
            $this->protocalVersion = ord($this->read(1));
151
            $this->debug(sprintf("Protocol Version: %d", $this->protocalVersion));
152
            if ($this->protocalVersion === 0xFF) { //error
153
                $fieldCount = $this->protocalVersion;
154
                $this->protocalVersion = 0;
155
                printf("Error:\n");
156
157
                $this->rsState = self::RS_STATE_HEADER;
158
                $this->resultFields = [];
159
                $this->resultRows = [];
160
                if ($this->phase === self::PHASE_AUTH_SENT || $this->phase === self::PHASE_GOT_INIT) {
161
                    $this->phase = self::PHASE_AUTH_ERR;
162
                }
163
164
                goto field;
165
            }
166
            if (($p = $this->search("\x00")) === false) {
167
                printf("Finish\n");
168
                //finish
169
                return;
170
            }
171
172
            $options = &$this->connectOptions;
173
174
            $options['serverVersion'] = $this->read($p, 1);
175
            $options['threadId']      = BinarySupport::bytes2int($this->read(4), true);
176
            $this->scramble           = $this->read(8, 1);
177
            $options['ServerCaps']    = BinarySupport::bytes2int($this->read(2), true);
178
            $options['serverLang']    = ord($this->read(1));
179
            $options['serverStatus']  = BinarySupport::bytes2int($this->read(2, 13), true);
180
            $restScramble             = $this->read(12, 1);
181
            $this->scramble          .= $restScramble;
182
183
            $this->nextRequest(true);
184
        } else {
185
            $fieldCount = ord($this->read(1));
186
field:
187
            if ($fieldCount === 0xFF) {
188
                //error packet
189
                $u             = unpack('v', $this->read(2));
190
                $this->errno   = $u[1];
191
                $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...
192
                $this->errmsg  = $this->read($this->pctSize - $len + $this->length());
193
                $this->debug(sprintf("Error Packet:%d %s\n", $this->errno, $this->errmsg));
194
195
                $this->nextRequest();
196
                $this->onError();
197
            } elseif ($fieldCount === 0x00) { //OK Packet Empty
198
                $this->debug('Ok Packet');
199
200
                $isAuthenticated = false;
201
                if ($this->phase === self::PHASE_AUTH_SENT) {
202
                    $this->phase = self::PHASE_HANDSHAKED;
203
                    $isAuthenticated = true;
204
                }
205
206
                $this->affectedRows = $this->parseEncodedBinarySupport();
207
                $this->insertId     = $this->parseEncodedBinarySupport();
208
209
                $u                  = unpack('v', $this->read(2));
210
                $this->serverStatus = $u[1];
211
212
                $u                  = unpack('v', $this->read(2));
213
                $this->warnCount    = $u[1];
214
215
                $this->message      = $this->read($this->pctSize - $len + $this->length());
216
217
                if ($isAuthenticated) {
218
                    $this->onAuthenticated();
219
                } else {
220
                    $this->onSuccess();
221
                }
222
                $this->debug(sprintf("AffectedRows: %d, InsertId: %d, WarnCount:%d", $this->affectedRows, $this->insertId, $this->warnCount));
223
                $this->nextRequest();
224
225
            } elseif ($fieldCount === 0xFE) { //EOF Packet
226
                $this->debug('EOF Packet');
227
                if ($this->rsState === self::RS_STATE_ROW) {
228
                    $this->debug('result done');
229
230
                    $this->nextRequest();
231
                    $this->onResultDone();
232
                } else {
233
                    ++ $this->rsState;
234
                }
235
236
            } else { //Data packet
237
                $this->debug('Data Packet');
238
                $this->prepend(chr($fieldCount));
239
240
                if ($this->rsState === self::RS_STATE_HEADER) {
241
                    $this->debug('Header packet of Data packet');
242
                    $extra = $this->parseEncodedBinarySupport();
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...
243
                    //var_dump($extra);
244
                    $this->rsState = self::RS_STATE_FIELD;
245
                } elseif ($this->rsState === self::RS_STATE_FIELD) {
246
                    $this->debug('Field packet of Data packet');
247
                    $field = [
248
                        'catalog'   => $this->parseEncodedString(),
249
                        'db'        => $this->parseEncodedString(),
250
                        'table'     => $this->parseEncodedString(),
251
                        'org_table' => $this->parseEncodedString(),
252
                        'name'      => $this->parseEncodedString(),
253
                        'org_name'  => $this->parseEncodedString()
254
                    ];
255
256
                    $this->skip(1);
257
                    $u                    = unpack('v', $this->read(2));
258
                    $field['charset']     = $u[1];
259
260
                    $u                    = unpack('v', $this->read(4));
261
                    $field['length']      = $u[1];
262
263
                    $field['type']        = ord($this->read(1));
264
265
                    $u                    = unpack('v', $this->read(2));
266
                    $field['flags']       = $u[1];
267
                    $field['decimals']    = ord($this->read(1));
268
                    //var_dump($field);
269
                    $this->resultFields[] = $field;
270
271
                } elseif ($this->rsState === self::RS_STATE_ROW) {
272
                    $this->debug('Row packet of Data packet');
273
                    $row = [];
274
                    for ($i = 0, $nf = sizeof($this->resultFields); $i < $nf; ++$i) {
275
                        $row[$this->resultFields[$i]['name']] = $this->parseEncodedString();
276
                    }
277
                    $this->resultRows[] = $row;
278
                    $command = $this->queue->dequeue();
279
                    $command->emit('result', array($row, $command, $command->getConnection()));
280
                    $this->queue->unshift($command);
281
                }
282
            }
283
        }
284
        $this->restBuffer($this->pctSize - $len + $this->length());
285
        goto packet;
286
    }
287
288
    protected function onError()
289
    {
290
        $command = $this->queue->dequeue();
291
        $error = new Exception($this->errmsg, $this->errno);
292
        $command->setError($error);
293
        $command->emit('error', array($error, $command, $command->getConnection()));
294
        $this->errmsg = '';
295
        $this->errno  = 0;
296
    }
297
298
    protected function onResultDone()
299
    {
300
        $command = $this->queue->dequeue();
301
        $command->resultRows   = $this->resultRows;
302
        $command->resultFields = $this->resultFields;
303
        $command->emit('results', array($this->resultRows, $command, $command->getConnection()));
304
        $command->emit('end', array($command, $command->getConnection()));
305
306
        $this->rsState      = self::RS_STATE_HEADER;
307
        $this->resultRows   = $this->resultFields = [];
308
    }
309
310
    protected function onSuccess()
311
    {
312
        $command = $this->queue->dequeue();
313
        if ($command->equals(Command::QUERY)) {
314
            $command->affectedRows = $this->affectedRows;
315
            $command->insertId     = $this->insertId;
316
            $command->warnCount    = $this->warnCount;
317
            $command->message      = $this->message;
318
        }
319
        $command->emit('success', array($command, $command->getConnection()));
320
    }
321
322
    protected function onAuthenticated()
323
    {
324
        $command = $this->queue->dequeue();
325
        $command->emit('authenticated', array($this->connectOptions));
326
    }
327
328
    protected function handleClose()
329
    {
330
        $this->emit('close');
331
        if ($this->queue->count()) {
332
            $command = $this->queue->dequeue();
333
            if ($command->equals(Command::QUIT)) {
334
                $command->emit('success');
335
            }
336
        }
337
    }
338
339
    /* begin of buffer operation APIs */
340
341
    public function append($str)
342
    {
343
        $this->buffer .= $str;
344
    }
345
346
    public function prepend($str)
347
    {
348
        $this->buffer = $str . substr($this->buffer, $this->bufferPos);
349
        $this->bufferPos = 0;
350
    }
351
352
    public function read($len, $skiplen = 0)
353
    {
354
        if (strlen($this->buffer) - $this->bufferPos - $len - $skiplen < 0) {
355
            throw new \LogicException('Logic Error');
356
        }
357
        $buffer = substr($this->buffer, $this->bufferPos, $len);
358
        $this->bufferPos += $len;
359
        if ($skiplen) {
360
            $this->bufferPos += $skiplen;
361
        }
362
363
        return $buffer;
364
    }
365
366
    public function skip($len)
367
    {
368
        $this->bufferPos += $len;
369
    }
370
371
    public function restBuffer($len)
372
    {
373
        if(strlen($this->buffer) === ($this->bufferPos+$len)){
374
            $this->buffer = '';
375
        }else{
376
            $this->buffer = substr($this->buffer,$this->bufferPos+$len);
377
        }
378
        $this->bufferPos = 0;
379
    }
380
381
    public function length()
382
    {
383
        return strlen($this->buffer) - $this->bufferPos;
384
    }
385
386
    public function search($what)
387
    {
388
        if (($p = strpos($this->buffer, $what, $this->bufferPos)) !== false) {
389
            return $p - $this->bufferPos;
390
        }
391
392
        return false;
393
    }
394
    /* end of buffer operation APIs */
395
396
    public function authenticate()
397
    {
398
        if ($this->phase !== self::PHASE_GOT_INIT) {
399
            return;
400
        }
401
        $this->phase = self::PHASE_AUTH_SENT;
402
403
        $clientFlags = Protocol::CLIENT_LONG_PASSWORD |
404
                Protocol::CLIENT_LONG_FLAG |
405
                Protocol::CLIENT_LOCAL_FILES |
406
                Protocol::CLIENT_PROTOCOL_41 |
407
                Protocol::CLIENT_INTERACTIVE |
408
                Protocol::CLIENT_TRANSACTIONS |
409
                Protocol::CLIENT_SECURE_CONNECTION |
410
                Protocol::CLIENT_MULTI_RESULTS |
411
                Protocol::CLIENT_MULTI_STATEMENTS |
412
                Protocol::CLIENT_CONNECT_WITH_DB;
413
414
        $packet = pack('VVc', $clientFlags, $this->maxPacketSize, $this->charsetNumber)
415
                . "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
416
                . $this->user . "\x00"
417
                . $this->getAuthToken($this->scramble, $this->pass)
418
                . ($this->dbname ? $this->dbname . "\x00" : '');
419
420
        $this->sendPacket($packet);
421
        $this->debug('Auth packet sent');
422
    }
423
424
    public function getAuthToken($scramble, $password = '')
425
    {
426
        if ($password === '') {
427
            return "\x00";
428
        }
429
        $token = sha1($scramble . sha1($hash1 = sha1($password, true), true), true) ^ $hash1;
430
431
        return $this->buildLenEncodedBinarySupport($token);
432
    }
433
434
    /**
435
     * Builds length-encoded BinarySupport string
436
     * @param string String
437
     * @return string Resulting BinarySupport string
438
     */
439
    public function buildLenEncodedBinarySupport($s)
440
    {
441
        if ($s === null) {
442
            return "\251";
443
        }
444
445
        $l = strlen($s);
446
447
        if ($l <= 250) {
448
            return chr($l) . $s;
449
        }
450
451
        if ($l <= 0xFFFF) {
452
            return "\252" . BinarySupport::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...
453
        }
454
455
        if ($l <= 0xFFFFFF) {
456
            return "\254" . BinarySupport::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...
457
        }
458
459
        return BinarySupport::int2bytes(8, $l, true) . $s;
460
    }
461
462
    /**
463
     * Parses length-encoded BinarySupport integer
464
     * @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...
465
     */
466
    public function parseEncodedBinarySupport()
467
    {
468
        $f = ord($this->read(1));
469
        if ($f <= 250) {
470
            return $f;
471
        }
472
        if ($f === 251) {
473
            return null;
474
        }
475
        if ($f === 255) {
476
            return false;
477
        }
478
        if ($f === 252) {
479
            return BinarySupport::bytes2int($this->read(2), true);
480
        }
481
        if ($f === 253) {
482
            return BinarySupport::bytes2int($this->read(3), true);
483
        }
484
485
        return BinarySupport::bytes2int($this->read(8), true);
486
    }
487
488
    /**
489
     * Parse length-encoded string
490
     * @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...
491
     */
492
    public function parseEncodedString()
493
    {
494
        $l = $this->parseEncodedBinarySupport();
495
        if (($l === null) || ($l === false)) {
496
            return $l;
497
        }
498
499
        return $this->read($l);
500
    }
501
502
    public function sendPacket($packet)
503
    {
504
        return $this->stream->write(BinarySupport::int2bytes(3, strlen($packet), true) . chr($this->seq++) . $packet);
505
    }
506
507
    protected function nextRequest($isHandshake = false)
508
    {
509
        if (!$isHandshake && $this->phase != self::PHASE_HANDSHAKED) {
510
            return false;
511
        }
512
        if (!$this->executor->isIdle()) {
513
            $command = $this->executor->dequeue();
514
            $this->queue->enqueue($command);
515
            if ($command->equals(Command::INIT_AUTHENTICATE)) {
516
                $this->authenticate();
517
            } else {
518
                $this->seq = 0;
519
                $this->sendPacket(chr($command->getID()) . $command->getSql());
520
            }
521
        }
522
523
        return true;
524
    }
525
}
526