%% @author Andy Gross <andy@basho.com> %% @author Justin Sheehy <justin@basho.com>%% @copyright 2007-2009 Basho Technologies%% Portions derived from code Copyright 2007-2008 Bob Ippolito, Mochi Media%%%% Licensed under the Apache License, Version 2.0 (the "License");%% you may not use this file except in compliance with the License.%% You may obtain a copy of the License at%%%% http://www.apache.org/licenses/LICENSE-2.0%%%% Unless required by applicable law or agreed to in writing, software%% distributed under the License is distributed on an "AS IS" BASIS,%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.%% See the License for the specific language governing permissions and%% limitations under the License.-module(webmachine_request_srv).-author('Justin Sheehy <justin@basho.com>').-author('Andy Gross <andy@basho.com>').-behaviour(gen_server).-export([start_link/5]).-export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2,code_change/3]).-include("webmachine_logger.hrl").-include_lib("include/wm_reqdata.hrl").-define(WMVSN,"1.3").-define(QUIP,"scale automagically into the cloud").% Maximum recv_body() length of 50MB-define(MAX_RECV_BODY,(50*(1024*1024))).% 120 second default idle timeout-define(IDLE_TIMEOUT,infinity).-record(state,{socket=undefined,metadata=dict:new(),range=undefined,peer=undefined,reqdata=undefined,bodyfetch=undefined,log_data=#wm_log_data{}}).start_link(Socket,Method,RawPath,Version,Headers)->gen_server:start_link(?MODULE,[Socket,Method,RawPath,Version,Headers],[]).init([Socket,Method,RawPath,Version,Headers])->%%process_flag(trap_exit, true),%% Calling get_peer() here is a little bit of an ugly way to populate the%% client IP address but it will do for now.{Peer,State}=get_peer(#state{socket=Socket,reqdata=wrq:create(Method,Version,RawPath,Headers)}),PeerState=State#state{reqdata=wrq:set_peer(Peer,State#state.reqdata)},LogData=#wm_log_data{start_time=now(),method=Method,headers=Headers,peer=State#state.peer,path=RawPath,version=Version,response_code=404,response_length=0},{ok,PeerState#state{log_data=LogData}}.handle_call(socket,_From,State)->Reply=State#state.socket,{reply,Reply,State};handle_call(get_reqdata,_From,State)->{reply,State#state.reqdata,State};handle_call({set_reqdata,RD},_From,State)->{reply,ok,State#state{reqdata=RD}};handle_call(method,_From,State)->{reply,wrq:method(State#state.reqdata),State};handle_call(version,_From,State)->{reply,wrq:version(State#state.reqdata),State};handle_call(raw_path,_From,State)->{reply,wrq:raw_path(State#state.reqdata),State};handle_call(req_headers,_From,State)->{reply,wrq:req_headers(State#state.reqdata),State};handle_call(req_body,_From,State=#state{bodyfetch=stream})->{reply,stream_conflict,State};handle_call(req_body,_From,State=#state{reqdata=RD})->{Body,FinalState}=caseRD#wm_reqdata.req_bodyofnot_fetched_yet->NewState=do_recv_body(State),NewRD=NewState#state.reqdata,{NewRD#wm_reqdata.req_body,NewState#state{bodyfetch=standard}};X->{X,State#state{bodyfetch=standard}}end,{reply,Body,FinalState};handle_call({stream_req_body,_},_From,State=#state{bodyfetch=standard})->{reply,stream_conflict,State};handle_call({stream_req_body,MaxHunk},_From,State)->{reply,do_stream_body(State,MaxHunk),State#state{bodyfetch=stream}};handle_call(resp_headers,_From,State)->{reply,wrq:resp_headers(State#state.reqdata),State};handle_call(resp_redirect,_From,State)->{reply,wrq:resp_redirect(State#state.reqdata),State};handle_call({get_resp_header,HdrName},_From,State)->Reply=mochiweb_headers:get_value(HdrName,wrq:resp_headers(State#state.reqdata)),{reply,Reply,State};handle_call(get_path_info,_From,State)->PropList=dict:to_list(wrq:path_info(State#state.reqdata)),{reply,PropList,State};handle_call({get_path_info,Key},_From,State)->{reply,wrq:path_info(Key,State#state.reqdata),State};handle_call(peer,_From,State)->{Reply,NewState}=get_peer(State),{reply,Reply,NewState};handle_call(range,_From,State)->{Reply,NewState}=get_range(State),{reply,Reply,NewState};handle_call(response_code,_From,State)->{reply,wrq:response_code(State#state.reqdata),State};handle_call(app_root,_From,State)->{reply,wrq:app_root(State#state.reqdata),State};handle_call(disp_path,_From,State)->{reply,wrq:disp_path(State#state.reqdata),State};handle_call(path,_From,State)->{reply,wrq:path(State#state.reqdata),State};handle_call({get_req_header,K},_From,State)->{reply,wrq:get_req_header(K,State#state.reqdata),State};handle_call({set_response_code,Code},_From,State)->NewState=State#state{reqdata=wrq:set_response_code(Code,State#state.reqdata)},{reply,ok,NewState};handle_call({set_resp_header,K,V},_From,State)->NewState=State#state{reqdata=wrq:set_resp_header(K,V,State#state.reqdata)},{reply,ok,NewState};handle_call({set_resp_headers,Hdrs},_From,State)->NewState=State#state{reqdata=wrq:set_resp_headers(Hdrs,State#state.reqdata)},{reply,ok,NewState};handle_call({remove_resp_header,K},_From,State)->NewState=State#state{reqdata=wrq:remove_resp_header(K,State#state.reqdata)},{reply,ok,NewState};handle_call({merge_resp_headers,Hdrs},_From,State)->NewState=State#state{reqdata=wrq:merge_resp_headers(Hdrs,State#state.reqdata)},{reply,ok,NewState};handle_call({append_to_response_body,Data},_From,State)->NewState=State#state{reqdata=wrq:append_to_response_body(Data,State#state.reqdata)},{reply,ok,NewState};handle_call({set_disp_path,P},_From,State)->NewState=State#state{reqdata=wrq:set_disp_path(P,State#state.reqdata)},{reply,ok,NewState};handle_call(do_redirect,_From,State)->NewState=State#state{reqdata=wrq:do_redirect(true,State#state.reqdata)},{reply,ok,NewState};handle_call({send_response,Code},_From,State)->{Reply,NewState}=caseCodeof200->send_ok_response(Code,State);_->send_response(Code,State)end,LogData=NewState#state.log_data,NewLogData=LogData#wm_log_data{finish_time=now()},{reply,Reply,NewState#state{log_data=NewLogData}};handle_call(resp_body,_From,State)->{reply,wrq:resp_body(State#state.reqdata),State};handle_call({set_resp_body,Body},_From,State)->NewState=State#state{reqdata=wrq:set_resp_body(Body,State#state.reqdata)},{reply,ok,NewState};handle_call(has_resp_body,_From,State)->Reply=casewrq:resp_body(State#state.reqdata)ofundefined->false;<<>>->false;[]->false;_->trueend,{reply,Reply,State};handle_call({get_metadata,Key},_From,State)->Reply=casedict:find(Key,State#state.metadata)of{ok,Value}->Value;error->undefinedend,{reply,Reply,State};handle_call({set_metadata,Key,Value},_From,State)->NewDict=dict:store(Key,Value,State#state.metadata),{reply,ok,State#state{metadata=NewDict}};handle_call(path_tokens,_From,State)->{reply,wrq:path_tokens(State#state.reqdata),State};handle_call(req_cookie,_From,State)->{reply,wrq:req_cookie(State#state.reqdata),State};handle_call(req_qs,_From,State)->{reply,wrq:req_qs(State#state.reqdata),State};handle_call({load_dispatch_data,PathProps,PathTokens,AppRoot,DispPath,WMReq},_From,State)->PathInfo=dict:from_list(PathProps),NewState=State#state{reqdata=wrq:load_dispatch_data(PathInfo,PathTokens,AppRoot,DispPath,WMReq,State#state.reqdata)},{reply,ok,NewState};handle_call(log_data,_From,State)->{reply,State#state.log_data,State}.handle_cast(stop,State)->{stop,normal,State}.handle_info(_Info,State)->{noreply,State}.terminate(_Reason,_State)->ok.code_change(_OldVsn,State,_Extra)->{ok,State}.get_peer(State)->caseState#state.peerofundefined->Socket=State#state.socket,Peer=caseinet:peername(Socket)of{ok,{Addr={10,_,_,_},_Port}}->caseget_header_value("x-forwarded-for",State)of{undefined,_}->inet_parse:ntoa(Addr);{Hosts,_}->string:strip(lists:last(string:tokens(Hosts,",")))end;{ok,{{127,0,0,1},_Port}}->caseget_header_value("x-forwarded-for",State)of{undefined,_}->"127.0.0.1";{Hosts,_}->string:strip(lists:last(string:tokens(Hosts,",")))end;{ok,{Addr,_Port}}->inet_parse:ntoa(Addr)end,NewState=State#state{peer=Peer},{Peer,NewState};_->{State#state.peer,State}end.get_header_value(K,State)->{wrq:get_req_header(K,State#state.reqdata),State}.get_outheader_value(K,State)->{mochiweb_headers:get_value(K,wrq:resp_headers(State#state.reqdata)),State}.send(Socket,Data)->casegen_tcp:send(Socket,Data)ofok->ok;{error,closed}->ok;_->exit(normal)end.send_ok_response(200,InitState)->RD0=InitState#state.reqdata,{Range,State}=get_range(InitState),caseRangeofXwhenX=:=undefined;X=:=fail->send_response(200,State);Ranges->{PartList,Size}=range_parts(wrq:resp_body(RD0),Ranges),casePartListof[]->%% no valid ranges%% could be 416, for now we'll just return 200send_response(200,State);PartList->{RangeHeaders,RangeBody}=parts_to_body(PartList,State,Size),RespHdrsRD=wrq:set_resp_headers([{"Accept-Ranges","bytes"}|RangeHeaders],RD0),RespBodyRD=wrq:set_resp_body(RangeBody,RespHdrsRD),NewState=State#state{reqdata=RespBodyRD},send_response(206,NewState)endend.send_response(Code,State=#state{reqdata=RD})->Length=iolist_size([wrq:resp_body(RD)]),send(State#state.socket,[make_version(wrq:version(RD)),make_code(Code),<<"\r\n">>|make_headers(Code,Length,RD)]),casewrq:method(RD)of'HEAD'->ok;_->send(State#state.socket,[wrq:resp_body(RD)])end,InitLogData=State#state.log_data,FinalLogData=InitLogData#wm_log_data{response_code=Code,response_length=Length},{ok,State#state{reqdata=wrq:set_response_code(Code,RD),log_data=FinalLogData}}.%% @spec body_length(state()) -> undefined | chunked | unknown_transfer_encoding | integer()%% @doc Infer body length from transfer-encoding and content-length headers.body_length(State)->caseget_header_value("transfer-encoding",State)of{undefined,_}->caseget_header_value("content-length",State)of{undefined,_}->undefined;{Length,_}->list_to_integer(Length)end;{"chunked",_}->chunked;Unknown->{unknown_transfer_encoding,Unknown}end.%% @spec do_recv_body(state()) -> state()%% @doc Receive the body of the HTTP request (defined by Content-Length).%% Will only receive up to the default max-body lengthdo_recv_body(State=#state{reqdata=RD})->State#state{reqdata=wrq:set_req_body(read_whole_stream(do_stream_body(State,?MAX_RECV_BODY),[]),RD)}.read_whole_stream({Hunk,Next},Acc0)->Acc=[Hunk|Acc0],caseNextofdone->iolist_to_binary(lists:reverse(Acc));_->read_whole_stream(Next(),Acc)end.do_stream_body(State=#state{reqdata=RD},MaxHunkSize)->caseget_header_value("expect",State)of{"100-continue",_}->send(State#state.socket,[make_version(wrq:version(RD)),make_code(100),<<"\r\n">>]);_Else->okend,casebody_length(State)of{unknown_transfer_encoding,X}->exit({unknown_transfer_encoding,X});undefined->{<<>>,done};0->{<<>>,done};chunked->stream_chunked_body(State#state.socket,MaxHunkSize);Length->stream_unchunked_body(State#state.socket,MaxHunkSize,Length)end.stream_unchunked_body(Socket,MaxHunk,DataLeft)->caseMaxHunk>=DataLeftoftrue->{ok,Data1}=gen_tcp:recv(Socket,DataLeft,?IDLE_TIMEOUT),{Data1,done};false->{ok,Data2}=gen_tcp:recv(Socket,MaxHunk,?IDLE_TIMEOUT),{Data2,fun()->stream_unchunked_body(Socket,MaxHunk,DataLeft-MaxHunk)end}end.stream_chunked_body(Socket,MaxHunk)->caseread_chunk_length(Socket)of0->{<<>>,done};ChunkLength->stream_chunked_body(Socket,MaxHunk,ChunkLength)end.stream_chunked_body(Socket,MaxHunk,LeftInChunk)->caseMaxHunk>=LeftInChunkoftrue->{ok,Data1}=gen_tcp:recv(Socket,LeftInChunk,?IDLE_TIMEOUT),{Data1,fun()->stream_chunked_body(Socket,MaxHunk)end};false->{ok,Data2}=gen_tcp:recv(Socket,MaxHunk,?IDLE_TIMEOUT),{Data2,fun()->stream_chunked_body(Socket,MaxHunk,LeftInChunk-MaxHunk)end}end.read_chunk_length(Socket)->inet:setopts(Socket,[{packet,line}]),casegen_tcp:recv(Socket,0,?IDLE_TIMEOUT)of{ok,Header}->inet:setopts(Socket,[{packet,raw}]),Splitter=fun(C)->C=/=$\randalsoC=/=$\nandalsoC=/=$end,{Hex,_Rest}=lists:splitwith(Splitter,binary_to_list(Header)),caseHexof[]->0;_->erlang:list_to_integer(Hex,16)end;_->exit(normal)end.get_range(State)->caseget_header_value("range",State)of{undefined,_}->{undefined,State#state{range=undefined}};{RawRange,_}->Range=parse_range_request(RawRange),{Range,State#state{range=Range}}end.range_parts({file,IoDevice},Ranges)->Size=iodevice_size(IoDevice),F=fun(Spec,Acc)->caserange_skip_length(Spec,Size)ofinvalid_range->Acc;V->[V|Acc]endend,LocNums=lists:foldr(F,[],Ranges),{ok,Data}=file:pread(IoDevice,LocNums),Bodies=lists:zipwith(fun({Skip,Length},PartialBody)->{Skip,Skip+Length-1,PartialBody}end,LocNums,Data),{Bodies,Size};range_parts(Body0,Ranges)->Body=iolist_to_binary(Body0),Size=size(Body),F=fun(Spec,Acc)->caserange_skip_length(Spec,Size)ofinvalid_range->Acc;{Skip,Length}-><<_:Skip/binary,PartialBody:Length/binary,_/binary>>=Body,[{Skip,Skip+Length-1,PartialBody}|Acc]endend,{lists:foldr(F,[],Ranges),Size}.range_skip_length(Spec,Size)->caseSpecof{none,R}whenR=<Size,R>=0->{Size-R,R};{none,_OutOfRange}->{0,Size};{R,none}whenR>=0,R<Size->{R,Size-R};{_OutOfRange,none}->invalid_range;{Start,End}when0=<Start,Start=<End,End<Size->{Start,End-Start+1};{_OutOfRange,_End}->invalid_rangeend.parse_range_request(RawRange)whenis_list(RawRange)->try"bytes="++RangeString=RawRange,Ranges=string:tokens(RangeString,","),lists:map(fun("-"++V)->{none,list_to_integer(V)};(R)->casestring:tokens(R,"-")of[S1,S2]->{list_to_integer(S1),list_to_integer(S2)};[S]->{list_to_integer(S),none}endend,Ranges)catch_:_->failend.parts_to_body([{Start,End,Body}],State,Size)->%% return body for a range reponse with a single bodyContentType=caseget_outheader_value("content-type",State)of{undefined,_}->"text/html";{CT,_}->CTend,HeaderList=[{"Content-Type",ContentType},{"Content-Range",["bytes ",make_io(Start),"-",make_io(End),"/",make_io(Size)]}],{HeaderList,Body};parts_to_body(BodyList,State,Size)whenis_list(BodyList)->%% return%% header Content-Type: multipart/byteranges; boundary=441934886133bdee4%% and multipart bodyContentType=caseget_outheader_value("content-type",State)of{undefined,_}->"text/html";{CT,_}->CTend,Boundary=mochihex:to_hex(crypto:rand_bytes(8)),HeaderList=[{"Content-Type",["multipart/byteranges; ","boundary=",Boundary]}],MultiPartBody=multipart_body(BodyList,ContentType,Boundary,Size),{HeaderList,MultiPartBody}.multipart_body([],_ContentType,Boundary,_Size)->["--",Boundary,"--\r\n"];multipart_body([{Start,End,Body}|BodyList],ContentType,Boundary,Size)->["--",Boundary,"\r\n","Content-Type: ",ContentType,"\r\n","Content-Range: ","bytes ",make_io(Start),"-",make_io(End),"/",make_io(Size),"\r\n\r\n",Body,"\r\n"|multipart_body(BodyList,ContentType,Boundary,Size)].iodevice_size(IoDevice)->{ok,Size}=file:position(IoDevice,eof),{ok,0}=file:position(IoDevice,bof),Size.make_io(Atom)whenis_atom(Atom)->atom_to_list(Atom);make_io(Integer)whenis_integer(Integer)->integer_to_list(Integer);make_io(Io)whenis_list(Io);is_binary(Io)->Io.make_code(X)whenis_integer(X)->[integer_to_list(X),[" "|httpd_util:reason_phrase(X)]];make_code(Io)whenis_list(Io);is_binary(Io)->Io.make_version({1,0})-><<"HTTP/1.0 ">>;make_version(_)-><<"HTTP/1.1 ">>.make_headers(Code,Length,RD)->Hdrs0=caseCodeof304->mochiweb_headers:make(wrq:resp_headers(RD));_->mochiweb_headers:enter("Content-Length",integer_to_list(Length),mochiweb_headers:make(wrq:resp_headers(RD)))end,ServerHeader="MochiWeb/1.1 WebMachine/"++?WMVSN++" ("++?QUIP++")",WithSrv=mochiweb_headers:enter("Server",ServerHeader,Hdrs0),Hdrs=casemochiweb_headers:get_value("date",WithSrv)ofundefined->mochiweb_headers:enter("Date",httpd_util:rfc1123_date(),WithSrv);_->WithSrvend,F=fun({K,V},Acc)->[make_io(K),<<": ">>,V,<<"\r\n">>|Acc]end,lists:foldl(F,[<<"\r\n">>],mochiweb_headers:to_list(Hdrs)).