Skip to content

Commit

Permalink
* added support to websocket protocol hixie draft 76 (thanks to serg…
Browse files Browse the repository at this point in the history
…io veiga)

 * added support to multiple websocket draft protocols (for backwards compatibility)
 * added ws_autoexit option which allows to get an event on websocket controlling processes (issue track #15, suggestion of esente)
 * added headers also in misultin websockets (thanks to jlirochon)
 * made it basho's rebar friendly (thanks to mrinalwadhwa)
  • Loading branch information
ostinelli committed Aug 11, 2010
1 parent ce00531 commit 7374dac
Show file tree
Hide file tree
Showing 13 changed files with 328 additions and 68 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ ERLC_FLAGS := -W -I $(INCLUDE_DIR) -o $(EBIN_DIR)
all: all:
@mkdir -p $(EBIN_DIR) @mkdir -p $(EBIN_DIR)
$(ERLC) $(ERLC_FLAGS) $(SRC_DIR)/*.erl $(ERLC) $(ERLC_FLAGS) $(SRC_DIR)/*.erl
@cp $(SRC_DIR)/*.app $(EBIN_DIR)/ @cp $(SRC_DIR)/misultin.app.src $(EBIN_DIR)/misultin.app


clean: clean:
@rm -rf $(EBIN_DIR)/* @rm -rf $(EBIN_DIR)/*
Expand All @@ -17,10 +17,10 @@ clean:
debug: debug:
@mkdir -p $(EBIN_DIR) @mkdir -p $(EBIN_DIR)
$(ERLC) -D log_debug $(ERLC_FLAGS) $(SRC_DIR)/*.erl $(ERLC) -D log_debug $(ERLC_FLAGS) $(SRC_DIR)/*.erl
@cp $(SRC_DIR)/*.app $(EBIN_DIR)/ @cp $(SRC_DIR)/misultin.app.src $(EBIN_DIR)/misultin.app


example: example:
@mkdir -p $(EBIN_DIR) @mkdir -p $(EBIN_DIR)
$(ERLC) $(ERLC_FLAGS) $(SRC_DIR)/*.erl $(ERLC) $(ERLC_FLAGS) $(SRC_DIR)/*.erl
@cp $(SRC_DIR)/*.app $(EBIN_DIR)/ @cp $(SRC_DIR)/misultin.app.src $(EBIN_DIR)/misultin.app
$(ERLC) $(ERLC_FLAGS) $(EXAMPLES_DIR)/*.erl $(ERLC) $(ERLC_FLAGS) $(EXAMPLES_DIR)/*.erl
6 changes: 6 additions & 0 deletions README.txt
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ API Documentation is available online on the Misultin's wiki: http://code.google
CHANGELOG CHANGELOG
========================================================================================================== ==========================================================================================================


0.6.1: - added support to websocket protocol hixie draft 76 [thanks to sergio veiga]
- added support to multiple websocket draft protocols [for backwards compatibility]
- added ws_autoexit option which allows to get an event on websocket controlling processes [issue track #15, suggestion of esente]
- added headers also in misultin websockets [thanks to jlirochon]
- made it basho's rebar friendly [thanks to mrinalwadhwa]

0.6: - added HTTP compression option 0.6: - added HTTP compression option
- refactoring of the main server loop, so that it is now isolated from the HTTP functionality - refactoring of the main server loop, so that it is now isolated from the HTTP functionality
- removed unnecessary compilation warnings - removed unnecessary compilation warnings
Expand Down
103 changes: 103 additions & 0 deletions examples/misultin_websocket_event_example.erl
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,103 @@
% ==========================================================================================================
% MISULTIN - Example: Shows misultin Websocket With an event support.
%
% >-|-|-(°>
%
% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
%
% Redistribution and use in source and binary forms, with or without modification, are permitted provided
% that the following conditions are met:
%
% * Redistributions of source code must retain the above copyright notice, this list of conditions and the
% following disclaimer.
% * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
% the following disclaimer in the documentation and/or other materials provided with the distribution.
% * Neither the name of the authors nor the names of its contributors may be used to endorse or promote
% products derived from this software without specific prior written permission.
%
% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
% WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
% ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
% HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
% POSSIBILITY OF SUCH DAMAGE.
% ==========================================================================================================
-module(misultin_websocket_event_example).
-export([start/1, stop/0]).

% start misultin http server
start(Port) ->
misultin:start_link([
{port, Port}, {loop, fun(Req) -> handle_http(Req, Port) end},
{ws_loop, fun(Ws) -> handle_websocket(Ws) end}, {ws_autoexit, false}
]).

% stop misultin
stop() ->
misultin:stop().

% callback on request received
handle_http(Req, Port) ->
% output
Req:ok([{"Content-Type", "text/html"}],
["
<html>
<head>
<script type=\"text/javascript\">
function addStatus(text){
var date = new Date();
document.getElementById('status').innerHTML = document.getElementById('status').innerHTML + date + \": \" + text + \"<br>\";
}
function ready(){
if (\"WebSocket\" in window) {
// browser supports websockets
var ws = new WebSocket(\"ws://localhost:", integer_to_list(Port) ,"/service\");
ws.onopen = function() {
// websocket is connected
addStatus(\"websocket connected!\");
// send hello data to server.
ws.send(\"hello server!\");
addStatus(\"sent message to server: 'hello server'!\");
};
ws.onmessage = function (evt) {
var receivedMsg = evt.data;
addStatus(\"server sent the following: '\" + receivedMsg + \"'\");
};
ws.onclose = function() {
// websocket was closed
addStatus(\"websocket was closed\");
};
} else {
// browser does not support websockets
addStatus(\"sorry, your browser does not support websockets.\");
}
}
</script>
</head>
<body onload=\"ready();\">
<div id=\"status\"></div>
</body>
</html>"]).

% callback on received websockets data
handle_websocket(Ws) ->
receive
{browser, Data} ->
Ws:send(["received '", Data, "'"]),
handle_websocket(Ws);
closed ->
% IMPORTANT: since we specified the {ws_autoexit, false} option, we need to manually ensure that this process exists
% [otherwise it will become a zombie]
io:format("The WebSocket was CLOSED!~n"),
closed;
_Ignore ->
handle_websocket(Ws)
after 5000 ->
Ws:send("pushing!"),
handle_websocket(Ws)
end.
8 changes: 6 additions & 2 deletions include/misultin.hrl
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
compress, % send compressed output if supported by browser compress, % send compressed output if supported by browser
stream_support, % stream support option stream_support, % stream support option
loop, % the fun handling requests loop, % the fun handling requests
ws_loop % the fun handling websockets ws_loop, % the loop handling websockets
ws_autoexit % true | false
}). }).


% Request % Request
Expand All @@ -82,10 +83,13 @@
-record(ws, { -record(ws, {
socket, % the socket handling the request socket, % the socket handling the request
socket_mode, % http | ssl socket_mode, % http | ssl
ws_autoexit, % websocket process is automatically killed: true | false
peer_addr, % peer IP | undefined peer_addr, % peer IP | undefined
peer_port, % peer port | undefined peer_port, % peer port | undefined
peer_cert, % undefined | the DER encoded peer certificate that can be decoded with public_key:pkix_decode_cert/2 peer_cert, % undefined | the DER encoded peer certificate that can be decoded with public_key:pkix_decode_cert/2
vsn, % {Maj,Min} | {'draft-hixie', Ver}
origin, % the originator origin, % the originator
host, % the host host, % the host
path % the websocket GET request path path, % the websocket GET request path
headers % [{Tag, Val}]
}). }).
2 changes: 1 addition & 1 deletion make.bat
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ FOR %%f in (examples\*.erl) DO erlc -W %command% -o ebin "%%f"
:COMPILE :COMPILE
mkdir ebin mkdir ebin
FOR %%f in (src\*.erl) DO erlc -W %command% -o ebin "%%f" FOR %%f in (src\*.erl) DO erlc -W %command% -o ebin "%%f"
copy src\misultin.app ebin\misultin.app /Y copy src\misultin.app.src ebin\misultin.app /Y
GOTO END GOTO END


:CLEAN :CLEAN
Expand Down
9 changes: 9 additions & 0 deletions src/misultin.app.src
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,9 @@
{application, misultin,
[
{description, "Lightweight HTTP(s) and Websockets Server Library"},
{vsn, "0.6.1"},
{modules, [misultin, misultin_req, misultin_socket, misultin_http, misultin_utility, misultin_websocket, misultin_ws]},
{registered, [misultin]},
{env, []},
{applications, [kernel, stdlib]}
]}.
8 changes: 5 additions & 3 deletions src/misultin.erl
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
% ========================================================================================================== % ==========================================================================================================
-module(misultin). -module(misultin).
-behaviour(gen_server). -behaviour(gen_server).
-vsn("0.6.0"). -vsn("0.6.1").


% gen_server callbacks % gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
Expand Down Expand Up @@ -107,7 +107,8 @@ init([Options]) ->
{compress, false, fun is_boolean/1, invalid_compress_option}, {compress, false, fun is_boolean/1, invalid_compress_option},
{stream_support, true, fun is_boolean/1, invalid_stream_support_option}, {stream_support, true, fun is_boolean/1, invalid_stream_support_option},
{loop, {error, undefined_loop}, fun is_function/1, loop_not_function}, {loop, {error, undefined_loop}, fun is_function/1, loop_not_function},
{ws_loop, none, fun is_function/1, ws_loop_not_function} {ws_loop, none, fun is_function/1, ws_loop_not_function},
{ws_autoexit, true, fun is_boolean/1, invalid_ws_autoexit_option}
], ],
OptionsVerified = lists:foldl(fun(OptionName, Acc) -> [get_option(OptionName, Options)|Acc] end, [], OptionProps), OptionsVerified = lists:foldl(fun(OptionName, Acc) -> [get_option(OptionName, Options)|Acc] end, [], OptionProps),
case proplists:get_value(error, OptionsVerified) of case proplists:get_value(error, OptionsVerified) of
Expand All @@ -124,6 +125,7 @@ init([Options]) ->
StreamSupport = proplists:get_value(stream_support, OptionsVerified), StreamSupport = proplists:get_value(stream_support, OptionsVerified),
Loop = proplists:get_value(loop, OptionsVerified), Loop = proplists:get_value(loop, OptionsVerified),
WsLoop = proplists:get_value(ws_loop, OptionsVerified), WsLoop = proplists:get_value(ws_loop, OptionsVerified),
WsAutoExit = proplists:get_value(ws_autoexit, OptionsVerified),
% ipv6 support % ipv6 support
?LOG_DEBUG("ip address is: ~p", [Ip]), ?LOG_DEBUG("ip address is: ~p", [Ip]),
% set additional options according to socket mode if necessary % set additional options according to socket mode if necessary
Expand Down Expand Up @@ -165,7 +167,7 @@ init([Options]) ->
% set options % set options
OptionsTcp = [binary, {packet, raw}, {ip, Ip}, {reuseaddr, true}, {active, false}, {backlog, Backlog}|AdditionalOptions], OptionsTcp = [binary, {packet, raw}, {ip, Ip}, {reuseaddr, true}, {active, false}, {backlog, Backlog}|AdditionalOptions],
% build custom_opts % build custom_opts
CustomOpts = #custom_opts{compress = Compress, stream_support = StreamSupport, loop = Loop, ws_loop = WsLoop}, CustomOpts = #custom_opts{compress = Compress, stream_support = StreamSupport, loop = Loop, ws_loop = WsLoop, ws_autoexit = WsAutoExit},
% create listening socket and acceptor % create listening socket and acceptor
case create_listener_and_acceptor(Port, OptionsTcp, RecvTimeout, SocketMode, CustomOpts) of case create_listener_and_acceptor(Port, OptionsTcp, RecvTimeout, SocketMode, CustomOpts) of
{ok, ListenSocket, AcceptorPid} -> {ok, ListenSocket, AcceptorPid} ->
Expand Down
17 changes: 9 additions & 8 deletions src/misultin_http.erl
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
% POSSIBILITY OF SUCH DAMAGE. % POSSIBILITY OF SUCH DAMAGE.
% ========================================================================================================== % ==========================================================================================================
-module(misultin_http). -module(misultin_http).
-vsn("0.6.0"). -vsn("0.6.1").


% API % API
-export([handle_data/8]). -export([handle_data/8]).
Expand All @@ -52,7 +52,8 @@
compress, compress,
stream_support, stream_support,
loop, loop,
ws_loop ws_loop,
ws_autoexit
}). }).


% includes % includes
Expand All @@ -64,7 +65,7 @@
% Callback from misultin_socket % Callback from misultin_socket
handle_data(Sock, SocketMode, ListenPort, PeerAddr, PeerPort, PeerCert, RecvTimeout, CustomOpts) -> handle_data(Sock, SocketMode, ListenPort, PeerAddr, PeerPort, PeerCert, RecvTimeout, CustomOpts) ->
% build connection & request records % build connection & request records
C = #c{sock = Sock, socket_mode = SocketMode, port = ListenPort, recv_timeout = RecvTimeout, compress = CustomOpts#custom_opts.compress, stream_support = CustomOpts#custom_opts.stream_support, loop = CustomOpts#custom_opts.loop, ws_loop = CustomOpts#custom_opts.ws_loop}, C = #c{sock = Sock, socket_mode = SocketMode, port = ListenPort, recv_timeout = RecvTimeout, compress = CustomOpts#custom_opts.compress, stream_support = CustomOpts#custom_opts.stream_support, loop = CustomOpts#custom_opts.loop, ws_loop = CustomOpts#custom_opts.ws_loop, ws_autoexit = CustomOpts#custom_opts.ws_autoexit},
Req = #req{socket = Sock, socket_mode = SocketMode, peer_addr = PeerAddr, peer_port = PeerPort, peer_cert = PeerCert}, Req = #req{socket = Sock, socket_mode = SocketMode, peer_addr = PeerAddr, peer_port = PeerPort, peer_cert = PeerCert},
% enter loop % enter loop
request(C, Req). request(C, Req).
Expand Down Expand Up @@ -138,14 +139,14 @@ headers(#c{sock = Sock, socket_mode = SocketMode, recv_timeout = RecvTimeout, ws
CheckWs = case WsLoop of CheckWs = case WsLoop of
none -> false; none -> false;
_Function -> misultin_websocket:check(Path, Headers) _Function -> misultin_websocket:check(Path, Headers)
end, end,
case CheckWs of case CheckWs of
false -> false ->
?LOG_DEBUG("normal http request received", []), ?LOG_DEBUG("normal http request received", []),
body(C, Req#req{headers = Headers}); body(C, Req#req{headers = Headers});
{true, Origin, Host, Path} -> {true, Vsn} ->
?LOG_DEBUG("websocket request received", []), ?LOG_DEBUG("websocket request received", []),
misultin_websocket:connect(#ws{socket = Sock, socket_mode = SocketMode, peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port, origin = Origin, host = Host, path = Path}, WsLoop) misultin_websocket:connect(Req, #ws{vsn = Vsn, socket = Sock, socket_mode = SocketMode, peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port, path = Path, headers = Headers, ws_autoexit = C#c.ws_autoexit}, WsLoop)
end; end;
{SocketMode, Sock, _Other} -> {SocketMode, Sock, _Other} ->
?LOG_DEBUG("tcp error treating headers: ~p, send bad request error back", [_Other]), ?LOG_DEBUG("tcp error treating headers: ~p, send bad request error back", [_Other]),
Expand Down Expand Up @@ -228,10 +229,10 @@ body(#c{sock = Sock, socket_mode = SocketMode, recv_timeout = RecvTimeout} = C,
request(C, #req{socket = Sock, socket_mode = SocketMode, peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port, peer_cert = Req#req.peer_cert}) request(C, #req{socket = Sock, socket_mode = SocketMode, peer_addr = Req#req.peer_addr, peer_port = Req#req.peer_port, peer_cert = Req#req.peer_cert})
end; end;
{error, timeout} -> {error, timeout} ->
?LOG_DEBUG("request timeout, sending error", []), ?LOG_WARNING("request timeout, sending error", []),
misultin_socket:send(Sock, misultin_utility:get_http_status_code(408), SocketMode); misultin_socket:send(Sock, misultin_utility:get_http_status_code(408), SocketMode);
_Other -> _Other ->
?LOG_DEBUG("tcp error treating post data: ~p, send bad request error back", [_Other]), ?LOG_ERROR("tcp error treating post data: ~p, send bad request error back", [_Other]),
misultin_socket:send(Sock, misultin_utility:get_http_status_code(400), SocketMode) misultin_socket:send(Sock, misultin_utility:get_http_status_code(400), SocketMode)
end end
end; end;
Expand Down
2 changes: 1 addition & 1 deletion src/misultin_req.erl
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
% POSSIBILITY OF SUCH DAMAGE. % POSSIBILITY OF SUCH DAMAGE.
% ========================================================================================================== % ==========================================================================================================
-module(misultin_req, [Req, SocketPid]). -module(misultin_req, [Req, SocketPid]).
-vsn("0.6.0"). -vsn("0.6.1").


% macros % macros
-define(PERCENT, 37). % $\% -define(PERCENT, 37). % $\%
Expand Down
2 changes: 1 addition & 1 deletion src/misultin_socket.erl
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
% POSSIBILITY OF SUCH DAMAGE. % POSSIBILITY OF SUCH DAMAGE.
% ========================================================================================================== % ==========================================================================================================
-module(misultin_socket). -module(misultin_socket).
-vsn("0.6.0"). -vsn("0.6.1").


% API % API
-export([start_link/5]). -export([start_link/5]).
Expand Down
27 changes: 24 additions & 3 deletions src/misultin_utility.erl
Original file line number Original file line Diff line number Diff line change
@@ -1,5 +1,5 @@
% ========================================================================================================== % ==========================================================================================================
% MISULTIN - Main % MISULTIN - Various Utilities
% %
% >-|-|-(°> % >-|-|-(°>
% %
Expand Down Expand Up @@ -28,10 +28,10 @@
% POSSIBILITY OF SUCH DAMAGE. % POSSIBILITY OF SUCH DAMAGE.
% ========================================================================================================== % ==========================================================================================================
-module(misultin_utility). -module(misultin_utility).
-vsn("0.6.0"). -vsn("0.6.1").


% API % API
-export([get_http_status_code/1, get_content_type/1, get_key_value/2]). -export([get_http_status_code/1, get_content_type/1, get_key_value/2, header_get_value/2]).




% ============================ \/ API ====================================================================== % ============================ \/ API ======================================================================
Expand Down Expand Up @@ -327,6 +327,27 @@ get_key_value(Key, List)->
{_K, Value}-> Value {_K, Value}-> Value
end. end.


% Function: Value | false
% Description: Find atom Tag in Headers, Headers being both atoms [for known headers] and strings. Comparison on string Header Tags is case insensitive.
header_get_value(Tag, Headers) when is_atom(Tag) ->
case lists:keyfind(Tag, 1, Headers) of
false ->
% header not found, test also conversion to string -> convert all string tags to lowercase (HTTP tags are case insensitive)
F = fun({HTag, HValue}) ->
case is_atom(HTag) of
true -> {HTag, HValue};
false -> {string:to_lower(HTag), HValue}
end
end,
HeadersStr = lists:map(F, Headers),
% test
case lists:keyfind(string:to_lower(atom_to_list(Tag)), 1, HeadersStr) of
false -> false;
{_, Value} -> Value
end;
{_, Value} -> Value
end.

% ============================ /\ API ====================================================================== % ============================ /\ API ======================================================================




Expand Down
Loading

0 comments on commit 7374dac

Please sign in to comment.