diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index 9a50fae462..0e0eaba6bd 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -310,6 +310,11 @@ bind_address = 127.0.0.1 ; Set to false to avoid scrubbing and revert to the previous behavior. ;scrub_json_request = true +; Set to a string to override how couchdb determines the peer of a request +; Default is undefined, so couchdb delegates to mochiweb which uses +; X-Forwarded-For logic. +;peer_header = + ;[jwt_auth] ; List of claims to validate ; can be the name of a claim like "exp" or a tuple if the claim requires diff --git a/src/chttpd/src/chttpd.erl b/src/chttpd/src/chttpd.erl index 485e012c2c..a078df2af6 100644 --- a/src/chttpd/src/chttpd.erl +++ b/src/chttpd/src/chttpd.erl @@ -266,7 +266,7 @@ handle_request_int(MochiReq) -> P end, - Peer = MochiReq:get(peer), + Peer = couch_httpd:peer(MochiReq), Method1 = case MochiReq:get(method) of @@ -1694,4 +1694,28 @@ handle_req_after_auth_test() -> ok = meck:unload(chttpd_handlers), ok = meck:unload(chttpd_auth). +custom_peer_test() -> + Headers = mochiweb_headers:make([ + {"HOST", "127.0.0.1:15984"}, {"X-Couch-Client-IP", "1.2.3.4"} + ]), + MochiReq = mochiweb_request:new( + socket, + [], + 'PUT', + "/newdb", + version, + Headers + ), + Req = #httpd{ + mochi_req = MochiReq, + begin_ts = {1458, 588713, 124003}, + original_method = 'GET', + peer = "4.3.2.1", + nonce = "nonce" + }, + ok = meck:new(config, [passthrough]), + ok = meck:expect(config, get, fun("chttpd", "peer_header") -> "x-couch-client-ip" end), + ?assertEqual("1.2.3.4", couch_httpd:peer(Req)), + ok = meck:unload(config). + -endif. diff --git a/src/chttpd/src/chttpd_external.erl b/src/chttpd/src/chttpd_external.erl index fe63de3b22..ed292ca9bd 100644 --- a/src/chttpd/src/chttpd_external.erl +++ b/src/chttpd/src/chttpd_external.erl @@ -93,7 +93,7 @@ json_req_obj_field(<<"body">>, #httpd{req_body = undefined, mochi_req = Req}, _D json_req_obj_field(<<"body">>, #httpd{req_body = Body}, _Db, _DocId) -> Body; json_req_obj_field(<<"peer">>, #httpd{peer = undefined, mochi_req = Req}, _, _) -> - ?l2b(Req:get(peer)); + ?l2b(couch_httpd:peer(Req)); json_req_obj_field(<<"peer">>, #httpd{peer = Peer}, _Db, _DocId) -> ?l2b(Peer); json_req_obj_field(<<"form">>, #httpd{mochi_req = Req, method = Method} = HttpReq, Db, DocId) -> diff --git a/src/couch/src/couch_auth_lockout.erl b/src/couch/src/couch_auth_lockout.erl index ceed57f113..39901a7d1d 100644 --- a/src/couch/src/couch_auth_lockout.erl +++ b/src/couch/src/couch_auth_lockout.erl @@ -27,8 +27,8 @@ is_locked_out(#httpd{} = Req, UserName, UserSalt) -> is_locked_out_int(#httpd{} = Req, Mode, UserName, UserSalt) -> LockoutThreshold = lockout_threshold(), - case peer(Req) of - nopeer -> + case Req#httpd.peer of + Peer when is_atom(Peer) -> false; Peer -> case ets_lru:lookup_d(?LRU, {Peer, UserName, UserSalt}) of @@ -48,8 +48,8 @@ is_locked_out_int(#httpd{} = Req, Mode, UserName, UserSalt) -> end. lockout(#httpd{} = Req, UserName, UserSalt) -> - case peer(Req) of - nopeer -> + case Req#httpd.peer of + Peer when is_atom(Peer) -> ok; Peer -> case lockout_mode() of @@ -74,12 +74,3 @@ lockout_mode() -> lockout_threshold() -> config:get_integer("chttpd_auth_lockout", "threshold", 5). - -peer(#httpd{mochi_req = MochiReq}) -> - Socket = mochiweb_request:get(socket, MochiReq), - case Socket of - {remote, _Pid, _Ref} -> - nopeer; - _ -> - mochiweb_request:get(peer, MochiReq) - end. diff --git a/src/couch/src/couch_httpd.erl b/src/couch/src/couch_httpd.erl index 5253e4d3cd..0a9e39e7ae 100644 --- a/src/couch/src/couch_httpd.erl +++ b/src/couch/src/couch_httpd.erl @@ -46,6 +46,7 @@ -export([handle_request/1]). -export([set_auth_handlers/0]). -export([maybe_decompress/2]). +-export([peer/1]). -define(HANDLER_NAME_IN_MODULE_POS, 6). -define(MAX_DRAIN_BYTES, 1048576). @@ -1424,7 +1425,23 @@ http_respond_(#httpd{mochi_req = MochiReq}, 413, Headers, Args, Type) -> http_respond_(#httpd{mochi_req = MochiReq}, Code, Headers, Args, Type) -> MochiReq:Type({Code, Headers, Args}). +peer(#httpd{} = Req) -> + peer(Req#httpd.mochi_req); peer(MochiReq) -> + case config:get("chttpd", "peer_header") of + undefined -> + peer_from_socket(MochiReq); + PeerHeader -> + Peer = MochiReq:get_header_value(PeerHeader), + case inet:parse_address(Peer) of + {ok, Addr} -> + inet:ntoa(Addr); + _ -> + peer_from_socket(MochiReq) + end + end. + +peer_from_socket(MochiReq) -> case MochiReq:get(socket) of {remote, Pid, _} -> node(Pid); diff --git a/src/couch/src/couch_httpd_auth.erl b/src/couch/src/couch_httpd_auth.erl index 3779be66bc..ba26ef50c3 100644 --- a/src/couch/src/couch_httpd_auth.erl +++ b/src/couch/src/couch_httpd_auth.erl @@ -813,18 +813,16 @@ integer_to_binary(Int, Len) when is_integer(Int), is_integer(Len) -> Padded = <>, binary:part(Padded, byte_size(Padded), -Len). -authentication_warning(#httpd{mochi_req = Req}, User) -> - Peer = Req:get(peer), +authentication_warning(#httpd{} = Req, User) -> couch_log:warning( "~p: Authentication failed for user ~s from ~s", - [?MODULE, User, Peer] + [?MODULE, User, Req#httpd.peer] ). -lockout_warning(#httpd{mochi_req = Req}, User) -> - Peer = Req:get(peer), +lockout_warning(#httpd{} = Req, User) -> couch_log:warning( "~p: Authentication rejected for locked-out user ~s from ~s", - [?MODULE, User, Peer] + [?MODULE, User, Req#httpd.peer] ). -ifdef(TEST). diff --git a/src/docs/src/config/http.rst b/src/docs/src/config/http.rst index 1ea18f2045..8f2114982b 100644 --- a/src/docs/src/config/http.rst +++ b/src/docs/src/config/http.rst @@ -288,6 +288,19 @@ HTTP Server Options [chttpd] disconnect_check_jitter_msec = 15000 + .. config:option:: peer_header :: Header to use for peer + + .. versionadded:: 3.5.3 + + If defined, CouchDB will use the value of this request header to determine peer. + The header value must be a single IPv4 or IPv6 address. If the header is missing, + CouchDB will look at the ``X-Forwarded-For`` header if present, or the socket + peer address if not. + This affects logging as well as the auth lockout feature. :: + + [chttpd] + peer_header = X-Couch-Client-IP + .. config:section:: httpd :: HTTP Server Options .. versionchanged:: 3.2 These options were moved to [chttpd] section: