basho / webmachine (http://webmachine.basho.com/)
A REST-based system for building web applications.
| commit 82: | ab37abfaf7ad |
| parent 81: | 15cd6ed98abc |
| branch: | default |
5 months ago
Changed (Δ4.7 KB):
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 |
|
|
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_ |
|
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_ |
|
179 |
try_path_binding(Rest, PathTokens, Bindings, ExtraDepth) |
|
92 |
180 |
end. |
93 |
181 |
|
94 |
bind |
|
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 |
|
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 |
|
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, |
|
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, |
|
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/ |
|
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, |
|
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, |
|
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 |
16 |
16 |
-module(wrq). |
17 |
17 |
-author('Justin Sheehy <justin@basho.com>'). |
18 |
18 |
|
19 |
-export([create/4,load_dispatch_data/ |
|
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 |
|
|
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) |
|
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 |
