PageRenderTime 263ms CodeModel.GetById 100ms app.highlight 114ms RepoModel.GetById 37ms app.codeStats 5ms

/src/server_gateways/ewgi_inets.erl

http://github.com/skarab/ewgi
Erlang | 473 lines | 377 code | 58 blank | 38 comment | 3 complexity | e7e8fd51e0b508e096479106a4ae3c84 MD5 | raw file
  1%%%-------------------------------------------------------------------
  2%%% File    : ewgi_inets.erl
  3%%% Author  : Hunter Morris <huntermorris@gmail.com>
  4%%% License :
  5%%% The contents of this file are subject to the Mozilla Public
  6%%% License Version 1.1 (the "License"); you may not use this file
  7%%% except in compliance with the License. You may obtain a copy of
  8%%% the License at http://www.mozilla.org/MPL/
  9%%%
 10%%% Software distributed under the License is distributed on an "AS IS"
 11%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 12%%% the License for the specific language governing rights and
 13%%% limitations under the License.
 14%%% The Initial Developer of the Original Code is S.G. Consulting
 15%%% srl. Portions created by S.G. Consulting s.r.l. are Copyright (C)
 16%%% 2007 S.G. Consulting srl. All Rights Reserved.
 17%%%
 18%%% @doc 
 19%%% <p>Reference implementation of a inets EWGI server gateway.</p>
 20%%% <p>Requires that environment variables 'app_module' and
 21%%% 'app_function' are set for application 'ewgi'</p>
 22%%%
 23%%% @end
 24%%%-------------------------------------------------------------------
 25-module(ewgi_inets).
 26
 27-export([do/1]).
 28-export([
 29		stream_process_deliver/2,
 30		stream_process_deliver_chunk/2,
 31		stream_process_deliver_final_chunk/2,
 32		stream_process_end/2
 33	]).
 34
 35-ifdef(HAS_R14).
 36-include_lib("inets/src/http_server/httpd.hrl").
 37-else.
 38-include_lib("inets/src/httpd.hrl").
 39-endif.
 40-include_lib("ewgi.hrl").
 41
 42do(A) ->
 43    try parse_arg(A) of
 44        Req when ?IS_EWGI_REQUEST(Req) ->
 45            try process_application(ewgi_api:context(Req, ewgi_api:empty_response())) of
 46                not_found ->
 47                    {proceed, [{response, {404, []}}]};
 48                Ctx when ?IS_EWGI_CONTEXT(Ctx) ->
 49                    handle_result(A, ?INSPECT_EWGI_RESPONSE(Ctx))
 50            catch
 51                _:Reason ->
 52                    error_logger:error_report(io_lib:format("Responding with 500 INTERNAL SERVER ERROR.~nReason: ~p~nStack: ~p~n", [Reason, erlang:get_stacktrace()])),
 53                    {break, [{response, {500, []}}]}
 54            end
 55    catch
 56        _:Reason ->
 57            error_logger:error_report(io_lib:format("Responding with 400 BAD REQUEST.~nReason: ~p~nStack: ~p~n", [Reason, erlang:get_stacktrace()])),
 58            {break, [{response, {400, []}}]}
 59    end.
 60
 61process_application(Ctx) ->
 62    M = case application:get_env(ewgi, app_module) of
 63            {ok, Mod} -> Mod;
 64            _ -> throw({error, "ewgi app_module environment variable not set"})
 65        end,
 66    F = case application:get_env(ewgi, app_function) of
 67            {ok, Fun} -> Fun;
 68            _ -> throw({error, "ewgi app_function environment variable not set"})
 69        end,
 70    Appl = fun(A) -> apply(M, F, [A]) end,
 71    ewgi_application:run(Appl, Ctx).
 72
 73parse_arg(A) when is_record(A, mod) ->
 74    ewgi_api:server_request_foldl(A, fun parse_element/2, fun parse_ewgi_element/2, fun parse_http_header_element/2).
 75
 76parse_element(auth_type, _Req) ->
 77    undefined;
 78
 79parse_element(content_length, #mod{parsed_header=H}) ->
 80    case proplists:get_value("content-length", H) of
 81        undefined -> undefined;
 82        Length when is_integer(Length) ->
 83            Length;
 84        Length when is_list(Length) ->
 85            list_to_integer(Length)
 86    end;
 87
 88parse_element(content_type, #mod{parsed_header=H}) ->
 89    proplists:get_value("content-type", H);
 90
 91parse_element(gateway_interface, _Req) ->
 92    "EWGI/1.0";
 93
 94parse_element(path_info, #mod{request_uri=U}) ->
 95    {_, _, Path, _, _} = ewgi_api:urlsplit(U),
 96    ewgi_api:unquote_path(Path);
 97
 98parse_element(path_translated, _Req) ->
 99    undefined;
100
101parse_element(query_string, #mod{request_uri=U}) ->
102    {_, _, _, QueryString, _} = ewgi_api:urlsplit(U),
103    QueryString;
104
105parse_element(remote_addr, #mod{socket_type={ssl, _}, socket=S}) ->
106    get_ip(ssl:peername(S));
107parse_element(remote_addr, #mod{socket=S}) ->
108    get_ip(inet:peername(S));
109
110parse_element(remote_host, _Req) ->
111    undefined;
112
113parse_element(remote_ident, _Req) ->
114    undefined;
115
116parse_element(remote_user, _Req) ->
117    undefined;
118
119parse_element(request_method, #mod{method=M}) ->
120    get_method(M);
121
122parse_element(script_name, _Req) ->
123    [];
124
125parse_element(server_name, #mod{parsed_header=H}) ->
126    case proplists:get_value("host", H) of
127        HostPort when is_list(HostPort) ->
128            hd(string:tokens(HostPort, ":"));
129        HostPort ->
130            HostPort
131    end;
132
133parse_element(server_port, #mod{parsed_header=H}) ->
134    case proplists:get_value("host", H) of
135        Host when is_list(Host) ->
136            case string:tokens(Host, ":") of
137                [_, Port] ->
138                    Port;
139                _ ->
140                    undefined
141            end;
142        _ ->
143            undefined
144    end;
145
146parse_element(server_protocol, #mod{http_version=V}) ->
147    V;
148
149parse_element(server_software, _Req) ->
150    "inets";
151
152%% All other elements are undefined
153parse_element(_, _) ->
154    undefined.
155
156parse_ewgi_element(read_input, #mod{entity_body=Buf}) ->
157    F = fun(Callback, Length) when is_integer(Length) -> % No chunk size specified, so use default
158                read_input(Callback, {Length, ?DEFAULT_CHUNKSIZE}, list_to_binary(Buf));
159           (Callback, {Length, ChunkSz}) ->
160                read_input(Callback, {Length, ChunkSz}, list_to_binary(Buf))
161        end,
162    F;
163
164parse_ewgi_element(write_error, Req) ->
165    F = fun(Msg) ->
166                error_logger:error_report([{message, Msg}, {request, Req}])
167        end,
168    F;
169
170parse_ewgi_element(url_scheme, #mod{socket_type={ssl, _}}) ->
171    "https";
172parse_ewgi_element(url_scheme, _) ->
173    "http";
174
175parse_ewgi_element(version, _) ->
176    {1, 0};
177
178parse_ewgi_element(data, A) ->
179    gb_trees:from_orddict([{"inets.mod", A}]);
180
181parse_ewgi_element(_, _) ->
182    undefined.
183
184get_header(K, #mod{parsed_header=H}) ->
185    proplists:get_value(K, H).
186
187parse_http_header_element(http_accept, A) ->
188    get_header("accept", A);
189
190parse_http_header_element(http_cookie, A) ->
191    get_header("cookie", A);
192
193parse_http_header_element(http_host, A) ->
194    get_header("host", A);
195
196parse_http_header_element(http_if_modified_since, A) ->
197    get_header("if-modified-since", A);
198
199parse_http_header_element(http_user_agent, A) ->
200    get_header("user-agent", A);
201
202parse_http_header_element(http_x_http_method_override, A) ->
203    get_header("x-http-method-override", A);
204
205parse_http_header_element(other, #mod{parsed_header=H}) ->
206    lists:foldl(fun parse_other_header/2, gb_trees:empty(), H);
207
208parse_http_header_element(_, _) ->
209    undefined.
210
211parse_other_header({K0, _}=Pair, Acc) ->
212    parse_other_header1(K0, ewgi_api:normalize_header(Pair), Acc).
213
214parse_other_header1(_, {"content-length", _}, Acc) ->
215    Acc;
216parse_other_header1(_, {"content-type", _}, Acc) ->
217    Acc;
218parse_other_header1(_, {"accept", _}, Acc) ->
219    Acc;
220parse_other_header1(_, {"cookie", _}, Acc) ->
221    Acc;
222parse_other_header1(_, {"host", _}, Acc) ->
223    Acc;
224parse_other_header1(_, {"if-modified-since", _}, Acc) ->
225    Acc;
226parse_other_header1(_, {"user-agent", _}, Acc) ->
227    Acc;
228parse_other_header1(_, {"x-http-method-override", _}, Acc) ->
229    Acc;
230parse_other_header1(K0, {K, V}, Acc) ->
231    Ex = case gb_trees:lookup(K, Acc) of
232             {value, L} -> L;
233             none ->       []
234         end,
235    gb_trees:enter(K, [{K0, V}|Ex], Acc).
236
237handle_result(#mod{config_db=Db}=A, Ctx) ->
238	case ewgi_api:response_message_body(Ctx) of
239		{push_stream, GeneratorPid, Timeout} when is_pid(GeneratorPid) ->
240			ChunkedAllowed = not httpd_response:is_disable_chunked_send(Db),
241			handle_push_stream(A, Ctx, ChunkedAllowed, GeneratorPid, Timeout);
242		Body0 ->
243			{Code, _} = ewgi_api:response_status(Ctx),
244			Headers0 = [{string:to_lower(H), binary_to_list(iolist_to_binary(V))} || {H, V} <- ewgi_api:response_headers(Ctx)],
245			Headers = lists:foldl(fun fold_header/2, [], Headers0),
246			case Body0 of
247				Body when is_function(Body, 0) ->
248					ChunkedAllowed = not httpd_response:is_disable_chunked_send(Db),
249					handle_result_wrap_stream(A, ChunkedAllowed, Code, Headers, Body);
250				_ ->
251					Body = [Body0],
252				Length = {content_length, integer_to_list(erlang:iolist_size(Body))},
253					{proceed, [{response, {response, [{code, Code}, Length] ++ Headers, Body}}]}
254			end
255	end.
256
257handle_result_wrap_stream(#mod{http_version=Ver}, ChunkedAllowed, Code, Headers, Body0)
258  when (Ver =/= "HTTP/1.1") or (not ChunkedAllowed) ->
259    Body = stream_to_list(Body0),
260    Length = {content_length, integer_to_list(erlang:iolist_size(Body))},
261    {proceed, [{response, {response, [{code, Code}, Length] ++ Headers, Body}}]};
262handle_result_wrap_stream(A, true, Code, Headers, Body) ->
263    ExtraHeaders = httpd_response:cache_headers(A),
264    httpd_response:send_header(A, Code, ExtraHeaders ++ [{transfer_encoding, "chunked"}|Headers]),
265    handle_stream(A, Code, Body, 0).
266
267handle_stream(A, Code, Generator, Size) when is_function(Generator, 0) ->
268    case (catch Generator()) of
269        {H, T} when is_function(T, 0) ->
270	    case H of
271		<<>> -> ok;
272		[] -> ok;
273		_ ->
274		    httpd_response:send_chunk(A, [H], false)
275	    end,
276            handle_stream(A, Code, T, Size + erlang:iolist_size([H]));
277        {} ->
278            httpd_response:send_final_chunk(A, false),
279	    {proceed, [{response, {already_sent, Code, Size}}]};
280	Error ->
281	    error_logger:error_report(io_lib:format("Unexpected stream ouput (~p): ~p~n", [Generator, Error])),
282            httpd_response:send_final_chunk(A, false)
283    end;
284handle_stream(A, _Code, Generator, _Size) ->
285    error_logger:error_report(io_lib:format("Invalid stream generator: ~p~n", [Generator])),
286    httpd_response:send_final_chunk(A, false).
287
288stream_to_list(S) when is_function(S, 0) ->
289    case S() of
290        {H, T} -> [H|stream_to_list(T)];
291        {} ->     []
292    end.
293
294handle_push_stream(#mod{http_version=Ver}, _Ctx, ChunkedAllowed, GeneratorPid, _Timeout)
295  when (Ver =/= "HTTP/1.1") or (not ChunkedAllowed) ->
296	GeneratorPid ! {discard, self()},
297    Body = "HTTP/1.1 required for streaming live data!",
298    Length = {content_length, integer_to_list(erlang:iolist_size(Body))},
299	NotSupported = 505,
300    {proceed, [{response, {response, [{code, NotSupported}, Length], Body}}]};
301handle_push_stream(A, Ctx, true, GeneratorPid, Timeout) ->
302	Socket = A#mod.socket,
303	GeneratorPid ! {push_stream_init, ?MODULE, self(), Socket},
304	receive
305	{push_stream_init, GeneratorPid, Code, Headers0, TransferEncoding} ->
306		Headers1 = [{string:to_lower(H), binary_to_list(iolist_to_binary(V))} || {H, V} <- Headers0],
307		Headers = lists:foldl(fun fold_header/2, [], Headers1),
308		ExtraHeaders = httpd_response:cache_headers(A),
309		httpd_response:send_header(A, Code, ExtraHeaders ++ Headers),
310		case TransferEncoding of
311			chunked ->
312				GeneratorPid ! {ok, self()}
313			;_ ->
314				%% WARNING: we're depending on the original ewgi_context here!!!!
315				case ewgi_api:request_method(Ctx) of
316					'HEAD' ->
317						GeneratorPid ! {discard, self()};
318					_ ->
319						GeneratorPid ! {ok, self()}
320				end	
321		end,
322		Socket = A#mod.socket,
323		wait_for_streamcontent_pid(Socket, GeneratorPid)
324	after Timeout ->
325		Body = "Gateway Timeout",
326		Length = {content_length, integer_to_list(erlang:iolist_size(Body))},
327		GatewayTimeout = 504,
328		{proceed, [{response, {response, [{code, GatewayTimeout}, Length], Body}}]}
329	end.
330
331%% Copied/adapted from yaws_server
332wait_for_streamcontent_pid(CliSock, ContentPid) ->
333    Ref = erlang:monitor(process, ContentPid),
334    gen_tcp:controlling_process(CliSock, ContentPid),
335    ContentPid ! {ok, self()},
336    receive
337        endofstreamcontent ->
338	    ok = gen_tcp:close(CliSock),
339            erlang:demonitor(Ref),
340            %% should just use demonitor [flush] option instead?
341            receive
342                {'DOWN', Ref, _, _, _} ->
343                    ok
344            after 0 ->
345                    ok
346            end;
347        {'DOWN', Ref, _, _, _} ->
348            ok
349    end,
350    done.
351
352%%--------------------------------------------------------------------
353%% Push Streams API - copied from yaws_api
354%%--------------------------------------------------------------------
355
356%% This won't work for SSL for now
357stream_process_deliver(Sock, IoList) ->
358    gen_tcp:send(Sock, IoList).
359
360%% This won't work for SSL for now either
361stream_process_deliver_chunk(Sock, IoList) ->
362    Chunk = case erlang:iolist_size(IoList) of
363                0 ->
364                    stream_process_deliver_final_chunk(Sock, IoList);
365                S ->
366                    [http_util:integer_to_hexlist(S), "\r\n", IoList, "\r\n"]
367            end,
368    gen_tcp:send(Sock, Chunk).
369stream_process_deliver_final_chunk(Sock, IoList) ->
370    Chunk = case erlang:iolist_size(IoList) of
371                0 ->
372                    <<"0\r\n\r\n">>;
373                S ->
374                    [http_util:integer_to_hexlist(S), "\r\n", IoList, "\r\n0\r\n\r\n"]
375            end,
376    gen_tcp:send(Sock, Chunk).
377
378stream_process_end(Sock, ServerPid) ->
379    gen_tcp:controlling_process(Sock, ServerPid),
380    ServerPid ! endofstreamcontent.
381
382%%--------------------------------------------------------------------
383
384fold_header({"accept-ranges", V}, Acc) ->
385    [{accept_ranges, V}|Acc];
386fold_header({"allow", V}, Acc) ->
387    [{allow, V}|Acc];
388fold_header({"cache-control", V}, Acc) ->
389    [{cache_control, V}|Acc];
390fold_header({"content-md5", V}, Acc) ->
391    [{content_MD5, V}|Acc];
392fold_header({"content-encoding", V}, Acc) ->
393    [{content_encoding, V}|Acc];
394fold_header({"content-language", V}, Acc) ->
395    [{content_language, V}|Acc];
396fold_header({"content-length", V}, Acc) ->
397    [{content_length, V}|Acc];
398fold_header({"content-location", V}, Acc) ->
399    [{content_location, V}|Acc];
400fold_header({"content-range", V}, Acc) ->
401    [{content_range, V}|Acc];
402fold_header({"content-type", V}, Acc) ->
403    [{content_type, V}|Acc];
404fold_header({"date", V}, Acc) ->
405    [{date, V}|Acc];
406fold_header({"etag", V}, Acc) ->
407    [{etag, V}|Acc];
408fold_header({"expires", V}, Acc) ->
409    [{expires, V}|Acc];
410fold_header({"last-modified", V}, Acc) ->
411    [{last_modified, V}|Acc];
412fold_header({"location", V}, Acc) ->
413    [{location, V}|Acc];
414fold_header({"pragma", V}, Acc) ->
415    [{pragma, V}|Acc];
416fold_header({"retry-after", V}, Acc) ->
417    [{retry_after, V}|Acc];
418fold_header({"server", V}, Acc) ->
419    [{server, V}|Acc];
420fold_header({"trailer", V}, Acc) ->
421    [{trailer, V}|Acc];
422fold_header({"transfer-encoding", V}, Acc) ->
423    [{transfer_encoding, V}|Acc];
424fold_header({"set-cookie", V}, Acc) ->
425    [{"set-cookie", V}|Acc];
426fold_header({"www-authenticate", V}, Acc) ->
427    [{"www-authenticate", V}|Acc];
428fold_header(_, Acc) ->
429    %% Ignore unrecognised headers
430    Acc.
431
432get_ip({ok, {Ip, _Port}}) ->
433    inet_parse:ntoa(Ip);
434get_ip(_) ->
435    undefined.
436
437get_method("OPTIONS") ->
438    'OPTIONS';
439get_method("GET") ->
440    'GET';
441get_method("HEAD") ->
442    'HEAD';
443get_method("POST") ->
444    'POST';
445get_method("PUT") ->
446    'PUT';
447get_method("DELETE") ->
448    'DELETE';
449get_method("TRACE") ->
450    'TRACE';
451get_method("CONNECT") ->
452    'CONNECT';
453get_method(Other) ->
454    Other.
455
456%% Final callback after entire input has been read
457read_input(Callback, {Length, _ChunkSz}, _Left) when is_function(Callback), Length =< 0 ->
458    Callback(eof);
459
460%% Continue reading and calling back with each chunk of data
461read_input(Callback, {Length, ChunkSz}, Left) when is_function(Callback), is_binary(Left) ->
462    L = recv_size(Length, ChunkSz),
463    <<Bin:L/bytes,Rest/bits>> = Left,
464    Rem = Length - size(Bin),
465    NewCallback = Callback({data, Bin}),
466    read_input(NewCallback, {Rem, ChunkSz}, Rest).
467
468%% Read either Length bytes or ChunkSz, whichever is smaller
469recv_size(Length, ChunkSz) when Length > 0, Length < ChunkSz ->
470    Length;
471recv_size(_, ChunkSz) ->
472    ChunkSz.
473