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