25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-10-01 14:44:07 +02:00

Make websocket work over tls

This commit is contained in:
Paweł Chmielowski 2012-09-14 18:29:16 +02:00
parent 6b3f228327
commit 2163cbb22e
5 changed files with 134 additions and 99 deletions

View File

@ -332,8 +332,8 @@ 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, Request, Socket, SockMod) -> process(Handlers, Request, Socket, SockMod, Trail) ->
{HandlerPathPrefix, HandlerModule, HandlerOpts, HandlersLeft} = {HandlerPathPrefix, HandlerModule, HandlerOpts, HandlersLeft} =
case Handlers of case Handlers of
[{Pfx, Mod} | Tail] -> [{Pfx, Mod} | Tail] ->
@ -352,16 +352,16 @@ process(Handlers, Request, Socket, SockMod) ->
%% ["foo", "bar"] %% ["foo", "bar"]
LocalPath = lists:nthtail(length(HandlerPathPrefix), Request#request.path), LocalPath = lists:nthtail(length(HandlerPathPrefix), Request#request.path),
code:ensure_loaded(HandlerModule), code:ensure_loaded(HandlerModule),
R = case erlang:function_exported(HandlerModule, socket_handoff, 5) of R = case erlang:function_exported(HandlerModule, socket_handoff, 6) of
false -> false ->
HandlerModule:process(LocalPath, Request); HandlerModule:process(LocalPath, Request);
_ -> _ ->
HandlerModule:socket_handoff(LocalPath, Request, Socket, SockMod, HandlerOpts) HandlerModule:socket_handoff(LocalPath, Request, Socket, SockMod, Trail, HandlerOpts)
end, end,
ejabberd_hooks:run(http_request_debug, [{LocalPath, Request}]), ejabberd_hooks:run(http_request_debug, [{LocalPath, Request}]),
R; R;
false -> false ->
process(HandlersLeft, Request, Socket, SockMod) process(HandlersLeft, Request, Socket, SockMod, Trail)
end. end.
extract_path_query(#state{request_method = Method, extract_path_query(#state{request_method = Method,
@ -417,7 +417,8 @@ process_request(#state{request_method = Method,
request_tp = TP, request_tp = TP,
websocket_handlers = WebSocketHandlers, websocket_handlers = WebSocketHandlers,
request_headers = RequestHeaders, request_headers = RequestHeaders,
request_handlers = RequestHandlers} = State) -> request_handlers = RequestHandlers,
trail = Trail} = State) ->
case extract_path_query(State) of case extract_path_query(State) of
false -> false ->
make_bad_request(State); make_bad_request(State);
@ -442,7 +443,7 @@ process_request(#state{request_method = Method,
tp = TP, tp = TP,
headers = RequestHeaders, headers = RequestHeaders,
ip = IP}, ip = IP},
case process(RequestHandlers ++ WebSocketHandlers, Request, Socket, SockMod) of case process(RequestHandlers ++ WebSocketHandlers, Request, Socket, SockMod, Trail) 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}

View File

@ -46,7 +46,8 @@
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()}],
buf :: binary()}).
-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.

View File

@ -34,7 +34,7 @@
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]). socket_handoff/6]).
-include("ejabberd.hrl"). -include("ejabberd.hrl").
@ -99,9 +99,9 @@ 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) -> socket_handoff(LocalPath, Request, Socket, SockMod, Buf, Opts) ->
ejabberd_websocket:socket_handoff(LocalPath, Request, Socket, SockMod, ejabberd_websocket:socket_handoff(LocalPath, Request, Socket, SockMod,
Opts, ?MODULE). Buf, Opts, ?MODULE).
%%% Internal %%% Internal

View File

@ -35,8 +35,7 @@
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/6]).
socket_handoff/5]).
-include("ejabberd.hrl"). -include("ejabberd.hrl").
@ -98,9 +97,9 @@ 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) -> socket_handoff(LocalPath, Request, Socket, SockMod, Buf, Opts) ->
ejabberd_websocket:socket_handoff(LocalPath, Request, Socket, SockMod, ejabberd_websocket:socket_handoff(LocalPath, Request, Socket, SockMod,
Opts, ?MODULE). Buf, Opts, ?MODULE).
%%% Internal %%% Internal

