Anonymous avatar Anonymous committed ab37abf

host-based dispatching

dispatch rules can now take two different forms:

The old form: {PathMatchSpec, Module, Paramters}
The new form: {HostMatchSpec, [{PathMatchSpec, Module, Parameters}]}

The former is equivalent to the latter with HostMatchSpec={['*'],'*'}

HostMatchSpec is matched against one of (in order of preference):
X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Server, Host

HostMatchSpec can have two forms:
{[HostPart], PortSpec}
or
[HostPart]
The latter is equivalent to the former with PortSpec='*'

The list of host parts is matched against the hostname extracted from a header in much the same way that PathMatchSpec is matched against the path.

Examples:

{[], root_resource, [x]}.
{['*'], [{[], root_resource, [x]}]}.
{{['*'],'*'}, [{[], root_resource, [x]}]}.
Will each match the root path of any host.

{["example","com"], [{[], root_resource, [x]},
{["static"], static_resource, [y]}]}.
Will dispatch the root of example.com to root_resource and example.com/static to static_resource.

{['*',"example","com"], [{[], root_resource, [x]},
{["static"], static_resource, [y]}]}.
Will do the same as above, but also for any subdomains of example.com.

{{[host,"local"], 8000}, [{[], res_A, [x]}]}.
{{[host,"local"], 8001}, [{[], res_B, [x]}]}.
Will dispatch requests to ?.local:8000/ to res_A and requests to ?.local:8001/ to resB, binding the host part immediately preceeding ".local" to 'host', such that wrq:get_path_info(host, ReqData) would return the matched string.

Comments (0)

Files changed (6)

include/wm_reqdata.hrl

                      disp_path, path, raw_path, path_info, path_tokens,
                      app_root,response_code,max_recv_body,
                      req_cookie, req_qs, req_headers, req_body,
-                     resp_redirect, resp_headers, resp_body
+                     resp_redirect, resp_headers, resp_body,
+                     host_tokens, port
                     }).
 

src/webmachine_dispatcher.erl

 -module(webmachine_dispatcher).
 -author('Robert Ahrens <rahrens@basho.com>').
 -author('Justin Sheehy <justin@basho.com>').
+-author('Bryan Fink <bryan@basho.com>').
 
--export([dispatch/2]).
+-export([dispatch/2, dispatch/3]).
 
 -define(SEPARATOR, $\/).
 -define(MATCH_ALL, '*').
 %% @doc Interface for URL dispatching.
 %% See also http://bitbucket.org/justin/webmachine/wiki/DispatchConfiguration
 dispatch(PathAsString, DispatchList) ->
+    dispatch([], PathAsString, DispatchList).
+
+%% @spec dispatch(Host::string(), Path::string(),
+%%                DispatchList::[matchterm()]) ->
+%%         dispterm() | dispfail()
+%% @doc Interface for URL dispatching.
+%% See also http://bitbucket.org/justin/webmachine/wiki/DispatchConfiguration
+dispatch(HostAsString, PathAsString, DispatchList) ->
     Path = string:tokens(PathAsString, [?SEPARATOR]),
     % URIs that end with a trailing slash are implicitly one token
     % "deeper" than we otherwise might think as we are "inside"
 		     true -> 1;
 		     _ -> 0
 		 end,
-    try_binding(DispatchList, Path, ExtraDepth).
+    {Host, Port} = split_host_port(HostAsString),
+    try_host_binding(DispatchList, lists:reverse(Host), Port,
+                     Path, ExtraDepth).
 
-%% @type matchterm() = {[pathterm()], matchmod(), matchopts()}.
+split_host_port(HostAsString) ->
+    case string:tokens(HostAsString, ":") of
+        [HostPart, PortPart] ->
+            {split_host(HostPart), list_to_integer(PortPart)};
+        [HostPart] ->
+            {split_host(HostPart), 80}
+    end.
+
+split_host(HostAsString) ->
+    string:tokens(HostAsString, ".").
+
+%% @type matchterm() = hostmatchterm() | pathmatchterm()
 % The dispatch configuration is a list of these terms, and the
