NAME
DBIx::MyServer - Server-side implementation of the MySQL network protocol
SYNOPSIS
Please see the scripts in the examples
directory. examples/dbi.pl
along with DBIx::MyParse::DBI
shows how to use this module by subclassing it. examples/echo.pl
shows how to use this module directly.
DESCRIPTION
This module emulates the server side of the MySQL protocol. This allows you to run your own faux-MySQL servers which can accept commands and queries and reply accordingly.
Please see examples/myserver.pl
for a system that allows building functional mysql servers that rewrite queries or return arbitary data.
CONSTRUCTOR
my $myserver = DBIx::MyServer->new( socket => $socket, parser => $parser, dbh => $dbh ... )
-
The following parameters are accepted:
socket
- the socket that will be used for communication with the client. The socket must be created in advance with socket() and an incoming connection must be already established using accept()dbh
- a DBI handle. DBIx::MyServer does not use it however it can be used by modules inheriting from DBIx::MyServerparser
- a DBIx::MyParser handle. If you do not override thecomQuery()
method, this handle will be used to parse the SQL in order to call the appropriatesqlcom...
method.banner
- the server version string that is announced to the client. If not specified, the name and the version of the DBIx::MyServer module is sent. Some clients, e.g. DBD::mysql attempt to parse that string as a version number in order to determine the server capabilities. Therefore, it may be a good idea to start yourbanner
with a string in the form5.0.37
. For example, theexamples/dbi.pl
example takes thebanner
from the actual MySQL server the connection is forwarded to. Please note that the MySQL client may isssue aselect @@version_comment limit 1
command and display the result to the user.server_charset
- the character set (and collation) the server announces to the client. A number is expected, not a character set or collation name. To onvert between numbers and names, please consult thesql/share/charsets/Index.xml
file from your MySQL source tree. The numbers are found under theid
property of each XML leaf.Generally you do not need to override the constructor. If you do, please keep in mind that it returns a blessed
@ARRAY
, not a blessed%HASH
, even though new() accepts an argument hash. This is done for performance reasons. The first 20 items from the array are reserved for the parent module, so please put your object properties into the array members from 20 onwards.
CONNECTION ESTABLISHMENT
You are responsible for opening your listening socket and accepting a connection on it (and possibly forking a child process). Once the connection has been accepted, you create a DBIx::MyServer
object and pass the socket to it. From then on, you have two options:
Procedural Connection Establishment
If you want to handle connection establishment yourself, you will need to call those two functions consequtively. Please see examples/echo.pl
for a script that uses procedural connection establishment.
$myserver->sendServerHello()
-
The server is the one that initiates the handshake by sending his greeting to the client.
my ($username, $database) = $myserver->readClientHello()
-
The client provides his username and the database it wants to connect to. If
$username
isundef
, the client disconnected before authenticating. To check the password, use$myserver-
passwordMatches($correct_password) which will return1
if the password provided by the client is correct andundef
otherwise.If you need to know the IP of the client, you need to extract it from the socket that you established yourself. Please check out the implementation of
DBIx::MyParse::handshake()
for the correct way to callgetpeername()
. The socket being serviced by the currentDBIx::MyParse
object can be obtained by callinggetSocket()
.If you want to let the client in, do a
$myserver->sendOK()
. Otherwise, use$myserver->sendError()
as described below.
Connection Establishment with Subclassing
If you are subclassing DBIx::MyParse
, the way DBIx::MyParse::DBI
does, to establish a connection you call:
$myserver->handshake()
-
which completes the handshake between the two parties. The return value will be
undef
if some I/O error occured, or the result of the client authorization routine. When the client sends its credentials, the module will call: -
whose default action is to accept only localhost connections regardless of username or password. You should override this method to install your own security requirements. If your
authorize()
returnsundef
, the connection will be rejected, if it returns anything else, the connection will be completed and the return value will be passed back to the caller ofhandshake()
. You can use this return value to communicate the access rights the particular user is entitled to, so that your script can know them and enforce them.In case you want to reject the connection, you need to send your own error message via
sendError()
. If you accept the connection, the module will send theOK
message back to the client for you.The password supplied by the client is irreversibly encrypted, therefore to verify it, you need to use:
$myserver->passwordMatches($expected_password)
-
which will return
undef
if the password does not match and1
otherwise.If the client supplied a database name on connect,
comInitDb($database)
will be called.For an example of a custom
authorize()
, please seeDBIx::MyParse::DBI
which does some extra connection setup.
COMMAND PROCESSING
Procedural Command Processing
If you want to handle each command individually on your own, you need to call
my ($command, $data) = $myserver->readCommand()
-
in a loop and process each command. Sending result sets and errors and terminating the connection is entirely up to you. The
examples/odbc.pl
script uses this approach to process queries in the simples possible way without reflecting much on their contents.
Command Processing with Subclassing
If you are subclassing DBIx::MyParse
, your main script needs to call:
$myserver->processCommand()
-
in a loop. The default
processCommand()
handler will obtain the next command from the client usingreadCommand()
and will call the appropriate individual command handler, described below. You can overrideprocessCommand()
if you want to process the entire packet yourself, in which case you are responsible for callingreadCommand()
yourself.processCommand()
will return whatever the individual command handler returned.undef
is reserved for I/O errors, which will allow you to conveniently exit your loop. Therefore it is recommended that any handlers that you override return1
to indicate correct operation or non-fatal errors.
INDIVIDUAL COMMAND HANDLERS
If you do not override the generic processCommand()
method, the following individual handlers will be called depending on the actual MySQL command received by the server. The default action of those handlers is to send a "command unsupported" error back to the client, unless specified otherwise below.
If you want to send an error or an OK message to the client as a response to a command or query, you need to use sendOK()
and sendError()
yourself. The parent module will not send any of those for you under no circumstances.
$myserver->comSleep($data)
-
The meaning of this command is not clear.
$myserver->comQuit()
-
The default action is to
return undef
, meaning thatprocessCommand()
will returnundef
, which you can use to exit your command loop. Alternatively, you can override this to calldie()
orexit()
, if you are forking individual child processes for each client. $myserver->comInitDb($database)
-
This command is used by the client to select a default working database. The default action is to set the default database for the parser object if one has been specified. This enables the parser to parse some SQL statements that require a default database, such as SHOW TABLE STATUS. The command is then converted into a
USE $database
SQL statement which in turn will triggercomQuery("USE $database")
orsqlcomChangeDB()
$myserver->comQuery($query_text)
-
This handler is called for all SQL queries received by the server. The action of the default handler is to parse the query using DBIx::MyParse and evoke a more specific handler. If the parsing results in an error,
sqlcomError()
is called which returns the parser error message to the client.If you override this handler, your implementation must return an array of three items. The first item is the value that will be returned to your main loop as the return value of
processCommand()
. You can return anything you please, including references to complex objects. Returningundef
can be used to conveniently terminate the command loop, so you are generally encouraged to return some true and definied value to indicate proper operation.The second item you can return is a reference to an array of field definitions created with
newDefinition()
. If you provide it, your field definitions will be sent to the client usingsendDefinitions()
. If you do not provide a reference, you are responsible for callingsendDefinitions()
yourself before you send any data.The third item you can return is a reference to an array of values that is the actual data to be sent to the client in response to the query. If you do not provide a reference, you are responsible for sending the data yourself by using the functions described elsewhere in this document.
$myserver->comFieldList($table)
-
This handler is called if the mysql client requests the field list for the specified table. The handler must create a set of field definitions using
newDefinition()
and then send them to the client usingsendDefinitions(\@definitions, 1)
. It is strongly reccomended that you provide your own working implementation forcomFieldList()
because this MySQL command is often issued by the various MySQL connectors and is essential for the functioning of theFEDERATED
table handler.For an example of easily handling
comFieldList()
, see DBIx::MyServer::DBI which uses DBI'scolumn_info
to return the definition of an actual MySQL table from another server. $myserver->comCreateDb($database_name)
-
This handler is called when
mysqladmin create $database_name
is used. The default is to convert the command into aCREATE DATABASE $database_name
SQL statement, so thatcomQuery("CREATE DATABASE $database_name")
is triggered, which in turn will callsqlcomCreateDb()
. $myserver->comDropDb($database_name)
-
This handler is called when
mysqladmin drop $database_name
is used. The default is to convert the command into aDROP DATABASE $database_name
SQL statement so thatcomQuery("DROP DATABASE $database_name")
is triggered, which in turn will callsqlcomDropDb()
. $myserver->comShutdown()
-
This handler is called when
mysqladmin shutdown
is used. The default action is to die(), in other words, any authorized client can shut down its own child, or the entire server, if the server is not forking. $myserver->comStatistics()
-
The default action is to send our PID to the client. If you want to override that, you can use
_sendPacket($string)
(rather thansendOK()
) to deliver a single string to your client. Please note thatmysql
andmysqladmin
may attempt to parse this string before displaying it, so you may wish to keep it identical to the one sent by real MySQL servers:Uptime: 10659 Threads: 1 Questions: 756 Slow queries: 0 Opens: 109 Flush tables: 1 Open tables: 30 Queries per second avg: 0.071
$myserver->comProcessInfo()
-
This handler is called when
mysqladmin processlist
is called from the command line. $myserver->comProcessKill($thread_id)
-
This handler is called as a response by the command issued by
mysqladmin kill
. InDBIx::MyServer
, the MySQL thread ID is equal to the PID of the server process. The default action will kill() the PID however you are only allowed to kill your own PID. $myserver->comPing()
-
This is used to check if the server is reachable and running. The default action is to do a
sendOK()
. If you are using that in a conjunction with an automated monitoring and alert system, you may wish to do asendOK()
only after doing extra health checks on your own, e.g. if you are reading data from an external data source, do not send an OK unless that source is reachable.
INDIVIDUAL QUERY HANDLERS
If you do not override the generic comQuery()
method, the incoming MySQL query will be parsed to a DBIx::MyParse::Query object and the object will be dispatched to one of the sqlcom...
handlers based on the type of the query.
The default handlers all return "query unsupported". If you override any of the default handlers, your handler must return one two or three items as described under comQuery()
.
Each sqlcom...
handler receives at least two arguments. The first one is the DBIx::MyParse::Query object produced by DBIx::MyParse. The second is the text of the query. sqlcom...
handlers dealing with databases will get the database name in question as their third argument.
If the parsing resulted in an error, sqlcomError($query, $query_string)
is called. The default action of this handler is to send the parser error message as is to the client.
RETURNING MESSAGES TO CLIENT
$myserver->sendOK($message, $affected_rows, $insert_id, $warning_count)
-
Returns a simple OK response, which can contain a custom message, the number of rows affected by the query, etc.
$myserver->sendError($message, $errno, $sqlstate)
-
Returns an error response. IF no
errno
andsqlstate
are specified, generic values will be sent to client.
RETURNING DATA TO CLIENT
my $definition = $myserver->newDefinition(name => 'field_name')
-
Prepares a new field definition that can then be sent to the client. Apart from
name
, the following attributes are supported:catalog, db, table, org_table name, org_name, length, type, flags, decimals, default
The default
type
isMYSQL_TYPE_STRING
. The complete list of type constants is available fromMyServer.pm
. $myserver->sendDefinitions( \@definitions, $skip_envelope )
-
Sends the previously prepared field definitions to the client. You need to do that before sending the first data row. You also need to use
sendDefinitions()
to send a table definition in response tocomFieldList()
command, however due to an inconsistency in the MySQL protocol,$skip_envelope
must be true. sendRow($array_ref)
andsendRows($array_ref)
-
Those two functions are used to send the actual data to the client.
sendRow()
expect a reference to the array containing the values you wish to send.sendRows()
expects a reference to an array containing references to arrays.Please note in the MySQL protocol, all values are sent consequtively without row boundaries. The number of columns in each row is determined by how many field definitions you send before you start sending the actual data.
sendEOF()
-
Indicates the end of the data from the record set.
PREPARED STATEMENTS
DBIx::MyServer does not currently support the prepared statement protocol. By default, the comStmtPrepare()
handler will return ER_UNSUPPORTED_PS back to the client, which should instruct a wise client to retry the query using conventional statemements.
As of Nov 7th, 2006, the ODBC Driver version 3.51.12 does not use prepared statements at all. The FEDERATED database engine does not use them. DBD::Mysql version 3.0008_1 will attempt a prepared statement and if ER_UNSUPPORTED_PS is received, it will fall back to normal statements. To force the use of normal statements, add mysql_emulated_prepare=1
to your DSN string.
SECURITY CONSIDERATIONS
The defaults of this module are meant to allow quick prototyping of MySQL servers by subclassing. Therefore, by default:
Only localhost connections are allowed if you use
handshake()
. Beyond checking that, no other access checks are made and all usernames are accepted. No password checks are made. After the handshake, any access restrictions on individual commands or queriesare entirely up to you.The default action for comQuit() and comShutdown() is to die() which allows anyone to bring down a server that is not forking child processes. The default action of
comProcessKill()
only allows you to kill your own server process via SIGTERM.A single cryptographic salt is used throughout the lifetime of the process, which may be a security risk.
AUTHOR
Philip Stoev, <philip@stoev.org<gt>
COPYRIGHT AND LICENSE
Copyright (C) 2006 by Philip Stoev
This library is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License Agreement.
Please note that that the MySQL Protocol is proprietary and is also covered by the GNU General Public License. Please see
http://dev.mysql.com/doc/internals/en/licensing-notice.html
If you have any licensing doubts, please contact MySQL AB directly.
SEE ALSO AND THANKS
The following sources of information have been very helpful in creating this module:
http://dev.mysql.com/doc/internals/en/client-server-protocol.html
http://www.redferni.uklinux.net/mysql/MySQL-Protocol.html
18 POD Errors
The following errors were encountered while parsing the POD:
- Around line 168:
'=item' outside of any '=over'
- Around line 196:
You forgot a '=back' before '=head1'
- Around line 207:
'=item' outside of any '=over'
- Around line 223:
You forgot a '=back' before '=head2'
- Around line 227:
'=item' outside of any '=over'
- Around line 254:
You forgot a '=back' before '=head1'
- Around line 260:
'=item' outside of any '=over'
- Around line 266:
You forgot a '=back' before '=head2'
- Around line 270:
'=item' outside of any '=over'
- Around line 280:
You forgot a '=back' before '=head1'
- Around line 289:
'=item' outside of any '=over'
- Around line 378:
You forgot a '=back' before '=head1'
- Around line 396:
'=item' outside of any '=over'
- Around line 404:
You forgot a '=back' before '=head1'
- Around line 406:
'=item' outside of any '=over'
- Around line 435:
You forgot a '=back' before '=head1'
- Around line 450:
'=item' outside of any '=over'
- Around line 1342:
You forgot a '=back' before '=head1'