diff --git a/.gitignore b/.gitignore index 073cc10..5acfa21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .eunit -doc/ deps *.o *.beam @@ -14,3 +13,6 @@ _build _checkout compile_commands.json rebar3 +doc/* +!doc/style.css +!doc/overview.edoc diff --git a/Makefile b/Makefile index 228a771..b69e3bc 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,15 @@ xref: $(REBAR) clean: $(REBAR) clean +## +## Doc targets +## +docs: $(REBAR) + $(REBAR) edoc + +edoc_private: $(REBAR) + $(REBAR) as edoc_private edoc + $(REBAR): $(ERL) -noshell -s inets -s ssl \ -eval '{ok, saved_to_file} = httpc:request(get, {"$(REBAR_URL)", []}, [], [{stream, "$(REBAR)"}])' \ diff --git a/doc/overview.edoc b/doc/overview.edoc new file mode 100644 index 0000000..adec61a --- /dev/null +++ b/doc/overview.edoc @@ -0,0 +1,5 @@ +@author Marc Worrell +@title cowmachine +@doc Webmachine for Zotonic and Cowboy. +@copyright Apache-2.0 +@reference This is an adaptation of webmachine for the Cowboy web server. \ No newline at end of file diff --git a/doc/style.css b/doc/style.css new file mode 100644 index 0000000..8015eb0 --- /dev/null +++ b/doc/style.css @@ -0,0 +1,75 @@ +/* standard EDoc style sheet */ +body { + font-family: Verdana, Arial, Helvetica, sans-serif; + margin-left: .25in; + margin-right: .2in; + margin-top: 0.2in; + margin-bottom: 0.2in; + color: #000000; + background-color: #ffffff; +} +h1,h2 { + margin-left: -0.2in; +} +div.navbar { + background-color: #add8e6; + padding: 0.2em; +} +h2.indextitle { + padding: 0.4em; + background-color: #add8e6; +} +h3.function,h3.typedecl { + background-color: #add8e6; + padding-left: 1em; +} +div.spec { + margin-left: 2em; + + background-color: #eeeeee; +} +a.module { + text-decoration:none +} +a.module:hover { + background-color: #eeeeee; +} +ul.definitions { + list-style-type: none; +} +ul.index { + list-style-type: none; + background-color: #eeeeee; +} + +/* + * Minor style tweaks + */ +ul { + list-style-type: square; +} +table { + border-collapse: collapse; +} +td { + padding: 3px; + vertical-align: middle; +} + +/* +Tune styles +*/ + +table[summary="navigation bar"] { + background-image: url('http://zotonic.com/lib/images/logo.png'); + background-repeat: no-repeat; + background-position: center; +} + +code, p>tt, a>tt { + font-size: 1.2em; +} + +p { + line-height: 1.5; +} diff --git a/rebar.config b/rebar.config index ed128c7..5e23d28 100644 --- a/rebar.config +++ b/rebar.config @@ -18,12 +18,28 @@ ]}, {xref_ignores, [ - ]}, - - {dialyzer, [ + ]} + ]}, + {edoc_private, [ + {edoc_opts, [ + {private, true} + ]} + ]}, + {check, [ + {dialyzer, [ {warnings, [ no_return ]} - ]} - ]} + ]}, + + {erl_opts, [ + debug_info + ]} + ] + } +]}. + + +{edoc_opts, [ + {preprocess, true}, {stylesheet, "style.css"} ]}. diff --git a/src/cowmachine.erl b/src/cowmachine.erl index ebdba49..e617215 100644 --- a/src/cowmachine.erl +++ b/src/cowmachine.erl @@ -2,6 +2,7 @@ %% @copyright 2016-2022 Marc Worrell %% %% @doc Cowmachine: webmachine middleware for Cowboy/Zotonic +%% @end %% Copyright 2016-2022 Marc Worrell %% @@ -34,11 +35,15 @@ -include("cowmachine_state.hrl"). -include("cowmachine_log.hrl"). +%% @private +-export_type([cmstate/0]). + %% @doc Cowboy middleware, route the new request. Continue with the cowmachine, %% requests a redirect or return a 400 on an unknown host. --spec execute(Req, Env) -> {ok, Req, Env} | {stop, Req} +-spec execute(Req, Env) -> Result when Req :: cowboy_req:req(), - Env :: cowboy_middleware:env(). + Env :: cowboy_middleware:env(), + Result :: {ok, Req, Env} | {stop, Req}. execute(Req, #{ cowmachine_controller := _Controller } = Env) -> ContextEnv = maps:get(cowmachine_context, Env, undefined), Context = cowmachine_req:init_context(Req, Env, ContextEnv), @@ -47,10 +52,13 @@ execute(Req, #{ cowmachine_controller := _Controller } = Env) -> %% @doc Handle a request, executes the cowmachine http states. Can be used by middleware %% functions to add some additional initialization of controllers or context. --spec request(Context, Options::map()) -> {ok, Req, Env} | {stop, Req} + +-spec request(Context, Options) -> Result when Context :: cowmachine_req:context(), - Req :: cowboy_req:req(), - Env :: cowboy_middleware:env(). + Options :: map(), + Req :: cowboy_req:req(), + Env :: cowboy_middleware:env(), + Result :: {ok, Req, Env} | {stop, Req}. request(Context, Options) -> Req = cowmachine_req:req(Context), Env = cowmachine_req:env(Context), @@ -62,6 +70,15 @@ request(Context, Options) -> Other end. +-spec request_1(Controller, Req, Env, Options, Context) -> Result when + Controller :: atom(), + Req :: cowboy_req:req(), + Env :: cowboy_middleware:env(), + Options :: map(), + Context :: cowmachine_req:context(), + Result :: {upgrade, UpgradeFun, State, Context} | cowboy_middleware:env() | any(), + UpgradeFun :: atom(), + State :: cmstate(). request_1(Controller, Req, Env, Options, Context) -> State = #cmstate{ controller = Controller, @@ -69,7 +86,7 @@ request_1(Controller, Req, Env, Options, Context) -> options = Options }, Site = maps:get(site, Env, undefined), - try + ReqResult = try EnvInit = cowmachine_req:init_env(Req, Env), Context1 = cowmachine_req:set_env(EnvInit, Context), case cowmachine_decision_core:handle_request(State, Context1) of @@ -116,9 +133,21 @@ request_1(Controller, Req, Env, Options, Context) -> class => Class, reason => Reason, stack => Stacktrace}, Req), handle_stop_request(500, Site, {throw, {Reason, Stacktrace}}, Req, Env, State, Context) - end. + end, + ReqResult. + % @todo add the error controller as an application env, if not defined then just terminate with the corresponding error code. + +-spec handle_stop_request(ResponseCode, Site, Reason, Req, Env, State, Context) -> Result when + ResponseCode :: integer(), + Site :: atom() | undefined, + Reason :: any(), + Req :: cowboy_req:req(), + Env :: cowboy_middleware:env(), + State :: cmstate(), + Context :: cowmachine_req:context(), + Result :: {ok, Req, Env} | {stop, Req}. handle_stop_request(ResponseCode, _Site, Reason, Req, Env, State, Context) -> State1 = State#cmstate{ controller = controller_http_error @@ -154,13 +183,19 @@ handle_stop_request(ResponseCode, _Site, Reason, Req, Env, State, Context) -> %% %% Logging %% - +-spec log(Report) -> Result when + Report :: map(), + Result :: any(). log(#{ level := Level } = Report) -> log_report(Level, Report#{ in => cowmachine, node => node() }). +-spec log(Report, Req) -> Result when + Report :: map(), + Req :: map(), + Result :: any(). log(#{ level := Level } = Report, Req) when is_map(Req) -> Report1 = lists:foldl(fun({Key, Fun}, Acc) -> case Fun(Req) of @@ -175,6 +210,10 @@ log(#{ level := Level } = Report, Req) when is_map(Req) -> node => node() }). +-spec log_report(LogLevel, Report) -> Result when + LogLevel :: debug | info | notice | warning | error, + Report :: map(), + Result :: any(). log_report(debug, Report) when is_map(Report) -> ?LOG_DEBUG(Report); log_report(info, Report) when is_map(Report) -> @@ -186,16 +225,36 @@ log_report(warning, Report) when is_map(Report) -> log_report(error, Report) when is_map(Report) -> ?LOG_ERROR(Report). +-spec src(IpInfo) -> Result when + IpInfo :: #{ peer := {IP, Port} } | any(), + Port :: integer(), + IP :: tuple(), + Result :: {ok, map()} | undefined. src(#{ peer := {IP, Port} }) -> {ok, ip_info(IP, Port)}; src(_) -> undefined. +-spec dst(DstInfo) -> Result when + DstInfo :: #{ sock := {IP, Port} } | #{ port := Port } | any(), + IP :: tuple(), + Port :: integer(), + Result :: {ok, #{IPType => string(), port => Port}} | {ok, #{ port => Port }} | undefined, + IPType :: ip4 | ip6. dst(#{ sock := {IP, Port} } ) -> {ok, ip_info(IP, Port)}; dst(#{ port := Port }) -> {ok, #{ port => Port }}; dst(_) -> undefined. +-spec path(PathInfo) -> Result when + PathInfo :: #{ path := Path } | any(), + Path :: any(), + Result :: {ok, Path} | undefined. path(#{ path := Path }) -> {ok, Path}; path(_) -> undefined. +-spec ip_info(IP, Port) -> Result when + IP :: tuple(), + Port :: integer(), + IPType :: ip4 | ip6, + Result :: #{IPType => string(), port => Port}. ip_info(IP, Port) -> IPType = case tuple_size(IP) of 4 -> ip4; 8 -> ip6 end, #{IPType => inet_parse:ntoa(IP), port => Port}. diff --git a/src/cowmachine_accept_language.erl b/src/cowmachine_accept_language.erl index 6bd737c..bf6de4e 100644 --- a/src/cowmachine_accept_language.erl +++ b/src/cowmachine_accept_language.erl @@ -2,6 +2,7 @@ %% @copyright 2017-2019 Marc Worrell %% %% @doc Accept-Language handling. +%% @end %% Copyright 2017-2019 Marc Worrell %% @@ -26,9 +27,10 @@ accept_list/2 ]). - --spec accept_header([{binary(),[ binary() ]}], cowmachine_req:context()|binary()|undefined) -> - {ok, binary()} | {error, nomatch|header}. +-spec accept_header(AvailableLangs, AcceptHeader) -> Result when + AvailableLangs :: [{binary(),[ binary() ]}], + AcceptHeader :: cowmachine_req:context() | binary() | undefined, + Result :: {ok, binary()} | {error, nomatch | header}. accept_header(_AvailableLangs, undefined) -> {error, nomatch}; accept_header(AvailableLangs, AcceptHeader) when is_binary(AcceptHeader) -> @@ -46,6 +48,9 @@ accept_header(AvailableLangs, AcceptHeader) when is_binary(AcceptHeader) -> accept_header(AvailableLangs, Context) -> accept_header(AvailableLangs, cowmachine_req:get_req_header(<<"accept-language">>, Context)). +-spec parse(AcceptHeader) -> Result when + AcceptHeader :: binary(), + Result :: [{binary(), cow_http_hd:qvalue()}] | error. parse(AcceptHeader) -> try cow_http_hd:parse_accept_language(AcceptHeader) @@ -54,8 +59,10 @@ parse(AcceptHeader) -> end. --spec accept_list([{binary(), [ binary() ]}], [binary()]) -> - {ok, binary()} | {error, nomatch}. +-spec accept_list(AvailableLangs, AcceptableLangs) -> Result when + AvailableLangs :: [{binary(), [ binary() ]}], + AcceptableLangs :: [binary()], + Result :: {ok, binary()} | {error, nomatch}. accept_list(AvailableLangs, AcceptableLangs) -> case match_language(AvailableLangs, AcceptableLangs) of {ok, _} = OK -> OK; @@ -68,10 +75,16 @@ accept_list(AvailableLangs, AcceptableLangs) -> end end. +-spec sort_accept(List) -> Result when + List :: list(), + Result :: list(). sort_accept([]) -> []; sort_accept(List) -> lists:keysort(2, fix_order(List,1,[])). +-spec ensure_baselangs(Langs) -> Result when + Langs :: list(), + Result :: list(). ensure_baselangs(Langs) -> lists:foldl( fun @@ -90,11 +103,24 @@ ensure_baselangs(Langs) -> % Modify the priority so that for languages with equal priority the first mentioned % will be chosen. + +-spec fix_order(LangList, N, Acc) -> Result when + LangList :: [LangItem], + LangItem :: {Lang, Prio}, + Lang :: binary(), + Prio :: integer(), + N :: non_neg_integer(), + Acc :: LangList, + Result :: Acc. fix_order([], _N, Acc) -> Acc; fix_order([{Lang,Prio}|Langs], N, Acc) -> fix_order(Langs, N+1, [{Lang, Prio*100-N}|Acc]). +-spec match_language(AvailableLangs, AcceptList) -> Result when + AvailableLangs :: [{binary(), [ binary() ]}], + AcceptList :: [binary()], + Result :: {ok, binary()} | error. match_language(AvailableLangs, AcceptList) -> case firstmap(fun(Lang) -> available_language(Lang, AvailableLangs) end, AcceptList) of {ok, _} = OK -> OK; @@ -116,6 +142,11 @@ available_language(Lang, AvailableLangs) -> false -> error end. +-spec fallback_language(Lang, [{AvailableLang,FallbackLangs}]) -> Result when + Lang :: binary(), + AvailableLang :: binary(), + FallbackLangs :: [binary()], + Result :: error | {ok, AvailableLang}. fallback_language(_Lang, []) -> error; fallback_language(Lang, [{AvailableLang,FallbackLangs}|AvailableLangs]) -> @@ -124,6 +155,9 @@ fallback_language(Lang, [{AvailableLang,FallbackLangs}|AvailableLangs]) -> false -> fallback_language(Lang, AvailableLangs) end. +-spec main_languages(Accept) -> Result when + Accept :: [binary()], + Result :: [binary()]. main_languages(Accept) -> Accept1 = lists:foldl( fun diff --git a/src/cowmachine_controller.erl b/src/cowmachine_controller.erl index 6f4417a..984cef7 100644 --- a/src/cowmachine_controller.erl +++ b/src/cowmachine_controller.erl @@ -1,6 +1,7 @@ %% @author Justin Sheehy %% @author Andy Gross %% @copyright 2007-2009 Basho Technologies +%% @end %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -26,6 +27,13 @@ -include("cowmachine_state.hrl"). +-export_type([cmstate/0]). + +%% @doc Get default value by Key. +-spec default(DefaultID, Context) -> Result when + DefaultID:: service_available | resource_exists | auth_required | is_authorized | forbidden | upgrades_provided | allow_missing_post | malformed_request | uri_too_long | known_content_type | valid_content_headers | valid_entity_length | options | allowed_methods | known_methods | validate_content_checksum | content_types_provided | content_types_accepted | delete_resource | delete_completed | post_is_create | create_path | base_uri | process_post | language_available | charsets_provided | content_encodings_provided | transfer_encodings_provided | variances | is_conflict | multiple_choices | previously_existed | moved_permanently | moved_temporarily | last_modified | expires | generate_etag | finish_request, + Context :: cowmachine_req:context(), + Result :: no_charset | no_default | undefined | boolean() | list(binary()). default(service_available, _Context) -> true; default(resource_exists, _Context) -> @@ -124,6 +132,10 @@ default(_, _Context) -> %% @doc Content types that are textual and should have a charset defined. + +-spec is_text(ContentType) -> Result when + ContentType :: cow_http_hd:media_type(), + Result :: boolean(). is_text({<<"text">>, _, _}) -> true; is_text({<<"application">>, <<"x-javascript">>, _}) -> true; is_text({<<"application">>, <<"javascript">>, _}) -> true; @@ -134,6 +146,14 @@ is_text(_Mime) -> false. %% @TODO Re-add logging code +%% @doc Export and run function `Fun'. + +-spec do(Fun, State, Context) -> Result when + Fun :: atom(), + State :: cmstate(), + Context :: cowmachine_req:context(), + Result :: {ContentType, Context}, + ContentType :: cow_http_hd:media_type(). do(Fun, #cmstate{ controller = Controller }, Context) when is_atom(Fun) -> case erlang:function_exported(Controller, Fun, 1) of true -> @@ -145,6 +165,15 @@ do(Fun, #cmstate{ controller = Controller }, Context) when is_atom(Fun) -> end end. +%% @doc Export and process `State' with `Context'. + +-spec do_process(ContentType, State, Context) -> Result when + ContentType :: cow_http_hd:media_type(), + State :: cmstate(), + Context :: cowmachine_req:context(), + Result :: {Res, Context}, + Res :: boolean() | cowmachine_req:halt() | {error, any(), any()} | {error, any()} | + cowmachine_req:resp_body(). do_process(ContentType, #cmstate{ controller = Controller }, Context) -> case erlang:function_exported(Controller, process, 4) of true -> diff --git a/src/cowmachine_decision_core.erl b/src/cowmachine_decision_core.erl index c6bfed0..1b6f7d1 100644 --- a/src/cowmachine_decision_core.erl +++ b/src/cowmachine_decision_core.erl @@ -2,6 +2,7 @@ %% @author Andy Gross %% @author Bryan Fink %% @copyright 2007-2009 Basho Technologies +%% @end %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -31,12 +32,28 @@ -include("cowmachine_state.hrl"). -include("cowmachine_log.hrl"). +-export_type([cmstate/0]). + +%% @doc Handle Cowmachine state request. +%% @throws {stop_request, Code, Reason} + +-spec handle_request(CmState, Context) -> Result when + CmState :: cmstate(), + Context :: cowmachine_req:context(), + Result :: {atom(), StateResult, Context} | {upgrade, UpgradeFun, StateResult, Context}, + StateResult :: CmState, + UpgradeFun :: atom(). handle_request(#cmstate{ controller = Controller } = CmState, Context) -> code:ensure_loaded(Controller), d(v3b13, CmState, Context). %% @doc Call the controller --spec controller_call(atom(), #cmstate{}, term()) -> {term(), #cmstate{}, term()}. + +-spec controller_call(Callback, State, Context) -> Result when + Callback :: atom(), + State :: cmstate(), + Context :: term(), + Result :: {term(), #cmstate{}, term()}. controller_call(Callback, #cmstate{cache=Cache} = State, Context) -> case is_cacheable(Callback) of true -> @@ -53,6 +70,10 @@ controller_call(Callback, #cmstate{cache=Cache} = State, Context) -> {T, State, Context1} end. +-spec is_cacheable(Callback) -> Result when + Callback :: charsets_provided | content_types_provided | content_encodings_provided | + last_modified | generate_etag | any(), + Result :: boolean(). is_cacheable(charsets_provided) -> true; is_cacheable(content_types_provided) -> true; is_cacheable(content_encodings_provided) -> true; @@ -60,15 +81,35 @@ is_cacheable(last_modified) -> true; is_cacheable(generate_etag) -> true; is_cacheable(_) -> false. +-spec controller_call_process(ContentType, State, Context) -> Result when + ContentType :: cow_http_hd:media_type(), + State :: cmstate(), + Context :: cowmachine_req:context(), + Result :: {boolean() | ContentType, State, Context}. controller_call_process(ContentType, State, Context) -> {T, Context1} = cowmachine_controller:do_process(ContentType, State, Context), {T, State, Context1}. - +-spec d(DecisionID, State, Context) -> Result when + DecisionID :: v3b13 | v3b12 | v3b11 | v3b10 | v3b9 | v3b9a | v3b9b | v3b8 | v3b7 | v3b6_upgrade | v3b6 | v3b5 | v3b4 | v3b3 | v3c3 | v3c4 | v3d4 | v3d5 | v3e5 | v3e6 | v3f6 | v3f7 | v3g7 | v3g8 | v3g9 | v3g11 | v3h7 | v3h10 | v3h11 | v3h12 | v3i4 | v3i7 | v3i12 | v3i13 | v3j18 | v3k5 | v3k7 | v3k13 | v3l5 | v3l7 | v3l13 | v3l14 | v3l15 | v3l17 | v3m5 | v3m7 | v3m16 | v3m20 | v3m20b | v3n5 | v3n11 | v3n16 | v3o14 | v3o16 | v3o18 | v3o18b | v3o20 | v3p3 | v3p11, + State :: cmstate(), + Context :: cowmachine_req:context(), + Result :: {atom(), StateResult, Context} | {upgrade, UpgradeFun, StateResult, Context}, + StateResult :: State, + UpgradeFun :: atom(). d(DecisionID, State, Context) -> % webmachine_controller:log_d(Rs, DecisionID), decision(DecisionID, State, Context). +%% @doc Cowmachine response. +%% @throws {stop_request, Code} + +-spec respond(Code, State, Context) -> Result when + Code :: integer(), + State :: cmstate(), + Context :: cowmachine_req:context(), + %Result :: {State, Context}. + Result :: {term(), #cmstate{}, term()}. respond(Code, State, Context) -> {State1, Context1} = case Code of Ok when Ok >= 200, Ok =< 299 -> @@ -112,10 +153,18 @@ respond(Code, State, Context) -> Context3 = cowmachine_req:set_response_code(Code, Context2), controller_call(finish_request, State2, Context3). +-spec respond(Code, Headers, State, Context) -> Result when + Code :: integer(), + Headers :: [{binary(), binary()}], + State :: cmstate(), + Context :: cowmachine_req:context(), + Result :: {term(), #cmstate{}, term()}. respond(Code, Headers, State, Context) -> ContextHs = cowmachine_req:set_resp_headers(Headers, Context), respond(Code, State, ContextHs). +%% @throws {stop_request, Code, Reason} + error_response(Code, Reason, State, Context) -> controller_call(finish_request, State, Context), throw({stop_request, Code, Reason}). @@ -565,10 +614,10 @@ decision(v3n11, State, Context) -> {S2, PathCtx} end, {Res, S4, C4} = accept_process_helper(LocS, LocCtx), - case Res of - {halt, Code} -> respond(Code, S4, C4); + case Res of {error, _,_} -> error_response(Res, S4, C4); {error, _} -> error_response(Res, S4, C4); + {halt, Code} -> respond(Code, S4, C4); _ -> {stage1_ok, S4, C4} end; undefined -> @@ -687,8 +736,9 @@ decision(v3p11, State, Context) -> end. -%% Check if the request content-type is acceptable - if acceptable then also call the +%% @doc Check if the request content-type is acceptable - if acceptable then also call the %% controller's process function. + accept_process_helper(State, Context) -> case cowmachine_req:has_req_body(Context) orelse should_have_req_body(Context) of true -> @@ -728,8 +778,8 @@ process_helper(ContentTypeAccepted, State, Context) -> {halt, _} -> Result; {error, _, _} -> Result; {error, _} -> Result; - true -> Result; - false -> Result; + true when is_boolean(Res) -> Result; + false when is_boolean(Res) -> Result; RespBody -> C3 = cowmachine_req:set_resp_body(RespBody, C2), {body, S2, C3} diff --git a/src/cowmachine_proxy.erl b/src/cowmachine_proxy.erl index e640be0..0136ddf 100644 --- a/src/cowmachine_proxy.erl +++ b/src/cowmachine_proxy.erl @@ -1,7 +1,10 @@ %% @author Marc Worrell %% @copyright 2016-2019 Marc Worrell %% -%% @doc Middleware to update proxy settings in the Cowboy Req +%% @doc Middleware to update proxy settings in the Cowboy Req. +%% @reference See more information related to Cowboy Req at +%% cowboy_req(3). +%% @end %% Copyright 2016-2019 Marc Worrell %% @@ -31,13 +34,21 @@ -include("cowmachine_log.hrl"). %% @doc Cowboy middleware, route the new request. Continue with the cowmachine, -%% requests a redirect or return a 400 on an unknown host. --spec execute(Req, Env) -> {ok, Req, Env} | {stop, Req} - when Req::cowboy_req:req(), Env::cowboy_middleware:env(). +%% requests a redirect or return a `400' on an unknown host. + +-spec execute(Req, Env) -> Result when + Req :: cowboy_req:req(), + Env :: cowboy_middleware:env(), + Result :: {ok, Req, Env} | {stop, Req}. execute(Req, Env) -> {ok, Req, update_env(Req, Env)}. --spec update_env(cowboy_req:req(), cowboy_middleware:env()) -> cowboy_middleware:env(). +%% @doc Update the environment based on the content of the request. + +-spec update_env(Req, Env) -> Result when + Req :: cowboy_req:req(), + Env :: cowboy_middleware:env(), + Result :: cowboy_middleware:env(). update_env(Req, Env) -> case cowboy_req:header(<<"forwarded">>, Req) of undefined -> @@ -51,7 +62,12 @@ update_env(Req, Env) -> update_env_proxy(Forwarded, Req, Env) end. -%% @doc Fetch the metadata from the request itself +%% @doc Fetch the metadata from the request itself. + +-spec update_env_direct(Req, Env) -> Result when + Req :: cowboy_req:req(), + Env :: cowboy_middleware:env(), + Result :: cowboy_middleware:env(). update_env_direct(Req, Env) -> {Peer, _Port} = cowboy_req:peer(Req), Env#{ @@ -63,7 +79,13 @@ update_env_direct(Req, Env) -> cowmachine_remote => list_to_binary(inet_parse:ntoa(Peer)) }. -%% @doc Handle the "Forwarded" header, added by the proxy. +%% @doc Handle the `Forwarded' header, added by the proxy. + +-spec update_env_proxy(Forwarded, Req, Env) -> Result when + Forwarded :: binary(), + Req :: cowboy_req:req(), + Env :: cowboy_middleware:env(), + Result :: cowboy_middleware:env(). update_env_proxy(Forwarded, Req, Env) -> {Peer, _Port} = cowboy_req:peer(Req), case is_trusted_proxy(Peer) of @@ -104,7 +126,8 @@ update_env_proxy(Forwarded, Req, Env) -> update_env_direct(Req, Env) end. -%% @doc Handle the "X-Forwarded-For" header, added by the proxy. +%% @doc Handle the `X-Forwarded-For' header, added by the proxy. + update_env_old_proxy(XForwardedFor, Req, Env) -> {Peer, _Port} = cowboy_req:peer(Req), case is_trusted_proxy(Peer) of @@ -145,15 +168,27 @@ update_env_old_proxy(XForwardedFor, Req, Env) -> end. +-spec trim(String) -> Result when + String :: undefined | iodata(), + Result :: undefined | binary(). trim(undefined) -> undefined; trim(S) -> z_string:trim(S). +-spec parse_host(Host) -> Result when + Host :: undefined | binary(), + Result :: undefined | binary(). parse_host(undefined) -> undefined; parse_host(Host) -> {Host1, _} = cow_http_hd:parse_host(Host), sanitize_host(Host1). +-spec parse_for(For, Req) -> Result when + For :: undefined | binary(), + Req :: cowboy_req:req(), + Result :: {Host, Adr}, + Host :: binary(), + Adr :: inet:ip_address(). parse_for(undefined, Req) -> {Peer, _Port} = cowboy_req:peer(Req), {list_to_binary(inet_parse:ntoa(Peer)), Peer}; @@ -171,44 +206,95 @@ parse_for(For, Req) -> {Peer, sanitize(For)} end. +%% @equiv sanitize(For, <<>>) + +-spec sanitize(For) -> Result when + For :: binary(), + Result :: binary(). sanitize(For) -> sanitize(For, <<>>). +-spec sanitize(For, Acc) -> Result when + For :: binary(), + Acc :: binary(), + Result :: binary(). sanitize(<<>>, Acc) -> Acc; sanitize(<>, Acc) when ?IS_URI_UNRESERVED(C) -> sanitize(Rest, <>); sanitize(<<_, Rest/binary>>, Acc) -> sanitize(Rest, <>). --spec parse_forwarded(binary()) -> [{binary(), binary()}]. +%% @equiv forwarded_list(Header, []) + +-spec parse_forwarded(Header) -> Result when + Header :: binary(), + Result :: [{binary(), binary()}]. parse_forwarded(Header) when is_binary(Header) -> forwarded_list(Header, []). +-spec forwarded_list(Header, Acc) -> Result when + Header :: binary(), + Acc :: [{binary(),binary()}], + Result :: [{binary(),binary()}]. forwarded_list(<<>>, Acc) -> lists:reverse(Acc); forwarded_list(<<$,, R/bits>>, _Acc) -> forwarded_list(R, []); forwarded_list(<< C, R/bits >>, Acc) when ?IS_WS(C) -> forwarded_list(R, Acc); forwarded_list(<< $;, R/bits >>, Acc) -> forwarded_list(R, Acc); forwarded_list(<< C, R/bits >>, Acc) when ?IS_ALPHANUM(C) -> forwarded_pair(R, Acc, << (lower(C)) >>). +-spec forwarded_pair(Header, Acc, T) -> Result when + Header :: binary(), + Acc :: [{binary(),binary()}], + T :: binary(), + Result :: [{binary(),binary()}]. forwarded_pair(<< C, R/bits >>, Acc, T) when ?IS_ALPHANUM(C) -> forwarded_pair(R, Acc, << T/binary, (lower(C)) >>); forwarded_pair(R, Acc, T) -> forwarded_pair_eq(R, Acc, T). +-spec forwarded_pair_eq(Header, Acc, T) -> Result when + Header :: binary(), + Acc :: [{binary(),binary()}], + T :: binary(), + Result :: [{binary(),binary()}]. forwarded_pair_eq(<< C, R/bits >>, Acc, T) when ?IS_WS(C) -> forwarded_pair_eq(R, Acc, T); forwarded_pair_eq(<< $=, R/bits >>, Acc, T) -> forwarded_pair_value(R, Acc, T). +-spec forwarded_pair_value(Header, Acc, T) -> Result when + Header :: binary(), + Acc :: [{binary(),binary()}], + T :: binary(), + Result :: [{binary(),binary()}]. forwarded_pair_value(<< C, R/bits>>, Acc, T) when ?IS_WS(C) -> forwarded_pair_value(R, Acc, T); forwarded_pair_value(<< $", R/bits>>, Acc, T) -> forwarded_pair_value_quoted(R, Acc, T, <<>>); forwarded_pair_value(<< C, R/bits>>, Acc, T) -> forwarded_pair_value_token(R, Acc, T, << (lower(C)) >>). +-spec forwarded_pair_value_token(Header, Acc, T, V) -> Result when + Header :: binary(), + Acc :: [{binary(),binary()}], + T :: binary(), + V :: binary(), + Result :: [{binary(),binary()}]. forwarded_pair_value_token(<< C, R/bits>>, Acc, T, V) when ?IS_TOKEN(C) -> forwarded_pair_value_token(R, Acc, T, << V/binary, (lower(C)) >>); forwarded_pair_value_token(R, Acc, T, V) -> forwarded_list(R, [{T, V}|Acc]). +-spec forwarded_pair_value_quoted(Header, Acc, T, V) -> Result when + Header :: binary(), + Acc :: [{binary(),binary()}], + T :: binary(), + V :: binary(), + Result :: [{binary(),binary()}]. forwarded_pair_value_quoted(<< $", R/bits >>, Acc, T, V) -> forwarded_list(R, [{T, V}|Acc]); forwarded_pair_value_quoted(<< $\\, C, R/bits >>, Acc, T, V) -> forwarded_pair_value_quoted(R, Acc, T, << V/binary, (lower(C)) >>); forwarded_pair_value_quoted(<< C, R/bits >>, Acc, T, V) -> forwarded_pair_value_quoted(R, Acc, T, << V/binary, (lower(C)) >>). +-spec lower(Character) -> Result when + Character :: char(), + Result :: char(). lower(C) when C >= $A, C =< $Z -> C + 32; lower(C) -> C. %% @doc Check if the given proxy is trusted. + +-spec is_trusted_proxy(Peer) -> Result when + Peer :: inet:ip_address(), + Result :: boolean(). is_trusted_proxy(Peer) -> case application:get_env(cowmachine, proxy_allowlist) of {ok, ProxyAllowlist} -> @@ -217,6 +303,12 @@ is_trusted_proxy(Peer) -> is_trusted_proxy(local, Peer) end. +-spec is_trusted_proxy(Marker, Peer) -> Result when + Marker :: ProxyMarker | ProxyAllowlist, + ProxyMarker :: any | ip_whitelist | local | none, + ProxyAllowlist :: list() | binary(), + Peer :: inet:ip_address(), + Result :: boolean(). is_trusted_proxy(none, _Peer) -> false; is_trusted_proxy(any, _Peer) -> @@ -236,12 +328,20 @@ is_trusted_proxy(Allowlist, Peer) when is_list(Allowlist); is_binary(Allowlist) % Extra host sanitization as cowboy is too lenient. % Cowboy did already do the lowercasing of the hostname + +-spec sanitize_host(Host) -> Result when + Host :: binary(), + Result :: binary(). sanitize_host(<<$[, _/binary>> = Host) -> % IPv6 address, sanitized by cowboy Host; sanitize_host(Host) -> sanitize_host(Host, <<>>). +-spec sanitize_host(Host, Acc) -> Result when + Host :: binary(), + Acc :: binary(), + Result :: binary(). sanitize_host(<<>>, Acc) -> Acc; sanitize_host(<>, Acc) when C >= $a, C =< $z -> sanitize_host(Rest, <>); sanitize_host(<>, Acc) when C >= $0, C =< $9 -> sanitize_host(Rest, <>); diff --git a/src/cowmachine_req.erl b/src/cowmachine_req.erl index d355695..63bea12 100644 --- a/src/cowmachine_req.erl +++ b/src/cowmachine_req.erl @@ -1,7 +1,10 @@ %% @author Marc Worrell %% @copyright 2016-2019 Marc Worrell %% -%% @doc Request functions for cowmachine +%% @doc Request functions for cowmachine. +%% @reference See more information related to Cowboy HTTP server for Erlang/OTP at +%% Cowboy. +%% @end %% Copyright 2016-2019 Marc Worrell %% @@ -117,7 +120,9 @@ any() => any() }. +-type context() :: context_map() | tuple(). %% The request context stores everything needed for the request handling. +%% %% Inside Zotonic all functions work with a site specific context, this %% context has optionally a request. That is why the context is wrapped around %% the cowboy request. @@ -125,12 +130,11 @@ %% The cowmachine context must be the cowreq record, a tuple or a map. %% If it is a tuple then it is assumed to be a record then the cowreq %% is at position 2 and the cowenv at position 3. --type context() :: context_map() | tuple(). -%% Used to stop a request with a specific HTTP status code + -type halt() :: {error, term()} | {halt, 200..599}. +%% Used to stop a request with a specific HTTP status code -%% Response body, can be data, a file, device or streaming functions. -type resp_body() :: iodata() | {device, Size::non_neg_integer(), file:io_device()} | {device, file:io_device()} @@ -141,13 +145,14 @@ | {stream, streamfun()} | {stream, Size::non_neg_integer(), streamfun()} | {writer, writerfun()} - | undefined. + | undefined. +%% Response body, can be data, a file, device or streaming functions. -%% Streaming function, repeatedly called to fetch the next chunk -type streamfun() :: fun( ( parts(), context() ) -> {streamdata(), streamfun_next()} ) | fun( ( context() ) -> {streamdata(), streamfun_next()} ) | fun( () -> {streamdata(), streamfun_next()} ) | done. +%% Streaming function, repeatedly called to fetch the next chunk -type streamfun_next() :: fun( ( context() ) -> {streamdata(), streamfun_next()} ) | fun( () -> {streamdata(), streamfun_next()} ) @@ -157,15 +162,16 @@ | {file, non_neg_integer(), file:filename_all()} | {file, file:filename_all()}. -%% Writer function, calls output function till finished -type writerfun() :: fun( (outputfun(), context()) -> context() ). +%% Writer function, calls output function till finished + -type outputfun() :: fun( (iodata(), IsFinal::boolean(), context()) -> context() ). -%% Media types for accepted and provided content types -type media_type() :: binary() | {binary(), binary(), list( {binary(), binary()} )} | {binary(), binary()} | {binary(), list( {binary(), binary()} )}. +%% Media types for accepted and provided content types -type parts() :: all | {ranges(), Size :: non_neg_integer(), Boundary :: binary(), ContentType :: binary()}. @@ -184,8 +190,12 @@ ranges/0 ]). -%% @doc Set some intial metadata in the cowboy req --spec init_env(cowboy_req:req(), cowboy_middleware:env()) -> cowboy_middleware:env(). +%% @doc Set some intial metadata in the cowboy req. + +-spec init_env(Req, Env) -> Result when + Req :: cowboy_req:req(), + Env :: cowboy_middleware:env(), + Result :: cowboy_middleware:env(). init_env(Req, Env) -> Bindings = maps:get(bindings, Req, #{}), Env1 = lists:foldl( @@ -213,14 +223,23 @@ init_env(Req, Env) -> cowmachine_cookies => cowboy_req:parse_cookies(Req) }. +-spec ensure_proxy_args(Req, Env) -> Result when + Req :: cowboy_req:req(), + Env :: cowboy_middleware:env(), + Result :: cowboy_middleware:env(). ensure_proxy_args(Req, Env) -> case maps:get(cowmachine_proxy, Env, undefined) of undefined -> cowmachine_proxy:update_env(Req, Env); IsProxy when is_boolean(IsProxy) -> Env end. -%% @doc Initialize the context with the Req and Env --spec init_context( cowboy_req:req(), cowboy_middleware:env(), undefined | map() | tuple()) -> context(). +%% @doc Initialize the context with the `Req' and `Env'. + +-spec init_context(Req, Env, Options) -> Result when + Req :: cowboy_req:req(), + Env :: cowboy_middleware:env(), + Options :: undefined | map() | tuple(), + Result :: context(). init_context( Req, Env, undefined ) -> init_context(Req, Env, #{}); init_context( Req, Env, M ) when is_map(M) -> @@ -230,56 +249,86 @@ init_context( Req, Env, Tuple) when is_tuple(Tuple) -> setelement(3, T1, Env). %% @doc Update the cowboy request in the context. --spec set_req(cowboy_req:req(), context()) -> context(). + +-spec set_req(Req, Context) -> Result when + Req :: cowboy_req:req(), + Context :: context(), + Result :: context(). set_req(Req, Context) when is_tuple(Context) -> erlang:setelement(2, Context, Req); set_req(Req, #{ cowreq := _ } = Map) -> Map#{ cowreq => Req }. %% @doc Fetch the cowboy request from the context. --spec req(context()) -> cowboy_req:req(). + +-spec req(Context) -> Result when + Context :: context(), + Result :: cowboy_req:req(). req(Context) when is_tuple(Context) -> erlang:element(2, Context); req(#{ cowreq := Req }) -> Req. %% @doc Update the cowboy middleware env in the context. --spec set_env(cowboy_middleware:env(), context()) -> context(). + +-spec set_env(Env, Context) -> Result when + Env :: cowboy_middleware:env(), + Context :: context(), + Result :: context(). set_env(Env, Context) when is_tuple(Context) -> erlang:setelement(3, Context, Env); set_env(Env, #{ cowenv := _ } = Map) -> Map#{ cowenv => Env }. %% @doc Fetch the cowboy middleware env from the context. --spec env(context()) -> cowboy_middleware:env(). + +-spec env(Context) -> Result when + Context :: context(), + Result :: cowboy_middleware:env(). env(Context) when is_tuple(Context) -> erlang:element(3, Context); env(#{ cowenv := Env }) -> Env. +%% @doc Return the current cowmachine controller. -%% @doc Return the current cowmachine controller --spec controller(context()) -> module(). +-spec controller(Context) -> Result when + Context :: context(), + Result :: module(). controller(Context) -> maps:get(cowmachine_controller, env(Context)). -%% @doc Return the current cowmachine controller options --spec controller_options(context()) -> list(). +%% @doc Return the current cowmachine controller options. + +-spec controller_options(Context) -> Result when + Context :: context(), + Result :: list(). controller_options(Context) -> maps:get(cowmachine_controller_options, env(Context), []). %% @doc Return the cowmachine site. --spec site(context()) -> atom(). + +-spec site(Context) -> Site when + Context :: context(), + Site :: atom(). site(Context) -> maps:get(cowmachine_site, env(Context)). -%% @doc Return the request Method --spec method(context()) -> binary(). +%% @doc Return the request Method. + +-spec method(Context) -> Result when + Context :: context(), + Result :: binary(). method(Context) -> cowboy_req:method(req(Context)). -%% @doc Return the http version as a tuple {minor,major}. --spec version(context()) -> {Major::integer(), Minor::integer()}. +%% @doc Return the http version as a tuple `{major, minor}'. + +-spec version(Context) -> Result when + Context :: context(), + Result :: {Major, Minor}, + Major :: integer(), + Minor :: integer(). version(Context) -> case cowboy_req:version(req(Context)) of 'HTTP/1.0' -> {1,0}; @@ -288,7 +337,10 @@ version(Context) -> end. %% @doc Return the base uri of the request. --spec base_uri(context()) -> binary(). + +-spec base_uri(Context) -> Result when + Context :: context(), + Result :: binary(). base_uri(Context) -> Scheme = scheme(Context), Uri = [ @@ -304,49 +356,73 @@ base_uri(Context) -> ], iolist_to_binary(Uri). -%% @doc Return the scheme used (https or http) --spec scheme(context()) -> http|https. +%% @doc Return the scheme used (`https' or `http'). + +-spec scheme(Context) -> Result when + Context :: context(), + Result :: http | https. scheme(Context) -> case maps:get(cowmachine_forwarded_proto, env(Context)) of <<"http">> -> http; <<"https">> -> https end. -%% @doc Return the http host --spec host(context()) -> binary(). +%% @doc Return the http host. + +-spec host(Context) -> Result when + Context :: context(), + Result :: binary(). host(Context) -> maps:get(cowmachine_forwarded_host, env(Context)). -%% @doc Return the http port --spec port(context()) -> integer(). +%% @doc Return the http port. + +-spec port(Context) -> Result when + Context :: context(), + Result :: integer(). port(Context) -> maps:get(cowmachine_forwarded_port, env(Context)). -%% @doc Check if the connection is secure (SSL) --spec is_ssl(context()) -> boolean(). +%% @doc Check if the connection is secure (SSL). + +-spec is_ssl(Context) -> Result when + Context :: context(), + Result :: boolean(). is_ssl(Context) -> https =:= scheme(Context). -%% @doc Check if the request is forwarded by a proxy --spec is_proxy(context()) -> boolean(). +%% @doc Check if the request is forwarded by a proxy. + +-spec is_proxy(Context) -> Result when + Context :: context(), + Result :: boolean(). is_proxy(Context) -> maps:get(cowmachine_proxy, env(Context)). -%% @doc Return the peer of this request, take x-forwarded-for into account if the peer -%% is an ip4 LAN address --spec peer(context()) -> binary(). +%% @doc Return the peer of this request, take `x-forwarded-for' into account if the peer +%% is an ip4 LAN address. + +-spec peer(Context) -> Result when + Context :: context(), + Result :: binary(). peer(Context) -> maps:get(cowmachine_remote, env(Context)). -%% @doc Return the peer of this request, take x-forwarded-for into account if the peer -%% is an ip4 LAN address --spec peer_ip(context()) -> tuple(). +%% @doc Return the peer of this request, take `x-forwarded-for' into account if the peer +%% is an ip4 LAN address. + +-spec peer_ip(Context) -> Result when + Context :: context(), + Result :: tuple(). peer_ip(Context) -> maps:get(cowmachine_remote_ip, env(Context)). -%% @doc Return the undecoded request path as-is, including the query string --spec raw_path(context()) -> binary(). +%% @doc Return the undecoded request path as-is, including the query string. + +-spec raw_path(Context) -> Result when + Context :: context(), + Result :: binary(). raw_path(Context) -> Path = cowboy_req:path(req(Context)), case qs(Context) of @@ -354,38 +430,58 @@ raw_path(Context) -> Qs -> <> end. -%% @doc Return the undecoded request path as-is --spec path(context()) -> binary(). +%% @doc Return the undecoded request path as-is. + +-spec path(Context) -> Result when + Context :: context(), + Result :: binary(). path(Context) -> cowboy_req:path(req(Context)). -%% @doc Return the undecoded query string, <<>> when no query string. --spec qs(context()) -> binary(). +%% @doc Return the undecoded query string, `<<>>' when no query string. + +-spec qs(Context) -> Result when + Context :: context(), + Result :: binary(). qs(Context) -> cowboy_req:qs(req(Context)). -%% @doc Return the decoded query string, [] when no query string. --spec req_qs(context()) -> list({binary(), binary()}). +%% @doc Return the decoded query string, `[]' when no query string. + +-spec req_qs(Context) -> Result when + Context :: context(), + Result :: list({binary(), binary()}). req_qs(Context) -> Qs = qs(Context), cowmachine_util:parse_qs(Qs). %% @doc Fetch all bindings from the dispatcher. --spec path_info(context()) -> cowboy_router:bindings(). + +-spec path_info(Context) -> Result when + Context :: context(), + Result :: cowboy_router:bindings(). path_info(Context) -> maps:get(bindings, req(Context), #{}). %% @doc Fetch a request header, the header must be a lowercase binary. --spec get_req_header(binary(), context()) -> binary() | undefined. + +-spec get_req_header(Header, Context) -> Result when + Header :: binary(), + Context :: context(), + Result :: undefined | binary(). get_req_header(H, Context) when is_binary(H) -> cowboy_req:header(H, req(Context)). %% @doc Fetch all request headers. --spec get_req_headers(context()) -> #{ binary() => binary() }. + +-spec get_req_headers(Context) -> Result when + Context :: context(), + Result :: #{ binary() => binary() }. get_req_headers(Context) -> cowboy_req:headers(req(Context)). %% @doc Set the content type of the response + -spec set_resp_content_type(cow_http_hd:media_type() | binary(), context()) -> context(). set_resp_content_type(CT, Context) when is_binary(CT) -> set_resp_content_type(cow_http_hd:parse_content_type(CT), Context); @@ -393,27 +489,40 @@ set_resp_content_type(CT, Context) when is_tuple(CT) -> Env = env(Context), set_env(Env#{ cowmachine_resp_content_type => CT }, Context). -%% @doc Fetch the content type of the response --spec resp_content_type(context()) -> cow_http_hd:media_type(). +%% @doc Fetch the content type of the response. + +-spec resp_content_type(Context) -> Result when + Context :: context(), + Result :: cow_http_hd:media_type(). resp_content_type(Context) -> maps:get(cowmachine_resp_content_type, env(Context)). +%% @doc Set the `is_range_ok' flag. -%% @doc Set the 'is_range_ok' flag. --spec set_range_ok(boolean(), context()) -> context(). +-spec set_range_ok(IsRangeOk, Context) -> Result when + IsRangeOk :: boolean(), + Context :: context(), + Result :: context(). set_range_ok(IsRangeOk, Context) -> Env = env(Context), set_env(Env#{cowmachine_range_ok => IsRangeOk}, Context). -%% @doc Fetch the 'is_range_ok' flag. +%% @doc Fetch the `is_range_ok' flag. + -spec is_range_ok(context()) -> boolean(). is_range_ok(Context) -> maps:get(cowmachine_range_ok, env(Context)). -%% @doc Add a response header, replacing an existing header with the same name. +%% @doc Add a response header, replacing an existing header with the same name.
%% The header must be a lowercased binary. If the value is a list of binaries then %% they are joined with a comma as separator. --spec set_resp_header(binary(), binary()|list(binary())|string(), context()) -> context(). +%% @throws {http_invalid_location, Header, Value} + +-spec set_resp_header(Header, Value, Context) -> Result when + Header :: binary(), + Value :: binary() | [binary()] |string(), + Context :: context(), + Result :: context(). set_resp_header(Header, [V|Rs], Context) when is_binary(Header), is_binary(V) -> V1 = iolist_to_binary([V, [ [", ", R] || R <- Rs] ]), set_resp_header(Header, V1, Context); @@ -429,6 +538,13 @@ set_resp_header(Header, Value, Context) set_resp_header(Header, Value, Context) when is_binary(Header) -> set_resp_header_1(Header, Value, Context). +%% @throws {http_invalid_location, Header, Value} + +-spec set_resp_header_1(Header, Value, Context) -> Result when + Header :: binary(), + Value :: binary() | [binary()] |string(), + Context :: context(), + Result :: context(). set_resp_header_1(Header, Value, Context) -> V = z_convert:to_binary(Value), case cowmachine_util:is_valid_header(Header) andalso cowmachine_util:is_valid_header_value(V) of @@ -440,108 +556,179 @@ set_resp_header_1(Header, Value, Context) -> end. %% @doc Set multiple response headers. --spec set_resp_headers([{binary(), binary()}], context()) -> context(). + +-spec set_resp_headers(Headers, Context) -> Result when + Headers :: [{binary(), binary()}], + Context :: context(), + Result :: context(). set_resp_headers([], Context) -> Context; set_resp_headers([{Header, Value}|Hs], Context) -> Context1 = set_resp_header(Header, Value, Context), set_resp_headers(Hs, Context1). -%% @doc Fetch the response header, undefined if not set. --spec get_resp_header(binary(), context()) -> binary() | undefined. +%% @doc Fetch the response header, `undefined' if not set. + +-spec get_resp_header(Header, Context) -> Result when + Header :: binary(), + Context :: context(), + Result :: undefined | binary(). get_resp_header(Header, Context) when is_binary(Header) -> Hs = maps:get(resp_headers, req(Context), #{}), maps:get(Header, Hs, undefined). %% @doc Fetch all response headers. --spec get_resp_headers(context()) -> map(). + +-spec get_resp_headers(Context) -> Result when + Context :: context(), + Result :: map(). get_resp_headers(Context) -> maps:get(resp_headers, req(Context), #{}). %% @doc Remove the response header from the list for response headers. --spec remove_resp_header(binary(), context()) -> context(). + +-spec remove_resp_header(Header, Context) -> Result when + Header :: binary(), + Context :: context(), + Result :: context(). remove_resp_header(Header, Context) when is_binary(Header) -> Req = cowboy_req:delete_resp_header(Header, req(Context)), set_req(Req, Context). -%% @doc Add a cookie to the response cookies --spec set_resp_cookie(binary(), binary(), list(), context()) -> context(). +%% @doc Add a cookie to the response cookies. + +-spec set_resp_cookie(Key, Value, Options, Context) -> Result when + Key :: binary(), + Value :: binary(), + Options :: list(), + Context :: context(), + Result :: context(). set_resp_cookie(Key, Value, Options, Context) when is_binary(Key), is_binary(Value) -> Options1 = [ {K,V} || {K,V} <- Options, V =/= undefined ], Req = cowboy_req:set_resp_cookie(Key, Value, req(Context), maps:from_list(Options1)), set_req(Req, Context). %% @doc Fetch all response cookies. --spec get_resp_cookies(context()) -> [ {binary(),binary()} ]. + +-spec get_resp_cookies(Context) -> Result when + Context :: context(), + Result :: [{binary(), binary()}]. get_resp_cookies(Context) -> maps:to_list(maps:get(resp_cookies, req(Context))). %% @doc Fetch the value of a cookie. --spec get_cookie_value(binary(), context()) -> binary() | undefined. + +-spec get_cookie_value(Name, Context) -> Result when + Name :: binary(), + Context :: context(), + Result :: undefined | binary(). get_cookie_value(Name, Context) when is_binary(Name) -> Cookies = maps:get(cowmachine_cookies, env(Context)), proplists:get_value(Name, Cookies). %% @doc Fetch all cookies. --spec req_cookie(context()) -> list(). + +-spec req_cookie(Context) -> Result when + Context :: context(), + Result :: list(). req_cookie(Context) -> maps:get(cowmachine_cookies, env(Context)). %% @doc Set the preliminary HTTP response code for the request. This can be changed. --spec set_response_code(integer(), context()) -> context(). + +-spec set_response_code(Code, Context) -> Result when + Code :: integer(), + Context :: context(), + Result :: context(). set_response_code(Code, Context) when is_integer(Code) -> Env = env(Context), set_env(Env#{cowmachine_resp_code => Code}, Context). %% @doc Fetch the preliminary HTTP response code for the request. This can be changed. --spec response_code(context()) -> integer(). + +-spec response_code(Context) -> Result when + Context :: context(), + Result :: integer(). response_code(Context) -> maps:get(cowmachine_resp_code, env(Context)). %% @doc Set the chosen charset. --spec set_resp_chosen_charset(binary()|undefined, context()) -> context(). + +-spec set_resp_chosen_charset(CharSet, Context) -> Result when + CharSet :: undefined | binary(), + Context :: context(), + Result :: context(). set_resp_chosen_charset(CharSet, Context) when is_binary(CharSet); CharSet =:= undefined -> Env = env(Context), set_env(Env#{cowmachine_resp_chosen_charset => CharSet}, Context). %% @doc Get the chosen charset. --spec resp_chosen_charset(context()) -> binary() | undefined. + +-spec resp_chosen_charset(Context) -> Result when + Context :: context(), + Result :: undefined | binary(). resp_chosen_charset(Context) -> maps:get(cowmachine_resp_chosen_charset, env(Context)). -%% @doc Set the transfer encoding --spec set_resp_transfer_encoding({binary(), function()}, context()) -> context(). +%% @doc Set the transfer encoding. + +-spec set_resp_transfer_encoding(Enc, Context) -> Result when + Enc :: {binary(), function()}, + Context :: context(), + Result :: context(). set_resp_transfer_encoding(Enc, Context) -> Env = env(Context), set_env(Env#{cowmachine_resp_transfer_encoding => Enc}, Context). %% @doc Get the transfer encoding. --spec resp_transfer_encoding(context()) -> {binary(), function()} | undefined. + +-spec resp_transfer_encoding(Context) -> Result when + Context :: context(), + Result :: undefined | {binary(), function()}. resp_transfer_encoding(Context) -> maps:get(cowmachine_resp_transfer_encoding, env(Context)). -%% @doc Set the content encoding --spec set_resp_content_encoding(binary(), context()) -> context(). +%% @doc Set the content encoding. + +-spec set_resp_content_encoding(Enc, Context) -> Result when + Enc :: binary(), + Context :: context(), + Result :: context(). set_resp_content_encoding(Enc, Context) when is_binary(Enc) -> Env = env(Context), set_env(Env#{cowmachine_resp_content_encoding => Enc}, Context). %% @doc Get the content encoding. --spec resp_content_encoding(context()) -> binary(). + +-spec resp_content_encoding(Context) -> Result when + Context :: context(), + Result :: binary(). resp_content_encoding(Context) -> maps:get(cowmachine_resp_content_encoding, env(Context)). -%% @doc Encode the content according to the selected content encoding --spec encode_content(iodata(), context()) -> iolist(). +%% @doc Encode the content according to the selected content encoding. + +-spec encode_content(Content, Context) -> Result when + Content :: iodata(), + Context :: context(), + Result :: iolist(). encode_content(Content, Context) -> encode_content_1(resp_content_encoding(Context), Content). +-spec encode_content_1(Enc, Content) -> Result when + Enc :: binary(), + Content :: iodata(), + Result :: iolist(). encode_content_1(<<"gzip">>, Content) -> zlib:gzip(Content); encode_content_1(<<"identity">>, Content) -> Content. -%% @doc Set the 'redirect' flag, used during POST processing to check if a 303 should be returned. --spec set_resp_redirect(boolean() | binary(), context()) -> context(). +%% @doc Set the `redirect' flag, used during POST processing to check if a `303' should be returned. + +-spec set_resp_redirect(Location, Context) -> Result when + Location :: boolean() | binary(), + Context :: context(), + Result :: context(). set_resp_redirect(Location, Context) when is_binary(Location) -> Env = env(Context), Context1 = set_resp_header(<<"location">>, Location, Context), @@ -550,35 +737,55 @@ set_resp_redirect(IsRedirect, Context) when is_boolean(IsRedirect) -> Env = env(Context), set_env(Env#{ cowmachine_resp_redirect => IsRedirect }, Context). -%% @doc Return the 'redirect' flag, used during POST processing to check if a 303 should be returned. --spec resp_redirect(context()) -> boolean(). +%% @doc Return the `redirect' flag, used during POST processing to check if a `303' should be returned. + +-spec resp_redirect(Context) -> Result when + Context :: context(), + Result :: boolean(). resp_redirect(Context) -> maps:get(cowmachine_resp_redirect, env(Context)). %% @doc Set the dispatch path of the request. --spec set_disp_path(binary(), context()) -> context(). + +-spec set_disp_path(Path, Context) -> Result when + Path :: binary(), + Context :: context(), + Result :: context(). set_disp_path(Path, Context) -> Env = env(Context), set_env(Env#{cowmachine_disp_path => Path}, Context). %% @doc Return the dispatch path of the request. --spec disp_path(context()) -> binary() | undefined. + +-spec disp_path(Context) -> Result when + Context :: context(), + Result :: undefined | binary(). disp_path(Context) -> maps:get(cowmachine_disp_path, env(Context)). %% @doc Set the response body, this must be converted to a response body that Cowboy can handle. --spec set_resp_body(resp_body(), context()) -> context(). + +-spec set_resp_body(RespBody, Context) -> Result when + RespBody :: resp_body(), + Context :: context(), + Result :: context(). set_resp_body(RespBody, Context) -> Env = env(Context), set_env(Env#{cowmachine_resp_body => RespBody}, Context). %% @doc Return the response body, this must be converted to a response body that Cowboy can handle. --spec resp_body(context()) -> resp_body(). + +-spec resp_body(Context) -> Result when + Context :: context(), + Result :: resp_body(). resp_body(Context) -> maps:get(cowmachine_resp_body, env(Context)). %% @doc Check if a response body has been set. --spec has_resp_body(context()) -> boolean(). + +-spec has_resp_body(Context) -> Result when + Context :: context(), + Result :: boolean(). has_resp_body(Context) -> case maps:get(cowmachine_resp_body, env(Context)) of undefined -> false; @@ -587,19 +794,28 @@ has_resp_body(Context) -> _ -> true end. - %% @doc Check if the request has a body --spec has_req_body(context()) -> boolean(). + +-spec has_req_body(Context) -> Result when + Context :: context(), + Result :: boolean(). has_req_body(Context) -> cowboy_req:has_body(req(Context)). -%% @doc Fetch the request body as a single binary. -%% Per default we don't receive more than ~128K bytes. --spec req_body(context()) -> {binary()|undefined, context()}. +%% @doc Fetch the request body as a single binary.
+%% Per default we don't receive more than `~128K' bytes. +%% @equiv req_body(128*1024, Context) + +-spec req_body(Context) -> Result when + Context :: context(), + Result :: {undefined | binary(), context()}. req_body(Context) -> req_body(128*1024, Context). --spec req_body(non_neg_integer(), context()) -> {binary()|undefined, context()}. +-spec req_body(MaxLength, Context) -> Result when + MaxLength :: non_neg_integer(), + Context :: context(), + Result :: {undefined | binary(), context()}. req_body(MaxLength, Context) when MaxLength > 0 -> Req = req(Context), Opts = #{ @@ -619,7 +835,10 @@ req_body(MaxLength, Context) when MaxLength > 0 -> {undefined, set_req(Req2, Context)} end. --spec stream_req_body(non_neg_integer(), context()) -> {ok|more, binary(), context()}. +-spec stream_req_body(ChunkSize, Context) -> Result when + ChunkSize :: non_neg_integer(), + Context :: context(), + Result :: {ok | more, binary(), context()}. stream_req_body(ChunkSize, Context) -> Opts = #{ % length => 1024*1024*1024, @@ -630,13 +849,20 @@ stream_req_body(ChunkSize, Context) -> {Next, Chunk, set_req(Req1, Context)}. --spec set_metadata(atom(), term(), context()) -> context(). +-spec set_metadata(Key, Value, Context) -> Result when + Key :: atom(), + Value :: term(), + Context :: context(), + Result :: context(). set_metadata(Key, Value, Context) -> Env = env(Context), Env1 = Env#{ {cowmachine, Key} => Value }, set_env(Env1, Context). --spec get_metadata(atom(), context()) -> term() | undefined. +-spec get_metadata(Key, Context) -> Result when + Key :: atom(), + Context :: context(), + Result :: undefined | term(). get_metadata('chosen-charset', Context) -> resp_chosen_charset(Context); get_metadata('content-encoding', Context) -> diff --git a/src/cowmachine_response.erl b/src/cowmachine_response.erl index 2c7036d..9117a9c 100644 --- a/src/cowmachine_response.erl +++ b/src/cowmachine_response.erl @@ -1,7 +1,13 @@ %% @author Justin Sheehy %% @author Andy Gross -%% @copyright 2007-2009 Basho Technologies, 2018-2022 Marc Worrell +%% @copyright 2007-2009 Basho Technologies, 2018-2022 Marc Worrell. %% Based on mochiweb_request.erl, which is Copyright 2007 Mochi Media, Inc. +%% @doc Response functions, generate the response inclusive body and headers. +%% The body can be sourced from multiple sources. These sources include files, +%% binary files and functions. +%% The response body function handles range requests. +%% @reference mochiweb_request.erl +%% @end %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -15,10 +21,6 @@ %% See the License for the specific language governing permissions and %% limitations under the License. -%% @doc Response functions, generate the response inclusive body and headers. -%% The body can be sourced from multiple sources. These sources include files, -%% binary files and functions. -%% The response body function handles range requests. -module(cowmachine_response). -author('Marc Worrell '). @@ -35,6 +37,9 @@ -define(FILE_CHUNK_LENGTH, 16#80000). % 512KB +%% @doc Returns server header. +-spec server_header() -> Result when + Result :: binary(). server_header() -> case application:get_env(cowmachine, server_header) of {ok, Server} -> z_convert:to_binary(Server); @@ -45,9 +50,13 @@ server_header() -> end end. --spec send_response(cowmachine_req:context()) -> {ok, Req, Env} | {stop, Req} - when Req :: cowboy_req:req(), - Env :: cowboy_middleware:env(). +%% @doc Send responce. +-spec send_response(Context) -> Result + when + Context :: cowmachine_req:context(), + Result :: {ok, Req, Env} | {stop, Req}, + Req :: cowboy_req:req(), + Env :: cowboy_middleware:env(). send_response(Context) -> ControllerOptions = cowmachine_req:controller_options(Context), HttpStatusCode = case proplists:get_value(http_status_code, ControllerOptions) of @@ -61,6 +70,10 @@ send_response(Context) -> %% Internal functions %% -------------------------------------------------------------------------------------------------- +-spec send_response_range(Code, Context) -> Result when + Code :: integer(), + Context :: cowmachine_req:context(), + Result :: cowmachine_req:context(). send_response_range(200, Context) -> {Range, Context1} = get_range(Context), case Range of @@ -89,10 +102,20 @@ send_response_range(200, Context) -> send_response_range(Code, Context) -> send_response_code(Code, all, Context). +-spec send_response_code(Code, Parts, Context) -> Result when + Code :: integer(), + Parts :: cowmachine_req:parts(), + Context :: cowmachine_req:context(), + Result :: cowmachine_req:context(). send_response_code(Code, Parts, Context) -> send_response_bodyfun(cowmachine_req:resp_body(Context), Code, Parts, Context). - +-spec send_response_bodyfun(RespBody, Code, Parts, Context) -> Result when + RespBody :: cowmachine_req:resp_body(), + Code :: integer(), + Parts :: cowmachine_req:parts(), + Context :: cowmachine_req:context(), + Result :: cowmachine_req:context(). % send_response_bodyfun(undefined, Code, Parts, Context) -> % send_response_bodyfun(<<>>, Code, Parts, Context); send_response_bodyfun({device, IO}, Code, Parts, Context) -> @@ -174,7 +197,13 @@ send_response_bodyfun(Body, Code, Parts, Context) -> Context1 = cowmachine_req:set_req(Req1, Context), send_parts(Context1, Parts, iolist_to_binary(Body)). - +-spec start_response_stream(Code, Length, InitialStream, Parts, Context) -> Result when + Code :: integer(), + Length :: non_neg_integer() | undefined, + InitialStream :: function() | tuple(), + Parts :: cowmachine_req:parts(), + Context :: cowmachine_req:context(), + Result :: cowmachine_req:context(). start_response_stream(Code, Length, InitialStream, Parts, Context) -> {Code1, Context1, Parts1} = case is_streaming_range(InitialStream) of false when Parts =/= all -> @@ -200,6 +229,10 @@ start_response_stream(Code, Length, InitialStream, Parts, Context) -> end, send_stream_body(FirstHunk, Context2). +-spec stream_initial_fun(Fun, Parts) -> Result when + Fun :: function(), + Parts :: cowmachine_req:parts(), + Result :: done | function(). stream_initial_fun(F, Parts) when is_function(F, 2) -> fun(Ctx) -> F(Ctx, Parts) end; stream_initial_fun(F, _Parts) when is_function(F) -> @@ -208,7 +241,11 @@ stream_initial_fun(done, _Parts) -> done. -%% Check if we support ranges on the data stream (body or function) +%% @doc Check if we support ranges on the data stream (body or function) +-spec is_streaming_range(Stream) -> Result when + Stream :: Fun | {binary(), Fun} | {any(), Fun} | {any, done}, + Fun :: function(), + Result :: boolean(). is_streaming_range(Fun) when is_function(Fun, 2) -> true; is_streaming_range(Fun) when is_function(Fun) -> false; is_streaming_range({<<>>, Fun}) when is_function(Fun, 2) -> @@ -221,6 +258,12 @@ is_streaming_range({_, done}) -> false. %% @todo Add the cookies! + +-spec response_headers(Code, Length, Context) -> Result when + Code :: integer(), + Length :: non_neg_integer() | undefined, + Context :: cowmachine_req:context(), + Result :: map(). response_headers(Code, Length, Context) -> Hdrs = cowmachine_req:get_resp_headers(Context), Hdrs1 = case Code of @@ -236,6 +279,9 @@ response_headers(Code, Length, Context) -> <<"date">> => cowboy_clock:rfc1123() }. +-spec response_headers(Context) -> Result when + Context :: cowmachine_req:context(), + Result :: map(). response_headers(Context) -> Hdrs = cowmachine_req:get_resp_headers(Context), Hdrs#{ @@ -243,8 +289,18 @@ response_headers(Context) -> <<"date">> => cowboy_clock:rfc1123() }. - % With continuation +%% @doc Send stream body. + +-spec send_stream_body(FunContext, Context) -> Result when + FunContext :: {InitialData, InitialFun} | InitialFun, + InitialData :: binary() | {file, Filename} | {file, Size, Filename} | done | WriterFun, + Filename :: filelib:filename_all(), + WriterFun :: function(), + Size :: non_neg_integer(), + InitialFun :: function(), + Context :: cowmachine_req:context(), + Result :: cowmachine_req:context(). send_stream_body({<<>>, done}, Context) -> % @TODO: in cowboy this give a wrong termination with two 0 size chunks send_chunk(Context, <<>>, fin); @@ -270,6 +326,10 @@ send_stream_body(WriterFun, Context) when is_function(WriterFun, 0) -> _ = WriterFun(), Context. +-spec next(Fun, Context) -> Result when + Fun :: fun((any()) -> any()), + Context :: cowmachine_req:context(), + Result :: Context. next(Fun, Context) when is_function(Fun, 1) -> send_stream_body(Fun(Context), Context); next(Fun, Context) when is_function(Fun, 0) -> @@ -277,12 +337,29 @@ next(Fun, Context) when is_function(Fun, 0) -> next(done, Context) -> Context. +-spec fin(Next) -> Result when + Next :: done | any(), + Result :: fin | nofin. fin(done) -> fin; fin(_) -> nofin. +%%@equiv send_file_body_loop(Context, 0, Length, IO, fin) + +-spec send_device_body(Context, Length, IO) -> Result when + Context :: cowmachine_req:context(), + Length :: non_neg_integer(), + IO :: file:io_device(), + Result :: cowmachine_req:context(). send_device_body(Context, Length, IO) -> send_file_body_loop(Context, 0, Length, IO, fin). +-spec send_file_body(Context, Length, File, FinNoFin) -> Result when + Context :: cowmachine_req:context(), + Length :: non_neg_integer(), + File :: Filename | file:iodata(), + Filename :: file:name_all(), + FinNoFin :: fin | nofin, + Result :: cowmachine_req:context(). send_file_body(Context, Length, Filename, FinNoFin) -> {ok, FD} = file:open(Filename, [read,raw,binary]), try @@ -291,6 +368,11 @@ send_file_body(Context, Length, Filename, FinNoFin) -> file:close(FD) end. +-spec send_device_body_parts(Context, Parts, IO) -> Result when + Context :: cowmachine_req:context(), + Parts :: cowmachine_req:parts(), + IO :: file:io_device(), + Result :: cowmachine_req:context(). send_device_body_parts(Context, {[{From,Length}], _Size, _Boundary, _ContentType}, IO) -> {ok, _} = file:position(IO, From), send_file_body_loop(Context, 0, Length, IO, fin); @@ -305,7 +387,11 @@ send_device_body_parts(Context, {Parts, Size, Boundary, ContentType}, IO) -> Parts), send_chunk(Context, end_boundary(Boundary), fin). --spec send_file_body_parts( cowmachine_req:context(), cowmachine_req:parts(), file:filename_all() ) -> ok | {error, term()}. +-spec send_file_body_parts(Context, Parts, Filename) -> Result when + Context :: cowmachine_req:context(), + Parts :: cowmachine_req:parts(), + Filename :: file:filename_all(), + Result :: ok | {error, term()}. send_file_body_parts(Context, Parts, Filename) -> {ok, FD} = file:open(Filename, [raw,binary]), try @@ -314,7 +400,11 @@ send_file_body_parts(Context, Parts, Filename) -> file:close(FD) end. --spec send_parts( cowmachine_req:context(), cowmachine_req:parts(), binary() ) -> cowmachine_req:context(). +-spec send_parts(Context, Parts, Bin) -> Result when + Context :: cowmachine_req:context(), + Parts :: cowmachine_req:parts(), + Bin :: binary(), + Result :: cowmachine_req:context(). send_parts(Context, {[{From,Length}], _Size, _Boundary, _ContentType}, Bin) -> send_chunk(Context, binary:part(Bin,From,Length), fin); send_parts(Context, {Parts, Size, Boundary, ContentType}, Bin) -> @@ -330,7 +420,13 @@ send_parts(Context, {Parts, Size, Boundary, ContentType}, Bin) -> Parts), send_chunk(Context, end_boundary(Boundary), fin). - +-spec send_file_body_loop(Context, Offset, Size, Device, FinNoFin) -> Result when + Context :: cowmachine_req:context(), + Offset :: integer(), + Size :: non_neg_integer(), + Device :: file:io_device(), + FinNoFin :: fin | nofin, + Result :: cowmachine_req:context(). send_file_body_loop(Context, Offset, Size, _Device, FinNoFin) when Offset =:= Size -> send_chunk(Context, <<>>, FinNoFin); send_file_body_loop(Context, Offset, Size, Device, FinNoFin) when Size - Offset =< ?FILE_CHUNK_LENGTH -> @@ -341,6 +437,10 @@ send_file_body_loop(Context, Offset, Size, Device, FinNoFin) -> send_chunk(Context, Data, nofin), send_file_body_loop(Context, Offset+iolist_size(Data), Size, Device, FinNoFin). +-spec send_writer_body(Context, BodyFun) -> Result when + Context :: cowmachine_req:context(), + BodyFun :: function(), + Result :: any(). send_writer_body(Context, BodyFun) -> BodyFun(fun(Data, false, ContextW) -> send_chunk(ContextW, Data, nofin); @@ -349,6 +449,11 @@ send_writer_body(Context, BodyFun) -> end, Context). +-spec send_chunk(Context, Data, IsFin) -> Result when + Context :: cowmachine_req:context(), + Data :: iolist() | binary(), + IsFin :: fin | nofin, + Result :: Context. send_chunk(Context, <<>>, nofin) -> Context; send_chunk(Context, [], nofin) -> @@ -359,8 +464,10 @@ send_chunk(Context, Data, FinNoFin) -> ok = cowboy_req:stream_body(Data1, FinNoFin, Req), Context. --spec get_range(cowmachine_req:context()) -> {Range, cowmachine_req:context()} - when Range :: undefined | [ {integer()|none, integer()|none} ]. +-spec get_range(Context) -> Result when + Context :: cowmachine_req:context(), + Result :: {Range, cowmachine_req:context()}, + Range :: undefined | [ {integer()|none, integer()|none} ]. get_range(Context) -> Env = cowmachine_req:env(Context), Range = case maps:get(cowmachine_range_ok, Env) of @@ -378,7 +485,9 @@ get_range(Context) -> Env1 = Env#{ cowmachine_range => Range }, {Range, cowmachine_req:set_env(Env1, Context)}. --spec parse_range_request(binary()|undefined) -> undefined | [{integer()|none,integer()|none}]. +-spec parse_range_request(RangeRequest) -> Result when + RangeRequest :: undefined | binary(), + Result :: undefined | [{integer()|none,integer()|none}]. parse_range_request(<<"bytes=", RangeString/binary>>) -> try Ranges = binary:split(binary:replace(RangeString, <<" ">>, <<>>, [global]), <<",">>, [global]), @@ -403,11 +512,19 @@ parse_range_request(_) -> undefined. % Map request ranges to byte ranges, taking the total body length into account. --spec range_parts([{integer()|none,integer()|none}], integer()) -> [{integer(),integer()}]. + +-spec range_parts(Ranges, Size) -> Result when + Ranges :: [{integer() | none, integer() | none}], + Size :: integer(), + Result :: [{integer(),integer()}]. range_parts(Ranges, Size) -> Ranges1 = [ range_skip_length(Spec, Size) || Spec <- Ranges ], [ R || R <- Ranges1, R =/= invalid_range ]. +-spec range_skip_length(Spec, Size) -> Result when + Spec :: {integer() | none, integer() | none}, + Size :: integer(), + Result :: invalid_range | {integer(),integer()}. range_skip_length({none, R}, Size) when R =< Size, R >= 0 -> {Size - R, R}; range_skip_length({none, _OutOfRange}, Size) -> @@ -421,8 +538,9 @@ range_skip_length({Start, End}, Size) when 0 =< Start, Start =< End, End < Size range_skip_length({_OutOfRange, _End}, _Size) -> invalid_range. --spec get_resp_body_size(cowmachine_req:resp_body()) -> - {ok, integer(), cowmachine_req:resp_body()} +-spec get_resp_body_size(RespBody) -> Result when + RespBody :: cowmachine_req:resp_body(), + Result :: {ok, integer(), cowmachine_req:resp_body()} | {error, nosize}. get_resp_body_size({device, Size, _} = Body) -> {ok, Size, Body}; @@ -442,6 +560,11 @@ get_resp_body_size(L) when is_list(L) -> get_resp_body_size(_) -> {error, nosize}. +-spec make_range_headers(Parts, Size, ContentType) -> Result when + Parts :: list(), + Size :: non_neg_integer(), + ContentType :: binary() | undefined, + Result :: {list(), none} | {list(), binary()}. make_range_headers([{Start, Length}], Size, _ContentType) -> HeaderList = [{<<"accept-ranges">>, <<"bytes">>}, {<<"content-range">>, iolist_to_binary([ @@ -460,6 +583,13 @@ make_range_headers(Parts, Size, ContentType) when is_list(Parts) -> {<<"content-length">>, integer_to_binary(TotalLength)}], {HeaderList, Boundary}. +-spec part_preamble(Boundary, CType, Start, Length, Size) -> Result when + Boundary :: binary(), + CType :: binary(), + Start :: non_neg_integer(), + Length :: non_neg_integer(), + Size :: non_neg_integer(), + Result :: [binary()]. part_preamble(Boundary, CType, Start, Length, Size) -> [boundary(Boundary), <<"content-type: ">>, CType, @@ -468,14 +598,26 @@ part_preamble(Boundary, CType, Start, Length, Size) -> <<"/">>, integer_to_binary(Size), <<"\r\n\r\n">>]. +-spec boundary() -> Result when + Result :: binary(). boundary() -> A = rand:uniform(100000000), B = rand:uniform(100000000), <<(integer_to_binary(A))/binary, $_, (integer_to_binary(B))/binary>>. +-spec boundary(B) -> Result when + B :: binary(), + Result :: binary(). boundary(B) -> <<"--", B/binary, "\r\n">>. + +-spec end_boundary(B) -> Result when + B :: binary(), + Result :: binary(). end_boundary(B) -> <<"--", B/binary, "--\r\n">>. +-spec make_io(Integer) -> Result when + Integer :: integer(), + Result :: list(). make_io(Integer) when is_integer(Integer) -> integer_to_list(Integer). % make_io(Atom) when is_atom(Atom) -> @@ -483,8 +625,10 @@ make_io(Integer) when is_integer(Integer) -> % make_io(Io) when is_list(Io); is_binary(Io) -> % Io. - --spec iodevice_size(file:io_device()) -> integer(). +-spec iodevice_size(IoDevice) -> Result when + IoDevice :: file:io_device(), + Size :: non_neg_integer(), + Result :: Size. iodevice_size(IoDevice) -> {ok, Size} = file:position(IoDevice, eof), {ok, 0} = file:position(IoDevice, bof), diff --git a/src/cowmachine_state.hrl b/src/cowmachine_state.hrl index 77cbaed..dd2532e 100644 --- a/src/cowmachine_state.hrl +++ b/src/cowmachine_state.hrl @@ -1,5 +1,3 @@ -%% @doc Data for cowmachine's decision core - -record(cmstate, { % Cowboy state controller :: atom(), @@ -10,4 +8,16 @@ options = #{} :: map() }). +-type cmstate() :: #cmstate{ + % Cowboy state + controller :: atom(), + is_process_called :: boolean(), + + % Memo cache for controller calls + cache :: map(), + options :: map() +}. %% Data for cowmachine's decision core + + + -define(DBG(Msg), error_logger:info_msg("DEBUG: ~p:~p ~p~n", [?MODULE, ?LINE, Msg])). diff --git a/src/cowmachine_util.erl b/src/cowmachine_util.erl index cf687c0..abe1600 100644 --- a/src/cowmachine_util.erl +++ b/src/cowmachine_util.erl @@ -3,6 +3,7 @@ %% @copyright 2007-2008 Basho Technologies %% %% @doc Utilities for parsing, quoting, and negotiation. +%% @end %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. @@ -33,11 +34,18 @@ -export([split_quoted_strings/1]). -%% @doc Check header valid characters, see https://www.ietf.org/rfc/rfc822.txt --spec is_valid_header( binary() ) -> boolean(). +%% @doc Check header valid characters, +%% see rfc822 + +-spec is_valid_header(Header) -> Result when + Header :: binary(), + Result :: boolean(). is_valid_header(<<>>) -> false; is_valid_header(H) -> is_valid_header_1(H). +-spec is_valid_header_1(Header) -> Result when + Header :: binary(), + Result :: boolean(). is_valid_header_1(<<>>) -> true; is_valid_header_1(<<$:, _/binary>>) -> false; is_valid_header_1(<>) when C >= $A, C =< $Z -> false; @@ -45,7 +53,10 @@ is_valid_header_1(<>) when C >= 33, C =< 126 -> is_valid_header_1(R is_valid_header_1(_) -> false. %% @doc Check if the given value is acceptaple for a http header value. --spec is_valid_header_value( binary() ) -> boolean(). + +-spec is_valid_header_value(Header) -> Result when + Header :: binary(), + Result :: boolean(). is_valid_header_value(<<>>) -> true; is_valid_header_value(<<13, _/binary>>) -> false; is_valid_header_value(<<10, _/binary>>) -> false; @@ -56,7 +67,10 @@ is_valid_header_value(_) -> false. %% @doc Check if the given location is safe to use as a location header. This is %% uses as a defense against urls with scripts. The test is quite strict and will %% drop values that might have been acceptable. --spec valid_location( binary() ) -> {true, binary()} | false. + +-spec valid_location(Location) -> Result when + Location :: binary(), + Result :: {true, binary()} | false. valid_location(Location) -> case is_valid_header_value(Location) of true -> @@ -70,7 +84,10 @@ valid_location(Location) -> end. %% @doc Parse the HTTP date (IMF-fixdate, rfc850, asctime). --spec convert_request_date(binary()) -> calendar:datetime(). + +-spec convert_request_date(Date) -> Result when + Date :: binary(), + Result :: calendar:datetime(). convert_request_date(Date) -> try cow_date:parse_date(Date) @@ -78,22 +95,34 @@ convert_request_date(Date) -> error:_ -> bad_date end. -%% @doc Match the Accept request header with content_types_provided -%% Return the Content-Type for the response. -%% If there is no acceptable/available match, return the atom 'none'. -%% AcceptHead is the value of the request's Accept header -%% Provided is a list of media types the controller can provide. -%% each is either a binary e.g. -- <<"text/html">> -%% or a binary and parameters e.g. -- {<<"text/html">>,[{<<"level">>,<<"1">>}]} -%% or two binaries e.g. {<<"text">>, <<"html">>} -%% or two binaries and parameters e.g. -- {<<"text">>,<<"html">>,[{<<"level">>,<<"1">>}]} +%% @doc Match the `Accept' request header with `content_types_provided'.
+%% Return the `Content-Type' for the response.
+%% If there is no acceptable/available match, return the atom `none'.
+%% `AcceptHead' is the value of the request's `Accept' header.
+%% `Provided' is a list of media types the controller can provide: +%%
    +%%
  • a binary e.g. — `<<"text/html">>'
  • +%%
  • a binary and parameters e.g. — `{<<"text/html">>,[{<<"level">>,<<"1">>}]}'
  • +%%
  • two binaries e.g. `{<<"text">>, <<"html">>}'
  • +%%
  • two binaries and parameters e.g. — `{<<"text">>,<<"html">>,[{<<"level">>,<<"1">>}]}'
  • +%%
%% (the plain string case with no parameters is much more common) --spec choose_media_type_provided( list(), binary() ) -> cow_http_hd:media_type() | none. + +-spec choose_media_type_provided(Provided, AcceptHead) -> Result when + Provided :: MediaTypes, + MediaTypes :: [MediaType], + MediaType :: cow_http_hd:media_type(), + AcceptHead :: binary(), + Result :: cow_http_hd:media_type() | none. choose_media_type_provided(Provided, AcceptHead) when is_list(Provided), is_binary(AcceptHead) -> Requested = accept_header_to_media_types(AcceptHead), Prov1 = normalize_provided(Provided), choose_media_type_provided_1(Prov1, Requested). +-spec choose_media_type_provided_1(Provided, Requested) -> Result when + Provided :: list(), + Requested :: [cow_http_hd:media_type()], + Result :: cow_http_hd:media_type() | none. choose_media_type_provided_1(_Provided, []) -> none; choose_media_type_provided_1(Provided, [H|T]) -> @@ -102,7 +131,12 @@ choose_media_type_provided_1(Provided, [H|T]) -> CT -> CT end. -% Return the first matching content type or the atom 'none' +% @doc Return the first matching content type or the atom `none'. + +-spec media_match_provided(MediaTypes, Provided) -> Result when + MediaTypes :: {binary(), binary()} | {binary(), binary()} | any(), + Provided :: list(), + Result :: cow_http_hd:media_type() | none. media_match_provided(_, []) -> none; media_match_provided({<<"*">>, <<"*">>, []}, [H|_]) -> @@ -119,21 +153,32 @@ media_match_provided({TypeA, TypeB, Params}, Provided) -> [M|_] -> M end. -%% @doc Match the Content-Type request header with content_types_accepted against --spec is_media_type_accepted( list(), cow_http_hd:media_type() ) -> boolean(). +%% @doc Match the `Content-Type' request header with `content_types_accepted' against. + +-spec is_media_type_accepted(ContentTypesAccepted, ContentTypeReqHeader) -> Result when + ContentTypesAccepted :: list(), + ContentTypeReqHeader :: cow_http_hd:media_type(), + Result :: boolean(). is_media_type_accepted([], _ReqHeader) -> true; is_media_type_accepted(ContentTypesAccepted, ContentTypeReqHeader) when is_list(ContentTypesAccepted), is_tuple(ContentTypeReqHeader) -> ContentTypesAccepted1 = normalize_provided(ContentTypesAccepted), choose_media_type_accepted_1(ContentTypesAccepted1, ContentTypeReqHeader) =/= none. --spec choose_media_type_accepted( list( cowmachine_req:media_type() ), cow_http_hd:media_type() ) -> cowmachine_req:media_type(). +-spec choose_media_type_accepted(ContentTypesAccepted, ReqHeader) -> Result when + ContentTypesAccepted :: [cowmachine_req:media_type()], + ReqHeader :: cow_http_hd:media_type(), + Result :: cowmachine_req:media_type() | none. choose_media_type_accepted([], ReqHeader) -> ReqHeader; choose_media_type_accepted(ContentTypesAccepted, ContentTypeReqHeader) when is_list(ContentTypesAccepted), is_tuple(ContentTypeReqHeader) -> ContentTypesAccepted1 = normalize_provided(ContentTypesAccepted), choose_media_type_accepted_1(ContentTypesAccepted1, ContentTypeReqHeader). +-spec choose_media_type_accepted_1(ContentTypesAccepted, ReqHeader) -> Result when + ContentTypesAccepted :: [cowmachine_req:media_type()], + ReqHeader :: cow_http_hd:media_type(), + Result :: cowmachine_req:media_type() | none. choose_media_type_accepted_1([], _CTReq) -> none; choose_media_type_accepted_1([{A1,A2,APs} = AT|T], {CT1,CT2,CTPs} = CTReq) -> @@ -150,7 +195,12 @@ choose_media_type_accepted_1([{A1,A2,APs} = AT|T], {CT1,CT2,CTPs} = CTReq) -> end. - +-spec media_type_match(Req1, Req2, Prov1, Prov2) -> Result when + Req1 :: binary(), + Req2 :: binary(), + Prov1 :: binary(), + Prov2 :: binary(), + Result :: boolean(). media_type_match(Req1, Req2, Req1, Req2) -> true; media_type_match(<<"*">>, <<"*">>, _Prov1, _Prov2) -> true; media_type_match(Req1, <<"*">>, Req1, _Prov2) -> true; @@ -158,6 +208,11 @@ media_type_match(_Req1, _Req2, _Prov1, _Prov2) -> false. %% @doc Match the media parameters. Provided must be a subset of requested. %% There may not be a type in provided that is not in requested. + +-spec media_params_match(ReqList, ProvList) -> Result when + ReqList :: list(), + ProvList :: list(), + Result :: boolean(). media_params_match(_ReqList, []) -> true; media_params_match(ReqList, ReqList) -> true; media_params_match(ReqList, ProvList) -> @@ -169,7 +224,10 @@ media_params_match(ReqList, ProvList) -> % Given the value of an accept header, produce an ordered list based on the q-values. % The first result being the highest-priority requested type. --spec accept_header_to_media_types(binary()) -> list({binary(), binary(), list({binary(),binary()})}). + +-spec accept_header_to_media_types(HeadVal) -> Result when + HeadVal :: binary(), + Result :: [cow_http_hd:media_type()]. accept_header_to_media_types(HeadVal) -> try MTs = cow_http_hd:parse_accept(HeadVal), @@ -179,9 +237,15 @@ accept_header_to_media_types(HeadVal) -> _:_ -> [] end. +-spec normalize_provided(Provided) -> Result when + Provided :: [cowmachine_req:media_type()], + Result :: cow_http_hd:media_type(). normalize_provided(Provided) -> [ normalize_content_type(X) || X <- Provided ]. +-spec normalize_content_type(Type) -> Result when + Type :: cowmachine_req:media_type(), + Result :: cow_http_hd:media_type(). normalize_content_type(Type) when is_binary(Type) -> cow_http_hd:parse_content_type(Type); normalize_content_type({Type,Params}) when is_binary(Type), is_list(Params) -> @@ -192,23 +256,38 @@ normalize_content_type({Type1,Type2}) when is_binary(Type1), is_binary(Type2) -> normalize_content_type({Type1,Type2,Params}) when is_binary(Type1), is_binary(Type2), is_list(Params) -> {Type1, Type2, Params}. --spec format_content_type( cow_http_hd:media_type() ) -> binary(). +-spec format_content_type(MediaType) -> Result when + MediaType :: cow_http_hd:media_type(), + Result :: binary(). format_content_type({T1, T2, []}) -> <>; format_content_type({T1, T2, Params}) -> ParamsBin = [ [$;, Param, $=, Value] || {Param,Value} <- Params ], iolist_to_binary([T1, $/, T2, ParamsBin]). -%% @doc Select the best fitting character set or 'none' --spec choose_charset([binary()], binary()) -> binary() | none. +%% @doc Select the best fitting character set or `none' + +-spec choose_charset(CSets, AccCharHdr) -> Result when + CSets :: [binary()], + AccCharHdr :: binary(), + Result :: none | binary(). choose_charset(CSets, AccCharHdr) -> do_choose(CSets, AccCharHdr, <<"utf-8">>). -%% @doc Select the best fitting encoding or 'none' --spec choose_encoding([binary()], binary()) -> binary() | none. +%% @doc Select the best fitting encoding or `none' + +-spec choose_encoding(Encs, AccEncHdr) -> Result when + Encs :: [binary()], + AccEncHdr :: binary(), + Result :: none | binary(). choose_encoding(Encs, AccEncHdr) -> do_choose(Encs, AccEncHdr, <<"identity">>). +-spec do_choose(Choices, Header, Default) -> Result when + Choices :: [binary()], + Header :: binary(), + Default :: binary(), + Result :: none | binary(). do_choose(Choices, Header, Default) -> try Accepted = cow_http_hd:parse_accept_encoding(Header), @@ -235,6 +314,13 @@ do_choose(Choices, Header, Default) -> Default end. +-spec do_choose(Default, DefaultOkay, AnyOkay, Choices, Accepted) -> Result when + Default :: binary(), + DefaultOkay :: yes | no, + AnyOkay :: yes | no, + Choices :: list(), + Accepted :: list(), + Result :: binary() | none. do_choose(_Default, _DefaultOkay, _AnyOkay, [], _Accepted) -> none; do_choose(_Default, _DefaultOkay, yes, [Choice|_], []) -> @@ -274,13 +360,23 @@ do_choose(Default, DefaultOkay, AnyOkay, Choices, [{Acc,_Prio}|AccRest]) -> %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% -%% @doc Parse an application/x-www-form-urlencoded string. --spec parse_qs(binary()) -> list({binary(),binary()}). +%% @doc Parse an `application/x-www-form-urlencoded' string. + +-spec parse_qs(String) -> Result when + String :: binary(), + Result :: list({binary(),binary()}). parse_qs(<<>>) -> []; parse_qs(Qs) -> parse_qs_name(Qs, [], <<>>). +%% @throws invalid_percent_encoding + +-spec parse_qs_name(String, Acc, Name) -> Result when + String :: binary(), + Acc :: list(), + Name :: binary(), + Result :: list({binary(),binary()}). parse_qs_name(<< $%, H, L, Rest/binary >>, Acc, Name) -> C = (unhex(H) bsl 4 bor unhex(L)), parse_qs_name(Rest, Acc, << Name/binary, C >>); @@ -305,6 +401,14 @@ parse_qs_name(<<>>, Acc, Name) -> parse_qs_name(_Rest, _Acc, _Name) -> throw(invalid_percent_encoding). +%% @throws invalid_percent_encoding + +-spec parse_qs_value(String, Acc, Name, Value) -> Result when + String :: binary(), + Acc :: list(), + Name :: binary(), + Value :: binary(), + Result :: list({binary(),binary()}). parse_qs_value(<< $%, H, L, Rest/binary >>, Acc, Name, Value) -> C = (unhex(H) bsl 4 bor unhex(L)), parse_qs_value(Rest, Acc, Name, << Value/binary, C >>); @@ -319,6 +423,10 @@ parse_qs_value(<<>>, Acc, Name, Value) -> parse_qs_value(_Rest, _Acc, _Name, _Value) -> throw(invalid_percent_encoding). +%% @throws invalid_percent_encoding +-spec unhex(Char) -> Result when + Char :: char(), + Result :: 1..15. unhex($0) -> 0; unhex($1) -> 1; unhex($2) -> 2; @@ -348,7 +456,10 @@ unhex(_) -> throw(invalid_percent_encoding). %% copyright 2007 Mochi Media, Inc. %% @doc Parse a Content-Type like header, return the main Content-Type %% and a property list of options. --spec parse_header(binary()) -> {binary(), list({binary(),binary()})}. + +-spec parse_header(String) -> Result when + String :: binary(), + Result :: {binary(), list({binary(),binary()})}. parse_header(String) -> %% TODO: This is exactly as broken as Python's cgi module. %% Should parse properly like mochiweb_cookies. @@ -364,11 +475,18 @@ parse_header(String) -> end, {z_string:to_lower(Type), lists:foldr(F, [], Parts)}. +-spec unquote_header(Header) -> Result when + Header :: binary(), + Result :: binary(). unquote_header(<<$", Rest/binary>>) -> unquote_header(Rest, <<>>); unquote_header(S) -> S. +-spec unquote_header(Header, Acc) -> Result when + Header :: binary(), + Acc :: binary(), + Result :: binary(). unquote_header(<<>>, Acc) -> Acc; unquote_header(<<$">>, Acc) -> Acc; unquote_header(<<$\\, C, Rest/binary>>, Acc) -> @@ -377,12 +495,18 @@ unquote_header(<>, Acc) -> unquote_header(Rest, <>). --spec quoted_string( binary() ) -> binary(). +-spec quoted_string(String) -> Result when + String :: binary(), + Result :: binary(). quoted_string(<<$", _Rest/binary>> = Str) -> Str; quoted_string(Str) -> escape_quotes(Str, <<$">>). % Initialize Acc with opening quote +-spec escape_quotes(String, Acc) -> Result when + String :: binary(), + Acc :: binary(), + Result :: binary(). escape_quotes(<<>>, Acc) -> <>; % Append final quote escape_quotes(<<$\\, Char, Rest/binary>>, Acc) -> @@ -393,10 +517,16 @@ escape_quotes(<>, Acc) -> escape_quotes(Rest, <>). --spec split_quoted_strings( binary() ) -> list( binary() ). +-spec split_quoted_strings(String) -> Result when + String :: binary(), + Result :: [binary()]. split_quoted_strings(Str) -> split_quoted_strings(Str, []). +-spec split_quoted_strings(String, Acc) -> Result when + String :: binary(), + Acc :: [binary()], + Result :: [binary()]. split_quoted_strings(<<>>, Acc) -> lists:reverse(Acc); split_quoted_strings(<<$", Rest/binary>>, Acc) -> @@ -405,6 +535,10 @@ split_quoted_strings(<<$", Rest/binary>>, Acc) -> split_quoted_strings(<<_Skip, Rest/binary>>, Acc) -> split_quoted_strings(Rest, Acc). +-spec unescape_quoted_string(String, Acc) -> Result when + String :: binary(), + Acc :: bitstring(), + Result :: {bitstring(),binary()}. unescape_quoted_string(<<>>, Acc) -> {Acc, <<>>}; unescape_quoted_string(<<$\\, Char, Rest/binary>>, Acc) -> % Any quoted char should be unquoted diff --git a/src/cowmachine_websocket_upgrade.erl b/src/cowmachine_websocket_upgrade.erl index 093479f..eafc92f 100644 --- a/src/cowmachine_websocket_upgrade.erl +++ b/src/cowmachine_websocket_upgrade.erl @@ -5,7 +5,11 @@ ]). %% @doc Upgrade the request to a websocket request --spec upgrade(atom(), cowmachine_req:context()) -> {ok, cowboy_req:req(), cowboy_middleware:env()}. + +-spec upgrade(Handler, Context) -> Result when + Handler :: atom(), + Context :: cowmachine_req:context(), + Result :: {ok, cowboy_req:req(), cowboy_middleware:env()}. upgrade(Handler, Context) -> Req = cowmachine_req:req(Context), Env = cowmachine_req:env(Context),