diff --git a/.gitignore b/.gitignore
index ac8f968..555cc1f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
# Compiled source #
###################
*.com
-*.class
+*.beam
*.dll
*.exe
*.o
diff --git a/Emakefile b/Emakefile
index 462cf73..6fbe5e7 100644
--- a/Emakefile
+++ b/Emakefile
@@ -1,3 +1,3 @@
-{'src/mod_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"},{i,"/usr/lib/ejabberd/include/web","src"}]}.
-{'src/ejabberd_xmpp_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"},{i,"/usr/lib/ejabberd/include/web","src"}]}.
-{'src/ejabberd_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"},{i,"/usr/lib/ejabberd/include/web","src"}]}.
\ No newline at end of file
+{'src/mod_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"}]}.
+{'src/ejabberd_xmpp_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"}]}.
+{'src/ejabberd_websocket', [{outdir, "ebin"},{i,"/usr/lib/ejabberd/include"}]}.
diff --git a/README.markdown b/README.markdown
index 065fee6..f250600 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,6 +1,3 @@
-> This repo is not maintained and most of the code in here is probably deprecated.
-
-
# Websocket Module for Ejabberd
This is a module that adds websocket support for the [ejabberd](http://www.ejabberd.im/) XMPP server. It's a more elegant, modern and faster replacement to Bosh.
@@ -17,6 +14,8 @@ It is an implementation of the [XMPP Over Websocket Draft](http://tools.ietf.org
### Install
cp ebin/*.beam /path/to/ejabberd/lib/ebin/
+As for ejabberd v.13.12 I have had to install p1_logger (processone/p1_logger in github) as well. It is also possible to install /lager/. I don't know what is that.
+
### Configure
In the listeners section add the following line:
diff --git a/src/ejabberd_websocket.erl b/src/ejabberd_websocket.erl
index 8d6e199..126d01c 100644
--- a/src/ejabberd_websocket.erl
+++ b/src/ejabberd_websocket.erl
@@ -1,9 +1,11 @@
-%%%----------------------------------------------------------------------
+%%----------------------------------------------------------------------
%%% File : ejabberd_websocket.erl
%%% Author : Nathan Zorn
%%% Purpose : Listener for XMPP over websockets
%%%----------------------------------------------------------------------
+%% TODO: downcase all http keys (issue with Sec-Web[Ss]ocket-Key under FF
+
-module(ejabberd_websocket).
-author('nathan.zorn@gmail.com').
@@ -17,6 +19,8 @@
-export([init/2]).
%% Includes
-include("ejabberd.hrl").
+-define(LAGER, true).
+-include("logger.hrl").
-include("jlib.hrl").
-include("ejabberd_websocket.hrl").
%% record used to keep track of listener state
@@ -39,6 +43,10 @@
websocket_pid,
trail = ""
}).
+
+%% RFC-2616 HTML 1.1 specification
+-define(RFC2616_SEPARATORS, "()<>@,;:\\\"/[]?={} \t").
+
-define(MAXKEY_LENGTH, 4294967295).
%% Supervisor Start
start(SockData, Opts) ->
@@ -50,29 +58,29 @@ start_link(SockData, Opts) ->
init({SockMod, Socket}, Opts) ->
TLSEnabled = lists:member(tls, Opts),
TLSOpts1 = lists:filter(fun({certfile, _}) -> true;
- (_) -> false
- end, Opts),
+ (_) -> false
+ end, Opts),
TLSOpts = [verify_none | TLSOpts1],
{SockMod1, Socket1} =
- if
- TLSEnabled ->
- inet:setopts(Socket, [{recbuf, 8192}]),
- {ok, TLSSocket} = tls:tcp_to_tls(Socket, TLSOpts),
- {tls, TLSSocket};
- true ->
- {SockMod, Socket}
- end,
+ if
+ TLSEnabled ->
+ inet:setopts(Socket, [{recbuf, 8192}]),
+ {ok, TLSSocket} = tls:tcp_to_tls(Socket, TLSOpts),
+ {tls, TLSSocket};
+ true ->
+ {SockMod, Socket}
+ end,
case SockMod1 of
- gen_tcp ->
- inet:setopts(Socket1, [{packet, http}, {recbuf, 8192}]);
- _ ->
- ok
+ gen_tcp ->
+ inet:setopts(Socket1, [{packet, http}, {recbuf, 8192}]);
+ _ ->
+ ok
end,
RequestHandlers =
- case lists:keysearch(request_handlers, 1, Opts) of
- {value, {request_handlers, H}} -> H;
- false -> []
- end,
+ case lists:keysearch(request_handlers, 1, Opts) of
+ {value, {request_handlers, H}} -> H;
+ false -> []
+ end,
?INFO_MSG("started: ~p", [{SockMod1, Socket1}]),
State = #state{sockmod = SockMod1,
socket = Socket1,
@@ -85,187 +93,206 @@ socket_type() ->
raw.
receive_headers(State) ->
- SockMod = State#state.sockmod,
- Socket = State#state.socket,
- Data = SockMod:recv(Socket, 0, 300000),
- ?DEBUG("Data in ~p: headers : ~p",[State, Data]),
- case State#state.sockmod of
- gen_tcp ->
- NewState = process_header(State, Data),
- case NewState#state.end_of_request of
- true ->
- ok;
- _ ->
- receive_headers(NewState)
- end;
- _ ->
- case Data of
- {ok, Binary} ->
- ?DEBUG("not gen_tcp, ssl? ~p~n", [Binary]),
- {Request, Trail} = parse_request(
- State,
- State#state.trail ++
- binary_to_list(Binary)),
- State1 = State#state{trail = Trail},
- NewState = lists:foldl(
- fun(D, S) ->
- case S#state.end_of_request of
- true ->
- S;
- _ ->
- process_header(S, D)
- end
- end, State1, Request),
- case NewState#state.end_of_request of
- true ->
- ok;
- _ ->
- receive_headers(NewState)
- end;
- Req ->
- ?DEBUG("not gen_tcp or ok: ~p~n", [Req]),
- ok
- end
- end.
+ SockMod = State#state.sockmod,
+ Socket = State#state.socket,
+ Data = SockMod:recv(Socket, 0, 300000),
+ ?WARNING_MSG("Data in ~p: headers : ~p",[State, Data]),
+ case State#state.sockmod of
+ gen_tcp ->
+ NewState = process_header(State, Data),
+ case NewState#state.end_of_request of
+ true -> ok;
+ _ -> receive_headers(NewState)
+ end;
+ _ ->
+ case Data of
+ {ok, Binary} ->
+ ?DEBUG("not gen_tcp, ssl? ~p~n", [Binary]),
+ {Request, Trail} = parse_request(State,
+ State#state.trail ++
+ binary_to_list(Binary)),
+ State1 = State#state{trail = Trail},
+ NewState = lists:foldl(fun(D, S) ->
+ case S#state.end_of_request of
+ true -> S;
+ _ -> process_header(S, D)
+ end
+ end, State1, Request),
+ case NewState#state.end_of_request of
+ true -> ok;
+ _ -> receive_headers(NewState)
+ end;
+ Req ->
+ ?DEBUG("not gen_tcp or ok: ~p~n", [Req]),
+ ok
+ end
+ end.
process_header(State, Data) ->
- case Data of
- {ok, {http_request, Method, Uri, Version}} ->
- KeepAlive = case Version of
- {1, 1} ->
- true;
- _ ->
- false
- end,
- Path = case Uri of
- {absoluteURI, _Scheme, _Host, _Port, P} -> {abs_path, P};
- _ -> Uri
- end,
- State#state{request_method = Method,
- request_version = Version,
- request_path = Path,
- request_keepalive = KeepAlive};
- {ok, {http_header, _, 'Connection'=Name, _, Conn}} ->
- KeepAlive1 = case jlib:tolower(Conn) of
- "keep-alive" ->
- true;
- "close" ->
- false;
- _ ->
- State#state.request_keepalive
- end,
- State#state{request_keepalive = KeepAlive1,
- request_headers=add_header(Name, Conn, State)};
- {ok, {http_header, _, 'Content-Length'=Name, _, SLen}} ->
- case catch list_to_integer(SLen) of
- Len when is_integer(Len) ->
- State#state{request_content_length = Len,
- request_headers=add_header(Name, SLen, State)};
- _ ->
- State
- end;
- {ok, {http_header, _, 'Host'=Name, _, Host}} ->
- State#state{request_host = Host,
- request_headers=add_header(Name, Host, State)};
- {ok, {http_header, _, Name, _, Value}} ->
- State#state{request_headers=add_header(Name, Value, State)};
- {ok, http_eoh} when State#state.request_host == undefined ->
- ?WARNING_MSG("An HTTP request without 'Host' HTTP header was received.", []),
- throw(http_request_no_host_header);
- {ok, http_eoh} ->
- ?DEBUG("(~w) http query: ~w ~s~n",
- [State#state.socket,
- State#state.request_method,
- element(2, State#state.request_path)]),
- Out = process_request(State),
- %% Test for web socket
- case (Out =/= false) and is_websocket_upgrade(State#state.request_headers) of
- true ->
- ?DEBUG("Websocket!",[]),
- SockMod = State#state.sockmod,
- Socket = State#state.socket,
- case SockMod of
- gen_tcp ->
- inet:setopts(Socket, [{packet, raw}]);
- _ ->
- ok
- end,
- %% handle hand shake
- case handshake(State) of
- true ->
- case sub_protocol(State#state.request_headers) of
- "xmpp" ->
- %% send the state back
- #state{sockmod = SockMod,
- socket = Socket,
- request_handlers = State#state.request_handlers};
- _ ->
- ?DEBUG("Bad sub protocol",[]),
- #state{end_of_request = true,
- request_handlers = State#state.request_handlers}
- end;
- _ ->
- ?DEBUG("Bad Handshake",[]),
- #state{end_of_request = true,
- request_handlers = State#state.request_handlers}
- end;
- _ ->
- ?DEBUG("Regular HTTP",[]),
- #state{end_of_request = true,
- request_handlers = State#state.request_handlers}
- end;
- {error, closed} ->
- ?ERROR_MSG("Socket closed", [State]),
- process_data(State, socket_closed),
- #state{end_of_request = true,
- request_handlers = State#state.request_handlers};
- {error, timeout} ->
- ?DEBUG("Socket recv timed out. Return the same State.",[]),
- State;
- {ok, HData} ->
- PData = case State#state.partial of
- <<>> ->
- HData;
- <> ->
- <>
- end,
- {_Out, Partial, Pid} = case process_data(State, PData) of
- {O, P} ->
- {O, P, false};
- {Output, Part, ProcId} ->
- {Output, Part, ProcId};
- Error ->
- {Error, undefined, undefined}
- end,
- ?DEBUG("C2SPid:~p~n",[Pid]),
- case Pid of
- false ->
- #state{sockmod = State#state.sockmod,
- socket = State#state.socket,
- partial = Partial,
- request_handlers = State#state.request_handlers};
+ case Data of
+ {ok, {http_request, Method, Uri, Version}} ->
+ KeepAlive = case Version of
+ {1, 1} ->
+ true;
+ _ ->
+ false
+ end,
+ Path = case Uri of
+ {absoluteURI, _Scheme, _Host, _Port, P} -> {abs_path, P};
+ _ -> Uri
+ end,
+ State#state{request_method = Method,
+ request_version = Version,
+ request_path = Path,
+ request_keepalive = KeepAlive};
+ {ok, {http_header, _, 'Connection'=Name, _, Con}} ->
+ ConLine = strip_spaces(Con),
+ ConDifList = string:tokens(ConLine, ?RFC2616_SEPARATORS), %% these can be in different case
+ ConList = lists:map(fun string:to_lower/1, ConDifList),
+ KeepAlive1 = case lists:any(fun is_keep_alive/1, ConList) of
+ true -> true;
+ _ ->
+ case lists:any(fun is_close/1, ConList) of
+ true -> true;
+ _ -> State#state.request_keepalive
+ end
+ end,
+ State#state{request_keepalive = KeepAlive1,
+ request_headers=add_header(Name, ConList, State)};
+ {ok, {http_header, _, 'Content-Length'=Name, _, SLen}} ->
+ case catch list_to_integer(SLen) of
+ Len when is_integer(Len) ->
+ State#state{request_content_length = Len,
+ request_headers=add_header(Name, SLen, State)};
+ _ ->
+ State
+ end;
+ {ok, {http_header, _, 'Host'=Name, _, Host}} ->
+ State#state{request_host = Host,
+ request_headers=add_header(Name, Host, State)};
+ {ok, {http_header, _, Name, _, Value}} ->
+ State#state{request_headers=add_header(Name, Value, State)};
+ {ok, http_eoh} when State#state.request_host == undefined ->
+ ?WARNING_MSG("An HTTP request without 'Host' HTTP header was received.", []),
+ throw(http_request_no_host_header);
+ {ok, http_eoh} ->
+ ?DEBUG("(~w) http query: ~w ~s~n",
+ [State#state.socket,
+ State#state.request_method,
+ element(2, State#state.request_path)]),
+ Out = process_request(State),
+ IsUpgrade = is_websocket_upgrade(State#state.request_headers),
+ %% Test for web socket
+ case (Out =/= false) and IsUpgrade of
+ true ->
+ ?DEBUG("Websocket!",[]),
+ SockMod = State#state.sockmod,
+ Socket = State#state.socket,
+ case SockMod of
+ gen_tcp ->
+ inet:setopts(Socket, [{packet, raw}]);
+ _ ->
+ ok
+ end,
+ %% handle hand shake
+ case handshake2(State) of
+ true ->
+ case sub_protocol(State#state.request_headers) of
+ "xmpp" ->
+ %% send the state back
+ #state{sockmod = SockMod,
+ socket = Socket,
+ request_handlers = State#state.request_handlers};
_ ->
- #state{sockmod = State#state.sockmod,
- socket = State#state.socket,
- partial = Partial,
- websocket_pid = Pid,
- request_handlers = State#state.request_handlers}
- end;
+ ?DEBUG("Bad sub protocol",[]),
+ #state{end_of_request = true,
+ request_handlers = State#state.request_handlers}
+ end;
+ _ ->
+ ?DEBUG("Bad Handshake",[]),
+ #state{end_of_request = true,
+ request_handlers = State#state.request_handlers}
+ end;
_ ->
- ?DEBUG("Not expected: ~p~n",[Data]),
- #state{end_of_request = true,
- request_handlers = State#state.request_handlers}
- end.
+ ?DEBUG("Regular HTTP",[]),
+ #state{end_of_request = true,
+ request_handlers = State#state.request_handlers}
+ end;
+ {error, closed} ->
+ ?ERROR_MSG("Socket closed", [State]),
+ #state{end_of_request = true,
+ request_handlers = State#state.request_handlers};
+ {error, timeout} ->
+ ?DEBUG("Socket recv timed out. Return the same State.",[]),
+ State;
+ {ok, HData} ->
+ PData = case State#state.partial of
+ <<>> ->
+ HData;
+ <> ->
+ <>
+ end,
+ {_Out, Partial, Pid} = case process_data(State, PData) of
+ {O, P} ->
+ {O, P, false};
+ {Output, Part, ProcId} ->
+ {Output, Part, ProcId};
+ Error ->
+ {Error, undefined, undefined}
+ end,
+ ?DEBUG("C2SPid:~p~n",[Pid]),
+ case Pid of
+ false ->
+ #state{sockmod = State#state.sockmod,
+ socket = State#state.socket,
+ partial = Partial,
+ request_handlers = State#state.request_handlers};
+ _ ->
+ #state{sockmod = State#state.sockmod,
+ socket = State#state.socket,
+ partial = Partial,
+ websocket_pid = Pid,
+ request_handlers = State#state.request_handlers}
+ end;
+ _ ->
+ ?DEBUG("Not expected: ~p~n",[Data]),
+ #state{end_of_request = true,
+ request_handlers = State#state.request_handlers}
+ end.
add_header(Name, Value, State) ->
[{Name, Value} | State#state.request_headers].
is_websocket_upgrade(RequestHeaders) ->
- Connection = {'Connection', "Upgrade"} == lists:keyfind('Connection', 1,
- RequestHeaders),
- Upgrade = {'Upgrade', "WebSocket"} == lists:keyfind('Upgrade', 1,
- RequestHeaders),
- Connection and Upgrade.
+ {'Connection', ConnectionList} = lists:keyfind('Connection', 1, RequestHeaders),
+ {'Upgrade', Upgrade} = lists:keyfind('Upgrade', 1, RequestHeaders),
+ lists:any(fun is_upgrade/1, ConnectionList) and (string:to_lower(Upgrade) == "websocket").
+
+handshake2(State) ->
+ SubProto = sub_protocol(State#state.request_headers),
+ SubProtoHeader = case SubProto of
+ undefined -> "";
+ P -> ["Sec-WebSocket-Protocol: ", P, "\r\n"]
+ end,
+ {"Sec-Websocket-Key", SecKey} = lists:keyfind("Sec-Websocket-Key", 1, State#state.request_headers),
+ Guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", %% TODO: Generate real unique id
+ WebSocketSec = base64:encode(crypto:hash(sha, SecKey ++ Guid)),
+ Res = ["HTTP/1.1 101 Switching Protocols\r\n",
+ "Upgrade: websocket\r\n",
+ "Connection: Upgrade\r\n",
+ SubProtoHeader,
+ ["Sec-WebSocket-Accept: ", WebSocketSec, "\r\n"],
+ "\r\n"
+ ],
+
+ ?DEBUG("Respond with:~n~p~n", [Res]),
+
+ case send_text(State, Res) of
+ ok -> true;
+ E ->
+ ?DEBUG("ERROR Sending text:~p~n",[E]),
+ false
+ end.
handshake(State) ->
SockMod = State#state.sockmod,
@@ -294,7 +321,7 @@ handshake(State) ->
State#state.request_path,
SubProto,
Sig),
- ?DEBUG("Sending handshake response:~p~n",[Res]),
+ ?WARNING_MSG("Sending handshake response:~p~n",[Res]),
%% send response
case send_text(State, Res) of
ok -> true;
@@ -338,10 +365,11 @@ process_data(State, Data) ->
{error, _Error} ->
undefined
end,
+ ?DEBUG("Incoming data: ~p~n", [Data]),
Request = #wsrequest{method = State#state.request_method,
path = Path,
headers = State#state.request_headers,
- data = Data,
+ data = binary_to_list(Data),
fsmref = State#state.websocket_pid,
wsocket = Socket,
wsockmod = SockMod,
@@ -380,7 +408,7 @@ process([], _) ->
false;
process(RequestHandlers, Request) ->
[{HandlerPathPrefix, HandlerModule} | HandlersLeft] = RequestHandlers,
- case (lists:prefix(HandlerPathPrefix, Request#wsrequest.path) or
+ case (lists:prefix([binary_to_list(HandlerPathPrefix)], Request#wsrequest.path) or
(HandlerPathPrefix==Request#wsrequest.path)) of
true ->
?DEBUG("~p matches ~p",
@@ -389,8 +417,8 @@ process(RequestHandlers, Request) ->
%% 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#wsrequest.path),
+ LocalPath = "",%%lists:nthtail(length(binary_to_list(HandlerPathPrefix)),
+ %% Request#wsrequest.path),
HandlerModule:process(LocalPath, Request);
false ->
process(HandlersLeft, Request)
@@ -408,7 +436,7 @@ send_text(State, Text) ->
end.
%% sign data
websocket_sign(Part1, Part2, Key3) ->
- crypto:md5( <> ).
%% verify websocket keys
websocket_verify_keys(Key1, Key2) ->
@@ -574,22 +602,22 @@ old_hex_to_integer(Hex) ->
% The following code is mostly taken from yaws_ssl.erl
parse_request(State, Data) ->
- case Data of
- [] ->
- {[], []};
- _ ->
- ?DEBUG("GOT ssl data ~p~n", [Data]),
- {R, Trail} = case State#state.request_method of
- undefined ->
- {R1, Trail1} = get_req(Data),
- ?DEBUG("Parsed request ~p~n", [R1]),
- {[R1], Trail1};
- _ ->
- {[], Data}
- end,
- {H, Trail2} = get_headers(Trail),
- {R ++ H, Trail2}
- end.
+ case Data of
+ [] ->
+ {[], []};
+ _ ->
+ ?DEBUG("GOT ssl data ~p~n", [Data]),
+ {R, Trail} = case State#state.request_method of
+ undefined ->
+ {R1, Trail1} = get_req(Data),
+ ?DEBUG("Parsed request ~p~n", [R1]),
+ {[R1], Trail1};
+ _ ->
+ {[], Data}
+ end,
+ {H, Trail2} = get_headers(Trail),
+ {R ++ H, Trail2}
+ end.
get_req("\r\n\r\n" ++ _) ->
bad_request;
@@ -694,7 +722,11 @@ get_headers(H, Tail) ->
parse_line("Connection:" ++ Con) ->
- [{ok, {http_header, undefined, 'Connection', undefined, strip_spaces(Con)}}];
+ ?DEBUG("Connection is found in headers with line: ~p~n", [Con]),
+ ConLine = strip_spaces(Con),
+ ConDifList = string:tokens(ConLine, ?RFC2616_SEPARATORS), %% these can be in different case
+ ConList = lists:map(fun string:to_lower/1, ConDifList),
+ [{ok, {http_header, undefined, 'Connection', undefined, ConList}}];
parse_line("Host:" ++ Con) ->
[{ok, {http_header, undefined, 'Host', undefined, strip_spaces(Con)}}];
parse_line("Accept:" ++ Con) ->
@@ -743,6 +775,14 @@ parse_line(S) ->
[]
end.
+is_upgrade("upgrade") -> true;
+is_upgrade(_) -> false.
+
+is_keep_alive("keep-alive") -> true;
+is_keep_alive(_) -> false.
+
+is_close("close") -> true;
+is_close(_) -> false.
is_space($\s) ->
true;
diff --git a/src/ejabberd_xmpp_websocket.erl b/src/ejabberd_xmpp_websocket.erl
index 96e8f19..3a919ec 100644
--- a/src/ejabberd_xmpp_websocket.erl
+++ b/src/ejabberd_xmpp_websocket.erl
@@ -30,6 +30,8 @@
process_request/5]).
-include("ejabberd.hrl").
+-define(LAGER, true).
+-include("logger.hrl").
%% Module constants
-define(NULL_PEER, {{0, 0, 0, 0}, 0}).
@@ -37,9 +39,8 @@
% idle sessions
-define(MAX_PAUSE, 120). % may num of sec a client is allowed to pause
% the session
--define(NS_CLIENT, "jabber:client").
--define(NS_STREAM, "http://etherx.jabber.org/streams").
--define(TEST, 1).
+-define(NS_CLIENT, <<"jabber:client">>).
+-define(NS_STREAM, <<"http://etherx.jabber.org/streams">>).
%% Erlang Records for state
-record(wsr, {socket, sockmod, key, out}).
@@ -63,10 +64,10 @@
}).
start(Host, Sid, Key, IP) ->
- Proc = gen_mod:get_module_proc(Host, ejabberd_mod_websocket),
+ Proc = gen_mod:get_module_proc(list_to_binary(Host), ejabberd_mod_websocket),
case catch supervisor:start_child(Proc, [Sid, Key, IP]) of
- {ok, Pid} -> {ok, Pid};
- Reason ->
+ {ok, Pid} -> {ok, Pid};
+ Reason ->
?ERROR_MSG("~p~n",[Reason]),
{error, "Cannot start XMPP, Websocket session"}
end.
@@ -241,7 +242,6 @@ handle_sync_event({send_xml, Packet}, _From, StateName,
{reply, Reply, StateName, StateData#state{output = Output}};
handle_sync_event({send_xml, Packet}, _From, StateName, StateData) ->
Output = [Packet | StateData#state.output],
- ?DEBUG("Data from C2S(timer):~p:~p~n",[Output,StateData]),
cancel_timer(StateData#state.timer),
Timer = set_inactivity_timer(StateData#state.pause,
StateData#state.max_inactivity),
@@ -365,16 +365,16 @@ terminate(_Reason, _StateName, StateData) ->
%% Internal functions
%%%
stream_start(ParsedPayload) ->
- ?DEBUG("~p~n",[ParsedPayload]),
+ ?DEBUG("Stream started: ~p~n",[ParsedPayload]),
case ParsedPayload of
{xmlelement, "stream:stream", Attrs, _} ->
{"to",Host} = lists:keyfind("to", 1, Attrs),
- Sid = sha:sha(term_to_binary({now(), make_ref()})),
+ Sid = crypto:hash(sha, term_to_binary({now(), make_ref()})),
Key = "",
{Host, Sid, Key};
{xmlstreamstart, _Name, Attrs} ->
{"to",Host} = lists:keyfind("to", 1, Attrs),
- Sid = sha:sha(term_to_binary({now(), make_ref()})),
+ Sid = crypto:hash(sha, term_to_binary({now(), make_ref()})),
Key = "",
{Host, Sid, Key};
_ ->
@@ -408,18 +408,82 @@ send_receiver_reply(Receiver, Reply) ->
gen_fsm:reply(Receiver, Reply).
%% send data to socket
+send_text(StateData, {xmlel, Name, Attributes, Children}) ->
+ send_text(StateData, list_to_binary(build_xml_element({xmlel, Name, Attributes, Children})));
send_text(StateData, Text) ->
?DEBUG("Send XML on stream = ~p", [Text]),
(StateData#state.websocket_sockmod):send(StateData#state.websocket_s,
- [0, Text, 255]).
+ websocket_encode(binary_to_list(Text), no_mask)).
+
+build_xml_element({xmlel, Name, [], []}) ->
+ "<" ++ binary_to_list(Name) ++ " />";
+build_xml_element({xmlel, Name, [], Children}) ->
+ "<" ++ binary_to_list(Name) ++ ">" ++
+ build_xml_element({xmllist, Children}) ++
+ "" ++ binary_to_list(Name) ++ ">";
+build_xml_element({xmlel, Name, Attributes, []}) ->
+ "<" ++ binary_to_list(Name) ++ build_attributes(Attributes) ++ "/>";
+build_xml_element({xmlel, Name, Attributes, Children}) ->
+ "<" ++ binary_to_list(Name) ++ build_attributes(Attributes) ++ ">" ++
+ build_xml_element({xmllist, Children}) ++
+ "" ++ binary_to_list(Name) ++ ">";
+build_xml_element({xmllist, []}) ->
+ "";
+build_xml_element({xmllist, [Element | OtherElements]}) ->
+ build_xml_element(Element) ++ build_xml_element({xmllist, OtherElements});
+build_xml_element({xmlcdata, CData}) ->
+ binary_to_list(CData).
+ %% "".
+build_attributes([]) ->
+ "";
+build_attributes([{AttrName, AttrValue} | Other]) ->
+ " " ++ binary_to_list(AttrName) ++ "=\"" ++ binary_to_list(AttrValue) ++ "\"" ++ build_attributes(Other).
+
+binary_len(<>, PayloadLen) ->
+ if PayloadLen =< 125 ->
+ << Start:1, PayloadLen:7 >>;
+ PayloadLen =< 65535 ->
+ << Start:1, 126:7, PayloadLen:16 >>;
+ true ->
+ << Start:1, 127:7, PayloadLen:64 >>
+ end.
+
+websocket_encode(Text, no_mask) ->
+ PayloadLen = string:len(Text),
+ Textb = list_to_binary(Text),
+ PayloadLenb = binary_len(<<0:1>>, PayloadLen),
+ << 1:1, 0:3, 1:4, PayloadLenb/binary, Textb/binary>>;
+websocket_encode(Text, mask) ->
+ websocket_encode(Text, mask, random:uniform((1 bsl 32) - 1)).
+
+websocket_encode(Text, mask, Mask) ->
+ PayloadLen = string:len(Text),
+ Textb = apply_mask(list_to_binary(Text), Mask),
+ PayloadLenb = binary_len(<<1:1>>, PayloadLen),
+ << 1:1, 0:3, 1:4, PayloadLenb/binary, Mask:32, Textb/binary>>.
+
+apply_mask(Data, Mask) ->
+ apply_mask(Data, Mask, <<>>).
+apply_mask(<<>>, _Mask, <<>>) ->
+ <<>>;
+apply_mask(<<>>, _Mask, <>) ->
+ <>;
+apply_mask(<>, Mask, <>) ->
+ apply_mask(<>, Mask, <>);
+apply_mask(<>, Mask, <>) ->
+ <>;
+apply_mask(<>, Mask, <>) ->
+ <>;
+apply_mask(<>, Mask, <>) ->
+ <>.
send_element(StateData, {xmlstreamstart, Name, Attrs}) ->
XmlString = streamstart_tobinary({xmlstreamstart, Name, Attrs}),
send_text(StateData, XmlString);
-send_element(StateData, {xmlstreamend, "stream:stream"}) ->
+send_element(StateData, {xmlstreamend, <<"stream:stream">>}) ->
send_text(StateData, <<"">>);
send_element(StateData, El) ->
- send_text(StateData, xml:element_to_binary(El)).
+ send_text(StateData, El).
send_stream_start(C2SPid, Attrs) ->
StreamTo = case lists:keyfind("to", 1, Attrs) of
@@ -437,17 +501,17 @@ send_stream_start(C2SPid, Attrs) ->
gen_fsm:send_event(
C2SPid,
{xmlstreamstart, "stream:stream",
- [{"to", To},
- {"xmlns", ?NS_CLIENT},
- {"xmlns:stream", ?NS_STREAM}]});
+ [{<<"to">>, list_to_binary(To)},
+ {<<"xmlns">>, ?NS_CLIENT},
+ {<<"xmlns:stream">>, ?NS_STREAM}]});
{To, Version} ->
gen_fsm:send_event(
C2SPid,
{xmlstreamstart, "stream:stream",
- [{"to", To},
- {"xmlns", ?NS_CLIENT},
- {"version", Version},
- {"xmlns:stream", ?NS_STREAM}]})
+ [{<<"to">>, list_to_binary(To)},
+ {<<"xmlns">>, ?NS_CLIENT},
+ {<<"version">>, list_to_binary(Version)},
+ {<<"xmlns:stream">>, ?NS_STREAM}]})
end.
send_data(FsmRef, Req) ->
?DEBUG("session pid:~p~n", [FsmRef]),
@@ -526,6 +590,23 @@ attrs_tostring(Str,[X|Rest]) ->
%% Tests
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
+websocket_encode_test() ->
+ <<129, 5, 72, 101, 108, 108, 111>> = websocket_encode("Hello", no_mask),
+ <<129, 133, 55, 250, 33, 61, 127, 159, 77, 81, 88>> = websocket_encode("Hello", mask, 939139389).
+
+build_xml_test() ->
+ StringXML = "",
+ ProducedXML = build_xml_element({xmlel,
+ <<"stream:error">>,
+ [],
+ [{xmlel,
+ <<"invalid-namespace">>,
+ [{<<"xmlns">>,<<"urn:ietf:params:xml:ns:xmpp-streams">>}],
+ [{xmlcdata,<<>>}]
+ }]
+ }),
+ StringXML = ProducedXML.
+
stream_start_end_test() ->
false = stream_start_end("stream no xml"),
{xmlstreamstart, _X, _Y} = stream_start_end(""),
diff --git a/src/mod_websocket.erl b/src/mod_websocket.erl
index 167ca17..225ec73 100644
--- a/src/mod_websocket.erl
+++ b/src/mod_websocket.erl
@@ -20,20 +20,25 @@
]).
-include("ejabberd.hrl").
+-define(LAGER, true).
+-include("logger.hrl").
-include("jlib.hrl").
-include("ejabberd_websocket.hrl").
--record(wsdatastate, {legacy=true,
- ft=undefined,
- flen,
- packet= <<>>,
- buffer= <<>>,
- partial= <<>>
+-record(wsdatastate, {legacy=true, %% do we need this?
+ ft=undefined, %% final segment
+ opcode, %% opcode 4 bit size
+ flen=undefined, %% payload length first value (if 126|127 - should use exlen)
+ exlen=undefined, %% extra payload length, 16 or 64 bits in size
+ mask=undefined, %% mask used in payload
+ packet= <<>>, %% exracted packet so fae
+ buffer= <<>>, %% received data so far
+ partial= <<>> %% extra bytes
}).
%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------
-process(Path, Req) ->
- ?DEBUG("Request data:~p:", [Path, Req]),
+process(_Path, Req) ->
+ ?DEBUG("Request data:~p~n", [Req#wsrequest.data]),
%% Validate Origin
case validate_origin(Req#wsrequest.headers) of
true ->
@@ -46,24 +51,37 @@ process(Path, Req) ->
Y ->
Y
end,
- ?DEBUG("Origin is valid.",[]),
+ ?DEBUG("Origin is valid ~p.~n",[Data]),
DState = #wsdatastate {legacy=true,
buffer=Data,
ft=undefined,
partial= <<>> },
- case process_data(DState) of
+ case process_data2(DState) of
{<<>>, Part} when is_binary(Part) ->
+ ?DEBUG("Empty packet~n", []),
{<<>>, Part};
{Out, <<>>} when is_binary(Out) ->
+ ?DEBUG("Request data:~p~n", [Out]),
IP = Req#wsrequest.ip,
%% websocket frame is finished process request
ejabberd_xmpp_websocket:process_request(
Req#wsrequest.wsockmod,
Req#wsrequest.wsocket,
Req#wsrequest.fsmref,
- Out,
+ binary_to_list(Out),
IP);
- Error -> Error %% pass the errors through
+ {_Reason, closed} ->
+ ?DEBUG("Imitating stream close event.", []),
+ IP = Req#wsrequest.ip,
+ ejabberd_xmpp_websocket:process_request(
+ Req#wsrequest.wsockmod,
+ Req#wsrequest.wsocket,
+ Req#wsrequest.fsmref,
+ "",
+ IP);
+ Error ->
+ ?DEBUG("Error occured ~p~n", [Error]),
+ Error %% pass the errors through
end;
_ ->
?DEBUG("Invalid Origin in Request: ~p~n",[Req]),
@@ -97,6 +115,95 @@ validate_origin([]) ->
validate_origin(Headers) ->
is_tuple(lists:keyfind("Origin", 1, Headers)).
+format_output(DState = #wsdatastate{opcode=1}) ->
+ {DState#wsdatastate.packet, DState#wsdatastate.partial};
+format_output(DState = #wsdatastate{opcode=8}) ->
+ {DState#wsdatastate.packet, closed};
+format_output(DState) ->
+ {DState#wsdatastate.packet, DState#wsdatastate.partial}.
+
+%% no buffer?
+process_data2(DState = #wsdatastate{buffer=undefined}) ->
+ ?INFO_MSG("Buffer undefined cought", []),
+ format_output(DState);
+%% buffer is empty
+process_data2(DState = #wsdatastate{buffer= <<>>}) ->
+ ?INFO_MSG("Buffer empty cought", []),
+ format_output(DState);
+%% first part of packet - final/or/not packet, reserved bits, opcode (text/bin data), is maske used (should be)
+%% and short payload size(up to 125 bytes).
+process_data2(DState = #wsdatastate{buffer= <>,
+ ft=undefined}) ->
+ warn_not_masked(Mask),
+ process_data2(DState#wsdatastate{buffer = <>,
+ ft = (Fin == 1),
+ opcode = OpCode,
+ flen = PayLoadLen});
+%% if payload is 16 bytes size
+process_data2(DState = #wsdatastate{buffer= <>,
+ flen=126,
+ exlen=undefined}) ->
+ process_data2(DState#wsdatastate{buffer = <>,
+ flen = ExLen,
+ exlen = ExLen});
+%% if payload is 64 bytes size
+process_data2(DState = #wsdatastate{buffer= <>,
+ flen=127,
+ exlen=undefined}) ->
+ warn_first_bit_should_not_be_one(FirstBit),
+ process_data2(DState#wsdatastate{buffer = <>,
+ flen = ExLen,
+ exlen = ExLen});
+%% exract mask
+process_data2(DState = #wsdatastate{buffer = <>,
+ mask = undefined}) ->
+ process_data2(DState#wsdatastate{buffer = <>,
+ mask = Mask});
+%%
+process_data2(DState = #wsdatastate{buffer = <>,
+ packet = <>,
+ mask = Mask,
+ flen = Len}) when Len > 3 ->
+ process_data2(DState#wsdatastate{buffer = <>,
+ flen = Len - 4,
+ packet = <>});
+process_data2(DState = #wsdatastate{buffer = <>,
+ packet = Packet,
+ mask = Mask,
+ flen = 3}) ->
+ process_data2(DState#wsdatastate{buffer = <>,
+ flen = 0,
+ packet = <>});
+process_data2(DState = #wsdatastate{buffer = <>,
+ packet = Packet,
+ mask = Mask,
+ flen = 2}) ->
+ process_data2(DState#wsdatastate{buffer = <>,
+ flen = 0,
+ packet = <>});
+process_data2(DState = #wsdatastate{buffer = <>,
+ packet = Packet,
+ mask = Mask,
+ flen = 1}) ->
+ process_data2(DState#wsdatastate{buffer = <>,
+ flen = 0,
+ packet = <>});
+process_data2(DState = #wsdatastate{buffer = <>,
+ flen = 0}) ->
+ process_data2(DState#wsdatastate{buffer = <<>>,
+ partial = <>}).
+
+
+
+
+%% This basically should be replaced with socket drop routine
+warn_not_masked(1) -> ok;
+warn_not_masked(_) -> ?WARNING_MSG("Payload was not masked!~n", []).
+
+%% Same, data size should not be too big. %x0000000000000000-7FFFFFFFFFFFFFFF :)
+warn_first_bit_should_not_be_one(0) -> ok;
+warn_first_bit_should_not_be_one(_) -> ?WARNING_MSG("64-bit extra payload length should not start with 1!~n", []).
+
process_data(DState = #wsdatastate{buffer=undefined}) ->
{DState#wsdatastate.packet, DState#wsdatastate.partial};
process_data(DState = #wsdatastate{buffer= <<>>}) ->