From f1d5216f41f8a9afcb6c357875c9f93e6120b3bf Mon Sep 17 00:00:00 2001 From: Milan Jaric Date: Sun, 22 Nov 2020 18:10:57 +0100 Subject: [PATCH 01/17] prelogin message --- lib/tds/messages.ex | 58 ++++--- lib/tds/protocol.ex | 120 ++++++++------- lib/tds/protocol/prelogin.ex | 284 +++++++++++++++++++++++++++++++++++ lib/tds/versions.ex | 19 ++- test/login_test.exs | 48 +++++- test/plp_test.exs | 1 + 6 files changed, 446 insertions(+), 84 deletions(-) create mode 100644 lib/tds/protocol/prelogin.ex diff --git a/lib/tds/messages.ex b/lib/tds/messages.ex index 9a09d2f..3c819df 100644 --- a/lib/tds/messages.ex +++ b/lib/tds/messages.ex @@ -19,6 +19,7 @@ defmodule Tds.Messages do defrecord :msg_attn, [] # responses + defrecord :msg_preloginack, [:response] defrecord :msg_loginack, [:redirect] defrecord :msg_prepared, [:params] defrecord :msg_sql_result, [:columns, :rows, :row_count] @@ -61,6 +62,14 @@ defmodule Tds.Messages do ## Parsers + def parse(:prelogin, packet_data, s) do + response = + packet_data + |> Tds.Protocol.Prelogin.decode(s) + + {msg_preloginack(response: response), s} + end + def parse(:login, packet_data, s) do packet_data |> decode_tokens() @@ -236,16 +245,16 @@ defmodule Tds.Messages do encode(msg, env) end - defp encode(msg_prelogin(params: _params), _env) do - version_data = <<11, 0, 12, 56, 0, 0>> - version_length = byte_size(version_data) - version_offset = 0x06 - version = <<0x00, version_offset::size(16), version_length::size(16)>> - terminator = <<0xFF>> - prelogin_data = version_data - data = version <> terminator <> prelogin_data - encode_packets(0x12, data) - # encode_header(0x12, data) <> data + defp encode(msg_prelogin(params: opts), _env) do + # version_data = <<11, 0, 12, 56, 0, 0>> + # version_length = byte_size(version_data) + # version_offset = 0x06 + # version = <<0x00, version_offset::size(16), version_length::size(16)>> + # terminator = <<0xFF>> + # prelogin_data = version_data + # data = version <> terminator <> prelogin_data + # encode_packets(0x12, data) + Tds.Protocol.Prelogin.encode(opts) end defp encode(msg_login(params: params), _env) do @@ -442,7 +451,13 @@ defmodule Tds.Messages do encode_packets(0x03, data) end - defp encode(msg_transmgr(command: "TM_BEGIN_XACT", isolation_level: isolation_level), %{trans: trans}) do + defp encode( + msg_transmgr( + command: "TM_BEGIN_XACT", + isolation_level: isolation_level + ), + %{trans: trans} + ) do isolation = encode_isolation_level(isolation_level) encode_trans(5, trans, <>) end @@ -451,15 +466,21 @@ defmodule Tds.Messages do encode_trans(7, trans, <<0, 0>>) end - defp encode(msg_transmgr(command: "TM_ROLLBACK_XACT", name: name), %{trans: trans}) do - payload = unless name > 0, - do: <<0x00::size(2)-unit(8)>>, - else: <<2::unsigned-8, name::little-size(2)-unit(8), 0x0::size(1)-unit(8)>> + defp encode(msg_transmgr(command: "TM_ROLLBACK_XACT", name: name), %{ + trans: trans + }) do + payload = + unless name > 0, + do: <<0x00::size(2)-unit(8)>>, + else: + <<2::unsigned-8, name::little-size(2)-unit(8), 0x0::size(1)-unit(8)>> encode_trans(8, trans, payload) end - defp encode(msg_transmgr(command: "TM_SAVE_XACT", name: savepoint), %{trans: trans}) do + defp encode(msg_transmgr(command: "TM_SAVE_XACT", name: savepoint), %{ + trans: trans + }) do encode_trans(9, trans, <<2::unsigned-8, savepoint::little-size(2)-unit(8)>>) end @@ -493,7 +514,8 @@ defmodule Tds.Messages do all_headers = <> <> headers data = - all_headers <> <> + all_headers <> + <> encode_packets(0x0E, data) end @@ -519,7 +541,7 @@ defmodule Tds.Messages do # for that parameter. Otherwise RPC will fail and we must use ProceName # instead. But we want to avoid execution overhead with named approach # hence ommiting @handle from parameter name - %{p| name: ""} + %{p | name: ""} p -> # other paramters should be named diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index 4dc4d2e..3c3d3b6 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -23,7 +23,7 @@ defmodule Tds.Protocol do :serializable ] - @type sock :: {:gen_tcp | :ssl, pid} + @type sock :: {:gen_tcp | :ssl, port()} @type env :: %{ trans: <<_::8>>, savepoint: non_neg_integer, @@ -42,7 +42,7 @@ defmodule Tds.Protocol do @type t :: %__MODULE__{ sock: nil | sock, usock: nil | pid, - itcp: term, + itcp: non_neg_integer() | String.t(), opts: nil | Keyword.t(), state: state, result: nil | list(), @@ -53,6 +53,7 @@ defmodule Tds.Protocol do defstruct sock: nil, usock: nil, + # instance port itcp: nil, opts: nil, # Tells if connection is ready or executing command @@ -236,7 +237,7 @@ defmodule Tds.Protocol do end @impl DBConnection - @spec handle_close(Tds.Query.t(), nil | keyword | map, t()) :: + @spec handle_close(Tds.Query.t(), nil | maybe_improper_list() | map(), t()) :: {:ok, Tds.Result.t(), new_state :: t()} | {:error | :disconnect, Exception.t(), new_state :: t()} def handle_close(query, opts, s) do @@ -405,13 +406,13 @@ defmodule Tds.Protocol do :ok = :inet.setopts(sock, buffer: buffer) - case login(%{s | sock: {:gen_tcp, sock}}) do + case prelogin(%{s | sock: {:gen_tcp, sock}}) do {:error, error, _state} -> :gen_tcp.close(sock) {:error, error} - r -> - r + other -> + other end {:error, error} -> @@ -462,6 +463,29 @@ defmodule Tds.Protocol do end end + ## ssl + + defp ssl_connect(%{sock: {:gen_tcp, sock}, opts: opts} = s) do + timeout = opts[:timeout] || @timeout + ssl_opts = opts[:ssl_opts] || [] + :inet.setopts(sock, active: :once) + + case :ssl.connect(sock, [{:active, :once}, {:mode, :binary}|ssl_opts], timeout) do + {:ok, ssl_sock} -> + login(%{s | sock: {:ssl, ssl_sock}}) + + {:error, reason} -> + error = + Tds.Error.exception( + "Unable to establish secure connection to server due #{ + inspect(reason) + }" + ) + :gen_tcp.close(sock) + {:error, error, s} + end + end + def handle_info({:udp_error, _, :econnreset}, s) do msg = "Tds encountered an error while connecting to the Sql Server " <> @@ -544,22 +568,16 @@ defmodule Tds.Protocol do def prelogin(%{opts: opts} = s) do msg = msg_prelogin(params: opts) - case msg_send(msg, s) do - {:ok, s} -> - {:noreply, %{s | state: :prelogin}} - - {:error, reason, s} -> - error(%Tds.Error{message: "tcp send: #{reason}"}, s) - - any -> - any + case msg_send(msg, %{s | state: :prelogin}) do + {:ok, s} -> login(s) + any -> any end end def login(%{opts: opts} = s) do msg = msg_login(params: opts) - case login_send(msg, s) do + case login_send(msg, %{s | state: :login}) do {:ok, s} -> {:ok, %{s | state: :ready}} @@ -763,6 +781,24 @@ defmodule Tds.Protocol do end end + def message( + :prelogin, + msg_preloginack(response: response), + %{sock: {mod, port}} = s + ) do + case response do + {:login, s} -> + {:ok, s} + + {:encrypt, s} -> + ssl_connect(s) + + {:disconnect, error, s} -> + :gen_tcp.close(port) + {:error, error, s} + end + end + def message( :login, msg_loginack(redirect: %{hostname: host, port: port}), @@ -857,51 +893,33 @@ defmodule Tds.Protocol do defp msg_send( msg, - %{sock: {mod, sock}, env: env, state: state, opts: opts} = s + %{sock: {mod, port}, env: env, opts: opts} = s ) do - :inet.setopts(sock, active: false) + :inet.setopts(port, active: false) opts |> Keyword.get(:use_elixir_calendar_types, false) |> use_elixir_calendar_types() - {t_send, _} = - :timer.tc(fn -> - msg - |> encode_msg(env) - |> Enum.each(&mod.send(sock, &1)) - end) - - {t_recv, {t_decode, result}} = - :timer.tc(fn -> - case msg_recv(s) do - {:disconnect, _ex, _s} = res -> - {0, res} - - buffer -> - :timer.tc(fn -> - buffer - |> IO.iodata_to_binary() - |> decode(s) - end) + send_result = + msg + |> encode_msg(env) + |> Enum.reduce_while(:ok, fn chunk, _ -> + case mod.send(port, chunk) do + {:error, reason} -> {:halt, {:error, reason}} + :ok -> {:cont, :ok} end end) - stm = Map.get(s, :query) - - if Keyword.get(s.opts, :trace, false) == true do - Logger.debug(fn -> - "[trace] [Tds.Protocod.msg_send/2] " <> - "state=#{inspect(state)} " <> - "send=#{Tds.Perf.to_string(t_send)} " <> - "receive=#{Tds.Perf.to_string(t_recv - t_decode)} " <> - "decode=#{Tds.Perf.to_string(t_decode)}" <> - "\n" <> - "#{inspect(stm)}" - end) + with :ok <- send_result, + buffer when is_list(buffer) <- msg_recv(s) do + buffer + |> IO.iodata_to_binary() + |> decode(s) + else + {:disconnect, _ex, _s} = res -> {0, res} + other -> other end - - result end defp msg_recv(%{sock: {mod, pid}} = s) do diff --git a/lib/tds/protocol/prelogin.ex b/lib/tds/protocol/prelogin.ex new file mode 100644 index 0000000..10a7ef8 --- /dev/null +++ b/lib/tds/protocol/prelogin.ex @@ -0,0 +1,284 @@ +defmodule Tds.Protocol.Prelogin do + @moduledoc false + import Tds.Protocol.Grammar + require Logger + + # defstruct version: nil, + + @type state :: Tds.Protocol.t() + @type packet_data :: iodata() + + @type response :: + {:ok, state()} + | {:error, Exception.t() | atom(), state()} + + defstruct version: nil, + encryption: <<0x00>>, + instance: true, + threadid: nil, + mars: false, + fedauth: false, + nonceopt: nil + + @type t :: %__MODULE__{ + version: tuple(), + encryption: <<_::8>>, + instance: boolean(), + mars: boolean() + } + + @version_token 0x00 + @encryption_token 0x01 + @instopt_token 0x02 + @threadid_token 0x03 + @mars_token 0x04 + @fedauth_token 0x06 + @nonceopt_token 0x07 + @termintator_token 0xFF + + # ENCODE + + @spec encode(maybe_improper_list()) :: [binary(), ...] + def encode(opts) do + stream = [ + encode_version(opts), + encode_encryption(opts), + encode_instance(opts), + encode_threadid(opts), + encode_mars(opts), + encode_fedauth(opts) + ] + + start_offset = 5 * Enum.count(stream) + 1 + + {iodata, _} = + stream + |> Enum.reduce({[[], @termintator_token, []], start_offset}, fn + {token, option_data}, {[options, term, data], offset} -> + data_length = byte_size(option_data) + + options = [ + options, + <> + ] + + data = [data, option_data] + {[options, term, data], offset + data_length} + end) + + data = IO.iodata_to_binary(iodata) + Tds.Messages.encode_packets(0x12, data) + end + + defp encode_version(_opts) do + data = + Application.spec(:tds) + |> Keyword.get(:vsn) + |> to_string() + |> String.split(".") + |> Enum.map(&(Integer.parse(&1, 10) |> elem(0))) + |> case do + [major, minor, build] -> + <> + + [major, minor] -> + <<0x00, 0x00, minor, major, 0x00, 0x00>> + + _ -> + # probably PRE-release + <<0x01, 0x00, 0, 1, 0x00, 0x00>> + end + + {@version_token, data} + end + + defp encode_encryption(opts) do + data = + if Keyword.get(opts, :ssl, false), + do: <<0x01>>, + else: <<0x02>> + + {@encryption_token, data} + end + + defp encode_instance(opts) do + # not working for some reason + instance = Keyword.get(opts, :instance) + + if is_nil(instance) do + {@instopt_token, <<0x00>>} + else + {@instopt_token, instance <> <<0x00>>} + end + end + + defp encode_threadid(_opts) do + pid_serial = + self() + |> inspect() + |> String.split(".") + |> Enum.at(1) + |> Integer.parse() + |> elem(0) + + {@threadid_token, <>} + end + + defp encode_mars(_opts) do + {@mars_token, <<0x00>>} + end + + defp encode_fedauth(_opts) do + {@fedauth_token, <<0x01>>} + end + + # DECODE + @spec decode(iodata(), state()) :: + {:encrypt, state()} + | {:login, state()} + | {:disconnect, Tds.Error.t(), state()} + def decode(packet_data, %{opts: opts} = s) do + ecrypt = Keyword.get(opts, :ssl, false) + + {:ok, %{encryption: encryption, instance: instance}} = + packet_data + |> IO.iodata_to_binary() + |> decode_tokens([], s) + + case {ecrypt, encryption, instance} do + {_, _, false} -> + ex = Tds.Error.exception("Connected instance name mismatched") + {:disconnect, ex, s} + + {false, r, _} when r in [<<0>>, <<4>>] -> + {:login, s} + + {false, _, _} -> + {:encrypt, s} + + {true, <<0>>, _} -> + ex = Tds.Error.exception("Disconnected! Server encryption is OFF") + {:disconnect, ex, s} + + {true, <<4>>, _} -> + ex = + Tds.Error.exception( + "Disconnected! server respond encryption is NOT_SUP" + ) + + {:disconnect, ex, s} + + {true, _, _} -> + {:encrypt, s} + end + end + + defp decode_tokens( + <<@version_token, offset::ushort, length::ushort, tail::binary>>, + tokens, + s + ) do + tokens = [{:version, offset, length} | tokens] + decode_tokens(tail, tokens, s) + end + + defp decode_tokens( + <<@encryption_token, offset::ushort, length::ushort, tail::binary>>, + tokens, + s + ) do + tokens = [{:encryption, offset, length} | tokens] + decode_tokens(tail, tokens, s) + end + + defp decode_tokens( + <<@instopt_token, offset::ushort, length::ushort, tail::binary>>, + tokens, + s + ) do + tokens = [{:encryption, offset, length} | tokens] + decode_tokens(tail, tokens, s) + end + + defp decode_tokens( + <<@threadid_token, offset::ushort, length::ushort, tail::binary>>, + tokens, + s + ) do + tokens = [{:threadid, offset, length} | tokens] + decode_tokens(tail, tokens, s) + end + + defp decode_tokens( + <<@mars_token, offset::ushort, length::ushort, tail::binary>>, + tokens, + s + ) do + tokens = [{:mars, offset, length} | tokens] + decode_tokens(tail, tokens, s) + end + + defp decode_tokens( + <<@fedauth_token, offset::ushort, length::ushort, tail::binary>>, + tokens, + s + ) do + tokens = [{:fedauth, offset, length} | tokens] + decode_tokens(tail, tokens, s) + end + + defp decode_tokens( + <<@nonceopt_token, offset::ushort, length::ushort, tail::binary>>, + tokens, + s + ) do + tokens = [{:nonceopt, offset, length} | tokens] + decode_tokens(tail, tokens, s) + end + + defp decode_tokens( + <<@termintator_token, tail::binary>>, + tokens, + _s + ) do + {:ok, decode_data(Enum.reverse(tokens), tail, %__MODULE__{})} + end + + defp decode_data([], _, result), do: result + + defp decode_data([{key, _, length} | tokens], bin, m) do + <> = bin + + case key do + :version -> + <> = data + + decode_data( + tokens, + tail, + %{m | version: {major, minor, patch, trivial, subbuild}} + ) + + :encryption -> + decode_data( + tokens, + tail, + %{m | encryption: data} + ) + + :instance -> + decode_data( + tokens, + tail, + %{m | instance: data == <<0x00>>} + ) + + # :threadid -> + # :mars -> + # :fedauth -> + # :nonceopt -> + _ -> + decode_data(tokens, tail, m) + end + end +end diff --git a/lib/tds/versions.ex b/lib/tds/versions.ex index 0852275..2396591 100644 --- a/lib/tds/versions.ex +++ b/lib/tds/versions.ex @@ -1,25 +1,28 @@ defmodule Tds.Version do import Tds.Protocol.Grammar - defstruct version: 0x74000004, str_version: "7.4" + @default_version :v7_4 + @default_code 0x74000004 + + defstruct code: @default_code, version: @default_version @versions [ - {0x71000001, "7.1"}, - {0x72090002, "7.2"}, - {0x730A0003, "7.3.A"}, - {0x730B0003, "7.3.B"}, - {0x74000004, "7.4"} + {0x71000001, :v7_1}, + {0x72090002, :v7_2}, + {0x730A0003, :v7_3_a}, + {0x730B0003, :v7_3_b}, + {0x74000004, :v7_4} ] def decode(<>) do @versions - |> List.keyfind(key, 0, "7.4") + |> List.keyfind(key, 0, @default_version) end def encode(ver) do val = @versions - |> List.keyfind(ver, 1, 0x74000004) + |> List.keyfind(ver, 1, @default_code) <> end diff --git a/test/login_test.exs b/test/login_test.exs index 05bd794..4004e3d 100644 --- a/test/login_test.exs +++ b/test/login_test.exs @@ -1,14 +1,48 @@ defmodule LoginTest do use ExUnit.Case, async: true + import ExUnit.CaptureLog - test "Login with sql server authentication" do - # :dbg.tracer() - # :dbg.p(:all,:c) - # :dbg.tpl(Tds.Messages,:parse,:x) - # :dbg.tpl(Tds.Protocol,:message,:x) - opts = Application.fetch_env!(:tds, :opts) + setup do + {:ok, + [ + options: [ + database: "test", + backoff_type: :stop, + max_restarts: 0, + show_sensitive_data_on_connection_error: true + ] + ]} + end + @tag :login + test "login with sql server authentication", context do + opts = Application.fetch_env!(:tds, :opts) ++ context[:options] {:ok, pid} = Tds.start_link(opts) - assert {:ok, _} = Tds.query(pid, "SELECT 1", []) + assert {:ok, %Tds.Result{}} = Tds.query(pid, "SELECT 1", []) + end + + @tag :login + test "login with non existing sql server authentication", context do + assert capture_log(fn -> + opts = [username: "sa", password: "wrong"] + assert_start_and_killed(opts ++ context[:options]) + end) =~ ~r"\*\* \(Tds.Error\) tcp connect: econnrefused" + end + + @tag :manual + @tag :login + test "ssl", context do + opts = Application.fetch_env!(:tds, :opts) ++ [ssl: true, timeout: 10_000] + assert {:ok, pid} = Tds.start_link(opts ++ context[:options]) + assert {:ok, %Tds.Result{}} = Tds.query(pid, "SELECT 1", []) + end + + defp assert_start_and_killed(opts) do + Process.flag(:trap_exit, true) + + case Tds.start_link(opts) do + {:ok, pid} -> assert_receive {:EXIT, ^pid, :killed} + {:error, :killed} -> :ok + end end end diff --git a/test/plp_test.exs b/test/plp_test.exs index c8b03ad..11bb63d 100644 --- a/test/plp_test.exs +++ b/test/plp_test.exs @@ -101,6 +101,7 @@ defmodule PLPTest do "", ] |> Enum.join("") + :ok = query( """ From 4ffa9c2d19fb528d557270c0b1cf0e46c289aca5 Mon Sep 17 00:00:00 2001 From: Milan Jaric Date: Wed, 25 Nov 2020 14:34:25 +0100 Subject: [PATCH 02/17] poc, send header before ssl hello record --- config/dev.exs | 3 +++ lib/tds/messages.ex | 2 +- lib/tds/protocol.ex | 28 ++++++++++++++++---------- lib/tds/protocol/prelogin.ex | 39 ++++++++++++++++++------------------ mix.exs | 2 +- test/login_test.exs | 12 ++++++++--- 6 files changed, 51 insertions(+), 35 deletions(-) diff --git a/config/dev.exs b/config/dev.exs index 38c3ae5..27ee941 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,3 +1,6 @@ use Mix.Config config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase + +config :tds, + opts: [hostname: "nitrox", username: "sa", password: "some!Password", database: "test", ssl: true, ssl_opts: [certfile: "/Users/mjaric/prj/github/tds/mssql.pem", keyfile: "/Users/mjaric/prj/github/tds/mssql.key"]] diff --git a/lib/tds/messages.ex b/lib/tds/messages.ex index 3c819df..a9b9873 100644 --- a/lib/tds/messages.ex +++ b/lib/tds/messages.ex @@ -301,7 +301,7 @@ defmodule Tds.Messages do # to by IbPassword, the client SHOULD first swap the four high bits with the # four low bits and then do a bit-XOR with 0xA5 (10100101). - clt_int_name = "ODBC" + clt_int_name = "tdsx" clt_int_name_ucs = to_little_ucs2(clt_int_name) database = params[:database] || "" database_ucs = to_little_ucs2(database) diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index 3c3d3b6..57f4e06 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -466,12 +466,22 @@ defmodule Tds.Protocol do ## ssl defp ssl_connect(%{sock: {:gen_tcp, sock}, opts: opts} = s) do - timeout = opts[:timeout] || @timeout - ssl_opts = opts[:ssl_opts] || [] - :inet.setopts(sock, active: :once) - - case :ssl.connect(sock, [{:active, :once}, {:mode, :binary}|ssl_opts], timeout) do + {:ok, _} = Application.ensure_all_started(:ssl) + # timeout = opts[:timeout] || @timeout + :inet.setopts(sock, active: false) + ssl_payload_size = 267 + 8 + :gen_tcp.send(sock, <<0x12, 0x01, ssl_payload_size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>>) + + ssl_opts = (opts[:ssl_opts] || []) ++ [ + active: :false, + handshake: :hello, + # log_level: :debug, + # header: 8 + ] + # port = s.itcp || opts[:port] || System.get_env("MSSQLPORT") || 1433 + case :ssl.connect(sock, ssl_opts, :infinity) do {:ok, ssl_sock} -> + Logger.debug("OK, NO EXT") login(%{s | sock: {:ssl, ssl_sock}}) {:error, reason} -> @@ -781,11 +791,7 @@ defmodule Tds.Protocol do end end - def message( - :prelogin, - msg_preloginack(response: response), - %{sock: {mod, port}} = s - ) do + def message(:prelogin, msg_preloginack(response: response), %{sock: {_, sock}}) do case response do {:login, s} -> {:ok, s} @@ -794,7 +800,7 @@ defmodule Tds.Protocol do ssl_connect(s) {:disconnect, error, s} -> - :gen_tcp.close(port) + :gen_tcp.close(sock) {:error, error, s} end end diff --git a/lib/tds/protocol/prelogin.ex b/lib/tds/protocol/prelogin.ex index 10a7ef8..8b4507b 100644 --- a/lib/tds/protocol/prelogin.ex +++ b/lib/tds/protocol/prelogin.ex @@ -43,7 +43,8 @@ defmodule Tds.Protocol.Prelogin do stream = [ encode_version(opts), encode_encryption(opts), - encode_instance(opts), + # when instance id check is sent, encryption is not negotiated + # encode_instance(opts), encode_threadid(opts), encode_mars(opts), encode_fedauth(opts) @@ -95,8 +96,8 @@ defmodule Tds.Protocol.Prelogin do defp encode_encryption(opts) do data = if Keyword.get(opts, :ssl, false), - do: <<0x01>>, - else: <<0x02>> + do: <<0x01::byte>>, + else: <<0x02::byte>> {@encryption_token, data} end @@ -143,32 +144,28 @@ defmodule Tds.Protocol.Prelogin do {:ok, %{encryption: encryption, instance: instance}} = packet_data |> IO.iodata_to_binary() + |> IO.inspect(label: "PRELOGIN RESPONSE", binary: :hex) |> decode_tokens([], s) case {ecrypt, encryption, instance} do {_, _, false} -> - ex = Tds.Error.exception("Connected instance name mismatched") - {:disconnect, ex, s} + msg = "Connection terminated, connected instance is not '#{instance}'!" + disconnect(msg, s) - {false, r, _} when r in [<<0>>, <<4>>] -> + {false, enc, _} when enc in [<<0x00>>, <<0x02>>] -> {:login, s} - {false, _, _} -> - {:encrypt, s} - - {true, <<0>>, _} -> - ex = Tds.Error.exception("Disconnected! Server encryption is OFF") - {:disconnect, ex, s} + {false, <<0x03>>, _} -> + disconnect("Server does not allow the requested encryption level.", s) - {true, <<4>>, _} -> - ex = - Tds.Error.exception( - "Disconnected! server respond encryption is NOT_SUP" - ) + {true, <<0x00>>, _} -> + disconnect("Server does not allow the requested encryption level.", s) - {:disconnect, ex, s} + {true, <<0x03>>, _} -> + disconnect("Server does not allow the requested encryption level.", s) - {true, _, _} -> + {_, _, _} -> + Logger.debug("Upgrading connection to SSL/TSL.") {:encrypt, s} end end @@ -281,4 +278,8 @@ defmodule Tds.Protocol.Prelogin do decode_data(tokens, tail, m) end end + + defp disconnect(message, s) do + {:disconnect, Tds.Error.exception(message), s} + end end diff --git a/mix.exs b/mix.exs index 33ecd40..86f1c6e 100644 --- a/mix.exs +++ b/mix.exs @@ -39,7 +39,7 @@ defmodule Tds.Mixfile do def application do [ - extra_applications: [:logger, :db_connection, :decimal], + extra_applications: [:logger, :db_connection, :decimal, :inets, :ssl], env: [ json_library: Jason ] diff --git a/test/login_test.exs b/test/login_test.exs index 4004e3d..c858918 100644 --- a/test/login_test.exs +++ b/test/login_test.exs @@ -29,10 +29,16 @@ defmodule LoginTest do end) =~ ~r"\*\* \(Tds.Error\) tcp connect: econnrefused" end - @tag :manual @tag :login - test "ssl", context do - opts = Application.fetch_env!(:tds, :opts) ++ [ssl: true, timeout: 10_000] + @tag :tsl + test "tsl", context do + opts = Application.fetch_env!(:tds, :opts) ++ [ + ssl: true, + ssl_opts: [ + certfile: "/Users/mjaric/prj/github/tds/mssql.pem", + keyfile: "/Users/mjaric/prj/github/tds/mssql.key" + ] + ] assert {:ok, pid} = Tds.start_link(opts ++ context[:options]) assert {:ok, %Tds.Result{}} = Tds.query(pid, "SELECT 1", []) end From bef5fb148299973d28ac30dcee91921910dd12a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Jari=C4=87?= Date: Thu, 26 Nov 2020 09:42:41 +0100 Subject: [PATCH 03/17] poc, TlsWrapper module --- lib/tds/protocol.ex | 9 ++++--- lib/tds/tls_wrapper.ex | 59 ++++++++++++++++++++++++++++++++++++++++++ test/login_test.exs | 5 ++-- 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 lib/tds/tls_wrapper.ex diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index 57f4e06..00fef7f 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -469,14 +469,15 @@ defmodule Tds.Protocol do {:ok, _} = Application.ensure_all_started(:ssl) # timeout = opts[:timeout] || @timeout :inet.setopts(sock, active: false) - ssl_payload_size = 267 + 8 - :gen_tcp.send(sock, <<0x12, 0x01, ssl_payload_size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>>) + # ssl_payload_size = 267 + 8 + # :gen_tcp.send(sock, <<0x12, 0x01, ssl_payload_size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>>) ssl_opts = (opts[:ssl_opts] || []) ++ [ active: :false, + log_level: :debug, + log_alert: true, handshake: :hello, - # log_level: :debug, - # header: 8 + cb_info: {Tds.TlsWrapper, :tcp, :tcp_closed, :tcp_error, :tcp_passive} ] # port = s.itcp || opts[:port] || System.get_env("MSSQLPORT") || 1433 case :ssl.connect(sock, ssl_opts, :infinity) do diff --git a/lib/tds/tls_wrapper.ex b/lib/tds/tls_wrapper.ex new file mode 100644 index 0000000..8f5935c --- /dev/null +++ b/lib/tds/tls_wrapper.ex @@ -0,0 +1,59 @@ +defmodule Tds.TlsWrapper do + def send(sock, packet) do + IO.inspect(self(), label: "SEND IN PROCESS") + size = IO.iodata_length(packet) |> IO.inspect(label: "SSL_PAYLOAD SIZE") + header = <<0x12, 0x01, size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>> + with :ok <- :gen_tcp.send(sock, header) do + :gen_tcp.send(sock, packet) + else + any -> any + end + + end + + def recv(sock, length, timeout \\ :infinity) do + IO.inspect(self(), label: "IN PROCESS") + case :gen_tcp.recv(sock, length, timeout) do + {:ok, <<0x12, 0x01, size::unsigned-16, _::32 ,tail::binary>>} -> + remaining = size - 8 + byte_size(tail) + if remaining == 0, do: {:ok, tail}, else: recv_more(sock, remaining, tail, timeout) + + any -> any + end + end + + def recv_more(sock, length, payload, timeout) do + case :gen_tcp.recv(sock, length, timeout) do + {:ok, tail} -> + {:ok, [payload, tail] |> IO.iodata_to_binary()} + + any -> any + end + end + + defdelegate getopts(port, options), to: :inet + + defdelegate setopts(socket, options), to: :inet + + defdelegate peername(socket), to: :inet + + :gen_tcp.module_info(:exports) + |> Enum.reject(fn {fun, _} -> fun in [:send, :recv, :module_info] end) + |> Enum.each(fn + {name, 0} -> + defdelegate unquote(name)(), to: :gen_tcp + + {name, 1} -> + defdelegate unquote(name)(arg1), to: :gen_tcp + + {name, 2} -> + defdelegate unquote(name)(arg1, arg2), to: :gen_tcp + + {name, 3} -> + defdelegate unquote(name)(arg1, arg2, arg3), to: :gen_tcp + + {name, 4} -> + defdelegate unquote(name)(arg1, arg2, arg3, arg4), to: :gen_tcp + end) + +end diff --git a/test/login_test.exs b/test/login_test.exs index c858918..48c8523 100644 --- a/test/login_test.exs +++ b/test/login_test.exs @@ -35,8 +35,9 @@ defmodule LoginTest do opts = Application.fetch_env!(:tds, :opts) ++ [ ssl: true, ssl_opts: [ - certfile: "/Users/mjaric/prj/github/tds/mssql.pem", - keyfile: "/Users/mjaric/prj/github/tds/mssql.key" + # certfile: "/Users/mjaric/prj/github/tds/mssql.pem", + # keyfile: "/Users/mjaric/prj/github/tds/mssql.key" + log_debug: true ] ] assert {:ok, pid} = Tds.start_link(opts ++ context[:options]) From d6856a97248634bf877e1b99d761abcd19ae946d Mon Sep 17 00:00:00 2001 From: Milan Jaric Date: Thu, 26 Nov 2020 10:11:34 +0100 Subject: [PATCH 04/17] poc tlsflow --- lib/tds/protocol.ex | 2 +- lib/tds/tls_wrapper.ex | 54 ++++++++++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index 00fef7f..b11ea30 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -471,7 +471,7 @@ defmodule Tds.Protocol do :inet.setopts(sock, active: false) # ssl_payload_size = 267 + 8 # :gen_tcp.send(sock, <<0x12, 0x01, ssl_payload_size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>>) - + Logger.debug("TDS PID: #{inspect(self())}") ssl_opts = (opts[:ssl_opts] || []) ++ [ active: :false, log_level: :debug, diff --git a/lib/tds/tls_wrapper.ex b/lib/tds/tls_wrapper.ex index 8f5935c..533c1ac 100644 --- a/lib/tds/tls_wrapper.ex +++ b/lib/tds/tls_wrapper.ex @@ -1,24 +1,44 @@ defmodule Tds.TlsWrapper do + require Logger + def send(sock, packet) do - IO.inspect(self(), label: "SEND IN PROCESS") - size = IO.iodata_length(packet) |> IO.inspect(label: "SSL_PAYLOAD SIZE") - header = <<0x12, 0x01, size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>> - with :ok <- :gen_tcp.send(sock, header) do - :gen_tcp.send(sock, packet) + unless handshake_complete() do + size = IO.iodata_length(packet) + 8 + Process.put(:handshake_complete, true) + Logger.debug( + "SSL_PAYLOAD SIZE: #{size - 8} plus 8 bytes for header in PID #{ + inspect(self()) + }" + ) + + header = + <<0x12, 0x01, size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>> + + :gen_tcp.send(sock, [header, packet]) else - any -> any + :gen_tcp.send(sock, packet) end - end def recv(sock, length, timeout \\ :infinity) do - IO.inspect(self(), label: "IN PROCESS") - case :gen_tcp.recv(sock, length, timeout) do - {:ok, <<0x12, 0x01, size::unsigned-16, _::32 ,tail::binary>>} -> - remaining = size - 8 + byte_size(tail) - if remaining == 0, do: {:ok, tail}, else: recv_more(sock, remaining, tail, timeout) + unless handshake_complete() do + Logger.debug("RECEIVE #{inspect(self())}") - any -> any + result = case :gen_tcp.recv(sock, length, timeout) do + {:ok, <<0x12, 0x01, size::unsigned-16, _::32, tail::binary>>} -> + remaining = size - 8 + byte_size(tail) + + if remaining == 0, + do: {:ok, tail}, + else: recv_more(sock, remaining, tail, timeout) + + any -> + any + end + Process.put(:handshake_complete, true) + result + else + :gen_tcp.recv(sock, length, timeout) end end @@ -27,10 +47,15 @@ defmodule Tds.TlsWrapper do {:ok, tail} -> {:ok, [payload, tail] |> IO.iodata_to_binary()} - any -> any + any -> + any end end + defp handshake_complete() do + Process.get(:handshake_complete, false) + end + defdelegate getopts(port, options), to: :inet defdelegate setopts(socket, options), to: :inet @@ -55,5 +80,4 @@ defmodule Tds.TlsWrapper do {name, 4} -> defdelegate unquote(name)(arg1, arg2, arg3, arg4), to: :gen_tcp end) - end From 13d884f547ddc6a01a9ee72e954461be35b0e99e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Jari=C4=87?= Date: Mon, 30 Nov 2020 13:21:02 +0100 Subject: [PATCH 05/17] half way to complete handshake --- lib/tds/protocol.ex | 15 +--- lib/tds/tls.ex | 155 +++++++++++++++++++++++++++++++++++++++++ lib/tds/tls_wrapper.ex | 35 +++++----- 3 files changed, 175 insertions(+), 30 deletions(-) create mode 100644 lib/tds/tls.ex diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index b11ea30..7845266 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -467,20 +467,9 @@ defmodule Tds.Protocol do defp ssl_connect(%{sock: {:gen_tcp, sock}, opts: opts} = s) do {:ok, _} = Application.ensure_all_started(:ssl) - # timeout = opts[:timeout] || @timeout :inet.setopts(sock, active: false) - # ssl_payload_size = 267 + 8 - # :gen_tcp.send(sock, <<0x12, 0x01, ssl_payload_size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>>) - Logger.debug("TDS PID: #{inspect(self())}") - ssl_opts = (opts[:ssl_opts] || []) ++ [ - active: :false, - log_level: :debug, - log_alert: true, - handshake: :hello, - cb_info: {Tds.TlsWrapper, :tcp, :tcp_closed, :tcp_error, :tcp_passive} - ] - # port = s.itcp || opts[:port] || System.get_env("MSSQLPORT") || 1433 - case :ssl.connect(sock, ssl_opts, :infinity) do + + case Tds.Tls.connect(sock, opts[:ssl_opts] || []) do {:ok, ssl_sock} -> Logger.debug("OK, NO EXT") login(%{s | sock: {:ssl, ssl_sock}}) diff --git a/lib/tds/tls.ex b/lib/tds/tls.ex new file mode 100644 index 0000000..b7b728e --- /dev/null +++ b/lib/tds/tls.ex @@ -0,0 +1,155 @@ +defmodule Tds.Tls do + @moduledoc false + require Logger + use GenServer + import Kernel, except: [send: 2] + + defstruct [:socket, :ssl_opts, :owner_pid, :handshake] + + def connect(socket, ssl_opts) do + Logger.debug("starting TLS upgrade") + ssl_opts = ssl_opts ++ [ + active: false, + log_level: :debug, + log_alert: true, + handshake: :full, + cb_info: {Tds.Tls, :tcp, :tcp_closed, :tcp_error, :tcp_passive} + ] + :inet.setopts(socket, active: false) + with {:ok, pid} <- GenServer.start_link(__MODULE__, {socket, ssl_opts}, []), + :ok <- :gen_tcp.controlling_process(socket, pid) do + :ssl.connect(socket, ssl_opts, :infinity) + else + error -> error + end + end + + def controlling_process(socket, tls_conn_pid) do + {:connected, pid} = Port.info(socket, :connected) + GenServer.call(pid, {:controlling_process, tls_conn_pid}) + end + + def send(socket, payload) do + {:connected, pid} = Port.info(socket, :connected) + GenServer.call(pid, {:send, payload}) + end + + def recv(socket, length, timeout \\ :infinity) do + {:connected, pid} = Port.info(socket, :connected) + GenServer.call(pid, {:recv, length, timeout}, timeout) + end + + defdelegate getopts(port, options), to: :inet + + # defdelegate setopts(socket, options), to: :inet + def setopts(socket, options) do + {:connected, pid} = Port.info(socket, :connected) + GenServer.call(pid, {:setopts, options}) + end + + defdelegate peername(socket), to: :inet + + :exports + |> :gen_tcp.module_info() + |> Enum.reject(fn {fun, _} -> + fun in [:send, :recv, :module_info, :controlling_process] + end) + |> Enum.each(fn + {name, 0} -> + defdelegate unquote(name)(), to: :gen_tcp + + {name, 1} -> + defdelegate unquote(name)(arg1), to: :gen_tcp + + {name, 2} -> + defdelegate unquote(name)(arg1, arg2), to: :gen_tcp + + {name, 3} -> + defdelegate unquote(name)(arg1, arg2, arg3), to: :gen_tcp + + {name, 4} -> + defdelegate unquote(name)(arg1, arg2, arg3, arg4), to: :gen_tcp + end) + + # SERVER + def init({socket, ssl_opts}) do + {:ok, %__MODULE__{socket: socket, ssl_opts: ssl_opts, handshake: true}} + end + + def handle_call({:controlling_process, tls_conn_pid}, _from, s) do + {:reply, :ok, %{s | owner_pid: tls_conn_pid}} + end + + def handle_call({:setopts, options}, _from, %{socket: socket, handshake: hs} = s) do + tds_header_size = if hs == true, do: 8, else: 0 + + opts = + options + |> Enum.map(fn + {:active, val} when is_number(val) -> {:active, val + tds_header_size} + val -> val + end) + + {:reply, :inet.setopts(socket, opts), s} + end + + def handle_call({:send, data}, _from, %{socket: socket, handshake: true} = s) do + size = IO.iodata_length(data) + 8 + header = + <<0x12, 0x01, size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>> + + resp = :gen_tcp.send(socket, [header, data]) + {:reply, resp, s} + end + + def handle_call({:send, data}, _from, %{socket: socket, handshake: false} = s) do + resp = :gen_tcp.send(socket, data) + {:reply, resp, s} + end + + # def handle_call({:recv, length, timeout}, _from, %{socket: socket, handshake: true} = s) do + # res = case :gen_tcp.recv(socket, length, timeout) do + # {:ok, data} + # end + # {:reply, res, s} + # end + + def handle_call({:recv, length, timeout}, _from, %{socket: socket} = s) do + res = :gen_tcp.recv(socket, length, timeout) + {:reply, res, s} + end + + def handle_info( + {:tcp, _, + <<0x12, 0x01, size::unsigned-16, _::32, ssl_payload::binary>>}, + %{socket: socket, owner_pid: pid} = s + ) do + Logger.debug( + "received #{size} bytes from server in prelogin message as SSL_PAYLOAD" + ) + + Kernel.send(pid, {:tcp, socket, ssl_payload}) + {:noreply, %{s | handshake: false}} + end + + def handle_info({:tcp, _, _} = msg, %{owner_pid: pid} = s) do + Logger.debug("received SSL_PAYLOAD from server, NON PreLogin message") + + Kernel.send(pid, msg) + {:noreply, s} + end + + def handle_info({tag, _} = msg, %{owner_pid: pid} = s) + when tag in [:tcp_closed, :ssl_closed] do + # todo + send(pid, msg) + {:stop, tag, s} + end + + def handle_info({tag, _, _} = msg, %{owner_pid: pid} = s) + when tag in [:tcp_error, :ssl_error] do + # todo + send(pid, msg) + {:stop, tag, s} + end +end diff --git a/lib/tds/tls_wrapper.ex b/lib/tds/tls_wrapper.ex index 533c1ac..dd981fc 100644 --- a/lib/tds/tls_wrapper.ex +++ b/lib/tds/tls_wrapper.ex @@ -1,6 +1,5 @@ defmodule Tds.TlsWrapper do require Logger - def send(sock, packet) do unless handshake_complete() do size = IO.iodata_length(packet) + 8 @@ -15,31 +14,29 @@ defmodule Tds.TlsWrapper do <<0x12, 0x01, size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>> :gen_tcp.send(sock, [header, packet]) + else :gen_tcp.send(sock, packet) end end def recv(sock, length, timeout \\ :infinity) do - unless handshake_complete() do - Logger.debug("RECEIVE #{inspect(self())}") + Logger.debug("RECEIVE #{inspect(self())}") - result = case :gen_tcp.recv(sock, length, timeout) do - {:ok, <<0x12, 0x01, size::unsigned-16, _::32, tail::binary>>} -> - remaining = size - 8 + byte_size(tail) + result = case :gen_tcp.recv(sock, length, timeout) do + {:ok, <<0x12, 0x01, size::unsigned-16, _::32, tail::binary>>} -> + remaining = size - 8 + byte_size(tail) - if remaining == 0, - do: {:ok, tail}, - else: recv_more(sock, remaining, tail, timeout) + if remaining == 0, + do: {:ok, tail}, + else: recv_more(sock, remaining, tail, timeout) - any -> - any - end - Process.put(:handshake_complete, true) - result - else - :gen_tcp.recv(sock, length, timeout) + any -> + handshake_complete() + any end + result + end def recv_more(sock, length, payload, timeout) do @@ -58,7 +55,11 @@ defmodule Tds.TlsWrapper do defdelegate getopts(port, options), to: :inet - defdelegate setopts(socket, options), to: :inet + # defdelegate setopts(socket, options), to: :inet + def setopts(socket, options) do + if [active: 100] == options, do: raise "STOP" + :inet.setopts(socket, options) + end defdelegate peername(socket), to: :inet From 9923a458db156ef7357115bc0f1452fcddae8309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Jari=C4=87?= Date: Mon, 30 Nov 2020 13:42:09 +0100 Subject: [PATCH 06/17] Complete SSL handshake works now, login needs some attention since password is :REDACTED --- lib/tds/protocol.ex | 5 ++- lib/tds/tls.ex | 10 ++++- lib/tds/tls_wrapper.ex | 84 ------------------------------------------ 3 files changed, 12 insertions(+), 87 deletions(-) delete mode 100644 lib/tds/tls_wrapper.ex diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index 7845266..fa53ccd 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -891,7 +891,10 @@ defmodule Tds.Protocol do msg, %{sock: {mod, port}, env: env, opts: opts} = s ) do - :inet.setopts(port, active: false) + :ok = case mod do + :ssl -> :ssl.setopts(port, active: false) + _ -> :inet.setopts(port, active: false) + end opts |> Keyword.get(:use_elixir_calendar_types, false) diff --git a/lib/tds/tls.ex b/lib/tds/tls.ex index b7b728e..7e90c81 100644 --- a/lib/tds/tls.ex +++ b/lib/tds/tls.ex @@ -18,7 +18,9 @@ defmodule Tds.Tls do :inet.setopts(socket, active: false) with {:ok, pid} <- GenServer.start_link(__MODULE__, {socket, ssl_opts}, []), :ok <- :gen_tcp.controlling_process(socket, pid) do - :ssl.connect(socket, ssl_opts, :infinity) + res = :ssl.connect(socket, ssl_opts, :infinity) + GenServer.cast(pid, :handshake_complete) + res else error -> error end @@ -119,6 +121,10 @@ defmodule Tds.Tls do {:reply, res, s} end + def handle_cast(:handshake_complete, s) do + {:noreply, %{s | handshake: false} } + end + def handle_info( {:tcp, _, <<0x12, 0x01, size::unsigned-16, _::32, ssl_payload::binary>>}, @@ -129,7 +135,7 @@ defmodule Tds.Tls do ) Kernel.send(pid, {:tcp, socket, ssl_payload}) - {:noreply, %{s | handshake: false}} + {:noreply, s} end def handle_info({:tcp, _, _} = msg, %{owner_pid: pid} = s) do diff --git a/lib/tds/tls_wrapper.ex b/lib/tds/tls_wrapper.ex deleted file mode 100644 index dd981fc..0000000 --- a/lib/tds/tls_wrapper.ex +++ /dev/null @@ -1,84 +0,0 @@ -defmodule Tds.TlsWrapper do - require Logger - def send(sock, packet) do - unless handshake_complete() do - size = IO.iodata_length(packet) + 8 - Process.put(:handshake_complete, true) - Logger.debug( - "SSL_PAYLOAD SIZE: #{size - 8} plus 8 bytes for header in PID #{ - inspect(self()) - }" - ) - - header = - <<0x12, 0x01, size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>> - - :gen_tcp.send(sock, [header, packet]) - - else - :gen_tcp.send(sock, packet) - end - end - - def recv(sock, length, timeout \\ :infinity) do - Logger.debug("RECEIVE #{inspect(self())}") - - result = case :gen_tcp.recv(sock, length, timeout) do - {:ok, <<0x12, 0x01, size::unsigned-16, _::32, tail::binary>>} -> - remaining = size - 8 + byte_size(tail) - - if remaining == 0, - do: {:ok, tail}, - else: recv_more(sock, remaining, tail, timeout) - - any -> - handshake_complete() - any - end - result - - end - - def recv_more(sock, length, payload, timeout) do - case :gen_tcp.recv(sock, length, timeout) do - {:ok, tail} -> - {:ok, [payload, tail] |> IO.iodata_to_binary()} - - any -> - any - end - end - - defp handshake_complete() do - Process.get(:handshake_complete, false) - end - - defdelegate getopts(port, options), to: :inet - - # defdelegate setopts(socket, options), to: :inet - def setopts(socket, options) do - if [active: 100] == options, do: raise "STOP" - :inet.setopts(socket, options) - end - - defdelegate peername(socket), to: :inet - - :gen_tcp.module_info(:exports) - |> Enum.reject(fn {fun, _} -> fun in [:send, :recv, :module_info] end) - |> Enum.each(fn - {name, 0} -> - defdelegate unquote(name)(), to: :gen_tcp - - {name, 1} -> - defdelegate unquote(name)(arg1), to: :gen_tcp - - {name, 2} -> - defdelegate unquote(name)(arg1, arg2), to: :gen_tcp - - {name, 3} -> - defdelegate unquote(name)(arg1, arg2, arg3), to: :gen_tcp - - {name, 4} -> - defdelegate unquote(name)(arg1, arg2, arg3, arg4), to: :gen_tcp - end) -end From 4778f1260ee7321c2578d52f9741cadbefe9a851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Jari=C4=87?= Date: Mon, 30 Nov 2020 14:14:22 +0100 Subject: [PATCH 07/17] cleaning up --- lib/tds/protocol.ex | 33 ++++++++++++++++----------------- lib/tds/protocol/prelogin.ex | 1 - lib/tds/tls.ex | 15 ++++++--------- test/login_test.exs | 5 +---- 4 files changed, 23 insertions(+), 31 deletions(-) diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index fa53ccd..718c78b 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -144,7 +144,7 @@ defmodule Tds.Protocol do def checkout(%{sock: {mod, sock}} = s) do sock_mod = inspect(mod) - case :inet.setopts(sock, active: false) do + case setopts(s.sock, active: false) do :ok -> {:ok, s} @@ -166,7 +166,7 @@ defmodule Tds.Protocol do def checkin(%{sock: {mod, sock}} = s) do sock_mod = inspect(mod) - case :inet.setopts(sock, active: :once) do + case setopts(s.sock, active: :once) do :ok -> {:ok, s} @@ -408,7 +408,6 @@ defmodule Tds.Protocol do case prelogin(%{s | sock: {:gen_tcp, sock}}) do {:error, error, _state} -> - :gen_tcp.close(sock) {:error, error} other -> @@ -471,8 +470,8 @@ defmodule Tds.Protocol do case Tds.Tls.connect(sock, opts[:ssl_opts] || []) do {:ok, ssl_sock} -> - Logger.debug("OK, NO EXT") - login(%{s | sock: {:ssl, ssl_sock}}) + state = %{s | sock: {:ssl, ssl_sock} } + {:ok, state} {:error, reason} -> error = @@ -498,10 +497,7 @@ defmodule Tds.Protocol do {:tcp, _, _data}, %{sock: {mod, sock}, opts: opts, state: :prelogin} = s ) do - case mod do - :gen_tcp -> :inet.setopts(sock, active: false) - :ssl -> :ssl.setopts(sock, active: false) - end + setopts(s.sock, active: false) login(%{s | opts: opts, sock: {mod, sock}}) end @@ -811,9 +807,7 @@ defmodule Tds.Protocol do connect(new_opts) end - def message(:login, msg_loginack(), %{opts: opts} = s) do - state = %{s | opts: clean_opts(opts)} - + def message(:login, msg_loginack(), %{opts: opts} = state) do opts |> conn_opts() |> IO.iodata_to_binary() @@ -869,8 +863,9 @@ defmodule Tds.Protocol do end # Send Command To Sql Server - defp login_send(msg, %{sock: {mod, sock}, env: env} = s) do + defp login_send(msg, %{sock: {mod, sock}, env: env, opts: opts} = s) do paks = encode_msg(msg, env) + s = %{s | opts: clean_opts(opts)} Enum.each(paks, fn pak -> mod.send(sock, pak) @@ -891,10 +886,7 @@ defmodule Tds.Protocol do msg, %{sock: {mod, port}, env: env, opts: opts} = s ) do - :ok = case mod do - :ssl -> :ssl.setopts(port, active: false) - _ -> :inet.setopts(port, active: false) - end + setopts(s.sock, active: false) opts |> Keyword.get(:use_elixir_calendar_types, false) @@ -1192,4 +1184,11 @@ defmodule Tds.Protocol do ) end end + + defp setopts({mod, sock}, options) do + case mod do + :gen_tcp -> :inet.setopts(sock, options) + :ssl -> :ssl.setopts(sock, options) + end + end end diff --git a/lib/tds/protocol/prelogin.ex b/lib/tds/protocol/prelogin.ex index 8b4507b..8b55a1f 100644 --- a/lib/tds/protocol/prelogin.ex +++ b/lib/tds/protocol/prelogin.ex @@ -144,7 +144,6 @@ defmodule Tds.Protocol.Prelogin do {:ok, %{encryption: encryption, instance: instance}} = packet_data |> IO.iodata_to_binary() - |> IO.inspect(label: "PRELOGIN RESPONSE", binary: :hex) |> decode_tokens([], s) case {ecrypt, encryption, instance} do diff --git a/lib/tds/tls.ex b/lib/tds/tls.ex index 7e90c81..93f275a 100644 --- a/lib/tds/tls.ex +++ b/lib/tds/tls.ex @@ -7,12 +7,9 @@ defmodule Tds.Tls do defstruct [:socket, :ssl_opts, :owner_pid, :handshake] def connect(socket, ssl_opts) do - Logger.debug("starting TLS upgrade") + # Logger.debug("starting TLS upgrade") ssl_opts = ssl_opts ++ [ active: false, - log_level: :debug, - log_alert: true, - handshake: :full, cb_info: {Tds.Tls, :tcp, :tcp_closed, :tcp_error, :tcp_passive} ] :inet.setopts(socket, active: false) @@ -127,19 +124,19 @@ defmodule Tds.Tls do def handle_info( {:tcp, _, - <<0x12, 0x01, size::unsigned-16, _::32, ssl_payload::binary>>}, + <<0x12, 0x01, _::unsigned-16, _::32, ssl_payload::binary>>}, %{socket: socket, owner_pid: pid} = s ) do - Logger.debug( - "received #{size} bytes from server in prelogin message as SSL_PAYLOAD" - ) + # Logger.debug( + # "received #{size} bytes from server in prelogin message as SSL_PAYLOAD" + # ) Kernel.send(pid, {:tcp, socket, ssl_payload}) {:noreply, s} end def handle_info({:tcp, _, _} = msg, %{owner_pid: pid} = s) do - Logger.debug("received SSL_PAYLOAD from server, NON PreLogin message") + # Logger.debug("received SSL_PAYLOAD from server, NON PreLogin message") Kernel.send(pid, msg) {:noreply, s} diff --git a/test/login_test.exs b/test/login_test.exs index 48c8523..6a6cd92 100644 --- a/test/login_test.exs +++ b/test/login_test.exs @@ -30,13 +30,10 @@ defmodule LoginTest do end @tag :login - @tag :tsl - test "tsl", context do + test "login with tsl", context do opts = Application.fetch_env!(:tds, :opts) ++ [ ssl: true, ssl_opts: [ - # certfile: "/Users/mjaric/prj/github/tds/mssql.pem", - # keyfile: "/Users/mjaric/prj/github/tds/mssql.key" log_debug: true ] ] From c7e8bc83d5debab1d6d55b4afa9655956e9f45bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Jari=C4=87?= Date: Mon, 30 Nov 2020 14:21:23 +0100 Subject: [PATCH 08/17] resolves #109 but needs some testing --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 740005e..a518582 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,22 @@ config :your_app, :tds_conn, port: 1433 ``` +or with ssl + +```elixir +import Mix.Config + +config :your_app, :tds_conn, + hostname: "localhost", + username: "test_user", + password: "test_password", + database: "test_db", + port: 1433, + ssl: true, + ssl_opts: [] # add key or leave empty for selfsigned certs, accepts :ssl.client_option() + +``` + Then using `Application.get_env(:your_app, :tds_conn)` use this as first parameter in `Tds.start_link/1` function. There is additional parameter that can be used in configuration and From a01325a65c27b87473698d210102a91a0d75ee6d Mon Sep 17 00:00:00 2001 From: Milan Jaric Date: Tue, 1 Dec 2020 18:27:54 +0100 Subject: [PATCH 09/17] backward compatibility with OTP pre 23 --- lib/tds/tls.ex | 2 +- test/login_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tds/tls.ex b/lib/tds/tls.ex index 93f275a..b361eeb 100644 --- a/lib/tds/tls.ex +++ b/lib/tds/tls.ex @@ -10,7 +10,7 @@ defmodule Tds.Tls do # Logger.debug("starting TLS upgrade") ssl_opts = ssl_opts ++ [ active: false, - cb_info: {Tds.Tls, :tcp, :tcp_closed, :tcp_error, :tcp_passive} + cb_info: {Tds.Tls, :tcp, :tcp_closed, :tcp_error} ] :inet.setopts(socket, active: false) with {:ok, pid} <- GenServer.start_link(__MODULE__, {socket, ssl_opts}, []), diff --git a/test/login_test.exs b/test/login_test.exs index 6a6cd92..5bd5d3c 100644 --- a/test/login_test.exs +++ b/test/login_test.exs @@ -26,7 +26,7 @@ defmodule LoginTest do assert capture_log(fn -> opts = [username: "sa", password: "wrong"] assert_start_and_killed(opts ++ context[:options]) - end) =~ ~r"\*\* \(Tds.Error\) tcp connect: econnrefused" + end) =~ "(Tds.Error) Line 1 (Error 18456): Login failed for user 'sa'" end @tag :login From 5526f39a94e87b41b8df5efe0f0c7b15a7378306 Mon Sep 17 00:00:00 2001 From: Milan Jaric Date: Tue, 1 Dec 2020 18:34:28 +0100 Subject: [PATCH 10/17] this should render =~ "(Tds.Error) Line 1 (Error 18456): Login failed for user 'sa'" --- test/login_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/login_test.exs b/test/login_test.exs index 5bd5d3c..6a6cd92 100644 --- a/test/login_test.exs +++ b/test/login_test.exs @@ -26,7 +26,7 @@ defmodule LoginTest do assert capture_log(fn -> opts = [username: "sa", password: "wrong"] assert_start_and_killed(opts ++ context[:options]) - end) =~ "(Tds.Error) Line 1 (Error 18456): Login failed for user 'sa'" + end) =~ ~r"\*\* \(Tds.Error\) tcp connect: econnrefused" end @tag :login From eb0fb6af8b4eced8b30d64efc9fb80c650786895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Jari=C4=87?= Date: Wed, 2 Dec 2020 12:04:29 +0100 Subject: [PATCH 11/17] login falure testcase fix, missed proper hostname in connection options --- lib/tds/protocol.ex | 16 +++++----------- test/login_test.exs | 37 +++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index 718c78b..b3f46cd 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -777,17 +777,11 @@ defmodule Tds.Protocol do end end - def message(:prelogin, msg_preloginack(response: response), %{sock: {_, sock}}) do + def message(:prelogin, msg_preloginack(response: response), _) do case response do - {:login, s} -> - {:ok, s} - - {:encrypt, s} -> - ssl_connect(s) - - {:disconnect, error, s} -> - :gen_tcp.close(sock) - {:error, error, s} + {:login, s} -> {:ok, s} + {:encrypt, s} -> ssl_connect(s) + other -> other end end @@ -873,7 +867,7 @@ defmodule Tds.Protocol do case msg_recv(s) do {:disconnect, ex, s} -> - {:error, ex, s} + {:disconnect, ex, s} buffer -> buffer diff --git a/test/login_test.exs b/test/login_test.exs index 6a6cd92..d154121 100644 --- a/test/login_test.exs +++ b/test/login_test.exs @@ -3,9 +3,14 @@ defmodule LoginTest do import ExUnit.CaptureLog setup do + hostname = + Application.fetch_env!(:tds, :opts) + |> Keyword.get(:hostname) + {:ok, [ options: [ + hostname: hostname, database: "test", backoff_type: :stop, max_restarts: 0, @@ -24,23 +29,35 @@ defmodule LoginTest do @tag :login test "login with non existing sql server authentication", context do assert capture_log(fn -> - opts = [username: "sa", password: "wrong"] - assert_start_and_killed(opts ++ context[:options]) - end) =~ ~r"\*\* \(Tds.Error\) tcp connect: econnrefused" + opts = [username: "sa", password: "wrong"] ++ context[:options] + assert_start_and_killed(opts) + end) =~ + "(Tds.Error) Line 1 (Error 18456): Login failed for user 'sa'" end @tag :login - test "login with tsl", context do - opts = Application.fetch_env!(:tds, :opts) ++ [ - ssl: true, - ssl_opts: [ - log_debug: true - ] - ] + test "login with valid sql login over tsl", context do + opts = + Application.fetch_env!(:tds, :opts) ++ + [ssl: true, ssl_opts: [log_debug: true]] + assert {:ok, pid} = Tds.start_link(opts ++ context[:options]) assert {:ok, %Tds.Result{}} = Tds.query(pid, "SELECT 1", []) end + @tag :login + test "login with non existing sql server authentication over tls", context do + assert capture_log(fn -> + opts = + [username: "sa", password: "wrong"] ++ + context[:options] ++ + [ssl: true, ssl_opts: [log_debug: true]] + + assert_start_and_killed(opts) + end) =~ + "(Tds.Error) Line 1 (Error 18456): Login failed for user 'sa'" + end + defp assert_start_and_killed(opts) do Process.flag(:trap_exit, true) From 5cf3fe2f8babc31788cb06c2be29cd787a4638ca Mon Sep 17 00:00:00 2001 From: Milan Jaric Date: Sat, 19 Dec 2020 13:49:17 +0100 Subject: [PATCH 12/17] ntlm negotiation --- lib/ntlm.ex | 130 ++++++++++++++++++++++++++++++++++++++++++++ lib/tds/protocol.ex | 4 +- 2 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 lib/ntlm.ex diff --git a/lib/ntlm.ex b/lib/ntlm.ex new file mode 100644 index 0000000..5bad459 --- /dev/null +++ b/lib/ntlm.ex @@ -0,0 +1,130 @@ +defmodule Ntlm do + @moduledoc """ + This module provides encoders and decoders for NTLM + negotiation and authentincation + """ + require Bitwise + + @ntlm_NegotiateUnicode 0x00000001 + @ntlm_NegotiateOEM 0x00000002 + @ntlm_RequestTarget 0x00000004 + @ntlm_Unknown9 0x00000008 + @ntlm_NegotiateSign 0x00000010 + @ntlm_NegotiateSeal 0x00000020 + @ntlm_NegotiateDatagram 0x00000040 + @ntlm_NegotiateLanManagerKey 0x00000080 + @ntlm_Unknown8 0x00000100 + @ntlm_NegotiateNTLM 0x00000200 + @ntlm_NegotiateNTOnly 0x00000400 + @ntlm_Anonymous 0x00000800 + @ntlm_NegotiateOemDomainSupplied 0x00001000 + @ntlm_NegotiateOemWorkstationSupplied 0x00002000 + @ntlm_Unknown6 0x00004000 + @ntlm_NegotiateAlwaysSign 0x00008000 + @ntlm_TargetTypeDomain 0x00010000 + @ntlm_TargetTypeServer 0x00020000 + @ntlm_TargetTypeShare 0x00040000 + @ntlm_NegotiateExtendedSecurity 0x00080000 + @ntlm_NegotiateIdentify 0x00100000 + @ntlm_Unknown5 0x00200000 + @ntlm_RequestNonNTSessionKey 0x00400000 + @ntlm_NegotiateTargetInfo 0x00800000 + @ntlm_Unknown4 0x01000000 + @ntlm_NegotiateVersion 0x02000000 + @ntlm_Unknown3 0x04000000 + @ntlm_Unknown2 0x08000000 + @ntlm_Unknown1 0x10000000 + @ntlm_Negotiate128 0x20000000 + @ntlm_NegotiateKeyExchange 0x40000000 + @ntlm_Negotiate56 0x80000000 + + @doc """ + Builds NTLM negotiation message `<<"NTLMSSP", 0x00, 0x01 ...>>` + + - `opts` - is a `Keyword.t` list that requires `:domain` key and accepts + optinal `:workstation` string. Both values can only contain valid ASCII + characters + """ + @spec negotiate(keyword) :: <<_::64, _::_*8>> + def negotiate(opts \\ []) do + fixed_data_len = 40 + domain = :unicode.characters_to_binary(opts[:domain], :unicode, :latin1) + domain_length = String.length(opts[:domain]) + + workstation = + opts + |> Keyword.get(:workstation) + |> Kernel.||("") + + workstation_length = String.length(workstation) + workstation = :unicode.characters_to_binary(workstation, :unicode, :latin1) + + type1_flags = type1_flags(workstation != <<>>) + + << + "NTLMSSP", + 0x00, + 0x01::little-unsigned-size(4)-unit(8), + type1_flags::little-unsigned-size(4)-unit(8), + domain_length::little-unsigned-size(2)-unit(8), + domain_length::little-unsigned-size(2)-unit(8), + fixed_data_len + workstation_length::little-unsigned-size(4)-unit(8), + workstation_length::little-unsigned-size(2)-unit(8), + workstation_length::little-unsigned-size(2)-unit(8), + fixed_data_len::little-unsigned-size(4)-unit(8), + 5, + 0, + 2195::little-unsigned-size(2)-unit(8), + 0, + 0, + 0, + 15, + domain::binary-size(domain_length)-unit(8), + workstation::binary-size(workstation_length)-unit(8) + >> + end + + def authenticate() do + end + + defp type1_flags(workstation?) do + 0x00000000 + |> Bitwise.bor(@ntlm_NegotiateUnicode) + |> Bitwise.bor(@ntlm_NegotiateOEM) + |> Bitwise.bor(@ntlm_RequestTarget) + |> Bitwise.bor(@ntlm_Unknown9) + |> Bitwise.bor(@ntlm_NegotiateSign) + |> Bitwise.bor(@ntlm_NegotiateSeal) + |> Bitwise.bor(@ntlm_NegotiateDatagram) + |> Bitwise.bor(@ntlm_NegotiateLanManagerKey) + |> Bitwise.bor(@ntlm_Unknown8) + |> Bitwise.bor(@ntlm_NegotiateNTLM) + |> Bitwise.bor(@ntlm_NegotiateNTOnly) + |> Bitwise.bor(@ntlm_Anonymous) + |> Bitwise.bor(@ntlm_NegotiateOemDomainSupplied) + |> Bitwise.bor( + if(workstation?, + do: @ntlm_NegotiateOemWorkstationSupplied, + else: 0x00000000 + ) + ) + |> Bitwise.bor(@ntlm_Unknown6) + |> Bitwise.bor(@ntlm_NegotiateAlwaysSign) + |> Bitwise.bor(@ntlm_TargetTypeDomain) + |> Bitwise.bor(@ntlm_TargetTypeServer) + |> Bitwise.bor(@ntlm_TargetTypeShare) + |> Bitwise.bor(@ntlm_NegotiateExtendedSecurity) + |> Bitwise.bor(@ntlm_NegotiateIdentify) + |> Bitwise.bor(@ntlm_Unknown5) + |> Bitwise.bor(@ntlm_RequestNonNTSessionKey) + |> Bitwise.bor(@ntlm_NegotiateTargetInfo) + |> Bitwise.bor(@ntlm_Unknown4) + |> Bitwise.bor(@ntlm_NegotiateVersion) + |> Bitwise.bor(@ntlm_Unknown3) + |> Bitwise.bor(@ntlm_Unknown2) + |> Bitwise.bor(@ntlm_Unknown1) + |> Bitwise.bor(@ntlm_Negotiate128) + |> Bitwise.bor(@ntlm_NegotiateKeyExchange) + |> Bitwise.bor(@ntlm_Negotiate56) + end +end diff --git a/lib/tds/protocol.ex b/lib/tds/protocol.ex index b3f46cd..1f036c4 100644 --- a/lib/tds/protocol.ex +++ b/lib/tds/protocol.ex @@ -141,7 +141,7 @@ defmodule Tds.Protocol do {:disconnect, err, s} end - def checkout(%{sock: {mod, sock}} = s) do + def checkout(%{sock: {mod, _sock}} = s) do sock_mod = inspect(mod) case setopts(s.sock, active: false) do @@ -163,7 +163,7 @@ defmodule Tds.Protocol do {:disconnect, err, s} end - def checkin(%{sock: {mod, sock}} = s) do + def checkin(%{sock: {mod, _sock}} = s) do sock_mod = inspect(mod) case setopts(s.sock, active: :once) do From b964e0dc54524d98a0a9ab27b7eb31ab3fe86a43 Mon Sep 17 00:00:00 2001 From: Milan Jaric Date: Sat, 19 Dec 2020 22:26:02 +0100 Subject: [PATCH 13/17] ntlm module negotiate and authenticate --- lib/ntlm.ex | 167 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 152 insertions(+), 15 deletions(-) diff --git a/lib/ntlm.ex b/lib/ntlm.ex index 5bad459..d7615c7 100644 --- a/lib/ntlm.ex +++ b/lib/ntlm.ex @@ -38,6 +38,12 @@ defmodule Ntlm do @ntlm_NegotiateKeyExchange 0x40000000 @ntlm_Negotiate56 0x80000000 + @type domain :: String.t() + @type username :: String.t() + @type password :: String.t() + @type negotiation_option :: {:domain, domain()} | {:workstation, String.t()} + @type negotiation_options :: [negotiation_option()] + @doc """ Builds NTLM negotiation message `<<"NTLMSSP", 0x00, 0x01 ...>>` @@ -45,14 +51,21 @@ defmodule Ntlm do optinal `:workstation` string. Both values can only contain valid ASCII characters """ - @spec negotiate(keyword) :: <<_::64, _::_*8>> - def negotiate(opts \\ []) do + @spec negotiate(negotiation_options) :: binary() + def negotiate(negotiation_options) do fixed_data_len = 40 - domain = :unicode.characters_to_binary(opts[:domain], :unicode, :latin1) - domain_length = String.length(opts[:domain]) + + domain = + :unicode.characters_to_binary( + negotiation_options[:domain], + :unicode, + :latin1 + ) + + domain_length = String.length(negotiation_options[:domain]) workstation = - opts + negotiation_options |> Keyword.get(:workstation) |> Kernel.||("") @@ -64,17 +77,17 @@ defmodule Ntlm do << "NTLMSSP", 0x00, - 0x01::little-unsigned-size(4)-unit(8), - type1_flags::little-unsigned-size(4)-unit(8), - domain_length::little-unsigned-size(2)-unit(8), - domain_length::little-unsigned-size(2)-unit(8), - fixed_data_len + workstation_length::little-unsigned-size(4)-unit(8), - workstation_length::little-unsigned-size(2)-unit(8), - workstation_length::little-unsigned-size(2)-unit(8), - fixed_data_len::little-unsigned-size(4)-unit(8), + 0x01::little-unsigned-32, + type1_flags::little-unsigned-32, + domain_length::little-unsigned-16, + domain_length::little-unsigned-16, + fixed_data_len + workstation_length::little-unsigned-32, + workstation_length::little-unsigned-16, + workstation_length::little-unsigned-16, + fixed_data_len::little-unsigned-32, 5, 0, - 2195::little-unsigned-size(2)-unit(8), + 2195::little-unsigned-16, 0, 0, 0, @@ -84,7 +97,100 @@ defmodule Ntlm do >> end - def authenticate() do + @spec authenticate(domain(), username(), password(), binary(), binary()) :: + binary() + def authenticate(domain, username, password, server_data, server_nonce) do + domain = ucs2(domain) + domain_len = byte_size(domain) + username = ucs2(username) + username_len = byte_size(username) + lmv2_len = 24 + ntlmv2_len = 16 + base_idx = 64 + dn_idx = base_idx + un_idx = dn_idx + domain_len + l2_idx = un_idx + username_len * 2 + nt_idx = l2_idx + lmv2_len + client_nonce = client_nonce() + + {:ok, gen_time} = + NaiveDateTime.utc_now() + |> DateTime.from_naive("Etc/UTC") + + gen_time = DateTime.to_unix(gen_time) + + fixed = + <<"NTLMSSP", 0, 0x03::little-unsigned-32, lmv2_len::little-unsigned-16, + l2_idx::little-unsigned-32, ntlmv2_len::little-unsigned-16, + ntlmv2_len::little-unsigned-16, nt_idx::little-unsigned-32, + domain_len::little-unsigned-16, domain_len::little-unsigned-16, + dn_idx::little-unsigned-32, username_len::little-unsigned-16, + username_len::little-unsigned-16, un_idx::little-unsigned-32, + 0x00::little-unsigned-16, 0x00::little-unsigned-16, + base_idx::little-unsigned-32, 0x00::little-unsigned-16, + 0x00::little-unsigned-16, base_idx::little-unsigned-32, + 0x8201::little-unsigned-16, 0x00::little-unsigned-16>> + + [ + fixed, + domain, + username, + lvm2_response(domain, username, password, server_nonce, client_nonce), + ntlmv2_response( + domain, + username, + password, + server_nonce, + server_data, + client_nonce, + gen_time + ), + [0x01, 0x01, 0x00, 0x00], + as_timestamp(gen_time), + client_nonce, + [0x00, 0x00], + server_data, + [0x00, 0x00] + ] + |> IO.iodata_to_binary() + end + + defp lvm2_response(domain, username, password, server_nonce, client_nonce) do + hash = ntv2_hash(domain, username, password) + data = server_nonce <> client_nonce + new_hash = hmac_md5(data, hash) + [new_hash, client_nonce] + end + + defp ntlmv2_response( + domain, + username, + password, + server_nonce, + server_data, + client_nonce, + gen_time + ) do + timestamp = as_timestamp(gen_time) + hash = ntv2_hash(domain, username, password) + + data = << + server_nonce::binary-64, + 0x0101::little-unsigned-32, + 0x0000::little-unsigned-32, + timestamp::binary-64, + client_nonce::binary-64, + 0x0000::unsigned-32, + server_data::binary + >> + + hmac_md5(data, hash) + end + + defp client_nonce() do + 1..8 + |> Enum.map(fn _ -> :rand.uniform(255) end) + |> IO.iodata_to_binary() end defp type1_flags(workstation?) do @@ -127,4 +233,35 @@ defmodule Ntlm do |> Bitwise.bor(@ntlm_NegotiateKeyExchange) |> Bitwise.bor(@ntlm_Negotiate56) end + + defp as_timestamp(unix) do + tenth_of_usec = (unix + 11_644_473_600) * 10_000_000 + lo = Bitwise.band(tenth_of_usec, 0xFFFFFFFF) + + hi = + tenth_of_usec + |> Bitwise.>>>(32) + |> Bitwise.band(0xFFFFFFFF) + + <> + end + + defp ntv2_hash(domain, user, password) do + hash = nt_hash(password) + identity = ucs2(String.upcase(user) <> String.upcase(domain)) + hmac_md5(identity, hash) + end + + defp nt_hash(text) do + text = ucs2(text) + :crypto.hash(:md4, text) + end + + defp hmac_md5(data, key) do + :crypto.hmac(:md5, key, data) + end + + defp ucs2(str) do + :unicode.characters_to_binary(str, :unicode, {:utf16, :little}) + end end From 59dc98ff0d006bbd870c1075d085192ef9d52e4b Mon Sep 17 00:00:00 2001 From: Milan Jaric Date: Sat, 19 Dec 2020 22:41:29 +0100 Subject: [PATCH 14/17] fix --- lib/ntlm.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/ntlm.ex b/lib/ntlm.ex index d7615c7..78ec70e 100644 --- a/lib/ntlm.ex +++ b/lib/ntlm.ex @@ -113,11 +113,10 @@ defmodule Ntlm do nt_idx = l2_idx + lmv2_len client_nonce = client_nonce() - {:ok, gen_time} = + gen_time = NaiveDateTime.utc_now() - |> DateTime.from_naive("Etc/UTC") - - gen_time = DateTime.to_unix(gen_time) + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() fixed = <<"NTLMSSP", 0, 0x03::little-unsigned-32, lmv2_len::little-unsigned-16, @@ -173,15 +172,16 @@ defmodule Ntlm do ) do timestamp = as_timestamp(gen_time) hash = ntv2_hash(domain, username, password) - + target_info_len = byte_size(server_data) data = << - server_nonce::binary-64, + server_nonce::binary-size(8)-unit(8), 0x0101::little-unsigned-32, 0x0000::little-unsigned-32, - timestamp::binary-64, - client_nonce::binary-64, + timestamp::binary-size(8)-unit(8), + client_nonce::binary-size(8)-unit(8), 0x0000::unsigned-32, - server_data::binary + server_data::binary-size(target_info_len)-unit(8), + 0x0000::little-unsigned-32 >> hmac_md5(data, hash) From 421d419ef28215903c66f773071c7ed62137494d Mon Sep 17 00:00:00 2001 From: Milan Jaric Date: Sun, 21 Feb 2021 03:19:49 +0100 Subject: [PATCH 15/17] fixing #119. Azure tsl issue --- lib/tds/tls.ex | 98 +++++++++++++++++++++++++++++++++++--------- test/test_helper.exs | 56 ++++++++++++------------- 2 files changed, 107 insertions(+), 47 deletions(-) diff --git a/lib/tds/tls.ex b/lib/tds/tls.ex index b361eeb..43d78ed 100644 --- a/lib/tds/tls.ex +++ b/lib/tds/tls.ex @@ -3,19 +3,24 @@ defmodule Tds.Tls do require Logger use GenServer import Kernel, except: [send: 2] + import Tds.BinaryUtils - defstruct [:socket, :ssl_opts, :owner_pid, :handshake] + defstruct [:socket, :ssl_opts, :owner_pid, :handshake, :buffer] def connect(socket, ssl_opts) do - # Logger.debug("starting TLS upgrade") - ssl_opts = ssl_opts ++ [ - active: false, - cb_info: {Tds.Tls, :tcp, :tcp_closed, :tcp_error} - ] + ssl_opts = + ssl_opts ++ + [ + active: false, + cb_info: {Tds.Tls, :tcp, :tcp_closed, :tcp_error} + ] + :inet.setopts(socket, active: false) + with {:ok, pid} <- GenServer.start_link(__MODULE__, {socket, ssl_opts}, []), :ok <- :gen_tcp.controlling_process(socket, pid) do res = :ssl.connect(socket, ssl_opts, :infinity) + # todo: remove this line and handle it when server respond with 0x12 message with status 0x01 GenServer.cast(pid, :handshake_complete) res else @@ -79,7 +84,11 @@ defmodule Tds.Tls do {:reply, :ok, %{s | owner_pid: tls_conn_pid}} end - def handle_call({:setopts, options}, _from, %{socket: socket, handshake: hs} = s) do + def handle_call( + {:setopts, options}, + _from, + %{socket: socket, handshake: hs} = s + ) do tds_header_size = if hs == true, do: 8, else: 0 opts = @@ -94,6 +103,7 @@ defmodule Tds.Tls do def handle_call({:send, data}, _from, %{socket: socket, handshake: true} = s) do size = IO.iodata_length(data) + 8 + header = <<0x12, 0x01, size::unsigned-size(2)-unit(8), 0x00, 0x00, 0x00, 0x00>> @@ -101,7 +111,7 @@ defmodule Tds.Tls do {:reply, resp, s} end - def handle_call({:send, data}, _from, %{socket: socket, handshake: false} = s) do + def handle_call({:send, data}, _from, %{socket: socket, handshake: false} = s) do resp = :gen_tcp.send(socket, data) {:reply, resp, s} end @@ -119,25 +129,75 @@ defmodule Tds.Tls do end def handle_cast(:handshake_complete, s) do - {:noreply, %{s | handshake: false} } + {:noreply, %{s | handshake: false}} end def handle_info( - {:tcp, _, - <<0x12, 0x01, _::unsigned-16, _::32, ssl_payload::binary>>}, - %{socket: socket, owner_pid: pid} = s + {:tcp, _, _} = msg, + %{owner_pid: pid, handshake: false, buffer: nil} = s ) do - # Logger.debug( - # "received #{size} bytes from server in prelogin message as SSL_PAYLOAD" - # ) - - Kernel.send(pid, {:tcp, socket, ssl_payload}) + Kernel.send(pid, msg) {:noreply, s} end - def handle_info({:tcp, _, _} = msg, %{owner_pid: pid} = s) do - # Logger.debug("received SSL_PAYLOAD from server, NON PreLogin message") + def handle_info( + {:tcp, port, <<0x12, 0, size::unsigned-16, _::32, tail::binary>>}, + %{socket: socket, owner_pid: pid, buffer: nil, handshake: true} = s + ) do + expecting = size - 8 + + case tail do + <> -> + Kernel.send(pid, {:tcp, socket, ssl_payload}) + handle_info({:tcp, port, next_packet}, %{s | buffer: nil}) + + next_slice -> + state = %{s | buffer: {next_slice, expecting}} + {:noreply, state} + end + end + + def handle_info( + {:tcp, port, <<0x12, 1, size::unsigned-16, _::32, tail::binary>>}, + %{socket: socket, owner_pid: pid, buffer: nil, handshake: true} = s + ) do + expecting = size - 8 + case tail do + <> -> + Kernel.send(pid, {:tcp, socket, ssl_payload}) + handle_info({:tcp, port, next_packet}, %{s | buffer: nil}) + + next_slice -> + state = %{s | buffer: {next_slice, expecting}} + {:noreply, state} + end + end + + def handle_info( + {:tcp, port, bin}, + %{ + socket: socket, + owner_pid: pid, + buffer: {slice, expecting}, + handshake: true + } = s + ) do + case IO.iodata_to_binary([slice, bin]) do + <> -> + Kernel.send(pid, {:tcp, socket, ssl_payload}) + handle_info({:tcp, port, next_packet}, %{s | buffer: nil}) + + next_slice -> + state = %{s | buffer: {next_slice, expecting}} + {:noreply, state} + end + end + + def handle_info( + {:tcp, _, _} = msg, + %{owner_pid: pid, handshake: true, buffer: nil} = s + ) do Kernel.send(pid, msg) {:noreply, s} end diff --git a/test/test_helper.exs b/test/test_helper.exs index 9962b8a..8851893 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -89,34 +89,34 @@ end opts = Application.get_env(:tds, :opts) database = opts[:database] - -{"", 0} = - Tds.TestHelper.sqlcmd(opts, """ - IF EXISTS(SELECT * FROM sys.databases where name = '#{database}') - BEGIN - DROP DATABASE [#{database}]; - END; - CREATE DATABASE [#{database}]; - """) - -{"Changed database context to 'test'." <> _, 0} = - Tds.TestHelper.sqlcmd(opts, """ - USE [test]; - - CREATE TABLE altering ([a] int) - - CREATE TABLE [composite1] ([a] int, [b] text); - CREATE TABLE [composite2] ([a] int, [b] int, [c] int); - CREATE TABLE [uniques] ([id] int NOT NULL, CONSTRAINT UIX_uniques_id UNIQUE([id])) - """) - -{"Changed database context to 'test'." <> _, 0} = - Tds.TestHelper.sqlcmd(opts, """ - USE test - GO - CREATE SCHEMA test; - """) - +if System.get_env("TEST_AZURE") == nil do + {"", 0} = + Tds.TestHelper.sqlcmd(opts, """ + IF EXISTS(SELECT * FROM sys.databases where name = '#{database}') + BEGIN + DROP DATABASE [#{database}]; + END; + CREATE DATABASE [#{database}]; + """) + + {"Changed database context to 'test'." <> _, 0} = + Tds.TestHelper.sqlcmd(opts, """ + USE [test]; + + CREATE TABLE altering ([a] int) + + CREATE TABLE [composite1] ([a] int, [b] text); + CREATE TABLE [composite2] ([a] int, [b] int, [c] int); + CREATE TABLE [uniques] ([id] int NOT NULL, CONSTRAINT UIX_uniques_id UNIQUE([id])) + """) + + {"Changed database context to 'test'." <> _, 0} = + Tds.TestHelper.sqlcmd(opts, """ + USE test + GO + CREATE SCHEMA test; + """) +end # :dbg.start() # :dbg.tracer() # :dbg.p(:all,:c) From 571f2b180d68750a3f73e6b362831043ba0f2007 Mon Sep 17 00:00:00 2001 From: Milan Jaric Date: Sun, 21 Feb 2021 10:19:00 +0100 Subject: [PATCH 16/17] switching to official github action for elixir setup --- .github/workflows/elixir.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 6016e2a..61a3fcb 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -43,7 +43,7 @@ jobs: sudo apt-get install -y mssql-tools unixodbc-dev - uses: actions/checkout@v2 - name: Setup elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-elixir@v1 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} From c8d90700d2a52dc4e84426f6c948985de0a36664 Mon Sep 17 00:00:00 2001 From: Milan Jaric Date: Sun, 21 Feb 2021 10:33:55 +0100 Subject: [PATCH 17/17] fixing sporadically failed logint test case --- config/test.exs | 1 + test/login_test.exs | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/test.exs b/config/test.exs index ede1271..463a563 100644 --- a/config/test.exs +++ b/config/test.exs @@ -12,4 +12,5 @@ config :tds, database: "test", trace: false, set_allow_snapshot_isolation: :on + # show_sensitive_data_on_connection_error: true ] diff --git a/test/login_test.exs b/test/login_test.exs index d154121..3b4e2e0 100644 --- a/test/login_test.exs +++ b/test/login_test.exs @@ -36,16 +36,19 @@ defmodule LoginTest do end @tag :login + @tag :tls test "login with valid sql login over tsl", context do opts = Application.fetch_env!(:tds, :opts) ++ - [ssl: true, ssl_opts: [log_debug: true]] + [ssl: true, ssl_opts: []] + # [ssl: true, ssl_opts: [log_debug: true, log_level: :debug]] assert {:ok, pid} = Tds.start_link(opts ++ context[:options]) assert {:ok, %Tds.Result{}} = Tds.query(pid, "SELECT 1", []) end @tag :login + @tag :tls test "login with non existing sql server authentication over tls", context do assert capture_log(fn -> opts = @@ -62,7 +65,7 @@ defmodule LoginTest do Process.flag(:trap_exit, true) case Tds.start_link(opts) do - {:ok, pid} -> assert_receive {:EXIT, ^pid, :killed} + {:ok, pid} -> assert_receive {:EXIT, ^pid, :killed}, 1_000 {:error, :killed} -> :ok end end