Commits

Will Glozer committed 48a40f0

implement support for SSL session encryption

  • Participants
  • Parent commits fcf8a3b

Comments (0)

Files changed (7)

 NAME		:= epgsql
-VERSION		:= 1.0
+VERSION		:= 1.1
 
 ERL  		:= erl
 ERLC 		:= erlc
 
   - database
   - port
+  - ssl (true | false | required)
 
   ok = pgsql:close(C).
 

File src/epgsql.app

 {application, epgsql,
  [{description, "PostgreSQL Client"},
-  {vsn, "1.0"},
-  {modules, [pgsql, pgsql_binary, pgsql_connection, pgsql_datetime, pgsql_types]},
+  {vsn, "1.1"},
+  {modules, [pgsql, pgsql_binary, pgsql_connection, pgsql_fdatetime,
+             pgsql_idatetime, pgsql_sock, pgsql_types]},
   {registered, []},
-  {applications, [kernel, stdlib]},
+  {applications, [kernel, stdlib, crypto, ssl]},
   {included_applications, []}]}.

File src/pgsql_connection.erl

 
 -export([init/1, handle_event/3, handle_sync_event/4]).
 -export([handle_info/3, terminate/3, code_change/4]).
--export([read/3]).
 
 -export([startup/3, auth/2, initializing/2, ready/2, ready/3]).
 -export([querying/2, parsing/2, binding/2, describing/2]).
 handle_sync_event(Event, _From, _State_Name, State) ->
     {stop, {unsupported_sync_event, Event}, State}.
 