+% first one whose host and path terms match the input is used.
+% Using a pathmatchterm() here is equivalent to using a hostmatchterm()
+% of the form {{['*'],'*'}, [pathmatchterm()]}.
+
+%% @type hostmatchterm() = {hostmatch(), [pathmatchterm()]}
+% The dispatch configuration contains a list of these terms, and the
+% first one whose host and one pathmatchterm match is used.
+
+%% @type hostmatch() = [hostterm()] | {[hostterm()], portterm()}
+% A host header (Host, X-Forwarded-For, etc.) will be matched against
+% this term.  Using a raws [hostterm()] list is equivalent to using
+% {[hostterm()], '*'}.
+
+%% @type hostterm() = '*' | string() | atom()
+% A list of hostterms is matched against a '.'-separated hostname.
+% The '*' hosterm matches all remaining tokens, and is only allowed at
+% the head of the list.
+% A string hostterm will match a token of exactly the same string.
+% Any atom hostterm other than '*' will match any token and will
+% create a binding in the result if a complete match occurs.
+
+%% @type portterm() = '*' | integer() | atom()
+% A portterm is matched against the integer port after any ':' in
+% the hostname, or 80 if no port is found.
+% The '*' portterm patches any port
+% An integer portterm will match a port of exactly the same integer.
+% Any atom portterm other than '*' will match any port and will
+% create a binding in the result if a complete match occurs.
+
+%% @type pathmatchterm() = {[pathterm()], matchmod(), matchopts()}.
+% The dispatch configuration contains a list of these terms, and the
 % first one whose list of pathterms matches the input path is used.
 
 %% @type pathterm() = '*' | string() | atom().
 
 %% @type dispfail() = {no_dispatch_match, pathtokens()}.
 
-try_binding([], PathTokens, _) ->
+try_host_binding([], Host, Port, Path, _Depth) ->
+    {no_dispatch_match, {Host, Port}, Path};
+try_host_binding([Dispatch|Rest], Host, Port, Path, Depth) ->
+    {{HostSpec,PortSpec},PathSpec} =
+        case Dispatch of
+            {{H,P},S} -> {{H,P},S};
+            {H,S}     -> {{H,?MATCH_ALL},S};
+            S         -> {{[?MATCH_ALL],?MATCH_ALL},[S]}
+        end,
+    case bind_port(PortSpec, Port, []) of
+        {ok, PortBindings} ->
+            case bind(lists:reverse(HostSpec), Host, PortBindings, 0) of
+                {ok, HostRemainder, HostBindings, _} ->
+                    case try_path_binding(PathSpec, Path, HostBindings, Depth) of
+                        {Mod, Props, PathRemainder, PathBindings,
+                         AppRoot, StringPath} ->
+                            {Mod, Props, HostRemainder, Port, PathRemainder,
+                             PathBindings, AppRoot, StringPath};
+                        {no_dispatch_match, _} ->
+                            try_host_binding(Rest, Host, Port, Path, Depth)
+                    end;
+                fail ->
+                    try_host_binding(Rest, Host, Port, Path, Depth)
+            end;
+        fail ->
+            try_host_binding(Rest, Host, Port, Path, Depth)
+    end.
+
+bind_port(Port, Port, Bindings) -> {ok, Bindings};
+bind_port(?MATCH_ALL, _Port, Bindings) -> {ok, Bindings};
+bind_port(PortAtom, Port, Bindings) when is_atom(PortAtom) ->
+    {ok, [{PortAtom, Port}|Bindings]};
+bind_port(_, _, _) -> fail.
+
+try_path_binding([], PathTokens, _, _) ->
     {no_dispatch_match, PathTokens};
