From 7774506692ada81e8c652df3cf7aeaa8d2cb1d14 Mon Sep 17 00:00:00 2001 From: Robert Dionne Date: Tue, 12 Apr 2011 06:43:07 -0400 Subject: [PATCH 1/7] Add support for descending=true argument to search queries. --- src/chttpd_external.erl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/chttpd_external.erl b/src/chttpd_external.erl index df27a29..9795d2d 100644 --- a/src/chttpd_external.erl +++ b/src/chttpd_external.erl @@ -111,6 +111,8 @@ json_query_keys([{<<"endkey">>, Value} | Rest], Acc) -> json_query_keys(Rest, [{<<"endkey">>, ?JSON_DECODE(Value)}|Acc]); json_query_keys([{<<"key">>, Value} | Rest], Acc) -> json_query_keys(Rest, [{<<"key">>, ?JSON_DECODE(Value)}|Acc]); +json_query_keys([{<<"descending">>, Value} | Rest], Acc) -> + json_query_keys(Rest, [{<<"descending">>, ?JSON_DECODE(Value)}|Acc]); json_query_keys([Term | Rest], Acc) -> json_query_keys(Rest, [Term|Acc]). From 85ebaba7b3e1d485e927a1d7246257a5450cf97a Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Wed, 1 Jun 2011 15:16:03 -0400 Subject: [PATCH 2/7] Improve illegal_database_name error message --- src/chttpd.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chttpd.erl b/src/chttpd.erl index 74349f9..db3f998 100644 --- a/src/chttpd.erl +++ b/src/chttpd.erl @@ -574,9 +574,9 @@ error_info({bad_ctype, Reason}) -> error_info(requested_range_not_satisfiable) -> {416, <<"requested_range_not_satisfiable">>, <<"Requested range not satisfiable">>}; error_info({error, illegal_database_name}) -> - {400, <<"illegal_database_name">>, <<"Only lowercase characters (a-z), " - "digits (0-9), and any of the characters _, $, (, ), +, -, and / " - "are allowed">>}; + {400, <<"illegal_database_name">>, <<"Only lowercase letters (a-z), " + "digits (0-9), and any of the characters _, $, (, ), +, -, and / are " + "allowed. Moreover, the database name must begin with a letter.">>}; error_info({missing_stub, Reason}) -> {412, <<"missing_stub">>, Reason}; error_info(not_implemented) -> From a2323f5abe03ac6d23808852603ff8015972f8b2 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Thu, 10 Mar 2011 11:31:35 -0500 Subject: [PATCH 3/7] Use twig for logging --- src/chttpd.erl | 12 ++++-------- src/chttpd_db.erl | 5 +++-- src/chttpd_rewrite.erl | 6 ++++-- src/chttpd_view.erl | 1 - 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/chttpd.erl b/src/chttpd.erl index db3f998..ce481dc 100644 --- a/src/chttpd.erl +++ b/src/chttpd.erl @@ -134,7 +134,7 @@ handle_request(MochiReq) -> ?LOG_ERROR("attempted upload of invalid JSON ~s", [S]), send_error(HttpReq, {bad_request, "invalid UTF-8 JSON"}); exit:{mochiweb_recv_error, E} -> - ?LOG_INFO(LogForClosedSocket ++ " - ~p", [E]), + twig:log(notice, LogForClosedSocket ++ " - ~p", [E]), exit(normal); throw:Error -> send_error(HttpReq, Error); @@ -142,8 +142,8 @@ handle_request(MochiReq) -> send_error(HttpReq, database_does_not_exist); Tag:Error -> Stack = erlang:get_stacktrace(), - ?LOG_ERROR("Uncaught error in HTTP request: ~p",[{Tag, Error}]), - ?LOG_INFO("Stacktrace: ~p",[Stack]), + twig:log(error, "req_err ~p:~p ~p", [Tag, Error, + json_stack({Error, nil, Stack})]), send_error(HttpReq, {Error, nil, Stack}) end, @@ -155,7 +155,7 @@ handle_request(MochiReq) -> {aborted, Resp:get(code)} end, Host = MochiReq:get_header_value("Host"), - ?LOG_INFO("~s ~s ~s ~s ~B ~p ~B", [Peer, Host, + twig:log(info, "~s ~s ~s ~s ~B ~p ~B", [Peer, Host, atom_to_list(Method1), RawUri, Code, Status, round(RequestTime)]), couch_stats_collector:record({couchdb, request_time}, RequestTime), case Result of @@ -450,10 +450,6 @@ send_chunk(Resp, Data) -> send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) -> couch_stats_collector:increment({httpd_status_codes, Code}), - if Code >= 400 -> - ?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]); - true -> ok - end, {ok, MochiReq:respond({Code, Headers ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers), Body})}. diff --git a/src/chttpd_db.erl b/src/chttpd_db.erl index 686e089..2c660d5 100644 --- a/src/chttpd_db.erl +++ b/src/chttpd_db.erl @@ -228,7 +228,7 @@ db_req(#httpd{method='POST', path_parts=[DbName], user_ctx=Ctx}=Req, Db) -> {ok, _} -> ok; {accepted, _} -> ok; Error -> - ?LOG_INFO("Batch doc error (~s): ~p",[DocId, Error]) + twig:log(debug, "Batch doc error (~s): ~p",[DocId, Error]) end end), @@ -629,7 +629,8 @@ db_doc_req(#httpd{method='PUT', user_ctx=Ctx}=Req, Db, DocId) -> {ok, _} -> ok; {accepted, _} -> ok; Error -> - ?LOG_INFO("Batch doc error (~s): ~p",[DocId, Error]) + twig:log(debug, "Batch doc error (~s): ~p", [DocId, + Error]) end end), send_json(Req, 202, [], {[ diff --git a/src/chttpd_rewrite.erl b/src/chttpd_rewrite.erl index f512ba5..d2ce3bf 100644 --- a/src/chttpd_rewrite.erl +++ b/src/chttpd_rewrite.erl @@ -164,7 +164,8 @@ handle_rewrite_req(#httpd{ % normalize final path (fix levels "." and "..") RawPath1 = ?b2l(iolist_to_binary(normalize_path(RawPath))), - ?LOG_DEBUG("rewrite to ~p ~n", [RawPath1]), + twig:log(debug, "rewrite ~s/~s ~p to ~s", [DbName, DesignName, + PathParts, RawPath1]), % build a new mochiweb request MochiReq1 = mochiweb_request:new(MochiReq:get(socket), @@ -385,7 +386,8 @@ path_to_list([<<"..">>|R], Acc, DotDotCount) when DotDotCount == 2 -> "false" -> path_to_list(R, [<<"..">>|Acc], DotDotCount+1); _Else -> - ?LOG_INFO("insecure_rewrite_rule ~p blocked", [lists:reverse(Acc) ++ [<<"..">>] ++ R]), + twig:log(notice, "insecure_rewrite_rule ~p blocked", + [lists:reverse(Acc) ++ [<<"..">>] ++ R]), throw({insecure_rewrite_rule, "too many ../.. segments"}) end; path_to_list([<<"..">>|R], Acc, DotDotCount) -> diff --git a/src/chttpd_view.erl b/src/chttpd_view.erl index 5041ee8..aa9c23d 100644 --- a/src/chttpd_view.erl +++ b/src/chttpd_view.erl @@ -345,7 +345,6 @@ view_group_etag(Group, Db) -> view_group_etag(Group, Db, nil). view_group_etag(#group{sig=Sig,current_seq=CurrentSeq}, _Db, Extra) -> - % ?LOG_ERROR("Group ~p",[Group]), % This is not as granular as it could be. % If there are updates to the db that do not effect the view index, % they will change the Etag. For more granular Etags we'd need to keep From 16d0b19265033d61dcb8382740f97e7957edee44 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Fri, 4 Mar 2011 08:59:12 -0500 Subject: [PATCH 4/7] Revert "Port workaround for COUCHDB-902" This reverts commit c484b5bb928c07f09e351a1dc0997d7b01182aa0. An upstream patch for the core issue was applied to BigCouch as 0827cb48. BugzID: 11551 BugzID: 11366 --- src/chttpd_db.erl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/chttpd_db.erl b/src/chttpd_db.erl index 2c660d5..3727a7e 100644 --- a/src/chttpd_db.erl +++ b/src/chttpd_db.erl @@ -995,10 +995,8 @@ db_attachment_req(#httpd{method=Method, user_ctx=Ctx}=Req, Db, DocId, FileNamePa end end, - #doc{atts=Atts, revs = {Pos, Revs}} = Doc, + #doc{atts=Atts} = Doc, DocEdited = Doc#doc{ - % prune revision list as a workaround for key tree bug (COUCHDB-902) - revs = {Pos, case Revs of [] -> []; [Hd|_] -> [Hd] end}, atts = NewAtt ++ [A || A <- Atts, A#att.name /= FileName] }, case fabric:update_doc(Db, DocEdited, [{user_ctx,Ctx}]) of From 013f1030096e99bb8edc893bbcb24461ed8fb60d Mon Sep 17 00:00:00 2001 From: "Paul J. Davis" Date: Wed, 14 Sep 2011 13:50:45 -0400 Subject: [PATCH 5/7] Backport timeout error message. This is from part of the commit b316a557 from chttpd-cloudant.com that contians a few unrelated patches some of which end up in the new cloudant_web repo. --- src/chttpd.erl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/chttpd.erl b/src/chttpd.erl index ce481dc..c8c4d74 100644 --- a/src/chttpd.erl +++ b/src/chttpd.erl @@ -579,6 +579,9 @@ error_info(not_implemented) -> {501, <<"not_implemented">>, <<"this feature is not yet implemented">>}; error_info({Error, null}) -> {500, couch_util:to_binary(Error), null}; +error_info(timeout) -> + {500, <<"timeout">>, <<"The request could not be processed in a reasonable" + " amount of time.">>}; error_info({Error, Reason}) -> {500, couch_util:to_binary(Error), couch_util:to_binary(Reason)}; error_info({Error, nil, _Stack}) -> From 73d684d5bac63208079ae68fcfaec0a855107e76 Mon Sep 17 00:00:00 2001 From: "Paul J. Davis" Date: Wed, 14 Sep 2011 13:51:29 -0400 Subject: [PATCH 6/7] Use module local calls for absolute_uri/2. The absolute_uri/2 implementation will be moved and this just prepares for that eventual outcome. --- src/chttpd.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chttpd.erl b/src/chttpd.erl index c8c4d74..45c87ce 100644 --- a/src/chttpd.erl +++ b/src/chttpd.erl @@ -633,7 +633,7 @@ error_headers(#httpd{mochi_req=MochiReq}=Req, 401=Code, ErrorStr, ReasonStr) -> end, UrlReturn = ?l2b(couch_util:url_encode(UrlReturnRaw)), UrlReason = ?l2b(couch_util:url_encode(ReasonStr)), - {302, [{"Location", couch_httpd:absolute_uri(Req, <>). server_header() -> From d6ef80e2517c037add623757058b7a623cb099b3 Mon Sep 17 00:00:00 2001 From: "Paul J. Davis" Date: Wed, 14 Sep 2011 13:47:46 -0400 Subject: [PATCH 7/7] Refactor chttpd for use with cloudant_web. This commit does a bit of reorganization to allow for less duplication of code between chttpd and chttpd-cloudant.com. With this change we can use the new cloudant_web repo to insert just the logic we need to handle multi-tenancy hosting on cloudant.com. --- src/chttpd.erl | 243 +++++++++++++++++++++------------------- src/chttpd_db.erl | 6 +- src/chttpd_external.erl | 6 +- src/chttpd_req.erl | 106 ++++++++++++++++++ 4 files changed, 242 insertions(+), 119 deletions(-) create mode 100644 src/chttpd_req.erl diff --git a/src/chttpd.erl b/src/chttpd.erl index 45c87ce..1fc8a23 100644 --- a/src/chttpd.erl +++ b/src/chttpd.erl @@ -13,7 +13,7 @@ -module(chttpd). -include_lib("couch/include/couch_db.hrl"). --export([start_link/0, stop/0, handle_request/1, config_change/2, +-export([start_link/0, stop/0, do_request/1, handle_request/4, config_change/2, primary_header_value/2, header_value/2, header_value/3, qs_value/2, qs_value/3, qs/1, path/1, absolute_uri/2, body_length/1, verify_is_server_admin/1, unquote/1, quote/1, recv/2, recv_chunked/4, @@ -22,8 +22,9 @@ server_header/0, start_chunked_response/3,send_chunk/2, start_response_length/4, send/2, start_json_response/2, start_json_response/3, end_json_response/1, send_response/4, - send_method_not_allowed/2, send_error/2, send_error/4, send_redirect/2, - send_chunked_error/2, send_json/2,send_json/3,send_json/4]). + send_method_not_allowed/2, send_error/2, send_error/4, send_error/5, + send_redirect/2, send_chunked_error/2, + send_json/2, send_json/3, send_json/4]). -export([start_delayed_json_response/2, start_delayed_json_response/3, start_delayed_json_response/4, @@ -32,6 +33,10 @@ send_delayed_error/2, end_delayed_json_response/1, get_delayed_req/1]). +-export([dispatch_request/2, log_request/7, admin_roles/0, + db_info/2, get_path/1, make_uri/2, build_uri/4, default_headers/1, + url_handler/1, db_url_handlers/0, design_url_handlers/0]). + -record(delayed_resp, { start_fun, req, @@ -42,7 +47,7 @@ start_link() -> Options = [ - {loop, fun ?MODULE:handle_request/1}, + {loop, make_loop_fun()}, {name, ?MODULE}, {ip, couch_config:get("chttpd", "bind_address", any)}, {port, couch_config:get("chttpd", "port", "5984")}, @@ -67,10 +72,8 @@ config_change("chttpd", "backlog") -> stop() -> mochiweb_http:stop(?MODULE). -handle_request(MochiReq) -> - Begin = now(), - - AuthenticationFuns = [ +do_request(MochiReq) -> + AuthFuns = [ fun couch_httpd_auth:cookie_authentication_handler/1, fun couch_httpd_auth:default_authentication_handler/1 ], @@ -79,8 +82,12 @@ handle_request(MochiReq) -> % removed, but URL quoting left intact RawUri = MochiReq:get(raw_path), {"/" ++ Path, _, _} = mochiweb_util:urlsplit_path(RawUri), - {HandlerKey, _, _} = mochiweb_util:partition(Path, "/"), + handle_request(?MODULE, MochiReq, AuthFuns, Path). + +handle_request(Module, MochiReq, AuthFuns, Path) -> + Begin = now(), + RawUri = MochiReq:get(raw_path), Peer = MochiReq:get(peer), LogForClosedSocket = io_lib:format("mochiweb_recv_error for ~s - ~p ~s", [ Peer, @@ -104,24 +111,25 @@ handle_request(MochiReq) -> Other -> Other end, - HttpReq = #httpd{ + BaseReq = #httpd{ mochi_req = MochiReq, method = Method, path_parts = [list_to_binary(chttpd:unquote(Part)) || Part <- string:tokens(Path, "/")], - db_url_handlers = db_url_handlers(), - design_url_handlers = design_url_handlers() + db_url_handlers = Module:db_url_handlers(), + design_url_handlers = Module:design_url_handlers() }, + ChttpdReq = chttpd_req:new(Module, BaseReq, MochiReq), + HttpReq = BaseReq#httpd{mochi_req=ChttpdReq}, % put small token on heap to keep requests synced to backend calls erlang:put(nonce, couch_util:to_hex(crypto:rand_bytes(4))), Result = try - case authenticate_request(HttpReq, AuthenticationFuns) of + case authenticate_request(Module, HttpReq, AuthFuns) of #httpd{} = Req -> - HandlerFun = url_handler(HandlerKey), - HandlerFun(possibly_hack(Req)); + Module:dispatch_request(possibly_hack(Module, Req), Path); Response -> Response end @@ -155,8 +163,10 @@ handle_request(MochiReq) -> {aborted, Resp:get(code)} end, Host = MochiReq:get_header_value("Host"), - twig:log(info, "~s ~s ~s ~s ~B ~p ~B", [Peer, Host, - atom_to_list(Method1), RawUri, Code, Status, round(RequestTime)]), + Module:log_request( + Peer, Host, atom_to_list(Method1), RawUri, + Code, Status, round(RequestTime) + ), couch_stats_collector:record({couchdb, request_time}, RequestTime), case Result of {ok, _} -> @@ -173,21 +183,21 @@ handle_request(MochiReq) -> %% 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) -> +possibly_hack(Module, #httpd{path_parts=[<<"_replicate">>]}=Req) -> {Props0} = couch_httpd:json_body_obj(Req), - Props1 = fix_uri(Req, Props0, <<"source">>), - Props2 = fix_uri(Req, Props1, <<"target">>), + Props1 = fix_uri(Module, Req, Props0, <<"source">>), + Props2 = fix_uri(Module, Req, Props1, <<"target">>), put(post_body, {Props2}), Req; -possibly_hack(Req) -> +possibly_hack(_Module, Req) -> Req. -fix_uri(Req, Props, Type) -> +fix_uri(Module, Req, Props, Type) -> case is_http(replication_uri(Type, Props)) of true -> Props; false -> - Uri = make_uri(Req,replication_uri(Type, Props)), + Uri = Module:make_uri(Req,replication_uri(Type, Props)), [{Type,Uri}|proplists:delete(Type,Props)] end. @@ -205,81 +215,43 @@ is_http(<<"https://", _/binary>>) -> true; is_http(_) -> false. - -make_uri(Req, Raw) -> - Url = list_to_binary(["http://", couch_config:get("httpd", "bind_address"), - ":", couch_config:get("chttpd", "port"), "/", Raw]), - Headers = [ - {<<"authorization">>, ?l2b(header_value(Req,"authorization",""))}, - {<<"cookie">>, ?l2b(header_value(Req,"cookie",""))} - ], - {[{<<"url">>,Url}, {<<"headers">>,{Headers}}]}. %%% end hack % Try authentication handlers in order until one returns a result -authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthFuns) -> +authenticate_request(_Module, #httpd{user_ctx=#user_ctx{}} = Req, _AuthFuns) -> Req; -authenticate_request(#httpd{} = Req, [AuthFun|Rest]) -> - authenticate_request(AuthFun(Req), Rest); -authenticate_request(#httpd{} = Req, []) -> +authenticate_request(Module, #httpd{} = Req, [AuthFun|Rest]) -> + authenticate_request(Module, AuthFun(Req), Rest); +authenticate_request(Module, #httpd{} = Req, []) -> case couch_config:get("chttpd", "require_valid_user", "false") of "true" -> throw({unauthorized, <<"Authentication required.">>}); "false" -> case couch_config:get("admins") of [] -> - Ctx = #user_ctx{roles=[<<"_reader">>, <<"_writer">>, <<"_admin">>]}, + Ctx = #user_ctx{roles=Module:admin_roles()}, Req#httpd{user_ctx = Ctx}; _ -> Req#httpd{user_ctx=#user_ctx{}} end end; -authenticate_request(Response, _AuthFuns) -> +authenticate_request(_Module, Response, _AuthFuns) -> Response. increment_method_stats(Method) -> couch_stats_collector:increment({httpd_request_methods, Method}). -url_handler("") -> fun chttpd_misc:handle_welcome_req/1; -url_handler("favicon.ico") -> fun chttpd_misc:handle_favicon_req/1; -url_handler("_utils") -> fun chttpd_misc:handle_utils_dir_req/1; -url_handler("_all_dbs") -> fun chttpd_misc:handle_all_dbs_req/1; -url_handler("_active_tasks") -> fun chttpd_misc:handle_task_status_req/1; -url_handler("_config") -> fun chttpd_misc:handle_config_req/1; -url_handler("_replicate") -> fun chttpd_misc:handle_replicate_req/1; -url_handler("_uuids") -> fun chttpd_misc:handle_uuids_req/1; -url_handler("_log") -> fun chttpd_misc:handle_log_req/1; -url_handler("_sleep") -> fun chttpd_misc:handle_sleep_req/1; -url_handler("_session") -> fun couch_httpd_auth:handle_session_req/1; -url_handler("_oauth") -> fun couch_httpd_oauth:handle_oauth_req/1; -%% showroom_http module missing in bigcouch -url_handler("_restart") -> fun showroom_http:handle_restart_req/1; -url_handler("_membership") -> fun mem3_httpd:handle_membership_req/1; -url_handler(_) -> fun chttpd_db:handle_request/1. - -db_url_handlers() -> - [ - {<<"_view_cleanup">>, fun chttpd_db:handle_view_cleanup_req/2}, - {<<"_compact">>, fun chttpd_db:handle_compact_req/2}, - {<<"_design">>, fun chttpd_db:handle_design_req/2}, - {<<"_temp_view">>, fun chttpd_view:handle_temp_view_req/2}, - {<<"_changes">>, fun chttpd_db:handle_changes_req/2}, - {<<"_search">>, fun chttpd_external:handle_search_req/2} - ]. - -design_url_handlers() -> - [ - {<<"_view">>, fun chttpd_view:handle_view_req/3}, - {<<"_show">>, fun chttpd_show:handle_doc_show_req/3}, - {<<"_list">>, fun chttpd_show:handle_view_list_req/3}, - {<<"_update">>, fun chttpd_show:handle_doc_update_req/3}, - {<<"_info">>, fun chttpd_db:handle_design_info_req/3}, - {<<"_rewrite">>, fun chttpd_rewrite:handle_rewrite_req/3} - ]. - % Utilities +make_loop_fun() -> + DefLoopFunName = "{chttpd, do_request}", + LoopFunName = couch_config:get("chttpd", "loop_fun", DefLoopFunName), + case (catch couch_util:parse_term(LoopFunName)) of + {ok, {Mod, Fun}} -> fun(Req) -> Mod:Fun(Req) end; + _ -> throw({error, invalid_loop_fun}) + end. + partition(Path) -> mochiweb_util:partition(Path, "/"). @@ -295,9 +267,8 @@ header_value(#httpd{mochi_req=MochiReq}, Key, Default) -> primary_header_value(#httpd{mochi_req=MochiReq}, Key) -> MochiReq:get_primary_header_value(Key). -serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot) -> - {ok, MochiReq:serve_file(RelativePath, DocumentRoot, - server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []))}. +serve_file(#httpd{mochi_req=MochiReq}, RelativePath, DocumentRoot) -> + {ok, MochiReq:serve_file(RelativePath, DocumentRoot, server_header())}. qs_value(Req, Key) -> qs_value(Req, Key, undefined). @@ -312,31 +283,7 @@ path(#httpd{mochi_req=MochiReq}) -> MochiReq:get(path). absolute_uri(#httpd{mochi_req=MochiReq}, Path) -> - XHost = couch_config:get("httpd", "x_forwarded_host", "X-Forwarded-Host"), - Host = case MochiReq:get_header_value(XHost) of - undefined -> - case MochiReq:get_header_value("Host") of - undefined -> - {ok, {Address, Port}} = inet:sockname(MochiReq:get(socket)), - inet_parse:ntoa(Address) ++ ":" ++ integer_to_list(Port); - Value1 -> - Value1 - end; - Value -> Value - end, - XSsl = couch_config:get("httpd", "x_forwarded_ssl", "X-Forwarded-Ssl"), - Scheme = case MochiReq:get_header_value(XSsl) of - "on" -> "https"; - _ -> - XProto = couch_config:get("httpd", "x_forwarded_proto", - "X-Forwarded-Proto"), - case MochiReq:get_header_value(XProto) of - % Restrict to "https" and "http" schemes only - "https" -> "https"; - _ -> "http" - end - end, - Scheme ++ "://" ++ Host ++ Path. + MochiReq:absolute_uri(Path). unquote(UrlEncodedString) -> mochiweb_util:unquote(UrlEncodedString). @@ -420,10 +367,10 @@ verify_is_server_admin(#httpd{user_ctx=#user_ctx{roles=Roles}}) -> false -> throw({unauthorized, <<"You are not a server admin.">>}) end. -start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Length) -> +start_response_length(#httpd{mochi_req=MochiReq}, Code, Headers, Length) -> couch_stats_collector:increment({httpd_status_codes, Code}), - Resp = MochiReq:start_response_length({Code, Headers ++ server_header() ++ - couch_httpd_auth:cookie_auth_header(Req, Headers), Length}), + Resp = MochiReq:start_response_length({Code, Headers ++ server_header(), + Length}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); _ -> ok @@ -434,10 +381,9 @@ send(Resp, Data) -> Resp:send(Data), {ok, Resp}. -start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) -> +start_chunked_response(#httpd{mochi_req=MochiReq}, Code, Headers) -> couch_stats_collector:increment({httpd_status_codes, Code}), - Resp = MochiReq:respond({Code, Headers ++ server_header() ++ - couch_httpd_auth:cookie_auth_header(Req, Headers), chunked}), + Resp = MochiReq:respond({Code, Headers ++ server_header(), chunked}), case MochiReq:get(method) of 'HEAD' -> throw({http_head_abort, Resp}); _ -> ok @@ -448,14 +394,13 @@ send_chunk(Resp, Data) -> Resp:write_chunk(Data), {ok, Resp}. -send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) -> +send_response(#httpd{mochi_req=MochiReq}, Code, Headers, Body) -> couch_stats_collector:increment({httpd_status_codes, Code}), - {ok, MochiReq:respond({Code, Headers ++ server_header() ++ - couch_httpd_auth:cookie_auth_header(Req, Headers), Body})}. + {ok, MochiReq:respond({Code, Headers ++ server_header(), Body})}. send_method_not_allowed(Req, Methods) -> send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>, - ?l2b("Only " ++ Methods ++ " allowed")). + ?l2b("Only " ++ Methods ++ " allowed"), []). send_json(Req, Value) -> send_json(Req, 200, Value). @@ -704,3 +649,75 @@ json_stack({_Error, _Reason, Stack}) -> end, Stack); json_stack(_) -> []. + +% chttpd request callbacks + +dispatch_request(Req, Path) -> + Handler = url_handler(Path), + Handler(Req). + +log_request(Peer, Host, Method, RawUri, Code, Status, RequestTime) -> + Args = [Peer, Host, Method, RawUri, Code, Status, RequestTime], + twig:log(info, "~s ~s ~s ~s ~B ~p ~B", Args). + +make_uri(Req, Raw) -> + Url = list_to_binary(["http://", couch_config:get("httpd", "bind_address"), + ":", couch_config:get("chttpd", "port"), "/", Raw]), + Headers = [ + {<<"authorization">>, ?l2b(header_value(Req,"authorization",""))}, + {<<"cookie">>, ?l2b(header_value(Req,"cookie",""))} + ], + {[{<<"url">>,Url}, {<<"headers">>,{Headers}}]}. + +admin_roles() -> + [<<"_reader">>, <<"_writer">>, <<"_admin">>]. + +db_info(_Req, Info) -> + Info. + +default_headers(Req) -> + couch_httpd_auth:cookie_auth_header(Req, []). + +get_path(#httpd{path_parts=Path}) -> + Path. + +build_uri(_HttpReq, Scheme, Host, Path) -> + Scheme ++ "://" ++ Host ++ Path. + +url_handler("") -> fun chttpd_misc:handle_welcome_req/1; +url_handler("favicon.ico") -> fun chttpd_misc:handle_favicon_req/1; +url_handler("_utils") -> fun chttpd_misc:handle_utils_dir_req/1; +url_handler("_all_dbs") -> fun chttpd_misc:handle_all_dbs_req/1; +url_handler("_active_tasks") -> fun chttpd_misc:handle_task_status_req/1; +url_handler("_config") -> fun chttpd_misc:handle_config_req/1; +url_handler("_replicate") -> fun chttpd_misc:handle_replicate_req/1; +url_handler("_uuids") -> fun chttpd_misc:handle_uuids_req/1; +url_handler("_log") -> fun chttpd_misc:handle_log_req/1; +url_handler("_sleep") -> fun chttpd_misc:handle_sleep_req/1; +url_handler("_session") -> fun couch_httpd_auth:handle_session_req/1; +url_handler("_oauth") -> fun couch_httpd_oauth:handle_oauth_req/1; +%% showroom_http module missing in bigcouch +url_handler("_restart") -> fun showroom_http:handle_restart_req/1; +url_handler("_system") -> fun chttpd_misc:handle_system_req/1; +url_handler("_membership") -> fun mem3_httpd:handle_membership_req/1; +url_handler(_) -> fun chttpd_db:handle_request/1. + +db_url_handlers() -> + [ + {<<"_view_cleanup">>, fun chttpd_db:handle_view_cleanup_req/2}, + {<<"_compact">>, fun chttpd_db:handle_compact_req/2}, + {<<"_design">>, fun chttpd_db:handle_design_req/2}, + {<<"_temp_view">>, fun chttpd_view:handle_temp_view_req/2}, + {<<"_changes">>, fun chttpd_db:handle_changes_req/2}, + {<<"_search">>, fun chttpd_external:handle_search_req/2} + ]. + +design_url_handlers() -> + [ + {<<"_view">>, fun chttpd_view:handle_view_req/3}, + {<<"_show">>, fun chttpd_show:handle_doc_show_req/3}, + {<<"_list">>, fun chttpd_show:handle_view_list_req/3}, + {<<"_update">>, fun chttpd_show:handle_doc_update_req/3}, + {<<"_info">>, fun chttpd_db:handle_design_info_req/3}, + {<<"_rewrite">>, fun chttpd_rewrite:handle_rewrite_req/3} + ]. diff --git a/src/chttpd_db.erl b/src/chttpd_db.erl index 3727a7e..b19d3b9 100644 --- a/src/chttpd_db.erl +++ b/src/chttpd_db.erl @@ -14,7 +14,7 @@ -include_lib("couch/include/couch_db.hrl"). -export([handle_request/1, handle_compact_req/2, handle_design_req/2, - db_req/2, couch_doc_open/4,handle_changes_req/2, + db_req/2, delete_db_req/2, couch_doc_open/4,handle_changes_req/2, update_doc_result_to_json/1, update_doc_result_to_json/2, handle_design_info_req/3, handle_view_cleanup_req/2]). @@ -198,13 +198,13 @@ do_db_req(#httpd{path_parts=[DbName|_], user_ctx=Ctx}=Req, Fun) -> fabric:get_security(DbName, [{user_ctx,Ctx}]), % calls check_is_reader Fun(Req, #db{name=DbName, user_ctx=Ctx}). -db_req(#httpd{method='GET',path_parts=[DbName]}=Req, _Db) -> +db_req(#httpd{mochi_req=MochiReq,method='GET',path_parts=[DbName]}=Req, _Db) -> % measure the time required to generate the etag, see if it's worth it T0 = now(), {ok, DbInfo} = fabric:get_db_info(DbName), DeltaT = timer:now_diff(now(), T0) / 1000, couch_stats_collector:record({couchdb, dbinfo}, DeltaT), - send_json(Req, {DbInfo}); + send_json(Req, {MochiReq:db_info(DbInfo)}); db_req(#httpd{method='POST', path_parts=[DbName], user_ctx=Ctx}=Req, Db) -> couch_httpd:validate_ctype(Req, "application/json"), diff --git a/src/chttpd_external.erl b/src/chttpd_external.erl index 9795d2d..4a0dbdb 100644 --- a/src/chttpd_external.erl +++ b/src/chttpd_external.erl @@ -61,7 +61,6 @@ process_external_req(HttpReq, Db, Name) -> json_req_obj(Req, Db) -> json_req_obj(Req, Db, null). json_req_obj(#httpd{mochi_req=Req, method=Method, - path_parts=Path, req_body=ReqBody }, Db, DocId) -> Body = case ReqBody of @@ -78,8 +77,9 @@ json_req_obj(#httpd{mochi_req=Req, Hlist = mochiweb_headers:to_list(Headers), {ok, Info} = fabric:get_db_info(Db), + Path = Req:path(), % add headers... - {[{<<"info">>, {Info}}, + {[{<<"info">>, {Req:db_info(Info)}}, {<<"uuid">>, couch_uuids:new()}, {<<"id">>, DocId}, {<<"method">>, Method}, @@ -90,7 +90,7 @@ json_req_obj(#httpd{mochi_req=Req, {<<"peer">>, ?l2b(Req:get(peer))}, {<<"form">>, to_json_terms(ParsedForm)}, {<<"cookie">>, to_json_terms(Req:parse_cookie())}, - {<<"userCtx">>, couch_util:json_user_ctx(Db)}]}. + {<<"userCtx">>, couch_util:json_user_ctx(Db#db{name=hd(Path)})}]}. to_json_terms(Data) -> to_json_terms(Data, []). diff --git a/src/chttpd_req.erl b/src/chttpd_req.erl new file mode 100644 index 0000000..88b0a0f --- /dev/null +++ b/src/chttpd_req.erl @@ -0,0 +1,106 @@ +-module(chttpd_req, [Module, HttpReq, MochiReq]). + +-export([path/0, absolute_uri/1]). +-export([get_header_value/1, get_primary_header_value/1, get/1, dump/0]). +-export([send/1, recv/1, recv/2, recv_body/0, recv_body/1, stream_body/3]). +-export([start_response/1, start_response_length/1, start_raw_response/1]). +-export([respond/1, ok/1]). +-export([not_found/0, not_found/1]). +-export([parse_post/0, parse_qs/0]). +-export([should_close/0, cleanup/0]). +-export([parse_cookie/0, get_cookie_value/1]). +-export([serve_file/2, serve_file/3]). +-export([accepted_encodings/1]). +-export([accepts_content_type/1]). + +path() -> + Module:get_path(HttpReq). + +absolute_uri(Path) -> + XHost = couch_config:get("httpd", "x_forwarded_host", "X-Forwarded-Host"), + Host = case MochiReq:get_header_value(XHost) of + undefined -> + case MochiReq:get_header_value("Host") of + undefined -> + {ok, {Address, Port}} = inet:sockname(MochiReq:get(socket)), + inet_parse:ntoa(Address) ++ ":" ++ integer_to_list(Port); + Value1 -> + Value1 + end; + Value -> Value + end, + XSsl = couch_config:get("httpd", "x_forwarded_ssl", "X-Forwarded-Ssl"), + Scheme = case MochiReq:get_header_value(XSsl) of + "on" -> "https"; + _ -> + XProto = couch_config:get("httpd", "x_forwarded_proto", + "X-Forwarded-Proto"), + case MochiReq:get_header_value(XProto) of + % Restrict to "https" and "http" schemes only + "https" -> "https"; + _ -> "http" + end + end, + Module:build_uri(HttpReq, Scheme, Host, Path). + +get_header_value(K) -> MochiReq:get_header_value(K). +get_primary_header_value(K) -> MochiReq:get_primary_header_value(K). +get(K) -> MochiReq:get(K). +dump() -> MochiReq:dump(). + +send(Data) -> MochiReq:send(Data). +recv(Length) -> MochiReq:recv(Length). +recv(Length, Timeout) -> MochiReq:recv(Length, Timeout). +recv_body() -> MochiReq:recv_body(). +recv_body(MaxBody) -> MochiReq:recv_body(MaxBody). +stream_body(Size, Fun, State) -> MochiReq:stream_body(Size, Fun, State). + +start_response({Code, RespHdrs}) -> + MochiReq:start_response({Code, set_headers(RespHdrs)}). + +start_response_length({Code, RespHdrs, Length}) -> + MochiReq:start_response_length({Code, set_headers(RespHdrs), Length}). + +start_raw_response({Code, RespHdrs}) -> + MochiReq:start_raw_length({Code, set_headers(RespHdrs)}). + +respond({Code, RespHdrs, Body}) -> + MochiReq:respond({Code, set_headers(RespHdrs), Body}). + +ok({CType, RespHdrs, Body}) -> + MochiReq:ok({CType, set_headers(RespHdrs), Body}); +ok(Info) -> + MochiReq:ok(Info). + +not_found() -> MochiReq:not_found(). +not_found(Headers) -> MochiReq:not_found(Headers). + +should_close() -> MochiReq:should_close(). +cleanup() -> MochiReq:cleanup(). + +parse_qs() -> MochiReq:parse_qs(). +parse_post() -> MochiReq:parse_post(). + +parse_cookie() -> MochiReq:parse_cookie(). +get_cookie_value(Key) -> MochiReq:get_cookie_value(Key). + +serve_file(Path, Root) -> MochiReq:serve_file(Path, Root). +serve_file(Path, Root, Headers) -> MochiReq:serve_file(Path, Root, Headers). + +accepted_encodings(Supported) -> MochiReq:accepted_encodings(Supported). +accepts_content_type(CType) -> MochiReq:accepts_content_type(CType). + +set_headers(RespHdrs) -> + DefaultHeaders = Module:default_headers(HttpReq), + set_default_headers(DefaultHeaders, RespHdrs). + +set_default_headers([], Headers) -> + Headers; +set_default_headers([{Key, Val} | Rest], Headers) -> + NewHeaders = + case couch_util:get_value(Key, Headers) of + undefined -> Headers ++ [{Key, Val}]; + _ -> Headers + end, + set_default_headers(Rest, NewHeaders). +