25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-09-29 14:37:44 +02:00

Unify paths for handling websocket and regular http requests

This allow to easily produce html output from error paths in websocket code,
and this ability is used to produce informational page when regular http
request is directed to websocket url. Additionally HEAD and OPTIONS request
are now handled correctly.
This commit is contained in:
Paweł Chmielowski 2012-09-14 17:51:54 +02:00
parent e58e6a09dd
commit 6b3f228327
5 changed files with 170 additions and 106 deletions

View File

@ -332,61 +332,36 @@ get_transfer_protocol(SockMod, HostPort) ->
%% matches the requested URL path, and pass control to it. If none is
%% found, answer with HTTP 404.
process([], _) -> ejabberd_web:error(not_found);
process(Handlers, #ws{} = Ws) ->
[{HandlerPathPrefix, HandlerModule, HandlerOpts} | HandlersLeft] =
Handlers,
case lists:prefix(HandlerPathPrefix, Ws#ws.path) or
(HandlerPathPrefix == Ws#ws.path)
of
true ->
?DEBUG("~p matches ~p",
[Ws#ws.path, HandlerPathPrefix]),
LocalPath = lists:nthtail(length(HandlerPathPrefix),
Ws#ws.path),
ejabberd_hooks:run(ws_debug, [{LocalPath, Ws}]),
Protocol = case lists:keysearch(protocol, 1,
HandlerOpts)
of
{value, {protocol, P}} -> P;
false -> undefined
end,
Origins = case lists:keysearch(origins, 1, HandlerOpts)
of
{value, {origins, O}} -> O;
false -> []
end,
Auth = case lists:keysearch(auth, 1, HandlerOpts) of
{value, {auth, A}} -> A;
false -> undefined
end,
WS2 = Ws#ws{local_path = LocalPath, protocol = Protocol,
acceptable_origins = Origins, auth_module = Auth},
case ejabberd_websocket:is_acceptable(WS2) of
true -> ejabberd_websocket:connect(WS2, HandlerModule);
false -> process(HandlersLeft, Ws)
end;
false -> process(HandlersLeft, Ws)
end;
process(Handlers, Request) ->
%% Only the first element in the path prefix is checked
[{HandlerPathPrefix, HandlerModule} | HandlersLeft] =
Handlers,
case lists:prefix(HandlerPathPrefix,
Request#request.path)
or (HandlerPathPrefix == Request#request.path)
of
true ->
?DEBUG("~p matches ~p",
[Request#request.path, HandlerPathPrefix]),
LocalPath = lists:nthtail(length(HandlerPathPrefix),
Request#request.path),
?DEBUG("~p", [Request#request.headers]),
R = HandlerModule:process(LocalPath, Request),
ejabberd_hooks:run(http_request_debug,
[{LocalPath, Request}]),
R;
false -> process(HandlersLeft, Request)
process([], _, _, _) -> ejabberd_web:error(not_found);
process(Handlers, Request, Socket, SockMod) ->
{HandlerPathPrefix, HandlerModule, HandlerOpts, HandlersLeft} =
case Handlers of
[{Pfx, Mod} | Tail] ->
{Pfx, Mod, [], Tail};
[{Pfx, Mod, Opts} | Tail] ->
{Pfx, Mod, Opts, Tail}
end,
case (lists:prefix(HandlerPathPrefix, Request#request.path) or
(HandlerPathPrefix==Request#request.path)) of
true ->
?DEBUG("~p matches ~p", [Request#request.path, HandlerPathPrefix]),
%% LocalPath is the path "local to the handler", i.e. if
%% the handler was registered to handle "/test/" and the
%% requested path is "/test/foo/bar", the local path is
%% ["foo", "bar"]
LocalPath = lists:nthtail(length(HandlerPathPrefix), Request#request.path),
code:ensure_loaded(HandlerModule),
R = case erlang:function_exported(HandlerModule, socket_handoff, 5) of
false ->
HandlerModule:process(LocalPath, Request);
_ ->
HandlerModule:socket_handoff(LocalPath, Request, Socket, SockMod, HandlerOpts)
end,
ejabberd_hooks:run(http_request_debug, [{LocalPath, Request}]),
R;
false ->
process(HandlersLeft, Request, Socket, SockMod)
end.
extract_path_query(#state{request_method = Method,
@ -433,7 +408,6 @@ extract_path_query(_State) ->
false.
process_request(#state{request_method = Method,
request_path = {abs_path, Path},
request_auth = Auth,
request_lang = Lang,
sockmod = SockMod,
@ -457,38 +431,18 @@ process_request(#state{request_method = Method,
end,
XFF = proplists:get_value('X-Forwarded-For', RequestHeaders, []),
IP = analyze_ip_xff(IPHere, XFF, Host),
case Method=:='GET' andalso ejabberd_websocket:check(Path, RequestHeaders) of
{true, VSN} ->
{_, Origin} = case lists:keyfind(<<"Sec-Websocket-Origin">>, 1, RequestHeaders) of
false -> lists:keyfind(<<"Origin">>, 1, RequestHeaders);
Value -> Value
end,
Ws = #ws{socket = Socket,
sockmod = SockMod,
ws_autoexit = false,
ip = IP,
path = LPath,
q = LQuery,
vsn = VSN,
host = Host,
port = Port,
origin = Origin,
headers = RequestHeaders
},
process(WebSocketHandlers, Ws);
false ->
Request = #request{method = Method,
path = LPath,
q = LQuery,
auth = Auth,
data = Data,
lang = Lang,
host = Host,
port = Port,
tp = TP,
headers = RequestHeaders,
ip = IP},
case process(RequestHandlers, Request) of
Request = #request{method = Method,
path = LPath,
q = LQuery,
auth = Auth,
data = Data,
lang = Lang,
host = Host,
port = Port,
tp = TP,
headers = RequestHeaders,
ip = IP},
case process(RequestHandlers ++ WebSocketHandlers, Request, Socket, SockMod) of
El when is_record(El, xmlel) ->
make_xhtml_output(State, 200, [], El);
{Status, Headers, El}
@ -501,8 +455,9 @@ process_request(#state{request_method = Method,
make_text_output(State, Status, Headers, Output);
{Status, Reason, Headers, Output}
when is_binary(Output) or is_list(Output) ->
make_text_output(State, Status, Reason, Headers, Output)
end
make_text_output(State, Status, Reason, Headers, Output);
_ ->
none
end
end;
process_request(State) -> make_bad_request(State).

View File

@ -46,10 +46,7 @@
path = [] :: [binary()],
headers = [] :: [{atom() | binary(), binary()}],
local_path = [] :: [binary()],
q = [] :: [{binary() | nokey, binary()}],
protocol :: binary(),
acceptable_origins = [] :: [binary()],
auth_module :: atom()}).
q = [] :: [{binary() | nokey, binary()}]}).
-type method() :: 'GET' | 'HEAD' | 'DELETE' | 'OPTIONS' | 'PUT' | 'POST' | 'TRACE'.
-type protocol() :: http | https.

View File

@ -33,7 +33,8 @@
-export([start/1, start_link/1, init/1, handle_event/3,
handle_sync_event/4, code_change/4, handle_info/3,
terminate/3, send/2, setopts/2, sockname/1, peername/1,
controlling_process/2, become_controller/2, close/1]).
controlling_process/2, become_controller/2, close/1,
socket_handoff/5]).
-include("ejabberd.hrl").
@ -98,6 +99,10 @@ become_controller(FsmRef, C2SPid) ->
close({http_ws, FsmRef, _IP}) ->
catch gen_fsm:sync_send_all_state_event(FsmRef, close).
socket_handoff(LocalPath, Request, Socket, SockMod, Opts) ->
ejabberd_websocket:socket_handoff(LocalPath, Request, Socket, SockMod,
Opts, ?MODULE).
%%% Internal
init([WS]) ->

View File

@ -35,7 +35,8 @@
handle_sync_event/4, code_change/4, handle_info/3,
terminate/3, send_xml/2, setopts/2, sockname/1,
peername/1, controlling_process/2, become_controller/2,
close/1]).
close/1,
socket_handoff/5]).
-include("ejabberd.hrl").
@ -97,6 +98,10 @@ become_controller(FsmRef, C2SPid) ->
close({http_ws, FsmRef, _IP}) ->
catch gen_fsm:sync_send_all_state_event(FsmRef, close).
socket_handoff(LocalPath, Request, Socket, SockMod, Opts) ->
ejabberd_websocket:socket_handoff(LocalPath, Request, Socket, SockMod,
Opts, ?MODULE).
%%% Internal
init([WS]) ->

View File

@ -40,7 +40,7 @@
-author('ecestari@process-one.net').
-export([connect/2, check/2, is_acceptable/1]).
-export([connect/2, check/2, socket_handoff/6]).
-include("ejabberd.hrl").
@ -48,14 +48,25 @@
-include("ejabberd_http.hrl").
-define(CT_XML, {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}).
-define(CT_PLAIN, {<<"Content-Type">>, <<"text/plain">>}).
-define(AC_ALLOW_ORIGIN, {<<"Access-Control-Allow-Origin">>, <<"*">>}).
-define(AC_ALLOW_METHODS, {<<"Access-Control-Allow-Methods">>, <<"GET, OPTIONS">>}).
-define(AC_ALLOW_HEADERS, {<<"Access-Control-Allow-Headers">>, <<"Content-Type">>}).
-define(AC_MAX_AGE, {<<"Access-Control-Max-Age">>, <<"86400">>}).
-define(OPTIONS_HEADER, [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS,
?AC_ALLOW_HEADERS, ?AC_MAX_AGE]).
-define(HEADER, [?CT_XML, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]).
check(_Path, Headers) ->
VsnSupported = [{'draft-hybi', 8}, {'draft-hybi', 13},
{'draft-hixie', 0}, {'draft-hixie', 68}],
check_websockets(VsnSupported, Headers).
is_acceptable(#ws{origin = Origin, protocol = Protocol,
headers = Headers, acceptable_origins = Origins,
auth_module = undefined}) ->
is_acceptable(_LocalPath, Origin, _IP, _Q, Headers, Protocol, Origins,
undefined) ->
ClientProtocol = find_subprotocol(Headers),
case {(Origins == []) or lists:member(Origin, Origins),
ClientProtocol, Protocol}
@ -68,13 +79,104 @@ is_acceptable(#ws{origin = Origin, protocol = Protocol,
{_, false, _} -> true;
{_, P, P} -> true;
_ = E ->
?INFO_MSG("Wrong protocol requested : ~p", [E]), false
?INFO_MSG("Wrong protocol requested : ~p", [E]),
false
end;
is_acceptable(#ws{local_path = LocalPath,
origin = Origin, ip = IP, q = Q, protocol = Protocol,
headers = Headers, auth_module = Module}) ->
Module:is_acceptable(LocalPath, Q, Origin, Protocol, IP,
Headers).
is_acceptable(LocalPath, Origin, IP, Q, Headers, Protocol, _Origins,
AuthModule) ->
AuthModule:is_acceptable(LocalPath, Q, Origin, Protocol, IP, Headers).
socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path,
headers = Headers, host = Host, port = Port},
Socket, SockMod, Opts, HandlerModule) ->
{_, Origin} = case lists:keyfind(<<"Sec-Websocket-Origin">>, 1, Headers) of
false ->
case lists:keyfind(<<"Origin">>, 1, Headers) of
false -> {value, undefined};
Value2 -> Value2
end;
Value -> Value
end,
case Origin of
undefined ->
{200, ?HEADER, get_human_html_xmlel()};
_ ->
Protocol = case lists:keysearch(protocol, 1, Opts) of
{value, {protocol, P}} -> P;
false -> undefined
end,
Origins = case lists:keysearch(origins, 1, Opts) of
{value, {origins, O}} -> O;
false -> []
end,
Auth = case lists:keysearch(auth, 1, Opts) of
{value, {auth, A}} -> A;
false -> undefined
end,
case is_acceptable(LocalPath, Origin, IP, Q, Headers, Protocol, Origins, Auth) of
true ->
case check(LocalPath, Headers) of
{true, Vsn} ->
WS = #ws{vsn = Vsn,
socket = Socket,
sockmod = SockMod,
ws_autoexit = false,
ip = IP,
origin = Origin,
q = Q,
host = Host,
port = Port,
path = Path,
headers = Headers,
local_path = LocalPath},
connect(WS, HandlerModule);
_ ->
{200, ?HEADER, get_human_html_xmlel()}
end;
_ ->
{403, ?HEADER, #xmlel{name = <<"h1">>,
children = [{xmlcdata, <<"403 Forbiden">>}]}}
end
end;
socket_handoff(_, #request{method = 'OPTIONS'}, _, _, _, _) ->
{200, ?OPTIONS_HEADER, []};
socket_handoff(_, #request{method = 'HEAD'}, _, _, _, _) ->
{200, ?HEADER, []};
socket_handoff(_, _, _, _, _, _) ->
{400, ?HEADER, #xmlel{name = <<"h1">>,
children = [{xmlcdata, <<"400 Bad Request">>}]}}.
get_human_html_xmlel() ->
Heading = <<"ejabberd ", (jlib:atom_to_binary(?MODULE))/binary>>,
#xmlel{name = <<"html">>,
attrs =
[{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}],
children =
[#xmlel{name = <<"head">>, attrs = [],
children =
[#xmlel{name = <<"title">>, attrs = [],
children = [{xmlcdata, Heading}]}]},
#xmlel{name = <<"body">>, attrs = [],
children =
[#xmlel{name = <<"h1">>, attrs = [],
children = [{xmlcdata, Heading}]},
#xmlel{name = <<"p">>, attrs = [],
children =
[{xmlcdata, <<"An implementation of ">>},
#xmlel{name = <<"a">>,
attrs =
[{<<"href">>,
<<"http://tools.ietf.org/html/rfc6455">>}],
children =
[{xmlcdata,
<<"WebSocket protocol">>}]}]},
#xmlel{name = <<"p">>, attrs = [],
children =
[{xmlcdata,
<<"This web page is only informative. To "
"use WebSocket connection you need a Jabber/XMPP "
"client that supports it.">>}]}]}]}.
connect(#ws{vsn = Vsn, socket = Socket, q = Q,
origin = Origin, host = Host, port = Port,