1%%%  This code was developped by IDEALX (http://IDEALX.org/) and
2%%%  contributors (their names can be found in the CONTRIBUTORS file).
3%%%  Copyright (C) 2000-2004 IDEALX
4%%%
5%%%  This program is free software; you can redistribute it and/or modify
6%%%  it under the terms of the GNU General Public License as published by
7%%%  the Free Software Foundation; either version 2 of the License, or
8%%%  (at your option) any later version.
9%%%
10%%%  This program is distributed in the hope that it will be useful,
11%%%  but WITHOUT ANY WARRANTY; without even the implied warranty of
12%%%  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13%%%  GNU General Public License for more details.
14%%%
15%%%  You should have received a copy of the GNU General Public License
16%%%  along with this program; if not, write to the Free Software
17%%%  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
18%%%
19
20%%%  In addition, as a special exception, you have the permission to
21%%%  link the code of this program with any library released under
22%%%  the EPL license and distribute linked combinations including
23%%%  the two; the MPL (Mozilla Public License), which EPL (Erlang
24%%%  Public License) is based on, is included in this exception.
25
26%%% common functions used by http clients to:
27%%%  - set HTTP requests
28%%%  - parse HTTP response from server
29
30-module(ts_http_common).
31-vc('$Id$ ').
32-author('nicolas.niclausse@niclux.org').
33
34-include("ts_profile.hrl").
35-include("ts_http.hrl").
36
37-include("ts_config.hrl").
38
39-export([
40         http_get/1,
41         http_post/1,
42         http_body/2,
43         http_no_body/2,
44         parse/2,
45         parse_req/1,
46         parse_req/2,
47         get_line/1
48        ]).
49
50%%----------------------------------------------------------------------
51%% Func: http_get/1
52%%----------------------------------------------------------------------
53http_get(Args) ->
54    http_no_body(?GET, Args).
55
56%%----------------------------------------------------------------------
57%% Func: http_get/1
58%% Args: #http_request
59%%----------------------------------------------------------------------
60%% normal request
61http_no_body(Method,#http_request{url=URL, version=Version, cookie=Cookie,
62                              headers=Headers, user_agent=UA,
63                              get_ims_date=undefined, soap_action=SOAPAction,
64                              host_header=Host}=Req)->
65    ?DebugF("~p ~p~n",[Method,URL]),
66    R = list_to_binary([Method, " ", URL," ", "HTTP/", Version, ?CRLF,
67                    set_header("Host",Host,Headers, ""),
68                    set_header("User-Agent",UA,Headers, ?USER_AGENT),
69                    set_header("Content-Type", undefined, Headers, undefined),
70                    set_header("Content-Length", undefined, Headers, undefined),
71                    authenticate(Req),
72                    oauth_sign(Method,Req),
73                    soap_action(SOAPAction),
74                    set_cookie_header({Cookie, Host, URL}),
75                    headers(Headers),
76                    ?CRLF]),
77    ?DebugF("Headers~n-------------~n~s~n",[R]),
78    R;
79%% if modified since request
80http_no_body(Method,#http_request{url=URL, version=Version, cookie=Cookie,
81                             headers=Headers, user_agent=UA,
82                             get_ims_date=Date, soap_action=SOAPAction,
83                             host_header=Host}=Req) ->
84    ?DebugF("~p ~p~n",[Method, URL]),
85    list_to_binary([Method, " ", URL," ", "HTTP/", Version, ?CRLF,
86                    ["If-Modified-Since: ", Date, ?CRLF],
87                    set_header("Host",Host,Headers, ""),
88                    set_header("User-Agent",UA,Headers, ?USER_AGENT),
89                    set_header("Content-Type", undefined, Headers, undefined),
90                    set_header("Content-Length", undefined, Headers, undefined),
91                    soap_action(SOAPAction),
92                    authenticate(Req),
93                    oauth_sign(Method,Req),
94                    set_cookie_header({Cookie, Host, URL}),
95                    headers(Headers),
96                    ?CRLF]).
97
98%%----------------------------------------------------------------------
99%% Func: http_post/1
100%%----------------------------------------------------------------------
101http_post(Args) ->
102    http_body(?POST, Args).
103
104%%----------------------------------------------------------------------
105%% Func: http_body/2
106%% Args: #http_request
107%%----------------------------------------------------------------------
108http_body(Method,#http_request{url=URL, version=Version,
109                               cookie=Cookie, headers=Headers,
110                               user_agent=UA, soap_action=SOAPAction,
111                               content_type=ContentType,
112                               body=Content, host_header=Host}=Req) ->
113    ContentLength=integer_to_list(size(Content)),
114    ?DebugF("Content Length of POST: ~p~n.", [ContentLength]),
115
116    H = [Method, " ", URL," ", "HTTP/", Version, ?CRLF,
117               set_header("Host",Host,Headers, ""),
118               set_header("User-Agent",UA,Headers, ?USER_AGENT),
119               set_header("Content-Type", ContentType, Headers, undefined),
120               set_header("Content-Length", ContentLength, Headers, undefined),
121               authenticate(Req),
122               soap_action(SOAPAction),
123               oauth_sign(Method, Req),
124               set_cookie_header({Cookie, Host, URL}),
125               headers(Headers),
126               ?CRLF
127              ],
128    ?LOGF("Headers~n-------------~n~s~n",[H],?DEB),
129    list_to_binary([H, Content ]).
130
131%%----------------------------------------------------------------------
132%% some HTTP headers functions
133%%----------------------------------------------------------------------
134authenticate(#http_request{userid=undefined})-> [];
135authenticate(#http_request{passwd=undefined})-> [];
136authenticate(#http_request{passwd=Passwd, auth_type="basic",userid=UserId})->
137    AuthStr = ts_utils:encode_base64(lists:append([UserId,":",Passwd])),
138    ["Authorization: Basic ",AuthStr,?CRLF];
139
140authenticate(#http_request{method=Method, passwd=Passwd,userid=UserId,
141                           auth_type="digest", realm=Realm,
142                           digest_cnonce=CNonce, digest_nc=NC, digest_qop=QOP,
143                           digest_nonce=Nonce, digest_opaque=Opaque,
144                           url=URL
145                            }) ->
146    HA1 = md5_hex(string:join([UserId, Realm, Passwd], ":")),
147    HA2 = md5_hex(string:join([string:to_upper(atom_to_list(Method)), URL], ":")),
148    Response = digest_response({HA1, Nonce,NC, CNonce,QOP,HA2}),
149    digest_header(UserId,Realm,Nonce,URL,QOP,NC,CNonce,Response,Opaque).
150
151digest_header(User,Realm,Nonce,URI, QOP,NC,CNonce, Response,Opaque) ->
152    Acc= ["Authorization: Digest "
153          "username=\"",User,"\", ",
154          "realm=\"", Realm, "\", ",
155          "nonce=\"", Nonce, "\", ",
156          "uri=\"", URI, "\", ",
157          "response=\"", Response, "\""],
158    digest_header_opt(Acc, QOP, NC, CNonce, Opaque).
159
160%% qop and opaque are undefined
161digest_header_opt(Acc, undefined, _NC, _CNonce, undefined) ->
162    [Acc, ?CRLF];
163
164digest_header_opt(Acc, QOP, NC, CNonce, Opaque) when is_list(Opaque)->
165     NewAcc=[Acc,", opaque=\"",Opaque,"\""],
166    digest_header_opt(NewAcc,QOP,NC,CNonce,undefined);
167
168digest_header_opt(Acc, QOP, NC, CNonce,undefined) ->
169    NewAcc=[Acc,", qop=\"",QOP,"\"",
170                ", nc=", NC,
171                ", cnonce=\"", CNonce, "\""
172           ],
173    digest_header_opt(NewAcc,undefined,"","",undefined).
174
175digest_response({HA1,Nonce, _NC, _CNonce, undefined, HA2})-> %qop undefined
176    md5_hex(string:join([HA1, Nonce, HA2], ":"));
177digest_response({HA1,Nonce, NC, CNonce, QOP, HA2})->
178    md5_hex(string:join([HA1,Nonce,NC,CNonce,QOP,HA2], ":")).
179
180md5_hex(String)->
181    lists:flatten([io_lib:format("~2.16.0b",[N])||N<-binary_to_list(erlang:md5(String))]).
182
183
184oauth_sign(_, #http_request{oauth_consumer = undefined})->[];
185oauth_sign(Method, #http_request{url=URL,
186                         oauth_consumer=Consumer,
187                         oauth_access_token=AccessToken,
188                         oauth_access_secret=AccessSecret,
189                         oauth_url=ServerURL,
190                         content_type = ContentType,
191                         body = Body})->
192    %%UrlParams = oauth_uri:params_from_string(URL),
193    [_He|Ta] = string:tokens(URL,"?"),
194    UrlParams = oauth_uri:params_from_string(lists:flatten(Ta)),
195    AllParams = case ContentType of
196                    ?BODY_PARAM ->
197                        BodyParams = oauth_uri:params_from_string(lists:flatten(binary_to_list(Body))),
198                        UrlParams ++ BodyParams;
199                    _ ->
200                        UrlParams
201                end,
202    Params = oauth:signed_params(Method, ServerURL, AllParams, Consumer, AccessToken, AccessSecret),
203    ["Authorization: OAuth ", oauth_uri:params_to_header_string(Params),?CRLF].
204
205%%----------------------------------------------------------------------
206%% @spec set_header(Name::string, Val::string | undefined, Headers::List,
207%%                  Default::string) -> list()
208%% @doc If header Name is defined in Headers, print this one, otherwise,
209%%      print the given Value (or the default one if undefined)
210%% @end
211%%----------------------------------------------------------------------
212set_header(Name, Value, Headers, Default) when length(Headers) > 0 ->
213    case lists:keysearch(string:to_lower(Name), 1, normalize_headers(Headers)) of
214        {value, {_,Val}} -> [Name, ": ", Val, ?CRLF];
215        false      -> set_header(Name,Value,[], Default)
216    end;
217set_header(_Name, undefined, [], undefined) ->
218    [];
219set_header(Name, undefined, [], Default) ->
220    [Name++": ", Default, ?CRLF];
221set_header(Name, Value, [], _) ->
222    [Name++": ", Value, ?CRLF].
223
224soap_action(undefined) -> [];
225soap_action(SOAPAction) -> ["SOAPAction: \"", SOAPAction, "\"", ?CRLF].
226
227% user defined headers
228headers([]) -> [];
229headers(Headers) ->
230    HeadersToIgnore = ["host", "user-agent", "content-type", "content-length"],
231
232    lists:foldl(fun({Name, Value}, Result) ->
233        case lists:member(string:to_lower(Name), HeadersToIgnore) of
234            true ->
235                Result;
236            _ ->
237                [Name, ": ", Value, ?CRLF | Result]
238        end
239    end, [], lists:reverse(Headers)).
240
241normalize_headers([]) -> [];
242normalize_headers(Headers) ->
243    lists:map(fun({Name, Value}) -> {string:to_lower(Name), Value} end, Headers).
244
245
246%%----------------------------------------------------------------------
247%% Function: set_cookie_header/1
248%% Args: Cookies (list), Hostname (string), URL
249%% Purpose: set Cookie: Header
250%%----------------------------------------------------------------------
251set_cookie_header({[], _, _})   -> [];
252set_cookie_header({Cookies, Host, URL})->
253    MatchDomain = fun (A) -> matchdomain_url(A,Host,URL) end,
254    CurCookies = lists:filter(MatchDomain, Cookies),
255    set_cookie_header(CurCookies, Host, []).
256
257set_cookie_header([], _Host, [])    -> [];
258set_cookie_header([], _Host, Acc)   -> [lists:reverse(Acc), ?CRLF];
259set_cookie_header([Cookie|Cookies], Host, []) ->
260    set_cookie_header(Cookies, Host, [["Cookie: ", cookie_rec2str(Cookie)]]);
261set_cookie_header([Cookie|Cookies], Host, Acc) ->
262    set_cookie_header(Cookies, Host, [["; ", cookie_rec2str(Cookie)]|Acc]).
263
264cookie_rec2str(#cookie{key=Key, value=Val}) ->
265    lists:append([Key,"=",Val]).
266
267%%----------------------------------------------------------------------
268%% Function: matchdomain_url/3
269%% Purpose:  return a cookie only if domain match
270%% Returns:  true|false
271%%----------------------------------------------------------------------
272matchdomain_url(Cookie, _Host, "http"++URL) -> % absolute URL, a proxy is used.
273    %% FIXME: the domain stored is the domain of the proxy, we can't
274    %% check the domain currently :( We assume it's OK
275    %% FIXME: really check if it's a sub path; currently we only check
276    %% that the path is somewhere in the URL which is obviously not
277    %% the right thing to do.
278    string:str(URL,Cookie#cookie.path) > 0;
279matchdomain_url(Cookie, Host, URL) ->
280    SubDomain = string:str([$.|Host],Cookie#cookie.domain),
281    SubPath   = string:str(URL,Cookie#cookie.path), % FIXME:should use regexp:match
282    case {SubDomain,SubPath} of
283        {0,_} -> false;
284        {_,1} -> true;
285        {_,_} -> false
286    end.
287
288
289%%----------------------------------------------------------------------
290%% Func: parse/2
291%% Args: Data, State
292%% Returns: {NewState, Options for socket (list), Close}
293%% Purpose: parse the response from the server and keep information
294%%  about the response if State#state_rcv.session
295%%----------------------------------------------------------------------
296parse(closed, State=#state_rcv{session=Http}) ->
297    {State#state_rcv{session=reset_session(Http), ack_done = true}, [], true};
298
299parse(Data, State=#state_rcv{session=HTTP}) when element(1,HTTP#http.status)  == none;
300                                                 HTTP#http.partial == true ->
301
302    List = binary_to_list(Data),
303    TotalSize = size(Data),
304    Header = State#state_rcv.acc ++ List,
305
306    case parse_headers(HTTP, Header, State#state_rcv.host) of
307        %% Partial header:
308        {more, HTTPRec, Tail} ->
309            ?LOGF("Partial Header: [HTTP=~p : Tail=~p]~n",[HTTPRec, Tail],?DEB),
310            {State#state_rcv{ack_done=false,session=HTTPRec,acc=Tail},[],false};
311        %% Complete header, chunked encoding
312        {ok, Http=#http{content_length=0, chunk_toread=0}, Tail} ->
313            NewCookies = concat_cookies(Http#http.cookie, Http#http.session_cookies),
314            case parse_chunked(Tail, State#state_rcv{session=Http, acc=[]}) of
315                {NewState=#state_rcv{ack_done=false, session=NewHttp}, Opts} ->
316                    {NewState#state_rcv{session=NewHttp#http{session_cookies=NewCookies}}, Opts, false};
317                {NewState=#state_rcv{session=NewHttp}, Opts} ->
318                    {NewState#state_rcv{acc=[],session=NewHttp#http{session_cookies=NewCookies}}, Opts, Http#http.close}
319            end;
320        {ok, Http=#http{content_length=0, close=true}, _} ->
321            %% no content length, close=true: the server will close the connection
322            NewCookies = concat_cookies(Http#http.cookie, Http#http.session_cookies),
323            {State#state_rcv{ack_done = false,
324                             datasize = TotalSize,
325                             session=Http#http{session_cookies=NewCookies}}, [], true};
326        {ok, Http=#http{status={100,_}}, _} -> % Status 100 Continue, ignore.
327            %% FIXME: not tested
328            {State#state_rcv{ack_done=false,session=reset_session(Http)},[],false};
329        {ok, Http, Tail} ->
330            NewCookies = concat_cookies(Http#http.cookie, Http#http.session_cookies),
331            check_resp_size(Http#http{session_cookies=NewCookies}, length(Tail), State#state_rcv{acc=[]}, TotalSize, State#state_rcv.dump)
332    end;
333
334%% continued chunked transfer
335parse(Data, State=#state_rcv{session=Http}) when Http#http.chunk_toread >=0 ->
336    ?DebugF("Parse chunk data = [~s]~n", [Data]),
337    case read_chunk_data(Data,State,Http#http.chunk_toread,Http#http.body_size) of
338        {NewState=#state_rcv{ack_done=false}, NewOpts}->
339            {NewState, NewOpts, false};
340        {NewState, NewOpts}->
341            {NewState#state_rcv{acc=[]}, NewOpts, Http#http.close}
342    end;
343
344%% continued normal transfer
345parse(Data,  State=#state_rcv{session=Http, datasize=PreviousSize}) ->
346    DataSize = size(Data),
347    ?DebugF("HTTP Body size=~p ~n",[DataSize]),
348    CLength = Http#http.content_length,
349    case Http#http.body_size + DataSize of
350        CLength -> % end of response
351            {State#state_rcv{session=reset_session(Http), acc=[], ack_done = true, datasize = DataSize+PreviousSize},
352             [], Http#http.close};
353        Size ->
354            {State#state_rcv{session = Http#http{body_size = Size}, ack_done = false,
355                             datasize = DataSize+PreviousSize}, [], false}
356    end.
357
358%%----------------------------------------------------------------------
359%% Func: check_resp_size/5
360%% Purpose: Check response size
361%% Returns: {NewState= record(state_rcv), SockOpts, Close}
362%%----------------------------------------------------------------------
363check_resp_size(Http=#http{content_length=CLength, close=Close},
364                CLength, State, DataSize, _Dump) ->
365    %% end of response
366    {State#state_rcv{session= reset_session(Http), ack_done = true, datasize = DataSize }, [], Close};
367check_resp_size(Http=#http{content_length=CLength, close=Close},
368                BodySize, State, DataSize, Dump) when BodySize > CLength ->
369    ?LOGF("Error: HTTP Body (~p)> Content-Length (~p) !~n",
370          [BodySize, CLength], ?ERR),
371    log_error(Dump, error_http_bad_content_length),
372    {State#state_rcv{session= reset_session(Http), ack_done = true,
373                     datasize = DataSize }, [], Close};
374check_resp_size(Http=#http{}, BodySize,  State, DataSize,_Dump) ->
375    %% need to read more data
376    {State#state_rcv{session  = Http#http{body_size = BodySize},
377                     ack_done = false,
378                     datasize = DataSize },[],false}.
379
380%%----------------------------------------------------------------------
381%% Func: parse_chunked/2
382%% Purpose: parse 'Transfer-Encoding: chunked' for HTTP/1.1
383%% Returns: {NewState= record(state_rcv), SockOpts, Close}
384%%----------------------------------------------------------------------
385parse_chunked(Body, State)->
386    ?DebugF("Parse chunk data = [~s]~n", [Body]),
387    read_chunk(list_to_binary(Body), State, 0, 0).
388
389%%----------------------------------------------------------------------
390%% Func: read_chunk/4
391%% Purpose: the real stuff for parsing chunks is here
392%% Returns: {NewState= record(state_rcv), SockOpts, Close}
393%%----------------------------------------------------------------------
394read_chunk(<<>>, State, Int, Acc) ->
395    ?LOGF("No data in chunk [Int=~p, Acc=~p] ~n", [Int,Acc],?INFO),
396    AccInt = list_to_binary(httpd_util:integer_to_hexlist(Int)),
397    { State#state_rcv{acc = AccInt, ack_done = false }, [] }; % read more data
398%% this code has been inspired by inets/http_lib.erl
399%% Extensions not implemented
400read_chunk(<<Char:1/binary, Data/binary>>, State=#state_rcv{session=Http}, Int, Acc) ->
401    case Char of
402    <<C>> when $0=<C,C=<$9 ->
403        read_chunk(Data, State, 16*Int+(C-$0), Acc+1);
404    <<C>> when $a=<C,C=<$f ->
405        read_chunk(Data, State, 16*Int+10+(C-$a), Acc+1);
406    <<C>> when $A=<C,C=<$F ->
407        read_chunk(Data, State, 16*Int+10+(C-$A), Acc+1);
408    <<?CR>> when Int>0 ->
409        read_chunk_data(Data, State, Int+3, Acc+1);
410    <<?CR>> when Int==0, size(Data) == 3 -> %% should be the end of transfer
411            ?DebugF("Finish tranfer chunk ~p~n", [binary_to_list(Data)]),
412            {State#state_rcv{session= reset_session(Http), ack_done = true,
413                             datasize = Acc %% FIXME: is it the correct size?
414                            }, []};
415    <<?CR>> when Int==0, size(Data) < 3 ->  % lack ?CRLF, continue
416            { State#state_rcv{acc =  <<48, ?CR , Data/binary>>, ack_done=false }, [] };
417    <<C>> when C==$ -> % Some servers (e.g., Apache 1.3.6) throw in
418               % additional whitespace...
419        read_chunk(Data, State, Int, Acc+1);
420    _Other ->
421            ?LOGF("Unexpected error while parsing chunk ~p~n", [_Other] ,?WARN),
422            log_error(State#state_rcv.dump, error_http_unexpected_chunkdata),
423            {State#state_rcv{session= reset_session(Http), ack_done = true}, []}
424    end.
425
426
427%%----------------------------------------------------------------------
428%% Func: read_chunk_data/4
429%% Purpose: read 'Int' bytes of data
430%% Returns: {NewState= record(state_rcv), SockOpts}
431%%----------------------------------------------------------------------
432read_chunk_data(Data, State=#state_rcv{acc=[]}, Int, Acc) when size(Data) > Int->
433    ?DebugF("Read ~p bytes of chunk with size = ~p~n", [Int, size(Data)]),
434    <<_NewData:Int/binary, Rest/binary >> = Data,
435    read_chunk(Rest, State,  0, Int + Acc);
436read_chunk_data(Data, State=#state_rcv{acc=[],session=Http}, Int, Acc) -> % not enough data in buffer
437    BodySize = size(Data),
438    ?DebugF("Partial chunk received (~p/~p)~n", [BodySize,Int]),
439    NewHttp = Http#http{chunk_toread   = Int-BodySize,
440                        body_size      = BodySize + Acc},
441    {State#state_rcv{session  = NewHttp,
442                     ack_done = false, % continue to read data
443                     datasize = BodySize + Acc},[]};
444read_chunk_data(Data, State=#state_rcv{acc=Acc}, _Int, AccSize) ->
445    ?DebugF("Accumulated data = [~p]~n", [Acc]),
446    NewData = <<Acc/binary, Data/binary>>,
447    read_chunk(NewData, State#state_rcv{acc=[]}, 0, AccSize).
448
449%%----------------------------------------------------------------------
450%% Func: add_new_cookie/3
451%% Purpose: Separate cookie values from attributes
452%%----------------------------------------------------------------------
453add_new_cookie(Cookie, Host, OldCookies) ->
454    Fields = splitcookie(Cookie),
455    %% FIXME: bad domain if we use a Proxy (the domain will be equal
456    %% to the proxy domain instead of the server's domain
457    New = parse_set_cookie(Fields, #cookie{domain=[$.|Host],path="/"}),
458    concat_cookies([New],OldCookies).
459
460%%----------------------------------------------------------------------
461%% Function: splitcookie/3
462%% Purpose:  split according to string ";".
463%%  Not very elegant but 5x faster than the regexp:split version
464%%----------------------------------------------------------------------
465splitcookie(Cookie) -> splitcookie(Cookie, [], []).
466splitcookie([], Cur, Acc) -> [lists:reverse(Cur)|Acc];
467splitcookie(";"++Rest,Cur,Acc) ->
468    splitcookie(string:strip(Rest, both),[],[lists:reverse(Cur)|Acc]);
469splitcookie([Char|Rest],Cur,Acc)->splitcookie(Rest, [Char|Cur], Acc).
470
471%%----------------------------------------------------------------------
472%% Func: concat_cookie/2
473%% Purpose: add new cookies to a list of old ones. If the keys already
474%%          exists, replace with the new ones
475%%----------------------------------------------------------------------
476concat_cookies([],  Cookies) -> Cookies;
477concat_cookies(Cookie, []) -> Cookie;
478concat_cookies([New=#cookie{}|Rest], OldCookies)->
479    case lists:keysearch(New#cookie.key, #cookie.key, OldCookies) of
480        {value, #cookie{domain=Dom}} when Dom == New#cookie.domain -> %same domain
481                ?DebugF("Reset key ~p with new value ~p~n",[New#cookie.key,
482                                                            New#cookie.value]),
483                NewList = lists:keyreplace(New#cookie.key, #cookie.key, OldCookies, New),
484                concat_cookies(Rest, NewList);
485        {value, _Val} -> % same key, but different domains
486                concat_cookies(Rest, [New | OldCookies]);
487        false ->
488                concat_cookies(Rest, [New | OldCookies])
489    end.
490
491
492%%----------------------------------------------------------------------
493%% Func: parse_set_cookie/2
494%%       cf. RFC 2965
495%%----------------------------------------------------------------------
496parse_set_cookie([], Cookie) -> Cookie;
497parse_set_cookie([Field| Rest], Cookie=#cookie{}) ->
498    {Key,Val} = get_cookie_key(Field,[]),
499    ?DebugF("Parse cookie key ~p with value ~p~n",[Key, Val]),
500    parse_set_cookie(Rest, set_cookie_key(Key, Val, Cookie)).
501
502%%----------------------------------------------------------------------
503set_cookie_key([L|"ersion"],Val,Cookie) when L == $V; L==$v ->
504    Cookie#cookie{version=Val};
505set_cookie_key([L|"omain"],Val,Cookie) when L == $D; L==$d ->
506    Cookie#cookie{domain=Val};
507set_cookie_key([L|"ath"],Val,Cookie) when L == $P; L==$p ->
508    Cookie#cookie{path=Val};
509set_cookie_key([L|"ax-Age"],Val,Cookie) when L == $M; L==$m ->
510    Cookie#cookie{max_age=Val}; % NOT IMPLEMENTED
511set_cookie_key([L|"xpires"],Val,Cookie) when L == $E; L==$e ->
512    Cookie#cookie{expires=Val}; % NOT IMPLEMENTED
513set_cookie_key([L|"ort"],Val,Cookie) when L == $P; L==$p ->
514    Cookie#cookie{port=Val};
515set_cookie_key([L|"iscard"],_Val,Cookie) when L == $D; L==$d ->
516    Cookie#cookie{discard=true}; % NOT IMPLEMENTED
517set_cookie_key([L|"ecure"],_Val,Cookie) when L == $S; L==$s ->
518    Cookie#cookie{secure=true}; % NOT IMPLEMENTED
519set_cookie_key([L|"ommenturl"],_Val,Cookie) when L == $C; L==$c ->
520    Cookie; % don't care about comment
521set_cookie_key([L|"omment"],_Val,Cookie) when L == $C; L==$c ->
522    Cookie; % don't care about comment
523set_cookie_key(Key,Val,Cookie) ->
524    Cookie#cookie{key=Key,value=Val}.
525
526%%----------------------------------------------------------------------
527get_cookie_key([],Acc)         -> {lists:reverse(Acc), []};
528get_cookie_key([$=|Rest],Acc)  -> {lists:reverse(Acc), Rest};
529get_cookie_key([Char|Rest],Acc)-> get_cookie_key(Rest, [Char|Acc]).
530
531
532
533%%--------------------------------------------------------------------
534%% Func: parse_headers/3
535%% Purpose: Parse HTTP headers line by line
536%% Returns: {ok, #http, Body}
537%%--------------------------------------------------------------------
538parse_headers(H, Tail, Host) ->
539    case get_line(Tail) of
540    {line, Line, Tail2} ->
541        parse_headers(parse_line(Line, H, Host), Tail2, Host);
542    {lastline, Line, Tail2} ->
543        {ok, parse_line(Line, H#http{partial=false}, Host), Tail2};
544    {more} -> %% Partial header
545        {more, H#http{partial=true}, Tail}
546    end.
547
548%%--------------------------------------------------------------------
549%% Func: parse_req/1
550%% Purpose: Parse HTTP request
551%% Returns: {ok, #http_request, Body} | {more, Http , Tail}
552%%--------------------------------------------------------------------
553parse_req(Data) ->
554    parse_req([], Data).
555parse_req([], Data) ->
556    FunV = fun("http/"++V)->V;("HTTP/"++V)->V end,
557    case get_line(Data) of
558        {more} -> %% Partial header
559            {more, [], Data};
560        {line, Line, Tail} ->
561            [Method, RequestURI, Version] = string:tokens(Line," "),
562            parse_req(#http_request{method=http_method(Method),
563                                    url=RequestURI,
564                                    version=FunV(Version)},Tail);
565        {lastline, Line, Tail} ->
566            [Method, RequestURI, Version] = string:tokens(Line," "),
567            {ok, #http_request{method=http_method(Method),
568                               url=RequestURI,
569                               version=FunV(Version)},Tail}
570    end;
571parse_req(Http=#http_request{headers=H}, Data) ->
572    case get_line(Data) of
573        {line, Line, Tail} ->
574            NewH= [ts_utils:split2(Line,$:,strip) | H],
575            parse_req(Http#http_request{headers=NewH}, Tail);
576        {lastline, Line, Tail} ->
577            NewH= [ts_utils:split2(Line,$:,strip) | H],
578            {ok, Http#http_request{headers=NewH}, Tail};
579        {more} -> %% Partial header
580            {more, Http#http_request{id=partial}, Data}
581    end.
582
583%%--------------------------------------------------------------------
584http_method("get")-> 'GET';
585http_method("post")-> 'POST';
586http_method("head")-> 'HEAD';
587http_method("put")-> 'PUT';
588http_method("delete")-> 'DELETE';
589http_method("connect")-> 'CONNECT';
590http_method("propfind")-> 'PROPFIND';
591http_method("proppatch")-> 'PROPPATCH';
592http_method("copy")-> 'COPY';
593http_method("move")-> 'MOVE';
594http_method("lock")-> 'LOCK';
595http_method("unlock")-> 'UNLOCK';
596http_method("mkcol")-> 'MKCOL';
597http_method("mkactivity")-> 'MKACTIVITY';
598http_method("report")-> 'REPORT';
599http_method("options")-> 'OPTIONS';
600http_method("checkout")-> 'CHECKOUT';
601http_method("merge")-> 'MERGE';
602http_method("patch")-> 'PATCH';
603http_method(Method) ->
604    ?LOGF("Unknown  HTTP method: ~p~n", [Method] ,?WARN),
605    not_implemented.
606
607%%--------------------------------------------------------------------
608%% Func: parse_status/2
609%% Purpose: Parse HTTP status
610%% Returns: #http
611%%--------------------------------------------------------------------
612parse_status([A,B,C|_], Http=#http{status={Prev,_}}) ->
613    Status=list_to_integer([A,B,C]),
614    ?DebugF("HTTP Status ~p~n",[Status]),
615    ts_mon_cache:add({ count, Status }),
616    Http#http{status={Status,Prev}}.
617
618%%--------------------------------------------------------------------
619%% Func: parse_line/3
620%% Purpose: Parse a HTTP header
621%% Returns: #http
622%%--------------------------------------------------------------------
623parse_line("http/1.1 " ++ TailLine, Http, _Host )->
624    parse_status(TailLine, Http);
625parse_line("http/1.0 " ++ TailLine, Http, _Host)->
626    parse_status(TailLine, Http#http{close=true});
627
628parse_line("content-length: "++Tail, Http, _Host) when hd(Tail) /= $\s ->
629    %% tuning: handle common case (single LWS) to avoid a call to string:strip
630    CL = list_to_integer(Tail),
631    ?DebugF("HTTP Content-Length ~p~n",[CL]),
632    Http#http{content_length=CL};
633parse_line("content-length:  "++Tail, Http, _Host)-> % multiple white spaces
634    CL = list_to_integer(string:strip(Tail)),
635    ?DebugF("HTTP Content-Length ~p~n",[CL]),
636    Http#http{content_length=CL};
637parse_line("connection: close"++_Tail, Http, _Host)->
638    ?Debug("Connection Closed in Header ~n"),
639    Http#http{close=true};
640parse_line("content-encoding: "++Tail, Http=#http{compressed={Prev,_}}, _Host)->
641    ?DebugF("content encoding:~p ~n",[Tail]),
642    Http#http{compressed={list_to_atom(Tail),Prev}};
643parse_line("transfer-encoding:"++Tail, Http, _Host)->
644    ?DebugF("~p transfer encoding~n",[Tail]),
645    case string:strip(Tail) of
646        [C|"hunked"++_] when C == $C; C == $c ->
647            Http#http{chunk_toread=0};
648        _ ->
649            ?LOGF("Unknown transfer encoding ~p~n",[Tail],?NOTICE),
650            Http
651    end;
652parse_line("set-cookie: "++Tail, Http=#http{cookie=PrevCookies}, Host)->
653    Cookie = add_new_cookie(Tail, Host, PrevCookies),
654    ?DebugF("HTTP New cookie val ~p~n",[Cookie]),
655    Http#http{cookie=Cookie};
656parse_line("proxy-connection: keep-alive"++_Tail, Http, _Host)->
657    Http#http{close=false};
658parse_line("connection: Keep-Alive"++_Tail, Http, _Host)->
659    Http#http{close=false};
660parse_line(_Line, Http, _Host) ->
661    ?DebugF("Skip header ~p (Http record is ~p)~n", [_Line, Http]),
662    Http.
663
664%% code taken from yaws
665is_nb_space(X) ->
666    lists:member(X, [$\s, $\t]).
667% ret: {line, Line, Trail} | {lastline, Line, Trail}
668get_line(L) ->
669    get_line(L, true, []).
670get_line("\r\n\r\n" ++ Tail, _Cap, Cur) ->
671    {lastline, lists:reverse(Cur), Tail};
672get_line("\r\n", _, _) ->
673    {more};
674get_line("\r\n" ++ Tail, Cap, Cur) ->
675    case is_nb_space(hd(Tail)) of
676        true ->  %% multiline ... continue
677            get_line(Tail, Cap,[$\n, $\r | Cur]);
678        false ->
679            {line, lists:reverse(Cur), Tail}
680    end;
681get_line([$:|T], true, Cur) -> % ':' separator
682    get_line(T, false, [$:|Cur]);%the rest of the header isn't set to lower char
683get_line([H|T], false, Cur) ->
684    get_line(T, false, [H|Cur]);
685get_line([Char|T], true, Cur) when Char >= $A, Char =< $Z ->
686    get_line(T, true, [Char + 32|Cur]);
687get_line([H|T], true, Cur) ->
688    get_line(T, true, [H|Cur]);
689get_line([], _, _) -> %% Headers are fragmented ... We need more data
690    {more}.
691
692%% we need to keep the compressed value of the current request
693reset_session(#http{user_agent=UA,session_cookies=Cookies,
694                    compressed={Compressed,_}, status= {Status,_}, chunk_toread=Val}) when Val > -1 ->
695    #http{session_cookies=Cookies,user_agent=UA,compressed={false,Compressed}, chunk_toread=-2, status={none,Status}} ;
696reset_session(#http{user_agent=UA,session_cookies=Cookies,
697                    compressed={Compressed,_}, status= {Status,_}})  ->
698    #http{session_cookies=Cookies,user_agent=UA,compressed={false,Compressed}, status={none,Status}}.
699
700log_error(protocol,Error) ->
701    put(protocol_error,Error),
702    log_error2(protocol,Error);
703log_error(Type,Error) ->
704    log_error2(Type,Error).
705log_error2(_,Error)->
706    ts_mon_cache:add({count, Error}).
707