{"id":7933,"date":"2018-11-14T11:16:32","date_gmt":"2018-11-14T13:16:32","guid":{"rendered":"http:\/\/blog.plataformatec.com.br\/?p=7933"},"modified":"2019-01-04T18:07:55","modified_gmt":"2019-01-04T20:07:55","slug":"building-a-new-mysql-adapter-for-ecto-part-i-hello-world","status":"publish","type":"post","link":"https:\/\/blog.plataformatec.com.br\/2018\/11\/building-a-new-mysql-adapter-for-ecto-part-i-hello-world\/","title":{"rendered":"Building a new MySQL adapter for Ecto, Part I: Hello World"},"content":{"rendered":"
As you may have seen in the announcement<\/a>, Plataformatec is working on a new MySQL driver called MyXQL.<\/p>\n Writing a complete driver involves quite a bit of work. To name just a few things, we need to support: all protocol messages and data types, authentication schemes, connection options (TCP\/SSL\/UNIX domain socket), transactions and more. Rather than going through all of these in detail, I plan to distill this knowledge into 4 parts, each with a quick overview of a given area:<\/p>\n This also mimics how I approached the development of the library, my end goal was to integrate with Ecto and I wanted to be integrating end-to-end as soon and as often as possible. Rather than implementing each part fully, I implemented just enough to move forward knowing I can later go back and fill in remaining details. Without further ado, let’s get started!<\/p>\n Our “Hello World” will involve performing a “handshake”: connecting to a running MySQL server and authenticating a user. To avoid getting bogged down in authentication details, the simplest possible thing to do is to log in as user without password. Let’s create one:<\/p>\n We can check if everything went well by trying to log in as that user:<\/p>\n If you don’t have MySQL installed, I recommend setting it up via Homebrew, if you’re on macOS, or Docker. I ended up using Docker because I knew I needed to test on multiple server versions. Here’s how I set it up:<\/p>\n We can now connect to the server from IEx session:<\/p>\n Let’s break this down. Let’s now read data from the socket: ( To make sense of this, we’re gonna need to look into MySQL manual. Wait, the size of the payload is To make sense of the remaining payload we’re gonna use the We can see up to 16 bytes in each row and at the far right we have ASCII interpretation of each byte. Per “Initial Handshake Packet”<\/a> the first byte is the protocol version, always We can parse the server version, that’s a good start! There are other fields in this packet that in a complete adapter we’d have to handle, but for now we’ll simply ignore them. We’ll just take a note of the authentication method at the end to the packet, a null-terminated string After receiving “Initial Handshake Packet” the client is supposed to send “Handshake Response”<\/a>. We’ll again just gloss over the details:<\/p>\n Let’s break this down:<\/p>\n First, we use The first byte of the response is <\/a><\/p>\n","protected":false},"excerpt":{"rendered":" As you may have seen in the announcement, Plataformatec is working on a new MySQL driver called MyXQL. Writing a complete driver involves quite a bit of work. To name just a few things, we need to support: all protocol messages and data types, authentication schemes, connection options (TCP\/SSL\/UNIX domain socket), transactions and more. Rather … \u00bb<\/a><\/p>\n","protected":false},"author":70,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[1],"tags":[238,143],"aioseo_notices":[],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/7933"}],"collection":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/users\/70"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/comments?post=7933"}],"version-history":[{"count":35,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/7933\/revisions"}],"predecessor-version":[{"id":8332,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/7933\/revisions\/8332"}],"wp:attachment":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/media?parent=7933"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/categories?post=7933"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/tags?post=7933"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}\n
Hello World<\/h3>\n
$ mysql --user=root -e \"CREATE USER myxql_test\"\n<\/code><\/pre>\n
$ mysql --user=myxql_test -e \"SELECT NOW()\"\n+---------------------+\n| NOW() |\n+---------------------+\n| 2018-10-04 18:35:11 |\n+---------------------+\n<\/code><\/pre>\n
$ docker run --publish=3306:3306 --name myxql_test -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0.12\n# note we connect via TCP, instead of the default UNIX domain socket:\n$ mysql --protocol=tcp --user=root --password=secret -e \"CREATE USER myxql_test;\"\n\n$ mysql --protocol=tcp --user=myxql_test -e \"SELECT NOW()\"\n+---------------------+\n| NOW() |\n+---------------------+\n| 2018-10-04 18:40:04 |\n+---------------------+\n<\/code><\/pre>\n
iex> {:ok, sock} = :gen_tcp.connect('127.0.0.1', 3306, [:binary, active: false], 5000)\n{:ok, #Port<0.6>}\n<\/code><\/pre>\n
:gen_tcp.connect\/4<\/code><\/a> accepts:<\/p>\n
\n
:binary<\/code> option.
\nactive: false<\/code> means we’ll work with the socket in “passive mode”, meaning we’ll read data using blocking
:gen_tcp.recv\/3<\/code><\/a> call.<\/li>\n
0<\/code> means we read all available bytes,
5000<\/code> is the timeout in milliseconds)<\/p>\n
iex> {:ok, data} = :gen_tcp.recv(sock, 0, 5000)\niex> data\n<<74, 0, 0, 0, 10, 56, 46, 48, 46, 49, 50, 0, 12, 0, 0, 0, 11, 9, 19, 27, 96, 108, 77, 116, 0, 255, 255, 255, 2, 0, 255, 195, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 62, 29, 59, 1, ...>>\n<\/code><\/pre>\n
\nEach MySQL packet<\/a> has 3 elements: length of the payload (3-byte integer), sequence id (1-byte integer), and payload.
\nIn this case, the actual payload is the “Initial Handshake Packet”<\/a>. Let’s extract the payload part using binary matching (see <<>>\/1<\/a> for more information on binary matching):<\/p>\niex> <<payload_length::24, sequence_id::8, payload::binary>> = data\niex> payload_length\n4849664\niex> byte_size(payload)\n74\n<\/code><\/pre>\n
74<\/code> so why
payload_length<\/code> is
4849664<\/code>?! Numerical values when stored in a binary have “endianness”<\/a> which basically means whether we should read bits\/bytes from “little-end” (least significant bit) or “big-end” (most significant bit).
\nThus, a 3-byte integer <<74, 0, 0>><\/code> in “big-endian” is indeed
4849664<\/code> but in “little-endian” it’s
74<\/code>. Fortunately, bitstring syntax has great support for endianess and it’s as easy as adding
little<\/code> modifier (“big-endian” is the default):<\/p>\n
iex> <<payload_length::24-little, sequence_id::8, payload::binary>> = data\niex> payload_length\n74\n<\/code><\/pre>\n
binpp<\/code><\/a> package:<\/p>\n
iex> :binpp.pprint(payload)\n0000 0A 38 2E 30 2E 31 32 00 0F 00 00 00 27 73 79 59 .8.0.12.....'syY\n0001 7A 34 26 3B 00 FF FF FF 02 00 FF C3 15 00 00 00 z4&;.\u00ff\u00ff\u00ff..\u00ff\u00c3....\n0002 00 00 00 00 00 00 00 43 55 6B 60 74 5A 71 08 75 .......CUk`tZq.u\n0003 6F 08 2F 00 63 61 63 68 69 6E 67 5F 73 68 61 32 o.\/.caching_sha2\n0004 5F 70 61 73 73 77 6F 72 64 00 _password.\n<\/code><\/pre>\n
10<\/code> (
0x0A<\/code>), and what follows is a null-terminated server version string. Let’s extract that:<\/p>\n
iex> <<10, rest::binary>> = payload\niex> [server_version, rest] = :binary.split(rest, <<0x00>>)\niex> server_version\n\"8.0.12\"\n<\/code><\/pre>\n
\"caching_sha2_password\"<\/code>.<\/p>\n
iex> use Bitwise\niex> capability_flags = 0x00000200 ||| 0x00008000 ||| 0x00080000\niex> max_packet_size = 65535\niex> charset = 0x21\niex> username = \"myxql_test\"\niex> auth_response = <<0x00>>\niex> client_auth_plugin = \"caching_sha2_password\"\niex> payload = <<\n capability_flags::32-little,\n max_packet_size::32-little,\n charset, 0::8*23,\n username::binary, 0x00,\n auth_response::binary,\n client_auth_plugin::binary, 0x00\n>>\niex> sequence_id = 1\niex> data = <<byte_size(payload)::24-little, sequence_id, payload::binary>>\n<\/code><\/pre>\n
CLIENT_PROTOCOL_41<\/code>,
CLIENT_SECURE_CONNECTION<\/code>, and
CLIENT_PLUGIN_AUTH<\/code> capability flags<\/a> using “bitwise OR”. Secondly, we set the max packet size, charset (
0x21<\/code> is
utf8_general_ci<\/code>), filler (
0<\/code>s repeated 23 times), username, auth response (empty password is a null byte), and auth plugin name. Note, we encode
username<\/code> and
client_auth_plugin<\/code> as null-terminated strings. Finally, we generate
payload<\/code> and encode it in a packet with payload length and sequence id (it’s 2nd packet so sequence id is
1<\/code>). Let’s now send this and receive response from the server:<\/p>\n
iex> :ok = :gen_tcp.send(sock, data)\niex> {:ok, data} = :gen_tcp.recv(sock, 0)\niex> <<payload_length::24-little, sequence_id::8, payload::binary>> = data\niex> :binpp.pprint(payload)\n0000 00 00 00 02 00 00 00\n<\/code><\/pre>\n
0x00<\/code> which corresponds to the
OK_Packet<\/code><\/a>, authentication succedded! Even though we’ve glossed over many details, we’ve shown that we can integrate with the server end-to-end and that’s going to be a foundation we’ll built upon. There are many more packets that we’ll need to encode or decode and we’re gonna need a more structured approach which we will discuss in part II.<\/p>\n