C0 code coverage information

Generated on 2009-08-09 21:22:56 with etap 0.3.4.

Name Total lines Lines of code Total coverage Code coverage
couch_httpd ?? ?? ??
55% 
....1 % Licensed under the Apache License, Version 2.0 (the "License"); you may not
....2 % use this file except in compliance with the License. You may obtain a copy of
....3 % the License at
....4 %
....5 %   http://www.apache.org/licenses/LICENSE-2.0
....6 %
....7 % Unless required by applicable law or agreed to in writing, software
....8 % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
....9 % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
...10 % License for the specific language governing permissions and limitations under
...11 % the License.
...12 
...13 -module(couch_httpd).
...14 -include("couch_db.hrl").
...15 
...16 -export([start_link/0, stop/0, handle_request/5]).
...17 
...18 -export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,path/1,absolute_uri/2]).
...19 -export([verify_is_server_admin/1,unquote/1,quote/1,recv/2,recv_chunked/4,error_info/1]).
...20 -export([parse_form/1,json_body/1,json_body_obj/1,body/1,doc_etag/1, make_etag/1, etag_respond/3]).
...21 -export([primary_header_value/2,partition/1,serve_file/3, server_header/0]).
...22 -export([start_chunked_response/3,send_chunk/2]).
...23 -export([start_json_response/2, start_json_response/3, end_json_response/1]).
...24 -export([send_response/4,send_method_not_allowed/2,send_error/4, send_redirect/2,send_chunked_error/2]).
...25 -export([send_json/2,send_json/3,send_json/4]).
...26 
...27 start_link() ->
...28     % read config and register for configuration changes
...29 
...30     % just stop if one of the config settings change. couch_server_sup
...31     % will restart us and then we will pick up the new settings.
...32 
...33     BindAddress = couch_config:get("httpd", "bind_address", any),
...34     Port = couch_config:get("httpd", "port", "5984"),
...35 
...36     DefaultSpec = "{couch_httpd_db, handle_request}",
...37     DefaultFun = make_arity_1_fun(
...38         couch_config:get("httpd", "default_handler", DefaultSpec)
...39     ),
...40 
...41     UrlHandlersList = lists:map(
...42         fun({UrlKey, SpecStr}) ->
...43             {?l2b(UrlKey), make_arity_1_fun(SpecStr)}
...44         end, couch_config:get("httpd_global_handlers")),
...45 
...46     DbUrlHandlersList = lists:map(
...47         fun({UrlKey, SpecStr}) ->
...48             {?l2b(UrlKey), make_arity_2_fun(SpecStr)}
...49         end, couch_config:get("httpd_db_handlers")),
...50 
...51     DesignUrlHandlersList = lists:map(
...52         fun({UrlKey, SpecStr}) ->
...53             {?l2b(UrlKey), make_arity_2_fun(SpecStr)}
...54         end, couch_config:get("httpd_design_handlers")),
...55 
...56     UrlHandlers = dict:from_list(UrlHandlersList),
...57     DbUrlHandlers = dict:from_list(DbUrlHandlersList),
...58     DesignUrlHandlers = dict:from_list(DesignUrlHandlersList),
...59     Loop = fun(Req)->
...60         apply(?MODULE, handle_request, [
...61             Req, DefaultFun, UrlHandlers, DbUrlHandlers, DesignUrlHandlers
...62         ])
...63     end,
...64 
...65     % and off we go
...66 
...67     {ok, Pid} = case mochiweb_http:start([
...68         {loop, Loop},
...69         {name, ?MODULE},
...70         {ip, BindAddress},
...71         {port, Port}
...72     ]) of
...73     {ok, MochiPid} -> {ok, MochiPid};
...74     {error, Reason} ->
...75         io:format("Failure to start Mochiweb: ~s~n",[Reason]),
...76         throw({error, Reason})
...77     end,
...78 
...79     ok = couch_config:register(
...80         fun("httpd", "bind_address") ->
...81             ?MODULE:stop();
...82         ("httpd", "port") ->
...83             ?MODULE:stop();
...84         ("httpd", "default_handler") ->
...85             ?MODULE:stop();
...86         ("httpd_global_handlers", _) ->
...87             ?MODULE:stop();
...88         ("httpd_db_handlers", _) ->
...89             ?MODULE:stop()
...90         end, Pid),
...91 
...92     {ok, Pid}.
...93 
...94 % SpecStr is a string like "{my_module, my_fun}"
...95 %  or "{my_module, my_fun, <<"my_arg">>}"
...96 make_arity_1_fun(SpecStr) ->
...97     case couch_util:parse_term(SpecStr) of
...98     {ok, {Mod, Fun, SpecArg}} ->
...99         fun(Arg) -> apply(Mod, Fun, [Arg, SpecArg]) end;
..100     {ok, {Mod, Fun}} ->
..101         fun(Arg) -> apply(Mod, Fun, [Arg]) end
..102     end.
..103 
..104 make_arity_2_fun(SpecStr) ->
..105     case couch_util:parse_term(SpecStr) of
..106     {ok, {Mod, Fun, SpecArg}} ->
..107         fun(Arg1, Arg2) -> apply(Mod, Fun, [Arg1, Arg2, SpecArg]) end;
..108     {ok, {Mod, Fun}} ->
..109         fun(Arg1, Arg2) -> apply(Mod, Fun, [Arg1, Arg2]) end
..110     end.
..111 
..112 % SpecStr is "{my_module, my_fun}, {my_module2, my_fun2}"
..113 make_arity_1_fun_list(SpecStr) ->
..114     [make_arity_1_fun(FunSpecStr) || FunSpecStr <- re:split(SpecStr, "(?<=})\\s*,\\s*(?={)", [{return, list}])].
..115 
..116 stop() ->
..117     mochiweb_http:stop(?MODULE).
..118 
..119 
..120 handle_request(MochiReq, DefaultFun,
..121         UrlHandlers, DbUrlHandlers, DesignUrlHandlers) ->
..122     Begin = now(),
..123     AuthenticationFuns = make_arity_1_fun_list(
..124             couch_config:get("httpd", "authentication_handlers")),
..125     % for the path, use the raw path with the query string and fragment
..126     % removed, but URL quoting left intact
..127     RawUri = MochiReq:get(raw_path),
..128     {"/" ++ Path, _, _} = mochiweb_util:urlsplit_path(RawUri),
..129 
..130     HandlerKey =
..131     case mochiweb_util:partition(Path, "/") of
..132     {"", "", ""} ->
..133         <<"/">>; % Special case the root url handler
..134     {FirstPart, _, _} ->
..135         list_to_binary(FirstPart)
..136     end,
..137     ?LOG_DEBUG("~p ~s ~p~nHeaders: ~p", [
..138         MochiReq:get(method),
..139         RawUri,
..140         MochiReq:get(version),
..141         mochiweb_headers:to_list(MochiReq:get(headers))
..142     ]),
..143 
..144     Method1 =
..145     case MochiReq:get(method) of
..146         % already an atom
..147         Meth when is_atom(Meth) -> Meth;
..148 
..149         % Non standard HTTP verbs aren't atoms (COPY, MOVE etc) so convert when
..150         % possible (if any module references the atom, then it's existing).
..151         Meth -> couch_util:to_existing_atom(Meth)
..152     end,
..153 
..154     increment_method_stats(Method1),
..155 
..156     % alias HEAD to GET as mochiweb takes care of stripping the body
..157     Method = case Method1 of
..158         'HEAD' -> 'GET';
..159         Other -> Other
..160     end,
..161 
..162     HttpReq = #httpd{
..163         mochi_req = MochiReq,
..164         method = Method,
..165         path_parts = [list_to_binary(couch_httpd:unquote(Part))
..166                 || Part <- string:tokens(Path, "/")],
..167         db_url_handlers = DbUrlHandlers,
..168         design_url_handlers = DesignUrlHandlers
..169     },
..170 
..171     HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun),
..172 
..173     {ok, Resp} =
..174     try
..175         % Try authentication handlers in order until one returns a result
..176         case lists:foldl(fun(_Fun, #httpd{user_ctx=#user_ctx{}}=Req) -> Req;
..177                     (Fun, #httpd{}=Req) -> Fun(Req);
..178                     (_Fun, Response) -> Response
..179                 end, HttpReq, AuthenticationFuns) of
..180             #httpd{user_ctx=#user_ctx{}}=Req -> HandlerFun(Req);
..181             #httpd{}=Req ->
..182                 case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of
..183                     "true" ->
..184                         throw({unauthorized, <<"Authentication required.">>});
..185                     _ ->
..186                         HandlerFun(Req#httpd{user_ctx=#user_ctx{}})
..187                 end;
..188             Response -> Response
..189         end
..190     catch
..191         throw:Error ->
..192             ?LOG_DEBUG("Minor error in HTTP request: ~p",[Error]),
..193             ?LOG_DEBUG("Stacktrace: ~p",[erlang:get_stacktrace()]),
..194             send_error(HttpReq, Error);
..195         error:badarg ->
..196             ?LOG_ERROR("Badarg error in HTTP request",[]),
..197             ?LOG_INFO("Stacktrace: ~p",[erlang:get_stacktrace()]),
..198             send_error(HttpReq, badarg);
..199         error:function_clause ->
..200             ?LOG_ERROR("function_clause error in HTTP request",[]),
..201             ?LOG_INFO("Stacktrace: ~p",[erlang:get_stacktrace()]),
..202             send_error(HttpReq, function_clause);
..203         Tag:Error ->
..204             ?LOG_ERROR("Uncaught error in HTTP request: ~p",[{Tag, Error}]),
..205             ?LOG_INFO("Stacktrace: ~p",[erlang:get_stacktrace()]),
..206             send_error(HttpReq, Error)
..207     end,
..208 
..209     ?LOG_INFO("~s - - ~p ~s ~B", [
..210         MochiReq:get(peer),
..211         MochiReq:get(method),
..212         RawUri,
..213         Resp:get(code)
..214     ]),
..215     RequestTime = round(timer:now_diff(now(), Begin)/1000),
..216     couch_stats_collector:record({couchdb, request_time}, RequestTime),
..217     couch_stats_collector:increment({httpd, requests}),
..218     {ok, Resp}.
..219 
..220 increment_method_stats(Method) ->
..221     couch_stats_collector:increment({httpd_request_methods, Method}).
..222 
..223 
..224 % Utilities
..225 
..226 partition(Path) ->
..227     mochiweb_util:partition(Path, "/").
..228 
..229 header_value(#httpd{mochi_req=MochiReq}, Key) ->
..230     MochiReq:get_header_value(Key).
..231 
..232 header_value(#httpd{mochi_req=MochiReq}, Key, Default) ->
..233     case MochiReq:get_header_value(Key) of
..234     undefined -> Default;
..235     Value -> Value
..236     end.
..237 
..238 primary_header_value(#httpd{mochi_req=MochiReq}, Key) ->
..239     MochiReq:get_primary_header_value(Key).
..240 
..241 serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot) ->
..242     {ok, MochiReq:serve_file(RelativePath, DocumentRoot,
..243         server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []))}.
..244 
..245 qs_value(Req, Key) ->
..246     qs_value(Req, Key, undefined).
..247 
..248 qs_value(Req, Key, Default) ->
..249     proplists:get_value(Key, qs(Req), Default).
..250 
..251 qs(#httpd{mochi_req=MochiReq}) ->
..252     MochiReq:parse_qs().
..253 
..254 path(#httpd{mochi_req=MochiReq}) ->
..255     MochiReq:get(path).
..256 
..257 absolute_uri(#httpd{mochi_req=MochiReq}, Path) ->
..258     Host = case MochiReq:get_header_value("Host") of
..259         undefined ->
..260             {ok, {Address, Port}} = inet:sockname(MochiReq:get(socket)),
..261             inet_parse:ntoa(Address) ++ ":" ++ integer_to_list(Port);
..262         Value -> Value
..263     end,
..264     "http://" ++ Host ++ Path.
..265 
..266 unquote(UrlEncodedString) ->
..267     mochiweb_util:unquote(UrlEncodedString).
..268 
..269 quote(UrlDecodedString) ->
..270     mochiweb_util:quote_plus(UrlDecodedString).
..271 
..272 parse_form(#httpd{mochi_req=MochiReq}) ->
..273     mochiweb_multipart:parse_form(MochiReq).
..274 
..275 recv(#httpd{mochi_req=MochiReq}, Len) ->
..276     MochiReq:recv(Len).
..277 
..278 recv_chunked(#httpd{mochi_req=MochiReq}, MaxChunkSize, ChunkFun, InitState) ->
..279     % Fun is called once with each chunk
..280     % Fun({Length, Binary}, State)
..281     % called with Length == 0 on the last time.
..282     MochiReq:stream_body(MaxChunkSize, ChunkFun, InitState).
..283 
..284 body(#httpd{mochi_req=MochiReq, req_body=ReqBody}) ->
..285     case ReqBody of
..286         undefined ->
..287             % Maximum size of document PUT request body (4GB)
..288             MaxSize = list_to_integer(
..289                 couch_config:get("couchdb", "max_document_size", "4294967296")),
..290             MochiReq:recv_body(MaxSize);
..291         _Else ->
..292             ReqBody
..293     end.
..294 
..295 json_body(Httpd) ->
..296     ?JSON_DECODE(body(Httpd)).
..297 
..298 json_body_obj(Httpd) ->
..299     case json_body(Httpd) of
..300         {Props} -> {Props};
..301         _Else ->
..302             throw({bad_request, "Request body must be a JSON object"})
..303     end.
..304 
..305 
..306 doc_etag(#doc{revs={Start, [DiskRev|_]}}) ->
..307     "\"" ++ ?b2l(couch_doc:rev_to_str({Start, DiskRev})) ++ "\"".
..308 
..309 make_etag(Term) ->
..310     <> = erlang:md5(term_to_binary(Term)),
..311     list_to_binary("\"" ++ lists:flatten(io_lib:format("~.36B",[SigInt])) ++ "\"").
..312 
..313 etag_match(Req, CurrentEtag) when is_binary(CurrentEtag) ->
..314     etag_match(Req, binary_to_list(CurrentEtag));
..315 
..316 etag_match(Req, CurrentEtag) ->
..317     EtagsToMatch = string:tokens(
..318         couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
..319     lists:member(CurrentEtag, EtagsToMatch).
..320 
..321 etag_respond(Req, CurrentEtag, RespFun) ->
..322     case etag_match(Req, CurrentEtag) of
..323     true ->
..324         % the client has this in their cache.
..325         couch_httpd:send_response(Req, 304, [{"Etag", CurrentEtag}], <<>>);
..326     false ->
..327         % Run the function.
..328         RespFun()
..329     end.
..330 
..331 verify_is_server_admin(#httpd{user_ctx=#user_ctx{roles=Roles}}) ->
..332     case lists:member(<<"_admin">>, Roles) of
..333     true -> ok;
..334     false -> throw({unauthorized, <<"You are not a server admin.">>})
..335     end.
..336 
..337 
..338 
..339 start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) ->
..340     couch_stats_collector:increment({httpd_status_codes, Code}),
..341     {ok, MochiReq:respond({Code, Headers ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers), chunked})}.
..342 
..343 send_chunk(Resp, Data) ->
..344     Resp:write_chunk(Data),
..345     {ok, Resp}.
..346 
..347 send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) ->
..348     couch_stats_collector:increment({httpd_status_codes, Code}),
..349     if Code >= 400 ->
..350         ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]);
..351     true -> ok
..352     end,
..353     {ok, MochiReq:respond({Code, Headers ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers), Body})}.
..354 
..355 send_method_not_allowed(Req, Methods) ->
..356     send_response(Req, 405, [{"Allow", Methods}], <<>>).
..357 
..358 send_json(Req, Value) ->
..359     send_json(Req, 200, Value).
..360 
..361 send_json(Req, Code, Value) ->
..362     send_json(Req, Code, [], Value).
..363 
..364 send_json(Req, Code, Headers, Value) ->
..365     DefaultHeaders = [
..366         {"Content-Type", negotiate_content_type(Req)},
..367         {"Cache-Control", "must-revalidate"}
..368     ],
..369     Body = list_to_binary(
..370         [start_jsonp(Req), ?JSON_ENCODE(Value), end_jsonp(), $\n]
..371     ),
..372     send_response(Req, Code, DefaultHeaders ++ Headers, Body).
..373 
..374 start_json_response(Req, Code) ->
..375     start_json_response(Req, Code, []).
..376 
..377 start_json_response(Req, Code, Headers) ->
..378     DefaultHeaders = [
..379         {"Content-Type", negotiate_content_type(Req)},
..380         {"Cache-Control", "must-revalidate"}
..381     ],
..382     start_jsonp(Req), % Validate before starting chunked.
..383     %start_chunked_response(Req, Code, DefaultHeaders ++ Headers).
..384     {ok, Resp} = start_chunked_response(Req, Code, DefaultHeaders ++ Headers),
..385     case start_jsonp(Req) of
..386         [] -> ok;
..387         Start -> send_chunk(Resp, Start)
..388     end,
..389     {ok, Resp}.
..390 
..391 end_json_response(Resp) ->
..392     send_chunk(Resp, end_jsonp() ++ [$\n]),
..393     %send_chunk(Resp, [$\n]),
..394     send_chunk(Resp, []).
..395 
..396 start_jsonp(Req) ->
..397     case get(jsonp) of
..398         undefined -> put(jsonp, qs_value(Req, "callback", no_jsonp));
..399         _ -> ok
..400     end,
..401     case get(jsonp) of
..402         no_jsonp -> [];
..403         [] -> [];
..404         CallBack ->
..405             try
..406                 validate_callback(CallBack),
..407                 CallBack ++ "("
..408             catch
..409                 Error ->
..410                     put(jsonp, no_jsonp),
..411                     throw(Error)
..412             end
..413     end.
..414 
..415 end_jsonp() ->
..416     Resp = case get(jsonp) of
..417         no_jsonp -> [];
..418         [] -> [];
..419         _ -> ");"
..420     end,
..421     put(jsonp, undefined),
..422     Resp.
..423 
..424 validate_callback(CallBack) when is_binary(CallBack) ->
..425     validate_callback(binary_to_list(CallBack));
..426 validate_callback([]) ->
..427     ok;
..428 validate_callback([Char | Rest]) ->
..429     case Char of
..430         _ when Char >= $a andalso Char =< $z -> ok;
..431         _ when Char >= $A andalso Char =< $Z -> ok;
..432         _ when Char >= $0 andalso Char =< $9 -> ok;
..433         _ when Char == $. -> ok;
..434         _ when Char == $_ -> ok;
..435         _ when Char == $[ -> ok;
..436         _ when Char == $] -> ok;
..437         _ ->
..438             throw({bad_request, invalid_callback})
..439     end,
..440     validate_callback(Rest).
..441 
..442 
..443 error_info({Error, Reason}) when is_list(Reason) ->
..444     error_info({Error, ?l2b(Reason)});
..445 error_info(bad_request) ->
..446     {400, <<"bad_request">>, <<>>};
..447 error_info({bad_request, Reason}) ->
..448     {400, <<"bad_request">>, Reason};
..449 error_info({query_parse_error, Reason}) ->
..450     {400, <<"query_parse_error">>, Reason};
..451 error_info(not_found) ->
..452     {404, <<"not_found">>, <<"missing">>};
..453 error_info({not_found, Reason}) ->
..454     {404, <<"not_found">>, Reason};
..455 error_info({not_acceptable, Reason}) ->
..456     {406, <<"not_acceptable">>, Reason};
..457 error_info(conflict) ->
..458     {409, <<"conflict">>, <<"Document update conflict.">>};
..459 error_info({forbidden, Msg}) ->
..460     {403, <<"forbidden">>, Msg};
..461 error_info({unauthorized, Msg}) ->
..462     {401, <<"unauthorized">>, Msg};
..463 error_info(file_exists) ->
..464     {412, <<"file_exists">>, <<"The database could not be "
..465         "created, the file already exists.">>};
..466 error_info({bad_ctype, Reason}) ->
..467     {415, <<"bad_content_type">>, Reason};
..468 error_info({Error, Reason}) ->
..469     {500, couch_util:to_binary(Error), couch_util:to_binary(Reason)};
..470 error_info(Error) ->
..471     {500, <<"unknown_error">>, couch_util:to_binary(Error)}.
..472 
..473 send_error(_Req, {already_sent, Resp, _Error}) ->
..474     {ok, Resp};
..475 
..476 send_error(#httpd{mochi_req=MochiReq}=Req, Error) ->
..477     {Code, ErrorStr, ReasonStr} = error_info(Error),
..478     Headers = if Code == 401 ->
..479         case MochiReq:get_header_value("X-CouchDB-WWW-Authenticate") of
..480         undefined ->
..481             case couch_config:get("httpd", "WWW-Authenticate", nil) of
..482             nil ->
..483                 [];
..484             Type ->
..485                 [{"WWW-Authenticate", Type}]
..486             end;
..487         Type ->
..488             [{"WWW-Authenticate", Type}]
..489         end;
..490     true ->
..491         []
..492     end,
..493     send_error(Req, Code, Headers, ErrorStr, ReasonStr).
..494 
..495 send_error(Req, Code, ErrorStr, ReasonStr) ->
..496     send_error(Req, Code, [], ErrorStr, ReasonStr).
..497 
..498 send_error(Req, Code, Headers, ErrorStr, ReasonStr) ->
..499     send_json(Req, Code, Headers,
..500         {[{<<"error">>,  ErrorStr},
..501          {<<"reason">>, ReasonStr}]}).
..502 
..503 % give the option for list functions to output html or other raw errors
..504 send_chunked_error(Resp, {_Error, {[{<<"body">>, Reason}]}}) ->
..505     send_chunk(Resp, Reason),
..506     send_chunk(Resp, []);
..507 
..508 send_chunked_error(Resp, Error) ->
..509     {Code, ErrorStr, ReasonStr} = error_info(Error),
..510     JsonError = {[{<<"code">>, Code},
..511         {<<"error">>,  ErrorStr},
..512         {<<"reason">>, ReasonStr}]},
..513     send_chunk(Resp, ?l2b([$\n,?JSON_ENCODE(JsonError),$\n])),
..514     send_chunk(Resp, []).
..515 
..516 send_redirect(Req, Path) ->
..517      Headers = [{"Location", couch_httpd:absolute_uri(Req, Path)}],
..518      send_response(Req, 301, Headers, <<>>).
..519 
..520 negotiate_content_type(#httpd{mochi_req=MochiReq}) ->
..521     %% Determine the appropriate Content-Type header for a JSON response
..522     %% depending on the Accept header in the request. A request that explicitly
..523     %% lists the correct JSON MIME type will get that type, otherwise the
..524     %% response will have the generic MIME type "text/plain"
..525     AcceptedTypes = case MochiReq:get_header_value("Accept") of
..526         undefined       -> [];
..527         AcceptHeader    -> string:tokens(AcceptHeader, ", ")
..528     end,
..529     case lists:member("application/json", AcceptedTypes) of
..530         true  -> "application/json";
..531         false -> "text/plain;charset=utf-8"
..532     end.
..533 
..534 server_header() ->
..535     OTPVersion = "R" ++ integer_to_list(erlang:system_info(compat_rel)) ++ "B",
..536     [{"Server", "CouchDB/" ++ couch_server:get_version() ++
..537                 " (Erlang OTP/" ++ OTPVersion ++ ")"}].

Generated using etap 0.3.4.