diff --git a/src/web/ejabberd_http.erl b/src/web/ejabberd_http.erl index a9dbf7a44..f581c2060 100644 --- a/src/web/ejabberd_http.erl +++ b/src/web/ejabberd_http.erl @@ -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). diff --git a/src/web/ejabberd_http.hrl b/src/web/ejabberd_http.hrl index 51864f447..799f3ce15 100644 --- a/src/web/ejabberd_http.hrl +++ b/src/web/ejabberd_http.hrl @@ -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. diff --git a/src/web/ejabberd_http_ws.erl b/src/web/ejabberd_http_ws.erl index ec3bd4a09..521612d40 100644 --- a/src/web/ejabberd_http_ws.erl +++ b/src/web/ejabberd_http_ws.erl @@ -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]) -> diff --git a/src/web/ejabberd_http_wsjson.erl b/src/web/ejabberd_http_wsjson.erl index 1881daf22..ff14c96f4 100644 --- a/src/web/ejabberd_http_wsjson.erl +++ b/src/web/ejabberd_http_wsjson.erl @@ -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]) -> diff --git a/src/web/ejabberd_websocket.erl b/src/web/ejabberd_websocket.erl index 78b720b0f..17586701b 100644 --- a/src/web/ejabberd_websocket.erl +++ b/src/web/ejabberd_websocket.erl @@ -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,