Skip to content
This repository has been archived by the owner. It is now read-only.
Permalink
Browse files
Add bcrypt_pool for pooling a set of 'worker' port processes
  • Loading branch information
Hunter Morris authored and hntrmrrs committed Apr 3, 2011
1 parent 14d7d98 commit 0788af79b828fcc27345b31893054c089e75b066
Showing 7 changed files with 212 additions and 9 deletions.
@@ -5,6 +5,7 @@ _build/
priv/bcrypt
priv/bcrypt_nif.so
logs/*
.eunit

## libtool
.deps
@@ -1,10 +1,22 @@
CC = gcc
CFLAGS = -g -O2 -Wall
ERLANG_LIBS = -lerl_interface -lei -lpthread

BCRYPTLIBS = c_src/bcrypt_port.o c_src/bcrypt.o c_src/blowfish.o

all: compile

compile_port: priv/bcrypt

priv/bcrypt: $(BCRYPTLIBS)
$(CC) $(CFLAGS) $(BCRYPTLIBS) $(ERLANG_LIBS) -o $@

compile:
@ ./rebar compile

tests:
@ ./rebar eunit

clean:
@ ./rebar clean
@ ./rebar clean

@@ -1,8 +1,8 @@
%% -*- mode: erlang;erlang-indent-level: 2;indent-tabs-mode: nil -*-
{so_specs,
[{"priv/bcrypt_nif.so",
["c_src/blowfish.c", "c_src/bcrypt.c", "c_src/bcrypt_nif.c"]},
{"priv/bcrypt",
["c_src/blowfish.c", "c_src/bcrypt.c", "c_src/bcrypt_port.c"]}]}.
{so_name, "bcrypt_nif.so"}.
{port_sources, ["c_src/blowfish.c", "c_src/bcrypt.c", "c_src/bcrypt_nif.c"]}.

{erl_opts, [debug_info]}.

{pre_hooks, [{clean, "rm -f priv/bcrypt c_src/bcrypt_port.o"},
{compile, "make compile_port"}]}.
@@ -10,7 +10,7 @@
{default_log_rounds, 12},

% Mechanism to use 'nif' or 'port'
{mechanism, nif},
{mechanism, port},

% Size of port program pool
{pool_size, 4}
@@ -0,0 +1,81 @@
%% Copyright (c) 2011 Hunter Morris
%% Distributed under the MIT license; see LICENSE for details.
-module(bcrypt_pool).
-author('Hunter Morris <huntermorris@gmail.com>').

-behaviour(gen_server).

-export([start_link/0, available/1]).
-export([gen_salt/0, gen_salt/1]).
-export([hashpw/2]).

%% gen_server
-export([init/1, code_change/3, terminate/2,
handle_call/3, handle_cast/2, handle_info/2]).

-record(state, {
size = 0,
busy = 0,
requests = queue:new(),
ports = queue:new()
}).

-record(req, {mon, from}).

start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
available(Pid) -> gen_server:cast(?MODULE, {available, Pid}).

gen_salt() -> do_call(fun bcrypt_port:gen_salt/1, []).
gen_salt(Rounds) -> do_call(fun bcrypt_port:gen_salt/2, [Rounds]).
hashpw(Password, Salt) -> do_call(fun bcrypt_port:hashpw/3, [Password, Salt]).

init([]) ->
{ok, Size} = application:get_env(bcrypt, pool_size),
{ok, #state{size = Size}}.

terminate(shutdown, _) -> ok.

handle_call(request, {RPid, _} = From, #state{ports = P} = State) ->
case queue:out(P) of
{empty, P} ->
#state{size = Size, busy = B, requests = R} = State,
B1 =
if Size > B ->
{ok, _} = bcrypt_port_sup:start_child(),
B + 1;
true ->
B
end,
RRef = erlang:monitor(process, RPid),
R1 = queue:in(#req{mon = RRef, from = From}, R),
{noreply, State#state{requests = R1,
busy = B1}};
{{value, PPid}, P1} ->
#state{busy = B} = State,
{reply, {ok, PPid}, State#state{busy = B + 1, ports = P1}}
end;
handle_call(Msg, _, _) -> exit({unknown_call, Msg}).

handle_cast(
{available, Pid},
#state{requests = R, ports = P, busy = B} = S) ->
case queue:out(R) of
{empty, R} ->
{noreply, S#state{ports = queue:in(Pid, P), busy = B - 1}};
{{value, #req{mon = Mon, from = F}}, R1} ->
true = erlang:demonitor(Mon, [flush]),
gen_server:reply(F, {ok, Pid}),
{noreply, S#state{requests = R1}}
end;
handle_cast(Msg, _) -> exit({unknown_cast, Msg}).

handle_info({'DOWN', Ref, process, _Pid, _Reason}, #state{requests = R} = State) ->
R1 = queue:from_list(lists:keydelete(Ref, #req.mon, queue:to_list(R))),
{noreply, State#state{requests = R1}};
handle_info(Msg, _) -> exit({unknown_info, Msg}).
code_change(_OldVsn, State, _Extra) -> {ok, State}.

do_call(F, Args0) ->
{ok, Pid} = gen_server:call(?MODULE, request, infinity),
Args = [Pid|Args0],
apply(F, Args).
@@ -0,0 +1,108 @@
%% Copyright (c) 2011 Hunter Morris
%% Distributed under the MIT license; see LICENSE for details.
-module(bcrypt_port).
-author('Hunter Morris <hunter.morris@smarkets.com>').

-behaviour(gen_server).

%% API
-export([start_link/0, stop/0]).
-export([gen_salt/1, gen_salt/2]).
-export([hashpw/3]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).

-record(state, {port, default_log_rounds}).

-define(CMD_SALT, 0).
-define(CMD_HASHPW, 1).
-define(BCRYPT_ERROR(F, D), error_logger:error_msg(F, D)).
-define(BCRYPT_WARNING(F, D), error_logger:warning_msg(F, D)).

start_link() ->
Dir = case code:priv_dir(bcrypt) of
{error, bad_name} ->
case code:which(bcrypt) of
Filename when is_list(Filename) ->
filename:join(
[filename:dirname(Filename), "../priv"]);
_ ->
"../priv"
end;
Priv -> Priv
end,
Port = filename:join(Dir, "bcrypt"),
gen_server:start_link(?MODULE, [Port], []).

stop() -> gen_server:call(?MODULE, stop).

gen_salt(Pid) ->
R = crypto:rand_bytes(16),
gen_server:call(Pid, {encode_salt, R}, infinity).

gen_salt(Pid, LogRounds) ->
R = crypto:rand_bytes(16),
gen_server:call(Pid, {encode_salt, R, LogRounds}, infinity).

hashpw(Pid, Password, Salt) ->
gen_server:call(Pid, {hashpw, Password, Salt}, infinity).

%%====================================================================
%% gen_server callbacks
%%====================================================================
init([Filename]) ->
case file:read_file_info(Filename) of
{ok, _Info} ->
Port = open_port(
{spawn, Filename}, [{packet, 2}, binary, exit_status]),
ok = bcrypt_pool:available(self()),
{ok, Rounds} = application:get_env(bcrypt, default_log_rounds),
{ok, #state{port = Port, default_log_rounds = Rounds}};
{error, Reason} ->
?BCRYPT_ERROR("Can't open file ~p: ~p", [Filename, Reason]),
{stop, error_opening_bcrypt_file}
end.

terminate(_Reason, #state{port=Port}) ->
catch port_close(Port),
ok.

handle_call({encode_salt, R}, From, #state{default_log_rounds = LogRounds} = State) ->
handle_call({encode_salt, R, LogRounds}, From, State);
handle_call({encode_salt, R, LogRounds}, From, State) ->
Port = State#state.port,
Data = term_to_binary({?CMD_SALT, From, {R, LogRounds}}),
port_command(Port, Data),
{noreply, State};
handle_call({hashpw, Password, Salt}, From, State) ->
Port = State#state.port,
Data = term_to_binary({?CMD_HASHPW, From, {Password, Salt}}),
port_command(Port, Data),
{noreply, State};
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call(Msg, _, _) -> exit({unknown_call, Msg}).

handle_cast(Msg, _) -> exit({unknown_cast, Msg}).

code_change(_OldVsn, State, _Extra) -> {ok, State}.

handle_info({Port, {data, Data}}, #state{port=Port}=State) ->
{Cmd, To, Reply0} = binary_to_term(Data),
Reply =
case Reply0 of
"Invalid salt" -> {error, invalid_salt};
"Invalid salt length" -> {error, invalid_salt_length};
"Invalid number of rounds" -> {error, invalid_rounds};
_ -> {ok, Reply0}
end,
gen_server:reply(To, Reply),
ok = bcrypt_pool:available(self()),
{noreply, State};
handle_info({Port, {exit_status, Status}}, #state{port=Port}=State) ->
%% Rely on whomever is supervising this process to restart.
?BCRYPT_WARNING("Port died: ~p", [Status]),
{stop, port_died, State};
handle_info(Msg, _) -> exit({unknown_info, Msg}).
@@ -5,9 +5,10 @@

-behaviour(supervisor).

-export([start_link/0, init/1]).
-export([start_link/0, start_child/0, init/1]).

start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []).
start_child() -> supervisor:start_child(?MODULE, []).

init([]) ->
{ok, {{simple_one_for_one, 1, 1},

0 comments on commit 0788af7

Please sign in to comment.