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