basho / webmachine (http://webmachine.basho.com/)

A REST-based system for building web applications.

Clone this repository (size: 1.7 MB): HTTPS / SSH
$ hg clone http://hg.basho.com/webmachine/
commit 82: ab37abfaf7ad
parent 81: 15cd6ed98abc
branch: default
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.
Bryan Fink
5 months ago

Changed (Δ4.7 KB):

raw changeset »

include/wm_reqdata.hrl (2 lines added, 1 lines removed)

src/webmachine_dispatcher.erl (106 lines added, 18 lines removed)

src/webmachine_mochiweb.erl (17 lines added, 4 lines removed)

src/webmachine_request.erl (5 lines added, 3 lines removed)

src/webmachine_request_srv.erl (4 lines added, 2 lines removed)

src/wrq.erl (14 lines added, 6 lines removed)

Up to file-list include/wm_reqdata.hrl:

2
2
                     disp_path, path, raw_path, path_info, path_tokens,
3
3
                     app_root,response_code,max_recv_body,
4
4
                     req_cookie, req_qs, req_headers, req_body,
5
                     resp_redirect, resp_headers, resp_body
5
                     resp_redirect, resp_headers, resp_body,
6
                     host_tokens, port
6
7
                    }).
7
8

Up to file-list src/webmachine_dispatcher.erl:

19
19
-module(webmachine_dispatcher).
20
20
-author('Robert Ahrens <rahrens@basho.com>').
21
21
-author('Justin Sheehy <justin@basho.com>').
22
-author('Bryan Fink <bryan@basho.com>').
22
23
23
-export([dispatch/2]).
24
-export([dispatch/2, dispatch/3]).
24
25
25
26
-define(SEPARATOR, $\/).
26
27
-define(MATCH_ALL, '*').
30
31
%% @doc Interface for URL dispatching.
31
32
%% See also http://bitbucket.org/justin/webmachine/wiki/DispatchConfiguration
32
33
dispatch(PathAsString, DispatchList) ->
34
    dispatch([], PathAsString, DispatchList).
35
36
%% @spec dispatch(Host::string(), Path::string(),
37
%%                DispatchList::[matchterm()]) ->
38
%%         dispterm() | dispfail()
39
%% @doc Interface for URL dispatching.
40
%% See also http://bitbucket.org/justin/webmachine/wiki/DispatchConfiguration
41
dispatch(HostAsString, PathAsString, DispatchList) ->
33
42
    Path = string:tokens(PathAsString, [?SEPARATOR]),
34
43
    % URIs that end with a trailing slash are implicitly one token
35
44
    % "deeper" than we otherwise might think as we are "inside"
@@ -38,10 +47,54 @@ dispatch(PathAsString, DispatchList) ->
38
47
		     true -> 1;
39
48
		     _ -> 0
40
49
		 end,
41
    try_binding(DispatchList, Path, ExtraDepth).
50
    {Host, Port} = split_host_port(HostAsString),
51
    try_host_binding(DispatchList, lists:reverse(Host), Port,
52
                     Path, ExtraDepth).
42
53
43
%% @type matchterm() = {[pathterm()], matchmod(), matchopts()}.
54
split_host_port(HostAsString) ->
55
    case string:tokens(HostAsString, ":") of
56
        [HostPart, PortPart] ->
57
            {split_host(HostPart), list_to_integer(PortPart)};
58
        [HostPart] ->
59
            {split_host(HostPart), 80}
60
    end.
61
62
split_host(HostAsString) ->
63
    string:tokens(HostAsString, ".").
