1 %% @author Justin Sheehy <justin@basho.com>
2 %% @author Andy Gross <andy@basho.com>
3 %% @author Bryan Fink <bryan@basho.com>
4 %% @copyright 2007-2009 Basho Technologies
6 %% Licensed under the Apache License, Version 2.0 (the "License");
7 %% you may not use this file except in compliance with the License.
8 %% You may obtain a copy of the License at
10 %% http://www.apache.org/licenses/LICENSE-2.0
12 %% Unless required by applicable law or agreed to in writing, software
13 %% distributed under the License is distributed on an "AS IS" BASIS,
14 %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 %% See the License for the specific language governing permissions and
16 %% limitations under the License.
18 %% @doc Decision core for webmachine
20 -module(webmachine_decision_core).
21 -author('Justin Sheehy <justin@basho.com>').
22 -author('Andy Gross <andy@basho.com>').
23 -author('Bryan Fink <bryan@basho.com>').
24 -export([handle_request/2]).
26 -include("webmachine_logger.hrl").
28 handle_request(Resource, ReqState) ->
29 [erase(X) || X <- [decision, code, req_body, bytes_written, tmp_reqstate]],
30 put(resource, Resource),
31 put(reqstate, ReqState),
36 error_response(erlang:get_stacktrace())
41 Req = webmachine_request:new(RS0),
42 {Response, RS1} = Req:call(X),
47 Resource = get(resource),
48 {Reply, NewResource, NewRS} = Resource:do(Fun,get()),
49 put(resource, NewResource),
53 get_header_val(H) -> wrcall({get_req_header, H}).
55 method() -> wrcall(method).
58 put(decision, DecisionID),
59 log_decision(DecisionID),
62 respond(Code) when is_integer(Code) ->
63 respond({Code, undefined});
64 respond({_, _}=CodeAndPhrase) ->
65 Resource = get(resource),
67 respond(CodeAndPhrase, Resource, EndTime).
69 respond({Code, _ReasonPhrase}=CodeAndPhrase, Resource, EndTime)
70 when Code >= 400, Code < 600 ->
71 error_response(CodeAndPhrase, Resource, EndTime);
72 respond({304, _ReasonPhrase}=CodeAndPhrase, Resource, EndTime) ->
73 wrcall({remove_resp_header, "Content-Type"}),
74 case resource_call(generate_etag) of
76 ETag -> wrcall({set_resp_header, "ETag", webmachine_util:quoted_string(ETag)})
78 case resource_call(expires) of
81 wrcall({set_resp_header, "Expires",
82 webmachine_util:rfc1123_date(Exp)})
84 finish_response(CodeAndPhrase, Resource, EndTime);
85 respond(CodeAndPhrase, Resource, EndTime) ->
86 finish_response(CodeAndPhrase, Resource, EndTime).
88 finish_response({Code, _}=CodeAndPhrase, Resource, EndTime) ->
90 wrcall({set_response_code, CodeAndPhrase}),
91 resource_call(finish_request),
92 wrcall({send_response, CodeAndPhrase}),
93 RMod = wrcall({get_metadata, 'resource_module'}),
94 Notes = wrcall(notes),
95 LogData0 = wrcall(log_data),
96 LogData = LogData0#wm_log_data{resource_module=RMod,
99 spawn(fun() -> do_log(LogData) end),
102 error_response(Reason) ->
103 error_response(500, Reason).
105 error_response(Code, Reason) ->
106 Resource = get(resource),
108 error_response({Code, undefined}, Reason, Resource, EndTime).
110 error_response({Code, _}=CodeAndPhrase, Resource, EndTime) ->
111 error_response({Code, _}=CodeAndPhrase,
112 webmachine_error:reason(Code),
116 error_response({Code, _}=CodeAndPhrase, Reason, Resource, EndTime) ->
117 {ok, ErrorHandler} = application:get_env(webmachine, error_handler),
118 {ErrorHTML, ReqState} = ErrorHandler:render_error(
119 Code, {webmachine_request,get(reqstate)}, Reason),
120 put(reqstate, ReqState),
121 wrcall({set_resp_body, ErrorHTML}),
122 finish_response(CodeAndPhrase, Resource, EndTime).
124 decision_test(Test,TestVal,TrueFlow,FalseFlow) ->
126 {error, Reason} -> error_response(Reason);
127 {error, Reason0, Reason1} -> error_response({Reason0, Reason1});
128 {halt, Code} -> respond(Code);
129 TestVal -> decision_flow(TrueFlow, Test);
130 _ -> decision_flow(FalseFlow, Test)
133 decision_test_fn({error, Reason}, _TestFn, _TrueFlow, _FalseFlow) ->
134 error_response(Reason);
135 decision_test_fn({error, R0, R1}, _TestFn, _TrueFlow, _FalseFlow) ->
136 error_response({R0, R1});
137 decision_test_fn({halt, Code}, _TestFn, _TrueFlow, _FalseFlow) ->
139 decision_test_fn(Test,TestFn,TrueFlow,FalseFlow) ->
141 true -> decision_flow(TrueFlow, Test);
142 false -> decision_flow(FalseFlow, Test)
145 decision_flow(X, TestResult) when is_integer(X) ->
146 if X >= 500 -> error_response(X, TestResult);
149 decision_flow(X, _TestResult) when is_atom(X) -> d(X).
152 webmachine_log:log_access(LogData).
154 log_decision(DecisionID) ->
155 Resource = get(resource),
156 Resource:log_d(DecisionID).
158 %% "Service Available"
160 decision_test(resource_call(ping), pong, v3b13b, 503);
162 decision_test(resource_call(service_available), true, v3b12, 503);
165 decision_test(lists:member(method(), resource_call(known_methods)),
169 decision_test(resource_call(uri_too_long), true, 414, v3b10);
172 Methods = resource_call(allowed_methods),
173 case lists:member(method(), Methods) of
177 wrcall({set_resp_headers, [{"Allow",
178 string:join([atom_to_list(M) || M <- Methods], ", ")}]}),
182 %% "Content-MD5 present?"
184 decision_test(get_header_val("content-md5"), undefined, v3b9b, v3b9a);
185 %% "Content-MD5 valid?"
187 case resource_call(validate_content_checksum) of
189 error_response(Reason);
193 Checksum = base64:decode(get_header_val("content-md5")),
194 BodyHash = compute_body_md5(),
195 case BodyHash =:= Checksum of
206 decision_test(resource_call(malformed_request), true, 400, v3b8);
209 case resource_call(is_authorized) of
212 error_response(Reason);
216 wrcall({set_resp_header, "WWW-Authenticate", AuthHead}),
221 decision_test(resource_call(forbidden), true, 403, v3b6);
222 %% "Okay Content-* Headers?"
224 decision_test(resource_call(valid_content_headers), true, v3b5, 501);
225 %% "Known Content-Type?"
227 decision_test(resource_call(known_content_type), true, v3b4, 415);
228 %% "Req Entity Too Large?"
230 decision_test(resource_call(valid_entity_length), true, v3b3, 413);
235 Hdrs = resource_call(options),
236 wrcall({set_resp_headers, Hdrs}),
243 PTypes = [Type || {Type,_Fun} <- resource_call(content_types_provided)],
244 case get_header_val("accept") of
246 wrcall({set_metadata, 'content-type', hd(PTypes)}),
251 %% Acceptable media type available?
253 PTypes = [Type || {Type,_Fun} <- resource_call(content_types_provided)],
254 AcceptHdr = get_header_val("accept"),
255 case webmachine_util:choose_media_type(PTypes, AcceptHdr) of
259 wrcall({set_metadata, 'content-type', MType}),
262 %% Accept-Language exists?
264 decision_test(get_header_val("accept-language"),
265 undefined, v3e5, v3d5);
266 %% Acceptable Language available? %% WMACH-46 (do this as proper conneg)
268 decision_test(resource_call(language_available), true, v3e5, 406);
269 %% Accept-Charset exists?
271 case get_header_val("accept-charset") of
272 undefined -> decision_test(choose_charset("*"),
276 %% Acceptable Charset available?
278 decision_test(choose_charset(get_header_val("accept-charset")),
280 %% Accept-Encoding exists?
281 % (also, set content-type header here, now that charset is chosen)
283 CType = wrcall({get_metadata, 'content-type'}),
284 CSet = case wrcall({get_metadata, 'chosen-charset'}) of
286 CS -> "; charset=" ++ CS
288 wrcall({set_resp_header, "Content-Type", CType ++ CSet}),
289 case get_header_val("accept-encoding") of
291 decision_test(choose_encoding("identity;q=1.0,*;q=0.5"),
295 %% Acceptable encoding available?
297 decision_test(choose_encoding(get_header_val("accept-encoding")),
299 %% "Resource exists?"
301 % this is the first place after all conneg, so set Vary here
305 wrcall({set_resp_header, "Vary", string:join(Variances, ", ")})
307 decision_test(resource_call(resource_exists), true, v3g8, v3h7);
308 %% "If-Match exists?"
310 decision_test(get_header_val("if-match"), undefined, v3h10, v3g9);
311 %% "If-Match: * exists"
313 decision_test(get_header_val("if-match"), "*", v3h10, v3g11);
314 %% "ETag in If-Match"
316 ETags = webmachine_util:split_quoted_strings(get_header_val("if-match")),
317 decision_test_fn(resource_call(generate_etag),
318 fun(ETag) -> lists:member(ETag, ETags) end,
321 %% (note: need to reflect this change at in next version of diagram)
323 decision_test(get_header_val("if-match"), undefined, v3i7, 412);
324 %% "If-unmodified-since exists?"
326 decision_test(get_header_val("if-unmodified-since"),undefined,v3i12,v3h11);
327 %% "I-UM-S is valid date?"
329 IUMSDate = get_header_val("if-unmodified-since"),
330 decision_test(webmachine_util:convert_request_date(IUMSDate),
331 bad_date, v3i12, v3h12);
332 %% "Last-Modified > I-UM-S?"
334 ReqDate = get_header_val("if-unmodified-since"),
335 ReqErlDate = webmachine_util:convert_request_date(ReqDate),
336 ResErlDate = resource_call(last_modified),
337 decision_test(ResErlDate > ReqErlDate,
339 %% "Moved permanently? (apply PUT to different URI)"
341 case resource_call(moved_permanently) of
343 wrcall({set_resp_header, "Location", MovedURI}),
348 error_response(Reason);
354 decision_test(method(), 'PUT', v3i4, v3k7);
355 %% "If-none-match exists?"
357 decision_test(get_header_val("if-none-match"), undefined, v3l13, v3i13);
358 %% "If-None-Match: * exists?"
360 decision_test(get_header_val("if-none-match"), "*", v3j18, v3k13);
363 decision_test(lists:member(method(),['GET','HEAD']),
365 %% "Moved permanently?"
367 case resource_call(moved_permanently) of
369 wrcall({set_resp_header, "Location", MovedURI}),
374 error_response(Reason);
378 %% "Previously existed?"
380 decision_test(resource_call(previously_existed), true, v3k5, v3l7);
381 %% "Etag in if-none-match?"
383 ETags = webmachine_util:split_quoted_strings(get_header_val("if-none-match")),
384 decision_test_fn(resource_call(generate_etag),
385 %% Membership test is a little counter-intuitive here; if the
386 %% provided ETag is a member, we follow the error case out
388 fun(ETag) -> lists:member(ETag, ETags) end,
390 %% "Moved temporarily?"
392 case resource_call(moved_temporarily) of
394 wrcall({set_resp_header, "Location", MovedURI}),
399 error_response(Reason);
405 decision_test(method(), 'POST', v3m7, 404);
408 decision_test(get_header_val("if-modified-since"), undefined, v3m16, v3l14);
409 %% "IMS is valid date?"
411 IMSDate = get_header_val("if-modified-since"),
412 decision_test(webmachine_util:convert_request_date(IMSDate),
413 bad_date, v3m16, v3l15);
416 NowDateTime = calendar:universal_time(),
417 ReqDate = get_header_val("if-modified-since"),
418 ReqErlDate = webmachine_util:convert_request_date(ReqDate),
419 decision_test(ReqErlDate > NowDateTime,
421 %% "Last-Modified > IMS?"
423 ReqDate = get_header_val("if-modified-since"),
424 ReqErlDate = webmachine_util:convert_request_date(ReqDate),
425 ResErlDate = resource_call(last_modified),
426 decision_test(ResErlDate =:= undefined orelse ResErlDate > ReqErlDate,
430 decision_test(method(), 'POST', v3n5, 410);
431 %% "Server allows POST to missing resource?"
433 decision_test(resource_call(allow_missing_post), true, v3n11, 404);
436 decision_test(method(), 'DELETE', v3m20, v3n16);
437 %% DELETE enacted immediately?
438 %% Also where DELETE is forced.
440 Result = resource_call(delete_resource),
441 %% DELETE may have body and TCP connection will be closed unless body is read.
442 %% See mochiweb_request:should_close.
443 maybe_flush_body_stream(),
444 decision_test(Result, true, v3m20b, 500);
446 decision_test(resource_call(delete_completed), true, v3o20, 202);
447 %% "Server allows POST to missing resource?"
449 decision_test(resource_call(allow_missing_post), true, v3n11, 410);
452 Stage1 = case resource_call(post_is_create) of
454 case resource_call(create_path) of
455 undefined -> error_response("post_is_create w/o create_path");
457 case is_list(NewPath) of
458 false -> error_response({"create_path not a string", NewPath});
460 BaseUri = case resource_call(base_uri) of
461 undefined -> wrcall(base_uri);
463 case [lists:last(Any)] of
464 "/" -> lists:sublist(Any, erlang:length(Any) - 1);
468 FullPath = filename:join(["/", wrcall(path), NewPath]),
469 wrcall({set_disp_path, NewPath}),
470 case wrcall({get_resp_header, "Location"}) of
471 undefined -> wrcall({set_resp_header, "Location", BaseUri ++ FullPath});
475 Res = accept_helper(),
477 {respond, Code} -> respond(Code);
478 {halt, Code} -> respond(Code);
479 {error, _,_} -> error_response(Res);
480 {error, _} -> error_response(Res);
486 case resource_call(process_post) of
488 encode_body_if_set(),
490 {halt, Code} -> respond(Code);
491 Err -> error_response(Err)
496 case wrcall(resp_redirect) of
498 case wrcall({get_resp_header, "Location"}) of
500 Reason = "Response had do_redirect but no Location",
501 error_response(500, Reason);
512 decision_test(method(), 'POST', v3n11, v3o16);
515 case resource_call(is_conflict) of
516 true -> respond(409);
517 _ -> Res = accept_helper(),
519 {respond, Code} -> respond(Code);
520 {halt, Code} -> respond(Code);
521 {error, _,_} -> error_response(Res);
522 {error, _} -> error_response(Res);
528 decision_test(method(), 'PUT', v3o14, v3o18);
529 %% Multiple representations?
530 % (also where body generation for GET and HEAD is done)
532 BuildBody = case method() of
537 FinalBody = case BuildBody of
539 case resource_call(generate_etag) of
541 ETag -> wrcall({set_resp_header, "ETag", webmachine_util:quoted_string(ETag)})
543 CT = wrcall({get_metadata, 'content-type'}),
544 case resource_call(last_modified) of
547 wrcall({set_resp_header, "Last-Modified",
548 webmachine_util:rfc1123_date(LM)})
550 case resource_call(expires) of
553 wrcall({set_resp_header, "Expires",
554 webmachine_util:rfc1123_date(Exp)})
556 F = hd([Fun || {Type,Fun} <- resource_call(content_types_provided),
557 CT =:= webmachine_util:format_content_type(Type)]),
562 {error, _} -> error_response(FinalBody);
563 {error, _,_} -> error_response(FinalBody);
564 {halt, Code} -> respond(Code);
566 _ -> wrcall({set_resp_body,
567 encode_body(FinalBody)}),
572 decision_test(resource_call(multiple_choices), true, 300, 200);
573 %% Response includes an entity?
575 decision_test(wrcall(has_resp_body), true, v3o18, 204);
578 case resource_call(is_conflict) of
579 true -> respond(409);
580 _ -> Res = accept_helper(),
582 {respond, Code} -> respond(Code);
583 {halt, Code} -> respond(Code);
584 {error, _,_} -> error_response(Res);
585 {error, _} -> error_response(Res);
590 %% New resource? (at this point boils down to "has location header")
592 case wrcall({get_resp_header, "Location"}) of
593 undefined -> d(v3o20);
598 accept_helper(get_header_val("Content-Type")).
600 accept_helper(undefined) ->
601 accept_helper("application/octet-stream");
603 accept_helper("application/octet-stream");
605 {MT, MParams} = webmachine_util:media_type_to_detail(CT),
606 wrcall({set_metadata, 'mediaparams', MParams}),
607 case [Fun || {Type,Fun} <-
608 resource_call(content_types_accepted), MT =:= Type] of
610 AcceptedContentList ->
611 F = hd(AcceptedContentList),
612 case resource_call(F) of
614 encode_body_if_set(),
620 encode_body_if_set() ->
621 case wrcall(has_resp_body) of
623 Body = wrcall(resp_body),
624 wrcall({set_resp_body, encode_body(Body)}),
630 ChosenCSet = wrcall({get_metadata, 'chosen-charset'}),
632 case resource_call(charsets_provided) of
633 no_charset -> fun(X) -> X end;
634 CP -> hd([Fun || {CSet,Fun} <- CP, ChosenCSet =:= CSet])
636 ChosenEnc = wrcall({get_metadata, 'content-encoding'}),
637 Encoder = hd([Fun || {Enc,Fun} <- resource_call(encodings_provided),
640 {stream, StreamBody} ->
641 {stream, make_encoder_stream(Encoder, Charsetter, StreamBody)};
642 {known_length_stream, 0, _StreamBody} ->
643 {known_length_stream, 0, empty_stream()};
644 {known_length_stream, Size, StreamBody} ->
647 {known_length_stream, Size, empty_stream()};
649 {known_length_stream, Size, make_encoder_stream(Encoder, Charsetter, StreamBody)}
651 {stream, Size, Fun} ->
652 {stream, Size, make_size_encoder_stream(Encoder, Charsetter, Fun)};
654 {writer, {Encoder, Charsetter, BodyFun}};
656 Encoder(Charsetter(iolist_to_binary(Body)))
661 {<<>>, fun() -> {<<>>, done} end}.
663 make_encoder_stream(Encoder, Charsetter, {Body, done}) ->
664 {Encoder(Charsetter(Body)), done};
665 make_encoder_stream(Encoder, Charsetter, {Body, Next}) ->
666 {Encoder(Charsetter(Body)),
667 fun() -> make_encoder_stream(Encoder, Charsetter, Next()) end}.
669 make_size_encoder_stream(Encoder, Charsetter, Fun) ->
671 make_encoder_stream(Encoder, Charsetter, Fun(Start, End))
674 choose_encoding(AccEncHdr) ->
675 Encs = [Enc || {Enc,_Fun} <- resource_call(encodings_provided)],
676 case webmachine_util:choose_encoding(Encs, AccEncHdr) of
683 wrcall({set_resp_header, "Content-Encoding",ChosenEnc})
685 wrcall({set_metadata, 'content-encoding',ChosenEnc}),
689 choose_charset(AccCharHdr) ->
690 case resource_call(charsets_provided) of
694 CSets = [CSet || {CSet,_Fun} <- CL],
695 case webmachine_util:choose_charset(CSets, AccCharHdr) of
698 wrcall({set_metadata, 'chosen-charset',Charset}),
704 Accept = case length(resource_call(content_types_provided)) of
709 AcceptEncoding = case length(resource_call(encodings_provided)) of
712 _ -> ["Accept-Encoding"]
714 AcceptCharset = case resource_call(charsets_provided) of
720 _ -> ["Accept-Charset"]
723 Accept ++ AcceptEncoding ++ AcceptCharset ++ resource_call(variances).
731 md5_update(Ctx, Bin) ->
732 erlang:md5_update(Ctx, Bin).
735 erlang:md5_final(Ctx).
737 compute_body_md5() ->
738 case wrcall({req_body, 52428800}) of
740 compute_body_md5_stream();
745 compute_body_md5_stream() ->
747 compute_body_md5_stream(MD5Ctx, wrcall({stream_req_body, 8192}), <<>>).
749 compute_body_md5_stream(MD5, {Hunk, done}, Body) ->
750 %% Save the body so it can be retrieved later
751 put(reqstate, wrq:set_resp_body(Body, get(reqstate))),
752 md5_final(md5_update(MD5, Hunk));
753 compute_body_md5_stream(MD5, {Hunk, Next}, Body) ->
754 compute_body_md5_stream(md5_update(MD5, Hunk), Next(), <<Body/binary, Hunk/binary>>).
756 maybe_flush_body_stream() ->
757 maybe_flush_body_stream(wrcall({stream_req_body, 8192})).
759 maybe_flush_body_stream(stream_conflict) ->
761 maybe_flush_body_stream({_Hunk, done}) ->
763 maybe_flush_body_stream({_Hunk, Next}) ->
764 maybe_flush_body_stream(Next()).