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
|
%% matches the requested URL path, and pass control to it. If none is
|
||||||
%% found, answer with HTTP 404.
|
%% found, answer with HTTP 404.
|
||||||
|
|
||||||
process([], _) -> ejabberd_web:error(not_found);
|
process([], _, _, _) -> ejabberd_web:error(not_found);
|
||||||
process(Handlers, #ws{} = Ws) ->
|
process(Handlers, Request, Socket, SockMod) ->
|
||||||
[{HandlerPathPrefix, HandlerModule, HandlerOpts} | HandlersLeft] =
|
{HandlerPathPrefix, HandlerModule, HandlerOpts, HandlersLeft} =
|
||||||
Handlers,
|
case Handlers of
|
||||||
case lists:prefix(HandlerPathPrefix, Ws#ws.path) or
|
[{Pfx, Mod} | Tail] ->
|
||||||
(HandlerPathPrefix == Ws#ws.path)
|
{Pfx, Mod, [], Tail};
|
||||||
of
|
[{Pfx, Mod, Opts} | Tail] ->
|
||||||
true ->
|
{Pfx, Mod, Opts, Tail}
|
||||||
?DEBUG("~p matches ~p",
|
end,
|
||||||
[Ws#ws.path, HandlerPathPrefix]),
|
|
||||||
LocalPath = lists:nthtail(length(HandlerPathPrefix),
|
case (lists:prefix(HandlerPathPrefix, Request#request.path) or
|
||||||
Ws#ws.path),
|
(HandlerPathPrefix==Request#request.path)) of
|
||||||
ejabberd_hooks:run(ws_debug, [{LocalPath, Ws}]),
|
true ->
|
||||||
Protocol = case lists:keysearch(protocol, 1,
|
?DEBUG("~p matches ~p", [Request#request.path, HandlerPathPrefix]),
|
||||||
HandlerOpts)
|
%% LocalPath is the path "local to the handler", i.e. if
|
||||||
of
|
%% the handler was registered to handle "/test/" and the
|
||||||
{value, {protocol, P}} -> P;
|
%% requested path is "/test/foo/bar", the local path is
|
||||||
false -> undefined
|
%% ["foo", "bar"]
|
||||||
end,
|
LocalPath = lists:nthtail(length(HandlerPathPrefix), Request#request.path),
|
||||||
Origins = case lists:keysearch(origins, 1, HandlerOpts)
|
code:ensure_loaded(HandlerModule),
|
||||||
of
|
R = case erlang:function_exported(HandlerModule, socket_handoff, 5) of
|
||||||
{value, {origins, O}} -> O;
|
false ->
|
||||||
false -> []
|
HandlerModule:process(LocalPath, Request);
|
||||||
end,
|
_ ->
|
||||||
Auth = case lists:keysearch(auth, 1, HandlerOpts) of
|
HandlerModule:socket_handoff(LocalPath, Request, Socket, SockMod, HandlerOpts)
|
||||||
{value, {auth, A}} -> A;
|
end,
|
||||||
false -> undefined
|
ejabberd_hooks:run(http_request_debug, [{LocalPath, Request}]),
|
||||||
end,
|
R;
|
||||||
WS2 = Ws#ws{local_path = LocalPath, protocol = Protocol,
|
false ->
|
||||||
acceptable_origins = Origins, auth_module = Auth},
|
process(HandlersLeft, Request, Socket, SockMod)
|
||||||
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)
|
|
||||||
end.
|
end.
|
||||||
|
|
||||||
extract_path_query(#state{request_method = Method,
|
extract_path_query(#state{request_method = Method,
|
||||||
@ -433,7 +408,6 @@ extract_path_query(_State) ->
|
|||||||
false.
|
false.
|
||||||
|
|
||||||
process_request(#state{request_method = Method,
|
process_request(#state{request_method = Method,
|
||||||
request_path = {abs_path, Path},
|
|
||||||
request_auth = Auth,
|
request_auth = Auth,
|
||||||
request_lang = Lang,
|
request_lang = Lang,
|
||||||
sockmod = SockMod,
|
sockmod = SockMod,
|
||||||
@ -457,38 +431,18 @@ process_request(#state{request_method = Method,
|
|||||||
end,
|
end,
|
||||||
XFF = proplists:get_value('X-Forwarded-For', RequestHeaders, []),
|
XFF = proplists:get_value('X-Forwarded-For', RequestHeaders, []),
|
||||||
IP = analyze_ip_xff(IPHere, XFF, Host),
|
IP = analyze_ip_xff(IPHere, XFF, Host),
|
||||||
case Method=:='GET' andalso ejabberd_websocket:check(Path, RequestHeaders) of
|
Request = #request{method = Method,
|
||||||
{true, VSN} ->
|
path = LPath,
|
||||||
{_, Origin} = case lists:keyfind(<<"Sec-Websocket-Origin">>, 1, RequestHeaders) of
|
q = LQuery,
|
||||||
false -> lists:keyfind(<<"Origin">>, 1, RequestHeaders);
|
auth = Auth,
|
||||||
Value -> Value
|
data = Data,
|
||||||
end,
|
lang = Lang,
|
||||||
Ws = #ws{socket = Socket,
|
host = Host,
|
||||||
sockmod = SockMod,
|
port = Port,
|
||||||
ws_autoexit = false,
|
tp = TP,
|
||||||
ip = IP,
|
headers = RequestHeaders,
|
||||||
path = LPath,
|
ip = IP},
|
||||||
q = LQuery,
|
case process(RequestHandlers ++ WebSocketHandlers, Request, Socket, SockMod) of
|
||||||
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
|
|
||||||
El when is_record(El, xmlel) ->
|
El when is_record(El, xmlel) ->
|
||||||
make_xhtml_output(State, 200, [], El);
|
make_xhtml_output(State, 200, [], El);
|
||||||
{Status, Headers, El}
|
{Status, Headers, El}
|
||||||
@ -501,8 +455,9 @@ process_request(#state{request_method = Method,
|
|||||||
make_text_output(State, Status, Headers, Output);
|
make_text_output(State, Status, Headers, Output);
|
||||||
{Status, Reason, Headers, Output}
|
{Status, Reason, Headers, Output}
|
||||||
when is_binary(Output) or is_list(Output) ->
|
when is_binary(Output) or is_list(Output) ->
|
||||||
make_text_output(State, Status, Reason, Headers, Output)
|
make_text_output(State, Status, Reason, Headers, Output);
|
||||||
end
|
_ ->
|
||||||
|
none
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
process_request(State) -> make_bad_request(State).
|
process_request(State) -> make_bad_request(State).
|
||||||
|
@ -46,10 +46,7 @@
|
|||||||
path = [] :: [binary()],
|
path = [] :: [binary()],
|
||||||
headers = [] :: [{atom() | binary(), binary()}],
|
headers = [] :: [{atom() | binary(), binary()}],
|
||||||
local_path = [] :: [binary()],
|
local_path = [] :: [binary()],
|
||||||
q = [] :: [{binary() | nokey, binary()}],
|
q = [] :: [{binary() | nokey, binary()}]}).
|
||||||
protocol :: binary(),
|
|
||||||
acceptable_origins = [] :: [binary()],
|
|
||||||
auth_module :: atom()}).
|
|
||||||
|
|
||||||
-type method() :: 'GET' | 'HEAD' | 'DELETE' | 'OPTIONS' | 'PUT' | 'POST' | 'TRACE'.
|
-type method() :: 'GET' | 'HEAD' | 'DELETE' | 'OPTIONS' | 'PUT' | 'POST' | 'TRACE'.
|
||||||
-type protocol() :: http | https.
|
-type protocol() :: http | https.
|
||||||
|
@ -33,7 +33,8 @@
|
|||||||
-export([start/1, start_link/1, init/1, handle_event/3,
|
-export([start/1, start_link/1, init/1, handle_event/3,
|
||||||
handle_sync_event/4, code_change/4, handle_info/3,
|
handle_sync_event/4, code_change/4, handle_info/3,
|
||||||
terminate/3, send/2, setopts/2, sockname/1, peername/1,
|
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").
|
-include("ejabberd.hrl").
|
||||||
|
|
||||||
@ -98,6 +99,10 @@ become_controller(FsmRef, C2SPid) ->
|
|||||||
close({http_ws, FsmRef, _IP}) ->
|
close({http_ws, FsmRef, _IP}) ->
|
||||||
catch gen_fsm:sync_send_all_state_event(FsmRef, close).
|
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
|
%%% Internal
|
||||||
|
|
||||||
init([WS]) ->
|
init([WS]) ->
|
||||||
|
@ -35,7 +35,8 @@
|
|||||||
handle_sync_event/4, code_change/4, handle_info/3,
|
handle_sync_event/4, code_change/4, handle_info/3,
|
||||||
terminate/3, send_xml/2, setopts/2, sockname/1,
|
terminate/3, send_xml/2, setopts/2, sockname/1,
|
||||||
peername/1, controlling_process/2, become_controller/2,
|
peername/1, controlling_process/2, become_controller/2,
|
||||||
close/1]).
|
close/1,
|
||||||
|
socket_handoff/5]).
|
||||||
|
|
||||||
-include("ejabberd.hrl").
|
-include("ejabberd.hrl").
|
||||||
|
|
||||||
@ -97,6 +98,10 @@ become_controller(FsmRef, C2SPid) ->
|
|||||||
close({http_ws, FsmRef, _IP}) ->
|
close({http_ws, FsmRef, _IP}) ->
|
||||||
catch gen_fsm:sync_send_all_state_event(FsmRef, close).
|
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
|
%%% Internal
|
||||||
|
|
||||||
init([WS]) ->
|
init([WS]) ->
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
-author('ecestari@process-one.net').
|
-author('ecestari@process-one.net').
|
||||||
|
|
||||||
-export([connect/2, check/2, is_acceptable/1]).
|
-export([connect/2, check/2, socket_handoff/6]).
|
||||||
|
|
||||||
-include("ejabberd.hrl").
|
-include("ejabberd.hrl").
|
||||||
|
|
||||||
@ -48,14 +48,25 @@
|
|||||||
|
|
||||||
-include("ejabberd_http.hrl").
|
-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) ->
|
check(_Path, Headers) ->
|
||||||
VsnSupported = [{'draft-hybi', 8}, {'draft-hybi', 13},
|
VsnSupported = [{'draft-hybi', 8}, {'draft-hybi', 13},
|
||||||
{'draft-hixie', 0}, {'draft-hixie', 68}],
|
{'draft-hixie', 0}, {'draft-hixie', 68}],
|
||||||
check_websockets(VsnSupported, Headers).
|
check_websockets(VsnSupported, Headers).
|
||||||
|
|
||||||
is_acceptable(#ws{origin = Origin, protocol = Protocol,
|
is_acceptable(_LocalPath, Origin, _IP, _Q, Headers, Protocol, Origins,
|
||||||
headers = Headers, acceptable_origins = Origins,
|
undefined) ->
|
||||||
auth_module = undefined}) ->
|
|
||||||
ClientProtocol = find_subprotocol(Headers),
|
ClientProtocol = find_subprotocol(Headers),
|
||||||
case {(Origins == []) or lists:member(Origin, Origins),
|
case {(Origins == []) or lists:member(Origin, Origins),
|
||||||
ClientProtocol, Protocol}
|
ClientProtocol, Protocol}
|
||||||
@ -68,13 +79,104 @@ is_acceptable(#ws{origin = Origin, protocol = Protocol,
|
|||||||
{_, false, _} -> true;
|
{_, false, _} -> true;
|
||||||
{_, P, P} -> true;
|
{_, P, P} -> true;
|
||||||
_ = E ->
|
_ = E ->
|
||||||
?INFO_MSG("Wrong protocol requested : ~p", [E]), false
|
?INFO_MSG("Wrong protocol requested : ~p", [E]),
|
||||||
|
false
|
||||||
end;
|
end;
|
||||||
is_acceptable(#ws{local_path = LocalPath,
|
is_acceptable(LocalPath, Origin, IP, Q, Headers, Protocol, _Origins,
|
||||||
origin = Origin, ip = IP, q = Q, protocol = Protocol,
|
AuthModule) ->
|
||||||
headers = Headers, auth_module = Module}) ->
|
AuthModule:is_acceptable(LocalPath, Q, Origin, Protocol, IP, Headers).
|
||||||
Module: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,
|
connect(#ws{vsn = Vsn, socket = Socket, q = Q,
|
||||||
origin = Origin, host = Host, port = Port,
|
origin = Origin, host = Host, port = Port,
|
||||||
|
Loading…
Reference in New Issue
Block a user