% ``The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved via the world wide web at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%% 
%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
%% AB. All Rights Reserved.''
%% 
%%     $Id$
%%
%% This module is very loosely based on code initially developed by 
%% Johan Blom at Mobile Arts AB
%% Description:
%%% This version of the HTTP/1.1 client supports:
%%%      - RFC 2616 HTTP 1.1 client part
%%%      - RFC 2818 HTTP Over TLS

-module(http).

%% API
-export([request/1, request/4, cancel_request/1, set_options/1, 
	 verify_cookies/2, cookie_header/1]).

-include("http_internal.hrl").
-include("httpc_internal.hrl").

%%%=========================================================================
%%%  API
%%%=========================================================================

%%--------------------------------------------------------------------------
%% request(Method, Request, HTTPOptions, Options) ->
%%           {ok, {StatusLine, Headers, Body}} | {ok, {Status, Body}} |
%%           {ok, RequestId} | {error,Reason} | {ok, {saved_as, FilePath}
%%
%%	Method - atom() = head | get | put | post | trace | options| delete 
%%	Request - {Url, Headers} | {Url, Headers, ContentType, Body} 
%%	Url - string() 
%%	HTTPOptions - [HttpOption]
%%	HTTPOption - {timeout, Time} | {ssl, SSLOptions} | 
%%                   {proxy_auth, {User, Password}}
%%	Ssloptions = [SSLOption]
%%	SSLOption =  {verify, code()} | {depth, depth()} | {certfile, path()} |
%%	{keyfile, path()} | {password, string()} | {cacertfile, path()} |
%%	{ciphers, string()} 
%%	Options - [Option]
%%	Option - {sync, Boolean} | {body_format, BodyFormat} | 
%%	{full_result, Boolean} | {stream, To} |
%%      {headers_as_is, Boolean}  
%%	StatusLine = {HTTPVersion, StatusCode, ReasonPhrase}</v>
%%	HTTPVersion = string()
%%	StatusCode = integer()
%%	ReasonPhrase = string()
%%	Headers = [Header]
%%      Header = {Field, Value}
%%	Field = string()
%%	Value = string()
%%	Body = string() | binary() - HTLM-code
%%
%% Description: Sends a HTTP-request. The function can be both
%% syncronus and asynchronous in the later case the function will
%% return {ok, RequestId} and later on a message will be sent to the
%% calling process on the format {http, {RequestId, {StatusLine,
%% Headers, Body}}} or {http, {RequestId, {error, Reason}}}
%% %%--------------------------------------------------------------------------
request(Url) ->
    request(get, {Url, []}, [], []).

request(Method, {Url, Headers}, HTTPOptions, Options) 
  when Method==options;Method==get;Method==head;Method==delete;Method==trace ->
    case http_uri:parse(Url) of
	{error,Reason} ->
	    {error,Reason};
	ParsedUrl ->
	    handle_request(Method, Url, {ParsedUrl, Headers, [], []}, 
			   HTTPOptions, Options)
    end;
     
request(Method, {Url,Headers,ContentType,Body}, HTTPOptions, Options) 
  when Method==post;Method==put ->
    case http_uri:parse(Url) of
	{error,Reason} ->
	    {error,Reason};
	ParsedUrl ->
	    handle_request(Method, Url, 
			   {ParsedUrl, Headers, ContentType, Body}, 
			   HTTPOptions, Options)
    end.

%%--------------------------------------------------------------------------
%% request(RequestId) -> ok
%%   RequestId - As returned by request/4  
%%                                 
%% Description: Cancels a HTTP-request.
%%-------------------------------------------------------------------------
cancel_request(RequestId) ->
    ok = httpc_manager:cancel_request(RequestId), 
    receive  
	%% If the request was allready fullfilled throw away the 
	%% answer as the request has been canceled.
	{http, {RequestId, _}} ->
	    ok 
    after 0 ->
	    ok
    end.

%%--------------------------------------------------------------------------
%% set_options(Options) ->
%%   Options - [Option]
%%   Option - {proxy, {Proxy, NoProxy}} | {max_sessions, MaxSessions} | 
%%            {max_pipeline_length, MaxPipeline} | 
%%            {pipeline_timeout, PipelineTimeout} | {cookies, CookieMode}
%%            | {ipv6, Ipv6Mode}
%%   Proxy - {Host, Port}
%%   NoProxy - [Domain | HostName | IPAddress]   
%%   MaxSessions, MaxPipeline, PipelineTimeout = integer()   
%%   CookieMode - enabled | disabled | verify
%%   Ipv6Mode - enabled | disabled
%% Description: Informs the httpc_manager of the new settings. 
%%-------------------------------------------------------------------------
set_options(Options) ->
    ensure_started(no_scheme),
    httpc_manager:set_options(Options).

verify_cookies(SetCookieHeaders, Url) ->
    {_, _, Host, Port, Path, _} = http_uri:parse(Url),
    Cookies = http_cookie:cookies(SetCookieHeaders, Path, Host),
    httpc_manager:store_cookies(Cookies, {Host, Port}),
    ok.

cookie_header(Url) ->
    httpc_manager:cookies(Url).

%%%========================================================================
%%% Internal functions
%%%========================================================================
handle_request(Method, Url, {{Scheme, UserInfo, Host, Port, Path, Query},
			Headers, ContentType, Body}, HTTPOptions, Options) ->
    HTTPRecordOptions = http_options(HTTPOptions, #http_options{}),
    
    Sync = http_util:key1search(Options, sync, true),
    NewHeaders = lists:map(fun({Key, Val}) -> 
				   {http_util:to_lower(Key), Val} end,
			   Headers),
    Stream = http_util:key1search(Options, stream, none),

    case {Sync, Stream} of
	{true, self} ->
	    {error, streaming_error};
	_ ->
	    RecordHeaders = header_record(NewHeaders, #http_request_h{}, Host),
	    Request = #request{from = self(),
			       scheme = Scheme, address = {Host,Port},
			       path = Path, pquery = Query, method = Method,
			       headers = RecordHeaders, 
			       content = {ContentType,Body},
			       settings = HTTPRecordOptions,
			       abs_uri = Url, userinfo = UserInfo, 
			       stream = Stream, 
			       headers_as_is = 
			       headers_as_is(Headers, Options)},
	    
	    ensure_started(Scheme),
	    
	    case httpc_manager:request(Request) of
		{ok, RequestId} ->
		    handle_answer(RequestId, Sync, Options);
		{error, Reason} ->
		    {error, Reason}
	    end
    end.

handle_answer(RequestId, false, _) ->
    {ok, RequestId};
handle_answer(RequestId, true, Options) ->
    receive
	{http, {RequestId, saved_to_file}} ->
	    {ok, saved_to_file};
	{http, {RequestId, Result = {_,_,_}}} ->
	    return_answer(Options, Result);
	{http, {RequestId, {error, Reason}}} ->
	    {error, Reason}
    end.
 
return_answer(Options, {StatusLine, Headers, BinBody}) ->
    Body = 
	case http_util:key1search(Options, body_format, string) of
	    string ->
		binary_to_list(BinBody);
	    _ ->
		BinBody
	end,
    case http_util:key1search(Options, full_result, true) of
	true ->
	    {ok, {StatusLine, Headers, Body}};
	false ->
	    {_, Status, _} = StatusLine,
	    {ok, {Status, Body}}
    end.


%% This options is a workaround for http servers that do not follow the 
%% http standard and have case sensative header parsing. Should only be
%% used if there is no other way to communicate with the server or for
%% testing purpose.
headers_as_is(Headers, Options) ->
     case http_util:key1search(Options, headers_as_is, false) of
	 false ->
	     [];
	 true  ->
	     Headers
     end.

http_options([], Acc) ->
    Acc;
http_options([{timeout, Val} | Settings], Acc) 
  when is_integer(Val), Val >= 0->
    http_options(Settings, Acc#http_options{timeout = Val});
http_options([{timeout, infinity} | Settings], Acc) ->
    http_options(Settings, Acc#http_options{timeout = infinity});
http_options([{autoredirect, Val} | Settings], Acc)   
  when Val == true; Val == false ->
    http_options(Settings, Acc#http_options{autoredirect = Val});
http_options([{ssl, Val} | Settings], Acc) ->
    http_options(Settings, Acc#http_options{ssl = Val});
http_options([{relaxed, Val} | Settings], Acc)
  when Val == true; Val == false ->
    http_options(Settings, Acc#http_options{relaxed = Val});
http_options([{proxy_auth, Val = {User, Passwd}} | Settings], Acc) 
  when is_list(User),
       is_list(Passwd) ->
    http_options(Settings, Acc#http_options{proxy_auth = Val});
http_options([Option | Settings], Acc) ->
    error_logger:info_report("Invalid option ignored ~p~n", [Option]),
    http_options(Settings, Acc).

header_record([], RequestHeaders, Host) ->
    validate_headers(RequestHeaders, Host);
header_record([{"cache-control", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'cache-control' = Val},
		  Host);  
header_record([{"connection", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{connection = Val}, Host);
header_record([{"date", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{date = Val}, Host);  
header_record([{"pragma", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{pragma = Val}, Host);  
header_record([{"trailer", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{trailer = Val}, Host);  
header_record([{"transfer-encoding", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, 
		  RequestHeaders#http_request_h{'transfer-encoding' = Val},
		  Host);  
header_record([{"upgrade", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{upgrade = Val}, Host);  
header_record([{"via", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{via = Val}, Host);  
header_record([{"warning", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{warning = Val}, Host);  
header_record([{"accept", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{accept = Val}, Host);  
header_record([{"accept-charset", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'accept-charset' = Val}, 
		  Host);  
header_record([{"accept-encoding", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'accept-encoding' = Val},
		  Host);  
header_record([{"accept-language", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'accept-language' = Val},
		  Host);  
header_record([{"authorization", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{authorization = Val}, 
		  Host);  
header_record([{"expect", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{expect = Val}, Host);
header_record([{"from", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{from = Val}, Host);  
header_record([{"host", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{host = Val}, Host);
header_record([{"if-match", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'if-match' = Val},
		  Host);  
header_record([{"if-modified-since", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, 
		  RequestHeaders#http_request_h{'if-modified-since' = Val},
		  Host);  
header_record([{"if-none-match", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'if-none-match' = Val}, 
		  Host);  
header_record([{"if-range", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'if-range' = Val}, 
		  Host);  

header_record([{"if-unmodified-since", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'if-unmodified-since' 
						      = Val}, Host);  
header_record([{"max-forwards", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'max-forwards' = Val}, 
		  Host);  
header_record([{"proxy-authorization", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'proxy-authorization' 
						      = Val}, Host);  
header_record([{"range", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{range = Val}, Host);  
header_record([{"referer", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{referer = Val}, Host);  
header_record([{"te", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{te = Val}, Host);  
header_record([{"user-agent", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'user-agent' = Val}, 
		  Host);  
header_record([{"allow", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{allow = Val}, Host);  
header_record([{"content-encoding", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, 
		  RequestHeaders#http_request_h{'content-encoding' = Val},
		  Host);  
header_record([{"content-language", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, 
		  RequestHeaders#http_request_h{'content-language' = Val}, 
		  Host);  
header_record([{"content-length", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'content-length' = Val},
		  Host);  
header_record([{"content-location", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, 
		  RequestHeaders#http_request_h{'content-location' = Val},
		  Host);  
header_record([{"content-md5", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'content-md5' = Val}, 
		  Host);  
header_record([{"content-range", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'content-range' = Val},
		  Host);  
header_record([{"content-type", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'content-type' = Val}, 
		  Host);  
header_record([{"expires", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{expires = Val}, Host);  
header_record([{"last-modified", Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{'last-modified' = Val},
		  Host);  
header_record([{Key, Val} | Rest], RequestHeaders, Host) ->
    header_record(Rest, RequestHeaders#http_request_h{
			  other = [{Key, Val} |
				   RequestHeaders#http_request_h.other]}, 
		  Host).

validate_headers(RequestHeaders = #http_request_h{te = undefined}, Host) ->
    validate_headers(RequestHeaders#http_request_h{te = ""}, Host);
validate_headers(RequestHeaders = #http_request_h{host = undefined}, Host) ->
    validate_headers(RequestHeaders#http_request_h{host = Host}, Host);
validate_headers(RequestHeaders, _) ->
    RequestHeaders.

ensure_started(Scheme) ->
    %% Start of the inets application should really be handled by the 
    %% application using inets. 
    case application:start(inets) of
	{error,{already_started,inets}} ->
	    ok;
	{error, {{already_started,_}, % Started as an included application
	 {inets_app, start, _}}} ->
	    ok;
	ok ->
	    error_logger:info_report("The inets application was not started."
				     " Has now been started as a temporary" 
				     " application.")
    end,
    
    case Scheme of
	https ->
	    %% Start of the ssl application should really be handled by the 
	    %% application using inets. 
	    case application:start(ssl) of
		{error,{already_started,ssl}} ->
		    ok;
		%% Started as an included application
		{error, {{already_started,_}, 
		 {ssl_app, start, _}}} ->
		    ok;
		ok ->
		    error_logger:info_report("The ssl application was not "
					     "started. Has now been started "
					     "as a temporary application.")
	    end;
	_ ->
	    ok
    end.
