Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:

jobs:
test:
name: "test ${{ matrix.db }} ${{ matrix.pair.elixir }}/${{ matrix.pair.otp }} ${{ matrix.lint }}"
runs-on: ubuntu-20.04
strategy:
fail-fast: false
Expand All @@ -20,21 +21,22 @@ jobs:
pair:
- elixir: 1.11.3
otp: 23.2.5
- elixir: 1.14.0
otp: 25.1.2
- elixir: 1.16.2
otp: 26.2.5
include:
- db: mysql:8.0
pair:
elixir: 1.11.4
otp: 23.3.3
elixir: 1.16.2
otp: 26.2.5
lint: lint

- db: mysql:8.0
pair:
elixir: 1.7.4
otp: 21.3.8.24
env:
MIX_ENV: test
DB: ${{matrix.db}}
DB: ${{ matrix.db }}
MYSQL_UNIX_PORT: /var/run/mysqld/mysqld.sock
steps:
- run: sudo mkdir -p /var/run/mysqld
Expand Down
15 changes: 10 additions & 5 deletions lib/myxql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ defmodule MyXQL do
| {:password, String.t() | nil}
| {:charset, String.t() | nil}
| {:collation, String.t() | nil}
| {:ssl, boolean()}
| {:ssl_opts, [:ssl.tls_client_option()]}
| {:ssl, boolean | [:ssl.tls_client_option()]}
| {:connect_timeout, timeout()}
| {:handshake_timeout, timeout()}
| {:ping_timeout, timeout()}
Expand Down Expand Up @@ -70,9 +69,10 @@ defmodule MyXQL do
* `:collation` - A connection collation. Must be given with `:charset` option, and if set
it overwrites the default collation for the given charset. (default: `nil`)

* `:ssl` - Set to `true` if SSL should be used (default: `false`)

* `:ssl_opts` - A list of SSL options, see `:ssl.connect/2` (default: `[]`)
* `:ssl` - Enables SSL. Setting it to `true` enables SSL without server certificate verification,
which emits a warning. Instead, prefer to set it to a keyword list, with either
`:cacerts` or `:cacertfile` set to a CA trust store, to enable server certificate
verification. (default: `false`)

* `:connect_timeout` - Socket connect timeout in milliseconds (default:
`15_000`)
Expand Down Expand Up @@ -127,6 +127,11 @@ defmodule MyXQL do
iex> {:ok, pid} = MyXQL.start_link(protocol: :tcp)
{:ok, #PID<0.69.0>}

Start connection with SSL using CA certificate file:

iex> {:ok, pid} = MyXQL.start_link(ssl: [cacertfile: System.fetch_env!("DB_CA_CERT_FILE")])
{:ok, #PID<0.69.0>}

Run a query after connection has been established:

iex> {:ok, pid} = MyXQL.start_link(after_connect: &MyXQL.query!(&1, "SET time_zone = '+00:00'"))
Expand Down
52 changes: 42 additions & 10 deletions lib/myxql/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ defmodule MyXQL.Client do
:username,
:password,
:database,
:ssl?,
:ssl_opts,
:connect_timeout,
:handshake_timeout,
Expand All @@ -36,15 +35,32 @@ defmodule MyXQL.Client do
def new(opts) do
{address, port} = address_and_port(opts)

{ssl_opts, opts} =
case Keyword.pop(opts, :ssl, false) do
{false, opts} ->
{nil, opts}

{true, opts} ->
Logger.warning(
"setting ssl: true on your database connection offers only limited protection, " <>
"as the server's certificate is not verified. Set \"ssl: [cacertfile: \"/path/to/cacert.crt\"]\" instead"
)

# Read ssl_opts for backwards compatibility
Keyword.pop(opts, :ssl_opts, [])

{ssl_opts, opts} when is_list(ssl_opts) ->
{Keyword.merge(default_ssl_opts(), ssl_opts), opts}
end

%__MODULE__{
address: address,
port: port,
username:
Keyword.get(opts, :username, System.get_env("USER")) || raise(":username is missing"),
password: nilify(Keyword.get(opts, :password, System.get_env("MYSQL_PWD"))),
database: Keyword.get(opts, :database),
ssl?: Keyword.get(opts, :ssl, false),
ssl_opts: Keyword.get(opts, :ssl_opts, []),
ssl_opts: ssl_opts,
connect_timeout: Keyword.get(opts, :connect_timeout, @default_timeout),
handshake_timeout: Keyword.get(opts, :handshake_timeout, @default_timeout),
socket_options: (opts[:socket_options] || []) ++ @sock_opts,
Expand All @@ -54,6 +70,15 @@ defmodule MyXQL.Client do
}
end

defp default_ssl_opts do
[
verify: :verify_peer,
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
]
]
end

defp nilify(""), do: nil
defp nilify(other), do: other

Expand Down Expand Up @@ -379,9 +404,20 @@ defmodule MyXQL.Client do
com_query(client, "SET NAMES '#{charset}' COLLATE '#{collation}'")
end

defp maybe_upgrade_to_ssl(client, %{ssl?: true} = config, capability_flags, sequence_id) do
defp maybe_upgrade_to_ssl(client, %{ssl_opts: nil}, _capability_flags, sequence_id) do
{:ok, sequence_id, client}
end

defp maybe_upgrade_to_ssl(client, %{ssl_opts: ssl_opts} = config, capability_flags, sequence_id) do
{_, sock} = client.sock

ssl_opts =
if is_list(config.address) do
Keyword.put_new(ssl_opts, :server_name_indication, config.address)
else
ssl_opts
end

ssl_request =
ssl_request(
capability_flags: capability_flags,
Expand All @@ -392,15 +428,11 @@ defmodule MyXQL.Client do
payload = encode_ssl_request(ssl_request)

with :ok <- send_packet(client, payload, sequence_id),
{:ok, ssl_sock} <- :ssl.connect(sock, config.ssl_opts, config.connect_timeout) do
{:ok, ssl_sock} <- :ssl.connect(sock, ssl_opts, config.connect_timeout) do
{:ok, sequence_id + 1, %{client | sock: {:ssl, ssl_sock}}}
end
end

defp maybe_upgrade_to_ssl(client, %{ssl?: false}, _capability_flags, sequence_id) do
{:ok, sequence_id, client}
end

defp recv_handshake(client) do
recv_packet(client, &decode_initial_handshake/1)
end
Expand Down Expand Up @@ -477,7 +509,7 @@ defmodule MyXQL.Client do

defp perform_full_auth(client, config, "caching_sha2_password", auth_plugin_data, sequence_id) do
auth_response =
if config.ssl? do
if config.ssl_opts do
[config.password, 0]
else
# request public key
Expand Down
4 changes: 2 additions & 2 deletions lib/myxql/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ defmodule MyXQL.Protocol do
:client_transactions
])
|> maybe_put_capability_flag(:client_connect_with_db, !is_nil(config.database))
|> maybe_put_capability_flag(:client_ssl, config.ssl?)
|> maybe_put_capability_flag(:client_ssl, is_list(config.ssl_opts))

