Current section
Files
Jump to
Current section
Files
src/backwater_response.erl
%% Copyright (c) 2017-2018 Guilherme Andrade <backwater@gandrade.net>
%%
%% Permission is hereby granted, free of charge, to any person obtaining a
%% copy of this software and associated documentation files (the "Software"),
%% to deal in the Software without restriction, including without limitation
%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%% and/or sell copies of the Software, and to permit persons to whom the
%% Software is furnished to do so, subject to the following conditions:
%%
%% The above copyright notice and this permission notice shall be included in
%% all copies or substantial portions of the Software.
%%
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%% DEALINGS IN THE SOFTWARE.
-module(backwater_response).
-include_lib("hackney/include/hackney_lib.hrl").
-include("backwater_common.hrl").
-include("backwater_default_tweaks.hrl").
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
%% ------------------------------------------------------------------
%% API Function Exports
%% ------------------------------------------------------------------
-export([decode/4]). -ignore_xref({decode,4}).
-export([decode/5]).
%% ------------------------------------------------------------------
%% Type Definitions
%% ------------------------------------------------------------------
-type error() ::
{exception, {Class :: error | exit | throw, Reason :: term(), [erlang:stack_item()]}} |
{failure(), raw_response()}.
-export_type([error/0]).
-type failure() ::
{response_authentication, response_authentication_failure()} |
response_decode_failure() |
remote.
-export_type([failure/0]).
-type headers() :: [{nonempty_binary(), binary()}].
-export_type([headers/0]).
-type options() ::
#{ decode_unsafe_terms => boolean(),
max_encoded_result_size => non_neg_integer(),
rethrow_remote_exceptions => boolean() }.
-export_type([options/0]).
-type raw_response() :: {status_code_name(), CiHeaders :: headers(), RawBody :: binary()}.
-export_type([raw_response/0]).
-type response_authentication_failure() ::
backwater_signatures:response_validation_failure() |
wrong_body_digest.
-export_type([response_authentication_failure/0]).
-type response_decode_failure() ::
invalid_content_encoding |
invalid_content_type |
invalid_body.
-export_type([response_decode_failure/0]).
-type status_code() :: pos_integer().
-export_type([status_code/0]).
-type status_code_name() ::
ok |
bad_request |
unauthorized |
forbidden |
not_found |
not_acceptable |
payload_too_large |
unsupported_media_type |
internal_error |
{http, status_code()}.
-export_type([status_code_name/0]).
-type t() :: {ok, Value :: term()} | {error, error()} | no_return().
-export_type([t/0]).
-type t(OtherError) :: {ok, Value :: term()} | {error, error() | OtherError} | no_return().
-export_type([t/1]).
%% ------------------------------------------------------------------
%% API Function Definitions
%% ------------------------------------------------------------------
-spec decode(StatusCode, Headers, Body, RequestState) -> Response | no_return()
when StatusCode :: status_code(),
Headers :: headers(),
Body :: binary(),
RequestState :: backwater_request:state(),
Response :: t().
decode(StatusCode, Headers, Body, RequestState) ->
decode(StatusCode, Headers, Body, RequestState, #{}).
-spec decode(StatusCode, Headers, Body, RequestState, Options) -> Response | no_return()
when StatusCode :: status_code(),
Headers :: headers(),
Body :: binary(),
RequestState :: backwater_request:state(),
Options :: options(),
Response :: t().
decode(StatusCode, Headers, Body, RequestState, Options) ->
CiHeaders = lists:keymap(fun backwater_util:latin1_binary_to_lower/1, 1, Headers),
authenticate(StatusCode, CiHeaders, Body, RequestState, Options).
%% ------------------------------------------------------------------
%% Internal Function Definitions - Authentication
%% ------------------------------------------------------------------
-spec authenticate(status_code(), headers(), binary(), backwater_request:state(),
options()) -> t() | no_return().
authenticate(StatusCode, CiHeaders, Body, RequestState, Options) ->
#{ signed_request_msg := SignedRequestMsg } = RequestState,
ResponseMsg = backwater_signatures:new_response_msg(StatusCode, {ci_headers, CiHeaders}),
case backwater_signatures:validate_response_signature(SignedRequestMsg, ResponseMsg)
of
{ok, SignedResponseMsg} ->
authenticate_body(StatusCode, CiHeaders, Body, Options, SignedResponseMsg);
{error, Error} ->
failure_error({response_authentication, Error}, StatusCode, CiHeaders, Body)
end.
-spec authenticate_body(status_code(), headers(), binary(), options(),
backwater_signatures:signed_message()) -> t().
authenticate_body(StatusCode, CiHeaders, Body, Options, SignedResponseMsg) ->
case backwater_signatures:validate_signed_msg_body(SignedResponseMsg, Body) of
true ->
decode_(StatusCode, CiHeaders, Body, Options, SignedResponseMsg);
false ->
failure_error({response_authentication, wrong_body_digest}, StatusCode, CiHeaders, Body)
end.
%% ------------------------------------------------------------------
%% Internal Function Definitions - Decode Response
%% ------------------------------------------------------------------
-spec decode_(status_code(), headers(), binary(), options(),
backwater_signatures:signed_message()) -> t().
decode_(200 = StatusCode, CiHeaders, Body, Options, SignedResponseMsg) ->
RethrowRemoteExceptions = opt_rethrow_remote_exceptions(Options),
case decode_body(CiHeaders, Body, Options, SignedResponseMsg) of
{ok, {return, ReturnValue}} ->
{ok, ReturnValue};
{ok, {exception, {Class, Exception, Stacktrace}}} when RethrowRemoteExceptions ->
erlang:raise(Class, Exception, Stacktrace);
{ok, {exception, {Class, Exception, Stacktrace}}} ->
{error, {exception, {Class, Exception, Stacktrace}}};
{ok, _UnknownBodyFormat} ->
failure_error(invalid_body, StatusCode, CiHeaders, Body);
{error, Error} ->
failure_error(Error, StatusCode, CiHeaders, Body)
end;
decode_(StatusCode, CiHeaders, Body, _Options, _SignedResponseMsg) ->
failure_error(remote, StatusCode, CiHeaders, Body).
%% ------------------------------------------------------------------
%% Internal Function Definitions - Content Encoding
%% ------------------------------------------------------------------
-spec decode_body(headers(), binary(), options(),
backwater_signatures:signed_message())
-> {ok, term()} |
{error, response_decode_failure()}.
decode_body(CiHeaders, Body, Options, SignedResponseMsg) ->
ContentEncoding = get_content_encoding(CiHeaders, SignedResponseMsg),
handle_body_content_encoding(ContentEncoding, CiHeaders, Body, Options, SignedResponseMsg).
-spec handle_body_content_encoding(ContentEncoding :: binary(),
headers(), binary(), options(),
backwater_signatures:signed_message())
-> {ok, term()} |
{error, response_decode_failure()}.
handle_body_content_encoding(<<"gzip">>, CiHeaders, Body, Options, SignedResponseMsg) ->
MaxEncodedResultSize = opt_max_encoded_result_size(Options),
case backwater_encoding_gzip:decode(Body, MaxEncodedResultSize) of
{ok, UncompressedBody} ->
ContentTypeLookup = get_content_type(CiHeaders, SignedResponseMsg),
handle_body_content_type(ContentTypeLookup, UncompressedBody, Options);
{error, _Error} ->
{error, invalid_body}
end;
handle_body_content_encoding(<<"identity">>, CiHeaders, Body, Options, SignedResponseMsg) ->
ContentTypeLookup = get_content_type(CiHeaders, SignedResponseMsg),
handle_body_content_type(ContentTypeLookup, Body, Options);
handle_body_content_encoding(_OtherEncoding, _CiHeaders, _Body, _Config, _SignedResponseMsg) ->
{error, invalid_content_encoding}.
-spec get_content_encoding(headers(), backwater_signatures:signed_message()) -> binary().
get_content_encoding(CiHeaders, SignedResponseMsg) ->
case find_header_value(?OPAQUE_BINARY(<<"content-encoding">>), CiHeaders, SignedResponseMsg) of
{ok, ContentEncoding} ->
ContentEncoding;
error ->
<<"identity">>
end.
%% ------------------------------------------------------------------
%% Internal Function Definitions - Content Type
%% ------------------------------------------------------------------
-spec handle_body_content_type({ok, {nonempty_binary(), [nonempty_binary()]}} |
{error, invalid_content_type},
binary(), options())
-> {ok, term()} |
{error, invalid_content_type} |
{error, invalid_body}.
handle_body_content_type({ok, {<<"application/x-erlang-etf">>, _Params}}, Body, Options) ->
DecodeUnsafeTerms = opt_decode_unsafe_terms(Options),
case backwater_media_etf:decode(Body, DecodeUnsafeTerms) of
{ok, Decoded} ->
{ok, Decoded};
error ->
{error, invalid_body}
end;
handle_body_content_type({ok, {_OtherContentType, _Params}}, _Body, _Config) ->
{error, invalid_content_type};
handle_body_content_type({error, invalid_content_type}, _Body, _Config) ->
{error, invalid_content_type}.
-spec get_content_type(headers(), backwater_signatures:signed_message())
-> {ok, {nonempty_binary(), [nonempty_binary()]}} |
{error, invalid_content_type}.
get_content_type(CiHeaders, SignedResponseMsg) ->
case find_header_value(?OPAQUE_BINARY(<<"content-type">>), CiHeaders, SignedResponseMsg) of
{ok, ContentTypeBin} ->
case binary:split(ContentTypeBin, [<<";">>, <<" ">>, <<$\n>>, <<$\r>>],
[global, trim_all])
of
[ActualBinContentType | BinAttributes] ->
{ok, {ActualBinContentType, [V || V <- BinAttributes]}};
[] ->
{error, invalid_content_type}
end;
error ->
{error, invalid_content_type}
end.
%% ------------------------------------------------------------------
%% Internal Function Definitions - Utilities and Misfits
%% ------------------------------------------------------------------
-spec find_header_value(nonempty_binary(), headers(), backwater_signatures:signed_message())
-> {ok, binary()} | error.
find_header_value(CiName, CiHeaders, SignedResponseMsg) ->
case lists:keyfind(CiName, 1, CiHeaders) of
{CiName, Value} ->
assert_header_safety(CiName, SignedResponseMsg),
{ok, Value};
false ->
error
end.
-spec assert_header_safety(binary(), backwater_signatures:signed_message()) -> true | no_return().
assert_header_safety(CiName, SignedResponseMsg) ->
backwater_signatures:is_header_signed_in_signed_msg(CiName, SignedResponseMsg)
orelse error({using_unsafe_header, CiName}).
-spec opt_decode_unsafe_terms(options()) -> boolean().
opt_decode_unsafe_terms(Options) ->
maps:get(decode_unsafe_terms, Options, ?DEFAULT_OPT_DECODE_UNSAFE_TERMS).
-spec opt_max_encoded_result_size(options()) -> non_neg_integer().
opt_max_encoded_result_size(Options) ->
maps:get(max_encoded_result_size, Options, ?DEFAULT_OPT_MAX_ENCODED_RESULT_SIZE).
-spec opt_rethrow_remote_exceptions(options()) -> boolean().
opt_rethrow_remote_exceptions(Options) ->
maps:get(rethrow_remote_exceptions, Options, ?DEFAULT_OPT_RETHROW_REMOTE_EXCEPTIONS).
-spec failure_error(failure(), status_code(), headers(), binary())
-> {error, {failure(), raw_response()}}.
failure_error(Failure, StatusCode, CiHeaders, Body) ->
{error, {Failure, {status_code_name(StatusCode), CiHeaders, Body}}}.
-spec status_code_name(status_code()) -> status_code_name().
status_code_name(200) -> ok;
status_code_name(400) -> bad_request;
status_code_name(401) -> unauthorized;
status_code_name(403) -> forbidden;
status_code_name(404) -> not_found;
status_code_name(405) -> method_not_allowed;
status_code_name(406) -> not_acceptable;
status_code_name(413) -> payload_too_large;
status_code_name(415) -> unsupported_media_type;
status_code_name(500) -> internal_error;
status_code_name(Unknown) -> {http, Unknown}.
%% ------------------------------------------------------------------
%% Internal Function Definitions - Unit Tests
%% ------------------------------------------------------------------
-ifdef(TEST).
test_signatures_config() ->
backwater_signatures:config( crypto:strong_rand_bytes(32) ).
test_request_msg() ->
SignaturesConfig = test_signatures_config(),
{SignaturesConfig, backwater_signatures:new_request_msg(<<"POST">>, <<"/path">>, #{})}.
test_request_state() ->
{SignaturesConfig, RequestMsg} = test_request_msg(),
RequestId = crypto:strong_rand_bytes(16),
SignedRequestMsg =
backwater_signatures:sign_request(SignaturesConfig, RequestMsg, <<"request body">>, RequestId),
{SignaturesConfig, #{ signed_request_msg => SignedRequestMsg }}.
test_response(ResponseHeaders) ->
ResponseBody = crypto:strong_rand_bytes(1024),
{StatusCode, SignedResponseMsgHeaders, RequestState} = test_response(ResponseHeaders, ResponseBody),
{StatusCode, SignedResponseMsgHeaders, ResponseBody, RequestState}.
test_response(ResponseHeaders, ResponseBody) ->
{SignaturesConfig, RequestState} = test_request_state(),
#{ signed_request_msg := SignedRequestMsg } = RequestState,
StatusCode = 200,
StatusCodeName = status_code_name(StatusCode),
ResponseMsg = backwater_signatures:new_response_msg(StatusCode, ResponseHeaders),
SignedResponseMsg = backwater_signatures:sign_response(SignaturesConfig,
ResponseMsg, ResponseBody,
SignedRequestMsg),
SignedResponseMsgHeaders = backwater_signatures:list_real_msg_headers(SignedResponseMsg),
{StatusCodeName, SignedResponseMsgHeaders, RequestState}.
invalid_signature_test() ->
{StatusCodeName, SignedResponseMsgHeaders, ResponseBody, RequestState} = test_response(#{}),
CorruptSignedResponseMsgHeaders =
lists:keystore(<<"digest">>, 1, SignedResponseMsgHeaders, {<<"digest">>, <<>>}),
?assertMatch(
{error, {{response_authentication, invalid_signature},
{StatusCodeName, CorruptSignedResponseMsgHeaders, ResponseBody}}},
decode(200, CorruptSignedResponseMsgHeaders, ResponseBody, RequestState)).
invalid_body_digest_test() ->
{StatusCodeName, SignedResponseMsgHeaders, _ResponseBody, RequestState} = test_response(#{}),
CorruptResponseBody = crypto:strong_rand_bytes(1024),
?assertMatch(
{error, {{response_authentication, wrong_body_digest},
{StatusCodeName, SignedResponseMsgHeaders, CorruptResponseBody}}},
decode(200, SignedResponseMsgHeaders, CorruptResponseBody, RequestState)).
unknown_content_encoding_test() ->
ResponseHeaders = #{ <<"content-encoding">> => <<"something">> },
{StatusCodeName, SignedResponseMsgHeaders, ResponseBody, RequestState} =
test_response(ResponseHeaders),
?assertMatch(
{error, {invalid_content_encoding, {StatusCodeName, SignedResponseMsgHeaders, ResponseBody}}},
decode(200, SignedResponseMsgHeaders, ResponseBody, RequestState)).
missing_content_type_test() ->
{StatusCodeName, SignedResponseMsgHeaders, ResponseBody, RequestState} = test_response(#{}),
?assertMatch(
{error, {invalid_content_type, {StatusCodeName, SignedResponseMsgHeaders, ResponseBody}}},
decode(200, SignedResponseMsgHeaders, ResponseBody, RequestState)).
unknown_content_type_test() ->
ResponseHeaders = #{ <<"content-type">> => <<"something/something">> },
{StatusCodeName, SignedResponseMsgHeaders, ResponseBody, RequestState} =
test_response(ResponseHeaders),
?assertMatch(
{error, {invalid_content_type, {StatusCodeName, SignedResponseMsgHeaders, ResponseBody}}},
decode(200, SignedResponseMsgHeaders, ResponseBody, RequestState)).
malformed_content_type_test() ->
ResponseHeaders = #{ <<"content-type">> => <<>> },
{StatusCodeName, SignedResponseMsgHeaders, ResponseBody, RequestState} =
test_response(ResponseHeaders),
?assertMatch(
{error, {invalid_content_type, {StatusCodeName, SignedResponseMsgHeaders, ResponseBody}}},
decode(200, SignedResponseMsgHeaders, ResponseBody, RequestState)).
malformed_body_test() ->
ResponseHeaders = #{ <<"content-type">> => <<"application/x-erlang-etf">> },
{StatusCodeName, SignedResponseMsgHeaders, ResponseBody, RequestState} =
test_response(ResponseHeaders),
?assertMatch(
{error, {invalid_body, {StatusCodeName, SignedResponseMsgHeaders, ResponseBody}}},
decode(200, SignedResponseMsgHeaders, ResponseBody, RequestState)).
malformed_compressed_body_test() ->
ResponseHeaders = #{ <<"content-type">> => <<"application/x-erlang-etf">>,
<<"content-encoding">> => <<"gzip">> },
{StatusCodeName, SignedResponseMsgHeaders, ResponseBody, RequestState} =
test_response(ResponseHeaders),
?assertMatch(
{error, {invalid_body, {StatusCodeName, SignedResponseMsgHeaders, ResponseBody}}},
decode(200, SignedResponseMsgHeaders, ResponseBody, RequestState)).
unknown_body_format_test() ->
ResponseHeaders = #{ <<"content-type">> => <<"application/x-erlang-etf">> },
ResponseBody = term_to_binary(unknown),
{StatusCodeName, SignedResponseMsgHeaders, RequestState} = test_response(ResponseHeaders, ResponseBody),
?assertMatch(
{error, {invalid_body, {StatusCodeName, SignedResponseMsgHeaders, ResponseBody}}},
decode(200, SignedResponseMsgHeaders, ResponseBody, RequestState)).
-endif.