From 048ec2370cf60b81e5063b65c2717a0e1b8d29e0 Mon Sep 17 00:00:00 2001 From: Anatolii Kosorukov Date: Tue, 3 May 2022 23:08:27 +0300 Subject: [PATCH 1/2] General improvements Tune module descrition (exclude licence topic info from generating documentation by @end EDoc tag. Add documentation targets for generation public and private documenation. -spec. Simplified documentation content. Explicitly specified the name of function parameters in a uniform style. Introduced a wider use of the features of EDoc. Provide a description of the data types (-type) that were in the source codes in the generated EDoc documentation. This in the code seems non-obvious and even strange. But please look at the output when generating the documentation. The result - we have documentation for types. cowmachine_req. Fix version documentation type. Arranged the version values in the correct order. rebar.config Add documentation generation and testing profiles. Add option to generate public and private documentation. Moved the work with the dilyzer to a separate profile "check" from the "test" profile. It is more comfortable. It also speeds up work by doing only useful work. EDoc Tune css stylesheet. --- Makefile | 9 + rebar.config | 26 +- src/cowmachine.erl | 74 ++++- src/cowmachine_accept_language.erl | 44 ++- src/cowmachine_controller.erl | 29 ++ src/cowmachine_decision_core.erl | 64 +++- src/cowmachine_proxy.erl | 118 +++++++- src/cowmachine_req.erl | 426 ++++++++++++++++++++------- src/cowmachine_response.erl | 190 ++++++++++-- src/cowmachine_state.hrl | 14 +- src/cowmachine_util.erl | 196 ++++++++++-- src/cowmachine_websocket_upgrade.erl | 6 +- 12 files changed, 1005 insertions(+), 191 deletions(-) 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/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 81d1d44..541966c 100644 --- a/src/cowmachine.erl +++ b/src/cowmachine.erl @@ -2,6 +2,7 @@ %% @copyright 2016-2019 Marc Worrell %% %% @doc Cowmachine: webmachine middleware for Cowboy/Zotonic +%% @end %% Copyright 2016-2019 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,20 @@ request_1(Controller, Req, Env, Options, Context) -> class => Class, reason => Reason, stack => Stacktrace}, Req), {stop, cowboy_req:reply(500, Req)} - 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 +182,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 +209,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 +224,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), From cae2c54089748650aef1daea5d8ca8df080a1c88 Mon Sep 17 00:00:00 2001 From: Anatolii Kosorukov Date: Fri, 6 May 2022 16:13:58 +0300 Subject: [PATCH 2/2] Add style.ccs, overview.edoc Add description to .gitignore related to style.ccs and overview.edoc. --- .gitignore | 4 ++- doc/overview.edoc | 5 ++++ doc/style.css | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 doc/overview.edoc create mode 100644 doc/style.css 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/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; +}