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.
Failed Conditions
Push — master ( c06fef...396319 )
by Charlotte
03:16
created

src/Driver.php (1 issue)

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

735
                                $plugin = new $plug($this->parser, /** @scrutinizer ignore-type */ $message);
Loading history...
736
                                
737
                                $command = new \Plasma\Drivers\MySQL\Commands\AuthSwitchResponseCommand($message, $plugin, $password);
738
                                return $this->parser->invokeCommand($command);
739
                            }
740
                        }
741
                    }
742
                    
743
                    $deferred->reject((new \Plasma\Exception('Requested authentication method '.($name ? '"'.$name.'" ' : '').'is not supported')));
744 12
                } elseif($message instanceof \Plasma\Drivers\MySQL\Messages\AuthMoreDataMessage) {
745
                    if($plugin === null) {
746
                        $deferred->reject((new \Plasma\Exception('No auth plugin is in use, but we received auth more data packet')));
747
                        return $this->connection->close();
748
                    }
749
                    
750
                    try {
751
                        $command = $plugin->receiveMoreData($message);
752
                        return $this->parser->invokeCommand($command);
753
                    } catch (\Plasma\Exception $e) {
754
                        $deferred->reject($e);
755
                        $this->connection->close();
756
                    }
757 12
                } elseif($message instanceof \Plasma\Drivers\MySQL\Messages\OkResponseMessage) {
758 10
                    $this->parser->removeListener('message', $listener);
759
                }
760 12
            };
761
            
762 12
            $this->parser->on('message', $listener);
763
        }
764
        
765 12
        $this->parser->invokeCommand($auth);
766 12
        $this->connectionState = static::CONNECTION_AWAITING_RESPONSE;
767 12
    }
768
    
769
    /**
770
     * Validates the given options.
771
     * @param array  $options
772
     * @return void
773
     * @throws \InvalidArgumentException
774
     */
775 17
    protected function validateOptions(array $options) {
776 17
        $validator = \CharlotteDunois\Validation\Validator::make($options, array(
777 17
            'connector' => 'class:\React\Socket\ConnectorInterface=object',
778
            'tls.context' => 'array',
779
            'tls.force' => 'boolean',
780
            'tls.forceLocal' => 'boolean'
781
        ));
782
        
783 17
        $validator->throw(\InvalidArgumentException::class);
784 17
    }
785
}
786