summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--catlfish.config12
-rw-r--r--ebin/catlfish.app2
-rw-r--r--reltool.config1
-rw-r--r--src/catlfish_app.erl15
-rw-r--r--src/catlfish_sup.erl30
-rw-r--r--src/catlfish_web.erl48
-rw-r--r--src/v1.erl86
8 files changed, 134 insertions, 61 deletions
diff --git a/README.md b/README.md
index 514e87f..e344bd5 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ A compiled plop application in ../plop
A compiled https://github.com/davisp/jiffy (for JSON encoding and decoding) in ../jiffy
A compiled https://github.com/basho/lager (for logging) in ../lager
+A compiled https://github.com/mochi/mochiweb (for web server functionality) in ../mochiweb
# Start
diff --git a/catlfish.config b/catlfish.config
index c8fcb73..75d00fa 100644
--- a/catlfish.config
+++ b/catlfish.config
@@ -7,11 +7,15 @@
{error_logger_mf_dir, "log"},
{error_logger_mf_maxbytes, 10485760}, % 10 MB
{error_logger_mf_maxfiles, 10}]},
- {inets,
- [{services,
- [{httpd, [{proplist_file, "httpd_props.conf"}]}]}]},
{catlfish,
- [{known_roots_path, "known_roots"}]},
+ [{known_roots_path, "known_roots"},
+ {https_servers,
+ [{"127.0.0.1", 8080, v1}
+ ]},
+ {https_certfile, "catlfish/webroot/certs/webcert.pem"},
+ {https_keyfile, "catlfish/webroot/keys/webkey.pem"},
+ {https_cacertfile, "catlfish/webroot/certs/webcert.pem"}
+ ]},
{plop,
[{entry_root_path, "db/certentries/"},
{index_path, "db/index"},
diff --git a/ebin/catlfish.app b/ebin/catlfish.app
index 44c9e0f..beea7d6 100644
--- a/ebin/catlfish.app
+++ b/ebin/catlfish.app
@@ -8,5 +8,5 @@
[{description, "catlfish -- Certificate Transparency Log Server"},
{vsn, "0.2.0-dev"},
{modules, [v1, catlfish_app]},
- {applications, [kernel, stdlib, plop, inets, jiffy, lager]},
+ {applications, [kernel, stdlib, plop, inets, jiffy, lager, mochiweb]},
{mod, {catlfish_app, []}}]}.
diff --git a/reltool.config b/reltool.config
index 6f9d52a..75e417c 100644
--- a/reltool.config
+++ b/reltool.config
@@ -13,6 +13,7 @@
{excl_archive_filters, ["^include$","^priv$","^\\.git$"]},
{app, catlfish, [{app_file, all}, {lib_dir, "."}]},
{app, plop, [{app_file, all}, {lib_dir, "../plop"}]},
+ {app, mochiweb, [{app_file, all}, {lib_dir, "../mochiweb"}]},
{app, lager, [{app_file, all}, {lib_dir, "../lager"}]},
{app, goldrush, [{app_file, all}, {lib_dir, "../lager/deps/goldrush"}]},
{app, jiffy, [{app_file, all}, {lib_dir, "../jiffy"}]}
diff --git a/src/catlfish_app.erl b/src/catlfish_app.erl
index 39c3109..cfb55cd 100644
--- a/src/catlfish_app.erl
+++ b/src/catlfish_app.erl
@@ -12,19 +12,8 @@
%% Application callbacks
%% ===================================================================
-dummy() ->
- receive
- after
- infinity ->
- none
- end.
-
-start(_StartType, _StartArgs) ->
- io:format("starting catlfish~n", []),
- Pid = spawn(fun () ->
- dummy()
- end),
- {ok, Pid}.
+start(normal, Args) ->
+ catlfish_sup:start_link(Args).
stop(_State) ->
ok.
diff --git a/src/catlfish_sup.erl b/src/catlfish_sup.erl
new file mode 100644
index 0000000..e8f2de9
--- /dev/null
+++ b/src/catlfish_sup.erl
@@ -0,0 +1,30 @@
+%%% Copyright (c) 2014, NORDUnet A/S.
+%%% See LICENSE for licensing information.
+
+-module(catlfish_sup).
+-behaviour(supervisor).
+
+-export([start_link/1, init/1]).
+
+start_link(_Args) ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+ SSLOptions = [{certfile, application:get_env(catlfish, https_certfile, none)},
+ {keyfile, application:get_env(catlfish, https_keyfile, none)}],
+ Servers = lists:map(fun (Config) ->
+ {IpAddress, Port, Module} = Config,
+ {ok, IPv4Address} = inet:parse_ipv4strict_address(IpAddress),
+ WebConfig = [{ip, IPv4Address},
+ {port, Port},
+ {ssl, true},
+ {ssl_opts, SSLOptions}
+ ],
+ {catlfish_web,
+ {catlfish_web, start, [WebConfig, Module]},
+ permanent, 5000,
+ worker, dynamic}
+ end, application:get_env(catlfish, https_servers, [])),
+ {ok,
+ {{one_for_one, 3, 10},
+ Servers}}.
diff --git a/src/catlfish_web.erl b/src/catlfish_web.erl
new file mode 100644
index 0000000..cdc1a39
--- /dev/null
+++ b/src/catlfish_web.erl
@@ -0,0 +1,48 @@
+%%% Copyright (c) 2014, NORDUnet A/S.
+%%% See LICENSE for licensing information.
+
+-module(catlfish_web).
+-export([start/2, stop/0, loop/2]).
+
+start(Options, Module) ->
+ Loop = fun (Req) ->
+ ?MODULE:loop(Req, Module)
+ end,
+ mochiweb_http:start([{name, ?MODULE}, {loop, Loop} | Options]).
+
+stop() ->
+ mochiweb_http:stop(?MODULE).
+
+loop(Req, Module) ->
+ "/" ++ Path = Req:get(path),
+ try
+ case Req:get(method) of
+ 'GET' ->
+ Query = Req:parse_qs(),
+ lager:debug("GET ~p ~p", [Path, Query]),
+ Result = Module:request(get, Path, Query),
+ case Result of
+ none ->
+ Req:respond({404, [{"Content-Type", "text/plain"}], "Page not found"});
+ _ ->
+ Req:respond(Result)
+ end;
+ 'POST' ->
+ Body = Req:recv_body(),
+ lager:debug("POST ~p ~p", [Path, Body]),
+ Result = Module:request(post, Path, Body),
+ case Result of
+ none ->
+ Req:respond({404, [{"Content-Type", "text/plain"}], "Page not found"});
+ _ ->
+ Req:respond(Result)
+ end;
+ _ ->
+ Req:respond({501, [], []})
+ end
+ catch
+ Type:What ->
+ lager:error("Crash in ~p for path ~p: ~p ~n~p~n~p~n", [Module, Path, Type, What, erlang:get_stacktrace()]),
+ Req:respond({500, [{"Content-Type", "text/plain"}],
+ "Internal Server Error\n"})
+ end.
diff --git a/src/v1.erl b/src/v1.erl
index d5e65ea..0c13cbc 100644
--- a/src/v1.erl
+++ b/src/v1.erl
@@ -5,12 +5,10 @@
-module(v1).
%% API (URL)
--export(['add-chain'/3, 'add-pre-chain'/3, 'get-sth'/3,
- 'get-sth-consistency'/3, 'get-proof-by-hash'/3, 'get-entries'/3,
- 'get-roots'/3, 'get-entry-and-proof'/3]).
+-export([request/3]).
%% Public functions, i.e. part of URL.
-'add-chain'(SessionID, _Env, Input) ->
+request(post, "ct/v1/add-chain", Input) ->
R = case (catch jiffy:decode(Input)) of
{error, E} ->
html("add-chain: bad input:", E);
@@ -25,7 +23,7 @@
{ok, [Leaf | Chain]} ->
io:format("[info] adding ~p~n",
[x509:cert_string(LeafCert)]),
- catlfish:add_chain(Leaf, Chain);
+ success(catlfish:add_chain(Leaf, Chain));
{Err, Msg} ->
io:format("[info] rejecting ~p: ~p~n",
[x509:cert_string(LeafCert), Err]),
@@ -36,12 +34,12 @@
end;
_ -> html("add-chain: missing input: chain", Input)
end,
- deliver(SessionID, R).
+ R;
-'add-pre-chain'(SessionID, _Env, _Input) ->
- niy(SessionID).
+request(post, "ct/v1/add-pre-chain", _Input) ->
+ niy();
-'get-sth'(SessionID, _Env, _Input) ->
+request(get, "ct/v1/get-sth", _Query) ->
{ Treesize,
Timestamp,
Roothash,
@@ -51,10 +49,10 @@
{sha256_root_hash, base64:encode(Roothash)},
{tree_head_signature, base64:encode(
plop:serialise(Signature))}],
- deliver(SessionID, binary_to_list(jiffy:encode({R}))).
+ success(jiffy:encode({R}));
-'get-sth-consistency'(SessionID, _Env, Input) ->
- R = case lists:sort(httpd:parse_query(Input)) of
+request(get, "ct/v1/get-sth-consistency", Query) ->
+ R = case lists:sort(Query) of
[{"first", FirstInput}, {"second", SecondInput}] ->
{First, _} = string:to_integer(FirstInput),
{Second, _} = string:to_integer(SecondInput),
@@ -63,18 +61,18 @@
html("get-sth-consistency: bad input:",
[FirstInput, SecondInput]);
false ->
- binary_to_list(
+ success(
jiffy:encode(
{[{consistency,
[base64:encode(X) ||
X <- plop:consistency(First, Second)]}]}))
end;
- _ -> html("get-sth-consistency: bad input:", Input)
+ _ -> html("get-sth-consistency: bad input:", Query)
end,
- deliver(SessionID, R).
+ R;
-'get-proof-by-hash'(SessionID, _Env, Input) ->
- R = case lists:sort(httpd:parse_query(Input)) of
+request(get, "ct/v1/get-proof-by-hash", Query) ->
+ R = case lists:sort(Query) of
[{"hash", HashInput}, {"tree_size", TreeSizeInput}] ->
Hash = case (catch base64:decode(HashInput)) of
{'EXIT', _} -> error;
@@ -86,7 +84,7 @@
html("get-proof-by-hash: bad input:",
[HashInput, TreeSizeInput]);
false ->
- binary_to_list(
+ success(
jiffy:encode(
case plop:inclusion(Hash, TreeSize) of
{ok, Index, Path} ->
@@ -99,25 +97,25 @@
{error_message, list_to_binary(Msg)}]}
end))
end;
- _ -> html("get-proof-by-hash: bad input:", Input)
+ _ -> html("get-proof-by-hash: bad input:", Query)
end,
- deliver(SessionID, R).
+ R;
-'get-entries'(SessionID, _Env, Input) ->
- R = case lists:sort(httpd:parse_query(Input)) of
+request(get, "ct/v1/get-entries", Query) ->
+ R = case lists:sort(Query) of
[{"end", EndInput}, {"start", StartInput}] ->
{Start, _} = string:to_integer(StartInput),
{End, _} = string:to_integer(EndInput),
case lists:member(error, [Start, End]) of
true -> html("get-entries: bad input:", [Start, End]);
- false -> catlfish:entries(Start, min(End, Start + 999))
+ false -> success(catlfish:entries(Start, min(End, Start + 999)))
end;
- _ -> html("get-entries: bad input:", Input)
+ _ -> html("get-entries: bad input:", Query)
end,
- deliver(SessionID, R).
+ R;
-'get-entry-and-proof'(SessionID, _Env, Input) ->
- R = case lists:sort(httpd:parse_query(Input)) of
+request(get, "ct/v1/get-entry-and-proof", Query) ->
+ R = case lists:sort(Query) of
[{"leaf_index", IndexInput}, {"tree_size", TreeSizeInput}] ->
{Index, _} = string:to_integer(IndexInput),
{TreeSize, _} = string:to_integer(TreeSizeInput),
@@ -125,30 +123,32 @@
true ->
html("get-entry-and-proof: not integers: ",
[IndexInput, TreeSizeInput]);
- false -> catlfish:entry_and_proof(Index, TreeSize)
+ false -> success(catlfish:entry_and_proof(Index, TreeSize))
end;
- _ -> html("get-entry-and-proof: bad input:", Input)
+ _ -> html("get-entry-and-proof: bad input:", Query)
end,
- deliver(SessionID, R).
+ R;
-'get-roots'(SessionID, _Env, _Input) ->
+request(get, "ct/v1/get-roots", _Query) ->
R = [{certificates,
[base64:encode(Der) ||
Der <- catlfish:update_known_roots()]}],
- deliver(SessionID, binary_to_list(jiffy:encode({R}))).
+ success(jiffy:encode({R}));
+
+request(_Method, _Path, _) ->
+ none.
%% Private functions.
html(Text, Input) ->
- io_lib:format(
- "Content-Type: text/html\r\n\r\n" ++
- "<html><body><p>~n" ++
- "~s~n" ++
- "~p~n" ++
- "</body></html>~n", [Text, Input]).
+ {400, [{"Content-Type", "text/html"}],
+ io_lib:format(
+ "<html><body><p>~n" ++
+ "~s~n" ++
+ "~p~n" ++
+ "</body></html>~n", [Text, Input])}.
-niy(S) ->
- mod_esi:deliver(S, html("NIY - Not Implemented Yet|", [])).
+niy() ->
+ html("NIY - Not Implemented Yet|", []).
--spec deliver(any(), string()) -> ok | {error, _Reason}.
-deliver(Session, Data) ->
- mod_esi:deliver(Session, Data).
+success(Data) ->
+ {200, [{"Content-Type", "text/json"}], Data}.