changed CHANGELOG.md
 
@@ -1,5 +1,19 @@
1
1
# Changelog
2
2
3
+ ## v0.2.3 (2019-05-23)
4
+
5
+ ### Enhancements
6
+
7
+ * Default charset to utf8mb4
8
+ * Raise error when server does not support required capabilities
9
+ * Implement public key exchange for sha auth methods
10
+ * Support older MySQL versions (tested against 5.5 and 5.6)
11
+ * Change `MyXQL.start_option/0` to use `:ssl.tls_client_option/0` type
12
+
13
+ ### Bug fixes
14
+
15
+ * Handle error packet on handshake
16
+
3
17
## v0.2.2 (2019-04-05)
4
18
5
19
### Bug fixes
changed README.md
 
@@ -10,7 +10,11 @@ Documentation: <https://hexdocs.pm/myxql>
10
10
11
11
* Automatic decoding and encoding of Elixir values to and from MySQL text and binary protocols
12
12
* Supports transactions, prepared queries, streaming, pooling and more via [DBConnection](https://github.com/elixir-ecto/db_connection)
13
- * Supports MySQL 5.7.10+, 8.0, and MariaDB 10.3
13
+ * Supports MySQL 5.5+, 8.0, and MariaDB 10.3
14
+ * Supports `mysql_native_password`, `sha256_password` (\*), and `caching_sha256_password` (\*)
15
+ authentication plugins
16
+
17
+ \* Requires either SSL connection or the server must exchange it's public key during handshake.
14
18
15
19
## Usage
16
20
 
@@ -19,7 +23,7 @@ Add `:myxql` to your dependencies:
19
23
```elixir
20
24
def deps() do
21
25
[
22
- {:myxql, "~> 0.1.1"}
26
+ {:myxql, "~> 0.2.0"}
23
27
]
24
28
end
25
29
```
 
@@ -31,18 +35,20 @@ iex> MyXQL.query!(pid, "CREATE DATABASE IF NOT EXISTS blog")
31
35
iex> {:ok, pid} = MyXQL.start_link(username: "root", database: "blog")
32
36
iex> MyXQL.query!(pid, "CREATE TABLE posts IF NOT EXISTS (id serial primary key, title text)")
33
37
34
- iex> MyXQL.query(pid, "INSERT INTO posts (`title`) VALUES ('Post 1')")
35
- {:ok, %MyXQL.Result{columns: nil, last_insert_id: 1, num_rows: 1, rows: nil}}
38
+ iex> MyXQL.query!(pid, "INSERT INTO posts (`title`) VALUES ('Post 1')")
39
+ %MyXQL.Result{columns: nil, connection_id: 11204,, last_insert_id: 1, num_rows: 1, num_warnings: 0, rows: nil}
36
40
37
41
iex> MyXQL.query(pid, "INSERT INTO posts (`title`) VALUES (?), (?)", ["Post 2", "Post 3"])
38
- {:ok, %MyXQL.Result{columns: [], last_insert_id: 3, num_rows: 2, rows: nil}}
42
+ %MyXQL.Result{columns: nil, connection_id: 11204, last_insert_id: 2, num_rows: 2, num_warnings: 0, rows: nil}
39
43
40
44
iex> MyXQL.query(pid, "SELECT * FROM posts")
41
45
{:ok,
42
46
%MyXQL.Result{
43
47
columns: ["id", "title"],
48
+ connection_id: 11204,
44
49
last_insert_id: nil,
45
50
num_rows: 3,
51
+ num_warnings: 0,
46
52
rows: [[1, "Post 1"], [2, "Post 2"], [3, "Post 3"]]
47
53
}}
48
54
```
 
@@ -63,6 +69,8 @@ defmodule MyApp.Application do
63
69
end
64
70
```
65
71
72
+ and then we can refer to it by it's `:name`:
73
+
66
74
```elixir
67
75
iex> MyXQL.query!(:myxql, "SELECT NOW()").rows
68
76
[[~N[2018-12-28 13:42:31]]]
 
@@ -70,7 +78,7 @@ iex> MyXQL.query!(:myxql, "SELECT NOW()").rows
70
78
71
79
## Mariaex Compatibility
72
80
73
- See [Mariaex Compatibility](./MARIAEX_COMPATIBILITY.md) page for transition between drivers.
81
+ See [Mariaex Compatibility](https://github.com/elixir-ecto/myxql/blob/master/MARIAEX_COMPATIBILITY.md) page for transition between drivers.
74
82
75
83
## Data representation
76
84
 
@@ -120,7 +128,7 @@ mix deps.get
120
128
mix test
121
129
```
122
130
123
- See [`ci.sh`](ci.sh) for a script used to test against different server versions.
131
+ See [`scripts/ci.sh`](scripts/ci.sh) and [`scripts/test-versions.sh`](scripts/test-versions.sh) for a scripts used to test against different server versions.
124
132
125
133
## License
changed hex_metadata.config
 
@@ -1,15 +1,14 @@
1
1
{<<"app">>,<<"myxql">>}.
2
2
{<<"build_tools">>,[<<"mix">>]}.
3
- {<<"description">>,<<"MySQL 5.7+ driver for Elixir">>}.
3
+ {<<"description">>,<<"MySQL 5.5+ driver for Elixir">>}.
4
4
{<<"elixir">>,<<"~> 1.4">>}.
5
5
{<<"files">>,
6
6
[<<"lib">>,<<"lib/myxql">>,<<"lib/myxql/client.ex">>,
7
- <<"lib/myxql/error.ex">>,<<"lib/myxql/protocol">>,
8
- <<"lib/myxql/protocol/flags.ex">>,<<"lib/myxql/protocol/records.ex">>,
9
- <<"lib/myxql/protocol/types.ex">>,<<"lib/myxql/protocol/auth.ex">>,
10
- <<"lib/myxql/protocol/values.ex">>,
11
- <<"lib/myxql/protocol/server_error_codes.ex">>,
12
- <<"lib/myxql/protocol/messages.ex">>,<<"lib/myxql/query.ex">>,
7
+ <<"lib/myxql/protocol.ex">>,<<"lib/myxql/error.ex">>,
8
+ <<"lib/myxql/protocol">>,<<"lib/myxql/protocol/flags.ex">>,
9
+ <<"lib/myxql/protocol/records.ex">>,<<"lib/myxql/protocol/types.ex">>,
10
+ <<"lib/myxql/protocol/auth.ex">>,<<"lib/myxql/protocol/values.ex">>,
11
+ <<"lib/myxql/protocol/server_error_codes.ex">>,<<"lib/myxql/query.ex">>,
13
12
<<"lib/myxql/cursor.ex">>,<<"lib/myxql/result.ex">>,
14
13
<<"lib/myxql/connection.ex">>,<<"lib/myxql/text_query.ex">>,
15
14
<<"lib/myxql.ex">>,<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,
 
@@ -33,4 +32,4 @@
33
32
{<<"optional">>,true},
34
33
{<<"repository">>,<<"hexpm">>},
35
34
{<<"requirement">>,<<"~> 1.0">>}]]}.
36
- {<<"version">>,<<"0.2.2">>}.
35
+ {<<"version">>,<<"0.2.3">>}.
changed lib/myxql.ex
 
@@ -15,7 +15,7 @@ defmodule MyXQL do
15
15
| {:username, String.t()}
16
16
| {:password, String.t() | nil}
17
17
| {:ssl, boolean()}
18
- | {:ssl_opts, [:ssl.ssl_option()]}
18
+ | {:ssl_opts, [:ssl.tls_client_option()]}
19
19
| {:connect_timeout, timeout()}
20
20
| {:handshake_timeout, timeout()}
21
21
| {:ping_timeout, timeout()}
changed lib/myxql/client.ex
 
