diff --git a/include/wm_reqdata.hrl b/include/wm_reqdata.hrl index 5a2f710..0f83b06 100644 --- a/include/wm_reqdata.hrl +++ b/include/wm_reqdata.hrl @@ -25,6 +25,10 @@ req_body, resp_redirect :: boolean(), resp_headers, + resp_content_encoding :: string(), + resp_transfer_encoding :: undefined | {string(),function()}, + resp_content_type :: string(), + resp_chosen_charset :: string(), resp_body :: any(), resp_range :: atom(), host_tokens :: [string()], diff --git a/src/webmachine_controller.erl b/src/webmachine_controller.erl index 3b177b1..476231a 100644 --- a/src/webmachine_controller.erl +++ b/src/webmachine_controller.erl @@ -81,15 +81,24 @@ default(process_post) -> false; default(language_available) -> true; + +% The default setting is needed for non-charset responses such as image/png +% An example of how one might do actual negotiation: +% ["iso-8859-1", "utf-8"]; default(charsets_provided) -> no_charset; % this atom causes charset-negotation to short-circuit -% the default setting is needed for non-charset responses such as image/png -% an example of how one might do actual negotiation -% [{"iso-8859-1", fun(X) -> X end}, {"utf-8", make_utf8}]; -default(encodings_provided) -> - [{"identity", fun(X) -> X end}]; -% this is handy for auto-gzip of GET-only resources: -% [{"identity", fun(X) -> X end}, {"gzip", fun(X) -> zlib:gzip(X) end}]; + +% The content variations available to the controller. +default(content_encodings_provided) -> + ["identity"]; + +% How the content is transferred, this is handy for auto-gzip of GET-only resources. +% "identity" and "chunked" are always available to HTTP/1.1 clients. +% Example: +% [{"gzip", fun(X) -> zlib:gzip(X) end}]; +default(transfer_encodings_provided) -> + [{"gzip", fun(X) -> zlib:gzip(X) end}]; + default(variances) -> []; default(is_conflict) -> diff --git a/src/webmachine_decision_core.erl b/src/webmachine_decision_core.erl index d2dffcc..5d19e15 100644 --- a/src/webmachine_decision_core.erl +++ b/src/webmachine_decision_core.erl @@ -38,22 +38,22 @@ handle_request(Resource, ReqData) -> %% @doc Call the controller or a default. %% @spec controller_call(atom(), Resource, ReqData) -> {term(), NewResource, NewReqData} controller_call(Fun, Rs, Rd) -> - case cacheable(Fun) of - true -> - case proplists:lookup(Fun, Rd#wm_reqdata.cache) of - none -> - {T, Rs1, Rd1} = webmachine_controller:do(Fun, Rs, Rd), - {T, Rs1, Rd1#wm_reqdata{cache=[{Fun,T}|Rd1#wm_reqdata.cache]}}; - {Fun, Cached} -> - {Cached, Rs, Rd} - end; - false -> - webmachine_controller:do(Fun, Rs, Rd) - end. + case cacheable(Fun) of + true -> + case proplists:lookup(Fun, Rd#wm_reqdata.cache) of + none -> + {T, Rs1, Rd1} = webmachine_controller:do(Fun, Rs, Rd), + {T, Rs1, Rd1#wm_reqdata{cache=[{Fun,T}|Rd1#wm_reqdata.cache]}}; + {Fun, Cached} -> + {Cached, Rs, Rd} + end; + false -> + webmachine_controller:do(Fun, Rs, Rd) + end. cacheable(charsets_provided) -> true; cacheable(content_types_provided) -> true; -cacheable(encodings_provided) -> true; +cacheable(content_encodings_provided) -> true; cacheable(last_modified) -> true; cacheable(generate_etag) -> true; cacheable(_) -> false. @@ -71,6 +71,11 @@ d(DecisionID, Rs, Rd) -> respond(Code, Rs, Rd) -> {RsCode, RdCode} = case Code of + 200 -> + case get_header_val("te", Rd) of + undefined -> {Rs,Rd}; + TEHdr -> choose_transfer_encoding(TEHdr, Rs, Rd) + end; Code when Code =:= 403; Code =:= 404; Code =:= 410 -> {ok, ErrorHandler} = application:get_env(webzmachine, error_handler), Reason = {none, none, []}, @@ -112,11 +117,11 @@ decision_test({Test, Rs, Rd}, TestVal, TrueFlow, FalseFlow) -> decision_test(Test,TestVal,TrueFlow,FalseFlow, Rs, Rd) -> case Test of - {error, Reason} -> error_response(Reason, Rs, Rd); - {error, Reason0, Reason1} -> error_response({Reason0, Reason1}, Rs, Rd); - {halt, Code} -> respond(Code, Rs, Rd); - TestVal -> decision_flow(TrueFlow, Test, Rs, Rd); - _ -> decision_flow(FalseFlow, Test, Rs, Rd) + {error, Reason} -> error_response(Reason, Rs, Rd); + {error, Reason0, Reason1} -> error_response({Reason0, Reason1}, Rs, Rd); + {halt, Code} -> respond(Code, Rs, Rd); + TestVal -> decision_flow(TrueFlow, Test, Rs, Rd); + _ -> decision_flow(FalseFlow, Test, Rs, Rd) end. decision_test_fn({Test, Rs, Rd}, TestFn, TrueFlow, FalseFlow) -> @@ -150,9 +155,9 @@ do_log(LogData) -> end, case application:get_env(webzmachine, perf_log_dir) of {ok, _} -> - spawn(webmachine_perf_logger, log, [LogData]); - _ -> - ignore + spawn(webmachine_perf_logger, log, [LogData]); + _ -> + ignore end. @@ -160,7 +165,7 @@ do_log(LogData) -> %% "Service Available" decision(v3b13, Rs, Rd) -> decision_test(controller_call(ping, Rs, Rd), pong, v3b13b, 503); -decision(v3b13b, Rs, Rd) -> +decision(v3b13b, Rs, Rd) -> decision_test(controller_call(service_available, Rs, Rd), true, v3b12, 503); %% "Known method?" decision(v3b12, Rs, Rd) -> @@ -173,11 +178,11 @@ decision(v3b11, Rs, Rd) -> decision(v3b10, Rs, Rd) -> {Methods, Rs1, Rd1} = controller_call(allowed_methods, Rs, Rd), case lists:member(method(Rd1), Methods) of - true -> - d(v3b9, Rs1, Rd1); - false -> - RdAllow = wrq:set_resp_header("Allow", string:join([atom_to_list(M) || M <- Methods], ", "), Rd1), - respond(405, Rs1, RdAllow) + true -> + d(v3b9, Rs1, Rd1); + false -> + RdAllow = wrq:set_resp_header("Allow", string:join([atom_to_list(M) || M <- Methods], ", "), Rd1), + respond(405, Rs1, RdAllow) end; %% "Malformed?" decision(v3b9, Rs, Rd) -> @@ -186,12 +191,12 @@ decision(v3b9, Rs, Rd) -> decision(v3b8, Rs, Rd) -> {IsAuthorized, Rs1, Rd1} = controller_call(is_authorized, Rs, Rd), case IsAuthorized of - true -> - d(v3b7, Rs1, Rd1); - {error, Reason} -> - error_response(Reason, Rs1, Rd1); - {halt, Code} -> - respond(Code, Rs1, Rd1); + true -> + d(v3b7, Rs1, Rd1); + {error, Reason} -> + error_response(Reason, Rs1, Rd1); + {halt, Code} -> + respond(Code, Rs1, Rd1); AuthHead -> RdAuth = wrq:set_resp_header("WWW-Authenticate", AuthHead, Rd1), respond(401, Rs1, RdAuth) @@ -202,28 +207,28 @@ decision(v3b7, Rs, Rd) -> %% "Upgrade?" decision(v3b6_upgrade, Rs, Rd) -> case get_header_val("upgrade", Rd) of - undefined -> - decision(v3b6, Rs, Rd); - UpgradeHdr -> - case get_header_val("connection", Rd) of - undefined -> - decision(v3b6, Rs, Rd); - Connection -> - case contains_token("upgrade", Connection) of - true -> - {Choosen, Rs1, Rd1} = choose_upgrade(UpgradeHdr, Rs, Rd), - case Choosen of - none -> - decision(v3b6, Rs1, Rd1); - {_Protocol, UpgradeFunc} -> - %% TODO: log the upgrade action - {upgrade, UpgradeFunc, Rs1, Rd1} - end; - false -> - decision(v3b6, Rs, Rd) - end - end - end; + undefined -> + decision(v3b6, Rs, Rd); + UpgradeHdr -> + case get_header_val("connection", Rd) of + undefined -> + decision(v3b6, Rs, Rd); + Connection -> + case contains_token("upgrade", Connection) of + true -> + {Choosen, Rs1, Rd1} = choose_upgrade(UpgradeHdr, Rs, Rd), + case Choosen of + none -> + decision(v3b6, Rs1, Rd1); + {_Protocol, UpgradeFunc} -> + %% TODO: log the upgrade action + {upgrade, UpgradeFunc, Rs1, Rd1} + end; + false -> + decision(v3b6, Rs, Rd) + end + end + end; %% "Okay Content-* Headers?" decision(v3b6, Rs, Rd) -> decision_test(controller_call(valid_content_headers, Rs, Rd), true, v3b5, 501); @@ -236,22 +241,22 @@ decision(v3b4, Rs, Rd) -> %% "OPTIONS?" decision(v3b3, Rs, Rd) -> case wrq:method(Rd) of - 'OPTIONS' -> - {Hdrs, Rs1, Rd1} = controller_call(options, Rs, Rd), - respond(200, Hdrs, Rs1, Rd1); - _ -> - d(v3c3, Rs, Rd) + 'OPTIONS' -> + {Hdrs, Rs1, Rd1} = controller_call(options, Rs, Rd), + respond(200, Hdrs, Rs1, Rd1); + _ -> + d(v3c3, Rs, Rd) end; %% Accept exists? decision(v3c3, Rs, Rd) -> {ContentTypes, Rs1, Rd1} = controller_call(content_types_provided, Rs, Rd), PTypes = [Type || {Type,_Fun} <- ContentTypes], case get_header_val("accept", Rd1) of - undefined -> - {ok, RdCT} = webmachine_request:set_metadata('content-type', hd(PTypes), Rd1), - d(v3d4, Rs1, RdCT); - _ -> - d(v3c4, Rs1, Rd1) + undefined -> + RdCT = wrq:set_resp_content_type(hd(PTypes), Rd1), + d(v3d4, Rs1, RdCT); + _ -> + d(v3c4, Rs1, Rd1) end; %% Acceptable media type available? decision(v3c4, Rs, Rd) -> @@ -259,11 +264,11 @@ decision(v3c4, Rs, Rd) -> PTypes = [Type || {Type,_Fun} <- ContentTypesProvided], AcceptHdr = get_header_val("accept", Rd1), case webmachine_util:choose_media_type(PTypes, AcceptHdr) of - none -> - respond(406, Rs1, Rd1); - MType -> - {ok, RdCT} = webmachine_request:set_metadata('content-type', MType, Rd1), - d(v3d4, Rs, RdCT) + none -> + respond(406, Rs1, Rd1); + MType -> + RdCT = wrq:set_resp_content_type(MType, Rd1), + d(v3d4, Rs, RdCT) end; %% Accept-Language exists? decision(v3d4, Rs, Rd) -> @@ -283,19 +288,19 @@ decision(v3e6, Rs, Rd) -> %% Accept-Encoding exists? % (also, set content-type header here, now that charset is chosen) decision(v3f6, Rs, Rd) -> - CType = webmachine_request:get_metadata('content-type', Rd), - CSet = case webmachine_request:get_metadata('chosen-charset', Rd) of + CType = wrq:resp_content_type(Rd), + CSet = case wrq:resp_chosen_charset(Rd) of undefined -> ""; CS -> "; charset=" ++ CS end, Rd1 = wrq:set_resp_header("Content-Type", CType ++ CSet, Rd), case get_header_val("accept-encoding", Rd1) of - undefined -> decision_test(choose_encoding("identity;q=1.0,*;q=0.5", Rs, Rd1), none, 406, v3g7); + undefined -> decision_test(choose_content_encoding("identity;q=1.0,*;q=0.5", Rs, Rd1), none, 406, v3g7); _ -> d(v3f7, Rs, Rd1) end; %% Acceptable encoding available? decision(v3f7, Rs, Rd) -> - decision_test(choose_encoding(get_header_val("accept-encoding", Rd), Rs, Rd), none, 406, v3g7); + decision_test(choose_content_encoding(get_header_val("accept-encoding", Rd), Rs, Rd), none, 406, v3g7); %% "Resource exists?" decision(v3g7, Rs, Rd) -> % this is the first place after all conneg, so set Vary here @@ -337,15 +342,15 @@ decision(v3h12, Rs, Rd) -> decision(v3i4, Rs, Rd) -> {MovedPermanently, Rs1, Rd1} = controller_call(moved_permanently, Rs, Rd), case MovedPermanently of - {true, MovedURI} -> - RdLoc = wrq:set_resp_header("Location", MovedURI, Rd1), - respond(301, Rs1, RdLoc); - false -> - d(v3p3, Rs1, Rd1); - {error, Reason} -> - error_response(Reason, Rs1, Rd1); - {halt, Code} -> - respond(Code, Rs1, Rd1) + {true, MovedURI} -> + RdLoc = wrq:set_resp_header("Location", MovedURI, Rd1), + respond(301, Rs1, RdLoc); + false -> + d(v3p3, Rs1, Rd1); + {error, Reason} -> + error_response(Reason, Rs1, Rd1); + {halt, Code} -> + respond(Code, Rs1, Rd1) end; %% PUT? decision(v3i7, Rs, Rd) -> @@ -363,15 +368,15 @@ decision(v3j18, Rs, Rd) -> decision(v3k5, Rs, Rd) -> {MovedPermanently, Rs1, Rd1} = controller_call(moved_permanently, Rs, Rd), case MovedPermanently of - {true, MovedURI} -> - RdLoc = wrq:set_resp_header("Location", MovedURI, Rd1), - respond(301, Rs1, RdLoc); - false -> - d(v3l5, Rs1, Rd1); - {error, Reason} -> - error_response(Reason, Rs1, Rd1); - {halt, Code} -> - respond(Code, Rs1, Rd1) + {true, MovedURI} -> + RdLoc = wrq:set_resp_header("Location", MovedURI, Rd1), + respond(301, Rs1, RdLoc); + false -> + d(v3l5, Rs1, Rd1); + {error, Reason} -> + error_response(Reason, Rs1, Rd1); + {halt, Code} -> + respond(Code, Rs1, Rd1) end; %% "Previously existed?" decision(v3k7, Rs, Rd) -> @@ -389,15 +394,15 @@ decision(v3k13, Rs, Rd) -> decision(v3l5, Rs, Rd) -> {MovedTemporarily, Rs1, Rd1} = controller_call(moved_temporarily, Rs, Rd), case MovedTemporarily of - {true, MovedURI} -> - RdLoc = wrq:set_resp_header("Location", MovedURI, Rd1), - respond(307, Rs1, RdLoc); - false -> - d(v3m5, Rs1, Rd1); - {error, Reason} -> - error_response(Reason, Rs1, Rd1); - {halt, Code} -> - respond(Code, Rs1, Rd1) + {true, MovedURI} -> + RdLoc = wrq:set_resp_header("Location", MovedURI, Rd1), + respond(307, Rs1, RdLoc); + false -> + d(v3m5, Rs1, Rd1); + {error, Reason} -> + error_response(Reason, Rs1, Rd1); + {halt, Code} -> + respond(Code, Rs1, Rd1) end; %% "POST?" decision(v3l7, Rs, Rd) -> @@ -484,9 +489,7 @@ decision(v3n11, Rs, Rd) -> _ -> {ProcessPost, Rs2, Rd2} = controller_call(process_post, Rs1, Rd1), case ProcessPost of - true -> - {_, Rs3, Rd3} = encode_body_if_set(Rs2, Rd2), - {stage1_ok, Rs3, Rd3}; + true -> {stage1_ok, Rs2, Rd2}; {halt, Code} -> respond(Code, Rs2, Rd2); Err -> error_response(Err, Rs2, Rd2) end @@ -556,7 +559,7 @@ decision(v3o18, Rs, Rd) -> Exp -> wrq:set_resp_header("Expires", httpd_util:rfc1123_date(calendar:universal_time_to_local_time(Exp)), RdExp0) end, - CT = webmachine_request:get_metadata('content-type', RdExp), + CT = wrq:resp_content_type(RdExp), {ContentTypesProvided, RsCT, RdCT} = controller_call(content_types_provided, RsExp, RdExp), F = hd([Fun || {Type,Fun} <- ContentTypesProvided, CT =:= Type]), controller_call(F, RsCT, RdCT); @@ -569,8 +572,7 @@ decision(v3o18, Rs, Rd) -> {halt, Code} -> respond(Code, RsBody, RdBody); nop -> d(v3o18b, RsBody, RdBody); _ -> - {EncodedBody, RsEB, RdEB} = encode_body(FinalBody, RsBody, RdBody), - d(v3o18b, RsEB, wrq:set_resp_body(EncodedBody, RdEB)) + d(v3o18b, RsBody, wrq:set_resp_body(FinalBody, RdBody)) end; decision(v3o18b, Rs, Rd) -> @@ -614,77 +616,46 @@ accept_helper(Rs, Rd) -> {{respond, 415}, Rs1, Rd1}; AcceptedContentList -> F = hd(AcceptedContentList), - {Result, Rs2, Rd2} = controller_call(F, Rs1, Rd1), - case Result of - true -> - {_, RsEncoded, RdEncoded} = encode_body_if_set(Rs2, Rd2), - {true, RsEncoded, RdEncoded}; - _ -> - {Result, Rs2, Rd2} - end + controller_call(F, Rs1, Rd1) end. -encode_body_if_set(Rs, Rd) -> - case webmachine_request:has_resp_body(Rd) of - true -> - Body = wrq:resp_body(Rd), - {Encoded, Rs1, Rd1} = encode_body(Body, Rs, Rd), - {true, Rs1, wrq:set_resp_body(Encoded, Rd1)}; - _ -> - {false, Rs, Rd} + +choose_content_encoding(AccEncHdr, Rs, Rd) -> + {EncodingsProvided, Rs1, Rd1} = controller_call(content_encodings_provided, Rs, Rd), + case webmachine_util:choose_encoding(EncodingsProvided, AccEncHdr) of + none -> + {none, Rs1, Rd1}; + ChosenEnc -> + RdEnc = case ChosenEnc of + "identity" -> Rd1; + _ -> wrq:set_resp_header("Content-Encoding",ChosenEnc, Rd1) + end, + RdEnc1 = wrq:set_resp_content_encoding(ChosenEnc,RdEnc), + {ChosenEnc, Rs1, RdEnc1} end. -encode_body(Body, Rs, Rd) -> - ChosenCSet = webmachine_request:get_metadata('chosen-charset', Rd), - {CharSetsProvided, Rs1, Rd1} = controller_call(charsets_provided, Rs, Rd), - Charsetter = - case CharSetsProvided of - no_charset -> fun(X) -> X end; - CP -> hd([Fun || {CSet,Fun} <- CP, ChosenCSet =:= CSet]) - end, - ChosenEnc = webmachine_request:get_metadata('content-encoding', Rd1), - {EncodingsProvided, Rs2, Rd2} = controller_call(encodings_provided, Rs1, Rd1), - Encoder = hd([Fun || {Enc,Fun} <- EncodingsProvided, ChosenEnc =:= Enc]), - case Body of - {stream, StreamBody} -> - {{stream, make_encoder_stream(Encoder, Charsetter, StreamBody)}, Rs2, Rd2}; - {stream, Size, Fun} -> - {stream, Size, make_size_encoder_stream(Encoder, Charsetter, Fun)}; - {writer, BodyFun} -> - {{writer, {Encoder, Charsetter, BodyFun}}, Rs2, Rd2}; - {writer, Size, BodyFun} -> - {{writer, Size, {Encoder, Charsetter, BodyFun}}, Rs2, Rd2}; - _ -> - {Encoder(Charsetter(to_binary(Body))), Rs2, Rd2} - end. - - to_binary(Body) when is_tuple(Body) -> Body; - to_binary(Body) -> iolist_to_binary(Body). -make_size_encoder_stream(Encoder, Charsetter, Fun) -> - fun(Start, End) -> - make_encoder_stream(Encoder, Charsetter, Fun(Start, End)) - end. +choose_transfer_encoding(AccEncHdr, Rs, Rd) -> + choose_transfer_encoding(wrq:version(Rd), AccEncHdr, Rs, Rd). + +choose_transfer_encoding({1,0}, _AccEncHdr, Rs, Rd) -> + {Rs, Rd}; +choose_transfer_encoding({1,1}, AccEncHdr, Rs, Rd) -> + {EncodingsProvided, Rs1, Rd1} = controller_call(transfer_encodings_provided, Rs, Rd), + EncList = [ Enc || {Enc, _Func} <- EncodingsProvided ], + case webmachine_util:choose_encoding(EncList, AccEncHdr) of + none -> + {Rs1, Rd1}; + "identity" -> + {Rs1, Rd1}; + ChosenEnc -> + Enc = lists:keyfind(1, ChosenEnc, EncodingsProvided), + RdEnc = wrq:set_resp_transfer_encoding(Enc,Rd1), + {Rs1, RdEnc} + end; +choose_transfer_encoding(_, _AccEncHdr, Rs, Rd) -> + {Rs, Rd}. -make_encoder_stream(Encoder, Charsetter, {Body, done}) -> - {Encoder(Charsetter(Body)), done}; -make_encoder_stream(Encoder, Charsetter, {Body, Next}) -> - {Encoder(Charsetter(Body)), fun() -> make_encoder_stream(Encoder, Charsetter, Next()) end}. - -choose_encoding(AccEncHdr, Rs, Rd) -> - {EncodingsProvided, Rs1, Rd1} = controller_call(encodings_provided, Rs, Rd), - Encs = [Enc || {Enc,_Fun} <- EncodingsProvided], - case webmachine_util:choose_encoding(Encs, AccEncHdr) of - none -> - {none, Rs1, Rd1}; - ChosenEnc -> - RdEnc = case ChosenEnc of - "identity" -> Rd1; - _ -> wrq:set_resp_header("Content-Encoding",ChosenEnc, Rd1) - end, - {ok, RdEnc1} = webmachine_request:set_metadata('content-encoding',ChosenEnc,RdEnc), - {ChosenEnc, Rs1, RdEnc1} - end. choose_charset(AccCharHdr, Rs, Rd) -> {CharsetsProvided, Rs1, Rd1} = controller_call(charsets_provided, Rs, Rd), @@ -692,31 +663,35 @@ choose_charset(AccCharHdr, Rs, Rd) -> no_charset -> {no_charset, Rs1, Rd1}; CL -> - CSets = [CSet || {CSet,_Fun} <- CL], + CSets = [maybe_old_tuple_value(CSet) || CSet <- CL], case webmachine_util:choose_charset(CSets, AccCharHdr) of none -> {none, Rs1, Rd1}; Charset -> - {ok, RdCSet} = webmachine_request:set_metadata('chosen-charset', Charset, Rd1), + RdCSet = wrq:set_resp_chosen_charset(Charset, Rd1), {Charset, Rs1, RdCSet} end end. +maybe_old_tuple_value({A, _}) -> A; +maybe_old_tuple_value(A) -> A. + + choose_upgrade(UpgradeHdr, Rs, Rd) -> {UpgradesProvided, Rs1, Rd1} = controller_call(upgrades_provided, Rs, Rd), - Provided1 = [ {string:to_lower(Prot), Prot, PFun} || {Prot, PFun} <- UpgradesProvided], - Requested = [ string:to_lower(string:strip(Up)) || Up <- string:tokens(UpgradeHdr, ",") ], - {choose_upgrade1(Requested, Provided1), Rs1, Rd1}. - - choose_upgrade1([], _) -> - none; - choose_upgrade1([Req|Requested], Provided) -> - case lists:keysearch(Req, 1, Provided) of - false -> - choose_upgrade1(Requested, Provided); - {value, {_, Protocol, UpgradeFun}} -> - {Protocol, UpgradeFun} - end. + Provided1 = [ {string:to_lower(Prot), Prot, PFun} || {Prot, PFun} <- UpgradesProvided], + Requested = [ string:to_lower(string:strip(Up)) || Up <- string:tokens(UpgradeHdr, ",") ], + {choose_upgrade1(Requested, Provided1), Rs1, Rd1}. + +choose_upgrade1([], _) -> + none; +choose_upgrade1([Req|Requested], Provided) -> + case lists:keysearch(Req, 1, Provided) of + false -> + choose_upgrade1(Requested, Provided); + {value, {_, Protocol, UpgradeFun}} -> + {Protocol, UpgradeFun} + end. variances(Rs, Rd) -> @@ -726,11 +701,11 @@ variances(Rs, Rd) -> 0 -> []; _ -> ["Accept"] end, - {EncodingsProvided, Rs2, Rd2} = controller_call(encodings_provided, Rs1, Rd1), + {EncodingsProvided, Rs2, Rd2} = controller_call(content_encodings_provided, Rs1, Rd1), AcceptEncoding = case length(EncodingsProvided) of - 1 -> []; - 0 -> []; - _ -> ["Accept-Encoding"] + 1 -> []; + 0 -> []; + _ -> ["Accept-Encoding"] end, {CharsetsProvided, Rs3, Rd3} = controller_call(charsets_provided, Rs2, Rd2), AcceptCharset = case CharsetsProvided of diff --git a/src/webmachine_request.erl b/src/webmachine_request.erl index 50c8896..0681dbe 100644 --- a/src/webmachine_request.erl +++ b/src/webmachine_request.erl @@ -21,7 +21,7 @@ -author('Justin Sheehy '). -author('Andy Gross '). --define(WMVSN, "1.8.1 (compat)"). +-define(WMVSN, "2.0 (Z)"). -export([get_peer/1]). % used in initialization @@ -79,16 +79,18 @@ -include("wm_reqdata.hrl"). -define(IDLE_TIMEOUT, infinity). +-define(FILE_CHUNK_LENGTH, 65536). + get_peer(ReqData) -> case ReqData#wm_reqdata.peer of - undefined -> + undefined -> Socket = ReqData#wm_reqdata.socket, Peer = peer_from_peername(mochiweb_socket:peername(Socket), ReqData), NewReqData = ReqData#wm_reqdata{peer=Peer}, {Peer, NewReqData}; - _ -> - {ReqData#wm_reqdata.peer, ReqData} + _ -> + {ReqData#wm_reqdata.peer, ReqData} end. peer_from_peername({ok, {Addr={10, _, _, _}, _Port}}, ReqData) -> @@ -117,14 +119,93 @@ get_header_value(K, ReqData) -> get_outheader_value(K, ReqData) -> mochiweb_headers:get_value(K, wrq:resp_headers(ReqData)). -send(undefined, _Data) -> - ok; -send(Socket, Data) -> - case mochiweb_socket:send(Socket, iolist_to_binary(Data)) of - ok -> ok; - {error,closed} -> ok; - _ -> exit(normal) - end. +send_response(ReqData) -> + {Reply, RD1} = send_response_range(ReqData#wm_reqdata.response_code, ReqData), + NewLogData = (RD1#wm_reqdata.log_data)#wm_log_data{finish_time=os:timestamp()}, + {Reply, RD1#wm_reqdata{log_data=NewLogData}}. + +send_response_range(200, ReqData) -> + {Range, RangeRD} = get_range(ReqData), + case Range of + X when X =:= undefined; X =:= fail -> + send_response_code(200, all, RangeRD); + Ranges -> + case get_resp_body_size(wrq:resp_body(RangeRD)) of + {ok, Size, Body1} -> + RangeRD1 = wrq:set_resp_body(Body1, RangeRD), + case range_parts(Ranges, Size) of + [] -> + %% no valid ranges + %% could be 416, for now we'll just return 200 and the whole body + send_response_code(200, all, RangeRD1); + PartList -> + ContentType = get_outheader_value("content-type", RangeRD1), + {RangeHeaders, Boundary} = get_range_headers(PartList, Size, ContentType), + RespHdrsRD = wrq:set_resp_headers(RangeHeaders, RangeRD1), + send_response_code(206, {PartList, Size, Boundary, ContentType}, RespHdrsRD) + end; + {error, nosize} -> + send_response_code(200, all, RangeRD) + end + end; +send_response_range(Code, ReqData) -> + send_response_code(Code, all, ReqData). + +send_response_code(Code, Parts, ReqData) -> + ReqData1 = wrq:set_response_code(Code, ReqData), + LogData = (ReqData1#wm_reqdata.log_data)#wm_log_data{response_code=Code, response_length=0}, + send_response_bodyfun(wrq:resp_body(ReqData1), Code, Parts, ReqData, LogData). + + +send_response_bodyfun({device, IO}, Code, Parts, ReqData, LogData) -> + Length = iodevice_size(IO), + send_response_bodyfun({device, Length, IO}, Code, Parts, ReqData, LogData); +send_response_bodyfun({device, Length, IO}, Code, all, ReqData, LogData) -> + Writer = fun() -> send_device_body(ReqData#wm_reqdata.socket, Length, IO) end, + send_response_headers(Code, Length, undefined, Writer, ReqData, LogData); +send_response_bodyfun({device, _Length, IO}, Code, Parts, ReqData, LogData) -> + Writer = fun() -> send_device_body_parts(ReqData#wm_reqdata.socket, Parts, IO) end, + send_response_headers(Code, undefined, undefined, Writer, ReqData, LogData); +send_response_bodyfun({file, Filename}, Code, Parts, ReqData, LogData) -> + Length = filelib:file_size(Filename), + send_response_bodyfun({file, Length, Filename}, Code, Parts, ReqData, LogData); +send_response_bodyfun({file, Length, Filename}, Code, all, ReqData, LogData) -> + Writer = fun() -> send_file_body(ReqData#wm_reqdata.socket, Length, Filename) end, + send_response_headers(Code, Length, undefined, Writer, ReqData, LogData); +send_response_bodyfun({file, _Length, Filename}, Code, Parts, ReqData, LogData) -> + Writer = fun() -> send_file_body_parts(ReqData#wm_reqdata.socket, Parts, Filename) end, + send_response_headers(Code, undefined, undefined, Writer, ReqData, LogData); +send_response_bodyfun({stream, StreamFun}, Code, all, ReqData, LogData) -> + Writer = fun() -> send_stream_body(ReqData#wm_reqdata.socket, is_chunked_transfer(wrq:version(ReqData), chunked), StreamFun) end, + send_response_headers(Code, undefined, chunked, Writer, ReqData, LogData); +send_response_bodyfun({stream, Size, Fun}, Code, all, ReqData, LogData) -> + Writer = fun() -> send_stream_body(ReqData#wm_reqdata.socket, is_chunked_transfer(wrq:version(ReqData), chunked), Fun(0, Size-1)) end, + send_response_headers(Code, undefined, chunked, Writer, ReqData, LogData); +send_response_bodyfun({writer, WriterFun}, Code, all, ReqData, LogData) -> + Writer = fun() -> send_writer_body(ReqData#wm_reqdata.socket, is_chunked_transfer(wrq:version(ReqData), chunked), WriterFun) end, + send_response_headers(Code, undefined, chunked, Writer, ReqData, LogData); +send_response_bodyfun(Body, Code, all, ReqData, LogData) -> + Length = iolist_size(Body), + Writer = fun() -> send(ReqData#wm_reqdata.socket, Body), Length end, + send_response_headers(Code, Length, undefined, Writer, ReqData, LogData); +send_response_bodyfun(Body, Code, Parts, ReqData, LogData) -> + Writer = fun() -> send_parts(ReqData#wm_reqdata.socket, Body, Parts) end, + send_response_headers(Code, undefined, undefined, Writer, ReqData, LogData). + + +send_response_headers(Code, Length, Transfer, Writer, ReqData, LogData) -> + send(ReqData#wm_reqdata.socket, + [make_version(wrq:version(ReqData)), + make_code(Code), <<"\r\n">> | + make_headers(Code, Transfer, Length, ReqData)]), + send_response_body_data(wrq:method(ReqData), Writer, ReqData, LogData). + +send_response_body_data('HEAD', _Writer, ReqData, LogData) -> + {ok, ReqData#wm_reqdata{log_data=LogData}}; +send_response_body_data(_Method, Writer, ReqData, LogData) -> + Written = Writer(), + {ok, ReqData#wm_reqdata{log_data=LogData#wm_log_data{response_length=Written}}}. + send_stream_body(Socket, IsChunked, X) -> send_stream_body(Socket, IsChunked, X, 0). @@ -145,99 +226,195 @@ send_stream_body(Socket, IsChunked, {Data, Next}, SoFar) -> send_stream_body(Socket, IsChunked, Next(), Size + SoFar). -%% @todo Remove usage of the put/get to get the number of bytes written -%% Use a separate process to accumulate these counts -send_writer_body(Socket, IsChunked, {Encoder, Charsetter, BodyFun}) -> - put(bytes_written, 0), +send_writer_body(Socket, IsChunked, BodyFun) -> + CounterPid = spawn_link(fun counter_process/0), Writer = fun(Data) -> - Size = send_chunk(Socket, IsChunked, Encoder(Charsetter(Data))), - put(bytes_written, get(bytes_written) + Size), + Size = send_chunk(Socket, IsChunked, Data), + CounterPid ! {sent, Size}, Size end, BodyFun(Writer), send_chunk(Socket, IsChunked, <<>>), - get(bytes_written). + CounterPid ! {total, self()}, + receive + {total, Total} -> + Total + end. +counter_process() -> + counter_process_loop(0). -%% @todo Distinguish between HTTP/1.0 and 1.1 -%% HTTP/1.0 should not add the size (and transfer-encoding: chunked) -send_chunk(Socket, true, Data) -> - Size = iolist_size(Data), - send(Socket, mochihex:to_hex(Size)), - send(Socket, <<"\r\n">>), +counter_process_loop(N) -> + receive + {sent, N1} -> + counter_process_loop(N+N1); + {total, From} -> + From ! {total, N} + end. + +send_device_body(Socket, Length, IO) -> + send_file_body_loop(Socket, 0, Length, IO). + +send_file_body(Socket, Length, Filename) -> + send_file_body(mochiweb_socket:type(Socket), Socket, Length, Filename). + +send_file_body(ssl, Socket, Length, Filename) -> + send_file_body_read(Socket, Length, Filename); +send_file_body(plain, Socket, Length, Filename) -> + case erlang:function_exported(file, sendfile, 5) of + true -> + {ok, FD} = file:open(Filename, [raw,binary]), + {ok, Bytes} = file:sendfile(FD, Socket, 0, Length, []), + file:close(FD), + Bytes; + false -> + send_file_body_read(Socket, Length, Filename) + end. + +send_file_body_read(Socket, Length, Filename) -> + {ok, FD} = file:open(Filename, [raw,binary]), + Bytes = send_file_body_loop(Socket, 0, Length, FD), + file:close(FD), + Bytes. + +send_file_body_loop(_Socket, Offset, Size, _Device) when Offset =:= Size -> + Size; +send_file_body_loop(Socket, Offset, Size, Device) when Size - Offset =< ?FILE_CHUNK_LENGTH -> + {ok, Data} = file:read(Device, Size - Offset), send(Socket, Data), - send(Socket, <<"\r\n">>), Size; -send_chunk(_Socket, false, <<>>) -> - 0; -send_chunk(Socket, false, Data) -> - Size = iolist_size(Data), +send_file_body_loop(Socket, Offset, Size, Device) -> + {ok, Data} = file:read(Device, ?FILE_CHUNK_LENGTH), send(Socket, Data), - Size. + send_file_body_loop(Socket, Offset+?FILE_CHUNK_LENGTH, Size, Device). + + +send_device_body_parts(Socket, {[{From,Length}], _Size, _Boundary, _ContentType}, IO) -> + {ok, _} = file:position(IO, From), + send_file_body_loop(Socket, 0, Length, IO); +send_device_body_parts(Socket, {Parts, Size, Boundary, ContentType}, IO) -> + Bytes = [ + begin + {ok, _} = file:position(IO, From), + send(Socket, part_preamble(Boundary, ContentType, From, Length, Size)), + B = send_file_body_loop(Socket, 0, Length, IO), + send(Socket, <<"\r\n">>), + B + end + || {From,Length} <- Parts + ], + send(Socket, end_boundary(Boundary)), + lists:sum(Bytes). -send_response(ReqData) -> - {Reply, RD1} = case ReqData#wm_reqdata.response_code of - 200 -> send_ok_response(ReqData); - Code -> send_response(Code, ReqData) - end, - LogData = RD1#wm_reqdata.log_data, - NewLogData = LogData#wm_log_data{finish_time=os:timestamp()}, - {Reply, RD1#wm_reqdata{log_data=NewLogData}}. +send_file_body_parts(Socket, Parts, Filename) -> + send_file_body_parts(mochiweb_socket:type(Socket), Socket, Parts, Filename). -send_ok_response(ReqData) -> - {Range, RangeRD} = get_range(ReqData), - case Range of - X when X =:= undefined; X =:= fail -> - send_response(200, RangeRD); - Ranges -> - {PartList, Size} = range_parts(RangeRD, Ranges), - case PartList of - [] -> %% no valid ranges - %% could be 416, for now we'll just return 200 - send_response(200, RangeRD); - PartList -> - {RangeHeaders, RangeBody} = parts_to_body(PartList, Size, RangeRD), - RespHdrsRD = wrq:set_resp_headers([{"Accept-Ranges", "bytes"} | RangeHeaders], RangeRD), - RespBodyRD = wrq:set_resp_body(RangeBody, RespHdrsRD), - send_response(206, RespBodyRD) - end +send_file_body_parts(ssl, Socket, Parts, Filename) -> + send_file_body_parts_read(Socket, Parts, Filename); +send_file_body_parts(plain, Socket, Parts, Filename) -> + case erlang:function_exported(file, sendfile, 5) of + true -> + send_file_body_parts_sendfile(Socket, Parts, Filename); + false -> + send_file_body_parts_read(Socket, Parts, Filename) end. -send_response(Code, ReqData) -> - Body0 = wrq:resp_body(ReqData), - {Body, Transfer, Length} = case Body0 of - {stream, StreamBody} -> {{stream, StreamBody}, chunked, undefined}; - {writer, WriteBody} -> {{writer, WriteBody}, chunked, undefined}; - {stream, Size, Fun} -> {{stream, Fun(0, Size-1)}, chunked, undefined}; - _ -> {Body0, undefined, iolist_size([Body0])} - end, - send(ReqData#wm_reqdata.socket, - [make_version(wrq:version(ReqData)), - make_code(Code), <<"\r\n">> | - make_headers(Code, Transfer, Length, ReqData)]), - FinalLength = case wrq:method(ReqData) of - 'HEAD' -> - Length; - _ -> - case Body of - {stream, Body2} -> - send_stream_body(ReqData#wm_reqdata.socket, is_chunked_transfer(wrq:version(ReqData), Transfer), Body2); - {writer, Body2} -> - send_writer_body(ReqData#wm_reqdata.socket, is_chunked_transfer(wrq:version(ReqData), Transfer), Body2); - _ -> - send(ReqData#wm_reqdata.socket, Body), - Length +send_file_body_parts_sendfile(Socket, {[{From,Length}], _Size, _Boundary, _ContentType}, Filename) -> + {ok, FD} = file:open(Filename, [raw,binary]), + {ok, Bytes} = file:sendfile(FD, Socket, From, Length, []), + file:close(FD), + Bytes; +send_file_body_parts_sendfile(Socket, {Parts, Size, Boundary, ContentType}, Filename) -> + {ok, FD} = file:open(Filename, [raw,binary]), + Bytes = [ + begin + send(Socket, part_preamble(Boundary, ContentType, From, Length, Size)), + {ok, B} = file:sendfile(FD, Socket, From, Length, []), + send(Socket, <<"\r\n">>), + B end - end, - InitLogData = ReqData#wm_reqdata.log_data, - FinalLogData = InitLogData#wm_log_data{response_code=Code,response_length=FinalLength}, - ReqData1 = wrq:set_response_code(Code, ReqData), - {ok, ReqData1#wm_reqdata{log_data=FinalLogData}}. - + || {From,Length} <- Parts + ], + send(Socket, end_boundary(Boundary)), + file:close(FD), + lists:sum(Bytes). + +send_file_body_parts_read(Socket, Parts, Filename) -> + {ok, FD} = file:open(Filename, [raw,binary]), + Bytes = send_device_body_parts(Socket, Parts, FD), + file:close(FD), + Bytes. + + +send_parts(Socket, Bin, {[{From,To}], _Size, _Boundary, _ContentType}) -> + send(Socket, binary:part(Bin,From,To-From+1)); +send_parts(Socket, Bin, {Parts, Size, Boundary, ContentType}) -> + Bytes = [ + send_part_boundary(Socket, From, To, Size, binary:part(Bin,From,To-From+1), Boundary, ContentType) + || {From,To} <- Parts + ], + send(Socket, end_boundary(Boundary)), + lists:sum(Bytes). + +send_part_boundary(Socket, From, To, Size, Bin, Boundary, ContentType) -> + send(Socket, [ + part_preamble(Boundary, ContentType, From, To, Size), + Bin, <<"\r\n">> + ]), + size(Bin). -%% @doc Infer body length from transfer-encoding and content-length headers. + +send_chunk(Socket, true, Data) -> + Data1 = iolist_to_binary(Data), + Size = size(Data1), + _ = send(Socket, mochihex:to_hex(Size)), + _ = send(Socket, <<"\r\n">>), + _ = send(Socket, Data1), + _ = send(Socket, <<"\r\n">>), + Size; +send_chunk(_Socket, false, <<>>) -> + 0; +send_chunk(Socket, false, Data) -> + Data1 = iolist_to_binary(Data), + Size = size(Data1), + _ = send(Socket, Data1), + Size. + +send(undefined, _Data) -> + ok; +send(Socket, Bin) when is_binary(Bin) -> + case mochiweb_socket:send(Socket, Bin) of + ok -> ok; + {error,closed} -> ok; + _ -> exit(normal) + end; +send(Socket, IoList) when is_list(IoList) -> + send(Socket, iolist_to_binary(IoList)). + + +get_resp_body_size({device, Size, _} = Body) -> + {ok, Size, Body}; +get_resp_body_size({device, IO}) -> + Length = iodevice_size(IO), + {ok, Length, {device, Length, IO}}; +get_resp_body_size({file, Size, _} = Body) -> + {ok, Size, Body}; +get_resp_body_size({file, Filename}) -> + Length = filelib:file_size(Filename), + {ok, Length, {file, Length, Filename}}; +get_resp_body_size(B) when is_binary(B) -> + {ok, size(B), B}; +get_resp_body_size(L) when is_list(L) -> + B = iolist_to_binary(L), + {ok, size(B), B}; +get_resp_body_size(_) -> + {error, nosize}. + + +%% @doc Infer incoming body length from transfer-encoding and content-length headers. +%% @todo Should support gzip/compressed tranfer-encoding body_length(ReqData) -> case get_header_value("transfer-encoding", ReqData) of undefined -> @@ -273,10 +450,10 @@ read_whole_stream({Hunk,Next}, Acc0, MaxRecvBody, SizeAcc) -> recv_stream_body(ReqData, MaxHunkSize) -> put(mochiweb_request_recv, true), case get_header_value("expect", ReqData) of - "100-continue" -> - send(ReqData#wm_reqdata.socket, [make_version(wrq:version(ReqData)), make_code(100), <<"\r\n\r\n">>]); - _Else -> - ok + "100-continue" -> + send(ReqData#wm_reqdata.socket, [make_version(wrq:version(ReqData)), make_code(100), <<"\r\n\r\n">>]); + _Else -> + ok end, case body_length(ReqData) of {unknown_transfer_encoding, X} -> exit({unknown_transfer_encoding, X}); @@ -336,74 +513,31 @@ read_chunk_length(Socket) -> get_range(ReqData) -> case get_header_value("range", ReqData) of - undefined -> - {undefined, ReqData#wm_reqdata{range=undefined}}; - RawRange -> - Range = parse_range_request(RawRange), - {Range, ReqData#wm_reqdata{range=Range}} + undefined -> + {undefined, ReqData#wm_reqdata{range=undefined}}; + RawRange -> + Range = parse_range_request(RawRange), + {Range, ReqData#wm_reqdata{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={stream, Size, StreamFun}}, Ranges) -> - SkipLengths = [ range_skip_length(R, Size) || R <- Ranges], - {[ {Skip, Skip+Length-1, StreamFun} || {Skip, Length} <- SkipLengths ], - Size}; - -range_parts(_RD=#wm_reqdata{resp_body=Body0}, Ranges) -> - range_parts(Body0, Ranges); - -range_parts(Body0, Ranges) when is_binary(Body0); is_list(Body0) -> - 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. +% Map request ranges to byte ranges. +range_parts(Ranges, Size) -> + Ranges1 = [ range_skip_length(Spec, Size) || Spec <- Ranges ], + [ R || R <- Ranges1, R =/= invalid_range ]. + +range_skip_length({none, R}, Size) when R =< Size, R >= 0 -> + {Size - R, R}; +range_skip_length({none, _OutOfRange}, Size) -> + {0, Size}; +range_skip_length({R, none}, Size) when R >= 0, R < Size -> + {R, Size - R}; +range_skip_length({_OutOfRange, none}, _Size) -> + invalid_range; +range_skip_length({Start, End}, Size) when 0 =< Start, Start =< End, End < Size -> + {Start, End - Start + 1}; +range_skip_length({_OutOfRange, _End}, _Size) -> + invalid_range. parse_range_request(RawRange) when is_list(RawRange) -> try @@ -425,97 +559,33 @@ parse_range_request(RawRange) when is_list(RawRange) -> fail end. -parts_to_body([{Start, End, Body0}], Size, ReqData) -> - %% return body for a range reponse with a single body - ContentType = - case get_outheader_value("content-type", ReqData) of - undefined -> "text/html"; - CT -> CT - end, - HeaderList = [{"Content-Type", ContentType}, - {"Content-Range", - ["bytes ", - make_io(Start), "-", make_io(End), - "/", make_io(Size)]}], - Body = if is_function(Body0) -> - {stream, Body0(Start, End)}; - true -> - Body0 - end, - {HeaderList, Body}; -parts_to_body(BodyList, Size, ReqData) when is_list(BodyList) -> - %% return - %% header Content-Type: multipart/byteranges; boundary=441934886133bdee4 - %% and multipart body - ContentType = - case get_outheader_value("content-type", ReqData) of - undefined -> "text/html"; - CT -> CT - end, + +get_range_headers([{Start, Length}], Size, _ContentType) -> + HeaderList = [{"Accept-Ranges", "bytes"}, + {"Content-Range", ["bytes ", make_io(Start), "-", make_io(Start+Length-1), "/", make_io(Size)]}, + {"Content-Length", make_io(Length)}], + {HeaderList, none}; +get_range_headers(Parts, Size, ContentType) when is_list(Parts) -> Boundary = mochihex:to_hex(crypto:rand_bytes(8)), - HeaderList = [{"Content-Type", - ["multipart/byteranges; ", - "boundary=", Boundary]}], - MultiPartBody = case hd(BodyList) of - {_, _, Fun} when is_function(Fun) -> - stream_multipart_body(BodyList, ContentType, - Boundary, Size); - _ -> - multipart_body(BodyList, ContentType, - Boundary, Size) - end, - {HeaderList, MultiPartBody}. - -multipart_body([], _ContentType, Boundary, _Size) -> - end_boundary(Boundary); -multipart_body([{Start, End, Body} | BodyList], ContentType, Boundary, Size) -> - [part_preamble(Boundary, ContentType, Start, End, Size), - Body, <<"\r\n">> - | multipart_body(BodyList, ContentType, Boundary, Size)]. + Lengths = [ + iolist_size(part_preamble(Boundary, ContentType, Start, Length, Size)) + Length + 2 + || {Start,Length} <- Parts + ], + TotalLength = lists:sum(Lengths) + iolist_size(end_boundary(Boundary)), + HeaderList = [{"Accept-Ranges", "bytes"}, + {"Content-Type", ["multipart/byteranges; ", "boundary=", Boundary]}, + {"Content-Length", make_io(TotalLength)}], + {HeaderList, Boundary}. boundary(B) -> [<<"--">>, B, <<"\r\n">>]. end_boundary(B) -> [<<"--">>, B, <<"--\r\n">>]. -part_preamble(Boundary, CType, Start, End, Size) -> +part_preamble(Boundary, CType, Start, Length, Size) -> [boundary(Boundary), - <<"Content-Type: ">>, CType, <<"\r\n">>, - <<"Content-Range: bytes ">>, - mochiweb_util:make_io(Start), <<"-">>, mochiweb_util:make_io(End), - <<"/">>, mochiweb_util:make_io(Size), + <<"Content-Type: ">>, CType, + <<"\r\nContent-Range: bytes ">>, make_io(Start), <<"-">>, make_io(Start+Length-1), <<"/">>, make_io(Size), <<"\r\n\r\n">>]. -stream_multipart_body(BodyList, ContentType, Boundary, Size) -> - Helper = stream_multipart_body_helper( - BodyList, ContentType, Boundary, Size), - %% executing Helper() here is an optimization; - %% it's just as valid to say {<<>>, Helper} - {stream, Helper()}. - -stream_multipart_body_helper([], _CType, Boundary, _Size) -> - fun() -> {end_boundary(Boundary), done} end; -stream_multipart_body_helper([{Start, End, Fun}|Rest], - CType, Boundary, Size) -> - fun() -> - {part_preamble(Boundary, CType, Start, End, Size), - stream_multipart_part_helper( - fun() -> Fun(Start, End) end, - Rest, CType, Boundary, Size)} - end. - -stream_multipart_part_helper(Fun, Rest, CType, Boundary, Size) -> - fun() -> - case Fun() of - {Data, done} -> - %% when this part is done, start the next part - {[Data, <<"\r\n">>], - stream_multipart_body_helper( - Rest, CType, Boundary, Size)}; - {Data, Next} -> - %% this subpart has more data coming - {Data, stream_multipart_part_helper( - Next, Rest, CType, Boundary, Size)} - end - end. iodevice_size(IoDevice) -> {ok, Size} = file:position(IoDevice, eof), @@ -565,14 +635,14 @@ make_headers(Code, Transfer, Length, RD) -> {ok, ServerHeader} = application:get_env(webzmachine, server_header), WithSrv = mochiweb_headers:enter("Server", ServerHeader, Hdrs0), Hdrs = case mochiweb_headers:get_value("date", WithSrv) of - undefined -> + undefined -> mochiweb_headers:enter("Date", httpd_util:rfc1123_date(), WithSrv); - _ -> - WithSrv + _ -> + WithSrv end, F = fun({K, V}, Acc) -> - [make_io(K), <<": ">>, V, <<"\r\n">> | Acc] - end, + [make_io(K), <<": ">>, V, <<"\r\n">> | Acc] + end, lists:foldl(F, [<<"\r\n">>], mochiweb_headers:to_list(Hdrs)). @@ -686,11 +756,19 @@ do_redirect(ReqData) -> wrq:do_redirect(true, ReqData). resp_redirect(ReqData) -> wrq:resp_redirect(ReqData). +get_metadata('chosen-charset', ReqData) -> + wrq:resp_chosen_charset(ReqData); +get_metadata('content-encoding', ReqData) -> + wrq:resp_content_encoding(ReqData); +get_metadata('transfer-encoding', ReqData) -> + wrq:resp_transfer_encoding(ReqData); +get_metadata('content-type', ReqData) -> + wrq:resp_content_type(ReqData); get_metadata(Key, ReqData) -> case dict:find(Key, ReqData#wm_reqdata.metadata) of - {ok, Value} -> Value; - error -> undefined - end. + {ok, Value} -> Value; + error -> undefined + end. set_metadata(Key, Value, ReqData) -> NewDict = dict:store(Key, Value, ReqData#wm_reqdata.metadata), diff --git a/src/wrq.erl b/src/wrq.erl index 1d082f0..ff151fa 100644 --- a/src/wrq.erl +++ b/src/wrq.erl @@ -28,6 +28,14 @@ append_to_resp_body/2,append_to_response_body/2, max_recv_body/1,set_max_recv_body/2, get_cookie_value/2,get_qs_value/2,get_qs_value/3,set_peer/2]). +-export([ + resp_transfer_encoding/1, set_resp_transfer_encoding/2, + resp_content_encoding/1, set_resp_content_encoding/2, + resp_content_type/1, set_resp_content_type/2, + resp_chosen_charset/1, set_resp_chosen_charset/2, + + encode_content/2 + ]). % @type reqdata(). The opaque data type used for req/resp data structures. -include("wm_reqdata.hrl"). @@ -151,11 +159,35 @@ resp_redirect(_RD = #wm_reqdata{resp_redirect=false}) -> false. resp_headers(_RD = #wm_reqdata{resp_headers=RespH}) -> RespH. % mochiheaders resp_body(_RD = #wm_reqdata{resp_body=undefined}) -> undefined; -resp_body(_RD = #wm_reqdata{resp_body={stream,X}}) -> {stream,X}; -resp_body(_RD = #wm_reqdata{resp_body={stream,X,Y}}) -> {stream,X,Y}; -resp_body(_RD = #wm_reqdata{resp_body={writer,X}}) -> {writer,X}; +resp_body(_RD = #wm_reqdata{resp_body={file,_}=F}) -> F; +resp_body(_RD = #wm_reqdata{resp_body={file,_,_}=F}) -> F; +resp_body(_RD = #wm_reqdata{resp_body={device,_}=D}) -> D; +resp_body(_RD = #wm_reqdata{resp_body={device,_,_}=D}) -> D; +resp_body(_RD = #wm_reqdata{resp_body={stream,_}=S}) -> S; +resp_body(_RD = #wm_reqdata{resp_body={stream,_,_}=S}) -> S; +resp_body(_RD = #wm_reqdata{resp_body={writer,_}=W}) -> W; resp_body(_RD = #wm_reqdata{resp_body=RespB}) when is_binary(RespB) -> RespB; -resp_body(_RD = #wm_reqdata{resp_body=RespB}) -> iolist_to_binary(RespB). +resp_body(_RD = #wm_reqdata{resp_body=Resp}) when is_list(Resp) -> iolist_to_binary(Resp). + +%% -- + +resp_transfer_encoding(#wm_reqdata{resp_transfer_encoding=TransferEncoding}) -> TransferEncoding. +resp_content_encoding(#wm_reqdata{resp_content_encoding=ContentEncoding}) -> ContentEncoding. +resp_content_type(#wm_reqdata{resp_content_type=ContentType}) -> ContentType. +resp_chosen_charset(#wm_reqdata{resp_chosen_charset=Charset}) -> Charset. + +set_resp_transfer_encoding(TE, RD) when is_list(TE) -> RD#wm_reqdata{resp_transfer_encoding=TE}. +set_resp_content_encoding(CE, RD) -> RD#wm_reqdata{resp_content_encoding=CE}. +set_resp_content_type(CT, RD) -> RD#wm_reqdata{resp_content_type=CT}. +set_resp_chosen_charset(CS, RD) -> RD#wm_reqdata{resp_chosen_charset=CS}. + +%% @doc Utility function to encode the content using some well known content encodings. +encode_content(Content, ReqData) -> + encode_content_1(wrq:resp_content_encoding(ReqData), Content). + +encode_content_1("gzip", Content) -> zlib:gzip(Content); +encode_content_1("identity", Content) -> Content. + %% --