%% @author Justin Sheehy <justin@basho.com>
%% @author Andy Gross <andy@basho.com>
%% @author Bryan Fink <bryan@basho.com>
%% @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.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% @doc HTTP decision core for cowmachine
-module(cowmachine_decision_core).
-author('Justin Sheehy <justin@basho.com>').
-author('Andy Gross <andy@basho.com>').
-author('Bryan Fink <bryan@basho.com>').
-author('Marc Worrell <marc@worrell.nl>').
-export([handle_request/2]).
-export([respond/3]).
-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(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 ->
case maps:find(Callback, Cache) of
error ->
{T, Context1} = cowmachine_controller:do(Callback, State, Context),
State1 = State#cmstate{cache = Cache#{Callback => T}},
{T, State1, Context1};
{ok, Cached} ->
{Cached, State, Context}
end;
false ->
{T, Context1} = cowmachine_controller:do(Callback, 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;
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 :: {Res, State, Context},
Res :: boolean() | cowmachine_req:halt() | {error, any(), any()} | {error, any()} |
cowmachine_req:resp_body().
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 ->
% Response all ok
{State, Context};
301 ->
% Permanent redirect
{State, Context};
R when R =:= 303; R =:= 307 ->
% Temp redirects have no content and should not be cached
CtxNoCT = cowmachine_req:remove_resp_header(<<"content-type">>, Context),
{Etag, StateEt, CtxEt0} = controller_call(generate_etag, State, CtxNoCT),
CtxEt = case Etag of
undefined -> CtxEt0;
ETag -> cowmachine_req:set_resp_header(<<"etag">>, cow_uri:urlencode(ETag), CtxEt0)
end,
{Expires, StateExp, ExpCtx0} = controller_call(expires, StateEt, CtxEt),
ExpCtx = case Expires of
undefined ->
ExpCtx0;
Exp ->
cowmachine_req:set_resp_header(
<<"expires">>,
z_convert:to_binary(httpd_util:rfc1123_date(calendar:universal_time_to_local_time(Exp))),
ExpCtx0)
end,
{StateExp, ExpCtx};
E when E =:= 401; E =:= 403; E =:= 404; E=:= 410; (E >= 500 andalso E =< 599) ->
% Http errors - maybe handled by error controller
HasRespContentType = cowmachine_req:get_resp_header(<<"content-type">>, Context) =/= undefined,
HasBody = cowmachine_req:resp_body(Context) =/= undefined,
if
HasBody andalso HasRespContentType ->
{State, Context};
true ->
% Let the error controller handle 4xx and 5xx errors without body
controller_call(finish_request, State, Context),
throw({stop_request, Code})
end;
_ ->
{State, Context}
end,
% Set transfer encoding.
{State2, Context2} = case cowmachine_req:get_req_header(<<"te">>, Context1) of
undefined -> {State1, Context1};
TEHdr -> choose_transfer_encoding(TEHdr, State1, Context1)
end,
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}).
% Suppress no_local return error
-dialyzer({nowarn_function, error_response/3}).
error_response(Reason, State, Context) ->
error_response(500, Reason, State, Context).
decision_test({Test, Rs, Rd}, TestVal, TrueFlow, FalseFlow) ->
decision_test(Test, TestVal, TrueFlow, FalseFlow, Rs, Rd).
decision_test({error, Reason}, _Test, _TrueFlow, _FalseFlow, State, Context) ->
error_response(Reason, State, Context);
decision_test({error, Reason0, Reason1}, _Test, _TrueFlow, _FalseFlow, State, Context) ->
error_response({Reason0, Reason1}, State, Context);
decision_test({halt, Code}, _Test, _TrueFlow, _FalseFlow, State, Context) ->
respond(Code, State, Context);
decision_test(body, Test, TrueFlow, _FalseFlow, State, Context) ->
decision_flow(TrueFlow, Test, State, Context);
decision_test(Test, Test, TrueFlow, _FalseFlow, State, Context) ->
decision_flow(TrueFlow, Test, State, Context);
decision_test(Test, _Test, _TrueFlow, FalseFlow, State, Context) ->
decision_flow(FalseFlow, Test, State, Context).
decision_test_fn({Test, State, Context}, TestFn, TrueFlow, FalseFlow) ->
decision_test_fn(Test, TestFn, TrueFlow, FalseFlow, State, Context).
decision_test_fn({error, Reason}, _TestFn, _TrueFlow, _FalseFlow, State, Context) ->
error_response(Reason, State, Context);
decision_test_fn({error, R0, R1}, _TestFn, _TrueFlow, _FalseFlow, State, Context) ->
error_response({R0, R1}, State, Context);
decision_test_fn({halt, Code}, _TestFn, _TrueFlow, _FalseFlow, State, Context) ->
respond(Code, State, Context);
decision_test_fn(Test,TestFn,TrueFlow,FalseFlow, State, Context) ->
case TestFn(Test) of
true -> decision_flow(TrueFlow, Test, State, Context);
false -> decision_flow(FalseFlow, Test, State, Context)
end.
decision_flow(X, _TestResult, State, Context) when is_atom(X) ->
d(X, State, Context);
decision_flow(X, _TestResult, State, Context) when is_integer(X), X < 500 ->
respond(X, State, Context);
decision_flow(X, TestResult, State, Context) when is_integer(X), X >= 500 ->
error_response(X, TestResult, State, Context).
% decision_flow({ErrCode, Reason}, _TestResult, State, Context) when is_integer(ErrCode) ->
% error_response(ErrCode, Reason, State, Context).
%% "Service Available"
decision(v3b13, State, Context) ->
decision_test(controller_call(service_available, State, Context), true, v3b12, 503);
%% "Known method?"
decision(v3b12, State, Context) ->
{Methods, S1, C1} = controller_call(known_methods, State, Context),
decision_test(lists:member(cowmachine_req:method(C1), Methods), true, v3b11, 501, S1, C1);
%% "URI too long?"
decision(v3b11, State, Context) ->
decision_test(controller_call(uri_too_long, State, Context), true, 414, v3b10);
%% "Method allowed?"
decision(v3b10, State, Context) ->
{Methods, S1, C1} = controller_call(allowed_methods, State, Context),
case lists:member(cowmachine_req:method(C1), Methods) of
true ->
d(v3b9, S1, C1);
false ->
CtxAllow = cowmachine_req:set_resp_header(<<"allow">>, [z_convert:to_binary(M) || M <- Methods], C1),
respond(405, S1, CtxAllow)
end;
%% "Content-MD5 present?"
decision(v3b9, State, Context) ->
ContentMD5 = cowmachine_req:get_req_header(<<"content-md5">>, Context),
decision_test(ContentMD5, undefined, v3b9b, v3b9a, State, Context);
%% "Content-MD5 valid?"
decision(v3b9a, State, Context) ->
{Md5Valid, S1, C1} = controller_call(validate_content_checksum, State, Context),
case Md5Valid of
{error, Reason} ->
error_response(Reason, S1, C1);
{halt, Code} ->
respond(Code, S1, C1);
not_validated ->
Checksum = base64:decode(cowmachine_req:get_req_header(<<"content-md5">>, C1)),
{Body, C2} = cowmachine_req:req_body(C1),
BodyHash = crypto:hash(md5, Body),
case BodyHash =:= Checksum of
true -> d(v3b9b, S1, C2);
_ ->
respond(400, S1, C2)
end;
false ->
respond(400, S1, C1);
_ ->
d(v3b9b, S1, C1)
end;
%% "Malformed?"
decision(v3b9b, State, Context) ->
decision_test(controller_call(malformed_request, State, Context), true, 400, v3b8);
%% "Authorized?"
decision(v3b8, #cmstate{ options = Options } = State, Context) ->
Context1 = case maps:get(on_welformed, Options, undefined) of
undefined -> Context;
Fun when is_function(Fun) -> Fun(Context)
end,
{IsAuthorized, S1, C1} = controller_call(is_authorized, State, Context1),
case IsAuthorized of
true ->
d(v3b7, S1, C1);
false ->
CtxAuth = cowmachine_req:set_resp_header(<<"www-authenticate">>, <<"z.auth">>, C1),
respond(401, S1, CtxAuth);
AuthHead when is_binary(AuthHead) ->
CtxAuth = cowmachine_req:set_resp_header(<<"www-authenticate">>, AuthHead, C1),
respond(401, S1, CtxAuth);
{error, Reason} ->
error_response(Reason, S1, C1);
{halt, Code} ->
respond(Code, S1, C1)
end;
%% "Forbidden?"
decision(v3b7, State, Context) ->
decision_test(controller_call(forbidden, State, Context), true, 403, v3b6_upgrade);
%% "Upgrade?"
decision(v3b6_upgrade, State, Context) ->
case cowmachine_req:get_req_header(<<"upgrade">>, Context) of
undefined ->
decision(v3b6, State, Context);
UpgradeHdr ->
case cowmachine_req:get_req_header(<<"connection">>, Context) of
undefined ->
decision(v3b6, State, Context);
Connection ->
case contains_token(<<"upgrade">>, Connection) of
true ->
{Choosen, S1, C1} = choose_upgrade(UpgradeHdr, State, Context),
case Choosen of
none ->
decision(v3b6, S1, C1);
{_Protocol, UpgradeFunc} ->
{upgrade, UpgradeFunc, S1, C1}
end;
false ->
decision(v3b6, State, Context)
end
end
end;
%% "Okay Content-* Headers?"
decision(v3b6, State, Context) ->
decision_test(controller_call(valid_content_headers, State, Context), true, v3b5, 501);
%% "Known Content-Type?"
decision(v3b5, State, Context) ->
decision_test(controller_call(known_content_type, State, Context), true, v3b4, 415);
%% "Req Entity Too Large?"
decision(v3b4, State, Context) ->
decision_test(controller_call(valid_entity_length, State, Context), true, v3b3, 413);
%% "OPTIONS?"
decision(v3b3, State, Context) ->
case cowmachine_req:method(Context) of
<<"OPTIONS">> ->
{Hdrs, S1, C1} = controller_call(options, State, Context),
respond(200, Hdrs, S1, C1);
_ ->
d(v3c3, State, Context)
end;
%% Accept exists?
decision(v3c3, State, Context) ->
case cowmachine_req:get_req_header(<<"accept">>, Context) of
undefined ->
% No accept header, select the first content-type provided
{ContentTypes, S1, C1} = controller_call(content_types_provided, State, Context),
MType = cowmachine_util:normalize_content_type( hd(ContentTypes) ),
C2 = cowmachine_req:set_resp_content_type(MType, C1),
d(v3d4, S1, C2);
_ ->
d(v3c4, State, Context)
end;
%% Acceptable media type available? (check against Accept header)
decision(v3c4, State, Context) ->
{ContentTypesProvided, S1, C1} = controller_call(content_types_provided, State, Context),
AcceptHdr = cowmachine_req:get_req_header(<<"accept">>, C1),
case cowmachine_util:choose_media_type_provided(ContentTypesProvided, AcceptHdr) of
none ->
respond(406, S1, C1);
MType ->
C2 = cowmachine_req:set_resp_content_type(MType, C1),
d(v3d4, S1, C2)
end;
%% Accept-Language exists?
decision(v3d4, State, Context) ->
decision_test(cowmachine_req:get_req_header(<<"accept-language">>, Context), undefined, v3e5, v3d5, State, Context);
%% Acceptable Language available? %% WMACH-46 (do this as proper conneg)
decision(v3d5, State, Context) ->
decision_test(controller_call(language_available, State, Context), true, v3e5, 406);
%% Accept-Charset exists?
decision(v3e5, State, Context) ->
case cowmachine_req:get_req_header(<<"accept-charset">>, Context) of
undefined -> decision_test(choose_charset(<<"*">>, State, Context), none, 406, v3f6);
_ -> d(v3e6, State, Context)
end;
%% Acceptable Charset available?
decision(v3e6, State, Context) ->
decision_test(
choose_charset(cowmachine_req:get_req_header(<<"accept-charset">>, Context), State, Context),
none, 406, v3f6);
%% Accept-Encoding exists?
% (also, set content-type header here, now that charset is chosen)
decision(v3f6, State, Context) ->
{CT1, CT2, CTArgs} = CType = cowmachine_req:resp_content_type(Context),
CSet = case cowmachine_req:resp_chosen_charset(Context) of
undefined -> CType;
CS -> {CT1, CT2, [ {<<"charset">>, CS} | lists:keydelete(<<"charset">>, 1, CTArgs) ]}
end,
C1 = cowmachine_req:set_resp_header(<<"content-type">>, cowmachine_util:format_content_type(CSet), Context),
case cowmachine_req:get_req_header(<<"accept-encoding">>, C1) of
undefined ->
decision_test(
choose_content_encoding(<<"identity;q=1.0,*;q=0.5">>, State, C1),
none, 406, v3g7);
_ -> d(v3f7, State, C1)
end;
%% Acceptable encoding available?
decision(v3f7, State, Context) ->
decision_test(
choose_content_encoding(cowmachine_req:get_req_header(<<"accept-encoding">>, Context), State, Context),
none, 406, v3g7);
%% "Resource exists?"
decision(v3g7, State, Context) ->
% this is the first place after all conneg, so set Vary here
{Variances, S1, C1} = variances(State, Context),
VarCtx = case Variances of
[] -> C1;
_ -> cowmachine_req:set_resp_header(<<"vary">>, [Variances], C1)
end,
decision_test(controller_call(resource_exists, S1, VarCtx), true, v3g8, v3h7);
%% "If-Match exists?"
decision(v3g8, State, Context) ->
decision_test(cowmachine_req:get_req_header(<<"if-match">>, Context), undefined, v3h10, v3g9, State, Context);
%% "If-Match: * exists"
decision(v3g9, State, Context) ->
decision_test(cowmachine_req:get_req_header(<<"if-match">>, Context), <<"*">>, v3h10, v3g11, State, Context);
%% "ETag in If-Match"
decision(v3g11, State, Context) ->
ETags = cowmachine_util:split_quoted_strings(cowmachine_req:get_req_header(<<"if-match">>, Context)),
decision_test_fn(controller_call(generate_etag, State, Context),
fun(ETag) -> lists:member(ETag, ETags) end,
v3h10, 412);
%% "If-Match: * exists"
decision(v3h7, State, Context) ->
decision_test(cowmachine_req:get_req_header(<<"if-match">>, Context), "*", 412, v3i7, State, Context);
%% "If-unmodified-since exists?"
decision(v3h10, State, Context) ->
decision_test(cowmachine_req:get_req_header(<<"if-unmodified-since">>, Context), undefined, v3i12, v3h11, State, Context);
%% "I-UM-S is valid date?"
decision(v3h11, State, Context) ->
IUMSDate = cowmachine_req:get_req_header(<<"if-unmodified-since">>, Context),
decision_test(cowmachine_util:convert_request_date(IUMSDate), bad_date, v3i12, v3h12, State, Context);
%% "Last-Modified > I-UM-S?"
decision(v3h12, State, Context) ->
ReqDate = cowmachine_req:get_req_header(<<"if-unmodified-since">>, Context),
ReqErlDate = cowmachine_util:convert_request_date(ReqDate),
{ResErlDate, S1, C1} = controller_call(last_modified, State, Context),
decision_test(ResErlDate > ReqErlDate, true, 412, v3i12, S1, C1);
%% "Moved permanently? (apply PUT to different URI)"
decision(v3i4, State, Context) ->
{MovedPermanently, S1, C1} = controller_call(moved_permanently, State, Context),
case MovedPermanently of
{true, MovedURI} ->
LocCtx = cowmachine_req:set_resp_header(<<"location">>, MovedURI, C1),
respond(301, S1, LocCtx);
false ->
d(v3p3, S1, C1);
{error, Reason} ->
error_response(Reason, S1, C1);
{halt, Code} ->
respond(Code, S1, C1)
end;
%% PUT?
decision(v3i7, State, Context) ->
decision_test(cowmachine_req:method(Context), <<"PUT">>, v3i4, v3k7, State, Context);
%% "If-none-match exists?"
decision(v3i12, State, Context) ->
decision_test(cowmachine_req:get_req_header(<<"if-none-match">>, Context), undefined, v3l13, v3i13, State, Context);
%% "If-None-Match: * exists?"
decision(v3i13, State, Context) ->
decision_test(cowmachine_req:get_req_header(<<"if-none-match">>, Context), <<"*">>, v3j18, v3k13, State, Context);
%% GET or HEAD?
decision(v3j18, State, Context) ->
decision_test(lists:member(cowmachine_req:method(Context),[<<"GET">>, <<"HEAD">>]), true, 304, 412, State, Context);
%% "Moved permanently?"
decision(v3k5, State, Context) ->
{MovedPermanently, S1, C1} = controller_call(moved_permanently, State, Context),
case MovedPermanently of
{true, MovedURI} ->
LocCtx = cowmachine_req:set_resp_header(<<"location">>, MovedURI, C1),
respond(301, S1, LocCtx);
false ->
d(v3l5, S1, C1);
{error, Reason} ->
error_response(Reason, S1, C1);
{halt, Code} ->
respond(Code, S1, C1)
end;
%% "Previously existed?"
decision(v3k7, State, Context) ->
decision_test(controller_call(previously_existed, State, Context), true, v3k5, v3l7);
%% "Etag in if-none-match?"
decision(v3k13, State, Context) ->
ETags = cowmachine_util:split_quoted_strings(cowmachine_req:get_req_header(<<"if-none-match">>, Context)),
decision_test_fn(controller_call(generate_etag, State, Context),
%% Membership test is a little counter-intuitive here; if the
%% provided ETag is a member, we follow the error case out
%% via v3j18.
fun(ETag) -> lists:member(ETag, ETags) end,
v3j18, v3l13);
%% "Moved temporarily?"
decision(v3l5, State, Context) ->
{MovedTemporarily, S1, C1} = controller_call(moved_temporarily, State, Context),
case MovedTemporarily of
{true, MovedURI} ->
LocCtx = cowmachine_req:set_resp_header(<<"location">>, MovedURI, C1),
respond(307, S1, LocCtx);
false ->
d(v3m5, S1, C1);
{error, Reason} ->
error_response(Reason, S1, C1);
{halt, Code} ->
respond(Code, S1, C1)
end;
%% "POST?"
decision(v3l7, State, Context) ->
decision_test(cowmachine_req:method(Context), <<"POST">>, v3m7, 404, State, Context);
%% "IMS exists?"
decision(v3l13, State, Context) ->
decision_test(cowmachine_req:get_req_header(<<"if-modified-since">>, Context), undefined, v3m16, v3l14, State, Context);
%% "IMS is valid date?"
decision(v3l14, State, Context) ->
IMSDate = cowmachine_req:get_req_header(<<"if-modified-since">>, Context),
decision_test(cowmachine_util:convert_request_date(IMSDate), bad_date, v3m16, v3l15, State, Context);
%% "IMS > Now?"
decision(v3l15, State, Context) ->
NowDateTime = calendar:universal_time(),
ReqDate = cowmachine_req:get_req_header(<<"if-modified-since">>, Context),
ReqErlDate = cowmachine_util:convert_request_date(ReqDate),
decision_test(ReqErlDate > NowDateTime, true, v3m16, v3l17, State, Context);
%% "Last-Modified > IMS?"
decision(v3l17, State, Context) ->
ReqDate = cowmachine_req:get_req_header(<<"if-modified-since">>, Context),
ReqErlDate = cowmachine_util:convert_request_date(ReqDate),
{ResErlDate, S1, C1} = controller_call(last_modified, State, Context),
decision_test(ResErlDate =:= undefined orelse ResErlDate > ReqErlDate,
true, v3m16, 304, S1, C1);
%% "POST?"
decision(v3m5, State, Context) ->
decision_test(cowmachine_req:method(Context), <<"POST">>, v3n5, 410, State, Context);
%% "Server allows POST to missing resource?"
decision(v3m7, State, Context) ->
decision_test(controller_call(allow_missing_post, State, Context), true, v3n11, 404);
%% "DELETE?"
decision(v3m16, State, Context) ->
decision_test(cowmachine_req:method(Context), <<"DELETE">>, v3m20, v3n16, State, Context);
%% DELETE enacted immediately?
%% Also where DELETE is forced.
decision(v3m20, State, Context) ->
decision_test(accept_process_helper(State, Context), true, v3m20b, 500);
decision(v3m20b, State, Context) ->
decision_test(controller_call(delete_completed, State, Context), true, v3o20, 202);
%% "Server allows POST to missing resource?"
decision(v3n5, State, Context) ->
decision_test(controller_call(allow_missing_post, State, Context), true, v3n11, 410);
%% "Redirect?"
decision(v3n11, State, Context) ->
{PostIsCreate, S1, C1} = controller_call(post_is_create, State, Context),
{Stage1, SStage1, CtxStage1} = case PostIsCreate of
true ->
{CreatePath, S2, C2} = controller_call(create_path, S1, C1),
case CreatePath of
NewPath when is_binary(NewPath) ->
NewPath1 = case NewPath of
<<$/, Path/binary>> -> Path;
_ -> NewPath
end,
PathCtx = cowmachine_req:set_disp_path(<<$/, NewPath1/binary>>, C2),
{LocS, LocCtx} = case cowmachine_req:get_resp_header(<<"location">>, PathCtx) of
undefined ->
{BaseUri0, S3, C3} = controller_call(base_uri, S2, PathCtx),
BaseUri = case BaseUri0 of
undefined -> cowmachine_req:base_uri(C3);
_ -> BaseUri0
end,
Loc = case binary:last(BaseUri) of
$/ -> <<BaseUri/binary, NewPath1/binary>>;
_ -> <<BaseUri/binary, $/, NewPath1/binary>>
end,
{S3, cowmachine_req:set_resp_header(<<"location">>, Loc, PathCtx)};
_ ->
{S2, PathCtx}
end,
{Res, S4, C4} = accept_process_helper(LocS, LocCtx),
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 ->
error_response("post_is_create without create_path", S2, C2);
_ ->
error_response("create_path not a binary string", S2, C2)
end;
false ->
{ProcessPost, S2, C2} = accept_process_helper(S1, C1),
case ProcessPost of
{halt, Code} -> respond(Code, S2, C2);
{error, _} = Err -> error_response(Err, S2, C2);
{error, _, _} = Err -> error_response(Err, S2, C2);
_ -> {stage1_ok, S2, C2}
end
end,
case Stage1 of
stage1_ok ->
case cowmachine_req:resp_redirect(CtxStage1) of
true ->
case cowmachine_req:get_resp_header(<<"location">>, CtxStage1) of
undefined ->
cowmachine:log(#{
level => error,
at => ?AT,
code => 500,
controller => State#cmstate.controller,
text => "Response had set_resp_redirect but no Location header."},
Context),
respond(500, SStage1, CtxStage1);
_ ->
respond(303, SStage1, CtxStage1)
end;
false ->
d(v3p11, SStage1, CtxStage1)
end;
_ ->
{nop, SStage1, CtxStage1}
end;
%% "POST?"
decision(v3n16, State, Context) ->
decision_test(cowmachine_req:method(Context), <<"POST">>, v3n11, v3o16, State, Context);
%% Conflict? -- Only with PUT
decision(v3o14, State, Context) ->
{IsConflict, S1, C1} = controller_call(is_conflict, State, Context),
case IsConflict of
true ->
respond(409, S1, C1);
_ ->
{Res, SHelp, CHelp} = accept_process_helper(S1, C1),
case Res of
{halt, Code} -> respond(Code, SHelp, CHelp);
{error, _, _} -> error_response(Res, SHelp, CHelp);
{error, _} -> error_response(Res, SHelp, CHelp);
_ -> d(v3p11, SHelp, CHelp)
end
end;
%% "PUT?"
decision(v3o16, State, Context) ->
decision_test(cowmachine_req:method(Context), <<"PUT">>, v3o14, v3o18, State, Context);
%% Multiple representations?
% (also where body generation for GET and HEAD is done)
decision(v3o18, State, Context) ->
{ProcessResult, SBody, CBody} = case State#cmstate.is_process_called of
false ->
process_helper(undefined, State, Context);
true ->
{nop, State, Context}
end,
% If a response body was set then also set etag, modified, etc.
{S2, C2} = case cowmachine_req:has_resp_body(CBody) of
true ->
{S1, C1} = etag_etc_helper(SBody, CBody),
{S1, C1};
false ->
{SBody, CBody}
end,
case ProcessResult of
{halt, Code} -> respond(Code, S2, C2);
{error, _} -> error_response(ProcessResult, S2, C2);
{error, _,_} -> error_response(ProcessResult, S2, C2);
_ -> d(v3o18b, S2, C2)
end;
decision(v3o18b, State, Context) ->
decision_test(controller_call(multiple_choices, State, Context), true, 300, 200);
%% Response includes an entity?
decision(v3o20, State, Context) ->
decision_test(cowmachine_req:has_resp_body(Context), true, v3o18, 204, State, Context);
%% Conflict?
decision(v3p3, State, Context) ->
{IsConflict, S1, C1} = controller_call(is_conflict, State, Context),
case IsConflict of
true -> respond(409, S1, C1);
_ ->
{Res, SHelp, CHelp} = accept_process_helper(S1, C1),
case Res of
{halt, Code} -> respond(Code, SHelp, CHelp);
{error, _, _} -> error_response(Res, SHelp, CHelp);
{error, _} -> error_response(Res, SHelp, CHelp);
_ -> d(v3p11, SHelp, CHelp)
end
end;
%% New controller? (at this point boils down to "has location header")
decision(v3p11, State, Context) ->
case cowmachine_req:get_resp_header(<<"location">>, Context) of
undefined -> d(v3o20, State, Context);
_ -> respond(201, State, Context)
end.
%% @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 ->
CTParsed = case cowmachine_req:get_req_header(<<"content-type">>, Context) of
undefined ->
{<<"application">>, <<"octet-stream">>, []};
CTHeader ->
cow_http_hd:parse_content_type(CTHeader)
end,
RdCT = cowmachine_req:set_metadata(content_type, CTParsed, Context),
{ContentTypesAccepted, State1, Context1} = controller_call(content_types_accepted, State, RdCT),
case cowmachine_util:is_media_type_accepted(ContentTypesAccepted, CTParsed) of
false ->
{{halt, 415}, State1, Context1};
true ->
process_helper(CTParsed, State1, Context1)
end;
false ->
process_helper(undefined, State, Context)
end.
should_have_req_body(Context) ->
case cowmachine_req:method(Context) of
<<"POST">> -> true;
<<"PUT">> -> true;
<<"PATCH">> -> true;
_ -> false
end.
process_helper(_ContentTypeAccepted, #cmstate{ is_process_called = true } = State, Context) ->
cowmachine:log(#{ level => error, text => "ERROR in process handling, process_helper called twice."}, Context),
{{error, internal_logic}, State, Context};
process_helper(ContentTypeAccepted, State, Context) ->
S1 = State#cmstate{ is_process_called = true },
{Res, S2, C2} = Result = controller_call_process(ContentTypeAccepted, S1, Context),
case Res of
{halt, _} -> Result;
{error, _, _} -> Result;
{error, _} -> Result;
true -> Result;
false -> Result;
RespBody ->
C3 = cowmachine_req:set_resp_body(RespBody, C2),
{body, S2, C3}
end.
etag_etc_helper(State, Context) ->
{Etag, SEtag, CEtag0} = controller_call(generate_etag, State, Context),
CEtag = case Etag of
undefined -> CEtag0;
ETag -> cowmachine_req:set_resp_header(<<"etag">>, cowmachine_util:quoted_string(ETag), CEtag0)
end,
{LastModified, SLM, CLM0} = controller_call(last_modified, SEtag, CEtag),
CLM = case LastModified of
undefined -> CLM0;
LM -> cowmachine_req:set_resp_header(<<"last-modified">>,
z_convert:to_binary(httpd_util:rfc1123_date(calendar:universal_time_to_local_time(LM))),
CLM0)
end,
{Expires, SExp, CExp0} = controller_call(expires, SLM, CLM),
CExp = case Expires of
undefined -> CExp0;
Exp -> cowmachine_req:set_resp_header(<<"expires">>,
z_convert:to_binary(httpd_util:rfc1123_date(calendar:universal_time_to_local_time(Exp))),
CExp0)
end,
CIfRange = check_if_range(Etag, LastModified, CExp),
{SExp, CIfRange}.
% Only called for 'GET' and 'HEAD' - check if 206 result is allowed
check_if_range(Etag, LastModified, Context) ->
IsRangeOk = is_if_range_ok(cowmachine_req:get_req_header(<<"if-range">>, Context), Etag, LastModified),
cowmachine_req:set_range_ok(IsRangeOk, Context).
is_if_range_ok(undefined, _ETag, _LM) ->
true;
is_if_range_ok(<<"W/\"", _/binary>>, _ETag, _LM) ->
false;
is_if_range_ok(<<"w/\"", _/binary>>, _ETag, _LM) ->
false;
is_if_range_ok(<<$", _/binary>>, undefined, _LM) ->
false;
is_if_range_ok(<<$", _/binary>> = IfETag, ETag, _LM) ->
ETags = cowmachine_util:split_quoted_strings(IfETag),
lists:member(ETag, ETags);
is_if_range_ok(Date, _ETag, LM) ->
ErlDate = cowmachine_util:convert_request_date(Date),
ErlDate =/= undefined andalso ErlDate >= LM.
choose_content_encoding(AccEncHdr, State, Context) ->
{EncodingsProvided, Rs1, Rd1} = controller_call(content_encodings_provided, State, Context),
case cowmachine_util:choose_encoding(EncodingsProvided, AccEncHdr) of
none ->
{none, Rs1, Rd1};
ChosenEnc ->
RdEnc = case ChosenEnc of
<<"identity">> -> Rd1;
_ -> cowmachine_req:set_resp_header(<<"content-encoding">>,ChosenEnc, Rd1)
end,
RdEnc1 = cowmachine_req:set_resp_content_encoding(ChosenEnc,RdEnc),
{ChosenEnc, Rs1, RdEnc1}
end.
choose_transfer_encoding(AccEncHdr, State, Context) ->
choose_transfer_encoding(cowmachine_req:version(Context), AccEncHdr, State, Context).
choose_transfer_encoding({1,0}, _AccEncHdr, State, Context) ->
{State, Context};
choose_transfer_encoding({1,1}, AccEncHdr, State, Context) ->
{EncodingsProvided, Rs1, Rd1} = controller_call(transfer_encodings_provided, State, Context),
EncList = [ Enc || {Enc, _Func} <- EncodingsProvided ],
case cowmachine_util:choose_encoding(EncList, AccEncHdr) of
none ->
{Rs1, Rd1};
<<"identity">> ->
{Rs1, Rd1};
ChosenEnc ->
Enc = {ChosenEnc, _} = lists:keyfind(ChosenEnc, 1, EncodingsProvided),
RdEnc = cowmachine_req:set_resp_transfer_encoding(Enc, Rd1),
{Rs1, RdEnc}
end;
choose_transfer_encoding(_, _AccEncHdr, State, Context) ->
{State, Context}.
choose_charset(AccCharHdr, State, Context) ->
{CharsetsProvided, Rs1, Rd1} = controller_call(charsets_provided, State, Context),
case CharsetsProvided of
no_charset ->
{no_charset, Rs1, Rd1};
CL ->
CSets = [maybe_old_tuple_value(CSet) || CSet <- CL],
case cowmachine_util:choose_charset(CSets, AccCharHdr) of
none ->
{none, Rs1, Rd1};
Charset ->
RdCSet = cowmachine_req:set_resp_chosen_charset(Charset, Rd1),
{Charset, Rs1, RdCSet}
end
end.
maybe_old_tuple_value({A, _}) -> A;
maybe_old_tuple_value(A) -> A.
choose_upgrade(UpgradeHdr, State, Context) ->
{UpgradesProvided, Rs1, Rd1} = controller_call(upgrades_provided, State, Context),
Provided1 = [ {z_string:to_lower(Prot), Prot, PFun} || {Prot, PFun} <- UpgradesProvided],
Requested = [ z_string:to_lower(z_string:trim(Up)) || Up <- binary:split(UpgradeHdr, <<",">>, [global]) ],
{choose_upgrade1(Requested, Provided1), Rs1, Rd1}.
choose_upgrade1([], _) ->
none;
choose_upgrade1([Req|Requested], Provided) ->
case lists:keysearch(Req, 1, Provided) of
false ->
choose_upgrade1(Requested, Provided);
{value, {_, Protocol, UpgradeFun}} ->
{Protocol, UpgradeFun}
end.
variances(State, Context) ->
{ContentTypesProvided, Rs1, Rd1} = controller_call(content_types_provided, State, Context),
Accept = case length(ContentTypesProvided) of
1 -> [];
0 -> [];
_ -> [<<"accept">>]
end,
{EncodingsProvided, Rs2, Rd2} = controller_call(content_encodings_provided, Rs1, Rd1),
AcceptEncoding = case length(EncodingsProvided) of
1 -> [];
0 -> [];
_ -> [<<"accept-encoding">>]
end,
{CharsetsProvided, Rs3, Rd3} = controller_call(charsets_provided, Rs2, Rd2),
AcceptCharset = case CharsetsProvided of
no_charset ->
[];
CP ->
case length(CP) of
1 -> [];
0 -> [];
_ -> [<<"accept-charset">>]
end
end,
{Variances, Rs4, Rd4} = controller_call(variances, Rs3, Rd3),
{Accept ++ AcceptEncoding ++ AcceptCharset ++ Variances, Rs4, Rd4}.
contains_token(Token, HeaderString) ->
Tokens = lists:map(fun (T) ->
z_string:trim(z_string:to_lower(T))
end,
binary:split(HeaderString, <<",">>, [global])),
lists:member(Token, Tokens).