blob: 574dba9c8d91224c83fddd98bd7c66f3601e6eee [file] [log] [blame]
% 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.
-module(couch_httpd_vhost).
-behaviour(gen_server).
-vsn(1).
-behaviour(config_listener).
-compile(tuple_calls).
-export([start_link/0, reload/0, get_state/0, dispatch_host/1]).
-export([urlsplit_netloc/2, redirect_to_vhost/2]).
-export([host/1, split_host_port/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
% config_listener api
-export([handle_config_change/5, handle_config_terminate/3]).
-include_lib("couch/include/couch_db.hrl").
-define(SEPARATOR, $\/).
-define(MATCH_ALL, {bind, '*'}).
-define(RELISTEN_DELAY, 5000).
-record(vhosts_state, {
vhosts,
vhost_globals,
vhosts_fun}).
%% doc the vhost manager.
%% This gen_server keep state of vhosts added to the ini and try to
%% match the Host header (or forwarded) against rules built against
%% vhost list.
%%
%% Declaration of vhosts take place in the configuration file :
%%
%% [vhosts]
%% example.com = /example
%% *.example.com = /example
%%
%% The first line will rewrite the rquest to display the content of the
%% example database. This rule works only if the Host header is
%% 'example.com' and won't work for CNAMEs. Second rule on the other hand
%% match all CNAMES to example db. So www.example.com or db.example.com
%% will work.
%%
%% The wildcard ('*') should always be the last in the cnames:
%%
%% "*.db.example.com = /" will match all cname on top of db
%% examples to the root of the machine.
%%
%%
%% Rewriting Hosts to path
%% -----------------------
%%
%% Like in the _rewrite handler you could match some variable and use
%them to create the target path. Some examples:
%%
%% [vhosts]
%% *.example.com = /*
%% :dbname.example.com = /:dbname
%% :ddocname.:dbname.example.com = /:dbname/_design/:ddocname/_rewrite
%%
%% First rule pass wildcard as dbname, second do the same but use a
%% variable name and the third one allows you to use any app with
%% @ddocname in any db with @dbname .
%%
%% You could also change the default function to handle request by
%% changing the setting `redirect_vhost_handler` in `httpd` section of
%% the Ini:
%%
%% [httpd]
%% redirect_vhost_handler = {Module, Fun}
%%
%% The function take 2 args : the mochiweb request object and the target
%%% path.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%% @doc reload vhosts rules
reload() ->
gen_server:call(?MODULE, reload).
get_state() ->
gen_server:call(?MODULE, get_state).
%% @doc Try to find a rule matching current Host heade. some rule is
%% found it rewrite the Mochiweb Request else it return current Request.
dispatch_host(MochiReq) ->
case vhost_enabled() of
true ->
dispatch_host_int(MochiReq);
false ->
MochiReq
end.
dispatch_host_int(MochiReq) ->
#vhosts_state{
vhost_globals = VHostGlobals,
vhosts = VHosts,
vhosts_fun=Fun} = get_state(),
{"/" ++ VPath, Query, Fragment} = mochiweb_util:urlsplit_path(MochiReq:get(raw_path)),
VPathParts = string:tokens(VPath, "/"),
VHost = host(MochiReq),
{VHostParts, VhostPort} = split_host_port(VHost),
FinalMochiReq = case try_bind_vhost(VHosts, lists:reverse(VHostParts),
VhostPort, VPathParts) of
no_vhost_matched -> MochiReq;
{VhostTarget, NewPath} ->
case vhost_global(VHostGlobals, MochiReq) of
true ->
MochiReq;
_Else ->
NewPath1 = mochiweb_util:urlunsplit_path({NewPath, Query,
Fragment}),
MochiReq1 = mochiweb_request:new(MochiReq:get(socket),
MochiReq:get(method),
NewPath1,
MochiReq:get(version),
MochiReq:get(headers)),
Fun(MochiReq1, VhostTarget)
end
end,
FinalMochiReq.
append_path("/"=_Target, "/"=_Path) ->
"/";
append_path(Target, Path) ->
Target ++ Path.
% default redirect vhost handler
redirect_to_vhost(MochiReq, VhostTarget) ->
Path = MochiReq:get(raw_path),
Target = append_path(VhostTarget, Path),
couch_log:debug("Vhost Target: '~p'~n", [Target]),
Headers = mochiweb_headers:enter("x-couchdb-vhost-path", Path,
MochiReq:get(headers)),
% build a new mochiweb request
MochiReq1 = mochiweb_request:new(MochiReq:get(socket),
MochiReq:get(method),
Target,
MochiReq:get(version),
Headers),
% cleanup, It force mochiweb to reparse raw uri.
MochiReq1:cleanup(),
MochiReq1.
%% if so, then it will not be rewritten, but will run as a normal couchdb request.
%* normally you'd use this for _uuids _utils and a few of the others you want to
%% keep available on vhosts. You can also use it to make databases 'global'.
vhost_global( VhostGlobals, MochiReq) ->
RawUri = MochiReq:get(raw_path),
{"/" ++ Path, _, _} = mochiweb_util:urlsplit_path(RawUri),
Front = case couch_httpd:partition(Path) of
{"", "", ""} ->
"/"; % Special case the root url handler
{FirstPart, _, _} ->
FirstPart
end,
[true] == [true||V <- VhostGlobals, V == Front].
%% bind host
%% first it try to bind the port then the hostname.
try_bind_vhost([], _HostParts, _Port, _PathParts) ->
no_vhost_matched;
try_bind_vhost([VhostSpec|Rest], HostParts, Port, PathParts) ->
{{VHostParts, VPort, VPath}, Path} = VhostSpec,
case bind_port(VPort, Port) of
ok ->
case bind_vhost(lists:reverse(VHostParts), HostParts, []) of
{ok, Bindings, Remainings} ->
case bind_path(VPath, PathParts) of
{ok, PathParts1} ->
Path1 = make_target(Path, Bindings, Remainings, []),
{make_path(Path1), make_path(PathParts1)};
fail ->
try_bind_vhost(Rest, HostParts, Port,
PathParts)
end;
fail -> try_bind_vhost(Rest, HostParts, Port, PathParts)
end;
fail -> try_bind_vhost(Rest, HostParts, Port, PathParts)
end.
%% doc: build new patch from bindings. bindings are query args
%% (+ dynamic query rewritten if needed) and bindings found in
%% bind_path step.
%% TODO: merge code with rewrite. But we need to make sure we are
%% in string here.
make_target([], _Bindings, _Remaining, Acc) ->
lists:reverse(Acc);
make_target([?MATCH_ALL], _Bindings, Remaining, Acc) ->
Acc1 = lists:reverse(Acc) ++ Remaining,
Acc1;
make_target([?MATCH_ALL|_Rest], _Bindings, Remaining, Acc) ->
Acc1 = lists:reverse(Acc) ++ Remaining,
Acc1;
make_target([{bind, P}|Rest], Bindings, Remaining, Acc) ->
P2 = case couch_util:get_value({bind, P}, Bindings) of
undefined -> "undefined";
P1 -> P1
end,
make_target(Rest, Bindings, Remaining, [P2|Acc]);
make_target([P|Rest], Bindings, Remaining, Acc) ->
make_target(Rest, Bindings, Remaining, [P|Acc]).
%% bind port
bind_port(Port, Port) -> ok;
bind_port('*', _) -> ok;
bind_port(_,_) -> fail.
%% bind bhost
bind_vhost([],[], Bindings) -> {ok, Bindings, []};
bind_vhost([?MATCH_ALL], [], _Bindings) -> fail;
bind_vhost([?MATCH_ALL], Rest, Bindings) -> {ok, Bindings, Rest};
bind_vhost([], _HostParts, _Bindings) -> fail;
bind_vhost([{bind, Token}|Rest], [Match|RestHost], Bindings) ->
bind_vhost(Rest, RestHost, [{{bind, Token}, Match}|Bindings]);
bind_vhost([Cname|Rest], [Cname|RestHost], Bindings) ->
bind_vhost(Rest, RestHost, Bindings);
bind_vhost(_, _, _) -> fail.
%% bind path
bind_path([], PathParts) ->
{ok, PathParts};
bind_path(_VPathParts, []) ->
fail;
bind_path([Path|VRest],[Path|Rest]) ->
bind_path(VRest, Rest);
bind_path(_, _) ->
fail.
% utilities
%% create vhost list from ini
host(MochiReq) ->
XHost = config:get("httpd", "x_forwarded_host",
"X-Forwarded-Host"),
case MochiReq:get_header_value(XHost) of
undefined ->
case MochiReq:get_header_value("Host") of
undefined -> [];
Value1 -> Value1
end;
Value -> Value
end.
make_vhosts() ->
Vhosts = lists:foldl(fun
({_, ""}, Acc) ->
Acc;
({Vhost, Path}, Acc) ->
[{parse_vhost(Vhost), split_path(Path)}|Acc]
end, [], config:get("vhosts")),
lists:reverse(lists:usort(Vhosts)).
parse_vhost(Vhost) ->
case urlsplit_netloc(Vhost, []) of
{[], Path} ->
{make_spec("*", []), '*', Path};
{HostPort, []} ->
{H, P} = split_host_port(HostPort),
H1 = make_spec(H, []),
{H1, P, []};
{HostPort, Path} ->
{H, P} = split_host_port(HostPort),
H1 = make_spec(H, []),
{H1, P, string:tokens(Path, "/")}
end.
split_host_port(HostAsString) ->
case string:rchr(HostAsString, $:) of
0 ->
{split_host(HostAsString), '*'};
N ->
HostPart = string:substr(HostAsString, 1, N-1),
case (catch erlang:list_to_integer(string:substr(HostAsString,
N+1, length(HostAsString)))) of
{'EXIT', _} ->
{split_host(HostAsString), '*'};
Port ->
{split_host(HostPart), Port}
end
end.
split_host(HostAsString) ->
string:tokens(HostAsString, "\.").
split_path(Path) ->
make_spec(string:tokens(Path, "/"), []).
make_spec([], Acc) ->
lists:reverse(Acc);
make_spec([""|R], Acc) ->
make_spec(R, Acc);
make_spec(["*"|R], Acc) ->
make_spec(R, [?MATCH_ALL|Acc]);
make_spec([P|R], Acc) ->
P1 = parse_var(P),
make_spec(R, [P1|Acc]).
parse_var(P) ->
case P of
":" ++ Var ->
{bind, Var};
_ -> P
end.
% mochiweb doesn't export it.
urlsplit_netloc("", Acc) ->
{lists:reverse(Acc), ""};
urlsplit_netloc(Rest=[C | _], Acc) when C =:= $/; C =:= $?; C =:= $# ->
{lists:reverse(Acc), Rest};
urlsplit_netloc([C | Rest], Acc) ->
urlsplit_netloc(Rest, [C | Acc]).
make_path(Parts) ->
"/" ++ string:join(Parts,[?SEPARATOR]).
init(_) ->
ok = config:listen_for_changes(?MODULE, nil),
%% load configuration
{VHostGlobals, VHosts, Fun} = load_conf(),
State = #vhosts_state{
vhost_globals=VHostGlobals,
vhosts=VHosts,
vhosts_fun=Fun},
{ok, State}.
handle_call(reload, _From, _State) ->
{VHostGlobals, VHosts, Fun} = load_conf(),
{reply, ok, #vhosts_state{
vhost_globals=VHostGlobals,
vhosts=VHosts,
vhosts_fun=Fun}};
handle_call(get_state, _From, State) ->
{reply, State, State};
handle_call(_Msg, _From, State) ->
{noreply, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(restart_config_listener, State) ->
ok = config:listen_for_changes(?MODULE, nil),
{noreply, State};
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
handle_config_change("vhosts", _, _, _, _) ->
{ok, ?MODULE:reload()};
handle_config_change(_, _, _, _, _) ->
{ok, nil}.
handle_config_terminate(_, stop, _) ->
ok;
handle_config_terminate(_Server, _Reason, _State) ->
erlang:send_after(?RELISTEN_DELAY, whereis(?MODULE), restart_config_listener).
load_conf() ->
%% get vhost globals
VHostGlobals = re:split("_utils, _uuids, _session, _users", "\\s*,\\s*",
[{return, list}]),
%% build vhosts matching rules
VHosts = make_vhosts(),
%% build vhosts handler fun
DefaultVHostFun = "{couch_httpd_vhost, redirect_to_vhost}",
Fun = couch_httpd:make_arity_2_fun(DefaultVHostFun),
{VHostGlobals, VHosts, Fun}.
%% cheaply determine if there are any virtual hosts
%% configured at all.
vhost_enabled() ->
case config:get("vhosts") of
[] ->
false;
_ ->
true
end.