| % 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_handler). |
| -include_lib("couch/include/couch_db.hrl"). |
| -include_lib("couch_httpd/include/couch_httpd.hrl"). |
| |
| -export([ |
| start_link/2, |
| stop/1 |
| ]). |
| |
| -export([ |
| handle_request/2, |
| handle_request_int/2 |
| ]). |
| |
| -export([ |
| authenticate_request/3 |
| ]). |
| |
| |
| -record(delayed_resp, { |
| start_fun, |
| req, |
| code, |
| headers, |
| first_chunk, |
| resp=nil |
| }). |
| |
| start_link(Stack, http) -> |
| start_link(Stack, [{port, Stack:port()}]); |
| |
| start_link(Stack, https) -> |
| Port = config:get("ssl", "port", "6984"), |
| |
| Options = |
| [{port, Port}, |
| {ssl, true}, |
| {ssl_opts, ssl_options()}], |
| start_link(Stack, Options); |
| |
| start_link(Stack, Options) -> |
| {IP, ServerOpts} = get_stack_config(Stack), |
| |
| Options1 = Options ++ [ |
| {loop, fun(Req) -> ?MODULE:handle_request(Stack, Req) end}, |
| {name, Stack:name()}, |
| {ip, IP} |
| ], |
| |
| Options2 = lists:keymerge(1, lists:sort(Options1), lists:sort(ServerOpts)), |
| case mochiweb_http:start(Options2) of |
| {ok, Pid} -> |
| {ok, Pid}; |
| {error, Reason} -> |
| io:format("Failure to start Mochiweb: ~s~n", [Reason]), |
| {error, Reason} |
| end. |
| |
| stop(Name) -> |
| catch mochiweb_http:stop(Name), |
| ok. |
| |
| handle_request(Stack, MochiReq0) -> |
| erlang:put(?REWRITE_COUNT, 0), |
| MochiReq = couch_httpd_vhost:dispatch_host(MochiReq0), |
| handle_request_int(Stack, MochiReq). |
| |
| handle_request_int(Stack, MochiReq) -> |
| Begin = os:timestamp(), |
| |
| set_socket_options(Stack, MochiReq), |
| |
| % 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, |
| |
| Peer = MochiReq:get(peer), |
| |
| 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:notice("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, |
| |
| Nonce = couch_util:to_hex(crypto:rand_bytes(5)), |
| |
| HttpReq0 = #httpd{ |
| mochi_req = MochiReq, |
| begin_ts = Begin, |
| peer = Peer, |
| original_method = Method1, |
| nonce = Nonce, |
| method = Method, |
| path_parts = [list_to_binary(couch_httpd:unquote(Part)) |
| || Part <- string:tokens(Path, "/")], |
| requested_path_parts = [?l2b(couch_httpd:unquote(Part)) |
| || Part <- string:tokens(RequestedPath, "/")], |
| user_ctx = erlang:erase(pre_rewrite_user_ctx), |
| auth = erlang:erase(pre_rewrite_auth), |
| stack = Stack |
| }, |
| |
| % put small token on heap to keep requests synced to backend calls |
| erlang:put(nonce, Nonce), |
| |
| % suppress duplicate log |
| erlang:put(dont_log_request, true), |
| |
| {HttpReq2, Response} = case before_request(HttpReq0) of |
| {ok, HttpReq1} -> |
| process_request(HttpReq1); |
| {error, Response0} -> |
| {HttpReq0, Response0} |
| end, |
| |
| {Status, Code, Reason, Resp} = split_response(Response), |
| |
| HttpResp = #httpd_resp{ |
| code = Code, |
| status = Status, |
| response = Resp, |
| nonce = HttpReq2#httpd.nonce, |
| reason = Reason, |
| stack = Stack |
| }, |
| |
| case after_request(HttpReq2, HttpResp) of |
| #httpd_resp{status = ok, response = Resp} -> |
| {ok, Resp}; |
| #httpd_resp{status = aborted, reason = Reason} -> |
| couch_log:error("Response abnormally terminated: ~p", [Reason]), |
| exit(normal) |
| end. |
| |
| before_request(HttpReq) -> |
| try |
| couch_httpd_plugin:before_request(HttpReq) |
| catch Tag:Error -> |
| {error, catch_error(HttpReq, Tag, Error)} |
| end. |
| |
| after_request(HttpReq, HttpResp0) -> |
| {ok, HttpResp1} = |
| try |
| couch_httpd_plugin:after_request(HttpReq, HttpResp0) |
| catch _Tag:Error -> |
| Stack = erlang:get_stacktrace(), |
| couch_httpd:send_error(HttpReq, {Error, nil, Stack}), |
| {ok, HttpResp0#httpd_resp{status = aborted}} |
| end, |
| HttpResp2 = update_stats(HttpReq, HttpResp1), |
| maybe_log(HttpReq, HttpResp2), |
| HttpResp2. |
| |
| process_request(#httpd{mochi_req = MochiReq, stack = Stack} = HttpReq) -> |
| HandlerKey = |
| case HttpReq#httpd.path_parts of |
| [] -> <<>>; |
| [Key|_] -> ?l2b(couch_httpd:quote(Key)) |
| end, |
| |
| RawUri = MochiReq:get(raw_path), |
| |
| try |
| couch_httpd:validate_host(HttpReq), |
| check_request_uri_length(RawUri), |
| case couch_httpd_cors:maybe_handle_preflight_request(HttpReq) of |
| not_preflight -> |
| case couch_httpd_auth_plugin:authenticate(HttpReq) of |
| #httpd{} = Req -> |
| HandlerFun = couch_httpd_handlers:url_handler(HandlerKey, Stack), |
| AuthorizedReq = couch_httpd_auth_plugin:authorize(possibly_hack(Req)), |
| {AuthorizedReq, HandlerFun(AuthorizedReq)}; |
| Response -> |
| {HttpReq, Response} |
| end; |
| Response -> |
| {HttpReq, Response} |
| end |
| catch Tag:Error -> |
| {HttpReq, catch_error(HttpReq, Tag, Error)} |
| end. |
| |
| catch_error(_HttpReq, throw, {http_head_abort, Resp}) -> |
| {ok, Resp}; |
| catch_error(_HttpReq, throw, {http_abort, Resp, Reason}) -> |
| {aborted, Resp, Reason}; |
| catch_error(HttpReq, throw, {invalid_json, _}) -> |
| couch_httpd:send_error(HttpReq, {bad_request, "invalid UTF-8 JSON"}); |
| catch_error(HttpReq, exit, {mochiweb_recv_error, E}) -> |
| #httpd{ |
| mochi_req = MochiReq, |
| peer = Peer, |
| original_method = Method |
| } = HttpReq, |
| couch_log:notice("mochiweb_recv_error for ~s - ~p ~s - ~p", [ |
| Peer, |
| Method, |
| MochiReq:get(raw_path), |
| E]), |
| exit(normal); |
| catch_error(_HttpReq, exit, normal) -> |
| exit(normal); |
| catch_error(HttpReq, exit, {uri_too_long, _}) -> |
| couch_httpd:send_error(HttpReq, request_uri_too_long); |
| catch_error(HttpReq, exit, {body_too_large, _}) -> |
| couch_httpd:send_error(HttpReq, request_entity_too_large); |
| catch_error(HttpReq, throw, unacceptable_encoding) -> |
| couch_httpd:send_error(HttpReq, {not_acceptable, "unsupported encoding"}); |
| catch_error(HttpReq, throw, bad_accept_encoding_value) -> |
| couch_httpd:send_error(HttpReq, bad_request); |
| catch_error(HttpReq, throw, Error) -> |
| couch_httpd:send_error(HttpReq, Error); |
| catch_error(HttpReq, error, database_does_not_exist) -> |
| couch_httpd:send_error(HttpReq, database_does_not_exist); |
| catch_error(HttpReq, 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]), |
| couch_httpd:send_error(HttpReq, {bad_otp_release, ErrorReason}); |
| catch_error(HttpReq, Tag, Error) -> |
| Stack = erlang:get_stacktrace(), |
| % TODO improve logging and metrics collection for client disconnects |
| case {Tag, Error, Stack} of |
| {exit, normal, [{mochiweb_request, send, _, _} | _]} -> |
| exit(normal); % Client disconnect (R15+) |
| _Else -> |
| couch_httpd:send_error(HttpReq, {Error, nil, Stack}) |
| end. |
| |
| split_response({ok, #delayed_resp{resp=Resp}}) -> |
| {ok, Resp:get(code), undefined, Resp}; |
| split_response({ok, Resp}) -> |
| {ok, Resp:get(code), undefined, Resp}; |
| split_response({aborted, Resp, AbortReason}) -> |
| {aborted, Resp:get(code), AbortReason, Resp}. |
| |
| update_stats(HttpReq, #httpd_resp{end_ts = undefined} = Res) -> |
| update_stats(HttpReq, Res#httpd_resp{end_ts = os:timestamp()}); |
| update_stats(#httpd{begin_ts = BeginTime}, #httpd_resp{} = Res) -> |
| #httpd_resp{status = Status, end_ts = EndTime} = Res, |
| RequestTime = timer:now_diff(EndTime, BeginTime) / 1000, |
| couch_stats:update_histogram([couchdb, request_time], RequestTime), |
| case Status of |
| ok -> |
| couch_stats:increment_counter([couchdb, httpd, requests]); |
| aborted -> |
| couch_stats:increment_counter([couchdb, httpd, aborted_requests]) |
| end, |
| Res. |
| |
| maybe_log(#httpd{} = HttpReq, #httpd_resp{should_log = true} = HttpResp) -> |
| #httpd{ |
| mochi_req = MochiReq, |
| begin_ts = BeginTime, |
| original_method = Method, |
| peer = Peer, |
| nonce = Nonce |
| } = HttpReq, |
| #httpd_resp{ |
| end_ts = EndTime, |
| code = Code, |
| status = Status |
| } = HttpResp, |
| Host = MochiReq:get_header_value("Host"), |
| RawUri = MochiReq:get(raw_path), |
| RequestTime = timer:now_diff(EndTime, BeginTime) / 1000, |
| couch_log:notice("~s ~s ~s ~s ~s ~B ~p ~B", [Nonce, Peer, Host, |
| Method, RawUri, Code, Status, round(RequestTime)]); |
| maybe_log(_HttpReq, #httpd_resp{should_log = false}) -> |
| ok. |
| |
| |
| %% 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. |
| |
| check_request_uri_length(Uri) -> |
| check_request_uri_length(Uri, config:get("httpd", "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. |
| |
| fix_uri(Req, Props, Type) -> |
| case replication_uri(Type, Props) of |
| undefined -> |
| Props; |
| Uri0 -> |
| case is_http(Uri0) of |
| true -> |
| Props; |
| false -> |
| Uri = make_uri(Req, couch_httpd:quote(Uri0)), |
| [{Type,Uri}|proplists:delete(Type,Props)] |
| end |
| 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) -> |
| Port = couch_httpd:port(Req), |
| Url = list_to_binary(["http://", config:get("httpd", "bind_address"), |
| ":", Port, "/", Raw]), |
| Headers = [ |
| {<<"authorization">>, ?l2b(couch_httpd:header_value(Req,"authorization",""))}, |
| {<<"cookie">>, ?l2b(extract_cookie(Req))} |
| ], |
| {[{<<"url">>,Url}, {<<"headers">>,{Headers}}]}. |
| |
| extract_cookie(#httpd{mochi_req = MochiReq}) -> |
| case MochiReq:get_cookie_value("AuthSession") of |
| undefined -> |
| ""; |
| AuthSession -> |
| "AuthSession=" ++ AuthSession |
| end. |
| %%% end hack |
| |
| authenticate_request(#httpd{} = Req0, AuthModule, AuthFuns) -> |
| Req = Req0#httpd{ |
| auth_module = AuthModule, |
| authentication_handlers = AuthFuns}, |
| authenticate_request(Req, AuthFuns). |
| |
| % Try authentication handlers in order until one returns a result |
| authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthFuns) -> |
| Req; |
| authenticate_request(#httpd{} = Req, [{Name, AuthFun}|Rest]) -> |
| authenticate_request(maybe_set_handler(AuthFun(Req), Name), Rest); |
| authenticate_request(Response, _AuthFuns) -> |
| Response. |
| |
| maybe_set_handler(#httpd{user_ctx=#user_ctx{} = UserCtx} = Req, Name) -> |
| Req#httpd{user_ctx = UserCtx#user_ctx{handler = Name}}; |
| maybe_set_handler(Else, _) -> |
| Else. |
| |
| increment_method_stats(Method) -> |
| couch_stats:increment_counter([couchdb, httpd_request_methods, Method]). |
| |
| ssl_options() -> |
| {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 -> |
| io:format("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, couch_httpd_util:fun_from_spec(SpecStr, 3)}] |
| end |
| end, |
| ServerOpts ++ ClientOpts. |
| |
| set_socket_options(Stack, MochiReq) -> |
| case Stack:socket_options() of |
| undefined -> |
| ok; |
| SocketOpts -> |
| ok = mochiweb_socket:setopts(MochiReq:get(socket), SocketOpts) |
| end. |
| |
| get_stack_config(Stack) -> |
| IP = Stack:bind_address(), |
| ok = couch_httpd:validate_bind_address(IP), |
| {IP, Stack:server_options()}. |