Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

- added max_connections options parameter, which specifies maximum co…

…ncurrent open connections accepted by the server

- added post_max_size options parameter, which sets the maximum size of POST data
- added get_url_max_size options parameter, which sets the maximum length of URI
- added CHUNKED support, both for incoming requests and outgoing responses [thanks to yrashk suggestion]
- added trapping of client closing a browser in Comet applications [thanks to yrash]
- added SSL support for websockets [enhancement track #25, thanks to viplifes]
- added Comet Long Polling example
- added Comet iFrame example
- added the killing of alive processes on server shutdown
- the GET uri parameters are now also available on POST requests
- additional minor adjustments
  • Loading branch information...
commit b6f52866953b17339c2807fbde137d15e22720f0 1 parent 6c3d195
@ostinelli authored
Showing with 1,072 additions and 224 deletions.
  1. +1 −1  LICENSE.txt
  2. +16 −1 README.txt
  3. +56 −0 examples/misultin_chunked.erl
  4. +169 −0 examples/misultin_comet_iframe.erl
  5. +175 −0 examples/misultin_comet_iframe_event.erl
  6. +83 −0 examples/misultin_comet_long_polling.erl
  7. +1 −1  examples/misultin_compress.erl
  8. +4 −4 examples/misultin_echo.erl
  9. +1 −1  examples/misultin_file.erl
  10. +1 −1  examples/misultin_gen_server.erl
  11. +1 −1  examples/misultin_get_variable.erl
  12. +1 −1  examples/misultin_hello_world.erl
  13. +1 −1  examples/misultin_rest.erl
  14. +1 −1  examples/misultin_ssl.erl
  15. +1 −1  examples/misultin_stream.erl
  16. +2 −3 examples/misultin_websocket_event_example.erl
  17. +1 −1  examples/misultin_websocket_example.erl
  18. +100 −0 examples/misultin_websocket_example_ssl.erl
  19. +4 −1 include/misultin.hrl
  20. +1 −1  src/misultin.app.src
  21. +109 −32 src/misultin.erl
  22. +261 −140 src/misultin_http.erl
  23. +24 −7 src/misultin_req.erl
  24. +32 −14 src/misultin_socket.erl
  25. +3 −2 src/misultin_utility.erl
  26. +21 −7 src/misultin_websocket.erl
  27. +2 −2 src/misultin_ws.erl
View
2  LICENSE.txt
@@ -4,7 +4,7 @@ MISULTIN - An Erlang library for building fast lightweight HTTP servers.
>-|-|-(°>
-Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>, Joe Armstrong, Sean Hinde,
+Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>, Joe Armstrong, Sean Hinde,
Bob Ippolito <bob@mochimedia.com> for Mochi Media, Inc.
All rights reserved.
View
17 README.txt
@@ -63,12 +63,27 @@ API Documentation is available online on the Misultin's wiki: http://code.google
CHANGELOG
==========================================================================================================
+0.7: - added max_connections options parameter, which specifies maximum concurrent open connections
+ accepted by the server
+ - added post_max_size options parameter, which sets the maximum size of POST data
+ - added get_url_max_size options parameter, which sets the maximum length of URI
+ - added CHUNKED support, both for incoming requests and outgoing responses [thanks to yrashk
+ suggestion]
+ - added trapping of client closing a browser in Comet applications [thanks to yrash]
+ - added SSL support for websockets [enhancement track #25, thanks to viplifes]
+ - added Comet Long Polling example
+ - added Comet iFrame example
+ - added the killing of alive processes on server shutdown
+ - the GET uri parameters are now also available on POST requests
+ - additional minor adjustments
+
0.6.2: - refactored to considerably improve sending of static files
- minor bug corrections
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 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]
View
56 examples/misultin_chunked.erl
@@ -0,0 +1,56 @@
+% ==========================================================================================================
+% MISULTIN - Example: Chunk.
+%
+% >-|-|-(°>
+%
+% Copyright (C) 2011, 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_chunked).
+-export([start/1, stop/0]).
+
+% start misultin http server
+start(Port) ->
+ misultin:start_link([{port, Port}, {loop, fun(Req) -> handle_http(Req) end}]).
+
+% stop misultin
+stop() ->
+ misultin:stop().
+
+% callback on request received
+handle_http(Req) ->
+ % send headers
+ Req:chunk(head, [{"Content-Type", "text/html"}]),
+ % send chunk
+ Req:chunk("Sending CHUNK 1<br/>"),
+ timer:sleep(2000),
+ % send chunk
+ Req:chunk("Sending CHUNK 2<br/>"),
+ timer:sleep(2000),
+ % send chunk
+ Req:chunk("Sending CHUNK 3<br/>"),
+ % close
+ Req:chunk(done).
+
+
View
169 examples/misultin_comet_iframe.erl
@@ -0,0 +1,169 @@
+% ==========================================================================================================
+% MISULTIN - Example: Comet - iFrame Method
+%
+% >-|-|-(°>
+%
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>, Example taken from
+% <http://www.zeitoun.net/articles/comet_and_php/start>
+% 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_comet_iframe).
+-export([start/1, stop/0]).
+
+% start misultin http server
+start(Port) ->
+ misultin:start_link([{port, Port}, {loop, fun(Req) -> handle_http(Req, Port) end}]).
+
+% stop misultin
+stop() ->
+ misultin:stop().
+
+handle_http(Req, Port) ->
+ % dispatch to rest
+ handle(Req:get(method), Req:resource([lowercase, urldecode]), Req, Port).
+
+% handle a GET on /
+handle('GET', [], Req, Port) ->
+ % output
+ Req:ok([{"Content-Type", "text/html"}],
+ ["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">
+<html xmlns=\"http://www.w3.org/1999/xhtml\">
+ <head>
+ <title>Comet demo</title>
+ <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />
+ <script type=\"text/javascript\" src=\"http://ajax.googleapis.com/ajax/libs/prototype/1.7.0.0/prototype.js\"></script>
+ </head>
+ <body>
+ <div id=\"content\">The server time will be shown here in 5 seconds.</div>
+ <script type=\"text/javascript\">
+ var comet = {
+ connection: false,
+ iframediv: false,
+ initialize: function() {
+ if (navigator.appVersion.indexOf(\"MSIE\") != -1) {
+ // For IE browsers
+ comet.connection = new ActiveXObject(\"htmlfile\");
+ comet.connection.open();
+ comet.connection.write(\"<html>\");
+ comet.connection.write(\"<script>document.domain = '\"+document.domain+\"'\");
+ comet.connection.write(\"</html>\");
+ comet.connection.close();
+ comet.iframediv = comet.connection.createElement(\"div\");
+ comet.connection.appendChild(comet.iframediv);
+ comet.connection.parentWindow.comet = comet;
+ comet.iframediv.innerHTML = \"<iframe id='comet_iframe' src='http://localhost:", integer_to_list(Port), "/comet'></iframe>\";
+ } else if (navigator.appVersion.indexOf(\"KHTML\") != -1) {
+ // for KHTML browsers
+ comet.connection = document.createElement('iframe');
+ comet.connection.setAttribute('id', 'comet_iframe');
+ comet.connection.setAttribute('src', 'http://localhost:", integer_to_list(Port), "/comet');
+ with (comet.connection.style) {
+ position = \"absolute\";
+ left = top = \"-100px\";
+ height = width = \"1px\";
+ visibility = \"hidden\";
+ }
+ document.body.appendChild(comet.connection);
+ } else {
+ // For other browser (Firefox...)
+ comet.connection = document.createElement('iframe');
+ comet.connection.setAttribute('id', 'comet_iframe');
+ with (comet.connection.style) {
+ left = top = \"-100px\";
+ height = width = \"1px\";
+ visibility = \"hidden\";
+ display = 'none';
+ }
+ comet.iframediv = document.createElement('iframe');
+ comet.iframediv.setAttribute('src', 'http://localhost:", integer_to_list(Port), "/comet');
+ comet.connection.appendChild(comet.iframediv);
+ document.body.appendChild(comet.connection);
+ }
+ },
+ // this function will be called from /comet
+ printServerTime: function (time) {
+ $('content').innerHTML = time;
+ },
+ onUnload: function() {
+ if (comet.connection) {
+ comet.connection = false; // release the iframe to prevent problems with IE when reloading the page
+ }
+ }
+ }
+ Event.observe(window, \"load\", comet.initialize);
+ Event.observe(window, \"unload\", comet.onUnload);
+ </script>
+ </body>
+</html>
+ "]);
+
+% handle a GET on /comet
+handle('GET', ["comet"], Req, _Port) ->
+ % send headers
+ Req:stream(head, [{"Content-Type", "text/html"}, {"Cache-Control", "no-cache, must-revalidate"}, {"Expires", "Mon, 26 Jul 1997 05:00:00 GMT"}]),
+ % start the page
+ Req:stream("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">
+<html xmlns=\"http://www.w3.org/1999/xhtml\">
+ <head>
+ <title>Comet php backend</title>
+ <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />
+ </head>
+ <body>
+ <script type=\"text/javascript\">
+ // KHTML browser don't share javascripts between iframes
+ var is_khtml = navigator.appName.match(\"Konqueror\") || navigator.appVersion.match(\"KHTML\");
+ if (is_khtml){
+ var prototypejs = document.createElement('script');
+ prototypejs.setAttribute('type','text/javascript');
+ prototypejs.setAttribute('src','prototype.js');
+ var head = document.getElementsByTagName('head');
+ head[0].appendChild(prototypejs);
+ }
+ // load the comet object
+ var comet = window.parent.comet;
+ </script>
+ "),
+ % enter notification loop
+ notify(Req);
+
+% handle the 404 page not found
+handle(_, _, Req, _Port) ->
+ Req:ok([{"Content-Type", "text/plain"}], "Page not found.").
+
+% notification loop
+notify(Req) ->
+ % send a message every 5 seconds
+ timer:sleep(5000),
+ % get server local time
+ {_Date, {Hour, Minutes, Seconds}} = erlang:localtime(),
+ % send
+ Req:stream(["
+ <script type=\"text/javascript\">
+ comet.printServerTime(\"Server current time is: ", integer_to_list(Hour), ":", integer_to_list(Minutes), ":", integer_to_list(Seconds), ", will be updated in 5 seconds.", "\");
+ </script>
+ "]),
+ % loop
+ notify(Req).
+
+
View
175 examples/misultin_comet_iframe_event.erl
@@ -0,0 +1,175 @@
+% ==========================================================================================================
+% MISULTIN - Example: Comet - iFrame Method
+%
+% >-|-|-(°>
+%
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>, Example taken from
+% <http://www.zeitoun.net/articles/comet_and_php/start>
+% 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_comet_iframe_event).
+-export([start/1, stop/0]).
+
+% start misultin http server
+start(Port) ->
+ misultin:start_link([{port, Port}, {autoexit, false}, {loop, fun(Req) -> handle_http(Req, Port) end}]).
+
+% stop misultin
+stop() ->
+ misultin:stop().
+
+handle_http(Req, Port) ->
+ % dispatch to rest
+ handle(Req:get(method), Req:resource([lowercase, urldecode]), Req, Port).
+
+% handle a GET on /
+handle('GET', [], Req, Port) ->
+ % output
+ Req:ok([{"Content-Type", "text/html"}],
+ ["<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">
+<html xmlns=\"http://www.w3.org/1999/xhtml\">
+ <head>
+ <title>Comet demo</title>
+ <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />
+ <script type=\"text/javascript\" src=\"http://ajax.googleapis.com/ajax/libs/prototype/1.7.0.0/prototype.js\"></script>
+ </head>
+ <body>
+ <div id=\"content\">The server time will be shown here in 5 seconds.</div>
+ <script type=\"text/javascript\">
+ var comet = {
+ connection: false,
+ iframediv: false,
+ initialize: function() {
+ if (navigator.appVersion.indexOf(\"MSIE\") != -1) {
+ // For IE browsers
+ comet.connection = new ActiveXObject(\"htmlfile\");
+ comet.connection.open();
+ comet.connection.write(\"<html>\");
+ comet.connection.write(\"<script>document.domain = '\"+document.domain+\"'\");
+ comet.connection.write(\"</html>\");
+ comet.connection.close();
+ comet.iframediv = comet.connection.createElement(\"div\");
+ comet.connection.appendChild(comet.iframediv);
+ comet.connection.parentWindow.comet = comet;
+ comet.iframediv.innerHTML = \"<iframe id='comet_iframe' src='http://localhost:", integer_to_list(Port), "/comet'></iframe>\";
+ } else if (navigator.appVersion.indexOf(\"KHTML\") != -1) {
+ // for KHTML browsers
+ comet.connection = document.createElement('iframe');
+ comet.connection.setAttribute('id', 'comet_iframe');
+ comet.connection.setAttribute('src', 'http://localhost:", integer_to_list(Port), "/comet');
+ with (comet.connection.style) {
+ position = \"absolute\";
+ left = top = \"-100px\";
+ height = width = \"1px\";
+ visibility = \"hidden\";
+ }
+ document.body.appendChild(comet.connection);
+ } else {
+ // For other browser (Firefox...)
+ comet.connection = document.createElement('iframe');
+ comet.connection.setAttribute('id', 'comet_iframe');
+ with (comet.connection.style) {
+ left = top = \"-100px\";
+ height = width = \"1px\";
+ visibility = \"hidden\";
+ display = 'none';
+ }
+ comet.iframediv = document.createElement('iframe');
+ comet.iframediv.setAttribute('src', 'http://localhost:", integer_to_list(Port), "/comet');
+ comet.connection.appendChild(comet.iframediv);
+ document.body.appendChild(comet.connection);
+ }
+ },
+ // this function will be called from /comet
+ printServerTime: function (time) {
+ $('content').innerHTML = time;
+ },
+ onUnload: function() {
+ if (comet.connection) {
+ comet.connection = false; // release the iframe to prevent problems with IE when reloading the page
+ }
+ }
+ }
+ Event.observe(window, \"load\", comet.initialize);
+ Event.observe(window, \"unload\", comet.onUnload);
+ </script>
+ </body>
+</html>
+ "]);
+
+% handle a GET on /comet
+handle('GET', ["comet"], Req, _Port) ->
+ % send headers
+ Req:stream(head, [{"Content-Type", "text/html"}, {"Cache-Control", "no-cache, must-revalidate"}, {"Expires", "Mon, 26 Jul 1997 05:00:00 GMT"}]),
+ % start the page
+ Req:stream("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">
+<html xmlns=\"http://www.w3.org/1999/xhtml\">
+ <head>
+ <title>Comet php backend</title>
+ <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />
+ </head>
+ <body>
+ <script type=\"text/javascript\">
+ // KHTML browser don't share javascripts between iframes
+ var is_khtml = navigator.appName.match(\"Konqueror\") || navigator.appVersion.match(\"KHTML\");
+ if (is_khtml){
+ var prototypejs = document.createElement('script');
+ prototypejs.setAttribute('type','text/javascript');
+ prototypejs.setAttribute('src','prototype.js');
+ var head = document.getElementsByTagName('head');
+ head[0].appendChild(prototypejs);
+ }
+ // load the comet object
+ var comet = window.parent.comet;
+ </script>
+ "),
+ % enter notification loop
+ notify(Req);
+
+% handle the 404 page not found
+handle(_, _, Req, _Port) ->
+ Req:ok([{"Content-Type", "text/plain"}], "Page not found.").
+
+% notification loop
+notify(Req) ->
+ % send
+ receive
+ closed ->
+ % IMPORTANT: since we specified the {autoexit, false} option, we need to manually ensure that this process exits
+ % [otherwise it will become a zombie]
+ io:format("The client closed the connection, exiting process!~n");
+ _Ignore ->
+ notify(Req)
+ after 5000 ->
+ % get server local time
+ {_Date, {Hour, Minutes, Seconds}} = erlang:localtime(),
+ % send
+ Req:stream(["
+ <script type=\"text/javascript\">
+ comet.printServerTime(\"Server current time is: ", integer_to_list(Hour), ":", integer_to_list(Minutes), ":", integer_to_list(Seconds), ", will be updated in 5 seconds.", "\");
+ </script>
+ "]),
+ % loop
+ notify(Req)
+ end.
View
83 examples/misultin_comet_long_polling.erl
@@ -0,0 +1,83 @@
+% ==========================================================================================================
+% MISULTIN - Example: Comet - Long Polling Method
+%
+% >-|-|-(°>
+%
+% Copyright (C) 2011, 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_comet_long_polling).
+-export([start/1, stop/0]).
+
+% start misultin http server
+start(Port) ->
+ misultin:start_link([{port, Port}, {loop, fun(Req) -> handle_http(Req, Port) end}]).
+
+% stop misultin
+stop() ->
+ misultin:stop().
+
+handle_http(Req, Port) ->
+ % dispatch to rest
+ handle(Req:get(method), Req:resource([lowercase, urldecode]), Req, Port).
+
+% handle a GET on /
+handle('GET', [], Req, Port) ->
+ % output
+ Req:ok([{"Content-Type", "text/html"}],
+ ["
+ <html>
+ <head>
+ <script type=\"text/javascript\" src=\"http://code.jquery.com/jquery-1.5.1.min.js\"></script>
+ <script type=\"text/javascript\">
+ function misultinComet(){
+ $.get('http://localhost:", integer_to_list(Port), "/comet', {}, function(response){
+ toDiv(response);
+ setTimeout('misultinComet()', 1000);
+ });
+ }
+ function toDiv(content){
+ $('#content').append(content + '<br>');
+ }
+ $(document).ready(function() {
+ misultinComet();
+ });
+ </script>
+ </head>
+ <body>
+ Long Polling example, please wait 10 seconds for incoming data.<br><br>
+ <div id=\"content\"></div>
+ </body>
+ </html>
+ "]);
+
+% handle a GET on /comet
+handle('GET', ["comet"], Req, _Port) ->
+ % simulate a long polling with timer
+ timer:sleep(10000),
+ Req:ok([{"Content-Type", "text/plain"}], ["Message received from Long Polling, next message in 10 seconds."]);
+
+% handle the 404 page not found
+handle(_, _, Req, _Port) ->
+ Req:ok([{"Content-Type", "text/plain"}], "Page not found.").
View
2  examples/misultin_compress.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
View
8 examples/misultin_echo.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
@@ -42,11 +42,11 @@ stop() ->
handle_http(Req) ->
% get params depending on method
Method = Req:get(method),
- case Method of
+ Args = case Method of
'GET' ->
- Args = Req:parse_qs();
+ Req:parse_qs();
'POST' ->
- Args = Req:parse_post()
+ Req:parse_post()
end,
% build an XML with all parameters and values
BuildXml = fun({Param, Value}, Acc) ->
View
2  examples/misultin_file.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
View
2  examples/misultin_gen_server.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
View
2  examples/misultin_get_variable.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
View
2  examples/misultin_hello_world.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
View
2  examples/misultin_rest.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
View
2  examples/misultin_ssl.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
View
2  examples/misultin_stream.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2009, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
View
5 examples/misultin_websocket_event_example.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
@@ -93,8 +93,7 @@ handle_websocket(Ws) ->
closed ->
% IMPORTANT: since we specified the {ws_autoexit, false} option, we need to manually ensure that this process exits
% [otherwise it will become a zombie]
- io:format("The WebSocket was CLOSED!~n"),
- closed;
+ io:format("The WebSocket was CLOSED!~n");
_Ignore ->
handle_websocket(Ws)
after 5000 ->
View
2  examples/misultin_websocket_example.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
View
100 examples/misultin_websocket_example_ssl.erl
@@ -0,0 +1,100 @@
+% ==========================================================================================================
+% MISULTIN - Example: Shows misultin SSL Websocket support.
+%
+% >-|-|-(°>
+%
+% Copyright (C) 2011, 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_example_ssl).
+-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},
+ {ssl, [
+ {certfile, "../priv/test_certificate.pem"},
+ {keyfile, "../priv/test_privkey.pem"},
+ {password, "misultin"}
+ ]}]).
+
+% 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(\"wss://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);
+ _Ignore ->
+ handle_websocket(Ws)
+ after 5000 ->
+ Ws:send("pushing!"),
+ handle_websocket(Ws)
+ end.
View
5 include/misultin.hrl
@@ -1,7 +1,7 @@
% ==========================================================================================================
% MISULTIN - Include file
%
-% Copyright (C) 2009, Sean Hinde, Roberto Ostinelli <roberto@ostinelli.net>
+% Copyright (C) 2011, Sean Hinde, Roberto Ostinelli <roberto@ostinelli.net>
% All rights reserved.
%
% BSD License
@@ -55,9 +55,12 @@
% misultin server Options
-record(custom_opts, {
+ post_max_size, % maximum post size in bytes, defaults to 4 MB
+ get_url_max_size, % maximum GET url size in bytes, defaults to 2000
compress, % send compressed output if supported by browser
stream_support, % stream support option
loop, % the fun handling requests
+ autoexit, % true | false
ws_loop, % the loop handling websockets
ws_autoexit % true | false
}).
View
2  src/misultin.app.src
@@ -1,7 +1,7 @@
{application, misultin,
[
{description, "Lightweight HTTP(s) and Websockets Server Library"},
- {vsn, "0.6.2"},
+ {vsn, "0.7-dev"},
{modules, [misultin, misultin_req, misultin_socket, misultin_http, misultin_utility, misultin_websocket, misultin_ws]},
{registered, [misultin]},
{env, []},
View
141 src/misultin.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>, Sean Hinde.
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>, Sean Hinde.
% All rights reserved.
%
% Code portions from Sean Hinde have been originally taken under BSD license from Trapexit at the address:
@@ -32,16 +32,19 @@
% ==========================================================================================================
-module(misultin).
-behaviour(gen_server).
--vsn("0.6.2").
+-vsn("0.7-dev").
% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
% API
--export([start_link/1, stop/0, persistent_socket_pid_add/1, persistent_socket_pid_remove/1]).
+-export([start_link/1, stop/0]).
+-export([get_open_connections_count/0, http_pid_ref_add/1, http_pid_ref_remove/1, ws_pid_ref_add/1, ws_pid_ref_remove/1]).
% macros
-define(SERVER, ?MODULE).
+-define(APPLICATION_START_RETRYAFTER, 1000). % ms to check that a required application has been started
+-define(APPLICATION_START_RETRYCOUNT, 10). % retry count to check that a required application has been started
% records
-record(state, {
@@ -52,7 +55,10 @@
options,
acceptor,
recv_timeout,
- persistent_sock_references = [],
+ max_connections = 1024, % maximum allowed simultaneous connections
+ open_connections_count = 0,
+ http_pid_ref = [],
+ ws_pid_ref = [],
% misultin
custom_opts
}).
@@ -73,14 +79,29 @@ start_link(Options) when is_list(Options) ->
stop() ->
gen_server:cast(?SERVER, stop).
+% Function -> integer()
+% Description: Gets the count of the current open connections
+get_open_connections_count() ->
+ gen_server:call(?SERVER, get_open_connections_count).
+
+% Function -> ok
+% Description: Adds a new http pid reference to status
+http_pid_ref_add(HttpPid) ->
+ gen_server:cast(?SERVER, {add_http_pid, HttpPid}).
+
% Function -> ok
-% Description: Adds a new persistent socket pid reference to status
-persistent_socket_pid_add(WsPid) ->
+% Description: Remove a http pid reference from status
+http_pid_ref_remove(HttpPid) ->
+ gen_server:cast(?SERVER, {remove_http_pid, HttpPid}).
+
+% Function -> ok
+% Description: Adds a new websocket pid reference to status
+ws_pid_ref_add(WsPid) ->
gen_server:cast(?SERVER, {add_ws_pid, WsPid}).
% Function -> ok
-% Description: Remove a persistent socket pid reference from status
-persistent_socket_pid_remove(WsPid) ->
+% Description: Remove a websocket pid reference from status
+ws_pid_ref_remove(WsPid) ->
gen_server:cast(?SERVER, {remove_ws_pid, WsPid}).
% ============================ /\ API ======================================================================
@@ -102,10 +123,14 @@ init([Options]) ->
{port, 80, fun is_integer/1, port_not_integer},
{backlog, 128, fun is_integer/1, backlog_not_integer},
{recv_timeout, 30*1000, fun is_integer/1, recv_timeout_not_integer},
+ {max_connections, 1024, fun is_integer/1, invalid_max_connections_option},
{ssl, false, fun check_ssl_options/1, invalid_ssl_options},
% misultin
+ {post_max_size, 4*1012, fun is_integer/1, invalid_post_max_size_option}, % defaults to 4 MB
+ {get_url_max_size, 2000, fun is_integer/1, invalid_get_url_max_size_option},
{compress, false, fun is_boolean/1, invalid_compress_option},
{loop, {error, undefined_loop}, fun is_function/1, loop_not_function},
+ {autoexit, true, fun is_boolean/1, invalid_autoexit_option},
{ws_loop, none, fun is_function/1, ws_loop_not_function},
{ws_autoexit, true, fun is_boolean/1, invalid_ws_autoexit_option}
],
@@ -118,10 +143,14 @@ init([Options]) ->
Port = proplists:get_value(port, OptionsVerified),
Backlog = proplists:get_value(backlog, OptionsVerified),
RecvTimeout = proplists:get_value(recv_timeout, OptionsVerified),
+ MaxConnections = proplists:get_value(max_connections, OptionsVerified),
SslOptions0 = proplists:get_value(ssl, OptionsVerified),
% misultin options
+ PostMaxSize = proplists:get_value(post_max_size, OptionsVerified),
+ GetUrlMaxSize = proplists:get_value(get_url_max_size, OptionsVerified),
Compress = proplists:get_value(compress, OptionsVerified),
Loop = proplists:get_value(loop, OptionsVerified),
+ AutoExit = proplists:get_value(autoexit, OptionsVerified),
WsLoop = proplists:get_value(ws_loop, OptionsVerified),
WsAutoExit = proplists:get_value(ws_autoexit, OptionsVerified),
% ipv6 support
@@ -149,7 +178,7 @@ init([Options]) ->
% <http://www.erlang.org/cgi-bin/ezmlm-cgi?4:mss:50633:201004:fpopocbfkpppecdembbe>
AdditionalOptions = [{ssl_imp, new}|SslOptions0],
% start Ssl and crypto applications if necessary, and get outcomes
- AppStartResults = lists:keyfind(error, 1, [start_application(ssl), start_application(crypto)]),
+ AppStartResults = lists:keyfind(error, 1, [start_application(crypto), start_application(public_key), start_application(ssl)]),
case AppStartResults of
false ->
% all applications started succesfully
@@ -165,12 +194,12 @@ init([Options]) ->
% set options
OptionsTcp = [binary, {packet, raw}, {ip, Ip}, {reuseaddr, true}, {active, false}, {backlog, Backlog}|AdditionalOptions],
% build custom_opts
- CustomOpts = #custom_opts{compress = Compress, loop = Loop, ws_loop = WsLoop, ws_autoexit = WsAutoExit},
+ CustomOpts = #custom_opts{post_max_size = PostMaxSize, get_url_max_size = GetUrlMaxSize, compress = Compress, loop = Loop, autoexit = AutoExit, ws_loop = WsLoop, ws_autoexit = WsAutoExit},
% create listening socket and acceptor
- case create_listener_and_acceptor(Port, OptionsTcp, RecvTimeout, SocketMode, CustomOpts) of
+ case create_listener_and_acceptor(Port, OptionsTcp, RecvTimeout, MaxConnections, SocketMode, CustomOpts) of
{ok, ListenSocket, AcceptorPid} ->
?LOG_DEBUG("listening socket and acceptor succesfully started",[]),
- {ok, #state{listen_socket = ListenSocket, socket_mode = SocketMode, port = Port, options = OptionsTcp, acceptor = AcceptorPid, recv_timeout = RecvTimeout, custom_opts = CustomOpts}};
+ {ok, #state{listen_socket = ListenSocket, socket_mode = SocketMode, port = Port, options = OptionsTcp, acceptor = AcceptorPid, recv_timeout = RecvTimeout, max_connections = MaxConnections, custom_opts = CustomOpts}};
{error, Reason} ->
?LOG_ERROR("error starting listener socket: ~p", [Reason]),
{stop, Reason}
@@ -190,6 +219,10 @@ init([Options]) ->
% Description: Handling call messages.
% ----------------------------------------------------------------------------------------------------------
+% current open connections count
+handle_call(get_open_connections_count, _From, #state{open_connections_count = OpenConnectionsCount} = State) ->
+ {reply, OpenConnectionsCount, State};
+
% handle_call generic fallback
handle_call(_Request, _From, State) ->
{reply, undefined, State}.
@@ -204,13 +237,31 @@ handle_cast(stop, State) ->
?LOG_INFO("manual shutdown..", []),
{stop, normal, State};
-% add persistent socket reference to server
-handle_cast({add_ws_pid, PsPid}, #state{persistent_sock_references = PersistentSockReferences} = State) ->
- {noreply, State#state{persistent_sock_references = [PsPid|PersistentSockReferences]}};
-
-% remove persistent socket reference from server
-handle_cast({remove_ws_pid, PsPid}, #state{persistent_sock_references = PersistentSockReferences} = State) ->
- {noreply, State#state{persistent_sock_references = lists:delete(PsPid, PersistentSockReferences)}};
+% add a new http pid reference to status
+handle_cast({add_http_pid, HttpPid}, #state{open_connections_count = OpenConnectionsCount, http_pid_ref = HttpPidRef} = State) ->
+ ?LOG_DEBUG("adding http pid reference ~p", [HttpPid]),
+ % add monitor
+ erlang:monitor(process, HttpPid),
+ % return
+ {noreply, State#state{open_connections_count = OpenConnectionsCount + 1, http_pid_ref = [HttpPid|HttpPidRef]}};
+
+% remove http pid reference from server
+handle_cast({remove_http_pid, HttpPid}, #state{open_connections_count = OpenConnectionsCount, http_pid_ref = HttpPidRef} = State) ->
+ ?LOG_DEBUG("removing http pid reference ~p", [HttpPid]),
+ % remove monitor
+ catch erlang:demonitor(process, HttpPid),
+ % return
+ {noreply, State#state{open_connections_count = OpenConnectionsCount - 1, http_pid_ref = lists:delete(HttpPid, HttpPidRef)}};
+
+% add websocket pid reference to server
+handle_cast({add_ws_pid, WsPid}, #state{ws_pid_ref = WsPidRef} = State) ->
+ ?LOG_DEBUG("adding ws pid reference ~p", [WsPid]),
+ {noreply, State#state{ws_pid_ref = [WsPid|WsPidRef]}};
+
+% remove websocket pid reference from server
+handle_cast({remove_ws_pid, WsPid}, #state{ws_pid_ref = WsPidRef} = State) ->
+ ?LOG_DEBUG("removing ws pid reference ~p", [WsPid]),
+ {noreply, State#state{ws_pid_ref = lists:delete(WsPid, WsPidRef)}};
% handle_cast generic fallback (ignore)
handle_cast(_Msg, State) ->
@@ -226,11 +277,26 @@ handle_cast(_Msg, State) ->
% -> shutdown in progress, ignore
handle_info({'EXIT', Pid, {error, {{accept_failed, {shutdown, _}}}}}, #state{acceptor = Pid} = State) -> {noreply, State};
% -> respawn listening socket and acceptor
-handle_info({'EXIT', Pid, _Reason}, #state{listen_socket = ListenSocket, socket_mode = SocketMode, port = Port, acceptor = Pid, recv_timeout = RecvTimeout, custom_opts = CustomOpts} = State) ->
- ?LOG_WARNING("acceptor has died with reason: ~p, respawning", [_Reason]),
- AcceptorPid = misultin_socket:start_link(ListenSocket, Port, RecvTimeout, SocketMode, CustomOpts),
+handle_info({'EXIT', Pid, _Reason}, #state{listen_socket = ListenSocket, socket_mode = SocketMode, port = Port, acceptor = Pid, recv_timeout = RecvTimeout, max_connections = MaxConnections, custom_opts = CustomOpts} = State) ->
+ ?LOG_ERROR("acceptor has died with reason: ~p, respawning", [_Reason]),
+ AcceptorPid = misultin_socket:start_link(ListenSocket, Port, RecvTimeout, MaxConnections, SocketMode, CustomOpts),
{noreply, State#state{acceptor = AcceptorPid}};
+% Http process trapping
+handle_info({'DOWN', _Ref, process, _HttpPid, normal}, State) -> {noreply, State}; % normal exiting of an http process
+handle_info({'DOWN', _Ref, process, HttpPid, _Reason}, #state{open_connections_count = OpenConnectionsCount, http_pid_ref = HttpPidRef, ws_pid_ref = WsPidRef} = State) ->
+ State0 = case lists:member(HttpPid, HttpPidRef) of
+ true ->
+ ?LOG_ERROR("http process ~p has died with reason: ~p, removing from references of open connections", [HttpPid, _Reason]),
+ State#state{open_connections_count = OpenConnectionsCount - 1, http_pid_ref = lists:delete(HttpPid, HttpPidRef)};
+ false ->
+ ?LOG_WARNING("received info on a process ~p crash which is not an http process, with reason: ~p", [HttpPid, _Reason]),
+ State
+ end,
+ % no checking done to improve performance
+ ?LOG_DEBUG("open references are: ~p, count is now ~p, trying to remove from websocket open references", [State0#state.http_pid_ref, State0#state.open_connections_count]),
+ {noreply, State0#state{ws_pid_ref = lists:delete(HttpPid, WsPidRef)}};
+
% handle_info generic fallback (ignore)
handle_info(_Info, State) ->
?LOG_WARNING("received unknown info message: ~p", [_Info]),
@@ -241,13 +307,17 @@ handle_info(_Info, State) ->
% Description: This function is called by a gen_server when it is about to terminate. When it returns,
% the gen_server terminates with Reason. The return value is ignored.
% ----------------------------------------------------------------------------------------------------------
-terminate(_Reason, #state{listen_socket = ListenSocket, socket_mode = SocketMode, acceptor = AcceptorPid, persistent_sock_references = PersistentSockReferences}) ->
+terminate(_Reason, #state{listen_socket = ListenSocket, socket_mode = SocketMode, acceptor = AcceptorPid, http_pid_ref = HttpPidRef, ws_pid_ref = WsPidRef}) ->
?LOG_INFO("shutting down server with Pid ~p", [self()]),
% kill acceptor
exit(AcceptorPid, kill),
- % send a shutdown message to all persistent sockets, if any
- ?LOG_DEBUG("sending shutdown message to persistent sockets, if any", []),
- lists:foreach(fun(PsPid) -> catch PsPid ! shutdown end, PersistentSockReferences),
+ % send a shutdown message to all websockets, if any
+ ?LOG_DEBUG("sending shutdown message to websockets: ~p", [WsPidRef]),
+ lists:foreach(fun(WsPid) -> catch WsPid ! shutdown end, WsPidRef),
+ % force exit of all http processes, if not websockets
+ HttpPidRefNoWs = lists:subtract(HttpPidRef, WsPidRef),
+ ?LOG_DEBUG("forcing exit of http processes: ~p", [HttpPidRefNoWs]),
+ lists:foreach(fun(HttpPid) -> exit(HttpPid, kill) end, HttpPidRefNoWs),
% stop tcp socket
misultin_socket:close(ListenSocket, SocketMode),
terminated.
@@ -279,10 +349,9 @@ check_and_convert_string_to_ip(Ip) ->
check_ssl_options(SslOptions) ->
Opts = [verify, fail_if_no_peer_cert, verify_fun, depth, certfile, keyfile, password, cacertfile, ciphers, reuse_sessions, reuse_session],
F = fun({Name, _Value}) ->
- ?LOG_DEBUG("testing ~p", [Name]),
case lists:member(Name, Opts) of
- false -> ?LOG_DEBUG("NOT found ~p", [Name]),false;
- _ -> ?LOG_DEBUG("found ~p", [Name]),true
+ false -> false;
+ _ -> true
end
end,
lists:all(F, SslOptions).
@@ -313,20 +382,28 @@ get_option({OptionName, DefaultValue, CheckAndConvertFun, FailTypeError}, Option
start_application(Application) ->
case lists:keyfind(Application, 1, application:which_applications()) of
false ->
- application:start(Application);
+ ?LOG_DEBUG("starting application ~p", [Application]),
+ case application:start(Application) of
+ ok ->
+ ok;
+ {error, Reason} ->
+ ?LOG_ERROR("error starting application ~p", [Application]),
+ {error, Reason}
+ end;
_ ->
+ ?LOG_DEBUG("application ~p is already running, skip", [Application]),
ok
end.
% Function: -> {ok, ListenSocket, AcceptorPid} | {error, Error}
% Description: Create listening socket
-create_listener_and_acceptor(Port, Options, RecvTimeout, SocketMode, CustomOpts) ->
+create_listener_and_acceptor(Port, Options, RecvTimeout, MaxConnections, SocketMode, CustomOpts) ->
?LOG_DEBUG("starting listening ~p socket with options ~p on port ~p", [SocketMode, Options, Port]),
case misultin_socket:listen(Port, Options, SocketMode) of
{ok, ListenSocket} ->
?LOG_DEBUG("starting acceptor",[]),
% create acceptor
- AcceptorPid = misultin_socket:start_link(ListenSocket, Port, RecvTimeout, SocketMode, CustomOpts),
+ AcceptorPid = misultin_socket:start_link(ListenSocket, Port, RecvTimeout, MaxConnections, SocketMode, CustomOpts),
{ok, ListenSocket, AcceptorPid};
{error, Reason} ->
% error
View
401 src/misultin_http.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>, Sean Hinde.
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>, Sean Hinde.
% All rights reserved.
%
% Code portions from Sean Hinde have been originally taken under BSD license from Trapexit at the address:
@@ -31,7 +31,7 @@
% POSSIBILITY OF SUCH DAMAGE.
% ==========================================================================================================
-module(misultin_http).
--vsn("0.6.2").
+-vsn("0.7-dev").
% API
-export([handle_data/8]).
@@ -44,10 +44,14 @@
-record(c, {
sock,
socket_mode,
+ socket_mode_packet_name,
port,
recv_timeout,
+ post_max_size,
+ get_url_max_size,
compress,
loop,
+ autoexit,
ws_loop,
ws_autoexit
}).
@@ -60,11 +64,32 @@
% Callback from misultin_socket
handle_data(Sock, SocketMode, ListenPort, PeerAddr, PeerPort, PeerCert, RecvTimeout, CustomOpts) ->
+ % add pid reference
+ misultin:http_pid_ref_add(self()),
% build connection & request records
- C = #c{sock = Sock, socket_mode = SocketMode, port = ListenPort, recv_timeout = RecvTimeout, compress = CustomOpts#custom_opts.compress, loop = CustomOpts#custom_opts.loop, ws_loop = CustomOpts#custom_opts.ws_loop, ws_autoexit = CustomOpts#custom_opts.ws_autoexit},
+ SocketModePacketName = case SocketMode of
+ ssl -> httph;
+ _ -> http
+ end,
+ C = #c{
+ sock = Sock,
+ socket_mode = SocketMode,
+ socket_mode_packet_name = SocketModePacketName,
+ port = ListenPort,
+ recv_timeout = RecvTimeout,
+ post_max_size = CustomOpts#custom_opts.post_max_size,
+ get_url_max_size = CustomOpts#custom_opts.get_url_max_size,
+ compress = CustomOpts#custom_opts.compress,
+ loop = CustomOpts#custom_opts.loop,
+ autoexit = CustomOpts#custom_opts.autoexit,
+ 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},
% enter loop
- request(C, Req).
+ request(C, Req),
+ % remove pid reference
+ misultin:http_pid_ref_remove(self()).
% ============================ /\ API ======================================================================
@@ -72,16 +97,15 @@ handle_data(Sock, SocketMode, ListenPort, PeerAddr, PeerPort, PeerCert, RecvTime
% ============================ \/ INTERNAL FUNCTIONS =======================================================
% REQUEST: wait for a HTTP Request line. Transition to state headers if one is received.
-request(#c{sock = Sock, socket_mode = SocketMode, recv_timeout = RecvTimeout} = C, Req) ->
- misultin_socket:setopts(Sock, [{active, once}, {packet, http}], SocketMode),
+request(#c{sock = Sock, socket_mode = SocketMode, socket_mode_packet_name = SocketModePacketName, recv_timeout = RecvTimeout, get_url_max_size = GetUrlMaxSize} = C, Req) ->
+ misultin_socket:setopts(Sock, [{active, once}, {packet, SocketModePacketName}], SocketMode),
receive
+ {SocketMode, Sock, {http_request, _Method, {_, Uri} = _Path, _Version}} when length(Uri) > GetUrlMaxSize ->
+ ?LOG_WARNING("get url request uri of ~p exceed maximum length of ~p", [length(Uri), GetUrlMaxSize]),
+ misultin_socket:send(Sock, build_error_message(414, close), SocketMode),
+ handle_keepalive(close, C, Req);
{SocketMode, Sock, {http_request, Method, Path, Version}} ->
?LOG_DEBUG("received full headers of a new HTTP packet", []),
- % change packet type if in ssl mode
- case SocketMode of
- ssl -> misultin_socket:setopts(Sock, [{packet, httph}], SocketMode);
- _ -> ok
- end,
% go to headers
headers(C, Req#req{vsn = Version, method = Method, uri = Path, connection = default_connection(Version)}, []);
{SocketMode, Sock, {http_error, "\r\n"}} ->
@@ -89,26 +113,30 @@ request(#c{sock = Sock, socket_mode = SocketMode, recv_timeout = RecvTimeout} =
{SocketMode, Sock, {http_error, "\n"}} ->
request(C, Req);
{http, Sock, {http_error, _Other}} ->
- ?LOG_WARNING("received a http error, might be a ssl request while socket in http mode: ~p, sending forbidden response and closing socket", [_Other]),
- misultin_socket:send(Sock, build_error_message(403, Req), SocketMode),
- misultin_socket:close(Sock, SocketMode),
- exit(normal);
+ ?LOG_DEBUG("not the beginning of a request [maybe a ssl request while socket in http mode?]: ~p, sending bad request message and closing socket", [_Other]),
+ misultin_socket:send(Sock, build_error_message(400, close), SocketMode),
+ misultin_socket:close(Sock, SocketMode);
+ {tcp_closed, _Socket} ->
+ ?LOG_DEBUG("tcp connection was closed, exit", []),
+ ok;
+ {ssl_closed, _Socket} ->
+ ?LOG_DEBUG("ssl tcp connection was closed, exit", []),
+ ok;
_Other ->
- ?LOG_WARNING("tcp error on incoming request: ~p, closing socket", [_Other]),
- misultin_socket:close(Sock, SocketMode),
- exit(normal)
+ ?LOG_WARNING("tcp error on incoming request: ~p, closing socket and exiting", [_Other]),
+ misultin_socket:close(Sock, SocketMode)
after RecvTimeout ->
- ?LOG_DEBUG("normal receive timeout, exit", []),
- misultin_socket:close(Sock, SocketMode),
- exit(normal)
+ ?LOG_DEBUG("normal receive timeout, closing socket and exiting", []),
+ misultin_socket:close(Sock, SocketMode)
end.
% HEADERS: collect HTTP headers. After the end of header marker transition to body state.
headers(C, Req, H) ->
headers(C, Req, H, 0).
-headers(#c{sock = Sock, socket_mode = SocketMode}, Req, _H, ?MAX_HEADERS_COUNT) ->
+headers(#c{sock = Sock, socket_mode = SocketMode} = C, Req, _H, ?MAX_HEADERS_COUNT) ->
?LOG_DEBUG("too many headers sent, bad request",[]),
- misultin_socket:send(Sock, build_error_message(400, Req), SocketMode);
+ misultin_socket:send(Sock, build_error_message(400, close), SocketMode),
+ handle_keepalive(close, C, Req);
headers(#c{sock = Sock, socket_mode = SocketMode, recv_timeout = RecvTimeout, ws_loop = WsLoop} = C, Req, H, HeaderCount) ->
misultin_socket:setopts(Sock, [{active, once}], SocketMode),
receive
@@ -139,20 +167,30 @@ headers(#c{sock = Sock, socket_mode = SocketMode, recv_timeout = RecvTimeout, ws
case CheckWs of
false ->
?LOG_DEBUG("normal http request received", []),
- body(C, Req#req{headers = Headers});
+ % build final req with headers, uri and args, and then send to method dispatch
+ case get_uri_and_args(Req#req{headers = Headers}) of
+ {error, HttpErrorCode} ->
+ ?LOG_WARNING("error encountered when parsing uri and args: ~p", [HttpErrorCode]),
+ misultin_socket:send(C#c.sock, build_error_message(HttpErrorCode, Req#req.connection), SocketMode),
+ handle_keepalive(Req#req.connection, C, Req);
+ Req0 ->
+ method_dispatch(C, Req0)
+ end;
{true, Vsn} ->
?LOG_DEBUG("websocket request received", []),
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;
{SocketMode, Sock, _Other} ->
- ?LOG_DEBUG("tcp error treating headers: ~p, send bad request error back", [_Other]),
- misultin_socket:send(Sock, build_error_message(400, Req), SocketMode);
+ ?LOG_WARNING("tcp error treating headers: ~p, send bad request error back", [_Other]),
+ misultin_socket:send(Sock, build_error_message(400, Req#req.connection), SocketMode),
+ handle_keepalive(Req#req.connection, C, Req);
_Other ->
?LOG_DEBUG("received unknown message: ~p, ignoring", [_Other]),
ignored
after RecvTimeout ->
?LOG_DEBUG("headers timeout, sending request timeout error", []),
- misultin_socket:send(Sock, build_error_message(408, Req), SocketMode)
+ misultin_socket:send(Sock, build_error_message(408, close), SocketMode),
+ handle_keepalive(close, C, Req)
end.
% default connection
@@ -177,111 +215,167 @@ keep_alive({1,0}, Head) ->
keep_alive({0,9}, _) -> close;
keep_alive(_Vsn, _KA) -> close.
-% BODY: collect the body of the HTTP request if there is one, and lookup and call the implementation callback.
-% Depending on whether the request is persistent transition back to state request to await the next request or exit.
-body(#c{sock = Sock, socket_mode = SocketMode, recv_timeout = RecvTimeout} = C, Req) ->
+% Function -> Req | {error, HttpErrorNum}
+% Description: Build uri & args in Req
+get_uri_and_args(Req) ->
+ case Req#req.uri of
+ {abs_path, Path} ->
+ {F, Args} = split_at_q_mark(Path, []),
+ Req#req{args = Args, uri = {abs_path, F}};
+ {absoluteURI, http, _Host, _, Path} ->
+ {F, Args} = split_at_q_mark(Path, []),
+ Req#req{args = Args, uri = {absoluteURI, F}};
+ {absoluteURI, _Other_method, _Host, _, _Path} ->
+ {error, 501};
+ {scheme, _Scheme, _RequestString} ->
+ {error, 510};
+ _ ->
+ {error, 403}
+ end.
+
+% dispatch operations according to defined method
+method_dispatch(#c{sock = Sock, socket_mode = SocketMode} = C, Req) ->
case Req#req.method of
'GET' ->
?LOG_DEBUG("GET request received",[]),
- Close = handle_get(C, Req),
- case Close of
- close ->
- % close socket
- misultin_socket:close(Sock, SocketMode);
- keep_alive ->
- 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;
+ call_mfa(C, Req),
+ handle_keepalive(Req#req.connection, C, Req);
'POST' ->
?LOG_DEBUG("POST request received", []),
- case catch list_to_integer(Req#req.content_length) of
- {'EXIT', _} ->
- % TODO: provide a fallback when content length is not or wrongly specified
- ?LOG_DEBUG("specified content length is not a valid integer number: ~p", [Req#req.content_length]),
- misultin_socket:send(Sock, build_error_message(411, Req), SocketMode),
- exit(normal);
- 0 ->
- ?LOG_DEBUG("zero content-lenght specified, skipping parsing body of request", []),
- Close = handle_post(C, Req),
- case Close of
- close ->
- % close socket
- misultin_socket:close(Sock, SocketMode);
- keep_alive ->
- misultin_socket:setopts(Sock, [{packet, http}], SocketMode),
- 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;
- Len ->
- ?LOG_DEBUG("parsing POST content in body of request", []),
- misultin_socket:setopts(Sock, [{packet, raw}, {active, false}], SocketMode),
- case misultin_socket:recv(Sock, Len, RecvTimeout, SocketMode) of
- {ok, Bin} ->
- Close = handle_post(C, Req#req{body = Bin}),
- case Close of
- close ->
- % close socket
- misultin_socket:close(Sock, SocketMode);
- keep_alive ->
- misultin_socket:setopts(Sock, [{packet, http}], SocketMode),
- 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;
- {error, timeout} ->
- ?LOG_WARNING("request timeout, sending error", []),
- misultin_socket:send(Sock, build_error_message(408, Req), SocketMode);
- _Other ->
- ?LOG_ERROR("tcp error treating post data: ~p, send bad request error back", [_Other]),
- misultin_socket:send(Sock, build_error_message(400, Req), SocketMode)
- end
+ % read post body
+ case read_post_body(C, Req) of
+ {ok, Bin} ->
+ ?LOG_DEBUG("body read, proceed to handle the request",[]),
+ Req0 = Req#req{body = Bin},
+ call_mfa(C, Req0),
+ handle_keepalive(Req#req.connection, C, Req0);
+ {error, timeout} ->
+ ?LOG_WARNING("request timeout, sending error", []),
+ misultin_socket:send(Sock, build_error_message(408, Req#req.connection), SocketMode),
+ handle_keepalive(close, C, Req);
+ {error, unsupported} ->
+ ?LOG_DEBUG("no specified content length, or not a valid integer number: ~p", [Req#req.content_length]),
+ misultin_socket:send(Sock, build_error_message(411, Req#req.connection), SocketMode),
+ handle_keepalive(Req#req.connection, C, Req);
+ {error, post_max_size} ->
+ ?LOG_WARNING("post request entity too large", []),
+ misultin_socket:send(Sock, build_error_message(413, close), SocketMode),
+ handle_keepalive(close, C, Req);
+ {error, Reason} ->
+ ?LOG_ERROR("tcp error treating post data: ~p, send bad request error back", [Reason]),
+ misultin_socket:send(Sock, build_error_message(400, close), SocketMode),
+ handle_keepalive(close, C, Req)
end;
_Other ->
?LOG_DEBUG("method not implemented: ~p", [_Other]),
- misultin_socket:send(Sock, build_error_message(501, Req), SocketMode),
- exit(normal)
+ misultin_socket:send(C#c.sock, build_error_message(501, Req#req.connection), C#c.socket_mode),
+ handle_keepalive(Req#req.connection, C, Req)
end.
-% handle a get request
-handle_get(C, #req{socket_mode = SocketMode, connection = Conn} = Req) ->
- case Req#req.uri of
- {abs_path, Path} ->
- {F, Args} = split_at_q_mark(Path, []),
- call_mfa(C, Req#req{args = Args, uri = {abs_path, F}}),
- Conn;
- {absoluteURI, http, _Host, _, Path} ->
- {F, Args} = split_at_q_mark(Path, []),
- call_mfa(C, Req#req{args = Args, uri = {absoluteURI, F}}),
- Conn;
- {absoluteURI, _Other_method, _Host, _, _Path} ->
- misultin_socket:send(C#c.sock, build_error_message(501, Req), SocketMode),
- close;
- {scheme, _Scheme, _RequestString} ->
- misultin_socket:send(C#c.sock, build_error_message(510, Req), SocketMode),
- close;
- _ ->
- misultin_socket:send(C#c.sock, build_error_message(403, Req), SocketMode),
- close
+% Function -> {ok, Bin} | {error, Reason}
+% Description: Read the post body according to headers
+read_post_body(#c{sock = Sock, socket_mode = SocketMode, recv_timeout = RecvTimeout, post_max_size = PostMaxSize} = C, Req) ->
+ % check if content length has been provided
+ case catch list_to_integer(Req#req.content_length) of
+ {'EXIT', _} ->
+ % no specified content length, or not a valid integer number, check transfer encoding header
+ case misultin_utility:header_get_value('Transfer-Encoding', Req#req.headers) of
+ "chunked" ->
+ ?LOG_DEBUG("chunked content being sent by the client, parsing POST content and looping for additional chunks",[]),
+ read_post_body_chunk(C);
+ _ ->
+ {error, unsupported}
+ end;
+ 0 ->
+ ?LOG_DEBUG("zero content-lenght specified, skipping parsing body of request", []),
+ {ok, <<>>};
+ Len when Len =< PostMaxSize ->
+ ?LOG_DEBUG("content length has been specified, parsing POST content in body of request", []),
+ misultin_socket:setopts(Sock, [{packet, raw}, {active, false}], SocketMode),
+ case misultin_socket:recv(Sock, Len, RecvTimeout, SocketMode) of
+ {ok, Bin} ->
+ {ok, Bin};
+ {error, timeout} ->
+ {error, timeout};
+ Other ->
+ {error, Other}
+ end;
+ _Len ->
+ ?LOG_DEBUG("content length specified of ~p bytes exceed limit of ~p bytes", [_Len, PostMaxSize]),
+ {error, post_max_size}
+ end.
+
+% read body chunks
+read_post_body_chunk(C) ->
+ read_post_body_chunk_headline(C, <<>>).
+read_post_body_chunk_headline(#c{post_max_size = PostMaxSize}, Acc) when size(Acc) > PostMaxSize ->
+ ?LOG_DEBUG("total size of chunked parts of ~p bytes exceed limit of ~p bytes", [size(Acc), PostMaxSize]),
+ {error, post_max_size};
+read_post_body_chunk_headline(#c{sock = Sock, socket_mode = SocketMode, recv_timeout = RecvTimeout} = C, Acc) ->
+ misultin_socket:setopts(Sock, [{packet, line}, {active, false}], SocketMode),
+ case misultin_socket:recv(Sock, 0, RecvTimeout, SocketMode) of
+ {ok, HeadLineBin} ->
+ ?LOG_DEBUG("received a chunked headline: ~p", [HeadLineBin]),
+ case get_chunk_length(binary_to_list(HeadLineBin)) of
+ {ok, Len} ->
+ read_post_body_chunk_content(C, Acc, Len);
+ {error, Reason} ->
+ {error, Reason}
+ end;
+ {error, timeout} ->
+ {error, timeout};
+ Other ->
+ {error, Other}
+ end.
+read_post_body_chunk_content(#c{sock = Sock, socket_mode = SocketMode, recv_timeout = RecvTimeout} = C, Acc, Len) ->
+ ?LOG_DEBUG("receiving a chunk of ~p bytes", [Len]),
+ misultin_socket:setopts(Sock, [{packet, raw}, {active, false}], SocketMode),
+ case misultin_socket:recv(Sock, Len + 2, RecvTimeout, SocketMode) of % we need 2 more bytes for the chunked CRLF closing
+ {ok, Bin} ->
+ ?LOG_DEBUG("received chunk content, with 2 additional chunked closing bytes CRLF: ~p", [Bin]),
+ case Len of
+ 0 ->
+ ?LOG_DEBUG("client has finished sending chunks, resulting body is: ~p", [Acc]),
+ {ok, Acc};
+ _ ->
+ % continue with next chunk
+ ?LOG_DEBUG("waiting for next chunk", []),
+ read_post_body_chunk_headline(C, <<Acc/binary, Bin:Len/binary>>)
+ end;
+ {error, timeout} ->
+ {error, timeout};
+ Other ->
+ {error, Other}
end.
-% handle a post request
-handle_post(C, #req{socket_mode = SocketMode, connection = Conn} = Req) ->
- case Req#req.uri of
- {abs_path, _Path} ->
- call_mfa(C, Req),
- Conn;
- {absoluteURI, http, _Host, _, _Path} ->
- call_mfa(C, Req),
- Conn;
- {absoluteURI, _Other_method, _Host, _, _Path} ->
- misultin_socket:send(C#c.sock, build_error_message(501, Req), SocketMode),
- close;
- {scheme, _Scheme, _RequestString} ->
- misultin_socket:send(C#c.sock, build_error_message(501, Req), SocketMode),
- close;
- _ ->
- misultin_socket:send(C#c.sock, build_error_message(403, Req), SocketMode),
- close
+% Function -> {ok, Len} | done | {error, Reason}
+% Description: Get length of the next chunk.
+get_chunk_length(HeadLine) ->
+ % take away CRLF
+ HeadLine0 = case string:rchr(HeadLine, $\r) of
+ 0 -> HeadLine;
+ Pos -> string:substr(HeadLine, 1, Pos - 1)
+ end,
+ % take away ;
+ Tokens = string:tokens(HeadLine0, ";"),
+ case catch lists:nth(1, Tokens) of
+ {'EXIT', _} ->
+ {error, chunked_headline_empty};
+ HeadLenStr ->
+ case catch list_to_integer(HeadLenStr, 16) of
+ {'EXIT', _} -> {error, invalid_chunked_headline};
+ Len -> {ok, Len}
+ end
end.
+% handle the request and get back to the request loop
+handle_keepalive(close, #c{sock = Sock, socket_mode = SocketMode}, _Req) ->
+ catch misultin_socket:close(Sock, SocketMode);
+handle_keepalive(keep_alive, #c{sock = Sock, socket_mode = SocketMode} = C, Req) ->
+ 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}).
+
% Description: Main dispatcher
-call_mfa(#c{loop = Loop} = C, Request) ->
+call_mfa(#c{loop = Loop, autoexit = AutoExit} = C, Request) ->
% spawn custom loop
Self = self(),
LoopPid = spawn(fun() ->
@@ -290,20 +384,28 @@ call_mfa(#c{loop = Loop} = C, Request) ->
% start custom loop
Loop(Req)
end),
- erlang:monitor(process, LoopPid),
- socket_loop(C, Request, LoopPid).
+ % monitor the loop pid
+ Ref = erlang:monitor(process, LoopPid),
+ % enter loop
+ socket_loop(C, Request, LoopPid),
+ % demonitor
+ catch erlang:demonitor(Ref),
+ % close http loop or send message to LoopPid
+ loop_close(LoopPid, AutoExit).
% socket loop
-socket_loop(#c{sock = Sock, socket_mode = SocketMode, compress = Compress} = C, #req{headers = RequestHeaders} = Request, LoopPid) ->
+socket_loop(#c{sock = Sock, socket_mode = SocketMode, compress = Compress} = C, #req{headers = RequestHeaders} = Req, LoopPid) ->
+ % set to active in order to receive closed events
+ misultin_socket:setopts(Sock, [{active, once}], SocketMode),
% receive
receive
{stream_head, HttpCode, Headers0} ->
?LOG_DEBUG("sending stream head", []),
- Headers = add_output_header('Connection', {Headers0, Request}),
+ Headers = add_output_header('Connection', {Headers0, Req}),
Enc_headers = enc_headers(Headers),
Resp = [misultin_utility:get_http_status_code(HttpCode), Enc_headers, <<"\r\n">>],
misultin_socket:send(Sock, Resp, SocketMode),
- socket_loop(C, Request, LoopPid);
+ socket_loop(C, Req, LoopPid);
{HttpCode, Headers0, Body} ->
% received normal response
?LOG_DEBUG("sending normal response", []),
@@ -313,43 +415,63 @@ socket_loop(#c{sock = Sock, socket_mode = SocketMode, compress = Compress} = C,
Enc_headers = case Headers0 of
{HeadersList, HeadersStr} ->
Headers1 = add_output_header('Content-Length', {HeadersList, BodyBinary}),
- Headers = add_output_header('Connection', {Headers1, Request}),
+ Headers = add_output_header('Connection', {Headers1, Req}),
[HeadersStr|enc_headers(lists:flatten([CompressHeaders|Headers]))];
_ ->
Headers1 = add_output_header('Content-Length', {Headers0, BodyBinary}),
- Headers = add_output_header('Connection', {Headers1, Request}),
+ Headers = add_output_header('Connection', {Headers1, Req}),
enc_headers(lists:flatten([CompressHeaders|Headers]))
end,
% build and send response
Resp = [misultin_utility:get_http_status_code(HttpCode), Enc_headers, <<"\r\n">>, BodyBinary],
misultin_socket:send(Sock, Resp, SocketMode),
- socket_loop(C, Request, LoopPid);
+ socket_loop(C, Req, LoopPid);
{stream_data, Data} ->
?LOG_DEBUG("sending stream data", []),
misultin_socket:send(Sock, Data, SocketMode),
- socket_loop(C, Request, LoopPid);
+ socket_loop(C, Req, LoopPid);
stream_close ->
?LOG_DEBUG("closing stream", []),
misultin_socket:close(Sock, SocketMode),
- socket_loop(C, Request, LoopPid);
+ socket_loop(C, Req, LoopPid);
{stream_error, 404} ->
?LOG_ERROR("file not found", []),
- misultin_socket:send(Sock, build_error_message(404, Request), SocketMode),
- socket_loop(C, Request, LoopPid);
+ misultin_socket:send(Sock, build_error_message(404, Req#req.connection), SocketMode),
+ socket_loop(C, Req, LoopPid);
{stream_error, _Reason} ->
?LOG_ERROR("error sending stream: ~p", [_Reason]),
- misultin_socket:send(Sock, build_error_message(500, Request), SocketMode),
+ misultin_socket:send(Sock, build_error_message(500, Req#req.connection), SocketMode),
misultin_socket:close(Sock, SocketMode),
- socket_loop(C, Request, LoopPid);
+ socket_loop(C, Req, LoopPid);
{'DOWN', _Ref, process, LoopPid, normal} ->
?LOG_DEBUG("normal finishing of custom loop",[]),
ok;
{'DOWN', _Ref, process, LoopPid, _Reason} ->
- ?LOG_ERROR("error in custom loop: ~p serving request: ~p", [_Reason, Request]),
- misultin_socket:send(Sock, build_error_message(500, Request), SocketMode);
+ ?LOG_ERROR("error in custom loop: ~p serving request: ~p", [_Reason, Req]),
+ misultin_socket:send(Sock, build_error_message(500, Req#req.connection), SocketMode);
+ {tcp_closed, Sock} ->
+ ?LOG_DEBUG("client closed socket",[]),
+ tcp_closed;
+ {ssl_closed, Sock} ->
+ ?LOG_DEBUG("client closed ssl socket",[]),
+ ssl_closed;
_Else ->
- ?LOG_DEBUG("unknown message received: ~p, ignoring", [_Else]),
- socket_loop(C, Request, LoopPid)
+ ?LOG_WARNING("received message from client when client should not send messages since it should wait for a complete reponse: ~p, closing socket", [_Else]),
+ misultin_socket:send(Sock, build_error_message(409, Req#req.connection), SocketMode),
+ misultin_socket:close(Sock, SocketMode)
+ end.
+
+% Close socket and custom handling loop dependency
+loop_close(LoopPid, AutoExit) ->
+ case AutoExit of
+ true ->
+ % kill handling loop process
+ ?LOG_DEBUG("force the killing of the http handling loop",[]),
+ exit(LoopPid, kill);
+ false ->
+ % the killing of the http handling loop process is handled in the loop itself -> send event
+ ?LOG_DEBUG("send client closed event the http handling loop",[]),
+ LoopPid ! closed
end.
% Description: Ensure Body is binary.
@@ -489,13 +611,12 @@ list_to_number(L) ->
Value -> Value
end.
-build_error_message(HttpCode, Request) ->
+% build error message
+build_error_message(HttpCode, Connection) ->
% build headers
- Headers1 = add_output_header('Content-Length', {[], <<>>}),
- Headers = add_output_header('Connection', {Headers1, Request}),
+ Headers = [{'Content-Length', 0}, {'Connection', Connection}],
Enc_headers = enc_headers(Headers),
% build and send response
- [misultin_utility:get_http_status_code(HttpCode), Enc_headers, <<"\r\n">>].
-
+ [misultin_utility:get_http_status_code(HttpCode), Enc_headers, <<"\r\n">>].
% ============================ /\ INTERNAL FUNCTIONS =======================================================
View
31 src/misultin_req.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>,
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>,
% Bob Ippolito <bob@mochimedia.com> for Mochi Media, Inc.
% All rights reserved.
%
@@ -32,7 +32,7 @@
% POSSIBILITY OF SUCH DAMAGE.
% ==========================================================================================================
-module(misultin_req, [Req, SocketPid]).
--vsn("0.6.2").
+-vsn("0.7-dev").
% macros
-define(PERCENT, 37). % $\%
@@ -45,7 +45,7 @@
% API
-export([raw/0]).
-export([ok/1, ok/2, ok/3, respond/1, respond/2, respond/3, respond/4, raw_headers_respond/1, raw_headers_respond/2, raw_headers_respond/3, raw_headers_respond/4]).
--export([stream/1, stream/2, stream/3]).
+-export([chunk/1, chunk/2, stream/1, stream/2, stream/3]).
-export([get/1, parse_qs/0, parse_post/0, file/1, file/2, resource/1]).
% includes
@@ -114,9 +114,26 @@ raw_headers_respond(HttpCode, HeadersStr, Body) ->
raw_headers_respond(HttpCode, [], HeadersStr, Body).
raw_headers_respond(HttpCode, Headers, HeadersStr, Body) ->
SocketPid ! {HttpCode, {Headers, HeadersStr}, Body}.
+
+% Description: Chunked Transfer-Encoding.
+chunk(head) ->
+ chunk(head, []);
+chunk(done) ->
+ stream("0\r\n\r\n");
+chunk(Template) ->
+ stream([integer_to_list(length(Template), 16), "\r\n", Template, "\r\n"]).
+chunk(head, Headers) ->
+ % add Transfer-Encoding chunked header if needed
+ Headers0 = case misultin_utility:header_get_value('Transfer-Encoding', Headers) of
+ false -> [{'Transfer-Encoding', "chunked"} | Headers];
+ _ -> Headers
+ end,
+ stream(head, Headers0);
+chunk(Template, Vars) ->
+ Data = io_lib:format(Template, Vars),
+ stream([integer_to_list(length(Data), 16), "\r\n", Data, "\r\n"]).
-
-% Description: Start stream.
+% Description: Stream support.
stream(close) ->
SocketPid ! stream_close;
stream(head) ->
@@ -148,8 +165,8 @@ parse_qs() ->
% Description: Parse Post
parse_post() ->
% get header confirmation
- case misultin_utility:get_key_value('Content-Type', Req#req.headers) of
- undefined ->
+ case misultin_utility:header_get_value('Content-Type', Req#req.headers) of
+ false ->
[];
ContentType ->
[Type|_CharSet] = string:tokens(ContentType, ";"),
View
46 src/misultin_socket.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>, Sean Hinde.
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>, Sean Hinde.
% All rights reserved.
%
% Code portions from Sean Hinde have been originally taken under BSD license from Trapexit at the address:
@@ -31,13 +31,13 @@
% POSSIBILITY OF SUCH DAMAGE.
% ==========================================================================================================
-module(misultin_socket).
--vsn("0.6.2").
+-vsn("0.7-dev").
% API
--export([start_link/5]).
+-export([start_link/6]).
% callbacks
--export([listener/5]).
+-export([listener/6]).
% internal
-export([listen/3, setopts/3, recv/4, send/3, close/2]).
@@ -50,19 +50,27 @@
% Function: {ok,Pid} | ignore | {error, Error}
% Description: Starts the socket.
-start_link(ListenSocket, ListenPort, RecvTimeout, SocketMode, CustomOpts) ->
- proc_lib:spawn_link(?MODULE, listener, [ListenSocket, ListenPort, RecvTimeout, SocketMode, CustomOpts]).
+start_link(ListenSocket, ListenPort, RecvTimeout, MaxConnections, SocketMode, CustomOpts) ->
+ proc_lib:spawn_link(?MODULE, listener, [ListenSocket, ListenPort, RecvTimeout, MaxConnections, SocketMode, CustomOpts]).
% Function: {ok,Pid} | ignore | {error, Error}
% Description: Starts the socket.
-listener(ListenSocket, ListenPort, RecvTimeout, SocketMode, CustomOpts) ->
+listener(ListenSocket, ListenPort, RecvTimeout, MaxConnections, SocketMode, CustomOpts) ->
case catch accept(ListenSocket, SocketMode) of
{ok, {sslsocket, _, _} = Sock} ->
- % received a SSL socket -> spawn a ssl_accept process to avoid locking the main listener
+ % spawn a ssl_accept process to avoid locking the main listener
spawn(fun() ->
case ssl:ssl_accept(Sock, 60000) of
ok ->
- create_socket_pid(Sock, ListenPort, RecvTimeout, SocketMode, CustomOpts);
+ case misultin:get_open_connections_count() >= MaxConnections of
+ false ->
+ create_socket_pid(Sock, ListenPort, RecvTimeout, SocketMode, CustomOpts);
+ true ->
+ % too many open connections, send error and close
+ ?LOG_WARNING("too many open connections, refusing new request",[]),
+ send(Sock, [misultin_utility:get_http_status_code(503), <<"Connection: Close\r\n\r\n">>], SocketMode),
+ close(Sock, SocketMode)
+ end;
{error, _Reason} ->
% could not negotiate a SSL transaction, leave process
?LOG_WARNING("could not negotiate a SSL transaction: ~p", [_Reason]),
@@ -70,16 +78,26 @@ listener(ListenSocket, ListenPort, RecvTimeout, SocketMode, CustomOpts) ->
end
end),
% get back to accept loop
- listener(ListenSocket, ListenPort, RecvTimeout, SocketMode, CustomOpts);
+ listener(ListenSocket, ListenPort, RecvTimeout, MaxConnections, SocketMode, CustomOpts);
{ok, Sock} ->
- % received a HTTP socket
- create_socket_pid(Sock, ListenPort, RecvTimeout, SocketMode, CustomOpts),
+ % received a HTTP socket, check connections
+ case misultin:get_open_connections_count() >= MaxConnections of
+ false ->
+ create_socket_pid(Sock, ListenPort, RecvTimeout, SocketMode, CustomOpts);
+ true ->
+ % too many open connections, send error and close [spawn to avoid locking]
+ spawn(fun() ->
+ ?LOG_WARNING("too many open connections, refusing new request",[]),
+ send(Sock, [misultin_utility:get_http_status_code(503), <<"Connection: Close\r\n\r\n">>], SocketMode),
+ close(Sock, SocketMode)
+ end)
+ end,
% get back to accept loop
- listener(ListenSocket, ListenPort, RecvTimeout, SocketMode, CustomOpts);
+ listener(ListenSocket, ListenPort, RecvTimeout, MaxConnections, SocketMode, CustomOpts);
{error, _Error} ->
?LOG_WARNING("accept failed with error: ~p", [_Error]),
% get back to accept loop
- listener(ListenSocket, ListenPort, RecvTimeout, SocketMode, CustomOpts);
+ listener(ListenSocket, ListenPort, RecvTimeout, MaxConnections, SocketMode, CustomOpts);
{'EXIT', Error} ->
?LOG_ERROR("accept exited with error: ~p, quitting process", [Error]),
exit({error, {accept_failed, Error}})
View
5 src/misultin_utility.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>.
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>.
% All rights reserved.
%
% BSD License
@@ -28,7 +28,7 @@
% POSSIBILITY OF SUCH DAMAGE.
% ==========================================================================================================
-module(misultin_utility).
--vsn("0.6.2").
+-vsn("0.7-dev").
% API
-export([get_http_status_code/1, get_content_type/1, get_key_value/2, header_get_value/2]).
@@ -145,6 +145,7 @@ get_content_type(FileName) ->
".jpg" -> "image/jpeg";
".tif" -> "image/tiff";
".tiff" -> "image/tiff";
+ ".png" -> "image/png";
".htm" -> "text/html";
".html" -> "text/html";
".txt" -> "text/plain";
View
28 src/misultin_websocket.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>, Joe Armstrong.
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>, Joe Armstrong.
% All rights reserved.
%
% Code portions from Joe Armstrong have been originally taken under MIT license at the address:
@@ -31,7 +31,7 @@
% POSSIBILITY OF SUCH DAMAGE.
% ==========================================================================================================
-module(misultin_websocket).
--vsn("0.6.2").
+-vsn("0.7-dev").
% API
-export([check/2, connect/3]).
@@ -69,7 +69,7 @@ connect(Req, #ws{vsn = Vsn, socket = Socket, socket_mode = SocketMode, path = Pa
% set opts
misultin_socket:setopts(Socket, [{packet, 0}, {active, true}], SocketMode),
% add main websocket pid to misultin server reference
- misultin:persistent_socket_pid_add(self()),
+ misultin:ws_pid_ref_add(self()),
% start listening for incoming data
ws_loop(Socket, none, WsHandleLoopPid, SocketMode, WsAutoExit).
@@ -158,20 +158,28 @@ handshake({'draft-hixie', 76}, #req{socket = Sock, socket_mode = SocketMode}, He
end,
?LOG_DEBUG("got content in body of websocket request: ~p", [Body]),
% prepare handhsake response
+ WsMode = case SocketMode of
+ ssl -> "wss";
+ _ -> "ws"
+ end,
["HTTP/1.1 101 WebSocket Protocol Handshake\r\n",
"Upgrade: WebSocket\r\n",
"Connection: Upgrade\r\n",
"Sec-WebSocket-Origin: ", Origin, "\r\n",
- "Sec-WebSocket-Location: ws://", lists:concat([Host, Path]), "\r\n\r\n",
+ "Sec-WebSocket-Location: ", WsMode, "://", lists:concat([Host, Path]), "\r\n\r\n",
build_challenge({'draft-hixie', 76}, {Key1, Key2, Body})
];
-handshake({'draft-hixie', 68}, _Req, _Headers, {Path, Origin, Host}) ->
+handshake({'draft-hixie', 68}, #req{socket_mode = SocketMode} = _Req, _Headers, {Path, Origin, Host}) ->
% prepare handhsake response
+ WsMode = case SocketMode of
+ ssl -> "wss";
+ _ -> "ws"
+ end,
["HTTP/1.1 101 Web Socket Protocol Handshake\r\n",
"Upgrade: WebSocket\r\n",
"Connection: Upgrade\r\n",
"WebSocket-Origin: ", Origin , "\r\n",
- "WebSocket-Location: ws://", lists:concat([Host, Path]), "\r\n\r\n"
+ "WebSocket-Location: ", WsMode, "://", lists:concat([Host, Path]), "\r\n\r\n"
].
% Function: List
@@ -193,10 +201,16 @@ ws_loop(Socket, Buffer, WsHandleLoopPid, SocketMode, WsAutoExit) ->
receive
{tcp, Socket, Data} ->
handle_data(Buffer, binary_to_list(Data), Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
+ {ssl, Socket, Data} ->
+ handle_data(Buffer, binary_to_list(Data), Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
{tcp_closed, Socket} ->
?LOG_DEBUG("tcp connection was closed, exit", []),
% close websocket and custom controlling loop
websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
+ {ssl_closed, Socket} ->
+ ?LOG_DEBUG("ssl tcp connection was closed, exit", []),
+ % close websocket and custom controlling loop
+ websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit);
{'DOWN', Ref, process, WsHandleLoopPid, Reason} ->
case Reason of
normal ->
@@ -237,7 +251,7 @@ handle_data([], L, Socket, WsHandleLoopPid, SocketMode, WsAutoExit) ->
% Close socket and custom handling loop dependency
websocket_close(Socket, WsHandleLoopPid, SocketMode, WsAutoExit) ->
% remove main websocket pid from misultin server reference
- misultin:persistent_socket_pid_remove(self()),
+ misultin:ws_pid_ref_remove(self()),
case WsAutoExit of
true ->
% kill custom handling loop process
View
4 src/misultin_ws.erl
@@ -3,7 +3,7 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2010, Roberto Ostinelli <roberto@ostinelli.net>.
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>.
% All rights reserved.
%
% BSD License
@@ -28,7 +28,7 @@
% POSSIBILITY OF SUCH DAMAGE.
% ==========================================================================================================
-module(misultin_ws, [Ws, SocketPid]).