diff options
Diffstat (limited to 'deps/chttpd/src/chttpd.erl')
-rw-r--r-- | deps/chttpd/src/chttpd.erl | 782 |
1 files changed, 782 insertions, 0 deletions
diff --git a/deps/chttpd/src/chttpd.erl b/deps/chttpd/src/chttpd.erl new file mode 100644 index 00000000..a4f053aa --- /dev/null +++ b/deps/chttpd/src/chttpd.erl @@ -0,0 +1,782 @@ +% 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(chttpd). +-include_lib("couch/include/couch_db.hrl"). + +-export([start_link/0, start_link/1, start_link/2, + stop/0, handle_request/1, config_change/2, + primary_header_value/2, header_value/2, header_value/3, qs_value/2, + qs_value/3, qs/1, path/1, absolute_uri/2, body_length/1, + verify_is_server_admin/1, unquote/1, quote/1, recv/2, recv_chunked/4, + error_info/1, parse_form/1, json_body/1, json_body_obj/1, body/1, + doc_etag/1, make_etag/1, etag_respond/3, partition/1, serve_file/3, + server_header/0, start_chunked_response/3,send_chunk/2, + start_response_length/4, send/2, start_json_response/2, + start_json_response/3, end_json_response/1, send_response/4, + send_method_not_allowed/2, send_error/2, send_error/4, send_redirect/2, + send_chunked_error/2, send_json/2,send_json/3,send_json/4]). + +-export([start_delayed_json_response/2, start_delayed_json_response/3, + start_delayed_json_response/4, + start_delayed_chunked_response/3, start_delayed_chunked_response/4, + send_delayed_chunk/2, send_delayed_last_chunk/1, + send_delayed_error/2, end_delayed_json_response/1, + get_delayed_req/1]). + +-record(delayed_resp, { + start_fun, + req, + code, + headers, + first_chunk +}). + +start_link() -> + start_link(http). +start_link(http) -> + Port = couch_config:get("chttpd", "port", "5984"), + start_link(?MODULE, [{port, Port}]); + +start_link(https) -> + Port = couch_config:get("chttps", "port", "6984"), + CertFile = couch_config:get("chttps", "cert_file", nil), + KeyFile = couch_config:get("chttps", "key_file", nil), + Options = case CertFile /= nil andalso KeyFile /= nil of + true -> + SslOpts = [{certfile, CertFile}, {keyfile, KeyFile}], + + %% set password if one is needed for the cert + SslOpts1 = case couch_config:get("chttps", "password", nil) of + nil -> SslOpts; + Password -> + SslOpts ++ [{password, Password}] + end, + % do we verify certificates ? + FinalSslOpts = case couch_config:get("chttps", + "verify_ssl_certificates", "false") of + "false" -> SslOpts1; + "true" -> + case couch_config:get("chttps", + "cacert_file", nil) of + nil -> + io:format("Verify SSL certificate " + ++"enabled but file containing " + ++"PEM encoded CA certificates is " + ++"missing", []), + throw({error, missing_cacerts}); + CaCertFile -> + Depth = list_to_integer(couch_config:get("chttps", + "ssl_certificate_max_depth", + "1")), + FinalOpts = [ + {cacertfile, CaCertFile}, + {depth, Depth}, + {verify, verify_peer}], + % allows custom verify fun. + case couch_config:get("chttps", + "verify_fun", nil) of + nil -> FinalOpts; + SpecStr -> + FinalOpts + ++ [{verify_fun, couch_httpd:make_arity_3_fun(SpecStr)}] + end + end + end, + + [{port, Port}, + {ssl, true}, + {ssl_opts, FinalSslOpts}]; + false -> + io:format("SSL enabled but PEM certificates are missing.", []), + throw({error, missing_certs}) + end, + start_link(https, Options). + +start_link(Name, Options) -> + Options1 = Options ++ [ + {loop, fun ?MODULE:handle_request/1}, + {name, Name}, + {ip, couch_config:get("chttpd", "bind_address", any)} + ], + ServerOptsCfg = couch_config:get("chttpd", "server_options", "[]"), + {ok, ServerOpts} = couch_util:parse_term(ServerOptsCfg), + Options2 = lists:keymerge(1, lists:sort(Options1), lists:sort(ServerOpts)), + case mochiweb_http:start(Options2) of + {ok, Pid} -> + ok = couch_config:register(fun ?MODULE:config_change/2, Pid), + {ok, Pid}; + {error, Reason} -> + io:format("Failure to start Mochiweb: ~s~n", [Reason]), + {error, Reason} + end. + +config_change("chttpd", "bind_address") -> + ?MODULE:stop(); +config_change("chttpd", "port") -> + ?MODULE:stop(); +config_change("chttpd", "backlog") -> + ?MODULE:stop(); +config_change("chttpd", "server_options") -> + ?MODULE:stop(). + +stop() -> + catch mochiweb_http:stop(https), + mochiweb_http:stop(?MODULE). + +handle_request(MochiReq) -> + Begin = now(), + + case couch_config:get("chttpd", "socket_options") of + undefined -> + ok; + SocketOptsCfg -> + {ok, SocketOpts} = couch_util:parse_term(SocketOptsCfg), + ok = mochiweb_socket:setopts(MochiReq:get(socket), SocketOpts) + end, + + AuthenticationFuns = [ + fun couch_httpd_auth:cookie_authentication_handler/1, + fun couch_httpd_auth:default_authentication_handler/1 + ], + + % for the path, use the raw path with the query string and fragment + % removed, but URL quoting left intact + RawUri = MochiReq:get(raw_path), + {"/" ++ Path, _, _} = mochiweb_util:urlsplit_path(RawUri), + {HandlerKey, _, _} = mochiweb_util:partition(Path, "/"), + + Peer = MochiReq:get(peer), + LogForClosedSocket = io_lib:format("mochiweb_recv_error for ~s - ~p ~s", [ + Peer, + MochiReq:get(method), + RawUri + ]), + + Method1 = + case MochiReq:get(method) of + % already an atom + Meth when is_atom(Meth) -> Meth; + + % Non standard HTTP verbs aren't atoms (COPY, MOVE etc) so convert when + % possible (if any module references the atom, then it's existing). + Meth -> couch_util:to_existing_atom(Meth) + end, + increment_method_stats(Method1), + % alias HEAD to GET as mochiweb takes care of stripping the body + Method = case Method1 of + 'HEAD' -> 'GET'; + Other -> Other + end, + + HttpReq = #httpd{ + mochi_req = MochiReq, + method = Method, + path_parts = [list_to_binary(chttpd:unquote(Part)) + || Part <- string:tokens(Path, "/")], + db_url_handlers = db_url_handlers(), + design_url_handlers = design_url_handlers() + }, + + % put small token on heap to keep requests synced to backend calls + erlang:put(nonce, couch_util:to_hex(crypto:rand_bytes(4))), + + Result = + try + case authenticate_request(HttpReq, AuthenticationFuns) of + #httpd{} = Req -> + HandlerFun = url_handler(HandlerKey), + HandlerFun(possibly_hack(Req)); + Response -> + Response + end + catch + throw:{http_head_abort, Resp0} -> + {ok, Resp0}; + throw:{http_abort, Resp0, Reason0} -> + {aborted, Resp0, Reason0}; + throw:{invalid_json, S} -> + ?LOG_ERROR("attempted upload of invalid JSON ~s", [S]), + send_error(HttpReq, {bad_request, "invalid UTF-8 JSON"}); + exit:{mochiweb_recv_error, E} -> + ?LOG_INFO(LogForClosedSocket ++ " - ~p", [E]), + exit(normal); + throw:Error -> + send_error(HttpReq, Error); + error:database_does_not_exist -> + send_error(HttpReq, database_does_not_exist); + Tag:Error -> + Stack = erlang:get_stacktrace(), + ?LOG_ERROR("Uncaught error in HTTP request: ~p",[{Tag, Error}]), + ?LOG_INFO("Stacktrace: ~p",[Stack]), + send_error(HttpReq, {Error, nil, Stack}) + end, + + RequestTime = timer:now_diff(now(), Begin)/1000, + {Status, Code} = case Result of + {ok, Resp} -> + {ok, Resp:get(code)}; + {aborted, Resp, _} -> + {aborted, Resp:get(code)} + end, + Host = MochiReq:get_header_value("Host"), + ?LOG_INFO("~s ~s ~s ~s ~B ~p ~B", [Peer, Host, + atom_to_list(Method1), RawUri, Code, Status, round(RequestTime)]), + couch_stats_collector:record({couchdb, request_time}, RequestTime), + case Result of + {ok, _} -> + couch_stats_collector:increment({httpd, requests}), + {ok, Resp}; + {aborted, _, Reason} -> + couch_stats_collector:increment({httpd, aborted_requests}), + ?LOG_ERROR("Response abnormally terminated: ~p", [Reason]), + exit(normal) + end. + +%% HACK: replication currently handles two forms of input, #db{} style +%% and #http_db style. We need a third that makes use of fabric. #db{} +%% works fine for replicating the dbs and nodes database because they +%% aren't sharded. So for now when a local db is specified as the source or +%% the target, it's hacked to make it a full url and treated as a remote. +possibly_hack(#httpd{path_parts=[<<"_replicate">>]}=Req) -> + {Props0} = couch_httpd:json_body_obj(Req), + Props1 = fix_uri(Req, Props0, <<"source">>), + Props2 = fix_uri(Req, Props1, <<"target">>), + put(post_body, {Props2}), + Req; +possibly_hack(Req) -> + Req. + +fix_uri(Req, Props, Type) -> + case is_http(replication_uri(Type, Props)) of + true -> + Props; + false -> + Uri = make_uri(Req,replication_uri(Type, Props)), + [{Type,Uri}|proplists:delete(Type,Props)] + end. + +replication_uri(Type, PostProps) -> + case couch_util:get_value(Type, PostProps) of + {Props} -> + couch_util:get_value(<<"url">>, Props); + Else -> + Else + end. + +is_http(<<"http://", _/binary>>) -> + true; +is_http(<<"https://", _/binary>>) -> + true; +is_http(_) -> + false. + +make_uri(Req, Raw) -> + Url = list_to_binary(["http://", couch_config:get("httpd", "bind_address"), + ":", couch_config:get("chttpd", "port"), "/", Raw]), + Headers = [ + {<<"authorization">>, ?l2b(header_value(Req,"authorization",""))}, + {<<"cookie">>, ?l2b(header_value(Req,"cookie",""))} + ], + {[{<<"url">>,Url}, {<<"headers">>,{Headers}}]}. +%%% end hack + + +% Try authentication handlers in order until one returns a result +authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthFuns) -> + Req; +authenticate_request(#httpd{} = Req, [AuthFun|Rest]) -> + authenticate_request(AuthFun(Req), Rest); +authenticate_request(#httpd{} = Req, []) -> + case couch_config:get("chttpd", "require_valid_user", "false") of + "true" -> + throw({unauthorized, <<"Authentication required.">>}); + "false" -> + case couch_config:get("admins") of + [] -> + Ctx = #user_ctx{roles=[<<"_reader">>, <<"_writer">>, <<"_admin">>]}, + Req#httpd{user_ctx = Ctx}; + _ -> + Req#httpd{user_ctx=#user_ctx{}} + end + end; +authenticate_request(Response, _AuthFuns) -> + Response. + +increment_method_stats(Method) -> + couch_stats_collector:increment({httpd_request_methods, Method}). + +url_handler("") -> fun chttpd_misc:handle_welcome_req/1; +url_handler("favicon.ico") -> fun chttpd_misc:handle_favicon_req/1; +url_handler("_utils") -> fun chttpd_misc:handle_utils_dir_req/1; +url_handler("_all_dbs") -> fun chttpd_misc:handle_all_dbs_req/1; +url_handler("_active_tasks") -> fun chttpd_misc:handle_task_status_req/1; +url_handler("_config") -> fun chttpd_misc:handle_config_req/1; +url_handler("_replicate") -> fun chttpd_misc:handle_replicate_req/1; +url_handler("_uuids") -> fun chttpd_misc:handle_uuids_req/1; +url_handler("_log") -> fun chttpd_misc:handle_log_req/1; +url_handler("_sleep") -> fun chttpd_misc:handle_sleep_req/1; +url_handler("_session") -> fun couch_httpd_auth:handle_session_req/1; +url_handler("_oauth") -> fun couch_httpd_oauth:handle_oauth_req/1; +%% showroom_http module missing in bigcouch +url_handler("_restart") -> fun showroom_http:handle_restart_req/1; +url_handler("_membership") -> fun mem3_httpd:handle_membership_req/1; +url_handler(_) -> fun chttpd_db:handle_request/1. + +db_url_handlers() -> + [ + {<<"_view_cleanup">>, fun chttpd_db:handle_view_cleanup_req/2}, + {<<"_compact">>, fun chttpd_db:handle_compact_req/2}, + {<<"_design">>, fun chttpd_db:handle_design_req/2}, + {<<"_temp_view">>, fun chttpd_view:handle_temp_view_req/2}, + {<<"_changes">>, fun chttpd_db:handle_changes_req/2}, + {<<"_search">>, fun chttpd_external:handle_search_req/2} + ]. + +design_url_handlers() -> + [ + {<<"_view">>, fun chttpd_view:handle_view_req/3}, + {<<"_show">>, fun chttpd_show:handle_doc_show_req/3}, + {<<"_list">>, fun chttpd_show:handle_view_list_req/3}, + {<<"_update">>, fun chttpd_show:handle_doc_update_req/3}, + {<<"_info">>, fun chttpd_db:handle_design_info_req/3}, + {<<"_rewrite">>, fun chttpd_rewrite:handle_rewrite_req/3} + ]. + +% Utilities + +partition(Path) -> + mochiweb_util:partition(Path, "/"). + +header_value(#httpd{mochi_req=MochiReq}, Key) -> + MochiReq:get_header_value(Key). + +header_value(#httpd{mochi_req=MochiReq}, Key, Default) -> + case MochiReq:get_header_value(Key) of + undefined -> Default; + Value -> Value + end. + +primary_header_value(#httpd{mochi_req=MochiReq}, Key) -> + MochiReq:get_primary_header_value(Key). + +serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot) -> + {ok, MochiReq:serve_file(RelativePath, DocumentRoot, + server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []))}. + +qs_value(Req, Key) -> + qs_value(Req, Key, undefined). + +qs_value(Req, Key, Default) -> + couch_util:get_value(Key, qs(Req), Default). + +qs(#httpd{mochi_req=MochiReq}) -> + MochiReq:parse_qs(). + +path(#httpd{mochi_req=MochiReq}) -> + MochiReq:get(path). + +absolute_uri(#httpd{mochi_req=MochiReq}, Path) -> + XHost = couch_config:get("httpd", "x_forwarded_host", "X-Forwarded-Host"), + Host = case MochiReq:get_header_value(XHost) of + undefined -> + case MochiReq:get_header_value("Host") of + undefined -> + {ok, {Address, Port}} = inet:sockname(MochiReq:get(socket)), + inet_parse:ntoa(Address) ++ ":" ++ integer_to_list(Port); + Value1 -> + Value1 + end; + Value -> Value + end, + XSsl = couch_config:get("httpd", "x_forwarded_ssl", "X-Forwarded-Ssl"), + Scheme = case MochiReq:get_header_value(XSsl) of + "on" -> "https"; + _ -> + XProto = couch_config:get("httpd", "x_forwarded_proto", + "X-Forwarded-Proto"), + case MochiReq:get_header_value(XProto) of + % Restrict to "https" and "http" schemes only + "https" -> "https"; + _ -> + case MochiReq:get(scheme) of + https -> + "https"; + http -> + "http" + end + end + end, + Scheme ++ "://" ++ Host ++ Path. + +unquote(UrlEncodedString) -> + mochiweb_util:unquote(UrlEncodedString). + +quote(UrlDecodedString) -> + mochiweb_util:quote_plus(UrlDecodedString). + +parse_form(#httpd{mochi_req=MochiReq}) -> + mochiweb_multipart:parse_form(MochiReq). + +recv(#httpd{mochi_req=MochiReq}, Len) -> + MochiReq:recv(Len). + +recv_chunked(#httpd{mochi_req=MochiReq}, MaxChunkSize, ChunkFun, InitState) -> + % Fun is called once with each chunk + % Fun({Length, Binary}, State) + % called with Length == 0 on the last time. + MochiReq:stream_body(MaxChunkSize, ChunkFun, InitState). + +body_length(Req) -> + case header_value(Req, "Transfer-Encoding") of + undefined -> + case header_value(Req, "Content-Length") of + undefined -> undefined; + Length -> list_to_integer(Length) + end; + "chunked" -> chunked; + Unknown -> {unknown_transfer_encoding, Unknown} + end. + +body(Req) -> + couch_httpd:body(Req). + +json_body(Httpd) -> + ?JSON_DECODE(body(Httpd)). + +json_body_obj(Httpd) -> + case json_body(Httpd) of + {Props} -> {Props}; + _Else -> + throw({bad_request, "Request body must be a JSON object"}) + end. + + +doc_etag(#doc{revs={Start, [DiskRev|_]}}) -> + "\"" ++ ?b2l(couch_doc:rev_to_str({Start, DiskRev})) ++ "\"". + +make_etag(Term) -> + <<SigInt:128/integer>> = erlang:md5(term_to_binary(Term)), + list_to_binary(io_lib:format("\"~.36B\"",[SigInt])). + +etag_match(Req, CurrentEtag) when is_binary(CurrentEtag) -> + etag_match(Req, binary_to_list(CurrentEtag)); + +etag_match(Req, CurrentEtag) -> + EtagsToMatch = string:tokens( + chttpd:header_value(Req, "If-None-Match", ""), ", "), + lists:member(CurrentEtag, EtagsToMatch). + +etag_respond(Req, CurrentEtag, RespFun) -> + case etag_match(Req, CurrentEtag) of + true -> + % the client has this in their cache. + chttpd:send_response(Req, 304, [{"Etag", CurrentEtag}], <<>>); + false -> + % Run the function. + RespFun() + end. + +verify_is_server_admin(#httpd{user_ctx=#user_ctx{roles=Roles}}) -> + case lists:member(<<"_admin">>, Roles) of + true -> ok; + false -> throw({unauthorized, <<"You are not a server admin.">>}) + end. + +start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) -> + couch_stats_collector:increment({httpd_status_codes, Code}), + Resp = MochiReq:start_response_length({Code, Headers ++ server_header() ++ + couch_httpd_auth:cookie_auth_header(Req, Headers), Length}), + case MochiReq:get(method) of + 'HEAD' -> throw({http_head_abort, Resp}); + _ -> ok + end, + {ok, Resp}. + +send(Resp, Data) -> + Resp:send(Data), + {ok, Resp}. + +start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> + couch_stats_collector:increment({httpd_status_codes, Code}), + Resp = MochiReq:respond({Code, Headers ++ server_header() ++ + couch_httpd_auth:cookie_auth_header(Req, Headers), chunked}), + case MochiReq:get(method) of + 'HEAD' -> throw({http_head_abort, Resp}); + _ -> ok + end, + {ok, Resp}. + +send_chunk(Resp, Data) -> + Resp:write_chunk(Data), + {ok, Resp}. + +send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) -> + couch_stats_collector:increment({httpd_status_codes, Code}), + if Code >= 400 -> + ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]); + true -> ok + end, + {ok, MochiReq:respond({Code, Headers ++ server_header() ++ + couch_httpd_auth:cookie_auth_header(Req, Headers), Body})}. + +send_method_not_allowed(Req, Methods) -> + send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, + ?l2b("Only " ++ Methods ++ " allowed")). + +send_json(Req, Value) -> + send_json(Req, 200, Value). + +send_json(Req, Code, Value) -> + send_json(Req, Code, [], Value). + +send_json(Req, Code, Headers, Value) -> + couch_httpd:send_json(Req, Code, [reqid() | Headers], Value). + +start_json_response(Req, Code) -> + start_json_response(Req, Code, []). + +start_json_response(Req, Code, Headers) -> + couch_httpd:start_json_response(Req, Code, [reqid() | Headers]). + +end_json_response(Resp) -> + couch_httpd:end_json_response(Resp). + +start_delayed_json_response(Req, Code) -> + start_delayed_json_response(Req, Code, []). + +start_delayed_json_response(Req, Code, Headers) -> + start_delayed_json_response(Req, Code, Headers, ""). + +start_delayed_json_response(Req, Code, Headers, FirstChunk) -> + {ok, #delayed_resp{ + start_fun = fun start_json_response/3, + req = Req, + code = Code, + headers = Headers, + first_chunk = FirstChunk}}. + +start_delayed_chunked_response(Req, Code, Headers) -> + start_delayed_chunked_response(Req, Code, Headers, ""). + +start_delayed_chunked_response(Req, Code, Headers, FirstChunk) -> + {ok, #delayed_resp{ + start_fun = fun start_chunked_response/3, + req = Req, + code = Code, + headers = Headers, + first_chunk = FirstChunk}}. + +send_delayed_chunk(Resp, Chunk) -> + {ok, Resp1} = start_delayed_response(Resp), + send_chunk(Resp1, Chunk). + +send_delayed_last_chunk(Req) -> + send_delayed_chunk(Req, []). + +send_delayed_error(#httpd{}=Req, Reason) -> + {Code, ErrorStr, ReasonStr} = error_info(Reason), + send_error(Req, Code, ErrorStr, ReasonStr); +send_delayed_error(#delayed_resp{req=Req}, Reason) -> + {Code, ErrorStr, ReasonStr} = error_info(Reason), + send_error(Req, Code, ErrorStr, ReasonStr); +send_delayed_error(Resp, Reason) -> + throw({http_abort, Resp, Reason}). + +end_delayed_json_response(Resp) -> + {ok, Resp1} = start_delayed_response(Resp), + end_json_response(Resp1). + +get_delayed_req(#delayed_resp{req=#httpd{mochi_req=MochiReq}}) -> + MochiReq; +get_delayed_req(Resp) -> + Resp:get(request). + +start_delayed_response(#delayed_resp{start_fun=StartFun, req=Req, code=Code, + headers=Headers, first_chunk=FirstChunk}) -> + {ok, Resp} = StartFun(Req, Code, Headers), + case FirstChunk of + "" -> {ok, Resp}; + _ -> send_chunk(Resp, FirstChunk) + end; +start_delayed_response(Resp) -> + {ok, Resp}. + +error_info({Error, Reason}) when is_list(Reason) -> + error_info({Error, couch_util:to_binary(Reason)}); +error_info(bad_request) -> + {400, <<"bad_request">>, <<>>}; +error_info({bad_request, Reason}) -> + {400, <<"bad_request">>, Reason}; +error_info({query_parse_error, Reason}) -> + {400, <<"query_parse_error">>, Reason}; +error_info(database_does_not_exist) -> + {404, <<"not_found">>, <<"Database does not exist.">>}; +error_info(not_found) -> + {404, <<"not_found">>, <<"missing">>}; +error_info({not_found, Reason}) -> + {404, <<"not_found">>, Reason}; +error_info({not_acceptable, Reason}) -> + {406, <<"not_acceptable">>, Reason}; +error_info(conflict) -> + {409, <<"conflict">>, <<"Document update conflict.">>}; +error_info({conflict, _}) -> + {409, <<"conflict">>, <<"Document update conflict.">>}; +error_info({forbidden, Msg}) -> + {403, <<"forbidden">>, Msg}; +error_info({forbidden, Error, Msg}) -> + {403, Error, Msg}; +error_info({unauthorized, Msg}) -> + {401, <<"unauthorized">>, Msg}; +error_info(file_exists) -> + {412, <<"file_exists">>, <<"The database could not be " + "created, the file already exists.">>}; +error_info({r_quorum_not_met, Reason}) -> + {412, <<"read_quorum_not_met">>, Reason}; +error_info({w_quorum_not_met, Reason}) -> + {500, <<"write_quorum_not_met">>, Reason}; +error_info({bad_ctype, Reason}) -> + {415, <<"bad_content_type">>, Reason}; +error_info(requested_range_not_satisfiable) -> + {416, <<"requested_range_not_satisfiable">>, <<"Requested range not satisfiable">>}; +error_info({error, illegal_database_name}) -> + {400, <<"illegal_database_name">>, <<"Only lowercase characters (a-z), " + "digits (0-9), and any of the characters _, $, (, ), +, -, and / " + "are allowed">>}; +error_info({missing_stub, Reason}) -> + {412, <<"missing_stub">>, Reason}; +error_info(not_implemented) -> + {501, <<"not_implemented">>, <<"this feature is not yet implemented">>}; +error_info({Error, null}) -> + {500, couch_util:to_binary(Error), null}; +error_info({Error, Reason}) -> + {500, couch_util:to_binary(Error), couch_util:to_binary(Reason)}; +error_info({Error, nil, _Stack}) -> + error_info(Error); +error_info({Error, Reason, _Stack}) -> + error_info({Error, Reason}); +error_info(Error) -> + {500, couch_util:to_binary(Error), null}. + +error_headers(#httpd{mochi_req=MochiReq}=Req, 401=Code, ErrorStr, ReasonStr) -> + % this is where the basic auth popup is triggered + case MochiReq:get_header_value("X-CouchDB-WWW-Authenticate") of + undefined -> + case couch_config:get("httpd", "WWW-Authenticate", nil) of + nil -> + % If the client is a browser and the basic auth popup isn't turned on + % redirect to the session page. + case ErrorStr of + <<"unauthorized">> -> + case couch_config:get("couch_httpd_auth", "authentication_redirect", nil) of + nil -> {Code, []}; + AuthRedirect -> + case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of + "true" -> + % send the browser popup header no matter what if we are require_valid_user + {Code, [{"WWW-Authenticate", "Basic realm=\"server\""}]}; + _False -> + % if the accept header matches html, then do the redirect. else proceed as usual. + Accepts = case MochiReq:get_header_value("Accept") of + undefined -> + % According to the HTTP 1.1 spec, if the Accept + % header is missing, it means the client accepts + % all media types. + "html"; + Else -> + Else + end, + case re:run(Accepts, "\\bhtml\\b", + [{capture, none}, caseless]) of + nomatch -> + {Code, []}; + match -> + AuthRedirectBin = ?l2b(AuthRedirect), + % Redirect to the path the user requested, not + % the one that is used internally. + UrlReturnRaw = case MochiReq:get_header_value("x-couchdb-vhost-path") of + undefined -> MochiReq:get(path); + VHostPath -> VHostPath + end, + UrlReturn = ?l2b(couch_util:url_encode(UrlReturnRaw)), + UrlReason = ?l2b(couch_util:url_encode(ReasonStr)), + {302, [{"Location", couch_httpd:absolute_uri(Req, <<AuthRedirectBin/binary,"?return=",UrlReturn/binary,"&reason=",UrlReason/binary>>)}]} + end + end + end; + _Else -> + {Code, []} + end; + Type -> + {Code, [{"WWW-Authenticate", Type}]} + end; + Type -> + {Code, [{"WWW-Authenticate", Type}]} + end; +error_headers(_, Code, _, _) -> + {Code, []}. + +send_error(_Req, {already_sent, Resp, _Error}) -> + {ok, Resp}; + +send_error(Req, Error) -> + {Code, ErrorStr, ReasonStr} = error_info(Error), + {Code1, Headers} = error_headers(Req, Code, ErrorStr, ReasonStr), + send_error(Req, Code1, Headers, ErrorStr, ReasonStr, json_stack(Error)). + +send_error(Req, Code, ErrorStr, ReasonStr) -> + send_error(Req, Code, [], ErrorStr, ReasonStr, []). + +send_error(Req, Code, Headers, ErrorStr, ReasonStr) -> + send_error(Req, Code, Headers, ErrorStr, ReasonStr, []). + +send_error(Req, Code, Headers, ErrorStr, ReasonStr, Stack) -> + send_json(Req, Code, Headers, + {[{<<"error">>, ErrorStr}, + {<<"reason">>, ReasonStr} | + case Stack of [] -> []; _ -> [{stack, Stack}] end + ]}). + +% give the option for list functions to output html or other raw errors +send_chunked_error(Resp, {_Error, {[{<<"body">>, Reason}]}}) -> + send_chunk(Resp, Reason), + send_chunk(Resp, []); + +send_chunked_error(Resp, Error) -> + {Code, ErrorStr, ReasonStr} = error_info(Error), + JsonError = {[{<<"code">>, Code}, + {<<"error">>, ErrorStr}, + {<<"reason">>, ReasonStr} | + case json_stack(Error) of [] -> []; Stack -> [{stack, Stack}] end + ]}, + send_chunk(Resp, ?l2b([$\n,?JSON_ENCODE(JsonError),$\n])), + send_chunk(Resp, []). + +send_redirect(Req, Path) -> + Headers = [{"Location", chttpd:absolute_uri(Req, Path)}], + send_response(Req, 301, Headers, <<>>). + +server_header() -> + couch_httpd:server_header(). + +reqid() -> + {"X-Couch-Request-ID", get(nonce)}. + +json_stack({_Error, _Reason, Stack}) -> + lists:map(fun({M,F,A0}) -> + A = if is_integer(A0) -> A0; is_list(A0) -> length(A0); true -> 0 end, + list_to_binary(io_lib:format("~s:~s/~B", [M,F,A])); + (_) -> + <<"bad entry in stacktrace">> + end, Stack); +json_stack(_) -> + []. |