if config.ssl? && !has_capability_flag?(server_capability_flags, :client_ssl) do
if config.ssl_opts && !has_capability_flag?(server_capability_flags, :client_ssl) do
{:error, :server_does_not_support_ssl}
else
client_capability_flags =
Expand Down
11 changes: 6 additions & 5 deletions lib/myxql/protocol/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ defmodule MyXQL.Protocol.Auth do
auth_plugin_name == "mysql_native_password" ->
mysql_native_password(config.password, initial_auth_plugin_data)

auth_plugin_name == "sha256_password" and config.ssl? ->
config.password <> <<0>>

auth_plugin_name == "sha256_password" and not config.ssl? ->
<<1>>
auth_plugin_name == "sha256_password" ->
if config.ssl_opts do
config.password <> <<0>>
else
<<1>>
end

auth_plugin_name == "caching_sha2_password" ->
sha256_password(config.password, initial_auth_plugin_data)
Expand Down
20 changes: 10 additions & 10 deletions test/myxql/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule MyXQL.ClientTest do
import MyXQL.Protocol.{Flags, Records}

@opts TestHelper.opts()
@opts_with_ssl TestHelper.opts_with_ssl()

describe "connect" do
@tag public_key_exchange: true
Expand All @@ -15,7 +16,7 @@ defmodule MyXQL.ClientTest do

@tag ssl: true
test "default auth plugin (ssl)" do
opts = [username: "default_auth", password: "secret", ssl: true] ++ @opts
opts = [username: "default_auth", password: "secret"] ++ @opts_with_ssl
assert {:ok, client} = Client.connect(opts)
Client.com_quit(client)
end
Expand Down Expand Up @@ -47,7 +48,7 @@ defmodule MyXQL.ClientTest do

@tag ssl: true
test "no password (ssl)" do
opts = [username: "nopassword", ssl: true] ++ @opts
opts = [username: "nopassword"] ++ @opts_with_ssl
assert {:ok, client} = Client.connect(opts)
Client.com_quit(client)

Expand All @@ -73,7 +74,7 @@ defmodule MyXQL.ClientTest do

@tag mysql_native_password: true, ssl: true
test "mysql_native_password (ssl)" do
opts = [username: "mysql_native", password: "secret", ssl: true] ++ @opts
opts = [username: "mysql_native", password: "secret"] ++ @opts_with_ssl
assert {:ok, client} = Client.connect(opts)
Client.com_quit(client)
end
Expand Down Expand Up @@ -106,7 +107,7 @@ defmodule MyXQL.ClientTest do

