diff options
Diffstat (limited to 'deps/rabbitmq_web_dispatch/src')
12 files changed, 1015 insertions, 0 deletions
diff --git a/deps/rabbitmq_web_dispatch/src/rabbit_cowboy_middleware.erl b/deps/rabbitmq_web_dispatch/src/rabbit_cowboy_middleware.erl new file mode 100644 index 0000000000..c8186619f6 --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/rabbit_cowboy_middleware.erl @@ -0,0 +1,24 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_cowboy_middleware). +-behavior(cowboy_middleware). + +-export([execute/2]). + +execute(Req, Env) -> + %% Find the correct dispatch list for this path. + Listener = maps:get(rabbit_listener, Env), + case rabbit_web_dispatch_registry:lookup(Listener, Req) of + {ok, Dispatch} -> + {ok, Req, maps:put(dispatch, Dispatch, Env)}; + {error, Reason} -> + Req2 = cowboy_req:reply(500, + #{<<"content-type">> => <<"text/plain">>}, + "Registry Error: " ++ io_lib:format("~p", [Reason]), Req), + {stop, Req2} + end. diff --git a/deps/rabbitmq_web_dispatch/src/rabbit_cowboy_redirect.erl b/deps/rabbitmq_web_dispatch/src/rabbit_cowboy_redirect.erl new file mode 100644 index 0000000000..9271567d91 --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/rabbit_cowboy_redirect.erl @@ -0,0 +1,15 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_cowboy_redirect). + +-export([init/2]). + +init(Req0, RedirectPort) -> + URI = cowboy_req:uri(Req0, #{port => RedirectPort}), + Req = cowboy_req:reply(301, #{<<"location">> => URI}, Req0), + {ok, Req, RedirectPort}. diff --git a/deps/rabbitmq_web_dispatch/src/rabbit_cowboy_stream_h.erl b/deps/rabbitmq_web_dispatch/src/rabbit_cowboy_stream_h.erl new file mode 100644 index 0000000000..d51086a69d --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/rabbit_cowboy_stream_h.erl @@ -0,0 +1,63 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_cowboy_stream_h). +-behavior(cowboy_stream). + +-export([init/3]). +-export([data/4]). +-export([info/3]). +-export([terminate/3]). +-export([early_error/5]). + +-record(state, { + next :: any(), + req :: cowboy_req:req() +}). + +init(StreamId, Req, Opts) -> + {Commands, Next} = cowboy_stream:init(StreamId, Req, Opts), + {Commands, #state{next = Next, req = Req}}. + +data(StreamId, IsFin, Data, State = #state{next = Next}) -> + {Commands, Next1} = cowboy_stream:data(StreamId, IsFin, Data, Next), + {Commands, State#state{next = Next1}}. + +info(StreamId, Response, State = #state{next = Next, req = Req}) -> + Response1 = case Response of + {response, 404, Headers0, <<>>} -> + %% TODO: should we log the actual response? + log_response(Response, Req), + Json = rabbit_json:encode(#{ + error => list_to_binary(httpd_util:reason_phrase(404)), + reason => <<"Not Found">>}), + Headers1 = maps:put(<<"content-length">>, integer_to_list(iolist_size(Json)), Headers0), + Headers = maps:put(<<"content-type">>, <<"application/json">>, Headers1), + {response, 404, Headers, Json}; + {response, _, _, _} -> + log_response(Response, Req), + Response; + {headers, _, _} -> + log_stream_response(Response, Req), + Response; + _ -> + Response + end, + {Commands, Next1} = cowboy_stream:info(StreamId, Response1, Next), + {Commands, State#state{next = Next1}}. + +terminate(StreamId, Reason, #state{next = Next}) -> + cowboy_stream:terminate(StreamId, Reason, Next). + +early_error(StreamId, Reason, PartialReq, Resp, Opts) -> + cowboy_stream:early_error(StreamId, Reason, PartialReq, Resp, Opts). + +log_response({response, Status, _Headers, Body}, Req) -> + webmachine_log:log_access({Status, Body, Req}). + +log_stream_response({headers, Status, _Headers}, Req) -> + webmachine_log:log_access({Status, <<>>, Req}). diff --git a/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch.erl b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch.erl new file mode 100644 index 0000000000..24a81a986a --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch.erl @@ -0,0 +1,88 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_web_dispatch). + +-export([register_context_handler/5, register_static_context/6]). +-export([register_port_redirect/4]). +-export([unregister_context/1]). + +%% Handler Registration + +%% Registers a dynamic selector and handler combination, with a link +%% to display in lists. +register_handler(Name, Listener, Selector, Handler, Link) -> + rabbit_web_dispatch_registry:add(Name, Listener, Selector, Handler, Link). + +%% Methods for standard use cases + +%% Registers a dynamic handler under a fixed context path, with link +%% to display in the global context. +register_context_handler(Name, Listener, Prefix, Handler, LinkText) -> + register_handler( + Name, Listener, context_selector(Prefix), Handler, {Prefix, LinkText}), + {ok, Prefix}. + +%% Convenience function registering a fully static context to serve +%% content from a module-relative directory, with link to display in +%% the global context. +register_static_context(Name, Listener, Prefix, Module, FSPath, LinkText) -> + register_handler(Name, Listener, + context_selector(Prefix), + static_context_handler(Prefix, Module, FSPath), + {Prefix, LinkText}), + {ok, Prefix}. + +%% A context which just redirects the request to a different port. +register_port_redirect(Name, Listener, Prefix, RedirectPort) -> + register_context_handler( + Name, Listener, Prefix, + cowboy_router:compile([{'_', [{'_', rabbit_cowboy_redirect, RedirectPort}]}]), + rabbit_misc:format("Redirect to port ~B", [RedirectPort])). + +context_selector("") -> + fun(_Req) -> true end; +context_selector(Prefix) -> + Prefix1 = list_to_binary("/" ++ Prefix), + fun(Req) -> + Path = cowboy_req:path(Req), + (Path == Prefix1) orelse (binary:match(Path, << Prefix1/binary, $/ >>) =/= nomatch) + end. + +%% Produces a handler for use with register_handler that serves up +%% static content from a directory specified relative to the application +%% (owning the module) priv directory, or to the directory containing +%% the ebin directory containing the named module's beam file. +static_context_handler(Prefix, Module, FSPath) -> + FSPathInPriv = re:replace(FSPath, "^priv/?", "", [{return, list}]), + case application:get_application(Module) of + {ok, App} when FSPathInPriv =/= FSPath -> + %% The caller indicated a file in the application's `priv` + %% dir. + cowboy_router:compile([{'_', [ + {"/" ++ Prefix, cowboy_static, + {priv_file, App, filename:join(FSPathInPriv, "index.html")}}, + {"/" ++ Prefix ++ "/[...]", cowboy_static, + {priv_dir, App, FSPathInPriv}} + ]}]); + _ -> + %% The caller indicated a file we should access directly. + {file, Here} = code:is_loaded(Module), + ModuleRoot = filename:dirname(filename:dirname(Here)), + LocalPath = filename:join(ModuleRoot, FSPath), + cowboy_router:compile([{'_', [ + {"/" ++ Prefix, cowboy_static, + {file, LocalPath ++ "/index.html"}}, + {"/" ++ Prefix ++ "/[...]", cowboy_static, + {dir, LocalPath}} + ]}]) + end. + +%% The opposite of all those register_* functions. +unregister_context(Name) -> + rabbit_web_dispatch_registry:remove(Name). + diff --git a/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_app.erl b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_app.erl new file mode 100644 index 0000000000..1de05c77d7 --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_app.erl @@ -0,0 +1,21 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_web_dispatch_app). + +-behaviour(application). +-export([start/2,stop/1]). + +%% @spec start(_Type, _StartArgs) -> ServerRet +%% @doc application start callback for rabbit_web_dispatch. +start(_Type, _StartArgs) -> + rabbit_web_dispatch_sup:start_link(). + +%% @spec stop(_State) -> ServerRet +%% @doc application stop callback for rabbit_web_dispatch. +stop(_State) -> + ok. diff --git a/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_listing_handler.erl b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_listing_handler.erl new file mode 100644 index 0000000000..93ee349af1 --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_listing_handler.erl @@ -0,0 +1,27 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_web_dispatch_listing_handler). + +-export([init/2]). + +init(Req0, Listener) -> + HTMLPrefix = + "<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\">" + "<head><title>RabbitMQ Web Server</title></head>" + "<body><h1>RabbitMQ Web Server</h1><p>Contexts available:</p><ul>", + HTMLSuffix = "</ul></body></html>", + List = + case rabbit_web_dispatch_registry:list(Listener) of + [] -> + "<li>No contexts installed</li>"; + Contexts -> + [["<li><a href=\"/", Path, "/\">", Desc, "</a></li>"] + || {Path, Desc} <- Contexts] + end, + Req = cowboy_req:reply(200, #{}, [HTMLPrefix, List, HTMLSuffix], Req0), + {ok, Req, Listener}. diff --git a/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_registry.erl b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_registry.erl new file mode 100644 index 0000000000..8d933ac668 --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_registry.erl @@ -0,0 +1,208 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_web_dispatch_registry). + +-behaviour(gen_server). + +-export([start_link/0]). +-export([add/5, remove/1, set_fallback/2, lookup/2, list_all/0]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, + code_change/3]). +-export([list/1]). + +-import(rabbit_misc, [pget/2]). + +-define(ETS, rabbitmq_web_dispatch). + +%% This gen_server is merely to serialise modifications to the dispatch +%% table for listeners. + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +add(Name, Listener, Selector, Handler, Link) -> + gen_server:call(?MODULE, {add, Name, Listener, Selector, Handler, Link}, + infinity). + +remove(Name) -> + gen_server:call(?MODULE, {remove, Name}, infinity). + +%% @todo This needs to be dispatch instead of a fun too. +%% But I'm not sure what code is using this. +set_fallback(Listener, FallbackHandler) -> + gen_server:call(?MODULE, {set_fallback, Listener, FallbackHandler}, + infinity). + +lookup(Listener, Req) -> + case lookup_dispatch(Listener) of + {ok, {Selectors, Fallback}} -> + case catch match_request(Selectors, Req) of + {'EXIT', Reason} -> {error, {lookup_failure, Reason}}; + not_found -> {ok, Fallback}; + Dispatch -> {ok, Dispatch} + end; + Err -> + Err + end. + +%% This is called in a somewhat obfuscated manner in +%% rabbit_mgmt_external_stats:rabbit_web_dispatch_registry_list_all() +list_all() -> + gen_server:call(?MODULE, list_all, infinity). + +%% Callback Methods + +init([]) -> + ?ETS = ets:new(?ETS, [named_table, public]), + {ok, undefined}. + +handle_call({add, Name, Listener, Selector, Handler, Link = {_, Desc}}, _From, + undefined) -> + Continue = case rabbit_web_dispatch_sup:ensure_listener(Listener) of + new -> set_dispatch( + Listener, [], + listing_fallback_handler(Listener)), + listener_started(Listener), + true; + existing -> true; + ignore -> false + end, + case Continue of + true -> case lookup_dispatch(Listener) of + {ok, {Selectors, Fallback}} -> + Selector2 = lists:keystore( + Name, 1, Selectors, + {Name, Selector, Handler, Link}), + set_dispatch(Listener, Selector2, Fallback); + {error, {different, Desc2, Listener2}} -> + exit({incompatible_listeners, + {Desc, Listener}, {Desc2, Listener2}}) + end; + false -> ok + end, + {reply, ok, undefined}; + +handle_call({remove, Name}, _From, + undefined) -> + case listener_by_name(Name) of + {error, not_found} -> + rabbit_log:warning("HTTP listener registry could not find context ~p", + [Name]), + {reply, ok, undefined}; + {ok, Listener} -> + {ok, {Selectors, Fallback}} = lookup_dispatch(Listener), + Selectors1 = lists:keydelete(Name, 1, Selectors), + set_dispatch(Listener, Selectors1, Fallback), + case Selectors1 of + [] -> rabbit_web_dispatch_sup:stop_listener(Listener), + listener_stopped(Listener); + _ -> ok + end, + {reply, ok, undefined} + end; + +handle_call({set_fallback, Listener, FallbackHandler}, _From, + undefined) -> + {ok, {Selectors, _OldFallback}} = lookup_dispatch(Listener), + set_dispatch(Listener, Selectors, FallbackHandler), + {reply, ok, undefined}; + +handle_call(list_all, _From, undefined) -> + {reply, list(), undefined}; + +handle_call(Req, _From, State) -> + rabbit_log:error("Unexpected call to ~p: ~p~n", [?MODULE, Req]), + {stop, unknown_request, State}. + +handle_cast(_, State) -> + {noreply, State}. + +handle_info(_, State) -> + {noreply, State}. + +terminate(_, _) -> + true = ets:delete(?ETS), + ok. + +code_change(_, State, _) -> + {ok, State}. + +%%--------------------------------------------------------------------------- + +%% Internal Methods + +listener_started(Listener) -> + [rabbit_networking:tcp_listener_started(Protocol, Listener, IPAddress, Port) + || {Protocol, IPAddress, Port} <- listener_info(Listener)], + ok. + +listener_stopped(Listener) -> + [rabbit_networking:tcp_listener_stopped(Protocol, Listener, IPAddress, Port) + || {Protocol, IPAddress, Port} <- listener_info(Listener)], + ok. + +listener_info(Listener) -> + Protocol = case pget(protocol, Listener) of + undefined -> + case pget(ssl, Listener) of + true -> https; + _ -> http + end; + P -> + P + end, + Port = pget(port, Listener), + [{Protocol, IPAddress, Port} + || {IPAddress, _Port, _Family} + <- rabbit_networking:tcp_listener_addresses(Port)]. + +lookup_dispatch(Lsnr) -> + case ets:lookup(?ETS, pget(port, Lsnr)) of + [{_, Lsnr, S, F}] -> {ok, {S, F}}; + [{_, Lsnr2, S, _F}] -> {error, {different, first_desc(S), Lsnr2}}; + [] -> {error, {no_record_for_listener, Lsnr}} + end. + +first_desc([{_N, _S, _H, {_, Desc}} | _]) -> Desc. + +set_dispatch(Listener, Selectors, Fallback) -> + ets:insert(?ETS, {pget(port, Listener), Listener, Selectors, Fallback}). + +match_request([], _) -> + not_found; +match_request([{_Name, Selector, Dispatch, _Link}|Rest], Req) -> + case Selector(Req) of + true -> Dispatch; + false -> match_request(Rest, Req) + end. + +list() -> + [{Path, Desc, Listener} || + {_P, Listener, Selectors, _F} <- ets:tab2list(?ETS), + {_N, _S, _H, {Path, Desc}} <- Selectors]. + +-spec listener_by_name(atom()) -> {ok, term()} | {error, not_found}. +listener_by_name(Name) -> + case [L || {_P, L, S, _F} <- ets:tab2list(?ETS), contains_name(Name, S)] of + [Listener] -> {ok, Listener}; + [] -> {error, not_found} + end. + +contains_name(Name, Selectors) -> + lists:member(Name, [N || {N, _S, _H, _L} <- Selectors]). + +list(Listener) -> + {ok, {Selectors, _Fallback}} = lookup_dispatch(Listener), + [{Path, Desc} || {_N, _S, _H, {Path, Desc}} <- Selectors]. + +%%--------------------------------------------------------------------------- + +listing_fallback_handler(Listener) -> + cowboy_router:compile([{'_', [ + {"/", rabbit_web_dispatch_listing_handler, Listener} + ]}]). diff --git a/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_sup.erl b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_sup.erl new file mode 100644 index 0000000000..1d270550c0 --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_sup.erl @@ -0,0 +1,131 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_web_dispatch_sup). + +-behaviour(supervisor). + +-define(SUP, ?MODULE). + +%% External exports +-export([start_link/0, ensure_listener/1, stop_listener/1]). + +%% supervisor callbacks +-export([init/1]). + +%% @spec start_link() -> ServerRet +%% @doc API for starting the supervisor. +start_link() -> + supervisor:start_link({local, ?SUP}, ?MODULE, []). + +ensure_listener(Listener) -> + case proplists:get_value(port, Listener) of + undefined -> + {error, {no_port_given, Listener}}; + _ -> + {Transport, TransportOpts, ProtoOpts} = preprocess_config(Listener), + ProtoOptsMap = maps:from_list(ProtoOpts), + StreamHandlers = stream_handlers_config(ProtoOpts), + rabbit_log:debug("Starting HTTP[S] listener with transport ~s, options ~p and protocol options ~p, stream handlers ~p", + [Transport, TransportOpts, ProtoOptsMap, StreamHandlers]), + CowboyOptsMap = + maps:merge(#{env => + #{rabbit_listener => Listener}, + middlewares => + [rabbit_cowboy_middleware, cowboy_router, cowboy_handler], + stream_handlers => StreamHandlers}, + ProtoOptsMap), + Child = ranch:child_spec(rabbit_networking:ranch_ref(Listener), 100, + Transport, TransportOpts, + cowboy_clear, CowboyOptsMap), + case supervisor:start_child(?SUP, Child) of + {ok, _} -> new; + {error, {already_started, _}} -> existing; + {error, {E, _}} -> check_error(Listener, E) + end + end. + +stop_listener(Listener) -> + Name = rabbit_networking:ranch_ref(Listener), + ok = supervisor:terminate_child(?SUP, {ranch_listener_sup, Name}), + ok = supervisor:delete_child(?SUP, {ranch_listener_sup, Name}). + +%% @spec init([[instance()]]) -> SupervisorTree +%% @doc supervisor callback. +init([]) -> + Registry = {rabbit_web_dispatch_registry, + {rabbit_web_dispatch_registry, start_link, []}, + transient, 5000, worker, dynamic}, + Log = {rabbit_mgmt_access_logger, {gen_event, start_link, + [{local, webmachine_log_event}]}, + permanent, 5000, worker, [dynamic]}, + {ok, {{one_for_one, 10, 10}, [Registry, Log]}}. + +%% +%% Implementation +%% + +preprocess_config(Options) -> + case proplists:get_value(ssl, Options) of + true -> _ = rabbit_networking:ensure_ssl(), + case rabbit_networking:poodle_check('HTTP') of + ok -> case proplists:get_value(ssl_opts, Options) of + undefined -> auto_ssl(Options); + _ -> fix_ssl(Options) + end; + danger -> {ranch_tcp, transport_config(Options), protocol_config(Options)} + end; + _ -> {ranch_tcp, transport_config(Options), protocol_config(Options)} + end. + +auto_ssl(Options) -> + {ok, ServerOpts} = application:get_env(rabbit, ssl_options), + Remove = [verify, fail_if_no_peer_cert], + SSLOpts = [{K, V} || {K, V} <- ServerOpts, + not lists:member(K, Remove)], + fix_ssl([{ssl_opts, SSLOpts} | Options]). + +fix_ssl(Options) -> + SSLOpts = proplists:get_value(ssl_opts, Options), + {ranch_ssl, + transport_config(Options ++ rabbit_networking:fix_ssl_options(SSLOpts)), + protocol_config(Options)}. + +transport_config(Options0) -> + Options = proplists:delete(protocol, + proplists:delete(ssl, + proplists:delete(ssl_opts, + proplists:delete(cowboy_opts, + Options0)))), + case proplists:get_value(ip, Options) of + undefined -> + Options; + IP when is_tuple(IP) -> + Options; + IP when is_list(IP) -> + {ok, ParsedIP} = inet_parse:address(IP), + [{ip, ParsedIP}|proplists:delete(ip, Options)] + end. + +protocol_config(Options) -> + proplists:get_value(cowboy_opts, Options, []). + +stream_handlers_config(Options) -> + case lists:keyfind(compress, 1, Options) of + {compress, false} -> [rabbit_cowboy_stream_h, cowboy_stream_h]; + %% Compress by default. Since 2.0 the compress option in cowboys + %% has been replaced by the cowboy_compress_h handler + %% Compress is not applied if data < 300 bytes + _ -> [rabbit_cowboy_stream_h, cowboy_compress_h, cowboy_stream_h] + end. + +check_error(Listener, Error) -> + Ignore = proplists:get_value(ignore_in_use, Listener, false), + case {Error, Ignore} of + {eaddrinuse, true} -> ignore; + _ -> exit({could_not_start_listener, Listener, Error}) + end. diff --git a/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_util.erl b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_util.erl new file mode 100644 index 0000000000..8aca673a48 --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/rabbit_web_dispatch_util.erl @@ -0,0 +1,55 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_web_dispatch_util). + +-export([parse_auth_header/1]). +-export([relativise/2, unrelativise/2]). + +%% @todo remove +parse_auth_header(Header) -> + case Header of + "Basic " ++ Base64 -> + Str = base64:mime_decode_to_string(Base64), + case string:chr(Str, $:) of + 0 -> invalid; + N -> [list_to_binary(string:sub_string(Str, 1, N - 1)), + list_to_binary(string:sub_string(Str, N + 1))] + end; + _ -> + invalid + end. + +relativise("/" ++ F, "/" ++ T) -> + From = string:tokens(F, "/"), + To = string:tokens(T, "/"), + string:join(relativise0(From, To), "/"). + +relativise0([H], [H|_] = To) -> + To; +relativise0([H|From], [H|To]) -> + relativise0(From, To); +relativise0(From, []) -> + lists:duplicate(length(From), ".."); +relativise0([_|From], To) -> + lists:duplicate(length(From), "..") ++ To; +relativise0([], To) -> + To. + +unrelativise(_, "/" ++ T) -> "/" ++ T; +unrelativise(F, "./" ++ T) -> unrelativise(F, T); +unrelativise(F, "../" ++ T) -> unrelativise(strip_tail(F), T); +unrelativise(F, T) -> case string:str(F, "/") of + 0 -> T; + _ -> strip_tail(F) ++ "/" ++ T + end. + +strip_tail("") -> exit(not_enough_to_strip); +strip_tail(S) -> case string:rstr(S, "/") of + 0 -> ""; + I -> string:left(S, I - 1) + end. diff --git a/deps/rabbitmq_web_dispatch/src/webmachine_log.erl b/deps/rabbitmq_web_dispatch/src/webmachine_log.erl new file mode 100644 index 0000000000..dd4da91924 --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/webmachine_log.erl @@ -0,0 +1,239 @@ +%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you under the Apache License, +%% Version 2.0 (the "License"); you may not use this file +%% except in compliance with the License. You may obtain +%% a copy of the License at +%% +%% https://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, +%% software distributed under the License is distributed on an +%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +%% KIND, either express or implied. See the License for the +%% specific language governing permissions and limitations +%% under the License. + +%% @doc Helper functions for webmachine's default log handlers + +-module(webmachine_log). + +-include("webmachine_logger.hrl"). + +-export([add_handler/2, + call/2, + call/3, + datehour/0, + datehour/1, + defer_refresh/1, + delete_handler/1, + fix_log/2, + fmt_ip/1, + fmtnow/0, + log_access/1, + log_close/3, + log_open/1, + log_open/2, + log_write/2, + maybe_rotate/3, + month/1, + refresh/2, + suffix/1, + zeropad/2, + zone/0]). + +-record(state, {hourstamp :: non_neg_integer(), + filename :: string(), + handle :: file:io_device()}). + +%% @doc Add a handler to receive log events +-type add_handler_result() :: ok | {'EXIT', term()} | term(). +-spec add_handler(atom() | {atom(), term()}, term()) -> add_handler_result(). +add_handler(Mod, Args) -> + gen_event:add_handler(?EVENT_LOGGER, Mod, Args). + +%% @doc Make a synchronous call directly to a specific event handler +%% module +-type error() :: {error, bad_module} | {'EXIT', term()} | term(). +-spec call(atom(), term()) -> term() | error(). +call(Mod, Msg) -> + gen_event:call(?EVENT_LOGGER, Mod, Msg). + +%% @doc Make a synchronous call directly to a specific event handler +%% module +-spec call(atom(), term(), timeout()) -> term() | error(). +call(Mod, Msg, Timeout) -> + gen_event:call(?EVENT_LOGGER, Mod, Msg, Timeout). + +%% @doc Return a four-tuple containing year, month, day, and hour +%% of the current time. +-type datehour() :: {calendar:year(), calendar:month(), calendar:day(), calendar:hour()}. +-spec datehour() -> datehour(). +datehour() -> + datehour(os:timestamp()). + +%% @doc Return a four-tuple containing year, month, day, and hour +%% of the specified time. +-spec datehour(erlang:timestamp()) -> datehour(). +datehour(TS) -> + {{Y, M, D}, {H, _, _}} = calendar:now_to_universal_time(TS), + {Y, M, D, H}. + +%% @doc Defer the refresh of a log file. +-spec defer_refresh(atom()) -> {ok, timer:tref()} | {error, term()}. +defer_refresh(Mod) -> + {_, {_, M, S}} = calendar:universal_time(), + Time = 1000 * (3600 - ((M * 60) + S)), + timer:apply_after(Time, ?MODULE, refresh, [Mod, os:timestamp()]). + +%% @doc Remove a log handler +-type delete_handler_result() :: term() | {error, module_not_found} | {'EXIT', term()}. +-spec delete_handler(atom() | {atom(), term()}) -> delete_handler_result(). +delete_handler(Mod) -> + gen_event:delete_handler(?EVENT_LOGGER, Mod, []). + +%% Seek backwards to the last valid log entry +-spec fix_log(file:io_device(), non_neg_integer()) -> ok. +fix_log(_FD, 0) -> + ok; +fix_log(FD, 1) -> + {ok, 0} = file:position(FD, 0), + ok; +fix_log(FD, Location) -> + case file:pread(FD, Location - 1, 1) of + {ok, [$\n | _]} -> + ok; + {ok, _} -> + fix_log(FD, Location - 1) + end. + +%% @doc Format an IP address or host name +-spec fmt_ip(undefined | string() | inet:ip4_address() | inet:ip6_address()) -> string(). +fmt_ip(IP) when is_tuple(IP) -> + inet_parse:ntoa(IP); +fmt_ip(undefined) -> + "0.0.0.0"; +fmt_ip(HostName) -> + HostName. + +%% @doc Format the current time into a string +-spec fmtnow() -> string(). +fmtnow() -> + {{Year, Month, Date}, {Hour, Min, Sec}} = calendar:local_time(), + io_lib:format("[~2..0w/~s/~4..0w:~2..0w:~2..0w:~2..0w ~s]", + [Date,month(Month),Year, Hour, Min, Sec, zone()]). + +%% @doc Notify registered log event handler of an access event. +-spec log_access(tuple()) -> ok. +log_access({_, _, _}=LogData) -> + gen_event:sync_notify(?EVENT_LOGGER, {log_access, LogData}). + +%% @doc Close a log file. +-spec log_close(atom(), string(), file:io_device()) -> ok | {error, term()}. +log_close(Mod, Name, FD) -> + error_logger:info_msg("~p: closing log file: ~p~n", [Mod, Name]), + file:close(FD). + +%% @doc Open a new log file for writing +-spec log_open(string()) -> {file:io_device(), non_neg_integer()}. +log_open(FileName) -> + DateHour = datehour(), + {log_open(FileName, DateHour), DateHour}. + +%% @doc Open a new log file for writing +-spec log_open(string(), non_neg_integer()) -> file:io_device(). +log_open(FileName, DateHour) -> + LogName = FileName ++ suffix(DateHour), + error_logger:info_msg("opening log file: ~p~n", [LogName]), + filelib:ensure_dir(LogName), + {ok, FD} = file:open(LogName, [read, write, raw]), + {ok, Location} = file:position(FD, eof), + fix_log(FD, Location), + file:truncate(FD), + FD. + +-spec log_write(file:io_device(), iolist()) -> ok | {error, term()}. +log_write(FD, IoData) -> + file:write(FD, lists:flatten(IoData)). + +%% @doc Rotate a log file if the hour it represents +%% has passed. +-spec maybe_rotate(atom(), erlang:timestamp(), #state{}) -> #state{}. +maybe_rotate(Mod, Time, State) -> + ThisHour = datehour(Time), + if ThisHour == State#state.hourstamp -> + State; + true -> + defer_refresh(Mod), + log_close(Mod, State#state.filename, State#state.handle), + Handle = log_open(State#state.filename, ThisHour), + State#state{hourstamp=ThisHour, handle=Handle} + end. + +%% @doc Convert numeric month value to the abbreviation +-spec month(1..12) -> string(). +month(1) -> + "Jan"; +month(2) -> + "Feb"; +month(3) -> + "Mar"; +month(4) -> + "Apr"; +month(5) -> + "May"; +month(6) -> + "Jun"; +month(7) -> + "Jul"; +month(8) -> + "Aug"; +month(9) -> + "Sep"; +month(10) -> + "Oct"; +month(11) -> + "Nov"; +month(12) -> + "Dec". + +%% @doc Make a synchronous call to instruct a log handler to refresh +%% itself. +-spec refresh(atom(), erlang:timestamp()) -> ok | {error, term()}. +refresh(Mod, Time) -> + call(Mod, {refresh, Time}, infinity). + +-spec suffix(datehour()) -> string(). +suffix({Y, M, D, H}) -> + YS = zeropad(Y, 4), + MS = zeropad(M, 2), + DS = zeropad(D, 2), + HS = zeropad(H, 2), + lists:flatten([$., YS, $_, MS, $_, DS, $_, HS]). + +-spec zeropad(integer(), integer()) -> string(). +zeropad(Num, MinLength) -> + NumStr = integer_to_list(Num), + zeropad_str(NumStr, MinLength - length(NumStr)). + +-spec zeropad_str(string(), integer()) -> string(). +zeropad_str(NumStr, Zeros) when Zeros > 0 -> + zeropad_str([$0 | NumStr], Zeros - 1); +zeropad_str(NumStr, _) -> + NumStr. + +-spec zone() -> string(). +zone() -> + Time = erlang:universaltime(), + LocalTime = calendar:universal_time_to_local_time(Time), + DiffSecs = calendar:datetime_to_gregorian_seconds(LocalTime) - + calendar:datetime_to_gregorian_seconds(Time), + zone((DiffSecs/3600)*100). + +%% Ugly reformatting code to get times like +0000 and -1300 + +-spec zone(integer()) -> string(). +zone(Val) when Val < 0 -> + io_lib:format("-~4..0w", [trunc(abs(Val))]); +zone(Val) when Val >= 0 -> + io_lib:format("+~4..0w", [trunc(abs(Val))]). diff --git a/deps/rabbitmq_web_dispatch/src/webmachine_log_handler.erl b/deps/rabbitmq_web_dispatch/src/webmachine_log_handler.erl new file mode 100644 index 0000000000..e19b247d73 --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/webmachine_log_handler.erl @@ -0,0 +1,128 @@ +%% Copyright (c) 2011-2013 Basho Technologies, Inc. All Rights Reserved. +%% +%% This file is provided to you under the Apache License, +%% Version 2.0 (the "License"); you may not use this file +%% except in compliance with the License. You may obtain +%% a copy of the License at +%% +%% https://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, +%% software distributed under the License is distributed on an +%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +%% KIND, either express or implied. See the License for the +%% specific language governing permissions and limitations +%% under the License. + +%% @doc Default log handler for webmachine + +-module(webmachine_log_handler). + +-behaviour(gen_event). + +%% gen_event callbacks +-export([init/1, + handle_call/2, + handle_event/2, + handle_info/2, + terminate/2, + code_change/3]). + +-include("webmachine_logger.hrl"). + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-endif. + +-record(state, {hourstamp, filename, handle}). + +-define(FILENAME, "access.log"). + +%% =================================================================== +%% gen_event callbacks +%% =================================================================== + +%% @private +init([BaseDir]) -> + webmachine_log:defer_refresh(?MODULE), + FileName = filename:join(BaseDir, ?FILENAME), + {Handle, DateHour} = webmachine_log:log_open(FileName), + {ok, #state{filename=FileName, handle=Handle, hourstamp=DateHour}}. + +%% @private +handle_call({_Label, MRef, get_modules}, State) -> + {ok, {MRef, [?MODULE]}, State}; +handle_call({refresh, Time}, State) -> + {ok, ok, webmachine_log:maybe_rotate(?MODULE, Time, State)}; +handle_call(_Request, State) -> + {ok, ok, State}. + +%% @private +handle_event({log_access, LogData}, State) -> + NewState = webmachine_log:maybe_rotate(?MODULE, os:timestamp(), State), + Msg = format_req(LogData), + webmachine_log:log_write(NewState#state.handle, Msg), + {ok, NewState}; +handle_event(_Event, State) -> + {ok, State}. + +%% @private +handle_info(_Info, State) -> + {ok, State}. + +%% @private +terminate(_Reason, _State) -> + ok. + +%% @private +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% =================================================================== +%% Internal functions +%% =================================================================== + +%% We currently keep most of the Webmachine logging facility. But +%% since we are now using Cowboy, a few small parts had to change. +%% This is one such part. The code is however equivalent to Webmachine's. + +format_req({Status0, Body, Req}) -> + User = user_from_req(Req), + Time = webmachine_log:fmtnow(), + Status = integer_to_list(Status0), + Length1 = case Body of + {sendfile, _, Length0, _} -> Length0; + _ -> iolist_size(Body) + end, + Length = integer_to_list(Length1), + Method = cowboy_req:method(Req), + Path = cowboy_req:path(Req), + Peer = case cowboy_req:peer(Req) of + {Peer0, _Port} -> Peer0; + Other -> Other + end, + Version = cowboy_req:version(Req), + Referer = cowboy_req:header(<<"referer">>, Req, <<>>), + UserAgent = cowboy_req:header(<<"user-agent">>, Req, <<>>), + fmt_alog(Time, Peer, User, Method, Path, Version, + Status, Length, Referer, UserAgent). + +fmt_alog(Time, Ip, User, Method, Path, Version, + Status, Length, Referrer, UserAgent) -> + [webmachine_log:fmt_ip(Ip), " - ", User, [$\s], Time, [$\s, $"], Method, " ", Path, + " ", atom_to_list(Version), [$",$\s], + Status, [$\s], Length, [$\s,$"], Referrer, + [$",$\s,$"], UserAgent, [$",$\n]]. + +user_from_req(Req) -> + try cowboy_req:parse_header(<<"authorization">>, Req) of + {basic, Username, _} -> + Username; + {bearer, _} -> + rabbit_data_coercion:to_binary( + application:get_env(rabbitmq_management, uaa_client_id, "")); + _ -> + "-" + catch _:_ -> + "-" + end. diff --git a/deps/rabbitmq_web_dispatch/src/webmachine_logger.hrl b/deps/rabbitmq_web_dispatch/src/webmachine_logger.hrl new file mode 100644 index 0000000000..c07068ae6a --- /dev/null +++ b/deps/rabbitmq_web_dispatch/src/webmachine_logger.hrl @@ -0,0 +1,16 @@ +-record(wm_log_data, + {resource_module :: atom(), + start_time :: tuple(), + method :: atom(), + headers, + peer, + path :: string(), + version, + response_code, + response_length, + end_time :: tuple(), + finish_time :: tuple(), + notes}). +-type wm_log_data() :: #wm_log_data{}. + +-define(EVENT_LOGGER, webmachine_log_event). |