GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( bd87d2...776093 )
by Charlotte
03:49
created

Driver::createHandshakeResponse()   B

Complexity

Conditions 11
Paths 2

Size

Total Lines 60
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 24.864

Importance

Changes 0
Metric Value
cc 11
eloc 35
nc 2
nop 5
dl 0
loc 60
ccs 18
cts 35
cp 0.5143
crap 24.864
rs 7.3166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Plasma Driver MySQL component
4
 * Copyright 2018 PlasmaPHP, All Rights Reserved
5
 *
6
 * Website: https://github.com/PlasmaPHP
7
 * License: https://github.com/PlasmaPHP/driver-mysql/blob/master/LICENSE
8
*/
9
10
namespace Plasma\Drivers\MySQL;
11
12
/**
13
 * The MySQL Driver.
14
 * @internal
15
 */
16
class Driver implements \Plasma\DriverInterface {
17
    use \Evenement\EventEmitterTrait;
18
    
19
    /**
20
     * @var \React\EventLoop\LoopInterface
21
     */
22
    protected $loop;
23
    
24
    /**
25
     * @var array
26
     */
27
    protected $options = array(
28
        'tls.context' => array(),
29
        'tls.force' => true,
30
        'tls.forceLocal' => false
31
    );
32
    
33
    /**
34
     * @var \React\Socket\ConnectorInterface
35
     */
36
    protected $connector;
37
    
38
    /**
39
     * Internal class is intentional used, as there's no other way currently.
40
     * @var \React\Socket\StreamEncryption
41
     * @see https://github.com/reactphp/socket/issues/180
42
     */
43
    protected $encryption;
44
    
45
    /**
46
     * @var \React\Promise\Promise|null
47
     */
48
    protected $connectPromise;
49
    
50
    /**
51
     * @var \React\Socket\Connection
52
     */
53
    protected $connection;
54
    
55
    /**
56
     * @var int
57
     */
58
    protected $connectionState = \Plasma\DriverInterface::CONNECTION_CLOSED;
59
    
60
    /**
61
     * @var \Plasma\Drivers\MySQL\ProtocolParser
62
     */
63
    protected $parser;
64
    
65
    /**
66
     * @var array
67
     */
68
    protected $queue;
69
    
70
    /**
71
     * @var int
72
     */
73
    protected $busy = \Plasma\DriverInterface::STATE_IDLE;
74
    
75
    /**
76
     * @var bool
77
     */
78
    protected $transaction = false;
79
    
80
    /**
81
     * @var \React\Promise\Deferred
82
     */
83
    protected $goingAway;
84
    
85
    /**
86
     * Constructor.
87
     * @param \React\EventLoop\LoopInterface  $loop
88
     * @param array                           $options
89
     */
90 31
    function __construct(\React\EventLoop\LoopInterface $loop, array $options) {
91 31
        $this->validateOptions($options);
92
        
93 31
        $this->loop = $loop;
94 31
        $this->options = \array_merge($this->options, $options);
95
        
96 31
        $this->connector = ($options['connector'] ?? (new \React\Socket\Connector($loop)));
97 31
        $this->encryption = new \React\Socket\StreamEncryption($this->loop, false);
98 31
        $this->queue = array();
99 31
    }
100
    
101
    /**
102
     * Returns the event loop.
103
     * @return \React\EventLoop\LoopInterface
104
     */
105 8
    function getLoop(): \React\EventLoop\LoopInterface {
106 8
        return $this->loop;
107
    }
108
    
109
    /**
110
     * Retrieves the current connection state.
111
     * @return int
112
     */
113 6
    function getConnectionState(): int {
114 6
        return $this->connectionState;
115
    }
116
    
117
    /**
118
     * Retrieves the current busy state.
119
     * @return int
120
     */
121 1
    function getBusyState(): int {
122 1
        return $this->busy;
123
    }
124
    
125
    /**
126
     * Get the length of the driver backlog queue.
127
     * @return int
128
     */
129 1
    function getBacklogLength(): int {
130 1
        return \count($this->queue);
131
    }
132
    
133
    /**
134
     * Connects to the given URI.
135
     * @param string  $uri
136
     * @return \React\Promise\PromiseInterface
137
     */
138 15
    function connect(string $uri): \React\Promise\PromiseInterface {
139 15
        if($this->goingAway || $this->connectionState === \Plasma\DriverInterface::CONNECTION_UNUSABLE) {
140 1
            return \React\Promise\reject((new \Plasma\Exception('Connection is going away')));
141 14
        } elseif($this->connectionState === \Plasma\DriverInterface::CONNECTION_OK) {
142 1
            return \React\Promise\resolve();
143 14
        } elseif($this->connectPromise !== null) {
144 1
            return $this->connectPromise;
145
        }
146
        
147 14
        $uri = 'mysql://'.\ltrim($uri, 'mysql://');
148
        
149 14
        $parts = \parse_url($uri);
150 14
        if(!isset($parts['host'])) {
151 1
            return \React\Promise\reject((new \InvalidArgumentException('Invalid connect uri given')));
152
        }
153
        
154 13
        $host = $parts['host'].':'.($parts['port'] ?? 3306);
155 13
        $this->connectionState = static::CONNECTION_STARTED;
156 13
        $resolved = false;
157
        
158 13
        if(!empty($parts['query'])) {
159 1
            \parse_str($parts['query'], $query);
160 1
            $charset = $query['charset'] ?? null;
161 1
            $collate = $query['collate'] ?? null;
162
            
163 1
            unset($query);
164
        } else {
165 12
            $charset = null;
166 12
            $collate = null;
167
        }
168
        
169
        $this->connectPromise =  $this->connector->connect($host)->then(function (\React\Socket\ConnectionInterface $connection) use ($parts, &$resolved) {
170
            // See description of property encryption
171 13
            if(!($connection instanceof \React\Socket\Connection)) {
172
                throw new \LogicException('Custom connection class is NOT supported yet (encryption limitation)');
173
            }
174
            
175 13
            $this->busy = static::STATE_BUSY;
176 13
            $this->connectionState = static::CONNECTION_MADE;
177 13
            $this->connection = $connection;
178
            
179
            $this->connection->on('error', function (\Throwable $error) {
180
                $this->emit('error', array($error));
181 13
            });
182
            
183
            $this->connection->on('close', function () {
184 3
                $this->connection = null;
185 3
                $this->connectionState = static::CONNECTION_UNUSABLE;
186
                
187 3
                $this->emit('close');
188 13
            });
189
            
190 13
            $deferred = new \React\Promise\Deferred();
191 13
            $this->parser = new \Plasma\Drivers\MySQL\ProtocolParser($this, $this->connection);
192
            
193
            $this->parser->on('error', function (\Throwable $error) use (&$deferred, &$resolved) {
194
                if($resolved) {
195
                    $this->emit('error', array($error));
196
                } else {
197
                    $deferred->reject($error);
198
                }
199 13
            });
200
            
201 13
            $user = ($parts['user'] ?? 'root');
202 13
            $password = ($parts['pass'] ?? '');
203 13
            $db = (!empty($parts['path']) ? \ltrim($parts['path'], '/') : '');
204
            
205 13
            $credentials = \compact('user', 'password', 'db');
206
            
207 13
            $this->startHandshake($credentials, $deferred);
208
            return $deferred->promise()->then(function () use (&$resolved) {
209 12
                $this->busy = static::STATE_IDLE;
210 12
                $resolved = true;
211
                
212 12
                if(\count($this->queue) > 0) {
213
                    $this->parser->invokeCommand($this->getNextCommand());
214
                }
215 13
            });
216 13
        });
217
        
218 13
        if($charset) {
219
            $this->connectPromise = $this->connectPromise->then(function () use ($charset, $collate) {
220 1
                $query = 'SET NAMES "'.$charset.'"'.($collate ? ' COLLATE "'.$collate.'"' : '');
221
                
222 1
                $cmd = new \Plasma\Drivers\MySQL\Commands\QueryCommand($this, $query);
223 1
                $this->executeCommand($cmd);
224 1
            });
225
        }
226
        
227 13
        return $this->connectPromise;
228
    }
229
    
230
    /**
231
     * Pauses the underlying stream I/O consumption.
232
     * If consumption is already paused, this will do nothing.
233
     * @return bool  Whether the operation was successful.
234
     */
235 1
    function pauseStreamConsumption(): bool {
236 1
        if($this->connection === null || $this->goingAway) {
237 1
            return false;
238
        }
239
        
240
        $this->connection->pause();
241
        return true;
242
    }
243
    
244
    /**
245
     * Resumes the underlying stream I/O consumption.
246
     * If consumption is not paused, this will do nothing.
247
     * @return bool  Whether the operation was successful.
248
     */
249 1
    function resumeStreamConsumption(): bool {
250 1
        if($this->connection === null || $this->goingAway) {
251 1
            return false;
252
        }
253
        
254
        $this->connection->resume();
255
        return true;
256
    }
257
    
258
    /**
259
     * Closes all connections gracefully after processing all outstanding requests.
260
     * @return \React\Promise\PromiseInterface
261
     */
262 2
    function close(): \React\Promise\PromiseInterface {
263 2
        if($this->goingAway) {
264 2
            return $this->goingAway->promise();
265
        }
266
        
267 1
        $state = $this->connectionState;
268 1
        $this->connectionState = \Plasma\DriverInterface::CONNECTION_UNUSABLE;
269
        
270 1
        $this->goingAway = new \React\Promise\Deferred();
271
        
272 1
        if(\count($this->queue) === 0) {
273
            $this->goingAway->resolve();
274
        }
275
        
276
        return $this->goingAway->promise()->then(function () use ($state) {
277 1
            if($state !== static::CONNECTION_OK) {
278
                return;
279
            }
280
            
281 1
            $deferred = new \React\Promise\Deferred();
282
            
283 1
            $quit = new \Plasma\Drivers\MySQL\Commands\QuitCommand();
284
            
285
            $this->connection->once('close', function () use (&$deferred) {
286 1
                $deferred->resolve();
287 1
            });
288
            
289
            $quit->once('end', function () {
290
                $this->connection->close();
291 1
            });
292
            
293 1
            $this->parser->invokeCommand($quit);
294
            
295 1
            return $deferred->promise();
296 1
        });
297
    }
298
    
299
    /**
300
     * Forcefully closes the connection, without waiting for any outstanding requests. This will reject all outstanding requests.
301
     * @return void
302
     */
303 10
    function quit(): void {
304 10
        if($this->goingAway) {
305 1
            return;
306
        }
307
        
308 10
        $state = $this->connectionState;
309 10
        $this->connectionState = \Plasma\DriverInterface::CONNECTION_UNUSABLE;
310
        
311 10
        $this->goingAway = new \React\Promise\Deferred();
312 10
        $this->goingAway->resolve();
313
        
314
        /** @var \Plasma\Drivers\MySQL\Commands\CommandInterface  $command */
315 10
        while($command = \array_shift($this->queue)) {
316 1
            $command->emit('error', array((new \Plasma\Exception('Connection is going away'))));
317
        }
318
        
319 10
        if($state === static::CONNECTION_OK) {
320 1
            $quit = new \Plasma\Drivers\MySQL\Commands\QuitCommand();
321 1
            $this->parser->invokeCommand($quit);
322
            
323 1
            $this->connection->close();
324
        }
325 10
    }
326
    
327
    /**
328
     * Whether this driver is currently in a transaction.
329
     * @return bool
330
     */
331 2
    function isInTransaction(): bool {
332 2
        return $this->transaction;
333
    }
334
    
335
    /**
336
     * Executes a plain query. Resolves with a `QueryResultInterface` instance.
337
     * When the command is done, the driver must check itself back into the client.
338
     * @param \Plasma\ClientInterface  $client
339
     * @param string                   $query
340
     * @return \React\Promise\PromiseInterface
341
     * @throws \Plasma\Exception
342
     * @see \Plasma\QueryResultInterface
343
     */
344 7
    function query(\Plasma\ClientInterface $client, string $query): \React\Promise\PromiseInterface {
345 7
        if($this->goingAway) {
346 1
            return \React\Promise\reject((new \Plasma\Exception('Connection is going away')));
347 6
        } elseif($this->connectionState !== \Plasma\DriverInterface::CONNECTION_OK) {
348 1
            if($this->connectPromise !== null) {
349
                return $this->connectPromise->then(function () use (&$client, &$query) {
350
                    return $this->query($client, $query);
351
                });
352
            }
353
            
354 1
            throw new \Plasma\Exception('You forgot to call Driver::connect()!');
355
        }
356
        
357 5
        $command = new \Plasma\Drivers\MySQL\Commands\QueryCommand($this, $query);
358 5
        $this->executeCommand($command);
359
        
360 5
        if(!$this->transaction) {
361
            $command->once('end', function () use (&$client) {
362 3
                $client->checkinConnection($this);
363 3
            });
364
        }
365
        
366 5
        return $command->getPromise();
367
    }
368
    
369
    /**
370
     * Prepares a query. Resolves with a `StatementInterface` instance.
371
     * When the command is done, the driver must check itself back into the client.
372
     * @param \Plasma\ClientInterface  $client
373
     * @param string                   $query
374
     * @return \React\Promise\PromiseInterface
375
     * @throws \Plasma\Exception
376
     * @see \Plasma\StatementInterface
377
     */
378 4
    function prepare(\Plasma\ClientInterface $client, string $query): \React\Promise\PromiseInterface {
379 4
        if($this->goingAway) {
380 1
            return \React\Promise\reject((new \Plasma\Exception('Connection is going away')));
381 3
        } elseif($this->connectionState !== \Plasma\DriverInterface::CONNECTION_OK) {
382 1
            if($this->connectPromise !== null) {
383
                return $this->connectPromise->then(function () use (&$client, &$query) {
384
                    return $this->prepare($client, $query);
385
                });
386
            }
387
            
388 1
            throw new \Plasma\Exception('You forgot to call Driver::connect()!');
389
        }
390
        
391 2
        $command = new \Plasma\Drivers\MySQL\Commands\StatementPrepareCommand($client, $this, $query);
392 2
        $this->executeCommand($command);
393
        
394 2
        return $command->getPromise();
395
    }
396
    
397
    /**
398
     * Prepares and executes a query. Resolves with a `QueryResultInterface` instance.
399
     * This is equivalent to prepare -> execute -> close.
400
     * If you need to execute a query multiple times, prepare the query manually for performance reasons.
401
     * @param \Plasma\ClientInterface  $client
402
     * @param string                   $query
403
     * @param array                    $params
404
     * @return \React\Promise\PromiseInterface
405
     * @throws \Plasma\Exception
406
     * @see \Plasma\StatementInterface
407
     */
408 3
    function execute(\Plasma\ClientInterface $client, string $query, array $params = array()): \React\Promise\PromiseInterface {
409 3
        if($this->goingAway) {
410 1
            return \React\Promise\reject((new \Plasma\Exception('Connection is going away')));
411 2
        } elseif($this->connectionState !== \Plasma\DriverInterface::CONNECTION_OK) {
412 1
            if($this->connectPromise !== null) {
413
                return $this->connectPromise->then(function () use (&$client, &$query, $params) {
414
                    return $this->execute($client, $query, $params);
415
                });
416
            }
417
            
418 1
            throw new \Plasma\Exception('You forgot to call Driver::connect()!');
419
        }
420
        
421
        return $this->prepare($client, $query)->then(function (\Plasma\StatementInterface $statement) use ($params) {
422
            return $statement->execute($params)->then(function (\Plasma\QueryResultInterface $result) use (&$statement) {
423 1
                if($result instanceof \Plasma\StreamQueryResultInterface) {
424
                    $statement->close(null, function (\Throwable $error) {
425
                        $this->emit('error', array($error));
426 1
                    });
427
                    
428 1
                    return $result;
429
                }
430
                
431
                return $statement->close()->then(function () use ($result) {
432
                    return $result;
433
                });
434
            }, function (\Throwable $error) use (&$statement) {
435
                return $statement->close()->then(function () use ($error) {
436
                    throw $error;
437
                });
438 1
            });
439 1
        });
440
    }
441
    
442
    /**
443
     * Quotes the string for use in the query.
444
     * @param string  $str
445
     * @return string
446
     * @throws \LogicException  Thrown if the driver does not support quoting.
447
     * @throws \Plasma\Exception
448
     */
449
    function quote(string $str): string {
450
        throw new \LogicException('Not implemented yet'); // TODO
451
    }
452
    
453
    /**
454
     * Begins a transaction. Resolves with a `TransactionInterface` instance.
455
     *
456
     * Checks out a connection until the transaction gets committed or rolled back.
457
     * It must be noted that the user is responsible for finishing the transaction. The client WILL NOT automatically
458
     * check the connection back into the pool, as long as the transaction is not finished.
459
     *
460
     * Some databases, including MySQL, automatically issue an implicit COMMIT when a database definition language (DDL)
461
     * statement such as DROP TABLE or CREATE TABLE is issued within a transaction.
462
     * The implicit COMMIT will prevent you from rolling back any other changes within the transaction boundary.
463
     * @param \Plasma\ClientInterface  $client
464
     * @param int                      $isolation  See the `TransactionInterface` constants.
465
     * @return \React\Promise\PromiseInterface
466
     * @throws \Plasma\Exception
467
     * @see \Plasma\TransactionInterface
468
     */
469 3
    function beginTransaction(\Plasma\ClientInterface $client, int $isolation = \Plasma\TransactionInterface::ISOLATION_COMMITTED): \React\Promise\PromiseInterface {
470 3
        if($this->goingAway) {
471 1
            return \React\Promise\reject((new \Plasma\Exception('Connection is going away')));
472
        }
473
        
474 2
        if($this->transaction) {
475 1
            throw new \Plasma\Exception('Driver is already in transaction');
476
        }
477
        
478
        switch ($isolation) {
479 2
            case \Plasma\TransactionInterface::ISOLATION_UNCOMMITTED:
480
                $query = 'SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED';
481
            break;
482 2
            case \Plasma\TransactionInterface::ISOLATION_COMMITTED:
483 2
                $query = 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED';
484 2
            break;
485
            case \Plasma\TransactionInterface::ISOLATION_REPEATABLE:
486
                $query = 'SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ';
487
            break;
488
            case \Plasma\TransactionInterface::ISOLATION_SERIALIZABLE:
489
                $query = 'SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE';
490
            break;
491
            default:
492
                throw new \Plasma\Exception('Invalid isolation level given');
493
            break;
494
        }
495
        
496 2
        $this->transaction = true;
497
        
498
        return $this->query($client, $query)->then(function () use (&$client) {
499 2
            return $this->query($client, 'START TRANSACTION');
500
        })->then(function () use (&$client, $isolation) {
501 2
            return (new \Plasma\Transaction($client, $this, $isolation));
502
        })->then(null, function (\Throwable $e) {
503
            $this->transaction = false;
504
            throw $e;
505 2
        });
506
    }
507
    
508
    /**
509
     * Informationally closes a transaction. This method is used by `Transaction` to inform the driver of the end of the transaction.
510
     * @return void
511
     */
512 1
    function endTransaction(): void {
513 1
        $this->transaction = false;
514 1
    }
515
    
516
    /**
517
     * Runs the given command.
518
     * Returns a Promise, which resolves with the `end` event argument (defaults to `null),
519
     * or rejects with the `Throwable` of the `error` event.
520
     * When the command is done, the driver must check itself back into the client.
521
     * @param \Plasma\ClientInterface   $client
522
     * @param \Plasma\CommandInterface  $command
523
     * @return \React\Promise\PromiseInterface
524
     */
525 5
    function runCommand(\Plasma\ClientInterface $client, \Plasma\CommandInterface $command) {
526 5
        if($this->goingAway) {
527 1
            return \React\Promise\reject((new \Plasma\Exception('Connection is going away')));
528
        }
529
        
530
        return (new \React\Promise\Promise(function (callable $resolve, callable $reject) use (&$client, &$command) {
531
            $command->once('end', function ($value = null) use (&$client, &$resolve) {
532 2
                if(!$this->transaction) {
533 2
                    $client->checkinConnection($this);
534
                }
535
                
536 2
                $resolve($value);
537 4
            });
538
            
539
            $command->once('error', function (\Throwable $error) use (&$client, &$reject) {
540 2
                if(!$this->transaction) {
541 2
                    $client->checkinConnection($this);
542
                }
543
                
544 2
                $reject($error);
545 4
            });
546
            
547 4
            $this->executeCommand($command);
548 4
        }));
549
    }
550
    
551
    /**
552
     * Executes a command.
553
     * @param \Plasma\CommandInterface  $command
554
     * @return void
555
     * @internal
556
     */
557 11
    function executeCommand(\Plasma\CommandInterface $command): void {
558 11
        $this->queue[] = $command;
559
        //\assert((\Plasma\Drivers\MySQL\Messages\MessageUtility::debug('Command '.get_class($command).' added to queue') || true));
560
        
561 11
        if($this->parser && $this->busy === static::STATE_IDLE) {
562
            //\assert((\Plasma\Drivers\MySQL\Messages\MessageUtility::debug('Command '.get_class($command).' invoked into parser') || true));
563 10
            $this->parser->invokeCommand($this->getNextCommand());
564
        }
565 11
    }
566
    
567
    /**
568
     * Get the handshake message, or null if none received yet.
569
     * @return \Plasma\Drivers\MySQL\Messages\HandshakeMessage|null
570
     */
571 7
    function getHandshake(): ?\Plasma\Drivers\MySQL\Messages\HandshakeMessage {
572 7
        if($this->parser) {
573 7
            return $this->parser->getHandshakeMessage();
574
        }
575
        
576
        return null;
577
    }
578
    
579
    /**
580
     * Get the next command, or null.
581
     * @return \Plasma\CommandInterface|null
582
     * @internal
583
     */
584 10
    function getNextCommand(): ?\Plasma\CommandInterface {
585 10
        if(\count($this->queue) === 0) {
586 6
            if($this->goingAway) {
587
                $this->goingAway->resolve();
588
            }
589
            
590 6
            return null;
591 10
        } elseif($this->busy === static::STATE_BUSY) {
592
            return null;
593
        }
594
        
595
        /** @var \Plasma\CommandInterface  $command */
596 10
        $command =  \array_shift($this->queue);
597
        
598
        //\assert((\Plasma\Drivers\MySQL\Messages\MessageUtility::debug('Unshifted command '.get_class($command)) || true));
599
        
600 10
        if($command->waitForCompletion()) {
601 10
            $this->busy = static::STATE_BUSY;
602
            
603
            $command->once('error', function () use (&$command) {
604
                //\assert((\Plasma\Drivers\MySQL\Messages\MessageUtility::debug('Command '.get_class($command).' errored') || true));
605 2
                $this->busy = static::STATE_IDLE;
606
                
607 2
                $this->endCommand();
608 10
            });
609
            
610
            $command->once('end', function () use (&$command) {
611
                //\assert((\Plasma\Drivers\MySQL\Messages\MessageUtility::debug('Command '.get_class($command).' ended') || true));
612 9
                $this->busy = static::STATE_IDLE;
613
                
614 9
                $this->endCommand();
615 10
            });
616
        } else {
617 2
            $this->endCommand();
618
        }
619
        
620 10
        return $command;
621
    }
622
    
623
    /**
624
     * Finishes up a command.
625
     * @return void
626
     */
627 10
    protected function endCommand() {
628
        $this->loop->futureTick(function () {
629 8
            if($this->goingAway && \count($this->queue) === 0) {
630 1
                return $this->goingAway->resolve();
631
            }
632
            
633 8
            $this->parser->invokeCommand($this->getNextCommand());
634 10
        });
635 10
    }
636
    
637
    /**
638
     * Starts the handshake process.
639
     * @param array                    $credentials
640
     * @param \React\Promise\Deferred  $deferred
641
     * @return void
642
     */
643 13
    protected function startHandshake(array $credentials, \React\Promise\Deferred $deferred) {
644
        $listener = function (\Plasma\Drivers\MySQL\Messages\MessageInterface $message) use ($credentials, &$deferred, &$listener) {
645 13
            if($message instanceof \Plasma\Drivers\MySQL\Messages\HandshakeMessage) {
646 13
                $this->parser->removeListener('message', $listener);
647
                
648 13
                $this->connectionState = static::CONNECTION_SETENV;
649 13
                $clientFlags = \Plasma\Drivers\MySQL\ProtocolParser::CLIENT_CAPABILITIES;
650
                
651 13
                \extract($credentials);
652
                
653 13
                if($db !== '') {
654 5
                    $clientFlags |= \Plasma\Drivers\MySQL\CapabilityFlags::CLIENT_CONNECT_WITH_DB;
655
                }
656
                
657
                // Check if we support auth plugins
658 13
                $plugins = \Plasma\Drivers\MySQL\DriverFactory::getAuthPlugins();
659 13
                $plugin = null;
660
                
661 13
                foreach($plugins as $key => $plug) {
662 13
                    if(\is_int($key) && ($message->capability & $key) !== 0) {
663 13
                        $plugin = $plug;
664 13
                        $clientFlags |= \Plasma\Drivers\MySQL\CapabilityFlags::CLIENT_PLUGIN_AUTH;
665 13
                        break;
666
                    } elseif($key === $message->authPluginName) {
667
                        $plugin = $plug;
668
                        $clientFlags |= \Plasma\Drivers\MySQL\CapabilityFlags::CLIENT_PLUGIN_AUTH;
669
                        break;
670
                    }
671
                }
672
                
673 13
                $remote = \parse_url($this->connection->getRemoteAddress())['host'];
674
                
675 13
                if($remote !== '127.0.0.1' || $this->options['tls.forceLocal']) {
676
                    if(($message->capability & \Plasma\Drivers\MySQL\CapabilityFlags::CLIENT_SSL) !== 0) { // If SSL supported, connect through SSL
677
                        $clientFlags |= \Plasma\Drivers\MySQL\CapabilityFlags::CLIENT_SSL;
678
                        
679
                        $ssl = new \Plasma\Drivers\MySQL\Commands\SSLRequestCommand($message, $clientFlags);
680
                        
681
                        $ssl->once('end', function () use ($credentials, $clientFlags, $plugin, &$deferred, &$message) {
682
                            $this->connectionState = static::CONNECTION_SSL_STARTUP;
683
                            
684
                            $this->enableTLS()->then(function () use ($credentials, $clientFlags, $plugin, &$deferred, &$message) {
685
                                $this->createHandshakeResponse($message, $credentials, $clientFlags, $plugin, $deferred);
686
                            }, function (\Throwable $error) use (&$deferred) {
687
                                $deferred->reject($$error);
688
                                $this->connection->close();
689
                            });
690
                        });
691
                        
692
                        return $this->parser->invokeCommand($ssl);
693
                    } elseif($this->options['tls.force'] || $this->options['tls.forceLocal']) {
694
                        $deferred->reject((new \Plasma\Exception('TLS is not supported by the server')));
695
                        $this->connection->close();
696
                        return;
697
                    }
698
                }
699
                
700 13
                $this->createHandshakeResponse($message, $credentials, $clientFlags, $plugin, $deferred);
701
            }
702 13
        };
703
        
704 13
        $this->parser->on('message', $listener);
705
        
706
        $this->parser->on('message', function (\Plasma\Drivers\MySQL\Messages\MessageInterface $message) {
707 13
            if($message instanceof \Plasma\Drivers\MySQL\Messages\OkResponseMessage) {
708 12
                $this->connectionState = static::CONNECTION_OK;
709
            }
710
            
711 13
            $this->emit('eventRelay', array('message', $message));
712 13
        });
713 13
    }
714
    
715
    /**
716
     * Enables TLS on the connection.
717
     * @return \React\Promise\PromiseInterface
718
     */
719
    protected function enableTLS(): \React\Promise\PromiseInterface {
720
        // Set required SSL/TLS context options
721
        foreach($this->options['tls.context'] as $name => $value) {
722
            \stream_context_set_option($this->connection->stream, 'ssl', $name, $value);
723
        }
724
        
725
        return $this->encryption->enable($this->connection)->then(null, function (\Throwable $error) {
726
            $this->connection->close();
727
            throw new \RuntimeException('Connection failed during TLS handshake: '.$error->getMessage(), $error->getCode());
728
        });
729
    }
730
    
731
    /**
732
     * Sends the auth command.
733
     * @param \Plasma\Drivers\MySQL\Messages\HandshakeMessage  $message
734
     * @param array                                            $credentials
735
     * @param int                                              $clientFlags
736
     * @param string|null                                      $plugin
737
     * @param \React\Promise\Deferred                          $deferred
738
     * @return void
739
     */
740 13
    protected function createHandshakeResponse(
741
        \Plasma\Drivers\MySQL\Messages\HandshakeMessage $message, array $credentials, int $clientFlags, ?string $plugin, \React\Promise\Deferred $deferred
742
    ) {
743 13
        \extract($credentials);
744
        
745 13
        $auth = new \Plasma\Drivers\MySQL\Commands\HandshakeResponseCommand($this->parser, $message, $clientFlags, $plugin, $user, $password, $db);
746
        
747
        $auth->once('end', function () use (&$deferred) {
748 12
            $deferred->resolve();
749 13
        });
750
        
751
        $auth->once('error', function (\Throwable $error) use (&$deferred) {
752 1
            $deferred->reject($error);
753 1
            $this->connection->close();
754 13
        });
755
        
756 13
        if($plugin) {
757
            $listener = function (\Plasma\Drivers\MySQL\Messages\MessageInterface $message) use ($password, &$deferred, &$listener) {
758
                /** @var \Plasma\Drivers\MySQL\AuthPlugins\AuthPluginInterface|null  $plugin */
759 13
                static $plugin;
760
                
761 13
                if($message instanceof \Plasma\Drivers\MySQL\Messages\AuthSwitchRequestMessage) {
762
                    $name = $message->authPluginName;
763
                    
764
                    if($name !== null) {
765
                        $plugins = \Plasma\Drivers\MySQL\DriverFactory::getAuthPlugins();
766
                        foreach($plugins as $key => $plug) {
767
                            if($key === $name) {
768
                                $plugin = new $plug($this->parser, $this->parser->getHandshakeMessage());
769
                                
770
                                $command = new \Plasma\Drivers\MySQL\Commands\AuthSwitchResponseCommand($message, $plugin, $password);
771
                                return $this->parser->invokeCommand($command);
772
                            }
773
                        }
774
                    }
775
                    
776
                    $deferred->reject((new \Plasma\Exception('Requested authentication method '.($name ? '"'.$name.'" ' : '').'is not supported')));
777 13
                } elseif($message instanceof \Plasma\Drivers\MySQL\Messages\AuthMoreDataMessage) {
778
                    if($plugin === null) {
779
                        $deferred->reject((new \Plasma\Exception('No auth plugin is in use, but we received auth more data packet')));
780
                        return $this->connection->close();
781
                    }
782
                    
783
                    try {
784
                        $command = $plugin->receiveMoreData($message);
785
                        return $this->parser->invokeCommand($command);
786
                    } catch (\Plasma\Exception $e) {
787
                        $deferred->reject($e);
788
                        $this->connection->close();
789
                    }
790 13
                } elseif($message instanceof \Plasma\Drivers\MySQL\Messages\OkResponseMessage) {
791 12
                    $this->parser->removeListener('message', $listener);
792
                }
793 13
            };
794
            
795 13
            $this->parser->on('message', $listener);
796
        }
797
        
798 13
        $this->parser->invokeCommand($auth);
799 13
        $this->connectionState = static::CONNECTION_AWAITING_RESPONSE;
800 13
    }
801
    
802
    /**
803
     * Validates the given options.
804
     * @param array  $options
805
     * @return void
806
     * @throws \InvalidArgumentException
807
     */
808 31
    protected function validateOptions(array $options) {
809 31
        $validator = \CharlotteDunois\Validation\Validator::make($options, array(
810 31
            'connector' => 'class:\React\Socket\ConnectorInterface=object',
811
            'tls.context' => 'array',
812
            'tls.force' => 'boolean',
813
            'tls.forceLocal' => 'boolean'
814
        ));
815
        
816 31
        $validator->throw(\InvalidArgumentException::class);
817 31
    }
818
}
819