-try_binding([{PathSchema, Mod, Props}|Rest], PathTokens, ExtraDepth) ->
-    case bind_path(PathSchema, PathTokens, [], 0) of
-        {ok, Remainder, Bindings, Depth} ->
-            {Mod, Props, Remainder, Bindings,
+try_path_binding([{PathSchema, Mod, Props}|Rest], PathTokens,
+                 Bindings, ExtraDepth) ->
+    case bind(PathSchema, PathTokens, Bindings, 0) of
+        {ok, Remainder, NewBindings, Depth} ->
+            {Mod, Props, Remainder, NewBindings,
              calculate_app_root(Depth + ExtraDepth), reconstitute(Remainder)};
         fail -> 
-            try_binding(Rest, PathTokens, ExtraDepth)
+            try_path_binding(Rest, PathTokens, Bindings, ExtraDepth)
     end.
 
-bind_path([], [], Bindings, Depth) ->
+bind([], [], Bindings, Depth) ->
     {ok, [], Bindings, Depth};
-bind_path([?MATCH_ALL], PathRest, Bindings, Depth) when is_list(PathRest) ->
-    {ok, PathRest, Bindings, Depth + length(PathRest)};
-bind_path(_, [], _, _) ->
+bind([?MATCH_ALL], Rest, Bindings, Depth) when is_list(Rest) ->
+    {ok, Rest, Bindings, Depth + length(Rest)};
+bind(_, [], _, _) ->
     fail;
-bind_path([Token|Rest],[Match|PathRest],Bindings,Depth) when is_atom(Token) ->
-    bind_path(Rest, PathRest, [{Token, Match}|Bindings], Depth + 1);
-bind_path([Token|Rest], [Token|PathRest], Bindings, Depth) ->
-    bind_path(Rest, PathRest, Bindings, Depth + 1);
-bind_path(_, _, _, _) ->
+bind([Token|RestToken],[Match|RestMatch],Bindings,Depth) when is_atom(Token) ->
+    bind(RestToken, RestMatch, [{Token, Match}|Bindings], Depth + 1);
+bind([Token|RestToken], [Token|RestMatch], Bindings, Depth) ->
+    bind(RestToken, RestMatch, Bindings, Depth + 1);
+bind(_, _, _, _) ->
     fail.
 
 reconstitute([]) -> "";

src/webmachine_mochiweb.erl

 loop(MochiReq) ->
     Req = webmachine:new_request(mochiweb, MochiReq),
     {ok, DispatchList} = application:get_env(webmachine, dispatch_list),
-    case webmachine_dispatcher:dispatch(Req:path(), DispatchList) of
-        {no_dispatch_match, _UnmatchedPathTokens} ->
+    Host = case host_headers(Req) of
+               [H|_] -> H;
+               [] -> []
+           end,
+    case webmachine_dispatcher:dispatch(Host, Req:path(), DispatchList) of
+        {no_dispatch_match, _UnmatchedHost, _UnmatchedPathTokens} ->
             {ok, ErrorHandler} = application:get_env(webmachine, error_handler),
 	    ErrorHTML = ErrorHandler:render_error(404, Req, {none, none, []}),
 	    Req:append_to_response_body(ErrorHTML),
 		end,
 	    spawn(LogModule, log_access, [LogData]),
 	    Req:stop();
-        {Mod, ModOpts, PathTokens, Bindings, AppRoot, StringPath} ->
+        {Mod, ModOpts, HostTokens, Port, PathTokens, Bindings,
+         AppRoot, StringPath} ->
             BootstrapResource = webmachine_resource:new(x,x,x,x),
             {ok, Resource} = BootstrapResource:wrap(Mod, ModOpts),
-	    Req:load_dispatch_data(Bindings,PathTokens,AppRoot,StringPath,Req),
+	    Req:load_dispatch_data(Bindings,HostTokens,Port,PathTokens,
+                                   AppRoot,StringPath,Req),
 	    Req:set_metadata('resource_module', Mod),
             webmachine_decision_core:handle_request(Req, Resource)
     end.
 get_option(Option, Options) ->
     {proplists:get_value(Option, Options), proplists:delete(Option, Options)}.
 
+host_headers(Req) ->
+    [ V || V <- [Req:get_header_value(H)
+                 || H <- ["x-forwarded-for",
+                          "x-forwarded-host",
+                          "x-forwarded-server",
+                          "host"]],
+           V /= undefined].

src/webmachine_request.erl

 	 get_metadata/1,
 	 get_path_info/0,
 	 get_path_info/1,
-	 load_dispatch_data/5,
+	 load_dispatch_data/7,
 	 get_path_tokens/0,
 	 get_app_root/0,
 	 parse_cookie/0,
 app_root() -> call(app_root).
 get_app_root() -> app_root().
 
-load_dispatch_data(Bindings, PathTokens, AppRoot, DispPath, Req) ->
-    call({load_dispatch_data, Bindings, PathTokens, AppRoot, DispPath, Req}).
+load_dispatch_data(Bindings, HostTokens, Port, PathTokens,
+                   AppRoot, DispPath, Req) ->
+    call({load_dispatch_data, Bindings, HostTokens, Port,
+          PathTokens, AppRoot, DispPath, Req}).
 
 log_data() -> call(log_data).

src/webmachine_request_srv.erl

     {reply, wrq:req_cookie(State#state.reqdata), State};
 handle_call(req_qs, _From, State) ->
     {reply, wrq:req_qs(State#state.reqdata), State};
-handle_call({load_dispatch_data, PathProps,PathTokens,AppRoot,DispPath,WMReq},
+handle_call({load_dispatch_data, PathProps,HostTokens,Port,
+             PathTokens,AppRoot,DispPath,WMReq},
             _From, State) ->
     PathInfo = dict:from_list(PathProps),
     NewState = State#state{reqdata=wrq:load_dispatch_data(
-               PathInfo,PathTokens,AppRoot,DispPath,WMReq,State#state.reqdata)},
+               PathInfo,HostTokens,Port,PathTokens,AppRoot,
+               DispPath,WMReq,State#state.reqdata)},
     {reply, ok, NewState};
 handle_call(log_data, _From, State) -> {reply, State#state.log_data, State}.
 
 -module(wrq).
 -author('Justin Sheehy <justin@basho.com>').
 
--export([create/4,load_dispatch_data/6]).
+-export([create/4,load_dispatch_data/8]).
 -export([method/1,version/1,peer/1,disp_path/1,path/1,raw_path/1,path_info/1,
          response_code/1,req_cookie/1,req_qs/1,req_headers/1,req_body/1,
          stream_req_body/2,resp_redirect/1,resp_headers/1,resp_body/1,
-        app_root/1,path_tokens/1]).
+         app_root/1,path_tokens/1, host_tokens/1, port/1]).
 -export([path_info/2,get_req_header/2,do_redirect/2,fresh_resp_headers/2,
          get_resp_header/2,set_resp_header/3,set_resp_headers/2,
          set_disp_path/2,set_req_body/2,set_resp_body/2,set_response_code/2,
     {_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath),
     ReqQS = mochiweb_util:parse_qs(QueryString),
     RD#wm_reqdata{path=Path,req_cookie=Cookie,req_qs=ReqQS}.
-load_dispatch_data(PathInfo, PathTokens, AppRoot, DispPath, WMReq, RD) ->
-    RD#wm_reqdata{path_info=PathInfo,path_tokens=PathTokens,
-                 app_root=AppRoot,disp_path=DispPath,wmreq=WMReq}.
+load_dispatch_data(PathInfo, HostTokens, Port, PathTokens, AppRoot,
+                   DispPath, WMReq, RD) ->
+    RD#wm_reqdata{path_info=PathInfo,host_tokens=HostTokens,
+                  port=Port,path_tokens=PathTokens,
+                  app_root=AppRoot,disp_path=DispPath,wmreq=WMReq}.
 
 method(_RD = #wm_reqdata{method=Method}) -> Method.
 
 
 path_tokens(_RD = #wm_reqdata{path_tokens=PathT}) -> PathT. % list of strings
 
+host_tokens(_RD = #wm_reqdata{host_tokens=HostT}) -> HostT. % list of strings
+
+port(_RD = #wm_reqdata{port=Port}) -> Port. % integer
+
 response_code(_RD = #wm_reqdata{response_code=C}) when is_integer(C) -> C.
 
 req_cookie(_RD = #wm_reqdata{req_cookie=C}) when is_list(C) -> C. % string
 
 path_info(Key, RD) when is_atom(Key) ->
     case dict:find(Key, path_info(RD)) of
-        {ok, Value} when is_list(Value) -> Value; % string
+        {ok, Value} when is_list(Value); is_integer(Value) ->
+            Value; % string (for host or path match)
+                   % or integer (for port match)
         error -> undefined
     end.
 
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.