diff options
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | catlfish.config | 12 | ||||
-rw-r--r-- | ebin/catlfish.app | 2 | ||||
-rw-r--r-- | reltool.config | 1 | ||||
-rw-r--r-- | src/catlfish_app.erl | 15 | ||||
-rw-r--r-- | src/catlfish_sup.erl | 30 | ||||
-rw-r--r-- | src/catlfish_web.erl | 48 | ||||
-rw-r--r-- | src/v1.erl | 86 |
8 files changed, 134 insertions, 61 deletions
@@ -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. @@ -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}. |