mirror of
https://github.com/processone/ejabberd.git
synced 2024-10-31 15:21:38 +01: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:
parent
e58e6a09dd
commit
6b3f228327
@ -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
|
||||
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",
|
||||
[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
|
||||
?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,
|
||||
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}]),
|
||||
ejabberd_hooks:run(http_request_debug, [{LocalPath, Request}]),
|
||||
R;
|
||||
false -> process(HandlersLeft, Request)
|
||||
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,26 +431,6 @@ 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,
|
||||
@ -488,7 +442,7 @@ process_request(#state{request_method = Method,
|
||||
tp = TP,
|
||||
headers = RequestHeaders,
|
||||
ip = IP},
|
||||
case process(RequestHandlers, Request) of
|
||||
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).
|
||||
|
@ -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.
|
||||
|
@ -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]) ->
|
||||
|
@ -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]) ->
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user