%% @author Andy Gross %% @author Justin Sheehy %% @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 '). -author('Andy Gross '). -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.5"). -define(QUIP, "hack the charles gibson"). % 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=#wm_reqdata{req_body=RBody}}, _From, State) -> TheRD = case RBody of not_fetched_yet -> OldRD = State#state.reqdata, OldBody = OldRD#wm_reqdata.req_body, RD#wm_reqdata{req_body=OldBody}; _ -> RD end, {reply, ok, State#state{reqdata=TheRD}}; 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, MaxRecvBody}, _From, State0=#state{reqdata=RD0}) -> RD=RD0#wm_reqdata{max_recv_body=MaxRecvBody}, State=State0#state{reqdata=RD}, {Body, FinalState} = case RD#wm_reqdata.req_body of not_fetched_yet -> NewBody = do_recv_body(State), NewRD = RD#wm_reqdata{req_body=NewBody}, {NewBody, State#state{bodyfetch=standard,reqdata=NewRD}}; 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, recv_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} = case Code of 200 -> 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 = case wrq:resp_body(State#state.reqdata) of undefined -> false; <<>> -> false; [] -> false; _ -> true end, {reply, Reply, State}; handle_call({get_metadata, Key}, _From, State) -> Reply = case dict:find(Key, State#state.metadata) of {ok, Value} -> Value; error -> undefined end, {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,HostTokens,Port, PathTokens,AppRoot,DispPath,WMReq}, _From, State) -> PathInfo = dict:from_list(PathProps), NewState = State#state{reqdata=wrq:load_dispatch_data( PathInfo,HostTokens,Port,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) -> case State#state.peer of undefined -> Socket = State#state.socket, Peer = case inet:peername(Socket) of {ok, {Addr={10, _, _, _}, _Port}} -> case get_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}} -> case get_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) -> case gen_tcp:send(Socket, iolist_to_binary(Data)) of ok -> ok; {error,closed} -> ok; _ -> exit(normal) end. send_stream_body(Socket, X) -> send_stream_body(Socket, X, 0). send_stream_body(Socket, {<<>>, done}, SoFar) -> send_chunk(Socket, <<>>), SoFar; send_stream_body(Socket, {Data, done}, SoFar) -> Size = send_chunk(Socket, Data), send_chunk(Socket, <<>>), Size + SoFar; send_stream_body(Socket, {<<>>, Next}, SoFar) -> send_stream_body(Socket, Next(), SoFar); send_stream_body(Socket, {[], Next}, SoFar) -> send_stream_body(Socket, Next(), SoFar); send_stream_body(Socket, {Data, Next}, SoFar) -> Size = send_chunk(Socket, Data), send_stream_body(Socket, Next(), Size + SoFar). send_chunk(Socket, Data) -> Size = iolist_size(Data), send(Socket, mochihex:to_hex(Size)), send(Socket, <<"\r\n">>), send(Socket, Data), send(Socket, <<"\r\n">>), Size. send_ok_response(200, InitState) -> RD0 = InitState#state.reqdata, {Range, State} = get_range(InitState), case Range of X when X =:= undefined; X =:= fail -> send_response(200, State); Ranges -> {PartList, Size} = range_parts(RD0, Ranges), case PartList of [] -> %% no valid ranges %% could be 416, for now we'll just return 200 send_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) end end. send_response(Code, State=#state{reqdata=RD}) -> Body0 = wrq:resp_body(RD), {Body,Length} = case Body0 of {stream, StreamBody} -> {StreamBody, chunked}; _ -> {Body0, iolist_size([Body0])} end, send(State#state.socket, [make_version(wrq:version(RD)), make_code(Code), <<"\r\n">> | make_headers(Code, Length, RD)]), FinalLength = case wrq:method(RD) of 'HEAD' -> Length; _ -> case Length of chunked -> send_stream_body(State#state.socket, Body); _ -> send(State#state.socket, Body), Length end end, InitLogData = State#state.log_data, FinalLogData = InitLogData#wm_log_data{response_code=Code, response_length=FinalLength}, {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) -> case get_header_value("transfer-encoding", State) of {undefined, _} -> case get_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()) -> binary() %% @doc Receive the body of the HTTP request (defined by Content-Length). %% Will only receive up to the default max-body length do_recv_body(State=#state{reqdata=RD}) -> MRB = RD#wm_reqdata.max_recv_body, read_whole_stream(recv_stream_body(State, MRB), [], MRB, 0). read_whole_stream({Hunk,_}, _, MaxRecvBody, SizeAcc) when SizeAcc + byte_size(Hunk) > MaxRecvBody -> {error, req_body_too_large}; read_whole_stream({Hunk,Next}, Acc0, MaxRecvBody, SizeAcc) -> HunkSize = byte_size(Hunk), if SizeAcc + HunkSize > MaxRecvBody -> {error, req_body_too_large}; true -> Acc = [Hunk|Acc0], case Next of done -> iolist_to_binary(lists:reverse(Acc)); _ -> read_whole_stream(Next(), Acc, MaxRecvBody, SizeAcc + HunkSize) end end. recv_stream_body(State = #state{reqdata=RD}, MaxHunkSize) -> case get_header_value("expect", State) of {"100-continue", _} -> send(State#state.socket, [make_version(wrq:version(RD)), make_code(100), <<"\r\n">>]); _Else -> ok end, case body_length(State) of {unknown_transfer_encoding, X} -> exit({unknown_transfer_encoding, X}); undefined -> {<<>>, done}; 0 -> {<<>>, done}; chunked -> recv_chunked_body(State#state.socket, MaxHunkSize); Length -> recv_unchunked_body(State#state.socket, MaxHunkSize, Length) end. recv_unchunked_body(Socket, MaxHunk, DataLeft) -> case MaxHunk >= DataLeft of true -> {ok,Data1} = gen_tcp:recv(Socket,DataLeft,?IDLE_TIMEOUT), {Data1, done}; false -> {ok,Data2} = gen_tcp:recv(Socket,MaxHunk,?IDLE_TIMEOUT), {Data2, fun() -> recv_unchunked_body( Socket, MaxHunk, DataLeft-MaxHunk) end} end. recv_chunked_body(Socket, MaxHunk) -> case read_chunk_length(Socket) of 0 -> {<<>>, done}; ChunkLength -> recv_chunked_body(Socket,MaxHunk,ChunkLength) end. recv_chunked_body(Socket, MaxHunk, LeftInChunk) -> case MaxHunk >= LeftInChunk of true -> {ok,Data1} = gen_tcp:recv(Socket,LeftInChunk,?IDLE_TIMEOUT), {Data1, fun() -> recv_chunked_body(Socket, MaxHunk) end}; false -> {ok,Data2} = gen_tcp:recv(Socket,MaxHunk,?IDLE_TIMEOUT), {Data2, fun() -> recv_chunked_body(Socket, MaxHunk, LeftInChunk-MaxHunk) end} end. read_chunk_length(Socket) -> inet:setopts(Socket, [{packet, line}]), case gen_tcp:recv(Socket, 0, ?IDLE_TIMEOUT) of {ok, Header} -> inet:setopts(Socket, [{packet, raw}]), Splitter = fun (C) -> C =/= $\r andalso C =/= $\n andalso C =/= $ end, {Hex, _Rest} = lists:splitwith(Splitter, binary_to_list(Header)), case Hex of [] -> 0; _ -> erlang:list_to_integer(Hex, 16) end; _ -> exit(normal) end. get_range(State) -> case get_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(_RD=#wm_reqdata{resp_body={file, IoDevice}}, Ranges) -> Size = iodevice_size(IoDevice), F = fun (Spec, Acc) -> case range_skip_length(Spec, Size) of invalid_range -> Acc; V -> [V | Acc] end end, 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(RD=#wm_reqdata{resp_body={stream, {Hunk,Next}}}, Ranges) -> % for now, streamed bodies are read in full for range requests MRB = RD#wm_reqdata.max_recv_body, range_parts(read_whole_stream({Hunk,Next}, [], MRB, 0), Ranges); range_parts(_RD=#wm_reqdata{resp_body=Body0}, Ranges) -> Body = iolist_to_binary(Body0), Size = size(Body), F = fun(Spec, Acc) -> case range_skip_length(Spec, Size) of invalid_range -> Acc; {Skip, Length} -> <<_:Skip/binary, PartialBody:Length/binary, _/binary>> = Body, [{Skip, Skip + Length - 1, PartialBody} | Acc] end end, {lists:foldr(F, [], Ranges), Size}. range_skip_length(Spec, Size) -> case Spec of {none, R} when R =< Size, R >= 0 -> {Size - R, R}; {none, _OutOfRange} -> {0, Size}; {R, none} when R >= 0, R < Size -> {R, Size - R}; {_OutOfRange, none} -> invalid_range; {Start, End} when 0 =< Start, Start =< End, End < Size -> {Start, End - Start + 1}; {_OutOfRange, _End} -> invalid_range end. parse_range_request(RawRange) when is_list(RawRange) -> try "bytes=" ++ RangeString = RawRange, Ranges = string:tokens(RangeString, ","), lists:map(fun ("-" ++ V) -> {none, list_to_integer(V)}; (R) -> case string:tokens(R, "-") of [S1, S2] -> {list_to_integer(S1), list_to_integer(S2)}; [S] -> {list_to_integer(S), none} end end, Ranges) catch _:_ -> fail end. parts_to_body([{Start, End, Body}], State, Size) -> %% return body for a range reponse with a single body ContentType = case get_outheader_value("content-type", State) of {undefined, _} -> "text/html"; {CT, _} -> CT end, HeaderList = [{"Content-Type", ContentType}, {"Content-Range", ["bytes ", make_io(Start), "-", make_io(End), "/", make_io(Size)]}], {HeaderList, Body}; parts_to_body(BodyList, State, Size) when is_list(BodyList) -> %% return %% header Content-Type: multipart/byteranges; boundary=441934886133bdee4 %% and multipart body ContentType = case get_outheader_value("content-type", State) of {undefined, _} -> "text/html"; {CT, _} -> CT end, 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) when is_atom(Atom) -> atom_to_list(Atom); make_io(Integer) when is_integer(Integer) -> integer_to_list(Integer); make_io(Io) when is_list(Io); is_binary(Io) -> Io. make_code(X) when is_integer(X) -> [integer_to_list(X), [" " | httpd_util:reason_phrase(X)]]; make_code(Io) when is_list(Io); is_binary(Io) -> Io. make_version({1, 0}) -> <<"HTTP/1.0 ">>; make_version(_) -> <<"HTTP/1.1 ">>. make_headers(Code, Length, RD) -> Hdrs0 = case Code of 304 -> mochiweb_headers:make(wrq:resp_headers(RD)); _ -> case Length of chunked -> mochiweb_headers:enter( "Transfer-Encoding","chunked", mochiweb_headers:make(wrq:resp_headers(RD))); _ -> mochiweb_headers:enter( "Content-Length",integer_to_list(Length), mochiweb_headers:make(wrq:resp_headers(RD))) end end, ServerHeader = "MochiWeb/1.1 WebMachine/" ++ ?WMVSN ++ " (" ++ ?QUIP ++ ")", WithSrv = mochiweb_headers:enter("Server", ServerHeader, Hdrs0), Hdrs = case mochiweb_headers:get_value("date", WithSrv) of undefined -> mochiweb_headers:enter("Date", httpd_util:rfc1123_date(), WithSrv); _ -> WithSrv end, F = fun({K, V}, Acc) -> [make_io(K), <<": ">>, V, <<"\r\n">> | Acc] end, lists:foldl(F, [<<"\r\n">>], mochiweb_headers:to_list(Hdrs)).