cowboy_http_rest.erl 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919
  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 | {strong | weak, binary()},
  39. last_modified :: undefined | no_call | calendar:datetime(),
  40. expires :: undefined | no_call | calendar:datetime()
  41. }).
  42. -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{})
  49. -> {ok, #http_req{}} | close.
  50. upgrade(_ListenerPid, Handler, Opts, Req) ->
  51. try
  52. case erlang:function_exported(Handler, rest_init, 2) of
  53. true ->
  54. case Handler:rest_init(Req, Opts) of
  55. {ok, Req2, HandlerState} ->
  56. service_available(Req2, #state{handler=Handler,
  57. handler_state=HandlerState})
  58. end;
  59. false ->
  60. service_available(Req, #state{handler=Handler})
  61. end
  62. catch Class:Reason ->
  63. PLReq = lists:zip(record_info(fields, http_req), tl(tuple_to_list(Req))),
  64. error_logger:error_msg(
  65. "** Handler ~p terminating in rest_init/3~n"
  66. " for the reason ~p:~p~n** Options were ~p~n"
  67. "** Request was ~p~n** Stacktrace: ~p~n~n",
  68. [Handler, Class, Reason, Opts, PLReq, erlang:get_stacktrace()]),
  69. {ok, _Req2} = cowboy_http_req:reply(500, Req),
  70. close
  71. end.
  72. service_available(Req, State) ->
  73. expect(Req, State, service_available, true, fun known_methods/2, 503).
  74. %% known_methods/2 should return a list of atoms or binary methods.
  75. known_methods(Req=#http_req{method=Method}, State) ->
  76. case call(Req, State, known_methods) of
  77. no_call when Method =:= 'HEAD'; Method =:= 'GET'; Method =:= 'POST';
  78. Method =:= 'PUT'; Method =:= 'DELETE'; Method =:= 'TRACE';
  79. Method =:= 'CONNECT'; Method =:= 'OPTIONS' ->
  80. next(Req, State, fun uri_too_long/2);
  81. no_call ->
  82. next(Req, State, 501);
  83. {halt, Req2, HandlerState} ->
  84. terminate(Req2, State#state{handler_state=HandlerState});
  85. {List, Req2, HandlerState} ->
  86. State2 = State#state{handler_state=HandlerState},
  87. case lists:member(Method, List) of
  88. true -> next(Req2, State2, fun uri_too_long/2);
  89. false -> next(Req2, State2, 501)
  90. end
  91. end.
  92. uri_too_long(Req, State) ->
  93. expect(Req, State, uri_too_long, false, fun allowed_methods/2, 414).
  94. %% allowed_methods/2 should return a list of atoms or binary methods.
  95. allowed_methods(Req=#http_req{method=Method}, State) ->
  96. case call(Req, State, allowed_methods) of
  97. no_call when Method =:= 'HEAD'; Method =:= 'GET' ->
  98. next(Req, State, fun malformed_request/2);
  99. no_call ->
  100. method_not_allowed(Req, State, ['GET', 'HEAD']);
  101. {halt, Req2, HandlerState} ->
  102. terminate(Req2, State#state{handler_state=HandlerState});
  103. {List, Req2, HandlerState} ->
  104. State2 = State#state{handler_state=HandlerState},
  105. case lists:member(Method, List) of
  106. true -> next(Req2, State2, fun malformed_request/2);
  107. false -> method_not_allowed(Req2, State2, List)
  108. end
  109. end.
  110. method_not_allowed(Req, State, Methods) ->
  111. {ok, Req2} = cowboy_http_req:set_resp_header(
  112. <<"Allow">>, method_not_allowed_build(Methods, []), Req),
  113. respond(Req2, State, 405).
  114. method_not_allowed_build([], []) ->
  115. <<>>;
  116. method_not_allowed_build([], [_Ignore|Acc]) ->
  117. lists:reverse(Acc);
  118. method_not_allowed_build([Method|Tail], Acc) when is_atom(Method) ->
  119. Method2 = list_to_binary(atom_to_list(Method)),
  120. method_not_allowed_build(Tail, [<<", ">>, Method2|Acc]);
  121. method_not_allowed_build([Method|Tail], Acc) ->
  122. method_not_allowed_build(Tail, [<<", ">>, Method|Acc]).
  123. malformed_request(Req, State) ->
  124. expect(Req, State, malformed_request, false, fun is_authorized/2, 400).
  125. %% is_authorized/2 should return true or {false, WwwAuthenticateHeader}.
  126. is_authorized(Req, State) ->
  127. case call(Req, State, is_authorized) of
  128. no_call ->
  129. forbidden(Req, State);
  130. {halt, Req2, HandlerState} ->
  131. terminate(Req2, State#state{handler_state=HandlerState});
  132. {true, Req2, HandlerState} ->
  133. forbidden(Req2, State#state{handler_state=HandlerState});
  134. {{false, AuthHead}, Req2, HandlerState} ->
  135. {ok, Req3} = cowboy_http_req:set_resp_header(
  136. <<"Www-Authenticate">>, AuthHead, Req2),
  137. respond(Req3, State#state{handler_state=HandlerState}, 401)
  138. end.
  139. forbidden(Req, State) ->
  140. expect(Req, State, forbidden, false, fun valid_content_headers/2, 403).
  141. valid_content_headers(Req, State) ->
  142. expect(Req, State, valid_content_headers, true,
  143. fun known_content_type/2, 501).
  144. known_content_type(Req, State) ->
  145. expect(Req, State, known_content_type, true,
  146. fun valid_entity_length/2, 413).
  147. valid_entity_length(Req, State) ->
  148. expect(Req, State, valid_entity_length, true, fun options/2, 413).
  149. %% If you need to add additional headers to the response at this point,
  150. %% you should do it directly in the options/2 call using set_resp_headers.
  151. options(Req=#http_req{method='OPTIONS'}, State) ->
  152. case call(Req, State, options) of
  153. {halt, Req2, HandlerState} ->
  154. terminate(Req2, State#state{handler_state=HandlerState});
  155. {ok, Req2, HandlerState} ->
  156. respond(Req2, State#state{handler_state=HandlerState}, 200)
  157. end;
  158. options(Req, State) ->
  159. content_types_provided(Req, State).
  160. %% content_types_provided/2 should return a list of content types and their
  161. %% associated callback function as a tuple: {{Type, SubType, Params}, Fun}.
  162. %% Type and SubType are the media type as binary. Params is a list of
  163. %% Key/Value tuple, with Key and Value a binary. Fun is the name of the
  164. %% callback that will be used to return the content of the response. It is
  165. %% given as an atom.
  166. %%
  167. %% An example of such return value would be:
  168. %% {{<<"text">>, <<"html">>, []}, to_html}
  169. %%
  170. %% Note that it is also possible to return a binary content type that will
  171. %% then be parsed by Cowboy. However note that while this may make your
  172. %% resources a little more readable, this is a lot less efficient. An example
  173. %% of such a return value would be:
  174. %% {<<"text/html">>, to_html}
  175. content_types_provided(Req=#http_req{meta=Meta}, State) ->
  176. case call(Req, State, content_types_provided) of
  177. no_call ->
  178. not_acceptable(Req, State);
  179. {halt, Req2, HandlerState} ->
  180. terminate(Req2, State#state{handler_state=HandlerState});
  181. {[], Req2, HandlerState} ->
  182. not_acceptable(Req2, State#state{handler_state=HandlerState});
  183. {CTP, Req2, HandlerState} ->
  184. CTP2 = [normalize_content_types_provided(P) || P <- CTP],
  185. State2 = State#state{
  186. handler_state=HandlerState, content_types_p=CTP2},
  187. {Accept, Req3} = cowboy_http_req:parse_header('Accept', Req2),
  188. case Accept of
  189. undefined ->
  190. {PMT, _Fun} = HeadCTP = hd(CTP2),
  191. languages_provided(
  192. Req3#http_req{meta=[{media_type, PMT}|Meta]},
  193. State2#state{content_type_a=HeadCTP});
  194. Accept ->
  195. Accept2 = prioritize_accept(Accept),
  196. choose_media_type(Req3, State2, Accept2)
  197. end
  198. end.
  199. normalize_content_types_provided({ContentType, Handler})
  200. when is_binary(ContentType) ->
  201. {cowboy_http:content_type(ContentType), Handler};
  202. normalize_content_types_provided(Provided) ->
  203. Provided.
  204. prioritize_accept(Accept) ->
  205. lists:sort(
  206. fun ({MediaTypeA, Quality, _AcceptParamsA},
  207. {MediaTypeB, Quality, _AcceptParamsB}) ->
  208. %% Same quality, check precedence in more details.
  209. prioritize_mediatype(MediaTypeA, MediaTypeB);
  210. ({_MediaTypeA, QualityA, _AcceptParamsA},
  211. {_MediaTypeB, QualityB, _AcceptParamsB}) ->
  212. %% Just compare the quality.
  213. QualityA > QualityB
  214. end, Accept).
  215. %% Media ranges can be overridden by more specific media ranges or
  216. %% specific media types. If more than one media range applies to a given
  217. %% type, the most specific reference has precedence.
  218. %%
  219. %% We always choose B over A when we can't decide between the two.
  220. prioritize_mediatype({TypeA, SubTypeA, ParamsA}, {TypeB, SubTypeB, ParamsB}) ->
  221. case TypeB of
  222. TypeA ->
  223. case SubTypeB of
  224. SubTypeA -> length(ParamsA) > length(ParamsB);
  225. <<"*">> -> true;
  226. _Any -> false
  227. end;
  228. <<"*">> -> true;
  229. _Any -> false
  230. end.
  231. %% Ignoring the rare AcceptParams. Not sure what should be done about them.
  232. choose_media_type(Req, State, []) ->
  233. not_acceptable(Req, State);
  234. choose_media_type(Req, State=#state{content_types_p=CTP},
  235. [MediaType|Tail]) ->
  236. match_media_type(Req, State, Tail, CTP, MediaType).
  237. match_media_type(Req, State, Accept, [], _MediaType) ->
  238. choose_media_type(Req, State, Accept);
  239. match_media_type(Req, State, Accept, CTP,
  240. MediaType = {{<<"*">>, <<"*">>, _Params_A}, _QA, _APA}) ->
  241. match_media_type_params(Req, State, Accept, CTP, MediaType);
  242. match_media_type(Req, State, Accept,
  243. CTP = [{{Type, SubType_P, _PP}, _Fun}|_Tail],
  244. MediaType = {{Type, SubType_A, _PA}, _QA, _APA})
  245. when SubType_P =:= SubType_A; SubType_A =:= <<"*">> ->
  246. match_media_type_params(Req, State, Accept, CTP, MediaType);
  247. match_media_type(Req, State, Accept, [_Any|Tail], MediaType) ->
  248. match_media_type(Req, State, Accept, Tail, MediaType).
  249. match_media_type_params(Req=#http_req{meta=Meta}, State, Accept,
  250. [Provided = {PMT = {_TP, _STP, Params_P}, _Fun}|Tail],
  251. MediaType = {{_TA, _STA, Params_A}, _QA, _APA}) ->
  252. case lists:sort(Params_P) =:= lists:sort(Params_A) of
  253. true ->
  254. languages_provided(Req#http_req{meta=[{media_type, PMT}|Meta]},
  255. State#state{content_type_a=Provided});
  256. false ->
  257. match_media_type(Req, State, Accept, Tail, MediaType)
  258. end.
  259. %% languages_provided should return a list of binary values indicating
  260. %% which languages are accepted by the resource.
  261. %%
  262. %% @todo I suppose we should also ask the resource if it wants to
  263. %% set a language itself or if it wants it to be automatically chosen.
  264. languages_provided(Req, State) ->
  265. case call(Req, State, languages_provided) of
  266. no_call ->
  267. charsets_provided(Req, State);
  268. {halt, Req2, HandlerState} ->
  269. terminate(Req2, State#state{handler_state=HandlerState});
  270. {[], Req2, HandlerState} ->
  271. not_acceptable(Req2, State#state{handler_state=HandlerState});
  272. {LP, Req2, HandlerState} ->
  273. State2 = State#state{handler_state=HandlerState, languages_p=LP},
  274. {AcceptLanguage, Req3} =
  275. cowboy_http_req:parse_header('Accept-Language', Req2),
  276. case AcceptLanguage of
  277. undefined ->
  278. set_language(Req3, State2#state{language_a=hd(LP)});
  279. AcceptLanguage ->
  280. AcceptLanguage2 = prioritize_languages(AcceptLanguage),
  281. choose_language(Req3, State2, AcceptLanguage2)
  282. end
  283. end.
  284. %% A language-range matches a language-tag if it exactly equals the tag,
  285. %% or if it exactly equals a prefix of the tag such that the first tag
  286. %% character following the prefix is "-". The special range "*", if
  287. %% present in the Accept-Language field, matches every tag not matched
  288. %% by any other range present in the Accept-Language field.
  289. %%
  290. %% @todo The last sentence probably means we should always put '*'
  291. %% at the end of the list.
  292. prioritize_languages(AcceptLanguages) ->
  293. lists:sort(
  294. fun ({_TagA, QualityA}, {_TagB, QualityB}) ->
  295. QualityA > QualityB
  296. end, AcceptLanguages).
  297. choose_language(Req, State, []) ->
  298. not_acceptable(Req, State);
  299. choose_language(Req, State=#state{languages_p=LP}, [Language|Tail]) ->
  300. match_language(Req, State, Tail, LP, Language).
  301. match_language(Req, State, Accept, [], _Language) ->
  302. choose_language(Req, State, Accept);
  303. match_language(Req, State, _Accept, [Provided|_Tail], {'*', _Quality}) ->
  304. set_language(Req, State#state{language_a=Provided});
  305. match_language(Req, State, _Accept, [Provided|_Tail], {Provided, _Quality}) ->
  306. set_language(Req, State#state{language_a=Provided});
  307. match_language(Req, State, Accept, [Provided|Tail],
  308. Language = {Tag, _Quality}) ->
  309. Length = byte_size(Tag),
  310. case Provided of
  311. << Tag:Length/binary, $-, _Any/bits >> ->
  312. set_language(Req, State#state{language_a=Provided});
  313. _Any ->
  314. match_language(Req, State, Accept, Tail, Language)
  315. end.
  316. set_language(Req=#http_req{meta=Meta}, State=#state{language_a=Language}) ->
  317. {ok, Req2} = cowboy_http_req:set_resp_header(
  318. <<"Content-Language">>, Language, Req),
  319. charsets_provided(Req2#http_req{meta=[{language, Language}|Meta]}, State).
  320. %% charsets_provided should return a list of binary values indicating
  321. %% which charsets are accepted by the resource.
  322. charsets_provided(Req, State) ->
  323. case call(Req, State, charsets_provided) of
  324. no_call ->
  325. set_content_type(Req, State);
  326. {halt, Req2, HandlerState} ->
  327. terminate(Req2, State#state{handler_state=HandlerState});
  328. {[], Req2, HandlerState} ->
  329. not_acceptable(Req2, State#state{handler_state=HandlerState});
  330. {CP, Req2, HandlerState} ->
  331. State2 = State#state{handler_state=HandlerState, charsets_p=CP},
  332. {AcceptCharset, Req3} =
  333. cowboy_http_req:parse_header('Accept-Charset', Req2),
  334. case AcceptCharset of
  335. undefined ->
  336. set_content_type(Req3, State2#state{charset_a=hd(CP)});
  337. AcceptCharset ->
  338. AcceptCharset2 = prioritize_charsets(AcceptCharset),
  339. choose_charset(Req3, State2, AcceptCharset2)
  340. end
  341. end.
  342. %% The special value "*", if present in the Accept-Charset field,
  343. %% matches every character set (including ISO-8859-1) which is not
  344. %% mentioned elsewhere in the Accept-Charset field. If no "*" is present
  345. %% in an Accept-Charset field, then all character sets not explicitly
  346. %% mentioned get a quality value of 0, except for ISO-8859-1, which gets
  347. %% a quality value of 1 if not explicitly mentioned.
  348. prioritize_charsets(AcceptCharsets) ->
  349. AcceptCharsets2 = lists:sort(
  350. fun ({_CharsetA, QualityA}, {_CharsetB, QualityB}) ->
  351. QualityA > QualityB
  352. end, AcceptCharsets),
  353. case lists:keymember(<<"*">>, 1, AcceptCharsets2) of
  354. true -> AcceptCharsets2;
  355. false -> [{<<"iso-8859-1">>, 1000}|AcceptCharsets2]
  356. end.
  357. choose_charset(Req, State, []) ->
  358. not_acceptable(Req, State);
  359. choose_charset(Req, State=#state{charsets_p=CP}, [Charset|Tail]) ->
  360. match_charset(Req, State, Tail, CP, Charset).
  361. match_charset(Req, State, Accept, [], _Charset) ->
  362. choose_charset(Req, State, Accept);
  363. match_charset(Req, State, _Accept, [Provided|_Tail],
  364. {Provided, _Quality}) ->
  365. set_content_type(Req, State#state{charset_a=Provided});
  366. match_charset(Req, State, Accept, [_Provided|Tail], Charset) ->
  367. match_charset(Req, State, Accept, Tail, Charset).
  368. set_content_type(Req=#http_req{meta=Meta}, State=#state{
  369. content_type_a={{Type, SubType, Params}, _Fun},
  370. charset_a=Charset}) ->
  371. ParamsBin = set_content_type_build_params(Params, []),
  372. ContentType = [Type, <<"/">>, SubType, ParamsBin],
  373. ContentType2 = case Charset of
  374. undefined -> ContentType;
  375. Charset -> [ContentType, <<"; charset=">>, Charset]
  376. end,
  377. {ok, Req2} = cowboy_http_req:set_resp_header(
  378. <<"Content-Type">>, ContentType2, Req),
  379. encodings_provided(Req2#http_req{meta=[{charset, Charset}|Meta]}, State).
  380. set_content_type_build_params([], []) ->
  381. <<>>;
  382. set_content_type_build_params([], Acc) ->
  383. lists:reverse(Acc);
  384. set_content_type_build_params([{Attr, Value}|Tail], Acc) ->
  385. set_content_type_build_params(Tail, [[Attr, <<"=">>, Value], <<";">>|Acc]).
  386. %% @todo Match for identity as we provide nothing else for now.
  387. %% @todo Don't forget to set the Content-Encoding header when we reply a body
  388. %% and the found encoding is something other than identity.
  389. encodings_provided(Req, State) ->
  390. variances(Req, State).
  391. not_acceptable(Req, State) ->
  392. respond(Req, State, 406).
  393. %% variances/2 should return a list of headers that will be added
  394. %% to the Vary response header. The Accept, Accept-Language,
  395. %% Accept-Charset and Accept-Encoding headers do not need to be
  396. %% specified.
  397. %%
  398. %% @todo Do Accept-Encoding too when we handle it.
  399. %% @todo Does the order matter?
  400. variances(Req, State=#state{content_types_p=CTP,
  401. languages_p=LP, charsets_p=CP}) ->
  402. Variances = case CTP of
  403. [] -> [];
  404. [_] -> [];
  405. [_|_] -> [<<"Accept">>]
  406. end,
  407. Variances2 = case LP of
  408. [] -> Variances;
  409. [_] -> Variances;
  410. [_|_] -> [<<"Accept-Language">>|Variances]
  411. end,
  412. Variances3 = case CP of
  413. [] -> Variances2;
  414. [_] -> Variances2;
  415. [_|_] -> [<<"Accept-Charset">>|Variances2]
  416. end,
  417. {Variances4, Req3, State2} = case call(Req, State, variances) of
  418. no_call ->
  419. {Variances3, Req, State};
  420. {HandlerVariances, Req2, HandlerState} ->
  421. {Variances3 ++ HandlerVariances, Req2,
  422. State#state{handler_state=HandlerState}}
  423. end,
  424. case [[<<", ">>, V] || V <- Variances4] of
  425. [] ->
  426. resource_exists(Req3, State2);
  427. [[<<", ">>, H]|Variances5] ->
  428. {ok, Req4} = cowboy_http_req:set_resp_header(
  429. <<"Variances">>, [H|Variances5], Req3),
  430. resource_exists(Req4, State2)
  431. end.
  432. resource_exists(Req, State) ->
  433. expect(Req, State, resource_exists, true,
  434. fun if_match_exists/2, fun if_match_musnt_exist/2).
  435. if_match_exists(Req, State) ->
  436. case cowboy_http_req:parse_header('If-Match', Req) of
  437. {undefined, Req2} ->
  438. if_unmodified_since_exists(Req2, State);
  439. {'*', Req2} ->
  440. if_unmodified_since_exists(Req2, State);
  441. {ETagsList, Req2} ->
  442. if_match(Req2, State, ETagsList)
  443. end.
  444. if_match(Req, State, EtagsList) ->
  445. {Etag, Req2, State2} = generate_etag(Req, State),
  446. case lists:member(Etag, EtagsList) of
  447. true -> if_unmodified_since_exists(Req2, State2);
  448. %% Etag may be `undefined' which cannot be a member.
  449. false -> precondition_failed(Req2, State2)
  450. end.
  451. if_match_musnt_exist(Req, State) ->
  452. case cowboy_http_req:header('If-Match', Req) of
  453. {undefined, Req2} -> is_put_to_missing_resource(Req2, State);
  454. {_Any, Req2} -> precondition_failed(Req2, State)
  455. end.
  456. if_unmodified_since_exists(Req, State) ->
  457. case cowboy_http_req:parse_header('If-Unmodified-Since', Req) of
  458. {undefined, Req2} ->
  459. if_none_match_exists(Req2, State);
  460. {{error, badarg}, Req2} ->
  461. if_none_match_exists(Req2, State);
  462. {IfUnmodifiedSince, Req2} ->
  463. if_unmodified_since(Req2, State, IfUnmodifiedSince)
  464. end.
  465. %% If LastModified is the atom 'no_call', we continue.
  466. if_unmodified_since(Req, State, IfUnmodifiedSince) ->
  467. {LastModified, Req2, State2} = last_modified(Req, State),
  468. case LastModified > IfUnmodifiedSince of
  469. true -> precondition_failed(Req2, State2);
  470. false -> if_none_match_exists(Req2, State2)
  471. end.
  472. if_none_match_exists(Req, State) ->
  473. case cowboy_http_req:parse_header('If-None-Match', Req) of
  474. {undefined, Req2} ->
  475. if_modified_since_exists(Req2, State);
  476. {'*', Req2} ->
  477. precondition_is_head_get(Req2, State);
  478. {EtagsList, Req2} ->
  479. if_none_match(Req2, State, EtagsList)
  480. end.
  481. if_none_match(Req, State, EtagsList) ->
  482. {Etag, Req2, State2} = generate_etag(Req, State),
  483. case Etag of
  484. undefined ->
  485. precondition_failed(Req2, State2);
  486. Etag ->
  487. case lists:member(Etag, EtagsList) of
  488. true -> precondition_is_head_get(Req2, State2);
  489. false -> if_modified_since_exists(Req2, State2)
  490. end
  491. end.
  492. precondition_is_head_get(Req=#http_req{method=Method}, State)
  493. when Method =:= 'HEAD'; Method =:= 'GET' ->
  494. not_modified(Req, State);
  495. precondition_is_head_get(Req, State) ->
  496. precondition_failed(Req, State).
  497. if_modified_since_exists(Req, State) ->
  498. case cowboy_http_req:parse_header('If-Modified-Since', Req) of
  499. {undefined, Req2} ->
  500. method(Req2, State);
  501. {{error, badarg}, Req2} ->
  502. method(Req2, State);
  503. {IfModifiedSince, Req2} ->
  504. if_modified_since_now(Req2, State, IfModifiedSince)
  505. end.
  506. if_modified_since_now(Req, State, IfModifiedSince) ->
  507. case IfModifiedSince > erlang:universaltime() of
  508. true -> method(Req, State);
  509. false -> if_modified_since(Req, State, IfModifiedSince)
  510. end.
  511. if_modified_since(Req, State, IfModifiedSince) ->
  512. {LastModified, Req2, State2} = last_modified(Req, State),
  513. case LastModified of
  514. no_call ->
  515. method(Req2, State2);
  516. LastModified ->
  517. case LastModified > IfModifiedSince of
  518. true -> method(Req2, State2);
  519. false -> not_modified(Req2, State2)
  520. end
  521. end.
  522. not_modified(Req=#http_req{resp_headers=RespHeaders}, State) ->
  523. RespHeaders2 = lists:keydelete(<<"Content-Type">>, 1, RespHeaders),
  524. Req2 = Req#http_req{resp_headers=RespHeaders2},
  525. {Req3, State2} = set_resp_etag(Req2, State),
  526. {Req4, State3} = set_resp_expires(Req3, State2),
  527. respond(Req4, State3, 304).
  528. precondition_failed(Req, State) ->
  529. respond(Req, State, 412).
  530. is_put_to_missing_resource(Req=#http_req{method='PUT'}, State) ->
  531. moved_permanently(Req, State, fun is_conflict/2);
  532. is_put_to_missing_resource(Req, State) ->
  533. previously_existed(Req, State).
  534. %% moved_permanently/2 should return either false or {true, Location}
  535. %% with Location the full new URI of the resource.
  536. moved_permanently(Req, State, OnFalse) ->
  537. case call(Req, State, moved_permanently) of
  538. {{true, Location}, Req2, HandlerState} ->
  539. {ok, Req3} = cowboy_http_req:set_resp_header(
  540. <<"Location">>, Location, Req2),
  541. respond(Req3, State#state{handler_state=HandlerState}, 301);
  542. {false, Req2, HandlerState} ->
  543. OnFalse(Req2, State#state{handler_state=HandlerState});
  544. {halt, Req2, HandlerState} ->
  545. terminate(Req2, State#state{handler_state=HandlerState});
  546. no_call ->
  547. OnFalse(Req, State)
  548. end.
  549. previously_existed(Req, State) ->
  550. expect(Req, State, previously_existed, false,
  551. fun (R, S) -> is_post_to_missing_resource(R, S, 404) end,
  552. fun (R, S) -> moved_permanently(R, S, fun moved_temporarily/2) end).
  553. %% moved_temporarily/2 should return either false or {true, Location}
  554. %% with Location the full new URI of the resource.
  555. moved_temporarily(Req, State) ->
  556. case call(Req, State, moved_temporarily) of
  557. {{true, Location}, Req2, HandlerState} ->
  558. {ok, Req3} = cowboy_http_req:set_resp_header(
  559. <<"Location">>, Location, Req2),
  560. respond(Req3, State#state{handler_state=HandlerState}, 307);
  561. {false, Req2, HandlerState} ->
  562. is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState}, 410);
  563. {halt, Req2, HandlerState} ->
  564. terminate(Req2, State#state{handler_state=HandlerState});
  565. no_call ->
  566. is_post_to_missing_resource(Req, State, 410)
  567. end.
  568. is_post_to_missing_resource(Req=#http_req{method='POST'}, State, OnFalse) ->
  569. allow_missing_post(Req, State, OnFalse);
  570. is_post_to_missing_resource(Req, State, OnFalse) ->
  571. respond(Req, State, OnFalse).
  572. allow_missing_post(Req, State, OnFalse) ->
  573. expect(Req, State, allow_missing_post, true, fun post_is_create/2, OnFalse).
  574. method(Req=#http_req{method='DELETE'}, State) ->
  575. delete_resource(Req, State);
  576. method(Req=#http_req{method='POST'}, State) ->
  577. post_is_create(Req, State);
  578. method(Req=#http_req{method='PUT'}, State) ->
  579. is_conflict(Req, State);
  580. method(Req, State) ->
  581. set_resp_body(Req, State).
  582. %% delete_resource/2 should start deleting the resource and return.
  583. delete_resource(Req, State) ->
  584. expect(Req, State, delete_resource, false, 500, fun delete_completed/2).
  585. %% delete_completed/2 indicates whether the resource has been deleted yet.
  586. delete_completed(Req, State) ->
  587. expect(Req, State, delete_completed, true, fun has_resp_body/2, 202).
  588. %% post_is_create/2 indicates whether the POST method can create new resources.
  589. post_is_create(Req, State) ->
  590. expect(Req, State, post_is_create, false, fun process_post/2, fun create_path/2).
  591. %% When the POST method can create new resources, create_path/2 will be called
  592. %% and is expected to return the full path to the new resource
  593. %% (including the leading /).
  594. create_path(Req=#http_req{meta=Meta}, State) ->
  595. case call(Req, State, create_path) of
  596. {halt, Req2, HandlerState} ->
  597. terminate(Req2, State#state{handler_state=HandlerState});
  598. {Path, Req2, HandlerState} ->
  599. Location = create_path_location(Req2, Path),
  600. State2 = State#state{handler_state=HandlerState},
  601. {ok, Req3} = cowboy_http_req:set_resp_header(
  602. <<"Location">>, Location, Req2),
  603. put_resource(Req3#http_req{meta=[{put_path, Path}|Meta]},
  604. State2, 303)
  605. end.
  606. create_path_location(#http_req{transport=Transport, raw_host=Host,
  607. port=Port}, Path) ->
  608. TransportName = Transport:name(),
  609. << (create_path_location_protocol(TransportName))/binary, "://",
  610. Host/binary, (create_path_location_port(TransportName, Port))/binary,
  611. Path/binary >>.
  612. create_path_location_protocol(ssl) -> <<"https">>;
  613. create_path_location_protocol(_) -> <<"http">>.
  614. create_path_location_port(ssl, 443) ->
  615. <<>>;
  616. create_path_location_port(tcp, 80) ->
  617. <<>>;
  618. create_path_location_port(_, Port) ->
  619. <<":", (list_to_binary(integer_to_list(Port)))/binary>>.
  620. %% process_post should return true when the POST body could be processed
  621. %% and false when it hasn't, in which case a 500 error is sent.
  622. process_post(Req, State) ->
  623. case call(Req, State, process_post) of
  624. {halt, Req2, HandlerState} ->
  625. terminate(Req2, State#state{handler_state=HandlerState});
  626. {true, Req2, HandlerState} ->
  627. State2 = State#state{handler_state=HandlerState},
  628. next(Req2, State2, fun is_new_resource/2);
  629. {false, Req2, HandlerState} ->
  630. State2 = State#state{handler_state=HandlerState},
  631. respond(Req2, State2, 500)
  632. end.
  633. is_conflict(Req, State) ->
  634. expect(Req, State, is_conflict, false, fun put_resource/2, 409).
  635. put_resource(Req=#http_req{raw_path=RawPath, meta=Meta}, State) ->
  636. Req2 = Req#http_req{meta=[{put_path, RawPath}|Meta]},
  637. put_resource(Req2, State, fun is_new_resource/2).
  638. %% content_types_accepted should return a list of media types and their
  639. %% associated callback functions in the same format as content_types_provided.
  640. %%
  641. %% The callback will then be called and is expected to process the content
  642. %% pushed to the resource in the request body. The path to the new resource
  643. %% may be different from the request path, and is stored as request metadata.
  644. %% It is always defined past this point. It can be retrieved as demonstrated:
  645. %% {PutPath, Req2} = cowboy_http_req:meta(put_path, Req)
  646. put_resource(Req, State, OnTrue) ->
  647. case call(Req, State, content_types_accepted) of
  648. no_call ->
  649. respond(Req, State, 415);
  650. {halt, Req2, HandlerState} ->
  651. terminate(Req2, State#state{handler_state=HandlerState});
  652. {CTA, Req2, HandlerState} ->
  653. State2 = State#state{handler_state=HandlerState},
  654. {ContentType, Req3}
  655. = cowboy_http_req:parse_header('Content-Type', Req2),
  656. choose_content_type(Req3, State2, OnTrue, ContentType, CTA)
  657. end.
  658. %% The special content type '*' will always match. It can be used as a
  659. %% catch-all content type for accepting any kind of request content.
  660. %% Note that because it will always match, it should be the last of the
  661. %% list of content types, otherwise it'll shadow the ones following.
  662. choose_content_type(Req, State, _OnTrue, _ContentType, []) ->
  663. respond(Req, State, 415);
  664. choose_content_type(Req, State, OnTrue, ContentType, [{Accepted, Fun}|_Tail])
  665. when Accepted =:= '*' orelse Accepted =:= ContentType ->
  666. case call(Req, State, Fun) of
  667. {halt, Req2, HandlerState} ->
  668. terminate(Req2, State#state{handler_state=HandlerState});
  669. {true, Req2, HandlerState} ->
  670. State2 = State#state{handler_state=HandlerState},
  671. next(Req2, State2, OnTrue);
  672. {false, Req2, HandlerState} ->
  673. State2 = State#state{handler_state=HandlerState},
  674. respond(Req2, State2, 500)
  675. end;
  676. choose_content_type(Req, State, OnTrue, ContentType, [_Any|Tail]) ->
  677. choose_content_type(Req, State, OnTrue, ContentType, Tail).
  678. %% Whether we created a new resource, either through PUT or POST.
  679. %% This is easily testable because we would have set the Location
  680. %% header by this point if we did so.
  681. is_new_resource(Req, State) ->
  682. case cowboy_http_req:has_resp_header(<<"Location">>, Req) of
  683. true -> respond(Req, State, 201);
  684. false -> has_resp_body(Req, State)
  685. end.
  686. has_resp_body(Req, State) ->
  687. case cowboy_http_req:has_resp_body(Req) of
  688. true -> multiple_choices(Req, State);
  689. false -> respond(Req, State, 204)
  690. end.
  691. %% Set the response headers and call the callback found using
  692. %% content_types_provided/2 to obtain the request body and add
  693. %% it to the response.
  694. set_resp_body(Req=#http_req{method=Method},
  695. State=#state{content_type_a={_Type, Fun}})
  696. when Method =:= 'GET'; Method =:= 'HEAD' ->
  697. {Req2, State2} = set_resp_etag(Req, State),
  698. {LastModified, Req3, State3} = last_modified(Req2, State2),
  699. case LastModified of
  700. LastModified when is_atom(LastModified) ->
  701. Req4 = Req3;
  702. LastModified ->
  703. LastModifiedStr = httpd_util:rfc1123_date(LastModified),
  704. {ok, Req4} = cowboy_http_req:set_resp_header(
  705. <<"Last-Modified">>, LastModifiedStr, Req3)
  706. end,
  707. {Req5, State4} = set_resp_expires(Req4, State3),
  708. case call(Req5, State4, Fun) of
  709. {halt, Req6, HandlerState} ->
  710. terminate(Req6, State4#state{handler_state=HandlerState});
  711. {Body, Req6, HandlerState} ->
  712. State5 = State4#state{handler_state=HandlerState},
  713. {ok, Req7} = case Body of
  714. {stream, Len, Fun1} ->
  715. cowboy_http_req:set_resp_body_fun(Len, Fun1, Req6);
  716. _Contents ->
  717. cowboy_http_req:set_resp_body(Body, Req6)
  718. end,
  719. multiple_choices(Req7, State5)
  720. end;
  721. set_resp_body(Req, State) ->
  722. multiple_choices(Req, State).
  723. multiple_choices(Req, State) ->
  724. expect(Req, State, multiple_choices, false, 200, 300).
  725. %% Response utility functions.
  726. set_resp_etag(Req, State) ->
  727. {Etag, Req2, State2} = generate_etag(Req, State),
  728. case Etag of
  729. undefined ->
  730. {Req2, State2};
  731. Etag ->
  732. {ok, Req3} = cowboy_http_req:set_resp_header(
  733. <<"ETag">>, encode_etag(Etag), Req2),
  734. {Req3, State2}
  735. end.
  736. -spec encode_etag({strong | weak, binary()}) -> iolist().
  737. encode_etag({strong, Etag}) -> [$",Etag,$"];
  738. encode_etag({weak, Etag}) -> ["W/\"",Etag,$"].
  739. set_resp_expires(Req, State) ->
  740. {Expires, Req2, State2} = expires(Req, State),
  741. case Expires of
  742. Expires when is_atom(Expires) ->
  743. {Req2, State2};
  744. Expires ->
  745. ExpiresStr = httpd_util:rfc1123_date(Expires),
  746. {ok, Req3} = cowboy_http_req:set_resp_header(
  747. <<"Expires">>, ExpiresStr, Req2),
  748. {Req3, State2}
  749. end.
  750. %% Info retrieval. No logic.
  751. generate_etag(Req, State=#state{etag=no_call}) ->
  752. {undefined, Req, State};
  753. generate_etag(Req, State=#state{etag=undefined}) ->
  754. case call(Req, State, generate_etag) of
  755. no_call ->
  756. {undefined, Req, State#state{etag=no_call}};
  757. %% Previously the return value from the generate_etag/2 callback was set
  758. %% as the value of the ETag header in the response. Therefore the only
  759. %% valid return type was `binary()'. If a handler returns a `binary()'
  760. %% it must be mapped to the expected type or it'll always fail to
  761. %% compare equal to any entity tags present in the request headers.
  762. %% @todo Remove support for binary return values after 0.6.
  763. {Etag, Req2, HandlerState} when is_binary(Etag) ->
  764. [Etag2] = cowboy_http:entity_tag_match(Etag),
  765. {Etag2, Req2, State#state{handler_state=HandlerState, etag=Etag2}};
  766. {Etag, Req2, HandlerState} ->
  767. {Etag, Req2, State#state{handler_state=HandlerState, etag=Etag}}
  768. end;
  769. generate_etag(Req, State=#state{etag=Etag}) ->
  770. {Etag, Req, State}.
  771. last_modified(Req, State=#state{last_modified=no_call}) ->
  772. {undefined, Req, State};
  773. last_modified(Req, State=#state{last_modified=undefined}) ->
  774. case call(Req, State, last_modified) of
  775. no_call ->
  776. {undefined, Req, State#state{last_modified=no_call}};
  777. {LastModified, Req2, HandlerState} ->
  778. {LastModified, Req2, State#state{handler_state=HandlerState,
  779. last_modified=LastModified}}
  780. end;
  781. last_modified(Req, State=#state{last_modified=LastModified}) ->
  782. {LastModified, Req, State}.
  783. expires(Req, State=#state{expires=no_call}) ->
  784. {undefined, Req, State};
  785. expires(Req, State=#state{expires=undefined}) ->
  786. case call(Req, State, expires) of
  787. no_call ->
  788. {undefined, Req, State#state{expires=no_call}};
  789. {Expires, Req2, HandlerState} ->
  790. {Expires, Req2, State#state{handler_state=HandlerState,
  791. expires=Expires}}
  792. end;
  793. expires(Req, State=#state{expires=Expires}) ->
  794. {Expires, Req, State}.
  795. %% REST primitives.
  796. expect(Req, State, Callback, Expected, OnTrue, OnFalse) ->
  797. case call(Req, State, Callback) of
  798. no_call ->
  799. next(Req, State, OnTrue);
  800. {halt, Req2, HandlerState} ->
  801. terminate(Req2, State#state{handler_state=HandlerState});
  802. {Expected, Req2, HandlerState} ->
  803. next(Req2, State#state{handler_state=HandlerState}, OnTrue);
  804. {_Unexpected, Req2, HandlerState} ->
  805. next(Req2, State#state{handler_state=HandlerState}, OnFalse)
  806. end.
  807. call(Req, #state{handler=Handler, handler_state=HandlerState}, Fun) ->
  808. case erlang:function_exported(Handler, Fun, 2) of
  809. true -> Handler:Fun(Req, HandlerState);
  810. false -> no_call
  811. end.
  812. next(Req, State, Next) when is_function(Next) ->
  813. Next(Req, State);
  814. next(Req, State, StatusCode) when is_integer(StatusCode) ->
  815. respond(Req, State, StatusCode).
  816. %% @todo Allow some sort of callback for custom error pages.
  817. respond(Req, State, StatusCode) ->
  818. {ok, Req2} = cowboy_http_req:reply(StatusCode, Req),
  819. terminate(Req2, State).
  820. terminate(Req, #state{handler=Handler, handler_state=HandlerState}) ->
  821. case erlang:function_exported(Handler, rest_terminate, 2) of
  822. true -> ok = Handler:rest_terminate(
  823. Req#http_req{resp_state=locked}, HandlerState);
  824. false -> ok
  825. end,
  826. {ok, Req}.