@tag sha256_password: true, ssl: true
test "sha256_password (ssl)" do
opts = [username: "sha256_password", password: "secret", ssl: true] ++ @opts
opts = [username: "sha256_password", password: "secret"] ++ @opts_with_ssl
assert {:ok, client} = Client.connect(opts)
Client.com_quit(client)
end
Expand All @@ -119,13 +120,13 @@ defmodule MyXQL.ClientTest do

@tag sha256_password: true, ssl: true
test "sha256_password (bad password) (ssl)" do
opts = [username: "sha256_password", password: "bad", ssl: true] ++ @opts
opts = [username: "sha256_password", password: "bad"] ++ @opts_with_ssl
{:error, err_packet(message: "Access denied" <> _)} = Client.connect(opts)
end

@tag sha256_password: true, ssl: true
test "sha256_password (empty password) (ssl)" do
opts = [username: "sha256_empty", ssl: true] ++ @opts
opts = [username: "sha256_empty"] ++ @opts_with_ssl
assert {:ok, client} = Client.connect(opts)
Client.com_quit(client)
end
Expand Down Expand Up @@ -156,7 +157,7 @@ defmodule MyXQL.ClientTest do

@tag caching_sha2_password: true, ssl: true
test "caching_sha2_password (ssl)" do
opts = [username: "caching_sha2_password", password: "secret", ssl: true] ++ @opts
opts = [username: "caching_sha2_password", password: "secret"] ++ @opts_with_ssl
assert {:ok, client} = Client.connect(opts)
Client.com_quit(client)
end
Expand All @@ -169,7 +170,7 @@ defmodule MyXQL.ClientTest do

@tag caching_sha2_password: true, ssl: true
test "caching_sha2_password (bad password) (ssl)" do
opts = [username: "caching_sha2_password", password: "bad", ssl: true] ++ @opts
opts = [username: "caching_sha2_password", password: "bad"] ++ @opts_with_ssl
{:error, err_packet(message: "Access denied" <> _)} = Client.connect(opts)
end

Expand All @@ -194,8 +195,7 @@ defmodule MyXQL.ClientTest do

@tag ssl: false
test "client requires ssl but server does not support it" do
opts = [ssl: true] ++ @opts
assert {:error, :server_does_not_support_ssl} = Client.connect(opts)
assert {:error, :server_does_not_support_ssl} = Client.connect(@opts_with_ssl)
end

test "default charset" do
Expand Down
14 changes: 10 additions & 4 deletions test/myxql_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ defmodule MyXQLTest do
import ExUnit.CaptureLog

@opts TestHelper.opts()
@opts_with_ssl TestHelper.opts_with_ssl()

describe "connect" do
@tag ssl: true
test "connect with bad SSL opts" do
assert capture_log(fn ->
opts = [ssl: true, ssl_opts: [ciphers: [:bad]]] ++ @opts
opts = put_in(@opts_with_ssl[:ssl][:ciphers], [:bad])
assert_start_and_killed(opts)
end) =~
"** (DBConnection.ConnectionError) (127.0.0.1:3306) Invalid TLS option: {ciphers,[bad]}"
Expand Down Expand Up @@ -164,7 +165,9 @@ defmodule MyXQLTest do

test "#{@protocol}: query with multiple rows", c do
%MyXQL.Result{num_rows: 2} =
MyXQL.query!(c.conn, "INSERT INTO integers VALUES (10), (20)", [], query_type: @protocol)
MyXQL.query!(c.conn, "INSERT INTO integers VALUES (10), (20)", [],
query_type: @protocol
)

assert {:ok, %MyXQL.Result{columns: ["x"], rows: [[10], [20]]}} =
MyXQL.query(c.conn, "SELECT * FROM integers")
Expand All @@ -176,7 +179,9 @@ defmodule MyXQLTest do
values = Enum.map_join(1..num, ", ", &"(#{&1})")

result =
MyXQL.query!(c.conn, "INSERT INTO integers VALUES " <> values, [], query_type: @protocol)
MyXQL.query!(c.conn, "INSERT INTO integers VALUES " <> values, [],
query_type: @protocol
)

assert result.num_rows == num

Expand Down Expand Up @@ -784,8 +789,9 @@ defmodule MyXQLTest do
end
end

@tag :skip
describe "idle ping" do
@describetag :skip

test "query before and after" do
opts = [backoff_type: :stop, idle_interval: 1] ++ @opts
{:ok, pid} = MyXQL.start_link(opts)
Expand Down
13 changes: 9 additions & 4 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
defmodule TestHelper do
def opts() do
def opts do
[
hostname: "127.0.0.1",
username: "root",
database: "myxql_test",
timeout: 5000,
ssl_opts: ssl_opts(),
backoff_type: :stop,
max_restarts: 0,
pool_size: 1,
show_sensitive_data_on_connection_error: true
]
end

defp ssl_opts() do
[versions: [:"tlsv1.2"]]
def opts_with_ssl do
opts() ++
[
ssl: [
verify: :verify_none,
versions: [:"tlsv1.2"]
]
]
end

def setup_server() do
Expand Down