changed
CHANGELOG.md
|
@@ -1,5 +1,19 @@
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+ ## v0.7.0 (2024-05-20)
|
4
|
+
|
5
|
+ * Add `:enable_cleartext_plugin` option
|
6
|
+
|
7
|
+ * Filter client capabilities that can't be met by server
|
8
|
+
|
9
|
+ * Fix float/double parsing for text protocol when fractional part is missing
|
10
|
+
|
11
|
+ * Fix setting `:socket_options`, they are now merged
|
12
|
+
|
13
|
+ * `:ssl_opts` is deprecated in favor of `ssl: options`
|
14
|
+
|
15
|
+ * `ssl: true` now emits a warning, as it does not execute server certificate verification
|
16
|
+
|
3
17
|
## v0.6.4 (2023-12-04)
|
4
18
|
|
5
19
|
* Let DBConnection rollback for failed commit or disconnect failed begin/rollback
|
changed
README.md
|
@@ -19,9 +19,9 @@ Documentation: <https://hexdocs.pm/myxql>
|
19
19
|
Add `:myxql` to your dependencies:
|
20
20
|
|
21
21
|
```elixir
|
22
|
- def deps() do
|
22
|
+ def deps do
|
23
23
|
[
|
24
|
- {:myxql, "~> 0.6.0"}
|
24
|
+ {:myxql, "~> 0.7.0"}
|
25
25
|
]
|
26
26
|
end
|
27
27
|
```
|
|
@@ -153,7 +153,7 @@ input, consider implementing an [custom Ecto type](https://hexdocs.pm/ecto/Ecto.
|
153
153
|
|
154
154
|
Run tests:
|
155
155
|
|
156
|
- ```
|
156
|
+ ```text
|
157
157
|
git clone [email protected]:elixir-ecto/myxql.git
|
158
158
|
cd myxql
|
159
159
|
mix deps.get
|
changed
hex_metadata.config
|
@@ -1,6 +1,6 @@
|
1
1
|
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/elixir-ecto/myxql">>}]}.
|
2
2
|
{<<"name">>,<<"myxql">>}.
|
3
|
- {<<"version">>,<<"0.6.4">>}.
|
3
|
+ {<<"version">>,<<"0.7.0">>}.
|
4
4
|
{<<"description">>,<<"MySQL 5.5+ driver for Elixir">>}.
|
5
5
|
{<<"elixir">>,<<"~> 1.7">>}.
|
6
6
|
{<<"app">>,<<"myxql">>}.
|
changed
lib/myxql.ex
|
@@ -16,13 +16,13 @@ defmodule MyXQL do
|
16
16
|
| {:password, String.t() | nil}
|
17
17
|
| {:charset, String.t() | nil}
|
18
18
|
| {:collation, String.t() | nil}
|
19
|
- | {:ssl, boolean()}
|
20
|
- | {:ssl_opts, [:ssl.tls_client_option()]}
|
19
|
+ | {:ssl, boolean | [:ssl.tls_client_option()]}
|
21
20
|
| {:connect_timeout, timeout()}
|
22
21
|
| {:handshake_timeout, timeout()}
|
23
22
|
| {:ping_timeout, timeout()}
|
24
23
|
| {:prepare, :force_named | :named | :unnamed}
|
25
24
|
| {:disconnect_on_error_codes, [atom()]}
|
25
|
+ | {:enable_cleartext_plugin, boolean()}
|
26
26
|
| DBConnection.start_option()
|
27
27
|
|
28
28
|
@type option() :: DBConnection.option()
|
|
@@ -69,9 +69,10 @@ defmodule MyXQL do
|
69
69
|
* `:collation` - A connection collation. Must be given with `:charset` option, and if set
|
70
70
|
it overwrites the default collation for the given charset. (default: `nil`)
|
71
71
|
|
72
|
- * `:ssl` - Set to `true` if SSL should be used (default: `false`)
|
73
|
-
|
74
|
- * `:ssl_opts` - A list of SSL options, see `:ssl.connect/2` (default: `[]`)
|
72
|
+ * `:ssl` - Enables SSL. Setting it to `true` enables SSL without server certificate verification,
|
73
|
+ which emits a warning. Instead, prefer to set it to a keyword list, with either
|
74
|
+ `:cacerts` or `:cacertfile` set to a CA trust store, to enable server certificate
|
75
|
+ verification. (default: `false`)
|
75
76
|
|
76
77
|
* `:connect_timeout` - Socket connect timeout in milliseconds (default:
|
77
78
|
`15_000`)
|
|
@@ -100,6 +101,8 @@ defmodule MyXQL do
|
100
101
|
will disconnect the connection. See "Disconnecting on Errors" section below for more
|
101
102
|
information.
|
102
103
|
|
104
|
+ * `:enable_cleartext_plugin` - Set to `true` to send password as cleartext (default: `false`)
|
105
|
+
|
103
106
|
The given options are passed down to DBConnection, some of the most commonly used ones are
|
104
107
|
documented below:
|
105
108
|
|
|
@@ -124,6 +127,11 @@ defmodule MyXQL do
|
124
127
|
iex> {:ok, pid} = MyXQL.start_link(protocol: :tcp)
|
125
128
|
{:ok, #PID<0.69.0>}
|
126
129
|
|
130
|
+ Start connection with SSL using CA certificate file:
|
131
|
+
|
132
|
+ iex> {:ok, pid} = MyXQL.start_link(ssl: [cacertfile: System.fetch_env!("DB_CA_CERT_FILE")])
|
133
|
+ {:ok, #PID<0.69.0>}
|
134
|
+
|
127
135
|
Run a query after connection has been established:
|
128
136
|
|
129
137
|
iex> {:ok, pid} = MyXQL.start_link(after_connect: &MyXQL.query!(&1, "SET time_zone = '+00:00'"))
|
changed
lib/myxql/client.ex
|
@@ -7,6 +7,8 @@ defmodule MyXQL.Client do
|
7
7
|
|
8
8
|
defstruct [:sock, :connection_id]
|
9
9
|
|
10
|
+ @sock_opts [mode: :binary, packet: :raw, active: false]
|
11
|
+
|
10
12
|
defmodule Config do
|
11
13
|
@moduledoc false
|
12
14
|
|
|
@@ -18,19 +20,39 @@ defmodule MyXQL.Client do
|
18
20
|
:username,
|
19
21
|
:password,
|
20
22
|
:database,
|
21
|
- :ssl?,
|
22
23
|
:ssl_opts,
|
23
24
|
:connect_timeout,
|
24
25
|
:handshake_timeout,
|
25
26
|
:socket_options,
|
26
27
|
:max_packet_size,
|
27
28
|
:charset,
|
28
|
- :collation
|
29
|
+ :collation,
|
30
|
+ :enable_cleartext_plugin
|
29
31
|
]
|
30
32
|
|
33
|
+ @sock_opts [mode: :binary, packet: :raw, active: false]
|
34
|
+
|
31
35
|
def new(opts) do
|
32
36
|
{address, port} = address_and_port(opts)
|
33
37
|
|
38
|
+ {ssl_opts, opts} =
|
39
|
+ case Keyword.pop(opts, :ssl, false) do
|
40
|
+ {false, opts} ->
|
41
|
+ {nil, opts}
|
42
|
+
|
43
|
+ {true, opts} ->
|
44
|
+ Logger.warning(
|
45
|
+ "setting ssl: true on your database connection offers only limited protection, " <>
|
46
|
+ "as the server's certificate is not verified. Set \"ssl: [cacertfile: \"/path/to/cacert.crt\"]\" instead"
|
47
|
+ )
|
48
|
+
|
49
|
+ # Read ssl_opts for backwards compatibility
|
50
|
+ Keyword.pop(opts, :ssl_opts, [])
|
51
|
+
|
52
|
+ {ssl_opts, opts} when is_list(ssl_opts) ->
|
53
|
+ {Keyword.merge(default_ssl_opts(), ssl_opts), opts}
|
54
|
+ end
|
55
|
+
|
34
56
|
%__MODULE__{
|
35
57
|
address: address,
|
36
58
|
port: port,
|
|
@@ -38,17 +60,25 @@ defmodule MyXQL.Client do
|
38
60
|
Keyword.get(opts, :username, System.get_env("USER")) || raise(":username is missing"),
|
39
61
|
password: nilify(Keyword.get(opts, :password, System.get_env("MYSQL_PWD"))),
|
40
62
|
database: Keyword.get(opts, :database),
|
41
|
- ssl?: Keyword.get(opts, :ssl, false),
|
42
|
- ssl_opts: Keyword.get(opts, :ssl_opts, []),
|
63
|
+ ssl_opts: ssl_opts,
|
43
64
|
connect_timeout: Keyword.get(opts, :connect_timeout, @default_timeout),
|
44
65
|
handshake_timeout: Keyword.get(opts, :handshake_timeout, @default_timeout),
|
45
|
- socket_options:
|
46
|
- Keyword.merge([mode: :binary, packet: :raw, active: false], opts[:socket_options] || []),
|
66
|
+ socket_options: (opts[:socket_options] || []) ++ @sock_opts,
|
47
67
|
charset: Keyword.get(opts, :charset),
|
48
|
- collation: Keyword.get(opts, :collation)
|
68
|
+ collation: Keyword.get(opts, :collation),
|
69
|
+ enable_cleartext_plugin: Keyword.get(opts, :enable_cleartext_plugin, false)
|
49
70
|
}
|
50
71
|
end
|
51
72
|
|
73
|
+ defp default_ssl_opts do
|
74
|
+ [
|
75
|
+ verify: :verify_peer,
|
76
|
+ customize_hostname_check: [
|
77
|
+ match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
|
78
|
+ ]
|
79
|
+ ]
|
80
|
+ end
|
81
|
+
|
52
82
|
defp nilify(""), do: nil
|
53
83
|
defp nilify(other), do: other
|
54
84
|
|
|
@@ -293,7 +323,7 @@ defmodule MyXQL.Client do
|
293
323
|
buffer? = Keyword.has_key?(socket_options, :buffer)
|
294
324
|
client = %__MODULE__{connection_id: nil, sock: nil}
|
295
325
|
|
296
|
- case :gen_tcp.connect(address, port, socket_options, connect_timeout) do
|
326
|
+ case :gen_tcp.connect(address, port, socket_options ++ @sock_opts, connect_timeout) do
|
297
327
|
{:ok, sock} when buffer? ->
|
298
328
|
{:ok, %{client | sock: {:gen_tcp, sock}}}
|
299
329
|
|
|
@@ -374,9 +404,20 @@ defmodule MyXQL.Client do
|
374
404
|
com_query(client, "SET NAMES '#{charset}' COLLATE '#{collation}'")
|
375
405
|
end
|
376
406
|
|
377
|
- defp maybe_upgrade_to_ssl(client, %{ssl?: true} = config, capability_flags, sequence_id) do
|
407
|
+ defp maybe_upgrade_to_ssl(client, %{ssl_opts: nil}, _capability_flags, sequence_id) do
|
408
|
+ {:ok, sequence_id, client}
|
409
|
+ end
|
410
|
+
|
411
|
+ defp maybe_upgrade_to_ssl(client, %{ssl_opts: ssl_opts} = config, capability_flags, sequence_id) do
|
378
412
|
{_, sock} = client.sock
|
379
413
|
|
414
|
+ ssl_opts =
|
415
|
+ if is_list(config.address) do
|
416
|
+ Keyword.put_new(ssl_opts, :server_name_indication, config.address)
|
417
|
+ else
|
418
|
+ ssl_opts
|
419
|
+ end
|
420
|
+
|
380
421
|
ssl_request =
|
381
422
|
ssl_request(
|
382
423
|
capability_flags: capability_flags,
|
|
@@ -387,15 +428,11 @@ defmodule MyXQL.Client do
|
387
428
|
payload = encode_ssl_request(ssl_request)
|
388
429
|
|
389
430
|
with :ok <- send_packet(client, payload, sequence_id),
|
390
|
- {:ok, ssl_sock} <- :ssl.connect(sock, config.ssl_opts, config.connect_timeout) do
|
431
|
+ {:ok, ssl_sock} <- :ssl.connect(sock, ssl_opts, config.connect_timeout) do
|
391
432
|
{:ok, sequence_id + 1, %{client | sock: {:ssl, ssl_sock}}}
|
392
433
|
end
|
393
434
|
end
|
394
435
|
|
395
|
- defp maybe_upgrade_to_ssl(client, %{ssl?: false}, _capability_flags, sequence_id) do
|
396
|
- {:ok, sequence_id, client}
|
397
|
- end
|
398
|
-
|
399
436
|
defp recv_handshake(client) do
|
400
437
|
recv_packet(client, &decode_initial_handshake/1)
|
401
438
|
end
|
|
@@ -472,7 +509,7 @@ defmodule MyXQL.Client do
|
472
509
|
|
473
510
|
defp perform_full_auth(client, config, "caching_sha2_password", auth_plugin_data, sequence_id) do
|
474
511
|
auth_response =
|
475
|
- if config.ssl? do
|
512
|
+ if config.ssl_opts do
|
476
513
|
[config.password, 0]
|
477
514
|
else
|
478
515
|
# request public key
|
changed
lib/myxql/protocol.ex
|
@@ -142,6 +142,18 @@ defmodule MyXQL.Protocol do
|
142
142
|
decode_connect_err_packet_body(rest)
|
143
143
|
end
|
144
144
|
|
145
|
+ defp filter_capabilities(allowed_flags, requested_flags) do
|
146
|
+ requested_capabilities = list_capability_flags(requested_flags)
|
147
|
+
|
148
|
+ Enum.reduce(requested_capabilities, requested_flags, fn name, acc ->
|
149
|
+ if has_capability_flag?(allowed_flags, name) do
|
150
|
+ acc
|
151
|
+ else
|
152
|
+ remove_capability_flag(acc, name)
|
153
|
+ end
|
154
|
+ end)
|
155
|
+ end
|
156
|
+
|
145
157
|
defp ensure_capabilities(capability_flags, names) do
|
146
158
|
Enum.reduce_while(names, :ok, fn name, _acc ->
|
147
159
|
if has_capability_flag?(capability_flags, name) do
|
|
@@ -167,16 +179,15 @@ defmodule MyXQL.Protocol do
|
167
179
|
:client_transactions
|
168
180
|
])
|
169
181
|
|> maybe_put_capability_flag(:client_connect_with_db, !is_nil(config.database))
|
170
|
- |> maybe_put_capability_flag(:client_ssl, config.ssl?)
|
182
|
+ |> maybe_put_capability_flag(:client_ssl, is_list(config.ssl_opts))
|
171
183
|
|
172
|
- if config.ssl? && !has_capability_flag?(server_capability_flags, :client_ssl) do
|
184
|
+ if config.ssl_opts && !has_capability_flag?(server_capability_flags, :client_ssl) do
|
173
185
|
{:error, :server_does_not_support_ssl}
|
174
186
|
else
|
175
|
- client_capabilities = list_capability_flags(client_capability_flags)
|
187
|
+ client_capability_flags =
|
188
|
+ filter_capabilities(server_capability_flags, client_capability_flags)
|
176
189
|
|
177
|
- with :ok <- ensure_capabilities(server_capability_flags, client_capabilities) do
|
178
|
- {:ok, client_capability_flags}
|
179
|
- end
|
190
|
+ {:ok, client_capability_flags}
|
180
191
|
end
|
181
192
|
end
|
changed
lib/myxql/protocol/auth.ex
|
@@ -33,14 +33,18 @@ defmodule MyXQL.Protocol.Auth do
|
33
33
|
config.password == nil ->
|
34
34
|
""
|
35
35
|
|
36
|
+ auth_plugin_name == "mysql_clear_password" and config.enable_cleartext_plugin ->
|
37
|
+ config.password <> <<0>>
|
38
|
+
|
36
39
|
auth_plugin_name == "mysql_native_password" ->
|
37
40
|
mysql_native_password(config.password, initial_auth_plugin_data)
|
38
41
|
|
39
|
- auth_plugin_name == "sha256_password" and config.ssl? ->
|
40
|
- config.password <> <<0>>
|
41
|
-
|
42
|
- auth_plugin_name == "sha256_password" and not config.ssl? ->
|
43
|
- <<1>>
|
42
|
+ auth_plugin_name == "sha256_password" ->
|
43
|
+ if config.ssl_opts do
|
44
|
+ config.password <> <<0>>
|
45
|
+ else
|
46
|
+ <<1>>
|
47
|
+ end
|
44
48
|
|
45
49
|
auth_plugin_name == "caching_sha2_password" ->
|
46
50
|
sha256_password(config.password, initial_auth_plugin_data)
|
changed
lib/myxql/protocol/flags.ex
|
@@ -34,6 +34,8 @@ defmodule MyXQL.Protocol.Flags do
|
34
34
|
|
35
35
|
def has_capability_flag?(flags, name), do: has_flag?(@capability_flags, flags, name)
|
36
36
|
|
37
|
+ def remove_capability_flag(flags, name), do: remove_flag(@capability_flags, flags, name)
|
38
|
+
|
37
39
|
def put_capability_flags(flags \\ 0, names), do: put_flags(@capability_flags, flags, names)
|
38
40
|
|
39
41
|
def list_capability_flags(flags), do: list_flags(@capability_flags, flags)
|
|
@@ -93,6 +95,11 @@ defmodule MyXQL.Protocol.Flags do
|
93
95
|
Enum.reduce(names, flags, &(&2 ||| Keyword.fetch!(all_flags, &1)))
|
94
96
|
end
|
95
97
|
|
98
|
+ defp remove_flag(all_flags, flags, name) do
|
99
|
+ value = Keyword.fetch!(all_flags, name)
|
100
|
+ flags &&& ~~~value
|
101
|
+ end
|
102
|
+
|
96
103
|
def list_flags(all_flags, flags) do
|
97
104
|
all_flags
|
98
105
|
|> Keyword.keys()
|
changed
lib/myxql/protocol/types.ex
|
@@ -66,6 +66,8 @@ defmodule MyXQL.Protocol.Types do
|
66
66
|
string
|
67
67
|
end
|
68
68
|
|
69
|
+ def take_string_nul(""), do: {nil, ""}
|
70
|
+
|
69
71
|
def take_string_nul(binary) do
|
70
72
|
[string, rest] = :binary.split(binary, <<0>>)
|
71
73
|
{string, rest}
|
changed
lib/myxql/protocol/values.ex
|
@@ -129,7 +129,11 @@ defmodule MyXQL.Protocol.Values do
|
129
129
|
end
|
130
130
|
|
131
131
|
def decode_text_value(value, type) when type in [:float, :double] do
|
132
|
- String.to_float(value)
|
132
|
+ if String.contains?(value, ".") do
|
133
|
+ String.to_float(value)
|
134
|
+ else
|
135
|
+ String.to_integer(value) * 1.0
|
136
|
+ end
|
133
137
|
end
|
134
138
|
|
135
139
|
# Note: MySQL implements `NUMERIC` as `DECIMAL`s
|
changed
lib/myxql/result.ex
|
@@ -18,7 +18,7 @@ defmodule MyXQL.Result do
|
18
18
|
If `result.num_warnings` is non-zero it means there were warnings and they can be
|
19
19
|
retrieved by making another query:
|
20
20
|
|
21
|
- MyXQL.query!(conn, "SHOW WARNINGS")
|
21
|
+ MyXQL.query!(conn, "SHOW WARNINGS", [], query_type: :text)
|
22
22
|
|
23
23
|
"""
|
changed
mix.exs
|
@@ -1,7 +1,7 @@
|
1
1
|
defmodule MyXQL.MixProject do
|
2
2
|
use Mix.Project
|
3
3
|
|
4
|
- @version "0.6.4"
|
4
|
+ @version "0.7.0"
|
5
5
|
@source_url "https://github.com/elixir-ecto/myxql"
|
6
6
|
|
7
7
|
def project() do
|