cowboy_http_rest.erl 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
  1. %% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
  2. %%
  3. %% Permission to use, copy, modify, and/or distribute this software for any
  4. %% purpose with or without fee is hereby granted, provided that the above
  5. %% copyright notice and this permission notice appear in all copies.
  6. %%
  7. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  8. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  9. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  10. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  11. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  12. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  13. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  14. %% @doc Experimental REST protocol implementation.
  15. %%
  16. %% Based on the Webmachine Diagram from Alan Dean and Justin Sheehy, which
  17. %% can be found in the Webmachine source tree, and on the Webmachine
  18. %% documentation available at http://wiki.basho.com/Webmachine.html
  19. %% at the time of writing.
  20. -module(cowboy_http_rest).
  21. -export([upgrade/4]).
  22. -record(state, {
  23. %% Handler.
  24. handler :: atom(),
  25. handler_state :: any(),
  26. %% Media type.
  27. content_types_p = [] ::
  28. [{{binary(), binary(), [{binary(), binary()}]}, atom()}],
  29. content_type_a :: undefined
  30. | {{binary(), binary(), [{binary(), binary()}]}, atom()},
  31. %% Language.
  32. languages_p = [] :: [binary()],
  33. language_a :: undefined | binary(),
  34. %% Charset.
  35. charsets_p = [] :: [binary()],
  36. charset_a :: undefined | binary(),
  37. %% Cached resource calls.
  38. etag :: undefined | no_call | binary(),
  39. last_modified :: undefined | no_call | cowboy_clock:datetime(),
  40. expires :: undefined | no_call | cowboy_clock:datetime()
  41. }).
  42. -include("include/http.hrl").
  43. %% @doc Upgrade a HTTP request to the REST protocol.
  44. %%
  45. %% You do not need to call this function manually. To upgrade to the REST
  46. %% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em>
  47. %% in your <em>cowboy_http_handler:init/3</em> handler function.
  48. -spec upgrade(pid(), module(), any(), #http_req{}) -> {ok, #http_req{}}.
  49. upgrade(_ListenerPid, Handler, Opts, Req) ->
  50. try
  51. case erlang:function_exported(Handler, rest_init, 2) of
  52. true ->
  53. case Handler:rest_init(Req, Opts) of
  54. {ok, Req2, HandlerState} ->
  55. service_available(Req2, #state{handler=Handler,
  56. handler_state=HandlerState})
  57. end;
  58. false ->
  59. service_available(Req, #state{handler=Handler})
  60. end
  61. catch Class:Reason ->
  62. error_logger:error_msg(
  63. "** Handler ~p terminating in rest_init/3~n"
  64. " for the reason ~p:~p~n** Options were ~p~n"
  65. "** Request was ~p~n** Stacktrace: ~p~n~n",
  66. [Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()]),
  67. {ok, _Req2} = cowboy_http_req:reply(500, Req),
  68. ok
  69. end.
  70. service_available(Req, State) ->
  71. expect(Req, State, service_available, true, fun known_methods/2, 503).
  72. %% known_methods/2 should return a list of atoms or binary methods.
  73. known_methods(Req=#http_req{method=Method}, State) ->
  74. case call(Req, State, known_methods) of
  75. no_call when Method =:= 'HEAD'; Method =:= 'GET'; Method =:= 'POST';
  76. Method =:= 'PUT'; Method =:= 'DELETE'; Method =:= 'TRACE';
  77. Method =:= 'CONNECT'; Method =:= 'OPTIONS' ->
  78. next(Req, State, fun uri_too_long/2);
  79. no_call ->
  80. next(Req, State, 501);
  81. {List, Req2, HandlerState2} ->
  82. State2 = State#state{handler_state=HandlerState2},
  83. case lists:member(Method, List) of
  84. true -> next(Req2, State2, fun uri_too_long/2);
  85. false -> next(Req2, State2, 501)
  86. end
  87. end.
  88. uri_too_long(Req, State) ->
  89. expect(Req, State, uri_too_long, false, fun allowed_methods/2, 414).
  90. %% allowed_methods/2 should return a list of atoms or binary methods.
  91. allowed_methods(Req=#http_req{method=Method}, State) ->
  92. case call(Req, State, allowed_methods) of
  93. no_call when Method =:= 'HEAD'; Method =:= 'GET' ->
  94. next(Req, State, fun malformed_request/2);
  95. no_call ->
  96. method_not_allowed(Req, State, ['GET', 'HEAD']);
  97. {List, Req2, HandlerState2} ->
  98. State2 = State#state{handler_state=HandlerState2},
  99. case lists:member(Method, List) of
  100. true -> next(Req2, State2, fun malformed_request/2);
  101. false -> method_not_allowed(Req2, State2, List)
  102. end
  103. end.
  104. method_not_allowed(Req, State, Methods) ->
  105. {ok, Req2} = cowboy_http_req:set_resp_header(
  106. <<"Allow">>, method_not_allowed_build(Methods, []), Req),
  107. respond(Req2, State, 405).
  108. method_not_allowed_build([], []) ->
  109. <<>>;
  110. method_not_allowed_build([], [_Ignore|Acc]) ->
  111. lists:reverse(Acc);
  112. method_not_allowed_build([Method|Tail], Acc) when is_atom(Method) ->
  113. Method2 = list_to_binary(atom_to_list(Method)),
  114. method_not_allowed_build(Tail, [<<", ">>, Method2|Acc]);
  115. method_not_allowed_build([Method|Tail], Acc) ->
  116. method_not_allowed_build(Tail, [<<", ">>, Method|Acc]).
  117. malformed_request(Req, State) ->
  118. expect(Req, State, malformed_request, false, fun is_authorized/2, 400).
  119. %% is_authorized/2 should return true or {false, WwwAuthenticateHeader}.
  120. is_authorized(Req, State) ->
  121. case call(Req, State, is_authorized) of
  122. no_call ->
  123. forbidden(Req, State);
  124. {true, Req2, HandlerState2} ->
  125. forbidden(Req2, State#state{handler_state=HandlerState2});
  126. {{false, AuthHead}, Req2, HandlerState2} ->
  127. {ok, Req3} = cowboy_http_req:set_resp_header(
  128. <<"Www-Authenticate">>, AuthHead, Req2),
  129. respond(Req3, State#state{handler_state=HandlerState2}, 401)
  130. end.
  131. forbidden(Req, State) ->
  132. expect(Req, State, forbidden, false, fun valid_content_headers/2, 403).
  133. valid_content_headers(Req, State) ->
  134. expect(Req, State, valid_content_headers, true,
  135. fun known_content_type/2, 501).
  136. known_content_type(Req, State) ->
  137. expect(Req, State, known_content_type, true,
  138. fun valid_entity_length/2, 413).
  139. valid_entity_length(Req, State) ->
  140. expect(Req, State, valid_entity_length, true, fun options/2, 413).
  141. %% If you need to add additional headers to the response at this point,
  142. %% you should do it directly in the options/2 call using set_resp_headers.
  143. options(Req=#http_req{method='OPTIONS'}, State) ->
  144. {ok, Req2, HandlerState2} = call(Req, State, options),
  145. respond(Req2, State#state{handler_state=HandlerState2}, 200);
  146. options(Req, State) ->
  147. content_types_provided(Req, State).
  148. %% content_types_provided/2 should return a list of content types and their
  149. %% associated callback function as a tuple: {{Type, SubType, Params}, Fun}.
  150. %% Type and SubType are the media type as binary. Params is a list of
  151. %% Key/Value tuple, with Key and Value a binary. Fun is the name of the
  152. %% callback that will be used to return the content of the response. It is
  153. %% given as an atom.
  154. %%
  155. %% An example of such return value would be:
  156. %% {{<<"text">>, <<"html">>, []}, to_html}
  157. content_types_provided(Req, State) ->
  158. case call(Req, State, content_types_provided) of
  159. no_call ->
  160. not_acceptable(Req, State);
  161. {[], Req2, HandlerState2} ->
  162. not_acceptable(Req2, State#state{handler_state=HandlerState2});
  163. {CTP, Req2, HandlerState2} ->
  164. State2 = State#state{handler_state=HandlerState2, content_types_p=CTP},
  165. {Accept, Req3} = cowboy_http_req:parse_header('Accept', Req2),
  166. case Accept of
  167. undefined ->
  168. languages_provided(Req3,
  169. State2#state{content_type_a=hd(CTP)});
  170. Accept ->
  171. Accept2 = prioritize_accept(Accept),
  172. choose_media_type(Req3, State2, Accept2)
  173. end
  174. end.
  175. prioritize_accept(Accept) ->
  176. lists:sort(
  177. fun ({MediaTypeA, Quality, _AcceptParamsA},
  178. {MediaTypeB, Quality, _AcceptParamsB}) ->
  179. %% Same quality, check precedence in more details.
  180. prioritize_mediatype(MediaTypeA, MediaTypeB);
  181. ({_MediaTypeA, QualityA, _AcceptParamsA},
  182. {_MediaTypeB, QualityB, _AcceptParamsB}) ->
  183. %% Just compare the quality.
  184. QualityA > QualityB
  185. end, Accept).
  186. %% Media ranges can be overridden by more specific media ranges or
  187. %% specific media types. If more than one media range applies to a given
  188. %% type, the most specific reference has precedence.
  189. %%
  190. %% We always choose B over A when we can't decide between the two.
  191. prioritize_mediatype({TypeA, SubTypeA, ParamsA}, {TypeB, SubTypeB, ParamsB}) ->
  192. case TypeB of
  193. TypeA ->
  194. case SubTypeB of
  195. SubTypeA -> length(ParamsA) > length(ParamsB);
  196. <<"*">> -> true;
  197. _Any -> false
  198. end;
  199. <<"*">> -> true;
  200. _Any -> false
  201. end.
  202. %% Ignoring the rare AcceptParams. Not sure what should be done about them.
  203. choose_media_type(Req, State, []) ->
  204. not_acceptable(Req, State);
  205. choose_media_type(Req, State=#state{content_types_p=CTP},
  206. [MediaType|Tail]) ->
  207. match_media_type(Req, State, Tail, CTP, MediaType).
  208. match_media_type(Req, State, Accept, [], _MediaType) ->
  209. choose_media_type(Req, State, Accept);
  210. match_media_type(Req, State, Accept,
  211. [Provided = {{Type, SubType_P, Params_P}, _Fun}|Tail],
  212. MediaType = {{Type, SubType_A, Params_A}, _Quality, _AcceptParams})
  213. when SubType_P =:= SubType_A; SubType_A =:= <<"*">> ->
  214. case lists:sort(Params_P) =:= lists:sort(Params_A) of
  215. true ->
  216. languages_provided(Req, State#state{content_type_a=Provided});
  217. false ->
  218. match_media_type(Req, State, Accept, Tail, MediaType)
  219. end;
  220. match_media_type(Req, State, Accept, [_Any|Tail], MediaType) ->
  221. match_media_type(Req, State, Accept, Tail, MediaType).
  222. %% languages_provided should return a list of binary values indicating
  223. %% which languages are accepted by the resource.
  224. %%
  225. %% @todo I suppose we should also ask the resource if it wants to
  226. %% set a language itself or if it wants it to be automatically chosen.
  227. languages_provided(Req, State) ->
  228. case call(Req, State, languages_provided) of
  229. no_call ->
  230. charsets_provided(Req, State);
  231. {[], Req2, HandlerState2} ->
  232. not_acceptable(Req2, State#state{handler_state=HandlerState2});
  233. {LP, Req2, HandlerState2} ->
  234. State2 = State#state{handler_state=HandlerState2, languages_p=LP},
  235. {AcceptLanguage, Req3} =
  236. cowboy_http_req:parse_header('Accept-Language', Req2),
  237. case AcceptLanguage of
  238. undefined ->
  239. set_language(Req3, State2#state{language_a=hd(LP)});
  240. AcceptLanguage ->
  241. AcceptLanguage2 = prioritize_languages(AcceptLanguage),
  242. choose_language(Req3, State2, AcceptLanguage2)
  243. end
  244. end.
  245. %% A language-range matches a language-tag if it exactly equals the tag,
  246. %% or if it exactly equals a prefix of the tag such that the first tag
  247. %% character following the prefix is "-". The special range "*", if
  248. %% present in the Accept-Language field, matches every tag not matched
  249. %% by any other range present in the Accept-Language field.
  250. %%
  251. %% @todo The last sentence probably means we should always put '*'
  252. %% at the end of the list.
  253. prioritize_languages(AcceptLanguages) ->
  254. lists:sort(
  255. fun ({_TagA, QualityA}, {_TagB, QualityB}) ->
  256. QualityA > QualityB
  257. end, AcceptLanguages).
  258. choose_language(Req, State, []) ->
  259. not_acceptable(Req, State);
  260. choose_language(Req, State=#state{languages_p=LP}, [Language|Tail]) ->
  261. match_language(Req, State, Tail, LP, Language).
  262. match_language(Req, State, Accept, [], _Language) ->
  263. choose_language(Req, State, Accept);
  264. match_language(Req, State, _Accept, [Provided|_Tail], {'*', _Quality}) ->
  265. set_language(Req, State#state{language_a=Provided});
  266. match_language(Req, State, _Accept, [Provided|_Tail], {Provided, _Quality}) ->
  267. set_language(Req, State#state{language_a=Provided});
  268. match_language(Req, State, Accept, [Provided|Tail],
  269. Language = {Tag, _Quality}) ->
  270. Length = byte_size(Tag),
  271. case Provided of
  272. << Tag:Length/binary, $-, _Any/bits >> ->
  273. set_language(Req, State#state{language_a=Provided});
  274. _Any ->
  275. match_language(Req, State, Accept, Tail, Language)
  276. end.
  277. set_language(Req, State=#state{language_a=Language}) ->
  278. {ok, Req2} = cowboy_http_req:set_resp_header(
  279. <<"Content-Language">>, Language, Req),
  280. charsets_provided(Req2, State).
  281. %% charsets_provided should return a list of binary values indicating
  282. %% which charsets are accepted by the resource.
  283. charsets_provided(Req, State) ->
  284. case call(Req, State, charsets_provided) of
  285. no_call ->
  286. set_content_type(Req, State);
  287. {[], Req2, HandlerState2} ->
  288. not_acceptable(Req2, State#state{handler_state=HandlerState2});
  289. {CP, Req2, HandlerState2} ->
  290. State2 = State#state{handler_state=HandlerState2, charsets_p=CP},
  291. {AcceptCharset, Req3} =
  292. cowboy_http_req:parse_header('Accept-Charset', Req2),
  293. case AcceptCharset of
  294. undefined ->
  295. set_content_type(Req3, State2#state{charset_a=hd(CP)});
  296. AcceptCharset ->
  297. AcceptCharset2 = prioritize_charsets(AcceptCharset),
  298. choose_charset(Req3, State2, AcceptCharset2)
  299. end
  300. end.
  301. %% The special value "*", if present in the Accept-Charset field,
  302. %% matches every character set (including ISO-8859-1) which is not
  303. %% mentioned elsewhere in the Accept-Charset field. If no "*" is present
  304. %% in an Accept-Charset field, then all character sets not explicitly
  305. %% mentioned get a quality value of 0, except for ISO-8859-1, which gets
  306. %% a quality value of 1 if not explicitly mentioned.
  307. prioritize_charsets(AcceptCharsets) ->
  308. AcceptCharsets2 = lists:sort(
  309. fun ({_CharsetA, QualityA}, {_CharsetB, QualityB}) ->
  310. QualityA > QualityB
  311. end, AcceptCharsets),
  312. case lists:keymember(<<"*">>, 1, AcceptCharsets2) of
  313. true -> AcceptCharsets2;
  314. false -> [{<<"iso-8859-1">>, 1000}|AcceptCharsets2]
  315. end.
  316. choose_charset(Req, State, []) ->
  317. not_acceptable(Req, State);
  318. choose_charset(Req, State=#state{charsets_p=CP}, [Charset|Tail]) ->
  319. match_charset(Req, State, Tail, CP, Charset).
  320. match_charset(Req, State, Accept, [], _Charset) ->
  321. choose_charset(Req, State, Accept);
  322. match_charset(Req, State, _Accept, [Provided|_Tail],
  323. {Provided, _Quality}) ->
  324. set_content_type(Req, State#state{charset_a=Provided});
  325. match_charset(Req, State, Accept, [_Provided|Tail], Charset) ->
  326. match_charset(Req, State, Accept, Tail, Charset).
  327. set_content_type(Req, State=#state{
  328. content_type_a={{Type, SubType, Params}, _Fun},
  329. charset_a=Charset}) ->
  330. ParamsBin = set_content_type_build_params(Params, []),
  331. ContentType = [Type, <<"/">>, SubType, ParamsBin],
  332. ContentType2 = case Charset of
  333. undefined -> ContentType;
  334. Charset -> [ContentType, <<"; charset=">>, Charset]
  335. end,
  336. {ok, Req2} = cowboy_http_req:set_resp_header(
  337. <<"Content-Type">>, ContentType2, Req),
  338. encodings_provided(Req2, State).
  339. set_content_type_build_params([], []) ->
  340. <<>>;
  341. set_content_type_build_params([], Acc) ->
  342. lists:reverse(Acc);
  343. set_content_type_build_params([{Attr, Value}|Tail], Acc) ->
  344. set_content_type_build_params(Tail, [[Attr, <<"=">>, Value], <<";">>|Acc]).
  345. %% @todo Match for identity as we provide nothing else for now.
  346. %% @todo Don't forget to set the Content-Encoding header when we reply a body
  347. %% and the found encoding is something other than identity.
  348. encodings_provided(Req, State) ->
  349. variances(Req, State).
  350. not_acceptable(Req, State) ->
  351. respond(Req, State, 406).
  352. %% variances/2 should return a list of headers that will be added
  353. %% to the Vary response header. The Accept, Accept-Language,
  354. %% Accept-Charset and Accept-Encoding headers do not need to be
  355. %% specified.
  356. %%
  357. %% @todo Do Accept-Encoding too when we handle it.
  358. %% @todo Does the order matter?
  359. variances(Req, State=#state{content_types_p=CTP,
  360. languages_p=LP, charsets_p=CP}) ->
  361. Variances = case length(CTP) of
  362. 0 -> [];
  363. 1 -> [];
  364. _NCT -> [<<"Accept">>]
  365. end,
  366. Variances2 = case length(LP) of
  367. 0 -> Variances;
  368. 1 -> Variances;
  369. _NL -> [<<"Accept-Language">>|Variances]
  370. end,
  371. Variances3 = case length(CP) of
  372. 0 -> Variances2;
  373. 1 -> Variances2;
  374. _NC -> [<<"Accept-Charset">>|Variances2]
  375. end,
  376. {Variances4, Req3, State2} = case call(Req, State, variances) of
  377. no_call ->
  378. {Variances3, Req, State};
  379. {HandlerVariances, Req2, HandlerState} ->
  380. {Variances3 ++ HandlerVariances, Req2,
  381. State#state{handler_state=HandlerState}}
  382. end,
  383. case lists:flatten([[<<", ">>, V] || V <- Variances4]) of
  384. [] ->
  385. resource_exists(Req3, State2);
  386. [<<", ">>, Variances5] ->
  387. {ok, Req4} = cowboy_http_req:set_resp_header(
  388. <<"Variances">>, Variances5, Req3),
  389. resource_exists(Req4, State2)
  390. end.
  391. resource_exists(Req, State) ->
  392. expect(Req, State, resource_exists, true,
  393. fun if_match_exists/2, fun if_match_musnt_exist/2).
  394. if_match_exists(Req, State) ->
  395. case cowboy_http_req:parse_header('If-Match', Req) of
  396. {undefined, Req2} ->
  397. if_unmodified_since_exists(Req2, State);
  398. {'*', Req2} ->
  399. if_unmodified_since_exists(Req2, State);
  400. {ETagsList, Req2} ->
  401. if_match(Req2, State, ETagsList)
  402. end.
  403. if_match(Req, State, EtagsList) ->
  404. {Etag, Req2, State2} = generate_etag(Req, State),
  405. case Etag of
  406. no_call ->
  407. precondition_failed(Req2, State2);
  408. Etag ->
  409. case lists:member(Etag, EtagsList) of
  410. true -> if_unmodified_since_exists(Req2, State2);
  411. false -> precondition_failed(Req2, State2)
  412. end
  413. end.
  414. if_match_musnt_exist(Req, State) ->
  415. case cowboy_http_req:header('If-Match', Req) of
  416. {undefined, Req2} -> is_put_to_missing_resource(Req2, State);
  417. {_Any, Req2} -> precondition_failed(Req2, State)
  418. end.
  419. if_unmodified_since_exists(Req, State) ->
  420. case cowboy_http_req:parse_header('If-Unmodified-Since', Req) of
  421. {undefined, Req2} ->
  422. if_none_match_exists(Req2, State);
  423. {{error, badarg}, Req2} ->
  424. if_none_match_exists(Req2, State);
  425. {IfUnmodifiedSince, Req2} ->
  426. if_unmodified_since(Req2, State, IfUnmodifiedSince)
  427. end.
  428. %% If LastModified is the atom 'no_call', we continue.
  429. if_unmodified_since(Req, State, IfUnmodifiedSince) ->
  430. {LastModified, Req2, State2} = last_modified(Req, State),
  431. case LastModified > IfUnmodifiedSince of
  432. true -> precondition_failed(Req2, State2);
  433. false -> if_none_match_exists(Req2, State2)
  434. end.
  435. if_none_match_exists(Req, State) ->
  436. case cowboy_http_req:parse_header('If-None-Match', Req) of
  437. {undefined, Req2} ->
  438. if_modified_since_exists(Req2, State);
  439. {'*', Req2} ->
  440. precondition_is_head_get(Req2, State);
  441. {EtagsList, Req2} ->
  442. if_none_match(Req2, State, EtagsList)
  443. end.
  444. if_none_match(Req, State, EtagsList) ->
  445. {Etag, Req2, State2} = generate_etag(Req, State),
  446. case Etag of
  447. no_call ->
  448. precondition_failed(Req2, State2);
  449. Etag ->
  450. case lists:member(Etag, EtagsList) of
  451. true -> precondition_is_head_get(Req2, State2);
  452. false -> if_modified_since_exists(Req2, State2)
  453. end
  454. end.
  455. precondition_is_head_get(Req=#http_req{method=Method}, State)
  456. when Method =:= 'HEAD'; Method =:= 'GET' ->
  457. not_modified(Req, State);
  458. precondition_is_head_get(Req, State) ->
  459. precondition_failed(Req, State).
  460. if_modified_since_exists(Req, State) ->
  461. case cowboy_http_req:parse_header('If-Modified-Since', Req) of
  462. {undefined, Req2} ->
  463. method(Req2, State);
  464. {{error, badarg}, Req2} ->
  465. method(Req2, State);
  466. {IfModifiedSince, Req2} ->
  467. if_modified_since_now(Req2, State, IfModifiedSince)
  468. end.
  469. if_modified_since_now(Req, State, IfModifiedSince) ->
  470. case IfModifiedSince > erlang:universaltime() of
  471. true -> method(Req, State);
  472. false -> if_modified_since(Req, State, IfModifiedSince)
  473. end.
  474. if_modified_since(Req, State, IfModifiedSince) ->
  475. {LastModified, Req2, State2} = last_modified(Req, State),
  476. case LastModified of
  477. no_call ->
  478. method(Req2, State2);
  479. LastModified ->
  480. case LastModified > IfModifiedSince of
  481. true -> method(Req2, State2);
  482. false -> not_modified(Req2, State2)
  483. end
  484. end.
  485. not_modified(Req=#http_req{resp_headers=RespHeaders}, State) ->
  486. RespHeaders2 = lists:keydelete(<<"Content-Type">>, 1, RespHeaders),
  487. Req2 = Req#http_req{resp_headers=RespHeaders2},
  488. {Req3, State2} = set_resp_etag(Req2, State),
  489. {Req4, State3} = set_resp_expires(Req3, State2),
  490. respond(Req4, State3, 304).
  491. precondition_failed(Req, State) ->
  492. respond(Req, State, 412).
  493. is_put_to_missing_resource(Req=#http_req{method='PUT'}, State) ->
  494. moved_permanently(Req, State, fun is_conflict/2);
  495. is_put_to_missing_resource(Req, State) ->
  496. previously_existed(Req, State).
  497. %% moved_permanently/2 should return either false or {true, Location}
  498. %% with Location the full new URI of the resource.
  499. moved_permanently(Req, State, OnFalse) ->
  500. case call(Req, State, moved_permanently) of
  501. {{true, Location}, Req2, HandlerState2} ->
  502. {ok, Req3} = cowboy_http_req:set_resp_header(
  503. <<"Location">>, Location, Req2),
  504. respond(Req3, State#state{handler_state=HandlerState2}, 301);
  505. {false, Req2, HandlerState2} ->
  506. OnFalse(Req2, State#state{handler_state=HandlerState2});
  507. no_call ->
  508. OnFalse(Req, State)
  509. end.
  510. previously_existed(Req, State) ->
  511. expect(Req, State, previously_existed, false,
  512. fun (R, S) -> is_post_to_missing_resource(R, S, 404) end,
  513. fun (R, S) -> moved_permanently(R, S, fun moved_temporarily/2) end).
  514. %% moved_temporarily/2 should return either false or {true, Location}
  515. %% with Location the full new URI of the resource.
  516. moved_temporarily(Req, State) ->
  517. case call(Req, State, moved_temporarily) of
  518. {{true, Location}, Req2, HandlerState2} ->
  519. {ok, Req3} = cowboy_http_req:set_resp_header(
  520. <<"Location">>, Location, Req2),
  521. respond(Req3, State#state{handler_state=HandlerState2}, 307);
  522. {false, Req2, HandlerState2} ->
  523. is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState2}, 410);
  524. no_call ->
  525. is_post_to_missing_resource(Req, State, 410)
  526. end.
  527. is_post_to_missing_resource(Req=#http_req{method='POST'}, State, OnFalse) ->
  528. allow_missing_post(Req, State, OnFalse);
  529. is_post_to_missing_resource(Req, State, OnFalse) ->
  530. respond(Req, State, OnFalse).
  531. allow_missing_post(Req, State, OnFalse) ->
  532. expect(Req, State, allow_missing_post, true, fun post_is_create/2, OnFalse).
  533. method(Req=#http_req{method='DELETE'}, State) ->
  534. delete_resource(Req, State);
  535. method(Req=#http_req{method='POST'}, State) ->
  536. post_is_create(Req, State);
  537. method(Req=#http_req{method='PUT'}, State) ->
  538. is_conflict(Req, State);
  539. method(Req, State) ->
  540. set_resp_body(Req, State).
  541. %% delete_resource/2 should start deleting the resource and return.
  542. delete_resource(Req, State) ->
  543. expect(Req, State, delete_resource, true, fun delete_completed/2, 500).
  544. %% delete_completed/2 indicates whether the resource has been deleted yet.
  545. delete_completed(Req, State) ->
  546. expect(Req, State, delete_completed, true, fun has_resp_body/2, 202).
  547. %% post_is_create/2 indicates whether the POST method can create new resources.
  548. post_is_create(Req, State) ->
  549. expect(Req, State, post_is_create, false, fun process_post/2, fun create_path/2).
  550. %% When the POST method can create new resources, create_path/2 will be called
  551. %% and is expected to return the full URI of the new resource.
  552. create_path(Req, State) ->
  553. case call(Req, State, create_path) of
  554. {Location, Req2, HandlerState} ->
  555. State2 = State#state{handler_state=HandlerState},
  556. {ok, Req3} = cowboy_http_req:set_resp_header(
  557. <<"Location">>, Location, Req2),
  558. put_resource(Req3, State2, 303)
  559. end.
  560. %% @todo process_post/2 isn't fully implemented yet.
  561. process_post(Req, State) ->
  562. case call(Req, State, process_post) of
  563. {ok, _Req2, HandlerState} ->
  564. _ = _State2 = State#state{handler_state=HandlerState},
  565. todo %% @todo ???
  566. end.
  567. is_conflict(Req, State) ->
  568. expect(Req, State, is_conflict, false, fun put_resource/2, 409).
  569. put_resource(Req, State) ->
  570. put_resource(Req, State, fun is_new_resource/2).
  571. %% content_types_accepted should return a list of media types and their
  572. %% associated callback functions in the same format as content_types_provided.
  573. %%
  574. %% The callback will then be called and is expected to process the content
  575. %% pushed to the resource in the request body.
  576. put_resource(Req, State, OnTrue) ->
  577. case call(Req, State, content_types_accepted) of
  578. no_call ->
  579. respond(Req, State, 415);
  580. {CTA, Req2, HandlerState2} ->
  581. State2 = State#state{handler_state=HandlerState2},
  582. {ContentType, Req3}
  583. = cowboy_http_req:parse_header('Content-Type', Req2),
  584. choose_content_type(Req3, State2, OnTrue, ContentType, CTA)
  585. end.
  586. choose_content_type(Req, State, _OnTrue, _ContentType, []) ->
  587. respond(Req, State, 415);
  588. choose_content_type(Req, State, OnTrue, ContentType,
  589. [{Accepted, Fun}|_Tail]) when ContentType =:= Accepted ->
  590. case call(Req, State, Fun) of
  591. {ok, Req2, HandlerState} ->
  592. State2 = State#state{handler_state=HandlerState},
  593. next(Req2, State2, OnTrue)
  594. end;
  595. choose_content_type(Req, State, OnTrue, ContentType, [_Any|Tail]) ->
  596. choose_content_type(Req, State, OnTrue, ContentType, Tail).
  597. %% Whether we created a new resource, either through PUT or POST.
  598. %% This is easily testable because we would have set the Location
  599. %% header by this point if we did so.
  600. is_new_resource(Req, State) ->
  601. case cowboy_http_req:has_resp_header(<<"Location">>, Req) of
  602. true -> respond(Req, State, 201);
  603. false -> has_resp_body(Req, State)
  604. end.
  605. has_resp_body(Req, State) ->
  606. case cowboy_http_req:has_resp_body(Req) of
  607. true -> multiple_choices(Req, State);
  608. false -> respond(Req, State, 204)
  609. end.
  610. %% Set the response headers and call the callback found using
  611. %% content_types_provided/2 to obtain the request body and add
  612. %% it to the response.
  613. %%
  614. %% @todo We should give the chosen language and charset to the callback.
  615. set_resp_body(Req=#http_req{method=Method},
  616. State=#state{content_type_a={_Type, Fun}})
  617. when Method =:= 'GET'; Method =:= 'HEAD' ->
  618. {Req2, State2} = set_resp_etag(Req, State),
  619. {LastModified, Req3, State3} = last_modified(Req2, State2),
  620. case LastModified of
  621. LastModified when is_atom(LastModified) ->
  622. Req4 = Req3;
  623. LastModified ->
  624. LastModifiedStr = httpd_util:rfc1123_date(LastModified),
  625. {ok, Req4} = cowboy_http_req:set_resp_header(
  626. <<"Last-Modified">>, LastModifiedStr, Req3)
  627. end,
  628. {Req5, State4} = set_resp_expires(Req4, State3),
  629. case call(Req5, State4, Fun) of
  630. {Body, Req6, HandlerState} ->
  631. State5 = State4#state{handler_state=HandlerState},
  632. {ok, Req7} = cowboy_http_req:set_resp_body(Body, Req6),
  633. multiple_choices(Req7, State5)
  634. end;
  635. set_resp_body(Req, State) ->
  636. multiple_choices(Req, State).
  637. multiple_choices(Req, State) ->
  638. expect(Req, State, multiple_choices, false, 200, 300).
  639. %% Response utility functions.
  640. set_resp_etag(Req, State) ->
  641. {Etag, Req2, State2} = generate_etag(Req, State),
  642. case Etag of
  643. undefined ->
  644. {Req2, State2};
  645. Etag ->
  646. {ok, Req3} = cowboy_http_req:set_resp_header(
  647. <<"Etag">>, Etag, Req2),
  648. {Req3, State2}
  649. end.
  650. set_resp_expires(Req, State) ->
  651. {Expires, Req2, State2} = expires(Req, State),
  652. case Expires of
  653. Expires when is_atom(Expires) ->
  654. {Req2, State2};
  655. Expires ->
  656. ExpiresStr = httpd_util:rfc1123_date(Expires),
  657. {ok, Req3} = cowboy_http_req:set_resp_header(
  658. <<"Expires">>, ExpiresStr, Req2),
  659. {Req3, State2}
  660. end.
  661. %% Info retrieval. No logic.
  662. generate_etag(Req, State=#state{etag=no_call}) ->
  663. {undefined, Req, State};
  664. generate_etag(Req, State=#state{etag=undefined}) ->
  665. case call(Req, State, generate_etag) of
  666. no_call ->
  667. {undefined, Req, State#state{etag=no_call}};
  668. {Etag, Req2, HandlerState2} ->
  669. {Etag, Req2, State#state{handler_state=HandlerState2, etag=Etag}}
  670. end;
  671. generate_etag(Req, State=#state{etag=Etag}) ->
  672. {Etag, Req, State}.
  673. last_modified(Req, State=#state{last_modified=no_call}) ->
  674. {undefined, Req, State};
  675. last_modified(Req, State=#state{last_modified=undefined}) ->
  676. case call(Req, State, last_modified) of
  677. no_call ->
  678. {undefined, Req, State#state{last_modified=no_call}};
  679. {LastModified, Req2, HandlerState2} ->
  680. {LastModified, Req2, State#state{handler_state=HandlerState2,
  681. last_modified=LastModified}}
  682. end;
  683. last_modified(Req, State=#state{last_modified=LastModified}) ->
  684. {LastModified, Req, State}.
  685. expires(Req, State=#state{expires=no_call}) ->
  686. {undefined, Req, State};
  687. expires(Req, State=#state{expires=undefined}) ->
  688. case call(Req, State, expires) of
  689. no_call ->
  690. {undefined, Req, State#state{expires=no_call}};
  691. {Expires, Req2, HandlerState2} ->
  692. {Expires, Req2, State#state{handler_state=HandlerState2,
  693. expires=Expires}}
  694. end;
  695. expires(Req, State=#state{expires=Expires}) ->
  696. {Expires, Req, State}.
  697. %% REST primitives.
  698. expect(Req, State, Callback, Expected, OnTrue, OnFalse) ->
  699. case call(Req, State, Callback) of
  700. no_call ->
  701. next(Req, State, OnTrue);
  702. {Expected, Req2, HandlerState2} ->
  703. next(Req2, State#state{handler_state=HandlerState2}, OnTrue);
  704. {_Unexpected, Req2, HandlerState2} ->
  705. next(Req2, State#state{handler_state=HandlerState2}, OnFalse)
  706. end.
  707. call(Req, #state{handler=Handler, handler_state=HandlerState}, Fun) ->
  708. case erlang:function_exported(Handler, Fun, 2) of
  709. true -> Handler:Fun(Req, HandlerState);
  710. false -> no_call
  711. end.
  712. next(Req, State, Next) when is_function(Next) ->
  713. Next(Req, State);
  714. next(Req, State, StatusCode) when is_integer(StatusCode) ->
  715. respond(Req, State, StatusCode).
  716. %% @todo Allow some sort of callback for custom error pages.
  717. respond(Req, State, StatusCode) ->
  718. {ok, Req2} = cowboy_http_req:reply(StatusCode, Req),
  719. terminate(Req2, State).
  720. terminate(Req, #state{handler=Handler, handler_state=HandlerState}) ->
  721. case erlang:function_exported(Handler, rest_terminate, 2) of
  722. true -> ok = Handler:rest_terminate(
  723. Req#http_req{resp_state=locked}, HandlerState);
  724. false -> ok
  725. end,
  726. {ok, Req}.