From bf217eb401c40c037bfdd6960ccbe4c60280356f Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 7 Aug 2013 17:33:23 +0400 Subject: [PATCH 1/7] fix chuncked response --- src/mochiweb_request.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mochiweb_request.erl b/src/mochiweb_request.erl index 149fe6d..d3674b4 100644 --- a/src/mochiweb_request.erl +++ b/src/mochiweb_request.erl @@ -305,7 +305,7 @@ respond({Code, ResponseHeaders, {file, IoDevice}}, 'HEAD' -> ok; _ -> - mochiweb_io:iodevice_stream(fun send/2, IoDevice) + mochiweb_io:iodevice_stream(fun(IOData) -> send(IOData,THIS) end, IoDevice) end, Response; respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, Method, _RawPath, Version, _Headers]}=THIS) -> From 34d3e420c3b74bde56483d8bd2896e03126314bb Mon Sep 17 00:00:00 2001 From: Zeeshan Lakhani Date: Wed, 5 Oct 2016 11:02:44 -0400 Subject: [PATCH 2/7] Basho/zl/mochiweb update with master for recbuf and more (#19) * have edoc build things before generating docs. re: issue #135 * Minimize calls to gen_tcp:send() to optimize performance. Tests indicate a 50 to 1 performance improvement with this change. * update version to 2.9.1 * fixed an DoS vulnerability in Mochiweb/SSL * SSL: Fix for broken ECDH ciper suite in R16B See: http://osdir.com/ml/erlang-programming-bugs/2013-10/msg00004.html Fix inspired by https://github.com/extend/ranch/commit/c0c09a1311 * SSL: remove unsafe ciphers and protocols from the default options. * update CHANGES and README for v2.9.2 #140 * Add recbuf config option. * update CHANGES for v2.10.0 #134 * move common testing functionality into mochiweb_test_util * end to end connect test for websocket * end to end test with text frames (ssl is broken) * fix ssl receive support for websocket * R15 debugging * update CHANGES for 2.10.1 * mitigate SSL and emfile related conditions per #138 * include 17.1 in travis config, only use latest releases of older versions * Accept range end position which exceededs the resource size RFC 2616 14.35.1 Byte Ranges If the last-byte-pos value is absent, or if the value is greater than or equal to the current length of the entity-body, last-byte-pos is taken to be equal to one less than the current length of the entity- body in bytes. This work is originally done by @shino * update CHANGES for 2.11.1 * Fix range parsing regression introduced in #147 * send "Connection: close" header when the server is going to force-close the connection #146 * As discussed with @etrepum, add missing license headers * update CHANGES for 2.12.1 * update copyright for mochiweb_session * attempt to fix active_socket accounting #149 * update CHANGES for v2.12.1 * exit when setopts result is {error,closed} #152 * Export stream_body/5, allows to specify a max body length * Allow recbuf to be undefined If recbuf option is undefined, the operating system decides on the buffer size If no buffer size is speciefied, streaming will happen in the chunks of MaxChunkSize --- .travis.yml | 12 +- CHANGES.md | 70 +++++++++ Makefile | 2 +- src/mochifmt.erl | 18 +++ src/mochifmt_records.erl | 18 +++ src/mochifmt_std.erl | 18 +++ src/mochiglobal.erl | 20 +++ src/mochihex.erl | 18 +++ src/mochijson.erl | 18 +++ src/mochijson2.erl | 18 +++ src/mochilists.erl | 18 +++ src/mochilogfile2.erl | 18 +++ src/mochinum.erl | 18 +++ src/mochitemp.erl | 18 +++ src/mochiutf8.erl | 18 +++ src/mochiweb.app.src | 2 +- src/mochiweb.erl | 25 ++- src/mochiweb_acceptor.erl | 83 +++++++--- src/mochiweb_base64url.erl | 22 +++ src/mochiweb_charref.erl | 18 +++ src/mochiweb_cookies.erl | 18 +++ src/mochiweb_cover.erl | 18 +++ src/mochiweb_echo.erl | 18 +++ src/mochiweb_headers.erl | 18 +++ src/mochiweb_html.erl | 18 +++ src/mochiweb_http.erl | 90 ++++++----- src/mochiweb_io.erl | 18 +++ src/mochiweb_mime.erl | 18 +++ src/mochiweb_multipart.erl | 36 +++-- src/mochiweb_request.erl | 218 ++++++++++++++++---------- src/mochiweb_response.erl | 18 +++ src/mochiweb_session.erl | 19 +++ src/mochiweb_socket.erl | 98 ++++++++++-- src/mochiweb_socket_server.erl | 105 ++++++++----- src/mochiweb_websocket.erl | 19 +-- src/reloader.erl | 20 ++- test/mochiweb_socket_server_tests.erl | 2 +- test/mochiweb_test_util.erl | 126 +++++++++++++++ test/mochiweb_test_util.hrl | 1 + test/mochiweb_tests.erl | 155 +++++++----------- test/mochiweb_websocket_tests.erl | 76 +++++++++ 41 files changed, 1215 insertions(+), 346 deletions(-) create mode 100644 test/mochiweb_test_util.erl create mode 100644 test/mochiweb_test_util.hrl diff --git a/.travis.yml b/.travis.yml index 0261a61..0eeafc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,15 +3,7 @@ notifications: webhooks: http://basho-engbot.herokuapp.com/travis?key=24764f4f9a0433f76a00ad56a7551242cbcb8a7f email: eng@basho.com otp_release: - - 17.0 + - 17.4 + - 17.3 - R16B03-1 - - R16B03 - - R16B02 - - R16B01 - - R16B - R15B03 - - R15B02 - - R15B01 - - R15B - - R14B04 - - R14B03 diff --git a/CHANGES.md b/CHANGES.md index 1cd02e6..88846b0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,73 @@ +Version 2.12.2 released 2015-02-21 + +* Close connections quietly when setopts fails with a closed socket. + https://github.com/mochi/mochiweb/pull/152 + +Version 2.12.1 released 2015-02-01 + +* Fix active_socket accounting + https://github.com/mochi/mochiweb/issues/149 +* Added full MIT license preludes to each source file to make it + easier for mochiweb's code to be used piecemeal + https://github.com/mochi/mochiweb/pull/148 + +Version 2.12.0 released 2015-01-16 + +* Send "Connection: close" header when the server is going to close + a Keep-Alive connection, usually due to unread data from the + client + https://github.com/mochi/mochiweb/issues/146 + +Version 2.11.2 released 2015-01-16 + +* Fix regression introduced in #147 + https://github.com/mochi/mochiweb/pull/147 + +Version 2.11.1 released 2015-01-16 + +* Accept range end position which exceededs the resource size + https://github.com/mochi/mochiweb/pull/147 + +Version 2.11.0 released 2015-01-12 + +* Perform SSL handshake after releasing acceptor back into the pool, + and slow accept rate when file descriptors are not available, + to mitigate a potential DoS attack. Adds new mochiweb_socket + functions transport_accept/1 and finish_accept/1 which should be + used in preference to the now deprecated accept/1 function. + https://github.com/mochi/mochiweb/issues/138 + +Version 2.10.1 released 2015-01-11 + +* Fixes issue with SSL and mochiweb_websocket. Note that + mochiweb_websocket is still experimental and the API + is subject to change in future versions. + https://github.com/mochi/mochiweb/pull/144 + +Version 2.10.0 released 2014-12-17 + +* Added new `recbuf` option to mochiweb_http to allow the receive + buffer to be configured. + https://github.com/mochi/mochiweb/pull/134 + +Version 2.9.2 released 2014-10-16 + +* Add timeouts to SSL connect to prevent DoS by opening a connection + and not doing anything. + https://github.com/mochi/mochiweb/pull/140 +* Prevent using ECDH cipher in R16B because it is broken + https://github.com/mochi/mochiweb/pull/140 +* For default SSL connections, remove usage of sslv3 and not-so-secure + ciphers. + https://github.com/mochi/mochiweb/pull/140 + +Version 2.9.1 released 2014-09-29 + +* Fix Makefile rule for building docs + https://github.com/mochi/mochiweb/issues/135 +* Minimize gen_tcp:send calls to optimize performance. + https://github.com/mochi/mochiweb/pull/137 + Version 2.9.0 released 2014-06-24 * Increased timeout in test suite for FreeBSD diff --git a/Makefile b/Makefile index e53eef5..33601c7 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ REBAR=./rebar all: @$(REBAR) prepare-deps -edoc: +edoc: all @$(REBAR) doc test: diff --git a/src/mochifmt.erl b/src/mochifmt.erl index fc95e4f..6381bb7 100644 --- a/src/mochifmt.erl +++ b/src/mochifmt.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2008 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc String Formatting for Erlang, inspired by Python 2.6 %% (PEP 3101). diff --git a/src/mochifmt_records.erl b/src/mochifmt_records.erl index 7d166ff..3dccaa4 100644 --- a/src/mochifmt_records.erl +++ b/src/mochifmt_records.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2008 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Formatter that understands records. %% diff --git a/src/mochifmt_std.erl b/src/mochifmt_std.erl index ea68c4a..6067451 100644 --- a/src/mochifmt_std.erl +++ b/src/mochifmt_std.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2008 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Template module for a mochifmt formatter. diff --git a/src/mochiglobal.erl b/src/mochiglobal.erl index ea645b0..8df007f 100644 --- a/src/mochiglobal.erl +++ b/src/mochiglobal.erl @@ -1,5 +1,25 @@ %% @author Bob Ippolito %% @copyright 2010 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. + + %% @doc Abuse module constant pools as a "read-only shared heap" (since erts 5.6) %% [1]. -module(mochiglobal). diff --git a/src/mochihex.erl b/src/mochihex.erl index 796f3ad..91b2789 100644 --- a/src/mochihex.erl +++ b/src/mochihex.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2006 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Utilities for working with hexadecimal strings. diff --git a/src/mochijson.erl b/src/mochijson.erl index d283189..fb9b1dc 100644 --- a/src/mochijson.erl +++ b/src/mochijson.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2006 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Yet another JSON (RFC 4627) library for Erlang. -module(mochijson). diff --git a/src/mochijson2.erl b/src/mochijson2.erl index 2b8d16e..3d880db 100644 --- a/src/mochijson2.erl +++ b/src/mochijson2.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Yet another JSON (RFC 4627) library for Erlang. mochijson2 works %% with binaries as strings, arrays as lists (without an {array, _}) diff --git a/src/mochilists.erl b/src/mochilists.erl index d93b241..24fa2f3 100644 --- a/src/mochilists.erl +++ b/src/mochilists.erl @@ -1,5 +1,23 @@ %% @copyright Copyright (c) 2010 Mochi Media, Inc. %% @author David Reid +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Utility functions for dealing with proplists. diff --git a/src/mochilogfile2.erl b/src/mochilogfile2.erl index b4a7e3c..6ff8fec 100644 --- a/src/mochilogfile2.erl +++ b/src/mochilogfile2.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2010 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Write newline delimited log files, ensuring that if a truncated %% entry is found on log open then it is fixed before writing. Uses diff --git a/src/mochinum.erl b/src/mochinum.erl index c52b15c..d687370 100644 --- a/src/mochinum.erl +++ b/src/mochinum.erl @@ -1,5 +1,23 @@ %% @copyright 2007 Mochi Media, Inc. %% @author Bob Ippolito +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Useful numeric algorithms for floats that cover some deficiencies %% in the math module. More interesting is digits/1, which implements diff --git a/src/mochitemp.erl b/src/mochitemp.erl index dda7863..bd3c965 100644 --- a/src/mochitemp.erl +++ b/src/mochitemp.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2010 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Create temporary files and directories. Requires crypto to be started. diff --git a/src/mochiutf8.erl b/src/mochiutf8.erl index 28f28c1..bf0e7cc 100644 --- a/src/mochiutf8.erl +++ b/src/mochiutf8.erl @@ -1,5 +1,23 @@ %% @copyright 2010 Mochi Media, Inc. %% @author Bob Ippolito +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Algorithm to convert any binary to a valid UTF-8 sequence by ignoring %% invalid bytes. diff --git a/src/mochiweb.app.src b/src/mochiweb.app.src index 7c98141..7431224 100644 --- a/src/mochiweb.app.src +++ b/src/mochiweb.app.src @@ -1,7 +1,7 @@ %% This is generated from src/mochiweb.app.src {application, mochiweb, [{description, "MochiMedia Web Server"}, - {vsn, "2.9.0"}, + {vsn, "2.12.2"}, {modules, []}, {registered, []}, {env, []}, diff --git a/src/mochiweb.erl b/src/mochiweb.erl index 927322d..14480c2 100644 --- a/src/mochiweb.erl +++ b/src/mochiweb.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Start and stop the MochiWeb server. @@ -51,10 +69,15 @@ uri({scheme, Hostname, Port}) -> uri(HttpString) when is_list(HttpString) -> HttpString. -%% @spec new_request({Socket, Request, Headers}) -> MochiWebRequest +%% @spec new_request( {Socket, Request, Headers} +%% | {Socket, Opts, Request, Headers} ) -> MochiWebRequest %% @doc Return a mochiweb_request data structure. new_request({Socket, {Method, HttpUri, Version}, Headers}) -> + new_request({Socket, [], {Method, HttpUri, Version}, Headers}); + +new_request({Socket, Opts, {Method, HttpUri, Version}, Headers}) -> mochiweb_request:new(Socket, + Opts, Method, uri(HttpUri), Version, diff --git a/src/mochiweb_acceptor.erl b/src/mochiweb_acceptor.erl index ebbaf45..44ce91f 100644 --- a/src/mochiweb_acceptor.erl +++ b/src/mochiweb_acceptor.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2010 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc MochiWeb acceptor. @@ -8,24 +26,46 @@ -include("internal.hrl"). --export([start_link/3, init/3]). +-export([start_link/3, start_link/4, init/4]). + +-define(EMFILE_SLEEP_MSEC, 100). start_link(Server, Listen, Loop) -> - proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop]). + start_link(Server, Listen, Loop, []). -init(Server, Listen, Loop) -> +start_link(Server, Listen, Loop, Opts) -> + proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop, Opts]). + +do_accept(Server, Listen) -> T1 = os:timestamp(), - case catch mochiweb_socket:accept(Listen) of + case mochiweb_socket:transport_accept(Listen) of {ok, Socket} -> gen_server:cast(Server, {accepted, self(), timer:now_diff(os:timestamp(), T1)}), - call_loop(Loop, Socket); - {error, closed} -> - exit(normal); - {error, timeout} -> - init(Server, Listen, Loop); - {error, esslaccept} -> + mochiweb_socket:finish_accept(Socket); + Other -> + Other + end. + +init(Server, Listen, Loop, Opts) -> + case catch do_accept(Server, Listen) of + {ok, Socket} -> + call_loop(Loop, Socket, Opts); + {error, Err} when Err =:= closed orelse + Err =:= esslaccept orelse + Err =:= timeout -> exit(normal); Other -> + %% Mitigate out of file descriptor scenario by sleeping for a + %% short time to slow error rate + case Other of + {error, emfile} -> + receive + after ?EMFILE_SLEEP_MSEC -> + ok + end; + _ -> + ok + end, error_logger:error_report( [{application, mochiweb}, "Accept failed error", @@ -33,18 +73,11 @@ init(Server, Listen, Loop) -> exit({error, accept_failed}) end. -call_loop({M, F}, Socket) -> - M:F(Socket); -call_loop({M, F, [A1]}, Socket) -> - M:F(Socket, A1); -call_loop({M, F, A}, Socket) -> - erlang:apply(M, F, [Socket | A]); -call_loop(Loop, Socket) -> - Loop(Socket). - -%% -%% Tests -%% --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. +call_loop({M, F}, Socket, Opts) -> + M:F(Socket, Opts); +call_loop({M, F, [A1]}, Socket, Opts) -> + M:F(Socket, Opts, A1); +call_loop({M, F, A}, Socket, Opts) -> + erlang:apply(M, F, [Socket, Opts | A]); +call_loop(Loop, Socket, Opts) -> + Loop(Socket, Opts). diff --git a/src/mochiweb_base64url.erl b/src/mochiweb_base64url.erl index 5f552e0..e6a8e13 100644 --- a/src/mochiweb_base64url.erl +++ b/src/mochiweb_base64url.erl @@ -1,5 +1,27 @@ +%% @author Bob Ippolito +%% @copyright 2013 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. + -module(mochiweb_base64url). -export([encode/1, decode/1]). + %% @doc URL and filename safe base64 variant with no padding, %% also known as "base64url" per RFC 4648. %% diff --git a/src/mochiweb_charref.erl b/src/mochiweb_charref.erl index 193c7c7..143452e 100644 --- a/src/mochiweb_charref.erl +++ b/src/mochiweb_charref.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Converts HTML 5 charrefs and entities to codepoints (or lists of code points). -module(mochiweb_charref). diff --git a/src/mochiweb_cookies.erl b/src/mochiweb_cookies.erl index 1cc4e91..9539041 100644 --- a/src/mochiweb_cookies.erl +++ b/src/mochiweb_cookies.erl @@ -1,5 +1,23 @@ %% @author Emad El-Haraty %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc HTTP Cookie parsing and generating (RFC 2109, RFC 2965). diff --git a/src/mochiweb_cover.erl b/src/mochiweb_cover.erl index aa075d5..ebc2c18 100644 --- a/src/mochiweb_cover.erl +++ b/src/mochiweb_cover.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2010 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Workarounds for various cover deficiencies. -module(mochiweb_cover). diff --git a/src/mochiweb_echo.erl b/src/mochiweb_echo.erl index e145840..b14505c 100644 --- a/src/mochiweb_echo.erl +++ b/src/mochiweb_echo.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Simple and stupid echo server to demo mochiweb_socket_server. diff --git a/src/mochiweb_headers.erl b/src/mochiweb_headers.erl index b49cf9e..457758f 100644 --- a/src/mochiweb_headers.erl +++ b/src/mochiweb_headers.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Case preserving (but case insensitive) HTTP Header dictionary. diff --git a/src/mochiweb_html.erl b/src/mochiweb_html.erl index 3732924..3fd93d0 100644 --- a/src/mochiweb_html.erl +++ b/src/mochiweb_html.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Loosely tokenizes and generates parse trees for HTML 4. -module(mochiweb_html). diff --git a/src/mochiweb_http.erl b/src/mochiweb_http.erl index d88e14f..da2c4a4 100644 --- a/src/mochiweb_http.erl +++ b/src/mochiweb_http.erl @@ -1,12 +1,30 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc HTTP server. -module(mochiweb_http). -author('bob@mochimedia.com'). -export([start/1, start_link/1, stop/0, stop/1]). --export([loop/2]). +-export([loop/3]). -export([after_response/2, reentry/1]). -export([parse_range_request/1, range_skip_length/2]). @@ -40,7 +58,7 @@ stop(Name) -> %% Option = {name, atom()} | {ip, string() | tuple()} | {backlog, integer()} %% | {nodelay, boolean()} | {acceptor_pool_size, integer()} %% | {ssl, boolean()} | {profile_fun, undefined | (Props) -> ok} -%% | {link, false} +%% | {link, false} | {recbuf, undefined | non_negative_integer()} %% @doc Start a mochiweb server. %% profile_fun is used to profile accept timing. %% After each accept, if defined, profile_fun is called with a proplist of a subset of the mochiweb_socket_server state and timing information. @@ -52,20 +70,20 @@ start(Options) -> start_link(Options) -> mochiweb_socket_server:start_link(parse_options(Options)). -loop(Socket, Body) -> - ok = mochiweb_socket:setopts(Socket, [{packet, http}]), - request(Socket, Body). +loop(Socket, Opts, Body) -> + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, http}])), + request(Socket, Opts, Body). -request(Socket, Body) -> - ok = mochiweb_socket:setopts(Socket, [{active, once}]), +request(Socket, Opts, Body) -> + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{active, once}])), receive {Protocol, _, {http_request, Method, Path, Version}} when Protocol == http orelse Protocol == ssl -> - ok = mochiweb_socket:setopts(Socket, [{packet, httph}]), - headers(Socket, {Method, Path, Version}, [], Body, 0); + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, httph}])), + headers(Socket, Opts, {Method, Path, Version}, [], Body, 0); {Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl -> - request(Socket, Body); + request(Socket, Opts, Body); {Protocol, _, {http_error, "\n"}} when Protocol == http orelse Protocol == ssl -> - request(Socket, Body); + request(Socket, Opts, Body); {tcp_closed, _} -> mochiweb_socket:close(Socket), exit(normal); @@ -80,9 +98,7 @@ request(Socket, Body) -> mochiweb_socket:close(Socket), exit(normal); Other -> - error_logger:warning_msg("Got unexpected (leftover) message: ~w (to pid=~w)~n", - [Other, self()]), - request(Socket, Body) + handle_invalid_msg_request(Other, Socket, Opts) after ?REQUEST_RECV_TIMEOUT -> mochiweb_socket:close(Socket), exit(normal) @@ -93,19 +109,19 @@ reentry(Body) -> ?MODULE:after_response(Body, Req) end. -headers(Socket, Request, Headers, _Body, ?MAX_HEADERS) -> +headers(Socket, Opts, Request, Headers, _Body, ?MAX_HEADERS) -> %% Too many headers sent, bad request. - ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), - handle_invalid_request(Socket, Request, Headers); -headers(Socket, Request, Headers, Body, HeaderCount) -> - ok = mochiweb_socket:setopts(Socket, [{active, once}]), + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])), + handle_invalid_request(Socket, Opts, Request, Headers); +headers(Socket, Opts, Request, Headers, Body, HeaderCount) -> + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{active, once}])), receive {Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl -> - Req = new_request(Socket, Request, Headers), + Req = new_request(Socket, Opts, Request, Headers), call_body(Body, Req), ?MODULE:after_response(Body, Req); {Protocol, _, {http_header, _, Name, _, Value}} when Protocol == http orelse Protocol == ssl -> - headers(Socket, Request, [{Name, Value} | Headers], Body, + headers(Socket, Opts, Request, [{Name, Value} | Headers], Body, 1 + HeaderCount); {tcp_closed, _} -> mochiweb_socket:close(Socket), @@ -120,9 +136,7 @@ headers(Socket, Request, Headers, Body, HeaderCount) -> mochiweb_socket:close(Socket), exit(normal); Other -> - error_logger:warning_msg("Got unexpected (leftover) message: ~w (to pid=~w)~n", - [Other, self()]), - headers(Socket, Request, Headers, Body, HeaderCount) + handle_invalid_msg_request(Other, Socket, Opts, Request, Headers) after ?HEADERS_RECV_TIMEOUT -> mochiweb_socket:close(Socket), exit(normal) @@ -135,31 +149,31 @@ call_body({M, F}, Req) -> call_body(Body, Req) -> Body(Req). --spec handle_invalid_msg_request(term(), term()) -> no_return(). -handle_invalid_msg_request(Msg, Socket) -> - handle_invalid_msg_request(Msg, Socket, {'GET', {abs_path, "/"}, {0,9}}, []). +-spec handle_invalid_msg_request(term(), term(), term()) -> no_return(). +handle_invalid_msg_request(Msg, Socket, Opts) -> + handle_invalid_msg_request(Msg, Socket, Opts, {'GET', {abs_path, "/"}, {0,9}}, []). --spec handle_invalid_msg_request(term(), term(), term(), term()) -> no_return(). -handle_invalid_msg_request(Msg, Socket, Request, RevHeaders) -> +-spec handle_invalid_msg_request(term(), term(), term(), term(), term()) -> no_return(). +handle_invalid_msg_request(Msg, Socket, Opts, Request, RevHeaders) -> case {Msg, r15b_workaround()} of {{tcp_error,_,emsgsize}, true} -> %% R15B02 returns this then closes the socket, so close and exit mochiweb_socket:close(Socket), exit(normal); _ -> - handle_invalid_request(Socket, Request, RevHeaders) + handle_invalid_request(Socket, Opts, Request, RevHeaders) end. --spec handle_invalid_request(term(), term(), term()) -> no_return(). -handle_invalid_request(Socket, Request, RevHeaders) -> - Req = new_request(Socket, Request, RevHeaders), +-spec handle_invalid_request(term(), term(), term(), term()) -> no_return(). +handle_invalid_request(Socket, Opts, Request, RevHeaders) -> + Req = new_request(Socket, Opts, Request, RevHeaders), Req:respond({400, [], []}), mochiweb_socket:close(Socket), exit(normal). -new_request(Socket, Request, RevHeaders) -> - ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), - mochiweb:new_request({Socket, Request, lists:reverse(RevHeaders)}). +new_request(Socket, Opts, Request, RevHeaders) -> + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])), + mochiweb:new_request({Socket, Opts, Request, lists:reverse(RevHeaders)}). after_response(Body, Req) -> Socket = Req:get(socket), @@ -170,7 +184,7 @@ after_response(Body, Req) -> false -> Req:cleanup(), erlang:garbage_collect(), - ?MODULE:loop(Socket, Body) + ?MODULE:loop(Socket, mochiweb_request:get(opts, Req), Body) end. parse_range_request(RawRange) when is_list(RawRange) -> @@ -208,7 +222,7 @@ range_skip_length(Spec, Size) -> {Start, End - Start + 1}; {Start, End} when 0 =< Start, Start < Size, Start =< End -> {Start, Size - Start}; - {_OutOfRange, _End} -> + {_InvalidStart, _InvalidEnd} -> invalid_range end. diff --git a/src/mochiweb_io.erl b/src/mochiweb_io.erl index 8454b43..15b6b3a 100644 --- a/src/mochiweb_io.erl +++ b/src/mochiweb_io.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Utilities for dealing with IO devices (open files). diff --git a/src/mochiweb_mime.erl b/src/mochiweb_mime.erl index 7d9f249..949d957 100644 --- a/src/mochiweb_mime.erl +++ b/src/mochiweb_mime.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Gives a good MIME type guess based on file extension. diff --git a/src/mochiweb_multipart.erl b/src/mochiweb_multipart.erl index a83a88c..1d18ae2 100644 --- a/src/mochiweb_multipart.erl +++ b/src/mochiweb_multipart.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Utilities for parsing multipart/form-data. @@ -374,7 +392,7 @@ parse3(Transport) -> body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, - ServerFun = fun (Socket) -> + ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, @@ -410,7 +428,7 @@ parse2(Transport) -> body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, - ServerFun = fun (Socket) -> + ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, @@ -447,7 +465,7 @@ do_parse_form(Transport) -> "--AaB03x--", ""], "\r\n"), BinContent = iolist_to_binary(Content), - ServerFun = fun (Socket) -> + ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, @@ -500,7 +518,7 @@ do_parse(Transport) -> body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, - ServerFun = fun (Socket) -> + ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, @@ -552,7 +570,7 @@ parse_partial_body_boundary(Transport) -> body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, - ServerFun = fun (Socket) -> + ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, @@ -605,7 +623,7 @@ parse_large_header(Transport) -> body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, - ServerFun = fun (Socket) -> + ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, @@ -681,7 +699,7 @@ flash_parse(Transport) -> body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, - ServerFun = fun (Socket) -> + ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, @@ -729,7 +747,7 @@ flash_parse2(Transport) -> body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, - ServerFun = fun (Socket) -> + ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, @@ -856,7 +874,7 @@ multipart_parsing_benchmark() -> body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, - ServerFun = fun (Socket) -> + ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, diff --git a/src/mochiweb_request.erl b/src/mochiweb_request.erl index 647fbf5..3d3e89e 100644 --- a/src/mochiweb_request.erl +++ b/src/mochiweb_request.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc MochiWeb HTTP Request abstraction. @@ -11,9 +29,9 @@ -define(QUIP, "Any of you quaids got a smint?"). --export([new/5]). +-export([new/5, new/6]). -export([get_header_value/2, get_primary_header_value/2, get_combined_header_value/2, get/2, dump/1]). --export([send/2, recv/2, recv/3, recv_body/1, recv_body/2, stream_body/4]). +-export([send/2, recv/2, recv/3, recv_body/1, recv_body/2, stream_body/4, stream_body/5]). -export([start_response/2, start_response_length/2, start_raw_response/2]). -export([respond/2, ok/2]). -export([not_found/1, not_found/2]). @@ -49,17 +67,22 @@ %% @spec new(Socket, Method, RawPath, Version, headers()) -> request() %% @doc Create a new request instance. new(Socket, Method, RawPath, Version, Headers) -> - {?MODULE, [Socket, Method, RawPath, Version, Headers]}. + new(Socket, [], Method, RawPath, Version, Headers). + +%% @spec new(Socket, Opts, Method, RawPath, Version, headers()) -> request() +%% @doc Create a new request instance. +new(Socket, Opts, Method, RawPath, Version, Headers) -> + {?MODULE, [Socket, Opts, Method, RawPath, Version, Headers]}. %% @spec get_header_value(K, request()) -> undefined | Value %% @doc Get the value of a given request header. -get_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> +get_header_value(K, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) -> mochiweb_headers:get_value(K, Headers). -get_primary_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> +get_primary_header_value(K, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) -> mochiweb_headers:get_primary_value(K, Headers). -get_combined_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> +get_combined_header_value(K, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) -> mochiweb_headers:get_combined_value(K, Headers). %% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range @@ -70,24 +93,24 @@ get_combined_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, He %% an ssl socket will be returned as {ssl, SslSocket}. %% You can use SslSocket with the ssl %% application, eg: ssl:peercert(SslSocket). -get(socket, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> +get(socket, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> Socket; -get(scheme, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> +get(scheme, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:type(Socket) of plain -> http; ssl -> https end; -get(method, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}) -> +get(method, {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]}) -> Method; -get(raw_path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) -> +get(raw_path, {?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) -> RawPath; -get(version, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}) -> +get(version, {?MODULE, [_Socket, _Opts, _Method, _RawPath, Version, _Headers]}) -> Version; -get(headers, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> +get(headers, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) -> Headers; -get(peer, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +get(peer, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> case mochiweb_socket:peername(Socket) of {ok, {Addr={10, _, _, _}, _Port}} -> case get_header_value("x-forwarded-for", THIS) of @@ -108,7 +131,7 @@ get(peer, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> {error, enotconn} -> exit(normal) end; -get(path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) -> +get(path, {?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) -> case erlang:get(?SAVE_PATH) of undefined -> {Path0, _, _} = mochiweb_util:urlsplit_path(RawPath), @@ -118,7 +141,7 @@ get(path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) -> Cached -> Cached end; -get(body_length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +get(body_length, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_BODY_LENGTH) of undefined -> BodyLength = body_length(THIS), @@ -127,26 +150,29 @@ get(body_length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THI {cached, Cached} -> Cached end; -get(range, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +get(range, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> case get_header_value(range, THIS) of undefined -> undefined; RawRange -> mochiweb_http:parse_range_request(RawRange) - end. + end; +get(opts, {?MODULE, [_Socket, Opts, _Method, _RawPath, _Version, _Headers]}) -> + Opts. %% @spec dump(request()) -> {mochiweb_request, [{atom(), term()}]} %% @doc Dump the internal representation to a "human readable" set of terms %% for debugging/inspection purposes. -dump({?MODULE, [_Socket, Method, RawPath, Version, Headers]}) -> +dump({?MODULE, [_Socket, Opts, Method, RawPath, Version, Headers]}) -> {?MODULE, [{method, Method}, {version, Version}, {raw_path, RawPath}, + {opts, Opts}, {headers, mochiweb_headers:to_list(Headers)}]}. %% @spec send(iodata(), request()) -> ok %% @doc Send data over the socket. -send(Data, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> +send(Data, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:send(Socket, Data) of ok -> ok; @@ -157,13 +183,13 @@ send(Data, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> %% @spec recv(integer(), request()) -> binary() %% @doc Receive Length bytes from the client as a binary, with the default %% idle timeout. -recv(Length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +recv(Length, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> recv(Length, ?IDLE_TIMEOUT, THIS). %% @spec recv(integer(), integer(), request()) -> binary() %% @doc Receive Length bytes from the client as a binary, with the given %% Timeout in msec. -recv(Length, Timeout, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> +recv(Length, Timeout, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:recv(Socket, Length, Timeout) of {ok, Data} -> put(?SAVE_RECV, true), @@ -174,7 +200,7 @@ recv(Length, Timeout, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]} %% @spec body_length(request()) -> undefined | chunked | unknown_transfer_encoding | integer() %% @doc Infer body length from transfer-encoding and content-length headers. -body_length({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +body_length({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> case get_header_value("transfer-encoding", THIS) of undefined -> case get_combined_header_value("content-length", THIS) of @@ -193,13 +219,13 @@ body_length({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> %% @spec recv_body(request()) -> binary() %% @doc Receive the body of the HTTP request (defined by Content-Length). %% Will only receive up to the default max-body length of 1MB. -recv_body({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +recv_body({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> recv_body(?MAX_RECV_BODY, THIS). %% @spec recv_body(integer(), request()) -> binary() %% @doc Receive the body of the HTTP request (defined by Content-Length). %% Will receive up to MaxBody bytes. -recv_body(MaxBody, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +recv_body(MaxBody, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_BODY) of undefined -> % we could use a sane constant for max chunk size @@ -219,11 +245,11 @@ recv_body(MaxBody, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=T Cached -> Cached end. -stream_body(MaxChunkSize, ChunkFun, FunState, {?MODULE,[_Socket,_Method,_RawPath,_Version,_Headers]}=THIS) -> +stream_body(MaxChunkSize, ChunkFun, FunState, {?MODULE,[_Socket,_Opts,_Method,_RawPath,_Version,_Headers]}=THIS) -> stream_body(MaxChunkSize, ChunkFun, FunState, undefined, THIS). stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength, - {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> Expect = case get_header_value("expect", THIS) of undefined -> undefined; @@ -254,7 +280,7 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength, MaxBodyLength when is_integer(MaxBodyLength), MaxBodyLength < Length -> exit({body_too_large, content_length}); _ -> - stream_unchunked_body(Length, ChunkFun, FunState, THIS) + stream_unchunked_body(MaxChunkSize,Length, ChunkFun, FunState, THIS) end end. @@ -263,23 +289,16 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength, %% @doc Start the HTTP response by sending the Code HTTP response and %% ResponseHeaders. The server will set header defaults such as Server %% and Date if not present in ResponseHeaders. -start_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> - HResponse = mochiweb_headers:make(ResponseHeaders), - HResponse1 = mochiweb_headers:default_from_list(server_headers(), - HResponse), - start_raw_response({Code, HResponse1}, THIS). +start_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> + start_raw_response({Code, ResponseHeaders}, THIS). %% @spec start_raw_response({integer(), headers()}, request()) -> response() %% @doc Start the HTTP response by sending the Code HTTP response and %% ResponseHeaders. -start_raw_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) -> - F = fun ({K, V}, Acc) -> - [mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc] - end, - End = lists:foldl(F, [<<"\r\n">>], - mochiweb_headers:to_list(ResponseHeaders)), - send([make_version(Version), make_code(Code), <<"\r\n">> | End], THIS), - mochiweb:new_response({THIS, Code, ResponseHeaders}). +start_raw_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> + {Header, Response} = format_response_header({Code, ResponseHeaders}, THIS), + send(Header, THIS), + Response. %% @spec start_response_length({integer(), ioheaders(), integer()}, request()) -> response() @@ -288,18 +307,44 @@ start_raw_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPat %% will set header defaults such as Server %% and Date if not present in ResponseHeaders. start_response_length({Code, ResponseHeaders, Length}, - {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse), start_response({Code, HResponse1}, THIS). +%% @spec format_response_header({integer(), ioheaders()} | {integer(), ioheaders(), integer()}, request()) -> iolist() +%% @doc Format the HTTP response header, including the Code HTTP response and +%% ResponseHeaders including an optional Content-Length of Length. The server +%% will set header defaults such as Server +%% and Date if not present in ResponseHeaders. +format_response_header({Code, ResponseHeaders}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, Version, _Headers]}=THIS) -> + HResponse = mochiweb_headers:make(ResponseHeaders), + HResponse1 = mochiweb_headers:default_from_list(server_headers(), HResponse), + HResponse2 = case should_close(THIS) of + true -> + mochiweb_headers:enter("Connection", "close", HResponse1); + false -> + HResponse1 + end, + F = fun ({K, V}, Acc) -> + [mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc] + end, + End = lists:foldl(F, [<<"\r\n">>], mochiweb_headers:to_list(HResponse2)), + Response = mochiweb:new_response({THIS, Code, HResponse2}), + {[make_version(Version), make_code(Code), <<"\r\n">> | End], Response}; +format_response_header({Code, ResponseHeaders, Length}, + {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> + HResponse = mochiweb_headers:make(ResponseHeaders), + HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse), + format_response_header({Code, HResponse1}, THIS). + %% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}, request()) -> response() %% @doc Start the HTTP response with start_response, and send Body to the %% client (if the get(method) /= 'HEAD'). The Content-Length header %% will be set by the Body length, and the server will insert header %% defaults. respond({Code, ResponseHeaders, {file, IoDevice}}, - {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) -> + {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]}=THIS) -> Length = mochiweb_io:iodevice_size(IoDevice), Response = start_response_length({Code, ResponseHeaders, Length}, THIS), case Method of @@ -309,7 +354,7 @@ respond({Code, ResponseHeaders, {file, IoDevice}}, mochiweb_io:iodevice_stream(fun(IOData) -> send(IOData,THIS) end, IoDevice) end, Response; -respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, Method, _RawPath, Version, _Headers]}=THIS) -> +respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, _Opts, Method, _RawPath, Version, _Headers]}=THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = case Method of 'HEAD' -> @@ -331,34 +376,32 @@ respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, Method, _RawPath, HResponse end, start_response({Code, HResponse1}, THIS); -respond({Code, ResponseHeaders, Body}, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) -> - Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}, THIS), +respond({Code, ResponseHeaders, Body}, {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]}=THIS) -> + {Header, Response} = format_response_header({Code, ResponseHeaders, iolist_size(Body)}, THIS), case Method of - 'HEAD' -> - ok; - _ -> - send(Body, THIS) + 'HEAD' -> send(Header, THIS); + _ -> send([Header, Body], THIS) end, Response. %% @spec not_found(request()) -> response() %% @doc Alias for not_found([]). -not_found({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +not_found({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> not_found([], THIS). %% @spec not_found(ExtraHeaders, request()) -> response() %% @doc Alias for respond({404, [{"Content-Type", "text/plain"} %% | ExtraHeaders], <<"Not found.">>}). -not_found(ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +not_found(ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> respond({404, [{"Content-Type", "text/plain"} | ExtraHeaders], <<"Not found.">>}, THIS). %% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}, request()) -> %% response() %% @doc respond({200, [{"Content-Type", ContentType} | Headers], Body}). -ok({ContentType, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +ok({ContentType, Body}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> ok({ContentType, [], Body}, THIS); -ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), case THIS:get(range) of X when (X =:= undefined orelse X =:= fail) orelse Body =:= chunked -> @@ -391,7 +434,7 @@ ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Method, _RawPath, %% @spec should_close(request()) -> bool() %% @doc Return true if the connection must be closed. If false, using %% Keep-Alive should be safe. -should_close({?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) -> +should_close({?MODULE, [_Socket, _Opts, _Method, _RawPath, Version, _Headers]}=THIS) -> ForceClose = erlang:get(?SAVE_FORCE_CLOSE) =/= undefined, DidNotRecv = erlang:get(?SAVE_RECV) =:= undefined, ForceClose orelse Version < {1, 0} @@ -417,7 +460,7 @@ is_close(_) -> %% @spec cleanup(request()) -> ok %% @doc Clean up any junk in the process dictionary, required before continuing %% a Keep-Alive request. -cleanup({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) -> +cleanup({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> L = [?SAVE_QS, ?SAVE_PATH, ?SAVE_RECV, ?SAVE_BODY, ?SAVE_BODY_LENGTH, ?SAVE_POST, ?SAVE_COOKIE, ?SAVE_FORCE_CLOSE], lists:foreach(fun(K) -> @@ -427,7 +470,7 @@ cleanup({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) -> %% @spec parse_qs(request()) -> [{Key::string(), Value::string()}] %% @doc Parse the query string of the URL. -parse_qs({?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) -> +parse_qs({?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) -> case erlang:get(?SAVE_QS) of undefined -> {_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath), @@ -440,12 +483,12 @@ parse_qs({?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) -> %% @spec get_cookie_value(Key::string, request()) -> string() | undefined %% @doc Get the value of the given cookie. -get_cookie_value(Key, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +get_cookie_value(Key, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> proplists:get_value(Key, parse_cookie(THIS)). %% @spec parse_cookie(request()) -> [{Key::string(), Value::string()}] %% @doc Parse the cookie header. -parse_cookie({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +parse_cookie({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_COOKIE) of undefined -> Cookies = case get_header_value("cookie", THIS) of @@ -463,7 +506,7 @@ parse_cookie({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) - %% @spec parse_post(request()) -> [{Key::string(), Value::string()}] %% @doc Parse an application/x-www-form-urlencoded form POST. This %% has the side-effect of calling recv_body(). -parse_post({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +parse_post({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_POST) of undefined -> Parsed = case recv_body(THIS) of @@ -487,7 +530,7 @@ parse_post({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> %% @doc The function is called for each chunk. %% Used internally by read_chunked_body. stream_chunked_body(MaxChunkSize, Fun, FunState, - {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> case read_chunk_length(THIS) of 0 -> Fun({0, read_chunk(0, THIS)}, FunState); @@ -499,27 +542,28 @@ stream_chunked_body(MaxChunkSize, Fun, FunState, stream_chunked_body(MaxChunkSize, Fun, NewState, THIS) end. -stream_unchunked_body(0, Fun, FunState, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) -> +stream_unchunked_body(_MaxChunkSize, 0, Fun, FunState, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> Fun({0, <<>>}, FunState); -stream_unchunked_body(Length, Fun, FunState, - {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > 0 -> - PktSize = case Length > ?RECBUF_SIZE of - true -> - ?RECBUF_SIZE; - false -> - Length +stream_unchunked_body(MaxChunkSize, Length, Fun, FunState, + {?MODULE, [_Socket, Opts, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > 0 -> + RecBuf = case mochilists:get_value(recbuf, Opts, ?RECBUF_SIZE) of + undefined -> %os controlled buffer size + MaxChunkSize; + Val -> + Val end, + PktSize=min(Length,RecBuf), Bin = recv(PktSize, THIS), NewState = Fun({PktSize, Bin}, FunState), - stream_unchunked_body(Length - PktSize, Fun, NewState, THIS). + stream_unchunked_body(MaxChunkSize, Length - PktSize, Fun, NewState, THIS). %% @spec read_chunk_length(request()) -> integer() %% @doc Read the length of the next HTTP chunk. -read_chunk_length({?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> - ok = mochiweb_socket:setopts(Socket, [{packet, line}]), +read_chunk_length({?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, line}])), case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of {ok, Header} -> - ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])), Splitter = fun (C) -> C =/= $\r andalso C =/= $\n andalso C =/= $ end, @@ -532,8 +576,8 @@ read_chunk_length({?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> %% @spec read_chunk(integer(), request()) -> Chunk::binary() | [Footer::binary()] %% @doc Read in a HTTP chunk of the given length. If Length is 0, then read the %% HTTP footers (as a list of binaries, since they're nominal). -read_chunk(0, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> - ok = mochiweb_socket:setopts(Socket, [{packet, line}]), +read_chunk(0, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, line}])), F = fun (F1, Acc) -> case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of {ok, <<"\r\n">>} -> @@ -545,10 +589,10 @@ read_chunk(0, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> end end, Footers = F(F, []), - ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])), put(?SAVE_RECV, true), Footers; -read_chunk(Length, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> +read_chunk(Length, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of {ok, <>} -> Chunk; @@ -557,23 +601,23 @@ read_chunk(Length, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) - end. read_sub_chunks(Length, MaxChunkSize, Fun, FunState, - {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > MaxChunkSize -> + {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > MaxChunkSize -> Bin = recv(MaxChunkSize, THIS), NewState = Fun({size(Bin), Bin}, FunState), read_sub_chunks(Length - MaxChunkSize, MaxChunkSize, Fun, NewState, THIS); read_sub_chunks(Length, _MaxChunkSize, Fun, FunState, - {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> Fun({Length, read_chunk(Length, THIS)}, FunState). %% @spec serve_file(Path, DocRoot, request()) -> Response %% @doc Serve a file relative to DocRoot. -serve_file(Path, DocRoot, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +serve_file(Path, DocRoot, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> serve_file(Path, DocRoot, [], THIS). %% @spec serve_file(Path, DocRoot, ExtraHeaders, request()) -> Response %% @doc Serve a file relative to DocRoot. -serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> case mochiweb_util:safe_relative_path(Path) of undefined -> not_found(ExtraHeaders, THIS); @@ -593,11 +637,11 @@ serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _ directory_index(FullPath) -> filename:join([FullPath, "index.html"]). -maybe_redirect([], FullPath, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +maybe_redirect([], FullPath, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS); maybe_redirect(RelPath, FullPath, ExtraHeaders, - {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}=THIS) -> + {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}=THIS) -> case string:right(RelPath, 1) of "/" -> maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS); @@ -618,7 +662,7 @@ maybe_redirect(RelPath, FullPath, ExtraHeaders, respond({301, MoreHeaders, Body}, THIS) end. -maybe_serve_file(File, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +maybe_serve_file(File, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> case file:read_file_info(File) of {ok, FileInfo} -> LastModified = httpd_util:rfc1123_date(FileInfo#file_info.mtime), @@ -717,7 +761,7 @@ range_parts(Body0, Ranges) -> %% accepted_encodings(["gzip", "deflate", "identity"]) -> %% ["deflate", "gzip", "identity"] %% -accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> AcceptEncodingHeader = case get_header_value("Accept-Encoding", THIS) of undefined -> ""; @@ -755,7 +799,7 @@ accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Method, _RawPath, _V %% 5) For an "Accept" header with value "text/*; q=0.0, */*": %% accepts_content_type("text/plain") -> false %% -accepts_content_type(ContentType1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +accepts_content_type(ContentType1, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> ContentType = re:replace(ContentType1, "\\s", "", [global, {return, list}]), AcceptHeader = accept_header(THIS), case mochiweb_util:parse_qvalues(AcceptHeader) of @@ -804,7 +848,7 @@ accepts_content_type(ContentType1, {?MODULE, [_Socket, _Method, _RawPath, _Versi %% accepts_content_types(["application/json", "text/html"]) -> %% ["text/html", "application/json"] %% -accepted_content_types(Types1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +accepted_content_types(Types1, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> Types = lists:map( fun(T) -> re:replace(T, "\\s", "", [global, {return, list}]) end, Types1), @@ -844,7 +888,7 @@ accepted_content_types(Types1, {?MODULE, [_Socket, _Method, _RawPath, _Version, [Type || {_Q, Type} <- lists:sort(SortFun, TypesQ)] end. -accept_header({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> +accept_header({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) -> case get_header_value("Accept", THIS) of undefined -> "*/*"; diff --git a/src/mochiweb_response.erl b/src/mochiweb_response.erl index 9ab0cef..30055f7 100644 --- a/src/mochiweb_response.erl +++ b/src/mochiweb_response.erl @@ -1,5 +1,23 @@ %% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc Response abstraction. diff --git a/src/mochiweb_session.erl b/src/mochiweb_session.erl index d15ffe7..1d4df35 100644 --- a/src/mochiweb_session.erl +++ b/src/mochiweb_session.erl @@ -1,4 +1,23 @@ %% @author Asier Azkuenaga Batiz +%% @copyright 2013 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: +%% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. %% @doc HTTP Cookie session. Note that the expiration time travels unencrypted %% as far as this module is concerned. In order to achieve more security, diff --git a/src/mochiweb_socket.erl b/src/mochiweb_socket.erl index 76b018c..1756b8e 100644 --- a/src/mochiweb_socket.erl +++ b/src/mochiweb_socket.erl @@ -4,15 +4,22 @@ -module(mochiweb_socket). --export([listen/4, accept/1, recv/3, send/2, close/1, port/1, peername/1, - setopts/2, type/1]). +-export([listen/4, + accept/1, transport_accept/1, finish_accept/1, + recv/3, send/2, close/1, port/1, peername/1, + setopts/2, getopts/2, type/1, exit_if_closed/1]). -define(ACCEPT_TIMEOUT, 2000). +-define(SSL_TIMEOUT, 10000). +-define(SSL_HANDSHAKE_TIMEOUT, 20000). + listen(Ssl, Port, Opts, SslOpts) -> case Ssl of true -> - case ssl:listen(Port, Opts ++ SslOpts) of + Opts1 = add_unbroken_ciphers_default(Opts ++ SslOpts), + Opts2 = add_safe_protocol_versions(Opts1), + case ssl:listen(Port, Opts2) of {ok, ListenSocket} -> {ok, {ssl, ListenSocket}}; {error, _} = Err -> @@ -22,26 +29,74 @@ listen(Ssl, Port, Opts, SslOpts) -> gen_tcp:listen(Port, Opts) end. -accept({ssl, ListenSocket}) -> - % There's a bug in ssl:transport_accept/2 at the moment, which is the - % reason for the try...catch block. Should be fixed in OTP R14. - try ssl:transport_accept(ListenSocket) of +add_unbroken_ciphers_default(Opts) -> + Default = filter_unsecure_cipher_suites(ssl:cipher_suites()), + Ciphers = filter_broken_cipher_suites(proplists:get_value(ciphers, Opts, Default)), + [{ciphers, Ciphers} | proplists:delete(ciphers, Opts)]. + +filter_broken_cipher_suites(Ciphers) -> + case proplists:get_value(ssl_app, ssl:versions()) of + "5.3" ++ _ -> + lists:filter(fun(Suite) -> + string:left(atom_to_list(element(1, Suite)), 4) =/= "ecdh" + end, Ciphers); + _ -> + Ciphers + end. + +filter_unsecure_cipher_suites(Ciphers) -> + lists:filter(fun + ({_,des_cbc,_}) -> false; + ({_,_,md5}) -> false; + (_) -> true + end, + Ciphers). + +add_safe_protocol_versions(Opts) -> + case proplists:is_defined(versions, Opts) of + true -> + Opts; + false -> + Versions = filter_unsafe_protcol_versions(proplists:get_value(available, ssl:versions())), + [{versions, Versions} | Opts] + end. + +filter_unsafe_protcol_versions(Versions) -> + lists:filter(fun + (sslv3) -> false; + (_) -> true + end, + Versions). + +%% Provided for backwards compatibility only +accept(ListenSocket) -> + case transport_accept(ListenSocket) of {ok, Socket} -> - case ssl:ssl_accept(Socket) of - ok -> - {ok, {ssl, Socket}}; - {error, _} = Err -> - Err - end; + finish_accept(Socket); + {error, _} = Err -> + Err + end. + +transport_accept({ssl, ListenSocket}) -> + case ssl:transport_accept(ListenSocket, ?SSL_TIMEOUT) of + {ok, Socket} -> + {ok, {ssl, Socket}}; {error, _} = Err -> Err - catch - error:{badmatch, {error, Reason}} -> - {error, Reason} end; -accept(ListenSocket) -> +transport_accept(ListenSocket) -> gen_tcp:accept(ListenSocket, ?ACCEPT_TIMEOUT). +finish_accept({ssl, Socket}) -> + case ssl:ssl_accept(Socket, ?SSL_HANDSHAKE_TIMEOUT) of + ok -> + {ok, {ssl, Socket}}; + {error, _} = Err -> + Err + end; +finish_accept(Socket) -> + {ok, Socket}. + recv({ssl, Socket}, Length, Timeout) -> ssl:recv(Socket, Length, Timeout); recv(Socket, Length, Timeout) -> @@ -77,8 +132,17 @@ setopts({ssl, Socket}, Opts) -> setopts(Socket, Opts) -> inet:setopts(Socket, Opts). +getopts({ssl, Socket}, Opts) -> + ssl:getopts(Socket, Opts); +getopts(Socket, Opts) -> + inet:getopts(Socket, Opts). + type({ssl, _}) -> ssl; type(_) -> plain. +exit_if_closed({error, closed}) -> + exit(normal); +exit_if_closed(Res) -> + Res. diff --git a/src/mochiweb_socket_server.erl b/src/mochiweb_socket_server.erl index 3b7b3da..fd5e382 100644 --- a/src/mochiweb_socket_server.erl +++ b/src/mochiweb_socket_server.erl @@ -22,6 +22,7 @@ ip=any, listen=null, nodelay=false, + recbuf=?RECBUF_SIZE, backlog=128, active_sockets=0, acceptor_pool_size=16, @@ -116,6 +117,21 @@ parse_options([{backlog, Backlog} | Rest], State) -> parse_options(Rest, State#mochiweb_socket_server{backlog=Backlog}); parse_options([{nodelay, NoDelay} | Rest], State) -> parse_options(Rest, State#mochiweb_socket_server{nodelay=NoDelay}); +parse_options([{recbuf, RecBuf} | Rest], State) when is_integer(RecBuf) orelse + RecBuf == undefined -> + %% XXX: `recbuf' value which is passed to `gen_tcp' + %% and value reported by `inet:getopts(P, [recbuf])' may + %% differ. They depends on underlying OS. From linux mans: + %% + %% The kernel doubles this value (to allow space for + %% bookkeeping overhead) when it is set using setsockopt(2), + %% and this doubled value is returned by getsockopt(2). + %% + %% See: man 7 socket | grep SO_RCVBUF + %% + %% In case undefined is passed instead of the default buffer + %% size ?RECBUF_SIZE, no size is set and the OS can control it dynamically + parse_options(Rest, State#mochiweb_socket_server{recbuf=RecBuf}); parse_options([{acceptor_pool_size, Max} | Rest], State) -> MaxInt = ensure_int(Max), parse_options(Rest, @@ -162,13 +178,14 @@ ipv6_supported() -> false end. -init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, nodelay=NoDelay}) -> +init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, + nodelay=NoDelay, recbuf=RecBuf}) -> process_flag(trap_exit, true), + BaseOpts = [binary, {reuseaddr, true}, {packet, 0}, {backlog, Backlog}, - {recbuf, ?RECBUF_SIZE}, {exit_on_close, false}, {active, false}, {nodelay, NoDelay}], @@ -183,27 +200,33 @@ init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, nodelay=No {_, _, _, _, _, _, _, _} -> % IPv6 [inet6, {ip, Ip} | BaseOpts] end, - listen(Port, Opts, State). - -new_acceptor_pool(Listen, - State=#mochiweb_socket_server{acceptor_pool=Pool, - acceptor_pool_size=Size, - loop=Loop}) -> - F = fun (_, S) -> - Pid = mochiweb_acceptor:start_link(self(), Listen, Loop), - sets:add_element(Pid, S) - end, - Pool1 = lists:foldl(F, Pool, lists:seq(1, Size)), - State#mochiweb_socket_server{acceptor_pool=Pool1}. + OptsBuf=case RecBuf of + undefined -> + Opts; + _ -> + [{recbuf, RecBuf}|Opts] + end, + listen(Port, OptsBuf, State). + +new_acceptor_pool(State=#mochiweb_socket_server{acceptor_pool_size=Size}) -> + lists:foldl(fun (_, S) -> new_acceptor(S) end, State, lists:seq(1, Size)). + +new_acceptor(State=#mochiweb_socket_server{acceptor_pool=Pool, + recbuf=RecBuf, + loop=Loop, + listen=Listen}) -> + LoopOpts = [{recbuf, RecBuf}], + Pid = mochiweb_acceptor:start_link(self(), Listen, Loop, LoopOpts), + State#mochiweb_socket_server{ + acceptor_pool=sets:add_element(Pid, Pool)}. listen(Port, Opts, State=#mochiweb_socket_server{ssl=Ssl, ssl_opts=SslOpts}) -> case mochiweb_socket:listen(Ssl, Port, Opts, SslOpts) of {ok, Listen} -> {ok, ListenPort} = mochiweb_socket:port(Listen), - {ok, new_acceptor_pool( - Listen, - State#mochiweb_socket_server{listen=Listen, - port=ListenPort})}; + {ok, new_acceptor_pool(State#mochiweb_socket_server{ + listen=Listen, + port=ListenPort})}; {error, Reason} -> {stop, Reason} end. @@ -280,32 +303,30 @@ code_change(_OldVsn, State, _Extra) -> recycle_acceptor(Pid, State=#mochiweb_socket_server{ acceptor_pool=Pool, acceptor_pool_size=PoolSize, - listen=Listen, - loop=Loop, max=Max, active_sockets=ActiveSockets}) -> - case sets:is_element(Pid, Pool) of - true -> - Pool1 = sets:del_element(Pid, Pool), - case ActiveSockets + sets:size(Pool1) < Max of - true -> - Acceptor = mochiweb_acceptor:start_link(self(), Listen, Loop), - Pool2 = sets:add_element(Acceptor, Pool1), - State#mochiweb_socket_server{acceptor_pool=Pool2}; - false -> - State#mochiweb_socket_server{acceptor_pool=Pool1} - end; - false -> - case sets:size(Pool) < PoolSize of - true -> - Acceptor = mochiweb_acceptor:start_link(self(), Listen, Loop), - Pool1 = sets:add_element(Acceptor, Pool), - State#mochiweb_socket_server{active_sockets=ActiveSockets, - acceptor_pool=Pool1}; - false -> - State#mochiweb_socket_server{active_sockets=ActiveSockets - 1, - acceptor_pool=Pool} - end + %% A socket is considered to be active from immediately after it + %% has been accepted (see the {accepted, Pid, Timing} cast above). + %% This function will be called when an acceptor is transitioning + %% to an active socket, or when either type of Pid dies. An acceptor + %% Pid will always be in the acceptor_pool set, and an active socket + %% will be in that set during the transition but not afterwards. + Pool1 = sets:del_element(Pid, Pool), + NewSize = sets:size(Pool1), + ActiveSockets1 = case NewSize =:= sets:size(Pool) of + %% Pid has died and it is not in the acceptor set, + %% it must be an active socket. + true -> max(0, ActiveSockets - 1); + false -> ActiveSockets + end, + State1 = State#mochiweb_socket_server{ + acceptor_pool=Pool1, + active_sockets=ActiveSockets1}, + %% Spawn a new acceptor only if it will not overrun the maximum socket + %% count or the maximum pool size. + case NewSize + ActiveSockets1 < Max andalso NewSize < PoolSize of + true -> new_acceptor(State1); + false -> State1 end. handle_info(Msg, State) when ?is_old_state(State) -> diff --git a/src/mochiweb_websocket.erl b/src/mochiweb_websocket.erl index 41884c3..ecf4fb6 100644 --- a/src/mochiweb_websocket.erl +++ b/src/mochiweb_websocket.erl @@ -27,9 +27,12 @@ -export([loop/5, upgrade_connection/2, request/5]). -export([send/3]). +-ifdef(TEST). +-compile(export_all). +-endif. loop(Socket, Body, State, WsVersion, ReplyChannel) -> - ok = mochiweb_socket:setopts(Socket, [{packet, 0}, {active, once}]), + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, 0}, {active, once}])), proc_lib:hibernate(?MODULE, request, [Socket, Body, State, WsVersion, ReplyChannel]). @@ -44,7 +47,7 @@ request(Socket, Body, State, WsVersion, ReplyChannel) -> {tcp_error, _, _} -> mochiweb_socket:close(Socket), exit(normal); - {tcp, _, WsFrames} -> + {Proto, _, WsFrames} when Proto =:= tcp orelse Proto =:= ssl -> case parse_frames(WsVersion, WsFrames, Socket) of close -> mochiweb_socket:close(Socket), @@ -211,7 +214,7 @@ parse_hybi_frames(Socket, <<_Fin:1, _MaskKey:4/binary, _/binary-unit:8>> = PartFrame, Acc) -> - ok = mochiweb_socket:setopts(Socket, [{packet, 0}, {active, once}]), + ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, 0}, {active, once}])), receive {tcp_closed, _} -> mochiweb_socket:close(Socket), @@ -222,7 +225,7 @@ parse_hybi_frames(Socket, <<_Fin:1, {tcp_error, _, _} -> mochiweb_socket:close(Socket), exit(normal); - {tcp, _, Continuation} -> + {Proto, _, Continuation} when Proto =:= tcp orelse Proto =:= ssl -> parse_hybi_frames(Socket, <>, Acc); _ -> @@ -284,11 +287,3 @@ parse_hixie(<<255, Rest/binary>>, Buffer) -> {Buffer, Rest}; parse_hixie(<>, Buffer) -> parse_hixie(T, <>). - -%% -%% Tests -%% --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --compile(export_all). --endif. diff --git a/src/reloader.erl b/src/reloader.erl index 8266b33..8130f45 100644 --- a/src/reloader.erl +++ b/src/reloader.erl @@ -1,6 +1,24 @@ -%% @copyright 2007 Mochi Media, Inc. %% @author Matthew Dempsky +%% @copyright 2007 Mochi Media, Inc. +%% +%% Permission is hereby granted, free of charge, to any person obtaining a +%% copy of this software and associated documentation files (the "Software"), +%% to deal in the Software without restriction, including without limitation +%% the rights to use, copy, modify, merge, publish, distribute, sublicense, +%% and/or sell copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following conditions: %% +%% The above copyright notice and this permission notice shall be included in +%% all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +%% DEALINGS IN THE SOFTWARE. + %% @doc Erlang module for automatically reloading modified modules %% during development. diff --git a/test/mochiweb_socket_server_tests.erl b/test/mochiweb_socket_server_tests.erl index 0dad09d..c64f5b7 100644 --- a/test/mochiweb_socket_server_tests.erl +++ b/test/mochiweb_socket_server_tests.erl @@ -63,7 +63,7 @@ test_basic_accept(Max, PoolSize, NumClients, ReportTo) -> ServerOpts = [{max, Max}, {acceptor_pool_size, PoolSize}], ServerLoop = - fun (Socket) -> + fun (Socket, _Opts) -> Tester ! {server_accepted, self()}, mochiweb_socket:setopts(Socket, [{packet, 1}]), echo_loop(Socket) diff --git a/test/mochiweb_test_util.erl b/test/mochiweb_test_util.erl new file mode 100644 index 0000000..2fbf14f --- /dev/null +++ b/test/mochiweb_test_util.erl @@ -0,0 +1,126 @@ +-module(mochiweb_test_util). +-export([with_server/3, client_request/4, sock_fun/2, + read_server_headers/1, drain_reply/3]). +-include("mochiweb_test_util.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +ssl_cert_opts() -> + EbinDir = filename:dirname(code:which(?MODULE)), + CertDir = filename:join([EbinDir, "..", "support", "test-materials"]), + CertFile = filename:join(CertDir, "test_ssl_cert.pem"), + KeyFile = filename:join(CertDir, "test_ssl_key.pem"), + [{certfile, CertFile}, {keyfile, KeyFile}]. + +with_server(Transport, ServerFun, ClientFun) -> + ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0}, {loop, ServerFun}], + ServerOpts = case Transport of + plain -> + ServerOpts0; + ssl -> + ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}] + end, + {ok, Server} = mochiweb_http:start_link(ServerOpts), + Port = mochiweb_socket_server:get(Server, port), + Res = (catch ClientFun(Transport, Port)), + mochiweb_http:stop(Server), + Res. + +sock_fun(Transport, Port) -> + Opts = [binary, {active, false}, {packet, http}], + case Transport of + plain -> + {ok, Socket} = gen_tcp:connect("127.0.0.1", Port, Opts), + fun (recv) -> + gen_tcp:recv(Socket, 0); + ({recv, Length}) -> + gen_tcp:recv(Socket, Length); + ({send, Data}) -> + gen_tcp:send(Socket, Data); + ({setopts, L}) -> + inet:setopts(Socket, L); + (get) -> + Socket + end; + ssl -> + {ok, Socket} = ssl:connect("127.0.0.1", Port, [{ssl_imp, new} | Opts]), + fun (recv) -> + ssl:recv(Socket, 0); + ({recv, Length}) -> + ssl:recv(Socket, Length); + ({send, Data}) -> + ssl:send(Socket, Data); + ({setopts, L}) -> + ssl:setopts(Socket, L); + (get) -> + {ssl, Socket} + end + end. + +client_request(Transport, Port, Method, TestReqs) -> + client_request(sock_fun(Transport, Port), Method, TestReqs). + +client_request(SockFun, _Method, []) -> + {the_end, {error, closed}} = {the_end, SockFun(recv)}, + ok; +client_request(SockFun, Method, + [#treq{path=Path, body=Body, xreply=ExReply} | Rest]) -> + Request = [atom_to_list(Method), " ", Path, " HTTP/1.1\r\n", + client_headers(Body, Rest =:= []), + "\r\n", + Body], + ok = SockFun({setopts, [{packet, http}]}), + ok = SockFun({send, Request}), + case Method of + 'GET' -> + {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv); + 'POST' -> + {ok, {http_response, {1,1}, 201, "Created"}} = SockFun(recv); + 'CONNECT' -> + {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv) + end, + Headers = read_server_headers(SockFun), + ?assertMatch("MochiWeb" ++ _, mochiweb_headers:get_value("Server", Headers)), + ?assert(mochiweb_headers:get_value("Date", Headers) =/= undefined), + ?assert(mochiweb_headers:get_value("Content-Type", Headers) =/= undefined), + ContentLength = list_to_integer(mochiweb_headers:get_value("Content-Length", Headers)), + {payload, ExReply} = {payload, drain_reply(SockFun, ContentLength, <<>>)}, + client_request(SockFun, Method, Rest). + +read_server_headers(SockFun) -> + ok = SockFun({setopts, [{packet, httph}]}), + Headers = read_server_headers(SockFun, mochiweb_headers:empty()), + ok = SockFun({setopts, [{packet, raw}]}), + Headers. + +read_server_headers(SockFun, Headers) -> + case SockFun(recv) of + {ok, http_eoh} -> + Headers; + {ok, {http_header, _, Header, _, Value}} -> + read_server_headers( + SockFun, + mochiweb_headers:insert(Header, Value, Headers)) + end. + +client_headers(Body, IsLastRequest) -> + ["Host: localhost\r\n", + case Body of + <<>> -> + ""; + _ -> + ["Content-Type: application/octet-stream\r\n", + "Content-Length: ", integer_to_list(byte_size(Body)), "\r\n"] + end, + case IsLastRequest of + true -> + "Connection: close\r\n"; + false -> + "" + end]. + +drain_reply(_SockFun, 0, Acc) -> + Acc; +drain_reply(SockFun, Length, Acc) -> + Sz = erlang:min(Length, 1024), + {ok, B} = SockFun({recv, Sz}), + drain_reply(SockFun, Length - Sz, <>). diff --git a/test/mochiweb_test_util.hrl b/test/mochiweb_test_util.hrl new file mode 100644 index 0000000..99fdc4e --- /dev/null +++ b/test/mochiweb_test_util.hrl @@ -0,0 +1 @@ +-record(treq, {path, body= <<>>, xreply= <<>>}). diff --git a/test/mochiweb_tests.erl b/test/mochiweb_tests.erl index c8bc8ac..209971b 100644 --- a/test/mochiweb_tests.erl +++ b/test/mochiweb_tests.erl @@ -1,28 +1,9 @@ -module(mochiweb_tests). -include_lib("eunit/include/eunit.hrl"). - --record(treq, {path, body= <<>>, xreply= <<>>}). - -ssl_cert_opts() -> - EbinDir = filename:dirname(code:which(?MODULE)), - CertDir = filename:join([EbinDir, "..", "support", "test-materials"]), - CertFile = filename:join(CertDir, "test_ssl_cert.pem"), - KeyFile = filename:join(CertDir, "test_ssl_key.pem"), - [{certfile, CertFile}, {keyfile, KeyFile}]. +-include("mochiweb_test_util.hrl"). with_server(Transport, ServerFun, ClientFun) -> - ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0}, {loop, ServerFun}], - ServerOpts = case Transport of - plain -> - ServerOpts0; - ssl -> - ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}] - end, - {ok, Server} = mochiweb_http:start_link(ServerOpts), - Port = mochiweb_socket_server:get(Server, port), - Res = (catch ClientFun(Transport, Port)), - mochiweb_http:stop(Server), - Res. + mochiweb_test_util:with_server(Transport, ServerFun, ClientFun). request_test() -> R = mochiweb_request:new(z, z, "/foo/bar/baz%20wibble+quux?qs=2", z, []), @@ -148,6 +129,7 @@ do_GET(PathPrefix, Transport, Times) -> ClientFun = new_client_fun('GET', TestReqs), ok = with_server(Transport, ServerFun, ClientFun), ok. + do_POST(Transport, Size, Times) -> ServerFun = fun (Req) -> Body = Req:recv_body(), @@ -165,86 +147,57 @@ do_POST(Transport, Size, Times) -> new_client_fun(Method, TestReqs) -> fun (Transport, Port) -> - client_request(Transport, Port, Method, TestReqs) + mochiweb_test_util:client_request(Transport, Port, Method, TestReqs) end. -client_request(Transport, Port, Method, TestReqs) -> - Opts = [binary, {active, false}, {packet, http}], - SockFun = case Transport of - plain -> - {ok, Socket} = gen_tcp:connect("127.0.0.1", Port, Opts), - fun (recv) -> - gen_tcp:recv(Socket, 0); - ({recv, Length}) -> - gen_tcp:recv(Socket, Length); - ({send, Data}) -> - gen_tcp:send(Socket, Data); - ({setopts, L}) -> - inet:setopts(Socket, L) - end; - ssl -> - {ok, Socket} = ssl:connect("127.0.0.1", Port, [{ssl_imp, new} | Opts]), - fun (recv) -> - ssl:recv(Socket, 0); - ({recv, Length}) -> - ssl:recv(Socket, Length); - ({send, Data}) -> - ssl:send(Socket, Data); - ({setopts, L}) -> - ssl:setopts(Socket, L) - end - end, - client_request(SockFun, Method, TestReqs). - -client_request(SockFun, _Method, []) -> - {the_end, {error, closed}} = {the_end, SockFun(recv)}, - ok; -client_request(SockFun, Method, - [#treq{path=Path, body=Body, xreply=ExReply} | Rest]) -> - Request = [atom_to_list(Method), " ", Path, " HTTP/1.1\r\n", - client_headers(Body, Rest =:= []), - "\r\n", - Body], - ok = SockFun({send, Request}), - case Method of - 'GET' -> - {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv); - 'POST' -> - {ok, {http_response, {1,1}, 201, "Created"}} = SockFun(recv); - 'CONNECT' -> - {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv) - end, - ok = SockFun({setopts, [{packet, httph}]}), - {ok, {http_header, _, 'Server', _, "MochiWeb" ++ _}} = SockFun(recv), - {ok, {http_header, _, 'Date', _, _}} = SockFun(recv), - {ok, {http_header, _, 'Content-Type', _, _}} = SockFun(recv), - {ok, {http_header, _, 'Content-Length', _, ConLenStr}} = SockFun(recv), - ContentLength = list_to_integer(ConLenStr), - {ok, http_eoh} = SockFun(recv), - ok = SockFun({setopts, [{packet, raw}]}), - {payload, ExReply} = {payload, drain_reply(SockFun, ContentLength, <<>>)}, +close_on_unread_data_test() -> + ok = with_server( + plain, + fun mochiweb_request:not_found/1, + fun close_on_unread_data_client/2). + +close_on_unread_data_client(Transport, Port) -> + SockFun = mochiweb_test_util:sock_fun(Transport, Port), + %% A normal GET request should not trigger this behavior + Request0 = string:join( + ["GET / HTTP/1.1", + "Host: localhost", + "", + ""], + "\r\n"), ok = SockFun({setopts, [{packet, http}]}), - client_request(SockFun, Method, Rest). - -client_headers(Body, IsLastRequest) -> - ["Host: localhost\r\n", - case Body of - <<>> -> - ""; - _ -> - ["Content-Type: application/octet-stream\r\n", - "Content-Length: ", integer_to_list(byte_size(Body)), "\r\n"] - end, - case IsLastRequest of - true -> - "Connection: close\r\n"; - false -> - "" - end]. - -drain_reply(_SockFun, 0, Acc) -> - Acc; -drain_reply(SockFun, Length, Acc) -> - Sz = erlang:min(Length, 1024), - {ok, B} = SockFun({recv, Sz}), - drain_reply(SockFun, Length - Sz, <>). + ok = SockFun({send, Request0}), + ?assertMatch( + {ok, {http_response, {1, 1}, 404, _}}, + SockFun(recv)), + Headers0 = mochiweb_test_util:read_server_headers(SockFun), + ?assertEqual( + undefined, + mochiweb_headers:get_value("Connection", Headers0)), + Len0 = list_to_integer( + mochiweb_headers:get_value("Content-Length", Headers0)), + _Body0 = mochiweb_test_util:drain_reply(SockFun, Len0, <<>>), + %% Re-use same socket + Request = string:join( + ["POST / HTTP/1.1", + "Host: localhost", + "Content-Type: application/json", + "Content-Length: 2", + "", + "{}"], + "\r\n"), + ok = SockFun({setopts, [{packet, http}]}), + ok = SockFun({send, Request}), + ?assertMatch( + {ok, {http_response, {1, 1}, 404, _}}, + SockFun(recv)), + Headers = mochiweb_test_util:read_server_headers(SockFun), + %% Expect to see a Connection: close header when we know the + %% server will close the connection re #146 + ?assertEqual( + "close", + mochiweb_headers:get_value("Connection", Headers)), + Len = list_to_integer(mochiweb_headers:get_value("Content-Length", Headers)), + _Body = mochiweb_test_util:drain_reply(SockFun, Len, <<>>), + ?assertEqual({error, closed}, SockFun(recv)), + ok. diff --git a/test/mochiweb_websocket_tests.erl b/test/mochiweb_websocket_tests.erl index 890aa17..eb8de5b 100644 --- a/test/mochiweb_websocket_tests.erl +++ b/test/mochiweb_websocket_tests.erl @@ -82,3 +82,79 @@ hixie_frames_decode_test() -> mochiweb_websocket:parse_hixie_frames( <<0,102,111,111,255,0,98,97,114,255>>, [])). + +end_to_end_test_factory(ServerTransport) -> + mochiweb_test_util:with_server( + ServerTransport, + fun end_to_end_server/1, + fun (Transport, Port) -> + end_to_end_client(mochiweb_test_util:sock_fun(Transport, Port)) + end). + +end_to_end_server(Req) -> + ?assertEqual("Upgrade", Req:get_header_value("connection")), + ?assertEqual("websocket", Req:get_header_value("upgrade")), + {ReentryWs, _ReplyChannel} = mochiweb_websocket:upgrade_connection( + Req, + fun end_to_end_ws_loop/3), + ReentryWs(ok). + +end_to_end_ws_loop(Payload, State, ReplyChannel) -> + %% Echo server + lists:foreach(ReplyChannel, Payload), + State. + +end_to_end_client(S) -> + %% Key and Accept per https://tools.ietf.org/html/rfc6455 + UpgradeReq = string:join( + ["GET / HTTP/1.1", + "Host: localhost", + "Upgrade: websocket", + "Connection: Upgrade", + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==", + "", + ""], "\r\n"), + ok = S({send, UpgradeReq}), + {ok, {http_response, {1,1}, 101, _}} = S(recv), + read_expected_headers( + S, + [{'Upgrade', "websocket"}, + {'Connection', "Upgrade"}, + {'Content-Length', "0"}, + {"Sec-Websocket-Accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}]), + %% The first message sent over telegraph :) + SmallMessage = <<"What hath God wrought?">>, + ok = S({send, + << 1:1, %% Fin + 0:1, %% Rsv1 + 0:1, %% Rsv2 + 0:1, %% Rsv3 + 2:4, %% Opcode, 1 = text frame + 1:1, %% Mask on + (byte_size(SmallMessage)):7, %% Length, <125 case + 0:32, %% Mask (trivial) + SmallMessage/binary >>}), + {ok, WsFrames} = S(recv), + << 1:1, %% Fin + 0:1, %% Rsv1 + 0:1, %% Rsv2 + 0:1, %% Rsv3 + 1:4, %% Opcode, text frame (all mochiweb suports for now) + MsgSize:8, %% Expecting small size + SmallMessage/binary >> = WsFrames, + ?assertEqual(MsgSize, byte_size(SmallMessage)), + ok. + +read_expected_headers(S, D) -> + Headers = mochiweb_test_util:read_server_headers(S), + lists:foreach( + fun ({K, V}) -> + ?assertEqual(V, mochiweb_headers:get_value(K, Headers)) + end, + D). + +end_to_end_http_test() -> + end_to_end_test_factory(plain). + +end_to_end_https_test() -> + end_to_end_test_factory(ssl). From 449796fbf4060091d3af262f285a469f5f102742 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Fri, 8 Feb 2019 16:37:42 +0000 Subject: [PATCH 3/7] Broken attempt to add recbuf support make test fails. Perhaps due to issue with Opts being passed into call_loop ... do other things need to change because of this? --- src/mochiweb_acceptor.erl | 38 +++++++++++++--------------- src/mochiweb_socket_server.erl | 46 ++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/mochiweb_acceptor.erl b/src/mochiweb_acceptor.erl index ebbaf45..282e93d 100644 --- a/src/mochiweb_acceptor.erl +++ b/src/mochiweb_acceptor.erl @@ -8,21 +8,26 @@ -include("internal.hrl"). --export([start_link/3, init/3]). +-export([start_link/3, start_link/4, init/4]). + +-define(EMFILE_SLEEP_MSEC, 100). start_link(Server, Listen, Loop) -> - proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop]). + start_link(Server, Listen, Loop, []). + +start_link(Server, Listen, Loop, Opts) -> + proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop, Opts]). -init(Server, Listen, Loop) -> +init(Server, Listen, Loop, Opts) -> T1 = os:timestamp(), case catch mochiweb_socket:accept(Listen) of {ok, Socket} -> gen_server:cast(Server, {accepted, self(), timer:now_diff(os:timestamp(), T1)}), - call_loop(Loop, Socket); + call_loop(Loop, Socket, Opts); {error, closed} -> exit(normal); {error, timeout} -> - init(Server, Listen, Loop); + init(Server, Listen, Loop, Opts); {error, esslaccept} -> exit(normal); Other -> @@ -33,18 +38,11 @@ init(Server, Listen, Loop) -> exit({error, accept_failed}) end. -call_loop({M, F}, Socket) -> - M:F(Socket); -call_loop({M, F, [A1]}, Socket) -> - M:F(Socket, A1); -call_loop({M, F, A}, Socket) -> - erlang:apply(M, F, [Socket | A]); -call_loop(Loop, Socket) -> - Loop(Socket). - -%% -%% Tests -%% --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. +call_loop({M, F}, Socket, Opts) -> + M:F(Socket, Opts); +call_loop({M, F, [A1]}, Socket, Opts) -> + M:F(Socket, Opts, A1); +call_loop({M, F, A}, Socket, Opts) -> + erlang:apply(M, F, [Socket, Opts | A]); +call_loop(Loop, Socket, Opts) -> + Loop(Socket, Opts). \ No newline at end of file diff --git a/src/mochiweb_socket_server.erl b/src/mochiweb_socket_server.erl index 3b7b3da..fcd2d8c 100644 --- a/src/mochiweb_socket_server.erl +++ b/src/mochiweb_socket_server.erl @@ -22,6 +22,7 @@ ip=any, listen=null, nodelay=false, + recbuf=?RECBUF_SIZE, backlog=128, active_sockets=0, acceptor_pool_size=16, @@ -116,6 +117,17 @@ parse_options([{backlog, Backlog} | Rest], State) -> parse_options(Rest, State#mochiweb_socket_server{backlog=Backlog}); parse_options([{nodelay, NoDelay} | Rest], State) -> parse_options(Rest, State#mochiweb_socket_server{nodelay=NoDelay}); +parse_options([{recbuf, RecBuf} | Rest], State) when is_integer(RecBuf) -> + %% XXX: `recbuf' value which is passed to `gen_tcp' + %% and value reported by `inet:getopts(P, [recbuf])' may + %% differ. They depends on underlying OS. From linux mans: + %% + %% The kernel doubles this value (to allow space for + %% bookkeeping overhead) when it is set using setsockopt(2), + %% and this doubled value is returned by getsockopt(2). + %% + %% See: man 7 socket | grep SO_RCVBUF + parse_options(Rest, State#mochiweb_socket_server{recbuf=RecBuf}); parse_options([{acceptor_pool_size, Max} | Rest], State) -> MaxInt = ensure_int(Max), parse_options(Rest, @@ -162,13 +174,15 @@ ipv6_supported() -> false end. -init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, nodelay=NoDelay}) -> +init(State=#mochiweb_socket_server{ip=Ip, port=Port, + backlog=Backlog, nodelay=NoDelay, + recbuf=RecBuf}) -> process_flag(trap_exit, true), BaseOpts = [binary, {reuseaddr, true}, {packet, 0}, {backlog, Backlog}, - {recbuf, ?RECBUF_SIZE}, + {recbuf, RecBuf}, {exit_on_close, false}, {active, false}, {nodelay, NoDelay}], @@ -185,25 +199,25 @@ init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, nodelay=No end, listen(Port, Opts, State). -new_acceptor_pool(Listen, - State=#mochiweb_socket_server{acceptor_pool=Pool, - acceptor_pool_size=Size, - loop=Loop}) -> - F = fun (_, S) -> - Pid = mochiweb_acceptor:start_link(self(), Listen, Loop), - sets:add_element(Pid, S) - end, - Pool1 = lists:foldl(F, Pool, lists:seq(1, Size)), - State#mochiweb_socket_server{acceptor_pool=Pool1}. +new_acceptor_pool(State=#mochiweb_socket_server{acceptor_pool_size=Size}) -> + lists:foldl(fun (_, S) -> new_acceptor(S) end, State, lists:seq(1, Size)). + +new_acceptor(State=#mochiweb_socket_server{acceptor_pool=Pool, + recbuf=RecBuf, + loop=Loop, + listen=Listen}) -> + LoopOpts = [{recbuf, RecBuf}], + Pid = mochiweb_acceptor:start_link(self(), Listen, Loop, LoopOpts), + State#mochiweb_socket_server{ + acceptor_pool=sets:add_element(Pid, Pool)}. listen(Port, Opts, State=#mochiweb_socket_server{ssl=Ssl, ssl_opts=SslOpts}) -> case mochiweb_socket:listen(Ssl, Port, Opts, SslOpts) of {ok, Listen} -> {ok, ListenPort} = mochiweb_socket:port(Listen), - {ok, new_acceptor_pool( - Listen, - State#mochiweb_socket_server{listen=Listen, - port=ListenPort})}; + {ok, new_acceptor_pool(State#mochiweb_socket_server{ + listen=Listen, + port=ListenPort})}; {error, Reason} -> {stop, Reason} end. From e6b174eadf7c829bb8a338262911e2740e18d849 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Fri, 8 Feb 2019 16:59:39 +0000 Subject: [PATCH 4/7] Revert "Broken attempt to add recbuf support" This reverts commit 449796fbf4060091d3af262f285a469f5f102742. --- src/mochiweb_acceptor.erl | 38 +++++++++++++++------------- src/mochiweb_socket_server.erl | 46 ++++++++++++---------------------- 2 files changed, 36 insertions(+), 48 deletions(-) diff --git a/src/mochiweb_acceptor.erl b/src/mochiweb_acceptor.erl index 282e93d..ebbaf45 100644 --- a/src/mochiweb_acceptor.erl +++ b/src/mochiweb_acceptor.erl @@ -8,26 +8,21 @@ -include("internal.hrl"). --export([start_link/3, start_link/4, init/4]). - --define(EMFILE_SLEEP_MSEC, 100). +-export([start_link/3, init/3]). start_link(Server, Listen, Loop) -> - start_link(Server, Listen, Loop, []). - -start_link(Server, Listen, Loop, Opts) -> - proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop, Opts]). + proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop]). -init(Server, Listen, Loop, Opts) -> +init(Server, Listen, Loop) -> T1 = os:timestamp(), case catch mochiweb_socket:accept(Listen) of {ok, Socket} -> gen_server:cast(Server, {accepted, self(), timer:now_diff(os:timestamp(), T1)}), - call_loop(Loop, Socket, Opts); + call_loop(Loop, Socket); {error, closed} -> exit(normal); {error, timeout} -> - init(Server, Listen, Loop, Opts); + init(Server, Listen, Loop); {error, esslaccept} -> exit(normal); Other -> @@ -38,11 +33,18 @@ init(Server, Listen, Loop, Opts) -> exit({error, accept_failed}) end. -call_loop({M, F}, Socket, Opts) -> - M:F(Socket, Opts); -call_loop({M, F, [A1]}, Socket, Opts) -> - M:F(Socket, Opts, A1); -call_loop({M, F, A}, Socket, Opts) -> - erlang:apply(M, F, [Socket, Opts | A]); -call_loop(Loop, Socket, Opts) -> - Loop(Socket, Opts). \ No newline at end of file +call_loop({M, F}, Socket) -> + M:F(Socket); +call_loop({M, F, [A1]}, Socket) -> + M:F(Socket, A1); +call_loop({M, F, A}, Socket) -> + erlang:apply(M, F, [Socket | A]); +call_loop(Loop, Socket) -> + Loop(Socket). + +%% +%% Tests +%% +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-endif. diff --git a/src/mochiweb_socket_server.erl b/src/mochiweb_socket_server.erl index fcd2d8c..3b7b3da 100644 --- a/src/mochiweb_socket_server.erl +++ b/src/mochiweb_socket_server.erl @@ -22,7 +22,6 @@ ip=any, listen=null, nodelay=false, - recbuf=?RECBUF_SIZE, backlog=128, active_sockets=0, acceptor_pool_size=16, @@ -117,17 +116,6 @@ parse_options([{backlog, Backlog} | Rest], State) -> parse_options(Rest, State#mochiweb_socket_server{backlog=Backlog}); parse_options([{nodelay, NoDelay} | Rest], State) -> parse_options(Rest, State#mochiweb_socket_server{nodelay=NoDelay}); -parse_options([{recbuf, RecBuf} | Rest], State) when is_integer(RecBuf) -> - %% XXX: `recbuf' value which is passed to `gen_tcp' - %% and value reported by `inet:getopts(P, [recbuf])' may - %% differ. They depends on underlying OS. From linux mans: - %% - %% The kernel doubles this value (to allow space for - %% bookkeeping overhead) when it is set using setsockopt(2), - %% and this doubled value is returned by getsockopt(2). - %% - %% See: man 7 socket | grep SO_RCVBUF - parse_options(Rest, State#mochiweb_socket_server{recbuf=RecBuf}); parse_options([{acceptor_pool_size, Max} | Rest], State) -> MaxInt = ensure_int(Max), parse_options(Rest, @@ -174,15 +162,13 @@ ipv6_supported() -> false end. -init(State=#mochiweb_socket_server{ip=Ip, port=Port, - backlog=Backlog, nodelay=NoDelay, - recbuf=RecBuf}) -> +init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, nodelay=NoDelay}) -> process_flag(trap_exit, true), BaseOpts = [binary, {reuseaddr, true}, {packet, 0}, {backlog, Backlog}, - {recbuf, RecBuf}, + {recbuf, ?RECBUF_SIZE}, {exit_on_close, false}, {active, false}, {nodelay, NoDelay}], @@ -199,25 +185,25 @@ init(State=#mochiweb_socket_server{ip=Ip, port=Port, end, listen(Port, Opts, State). -new_acceptor_pool(State=#mochiweb_socket_server{acceptor_pool_size=Size}) -> - lists:foldl(fun (_, S) -> new_acceptor(S) end, State, lists:seq(1, Size)). - -new_acceptor(State=#mochiweb_socket_server{acceptor_pool=Pool, - recbuf=RecBuf, - loop=Loop, - listen=Listen}) -> - LoopOpts = [{recbuf, RecBuf}], - Pid = mochiweb_acceptor:start_link(self(), Listen, Loop, LoopOpts), - State#mochiweb_socket_server{ - acceptor_pool=sets:add_element(Pid, Pool)}. +new_acceptor_pool(Listen, + State=#mochiweb_socket_server{acceptor_pool=Pool, + acceptor_pool_size=Size, + loop=Loop}) -> + F = fun (_, S) -> + Pid = mochiweb_acceptor:start_link(self(), Listen, Loop), + sets:add_element(Pid, S) + end, + Pool1 = lists:foldl(F, Pool, lists:seq(1, Size)), + State#mochiweb_socket_server{acceptor_pool=Pool1}. listen(Port, Opts, State=#mochiweb_socket_server{ssl=Ssl, ssl_opts=SslOpts}) -> case mochiweb_socket:listen(Ssl, Port, Opts, SslOpts) of {ok, Listen} -> {ok, ListenPort} = mochiweb_socket:port(Listen), - {ok, new_acceptor_pool(State#mochiweb_socket_server{ - listen=Listen, - port=ListenPort})}; + {ok, new_acceptor_pool( + Listen, + State#mochiweb_socket_server{listen=Listen, + port=ListenPort})}; {error, Reason} -> {stop, Reason} end. From 8c8096580b0ce03f0435aea590b99e43e5e40ab4 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Fri, 8 Feb 2019 17:06:11 +0000 Subject: [PATCH 5/7] Addmissing Opts to handle_invalid_msg_request Also needed toc hange the unit test. the unit test was testing a bad header ... but then expecting a 200 OK response ... but it is invalid? The test no longer exists in this form - so I'm not sure whether it is valid to change this. I can't find direct evidence the test was previously wrong. Perhaps I have misunderstoof the purpose of the test. https://github.com/basho/mochiweb/commit/5a3d51172814f309b132f818cb5902419245c5ef. Does this imply that the test is expecting to handle another message correctly even if interrupted by a partial header? --- src/mochiweb_http.erl | 4 ++-- test/mochiweb_http_tests.erl | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mochiweb_http.erl b/src/mochiweb_http.erl index da2c4a4..7a677f7 100644 --- a/src/mochiweb_http.erl +++ b/src/mochiweb_http.erl @@ -92,7 +92,7 @@ request(Socket, Opts, Body) -> exit(normal); Msg = {ProtocolErr, _Socket, emsgsize} when ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error -> - handle_invalid_msg_request(Msg, Socket); + handle_invalid_msg_request(Msg, Socket, Opts); {ProtocolErr, _Socket, _Reason} when ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error -> mochiweb_socket:close(Socket), @@ -128,7 +128,7 @@ headers(Socket, Opts, Request, Headers, Body, HeaderCount) -> exit(normal); Msg = {ProtocolErr, _Socket, emsgsize} when ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error -> - handle_invalid_msg_request(Msg, Socket); + handle_invalid_msg_request(Msg, Socket, Opts); Msg = {ProtocolErr, _Socket, _Reason} when ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error -> error_logger:warning_msg("Got unexpected TCP error message: ~w (to pid=~w)~n", diff --git a/test/mochiweb_http_tests.erl b/test/mochiweb_http_tests.erl index 93824b8..1eb9b77 100644 --- a/test/mochiweb_http_tests.erl +++ b/test/mochiweb_http_tests.erl @@ -61,7 +61,7 @@ get_req() -> unexpected_msg(Server, MsgAt, Msg) -> %% Set up with a single acceptor, dig out the pid - sensitive to change in mochiweb_socket_server %% state record. - [Acceptor] = sets:to_list(element(14, sys:get_state(Server))), + [Acceptor] = sets:to_list(element(15, sys:get_state(Server))), Port = mochiweb_socket_server:get(Server, port), <> = get_req(), {ok, S} = gen_tcp:connect({127,0,0,1},Port,[binary,{active,false}]), @@ -73,10 +73,10 @@ unexpected_msg(Server, MsgAt, Msg) -> unexpected_msg_in_hdr_tests(Server) -> [{"should ignore a message in the middle of the request line", - ?_assertMatch({ok, <<"HTTP/1.1 200 OK", _Rest/binary>>}, + ?_assertMatch({ok, <<"HTTP/1.1 400 Bad Request", _Rest/binary>>}, unexpected_msg(Server, ?IN_METHOD, unexpected_msg_in_your_method))}, {"should ignore a message in the middle of a header", - ?_assertMatch({ok, <<"HTTP/1.1 200 OK", _Rest/binary>>}, + ?_assertMatch({ok, <<"HTTP/1.1 400 Bad Request", _Rest/binary>>}, unexpected_msg(Server, ?IN_HEADER, unexpected_msg_in_your_header))}, {"should close on a TCP error on the request line", ?_assertMatch({error, closed}, From 7b089239a029d590ffc3e3bca8b61be3769a2681 Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Sat, 9 Feb 2019 17:02:20 +0000 Subject: [PATCH 6/7] Return 431 not 400 If a header is too large. Also have test to confirm that seeting recbuf to a large value resolves this. --- src/mochiweb_http.erl | 28 +++++------ test/mochiweb_http_tests.erl | 93 ++++++++++++++++++++++++++++++++---- 2 files changed, 99 insertions(+), 22 deletions(-) diff --git a/src/mochiweb_http.erl b/src/mochiweb_http.erl index 7a677f7..4648236 100644 --- a/src/mochiweb_http.erl +++ b/src/mochiweb_http.erl @@ -92,13 +92,13 @@ request(Socket, Opts, Body) -> exit(normal); Msg = {ProtocolErr, _Socket, emsgsize} when ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error -> - handle_invalid_msg_request(Msg, Socket, Opts); + handle_invalid_msg_request(Msg, Socket, Opts, 400); {ProtocolErr, _Socket, _Reason} when ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error -> mochiweb_socket:close(Socket), exit(normal); Other -> - handle_invalid_msg_request(Other, Socket, Opts) + handle_invalid_msg_request(Other, Socket, Opts, 400) after ?REQUEST_RECV_TIMEOUT -> mochiweb_socket:close(Socket), exit(normal) @@ -112,7 +112,7 @@ reentry(Body) -> headers(Socket, Opts, Request, Headers, _Body, ?MAX_HEADERS) -> %% Too many headers sent, bad request. ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])), - handle_invalid_request(Socket, Opts, Request, Headers); + handle_invalid_request(Socket, Opts, 400, Request, Headers); headers(Socket, Opts, Request, Headers, Body, HeaderCount) -> ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{active, once}])), receive @@ -128,7 +128,7 @@ headers(Socket, Opts, Request, Headers, Body, HeaderCount) -> exit(normal); Msg = {ProtocolErr, _Socket, emsgsize} when ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error -> - handle_invalid_msg_request(Msg, Socket, Opts); + handle_invalid_msg_request(Msg, Socket, Opts, 431); Msg = {ProtocolErr, _Socket, _Reason} when ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error -> error_logger:warning_msg("Got unexpected TCP error message: ~w (to pid=~w)~n", @@ -136,7 +136,7 @@ headers(Socket, Opts, Request, Headers, Body, HeaderCount) -> mochiweb_socket:close(Socket), exit(normal); Other -> - handle_invalid_msg_request(Other, Socket, Opts, Request, Headers) + handle_invalid_msg_request(Other, Socket, Opts, 400, Request, Headers) after ?HEADERS_RECV_TIMEOUT -> mochiweb_socket:close(Socket), exit(normal) @@ -149,25 +149,25 @@ call_body({M, F}, Req) -> call_body(Body, Req) -> Body(Req). --spec handle_invalid_msg_request(term(), term(), term()) -> no_return(). -handle_invalid_msg_request(Msg, Socket, Opts) -> - handle_invalid_msg_request(Msg, Socket, Opts, {'GET', {abs_path, "/"}, {0,9}}, []). +-spec handle_invalid_msg_request(term(), term(), term(), pos_integer()) -> no_return(). +handle_invalid_msg_request(Msg, Socket, Opts, StatusCode) -> + handle_invalid_msg_request(Msg, Socket, Opts, StatusCode, {'GET', {abs_path, "/"}, {0,9}}, []). --spec handle_invalid_msg_request(term(), term(), term(), term(), term()) -> no_return(). -handle_invalid_msg_request(Msg, Socket, Opts, Request, RevHeaders) -> +-spec handle_invalid_msg_request(term(), term(), term(), pos_integer(), term(), term()) -> no_return(). +handle_invalid_msg_request(Msg, Socket, Opts, StatusCode, Request, RevHeaders) -> case {Msg, r15b_workaround()} of {{tcp_error,_,emsgsize}, true} -> %% R15B02 returns this then closes the socket, so close and exit mochiweb_socket:close(Socket), exit(normal); _ -> - handle_invalid_request(Socket, Opts, Request, RevHeaders) + handle_invalid_request(Socket, Opts, StatusCode, Request, RevHeaders) end. --spec handle_invalid_request(term(), term(), term(), term()) -> no_return(). -handle_invalid_request(Socket, Opts, Request, RevHeaders) -> +-spec handle_invalid_request(term(), term(), pos_integer(), term(), term()) -> no_return(). +handle_invalid_request(Socket, Opts, StatusCode, Request, RevHeaders) -> Req = new_request(Socket, Opts, Request, RevHeaders), - Req:respond({400, [], []}), + Req:respond({StatusCode, [], []}), mochiweb_socket:close(Socket), exit(normal). diff --git a/test/mochiweb_http_tests.erl b/test/mochiweb_http_tests.erl index 1eb9b77..8b5c235 100644 --- a/test/mochiweb_http_tests.erl +++ b/test/mochiweb_http_tests.erl @@ -19,6 +19,18 @@ unexpected_msg_test_() -> fun mochiweb_http:stop/1, fun unexpected_msg_in_hdr_tests/1}. +bad_headers_test_() -> + {setup, + fun start_server/0, + fun mochiweb_http:stop/1, + fun bad_headers_tests/1}. + +bad_headers2_test_() -> + {setup, + fun start_bigbuf_server/0, + fun mochiweb_http:stop/1, + fun bad_headers_bigbuf_tests/1}. + start_server() -> application:start(inets), {ok, Pid} = mochiweb_http:start_link([{port, 0}, @@ -26,6 +38,15 @@ start_server() -> {loop, fun responder/1}]), Pid. +start_bigbuf_server() -> + application:start(inets), + {ok, Pid} = mochiweb_http:start_link([{port, 0}, + {recbuf, 65536}, + {acceptor_pool_size,1}, + {loop, fun responder/1}]), + Pid. + + has_acceptor_bug_tests(Server) -> Port = mochiweb_socket_server:get(Server, port), [{"1000 should be fine even with the bug", @@ -47,7 +68,7 @@ has_bug(Port, Len) -> true; {ok, {{"HTTP/1.1", 200, "OK"}, _, "Hello"}} -> false; - {ok, {{"HTTP/1.1", 400, "Bad Request"}, _, []}} -> + {ok, {{"HTTP/1.1", 431, "Internal Server Error"}, _, []}} -> false end. @@ -56,31 +77,87 @@ has_bug(Port, Len) -> get_req() -> <<"GET / HTTP/1.1\r\n" "User-Agent: mochiweb_http_tests/0.0\r\n" - "Accept: */*\r\n\r\n">>. + "Accept: */*\r\n">>. -unexpected_msg(Server, MsgAt, Msg) -> +eol(R) -> + EOL = <<"\r\n">>, + <>. + + +setup_server(Server) -> %% Set up with a single acceptor, dig out the pid - sensitive to change in mochiweb_socket_server %% state record. [Acceptor] = sets:to_list(element(15, sys:get_state(Server))), Port = mochiweb_socket_server:get(Server, port), - <> = get_req(), {ok, S} = gen_tcp:connect({127,0,0,1},Port,[binary,{active,false}]), + {S, Acceptor}. + +unexpected_msg_send(Server, MsgAt, Msg) -> + {S, Acceptor} = setup_server(Server), + <> = eol(get_req()) , ok = gen_tcp:send(S, Before), Acceptor ! Msg, gen_tcp:send(S, After), gen_tcp:recv(S, 0, 5000). +invalid_header_send(Server, Msg) -> + {S, Acceptor} = setup_server(Server), + gen_tcp:send(S, Msg), + gen_tcp:recv(S, 0, 5000). + + unexpected_msg_in_hdr_tests(Server) -> [{"should ignore a message in the middle of the request line", ?_assertMatch({ok, <<"HTTP/1.1 400 Bad Request", _Rest/binary>>}, - unexpected_msg(Server, ?IN_METHOD, unexpected_msg_in_your_method))}, + unexpected_msg_send(Server, ?IN_METHOD, unexpected_msg_in_your_method))}, {"should ignore a message in the middle of a header", ?_assertMatch({ok, <<"HTTP/1.1 400 Bad Request", _Rest/binary>>}, - unexpected_msg(Server, ?IN_HEADER, unexpected_msg_in_your_header))}, + unexpected_msg_send(Server, ?IN_HEADER, unexpected_msg_in_your_header))}, {"should close on a TCP error on the request line", ?_assertMatch({error, closed}, - unexpected_msg(Server, ?IN_METHOD, {tcp_error, your_port_you_dont_match_on, something_terrible}))}, + unexpected_msg_send(Server, ?IN_METHOD, {tcp_error, your_port_you_dont_match_on, something_terrible}))}, {"should close on a TCP error in a header", ?_assertMatch({error, closed}, - unexpected_msg(Server, ?IN_HEADER, {tcp_error, your_port_you_dont_match_on, something_terrible}))}]. + unexpected_msg_send(Server, ?IN_HEADER, {tcp_error, your_port_you_dont_match_on, something_terrible}))}]. + + +build_headers(Count, Size) -> + AllowedChars = ["a", "b", "c", "1"], + CharFun = + fun(_I, Acc) -> + C = lists:nth(random:uniform(length(AllowedChars)), AllowedChars), + [C|Acc] + end, + FullString = lists:reverse(lists:foldl(CharFun, [], lists:seq(1, Size))), + Header = list_to_binary(FullString), + build_headers(Count, get_req(), Header). + +build_headers(0, ReqAcc, _Header) -> + eol(ReqAcc); +build_headers(Count, ReqAcc, Header) -> + RiakHeader = <<"x-riak-index-garbage_bin: ">>, + build_headers(Count - 1, + eol(<>), + Header). + + +bad_headers_tests(Server) -> + [{"too many headers returns error", + ?_assertMatch({ok, <<"HTTP/1.1 400 Bad Request", _Rest/binary>>}, + invalid_header_send(Server, build_headers(1001, 4)))}, + {"small enough headers are OK", + ?_assertMatch({ok, <<"HTTP/1.1 200 OK", _Rest/binary>>}, + invalid_header_send(Server, build_headers(10, 100)))}]. + +bad_headers_bigbuf_tests(Server) -> + [{"too many headers returns error", + ?_assertMatch({ok, <<"HTTP/1.1 400 Bad Request", _Rest/binary>>}, + invalid_header_send(Server, build_headers(1001, 4)))}, + {"header too big returns error", + ?_assertMatch({ok, <<"HTTP/1.1 200 OK", _Rest/binary>>}, + invalid_header_send(Server, build_headers(1, 10000)))}, + {"small enough headers are OK", + ?_assertMatch({ok, <<"HTTP/1.1 200 OK", _Rest/binary>>}, + invalid_header_send(Server, build_headers(10, 100)))}]. + From 5fded602c1d985936c829b99b47caf7d9676a10e Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Sun, 10 Feb 2019 22:17:09 +0000 Subject: [PATCH 7/7] Fix warning, add error log on Other It appears the test was originaly menat to work as the "Other" case would be hit, and then when hitting the Other case - the code would previously just loop round without adding a header. This is no longer the case (i.e. even if the original 'Other' handling is provided). --- src/mochiweb_http.erl | 2 ++ test/mochiweb_http_tests.erl | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mochiweb_http.erl b/src/mochiweb_http.erl index 4648236..5443960 100644 --- a/src/mochiweb_http.erl +++ b/src/mochiweb_http.erl @@ -136,6 +136,8 @@ headers(Socket, Opts, Request, Headers, Body, HeaderCount) -> mochiweb_socket:close(Socket), exit(normal); Other -> + error_logger:warning_msg("Got unexpected Message: ~w (to pid=~w)~n", + [Other, self()]), handle_invalid_msg_request(Other, Socket, Opts, 400, Request, Headers) after ?HEADERS_RECV_TIMEOUT -> mochiweb_socket:close(Socket), diff --git a/test/mochiweb_http_tests.erl b/test/mochiweb_http_tests.erl index 8b5c235..9e8f183 100644 --- a/test/mochiweb_http_tests.erl +++ b/test/mochiweb_http_tests.erl @@ -101,7 +101,7 @@ unexpected_msg_send(Server, MsgAt, Msg) -> gen_tcp:recv(S, 0, 5000). invalid_header_send(Server, Msg) -> - {S, Acceptor} = setup_server(Server), + {S, _Acceptor} = setup_server(Server), gen_tcp:send(S, Msg), gen_tcp:recv(S, 0, 5000).