Author: jan Date: Fri Feb 19 19:11:53 2010 New Revision: 911942 URL: http://svn.apache.org/viewvc?rev=911942&view=rev Log: backport vhosts Added: couchdb/branches/0.11.x/test/etap/160-vhosts.t (with props) Modified: couchdb/branches/0.11.x/etc/couchdb/local.ini couchdb/branches/0.11.x/src/couchdb/couch_httpd.erl couchdb/branches/0.11.x/src/couchdb/couch_httpd_misc_handlers.erl couchdb/branches/0.11.x/src/couchdb/couch_httpd_rewrite.erl couchdb/branches/0.11.x/test/etap/110-replication-httpc.t couchdb/branches/0.11.x/test/etap/111-replication-changes-feed.t couchdb/branches/0.11.x/test/etap/112-replication-missing-revs.t Modified: couchdb/branches/0.11.x/etc/couchdb/local.ini URL: http://svn.apache.org/viewvc/couchdb/branches/0.11.x/etc/couchdb/local.ini?rev=911942&r1=911941&r2=911942&view=diff ============================================================================== --- couchdb/branches/0.11.x/etc/couchdb/local.ini (original) +++ couchdb/branches/0.11.x/etc/couchdb/local.ini Fri Feb 19 19:11:53 2010 @@ -16,6 +16,16 @@ [log] ;level = debug + +; To enable Virtual Hosts in CouchDB, add a vhost = path directive. All requests to +; the Virual Host will be redirected to the path. In the example below all requests +; to http://example.com/ are redirected to /database. +; If you run CouchDB on a specific port, include the port number in the vhost: +; example.com:5984 = /database + +[vhosts] +;example.com = /database/ + [update_notification] ;unique notifier name=/full/path/to/exe -with "cmd line arg" Modified: couchdb/branches/0.11.x/src/couchdb/couch_httpd.erl URL: http://svn.apache.org/viewvc/couchdb/branches/0.11.x/src/couchdb/couch_httpd.erl?rev=911942&r1=911941&r2=911942&view=diff ============================================================================== --- couchdb/branches/0.11.x/src/couchdb/couch_httpd.erl (original) +++ couchdb/branches/0.11.x/src/couchdb/couch_httpd.erl Fri Feb 19 19:11:53 2010 @@ -13,7 +13,7 @@ -module(couch_httpd). -include("couch_db.hrl"). --export([start_link/0, stop/0, handle_request/5]). +-export([start_link/0, stop/0, handle_request/6]). -export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,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]). @@ -25,7 +25,7 @@ -export([start_json_response/2, start_json_response/3, end_json_response/1]). -export([send_response/4,send_method_not_allowed/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]). +-export([accepted_encodings/1,handle_request_int/5]). start_link() -> % read config and register for configuration changes @@ -35,6 +35,7 @@ BindAddress = couch_config:get("httpd", "bind_address", any), Port = couch_config:get("httpd", "port", "5984"), + VirtualHosts = couch_config:get("vhosts"), DefaultSpec = "{couch_httpd_db, handle_request}", DefaultFun = make_arity_1_fun( @@ -61,7 +62,8 @@ DesignUrlHandlers = dict:from_list(DesignUrlHandlersList), Loop = fun(Req)-> apply(?MODULE, handle_request, [ - Req, DefaultFun, UrlHandlers, DbUrlHandlers, DesignUrlHandlers + Req, DefaultFun, UrlHandlers, DbUrlHandlers, DesignUrlHandlers, + VirtualHosts ]) end, @@ -89,6 +91,8 @@ ("httpd_global_handlers", _) -> ?MODULE:stop(); ("httpd_db_handlers", _) -> + ?MODULE:stop(); + ("vhosts", _) -> ?MODULE:stop() end, Pid), @@ -127,9 +131,46 @@ stop() -> mochiweb_http:stop(?MODULE). +%% + +% if there's a vhost definition that matches the request, redirect internally +redirect_to_vhost(MochiReq, DefaultFun, + UrlHandlers, DbUrlHandlers, DesignUrlHandlers, VhostTarget) -> + + Path = MochiReq:get(path), + Target = VhostTarget ++ Path, + ?LOG_DEBUG("Vhost Target: '~p'~n", [Target]), + % build a new mochiweb request + MochiReq1 = mochiweb_request:new(MochiReq:get(socket), + MochiReq:get(method), + Target, + MochiReq:get(version), + MochiReq:get(headers)), + % cleanup, It force mochiweb to reparse raw uri. + MochiReq1:cleanup(), + + handle_request_int(MochiReq1, DefaultFun, + UrlHandlers, DbUrlHandlers, DesignUrlHandlers). handle_request(MochiReq, DefaultFun, - UrlHandlers, DbUrlHandlers, DesignUrlHandlers) -> + UrlHandlers, DbUrlHandlers, DesignUrlHandlers, VirtualHosts) -> + + % grab Host from Req + Vhost = MochiReq:get_header_value("Host"), + + % find Vhost in config + case proplists:get_value(Vhost, VirtualHosts) of + undefined -> % business as usual + handle_request_int(MochiReq, DefaultFun, + UrlHandlers, DbUrlHandlers, DesignUrlHandlers); + VhostTarget -> + redirect_to_vhost(MochiReq, DefaultFun, + UrlHandlers, DbUrlHandlers, DesignUrlHandlers, VhostTarget) + end. + + +handle_request_int(MochiReq, DefaultFun, + UrlHandlers, DbUrlHandlers, DesignUrlHandlers) -> Begin = now(), AuthenticationSrcs = make_fun_spec_strs( couch_config:get("httpd", "authentication_handlers")), Modified: couchdb/branches/0.11.x/src/couchdb/couch_httpd_misc_handlers.erl URL: http://svn.apache.org/viewvc/couchdb/branches/0.11.x/src/couchdb/couch_httpd_misc_handlers.erl?rev=911942&r1=911941&r2=911942&view=diff ============================================================================== --- couchdb/branches/0.11.x/src/couchdb/couch_httpd_misc_handlers.erl (original) +++ couchdb/branches/0.11.x/src/couchdb/couch_httpd_misc_handlers.erl Fri Feb 19 19:11:53 2010 @@ -46,6 +46,7 @@ {"Expires", httpd_util:rfc1123_date(OneYearFromNow)} ], couch_httpd:serve_file(Req, "favicon.ico", DocumentRoot, CachingHeaders); + handle_favicon_req(Req, _) -> send_method_not_allowed(Req, "GET,HEAD"). Modified: couchdb/branches/0.11.x/src/couchdb/couch_httpd_rewrite.erl URL: http://svn.apache.org/viewvc/couchdb/branches/0.11.x/src/couchdb/couch_httpd_rewrite.erl?rev=911942&r1=911941&r2=911942&view=diff ============================================================================== --- couchdb/branches/0.11.x/src/couchdb/couch_httpd_rewrite.erl (original) +++ couchdb/branches/0.11.x/src/couchdb/couch_httpd_rewrite.erl Fri Feb 19 19:11:53 2010 @@ -23,25 +23,25 @@ -define(MATCH_ALL, '*'). -%% doc The http rewrite handler. All rewriting is done from +%% doc The http rewrite handler. All rewriting is done from %% /dbname/_design/ddocname/_rewrite by default. %% %% each rules should be in rewrites member of the design doc. %% Ex of a complete rule : %% -%% { -%% .... -%% "rewrites": [ -%% { -%% "from": "", -%% "to": "index.html", -%% "method": "GET", -%% "query": {} -%% } -%% ] -%% } +%% { +%% .... +%% "rewrites": [ +%% { +%% "from": "", +%% "to": "index.html", +%% "method": "GET", +%% "query": {} +%% } +%% ] +%% } %% -%% from: is the path rule used to bind current uri to the rule. It +%% from: is the path rule used to bind current uri to the rule. It %% use pattern matching for that. %% %% to: rule to rewrite an url. It can contain variables depending on binding @@ -51,27 +51,27 @@ %% method: method to bind the request method to the rule. by default "*" %% query: query args you want to define they can contain dynamic variable %% by binding the key to the bindings -%% +%% %% %% to and from are path with patterns. pattern can be string starting with ":" or %% "*". ex: %% /somepath/:var/* %% -%% This path is converted in erlang list by splitting "/". Each var are +%% This path is converted in erlang list by splitting "/". Each var are %% converted in atom. "*" is converted to '*' atom. The pattern matching is done -%% by splitting "/" in request url in a list of token. A string pattern will -%% match equal token. The star atom ('*' in single quotes) will match any number -%% of tokens, but may only be present as the last pathtern in a pathspec. If all +%% by splitting "/" in request url in a list of token. A string pattern will +%% match equal token. The star atom ('*' in single quotes) will match any number +%% of tokens, but may only be present as the last pathtern in a pathspec. If all %% tokens are matched and all pathterms are used, then the pathspec matches. It works %% like webmachine. Each identified token will be reused in to rule and in query %% -%% The pattern matching is done by first matching the request method to a rule. by +%% The pattern matching is done by first matching the request method to a rule. by %% default all methods match a rule. (method is equal to "*" by default). Then -%% It will try to match the path to one rule. If no rule match, then a 404 error +%% It will try to match the path to one rule. If no rule match, then a 404 error %% is displayed. -%% +%% %% Once a rule is found we rewrite the request url using the "to" and -%% "query" members. The identified token are matched to the rule and +%% "query" members. The identified token are matched to the rule and %% will replace var. if '*' is found in the rule it will contain the remaining %% part if it exists. %% @@ -83,15 +83,15 @@ %% "to": "/some/"} k = v %% %% {"from": "/a/b", /a/b /some/b?var=b var =:= b -%% "to": "/some/:var"} +%% "to": "/some/:var"} %% -%% {"from": "/a", /a /some +%% {"from": "/a", /a /some %% "to": "/some/*"} %% -%% {"from": "/a/*", /a/b/c /some/b/c +%% {"from": "/a/*", /a/b/c /some/b/c %% "to": "/some/*"} %% -%% {"from": "/a", /a /some +%% {"from": "/a", /a /some %% "to": "/some/*"} %% %% {"from": "/a/:foo/*", /a/b/c /some/b/c?foo=b foo =:= b @@ -99,7 +99,7 @@ %% %% {"from": "/a/:foo", /a/b /some/?k=b&foo=b foo =:= b %% "to": "/some", -%% "query": { +%% "query": { %% "k": ":foo" %% }} %% @@ -113,32 +113,32 @@ path_parts=[DbName, <<"_design">>, DesignName, _Rewrite|PathParts], method=Method, mochi_req=MochiReq}=Req, _Db, DDoc) -> - + % we are in a design handler DesignId = <<"_design/", DesignName/binary>>, Prefix = <<"/", DbName/binary, "/", DesignId/binary>>, QueryList = couch_httpd:qs(Req), - + #doc{body={Props}} = DDoc, - + % get rules from ddoc case proplists:get_value(<<"rewrites">>, Props) of undefined -> - couch_httpd:send_error(Req, 404, <<"rewrite_error">>, + couch_httpd:send_error(Req, 404, <<"rewrite_error">>, <<"Invalid path.">>); Rules -> % create dispatch list from rules DispatchList = [make_rule(Rule) || {Rule} <- Rules], - + %% get raw path by matching url to a rule. - RawPath = case try_bind_path(DispatchList, Method, PathParts, + RawPath = case try_bind_path(DispatchList, Method, PathParts, QueryList) of no_dispatch_path -> throw(not_found); - {NewPathParts, Bindings} -> + {NewPathParts, Bindings} -> Parts = [mochiweb_util:quote_plus(X) || X <- NewPathParts], - - % build new path, reencode query args, eventually convert + + % build new path, reencode query args, eventually convert % them to json Path = lists:append( string:join(Parts, [?SEPARATOR]), @@ -146,20 +146,20 @@ [] -> []; _ -> [$?, encode_query(Bindings)] end), - + % if path is relative detect it and rewrite path case mochiweb_util:safe_relative_path(Path) of - undefined -> + undefined -> ?b2l(Prefix) ++ "/" ++ Path; - P1 -> + P1 -> ?b2l(Prefix) ++ "/" ++ P1 end - + end, % normalize final path (fix levels "." and "..") RawPath1 = ?b2l(iolist_to_binary(normalize_path(RawPath))), - + ?LOG_DEBUG("rewrite to ~p ~n", [RawPath1]), % build a new mochiweb request @@ -168,27 +168,26 @@ RawPath1, MochiReq:get(version), MochiReq:get(headers)), - + % cleanup, It force mochiweb to reparse raw uri. MochiReq1:cleanup(), - + #httpd{ db_url_handlers = DbUrlHandlers, design_url_handlers = DesignUrlHandlers, default_fun = DefaultFun, url_handlers = UrlHandlers } = Req, - - couch_httpd:handle_request(MochiReq1, DefaultFun, + couch_httpd:handle_request_int(MochiReq1, DefaultFun, UrlHandlers, DbUrlHandlers, DesignUrlHandlers) end. - + %% @doc Try to find a rule matching current url. If none is found %% 404 error not_found is raised try_bind_path([], _Method, _PathParts, _QueryList) -> - no_dispatch_path; + no_dispatch_path; try_bind_path([Dispatch|Rest], Method, PathParts, QueryList) -> [{PathParts1, Method1}, RedirectPath, QueryArgs] = Dispatch, case bind_method(Method1, Method) of @@ -196,12 +195,12 @@ case bind_path(PathParts1, PathParts, []) of {ok, Remaining, Bindings} -> Bindings1 = Bindings ++ QueryList, - - % we parse query args from the rule and fill - % it eventually with bindings vars + + % we parse query args from the rule and fill + % it eventually with bindings vars QueryArgs1 = make_query_list(QueryArgs, Bindings1, []), - % remove params in QueryLists1 that are already in + % remove params in QueryLists1 that are already in % QueryArgs1 Bindings2 = lists:foldl(fun({K, V}, Acc) -> K1 = to_atom(K), @@ -213,17 +212,17 @@ end, [], Bindings1), FinalBindings = Bindings2 ++ QueryArgs1, - NewPathParts = make_new_path(RedirectPath, FinalBindings, - Remaining, []), - {NewPathParts, FinalBindings}; + NewPathParts = make_new_path(RedirectPath, FinalBindings, + Remaining, []), + {NewPathParts, FinalBindings}; fail -> try_bind_path(Rest, Method, PathParts, QueryList) - end; + end; false -> try_bind_path(Rest, Method, PathParts, QueryList) end. - -%% rewriting dynamically the quey list given as query member in + +%% rewriting dynamically the quey list given as query member in %% rewrites. Each value is replaced by one binding or an argument %% passed in url. make_query_list([], _Bindings, Acc) -> @@ -239,7 +238,7 @@ make_query_list(Rest, Bindings, [{to_atom(Key), Value1}|Acc]); make_query_list([{Key, Value}|Rest], Bindings, Acc) -> make_query_list(Rest, Bindings, [{to_atom(Key), Value}|Acc]). - + replace_var(Key, Value, Bindings) -> case Value of <<":", Var/binary>> -> @@ -270,16 +269,16 @@ Acc1 = lists:reverse(Acc) ++ Remaining, Acc1; make_new_path([P|Rest], Bindings, Remaining, Acc) when is_atom(P) -> - P2 = case proplists:get_value(P, Bindings) of + P2 = case proplists:get_value(P, Bindings) of undefined -> << "undefined">>; P1 -> P1 end, make_new_path(Rest, Bindings, Remaining, [P2|Acc]); make_new_path([P|Rest], Bindings, Remaining, Acc) -> make_new_path(Rest, Bindings, Remaining, [P|Acc]). - -%% @doc If method of the query fith the rule method. If the + +%% @doc If method of the query fith the rule method. If the %% method rule is '*', which is the default, all %% request method will bind. It allows us to make rules %% depending on HTTP method. @@ -288,7 +287,7 @@ bind_method(Method, Method) -> true; bind_method(_, _) -> - false. + false. %% @doc bind path. Using the rule from we try to bind variables given @@ -305,14 +304,14 @@ bind_path(RestToken, RestMatch, Bindings); bind_path(_, _, _) -> fail. - + %% normalize path. normalize_path(Path) -> - "/" ++ string:join(normalize_path1(string:tokens(Path, + "/" ++ string:join(normalize_path1(string:tokens(Path, "/"), []), [?SEPARATOR]). - - + + normalize_path1([], Acc) -> lists:reverse(Acc); normalize_path1([".."|Rest], Acc) -> @@ -327,8 +326,8 @@ normalize_path1([Path|Rest], Acc) -> normalize_path1(Rest, [Path|Acc]). - -%% @doc transform json rule in erlang for pattern matching + +%% @doc transform json rule in erlang for pattern matching make_rule(Rule) -> Method = case proplists:get_value(<<"method">>, Rule) of undefined -> '*'; @@ -344,17 +343,17 @@ parse_path(From) end, ToParts = case proplists:get_value(<<"to">>, Rule) of - undefined -> + undefined -> throw({error, invalid_rewrite_target}); To -> parse_path(To) end, [{FromParts, Method}, ToParts, QueryArgs]. - + parse_path(Path) -> {ok, SlashRE} = re:compile(<<"\\/">>), path_to_list(re:split(Path, SlashRE), []). - + %% @doc convert a path rule (from or to) to an erlang list %% * and path variable starting by ":" are converted %% in erlang atom. @@ -380,17 +379,17 @@ V; false -> mochiweb_util:quote_plus(V) - end, + end, [{K, V1} | Acc] end, [], Props), lists:flatten(mochiweb_util:urlencode(Props1)). to_atom(V) when is_atom(V) -> - V; + V; to_atom(V) when is_binary(V) -> to_atom(?b2l(V)); to_atom(V) -> list_to_atom(V). - + to_json(V) -> iolist_to_binary(?JSON_ENCODE(V)). Modified: couchdb/branches/0.11.x/test/etap/110-replication-httpc.t URL: http://svn.apache.org/viewvc/couchdb/branches/0.11.x/test/etap/110-replication-httpc.t?rev=911942&r1=911941&r2=911942&view=diff ============================================================================== --- couchdb/branches/0.11.x/test/etap/110-replication-httpc.t (original) +++ couchdb/branches/0.11.x/test/etap/110-replication-httpc.t Fri Feb 19 19:11:53 2010 @@ -19,7 +19,7 @@ auth = [], resource = "", headers = [ - {"User-Agent", "CouchDb/"++couch_server:get_version()}, + {"User-Agent", "CouchDB/"++couch_server:get_version()}, {"Accept", "application/json"}, {"Accept-Encoding", "gzip"} ], Modified: couchdb/branches/0.11.x/test/etap/111-replication-changes-feed.t URL: http://svn.apache.org/viewvc/couchdb/branches/0.11.x/test/etap/111-replication-changes-feed.t?rev=911942&r1=911941&r2=911942&view=diff ============================================================================== --- couchdb/branches/0.11.x/test/etap/111-replication-changes-feed.t (original) +++ couchdb/branches/0.11.x/test/etap/111-replication-changes-feed.t Fri Feb 19 19:11:53 2010 @@ -22,7 +22,7 @@ auth = [], resource = "", headers = [ - {"User-Agent", "CouchDb/"++couch_server:get_version()}, + {"User-Agent", "CouchDB/"++couch_server:get_version()}, {"Accept", "application/json"}, {"Accept-Encoding", "gzip"} ], Modified: couchdb/branches/0.11.x/test/etap/112-replication-missing-revs.t URL: http://svn.apache.org/viewvc/couchdb/branches/0.11.x/test/etap/112-replication-missing-revs.t?rev=911942&r1=911941&r2=911942&view=diff ============================================================================== --- couchdb/branches/0.11.x/test/etap/112-replication-missing-revs.t (original) +++ couchdb/branches/0.11.x/test/etap/112-replication-missing-revs.t Fri Feb 19 19:11:53 2010 @@ -23,7 +23,7 @@ auth = [], resource = "", headers = [ - {"User-Agent", "CouchDb/"++couch_server:get_version()}, + {"User-Agent", "CouchDB/"++couch_server:get_version()}, {"Accept", "application/json"}, {"Accept-Encoding", "gzip"} ], Added: couchdb/branches/0.11.x/test/etap/160-vhosts.t URL: http://svn.apache.org/viewvc/couchdb/branches/0.11.x/test/etap/160-vhosts.t?rev=911942&view=auto ============================================================================== --- couchdb/branches/0.11.x/test/etap/160-vhosts.t (added) +++ couchdb/branches/0.11.x/test/etap/160-vhosts.t Fri Feb 19 19:11:53 2010 @@ -0,0 +1,96 @@ +#!/usr/bin/env escript +%% -*- erlang -*- + +% 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. + +%% XXX: Figure out how to -include("couch_rep.hrl") +-record(http_db, { + url, + auth = [], + resource = "", + headers = [ + {"User-Agent", "CouchDB/"++couch_server:get_version()}, + {"Accept", "application/json"}, + {"Accept-Encoding", "gzip"} + ], + qs = [], + method = get, + body = nil, + options = [ + {response_format,binary}, + {inactivity_timeout, 30000} + ], + retries = 10, + pause = 1, + conn = nil +}). + +server() -> "http://127.0.0.1:5984/". +dbname() -> "etap-test-db". + +config_files() -> + lists:map(fun test_util:build_file/1, [ + "etc/couchdb/default_dev.ini", + "etc/couchdb/local_dev.ini" + ]). + +main(_) -> + test_util:init_code_path(), + + etap:plan(2), + case (catch test()) of + ok -> + etap:end_tests(); + Other -> + etap:diag(io_lib:format("Test died abnormally: ~p", [Other])), + etap:bail(Other) + end, + ok. + +test() -> + couch_server_sup:start_link(config_files()), + ibrowse:start(), + crypto:start(), + + couch_server:delete(list_to_binary(dbname()), []), + {ok, Db} = couch_db:create(list_to_binary(dbname()), []), + + %% end boilerplate, start test + + couch_config:set("vhosts", "example.com", "/etap-test-db", false), + test_regular_request(), + test_vhost_request(), + + %% restart boilerplate + couch_db:close(Db), + couch_server:delete(list_to_binary(dbname()), []), + ok. + +test_regular_request() -> + case ibrowse:send_req(server(), [], get, []) of + {ok, _, _, Body} -> + {[{<<"couchdb">>, <<"Welcome">>}, + {<<"version">>,_} + ]} = couch_util:json_decode(Body), + etap:is(true, true, "should return server info"); + _Else -> false + end. + +test_vhost_request() -> + case ibrowse:send_req(server(), [], get, [], [{host_header, "example.com"}]) of + {ok, _, _, Body} -> + {[{<<"db_name">>, <<"etap-test-db">>},_,_,_,_,_,_,_,_]} + = couch_util:json_decode(Body), + etap:is(true, true, "should return database info"); + _Else -> false + end. Propchange: couchdb/branches/0.11.x/test/etap/160-vhosts.t ------------------------------------------------------------------------------ svn:executable = *