From 6b3f2283270db8dab3b54a827ac90ad181660037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Chmielowski?= Date: Fri, 14 Sep 2012 17:51:54 +0200 Subject: [PATCH] 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. --- src/web/ejabberd_http.erl | 135 +++++++++++-------------------- src/web/ejabberd_http.hrl | 5 +- src/web/ejabberd_http_ws.erl | 7 +- src/web/ejabberd_http_wsjson.erl | 7 +- src/web/ejabberd_websocket.erl | 122 +++++++++++++++++++++++++--- 5 files changed, 170 insertions(+), 106 deletions(-) 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,