|
@@ -0,0 +1,758 @@
|
|
|
+%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
|
|
|
+%%
|
|
|
+%% Permission to use, copy, modify, and/or distribute this software for any
|
|
|
+%% purpose with or without fee is hereby granted, provided that the above
|
|
|
+%% copyright notice and this permission notice appear in all copies.
|
|
|
+%%
|
|
|
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
|
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
|
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
|
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
|
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
|
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
|
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
+
|
|
|
+%% @doc Experimental REST protocol implementation.
|
|
|
+%%
|
|
|
+%% Based on the Webmachine Diagram from Alan Dean and Justin Sheehy, which
|
|
|
+%% can be found in the Webmachine source tree, and on the Webmachine
|
|
|
+%% documentation available at http://wiki.basho.com/Webmachine.html
|
|
|
+%% at the time of writing.
|
|
|
+-module(cowboy_http_rest).
|
|
|
+-export([upgrade/4]).
|
|
|
+
|
|
|
+-record(state, {
|
|
|
+ %% Handler.
|
|
|
+ handler :: atom(),
|
|
|
+ handler_state :: any(),
|
|
|
+
|
|
|
+ %% Media type.
|
|
|
+ content_types_p = [] ::
|
|
|
+ [{{binary(), binary(), [{binary(), binary()}]}, atom()}],
|
|
|
+ content_type_a :: undefined
|
|
|
+ | {{binary(), binary(), [{binary(), binary()}]}, atom()},
|
|
|
+
|
|
|
+ %% Language.
|
|
|
+ languages_p = [] :: [binary()],
|
|
|
+ language_a :: undefined | binary(),
|
|
|
+
|
|
|
+ %% Charset.
|
|
|
+ charsets_p = [] :: [binary()],
|
|
|
+ charset_a :: undefined | binary(),
|
|
|
+
|
|
|
+ %% Cached resource calls.
|
|
|
+ etag :: undefined | no_call | binary(),
|
|
|
+ last_modified :: undefined | no_call | cowboy_clock:datetime(),
|
|
|
+ expires :: undefined | no_call | cowboy_clock:datetime()
|
|
|
+}).
|
|
|
+
|
|
|
+-include("include/http.hrl").
|
|
|
+
|
|
|
+%% @doc Upgrade a HTTP request to the REST protocol.
|
|
|
+%%
|
|
|
+%% You do not need to call this function manually. To upgrade to the REST
|
|
|
+%% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em>
|
|
|
+%% in your <em>cowboy_http_handler:init/3</em> handler function.
|
|
|
+-spec upgrade(pid(), module(), any(), #http_req{}) -> ok.
|
|
|
+upgrade(_ListenerPid, Handler, Opts, Req) ->
|
|
|
+ try
|
|
|
+ case erlang:function_exported(Handler, rest_init, 2) of
|
|
|
+ true ->
|
|
|
+ case Handler:rest_init(Req, Opts) of
|
|
|
+ {ok, Req2, HandlerState} ->
|
|
|
+ service_available(Req2, #state{handler=Handler,
|
|
|
+ handler_state=HandlerState})
|
|
|
+ end;
|
|
|
+ false ->
|
|
|
+ service_available(Req, #state{handler=Handler})
|
|
|
+ end
|
|
|
+ catch Class:Reason ->
|
|
|
+ error_logger:error_msg(
|
|
|
+ "** Handler ~p terminating in rest_init/3~n"
|
|
|
+ " for the reason ~p:~p~n** Options were ~p~n"
|
|
|
+ "** Request was ~p~n** Stacktrace: ~p~n~n",
|
|
|
+ [Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()]),
|
|
|
+ {ok, _Req2} = cowboy_http_req:reply(500, Req),
|
|
|
+ ok
|
|
|
+ end.
|
|
|
+
|
|
|
+service_available(Req, State) ->
|
|
|
+ expect(Req, State, service_available, true, fun known_methods/2, 503).
|
|
|
+
|
|
|
+known_methods(Req=#http_req{method=Method}, State) ->
|
|
|
+ case call(Req, State, known_methods) of
|
|
|
+ no_call ->
|
|
|
+ next(Req, State, fun uri_too_long/2);
|
|
|
+ {List, Req2, HandlerState2} ->
|
|
|
+ State2 = State#state{handler_state=HandlerState2},
|
|
|
+ case lists:member(Method, List) of
|
|
|
+ true -> next(Req2, State2, fun uri_too_long/2);
|
|
|
+ false -> next(Req2, State2, 501)
|
|
|
+ end
|
|
|
+ end.
|
|
|
+
|
|
|
+uri_too_long(Req, State) ->
|
|
|
+ expect(Req, State, uri_too_long, false, fun allowed_methods/2, 414).
|
|
|
+
|
|
|
+allowed_methods(Req=#http_req{method=Method}, State) ->
|
|
|
+ case call(Req, State, allowed_methods) of
|
|
|
+ no_call ->
|
|
|
+ next(Req, State, fun malformed_request/2);
|
|
|
+ {List, Req2, HandlerState2} ->
|
|
|
+ State2 = State#state{handler_state=HandlerState2},
|
|
|
+ case lists:member(Method, List) of
|
|
|
+ true -> next(Req2, State2, fun malformed_request/2);
|
|
|
+ false -> method_not_allowed(Req2, State2, List)
|
|
|
+ end
|
|
|
+ end.
|
|
|
+
|
|
|
+method_not_allowed(Req, State, Methods) ->
|
|
|
+ {ok, Req2} = cowboy_http_req:set_resp_header(
|
|
|
+ <<"Allow">>, method_not_allowed_build(Methods, []), Req),
|
|
|
+ respond(Req2, State, 405).
|
|
|
+
|
|
|
+method_not_allowed_build([], []) ->
|
|
|
+ <<>>;
|
|
|
+method_not_allowed_build([], [_Ignore|Acc]) ->
|
|
|
+ lists:reverse(Acc);
|
|
|
+method_not_allowed_build([Method|Tail], Acc) ->
|
|
|
+ Method2 = list_to_binary(atom_to_list(Method)),
|
|
|
+ method_not_allowed_build(Tail, [<<", ">>, Method2|Acc]).
|
|
|
+
|
|
|
+malformed_request(Req, State) ->
|
|
|
+ expect(Req, State, malformed_request, false, fun is_authorized/2, 400).
|
|
|
+
|
|
|
+is_authorized(Req, State) ->
|
|
|
+ case call(Req, State, is_authorized) of
|
|
|
+ no_call ->
|
|
|
+ forbidden(Req, State);
|
|
|
+ {true, Req2, HandlerState2} ->
|
|
|
+ forbidden(Req2, State#state{handler_state=HandlerState2});
|
|
|
+ {{false, AuthHead}, Req2, HandlerState2} ->
|
|
|
+ {ok, Req3} = cowboy_http_req:set_resp_header(
|
|
|
+ <<"Www-Authenticate">>, AuthHead, Req2),
|
|
|
+ respond(Req3, State#state{handler_state=HandlerState2}, 401)
|
|
|
+ end.
|
|
|
+
|
|
|
+forbidden(Req, State) ->
|
|
|
+ expect(Req, State, forbidden, false, fun valid_content_headers/2, 403).
|
|
|
+
|
|
|
+valid_content_headers(Req, State) ->
|
|
|
+ expect(Req, State, valid_content_headers, true,
|
|
|
+ fun known_content_type/2, 501).
|
|
|
+
|
|
|
+known_content_type(Req, State) ->
|
|
|
+ expect(Req, State, known_content_type, true,
|
|
|
+ fun valid_entity_length/2, 413).
|
|
|
+
|
|
|
+valid_entity_length(Req, State) ->
|
|
|
+ expect(Req, State, valid_entity_length, true, fun options/2, 413).
|
|
|
+
|
|
|
+options(Req=#http_req{method='OPTIONS'}, State) ->
|
|
|
+ {ok, Req2, HandlerState2} = call(Req, State, options),
|
|
|
+ respond(Req2, State#state{handler_state=HandlerState2}, 200);
|
|
|
+options(Req, State) ->
|
|
|
+ content_types_provided(Req, State).
|
|
|
+
|
|
|
+%% The content_types_provided function MUST be defined in the resource
|
|
|
+%% from this point onward.
|
|
|
+content_types_provided(Req, State) ->
|
|
|
+ case call(Req, State, content_types_provided) of
|
|
|
+ no_call ->
|
|
|
+ not_acceptable(Req, State);
|
|
|
+ {[], Req2, HandlerState2} ->
|
|
|
+ not_acceptable(Req2, State#state{handler_state=HandlerState2});
|
|
|
+ {CTP, Req2, HandlerState2} ->
|
|
|
+ State2 = State#state{handler_state=HandlerState2, content_types_p=CTP},
|
|
|
+ {Accept, Req3} = cowboy_http_req:parse_header('Accept', Req2),
|
|
|
+ case Accept of
|
|
|
+ undefined ->
|
|
|
+ languages_provided(Req3,
|
|
|
+ State2#state{content_type_a=hd(CTP)});
|
|
|
+ Accept ->
|
|
|
+ Accept2 = prioritize_accept(Accept),
|
|
|
+ choose_media_type(Req3, State2, Accept2)
|
|
|
+ end
|
|
|
+ end.
|
|
|
+
|
|
|
+prioritize_accept(Accept) ->
|
|
|
+ lists:sort(
|
|
|
+ fun ({MediaTypeA, Quality, _AcceptParamsA},
|
|
|
+ {MediaTypeB, Quality, _AcceptParamsB}) ->
|
|
|
+ %% Same quality, check precedence in more details.
|
|
|
+ prioritize_mediatype(MediaTypeA, MediaTypeB);
|
|
|
+ ({_MediaTypeA, QualityA, _AcceptParamsA},
|
|
|
+ {_MediaTypeB, QualityB, _AcceptParamsB}) ->
|
|
|
+ %% Just compare the quality.
|
|
|
+ QualityA > QualityB
|
|
|
+ end, Accept).
|
|
|
+
|
|
|
+%% Media ranges can be overridden by more specific media ranges or
|
|
|
+%% specific media types. If more than one media range applies to a given
|
|
|
+%% type, the most specific reference has precedence.
|
|
|
+%%
|
|
|
+%% We always choose B over A when we can't decide between the two.
|
|
|
+prioritize_mediatype({TypeA, SubTypeA, ParamsA}, {TypeB, SubTypeB, ParamsB}) ->
|
|
|
+ case TypeB of
|
|
|
+ TypeA ->
|
|
|
+ case SubTypeB of
|
|
|
+ SubTypeA -> length(ParamsA) > length(ParamsB);
|
|
|
+ <<"*">> -> true;
|
|
|
+ _Any -> false
|
|
|
+ end;
|
|
|
+ <<"*">> -> true;
|
|
|
+ _Any -> false
|
|
|
+ end.
|
|
|
+
|
|
|
+%% Ignoring the rare AcceptParams. Not sure what should be done about them.
|
|
|
+choose_media_type(Req, State, []) ->
|
|
|
+ not_acceptable(Req, State);
|
|
|
+choose_media_type(Req, State=#state{content_types_p=CTP},
|
|
|
+ [MediaType|Tail]) ->
|
|
|
+ match_media_type(Req, State, Tail, CTP, MediaType).
|
|
|
+
|
|
|
+match_media_type(Req, State, Accept, [], _MediaType) ->
|
|
|
+ choose_media_type(Req, State, Accept);
|
|
|
+match_media_type(Req, State, Accept,
|
|
|
+ [Provided = {{Type, SubType_P, Params_P}, _Fun}|Tail],
|
|
|
+ MediaType = {{Type, SubType_A, Params_A}, _Quality, _AcceptParams})
|
|
|
+ when SubType_P =:= SubType_A; SubType_A =:= <<"*">> ->
|
|
|
+ case lists:sort(Params_P) =:= lists:sort(Params_A) of
|
|
|
+ true ->
|
|
|
+ languages_provided(Req, State#state{content_type_a=Provided});
|
|
|
+ false ->
|
|
|
+ match_media_type(Req, State, Accept, Tail, MediaType)
|
|
|
+ end;
|
|
|
+match_media_type(Req, State, Accept, [_Any|Tail], MediaType) ->
|
|
|
+ match_media_type(Req, State, Accept, Tail, MediaType).
|
|
|
+
|
|
|
+%% @todo I suppose we should also ask the resource if it wants to
|
|
|
+%% set a language itself or if it wants it to be automatically chosen.
|
|
|
+languages_provided(Req, State) ->
|
|
|
+ case call(Req, State, languages_provided) of
|
|
|
+ no_call ->
|
|
|
+ charsets_provided(Req, State);
|
|
|
+ {[], Req2, HandlerState2} ->
|
|
|
+ not_acceptable(Req2, State#state{handler_state=HandlerState2});
|
|
|
+ {LP, Req2, HandlerState2} ->
|
|
|
+ State2 = State#state{handler_state=HandlerState2, languages_p=LP},
|
|
|
+ {AcceptLanguage, Req3} =
|
|
|
+ cowboy_http_req:parse_header('Accept-Language', Req2),
|
|
|
+ case AcceptLanguage of
|
|
|
+ undefined ->
|
|
|
+ set_language(Req3, State2#state{language_a=hd(LP)});
|
|
|
+ AcceptLanguage ->
|
|
|
+ AcceptLanguage2 = prioritize_languages(AcceptLanguage),
|
|
|
+ choose_language(Req3, State2, AcceptLanguage2)
|
|
|
+ end
|
|
|
+ end.
|
|
|
+
|
|
|
+%% A language-range matches a language-tag if it exactly equals the tag,
|
|
|
+%% or if it exactly equals a prefix of the tag such that the first tag
|
|
|
+%% character following the prefix is "-". The special range "*", if
|
|
|
+%% present in the Accept-Language field, matches every tag not matched
|
|
|
+%% by any other range present in the Accept-Language field.
|
|
|
+%%
|
|
|
+%% @todo The last sentence probably means we should always put '*'
|
|
|
+%% at the end of the list.
|
|
|
+prioritize_languages(AcceptLanguages) ->
|
|
|
+ lists:sort(
|
|
|
+ fun ({_TagA, QualityA}, {_TagB, QualityB}) ->
|
|
|
+ QualityA > QualityB
|
|
|
+ end, AcceptLanguages).
|
|
|
+
|
|
|
+choose_language(Req, State, []) ->
|
|
|
+ not_acceptable(Req, State);
|
|
|
+choose_language(Req, State=#state{languages_p=LP}, [Language|Tail]) ->
|
|
|
+ match_language(Req, State, Tail, LP, Language).
|
|
|
+
|
|
|
+match_language(Req, State, Accept, [], _Language) ->
|
|
|
+ choose_language(Req, State, Accept);
|
|
|
+match_language(Req, State, _Accept, [Provided|_Tail], {'*', _Quality}) ->
|
|
|
+ set_language(Req, State#state{language_a=Provided});
|
|
|
+match_language(Req, State, _Accept, [Provided|_Tail], {Provided, _Quality}) ->
|
|
|
+ set_language(Req, State#state{language_a=Provided});
|
|
|
+match_language(Req, State, Accept, [Provided|Tail],
|
|
|
+ Language = {Tag, _Quality}) ->
|
|
|
+ Length = byte_size(Tag),
|
|
|
+ case Provided of
|
|
|
+ << Tag:Length/binary, $-, _Any/bits >> ->
|
|
|
+ set_language(Req, State#state{language_a=Provided});
|
|
|
+ _Any ->
|
|
|
+ match_language(Req, State, Accept, Tail, Language)
|
|
|
+ end.
|
|
|
+
|
|
|
+set_language(Req, State=#state{language_a=Language}) ->
|
|
|
+ {ok, Req2} = cowboy_http_req:set_resp_header(
|
|
|
+ <<"Content-Language">>, Language, Req),
|
|
|
+ charsets_provided(Req2, State).
|
|
|
+
|
|
|
+charsets_provided(Req, State) ->
|
|
|
+ case call(Req, State, charsets_provided) of
|
|
|
+ no_call ->
|
|
|
+ set_content_type(Req, State);
|
|
|
+ {[], Req2, HandlerState2} ->
|
|
|
+ not_acceptable(Req2, State#state{handler_state=HandlerState2});
|
|
|
+ {CP, Req2, HandlerState2} ->
|
|
|
+ State2 = State#state{handler_state=HandlerState2, charsets_p=CP},
|
|
|
+ {AcceptCharset, Req3} =
|
|
|
+ cowboy_http_req:parse_header('Accept-Charset', Req2),
|
|
|
+ case AcceptCharset of
|
|
|
+ undefined ->
|
|
|
+ set_content_type(Req3, State2#state{charset_a=hd(CP)});
|
|
|
+ AcceptCharset ->
|
|
|
+ AcceptCharset2 = prioritize_charsets(AcceptCharset),
|
|
|
+ choose_charset(Req3, State2, AcceptCharset2)
|
|
|
+ end
|
|
|
+ end.
|
|
|
+
|
|
|
+%% The special value "*", if present in the Accept-Charset field,
|
|
|
+%% matches every character set (including ISO-8859-1) which is not
|
|
|
+%% mentioned elsewhere in the Accept-Charset field. If no "*" is present
|
|
|
+%% in an Accept-Charset field, then all character sets not explicitly
|
|
|
+%% mentioned get a quality value of 0, except for ISO-8859-1, which gets
|
|
|
+%% a quality value of 1 if not explicitly mentioned.
|
|
|
+prioritize_charsets(AcceptCharsets) ->
|
|
|
+ AcceptCharsets2 = lists:sort(
|
|
|
+ fun ({_CharsetA, QualityA}, {_CharsetB, QualityB}) ->
|
|
|
+ QualityA > QualityB
|
|
|
+ end, AcceptCharsets),
|
|
|
+ case lists:keymember(<<"*">>, 1, AcceptCharsets2) of
|
|
|
+ true -> AcceptCharsets2;
|
|
|
+ false -> [{<<"iso-8859-1">>, 1000}|AcceptCharsets2]
|
|
|
+ end.
|
|
|
+
|
|
|
+choose_charset(Req, State, []) ->
|
|
|
+ not_acceptable(Req, State);
|
|
|
+choose_charset(Req, State=#state{charsets_p=CP}, [Charset|Tail]) ->
|
|
|
+ match_charset(Req, State, Tail, CP, Charset).
|
|
|
+
|
|
|
+match_charset(Req, State, Accept, [], _Charset) ->
|
|
|
+ choose_charset(Req, State, Accept);
|
|
|
+match_charset(Req, State, _Accept, [Provided|_Tail],
|
|
|
+ {Provided, _Quality}) ->
|
|
|
+ set_content_type(Req, State#state{charset_a=Provided});
|
|
|
+match_charset(Req, State, Accept, [_Provided|Tail], Charset) ->
|
|
|
+ match_charset(Req, State, Accept, Tail, Charset).
|
|
|
+
|
|
|
+set_content_type(Req, State=#state{
|
|
|
+ content_type_a={{Type, SubType, Params}, _Fun},
|
|
|
+ charset_a=Charset}) ->
|
|
|
+ ParamsBin = set_content_type_build_params(Params, []),
|
|
|
+ ContentType = [Type, <<"/">>, SubType, ParamsBin],
|
|
|
+ ContentType2 = case Charset of
|
|
|
+ undefined -> ContentType;
|
|
|
+ Charset -> [ContentType, <<"; charset=">>, Charset]
|
|
|
+ end,
|
|
|
+ {ok, Req2} = cowboy_http_req:set_resp_header(
|
|
|
+ <<"Content-Type">>, ContentType2, Req),
|
|
|
+ encodings_provided(Req2, State).
|
|
|
+
|
|
|
+set_content_type_build_params([], []) ->
|
|
|
+ <<>>;
|
|
|
+set_content_type_build_params([], Acc) ->
|
|
|
+ lists:reverse(Acc);
|
|
|
+set_content_type_build_params([{Attr, Value}|Tail], Acc) ->
|
|
|
+ set_content_type_build_params(Tail, [[Attr, <<"=">>, Value], <<";">>|Acc]).
|
|
|
+
|
|
|
+%% @todo Match for identity as we provide nothing else for now.
|
|
|
+%% @todo Don't forget to set the Content-Encoding header when we reply a body
|
|
|
+%% and the found encoding is something other than identity.
|
|
|
+encodings_provided(Req, State) ->
|
|
|
+ variances(Req, State).
|
|
|
+
|
|
|
+not_acceptable(Req, State) ->
|
|
|
+ respond(Req, State, 406).
|
|
|
+
|
|
|
+%% @todo Do Accept-Encoding too when we handle it.
|
|
|
+%% @todo Does the order matter?
|
|
|
+variances(Req, State=#state{content_types_p=CTP,
|
|
|
+ languages_p=LP, charsets_p=CP}) ->
|
|
|
+ Variances = case length(CTP) of
|
|
|
+ 0 -> [];
|
|
|
+ 1 -> [];
|
|
|
+ _NCT -> [<<"Accept">>]
|
|
|
+ end,
|
|
|
+ Variances2 = case length(LP) of
|
|
|
+ 0 -> Variances;
|
|
|
+ 1 -> Variances;
|
|
|
+ _NL -> [<<"Accept-Language">>|Variances]
|
|
|
+ end,
|
|
|
+ Variances3 = case length(CP) of
|
|
|
+ 0 -> Variances2;
|
|
|
+ 1 -> Variances2;
|
|
|
+ _NC -> [<<"Accept-Charset">>|Variances2]
|
|
|
+ end,
|
|
|
+ {Variances4, Req3, State2} = case call(Req, State, variances) of
|
|
|
+ no_call ->
|
|
|
+ {Variances3, Req, State};
|
|
|
+ {HandlerVariances, Req2, HandlerState} ->
|
|
|
+ {Variances3 ++ HandlerVariances, Req2,
|
|
|
+ State#state{handler_state=HandlerState}}
|
|
|
+ end,
|
|
|
+ case lists:flatten([[<<", ">>, V] || V <- Variances4]) of
|
|
|
+ [] ->
|
|
|
+ resource_exists(Req3, State2);
|
|
|
+ [<<", ">>, Variances5] ->
|
|
|
+ {ok, Req4} = cowboy_http_req:set_resp_header(
|
|
|
+ <<"Variances">>, Variances5, Req3),
|
|
|
+ resource_exists(Req4, State2)
|
|
|
+ end.
|
|
|
+
|
|
|
+resource_exists(Req, State) ->
|
|
|
+ expect(Req, State, resource_exists, true,
|
|
|
+ fun if_match_exists/2, fun if_match_musnt_exist/2).
|
|
|
+
|
|
|
+if_match_exists(Req, State) ->
|
|
|
+ case cowboy_http_req:parse_header('If-Match', Req) of
|
|
|
+ {undefined, Req2} ->
|
|
|
+ if_unmodified_since_exists(Req2, State);
|
|
|
+ {'*', Req2} ->
|
|
|
+ if_unmodified_since_exists(Req2, State);
|
|
|
+ {ETagsList, Req2} ->
|
|
|
+ if_match(Req2, State, ETagsList)
|
|
|
+ end.
|
|
|
+
|
|
|
+if_match(Req, State, EtagsList) ->
|
|
|
+ {Etag, Req2, State2} = generate_etag(Req, State),
|
|
|
+ case Etag of
|
|
|
+ no_call ->
|
|
|
+ precondition_failed(Req2, State2);
|
|
|
+ Etag ->
|
|
|
+ case lists:member(Etag, EtagsList) of
|
|
|
+ true -> if_unmodified_since_exists(Req2, State2);
|
|
|
+ false -> precondition_failed(Req2, State2)
|
|
|
+ end
|
|
|
+ end.
|
|
|
+
|
|
|
+if_match_musnt_exist(Req, State) ->
|
|
|
+ case cowboy_http_req:header('If-Match', Req) of
|
|
|
+ {undefined, Req2} -> is_put_to_missing_resource(Req2, State);
|
|
|
+ {_Any, Req2} -> precondition_failed(Req2, State)
|
|
|
+ end.
|
|
|
+
|
|
|
+if_unmodified_since_exists(Req, State) ->
|
|
|
+ case cowboy_http_req:parse_header('If-Unmodified-Since', Req) of
|
|
|
+ {undefined, Req2} ->
|
|
|
+ if_none_match_exists(Req2, State);
|
|
|
+ {{error, badarg}, Req2} ->
|
|
|
+ if_none_match_exists(Req2, State);
|
|
|
+ {IfUnmodifiedSince, Req2} ->
|
|
|
+ if_unmodified_since(Req2, State, IfUnmodifiedSince)
|
|
|
+ end.
|
|
|
+
|
|
|
+%% If LastModified is the atom 'no_call', we continue.
|
|
|
+if_unmodified_since(Req, State, IfUnmodifiedSince) ->
|
|
|
+ {LastModified, Req2, State2} = last_modified(Req, State),
|
|
|
+ case LastModified > IfUnmodifiedSince of
|
|
|
+ true -> precondition_failed(Req2, State2);
|
|
|
+ false -> if_none_match_exists(Req2, State2)
|
|
|
+ end.
|
|
|
+
|
|
|
+if_none_match_exists(Req, State) ->
|
|
|
+ case cowboy_http_req:parse_header('If-None-Match', Req) of
|
|
|
+ {undefined, Req2} ->
|
|
|
+ if_modified_since_exists(Req2, State);
|
|
|
+ {'*', Req2} ->
|
|
|
+ precondition_is_head_get(Req2, State);
|
|
|
+ {EtagsList, Req2} ->
|
|
|
+ if_none_match(Req2, State, EtagsList)
|
|
|
+ end.
|
|
|
+
|
|
|
+if_none_match(Req, State, EtagsList) ->
|
|
|
+ {Etag, Req2, State2} = generate_etag(Req, State),
|
|
|
+ case Etag of
|
|
|
+ no_call ->
|
|
|
+ precondition_failed(Req2, State2);
|
|
|
+ Etag ->
|
|
|
+ case lists:member(Etag, EtagsList) of
|
|
|
+ true -> precondition_is_head_get(Req2, State2);
|
|
|
+ false -> if_modified_since_exists(Req2, State2)
|
|
|
+ end
|
|
|
+ end.
|
|
|
+
|
|
|
+precondition_is_head_get(Req=#http_req{method=Method}, State)
|
|
|
+ when Method =:= 'HEAD'; Method =:= 'GET' ->
|
|
|
+ not_modified(Req, State);
|
|
|
+precondition_is_head_get(Req, State) ->
|
|
|
+ precondition_failed(Req, State).
|
|
|
+
|
|
|
+if_modified_since_exists(Req, State) ->
|
|
|
+ case cowboy_http_req:parse_header('If-Modified-Since', Req) of
|
|
|
+ {undefined, Req2} ->
|
|
|
+ method(Req2, State);
|
|
|
+ {{error, badarg}, Req2} ->
|
|
|
+ method(Req2, State);
|
|
|
+ {IfModifiedSince, Req2} ->
|
|
|
+ if_modified_since_now(Req2, State, IfModifiedSince)
|
|
|
+ end.
|
|
|
+
|
|
|
+if_modified_since_now(Req, State, IfModifiedSince) ->
|
|
|
+ case IfModifiedSince > erlang:universaltime() of
|
|
|
+ true -> method(Req, State);
|
|
|
+ false -> if_modified_since(Req, State, IfModifiedSince)
|
|
|
+ end.
|
|
|
+
|
|
|
+if_modified_since(Req, State, IfModifiedSince) ->
|
|
|
+ {LastModified, Req2, State2} = last_modified(Req, State),
|
|
|
+ case LastModified of
|
|
|
+ no_call ->
|
|
|
+ method(Req2, State2);
|
|
|
+ LastModified ->
|
|
|
+ case LastModified > IfModifiedSince of
|
|
|
+ true -> method(Req2, State2);
|
|
|
+ false -> not_modified(Req2, State2)
|
|
|
+ end
|
|
|
+ end.
|
|
|
+
|
|
|
+not_modified(Req=#http_req{resp_headers=RespHeaders}, State) ->
|
|
|
+ RespHeaders2 = lists:keydelete(<<"Content-Type">>, 1, RespHeaders),
|
|
|
+ Req2 = Req#http_req{resp_headers=RespHeaders2},
|
|
|
+ {Req3, State2} = set_resp_etag(Req2, State),
|
|
|
+ {Req4, State3} = set_resp_expires(Req3, State2),
|
|
|
+ respond(Req4, State3, 304).
|
|
|
+
|
|
|
+precondition_failed(Req, State) ->
|
|
|
+ respond(Req, State, 412).
|
|
|
+
|
|
|
+is_put_to_missing_resource(Req=#http_req{method='PUT'}, State) ->
|
|
|
+ moved_permanently(Req, State, fun is_conflict/2);
|
|
|
+is_put_to_missing_resource(Req, State) ->
|
|
|
+ previously_existed(Req, State).
|
|
|
+
|
|
|
+moved_permanently(Req, State, OnFalse) ->
|
|
|
+ case call(Req, State, moved_permanently) of
|
|
|
+ {{true, Location}, Req2, HandlerState2} ->
|
|
|
+ {ok, Req3} = cowboy_http_req:set_resp_header(
|
|
|
+ <<"Location">>, Location, Req2),
|
|
|
+ respond(Req3, State#state{handler_state=HandlerState2}, 301);
|
|
|
+ {false, Req2, HandlerState2} ->
|
|
|
+ OnFalse(Req2, State#state{handler_state=HandlerState2});
|
|
|
+ no_call ->
|
|
|
+ OnFalse(Req, State)
|
|
|
+ end.
|
|
|
+
|
|
|
+previously_existed(Req, State) ->
|
|
|
+ expect(Req, State, previously_existed, false,
|
|
|
+ fun (R, S) -> is_post_to_missing_resource(R, S, 404) end,
|
|
|
+ fun (R, S) -> moved_permanently(R, S, fun moved_temporarily/2) end).
|
|
|
+
|
|
|
+moved_temporarily(Req, State) ->
|
|
|
+ case call(Req, State, moved_temporarily) of
|
|
|
+ {{true, Location}, Req2, HandlerState2} ->
|
|
|
+ {ok, Req3} = cowboy_http_req:set_resp_header(
|
|
|
+ <<"Location">>, Location, Req2),
|
|
|
+ respond(Req3, State#state{handler_state=HandlerState2}, 307);
|
|
|
+ {false, Req2, HandlerState2} ->
|
|
|
+ is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState2}, 410);
|
|
|
+ no_call ->
|
|
|
+ is_post_to_missing_resource(Req, State, 410)
|
|
|
+ end.
|
|
|
+
|
|
|
+is_post_to_missing_resource(Req=#http_req{method='POST'}, State, OnFalse) ->
|
|
|
+ allow_missing_post(Req, State, OnFalse);
|
|
|
+is_post_to_missing_resource(Req, State, OnFalse) ->
|
|
|
+ respond(Req, State, OnFalse).
|
|
|
+
|
|
|
+allow_missing_post(Req, State, OnFalse) ->
|
|
|
+ expect(Req, State, allow_missing_post, true, fun post_is_create/2, OnFalse).
|
|
|
+
|
|
|
+method(Req=#http_req{method='DELETE'}, State) ->
|
|
|
+ delete_resource(Req, State);
|
|
|
+method(Req=#http_req{method='POST'}, State) ->
|
|
|
+ post_is_create(Req, State);
|
|
|
+method(Req=#http_req{method='PUT'}, State) ->
|
|
|
+ is_conflict(Req, State);
|
|
|
+method(Req, State) ->
|
|
|
+ set_resp_body(Req, State).
|
|
|
+
|
|
|
+delete_resource(Req, State) ->
|
|
|
+ expect(Req, State, delete_resource, true, fun delete_completed/2, 500).
|
|
|
+
|
|
|
+delete_completed(Req, State) ->
|
|
|
+ expect(Req, State, delete_completed, true, fun has_resp_body/2, 202).
|
|
|
+
|
|
|
+post_is_create(Req, State) ->
|
|
|
+ expect(Req, State, post_is_create, false, fun process_post/2, fun create_path/2).
|
|
|
+
|
|
|
+create_path(Req, State) ->
|
|
|
+ case call(Req, State, create_path) of
|
|
|
+ {Location, Req2, HandlerState} ->
|
|
|
+ State2 = State#state{handler_state=HandlerState},
|
|
|
+ {ok, Req3} = cowboy_http_req:set_resp_header(
|
|
|
+ <<"Location">>, Location, Req2),
|
|
|
+ put_resource(Req3, State2, 303)
|
|
|
+ end.
|
|
|
+
|
|
|
+process_post(Req, State) ->
|
|
|
+ case call(Req, State, process_post) of
|
|
|
+ {ok, _Req2, HandlerState} ->
|
|
|
+ _ = _State2 = State#state{handler_state=HandlerState},
|
|
|
+ todo %% @todo ???
|
|
|
+ end.
|
|
|
+
|
|
|
+is_conflict(Req, State) ->
|
|
|
+ expect(Req, State, is_conflict, false, fun put_resource/2, 409).
|
|
|
+
|
|
|
+put_resource(Req, State) ->
|
|
|
+ put_resource(Req, State, fun is_new_resource/2).
|
|
|
+
|
|
|
+put_resource(Req, State, OnTrue) ->
|
|
|
+ case call(Req, State, content_types_accepted) of
|
|
|
+ no_call ->
|
|
|
+ respond(Req, State, 415);
|
|
|
+ {CTA, Req2, HandlerState2} ->
|
|
|
+ State2 = State#state{handler_state=HandlerState2},
|
|
|
+ {ContentType, Req3}
|
|
|
+ = cowboy_http_req:parse_header('Content-Type', Req2),
|
|
|
+ choose_content_type(Req3, State2, OnTrue, ContentType, CTA)
|
|
|
+ end.
|
|
|
+
|
|
|
+choose_content_type(Req, State, _OnTrue, _ContentType, []) ->
|
|
|
+ respond(Req, State, 415);
|
|
|
+choose_content_type(Req, State, OnTrue, ContentType,
|
|
|
+ [{Accepted, Fun}|_Tail]) when ContentType =:= Accepted ->
|
|
|
+ case call(Req, State, Fun) of
|
|
|
+ {ok, Req2, HandlerState} ->
|
|
|
+ State2 = State#state{handler_state=HandlerState},
|
|
|
+ next(Req2, State2, OnTrue)
|
|
|
+ end;
|
|
|
+choose_content_type(Req, State, OnTrue, ContentType, [_Any|Tail]) ->
|
|
|
+ choose_content_type(Req, State, OnTrue, ContentType, Tail).
|
|
|
+
|
|
|
+is_new_resource(Req, State) ->
|
|
|
+ case cowboy_http_req:has_resp_header(<<"Location">>, Req) of
|
|
|
+ true -> respond(Req, State, 201);
|
|
|
+ false -> has_resp_body(Req, State)
|
|
|
+ end.
|
|
|
+
|
|
|
+has_resp_body(Req, State) ->
|
|
|
+ case cowboy_http_req:has_resp_body(Req) of
|
|
|
+ true -> multiple_choices(Req, State);
|
|
|
+ false -> respond(Req, State, 204)
|
|
|
+ end.
|
|
|
+
|
|
|
+set_resp_body(Req=#http_req{method=Method},
|
|
|
+ State=#state{content_type_a={_Type, Fun}})
|
|
|
+ when Method =:= 'GET'; Method =:= 'HEAD' ->
|
|
|
+ {Req2, State2} = set_resp_etag(Req, State),
|
|
|
+ {LastModified, Req3, State3} = last_modified(Req2, State2),
|
|
|
+ case LastModified of
|
|
|
+ LastModified when is_atom(LastModified) ->
|
|
|
+ Req4 = Req3;
|
|
|
+ LastModified ->
|
|
|
+ LastModifiedStr = httpd_util:rfc1123_date(LastModified),
|
|
|
+ {ok, Req4} = cowboy_http_req:set_resp_header(
|
|
|
+ <<"Last-Modified">>, LastModifiedStr, Req3)
|
|
|
+ end,
|
|
|
+ {Req5, State4} = set_resp_expires(Req4, State3),
|
|
|
+ case call(Req5, State4, Fun) of
|
|
|
+ {Body, Req6, HandlerState} ->
|
|
|
+ State5 = State4#state{handler_state=HandlerState},
|
|
|
+ {ok, Req7} = cowboy_http_req:set_resp_body(Body, Req6),
|
|
|
+ multiple_choices(Req7, State5)
|
|
|
+ end;
|
|
|
+set_resp_body(Req, State) ->
|
|
|
+ multiple_choices(Req, State).
|
|
|
+
|
|
|
+multiple_choices(Req, State) ->
|
|
|
+ expect(Req, State, multiple_choices, false, 200, 300).
|
|
|
+
|
|
|
+%% Response utility functions.
|
|
|
+
|
|
|
+set_resp_etag(Req, State) ->
|
|
|
+ {Etag, Req2, State2} = generate_etag(Req, State),
|
|
|
+ case Etag of
|
|
|
+ undefined ->
|
|
|
+ {Req2, State2};
|
|
|
+ Etag ->
|
|
|
+ {ok, Req3} = cowboy_http_req:set_resp_header(
|
|
|
+ <<"Etag">>, Etag, Req2),
|
|
|
+ {Req3, State2}
|
|
|
+ end.
|
|
|
+
|
|
|
+set_resp_expires(Req, State) ->
|
|
|
+ {Expires, Req2, State2} = expires(Req, State),
|
|
|
+ case Expires of
|
|
|
+ Expires when is_atom(Expires) ->
|
|
|
+ {Req2, State2};
|
|
|
+ Expires ->
|
|
|
+ ExpiresStr = httpd_util:rfc1123_date(Expires),
|
|
|
+ {ok, Req3} = cowboy_http_req:set_resp_header(
|
|
|
+ <<"Expires">>, ExpiresStr, Req2),
|
|
|
+ {Req3, State2}
|
|
|
+ end.
|
|
|
+
|
|
|
+%% Info retrieval. No logic.
|
|
|
+
|
|
|
+generate_etag(Req, State=#state{etag=no_call}) ->
|
|
|
+ {undefined, Req, State};
|
|
|
+generate_etag(Req, State=#state{etag=undefined}) ->
|
|
|
+ case call(Req, State, generate_etag) of
|
|
|
+ no_call ->
|
|
|
+ {undefined, Req, State#state{etag=no_call}};
|
|
|
+ {Etag, Req2, HandlerState2} ->
|
|
|
+ {Etag, Req2, State#state{handler_state=HandlerState2, etag=Etag}}
|
|
|
+ end;
|
|
|
+generate_etag(Req, State=#state{etag=Etag}) ->
|
|
|
+ {Etag, Req, State}.
|
|
|
+
|
|
|
+last_modified(Req, State=#state{last_modified=no_call}) ->
|
|
|
+ {undefined, Req, State};
|
|
|
+last_modified(Req, State=#state{last_modified=undefined}) ->
|
|
|
+ case call(Req, State, last_modified) of
|
|
|
+ no_call ->
|
|
|
+ {undefined, Req, State#state{last_modified=no_call}};
|
|
|
+ {LastModified, Req2, HandlerState2} ->
|
|
|
+ {LastModified, Req2, State#state{handler_state=HandlerState2,
|
|
|
+ last_modified=LastModified}}
|
|
|
+ end;
|
|
|
+last_modified(Req, State=#state{last_modified=LastModified}) ->
|
|
|
+ {LastModified, Req, State}.
|
|
|
+
|
|
|
+expires(Req, State=#state{expires=no_call}) ->
|
|
|
+ {undefined, Req, State};
|
|
|
+expires(Req, State=#state{expires=undefined}) ->
|
|
|
+ case call(Req, State, expires) of
|
|
|
+ no_call ->
|
|
|
+ {undefined, Req, State#state{expires=no_call}};
|
|
|
+ {Expires, Req2, HandlerState2} ->
|
|
|
+ {Expires, Req2, State#state{handler_state=HandlerState2,
|
|
|
+ expires=Expires}}
|
|
|
+ end;
|
|
|
+expires(Req, State=#state{expires=Expires}) ->
|
|
|
+ {Expires, Req, State}.
|
|
|
+
|
|
|
+%% REST primitives.
|
|
|
+
|
|
|
+expect(Req, State, Callback, Expected, OnTrue, OnFalse) ->
|
|
|
+ case call(Req, State, Callback) of
|
|
|
+ no_call ->
|
|
|
+ next(Req, State, OnTrue);
|
|
|
+ {Expected, Req2, HandlerState2} ->
|
|
|
+ next(Req2, State#state{handler_state=HandlerState2}, OnTrue);
|
|
|
+ {_Unexpected, Req2, HandlerState2} ->
|
|
|
+ next(Req2, State#state{handler_state=HandlerState2}, OnFalse)
|
|
|
+ end.
|
|
|
+
|
|
|
+call(Req, #state{handler=Handler, handler_state=HandlerState}, Fun) ->
|
|
|
+ case erlang:function_exported(Handler, Fun, 2) of
|
|
|
+ true -> Handler:Fun(Req, HandlerState);
|
|
|
+ false -> no_call
|
|
|
+ end.
|
|
|
+
|
|
|
+next(Req, State, Next) when is_function(Next) ->
|
|
|
+ Next(Req, State);
|
|
|
+next(Req, State, StatusCode) when is_integer(StatusCode) ->
|
|
|
+ respond(Req, State, StatusCode).
|
|
|
+
|
|
|
+%% @todo Allow some sort of callback for custom error pages.
|
|
|
+respond(Req, State, StatusCode) ->
|
|
|
+ {ok, Req2} = cowboy_http_req:reply(StatusCode, Req),
|
|
|
+ terminate(Req2, State).
|
|
|
+
|
|
|
+terminate(Req, #state{handler=Handler, handler_state=HandlerState}) ->
|
|
|
+ case erlang:function_exported(Handler, rest_terminate, 2) of
|
|
|
+ true -> ok = Handler:rest_terminate(Req, HandlerState);
|
|
|
+ false -> ok
|
|
|
+ end.
|