aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaweł Chmielowski <pchmielowski@process-one.net>2012-09-14 17:51:54 +0200
committerPaweł Chmielowski <pchmielowski@process-one.net>2012-09-14 17:51:54 +0200
commit6b3f2283270db8dab3b54a827ac90ad181660037 (patch)
tree2d98880f3af688903754004ff1544609b3b7b9f8 /src
parentUnify 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.erl135
-rw-r--r--src/web/ejabberd_http.hrl5
-rw-r--r--src/web/ejabberd_http_ws.erl7
-rw-r--r--src/web/ejabberd_http_wsjson.erl7
-rw-r--r--src/web/ejabberd_websocket.erl122
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,