From 89fa75ee1bddfd92a698efa7e11f427d81b77e64 Mon Sep 17 00:00:00 2001 From: Michael Klishin Date: Thu, 28 Jul 2022 15:14:32 +0400 Subject: [PATCH] Fold trust_store_http into this repo It does not have any external users and we'd prefer to evolve it together with the RabbitMQ tier 1 plugin that depends on it. Pair: @pjk25 --- .gitignore | 1 + deps/rabbitmq_trust_store/BUILD.bazel | 2 +- deps/trust_store_http/.gitignore | 22 +++++ .../trust_store_http/BUILD.bazel | 0 deps/trust_store_http/Makefile | 22 +++++ deps/trust_store_http/README.md | 71 ++++++++++++++++ deps/trust_store_http/rel/sys.config | 2 + deps/trust_store_http/rel/vm.args | 3 + deps/trust_store_http/relx.config | 4 + .../trust_store_http/src/trust_store_http.erl | 12 +++ .../src/trust_store_http_app.erl | 81 +++++++++++++++++++ .../src/trust_store_http_sup.erl | 17 ++++ .../src/trust_store_invalid_handler.erl | 13 +++ .../src/trust_store_list_handler.erl | 68 ++++++++++++++++ 14 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 deps/trust_store_http/.gitignore rename BUILD.trust_store_http => deps/trust_store_http/BUILD.bazel (100%) create mode 100644 deps/trust_store_http/Makefile create mode 100644 deps/trust_store_http/README.md create mode 100644 deps/trust_store_http/rel/sys.config create mode 100644 deps/trust_store_http/rel/vm.args create mode 100644 deps/trust_store_http/relx.config create mode 100644 deps/trust_store_http/src/trust_store_http.erl create mode 100644 deps/trust_store_http/src/trust_store_http_app.erl create mode 100644 deps/trust_store_http/src/trust_store_http_sup.erl create mode 100644 deps/trust_store_http/src/trust_store_invalid_handler.erl create mode 100644 deps/trust_store_http/src/trust_store_list_handler.erl diff --git a/.gitignore b/.gitignore index 769653969d..456e50830d 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ !/deps/rabbitmq_web_mqtt_examples/ !/deps/rabbitmq_web_stomp/ !/deps/rabbitmq_web_stomp_examples/ +!/deps/trust_store_http/ /escript/ /escript.lock /plugins/ diff --git a/deps/rabbitmq_trust_store/BUILD.bazel b/deps/rabbitmq_trust_store/BUILD.bazel index 607d1ecf89..e338e6241f 100644 --- a/deps/rabbitmq_trust_store/BUILD.bazel +++ b/deps/rabbitmq_trust_store/BUILD.bazel @@ -88,7 +88,7 @@ suites = [ flaky = True, runtime_deps = [ "@ct_helper//:erlang_app", - "@trust_store_http//:erlang_app", + "//deps/trust_store_http:erlang_app", ], ), ] diff --git a/deps/trust_store_http/.gitignore b/deps/trust_store_http/.gitignore new file mode 100644 index 0000000000..7483b24291 --- /dev/null +++ b/deps/trust_store_http/.gitignore @@ -0,0 +1,22 @@ +*~ +.sw? +.*.sw? +*.beam +*.coverdata +/.erlang.mk/ +/cover/ +/deps/ +/doc/ +/ebin/ +/escript/ +/git-revisions.txt +/logs/ +/plugins/ +/rebar.config +/rebar.lock +/_rel/ +/sbin/ +/test/ct.cover.spec +/xrefr + +/trust_store_http.d diff --git a/BUILD.trust_store_http b/deps/trust_store_http/BUILD.bazel similarity index 100% rename from BUILD.trust_store_http rename to deps/trust_store_http/BUILD.bazel diff --git a/deps/trust_store_http/Makefile b/deps/trust_store_http/Makefile new file mode 100644 index 0000000000..0a636fa497 --- /dev/null +++ b/deps/trust_store_http/Makefile @@ -0,0 +1,22 @@ +PROJECT = trust_store_http +PROJECT_DESCRIPTION = Trust store HTTP server +PROJECT_VERSION = 1.0.0 + +define PROJECT_APP_EXTRA_KEYS + {broker_version_requirements, []} +endef + +LOCAL_DEPS = ssl +DEPS = cowboy jsx + +DEP_EARLY_PLUGINS = rabbit_common/mk/rabbitmq-early-plugin.mk +DEP_PLUGINS = rabbit_common/mk/rabbitmq-plugin.mk + +# FIXME: Use erlang.mk patched for RabbitMQ, while waiting for PRs to be +# reviewed and merged. + +ERLANG_MK_REPO = https://github.com/rabbitmq/erlang.mk.git +ERLANG_MK_COMMIT = rabbitmq-tmp + +include ../../rabbitmq-components.mk +include ../../erlang.mk diff --git a/deps/trust_store_http/README.md b/deps/trust_store_http/README.md new file mode 100644 index 0000000000..842e8fcc06 --- /dev/null +++ b/deps/trust_store_http/README.md @@ -0,0 +1,71 @@ +# Example Trust Store HTTP Server for RabbitMQ + +This tiny HTTP server serves CA certificates from a user-specified local directory. +It is meant to be used with [RabbitMQ trust store plugin](https://github.com/rabbitmq/rabbitmq-trust-store) +in its test suite and as an example. + +## Endpoints + + * `/`: serves a list of certificates in JSON. The format is `{"certificates":[{"id": , "path": }, ...]}` + * `/certs/`: access for PEM encoded certificate files + * `/invlid`: serves invalid JSON, to be used in integration tests + +``` + = : + = /certs/ + = name of a PEM file in the listed directory +``` + +## Usage + +To rebuild and run a release (requires Erlang to be installed): + +``` +gmake run CERT_DIR="/my/cacert/directory" PORT=8080 +``` + +To run from the pre-built escript (requires Erlang to be installed): + +``` +gmake +CERT_DIR="/my/cacert/directory" PORT=8080 ./_rel/trust_store_http_release/bin/trust_store_http_release console +``` + + +## HTTPS + +To start an HTTPS server, you should provide ssl options. It can be done via +Erlang `.config` file format: + +``` +[{trust_store_http, + [{ssl_options, [{cacertfile,"/path/to/testca/cacert.pem"}, + {certfile,"/path/to/server/cert.pem"}, + {keyfile,"/path/to/server/key.pem"}, + {verify,verify_peer}, + {fail_if_no_peer_cert,false}]}]}] +``` + + +This configuration can be added to `rel/sys.config` +if you're running the application from source `make run` + +Or it can be specified as an environment variable: + +``` +CERT_DIR="/my/cacert/directory" PORT=8443 CONFIG_FILE=my_config.config ./_rel/trust_store_http_release/bin/trust_store_http_release console +``` + +Port and directory can be also set via config file: + + +``` +[{trust_store_http, + [{directory, "/tmp/certs"}, + {port, 8081}, + {ssl_options, [{cacertfile,"/path/to/testca/cacert.pem"}, + {certfile,"/path/to/server/cert.pem"}, + {keyfile,"/path/to/server/key.pem"}, + {verify,verify_peer}, + {fail_if_no_peer_cert,false}]}]}] +``` diff --git a/deps/trust_store_http/rel/sys.config b/deps/trust_store_http/rel/sys.config new file mode 100644 index 0000000000..0dbd0fd1f4 --- /dev/null +++ b/deps/trust_store_http/rel/sys.config @@ -0,0 +1,2 @@ +[ +]. diff --git a/deps/trust_store_http/rel/vm.args b/deps/trust_store_http/rel/vm.args new file mode 100644 index 0000000000..c8a41f9a52 --- /dev/null +++ b/deps/trust_store_http/rel/vm.args @@ -0,0 +1,3 @@ +-name trust_store_http@127.0.0.1 +-setcookie trust_store_http +-heart diff --git a/deps/trust_store_http/relx.config b/deps/trust_store_http/relx.config new file mode 100644 index 0000000000..d55424b77a --- /dev/null +++ b/deps/trust_store_http/relx.config @@ -0,0 +1,4 @@ +{release, {trust_store_http_release, "1"}, [trust_store_http, sasl, runtime_tools]}. +{extended_start_script, true}. +{sys_config, "rel/sys.config"}. +{vm_args, "rel/vm.args"}. diff --git a/deps/trust_store_http/src/trust_store_http.erl b/deps/trust_store_http/src/trust_store_http.erl new file mode 100644 index 0000000000..0ac7599823 --- /dev/null +++ b/deps/trust_store_http/src/trust_store_http.erl @@ -0,0 +1,12 @@ +-module(trust_store_http). + + +-export([main/1]). + +main([]) -> + io:format("~nStarting trust store server ~n", []), + application:ensure_all_started(trust_store_http), + io:format("~nTrust store server started on port ~p ~n", + [application:get_env(trust_store_http, port, undefined)]), + user_drv:start(), + timer:sleep(infinity). \ No newline at end of file diff --git a/deps/trust_store_http/src/trust_store_http_app.erl b/deps/trust_store_http/src/trust_store_http_app.erl new file mode 100644 index 0000000000..2fd861405a --- /dev/null +++ b/deps/trust_store_http/src/trust_store_http_app.erl @@ -0,0 +1,81 @@ +-module(trust_store_http_app). +-behaviour(application). + +-export([start/2]). +-export([stop/1]). + +start(_Type, _Args) -> + init_config(), + Directory = get_directory(), + Port = get_port(), + Dispatch = cowboy_router:compile([ + {'_', [ + {"/", trust_store_list_handler, []}, + {"/invalid", trust_store_invalid_handler, []}, + {"/certs/[...]", cowboy_static, + {dir, Directory, [{mimetypes, {<<"text">>, <<"html">>, []}}]}}]} + ]), + case get_ssl_options() of + undefined -> start_http(Dispatch, Port); + SslOptions -> start_https(Dispatch, Port, SslOptions) + end, + trust_store_http_sup:start_link(). + +stop(_State) -> + ok. + +start_http(Dispatch, undefined) -> + start_http(Dispatch, 8080); +start_http(Dispatch, Port) -> + {ok, _} = cowboy:start_clear(trust_store_http_listener, + [{port, Port}], + #{env => #{dispatch => Dispatch}}). + +start_https(Dispatch, undefined, SslOptions) -> + start_https(Dispatch, 8443, SslOptions); +start_https(Dispatch, Port, SslOptions) -> + {ok, _} = cowboy:start_tls(trust_store_https_listener, + [{port, Port}] ++ SslOptions, + #{env => #{dispatch => Dispatch}}). + +get_directory() -> + Dir = case os:getenv("CERT_DIR") of + false -> + case application:get_env(trust_store_http, directory, undefined) of + undefined -> + {ok, CurrentDir} = file:get_cwd(), + CurrentDir; + AppDir -> AppDir + end; + EnvDir -> EnvDir + end, + application:set_env(trust_store_http, directory, Dir), + Dir. + +get_ssl_options() -> + application:get_env(trust_store_http, ssl_options, undefined). + +get_port() -> + case os:getenv("PORT") of + false -> + application:get_env(trust_store_http, port, undefined); + PortEnv -> + Port = list_to_integer(PortEnv), + application:set_env(trust_store_http, port, Port), + Port + end. + +init_config() -> + case os:getenv("CONFIG_FILE") of + false -> ok; + ConfigFile -> + case file:consult(ConfigFile) of + {ok, [Config]} -> + lists:foreach(fun({App, Conf}) -> + [application:set_env(App, Key, Val) + || {Key, Val} <- Conf] + end, + Config); + _ -> ok + end + end. diff --git a/deps/trust_store_http/src/trust_store_http_sup.erl b/deps/trust_store_http/src/trust_store_http_sup.erl new file mode 100644 index 0000000000..bb209bc885 --- /dev/null +++ b/deps/trust_store_http/src/trust_store_http_sup.erl @@ -0,0 +1,17 @@ +-module(trust_store_http_sup). +-behaviour(supervisor). + +-export([start_link/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + Specs = [], + Flags = #{ + strategy => one_for_one, + intensity => 1, + period => 5 + }, + {ok, {Flags, Specs}}. diff --git a/deps/trust_store_http/src/trust_store_invalid_handler.erl b/deps/trust_store_http/src/trust_store_invalid_handler.erl new file mode 100644 index 0000000000..7d392dadf0 --- /dev/null +++ b/deps/trust_store_http/src/trust_store_invalid_handler.erl @@ -0,0 +1,13 @@ +-module(trust_store_invalid_handler). +-behaviour(cowboy_handler). + +-export([init/2]). +-export([terminate/3]). + +init(Req, State) -> + %% serves some invalid JSON + Req2 = cowboy_req:reply(200, #{<<"content-type">> => <<"application/json">>}, <<"{_1}}1}">>, Req), + {ok, Req2, State}. + +terminate(_Reason, _Req, _State) -> + ok. diff --git a/deps/trust_store_http/src/trust_store_list_handler.erl b/deps/trust_store_http/src/trust_store_list_handler.erl new file mode 100644 index 0000000000..88300eb30e --- /dev/null +++ b/deps/trust_store_http/src/trust_store_list_handler.erl @@ -0,0 +1,68 @@ +-module(trust_store_list_handler). +-behaviour(cowboy_handler). + +-include_lib("kernel/include/file.hrl"). + +-export([init/2]). +-export([terminate/3]). + +init(Req, State) -> + {ok, Directory} = application:get_env(trust_store_http, directory), + case list_files(Directory) of + {ok, Files} -> respond(Files, Req, State); + {error, Err} -> respond_error(Err, Req, State) + end. + +terminate(_Reason, _Req, _State) -> + ok. + +respond(Files, Req, State) -> + ResponseBody = json_encode(Files), + Headers = #{<<"content-type">> => <<"application/json">>}, + Req2 = cowboy_req:reply(200, Headers, ResponseBody, Req), + {ok, Req2, State}. + +respond_error(Reason, Req, State) -> + Error = io_lib:format("Error listing certificates ~p", [Reason]), + logger:log(error, "~s", [Error]), + Req2 = cowboy_req:reply(500, [], iolist_to_binary(Error), Req), + {ok, Req2, State}. + +json_encode(Files) -> + Map = #{certificates => [ #{id => cert_id(FileName, FileDate, FileHash), + path => cert_path(FileName)} + || {FileName, FileDate, FileHash} <- Files ]}, + jsx:encode(Map). + +cert_id(FileName, FileDate, FileHash) -> + iolist_to_binary(io_lib:format("~s:~p:~p", [FileName, FileDate, FileHash])). + +cert_path(FileName) -> + iolist_to_binary(["/certs/", FileName]). + +-spec list_files(string()) -> [{string(), file:date_time(), integer()}]. +list_files(Directory) -> + case file:list_dir(Directory) of + {ok, FileNames} -> + PemFiles = [ FileName || FileName <- FileNames, + filename:extension(FileName) == ".pem" ], + {ok, lists:map( + fun(FileName) -> + FullName = filename:join(Directory, FileName), + {ok, Mtime} = modification_time(FullName, posix), + Hash = file_content_hash(FullName), + {FileName, Mtime, Hash} + end, + PemFiles)}; + {error, Err} -> {error, Err} + end. + +modification_time(FileName, Type) -> + case file:read_file_info(FileName, [{time, Type}]) of + {ok, #file_info{mtime = Mtime}} -> {ok, Mtime}; + {error, Reason} -> {error, Reason} + end. + +file_content_hash(Path) -> + {ok, Data} = file:read_file(Path), + erlang:phash2(Data).