64
65
%% @type matchterm() = hostmatchterm() | pathmatchterm()
44
66
% The dispatch configuration is a list of these terms, and the
67
% first one whose host and path terms match the input is used.
68
% Using a pathmatchterm() here is equivalent to using a hostmatchterm()
69
% of the form {{['*'],'*'}, [pathmatchterm()]}.
70
71
%% @type hostmatchterm() = {hostmatch(), [pathmatchterm()]}
72
% The dispatch configuration contains a list of these terms, and the
73
% first one whose host and one pathmatchterm match is used.
74
75
%% @type hostmatch() = [hostterm()] | {[hostterm()], portterm()}
76
% A host header (Host, X-Forwarded-For, etc.) will be matched against
77
% this term.  Using a raws [hostterm()] list is equivalent to using
78
% {[hostterm()], '*'}.
79
80
%% @type hostterm() = '*' | string() | atom()
81
% A list of hostterms is matched against a '.'-separated hostname.
82
% The '*' hosterm matches all remaining tokens, and is only allowed at
83
% the head of the list.
84
% A string hostterm will match a token of exactly the same string.
85
% Any atom hostterm other than '*' will match any token and will
86
% create a binding in the result if a complete match occurs.
87
88
%% @type portterm() = '*' | integer() | atom()
89
% A portterm is matched against the integer port after any ':' in
90
% the hostname, or 80 if no port is found.
91
% The '*' portterm patches any port
92
% An integer portterm will match a port of exactly the same integer.
93
% Any atom portterm other than '*' will match any port and will
94
% create a binding in the result if a complete match occurs.
95
96
%% @type pathmatchterm() = {[pathterm()], matchmod(), matchopts()}.
97
% The dispatch configuration contains a list of these terms, and the
45
98
% first one whose list of pathterms matches the input path is used.
46
99
47
100
%% @type pathterm() = '*' | string() | atom().
@@ -80,28 +133,63 @@ dispatch(PathAsString, DispatchList) ->
80
133
81
134
%% @type dispfail() = {no_dispatch_match, pathtokens()}.
82
135
83
try_binding([], PathTokens, _) ->
136
try_host_binding([], Host, Port, Path, _Depth) ->
137
    {no_dispatch_match, {Host, Port}, Path};
138
try_host_binding([Dispatch|Rest], Host, Port, Path, Depth) ->
139
    {{HostSpec,PortSpec},PathSpec} =
140
        case Dispatch of
141
            {{H,P},S} -> {{H,P},S};
142
            {H,S}     -> {{H,?MATCH_ALL},S};
143
            S         -> {{[?MATCH_ALL],?MATCH_ALL},[S]}
144
        end,
145
    case bind_port(PortSpec, Port, []) of
146
        {ok, PortBindings} ->
147
            case bind(lists:reverse(HostSpec), Host, PortBindings, 0) of
148
                {ok, HostRemainder, HostBindings, _} ->
149
                    case try_path_binding(PathSpec, Path, HostBindings, Depth) of
150
                        {Mod, Props, PathRemainder, PathBindings,
151
                         AppRoot, StringPath} ->
152
                            {Mod, Props, HostRemainder, Port, PathRemainder,
153
                             PathBindings, AppRoot, StringPath};
154
                        {no_dispatch_match, _} ->
155
                            try_host_binding(Rest, Host, Port, Path, Depth)
156
                    end;
157
                fail ->
158
                    try_host_binding(Rest, Host, Port, Path, Depth)
159
            end;
160
        fail ->
161
            try_host_binding(Rest, Host, Port, Path, Depth)
162
    end.
163
164
bind_port(Port, Port, Bindings) -> {ok, Bindings};
165
bind_port(?MATCH_ALL, _Port, Bindings) -> {ok, Bindings};
166
bind_port(PortAtom, Port, Bindings) when is_atom(PortAtom) ->
167
    {ok, [{PortAtom, Port}|Bindings]};
168
bind_port(_, _, _) -> fail.
169
170
try_path_binding([], PathTokens, _, _) ->
84
171
    {no_dispatch_match, PathTokens};
85
try_binding([{PathSchema, Mod, Props}|Rest], PathTokens, ExtraDepth) ->
86
    case bind_path(PathSchema, PathTokens, [], 0) of
87
        {ok, Remainder, Bindings, Depth} ->
