%%% BEGIN openflax/http/interface.erl %%%
%%%
%%% openflax - Open Source web server for Erlang/OTP
%%% Copyright (c)2004 Cat's Eye Technologies. All rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions
%%% are met:
%%%
%%% Redistributions of source code must retain the above copyright
%%% notice, this list of conditions and the following disclaimer.
%%%
%%% Redistributions in binary form must reproduce the above copyright
%%% notice, this list of conditions and the following disclaimer in
%%% the documentation and/or other materials provided with the
%%% distribution.
%%%
%%% Neither the name of Cat's Eye Technologies nor the names of its
%%% contributors may be used to endorse or promote products derived
%%% from this software without specific prior written permission.
%%%
%%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
%%% CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
%%% INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
%%% DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
%%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
%%% OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
%%% PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
%%% OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
%%% ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
%%% OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
%%% OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
%%% POSSIBILITY OF SUCH DAMAGE.
%% @doc The HTTP service for Openflax.
%%
%%
This module implements a middleman process which takes ownership
%% of the TCP/IP socket and does the low-level translation of the HTTP
%% protocol, so that the other parts of OpenFlax need not deal with
%% moving data to and from the socket itself. It uses the function
%% library module openflax.http.parse to do the HTTP
%% parsing, and is intended to be used only from the modules
%% openflax.http.request and
%% openflax.http.response.
%%
%% @end
-module(openflax.http.interface).
-vsn('$Id: interface.erl 31 2004-04-23 07:00:11Z catseye $').
-author('catseye@catseye.mb.ca').
-copyright('Copyright (c)2004 Cat`s Eye Technologies. All rights reserved.').
-export([start/4]).
-import(lists).
-import(gen_tcp).
%%% -------------------------- ENTRY POINT ------------------------ %%%
%% @spec start(fun(), interface(), port(), maxconn()) -> never_returns
%% @doc Creates a new HTTP service on the given port with the given
%% maximum number of simultaneous connections.
start(Fun, Interface, Port, MaxConn) ->
IfaceOpts = case Interface of
undefined ->
[];
String ->
[{ip, openflax.tcp:get_iface_ip_addr(Interface)}]
end,
openflax.tcp:server(fun(Socket, RemoteAddress) ->
Http = self(),
Pid = spawn_link(fun() -> Fun(Http, RemoteAddress) end),
server(Socket, Pid)
end, Port,
[list,
{reuseaddr, true},
{nodelay, true},
{active, false},
{packet, raw},
{recbuf, 8192},
{sndbuf, 8192}
] ++ IfaceOpts, MaxConn).
%% server(socket(), pid()) -> ok
server(Socket, Pid) ->
case catch get_request_line(Socket, Pid, "", "") of
{'EXIT', Reason} ->
deliver_error(Pid, bad_request, Reason);
ok ->
send_response(Socket, Pid)
end.
%%% -------------------------- ENTRY POINT ------------------------ %%%
% In the following functions,
% S means Socket
% P means Parent (pid)
% B means Buffer
% H means Head (of buffer)
% T means Tail (of buffer)
% A means Accumulator
% Z means Size (expected size of body)
% Y means body type (plain, chunked, or multipart)
% C means chunk-accumulator
% U means boundary (for multipart)
% E means ender (for multipart)
get_request_line(S, P, "", A) ->
get_request_line(S, P, replenish(S), A);
get_request_line(S, P, [10 | T], A) ->
deliver_request_line(P, A),
get_header(S, P, T, "", undefined, undefined);
get_request_line(S, P, [13, 10 | T], A) ->
deliver_request_line(P, A),
get_header(S, P, T, "", undefined, undefined);
get_request_line(S, P, [H | T], A) ->
get_request_line(S, P, T, [H | A]).
get_header(S, P, "", A, Z, Y) ->
get_header(S, P, replenish(S), A, Z, Y);
get_header(S, P, [13, 10, WS | T], A, Z, Y) when WS == 9; WS == 32 ->
get_header(S, P, T, [32 | A], Z, Y);
get_header(S, P, [13, 10, 13, 10 | T], A, Z, Y) ->
{Z0, Y0} = deliver_header(P, A, Z, Y),
P ! {self(), request_header_end, []},
get_body(S, P, T, [], Z0, Y0);
get_header(S, P, D=[13, 10], A, Z, Y) ->
get_header(S, P, D ++ replenish(S), A, Z, Y);
get_header(S, P, D=[13, 10, 13], A, Z, Y) ->
get_header(S, P, D ++ replenish(S), A, Z, Y);
get_header(S, P, [13, 10 | T], A, Z, Y) ->
{Z0, Y0} = deliver_header(P, A, Z, Y),
get_header(S, P, T, "", Z0, Y0);
get_header(S, P, [H | T], A, Z, Y) ->
get_header(S, P, T, [H | A], Z, Y).
get_body(S, P, B, A, Z, undefined) ->
% deliver_error(P, bad_request, unknown_body_encoding), ok;
get_body(S, P, B, A, Z, plain);
get_body(S, P, B, A, undefined, plain) ->
% deliver_error(P, bad_request, missing_size), ok;
get_body(S, P, B, A, 0, plain);
get_body(S, P, B, A, Z, plain) ->
get_plain_body(S, P, B, A, Z);
get_body(S, P, B, A, _Z, chunked) ->
get_chunked_body(S, P, B, A, []);
get_body(S, P, B, A, Z, {multipart, Boundary}) ->
get_multipart_body(S, P, B, A, Z, Boundary).
get_plain_body(_S, P, _B, _A, undefined) ->
deliver_plain_body(P, ""),
ok;
get_plain_body(_S, P, _B, A, 0) ->
deliver_plain_body(P, A),
ok;
get_plain_body(S, P, "", A, Z) ->
get_plain_body(S, P, replenish(S), A, Z);
get_plain_body(S, P, [H | T], A, Z) ->
get_plain_body(S, P, T, [H | A], Z-1).
get_chunked_body(S, P, "", A, C) ->
get_chunked_body(S, P, replenish(S), A, C);
get_chunked_body(S, P, [10 | T], A, C) ->
case get_chunk_size(A) of
0 ->
deliver_plain_body(P, lists:flatten(lists:reverse(A))),
ok;
ChunkSize ->
{ok, Chunk} = gen_tcp:recv(S, ChunkSize - length(T), 60000),
get_chunked_body(S, P, "", A, [T ++ Chunk | C])
end;
get_chunked_body(S, P, [H | T], A, C) ->
get_chunked_body(S, P, T, [H | A], C).
get_chunk_size(A) ->
A0 = string:strip(openflax.string:chomp(lists:reverse(A))),
[A1 | _] = string:tokens(A0, " "),
openflax.string:decode_hex(A1).
get_multipart_body(S, P, B, A, Z, U) ->
% openflax.app:debug(getting_multipart, U),
get_multipart_boundary(S, P, B, A, Z, "--" ++ U, "--" ++ U ++ "--").
get_multipart_boundary(S, P, "", A, Z, U, E) ->
get_multipart_boundary(S, P, replenish(S), A, Z, U, E);
get_multipart_boundary(S, P, [13, 10 | T], A, Z, U, E) ->
% openflax.app:debug(skinny_puppy, {lists:reverse(A), U, E}),
case lists:reverse(A) of
U ->
get_multipart_header(S, P, T, "", Z, U, E)
end;
get_multipart_boundary(S, P, [H | T], A, Z, U, E) ->
get_multipart_boundary(S, P, T, [H | A], Z, U, E).
get_multipart_header(S, P, "", A, Z, U, E) ->
get_multipart_header(S, P, replenish(S), A, Z, U, E);
get_multipart_header(S, P, [13, 10 | T], A, Z, U, E) ->
case lists:reverse(A) of
"" ->
get_multipart_content(S, P, T, "", Z, U, E);
Line ->
P ! {self(), multipart_header, openflax.http.parse:header_field(Line)},
get_multipart_header(S, P, T, "", Z, U, E)
end;
get_multipart_header(S, P, [H | T], A, Z, U, E) ->
get_multipart_header(S, P, T, [H | A], Z, U, E).
get_multipart_content(S, P, "", A, Z, U, E) ->
% get_multipart_content(S, P, replenish(S), A, Z, U, E);
P ! {self(), multipart_chunk, lists:reverse(A)},
get_multipart_content(S, P, replenish(S), "", Z, U, E);
get_multipart_content(S, P, [13], A, Z, U, E) ->
get_multipart_content(S, P, [13] ++ replenish(S), A, Z, U, E);
get_multipart_content(S, P, [13, 10 | T], A, Z, U, E) ->
% openflax.app:debug(fat_albert, {lists:reverse(A), U, E}),
case lists:reverse(A) of
U ->
get_multipart_header(S, P, T, "", Z, U, E);
E ->
% openflax.app:debug(multipart_ended, ok),
P ! {self(), multipart_end, ok},
ok;
Else ->
P ! {self(), multipart_chunk, Else},
get_multipart_content(S, P, T, "", Z, U, E)
end;
get_multipart_content(S, P, [H | T], A, Z, U, E) ->
get_multipart_content(S, P, T, [H | A], Z, U, E).
%%% ------------------------ RESPONSE GENERATOR --------------------- %%%
send_response(Socket, Pid) ->
receive
{Pid, send_response_line, Response} ->
gen_tcp:send(Socket, ["HTTP/1.1 ", Response, "\r\n"]),
% openflax.app:debug(responsing, {Response, Result}),
send_response(Socket, Pid);
{Pid, send_headers, List} ->
% openflax.app:debug(responsing, List),
lists:foreach(fun({Key, Value}) ->
case catch atom_to_list(Key) of
"res_" ++ Tail ->
gen_tcp:send(Socket,
[openflax.string:to_header(Tail), ": ",
openflax.string:from_term(Value), "\r\n"]);
_ ->
ok
end
end, List),
gen_tcp:send(Socket, "\r\n"),
send_response(Socket, Pid);
{Pid, send_data, Data} ->
% openflax.app:debug(responsing, Data),
gen_tcp:send(Socket, Data),
send_response(Socket, Pid);
{Pid, start_stream, {HandlerPid, _Size}} ->
stream_loop(Socket, HandlerPid),
Pid ! {self(), end_stream},
send_response(Socket, Pid);
{Pid, close} ->
% openflax.app:debug(closing, Socket),
gen_tcp:close(Socket)
end.
%% @spec stream_loop(socket(), pid()) -> ok
%% @doc Handles a response streamed through messages.
stream_loop(Socket, Pid) ->
receive
{Pid, stream_data, Data} ->
gen_tcp:send(Socket, Data),
Pid ! {self(), stream_acknowledge, data},
% Pid ! {self(), stream_error, Error}
stream_loop(Socket, Pid);
{Pid, stream_close} ->
Pid ! {self(), stream_acknowledge, close},
ok
end.
%%% --------------------------- UTILITIES ------------------------ %%%
replenish(Socket) ->
{ok, Buffer} = gen_tcp:recv(Socket, 0, 60000), Buffer.
deliver_request_line(Pid, Line) ->
% openflax.app:debug(delivering, {Pid, Line}),
Pid ! {self(), request_line,
openflax.http.parse:request_line(lists:reverse(Line))}.
deliver_header(Pid, Header, Size, BodyType) ->
R = (catch deliver_header0(Pid, Header, Size, BodyType)),
% openflax.app:debug(delhdr, R),
R.
deliver_header0(Pid, Header, Size, BodyType) ->
{Key, Value} = openflax.http.parse:header_field(lists:reverse(Header)),
Size0 = case Key of
req_content_length ->
list_to_integer(Value);
_ ->
Size
end,
BodyType0 = case {Key, Value} of
{req_transfer_encoding, ""} ->
plain;
{req_transfer_encoding, "chunked"} ->
chunked; % TODO: ignore (delete!) content length
{req_content_type, _} ->
case get_multipart_boundary(Value) of
{ok, Boundary} ->
{multipart, Boundary};
_ ->
BodyType
end;
_ ->
BodyType
end,
% openflax.app:debug(delivering_rq_h, {Key, Value}),
Pid ! {self(), request_header, {Key, Value}},
{Size0, BodyType0}.
deliver_plain_body(Pid, Body) ->
% openflax.app:debug(delivering, {Body}),
Pid ! {self(), request_body, openflax.http.parse:body(lists:reverse(Body))}.
deliver_error(Pid, Error, Reason) ->
% openflax.app:debug(delivering, {Error, Reason}),
Pid ! {self(), {error, {Error, Reason}}}.
get_multipart_boundary(Value) when is_list(Value), is_tuple(hd(Value)) ->
{String, _} = hd(Value),
case openflax.string:to_caps(String) of
"Multipart/Form-Data" ++ _ ->
case lists:keysearch("boundary", 1, Value) of
{value, {"boundary", Boundary}} ->
{ok, Boundary};
_ ->
error
end;
_ ->
error
end;
get_multipart_boundary(_Value) ->
error.
%%% END of openflax/http/interface.erl %%%