Browse Source

Refactor connect command API and password obfuscation

Sergey Prokhorov 6 years ago
parent
commit
2cf7fbe89a
5 changed files with 55 additions and 39 deletions
  1. 4 1
      README.md
  2. 11 7
      src/commands/epgsql_cmd_connect.erl
  3. 15 13
      src/epgsql.erl
  4. 13 9
      src/epgsqla.erl
  5. 12 9
      src/epgsqli.erl

+ 4 - 1
README.md

@@ -66,7 +66,7 @@ connect(Opts) -> {ok, Connection :: epgsql:connection()} | {error, Reason :: epg
   Opts ::
   Opts ::
     #{host :=     inet:ip_address() | inet:hostname(),
     #{host :=     inet:ip_address() | inet:hostname(),
       username := iodata(),
       username := iodata(),
-      password => iodata(),
+      password => iodata() | fun( () -> iodata() ),
       database => iodata(),
       database => iodata(),
       port =>     inet:port_number(),
       port =>     inet:port_number(),
       ssl =>      boolean() | required,
       ssl =>      boolean() | required,
@@ -91,6 +91,9 @@ ok = epgsql:close(C).
 
 
 Only `host` and `username` are mandatory, but most likely you would need `database` and `password`.
 Only `host` and `username` are mandatory, but most likely you would need `database` and `password`.
 
 
+- `password` - DB user password. It might be provided as string / binary or as a fun that returns
+   string / binary. Internally, plain password is wrapped to anonymous fun before it is sent to connection
+   process, so, if `connect` command crashes, plain password will not appear in crash logs.
 - `{timeout, TimeoutMs}` parameter will trigger an `{error, timeout}` result when the
 - `{timeout, TimeoutMs}` parameter will trigger an `{error, timeout}` result when the
    socket fails to connect within `TimeoutMs` milliseconds.
    socket fails to connect within `TimeoutMs` milliseconds.
 - `ssl` if set to `true`, perform an attempt to connect in ssl mode, but continue unencrypted
 - `ssl` if set to `true`, perform an attempt to connect in ssl mode, but continue unencrypted

+ 11 - 7
src/commands/epgsql_cmd_connect.erl

@@ -4,7 +4,7 @@
 %%%
 %%%
 -module(epgsql_cmd_connect).
 -module(epgsql_cmd_connect).
 -behaviour(epgsql_command).
 -behaviour(epgsql_command).
--export([hide_password/1]).
+-export([hide_password/1, opts_hide_password/1]).
 -export([init/1, execute/2, handle_message/4]).
 -export([init/1, execute/2, handle_message/4]).
 -export_type([response/0, connect_error/0]).
 -export_type([response/0, connect_error/0]).
 
 
@@ -42,12 +42,8 @@
 -define(AUTH_SASL_CONTINUE, 11).
 -define(AUTH_SASL_CONTINUE, 11).
 -define(AUTH_SASL_FINAL, 12).
 -define(AUTH_SASL_FINAL, 12).
 
 
-init({Host, Username, Password, Opts}) ->
-    Opts1 = maps:merge(Opts,
-                       #{host => Host,
-                         username => Username,
-                         password => Password}),
-    #connect{opts = Opts1}.
+init(#{host := _, username := _} = Opts) ->
+    #connect{opts = Opts}.
 
 
 execute(PgSock, #connect{opts = Opts, stage = connect} = State) ->
 execute(PgSock, #connect{opts = Opts, stage = connect} = State) ->
     #{host := Host,
     #{host := Host,
@@ -98,9 +94,17 @@ execute(PgSock, #connect{stage = auth, auth_send = {PacketId, Data}} = St) ->
     {ok, PgSock, St#connect{auth_send = undefined}}.
     {ok, PgSock, St#connect{auth_send = undefined}}.
 
 
 
 
+%% @doc Replace `password' in Opts map with obfuscated one
+opts_hide_password(#{password := Password} = Opts) ->
+    HiddenPassword = hide_password(Password),
+    Opts#{password => HiddenPassword};
+opts_hide_password(Opts) -> Opts.
+
+
 %% @doc this function wraps plaintext password to a lambda function, so, if
 %% @doc this function wraps plaintext password to a lambda function, so, if
 %% epgsql_sock process crashes when executing `connect` command, password will
 %% epgsql_sock process crashes when executing `connect` command, password will
 %% not appear in a crash log
 %% not appear in a crash log
+-spec hide_password(iodata()) -> fun( () -> iodata() ).
 hide_password(Password) when is_list(Password);
 hide_password(Password) when is_list(Password);
                              is_binary(Password) ->
                              is_binary(Password) ->
     fun() ->
     fun() ->

+ 15 - 13
src/epgsql.erl

@@ -61,7 +61,7 @@
         [connect_option()]
         [connect_option()]
       | #{host => host(),
       | #{host => host(),
           username => string(),
           username => string(),
-          password => string(),
+          password => iodata() | fun( () -> iodata() ),
           database => string(),
           database => string(),
           port => inet:port_number(),
           port => inet:port_number(),
           ssl => boolean() | required,
           ssl => boolean() | required,
@@ -124,12 +124,9 @@
 %% -- client interface --
 %% -- client interface --
 -spec connect(connect_opts())
 -spec connect(connect_opts())
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
-connect(Settings0) ->
-    Settings = to_map(Settings0),
-    Host = maps:get(host, Settings, "localhost"),
-    Username = maps:get(username, Settings, os:getenv("USER")),
-    Password = maps:get(password, Settings, ""),
-    connect(Host, Username, Password, Settings).
+connect(Opts) ->
+    {ok, C} = epgsql_sock:start_link(),
+    call_connect(C, Opts).
 
 
 connect(Host, Opts) ->
 connect(Host, Opts) ->
     connect(Host, os:getenv("USER"), "", Opts).
     connect(Host, os:getenv("USER"), "", Opts).
@@ -152,13 +149,17 @@ connect(Host, Username, Password, Opts) ->
 
 
 -spec connect(connection(), host(), string(), string(), connect_opts())
 -spec connect(connection(), host(), string(), string(), connect_opts())
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
         -> {ok, Connection :: connection()} | {error, Reason :: connect_error()}.
-connect(C, Host, Username, Password, Opts0) ->
-    Opts = to_map(Opts0),
-    Opts1 = maps:remove(password, Opts),
-    Password1 = epgsql_cmd_connect:hide_password(Password),
-    %% TODO connect timeout
+connect(C, Host, Username, Password, Opts) ->
+    Opts1 = maps:merge(epgsql:to_map(Opts),
+                       #{host => Host,
+                         username => Username,
+                         password => Password}),
+    call_connect(C, Opts1).
+
+call_connect(C, Opts) ->
+    Opts1 = epgsql_cmd_connect:opts_hide_password(Opts),
     case epgsql_sock:sync_command(
     case epgsql_sock:sync_command(
-           C, epgsql_cmd_connect, {Host, Username, Password1, Opts1}) of
+           C, epgsql_cmd_connect, Opts1) of
         connected ->
         connected ->
             %% If following call fails for you, try to add {codecs, []} connect option
             %% If following call fails for you, try to add {codecs, []} connect option
             {ok, _} = maybe_update_typecache(C, Opts),
             {ok, _} = maybe_update_typecache(C, Opts),
@@ -167,6 +168,7 @@ connect(C, Host, Username, Password, Opts0) ->
             Error
             Error
     end.
     end.
 
 
+
 maybe_update_typecache(C, Opts) ->
 maybe_update_typecache(C, Opts) ->
     maybe_update_typecache(C, maps:get(replication, Opts, undefined), maps:get(codecs, Opts, undefined)).
     maybe_update_typecache(C, maps:get(replication, Opts, undefined), maps:get(codecs, Opts, undefined)).
 
 

+ 13 - 9
src/epgsqla.erl

@@ -29,11 +29,8 @@ start_link() ->
     epgsql_sock:start_link().
     epgsql_sock:start_link().
 
 
 connect(Opts) ->
 connect(Opts) ->
-    Settings = epgsql:to_map(Opts),
-    Host = maps:get(host, Settings, "localhost"),
-    Username = maps:get(username, Settings, os:getenv("USER")),
-    Password = maps:get(password, Settings, ""),
-    connect(Host, Username, Password, Settings).
+    {ok, C} = epgsql_sock:start_link(),
+    call_connect(C, Opts).
 
 
 connect(Host, Opts) ->
 connect(Host, Opts) ->
     connect(Host, os:getenv("USER"), "", Opts).
     connect(Host, os:getenv("USER"), "", Opts).
@@ -48,11 +45,18 @@ connect(Host, Username, Password, Opts) ->
 -spec connect(epgsql:connection(), inet:ip_address() | inet:hostname(),
 -spec connect(epgsql:connection(), inet:ip_address() | inet:hostname(),
               string(), string(), epgsql:connect_opts()) -> reference().
               string(), string(), epgsql:connect_opts()) -> reference().
 connect(C, Host, Username, Password, Opts) ->
 connect(C, Host, Username, Password, Opts) ->
-    Opts1 = epgsql:to_map(Opts),
-    Opts2 = maps:remove(password, Opts1),
-    Password1 = epgsql_cmd_connect:hide_password(Password),
+    Opts1 = maps:merge(epgsql:to_map(Opts),
+                       #{host => Host,
+                         username => Username,
+                         password => Password}),
+    call_connect(C, Opts1).
+
+-spec call_connect(epgsql:connection(), epgsql:connect_opts()) -> reference().
+call_connect(C, Opts) when is_map(Opts) ->
+    Opts1 = epgsql_cmd_connect:opts_hide_password(Opts),
     complete_connect(
     complete_connect(
-      C, cast(C, epgsql_cmd_connect, {Host, Username, Password1, Opts2}), Opts2).
+      C, cast(C, epgsql_cmd_connect, Opts1), Opts1).
+
 
 
 -spec close(epgsql:connection()) -> ok.
 -spec close(epgsql:connection()) -> ok.
 close(C) ->
 close(C) ->

+ 12 - 9
src/epgsqli.erl

@@ -28,11 +28,8 @@ start_link() ->
     epgsql_sock:start_link().
     epgsql_sock:start_link().
 
 
 connect(Opts) ->
 connect(Opts) ->
-    Settings = epgsql:to_map(Opts),
-    Host = maps:get(host, Settings, "localhost"),
-    Username = maps:get(username, Settings, os:getenv("USER")),
-    Password = maps:get(password, Settings, ""),
-    connect(Host, Username, Password, Settings).
+    {ok, C} = epgsql_sock:start_link(),
+    call_connect(C, Opts).
 
 
 connect(Host, Opts) ->
 connect(Host, Opts) ->
     connect(Host, os:getenv("USER"), "", Opts).
     connect(Host, os:getenv("USER"), "", Opts).
@@ -47,11 +44,17 @@ connect(Host, Username, Password, Opts) ->
 -spec connect(epgsql:connection(), inet:ip_address() | inet:hostname(),
 -spec connect(epgsql:connection(), inet:ip_address() | inet:hostname(),
               string(), string(), epgsql:connect_opts()) -> reference().
               string(), string(), epgsql:connect_opts()) -> reference().
 connect(C, Host, Username, Password, Opts) ->
 connect(C, Host, Username, Password, Opts) ->
-    Opts1 = epgsql:to_map(Opts),
-    Opts2 = maps:remove(password, Opts1),
-    Password1 = epgsql_cmd_connect:hide_password(Password),
+    Opts1 = maps:merge(epgsql:to_map(Opts),
+                       #{host => Host,
+                         username => Username,
+                         password => Password}),
+    call_connect(C, Opts1).
+
+call_connect(C, Opts) ->
+    Opts1 = epgsql_cmd_connect:opts_hide_password(Opts),
     epgsqla:complete_connect(
     epgsqla:complete_connect(
-      C, incremental(C, epgsql_cmd_connect, {Host, Username, Password1, Opts2}), Opts2).
+      C, incremental(C, epgsql_cmd_connect, Opts1), Opts1).
+
 
 
 -spec close(epgsql:connection()) -> ok.
 -spec close(epgsql:connection()) -> ok.
 close(C) ->
 close(C) ->