PageRenderTime 58ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/ostinelli-misultin-1973f5c/src/misultin_websocket.erl

#
Erlang | 279 lines | 181 code | 22 blank | 76 comment | 2 complexity | bcc4e27252d620a3c02bf7a83db80a4a MD5 | raw file
Possible License(s): BSD-3-Clause
  1. % ==========================================================================================================
  2. % MISULTIN - WebSocket
  3. %
  4. % >-|-|-(?>
  5. %
  6. % Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>.
  7. % All rights reserved.
  8. %
  9. % Code portions from Joe Armstrong have been originally taken under MIT license at the address:
  10. % <http://armstrongonsoftware.blogspot.com/2009/12/comet-is-dead-long-live-websockets.html>
  11. %
  12. % BSD License
  13. %
  14. % Redistribution and use in source and binary forms, with or without modification, are permitted provided
  15. % that the following conditions are met:
  16. %
  17. % * Redistributions of source code must retain the above copyright notice, this list of conditions and the
  18. % following disclaimer.
  19. % * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
  20. % the following disclaimer in the documentation and/or other materials provided with the distribution.
  21. % * Neither the name of the authors nor the names of its contributors may be used to endorse or promote
  22. % products derived from this software without specific prior written permission.
  23. %
  24. % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
  25. % WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
  26. % PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
  27. % ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
  28. % TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  29. % HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  30. % NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  31. % POSSIBILITY OF SUCH DAMAGE.
  32. % ==========================================================================================================
  33. -module(misultin_websocket).
  34. -vsn("0.9").
  35. % API
  36. -export([check/3, connect/5]).
  37. -export([check_headers/2, websocket_close/4, ws_loop/4, send_to_browser/2, get_wsinfo/2, session_cmd/2]).
  38. % behaviour
  39. -export([behaviour_info/1]).
  40. % includes
  41. -include("../include/misultin.hrl").
  42. % ============================ \/ API ======================================================================
  43. % Check if the incoming request is a websocket handshake.
  44. -spec check(WsVersions::[websocket_version()], Path::string(), Headers::http_headers()) -> false | {true, Vsn::websocket_version()}.
  45. check(WsVersions, _Path, Headers) ->
  46. ?LOG_DEBUG("testing for a websocket request path: ~p headers: ~p", [_Path, Headers]),
  47. % checks
  48. check_websockets(WsVersions, Headers).
  49. % Connect and handshake with Websocket.
  50. -spec connect(ServerRef::pid(), SessionsRef::pid(), Req::#req{}, Ws::#ws{}, WsLoop::function()) -> true.
  51. connect(ServerRef, SessionsRef, #req{headers = Headers} = Req, #ws{vsn = Vsn, socket = Socket, socket_mode = SocketMode, path = Path} = Ws, WsLoop) ->
  52. ?LOG_DEBUG("building handshake response", []),
  53. % get data
  54. Origin = case misultin_utility:header_get_value('Sec-Websocket-Origin', Headers) of
  55. false ->
  56. misultin_utility:header_get_value('Origin', Headers);
  57. OriginHeader ->
  58. OriginHeader
  59. end,
  60. Host = misultin_utility:header_get_value('Host', Headers),
  61. % build handshake
  62. VsnMod = get_module_name_from_vsn(Vsn),
  63. HandshakeServer = VsnMod:handshake(Req, Headers, {Path, Origin, Host}),
  64. % send handshake back
  65. misultin_socket:send(Socket, HandshakeServer, SocketMode),
  66. % add data to ws record and spawn_link controlling process
  67. WsT = {misultin_ws, self()},
  68. WsHandleLoopPid = spawn_link(fun() -> WsLoop(WsT) end),
  69. % trap exit
  70. process_flag(trap_exit, true),
  71. % set opts
  72. misultin_socket:setopts(Socket, [{packet, 0}], SocketMode),
  73. % add main websocket pid to misultin server reference
  74. misultin_server:ws_pid_ref_add(ServerRef, self()),
  75. % enter loop
  76. enter_loop(WsHandleLoopPid, SessionsRef, Ws, Req#req.headers, Origin, Host).
  77. % Check if headers correspond to headers requirements.
  78. -spec check_headers(Headers::http_headers(), RequiredHeaders::http_headers()) -> true | http_headers().
  79. check_headers(Headers, RequiredHeaders) ->
  80. F = fun({Tag, Val}) ->
  81. % see if the required Tag is in the Headers
  82. case misultin_utility:header_get_value(Tag, Headers) of
  83. false -> true; % header not found, keep in list
  84. HVal ->
  85. case Val of
  86. ignore -> false; % ignore value -> ok, remove from list
  87. HVal -> false; % expected val -> ok, remove from list
  88. _ ->
  89. % check if header has multiple parameters (for instance FF7 websockets)
  90. not(lists:member(Val,string:tokens(HVal,", ")))
  91. end
  92. end
  93. end,
  94. case lists:filter(F, RequiredHeaders) of
  95. [] -> true;
  96. MissingHeaders -> MissingHeaders
  97. end.
  98. % Close socket and custom handling loop dependency
  99. -spec websocket_close(Socket::socket(), WsHandleLoopPid::pid(), SocketMode::socketmode(), WsAutoExit::boolean()) -> ok.
  100. websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit) ->
  101. case WsAutoExit of
  102. true ->
  103. % kill custom handling loop process
  104. exit(WsHandleLoopPid, kill);
  105. false ->
  106. % the killing of the custom handling loop process is handled in the loop itself -> send event
  107. WsHandleLoopPid ! closed
  108. end,
  109. % close main socket
  110. misultin_socket:close(Socket, SocketMode).
  111. % send to browser
  112. -spec send_to_browser(WsHandleLoopPid::pid(), Data::iolist() | binary()) -> {browser, Data::list()}.
  113. send_to_browser(WsHandleLoopPid, Data) ->
  114. WsHandleLoopPid ! {browser, Data}.
  115. % get ws info from websocket process handler
  116. -spec get_wsinfo(SocketPid::pid(), WsInfo::atom()) -> term().
  117. get_wsinfo(SocketPid, WsInfo) ->
  118. misultin_utility:call(SocketPid, {wsinfo, WsInfo}).
  119. % send a session command to the ws process handler
  120. -spec session_cmd(SocketPid::pid(), SessionCmd::term()) -> term().
  121. session_cmd(SocketPid, SessionCmd) ->
  122. misultin_utility:call(SocketPid, {session_cmd, SessionCmd}).
  123. % ============================ /\ API ======================================================================
  124. % ============================ \/ INTERNAL FUNCTIONS =======================================================
  125. % behaviour
  126. behaviour_info(callbacks) ->
  127. [
  128. {check_websocket, 1},
  129. {handshake, 3},
  130. {handle_data, 3},
  131. {send_format, 2}
  132. ];
  133. behaviour_info(_) ->
  134. undefined.
  135. % Loop to check for all available supported websocket protocols.
  136. -spec check_websockets(VsnSupported::[websocket_version()], Headers::http_headers()) -> false | {true, Vsn::websocket_version()}.
  137. check_websockets([], _Headers) -> false;
  138. check_websockets([Vsn|T], Headers) ->
  139. ?LOG_DEBUG("testing for websocket protocol ~p", [Vsn]),
  140. VsnMod = get_module_name_from_vsn(Vsn),
  141. case VsnMod:check_websocket(Headers) of
  142. false -> check_websockets(T, Headers);
  143. true -> {true, Vsn}
  144. end.
  145. % enter loop
  146. -spec enter_loop(WsHandleLoopPid::pid(), SessionsRef::pid(), Ws::#ws{}, Headers::http_headers(), Origin::string(), Host::string()) -> true.
  147. enter_loop(WsHandleLoopPid, SessionsRef, Ws, Headers, Origin, Host) ->
  148. % start listening for incoming data
  149. ws_loop(WsHandleLoopPid, SessionsRef, Ws#ws{headers = Headers, origin = Origin, host = Host}, undefined),
  150. % unlink
  151. process_flag(trap_exit, false),
  152. erlang:unlink(WsHandleLoopPid).
  153. % Main Websocket loop
  154. -spec ws_loop(WsHandleLoopPid::pid(), SessionsRef::pid(), Ws::#ws{}, State::term()) -> ok.
  155. ws_loop(WsHandleLoopPid, SessionsRef, #ws{vsn = Vsn, socket = Socket, socket_mode = SocketMode, ws_autoexit = WsAutoExit} = Ws, State) ->
  156. misultin_socket:setopts(Socket, [{active, once}], SocketMode),
  157. receive
  158. {tcp, Socket, Data} ->
  159. handle_data_receive(SessionsRef, WsHandleLoopPid, Data, Ws, State);
  160. {ssl, Socket, Data} ->
  161. handle_data_receive(SessionsRef, WsHandleLoopPid, Data, Ws, State);
  162. {WsHandleLoopPid, {wsinfo, WsInfo}} ->
  163. WsResponse = case WsInfo of
  164. raw -> Ws;
  165. socket -> Ws#ws.socket;
  166. socket_mode -> Ws#ws.socket_mode;
  167. peer_addr -> Ws#ws.peer_addr;
  168. peer_port -> Ws#ws.peer_port;
  169. peer_cert -> Ws#ws.peer_cert;
  170. vsn -> Ws#ws.vsn;
  171. origin -> Ws#ws.origin;
  172. host -> Ws#ws.host;
  173. path -> Ws#ws.path;
  174. headers -> Ws#ws.headers
  175. end,
  176. ?LOG_DEBUG("received ws info for: ~p, responding with ~p", [WsInfo, WsResponse]),
  177. misultin_utility:respond(WsHandleLoopPid, WsResponse),
  178. ws_loop(WsHandleLoopPid, SessionsRef, Ws, State);
  179. {CallerPid, {session_cmd, SessionCmd}} ->
  180. ?LOG_DEBUG("received a session command: ~p", [SessionCmd]),
  181. case misultin_utility:get_peer(Ws#ws.headers, Ws#ws.peer_addr) of
  182. {error, Reason} ->
  183. ?LOG_DEBUG("error getting remote peer_addr: ~p, cannot get session", [Reason]),
  184. misultin_utility:respond(CallerPid, {error, {peer_addr, Reason}}),
  185. ws_loop(WsHandleLoopPid, SessionsRef, Ws, State);
  186. {ok, PeerAddr} ->
  187. ?LOG_DEBUG("got remote peer_addr: ~p", [PeerAddr]),
  188. case SessionCmd of
  189. {session, Cookies} ->
  190. % websocket cannot create sessions since they don't generate set-cookies
  191. case misultin_sessions:session(SessionsRef, Cookies, PeerAddr, false) of
  192. {error, Reason} ->
  193. ?LOG_DEBUG("error getting session: ~p", [Reason]),
  194. misultin_utility:respond(CallerPid, {error, Reason});
  195. SessionInfo ->
  196. ?LOG_DEBUG("got session info: ~p", [SessionInfo]),
  197. % respond with session id
  198. misultin_utility:respond(CallerPid, SessionInfo),
  199. % loop
  200. ws_loop(WsHandleLoopPid, SessionsRef, Ws, State)
  201. end;
  202. {save_session_state, SessionId, SessionState} ->
  203. % save session state
  204. ?LOG_DEBUG("trying to save new session state: ~p", [SessionState]),
  205. Response = misultin_sessions:save_session_state(SessionsRef, SessionId, SessionState, PeerAddr),
  206. misultin_utility:respond(CallerPid, Response),
  207. % loop
  208. ws_loop(WsHandleLoopPid, SessionsRef, Ws, State)
  209. end
  210. end;
  211. {tcp_closed, Socket} ->
  212. ?LOG_DEBUG("tcp connection was closed, exit", []),
  213. % close websocket and custom controlling loop
  214. websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
  215. {ssl_closed, Socket} ->
  216. ?LOG_DEBUG("ssl tcp connection was closed, exit", []),
  217. % close websocket and custom controlling loop
  218. websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
  219. {'EXIT', WsHandleLoopPid, Reason} ->
  220. case Reason of
  221. normal ->
  222. ?LOG_DEBUG("linked websocket controlling loop stopped.", []);
  223. _ ->
  224. ?LOG_ERROR("linked websocket controlling loop crashed with reason: ~p", [Reason])
  225. end,
  226. % close websocket and custom controlling loop
  227. websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
  228. {send, Data} ->
  229. VsnMod = get_module_name_from_vsn(Vsn),
  230. ?LOG_DEBUG("sending data: ~p to websocket module: ~p", [Data, VsnMod]),
  231. misultin_socket:send(Socket, VsnMod:send_format(Data, State), SocketMode),
  232. ws_loop(WsHandleLoopPid, SessionsRef, Ws, State);
  233. shutdown ->
  234. ?LOG_DEBUG("shutdown request received, closing websocket with pid ~p", [self()]),
  235. % close websocket and custom controlling loop
  236. websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
  237. _Ignored ->
  238. ?LOG_WARNING("received unexpected message, ignoring: ~p", [_Ignored]),
  239. ws_loop(WsHandleLoopPid, SessionsRef, Ws, State)
  240. end.
  241. -spec handle_data_receive(SessionsRef::pid(), WsHandleLoopPid::pid(), Data::binary(), Ws::#ws{}, State::term()) -> ok.
  242. handle_data_receive(SessionsRef, WsHandleLoopPid, Data, #ws{vsn = Vsn, socket = Socket, socket_mode = SocketMode, ws_autoexit = WsAutoExit} = Ws, State) ->
  243. VsnMod = get_module_name_from_vsn(Vsn),
  244. case VsnMod:handle_data(Data, State, {Socket, SocketMode, WsHandleLoopPid}) of
  245. websocket_close ->
  246. misultin_websocket:websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
  247. {websocket_close, CloseData} ->
  248. misultin_socket:send(Socket, CloseData, SocketMode),
  249. misultin_websocket:websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
  250. NewState ->
  251. ws_loop(WsHandleLoopPid, SessionsRef, Ws, NewState)
  252. end.
  253. % convert websocket version to module name
  254. -spec get_module_name_from_vsn(Vsn::websocket_version()) -> atom().
  255. get_module_name_from_vsn(Vsn) ->
  256. list_to_atom("misultin_websocket_" ++ atom_to_list(Vsn)).
  257. % ============================ /\ INTERNAL FUNCTIONS =======================================================