diff options
Diffstat (limited to 'src/couch/src/couch_httpd.erl')
-rw-r--r-- | src/couch/src/couch_httpd.erl | 1492 |
1 files changed, 0 insertions, 1492 deletions
diff --git a/src/couch/src/couch_httpd.erl b/src/couch/src/couch_httpd.erl deleted file mode 100644 index 629cbbdcc..000000000 --- a/src/couch/src/couch_httpd.erl +++ /dev/null @@ -1,1492 +0,0 @@ -% 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(couch_httpd). - --compile(tuple_calls). - --include_lib("couch/include/couch_db.hrl"). - --export([start_link/0, start_link/1, stop/0, handle_request/5]). - --export([header_value/2, header_value/3, qs_value/2, qs_value/3, qs/1, qs_json_value/3]). --export([path/1, absolute_uri/2, body_length/1]). --export([verify_is_server_admin/1, unquote/1, quote/1, recv/2, recv_chunked/4, error_info/1]). --export([make_fun_spec_strs/1]). --export([make_arity_1_fun/1, make_arity_2_fun/1, make_arity_3_fun/1]). --export([parse_form/1, json_body/1, json_body_obj/1, body/1]). --export([doc_etag/1, doc_etag/3, make_etag/1, etag_match/2, etag_respond/3, etag_maybe/2]). --export([primary_header_value/2, partition/1, serve_file/3, serve_file/4, server_header/0]). --export([start_chunked_response/3, send_chunk/2, log_request/2]). --export([start_response_length/4, start_response/3, send/2]). --export([start_json_response/2, start_json_response/3, end_json_response/1]). --export([ - send_response/4, - send_response_no_cors/4, - send_method_not_allowed/2, - send_error/2, send_error/4, - send_redirect/2, - send_chunked_error/2 -]). --export([send_json/2, send_json/3, send_json/4, last_chunk/1, parse_multipart_request/3]). --export([accepted_encodings/1, handle_request_int/5, validate_referer/1, validate_ctype/2]). --export([http_1_0_keep_alive/2]). --export([validate_host/1]). --export([validate_bind_address/1]). --export([check_max_request_length/1]). --export([handle_request/1]). --export([set_auth_handlers/0]). --export([maybe_decompress/2]). - --define(HANDLER_NAME_IN_MODULE_POS, 6). --define(MAX_DRAIN_BYTES, 1048576). --define(MAX_DRAIN_TIME_MSEC, 1000). --define(DEFAULT_SOCKET_OPTIONS, "[{sndbuf, 262144}]"). --define(DEFAULT_AUTHENTICATION_HANDLERS, - "{couch_httpd_auth, cookie_authentication_handler}, " - "{couch_httpd_auth, default_authentication_handler}" -). - -start_link() -> - start_link(http). -start_link(http) -> - Port = config:get("httpd", "port", "5984"), - start_link(?MODULE, [{port, Port}]); -start_link(https) -> - Port = config:get("ssl", "port", "6984"), - {ok, Ciphers} = couch_util:parse_term(config:get("ssl", "ciphers", undefined)), - {ok, Versions} = couch_util:parse_term(config:get("ssl", "tls_versions", undefined)), - {ok, SecureRenegotiate} = couch_util:parse_term( - config:get("ssl", "secure_renegotiate", undefined) - ), - ServerOpts0 = - [ - {cacertfile, config:get("ssl", "cacert_file", undefined)}, - {keyfile, config:get("ssl", "key_file", undefined)}, - {certfile, config:get("ssl", "cert_file", undefined)}, - {password, config:get("ssl", "password", undefined)}, - {secure_renegotiate, SecureRenegotiate}, - {versions, Versions}, - {ciphers, Ciphers} - ], - - case - (couch_util:get_value(keyfile, ServerOpts0) == undefined orelse - couch_util:get_value(certfile, ServerOpts0) == undefined) - of - true -> - couch_log:error("SSL enabled but PEM certificates are missing", []), - throw({error, missing_certs}); - false -> - ok - end, - - ServerOpts = [Opt || {_, V} = Opt <- ServerOpts0, V /= undefined], - - ClientOpts = - case config:get("ssl", "verify_ssl_certificates", "false") of - "false" -> - []; - "true" -> - FailIfNoPeerCert = - case config:get("ssl", "fail_if_no_peer_cert", "false") of - "false" -> false; - "true" -> true - end, - [ - {depth, - list_to_integer( - config:get( - "ssl", - "ssl_certificate_max_depth", - "1" - ) - )}, - {fail_if_no_peer_cert, FailIfNoPeerCert}, - {verify, verify_peer} - ] ++ - case config:get("ssl", "verify_fun", undefined) of - undefined -> []; - SpecStr -> [{verify_fun, make_arity_3_fun(SpecStr)}] - end - end, - SslOpts = ServerOpts ++ ClientOpts, - - Options = - [ - {port, Port}, - {ssl, true}, - {ssl_opts, SslOpts} - ], - start_link(https, Options). -start_link(Name, Options) -> - BindAddress = - case config:get("httpd", "bind_address", "any") of - "any" -> any; - Else -> Else - end, - ok = validate_bind_address(BindAddress), - - {ok, ServerOptions} = couch_util:parse_term( - config:get("httpd", "server_options", "[]") - ), - {ok, SocketOptions} = couch_util:parse_term( - config:get("httpd", "socket_options", ?DEFAULT_SOCKET_OPTIONS) - ), - - set_auth_handlers(), - Handlers = get_httpd_handlers(), - - % ensure uuid is set so that concurrent replications - % get the same value. - couch_server:get_uuid(), - - Loop = fun(Req) -> - case SocketOptions of - [] -> - ok; - _ -> - ok = mochiweb_socket:setopts(Req:get(socket), SocketOptions) - end, - apply(?MODULE, handle_request, [Req | Handlers]) - end, - - % set mochiweb options - FinalOptions = lists:append([ - Options, - ServerOptions, - [ - {loop, Loop}, - {name, Name}, - {ip, BindAddress} - ] - ]), - - % launch mochiweb - case mochiweb_http:start(FinalOptions) of - {ok, MochiPid} -> - {ok, MochiPid}; - {error, Reason} -> - couch_log:error("Failure to start Mochiweb: ~s~n", [Reason]), - throw({error, Reason}) - end. - -stop() -> - mochiweb_http:stop(couch_httpd), - catch mochiweb_http:stop(https). - -set_auth_handlers() -> - AuthenticationSrcs = make_fun_spec_strs( - config:get( - "httpd", - "authentication_handlers", - ?DEFAULT_AUTHENTICATION_HANDLERS - ) - ), - AuthHandlers = lists:map( - fun(A) -> {auth_handler_name(A), make_arity_1_fun(A)} end, AuthenticationSrcs - ), - AuthenticationFuns = - AuthHandlers ++ - [ - %% must be last - fun couch_httpd_auth:party_mode_handler/1 - ], - ok = application:set_env(couch, auth_handlers, AuthenticationFuns). - -auth_handler_name(SpecStr) -> - lists:nth(?HANDLER_NAME_IN_MODULE_POS, re:split(SpecStr, "[\\W_]", [])). - -get_httpd_handlers() -> - {ok, HttpdGlobalHandlers} = application:get_env(couch, httpd_global_handlers), - - UrlHandlersList = lists:map( - fun({UrlKey, SpecStr}) -> - {?l2b(UrlKey), make_arity_1_fun(SpecStr)} - end, - HttpdGlobalHandlers - ), - - {ok, HttpdDbHandlers} = application:get_env(couch, httpd_db_handlers), - - DbUrlHandlersList = lists:map( - fun({UrlKey, SpecStr}) -> - {?l2b(UrlKey), make_arity_2_fun(SpecStr)} - end, - HttpdDbHandlers - ), - - {ok, HttpdDesignHandlers} = application:get_env(couch, httpd_design_handlers), - - DesignUrlHandlersList = lists:map( - fun({UrlKey, SpecStr}) -> - {?l2b(UrlKey), make_arity_3_fun(SpecStr)} - end, - HttpdDesignHandlers - ), - - UrlHandlers = dict:from_list(UrlHandlersList), - DbUrlHandlers = dict:from_list(DbUrlHandlersList), - DesignUrlHandlers = dict:from_list(DesignUrlHandlersList), - DefaultFun = make_arity_1_fun("{couch_httpd_db, handle_request}"), - [DefaultFun, UrlHandlers, DbUrlHandlers, DesignUrlHandlers]. - -% SpecStr is a string like "{my_module, my_fun}" -% or "{my_module, my_fun, <<"my_arg">>}" -make_arity_1_fun(SpecStr) -> - case couch_util:parse_term(SpecStr) of - {ok, {Mod, Fun, SpecArg}} -> - fun(Arg) -> Mod:Fun(Arg, SpecArg) end; - {ok, {Mod, Fun}} -> - fun(Arg) -> Mod:Fun(Arg) end - end. - -make_arity_2_fun(SpecStr) -> - case couch_util:parse_term(SpecStr) of - {ok, {Mod, Fun, SpecArg}} -> - fun(Arg1, Arg2) -> Mod:Fun(Arg1, Arg2, SpecArg) end; - {ok, {Mod, Fun}} -> - fun(Arg1, Arg2) -> Mod:Fun(Arg1, Arg2) end - end. - -make_arity_3_fun(SpecStr) -> - case couch_util:parse_term(SpecStr) of - {ok, {Mod, Fun, SpecArg}} -> - fun(Arg1, Arg2, Arg3) -> Mod:Fun(Arg1, Arg2, Arg3, SpecArg) end; - {ok, {Mod, Fun}} -> - fun(Arg1, Arg2, Arg3) -> Mod:Fun(Arg1, Arg2, Arg3) end - end. - -% SpecStr is "{my_module, my_fun}, {my_module2, my_fun2}" -make_fun_spec_strs(SpecStr) -> - re:split(SpecStr, "(?<=})\\s*,\\s*(?={)", [{return, list}]). - -handle_request(MochiReq) -> - Body = proplists:get_value(body, MochiReq:get(opts)), - erlang:put(mochiweb_request_body, Body), - apply(?MODULE, handle_request, [MochiReq | get_httpd_handlers()]). - -handle_request( - MochiReq, - DefaultFun, - UrlHandlers, - DbUrlHandlers, - DesignUrlHandlers -) -> - %% reset rewrite count for new request - erlang:put(?REWRITE_COUNT, 0), - - MochiReq1 = couch_httpd_vhost:dispatch_host(MochiReq), - - handle_request_int( - MochiReq1, - DefaultFun, - UrlHandlers, - DbUrlHandlers, - DesignUrlHandlers - ). - -handle_request_int(MochiReq, DefaultFun, - UrlHandlers, DbUrlHandlers, DesignUrlHandlers) -> - Begin = os:timestamp(), - % 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), - - % get requested path - RequestedPath = case MochiReq:get_header_value("x-couchdb-vhost-path") of - undefined -> - case MochiReq:get_header_value("x-couchdb-requested-path") of - undefined -> RawUri; - R -> R - end; - P -> P - end, - - HandlerKey = - case mochiweb_util:partition(Path, "/") of - {"", "", ""} -> - <<"/">>; % Special case the root url handler - {FirstPart, _, _} -> - list_to_binary(FirstPart) - end, - couch_log:debug("~p ~s ~p from ~p~nHeaders: ~p", [ - MochiReq:get(method), - RawUri, - MochiReq:get(version), - peer(MochiReq), - mochiweb_headers:to_list(MochiReq:get(headers)) - ]), - - 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), - - % allow broken HTTP clients to fake a full method vocabulary with an X-HTTP-METHOD-OVERRIDE header - MethodOverride = MochiReq:get_primary_header_value("X-HTTP-Method-Override"), - Method2 = case lists:member(MethodOverride, ["GET", "HEAD", "POST", - "PUT", "DELETE", - "TRACE", "CONNECT", - "COPY"]) of - true -> - couch_log:info("MethodOverride: ~s (real method was ~s)", - [MethodOverride, Method1]), - case Method1 of - 'POST' -> couch_util:to_existing_atom(MethodOverride); - _ -> - % Ignore X-HTTP-Method-Override when the original verb isn't POST. - % I'd like to send a 406 error to the client, but that'd require a nasty refactor. - % throw({not_acceptable, <<"X-HTTP-Method-Override may only be used with POST requests.">>}) - Method1 - end; - _ -> Method1 - end, - - % alias HEAD to GET as mochiweb takes care of stripping the body - Method = case Method2 of - 'HEAD' -> 'GET'; - Other -> Other - end, - - HttpReq = #httpd{ - mochi_req = MochiReq, - peer = peer(MochiReq), - method = Method, - requested_path_parts = - [?l2b(unquote(Part)) || Part <- string:tokens(RequestedPath, "/")], - path_parts = [?l2b(unquote(Part)) || Part <- string:tokens(Path, "/")], - db_url_handlers = DbUrlHandlers, - design_url_handlers = DesignUrlHandlers, - default_fun = DefaultFun, - url_handlers = UrlHandlers, - user_ctx = erlang:erase(pre_rewrite_user_ctx), - auth = erlang:erase(pre_rewrite_auth) - }, - - HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun), - - {ok, Resp} = - try - validate_host(HttpReq), - check_request_uri_length(RawUri), - case chttpd_cors:maybe_handle_preflight_request(HttpReq) of - not_preflight -> - case authenticate_request(HttpReq) of - #httpd{} = Req -> - HandlerFun(Req); - Response -> - Response - end; - Response -> - Response - end - catch - throw:{http_head_abort, Resp0} -> - {ok, Resp0}; - throw:{invalid_json, S} -> - couch_log:error("attempted upload of invalid JSON" - " (set log_level to debug to log it)", []), - couch_log:debug("Invalid JSON: ~p",[S]), - send_error(HttpReq, {bad_request, invalid_json}); - throw:unacceptable_encoding -> - couch_log:error("unsupported encoding method for the response", []), - send_error(HttpReq, {not_acceptable, "unsupported encoding"}); - throw:bad_accept_encoding_value -> - couch_log:error("received invalid Accept-Encoding header", []), - send_error(HttpReq, bad_request); - exit:{shutdown, Error} -> - exit({shutdown, Error}); - exit:normal -> - exit(normal); - exit:snappy_nif_not_loaded -> - ErrorReason = "To access the database or view index, Apache CouchDB" - " must be built with Erlang OTP R13B04 or higher.", - couch_log:error("~s", [ErrorReason]), - send_error(HttpReq, {bad_otp_release, ErrorReason}); - exit:{body_too_large, _} -> - send_error(HttpReq, request_entity_too_large); - exit:{uri_too_long, _} -> - send_error(HttpReq, request_uri_too_long); - ?STACKTRACE(throw, Error, Stack) - couch_log:debug("Minor error in HTTP request: ~p",[Error]), - couch_log:debug("Stacktrace: ~p",[Stack]), - send_error(HttpReq, Error); - ?STACKTRACE(error, badarg, Stack) - couch_log:error("Badarg error in HTTP request",[]), - couch_log:info("Stacktrace: ~p",[Stack]), - send_error(HttpReq, badarg); - ?STACKTRACE(error, function_clause, Stack) - couch_log:error("function_clause error in HTTP request",[]), - couch_log:info("Stacktrace: ~p",[Stack]), - send_error(HttpReq, function_clause); - ?STACKTRACE(ErrorType, Error, Stack) - couch_log:error("Uncaught error in HTTP request: ~p", - [{ErrorType, Error}]), - couch_log:info("Stacktrace: ~p",[Stack]), - send_error(HttpReq, Error) - end, - RequestTime = round(timer:now_diff(os:timestamp(), Begin)/1000), - couch_stats:update_histogram([couchdb, request_time], RequestTime), - couch_stats:increment_counter([couchdb, httpd, requests]), - {ok, Resp}. - -validate_host(#httpd{} = Req) -> - case chttpd_util:get_chttpd_config_boolean("validate_host", false) of - true -> - Host = hostname(Req), - ValidHosts = valid_hosts(), - case lists:member(Host, ValidHosts) of - true -> - ok; - false -> - throw({bad_request, <<"Invalid host header">>}) - end; - false -> - ok - end. - -hostname(#httpd{} = Req) -> - case header_value(Req, "Host") of - undefined -> - undefined; - Host -> - [Name | _] = re:split(Host, ":[0-9]+$", [{parts, 2}, {return, list}]), - Name - end. - -valid_hosts() -> - List = chttpd_util:get_chttpd_config("valid_hosts", ""), - re:split(List, ",", [{return, list}]). - -check_request_uri_length(Uri) -> - check_request_uri_length( - Uri, - chttpd_util:get_chttpd_config("max_uri_length") - ). - -check_request_uri_length(_Uri, undefined) -> - ok; -check_request_uri_length(Uri, MaxUriLen) when is_list(MaxUriLen) -> - case length(Uri) > list_to_integer(MaxUriLen) of - true -> - throw(request_uri_too_long); - false -> - ok - end. - -authenticate_request(Req) -> - {ok, AuthenticationFuns} = application:get_env(couch, auth_handlers), - chttpd:authenticate_request(Req, couch_auth_cache, AuthenticationFuns). - -increment_method_stats(Method) -> - couch_stats:increment_counter([couchdb, httpd_request_methods, Method]). - -validate_referer(Req) -> - Host = host_for_request(Req), - Referer = header_value(Req, "Referer", fail), - case Referer of - fail -> - throw({bad_request, <<"Referer header required.">>}); - Referer -> - {_, RefererHost, _, _, _} = mochiweb_util:urlsplit(Referer), - if - RefererHost =:= Host -> ok; - true -> throw({bad_request, <<"Referer header must match host.">>}) - end - end. - -validate_ctype(Req, Ctype) -> - case header_value(Req, "Content-Type") of - undefined -> - throw({bad_ctype, "Content-Type must be " ++ Ctype}); - ReqCtype -> - case string:tokens(ReqCtype, ";") of - [Ctype] -> ok; - [Ctype | _Rest] -> ok; - _Else -> throw({bad_ctype, "Content-Type must be " ++ Ctype}) - end - end. - -check_max_request_length(Req) -> - Len = list_to_integer(header_value(Req, "Content-Length", "0")), - MaxLen = chttpd_util:get_chttpd_config_integer( - "max_http_request_size", 4294967296 - ), - case Len > MaxLen of - true -> - exit({body_too_large, Len}); - false -> - ok - end. - -% 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). - -accepted_encodings(#httpd{mochi_req = MochiReq}) -> - case MochiReq:accepted_encodings(["gzip", "identity"]) of - bad_accept_encoding_value -> - throw(bad_accept_encoding_value); - [] -> - throw(unacceptable_encoding); - EncList -> - EncList - end. - -serve_file(Req, RelativePath, DocumentRoot) -> - serve_file(Req, RelativePath, DocumentRoot, []). - -serve_file(Req0, RelativePath0, DocumentRoot0, ExtraHeaders) -> - Headers0 = basic_headers(Req0, ExtraHeaders), - {ok, {Req1, Code1, Headers1, RelativePath1, DocumentRoot1}} = - chttpd_plugin:before_serve_file( - Req0, 200, Headers0, RelativePath0, DocumentRoot0 - ), - log_request(Req1, Code1), - #httpd{mochi_req = MochiReq} = Req1, - {ok, MochiReq:serve_file(RelativePath1, DocumentRoot1, Headers1)}. - -qs_value(Req, Key) -> - qs_value(Req, Key, undefined). - -qs_value(Req, Key, Default) -> - couch_util:get_value(Key, qs(Req), Default). - -qs_json_value(Req, Key, Default) -> - case qs_value(Req, Key, Default) of - Default -> - Default; - Result -> - ?JSON_DECODE(Result) - end. - -qs(#httpd{mochi_req = MochiReq}) -> - MochiReq:parse_qs(). - -path(#httpd{mochi_req = MochiReq}) -> - MochiReq:get(path). - -host_for_request(#httpd{mochi_req = MochiReq}) -> - XHost = chttpd_util:get_chttpd_config( - "x_forwarded_host", "X-Forwarded-Host" - ), - case MochiReq:get_header_value(XHost) of - undefined -> - case MochiReq:get_header_value("Host") of - undefined -> - {ok, {Address, Port}} = - case MochiReq:get(socket) of - {ssl, SslSocket} -> ssl:sockname(SslSocket); - Socket -> inet:sockname(Socket) - end, - inet_parse:ntoa(Address) ++ ":" ++ integer_to_list(Port); - Value1 -> - Value1 - end; - Value -> - Value - end. - -absolute_uri(#httpd{mochi_req = MochiReq} = Req, [$/ | _] = Path) -> - Host = host_for_request(Req), - XSsl = chttpd_util:get_chttpd_config("x_forwarded_ssl", "X-Forwarded-Ssl"), - Scheme = - case MochiReq:get_header_value(XSsl) of - "on" -> - "https"; - _ -> - XProto = chttpd_util:get_chttpd_config( - "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; -absolute_uri(_Req, _Path) -> - throw({bad_request, "path must begin with a /."}). - -unquote(UrlEncodedString) -> - chttpd: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, - chttpd_util:get_chttpd_config_integer( - "max_http_request_size", 4294967296 - ) - ). - -body_length(#httpd{mochi_req = MochiReq}) -> - MochiReq:get(body_length). - -body(#httpd{mochi_req = MochiReq, req_body = undefined}) -> - MaxSize = chttpd_util:get_chttpd_config_integer( - "max_http_request_size", 4294967296 - ), - MochiReq:recv_body(MaxSize); -body(#httpd{req_body = ReqBody}) -> - ReqBody. - -json_body(#httpd{req_body = undefined} = Httpd) -> - case body(Httpd) of - undefined -> - throw({bad_request, "Missing request body"}); - Body -> - ?JSON_DECODE(maybe_decompress(Httpd, Body)) - end; -json_body(#httpd{req_body = ReqBody}) -> - ReqBody. - -json_body_obj(Httpd) -> - case json_body(Httpd) of - {Props} -> {Props}; - _Else -> throw({bad_request, "Request body must be a JSON object"}) - end. - -maybe_decompress(Httpd, Body) -> - case header_value(Httpd, "Content-Encoding", "identity") of - "gzip" -> - zlib:gunzip(Body); - "identity" -> - Body; - Else -> - throw({bad_ctype, [Else, " is not a supported content encoding."]}) - end. - -doc_etag(#doc{id = Id, body = Body, revs = {Start, [DiskRev | _]}}) -> - doc_etag(Id, Body, {Start, DiskRev}). - -doc_etag(<<"_local/", _/binary>>, Body, {Start, DiskRev}) -> - make_etag({Start, DiskRev, Body}); -doc_etag(_Id, _Body, {Start, DiskRev}) -> - rev_etag({Start, DiskRev}). - -rev_etag({Start, DiskRev}) -> - Rev = couch_doc:rev_to_str({Start, DiskRev}), - <<$", Rev/binary, $">>. - -make_etag(Term) -> - <<SigInt:128/integer>> = couch_hash:md5_hash(term_to_binary(Term)), - iolist_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( - 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. - send_response(Req, 304, [{"ETag", CurrentEtag}], <<>>); - false -> - % Run the function. - RespFun() - end. - -etag_maybe(Req, RespFun) -> - try - RespFun() - catch - throw:{etag_match, ETag} -> - send_response(Req, 304, [{"ETag", ETag}], <<>>) - end. - -verify_is_server_admin(#httpd{user_ctx = UserCtx}) -> - verify_is_server_admin(UserCtx); -verify_is_server_admin(#user_ctx{roles = Roles}) -> - case lists:member(<<"_admin">>, Roles) of - true -> ok; - false -> throw({unauthorized, <<"You are not a server admin.">>}) - end. - -log_request(#httpd{mochi_req = MochiReq, peer = Peer} = Req, Code) -> - case erlang:get(dont_log_request) of - true -> - ok; - _ -> - couch_log:notice("~s - - ~s ~s ~B", [ - Peer, - MochiReq:get(method), - MochiReq:get(raw_path), - Code - ]), - gen_event:notify(couch_plugin, {log_request, Req, Code}) - end. - -log_response(Code, _) when Code < 400 -> - ok; -log_response(Code, Body) -> - case {erlang:get(dont_log_response), Body} of - {true, _} -> - ok; - {_, {json, JsonObj}} -> - ErrorMsg = couch_util:json_encode(JsonObj), - couch_log:error("httpd ~p error response:~n ~s", [Code, ErrorMsg]); - _ -> - couch_log:error("httpd ~p error response:~n ~s", [Code, Body]) - end. - -start_response_length(#httpd{mochi_req = MochiReq} = Req, Code, Headers0, Length) -> - Headers1 = basic_headers(Req, Headers0), - Resp = handle_response(Req, Code, Headers1, Length, start_response_length), - case MochiReq:get(method) of - 'HEAD' -> throw({http_head_abort, Resp}); - _ -> ok - end, - {ok, Resp}. - -start_response(#httpd{mochi_req = MochiReq} = Req, Code, Headers0) -> - Headers1 = basic_headers(Req, Headers0), - Resp = handle_response(Req, Code, Headers1, undefined, start_response), - case MochiReq:get(method) of - 'HEAD' -> throw({http_head_abort, Resp}); - _ -> ok - end, - {ok, Resp}. - -send({remote, Pid, Ref} = Resp, Data) -> - Pid ! {Ref, send, Data}, - {ok, Resp}; -send(Resp, Data) -> - Resp:send(Data), - {ok, Resp}. - -no_resp_conn_header([]) -> - true; -no_resp_conn_header([{Hdr, V} | Rest]) when is_binary(Hdr) -> - no_resp_conn_header([{?b2l(Hdr), V} | Rest]); -no_resp_conn_header([{Hdr, _} | Rest]) when is_list(Hdr) -> - case string:to_lower(Hdr) of - "connection" -> false; - _ -> no_resp_conn_header(Rest) - end. - -http_1_0_keep_alive(#httpd{mochi_req = MochiReq}, Headers) -> - http_1_0_keep_alive(MochiReq, Headers); -http_1_0_keep_alive(Req, Headers) -> - KeepOpen = Req:should_close() == false, - IsHttp10 = Req:get(version) == {1, 0}, - NoRespHeader = no_resp_conn_header(Headers), - case KeepOpen andalso IsHttp10 andalso NoRespHeader of - true -> [{"Connection", "Keep-Alive"} | Headers]; - false -> Headers - end. - -start_chunked_response(#httpd{mochi_req = MochiReq} = Req, Code, Headers0) -> - Headers1 = add_headers(Req, Headers0), - Resp = handle_response(Req, Code, Headers1, chunked, respond), - case MochiReq:get(method) of - 'HEAD' -> throw({http_head_abort, Resp}); - _ -> ok - end, - {ok, Resp}. - -send_chunk({remote, Pid, Ref} = Resp, Data) -> - Pid ! {Ref, chunk, Data}, - {ok, Resp}; -send_chunk(Resp, Data) -> - case iolist_size(Data) of - % do nothing - 0 -> ok; - _ -> Resp:write_chunk(Data) - end, - {ok, Resp}. - -last_chunk({remote, Pid, Ref} = Resp) -> - Pid ! {Ref, chunk, <<>>}, - {ok, Resp}; -last_chunk(Resp) -> - Resp:write_chunk([]), - {ok, Resp}. - -send_response(Req, Code, Headers0, Body) -> - Headers1 = chttpd_cors:headers(Req, Headers0), - send_response_no_cors(Req, Code, Headers1, Body). - -send_response_no_cors(#httpd{mochi_req = MochiReq} = Req, Code, Headers, Body) -> - Headers1 = http_1_0_keep_alive(MochiReq, Headers), - Headers2 = basic_headers_no_cors(Req, Headers1), - Headers3 = chttpd_xframe_options:header(Req, Headers2), - Headers4 = chttpd_prefer_header:maybe_return_minimal(Req, Headers3), - Resp = handle_response(Req, Code, Headers4, Body, respond), - log_response(Code, Body), - {ok, Resp}. - -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) -> - initialize_jsonp(Req), - AllHeaders = maybe_add_default_headers(Req, Headers), - send_response(Req, Code, AllHeaders, {json, Value}). - -start_json_response(Req, Code) -> - start_json_response(Req, Code, []). - -start_json_response(Req, Code, Headers) -> - initialize_jsonp(Req), - AllHeaders = maybe_add_default_headers(Req, Headers), - {ok, Resp} = start_chunked_response(Req, Code, AllHeaders), - case start_jsonp() of - [] -> ok; - Start -> send_chunk(Resp, Start) - end, - {ok, Resp}. - -end_json_response(Resp) -> - send_chunk(Resp, end_jsonp() ++ [$\n]), - last_chunk(Resp). - -maybe_add_default_headers(ForRequest, ToHeaders) -> - DefaultHeaders = [ - {"Cache-Control", "must-revalidate"}, - {"Content-Type", negotiate_content_type(ForRequest)} - ], - lists:ukeymerge(1, lists:keysort(1, ToHeaders), DefaultHeaders). - -initialize_jsonp(Req) -> - case get(jsonp) of - undefined -> put(jsonp, qs_value(Req, "callback", no_jsonp)); - _ -> ok - end, - case get(jsonp) of - no_jsonp -> - []; - [] -> - []; - CallBack -> - try - % make sure jsonp is configured on (default off) - case - chttpd_util:get_chttpd_config_boolean( - "allow_jsonp", false - ) - of - true -> - validate_callback(CallBack); - false -> - put(jsonp, no_jsonp) - end - catch - Error -> - put(jsonp, no_jsonp), - throw(Error) - end - end. - -start_jsonp() -> - case get(jsonp) of - no_jsonp -> []; - [] -> []; - CallBack -> ["/* CouchDB */", CallBack, "("] - end. - -end_jsonp() -> - case erlang:erase(jsonp) of - no_jsonp -> []; - [] -> []; - _ -> ");" - end. - -validate_callback(CallBack) when is_binary(CallBack) -> - validate_callback(binary_to_list(CallBack)); -validate_callback([]) -> - ok; -validate_callback([Char | Rest]) -> - case Char of - _ when Char >= $a andalso Char =< $z -> ok; - _ when Char >= $A andalso Char =< $Z -> ok; - _ when Char >= $0 andalso Char =< $9 -> ok; - _ when Char == $. -> ok; - _ when Char == $_ -> ok; - _ when Char == $[ -> ok; - _ when Char == $] -> ok; - _ -> throw({bad_request, invalid_callback}) - end, - validate_callback(Rest). - -error_info({Error, Reason}) when is_list(Reason) -> - error_info({Error, ?l2b(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}; -% Prior art for md5 mismatch resulting in a 400 is from AWS S3 -error_info(md5_mismatch) -> - {400, <<"content_md5_mismatch">>, <<"Possible message corruption.">>}; -error_info({illegal_docid, Reason}) -> - {400, <<"illegal_docid">>, Reason}; -error_info({illegal_partition, Reason}) -> - {400, <<"illegal_partition">>, Reason}; -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({forbidden, Msg}) -> - {403, <<"forbidden">>, 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(request_entity_too_large) -> - {413, <<"too_large">>, <<"the request entity is too large">>}; -error_info({request_entity_too_large, {attachment, AttName}}) -> - {413, <<"attachment_too_large">>, AttName}; -error_info({request_entity_too_large, DocID}) -> - {413, <<"document_too_large">>, DocID}; -error_info(request_uri_too_long) -> - {414, <<"too_long">>, <<"the request uri is too long">>}; -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, Name}}) -> - Message = - <<"Name: '", Name/binary, "'. Only lowercase characters (a-z), ", - "digits (0-9), and any of the characters _, $, (, ), +, -, and / ", - "are allowed. Must begin with a letter.">>, - {400, <<"illegal_database_name">>, Message}; -error_info({missing_stub, Reason}) -> - {412, <<"missing_stub">>, Reason}; -error_info({misconfigured_server, Reason}) -> - {500, <<"misconfigured_server">>, couch_util:to_binary(Reason)}; -error_info({Error, Reason}) -> - {500, couch_util:to_binary(Error), couch_util:to_binary(Reason)}; -error_info(Error) -> - {500, <<"unknown_error">>, couch_util:to_binary(Error)}. - -error_headers(#httpd{mochi_req = MochiReq} = Req, Code, ErrorStr, ReasonStr) -> - if - Code == 401 -> - % this is where the basic auth popup is triggered - case MochiReq:get_header_value("X-CouchDB-WWW-Authenticate") of - undefined -> - case chttpd_util:get_chttpd_config("WWW-Authenticate") of - undefined -> - % 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 - chttpd_util:get_chttpd_auth_config( - "authentication_redirect", "/_utils/session.html" - ) - of - undefined -> - {Code, []}; - AuthRedirect -> - case - chttpd_util:get_chttpd_auth_config_boolean( - "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 -> - case - MochiReq:accepts_content_type( - "application/json" - ) - of - true -> - {Code, []}; - false -> - case - MochiReq:accepts_content_type( - "text/html" - ) - of - true -> - % 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, - RedirectLocation = lists:flatten( - [ - AuthRedirect, - "?return=", - couch_util:url_encode( - UrlReturnRaw - ), - "&reason=", - couch_util:url_encode( - ReasonStr - ) - ] - ), - {302, [ - {"Location", - absolute_uri( - Req, - RedirectLocation - )} - ]}; - false -> - {Code, []} - end - end - end - end; - _Else -> - {Code, []} - end; - Type -> - {Code, [{"WWW-Authenticate", Type}]} - end; - Type -> - {Code, [{"WWW-Authenticate", Type}]} - end; - true -> - {Code, []} - end. - -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). - -send_error(Req, Code, ErrorStr, ReasonStr) -> - send_error(Req, Code, [], ErrorStr, ReasonStr). - -send_error(Req, Code, Headers, ErrorStr, ReasonStr) -> - send_json( - Req, - Code, - Headers, - {[ - {<<"error">>, ErrorStr}, - {<<"reason">>, ReasonStr} - ]} - ). - -% give the option for list functions to output html or other raw errors -send_chunked_error(Resp, {_Error, {[{<<"body">>, Reason}]}}) -> - send_chunk(Resp, Reason), - last_chunk(Resp); -send_chunked_error(Resp, Error) -> - {Code, ErrorStr, ReasonStr} = error_info(Error), - JsonError = - {[ - {<<"code">>, Code}, - {<<"error">>, ErrorStr}, - {<<"reason">>, ReasonStr} - ]}, - send_chunk(Resp, ?l2b([$\n, ?JSON_ENCODE(JsonError), $\n])), - last_chunk(Resp). - -send_redirect(Req, Path) -> - send_response(Req, 301, [{"Location", absolute_uri(Req, Path)}], <<>>). - -negotiate_content_type(_Req) -> - case get(jsonp) of - no_jsonp -> "application/json"; - [] -> "application/json"; - _Callback -> "application/javascript" - end. - -server_header() -> - [ - {"Server", - "CouchDB/" ++ couch_server:get_version() ++ - " (Erlang OTP/" ++ erlang:system_info(otp_release) ++ ")"} - ]. - --record(mp, {boundary, buffer, data_fun, callback}). - -parse_multipart_request(ContentType, DataFun, Callback) -> - Boundary0 = iolist_to_binary(get_boundary(ContentType)), - Boundary = <<"\r\n--", Boundary0/binary>>, - Mp = #mp{ - boundary = Boundary, - buffer = <<>>, - data_fun = DataFun, - callback = Callback - }, - {Mp2, _NilCallback} = read_until( - Mp, - <<"--", Boundary0/binary>>, - fun nil_callback/1 - ), - #mp{buffer = Buffer, data_fun = DataFun2, callback = Callback2} = - parse_part_header(Mp2), - {Buffer, DataFun2, Callback2}. - -nil_callback(_Data) -> - fun nil_callback/1. - -get_boundary({"multipart/" ++ _, Opts}) -> - case couch_util:get_value("boundary", Opts) of - S when is_list(S) -> - S - end; -get_boundary(ContentType) -> - {"multipart/" ++ _, Opts} = mochiweb_util:parse_header(ContentType), - get_boundary({"multipart/", Opts}). - -split_header(<<>>) -> - []; -split_header(Line) -> - {Name, Rest} = lists:splitwith( - fun(C) -> C =/= $: end, - binary_to_list(Line) - ), - [$: | Value] = - case Rest of - [] -> - throw({bad_request, <<"bad part header">>}); - Res -> - Res - end, - [{string:to_lower(string:strip(Name)), mochiweb_util:parse_header(Value)}]. - -read_until(#mp{data_fun = DataFun, buffer = Buffer} = Mp, Pattern, Callback) -> - case couch_util:find_in_binary(Pattern, Buffer) of - not_found -> - Callback2 = Callback(Buffer), - {Buffer2, DataFun2} = DataFun(), - Buffer3 = iolist_to_binary(Buffer2), - read_until(Mp#mp{data_fun = DataFun2, buffer = Buffer3}, Pattern, Callback2); - {partial, 0} -> - {NewData, DataFun2} = DataFun(), - read_until( - Mp#mp{ - data_fun = DataFun2, - buffer = iolist_to_binary([Buffer, NewData]) - }, - Pattern, - Callback - ); - {partial, Skip} -> - <<DataChunk:Skip/binary, Rest/binary>> = Buffer, - Callback2 = Callback(DataChunk), - {NewData, DataFun2} = DataFun(), - read_until( - Mp#mp{ - data_fun = DataFun2, - buffer = iolist_to_binary([Rest | NewData]) - }, - Pattern, - Callback2 - ); - {exact, 0} -> - PatternLen = size(Pattern), - <<_:PatternLen/binary, Rest/binary>> = Buffer, - {Mp#mp{buffer = Rest}, Callback}; - {exact, Skip} -> - PatternLen = size(Pattern), - <<DataChunk:Skip/binary, _:PatternLen/binary, Rest/binary>> = Buffer, - Callback2 = Callback(DataChunk), - {Mp#mp{buffer = Rest}, Callback2} - end. - -parse_part_header(#mp{callback = UserCallBack} = Mp) -> - {Mp2, AccCallback} = read_until( - Mp, - <<"\r\n\r\n">>, - fun(Next) -> acc_callback(Next, []) end - ), - HeaderData = AccCallback(get_data), - - Headers = - lists:foldl( - fun(Line, Acc) -> - split_header(Line) ++ Acc - end, - [], - re:split(HeaderData, <<"\r\n">>, []) - ), - NextCallback = UserCallBack({headers, Headers}), - parse_part_body(Mp2#mp{callback = NextCallback}). - -parse_part_body(#mp{boundary = Prefix, callback = Callback} = Mp) -> - {Mp2, WrappedCallback} = read_until( - Mp, - Prefix, - fun(Data) -> body_callback_wrapper(Data, Callback) end - ), - Callback2 = WrappedCallback(get_callback), - Callback3 = Callback2(body_end), - case check_for_last(Mp2#mp{callback = Callback3}) of - {last, #mp{callback = Callback3} = Mp3} -> - Mp3#mp{callback = Callback3(eof)}; - {more, Mp3} -> - parse_part_header(Mp3) - end. - -acc_callback(get_data, Acc) -> - iolist_to_binary(lists:reverse(Acc)); -acc_callback(Data, Acc) -> - fun(Next) -> acc_callback(Next, [Data | Acc]) end. - -body_callback_wrapper(get_callback, Callback) -> - Callback; -body_callback_wrapper(Data, Callback) -> - Callback2 = Callback({body, Data}), - fun(Next) -> body_callback_wrapper(Next, Callback2) end. - -check_for_last(#mp{buffer = Buffer, data_fun = DataFun} = Mp) -> - case Buffer of - <<"--", _/binary>> -> - {last, Mp}; - <<_, _, _/binary>> -> - {more, Mp}; - % not long enough - _ -> - {Data, DataFun2} = DataFun(), - check_for_last(Mp#mp{ - buffer = <<Buffer/binary, Data/binary>>, - data_fun = DataFun2 - }) - end. - -validate_bind_address(any) -> - ok; -validate_bind_address(Address) -> - case inet_parse:address(Address) of - {ok, _} -> ok; - _ -> throw({error, invalid_bind_address}) - end. - -add_headers(Req, Headers0) -> - Headers = basic_headers(Req, Headers0), - Headers1 = http_1_0_keep_alive(Req, Headers), - chttpd_prefer_header:maybe_return_minimal(Req, Headers1). - -basic_headers(Req, Headers0) -> - Headers1 = basic_headers_no_cors(Req, Headers0), - Headers2 = chttpd_xframe_options:header(Req, Headers1), - chttpd_cors:headers(Req, Headers2). - -basic_headers_no_cors(Req, Headers) -> - Headers ++ - server_header() ++ - couch_httpd_auth:cookie_auth_header(Req, Headers). - -handle_response(Req0, Code0, Headers0, Args0, Type) -> - {ok, {Req1, Code1, Headers1, Args1}} = before_response(Req0, Code0, Headers0, Args0), - couch_stats:increment_counter([couchdb, httpd_status_codes, Code1]), - log_request(Req0, Code1), - respond_(Req1, Code1, Headers1, Args1, Type). - -before_response(Req0, Code0, Headers0, {json, JsonObj}) -> - {ok, {Req1, Code1, Headers1, Body1}} = - chttpd_plugin:before_response(Req0, Code0, Headers0, JsonObj), - Body2 = [start_jsonp(), ?JSON_ENCODE(Body1), end_jsonp(), $\n], - {ok, {Req1, Code1, Headers1, Body2}}; -before_response(Req0, Code0, Headers0, Args0) -> - chttpd_plugin:before_response(Req0, Code0, Headers0, Args0). - -respond_(#httpd{mochi_req = MochiReq} = Req, Code, Headers, Args, Type) -> - case MochiReq:get(socket) of - {remote, Pid, Ref} -> - Pid ! {Ref, Code, Headers, Args, Type}, - {remote, Pid, Ref}; - _Else -> - http_respond_(Req, Code, Headers, Args, Type) - end. - -http_respond_(#httpd{mochi_req = MochiReq}, Code, Headers, _Args, start_response) -> - MochiReq:start_response({Code, Headers}); -http_respond_(#httpd{mochi_req = MochiReq}, 413, Headers, Args, Type) -> - % Special handling for the 413 response. Make sure the socket is closed as - % we don't know how much data was read before the error was thrown. Also - % drain all the data in the receive buffer to avoid connction being reset - % before the 413 response is parsed by the client. This is still racy, it - % just increases the chances of 413 being detected correctly by the client - % (rather than getting a brutal TCP reset). - erlang:put(mochiweb_request_force_close, true), - Result = MochiReq:Type({413, Headers, Args}), - Socket = MochiReq:get(socket), - mochiweb_socket:recv(Socket, ?MAX_DRAIN_BYTES, ?MAX_DRAIN_TIME_MSEC), - Result; -http_respond_(#httpd{mochi_req = MochiReq}, Code, Headers, Args, Type) -> - MochiReq:Type({Code, Headers, Args}). - -peer(MochiReq) -> - case MochiReq:get(socket) of - {remote, Pid, _} -> - node(Pid); - _ -> - MochiReq:get(peer) - end. - -%%%%%%%% module tests below %%%%%%%% - --ifdef(TEST). --include_lib("couch/include/couch_eunit.hrl"). - -maybe_add_default_headers_test_() -> - DummyRequest = [], - NoCache = {"Cache-Control", "no-cache"}, - ApplicationJson = {"Content-Type", "application/json"}, - % couch_httpd uses process dictionary to check if currently in a - % json serving method. Defaults to 'application/javascript' otherwise. - % Therefore must-revalidate and application/javascript should be added - % by chttpd if such headers are not present - MustRevalidate = {"Cache-Control", "must-revalidate"}, - ApplicationJavascript = {"Content-Type", "application/javascript"}, - Cases = [ - { - [], - [MustRevalidate, ApplicationJavascript], - "Should add Content-Type and Cache-Control to empty heaeders" - }, - - { - [NoCache], - [NoCache, ApplicationJavascript], - "Should add Content-Type only if Cache-Control is present" - }, - - { - [ApplicationJson], - [MustRevalidate, ApplicationJson], - "Should add Cache-Control if Content-Type is present" - }, - - { - [NoCache, ApplicationJson], - [NoCache, ApplicationJson], - "Should not add headers if Cache-Control and Content-Type are there" - } - ], - Tests = lists:map( - fun({InitialHeaders, ProperResult, Desc}) -> - {Desc, - ?_assertEqual( - ProperResult, - maybe_add_default_headers(DummyRequest, InitialHeaders) - )} - end, - Cases - ), - {"Tests adding default headers", Tests}. - -log_request_test_() -> - {setup, - fun() -> - ok = meck:new([couch_log]), - ok = meck:expect(couch_log, error, fun(Fmt, Args) -> - case catch io_lib_format:fwrite(Fmt, Args) of - {'EXIT', Error} -> Error; - _ -> ok - end - end) - end, - fun(_) -> - meck:unload() - end, - [ - fun() -> should_accept_code_and_message(true) end, - fun() -> should_accept_code_and_message(false) end - ]}. - -should_accept_code_and_message(DontLogFlag) -> - erlang:put(dont_log_response, DontLogFlag), - {"with dont_log_response = " ++ atom_to_list(DontLogFlag), [ - {"Should accept code 200 and string message", ?_assertEqual(ok, log_response(200, "OK"))}, - {"Should accept code 200 and JSON message", - ?_assertEqual(ok, log_response(200, {json, {[{ok, true}]}}))}, - {"Should accept code >= 400 and string error", - ?_assertEqual(ok, log_response(405, method_not_allowed))}, - {"Should accept code >= 400 and JSON error", - ?_assertEqual( - ok, - log_response(405, {json, {[{error, method_not_allowed}]}}) - )}, - {"Should accept code >= 500 and string error", ?_assertEqual(ok, log_response(500, undef))}, - {"Should accept code >= 500 and JSON error", - ?_assertEqual(ok, log_response(500, {json, {[{error, undef}]}}))} - ]}. - --endif. |