diff options
author | Paweł Chmielowski <pchmielowski@process-one.net> | 2012-09-14 17:51:54 +0200 |
---|---|---|
committer | Paweł Chmielowski <pchmielowski@process-one.net> | 2012-09-14 17:51:54 +0200 |
commit | 6b3f2283270db8dab3b54a827ac90ad181660037 (patch) | |
tree | 2d98880f3af688903754004ff1544609b3b7b9f8 /src | |
parent | Unify GET and POST handling code (diff) |
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.
Diffstat (limited to 'src')
-rw-r--r-- | src/web/ejabberd_http.erl | 135 | ||||
-rw-r--r-- | src/web/ejabberd_http.hrl | 5 | ||||
-rw-r--r-- | src/web/ejabberd_http_ws.erl | 7 | ||||
-rw-r--r-- | src/web/ejabberd_http_wsjson.erl | 7 | ||||
-rw-r--r-- | 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, |