View File

@ -40,7 +40,7 @@
-author('ecestari@process-one.net'). -author('ecestari@process-one.net').
-export([connect/2, check/2, socket_handoff/6]). -export([connect/2, check/2, socket_handoff/7]).
-include("ejabberd.hrl"). -include("ejabberd.hrl").
@ -88,7 +88,7 @@ is_acceptable(LocalPath, Origin, IP, Q, Headers, Protocol, _Origins,
socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path, socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path,
headers = Headers, host = Host, port = Port}, headers = Headers, host = Host, port = Port},
Socket, SockMod, Opts, HandlerModule) -> Socket, SockMod, Buf, Opts, HandlerModule) ->
{_, Origin} = case lists:keyfind(<<"Sec-Websocket-Origin">>, 1, Headers) of {_, Origin} = case lists:keyfind(<<"Sec-Websocket-Origin">>, 1, Headers) of
false -> false ->
case lists:keyfind(<<"Origin">>, 1, Headers) of case lists:keyfind(<<"Origin">>, 1, Headers) of
@ -128,7 +128,8 @@ socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path,
port = Port, port = Port,
path = Path, path = Path,
headers = Headers, headers = Headers,
local_path = LocalPath}, local_path = LocalPath,
buf = Buf},
connect(WS, HandlerModule); connect(WS, HandlerModule);
_ -> _ ->
@ -139,11 +140,11 @@ socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path,
children = [{xmlcdata, <<"403 Forbiden">>}]}} children = [{xmlcdata, <<"403 Forbiden">>}]}}
end end
end; end;
socket_handoff(_, #request{method = 'OPTIONS'}, _, _, _, _) -> socket_handoff(_, #request{method = 'OPTIONS'}, _, _, _, _, _) ->
{200, ?OPTIONS_HEADER, []}; {200, ?OPTIONS_HEADER, []};
socket_handoff(_, #request{method = 'HEAD'}, _, _, _, _) -> socket_handoff(_, #request{method = 'HEAD'}, _, _, _, _, _) ->
{200, ?HEADER, []}; {200, ?HEADER, []};
socket_handoff(_, _, _, _, _, _) -> socket_handoff(_, _, _, _, _, _, _) ->
{400, ?HEADER, #xmlel{name = <<"h1">>, {400, ?HEADER, #xmlel{name = <<"h1">>,
children = [{xmlcdata, <<"400 Bad Request">>}]}}. children = [{xmlcdata, <<"400 Bad Request">>}]}}.
@ -178,22 +179,30 @@ get_human_html_xmlel() ->
"use WebSocket connection you need a Jabber/XMPP " "use WebSocket connection you need a Jabber/XMPP "
"client that supports it.">>}]}]}]}. "client that supports it.">>}]}]}]}.
connect(#ws{vsn = Vsn, socket = Socket, q = Q, connect(#ws{vsn = Vsn, socket = Socket, sockmod = SockMod, origin = Origin,
origin = Origin, host = Host, port = Port, host = Host, ws_autoexit = WsAutoExit} = Ws,
sockmod = SockMod, path = Path, headers = Headers,
ws_autoexit = WsAutoExit} =
Ws,
WsLoop) -> WsLoop) ->
HandshakeServer = handshake(Vsn, Socket, SockMod, % build handshake
Headers, {Path, Q, Origin, Host, Port}), {NewWs, HandshakeResponse} = handshake(Ws),
SockMod:send(Socket, HandshakeServer), % send handshake back
SockMod:send(Socket, HandshakeResponse),
?DEBUG("Sent handshake response : ~p", ?DEBUG("Sent handshake response : ~p",
[HandshakeServer]), [HandshakeResponse]),
Ws0 = ejabberd_ws:new(Ws#ws{origin = Origin, Ws0 = ejabberd_ws:new(Ws#ws{origin = Origin,
host = Host}, host = Host},
self()), self()),
{ok, WsHandleLoopPid} = WsLoop:start_link(Ws0), {ok, WsHandleLoopPid} = WsLoop:start_link(Ws0),
erlang:monitor(process, WsHandleLoopPid), erlang:monitor(process, WsHandleLoopPid),
case NewWs#ws.buf of
<<>> ->
ok;
Data ->
self() ! {raw, Socket, Data}
end,
% set opts
case SockMod of case SockMod of
gen_tcp -> gen_tcp ->
inet:setopts(Socket, [{packet, 0}, {active, true}]); inet:setopts(Socket, [{packet, 0}, {active, true}]);
@ -273,28 +282,18 @@ check_headers(Headers, RequiredHeaders) ->
MissingHeaders -> MissingHeaders MissingHeaders -> MissingHeaders
end. end.
handshake({'draft-hixie', 0}, Sock, SocketMod, Headers, recv_data(#ws{buf = Buf} = Ws, Length, _Timeout) when size(Buf) >= Length->
{Path, Q, Origin, Host, Port}) -> <<Data:Length, Tail/binary>> = Buf,
{_, Key1} = lists:keyfind(<<"Sec-Websocket-Key1">>, 1, {Ws#ws{buf = Tail}, Data};
Headers), recv_data(#ws{buf = Buf, socket = Sock, sockmod = SockMod} = Ws, Length, Timeout) ->
{_, Key2} = lists:keyfind(<<"Sec-Websocket-Key2">>, 1, case SockMod of
Headers),
HostPort = case lists:keyfind('Host', 1, Headers) of
{_, Value} -> Value;
_ ->
str:join([Host,
jlib:integer_to_binary(Port)],
<<":">>)
end,
case SocketMod of
gen_tcp -> gen_tcp ->
inet:setopts(Sock, [{packet, raw}, {active, false}]); inet:setopts(Sock, [{packet, raw}, {active, false}]);
_ -> _ ->
SocketMod:setopts(Sock, SockMod:setopts(Sock, [{packet, raw}, {active, false}])
[{packet, raw}, {active, false}])
end, end,
Body = case SocketMod:recv(Sock, 8, 30 * 1000) of Data = case SockMod:recv(Sock, Length - size(Buf), Timeout) of
{ok, Bin} -> Bin; {ok, Bin} -> <<Buf/binary, Bin/binary>>;
{error, timeout} -> {error, timeout} ->
?WARNING_MSG("timeout in reading websocket body", []), ?WARNING_MSG("timeout in reading websocket body", []),
<<>>; <<>>;
@ -302,13 +301,35 @@ handshake({'draft-hixie', 0}, Sock, SocketMod, Headers,
?ERROR_MSG("tcp error treating data: ~p", [_Other]), ?ERROR_MSG("tcp error treating data: ~p", [_Other]),
<<>> <<>>
end, end,
QParams = lists:map(fun ({nokey, <<>>}) -> none; {Ws#ws{buf = <<>>}, Data}.
({K, V}) -> <<K/binary, "=", V/binary>>
% Function: List
% Description: Builds the server handshake response.
handshake(#ws{vsn = {'draft-hixie', 0}, headers = Headers, path = Path,
q = Q, origin = Origin, host = Host, port = Port,
sockmod = SockMod} = State) ->
{_, Key1} = lists:keyfind(<<"Sec-Websocket-Key1">>, 1, Headers),
{_, Key2} = lists:keyfind(<<"Sec-Websocket-Key2">>, 1, Headers),
HostPort = case lists:keyfind('Host', 1, Headers) of
{_, Value} -> Value;
_ -> string:join([Host, integer_to_list(Port)],":")
end, end,
Q), {NewState, Body} = recv_data(State, 8, 30*1000),
QParams = lists:map(
fun({nokey,[]})->
none;
({K, V})->
<<K/binary, "=", V/binary>>
end, Q),
QString = case QParams of QString = case QParams of
[none] -> <<"">>; [none]-> "";
QParams -> <<"?", (str:join(QParams, <<"&">>))/binary>> QParams-> "?" ++ string:join(QParams, "&")
end,
Protocol = case SockMod of
gen_tcp -> <<"ws://">>;
_ -> <<"wss://">>
end, end,
SubProtocolHeader = case find_subprotocol(Headers) of SubProtocolHeader = case find_subprotocol(Headers) of
false -> false ->
@ -316,22 +337,26 @@ handshake({'draft-hixie', 0}, Sock, SocketMod, Headers,
V -> V ->
[<<"Sec-Websocket-Protocol:">>, V, <<"\r\n">>] [<<"Sec-Websocket-Protocol:">>, V, <<"\r\n">>]
end, end,
[<<"HTTP/1.1 101 WebSocket Protocol Handshake\r\n">>, {NewState, [<<"HTTP/1.1 101 WebSocket Protocol Handshake\r\n">>,
<<"Upgrade: WebSocket\r\n">>, <<"Upgrade: WebSocket\r\n">>,
<<"Connection: Upgrade\r\n">>, <<"Connection: Upgrade\r\n">>,
<<"Sec-WebSocket-Origin: ">>, Origin, <<"\r\n">>, <<"Sec-WebSocket-Origin: ">>, Origin, <<"\r\n">>,
SubProtocolHeader, SubProtocolHeader,
<<"Sec-WebSocket-Location: ws://">>, HostPort, <<"/">>, <<"Sec-WebSocket-Location: ">>, Protocol, HostPort, <<"/">>,
str:join(Path, <<"/">>), QString, <<"\r\n\r\n">>, str:join(Path, <<"/">>), QString, <<"\r\n\r\n">>,
build_challenge({'draft-hixie', 0}, build_challenge({'draft-hixie', 0},
{Key1, Key2, Body})]; {Key1, Key2, Body})]};
handshake({'draft-hixie', 68}, _Sock, _SocketMod, handshake(#ws{vsn = {'draft-hixie', 68}, headers = Headers, origin = Origin,
Headers, {Path, _Q, Origin, Host, Port}) -> path = Path, host = Host, port = Port, sockmod = SockMod} = State) ->
SubProtocolHeader = case find_subprotocol(Headers) of SubProtocolHeader = case find_subprotocol(Headers) of
false -> false ->
[]; [];
V -> V ->
[<<"Sec-Websocket-Protocol:">>, V, <<"\r\n">>] [<<"Websocket-Protocol:">>, V, <<"\r\n">>]
end,
Protocol = case SockMod of
gen_tcp -> "ws://";
_ -> "wss://"
end, end,
HostPort = case lists:keyfind('Host', 1, Headers) of HostPort = case lists:keyfind('Host', 1, Headers) of
{_, Value} -> Value; {_, Value} -> Value;
@ -340,16 +365,15 @@ handshake({'draft-hixie', 68}, _Sock, _SocketMod,
iolist_to_binary(integer_to_list(Port))], iolist_to_binary(integer_to_list(Port))],
<<":">>) <<":">>)
end, end,
[<<"HTTP/1.1 101 Web Socket Protocol Handshake\r\n">>, {State, [<<"HTTP/1.1 101 Web Socket Protocol Handshake\r\n">>,
<<"Upgrade: WebSocket\r\n">>, <<"Upgrade: WebSocket\r\n">>,
<<"Connection: Upgrade\r\n">>, <<"Connection: Upgrade\r\n">>,
<<"WebSocket-Origin: ">>, Origin, <<"\r\n">>, <<"WebSocket-Origin: ">>, Origin, <<"\r\n">>,
SubProtocolHeader, SubProtocolHeader,
<<"WebSocket-Location: ws://">>, <<"WebSocket-Location: ">>, Protocol,
HostPort, <<"/">>, str:join(Path, <<"/">>), HostPort, <<"/">>, str:join(Path, <<"/">>),
<<"\r\n\r\n">>]; <<"\r\n\r\n">>]};
handshake({'draft-hybi', _}, _Sock, _SocketMod, Headers, handshake(#ws{vsn = {'draft-hybi', _}, headers = Headers} = State) ->
{_Path, _Q, _Origin, _Host, _Port}) ->
{_, Key} = lists:keyfind(<<"Sec-Websocket-Key">>, 1, {_, Key} = lists:keyfind(<<"Sec-Websocket-Key">>, 1,
Headers), Headers),
SubProtocolHeader = case find_subprotocol(Headers) of SubProtocolHeader = case find_subprotocol(Headers) of
@ -360,11 +384,11 @@ handshake({'draft-hybi', _}, _Sock, _SocketMod, Headers,
end, end,
Hash = jlib:encode_base64( Hash = jlib:encode_base64(
sha:sha1(<<Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11">>)), sha:sha1(<<Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11">>)),
[<<"HTTP/1.1 101 Switching Protocols\r\n">>, {State, [<<"HTTP/1.1 101 Switching Protocols\r\n">>,
<<"Upgrade: websocket\r\n">>, <<"Upgrade: websocket\r\n">>,
<<"Connection: Upgrade\r\n">>, <<"Connection: Upgrade\r\n">>,
SubProtocolHeader, SubProtocolHeader,
<<"Sec-WebSocket-Accept: ">>, Hash, <<"\r\n\r\n">>]. <<"Sec-WebSocket-Accept: ">>, Hash, <<"\r\n\r\n">>]}.
build_challenge({'draft-hixie', 0}, build_challenge({'draft-hixie', 0},
{Key1, Key2, Key3}) -> {Key1, Key2, Key3}) ->
@ -395,17 +419,17 @@ find_subprotocol(Headers) ->
ws_loop(Vsn, HandlerState, Socket, WsHandleLoopPid, ws_loop(Vsn, HandlerState, Socket, WsHandleLoopPid,
SocketMode, WsAutoExit) -> SocketMode, WsAutoExit) ->
receive receive
{tcp, Socket, Data} -> {DataType, _Socket, Data} when DataType =:= tcp orelse DataType =:= raw ->
{NewHandlerState, ToSend} = handle_data(Vsn, case handle_data(DataType, Vsn, HandlerState, Data, Socket, WsHandleLoopPid, SocketMode, WsAutoExit) of
HandlerState, Data, Socket, {NewHandlerState, ToSend} ->
WsHandleLoopPid, SocketMode, lists:foreach(fun(Pkt) -> SocketMode:send(Socket, Pkt)
WsAutoExit), end, ToSend),
lists:foreach(fun (Pkt) -> SocketMode:send(Socket, Pkt) ws_loop(Vsn, NewHandlerState, Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
end, Error ->
ToSend), ?DEBUG("tls decode error ~p", [Error]),
ws_loop(Vsn, NewHandlerState, Socket, WsHandleLoopPid, websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit)
SocketMode, WsAutoExit); end;
{tcp_closed, Socket} -> {tcp_closed, _Socket} ->
?DEBUG("tcp connection was closed, exit", []), ?DEBUG("tcp connection was closed, exit", []),
websocket_close(Socket, WsHandleLoopPid, SocketMode, websocket_close(Socket, WsHandleLoopPid, SocketMode,
WsAutoExit); WsAutoExit);
@ -609,6 +633,16 @@ process_hybi_8(#hybi_8_state{unprocessed =
process_hybi_8(State#hybi_8_state{unprocessed = <<>>}, process_hybi_8(State#hybi_8_state{unprocessed = <<>>},
<<UnprocessedPre/binary, Data/binary>>). <<UnprocessedPre/binary, Data/binary>>).
handle_data(tcp, Vsn, State, Data, Socket, WsHandleLoopPid, tls, WsAutoExit) ->
case tls:recv_data(Socket, Data) of
{ok, NewData} ->
handle_data(Vsn, State, NewData, Socket, WsHandleLoopPid, tls, WsAutoExit);
Error ->
Error
end;
handle_data(_, Vsn, State, Data, Socket, WsHandleLoopPid, SockMod, WsAutoExit) ->
handle_data(Vsn, State, Data, Socket, WsHandleLoopPid, SockMod, WsAutoExit).
handle_data({'draft-hybi', _}, State, Data, _Socket, handle_data({'draft-hybi', _}, State, Data, _Socket,
WsHandleLoopPid, _SocketMode, _WsAutoExit) -> WsHandleLoopPid, _SocketMode, _WsAutoExit) ->
{NewState, Recv, Send} = process_hybi_8(State, Data), {NewState, Recv, Send} = process_hybi_8(State, Data),