| a | b | |
|---|
| 0 | + | diff --git a/etc/couchdb/local.ini b/etc/couchdb/local.ini |
|---|
| 0 | + | index 96fcdc7..7399f15 100644 |
|---|
| 0 | + | --- a/etc/couchdb/local.ini |
|---|
| 0 | + | +++ b/etc/couchdb/local.ini |
|---|
| 0 | + | @@ -16,6 +16,16 @@ |
|---|
| 0 | + | [log] |
|---|
| 0 | + | ;level = debug |
|---|
| 0 | + | |
|---|
| 0 | + | + |
|---|
| 0 | + | +; To enable Virtual Hosts in CouchDB, add a vhost = path directive. All requests to |
|---|
| 0 | + | +; the Virual Host will be redirected to the path. In the example below all requests |
|---|
| 0 | + | +; to http://example.com/ are redirected to /database. |
|---|
| 0 | + | +; If you run CouchDB on a specific port, include the port number in the vhost: |
|---|
| 0 | + | +; example.com:5984 = /database |
|---|
| 0 | + | + |
|---|
| 0 | + | +[vhosts] |
|---|
| 0 | + | +;example.com = /database/ |
|---|
| 0 | + | + |
|---|
| 0 | + | [update_notification] |
|---|
| 0 | + | ;unique notifier name=/full/path/to/exe -with "cmd line arg" |
|---|
| 0 | + | |
|---|
| 0 | + | diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl |
|---|
| 0 | + | index 6261600..ae7d6c0 100644 |
|---|
| 0 | + | --- a/src/couchdb/couch_httpd.erl |
|---|
| 0 | + | +++ b/src/couchdb/couch_httpd.erl |
|---|
| 0 | + | @@ -13,7 +13,7 @@ |
|---|
| 0 | + | -module(couch_httpd). |
|---|
| 0 | + | -include("couch_db.hrl"). |
|---|
| 0 | + | |
|---|
| 0 | + | --export([start_link/0, stop/0, handle_request/5]). |
|---|
| 0 | + | +-export([start_link/0, stop/0, handle_request/6]). |
|---|
| 0 | + | |
|---|
| 0 | + | -export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,path/1,absolute_uri/2,body_length/1]). |
|---|
| 0 | + | -export([verify_is_server_admin/1,unquote/1,quote/1,recv/2,recv_chunked/4,error_info/1]). |
|---|
| 0 | + | @@ -25,7 +25,7 @@ |
|---|
| 0 | + | -export([start_json_response/2, start_json_response/3, end_json_response/1]). |
|---|
| 0 | + | -export([send_response/4,send_method_not_allowed/2,send_error/4, send_redirect/2,send_chunked_error/2]). |
|---|
| 0 | + | -export([send_json/2,send_json/3,send_json/4,last_chunk/1,parse_multipart_request/3]). |
|---|
| 0 | + | --export([accepted_encodings/1]). |
|---|
| 0 | + | +-export([accepted_encodings/1,handle_request_int/5]). |
|---|
| 0 | + | |
|---|
| 0 | + | start_link() -> |
|---|
| 0 | + | % read config and register for configuration changes |
|---|
| 0 | + | @@ -35,6 +35,7 @@ start_link() -> |
|---|
| 0 | + | |
|---|
| 0 | + | BindAddress = couch_config:get("httpd", "bind_address", any), |
|---|
| 0 | + | Port = couch_config:get("httpd", "port", "5984"), |
|---|
| 0 | + | + VirtualHosts = couch_config:get("vhosts"), |
|---|
| 0 | + | |
|---|
| 0 | + | DefaultSpec = "{couch_httpd_db, handle_request}", |
|---|
| 0 | + | DefaultFun = make_arity_1_fun( |
|---|
| 0 | + | @@ -61,7 +62,8 @@ start_link() -> |
|---|
| 0 | + | DesignUrlHandlers = dict:from_list(DesignUrlHandlersList), |
|---|
| 0 | + | Loop = fun(Req)-> |
|---|
| 0 | + | apply(?MODULE, handle_request, [ |
|---|
| 0 | + | - Req, DefaultFun, UrlHandlers, DbUrlHandlers, DesignUrlHandlers |
|---|
| 0 | + | + Req, DefaultFun, UrlHandlers, DbUrlHandlers, DesignUrlHandlers, |
|---|
| 0 | + | + VirtualHosts |
|---|
| 0 | + | ]) |
|---|
| 0 | + | end, |
|---|
| 0 | + | |
|---|
| 0 | + | @@ -89,6 +91,8 @@ start_link() -> |
|---|
| 0 | + | ("httpd_global_handlers", _) -> |
|---|
| 0 | + | ?MODULE:stop(); |
|---|
| 0 | + | ("httpd_db_handlers", _) -> |
|---|
| 0 | + | + ?MODULE:stop(); |
|---|
| 0 | + | + ("vhosts", _) -> |
|---|
| 0 | + | ?MODULE:stop() |
|---|
| 0 | + | end, Pid), |
|---|
| 0 | + | |
|---|
| 0 | + | @@ -127,9 +131,46 @@ make_fun_spec_strs(SpecStr) -> |
|---|
| 0 | + | stop() -> |
|---|
| 0 | + | mochiweb_http:stop(?MODULE). |
|---|
| 0 | + | |
|---|
| 0 | + | +%% |
|---|
| 0 | + | + |
|---|
| 0 | + | +% if there's a vhost definition that matches the request, redirect internally |
|---|
| 0 | + | +redirect_to_vhost(MochiReq, DefaultFun, |
|---|
| 0 | + | + UrlHandlers, DbUrlHandlers, DesignUrlHandlers, VhostTarget) -> |
|---|
| 0 | + | + |
|---|
| 0 | + | + Path = MochiReq:get(path), |
|---|
| 0 | + | + Target = VhostTarget ++ Path, |
|---|
| 0 | + | + ?LOG_DEBUG("Vhost Target: '~p'~n", [Target]), |
|---|
| 0 | + | + % build a new mochiweb request |
|---|
| 0 | + | + MochiReq1 = mochiweb_request:new(MochiReq:get(socket), |
|---|
| 0 | + | + MochiReq:get(method), |
|---|
| 0 | + | + Target, |
|---|
| 0 | + | + MochiReq:get(version), |
|---|
| 0 | + | + MochiReq:get(headers)), |
|---|
| 0 | + | + % cleanup, It force mochiweb to reparse raw uri. |
|---|
| 0 | + | + MochiReq1:cleanup(), |
|---|
| 0 | + | + |
|---|
| 0 | + | + handle_request_int(MochiReq1, DefaultFun, |
|---|
| 0 | + | + UrlHandlers, DbUrlHandlers, DesignUrlHandlers). |
|---|
| 0 | + | |
|---|
| 0 | + | handle_request(MochiReq, DefaultFun, |
|---|
| 0 | + | - UrlHandlers, DbUrlHandlers, DesignUrlHandlers) -> |
|---|
| 0 | + | + UrlHandlers, DbUrlHandlers, DesignUrlHandlers, VirtualHosts) -> |
|---|
| 0 | + | + |
|---|
| 0 | + | + % grab Host from Req |
|---|
| 0 | + | + Vhost = MochiReq:get_header_value("Host"), |
|---|
| 0 | + | + |
|---|
| 0 | + | + % find Vhost in config |
|---|
| 0 | + | + case proplists:get_value(Vhost, VirtualHosts) of |
|---|
| 0 | + | + undefined -> % business as usual |
|---|
| 0 | + | + handle_request_int(MochiReq, DefaultFun, |
|---|
| 0 | + | + UrlHandlers, DbUrlHandlers, DesignUrlHandlers); |
|---|
| 0 | + | + VhostTarget -> |
|---|
| 0 | + | + redirect_to_vhost(MochiReq, DefaultFun, |
|---|
| 0 | + | + UrlHandlers, DbUrlHandlers, DesignUrlHandlers, VhostTarget) |
|---|
| 0 | + | + end. |
|---|
| 0 | + | + |
|---|
| 0 | + | + |
|---|
| 0 | + | +handle_request_int(MochiReq, DefaultFun, |
|---|
| 0 | + | + UrlHandlers, DbUrlHandlers, DesignUrlHandlers) -> |
|---|
| 0 | + | Begin = now(), |
|---|
| 0 | + | AuthenticationSrcs = make_fun_spec_strs( |
|---|
| 0 | + | couch_config:get("httpd", "authentication_handlers")), |
|---|
| 0 | + | diff --git a/src/couchdb/couch_httpd_misc_handlers.erl b/src/couchdb/couch_httpd_misc_handlers.erl |
|---|
| 0 | + | index 2d67b32..a854330 100644 |
|---|
| 0 | + | --- a/src/couchdb/couch_httpd_misc_handlers.erl |
|---|
| 0 | + | +++ b/src/couchdb/couch_httpd_misc_handlers.erl |
|---|
| 0 | + | @@ -46,6 +46,7 @@ handle_favicon_req(#httpd{method='GET'}=Req, DocumentRoot) -> |
|---|
| 0 | + | {"Expires", httpd_util:rfc1123_date(OneYearFromNow)} |
|---|
| 0 | + | ], |
|---|
| 0 | + | couch_httpd:serve_file(Req, "favicon.ico", DocumentRoot, CachingHeaders); |
|---|
| 0 | + | + |
|---|
| 0 | + | handle_favicon_req(Req, _) -> |
|---|
| 0 | + | send_method_not_allowed(Req, "GET,HEAD"). |
|---|
| 0 | + | |
|---|
| 0 | + | diff --git a/src/couchdb/couch_httpd_rewrite.erl b/src/couchdb/couch_httpd_rewrite.erl |
|---|
| 0 | + | index 72ec954..7946809 100644 |
|---|
| 0 | + | --- a/src/couchdb/couch_httpd_rewrite.erl |
|---|
| 0 | + | +++ b/src/couchdb/couch_httpd_rewrite.erl |
|---|
| 0 | + | @@ -179,7 +179,7 @@ handle_rewrite_req(#httpd{ |
|---|
| 0 | + | url_handlers = UrlHandlers |
|---|
| 0 | + | } = Req, |
|---|
| 0 | + | |
|---|
| 0 | + | - couch_httpd:handle_request(MochiReq1, DefaultFun, |
|---|
| 0 | + | + couch_httpd:handle_request_int(MochiReq1, DefaultFun, |
|---|
| 0 | + | UrlHandlers, DbUrlHandlers, DesignUrlHandlers) |
|---|
| 0 | + | end. |
|---|
| 0 | + | |
|---|
| 0 | + | diff --git a/test/etap/160-vhosts.t b/test/etap/160-vhosts.t |
|---|
| 0 | + | new file mode 100755 |
|---|
| 0 | + | index 0000000..fa61cab |
|---|
| 0 | + | --- /dev/null |
|---|
| 0 | + | +++ b/test/etap/160-vhosts.t |
|---|
| 0 | + | @@ -0,0 +1,96 @@ |
|---|
| 0 | + | +#!/usr/bin/env escript |
|---|
| 0 | + | +%% -*- erlang -*- |
|---|
| 0 | + | + |
|---|
| 0 | + | +% Licensed under the Apache License, Version 2.0 (the "License"); you may not |
|---|
| 0 | + | +% use this file except in compliance with the License. You may obtain a copy of |
|---|
| 0 | + | +% the License at |
|---|
| 0 | + | +% |
|---|
| 0 | + | +% http://www.apache.org/licenses/LICENSE-2.0 |
|---|
| 0 | + | +% |
|---|
| 0 | + | +% Unless required by applicable law or agreed to in writing, software |
|---|
| 0 | + | +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
|---|
| 0 | + | +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
|---|
| 0 | + | +% License for the specific language governing permissions and limitations under |
|---|
| 0 | + | +% the License. |
|---|
| 0 | + | + |
|---|
| 0 | + | +%% XXX: Figure out how to -include("couch_rep.hrl") |
|---|
| 0 | + | +-record(http_db, { |
|---|
| 0 | + | + url, |
|---|
| 0 | + | + auth = [], |
|---|
| 0 | + | + resource = "", |
|---|
| 0 | + | + headers = [ |
|---|
| 0 | + | + {"User-Agent", "CouchDB/"++couch_server:get_version()}, |
|---|
| 0 | + | + {"Accept", "application/json"}, |
|---|
| 0 | + | + {"Accept-Encoding", "gzip"} |
|---|
| 0 | + | + ], |
|---|
| 0 | + | + qs = [], |
|---|
| 0 | + | + method = get, |
|---|
| 0 | + | + body = nil, |
|---|
| 0 | + | + options = [ |
|---|
| 0 | + | + {response_format,binary}, |
|---|
| 0 | + | + {inactivity_timeout, 30000} |
|---|
| 0 | + | + ], |
|---|
| 0 | + | + retries = 10, |
|---|
| 0 | + | + pause = 1, |
|---|
| 0 | + | + conn = nil |
|---|
| 0 | + | +}). |
|---|
| 0 | + | + |
|---|
| 0 | + | +server() -> "http://127.0.0.1:5984/". |
|---|
| 0 | + | +dbname() -> "etap-test-db". |
|---|
| 0 | + | + |
|---|
| 0 | + | +config_files() -> |
|---|
| 0 | + | + lists:map(fun test_util:build_file/1, [ |
|---|
| 0 | + | + "etc/couchdb/default_dev.ini", |
|---|
| 0 | + | + "etc/couchdb/local_dev.ini" |
|---|
| 0 | + | + ]). |
|---|
| 0 | + | + |
|---|
| 0 | + | +main(_) -> |
|---|
| 0 | + | + test_util:init_code_path(), |
|---|
| 0 | + | + |
|---|
| 0 | + | + etap:plan(2), |
|---|
| 0 | + | + case (catch test()) of |
|---|
| 0 | + | + ok -> |
|---|
| 0 | + | + etap:end_tests(); |
|---|
| 0 | + | + Other -> |
|---|
| 0 | + | + etap:diag(io_lib:format("Test died abnormally: ~p", [Other])), |
|---|
| 0 | + | + etap:bail(Other) |
|---|
| 0 | + | + end, |
|---|
| 0 | + | + ok. |
|---|
| 0 | + | + |
|---|
| 0 | + | +test() -> |
|---|
| 0 | + | + couch_server_sup:start_link(config_files()), |
|---|
| 0 | + | + ibrowse:start(), |
|---|
| 0 | + | + crypto:start(), |
|---|
| 0 | + | + |
|---|
| 0 | + | + couch_server:delete(list_to_binary(dbname()), []), |
|---|
| 0 | + | + {ok, Db} = couch_db:create(list_to_binary(dbname()), []), |
|---|
| 0 | + | + |
|---|
| 0 | + | + %% end boilerplate, start test |
|---|
| 0 | + | + |
|---|
| 0 | + | + couch_config:set("vhosts", "example.com", "/etap-test-db", false), |
|---|
| 0 | + | + test_regular_request(), |
|---|
| 0 | + | + test_vhost_request(), |
|---|
| 0 | + | + |
|---|
| 0 | + | + %% restart boilerplate |
|---|
| 0 | + | + couch_db:close(Db), |
|---|
| 0 | + | + couch_server:delete(list_to_binary(dbname()), []), |
|---|
| 0 | + | + ok. |
|---|
| 0 | + | + |
|---|
| 0 | + | +test_regular_request() -> |
|---|
| 0 | + | + case ibrowse:send_req(server(), [], get, []) of |
|---|
| 0 | + | + {ok, _, _, Body} -> |
|---|
| 0 | + | + {[{<<"couchdb">>, <<"Welcome">>}, |
|---|
| 0 | + | + {<<"version">>,_} |
|---|
| 0 | + | + ]} = couch_util:json_decode(Body), |
|---|
| 0 | + | + etap:is(true, true, "should return server info"); |
|---|
| 0 | + | + _Else -> false |
|---|
| 0 | + | + end. |
|---|
| 0 | + | + |
|---|
| 0 | + | +test_vhost_request() -> |
|---|
| 0 | + | + case ibrowse:send_req(server(), [], get, [], [{host_header, "example.com"}]) of |
|---|
| 0 | + | + {ok, _, _, Body} -> |
|---|
| 0 | + | + {[{<<"db_name">>, <<"etap-test-db">>},_,_,_,_,_,_,_,_]} |
|---|
| 0 | + | + = couch_util:json_decode(Body), |
|---|
| 0 | + | + etap:is(true, true, "should return database info"); |
|---|
| 0 | + | + _Else -> false |
|---|
| 0 | + | + end. |
|---|
| ... | |
|---|