%% Copyright (C) 2003 Joakim Grebenö <jocke@gleipnir.com>.
%% All rights reserved.
%%
%% Redistribution and use in source and binary forms, with or without
%% modification, are permitted provided that the following conditions
%% are met: 
%%
%% 1. Redistributions of source code must retain the above copyright
%%    notice, this list of conditions and the following disclaimer. 
%% 2. 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.
%%
%% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.

-module(xmlrpc_http).
-author('jocke@gleipnir.com').
-export([handler/4]).

-include("log.hrl").

-include("xmlrpc.hrl").

%% Exported: handler/4

handler(Socket, Timeout, Handler, State) ->
    case parse_request(Socket, Timeout) of
	{ok, Header} ->
	    ?DEBUG_LOG({header, Header}),
	    handle_payload(Socket, Timeout, Handler, State, Header);
	{status, StatusCode} ->
	    send(Socket, StatusCode),
	    handler(Socket, Timeout, Handler, State);
	{error, Reason} -> {error, Reason}
    end.

parse_request(Socket, Timeout) ->
    inet:setopts(Socket, [{packet, line}]),
    case gen_tcp:recv(Socket, 0, Timeout) of
	{ok, RequestLine} ->
	    case string:tokens(RequestLine, " \r\n") of
		["POST", _, "HTTP/1.0"] ->
		    ?DEBUG_LOG({http_version, "1.0"}),
		    parse_header(Socket, Timeout, #header{connection = close});
		["POST", _, "HTTP/1.1"] ->
		    ?DEBUG_LOG({http_version, "1.1"}),
		    parse_header(Socket, Timeout);
		[_Method, _, "HTTP/1.1"] -> {status, 501};
		["POST", _, _HTTPVersion] -> {status, 505};
		_ -> {status, 400}
	    end;
	{error, Reason} -> {error, Reason}
    end.

parse_header(Socket, Timeout) -> parse_header(Socket, Timeout, #header{}).

parse_header(Socket, Timeout, Header) ->
    case gen_tcp:recv(Socket, 0, Timeout) of
	{ok, "\r\n"} when Header#header.content_length == undefined ->
	    {status, 411};
	{ok, "\r\n"} when Header#header.content_type == undefined ->
	    {status, 400};
	{ok, "\r\n"} when Header#header.user_agent == undefined ->
	    {status, 400};
	{ok, "\r\n"} -> {ok, Header};
	{ok, HeaderField} ->
	    case split_header_field(HeaderField) of
		{"content-length:", ContentLength} ->
		    case catch list_to_integer(ContentLength) of
                        {'EXIT', {badarg,_}} -> {status, 400};
			N ->
			    parse_header(Socket, Timeout,
					 Header#header{content_length = N})
			  end;
		{"content-type:", "text/xml"} ->
		    parse_header(Socket, Timeout,
				 Header#header{content_type = "text/xml"});
		{"content-type:", "text/xml; charset=utf-8"} ->
		    parse_header(Socket, Timeout,
				 Header#header{content_type = "text/xml; charset=utf-8"});
		{"content-type:", _ContentType} -> {status, 415};
		{"user-agent:", UserAgent} ->
		    parse_header(Socket, Timeout,
				 Header#header{user_agent = UserAgent});
		{"connection:", "close"} ->
		    parse_header(Socket, Timeout,
				 Header#header{connection = close});
		{"connection:", [_,$e,$e,$p,$-,_,$l,$i,$v,$e]} ->
		    parse_header(Socket, Timeout,
				 Header#header{connection = undefined});
		{"authorization:", Authorization} ->
		    parse_header(Socket, Timeout,
				 Header#header{authorization = Authorization});
		{"x-forwarded-for:", XForwardedFor} ->
		    parse_header(Socket, Timeout,
				 Header#header{xforwardedfor = XForwardedFor});
		{"cookie:", Cookie} ->
			Cookies = [ Cookie | Header#header.cookies ],
		    parse_header(Socket, Timeout,
				 Header#header{cookies = Cookies});
		_ ->
		    ?DEBUG_LOG({skipped_header, HeaderField}),
		    parse_header(Socket, Timeout, Header)
	    end;
	{error, Reason} -> {error, Reason}
    end.

split_header_field(HeaderField) -> split_header_field(HeaderField, []).

split_header_field([], Name) -> {Name, ""};
split_header_field([$ |Rest], Name) -> {string:to_lower(lists:reverse(Name)), Rest -- "\r\n"};
split_header_field([C|Rest], Name) -> split_header_field(Rest, [C|Name]).

handle_payload(Socket, Timeout, Handler, State,
	       #header{connection = Connection} = Header) ->
    case get_payload(Socket, Timeout, Header#header.content_length) of
	{ok, Payload} ->
	    ?DEBUG_LOG({encoded_call, Payload}),
	    case xmlrpc_decode:payload(Payload) of
		{ok, DecodedPayload} ->
		    ?DEBUG_LOG({decoded_call, DecodedPayload}),
		    eval_payload(Socket, Timeout, Handler, State, Connection,
				 DecodedPayload, Header);
		{error, Reason} when Connection == close ->
		    ?ERROR_LOG({xmlrpc_decode, payload, Payload, Reason}),
		    send(Socket, 400);
		{error, Reason} ->
		    ?ERROR_LOG({xmlrpc_decode, payload, Payload, Reason}),
		    send(Socket, 400),
		    handler(Socket, Timeout, Handler, State)
	    end;
	{error, Reason} -> {error, Reason}
    end.

get_payload(Socket, Timeout, ContentLength) ->
    inet:setopts(Socket, [{packet, raw}]),
    gen_tcp:recv(Socket, ContentLength, Timeout).

%% Check whether module has defined new function
%% M:F(State, Payload, Header)
has_newcall(M, F) ->
    erlang:function_exported(M, F, 3).

%% Handle module call
do_call({M, F} = _Handler, State, Payload, Header) ->
	case has_newcall(M, F) of
		true ->
			M:F(State, Payload, Header);
		false ->
			M:F(State, Payload)
	end.

eval_payload(Socket, Timeout, {M, F} = Handler, State, Connection, Payload, Header) ->
    case catch do_call(Handler, State, Payload, Header) of
	{'EXIT', Reason} when Connection == close ->
	    ?ERROR_LOG({M, F, {'EXIT', Reason}}),
	    send(Socket, 500, "Connection: close\r\n");
	{'EXIT', Reason} ->
	    ?ERROR_LOG({M, F, {'EXIT', Reason}}),
	    send(Socket, 500),
	    handler(Socket, Timeout, Handler, State);
	{error, Reason} when Connection == close ->
	    ?ERROR_LOG({M, F, Reason}),
	    send(Socket, 500, "Connection: close\r\n");
	{error, Reason} ->
	    ?ERROR_LOG({M, F, Reason}),
	    send(Socket, 500),
	    handler(Socket, Timeout, Handler, State);
	{false, ResponsePayload} ->
	    encode_send(Socket, 200, "Connection: close\r\n", ResponsePayload);
	{false, ResponsePayload, ExtraHeaders} ->
        encode_send(Socket, 200, [ExtraHeaders, "Connection: close\r\n"], ResponsePayload);
	{true, _NewTimeout, _NewState, ResponsePayload} when
	      Connection == close ->
	    encode_send(Socket, 200, "Connection: close\r\n", ResponsePayload);
	{true, NewTimeout, NewState, ResponsePayload} ->
	    encode_send(Socket, 200, "", ResponsePayload),
	    handler(Socket, NewTimeout, Handler, NewState);
	{true, _NewTimeout, _NewState, ResponsePayload, ExtraHeaders} when
	      Connection == close ->
	    encode_send(Socket, 200, [ExtraHeaders, "Connection: close\r\n"], ResponsePayload);
	{true, NewTimeout, NewState, ResponsePayload, ExtraHeaders} ->
	    encode_send(Socket, 200, ExtraHeaders, ResponsePayload),
	    handler(Socket, NewTimeout, Handler, NewState)
    end.

encode_send(Socket, StatusCode, ExtraHeader, Payload) ->
    ?DEBUG_LOG({decoded_response, Payload}),
    case xmlrpc_encode:payload(Payload) of
	{ok, EncodedPayload} ->
	    ?DEBUG_LOG({encoded_response, lists:flatten(EncodedPayload)}),
	    send(Socket, StatusCode, ExtraHeader, EncodedPayload);
	{error, Reason} ->
	    ?ERROR_LOG({xmlrpc_encode, payload, Payload, Reason}),
	    send(Socket, 500)
    end.

send(Socket, StatusCode) -> send(Socket, StatusCode, "", "").

send(Socket, StatusCode, ExtraHeader) ->
    send(Socket, StatusCode, ExtraHeader, "").

send(Socket, StatusCode, ExtraHeader, Payload) ->
    Response =
	["HTTP/1.1 ", integer_to_list(StatusCode), " ",
	 reason_phrase(StatusCode), "\r\n",
	 "Content-Length: ", integer_to_list(lists:flatlength(Payload)),
	 "\r\n",
	 "Server: Erlang/1.13\r\n",
	 "Content-Type: text/xml\r\n",
	 ExtraHeader, "\r\n",
	 Payload],
    gen_tcp:send(Socket, Response).

reason_phrase(200) -> "OK";
reason_phrase(400) -> "Bad Request";
reason_phrase(411) -> "Length required";
reason_phrase(415) -> "Unsupported Media Type";
reason_phrase(500) -> "Internal Server Error";
reason_phrase(501) -> "Not Implemented";
reason_phrase(505) -> "HTTP Version not supported".