-handle_info({'EXIT', Pid, Reason}, _State_Name, State = #state{reader = Pid}) ->
+handle_info({'EXIT', Pid, Reason}, _State_Name, State = #state{sock = Pid}) ->
     {stop, Reason, State};
 
 handle_info(Info, _State_Name, State) ->
 
 terminate(_Reason, _State_Name, State = #state{sock = Sock})
   when Sock =/= undefined ->
-    send(State, $X, []),
-    gen_tcp:close(Sock);
+    send(State, $X, []);
 
 terminate(_Reason, _State_Name, _State) ->
     ok.
 %% -- states --
 
 startup({connect, Host, Username, Password, Opts}, From, State) ->
-    Port      = proplists:get_value(port, Opts, 5432),
-    Sock_Opts = [{active, false}, {packet, raw}, binary],
-    case gen_tcp:connect(Host, Port, Sock_Opts) of
+    case pgsql_sock:start_link(self(), Host, Username, Opts) of
         {ok, Sock} ->
-            Reader = spawn_link(?MODULE, read, [self(), Sock, <<>>]),
-
-            Opts2 = ["user", 0, Username, 0],
-            case proplists:get_value(database, Opts, undefined) of
-                undefined -> Opts3 = Opts2;
-                Database  -> Opts3 = [Opts2 | ["database", 0, Database, 0]]
-            end,
-
             put(username, Username),
             put(password, Password),
-            State2 = State#state{reader   = Reader,
-                                 sock     = Sock,
-                                 reply_to = From},
-            send(State2, [<<196608:32>>, Opts3, 0]),
-
+            State2 = State#state{sock = Sock, reply_to = From},
             {next_state, auth, State2};
         Error ->
             {stop, normal, Error, State}
     {stop, normal, State};
 
 %% ErrorResponse
-auth({$E, Bin}, State) ->
-    Error = decode_error(Bin),
-    case Error#error.code of
+auth({error, E}, State) ->
+    case E#error.code of
         <<"28000">> -> Why = invalid_authorization_specification;
         Any         -> Why = Any
     end,
     {next_state, initializing, State2};
 
 %% ErrorResponse
-initializing({$E, Bin}, State) ->
-    Error = decode_error(Bin),
-    case Error#error.code of
+initializing({error, E}, State) ->
+    case E#error.code of
         <<"28000">> -> Why = invalid_authorization_specification;
         Any         -> Why = Any
     end,
     {next_state, querying, State};
 
 %% ErrorResponse
-querying({$E, Bin}, State) ->
-    Error = decode_error(Bin),
-    notify(State, {error, Error}),
+querying({error, E}, State) ->
+    notify(State, {error, E}),
     {next_state, querying, State};
 
 %% ReadyForQuery
     {next_state, describing, State};
 
 %% ErrorResponse
-parsing({$E, Bin}, State) ->
-    Reply = {error, decode_error(Bin)},
+parsing({error, E}, State) ->
+    Reply = {error, E},
     send(State, $S, []),
     {next_state, parsing, State#state{reply = Reply}};
 
     {next_state, ready, State};
 
 %% ErrorResponse
-binding({$E, Bin}, State) ->
-    Reply = {error, decode_error(Bin)},
+binding({error, E}, State) ->
+    Reply = {error, E},
     send(State, $S, []),
     {next_state, binding, State#state{reply = Reply}};
 
     {next_state, ready, State};
 
 %% ErrorResponse
-describing({$E, Bin}, State) ->
-    Reply = {error, decode_error(Bin)},
+describing({error, E}, State) ->
+    Reply = {error, E},
     send(State, $S, []),
     {next_state, describing, State#state{reply = Reply}};
 
     {next_state, ready, State};
 
 %% ErrorResponse
-executing({$E, Bin}, State) ->
-    notify(State, {error, decode_error(Bin)}),
+executing({error, E}, State) ->
+    notify(State, {error, E}),
     {next_state, executing, State}.
 
 %% CloseComplete
     {next_state, ready, State};
 
 %% ErrorResponse
-closing({$E, Bin}, State) ->
-    Error = {error, decode_error(Bin)},
+closing({error, E}, State) ->
+    Error = {error, E},
     gen_fsm:reply(State#state.reply_to, Error),
     {next_state, ready, State}.
 
 %% ErrorResponse
-synchronizing({$E, Bin}, State) ->
-    Reply = {error, decode_error(Bin)},
+synchronizing({error, E}, State) ->
+    Reply = {error, E},
     {next_state, synchronizing, State#state{reply = Reply}};
 
 %% ReadyForQuery
 
 %% -- internal functions --
 
-%% decode a single null-terminated string
-decode_string(Bin) ->
-    decode_string(Bin, <<>>).
-
-decode_string(<<0, Rest/binary>>, Str) ->
-    {Str, Rest};
-decode_string(<<C, Rest/binary>>, Str) ->
-    decode_string(Rest, <<Str/binary, C>>).
-
-%% decode multiple null-terminated string
-decode_strings(Bin) ->
-    decode_strings(Bin, []).
-
-decode_strings(<<>>, Acc) ->
-    lists:reverse(Acc);
-decode_strings(Bin, Acc) ->
-    {Str, Rest} = decode_string(Bin),
-    decode_strings(Rest, [Str | Acc]).
-
-%% decode field
-decode_fields(Bin) ->
-    decode_fields(Bin, []).
-
-decode_fields(<<0>>, Acc) ->
-    Acc;
-decode_fields(<<Type:8, Rest/binary>>, Acc) ->
-    {Str, Rest2} = decode_string(Rest),
-    decode_fields(Rest2, [{Type, Str} | Acc]).
-
 %% decode data
 decode_data(Columns, Bin) ->
     decode_data(Columns, Bin, []).
 decode_columns(0, _Bin, Acc) ->
     lists:reverse(Acc);
 decode_columns(N, Bin, Acc) ->
-    {Name, Rest} = decode_string(Bin),
+    {Name, Rest} = pgsql_sock:decode_string(Bin),
     <<_Table_Oid:?int32, _Attrib_Num:?int16, Type_Oid:?int32,
      Size:?int16, Modifier:?int32, Format:?int16, Rest2/binary>> = Rest,
     Desc = #column{
 decode_complete(<<"BEGIN", 0>>)    -> 'begin';
 decode_complete(<<"ROLLBACK", 0>>) -> rollback;
 decode_complete(Bin) ->
-    {Str, _} = decode_string(Bin),
+    {Str, _} = pgsql_sock:decode_string(Bin),
     case string:tokens(binary_to_list(Str), " ") of
         ["INSERT", _Oid, Rows] -> {insert, list_to_integer(Rows)};
         ["UPDATE", Rows]       -> {update, list_to_integer(Rows)};
         ["DELETE", Rows]       -> {delete, list_to_integer(Rows)};
         ["MOVE", Rows]         -> {move, list_to_integer(Rows)};
         ["FETCH", Rows]        -> {fetch, list_to_integer(Rows)};
-        [Type | _Rest]         -> lower_atom(Type)
-    end.
-
-%% decode ErrorResponse
-decode_error(Bin) ->
-    Fields = decode_fields(Bin),
-    Error = #error{
-      severity = lower_atom(proplists:get_value($S, Fields)),
-      code     = proplists:get_value($C, Fields),
-      message  = proplists:get_value($M, Fields),
-      extra    = decode_error_extra(Fields)},
-    Error.
-
-decode_error_extra(Fields) ->
-    Types = [{$D, detail}, {$H, hint}, {$P, position}],
-    decode_error_extra(Types, Fields, []).
-
-decode_error_extra([], _Fields, Extra) ->
-    Extra;
-decode_error_extra([{Type, Name} | T], Fields, Extra) ->
-    case proplists:get_value(Type, Fields) of
-        undefined -> decode_error_extra(T, Fields, Extra);
-        Value     -> decode_error_extra(T, Fields, [{Name, Value} | Extra])
+        [Type | _Rest]         -> pgsql_sock:lower_atom(Type)
     end.
 
 %% encode types
 notify(#state{reply_to = {Pid, _Tag}}, Msg) ->
     Pid ! {pgsql, self(), Msg}.
 
-lower_atom(Str) when is_binary(Str) ->
-    lower_atom(binary_to_list(Str));
-lower_atom(Str) when is_list(Str) ->
-    list_to_atom(string:to_lower(Str)).
-
 to_binary(B) when is_binary(B) -> B;
 to_binary(L) when is_list(L)   -> list_to_binary(L).
 
 %% send data to server
 
 send(#state{sock = Sock}, Type, Data) ->
-    Bin = iolist_to_binary(Data),
-    gen_tcp:send(Sock, <<Type:8, (byte_size(Bin) + 4):?int32, Bin/binary>>).
-
-send(#state{sock = Sock}, Data) ->
-    Bin = iolist_to_binary(Data),
-    gen_tcp:send(Sock, <<(byte_size(Bin) + 4):?int32, Bin/binary>>).
-
-%% -- socket read loop --
-
-read(Fsm, Sock, Tail) ->
-    case gen_tcp:recv(Sock, 0) of
-        {ok, Bin} -> decode(Fsm, Sock, <<Tail/binary, Bin/binary>>);
-        Error     -> exit(Error)
-    end.
-
-decode(Fsm, Sock, <<Type:8, Len:?int32, Rest/binary>> = Bin) ->
-    Len2 = Len - 4,
-    case Rest of
-        <<Data:Len2/binary, Tail/binary>> when Type == $N ->
-            gen_fsm:send_all_state_event(Fsm, {notice, decode_error(Data)}),
-            decode(Fsm, Sock, Tail);
-        <<Data:Len2/binary, Tail/binary>> when Type == $S ->
-            [Name, Value] = decode_strings(Data),
-            gen_fsm:send_all_state_event(Fsm, {parameter_status, Name, Value}),
-            decode(Fsm, Sock, Tail);
-        <<Data:Len2/binary, Tail/binary>> ->
-            gen_fsm:send_event(Fsm, {Type, Data}),
-            decode(Fsm, Sock, Tail);
-        _Other ->
-            ?MODULE:read(Fsm, Sock, Bin)
-    end;
-decode(Fsm, Sock, Bin) ->
-    ?MODULE:read(Fsm, Sock, Bin).
+    pgsql_sock:send(Sock, Type, Data).

File src/pgsql_sock.erl

+%%% Copyright (C) 2009 - Will Glozer.  All rights reserved.
+
+-module(pgsql_sock).
+
+-behavior(gen_server).
+
+-export([start_link/4, send/2, send/3]).
+-export([decode_string/1, lower_atom/1]).
+
+-export([handle_call/3, handle_cast/2, handle_info/2]).
+-export([init/1, code_change/3, terminate/2]).
+
+-include("pgsql.hrl").
+
+-record(state, {c, mod, sock, tail}).
+
+-define(int16, 1/big-signed-unit:16).
+-define(int32, 1/big-signed-unit:32).
+
+%% -- client interface --
+
+start_link(C, Host, Username, Opts) ->
+    gen_server:start_link(?MODULE, [C, Host, Username, Opts], []).
+
+send(S, Type, Data) ->
+    Bin = iolist_to_binary(Data),
+    Msg = <<Type:8, (byte_size(Bin) + 4):?int32, Bin/binary>>,
+    gen_server:cast(S, {send, Msg}).
+
+send(S, Data) ->
+    Bin = iolist_to_binary(Data),
+    Msg = <<(byte_size(Bin) + 4):?int32, Bin/binary>>,
+    gen_server:cast(S, {send, Msg}).
+
+%% -- gen_server implementation --
+
+init([C, Host, Username, Opts]) ->
+    process_flag(trap_exit, true),
+
+    Opts2 = ["user", 0, Username, 0],
+    case proplists:get_value(database, Opts, undefined) of
+        undefined -> Opts3 = Opts2;
+        Database  -> Opts3 = [Opts2 | ["database", 0, Database, 0]]
+    end,
+
+    Port = proplists:get_value(port, Opts, 5432),
+    SockOpts = [{active, false}, {packet, raw}, binary],
+    {ok, S} = gen_tcp:connect(Host, Port, SockOpts),
+
+    State = #state{
+      c    = C,
+      mod  = gen_tcp,
+      sock = S,
+      tail = <<>>},
+
+    case proplists:get_value(ssl, Opts) of
+        T when T == true; T == required ->
+            ok = gen_tcp:send(S, <<8:?int32, 80877103:?int32>>),
+            {ok, <<Code>>} = gen_tcp:recv(S, 1),
+            State2 = start_ssl(Code, T, Opts, State);
+        _ ->
+            State2 = State
+    end,
+
+    setopts(State2, [{active, true}]),
+    send(self(), [<<196608:32>>, Opts3, 0]),
+    {ok, State2}.
+
+handle_call(Call, _From, State) ->
+    {stop, {unsupported_call, Call}, State}.
+
+handle_cast({send, Data}, State) ->
+    #state{mod = Mod, sock = Sock} = State,
+    ok = Mod:send(Sock, Data),
+    {noreply, State};
+
+handle_cast(Cast, State) ->
+    {stop, {unsupported_cast, Cast}, State}.
+
+handle_info({_, _Sock, Data}, #state{tail = Tail} = State) ->
+    State2 = decode(<<Tail/binary, Data/binary>>, State),
+    {noreply, State2};
+
+handle_info({Closed, _Sock}, State)
+  when Closed == tcp_closed; Closed == ssl_closed ->
+    {stop, sock_closed, State};
+
+handle_info({Error, _Sock, Reason}, State)
+  when Error == tcp_error; Error == ssl_error ->
+    {stop, {sock_error, Reason}, State};
+
+handle_info({'EXIT', _Pid, Reason}, State) ->
+    {stop, Reason, State};
+
+handle_info(Info, State) ->
+    {stop, {unsupported_info, Info}, State}.
+
+terminate(_Reason, _State) ->
+    ok.
+
+code_change(_OldVsn, State, _Extra) ->
+    {ok, State}.
+
+%% -- internal functions --
+
+start_ssl($S, _Flag, Opts, State) ->
+    #state{sock = S1} = State,
+    case ssl:connect(S1, Opts) of
+        {ok, S2}        -> State#state{mod = ssl, sock = S2};
+        {error, Reason} -> exit({ssl_negotiation_failed, Reason})
+    end;
+
+start_ssl($N, Flag, _Opts, State) ->
+    case Flag of
+        true     -> State;
+        required -> exit(ssl_not_available)
+    end.
+
+setopts(#state{mod = Mod, sock = Sock}, Opts) ->
+    case Mod of
+        gen_tcp -> inet:setopts(Sock, Opts);
+        ssl     -> ssl:setopts(Sock, Opts)
+    end.
+
+decode(<<Type:8, Len:?int32, Rest/binary>> = Bin, #state{c = C} = State) ->
+    Len2 = Len - 4,
+    case Rest of
+        <<Data:Len2/binary, Tail/binary>> when Type == $N ->
+            gen_fsm:send_all_state_event(C, {notice, decode_error(Data)}),
+            decode(Tail, State);
+        <<Data:Len2/binary, Tail/binary>> when Type == $S ->
+            [Name, Value] = decode_strings(Data),
+            gen_fsm:send_all_state_event(C, {parameter_status, Name, Value}),
+            decode(Tail, State);
+        <<Data:Len2/binary, Tail/binary>> when Type == $E ->
+            gen_fsm:send_event(C, {error, decode_error(Data)}),
+            decode(Tail, State);
+        <<Data:Len2/binary, Tail/binary>> ->
+            gen_fsm:send_event(C, {Type, Data}),
+            decode(Tail, State);
+        _Other ->
+            State#state{tail = Bin}
+    end;
+decode(Bin, State) ->
+    State#state{tail = Bin}.
+
+%% decode a single null-terminated string
+decode_string(Bin) ->
+    decode_string(Bin, <<>>).
+
+decode_string(<<0, Rest/binary>>, Str) ->
+    {Str, Rest};
+decode_string(<<C, Rest/binary>>, Str) ->
+    decode_string(Rest, <<Str/binary, C>>).
+
+%% decode multiple null-terminated string
+decode_strings(Bin) ->
+    decode_strings(Bin, []).
+
+decode_strings(<<>>, Acc) ->
+    lists:reverse(Acc);
+decode_strings(Bin, Acc) ->
+    {Str, Rest} = decode_string(Bin),
+    decode_strings(Rest, [Str | Acc]).
+
+%% decode field
+decode_fields(Bin) ->
+    decode_fields(Bin, []).
+
+decode_fields(<<0>>, Acc) ->
+    Acc;
+decode_fields(<<Type:8, Rest/binary>>, Acc) ->
+    {Str, Rest2} = decode_string(Rest),
+    decode_fields(Rest2, [{Type, Str} | Acc]).
+
+%% decode ErrorResponse
+decode_error(Bin) ->
+    Fields = decode_fields(Bin),
+    Error = #error{
+      severity = lower_atom(proplists:get_value($S, Fields)),
+      code     = proplists:get_value($C, Fields),
+      message  = proplists:get_value($M, Fields),
+      extra    = decode_error_extra(Fields)},
+    Error.
+
+decode_error_extra(Fields) ->
+    Types = [{$D, detail}, {$H, hint}, {$P, position}],
+    decode_error_extra(Types, Fields, []).
+
+decode_error_extra([], _Fields, Extra) ->
+    Extra;
+decode_error_extra([{Type, Name} | T], Fields, Extra) ->
+    case proplists:get_value(Type, Fields) of
+        undefined -> decode_error_extra(T, Fields, Extra);
+        Value     -> decode_error_extra(T, Fields, [{Name, Value} | Extra])
+    end.
+
+lower_atom(Str) when is_binary(Str) ->
+    lower_atom(binary_to_list(Str));
+lower_atom(Str) when is_list(Str) ->
+    list_to_atom(string:to_lower(Str)).

File test_src/pgsql_tests.erl

                       "epgsql_test_sha1",
                       [{port, ?port}, {database, "epgsql_test_db1"}]).
 
+connect_with_ssl_test() ->
+    lists:foreach(fun application:start/1, [crypto, ssl]),
+    with_connection(
+      fun(C) ->
+              {ok, _Cols, [{true}]} = pgsql:equery(C, "select ssl_is_used()")
+      end,
+      [{ssl, true}]).
+
 select_test() ->
     with_connection(
       fun(C) ->
     flush().
 
 with_connection(F) ->
-    Args = [{port, ?port}, {database, "epgsql_test_db1"}],
-    {ok, C} = pgsql:connect(?host, "epgsql_test", Args),
+    with_connection(F, []).
+
+with_connection(F, Args) ->
+    Args2 = [{port, ?port}, {database, "epgsql_test_db1"} | Args],
+    {ok, C} = pgsql:connect(?host, "epgsql_test", Args2),
     try
         F(C)
     after

File test_src/test_schema.sql

 --
 -- any 'trust all' must be commented out for the invalid password test
 -- to succeed.
+--
+-- ssl support must be configured, and the sslinfo contrib module
+-- loaded for the ssl tests to succeed.
 
 
 CREATE USER epgsql_test;