Skip to content

Commit

Permalink
Separate MySQL Plugin from DB Plugin
Browse files Browse the repository at this point in the history
These two plugins used to require eachother but somewhere along the way
when we changed plugin defintions we made the MySQL plugin extend the DB plugin so that
the MySQL plugin could respond to its own Query commands. However this breaks the intended internal
usage of the MySQL Plugin because we use a custom separate plugin to respond to Query. This change
brings back this intended functionality and separates the two plugins. It also:

1. DRYs the version response code
2. Fixes returning actual error messages to the client.
3. Adds support for queries Alteryx uses for schema discovery.
  • Loading branch information
coleaeason committed Jul 22, 2024
1 parent 93f7b0d commit ca4ab49
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 17 deletions.
36 changes: 29 additions & 7 deletions plugins/MySQL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ string MySQLPacket::serializeHandshake() {
// Just hard code the values for now
MySQLPacket handshake;
handshake.payload += lenEncInt(10); // protocol version
handshake.payload += "8.0.0"s; // server version
handshake.payload += BedrockPlugin_MySQL::mysqlVersion; // server version
handshake.payload += lenEncInt(0); // NULL
uint32_t connectionID = 1;
SAppend(handshake.payload, &connectionID, 4); // connection_id
Expand Down Expand Up @@ -229,14 +229,19 @@ string MySQLPacket::serializeERR(int sequenceID, uint16_t code, const string& me
return err.serialize();
}

BedrockPlugin_MySQL::BedrockPlugin_MySQL(BedrockServer& s) : BedrockPlugin_DB(s)
BedrockPlugin_MySQL::BedrockPlugin_MySQL(BedrockServer& s) : BedrockPlugin(s)
{
}

string BedrockPlugin_MySQL::getPort() {
return server.args.isSet("-mysql.host") ? server.args["-mysql.host"] : "localhost:3306";
}

// This plugin supports no commands.
unique_ptr<BedrockCommand> BedrockPlugin_MySQL::getCommand(SQLiteCommand&& baseCommand) {
return nullptr;
}

void BedrockPlugin_MySQL::onPortAccept(STCPManager::Socket* s) {
// Send Protocol::HandshakeV10
SINFO("Accepted MySQL request from '" << s->addr << "'");
Expand Down Expand Up @@ -314,7 +319,6 @@ void BedrockPlugin_MySQL::onPortRecv(STCPManager::Socket* s, SData& request) {
s->send(MySQLPacket::serializeQueryResponse(packet.sequenceID, result));
} else if (SIEquals(SToUpper(query), "SHOW /*!50002 FULL*/ TABLES;") ||
SIEquals(SToUpper(query), "SHOW FULL TABLES;")) {
// Return an empty list of tables
SINFO("Getting table list");

// Transform this into an internal request
Expand All @@ -324,6 +328,21 @@ void BedrockPlugin_MySQL::onPortRecv(STCPManager::Socket* s, SData& request) {
request["query"] =
"SELECT name as Tables_in_main, CASE type WHEN 'table' THEN 'BASE TABLE' WHEN 'view' THEN 'VIEW' "
"END as Table_type FROM sqlite_master WHERE type IN ('table', 'view');";
} else if (SIEquals(SToUpper(query),
"SELECT TABLE_NAME,TABLE_COMMENT,IF(TABLE_TYPE='BASE TABLE', 'TABLE', "
"TABLE_TYPE),TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=DATABASE() "
"AND ( TABLE_TYPE='BASE TABLE' OR TABLE_TYPE='VIEW' ) AND TABLE_NAME LIKE '%' ORDER BY "
"TABLE_SCHEMA, TABLE_NAME;")) {
// This is the query Alteryx users to get Table information to display in the GUI, so let's support it.
SINFO("Getting table list");

// Transform this into an internal request
request.methodLine = "Query";
request["format"] = "json";
request["sequenceID"] = SToStr(packet.sequenceID);
request["query"] =
"SELECT name as TABLE_NAME, '' as TABLE_COMMENT, UPPER(type) as TABLE_TYPE, 'main' as TABLE_SCHEMA "
"FROM sqlite_master WHERE type IN ('table', 'view') ORDER BY TABLE_SCHEMA, TABLE_NAME;";
} else if (SContains(query, "information_schema")) {
// Return an empty set
SINFO("Responding with empty routine list");
Expand All @@ -341,12 +360,14 @@ void BedrockPlugin_MySQL::onPortRecv(STCPManager::Socket* s, SData& request) {
s->send(MySQLPacket::serializeOK(packet.sequenceID));
} else if (SIEquals(SToUpper(query), "SELECT VERSION();")) {
// Return our fake version
SINFO("Responding with fake database list");
SINFO("Responding fake version string");
SQResult result;
result.headers.push_back("version()");
result.rows.resize(1);
result.rows.back().push_back("8.0.0");
result.rows.back().push_back(BedrockPlugin_MySQL::mysqlVersion);
s->send(MySQLPacket::serializeQueryResponse(packet.sequenceID, result));
// Add SHOW KEYS() support

} else {
// Transform this into an internal request
request.methodLine = "Query";
Expand Down Expand Up @@ -384,8 +405,9 @@ void BedrockPlugin_MySQL::onPortRequestComplete(const BedrockCommand& command, S
}
} else {
// Failure -- pass along the message
s->send(MySQLPacket::serializeERR(command.request.calc("sequenceID"), SToInt(command.response.methodLine),
command.response["error"]));
int64_t errorCode = SToInt64(SBefore(command.response.methodLine, " "));
string errorMessage = SAfter(command.response.methodLine, " ");
s->send(MySQLPacket::serializeERR(command.request.calc("sequenceID"), errorCode, errorMessage));
}
}

Expand Down
48 changes: 38 additions & 10 deletions plugins/MySQL.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ extern const char* g_MySQLVariables[MYSQL_NUM_VARIABLES][2];
*/
struct MySQLPacket {
// Attributes
// MySQL sequenceID which is used by clients and servers to
// order packets and resets back to 0 when a new "command" starts.
uint8_t sequenceID;

// The packet payload
string payload;

/**
Expand All @@ -30,14 +34,14 @@ struct MySQLPacket {
* Parse a MySQL packet from the wire
*
* @param packet Binary data received from the MySQL client
* @param size length of packet
* @param size length of packet
* @return Number of bytes deserialized, or 0 on failure
*/
int deserialize(const char* packet, const size_t size);

/**
* Creates a MySQL length-encoded integer
* See: https://dev.mysql.com/doc/internals/en/integer.html#packet-Protocol::LengthEncodedInteger
* See: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_integers.html
*
* @param val Integer value to be length-encoded
* @return Lenght-encoded integer value
Expand All @@ -46,22 +50,22 @@ struct MySQLPacket {

/**
* Creates a MySQL length-encoded string
* See: https://dev.mysql.com/doc/internals/en/string.html#packet-Protocol::LengthEncodedString
* See: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_strings.htm
*
* @param str The string to be length-encoded
* @return The length-encoded string
* @return length-encoded string
*/
static string lenEncStr(const string& str);

/**
* Creates the packet sent from the server to new connections
* See: https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeV10
* See: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_handshake_v10.html
*/
static string serializeHandshake();

/**
* Creates the packet used to respond to a COM_QUERY request
* See: https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnDefinition320
* See: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_query_response.html
*
* @param sequenceID The sequenceID of the request we are responding to
* @param result The results of the query we were asked to execte
Expand All @@ -71,7 +75,7 @@ struct MySQLPacket {

/**
* Creatse a standard OK packet
* See: https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html
* See: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_ok_packet.html
*
* @param sequenceID The sequenceID of the request we are responding to
* @return The OK packet to be sent to the client
Expand All @@ -80,7 +84,7 @@ struct MySQLPacket {

/**
* Sends ERR
* See: https://dev.mysql.com/doc/internals/en/packet-ERR_Packet.html#cs-packet-err-error-code
* See: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_err_packet.html
*
* @param sequenceID The sequenceID of the request we are responding to
* @param code The error code to show the user
Expand All @@ -91,18 +95,42 @@ struct MySQLPacket {
};

/**
* Declare the class we're going to implement below
* This plugin allows MySQL clients to connect to a bedrock instance. It requires the DB plugin
* or some other plugin that can process "Query" commandsin order to actually work, in other
* words this is essentially a network protocol wrapper aound the DB plugin.
*/
class BedrockPlugin_MySQL : public BedrockPlugin_DB {
class BedrockPlugin_MySQL : public BedrockPlugin {
public:
BedrockPlugin_MySQL(BedrockServer& s);
virtual const string& getName() const;

virtual unique_ptr<BedrockCommand> getCommand(SQLiteCommand&& baseCommand);

// This plugin listens on MySQL's port by default, but can be changed via CLI flag.
virtual string getPort();

// This function is called when bedrock accepts a new connection on a port owned
// by a given plugin. We use it to send the MySQL handshake.
virtual void onPortAccept(STCPManager::Socket* s);

// This function is called when bedrock receives data on a port owned by a given plugin.
// We do basically all query and processing in here.
virtual void onPortRecv(STCPManager::Socket* s, SData& request);

// This function is called when a requests completes, we use it to send OK and ERR Packets
// as appropriate based on the results of onPortRecv().
virtual void onPortRequestComplete(const BedrockCommand& command, STCPManager::Socket* s);

// Our fake mysql version. We don't necessarily
// conform to the same functionality or standards as this version, however
// some clients do version checking to see if they support connecting. We've
// set this to a major recent version so that modern clients can connect. In theory
// you can safely increment this to any valid future version unless there's breaking
// changes in the protocol.
static constexpr auto mysqlVersion = "8.0.35";

private:
// Attributes
static const string name;
string commandName;
};

0 comments on commit ca4ab49

Please sign in to comment.