616 lines
22 KiB
Erlang
616 lines
22 KiB
Erlang
%% @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.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)).
|
|
|