summaryrefslogtreecommitdiff
path: root/src/couch/src/couch_httpd.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/couch/src/couch_httpd.erl')
-rw-r--r--src/couch/src/couch_httpd.erl1492
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.