Repository navigation
Require MySQL proxy authentication and default to loopback #540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
25b41d4
e52a5e0
eb2f5dd
01f154b
3367db3
ac7b5d7
a067506
75cf662
6f2343b
d220655
e7bde02
d97ff1b
53c22a7
2385111
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,50 +1,143 @@ | ||
| <?php declare( strict_types = 1 ); | ||
|
|
||
| use WP_MySQL_Proxy\MySQL_Authentication; | ||
| use WP_MySQL_Proxy\MySQL_Protocol; | ||
| use WP_MySQL_Proxy\MySQL_Proxy; | ||
| use WP_MySQL_Proxy\Adapter\SQLite_Adapter; | ||
| use WP_MySQL_Proxy\Logger; | ||
|
|
||
| require_once $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php'; | ||
|
|
||
| // Process CLI arguments: | ||
| $shortopts = 'hd:p:l:'; | ||
| $longopts = array( 'help', 'database:', 'port:', 'log-level:' ); | ||
| $opts = getopt( $shortopts, $longopts ); | ||
| // Parse every argument: getopt() silently discards unknown or incomplete options. | ||
| $aliases = array( | ||
| 'h' => 'help', | ||
| 'd' => 'database', | ||
| 'P' => 'port', | ||
| 'l' => 'log-level', | ||
| 'u' => 'user', | ||
| ); | ||
| $options = array( 'help', 'database', 'port', 'log-level', 'bind-address', 'user', 'password-file', 'auth-plugin' ); | ||
| $args = array_slice( $argv, 1 ); | ||
| $opts = array(); | ||
|
|
||
| // Never include argument values in errors: an unknown option may contain a password. | ||
| $option_error = "Error: Invalid command-line options. Use --help for supported options and required values.\n"; | ||
|
|
||
| for ( $i = 0; $i < count( $args ); $i++ ) { | ||
| $arg = $args[ $i ]; | ||
| $name = ''; | ||
| $value = null; | ||
| if ( 0 === strpos( $arg, '--' ) ) { | ||
| $parts = explode( '=', substr( $arg, 2 ), 2 ); | ||
| $name = $parts[0]; | ||
| $value = $parts[1] ?? null; | ||
| } elseif ( '-' === substr( $arg, 0, 1 ) && strlen( $arg ) > 1 ) { | ||
| $name = $aliases[ $arg[1] ] ?? ''; | ||
| if ( strlen( $arg ) > 2 ) { | ||
| if ( 'help' === $name ) { | ||
| array_splice( $args, $i + 1, 0, array( '-' . substr( $arg, 2 ) ) ); | ||
| } else { | ||
| $value = substr( $arg, 2 ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if ( ! in_array( $name, $options, true ) || array_key_exists( $name, $opts ) | ||
| || ( 'help' === $name && null !== $value ) | ||
| ) { | ||
| fwrite( STDERR, $option_error ); | ||
| exit( 1 ); | ||
| } | ||
| if ( 'help' === $name ) { | ||
| $opts[ $name ] = true; | ||
| continue; | ||
| } | ||
| $value = $value ?? ( $args[ ++$i ] ?? null ); | ||
| if ( null === $value ) { | ||
| fwrite( STDERR, $option_error ); | ||
| exit( 1 ); | ||
| } | ||
| $opts[ $name ] = $value; | ||
| } | ||
|
|
||
| $help = <<<USAGE | ||
| Usage: wp-mysql-proxy.php [--port <port>] [--database <path/to/db.sqlite>] [--log-level <log_level>] | ||
| Usage: wp-mysql-proxy.php --user <username> [--password-file <path>] [--bind-address <address>] [--auth-plugin <name>] [--port <port>] [--database <path/to/db.sqlite>] [--log-level <log_level>] | ||
|
|
||
| Options: | ||
| -h, --help Show this help message and exit. | ||
| -p, --port=<port> The port to listen on. Default: 3306 | ||
| -u, --user=<username> The proxy account name. Required. | ||
| --password-file=<path> Read the password from a file instead of WP_MYSQL_PROXY_PASSWORD. | ||
| --bind-address=<ip> The IPv4 or IPv6 address to listen on. Default: 127.0.0.1 | ||
| --auth-plugin=<name> The account's authentication plugin, either | ||
| caching_sha2_password or mysql_native_password. | ||
| Use the latter only for clients that need it. | ||
| Default: caching_sha2_password | ||
| -P, --port=<port> The port to listen on. Default: 3306 | ||
| -d, --database=<path> The path to the SQLite database file. Default: :memory: | ||
| -l, --log-level=<level> The log level to use. One of 'error', 'warning', 'info', 'debug'. Default: info | ||
|
|
||
| A nonempty password is required via WP_MYSQL_PROXY_PASSWORD or --password-file. | ||
| Password files are read verbatim, including any trailing newline. | ||
|
|
||
| USAGE; | ||
|
|
||
| // Help. | ||
| if ( isset( $opts['h'] ) || isset( $opts['help'] ) ) { | ||
| if ( isset( $opts['help'] ) ) { | ||
| fwrite( STDERR, $help ); | ||
| exit( 0 ); | ||
| } | ||
|
|
||
| // Database path. | ||
| $db_path = $opts['d'] ?? $opts['database'] ?? ':memory:'; | ||
| $db_path = $opts['database'] ?? ':memory:'; | ||
|
|
||
| // Validate options before SQLite_Adapter opens or creates the database file. | ||
| // MySQL_Proxy checks the bind address too, but only after that. | ||
|
|
||
| // Bind address. | ||
| $bind_address = $opts['bind-address'] ?? '127.0.0.1'; | ||
| if ( false === filter_var( $bind_address, FILTER_VALIDATE_IP ) ) { | ||
| fwrite( STDERR, "Error: --bind-address must be a valid IPv4 or IPv6 address.\n" ); | ||
| exit( 1 ); | ||
| } | ||
|
|
||
| // Port. | ||
| $port = (int) ( $opts['p'] ?? $opts['port'] ?? 3306 ); | ||
| if ( $port < 1 || $port > 65535 ) { | ||
| $port = filter_var( $opts['port'] ?? 3306, FILTER_VALIDATE_INT ); | ||
| if ( false === $port || $port < 1 || $port > 65535 ) { | ||
| fwrite( STDERR, "Error: --port must be an integer between 1 and 65535. Use --help for more information.\n" ); | ||
| exit( 1 ); | ||
| } | ||
|
|
||
| // Log level. | ||
| $log_level = $opts['l'] ?? $opts['log-level'] ?? 'info'; | ||
| $log_level = $opts['log-level'] ?? 'info'; | ||
| if ( ! in_array( $log_level, Logger::LEVELS, true ) ) { | ||
| fwrite( STDERR, 'Error: --log-level must be one of: ' . implode( ', ', Logger::LEVELS ) . ". Use --help for more information.\n" ); | ||
| exit( 1 ); | ||
| } | ||
|
|
||
| // Account. Do not accept passwords as command-line arguments. | ||
| $password = getenv( 'WP_MYSQL_PROXY_PASSWORD' ); | ||
| if ( isset( $opts['password-file'] ) ) { | ||
| $password_file = $opts['password-file']; | ||
|
|
||
| // Pipes, such as /dev/stdin or a process substitution, keep the password off the disk. | ||
| if ( ! is_readable( $password_file ) || is_dir( $password_file ) ) { | ||
| fwrite( STDERR, "Error: --password-file must name a readable file.\n" ); | ||
| exit( 1 ); | ||
| } | ||
| $password = file_get_contents( $password_file ); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| try { | ||
| $authentication = new MySQL_Authentication( | ||
| $opts['user'] ?? '', | ||
| (string) $password, | ||
| $opts['auth-plugin'] ?? MySQL_Protocol::DEFAULT_AUTH_PLUGIN | ||
| ); | ||
| } catch ( InvalidArgumentException $e ) { | ||
| fwrite( STDERR, 'Error: ' . $e->getMessage() . " Use --help for more information.\n" ); | ||
| exit( 1 ); | ||
| } | ||
| unset( $password ); | ||
|
|
||
| if ( ! class_exists( 'WP_MySQL_On_SQLite' ) ) { | ||
| fwrite( | ||
| STDERR, | ||
|
|
@@ -57,8 +150,10 @@ | |
| $proxy = new MySQL_Proxy( | ||
| new SQLite_Adapter( $db_path ), | ||
| array( | ||
| 'port' => $port, | ||
| 'log_level' => $log_level, | ||
| 'authentication' => $authentication, | ||
| 'bind_address' => $bind_address, | ||
| 'port' => $port, | ||
| 'log_level' => $log_level, | ||
| ) | ||
| ); | ||
| $proxy->start(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,10 +2,10 @@ | |
|
|
||
| namespace WP_MySQL_Proxy\Adapter; | ||
|
|
||
| use Error; | ||
| use PDO; | ||
| use PDOException; | ||
| use PDOStatement; | ||
| use Throwable; | ||
| use WP_MySQL_Proxy\MySQL_Result; | ||
| use WP_MySQL_On_SQLite; | ||
| use WP_MySQL_Proxy\MySQL_Protocol; | ||
|
|
@@ -23,7 +23,7 @@ public function __construct( $sqlite_database_path ) { | |
|
|
||
| public function handle_query( string $query ): MySQL_Result { | ||
| $affected_rows = 0; | ||
| $last_insert_id = null; | ||
| $last_insert_id = 0; | ||
| $columns = array(); | ||
| $rows = array(); | ||
|
|
||
|
|
@@ -39,12 +39,13 @@ public function handle_query( string $query ): MySQL_Result { | |
| $columns = $this->computeColumnInfo( $statement ); | ||
| } | ||
| return MySQL_Result::from_data( $affected_rows, $last_insert_id, $columns, $rows ?? array() ); | ||
| } catch ( Throwable $e ) { | ||
| $error_info = $e->errorInfo ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase | ||
| if ( $e instanceof PDOException && $error_info ) { | ||
| return MySQL_Result::from_error( $error_info[0], $error_info[1], $error_info[2] ); | ||
| } catch ( PDOException $e ) { | ||
| // Only query errors reach the client. The session logs internal errors instead. | ||
| $error_info = $e->errorInfo; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase | ||
| if ( ! $error_info || $this->is_internal_error( $e ) ) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| throw $e; | ||
| } | ||
| return MySQL_Result::from_error( 'HY000', 1105, $e->getMessage() ?? 'Unknown error' ); | ||
| return MySQL_Result::from_error( $error_info[0], $error_info[1], $error_info[2] ); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -101,4 +102,22 @@ public function computeColumnInfo( PDOStatement $statement ) { | |
| } | ||
| return $columns; | ||
| } | ||
|
|
||
| /** | ||
| * Whether a query exception is a PHP error that the driver reported as a query error. | ||
| * | ||
| * A PHP error is a bug, not a query error, and its message can contain server | ||
| * file paths. | ||
| * | ||
| * @param PDOException $e The query exception. | ||
| * @return bool | ||
| */ | ||
| private function is_internal_error( PDOException $e ): bool { | ||
| for ( $cause = $e->getPrevious(); null !== $cause; $cause = $cause->getPrevious() ) { | ||
| if ( $cause instanceof Error ) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reject an option value that starts with
-when it comes from the next token.Line 55 takes the next argument as the value without checking its prefix.
--database --port=5stores--port=5as the database path. The port flag is lost, andSQLite_Adapterthen creates a file named--port=5.--user --auth-plugin=xsets the account name to--auth-plugin=xin the same way.None of these options need separate-token values that begin with
-. Report a missing value in that case. Users can still pass such a value with the--option=valueform.Proposed fix
Based on learnings: a parser that consumes the next token must report a missing value when that token begins with the option prefix.
Committable suggestion
Source: Learnings