/src/server_gateways/ewgi_mochiweb.erl

http://github.com/skarab/ewgi · Erlang · 421 lines · 300 code · 56 blank · 65 comment · 0 complexity · 4dcd6d9b4ba1849b2bb0a158d2b97c65 MD5 · raw file

  1. %%%-------------------------------------------------------------------
  2. %%% File : ewgi_mochiweb.erl
  3. %%% Authors : Filippo Pacini <filippo.pacini@gmail.com>
  4. %%% Hunter Morris <huntermorris@gmail.com>
  5. %%% License :
  6. %%% The contents of this file are subject to the Mozilla Public
  7. %%% License Version 1.1 (the "License"); you may not use this file
  8. %%% except in compliance with the License. You may obtain a copy of
  9. %%% the License at http://www.mozilla.org/MPL/
  10. %%%
  11. %%% Software distributed under the License is distributed on an "AS IS"
  12. %%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
  13. %%% the License for the specific language governing rights and
  14. %%% limitations under the License.
  15. %%% The Initial Developer of the Original Code is S.G. Consulting
  16. %%% srl. Portions created by S.G. Consulting s.r.l. are Copyright (C)
  17. %%% 2007 S.G. Consulting srl. All Rights Reserved.
  18. %%%
  19. %%% @doc
  20. %%% <p>Reference implementation of a MochiWeb EWGI server gateway.</p>
  21. %%%
  22. %%% @end
  23. %%%
  24. %%% Created : 12 Oct 2007 by Filippo Pacini <filippo.pacini@gmail.com>
  25. %%%-------------------------------------------------------------------
  26. -module(ewgi_mochiweb).
  27. %% ewgi callbacks
  28. -export([run/2]).
  29. -export([
  30. stream_process_deliver/2,
  31. stream_process_deliver_chunk/2,
  32. stream_process_deliver_final_chunk/2,
  33. stream_process_end/2
  34. ]).
  35. -include_lib("ewgi.hrl").
  36. -define(EWGI2MOCHI(Err, Hdrs), {element(1, Err), Hdrs, element(2, Err)}).
  37. %%====================================================================
  38. %% ewgi_server callbacks
  39. %%====================================================================
  40. run(Appl, MochiReq) ->
  41. try parse_arg(MochiReq) of
  42. Req when ?IS_EWGI_REQUEST(Req) ->
  43. try process_application(Appl, ewgi_api:context(Req, ewgi_api:empty_response())) of
  44. not_found ->
  45. MochiReq:not_found();
  46. Ctx when ?IS_EWGI_CONTEXT(Ctx) ->
  47. handle_result(?INSPECT_EWGI_RESPONSE(Ctx), MochiReq)
  48. catch
  49. _:Reason ->
  50. error_logger:error_report(Reason),
  51. MochiReq:respond({500, [], "Internal server error"})
  52. end
  53. catch
  54. _:Reason ->
  55. error_logger:error_report(Reason),
  56. MochiReq:respond({400, [], "Bad request"})
  57. end.
  58. %% Chunked response if a nullary function is returned
  59. handle_result(Ctx, Req) ->
  60. case ewgi_api:response_message_body(Ctx) of
  61. {push_stream, GeneratorPid, Timeout} when is_pid(GeneratorPid) ->
  62. handle_push_stream(Ctx, Req, GeneratorPid, Timeout);
  63. Body ->
  64. {Code, _} = ewgi_api:response_status(Ctx),
  65. Headers = ewgi_api:response_headers(Ctx),
  66. handle_result1(Code, Headers, Body, Req)
  67. end.
  68. handle_result1(Code, Headers, F, Req) when is_function(F, 0) ->
  69. MochiResp = Req:respond({Code, Headers, chunked}),
  70. %handle_stream_result(MochiResp, (catch F()));
  71. handle_stream(MochiResp, F);
  72. handle_result1(Code, Headers, L, Req) ->
  73. Req:respond({Code, Headers, L}).
  74. handle_push_stream(Ctx, Req, GeneratorPid, Timeout) ->
  75. Socket = Req:get(socket),
  76. GeneratorPid ! {push_stream_init, ?MODULE, self(), Socket},
  77. receive
  78. {push_stream_init, GeneratorPid, Code, Headers, TransferEncoding} ->
  79. case TransferEncoding of
  80. chunked ->
  81. Req:respond({Code, Headers, chunked}),
  82. GeneratorPid ! {ok, self()}
  83. ;_ ->
  84. %% mochiweb_request:respond/1 expects the full body in order to
  85. %% count the content-length but we already have that. What we're
  86. %% missing is the [to-be-sent] body.
  87. HResponse = mochiweb_headers:make(Headers),
  88. Req:start_response({Code, HResponse}),
  89. %% WARNING: we're depending on the original ewgi_context here!!!!
  90. case ewgi_api:request_method(Ctx) of
  91. 'HEAD' ->
  92. GeneratorPid ! {discard, self()};
  93. _ ->
  94. GeneratorPid ! {ok, self()}
  95. end
  96. end,
  97. wait_for_streamcontent_pid(Socket, GeneratorPid)
  98. after Timeout ->
  99. Req:respond({504, [], <<"Gateway Timeout">>})
  100. end.
  101. %% Treat a stream with chunked transfer encoding
  102. handle_stream(R, Generator) when is_function(Generator, 0) ->
  103. case (catch Generator()) of
  104. {H, T} when is_function(T, 0) ->
  105. %% Prevent finishing the chunked response
  106. case H of
  107. <<>> -> ok;
  108. [] -> ok;
  109. _ ->
  110. R:write_chunk(H)
  111. end,
  112. handle_stream(R, T);
  113. {} ->
  114. R:write_chunk([]);
  115. Error ->
  116. error_logger:error_report(io_lib:format("Unexpected stream ouput (~p): ~p~n", [Generator, Error])),
  117. R:write_chunk([])
  118. end;
  119. handle_stream(R, Generator) ->
  120. error_logger:error_report(io_lib:format("Invalid stream generator: ~p~n", [Generator])),
  121. R:write_chunk([]).
  122. %% Copied/adapted from yaws_server
  123. wait_for_streamcontent_pid(CliSock, ContentPid) ->
  124. Ref = erlang:monitor(process, ContentPid),
  125. gen_tcp:controlling_process(CliSock, ContentPid),
  126. ContentPid ! {ok, self()},
  127. receive
  128. endofstreamcontent ->
  129. ok = gen_tcp:close(CliSock),
  130. erlang:demonitor(Ref),
  131. %% should just use demonitor [flush] option instead?
  132. receive
  133. {'DOWN', Ref, _, _, _} ->
  134. ok
  135. after 0 ->
  136. ok
  137. end;
  138. {'DOWN', Ref, _, _, _} ->
  139. ok
  140. end,
  141. done.
  142. %%--------------------------------------------------------------------
  143. %% Push Streams API - copied from yaws_api
  144. %% We could use mochiweb's write_chunk function but that
  145. %% would require that we copy MochiResp around instead of
  146. %% just copying the socket.
  147. %%--------------------------------------------------------------------
  148. %% This won't work for SSL for now
  149. stream_process_deliver(Sock, IoList) ->
  150. gen_tcp:send(Sock, IoList).
  151. %% This won't work for SSL for now either
  152. stream_process_deliver_chunk(Sock, IoList) ->
  153. Chunk = case erlang:iolist_size(IoList) of
  154. 0 ->
  155. stream_process_deliver_final_chunk(Sock, IoList);
  156. S ->
  157. [mochihex:to_hex(S), "\r\n", IoList, "\r\n"]
  158. end,
  159. gen_tcp:send(Sock, Chunk).
  160. stream_process_deliver_final_chunk(Sock, IoList) ->
  161. Chunk = case erlang:iolist_size(IoList) of
  162. 0 ->
  163. <<"0\r\n\r\n">>;
  164. S ->
  165. [mochihex:to_hex(S), "\r\n", IoList, "\r\n0\r\n\r\n"]
  166. end,
  167. gen_tcp:send(Sock, Chunk).
  168. stream_process_end(Sock, ServerPid) ->
  169. gen_tcp:controlling_process(Sock, ServerPid),
  170. ServerPid ! endofstreamcontent.
  171. %%--------------------------------------------------------------------
  172. process_application(Appl, Ctx) when is_list(Appl) ->
  173. Path = ewgi_api:path_info(Ctx),
  174. process_mount_application(Ctx, Path, find_mount(Appl, Path));
  175. process_application(Appl, Ctx) ->
  176. ewgi_application:run(Appl, Ctx).
  177. process_mount_application(_, _, {not_found, _}) ->
  178. not_found;
  179. process_mount_application(Ctx0, Path0, {MountPoint, Application}) ->
  180. Path = case Path0 of
  181. "*" -> "*";
  182. _ -> string:substr(Path0, length(MountPoint) + 1)
  183. end,
  184. Ctx = ewgi_api:path_info(Path, ewgi_api:script_name(MountPoint, Ctx0)),
  185. ewgi_application:run(Application, Ctx).
  186. find_mount([], _) ->
  187. {not_found, fun (_, _) -> not_found end};
  188. find_mount(Mounts, "*") ->
  189. lists:last(Mounts);
  190. find_mount([{Path, _}=M|_], Path) ->
  191. M;
  192. find_mount([{Point, _}=M|T], Path) ->
  193. case string:str(Path, Point ++ "/") of
  194. 1 ->
  195. M;
  196. _ ->
  197. find_mount(T, Path)
  198. end.
  199. %%--------------------------------------------------------------------
  200. %%% Internal functions
  201. %%--------------------------------------------------------------------
  202. parse_arg(Req) ->
  203. ewgi_api:server_request_foldl(Req, fun parse_element/2, fun parse_ewgi_element/2, fun parse_http_header_element/2).
  204. parse_element(auth_type, _Req) ->
  205. undefined;
  206. parse_element(content_length, Req) ->
  207. case Req:get_header_value("content-length") of
  208. undefined -> undefined;
  209. Length when is_integer(Length) ->
  210. Length;
  211. Length when is_list(Length) ->
  212. list_to_integer(Length)
  213. end;
  214. parse_element(content_type, Req) ->
  215. Req:get_header_value("content-type");
  216. parse_element(gateway_interface, _Req) ->
  217. "EWGI/1.0";
  218. parse_element(path_info, Req) ->
  219. RawPath = Req:get(raw_path),
  220. case RawPath of
  221. RawPath when RawPath =:= '*' ->
  222. "*";
  223. RawPath ->
  224. {_, _, Path, _, _} = mochiweb_util:urlsplit(RawPath),
  225. ewgi_api:unquote_path(Path)
  226. end;
  227. %% Used to be:
  228. %% filename:dirname(filename:dirname(code:which(Appl)))++Req:get(path); The
  229. %% problem here is that the application only has a function which acts as the
  230. %% entry point to the application.
  231. parse_element(path_translated, _Req) ->
  232. undefined;
  233. parse_element(query_string, Req) ->
  234. RawPath = Req:get(raw_path),
  235. case RawPath of
  236. RawPath when RawPath =:= '*' ->
  237. undefined;
  238. RawPath ->
  239. {_, _, _, QueryString, _} = mochiweb_util:urlsplit(RawPath),
  240. QueryString
  241. end;
  242. parse_element(remote_addr, Req) ->
  243. Req:get(peer);
  244. parse_element(remote_host, _Req) ->
  245. undefined;
  246. parse_element(remote_ident, _Req) ->
  247. undefined;
  248. parse_element(remote_user, _Req) ->
  249. undefined;
  250. parse_element(request_method, Req) ->
  251. Req:get(method);
  252. %% Default value is empty string. If mount points are used, SCRIPT_NAME
  253. %% becomes the mount point.
  254. parse_element(script_name, _Req) ->
  255. [];
  256. parse_element(server_name, Req) ->
  257. HostPort = Req:get_header_value(host),
  258. case HostPort of
  259. HostPort when is_list(HostPort) ->
  260. hd(string:tokens(HostPort, ":"));
  261. HostPort -> HostPort
  262. end;
  263. parse_element(server_port, Req) ->
  264. HostPort0 = Req:get_header_value(host),
  265. case HostPort0 of
  266. HostPort0 when is_list(HostPort0) ->
  267. HostPort = string:tokens(HostPort0, ":"),
  268. case length(HostPort) of
  269. 2 -> lists:nth(2, HostPort);
  270. _ -> undefined
  271. end;
  272. _ ->
  273. undefined
  274. end;
  275. parse_element(server_protocol, Req) ->
  276. {Maj, Min} = Req:get(version),
  277. lists:flatten(io_lib:format("HTTP/~b.~b", [Maj, Min]));
  278. parse_element(server_software, _Req) ->
  279. "MochiWeb";
  280. %% All other elements are undefined
  281. parse_element(_, _) ->
  282. undefined.
  283. parse_ewgi_element(read_input, Req) ->
  284. F = fun(Callback, Length) ->
  285. case Req:get_header_value("expect") of
  286. "100-continue" ->
  287. Req:start_raw_response({100, gb_trees:empty()});
  288. _Else ->
  289. ok
  290. end,
  291. read_input(Callback, Length, Req)
  292. end,
  293. F;
  294. parse_ewgi_element(write_error, Req) ->
  295. F = fun(Msg) ->
  296. write_error(Msg, Req)
  297. end,
  298. F;
  299. %% https?
  300. parse_ewgi_element(url_scheme, _Req) ->
  301. "http";
  302. parse_ewgi_element(version, _Req) ->
  303. {1, 0};
  304. parse_ewgi_element(data, _Req) ->
  305. gb_trees:empty();
  306. %% Ignore others
  307. parse_ewgi_element(_, _) ->
  308. undefined.
  309. parse_http_header_element(http_accept, Req) ->
  310. Req:get_header_value("accept");
  311. parse_http_header_element(http_cookie, Req) ->
  312. Req:get_header_value("cookie");
  313. parse_http_header_element(http_host, Req) ->
  314. Req:get_header_value("host");
  315. parse_http_header_element(http_if_modified_since, Req) ->
  316. Req:get_header_value("if-modified-since");
  317. parse_http_header_element(http_user_agent, Req) ->
  318. Req:get_header_value("user-agent");
  319. parse_http_header_element(http_x_http_method_override, Req) ->
  320. Req:get_header_value("x-http-method-override");
  321. parse_http_header_element(other, Req) ->
  322. lists:foldl(fun({K0, _}=Pair, Acc) ->
  323. {K, V} = ewgi_api:normalize_header(Pair),
  324. case K of
  325. K when K =:= "content-length"
  326. ; K =:= "content-type"
  327. ; K =:= "accept"
  328. ; K =:= "cookie"
  329. ; K =:= "host"
  330. ; K =:= "if-modified-since"
  331. ; K =:= "user-agent"
  332. ; K =:= "x-http-method-override" ->
  333. Acc;
  334. _ ->
  335. Ex = case gb_trees:lookup(K, Acc) of
  336. {value, L} ->
  337. L;
  338. none ->
  339. []
  340. end,
  341. gb_trees:insert(K, [{K0, V}|Ex], Acc)
  342. end
  343. end, gb_trees:empty(), mochiweb_headers:to_list(Req:get(headers)));
  344. parse_http_header_element(_, _) ->
  345. undefined.
  346. %% No chunk size specified, so use default
  347. read_input(Callback, Length, Req) when is_integer(Length) ->
  348. read_input(Callback, {Length, ?DEFAULT_CHUNKSIZE}, Req);
  349. %% Final callback after entire input has been read
  350. read_input(Callback, {Length, _ChunkSz}, _Req) when is_function(Callback), Length =< 0 ->
  351. Callback(eof);
  352. %% Continue reading and calling back with each chunk of data
  353. read_input(Callback, {Length, ChunkSz}, Req) when is_function(Callback) ->
  354. Bin = recv_input(Req, Length, ChunkSz),
  355. Rem = Length - size(Bin),
  356. NewCallback = Callback({data, Bin}),
  357. read_input(NewCallback, {Rem, ChunkSz}, Req).
  358. %% Read either Length bytes or ChunkSz, whichever is smaller
  359. recv_input(Req, Length, ChunkSz) when Length > 0, Length < ChunkSz ->
  360. Req:recv(Length);
  361. recv_input(Req, _, ChunkSz) ->
  362. Req:recv(ChunkSz).
  363. %% Write errors to error_logger
  364. write_error(Msg, Req) ->
  365. error_logger:error_report([{message, Msg}, {request, Req}]).