--- Revision None +++ Revision 613465323335 @@ -0,0 +1,245 @@ +diff --git a/etc/couchdb/local.ini b/etc/couchdb/local.ini +index 96fcdc7..7399f15 100644 +--- a/etc/couchdb/local.ini ++++ b/etc/couchdb/local.ini +@@ -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" + +diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl +index 6261600..ae7d6c0 100644 +--- a/src/couchdb/couch_httpd.erl ++++ b/src/couchdb/couch_httpd.erl +@@ -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 @@ start_link() -> + + 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 @@ start_link() -> + 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 @@ start_link() -> + ("httpd_global_handlers", _) -> + ?MODULE:stop(); + ("httpd_db_handlers", _) -> ++ ?MODULE:stop(); ++ ("vhosts", _) -> + ?MODULE:stop() + end, Pid), + +@@ -127,9 +131,46 @@ make_fun_spec_strs(SpecStr) -> + 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")), +diff --git a/src/couchdb/couch_httpd_misc_handlers.erl b/src/couchdb/couch_httpd_misc_handlers.erl +index 2d67b32..a854330 100644 +--- a/src/couchdb/couch_httpd_misc_handlers.erl ++++ b/src/couchdb/couch_httpd_misc_handlers.erl +@@ -46,6 +46,7 @@ handle_favicon_req(#httpd{method='GET'}=Req, DocumentRoot) -> + {"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"). + +diff --git a/src/couchdb/couch_httpd_rewrite.erl b/src/couchdb/couch_httpd_rewrite.erl +index 72ec954..7946809 100644 +--- a/src/couchdb/couch_httpd_rewrite.erl ++++ b/src/couchdb/couch_httpd_rewrite.erl +@@ -179,7 +179,7 @@ handle_rewrite_req(#httpd{ + url_handlers = UrlHandlers + } = Req, + +- couch_httpd:handle_request(MochiReq1, DefaultFun, ++ couch_httpd:handle_request_int(MochiReq1, DefaultFun, + UrlHandlers, DbUrlHandlers, DesignUrlHandlers) + end. + +diff --git a/test/etap/160-vhosts.t b/test/etap/160-vhosts.t +new file mode 100755 +index 0000000..fa61cab +--- /dev/null ++++ b/test/etap/160-vhosts.t +@@ -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.