@@ -2,7 +2,7 @@ defmodule MyXQL.Client do
2
2
@moduledoc false
3
3
4
4
require Logger
5
- import MyXQL.Protocol.{Flags, Messages, Records, Types}
5
+ import MyXQL.{Protocol, Protocol.Records, Protocol.Types}
6
6
alias MyXQL.Protocol.Auth
7
7
8
8
defmodule Config do
 
@@ -127,6 +127,12 @@ defmodule MyXQL.Client do
127
127
send_packet(payload, 0, state)
128
128
end
129
129
130
+ def send_recv_packet(payload, decoder, sequence_id, sock) do
131
+ with :ok <- send_packet(payload, sequence_id, sock) do
132
+ recv_packet(decoder, sock)
133
+ end
134
+ end
135
+
130
136
def send_packet(payload, sequence_id, state) do
131
137
data = encode_packet(payload, sequence_id)
132
138
send_data(state, data)
 
@@ -238,129 +244,35 @@ defmodule MyXQL.Client do
238
244
with {:ok, initial_handshake(conn_id: conn_id) = initial_handshake} <- recv_handshake(state),
239
245
state = %{state | connection_id: conn_id},
240
246
sequence_id = 1,
241
- :ok <- ensure_capabilities(initial_handshake),
242
- {:ok, sequence_id, state} <- maybe_upgrade_to_ssl(config, sequence_id, state) do
243
- send_handshake_response(config, initial_handshake, sequence_id, state)
244
- end
245
- end
247
+ {:ok, capability_flags} <- build_capability_flags(config, initial_handshake),
248
+ {:ok, sequence_id, state} <-
249
+ maybe_upgrade_to_ssl(config, capability_flags, sequence_id, state) do
250
+ result =
251
+ handle_handshake_response(
252
+ config,
253
+ initial_handshake,
254
+ capability_flags,
255
+ sequence_id,
256
+ state
257
+ )
246
258
247
- defp recv_handshake(state) do
248
- recv_packet(&decode_initial_handshake/1, state)
249
- end
250
-
251
- defp ensure_capabilities(initial_handshake(capability_flags: capability_flags)) do
252
- if has_capability_flag?(capability_flags, :client_deprecate_eof) do
253
- :ok
254
- else
255
- {:error, :server_not_supported}
256
- end
257
- end
258
-
259
- defp send_handshake_response(
260
- config,
261
- initial_handshake,
262
- sequence_id,
263
- state
264
- ) do
265
- initial_handshake(
266
- auth_plugin_name: auth_plugin_name,
267
- auth_plugin_data: auth_plugin_data
268
- ) = initial_handshake
269
-
270
- auth_response = auth_response(auth_plugin_name, auth_plugin_data, config.password)
271
-
272
- payload =
273
- encode_handshake_response_41(
274
- config.username,
275
- auth_plugin_name,
276
- auth_response,
277
- config.database,
278
- config.ssl?
279
- )
280
-
281
- with :ok <- send_packet(payload, sequence_id, state) do
282
- case recv_packet(&decode_handshake_response/1, state) do
259
+ case result do
283
260
{:ok, ok_packet()} ->
284
261
{:ok, state}
285
262
286
263
{:ok, err_packet() = err_packet} ->
287
264
{:error, err_packet}
288
265
289
- {:ok, auth_switch_request(plugin_name: plugin_name, plugin_data: plugin_data)} ->
290
- with {:ok, auth_response} <-
291
- auth_switch_response(plugin_name, config.password, plugin_data, config.ssl?),
292
- :ok <- send_packet(auth_response, sequence_id + 2, state) do
293
- case recv_packet(&decode_handshake_response/1, state) do
294
- {:ok, ok_packet(num_warnings: 0)} ->
295
- {:ok, state}
296
-
297
- {:ok, err_packet() = err_packet} ->
298
- {:error, err_packet}
299
-
300
- {:error, _reason} = error ->
301
- error
302
- end
303
- end
304
-
305
- {:ok, :full_auth} ->
306
- if config.ssl? do
307
- auth_response = config.password <> <<0x00>>
308
-
309
- with :ok <- send_packet(auth_response, sequence_id + 2, state) do
310
- case recv_packet(&decode_handshake_response/1, state) do
311
- {:ok, ok_packet(num_warnings: 0)} ->
312
- {:ok, state}
313
-
314
- {:ok, err_packet() = err_packet} ->
315
- {:error, err_packet}
316
-
317
- {:error, _reason} = error ->
318
- error
319
- end
320
- end
321
- else
322
- auth_plugin_secure_connection_error(auth_plugin_name)
323
- end
324
-
325
- {:error, _reason} = error ->
326
- error
266
+ other ->
267
+ other
327
268
end
328
269
end
329
270
end
330
271
331
- defp auth_response(_plugin_name, _plugin_data, nil),
332
- do: nil
333
-
334
- defp auth_response("mysql_native_password", plugin_data, password),
335
- do: Auth.mysql_native_password(password, plugin_data)
336
-
337
- defp auth_response(plugin_name, plugin_data, password)
338
- when plugin_name in ["sha256_password", "caching_sha2_password"],
339
- do: Auth.sha256_password(password, plugin_data)
340
-
341
- defp auth_switch_response(_plugin_name, nil, _plugin_data, _ssl?),
342
- do: {:ok, <<>>}
343
-
344
- defp auth_switch_response("mysql_native_password", password, plugin_data, _ssl?),
345
- do: {:ok, Auth.mysql_native_password(password, plugin_data)}
346
-
347
- defp auth_switch_response(plugin_name, password, _plugin_data, ssl?)
348
- when plugin_name in ["sha256_password", "caching_sha2_password"] do
349
- if ssl? do
350
- {:ok, password <> <<0x00>>}
351
- else
352
- auth_plugin_secure_connection_error(plugin_name)
353
- end
354
- end
355
-
356
- # https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html#error_cr_auth_plugin_err
357
- defp auth_plugin_secure_connection_error(plugin_name) do
358
- {:error, {:auth_plugin_error, {plugin_name, "Authentication requires secure connection"}}}
359
- end
360
-
361
- defp maybe_upgrade_to_ssl(%{ssl?: true} = config, sequence_id, state) do
272
+ defp maybe_upgrade_to_ssl(%{ssl?: true} = config, capability_flags, sequence_id, state) do
362
273
{_, sock} = state.sock
363
- payload = encode_ssl_request(config.database)
274
+ ssl_request = ssl_request(capability_flags: capability_flags)
275
+ payload = encode_ssl_request(ssl_request)
364
276
365
277
with :ok <- send_packet(payload, sequence_id, state),
366
278
{:ok, ssl_sock} <- :ssl.connect(sock, config.ssl_opts, config.connect_timeout) do
 
