PageRenderTime 319ms CodeModel.GetById 100ms app.highlight 128ms RepoModel.GetById 86ms app.codeStats 0ms

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