88
            {Mod, Props, Remainder, Bindings,
172
try_path_binding([{PathSchema, Mod, Props}|Rest], PathTokens,
173
                 Bindings, ExtraDepth) ->
174
    case bind(PathSchema, PathTokens, Bindings, 0) of
175
        {ok, Remainder, NewBindings, Depth} ->
176
            {Mod, Props, Remainder, NewBindings,
89
177
             calculate_app_root(Depth + ExtraDepth), reconstitute(Remainder)};
90
178
        fail -> 
91
            try_binding(Rest, PathTokens, ExtraDepth)
179
            try_path_binding(Rest, PathTokens, Bindings, ExtraDepth)
92
180
    end.
93
181
94
bind_path([], [], Bindings, Depth) ->
182
bind([], [], Bindings, Depth) ->
95
183
    {ok, [], Bindings, Depth};
96
bind_path([?MATCH_ALL], PathRest, Bindings, Depth) when is_list(PathRest) ->
97
    {ok, PathRest, Bindings, Depth + length(PathRest)};
98
bind_path(_, [], _, _) ->
184
bind([?MATCH_ALL], Rest, Bindings, Depth) when is_list(Rest) ->
185
    {ok, Rest, Bindings, Depth + length(Rest)};
186
bind(_, [], _, _) ->
99
187
    fail;
100
bind_path([Token|Rest],[Match|PathRest],Bindings,Depth) when is_atom(Token) ->
101
    bind_path(Rest, PathRest, [{Token, Match}|Bindings], Depth + 1);
102
bind_path([Token|Rest], [Token|PathRest], Bindings, Depth) ->
103
    bind_path(Rest, PathRest, Bindings, Depth + 1);
104
bind_path(_, _, _, _) ->
188
bind([Token|RestToken],[Match|RestMatch],Bindings,Depth) when is_atom(Token) ->
189
    bind(RestToken, RestMatch, [{Token, Match}|Bindings], Depth + 1);
190
bind([Token|RestToken], [Token|RestMatch], Bindings, Depth) ->
191
    bind(RestToken, RestMatch, Bindings, Depth + 1);
192
bind(_, _, _, _) ->
105
193
    fail.
106
194
107
195
reconstitute([]) -> "";

Up to file-list src/webmachine_mochiweb.erl:

@@ -49,8 +49,12 @@ stop() ->
49
49
loop(MochiReq) ->
50
50
    Req = webmachine:new_request(mochiweb, MochiReq),
51
51
    {ok, DispatchList} = application:get_env(webmachine, dispatch_list),
52
    case webmachine_dispatcher:dispatch(Req:path(), DispatchList) of
53
        {no_dispatch_match, _UnmatchedPathTokens} ->
52
    Host = case host_headers(Req) of
53
               [H|_] -> H;
54
               [] -> []
55
           end,
56
    case webmachine_dispatcher:dispatch(Host, Req:path(), DispatchList) of
57
        {no_dispatch_match, _UnmatchedHost, _UnmatchedPathTokens} ->
54
58
            {ok, ErrorHandler} = application:get_env(webmachine, error_handler),
55
59
	    ErrorHTML = ErrorHandler:render_error(404, Req, {none, none, []}),
56
60
	    Req:append_to_response_body(ErrorHTML),
@@ -63,10 +67,12 @@ loop(MochiReq) ->
63
67
		end,
64
68
	    spawn(LogModule, log_access, [LogData]),
65
69
	    Req:stop();
66
        {Mod, ModOpts, PathTokens, Bindings, AppRoot, StringPath} ->
70
        {Mod, ModOpts, HostTokens, Port, PathTokens, Bindings,
71
         AppRoot, StringPath} ->
67
72
            BootstrapResource = webmachine_resource:new(x,x,x,x),
68
73
            {ok, Resource} = BootstrapResource:wrap(Mod, ModOpts),
69
	    Req:load_dispatch_data(Bindings,PathTokens,AppRoot,StringPath,Req),
74
	    Req:load_dispatch_data(Bindings,HostTokens,Port,PathTokens,
75
                                   AppRoot,StringPath,Req),
70
76
	    Req:set_metadata('resource_module', Mod),
71
77
            webmachine_decision_core:handle_request(Req, Resource)
72
78
    end.
@@ -74,3 +80,10 @@ loop(MochiReq) ->
74
80
get_option(Option, Options) ->
75
81
    {proplists:get_value(Option, Options), proplists:delete(Option, Options)}.
76
82
83
host_headers(Req) ->
84
    [ V || V <- [Req:get_header_value(H)
85
                 || H <- ["x-forwarded-for",
86
                          "x-forwarded-host",
87
                          "x-forwarded-server",
88
                          "host"]],
89
           V /= undefined].

Up to file-list src/webmachine_request.erl:

58
58
	 get_metadata/1,
59
59
	 get_path_info/0,
60
60
	 get_path_info/1,
61
	 load_dispatch_data/5,
61
	 load_dispatch_data/7,
62
62
	 get_path_tokens/0,
63
63
	 get_app_root/0,
64
64
	 parse_cookie/0,
@@ -172,7 +172,9 @@ get_path_tokens() -> path_tokens().
172
172
app_root() -> call(app_root).
173
173
get_app_root() -> app_root().
174
174
175
load_dispatch_data(Bindings, PathTokens, AppRoot, DispPath, Req) ->
176
    call({load_dispatch_data, Bindings, PathTokens, AppRoot, DispPath, Req}).
175
load_dispatch_data(Bindings, HostTokens, Port, PathTokens,
176
                   AppRoot, DispPath, Req) ->
177
    call({load_dispatch_data, Bindings, HostTokens, Port,
178
          PathTokens, AppRoot, DispPath, Req}).
177
179
178
180
log_data() -> call(log_data).

Up to file-list src/webmachine_request_srv.erl:

@@ -202,11 +202,13 @@ handle_call(req_cookie, _From, State) ->
202
202
    {reply, wrq:req_cookie(State#state.reqdata), State};
203
203
handle_call(req_qs, _From, State) ->
204
204
    {reply, wrq:req_qs(State#state.reqdata), State};
205
handle_call({load_dispatch_data, PathProps,PathTokens,AppRoot,DispPath,WMReq},
205
handle_call({load_dispatch_data, PathProps,HostTokens,Port,
206
             PathTokens,AppRoot,DispPath,WMReq},
206
207
            _From, State) ->
207
208
    PathInfo = dict:from_list(PathProps),
208
209
    NewState = State#state{reqdata=wrq:load_dispatch_data(
209
               PathInfo,PathTokens,AppRoot,DispPath,WMReq,State#state.reqdata)},
210
               PathInfo,HostTokens,Port,PathTokens,AppRoot,
211
               DispPath,WMReq,State#state.reqdata)},
210
212
    {reply, ok, NewState};
211
213
handle_call(log_data, _From, State) -> {reply, State#state.log_data, State}.
212
214

Up to file-list src/wrq.erl:

16
16
-module(wrq).
17
17
-author('Justin Sheehy <justin@basho.com>').
18
18
19
-export([create/4,load_dispatch_data/6]).
19
-export([create/4,load_dispatch_data/8]).
20
20
-export([method/1,version/1,peer/1,disp_path/1,path/1,raw_path/1,path_info/1,
21
21
         response_code/1,req_cookie/1,req_qs/1,req_headers/1,req_body/1,
22
22
         stream_req_body/2,resp_redirect/1,resp_headers/1,resp_body/1,
23
        app_root/1,path_tokens/1]).
23
         app_root/1,path_tokens/1, host_tokens/1, port/1]).
24
24
-export([path_info/2,get_req_header/2,do_redirect/2,fresh_resp_headers/2,
25
25
         get_resp_header/2,set_resp_header/3,set_resp_headers/2,
26
26
         set_disp_path/2,set_req_body/2,set_resp_body/2,set_response_code/2,
@@ -56,9 +56,11 @@ create(RD = #wm_reqdata{raw_path=RawPath
56
56
    {_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath),
57
57
    ReqQS = mochiweb_util:parse_qs(QueryString),
58
58
    RD#wm_reqdata{path=Path,req_cookie=Cookie,req_qs=ReqQS}.
59
load_dispatch_data(PathInfo, PathTokens, AppRoot, DispPath, WMReq, RD) ->
60
    RD#wm_reqdata{path_info=PathInfo,path_tokens=PathTokens,
61
                 app_root=AppRoot,disp_path=DispPath,wmreq=WMReq}.
59
load_dispatch_data(PathInfo, HostTokens, Port, PathTokens, AppRoot,
60
                   DispPath, WMReq, RD) ->
61
    RD#wm_reqdata{path_info=PathInfo,host_tokens=HostTokens,
62
                  port=Port,path_tokens=PathTokens,
63
                  app_root=AppRoot,disp_path=DispPath,wmreq=WMReq}.
62
64
63
65
method(_RD = #wm_reqdata{method=Method}) -> Method.
64
66
@@ -81,6 +83,10 @@ path_info(_RD = #wm_reqdata{path_info=Pa
81
83
82
84
path_tokens(_RD = #wm_reqdata{path_tokens=PathT}) -> PathT. % list of strings
83
85
86
host_tokens(_RD = #wm_reqdata{host_tokens=HostT}) -> HostT. % list of strings
87
88
port(_RD = #wm_reqdata{port=Port}) -> Port. % integer
89
84
90
response_code(_RD = #wm_reqdata{response_code=C}) when is_integer(C) -> C.
85
91
86
92
req_cookie(_RD = #wm_reqdata{req_cookie=C}) when is_list(C) -> C. % string
@@ -123,7 +129,9 @@ resp_body(_RD = #wm_reqdata{resp_body=Re
123
129
124
130
path_info(Key, RD) when is_atom(Key) ->
125
131
    case dict:find(Key, path_info(RD)) of
126
        {ok, Value} when is_list(Value) -> Value; % string
132
        {ok, Value} when is_list(Value); is_integer(Value) ->
133
            Value; % string (for host or path match)
134
                   % or integer (for port match)
127
135
        error -> undefined
128
136
    end.
129
137