@@ -368,10 +280,106 @@ defmodule MyXQL.Client do
368
280
end
369
281
end
370
282
371
- defp maybe_upgrade_to_ssl(%{ssl?: false}, sequence_id, state) do
283
+ defp maybe_upgrade_to_ssl(%{ssl?: false}, _capability_flags, sequence_id, state) do
372
284
{:ok, sequence_id, state}
373
285
end
374
286
287
+ defp recv_handshake(state) do
288
+ recv_packet(&decode_initial_handshake/1, state)
289
+ end
290
+
291
+ defp handle_handshake_response(config, initial_handshake, capability_flags, sequence_id, state) do
292
+ initial_handshake(
293
+ auth_plugin_name: initial_auth_plugin_name,
294
+ auth_plugin_data: initial_auth_plugin_data
295
+ ) = initial_handshake
296
+
297
+ auth_response = Auth.auth_response(config, initial_auth_plugin_name, initial_auth_plugin_data)
298
+
299
+ handshake_response =
300
+ handshake_response_41(
301
+ capability_flags: capability_flags,
302
+ username: config.username,
303
+ auth_plugin_name: initial_auth_plugin_name,
304
+ auth_response: auth_response,
305
+ database: config.database
306
+ )
307
+
308
+ payload = encode_handshake_response_41(handshake_response)
309
+
310
+ case send_recv_packet(payload, &decode_auth_response/1, sequence_id, state) do
311
+ {:ok, auth_switch_request(plugin_name: auth_plugin_name, plugin_data: auth_plugin_data)} ->
312
+ auth_response = Auth.auth_response(config, auth_plugin_name, initial_auth_plugin_data)
313
+
314
+ case send_recv_packet(auth_response, &decode_auth_response/1, sequence_id + 2, state) do
315
+ {:ok, :full_auth} ->
316
+ perform_full_auth(config, auth_plugin_name, auth_plugin_data, sequence_id + 2, state)
317
+
318
+ {:ok, auth_more_data(data: public_key)} ->
319
+ perform_public_key_auth(
320
+ config.password,
321
+ public_key,
322
+ auth_plugin_data,
323
+ sequence_id + 4,
324
+ state
325
+ )
326
+
327
+ other ->
328
+ other
329
+ end
330
+
331
+ {:ok, :full_auth} ->
332
+ perform_full_auth(
333
+ config,
334
+ initial_auth_plugin_name,
335
+ initial_auth_plugin_data,
336
+ sequence_id,
337
+ state
338
+ )
339
+
340
+ {:ok, auth_more_data(data: public_key)} ->
341
+ perform_public_key_auth(
342
+ config.password,
343
+ public_key,
344
+ initial_auth_plugin_data,
345
+ sequence_id + 2,
346
+ state
347
+ )
348
+
349
+ other ->
350
+ other
351
+ end
352
+ end
353
+
354
+ defp perform_public_key_auth(password, public_key, auth_plugin_data, sequence_id, state) do
355
+ auth_response = Auth.encrypt_sha_password(password, public_key, auth_plugin_data)
356
+ send_recv_packet(auth_response, &decode_auth_response/1, sequence_id, state)
357
+ end
358
+
359
+ defp perform_full_auth(config, "caching_sha2_password", auth_plugin_data, sequence_id, state) do
360
+ auth_response =
361
+ if config.ssl? do
362
+ [config.password, 0]
363
+ else
364
+ # request public key
365
+ <<2>>
366
+ end
367
+
368
+ case send_recv_packet(auth_response, &decode_auth_response/1, sequence_id + 2, state) do
369
+ {:ok, auth_more_data(data: public_key)} ->
370
+ perform_public_key_auth(
371
+ config.password,
372
+ public_key,
373
+ auth_plugin_data,
374
+ sequence_id + 4,
375
+ state
376
+ )
377
+
378
+ other ->
379
+ other
380
+ end
381
+ end
382
+
375
383
defp start_handshake_timer(:infinity, _), do: :infinity
376
384
377
385
defp start_handshake_timer(timeout, sock) do
changed lib/myxql/connection.ex
 
