| % 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"). |
| -include_lib("kernel/include/logger.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), |
| |
| ?LOG_DEBUG(#{ |
| what => vhost_redirect, |
| raw_path => Path, |
| target => Target |
| }), |
| 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 = chttpd_util:get_chttpd_config( |
| "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. |