@@ -3,8 +3,7 @@ defmodule MyXQL.Connection do
3
3
4
4
use DBConnection
5
5
import MyXQL.Protocol.{Flags, Records}
6
- alias MyXQL.{Client, Cursor, Query, Result, TextQuery}
7
- alias MyXQL.Protocol.ServerErrorCodes
6
+ alias MyXQL.{Client, Cursor, Query, Protocol, Result, TextQuery}
8
7
9
8
@disconnect_on_error_codes [
10
9
:ER_MAX_PREPARED_STMT_COUNT_REACHED
 
@@ -31,7 +30,7 @@ defmodule MyXQL.Connection do
31
30
@disconnect_on_error_codes ++ Keyword.get(opts, :disconnect_on_error_codes, [])
32
31
33
32
case Client.connect(opts) do
34
- {:ok, state} ->
33
+ {:ok, %{} = state} ->
35
34
state = %__MODULE__{
36
35
prepare: prepare,
37
36
disconnect_on_error_codes: disconnect_on_error_codes,
 
@@ -42,6 +41,9 @@ defmodule MyXQL.Connection do
42
41
43
42
{:ok, state}
44
43
44
+ {:ok, err_packet() = err_packet} ->
45
+ {:error, error(err_packet)}
46
+
45
47
{:error, :enoent} ->
46
48
exception = error(:enoent)
47
49
{:local, socket} = config.address
 
@@ -322,22 +324,10 @@ defmodule MyXQL.Connection do
322
324
end
323
325
324
326
defp error(err_packet(code: code, message: message)) do
325
- name = ServerErrorCodes.code_to_name(code)
327
+ name = Protocol.error_code_to_name(code)
326
328
%MyXQL.Error{message: "(#{code}) (#{name}) " <> message, mysql: %{code: code, name: name}}
327
329
end
328
330
329
- defp error({:auth_plugin_error, {auth_plugin, message}}) do
330
- message = "Authentication plugin '#{auth_plugin}' reported error: #{message}"
331
- code = 2061
332
- name = :CR_AUTH_PLUGIN_ERR
333
- %MyXQL.Error{message: "(#{code}) (#{name}) " <> message, mysql: %{code: code, name: name}}
334
- end
335
-
336
- defp error(:server_not_supported) do
337
- message = "MyXQL requires MySQL server 5.7.10+"
338
- %MyXQL.Error{message: message}
339
- end
340
-
341
331
defp error(reason) do
342
332
%DBConnection.ConnectionError{message: format_reason(reason)}
343
333
end
added lib/myxql/protocol.ex
 
@@ -0,0 +1,529 @@
1
+ defmodule MyXQL.Protocol do
2
+ @moduledoc false
3
+
4
+ import MyXQL.Protocol.{Flags, Records, Types}
5
+ alias MyXQL.Protocol.Values
6
+ use Bitwise
7
+
8
+ defdelegate error_code_to_name(code), to: MyXQL.Protocol.ServerErrorCodes, as: :code_to_name
9
+
10
+ # https://dev.mysql.com/doc/internals/en/com-stmt-execute.html
11
+ @cursor_types %{
12
+ cursor_type_no_cursor: 0x00,
13
+ cursor_type_read_only: 0x01,
14
+ cursor_type_for_update: 0x02,
15
+ cursor_type_scrollable: 0x04
16
+ }
17
+
18
+ ###########################################################
19
+ # Basic packets
20
+ #
21
+ # https://dev.mysql.com/doc/internals/en/mysql-packet.html
22
+ ###########################################################
23
+
24
+ def encode_packet(payload, sequence_id) do
25
+ payload_length = IO.iodata_length(payload)
26
+ [<<payload_length::uint3, sequence_id::uint1>>, payload]
27
+ end
28
+
29
+ def decode_generic_response(<<0x00, rest::bits>>) do
30
+ decode_ok_packet_body(rest)
31
+ end
32
+
33
+ def decode_generic_response(<<0xFF, rest::bits>>) do
34
+ decode_err_packet_body(rest)
35
+ end
36
+
37
+ defp decode_ok_packet_body(rest) do
38
+ {affected_rows, rest} = take_int_lenenc(rest)
39
+ {last_insert_id, rest} = take_int_lenenc(rest)
40
+
41
+ <<
42
+ status_flags::uint2,
43
+ num_warnings::uint2,
44
+ info::binary
45
+ >> = rest
46
+
47
+ ok_packet(
48
+ affected_rows: affected_rows,
49
+ last_insert_id: last_insert_id,
50
+ status_flags: status_flags,
51
+ num_warnings: num_warnings,
52
+ info: info
53
+ )
54
+ end
55
+
56
+ defp decode_err_packet_body(
57
+ <<code::uint2, _sql_state_marker::string(1), _sql_state::string(5), message::bits>>
58
+ ) do
59
+ err_packet(code: code, message: message)
60
+ end
61
+
62
+ def decode_eof_packet(<<0xFE, rest::binary>>) do
63
+ decode_eof_packet_body(rest)
64
+ end
65
+
66
+ defp decode_eof_packet_body(<<num_warnings::uint2, status_flags::uint2>>) do
67
+ eof_packet(
68
+ status_flags: status_flags,
69
+ num_warnings: num_warnings
70
+ )
71
+ end
72
+
73
+ defp decode_connect_err_packet_body(<<code::uint2, message::bits>>) do
74
+ err_packet(code: code, message: message)
75
+ end
76
+
77
+ ##############################################################
78
+ # Connection Phase
79
+ #
80
+ # https://dev.mysql.com/doc/internals/en/connection-phase.html
81
+ ##############################################################
82
+
83
+ def decode_initial_handshake(<<10, rest::binary>>) do
84
+ {server_version, rest} = take_string_nul(rest)
85
+
86
+ <<
87
+ conn_id::uint4,
88
+ auth_plugin_data1::string(8),
89
+ 0,
90
+ capability_flags1::uint2,
91
+ character_set::uint1,
92
+ status_flags::uint2,
93
+ capability_flags2::uint2,
94
+ rest::binary
95
+ >> = rest
96
+
97
+ <<capability_flags::uint4>> = <<capability_flags1::uint2, capability_flags2::uint2>>
98
+ # all set in servers since MySQL 4.1
99
+ required_capabilities = [:client_protocol_41, :client_plugin_auth, :client_secure_connection]
100
+
101
+ with :ok <- ensure_capabilities(capability_flags, required_capabilities) do
102
+ <<
103
+ auth_plugin_data_length::uint1,
104
+ _::uint(10),
105
+ rest::binary
106
+ >> = rest
107
+
108
+ take = max(13, auth_plugin_data_length - 8)
109
+ <<auth_plugin_data2::binary-size(take), auth_plugin_name::binary>> = rest
110
+ auth_plugin_data2 = decode_string_nul(auth_plugin_data2)
111
+ auth_plugin_name = decode_string_nul(auth_plugin_name)
112
+ auth_plugin_data = auth_plugin_data1 <> auth_plugin_data2
113
+
114
+ initial_handshake(
115
+ server_version: server_version,
116
+ conn_id: conn_id,
117
+ auth_plugin_name: auth_plugin_name,
118
+ auth_plugin_data: auth_plugin_data,
119
+ capability_flags: capability_flags,
120
+ character_set: character_set,
121
+ status_flags: status_flags
122
+ )
123
+ end
124
+ end
125
+
126
+ def decode_initial_handshake(<<0xFF, rest::bits>>) do
127
+ decode_connect_err_packet_body(rest)
128
+ end
129
+
130
+ defp ensure_capabilities(capability_flags, names) do
131
+ Enum.reduce_while(names, :ok, fn name, _acc ->
132
+ if has_capability_flag?(capability_flags, name) do
133
+ {:cont, :ok}
134
+ else
135
+ {:halt, {:error, {:server_missing_capability, name}}}
136
+ end
137
+ end)
138
+ end
139
+
140
+ def build_capability_flags(config, initial_handshake) do
141
+ initial_handshake(capability_flags: server_capability_flags) = initial_handshake
142
+
143
+ client_capability_flags =
144
+ put_capability_flags([
145
+ :client_protocol_41,
146
+ :client_plugin_auth,
147
+ :client_secure_connection,
148
+ :client_found_rows,
149
+ :client_multi_results,
150
+ # set by servers since 4.0
151
+ :client_transactions
152
+ ])
153
+ |> maybe_put_capability_flag(:client_connect_with_db, !is_nil(config.database))
154
+ |> maybe_put_capability_flag(:client_ssl, config.ssl?)
155
+
156
+ if config.ssl? && !has_capability_flag?(server_capability_flags, :client_ssl) do
157
+ {:error, :server_does_not_support_ssl}
158
+ else
159
+ client_capabilities = list_capability_flags(client_capability_flags)
160
+
161
+ with :ok <- ensure_capabilities(server_capability_flags, client_capabilities) do
162
+ {:ok, client_capability_flags}
163
+ end
164
+ end
165
+ end
166
+
167
+ defp maybe_put_capability_flag(flags, name, true), do: put_capability_flags(flags, [name])
168
+ defp maybe_put_capability_flag(flags, _name, false), do: flags
169
+
170
+ def encode_handshake_response_41(
171
+ handshake_response_41(
172
+ capability_flags: capability_flags,
173
+ max_packet_size: max_packet_size,
174
+ character_set: character_set,
175
+ username: username,
176
+ auth_plugin_name: auth_plugin_name,
177
+ auth_response: auth_response,
178
+ database: database
179
+ )
180
+ ) do
181
+ auth_response = if auth_response, do: encode_string_lenenc(auth_response), else: <<0>>
182
+ database = if database, do: <<database::binary, 0x00>>, else: ""
183
+
184
+ <<
185
+ capability_flags::uint4,
186
+ max_packet_size::uint4,
187
+ character_set,
188
+ 0::uint(23),
189
+ <<username::binary, 0x00>>,
190
+ auth_response::binary,
191
+ database::binary,
192
+ (<<auth_plugin_name::binary, 0x00>>)
193
+ >>
194
+ end
195
+
196
+ def encode_ssl_request(
197
+ ssl_request(
198
+ capability_flags: capability_flags,
199
+ max_packet_size: max_packet_size,
200
+ character_set: character_set
201
+ )
202
+ ) do
203
+ <<
204
+ capability_flags::uint4,
205
+ max_packet_size::uint4,
206
+ character_set,
207
+ 0::uint(23)
208
+ >>
209
+ end
210
+
211
+ def decode_auth_response(<<0x00, rest::binary>>) do
212
+ decode_ok_packet_body(rest)
213
+ end
214
+
215
+ def decode_auth_response(<<0xFF, rest::binary>>) do
216
+ decode_err_packet_body(rest)
217
+ end
218
+
219
+ def decode_auth_response(<<0x01, 0x04>>) do
220
+ :full_auth
221
+ end
222
+
223
+ def decode_auth_response(<<0x01, rest::binary>>) do
224
+ auth_more_data(data: rest)
225
+ end
226
+
227
+ def decode_auth_response(<<0xFE, rest::binary>>) do
228
+ {plugin_name, rest} = take_string_nul(rest)
229
+ {plugin_data, ""} = take_string_nul(rest)
230
+
231
+ auth_switch_request(plugin_name: plugin_name, plugin_data: plugin_data)
232
+ end
233
+
234
+ #################################################################
235
+ # Text & Binary Protocol
236
+ #
237
+ # https://dev.mysql.com/doc/internals/en/text-protocol.html
238
+ # https://dev.mysql.com/doc/internals/en/prepared-statements.html
239
+ #################################################################
240
+
241
+ # https://dev.mysql.com/doc/internals/en/com-ping.html
242
+ def encode_com(:com_ping) do
243
+ <<0x0E>>
244
+ end
245
+
246
+ # https://dev.mysql.com/doc/internals/en/com-query.html
247
+ def encode_com({:com_query, query}) do
248
+ [0x03, query]
249
+ end
250
+
251
+ # https://dev.mysql.com/doc/internals/en/com-stmt-prepare.html#packet-COM_STMT_PREPARE
252
+ def encode_com({:com_stmt_prepare, query}) do
253
+ [0x16, query]
254
+ end
255
+
256
+ # https://dev.mysql.com/doc/internals/en/com-stmt-close.html
257
+ def encode_com({:com_stmt_close, statement_id}) do
258
+ [0x19, <<statement_id::uint4>>]
259
+ end
260
+
261
+ # https://dev.mysql.com/doc/internals/en/com-stmt-reset.html
262
+ def encode_com({:com_stmt_reset, statement_id}) do
263
+ [0x1A, <<statement_id::uint4>>]
264
+ end
265
+
266
+ # https://dev.mysql.com/doc/internals/en/com-stmt-execute.html
267
+ def encode_com({:com_stmt_execute, statement_id, params, cursor_type}) do
268
+ command = 0x17
269
+ flags = Map.fetch!(@cursor_types, cursor_type)
270
+
271
+ # Always 0x01
272
+ iteration_count = 0x01
273
+
274
+ new_params_bound_flag = 1
275
+ {null_bitmap, types, values} = encode_params(params)
276
+
277
+ <<
278
+ command,
279
+ statement_id::uint4,
280
+ flags::uint1,
281
+ iteration_count::uint4,
282
+ null_bitmap::bitstring,
283
+ new_params_bound_flag::uint1,
284
+ types::binary,
285
+ values::binary
286
+ >>
287
+ end
288
+
289
+ # https://dev.mysql.com/doc/internals/en/com-stmt-fetch.html
290
+ def encode_com({:com_stmt_fetch, statement_id, num_rows}) do
291
+ <<
292
+ 0x1C,
293
+ statement_id::uint4,
294
+ num_rows::uint4
295
+ >>
296
+ end
297
+
298
+ # https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-COM_QUERY_Response
299
+ def decode_com_query_response(<<0x00, rest::binary>>, "", :initial) do
300
+ {:halt, decode_ok_packet_body(rest)}
301
+ end
302
+
303
+ def decode_com_query_response(<<0xFF, rest::binary>>, "", :initial) do
304
+ {:halt, decode_err_packet_body(rest)}
305
+ end
306
+
307
+ def decode_com_query_response(payload, next_data, state) do
308
+ decode_resultset(payload, next_data, state, &Values.decode_text_row/2)
309
+ end
310
+
311
+ def decode_com_stmt_prepare_response(
312
+ <<0x00, statement_id::uint4, num_columns::uint2, num_params::uint2, 0,
313
+ num_warnings::uint2>>,
314
+ next_data,
315
+ :initial
316
+ ) do
317
+ result =
318
+ com_stmt_prepare_ok(
319
+ statement_id: statement_id,
320
+ num_columns: num_columns,
321
+ num_params: num_params,
322
+ num_warnings: num_warnings
323
+ )
324
+
325
+ cond do
326
+ num_params > 0 ->
327
+ {:cont, {result, :params, num_params, num_columns}}
328
+
329
+ num_columns > 0 ->
330
+ {:cont, {result, :columns, num_columns}}
331
+
332
+ true ->
333
+ "" = next_data
334
+ {:halt, result}
335
+ end
336
+ end
337
+
338
+ def decode_com_stmt_prepare_response(<<rest::binary>>, "", :initial) do
339
+ {:halt, decode_generic_response(rest)}
340
+ end
341
+
342
+ # for now, we're simply consuming column_definition packets for params and columns,
343
+ # we might decode them in the future.
344
+
345
+ def decode_com_stmt_prepare_response(
346
+ payload,
347
+ _next_data,
348
+ {com_stmt_prepare_ok, :params, num_params, num_columns}
349
+ ) do
350
+ if num_params > 0 do
351
+ column_def() = decode_column_def(payload)
352
+ {:cont, {com_stmt_prepare_ok, :params, num_params - 1, num_columns}}
353
+ else
354
+ eof_packet() = decode_eof_packet(payload)
355
+
356
+ if num_columns > 0 do
357
+ {:cont, {com_stmt_prepare_ok, :columns, num_columns}}
358
+ else
359
+ {:halt, com_stmt_prepare_ok}
360
+ end
361
+ end
362
+ end
363
+
364
+ def decode_com_stmt_prepare_response(
365
+ payload,
366
+ next_data,
367
+ {com_stmt_prepare_ok, :columns, num_columns}
368
+ ) do
369
+ if num_columns > 0 do
370
+ column_def() = decode_column_def(payload)
371
+ {:cont, {com_stmt_prepare_ok, :columns, num_columns - 1}}
372
+ else
373
+ "" = next_data
374
+ eof_packet() = decode_eof_packet(payload)
375
+ {:halt, com_stmt_prepare_ok}
376
+ end
377
+ end
378
+
379
+ defp encode_params(params) do
380
+ null_type = 0x06
381
+
382
+ {count, null_bitmap, types, values} =
383
+ Enum.reduce(params, {0, 0, <<>>, <<>>}, fn
384
+ value, {idx, null_bitmap, types, values} ->
385
+ null_value = if value == nil, do: 1, else: 0
386
+ null_bitmap = null_bitmap ||| null_value <<< idx
387
+
388
+ if value == nil do
389
+ {idx + 1, null_bitmap, <<types::binary, null_type, unsigned_flag(value)>>, values}
390
+ else
391
+ {type, binary} = Values.encode_binary_value(value)
392
+ type = Values.type_atom_to_code(type)
393
+
394
+ {idx + 1, null_bitmap, <<types::binary, type, unsigned_flag(value)>>,
395
+ <<values::binary, binary::binary>>}
396
+ end
397
+ end)
398
+
399
+ null_bitmap_size = div(count + 7, 8)
400
+ {<<null_bitmap::uint(null_bitmap_size)>>, types, values}
401
+ end
402
+
403
+ defp unsigned_flag(value) when is_integer(value) and value >= 1 <<< 63, do: 0x80
404
+ defp unsigned_flag(_), do: 0x00
405
+
406
+ # https://dev.mysql.com/doc/internals/en/com-stmt-execute-response.html
407
+ def decode_com_stmt_execute_response(<<0x00, rest::binary>>, "", :initial) do
408
+ {:halt, decode_ok_packet_body(rest)}
409
+ end
410
+
411
+ def decode_com_stmt_execute_response(<<0xFF, rest::binary>>, "", :initial) do
412
+ {:halt, decode_err_packet_body(rest)}
413
+ end
414
+
415
+ def decode_com_stmt_execute_response(payload, next_data, state) do
416
+ decode_resultset(payload, next_data, state, &Values.decode_binary_row/2)
417
+ end
418
+
419
+ # https://dev.mysql.com/doc/internals/en/com-stmt-fetch-response.html
420
+ def decode_com_stmt_fetch_response(<<0xFF, rest::binary>>, "", {:initial, _column_defs}) do
421
+ {:halt, decode_err_packet_body(rest)}
422
+ end
423
+
424
+ def decode_com_stmt_fetch_response(payload, next_data, {:initial, column_defs}) do
425
+ decode_com_stmt_fetch_response(payload, next_data, {:rows, column_defs, 0, []})
426
+ end
427
+
428
+ def decode_com_stmt_fetch_response(payload, next_data, state) do
429
+ decode_resultset(payload, next_data, state, &Values.decode_binary_row/2)
430
+ end
431
+
432
+ def decode_column_def(<<3, "def", rest::binary>>) do
433
+ {_schema, rest} = take_string_lenenc(rest)
434
+ {_table, rest} = take_string_lenenc(rest)
435
+ {_org_table, rest} = take_string_lenenc(rest)
436
+ {name, rest} = take_string_lenenc(rest)
437
+ {_org_name, rest} = take_string_lenenc(rest)
438
+
439
+ <<
440
+ 0x0C,
441
+ _character_set::uint2,
442
+ _column_length::uint4,
443
+ type::uint1,
444
+ flags::uint2,
445
+ _decimals::uint1,
446
+ 0::uint2
447
+ >> = rest
448
+
449
+ column_def(
450
+ name: name,
451
+ type: Values.type_code_to_atom(type),
452
+ flags: flags,
453
+ unsigned?: has_column_flag?(flags, :unsigned_flag)
454
+ )
455
+ end
456
+
457
+ defp decode_resultset(payload, _next_data, :initial, _row_decoder) do
458
+ {:cont, {:column_defs, decode_int_lenenc(payload), []}}
459
+ end
460
+
461
+ defp decode_resultset(payload, _next_data, {:column_defs, num_columns, acc}, _row_decoder) do
462
+ column_def = decode_column_def(payload)
463
+ acc = [column_def | acc]
464
+
465
+ if num_columns > 1 do
466
+ {:cont, {:column_defs, num_columns - 1, acc}}
467
+ else
468
+ {:cont, {:column_defs_eof, Enum.reverse(acc)}}
469
+ end
470
+ end
471
+
472
+ defp decode_resultset(
473
+ <<0xFE, num_warnings::uint2, status_flags::uint2>>,
474
+ next_data,
475
+ {:column_defs_eof, column_defs},
476
+ _row_decoder
477
+ ) do
478
+ if has_status_flag?(status_flags, :server_status_cursor_exists) do
479
+ "" = next_data
480
+
481
+ {:halt,
482
+ resultset(
483
+ column_defs: column_defs,
484
+ num_rows: 0,
485
+ rows: [],
486
+ num_warnings: num_warnings,
487
+ status_flags: status_flags
488
+ )}
489
+ else
490
+ {:cont, {:rows, column_defs, 0, []}}
491
+ end
492
+ end
493
+
494
+ defp decode_resultset(
495
+ <<0xFE, num_warnings::uint2, status_flags::uint2>>,
496
+ _next_data,
497
+ {:rows, column_defs, num_rows, acc},
498
+ _row_decoder
499
+ ) do
500
+ resultset =
501
+ resultset(
502
+ column_defs: column_defs,
503
+ num_rows: num_rows,
504
+ rows: Enum.reverse(acc),
505
+ num_warnings: num_warnings,
506
+ status_flags: status_flags
507
+ )
508
+
509
+ if has_status_flag?(status_flags, :server_more_results_exists) do
510
+ {:cont, {:trailing_ok_packet, resultset}}
511
+ else
512
+ {:halt, resultset}
513
+ end
514
+ end
515
+
516
+ defp decode_resultset(payload, _next_data, {:rows, column_defs, num_rows, acc}, row_decoder) do
517
+ row = row_decoder.(payload, column_defs)
518
+ {:cont, {:rows, column_defs, num_rows + 1, [row | acc]}}
519
+ end
520
+
521
+ defp decode_resultset(payload, "", {:trailing_ok_packet, resultset}, _row_decoder) do
522
+ ok_packet(status_flags: status_flags) = decode_generic_response(payload)
523
+ {:halt, resultset(resultset, status_flags: status_flags)}
524
+ end
525
+
526
+ defp decode_resultset(_payload, _next_data, {:trailing_ok_packet, _resultset}, _row_decoder) do
527
+ {:error, :multiple_results}
528
+ end
529
+ end
changed lib/myxql/protocol/auth.ex
 
@@ -14,6 +14,33 @@ defmodule MyXQL.Protocol.Auth do
14
14
sha_hash(:sha256, password, auth_plugin_data)
15
15
end
16
16
17
+ def encrypt_sha_password(password, public_key, auth_plugin_data) do
18
+ password = password <> <<0>>
19
+ xor = :crypto.exor(password, :binary.part(auth_plugin_data, 0, byte_size(password)))
20
+ [entry] = :public_key.pem_decode(public_key)
21
+ public_key = :public_key.pem_entry_decode(entry)
22
+ :public_key.encrypt_public(xor, public_key, rsa_pad: :rsa_pkcs1_oaep_padding)
23
+ end
24
+
25
+ def auth_response(config, auth_plugin_name, initial_auth_plugin_data) do
26
+ cond do
27
+ config.password == nil ->
28
+ ""
29
+
30
+ auth_plugin_name == "mysql_native_password" ->
31
+ mysql_native_password(config.password, initial_auth_plugin_data)
32
+
33
+ auth_plugin_name == "sha256_password" and config.ssl? ->
34
+ config.password <> <<0>>
35
+
36
+ auth_plugin_name == "sha256_password" and not config.ssl? ->
37
+ <<1>>
38
+
39
+ auth_plugin_name == "caching_sha2_password" ->
40
+ sha256_password(config.password, initial_auth_plugin_data)
41
+ end
42
+ end
43
+
17
44
## Helpers
18
45
19
46
defp sha_hash(type, password, auth_plugin_data)
removed lib/myxql/protocol/messages.ex
 
@@ -1,412 +0,0 @@
1
- defmodule MyXQL.Protocol.Messages do
2
- @moduledoc false
3
- import MyXQL.Protocol.{Flags, Records, Types}
4
- alias MyXQL.Protocol.Values
5
- use Bitwise
6
-
7
- @max_packet_size 16_777_215
8
-
9
- defp capability_flags(database, ssl?) do
10
- put_capability_flags([
11
- :client_protocol_41,
12
- :client_deprecate_eof,
13
- :client_plugin_auth,
14
- :client_secure_connection,
15
- :client_found_rows,
16
- :client_multi_results,
17
- :client_transactions
18
- ])
19
- |> maybe_put_capability_flag(:client_connect_with_db, !is_nil(database))
20
- |> maybe_put_capability_flag(:client_ssl, ssl?)
21
- end
22
-
23
- defp maybe_put_capability_flag(flags, name, true), do: put_capability_flags(flags, [name])
24
- defp maybe_put_capability_flag(flags, _name, false), do: flags
25
-
26
- # https://dev.mysql.com/doc/internals/en/character-set.html#packet-Protocol::CharacterSet
27
- character_sets = %{
28
- utf8_general_ci: 0x21
29
- }
30
-
31
- for {name, code} <- character_sets do
32
- def character_set_name_to_code(unquote(name)), do: unquote(code)
33
- end
34
-
35
- # https://dev.mysql.com/doc/internals/en/com-stmt-execute.html
36
- @cursor_types %{
37
- cursor_type_no_cursor: 0x00,
38
- cursor_type_read_only: 0x01,
39
- cursor_type_for_update: 0x02,
40
- cursor_type_scrollable: 0x04
41
- }
42
-
43
- ###########################################################
44
- # Basic packets
45
- #
46
- # https://dev.mysql.com/doc/internals/en/mysql-packet.html
47
- ###########################################################
48
-
49
- def encode_packet(payload, sequence_id) do
50
- payload_length = IO.iodata_length(payload)
51
- [<<payload_length::uint3, sequence_id::uint1>>, payload]
52
- end
53
-
54
- def decode_generic_response(<<0x00, rest::binary>>) do
55
- {affected_rows, rest} = take_int_lenenc(rest)
56
- {last_insert_id, rest} = take_int_lenenc(rest)
57
-
58
- <<
59
- status_flags::uint2,
60
- num_warnings::uint2,
61
- info::binary
62
- >> = rest
63
-
64
- ok_packet(
65
- affected_rows: affected_rows,
66
- last_insert_id: last_insert_id,
67
- status_flags: status_flags,
68
- num_warnings: num_warnings,
69
- info: info
70
- )
71
- end
72
-
73
- def decode_generic_response(
74
- <<0xFF, code::uint2, _sql_state_marker::string(1), _sql_state::string(5),
75
- message::binary>>
76
- ) do
77
- err_packet(code: code, message: message)
78
- end
79
-
80
- # Note: header is last argument to allow binary optimization
81
- def decode_generic_response(<<rest::binary>>, header) do
82
- decode_generic_response(<<header, rest::binary>>)
83
- end
84
-
85
- ##############################################################
86
- # Connection Phase
87
- #
88
- # https://dev.mysql.com/doc/internals/en/connection-phase.html
89
- ##############################################################
90
-
91
- def decode_initial_handshake(payload) do
92
- protocol_version = 10
93
- <<^protocol_version, rest::binary>> = payload
94
- {server_version, rest} = take_string_nul(rest)
95
-
96
- <<
97
- conn_id::uint4,
98
- auth_plugin_data1::string(8),
99
- 0,
100
- capability_flags1::uint2,
101
- character_set::uint1,
102
- status_flags::uint2,
103
- capability_flags2::uint2,
104
- auth_plugin_data_length::uint1,
105
- _::uint(10),
106
- rest::binary
107
- >> = rest
108
-
109
- take = max(13, auth_plugin_data_length - 8)
110
- <<auth_plugin_data2::binary-size(take), auth_plugin_name::binary>> = rest
111
- auth_plugin_data2 = decode_string_nul(auth_plugin_data2)
112
- auth_plugin_name = decode_string_nul(auth_plugin_name)
113
- <<capability_flags::uint4>> = <<capability_flags1::uint2, capability_flags2::uint2>>
114
- auth_plugin_data = auth_plugin_data1 <> auth_plugin_data2
115
-
116
- initial_handshake(
117
- server_version: server_version,
118
- conn_id: conn_id,
119
- auth_plugin_name: auth_plugin_name,
120
- auth_plugin_data: auth_plugin_data,
121
- capability_flags: capability_flags,
122
- character_set: character_set,
123
- status_flags: status_flags
124
- )
125
- end
126
-
127
- def encode_handshake_response_41(
128
- username,
129
- auth_plugin_name,
130
- auth_response,
131
- database,
132
- ssl?
133
- ) do
134
- capability_flags = capability_flags(database, ssl?)
135
- auth_response = if auth_response, do: encode_string_lenenc(auth_response), else: <<0>>
136
- database = if database, do: <<database::binary, 0x00>>, else: ""
137
-
138
- <<
139
- capability_flags::uint4,
140
- @max_packet_size::uint4,
141
- character_set_name_to_code(:utf8_general_ci),
142
- 0::uint(23),
143
- <<username::binary, 0x00>>,
144
- auth_response::binary,
145
- database::binary,
146
- (<<auth_plugin_name::binary, 0x00>>)
147
- >>
148
- end
149
-
150
- def encode_ssl_request(database) do
151
- capability_flags = capability_flags(database, true)
152
-
153
- <<
154
- capability_flags::uint4,
155
- @max_packet_size::uint4,
156
- character_set_name_to_code(:utf8_general_ci),
157
- 0::uint(23)
158
- >>
159
- end
160
-
161
- def decode_handshake_response(<<header, rest::binary>>) when header in [0x00, 0xFF] do
162
- decode_generic_response(rest, header)
163
- end
164
-
165
- def decode_handshake_response(<<0x01, 0x04>>) do
166
- :full_auth
167
- end
168
-
169
- def decode_handshake_response(<<0xFE, rest::binary>>) do
170
- {plugin_name, rest} = take_string_nul(rest)
171
- {plugin_data, ""} = take_string_nul(rest)
172
-
173
- auth_switch_request(plugin_name: plugin_name, plugin_data: plugin_data)
174
- end
175
-
176
- #################################################################
177
- # Text & Binary Protocol
178
- #
179
- # https://dev.mysql.com/doc/internals/en/text-protocol.html
180
- # https://dev.mysql.com/doc/internals/en/prepared-statements.html
181
- #################################################################
182
-
183
- # https://dev.mysql.com/doc/internals/en/com-ping.html
184
- def encode_com(:com_ping) do
185
- <<0x0E>>
186
- end
187
-
188
- # https://dev.mysql.com/doc/internals/en/com-query.html
189
- def encode_com({:com_query, query}) do
190
- [0x03, query]
191
- end
192
-
193
- # https://dev.mysql.com/doc/internals/en/com-stmt-prepare.html#packet-COM_STMT_PREPARE
194
- def encode_com({:com_stmt_prepare, query}) do
195
- [0x16, query]
196
- end
197
-
198
- # https://dev.mysql.com/doc/internals/en/com-stmt-close.html
199
- def encode_com({:com_stmt_close, statement_id}) do
200
- [0x19, <<statement_id::uint4>>]
201
- end
202
-
203
- # https://dev.mysql.com/doc/internals/en/com-stmt-reset.html
204
- def encode_com({:com_stmt_reset, statement_id}) do
205
- [0x1A, <<statement_id::uint4>>]
206
- end
207
-
208
- # https://dev.mysql.com/doc/internals/en/com-stmt-execute.html
209
- def encode_com({:com_stmt_execute, statement_id, params, cursor_type}) do
210
- command = 0x17
211
- flags = Map.fetch!(@cursor_types, cursor_type)
212
-
213
- # Always 0x01
214
- iteration_count = 0x01
215
-
216
- new_params_bound_flag = 1
217
- {null_bitmap, types, values} = encode_params(params)
218
-
219
- <<
220
- command,
221
- statement_id::uint4,
222
- flags::uint1,
223
- iteration_count::uint4,
224
- null_bitmap::bitstring,
225
- new_params_bound_flag::uint1,
226
- types::binary,
227
- values::binary
228
- >>
229
- end
230
-
231
- # https://dev.mysql.com/doc/internals/en/com-stmt-fetch.html
232
- def encode_com({:com_stmt_fetch, statement_id, num_rows}) do
233
- <<
234
- 0x1C,
235
- statement_id::uint4,
236
- num_rows::uint4
237
- >>
238
- end
239
-
240
- # https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-COM_QUERY_Response
241
- def decode_com_query_response(<<header, rest::binary>>, "", :initial)
242
- when header in [0x00, 0xFF] do
243
- {:halt, decode_generic_response(rest, header)}
244
- end
245
-
246
- def decode_com_query_response(payload, next_data, state) do
247
- decode_resultset(payload, next_data, state, &Values.decode_text_row/2)
248
- end
249
-
250
- def decode_com_stmt_prepare_response(
251
- <<0x00, statement_id::uint4, num_columns::uint2, num_params::uint2, 0,
252
- num_warnings::uint2>>,
253
- next_data,
254
- :initial
255
- ) do
256
- result =
257
- com_stmt_prepare_ok(
258
- statement_id: statement_id,
259
- num_columns: num_columns,
260
- num_params: num_params,
261
- num_warnings: num_warnings
262
- )
263
-
264
- if num_columns + num_params > 0 do
265
- {:cont, {result, num_columns + num_params}}
266
- else
267
- "" = next_data
268
- {:halt, result}
269
- end
270
- end
271
-
272
- def decode_com_stmt_prepare_response(<<rest::binary>>, "", :initial) do
273
- {:halt, decode_generic_response(rest)}
274
- end
275
-
276
- # for now, we're simply consuming column_definition packets for params and columns,
277
- # we might decode them in the future.
278
- def decode_com_stmt_prepare_response(_payload, next_data, {com_stmt_prepare_ok, packets_left}) do
279
- if packets_left > 1 do
280
- {:cont, {com_stmt_prepare_ok, packets_left - 1}}
281
- else
282
- "" = next_data
283
- {:halt, com_stmt_prepare_ok}
284
- end
285
- end
286
-
287
- defp encode_params(params) do
288
- null_type = 0x06
289
-
290
- {count, null_bitmap, types, values} =
291
- Enum.reduce(params, {0, 0, <<>>, <<>>}, fn
292
- value, {idx, null_bitmap, types, values} ->
293
- null_value = if value == nil, do: 1, else: 0
294
- null_bitmap = null_bitmap ||| null_value <<< idx
295
-
296
- if value == nil do
297
- {idx + 1, null_bitmap, <<types::binary, null_type, unsigned_flag(value)>>, values}
298
- else
299
- {type, binary} = Values.encode_binary_value(value)
300
- type = Values.type_atom_to_code(type)
301
-
302
- {idx + 1, null_bitmap, <<types::binary, type, unsigned_flag(value)>>,
303
- <<values::binary, binary::binary>>}
304
- end
305
- end)
306
-
307
- null_bitmap_size = div(count + 7, 8)
308
- {<<null_bitmap::uint(null_bitmap_size)>>, types, values}
309
- end
310
-
311
- defp unsigned_flag(value) when is_integer(value) and value >= 1 <<< 63, do: 0x80
312
- defp unsigned_flag(_), do: 0x00
313
-
314
- # https://dev.mysql.com/doc/internals/en/com-stmt-execute-response.html
315
- def decode_com_stmt_execute_response(<<header, rest::binary>>, "", :initial)
316
- when header in [0x00, 0xFF] do
317
- {:halt, decode_generic_response(rest, header)}
318
- end
319
-
320
- def decode_com_stmt_execute_response(payload, next_data, state) do
321
- decode_resultset(payload, next_data, state, &Values.decode_binary_row/2)
322
- end
323
-
324
- # https://dev.mysql.com/doc/internals/en/com-stmt-fetch-response.html
325
- def decode_com_stmt_fetch_response(<<0xFF, rest::binary>>, "", {:initial, _column_defs}) do
326
- {:halt, decode_generic_response(rest, 0xFF)}
327
- end
328
-
329
- def decode_com_stmt_fetch_response(payload, next_data, {:initial, column_defs}) do
330
- decode_com_stmt_fetch_response(payload, next_data, {:rows, column_defs, 0, []})
331
- end
332
-
333
- def decode_com_stmt_fetch_response(payload, next_data, state) do
334
- decode_resultset(payload, next_data, state, &Values.decode_binary_row/2)
335
- end
336
-
337
- def decode_column_def(<<3, "def", rest::binary>>) do
338
- {_schema, rest} = take_string_lenenc(rest)
339
- {_table, rest} = take_string_lenenc(rest)
340
- {_org_table, rest} = take_string_lenenc(rest)
341
- {name, rest} = take_string_lenenc(rest)
342
- {_org_name, rest} = take_string_lenenc(rest)
343
-
344
- <<
345
- 0x0C,
346
- _character_set::uint2,
347
- _column_length::uint4,
348
- type::uint1,
349
- flags::uint2,
350
- _decimals::uint1,
351
- 0::uint2
352
- >> = rest
353
-
354
- column_def(
355
- name: name,
356
- type: Values.type_code_to_atom(type),
357
- flags: flags,
358
- unsigned?: has_column_flag?(flags, :unsigned_flag)
359
- )
360
- end
361
-
362
- defp decode_resultset(payload, _next_data, :initial, _row_decoder) do
363
- {:cont, {:column_defs, decode_int_lenenc(payload), []}}
364
- end
365
-
366
- defp decode_resultset(payload, _next_data, {:column_defs, num_columns, acc}, _row_decoder) do
367
- column_def = decode_column_def(payload)
368
- acc = [column_def | acc]
369
-
370
- if num_columns > 1 do
371
- {:cont, {:column_defs, num_columns - 1, acc}}
372
- else
373
- {:cont, {:rows, Enum.reverse(acc), 0, []}}
374
- end
375
- end
376
-
377
- defp decode_resultset(
378
- <<0xFE, 0, 0, status_flags::uint2, num_warnings::uint2>>,
379
- _next_data,
380
- {:rows, column_defs, num_rows, acc},
381
- _row_decoder
382
- ) do
383
- resultset =
384
- resultset(
385
- column_defs: column_defs,
386
- num_rows: num_rows,
387
- rows: Enum.reverse(acc),
388
- num_warnings: num_warnings,
389
- status_flags: status_flags
390
- )
391
-
392
- if has_status_flag?(status_flags, :server_more_results_exists) do
393
- {:cont, {:trailing_ok_packet, resultset}}
394
- else
395
- {:halt, resultset}
396
- end
397
- end
398
-
399
- defp decode_resultset(payload, _next_data, {:rows, column_defs, num_rows, acc}, row_decoder) do
400
- row = row_decoder.(payload, column_defs)
401
- {:cont, {:rows, column_defs, num_rows + 1, [row | acc]}}
402
- end
403
-
404
- defp decode_resultset(payload, "", {:trailing_ok_packet, resultset}, _row_decoder) do
405
- ok_packet(status_flags: status_flags) = decode_generic_response(payload)
406
- {:halt, resultset(resultset, status_flags: status_flags)}
407
- end
408
-
409
- defp decode_resultset(_payload, _next_data, {:trailing_ok_packet, _resultset}, _row_decoder) do
410
- {:error, :multiple_results}
411
- end
412
- end
changed lib/myxql/protocol/records.ex
 
@@ -3,9 +3,18 @@ defmodule MyXQL.Protocol.Records do
3
3
4
4
import Record
5
5
6
+ @default_max_packet_size 16_777_215
7
+
8
+ # https://dev.mysql.com/doc/internals/en/character-set.html#packet-Protocol::CharacterSet
9
+ # utf8mb4
10
+ @default_charset 45
11
+
6
12
# https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html
7
13
defrecord :ok_packet, [:affected_rows, :last_insert_id, :status_flags, :num_warnings, :info]
8
14
15
+ # https://dev.mysql.com/doc/internals/en/packet-EOF_Packet.html
16
+ defrecord :eof_packet, [:status_flags, :num_warnings]
17
+
9
18
# https://dev.mysql.com/doc/internals/en/packet-ERR_Packet.html
10
19
defrecord :err_packet, [:code, :message]
11
20
 
@@ -21,18 +30,27 @@ defmodule MyXQL.Protocol.Records do
21
30
]
22
31
23
32
# https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse
24
- defrecord :handshake_response_41, [
25
- :capability_flags,
26
- :max_packet_size,
27
- :character_set,
28
- :username,
29
- :auth_response,
30
- :database
31
- ]
33
+ defrecord :handshake_response_41,
34
+ capability_flags: nil,
35
+ max_packet_size: @default_max_packet_size,
36
+ character_set: @default_charset,
37
+ username: nil,
38
+ database: nil,
39
+ auth_plugin_name: nil,
40
+ auth_response: nil
41
+
42
+ # https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::SSLRequest
43
+ defrecord :ssl_request,
44
+ capability_flags: nil,
45
+ max_packet_size: @default_max_packet_size,
46
+ character_set: @default_charset
32
47
33
48
# https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
34
49
defrecord :auth_switch_request, [:plugin_name, :plugin_data]
35
50
51
+ # https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthMoreData
52
+ defrecord :auth_more_data, [:data]
53
+
36
54
# https://dev.mysql.com/doc/internals/en/com-stmt-prepare-response.html#packet-COM_STMT_PREPARE_OK
37
55
defrecord :com_stmt_prepare_ok, [:statement_id, :num_columns, :num_params, :num_warnings]
changed mix.exs
 
@@ -1,7 +1,7 @@
1
1
defmodule MyXQL.MixProject do
2
2
use Mix.Project
3
3
4
- @version "0.2.2"
4
+ @version "0.2.3"
5
5
@source_url "https://github.com/elixir-ecto/myxql"
6
6
7
7
def project() do
 
@@ -10,7 +10,8 @@ defmodule MyXQL.MixProject do
10
10
version: @version,
11
11
elixir: "~> 1.4",
12
12
start_permanent: Mix.env() == :prod,
13
- description: "MySQL 5.7+ driver for Elixir",
13
+ name: "MyXQL",
14
+ description: "MySQL 5.5+ driver for Elixir",
14
15
source_url: @source_url,
15
16
package: package(),
16
17
docs: docs(),