aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile93
-rw-r--r--README.md204
-rw-r--r--ROADMAP.md62
-rw-r--r--ebin/.gitignore0
-rw-r--r--examples/README.md6
-rw-r--r--examples/basic_auth/README.md43
-rw-r--r--examples/basic_auth/rebar.config4
-rw-r--r--examples/basic_auth/src/basic_auth.app.src15
-rw-r--r--examples/basic_auth/src/basic_auth.erl14
-rw-r--r--examples/basic_auth/src/basic_auth_app.erl25
-rw-r--r--examples/basic_auth/src/basic_auth_sup.erl23
-rw-r--r--examples/basic_auth/src/toppage_handler.erl32
-rwxr-xr-xexamples/basic_auth/start.sh4
-rw-r--r--examples/chunked_hello_world/src/chunked_hello_world_app.erl8
-rw-r--r--examples/chunked_hello_world/src/toppage_handler.erl4
-rw-r--r--examples/compress_response/README.md62
-rw-r--r--examples/compress_response/rebar.config4
-rw-r--r--examples/compress_response/src/compress_response.app.src15
-rw-r--r--examples/compress_response/src/compress_response.erl14
-rw-r--r--examples/compress_response/src/compress_response_app.erl26
-rw-r--r--examples/compress_response/src/compress_response_sup.erl23
-rw-r--r--examples/compress_response/src/toppage_handler.erl31
-rwxr-xr-xexamples/compress_response/start.sh3
-rw-r--r--examples/cookie/src/cookie_app.erl6
-rw-r--r--examples/cookie/src/toppage_handler.erl4
-rw-r--r--examples/echo_get/src/echo_get_app.erl8
-rw-r--r--examples/echo_get/src/toppage_handler.erl4
-rw-r--r--examples/echo_post/src/echo_post_app.erl8
-rw-r--r--examples/echo_post/src/toppage_handler.erl10
-rw-r--r--examples/hello_world/src/hello_world_app.erl8
-rw-r--r--examples/hello_world/src/toppage_handler.erl4
-rw-r--r--examples/rest_hello_world/src/rest_hello_world_app.erl8
-rw-r--r--examples/static/README.md5
-rw-r--r--examples/static/priv/small.mp4bin0 -> 383631 bytes
-rw-r--r--examples/static/priv/small.ogvbin0 -> 872453 bytes
-rw-r--r--examples/static/priv/video.html11
-rw-r--r--examples/static/src/static_app.erl8
-rwxr-xr-xexamples/static/start.sh3
-rw-r--r--guide/handlers.md55
-rw-r--r--guide/hooks.md77
-rw-r--r--guide/http_handlers.md27
-rw-r--r--guide/internals.md79
-rw-r--r--guide/introduction.md12
-rw-r--r--guide/loop_handlers.md62
-rw-r--r--guide/middlewares.md72
-rw-r--r--guide/req.md205
-rw-r--r--guide/rest_handlers.md28
-rw-r--r--guide/routing.md244
-rw-r--r--guide/static_handlers.md30
-rw-r--r--guide/toc.md41
-rw-r--r--guide/ws_handlers.md75
-rw-r--r--rebar.config8
-rw-r--r--rebar.tests.config8
-rw-r--r--src/cowboy.app.src2
-rw-r--r--src/cowboy.erl2
-rw-r--r--src/cowboy_app.erl2
-rw-r--r--src/cowboy_bstr.erl51
-rw-r--r--src/cowboy_client.erl2
-rw-r--r--src/cowboy_clock.erl4
-rw-r--r--src/cowboy_dispatcher.erl291
-rw-r--r--src/cowboy_handler.erl220
-rw-r--r--src/cowboy_http.erl88
-rw-r--r--src/cowboy_http_handler.erl12
-rw-r--r--src/cowboy_loop_handler.erl12
-rw-r--r--src/cowboy_middleware.erl36
-rw-r--r--src/cowboy_protocol.erl317
-rw-r--r--src/cowboy_req.erl201
-rw-r--r--src/cowboy_rest.erl114
-rw-r--r--src/cowboy_router.erl565
-rw-r--r--src/cowboy_static.erl12
-rw-r--r--src/cowboy_sup.erl2
-rw-r--r--src/cowboy_websocket.erl724
-rw-r--r--src/cowboy_websocket_handler.erl4
-rw-r--r--test/autobahn_SUITE.erl12
-rwxr-xr-xtest/autobahn_SUITE_data/test.py4
-rw-r--r--test/chunked_handler.erl6
-rw-r--r--test/cover.spec (renamed from cover.spec)0
-rw-r--r--test/eunit_SUITE.erl31
-rw-r--r--test/http_SUITE.erl282
-rw-r--r--test/http_handler.erl4
-rw-r--r--test/http_handler_echo_body.erl8
-rw-r--r--test/http_handler_errors.erl4
-rw-r--r--test/http_handler_init_shutdown.erl4
-rw-r--r--test/http_handler_long_polling.erl4
-rw-r--r--test/http_handler_loop_timeout.erl4
-rw-r--r--test/http_handler_multipart.erl4
-rw-r--r--test/http_handler_set_resp.erl4
-rw-r--r--test/http_handler_stream_body.erl18
-rw-r--r--test/rest_created_path_resource.erl35
-rw-r--r--test/rest_patch_resource.erl34
-rw-r--r--test/websocket_echo_handler.erl9
-rw-r--r--test/websocket_handler.erl9
-rw-r--r--test/websocket_handler_init_shutdown.erl9
-rw-r--r--test/ws_SUITE.erl137
-rw-r--r--test/ws_timeout_cancel_handler.erl9
-rw-r--r--test/ws_timeout_hibernate_handler.erl9
96 files changed, 3586 insertions, 1540 deletions
diff --git a/Makefile b/Makefile
index 004d732..127e176 100644
--- a/Makefile
+++ b/Makefile
@@ -1,59 +1,88 @@
# See LICENSE for licensing information.
PROJECT = cowboy
+RANCH_VSN = 0.6.0
+ERLC_OPTS = -Werror +debug_info +warn_export_all # +bin_opt_info +warn_missing_spec
-DIALYZER = dialyzer
-REBAR = rebar
+DEPS_DIR ?= $(CURDIR)/deps
+export DEPS_DIR
-all: app
+# Makefile tweaks.
+
+V ?= 0
+
+appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src;
+appsrc_verbose = $(appsrc_verbose_$(V))
+
+erlc_verbose_0 = @echo " ERLC " $(?F);
+erlc_verbose = $(erlc_verbose_$(V))
+
+gen_verbose_0 = @echo " GEN " $@;
+gen_verbose = $(gen_verbose_$(V))
+
+.PHONY: all clean-all app clean docs clean-docs tests autobahn build-plt dialyze
# Application.
+all: app
+
+clean-all: clean clean-docs
+ $(gen_verbose) rm -rf .$(PROJECT).plt $(DEPS_DIR) logs
+
deps/ranch:
- @$(REBAR) get-deps
+ @mkdir -p $(DEPS_DIR)
+ git clone -n -- https://github.com/extend/ranch.git $(DEPS_DIR)/ranch
+ cd $(DEPS_DIR)/ranch ; git checkout -q $(RANCH_VSN)
+
+MODULES = $(shell ls src/*.erl | sed 's/src\///;s/\.erl/,/' | sed '$$s/.$$//')
-app: deps/ranch
- @$(REBAR) compile
+app: deps/ranch ebin/$(PROJECT).app
+ $(appsrc_verbose) cat src/$(PROJECT).app.src \
+ | sed 's/{modules, \[\]}/{modules, \[$(MODULES)\]}/' \
+ > ebin/$(PROJECT).app
+ @$(MAKE) -C $(DEPS_DIR)/ranch
+
+ebin/$(PROJECT).app: src/*.erl
+ @mkdir -p ebin/
+ $(erlc_verbose) erlc -v $(ERLC_OPTS) -o ebin/ -pa ebin/ \
+ src/$(PROJECT)_middleware.erl $?
clean:
- @$(REBAR) clean
- rm -f test/*.beam
- rm -f erl_crash.dump
+ -@$(MAKE) -C $(DEPS_DIR)/ranch clean
+ $(gen_verbose) rm -rf ebin/ test/*.beam erl_crash.dump
+
+# Documentation.
docs: clean-docs
- @$(REBAR) doc skip_deps=true
+ $(gen_verbose) erl -noshell \
+ -eval 'edoc:application($(PROJECT), ".", []), init:stop().'
clean-docs:
- rm -f doc/*.css
- rm -f doc/*.html
- rm -f doc/*.png
- rm -f doc/edoc-info
+ $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info
# Tests.
-deps/proper:
- @$(REBAR) -C rebar.tests.config get-deps
- cd deps/proper && $(REBAR) compile
-
-tests: clean deps/proper app eunit ct
-
-inttests: clean deps/proper app eunit intct
-
-eunit:
- @$(REBAR) -C rebar.tests.config eunit skip_deps=true
+CT_RUN = ct_run \
+ -pa ebin $(DEPS_DIR)/*/ebin \
+ -dir test \
+ -logdir logs \
+ -cover test/cover.spec
-ct:
- @$(REBAR) -C rebar.tests.config ct skip_deps=true suites=http,ws
+tests: ERLC_OPTS += -DTEST=1
+tests: clean app
+ @mkdir -p logs/
+ @$(CT_RUN) -suite eunit_SUITE http_SUITE ws_SUITE
-intct:
- @$(REBAR) -C rebar.tests.config ct skip_deps=true suites=http,ws,autobahn
+autobahn: clean app
+ @mkdir -p logs/
+ @$(CT_RUN) -suite autobahn_SUITE
# Dialyzer.
-build-plt:
- @$(DIALYZER) --build_plt --output_plt .$(PROJECT).plt \
- --apps kernel stdlib sasl inets crypto public_key ssl deps/*
+build-plt: app
+ @dialyzer --build_plt --output_plt .$(PROJECT).plt \
+ --apps erts kernel stdlib crypto public_key ssl $(DEPS_DIR)/ranch
dialyze:
- @$(DIALYZER) --src src --plt .$(PROJECT).plt --no_native \
+ @dialyzer --src src --plt .$(PROJECT).plt --no_native \
-Werror_handling -Wrace_conditions -Wunmatched_returns # -Wunderspecs
diff --git a/README.md b/README.md
index b850083..730995b 100644
--- a/README.md
+++ b/README.md
@@ -31,207 +31,3 @@ Support
* Official IRC Channel: #ninenines on irc.freenode.net
* [Mailing Lists](http://lists.ninenines.eu)
* [Commercial Support](http://ninenines.eu/support)
-
-
-
-Old README (deprecated)
------------------------
-
-This and all following sections will be removed as soon as their
-equivalent appear in the Cowboy guide.
-
-Cowboy does nothing by default.
-
-Cowboy uses Ranch for handling connections, and provides convenience
-functions to start and stop Ranch listeners. The Ranch application
-must always be started before Cowboy. The crypto application must
-also be started.
-
-The `cowboy:start_http/4` function will handle HTTP connections
-using the TCP transport. Similarly, `cowboy:start_https/4` will
-handle HTTP connections using the SSL transport.
-
-You can start as many listeners as you need to. To allow this, you
-are required to give a name to your listeners. It is the first
-argument to the start functions. The name can be of any type.
-
-You can stop listeners using `cowboy:stop_listener/1`, giving it
-the name of the listener to be stopped.
-
-The following example demonstrates the startup of a very simple
-HTTP listener. It redirects all requests to the `my_handler`
-module.
-
-``` erlang
-application:start(crypto),
-application:start(ranch),
-application:start(cowboy),
-Dispatch = [
- %% {URIHost, list({URIPath, Handler, Opts})}
- {'_', [{'_', my_handler, []}]}
-],
-%% Name, NbAcceptors, TransOpts, ProtoOpts
-cowboy:start_http(my_http_listener, 100, [{port, 8080}],
- [{dispatch, Dispatch}]
-).
-```
-
-This is not enough though, you must also write the `my_handler`
-module to process the incoming HTTP requests. Of course Cowboy
-comes with predefined handlers for specific tasks but most of
-the time you'll need to write the handlers appropriate for your
-application.
-
-Following is an example of a "Hello World!" HTTP handler.
-
-``` erlang
--module(my_handler).
--export([init/3, handle/2, terminate/2]).
-
-init({tcp, http}, Req, Opts) ->
- {ok, Req, undefined_state}.
-
-handle(Req, State) ->
- {ok, Req2} = cowboy_req:reply(200, [], <<"Hello World!">>, Req),
- {ok, Req2, State}.
-
-terminate(Req, State) ->
- ok.
-```
-
-You can also write handlers that do not reply directly. Instead, such handlers
-will wait for an Erlang message from another process and only reply when
-receiving such message, or timeout if it didn't arrive in time.
-
-This is especially useful for long-polling functionality, as Cowboy will handle
-process hibernation and timeouts properly, preventing mistakes if you were to
-write the code yourself. A handler of that kind can be defined like this:
-
-``` erlang
--module(my_loop_handler).
--export([init/3, info/3, terminate/2]).
-
--define(TIMEOUT, 60000).
-
-init({tcp, http}, Req, Opts) ->
- {loop, Req, undefined_state, ?TIMEOUT, hibernate}.
-
-info({reply, Body}, Req, State) ->
- {ok, Req2} = cowboy_req:reply(200, [], Body, Req),
- {ok, Req2, State};
-info(Message, Req, State) ->
- {loop, Req, State, hibernate}.
-
-terminate(Req, State) ->
- ok.
-```
-
-It is of course possible to combine both type of handlers together as long as
-you return the proper tuple from init/3.
-
-Continue reading to learn how to dispatch rules and handle requests.
-
-Dispatch rules
---------------
-
-Cowboy allows you to dispatch HTTP requests directly to a specific handler
-based on the hostname and path information from the request. It also lets
-you define static options for the handler directly in the rules.
-
-To match the hostname and path, Cowboy requires a list of tokens. For
-example, to match the "ninenines.eu" domain name, you must specify
-`[<<"ninenines">>, <<"eu">>]`. Or, to match the "/path/to/my/resource"
-you must use `[<<"path">>, <<"to">>, <<"my">>, <<"resource">>]`. All the
-tokens must be given as binary.
-
-You can use the special token `'_'` (the atom underscore) to indicate that
-you accept anything in that position. For example if you have both
-"ninenines.eu" and "ninenines.fr" domains, you can use the match spec
-`[<<"ninenines">>, '_']` to match any top level extension.
-
-Finally, you can also match multiple leading segments of the domain name and
-multiple trailing segments of the request path using the atom `'...'` (the atom
-ellipsis) respectively as the first host token or the last path token. For
-example, host rule `['...', <<"ninenines">>, <<"eu">>]` can match both
-"cowboy.bugs.ninenines.eu" and "ninenines.eu" and path rule
-`[<<"projects">>, '...']` can match both "/projects" and
-"/projects/cowboy/issues/42". The host leading segments and the path trailing
-segments can later be retrieved through `cowboy_req:host_info/1` and
-`cowboy_req:path_info/1`.
-
-Any other atom used as a token will bind the value to this atom when
-matching. To follow on our hostnames example, `[<<"ninenines">>, ext]`
-would bind the values `<<"eu">>` and `<<"fr">>` to the ext atom, that you
-can later retrieve in your handler by calling `cowboy_req:binding/{2,3}`.
-
-You can also accept any match spec by using the atom `'_'` directly instead of
-a list of tokens. Our hello world example above uses this to forward all
-requests to a single handler.
-
-There is currently no way to match multiple tokens at once.
-
-Requests handling
------------------
-
-Requests are passed around in the Request variable. Although they are
-defined as a record, it is recommended to access them only through the
-cowboy_req module API.
-
-You can retrieve the HTTP method, HTTP version, peer address and port,
-host tokens, raw host, used port, path tokens, raw path, query string
-values, bound values from the dispatch step, header values from the
-request. You can also read the request body, if any, optionally parsing
-it as a query string. Finally, the request allows you to send a response
-to the client.
-
-See the cowboy_req module for more information.
-
-Websockets
-----------
-
-The Websocket protocol is built upon the HTTP protocol. It first sends
-an HTTP request for an handshake, performs it and then switches
-to Websocket. Therefore you need to write a standard HTTP handler to
-confirm the handshake should be completed and then the Websocket-specific
-callbacks.
-
-A simple handler doing nothing but sending a repetitive message using
-Websocket would look like this:
-
-``` erlang
--module(my_ws_handler).
--export([init/3]).
--export([websocket_init/3, websocket_handle/3,
- websocket_info/3, websocket_terminate/3]).
-
-init({tcp, http}, Req, Opts) ->
- {upgrade, protocol, cowboy_websocket}.
-
-websocket_init(TransportName, Req, _Opts) ->
- erlang:start_timer(1000, self(), <<"Hello!">>),
- {ok, Req, undefined_state}.
-
-websocket_handle({text, Msg}, Req, State) ->
- {reply, {text, << "That's what she said! ", Msg/binary >>}, Req, State};
-websocket_handle(_Data, Req, State) ->
- {ok, Req, State}.
-
-websocket_info({timeout, _Ref, Msg}, Req, State) ->
- erlang:start_timer(1000, self(), <<"How' you doin'?">>),
- {reply, {text, Msg}, Req, State};
-websocket_info(_Info, Req, State) ->
- {ok, Req, State}.
-
-websocket_terminate(_Reason, _Req, _State) ->
- ok.
-```
-
-Of course you can have an HTTP handler doing both HTTP and Websocket
-handling, but for the sake of this example we're ignoring the HTTP
-part entirely.
-
-As the Websocket protocol is still a draft the API is subject to change
-regularly when support to the most recent drafts gets added. Features may
-be added, changed or removed before the protocol gets finalized. Cowboy
-tries to implement all drafts transparently and give a single interface to
-handle them all, however.
diff --git a/ROADMAP.md b/ROADMAP.md
index 7dc19af..2186387 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -6,38 +6,21 @@ list of planned changes and work to be done on the Cowboy
server. It is non-exhaustive and subject to change. Items
are not ordered.
- * Write more, better examples.
+ * Add and improve examples
- The first step would be to port misultin's examples
- to Cowboy. Then these examples could be completed with
- examples for Cowboy specific features.
+ * Improve user guide
- The extend/cowboy_examples is to be used for this. As
- it is a separate repository, we can organize the file
- structure as appropriate. Ideally we would have one
- complete example per folder.
+ We need feedback to improve the guide.
- Examples should be commented. They may or may not be
- used for writing the user guides.
-
- * Write user guides.
-
- We currently have good API documentation, but no step
- by step user guides.
-
- * Write more, better tests.
+ * Add and improve tests
Amongst the areas less tested there is protocol upgrades
and the REST handler.
- Current tests should be completed with unit tests
- where applicable. We should probably also test the
- dependencies used, like erlang:decode_packet/3.
-
While eunit and ct tests are fine, some parts of the
code could benefit from PropEr tests.
- * Continuous performance testing.
+ * Continuous performance testing
Initially dubbed the Horse project, Cowboy could benefit
from a continuous performance testing tool that would
@@ -49,46 +32,25 @@ are not ordered.
Cowboy to other servers and eventually take ideas from
the servers that outperform Cowboy for the task being tested.
- * Improve HTTP/1.0 support.
+ * Full HTTP/1.1 support
+
+ * Improved HTTP/1.0 support
Most of the work on Cowboy has been done with HTTP/1.1
in mind. But there is still a need for HTTP/1.0 code in
Cowboy. The server code should be reviewed and tested
to ensure compatibility with remaining HTTP/1.0 products.
- * Complete the work on Websockets.
-
- Now that the Autobahn test suite is available (make inttests),
- we have a definite way to know whether Cowboy's implementation
- of Websockets is right. The work can thus be completed. The
- remaining task is proper UTF8 handling.
+ * SPDY support
- * SPDY support.
+The following items pertain to Ranch.
- While SPDY probably won't be added directly to Cowboy, work
- has been started on making Cowboy use SPDY.
-
- * Transport upgrades.
-
- Some protocols allow an upgrade from TCP to SSL without
- closing the connection. This is currently not possible
- through the Cowboy API.
-
- * Resizing the acceptor pool.
+ * Resizing the acceptor pool
We should be able to add more acceptors to a pool but also
to remove some of them as needed.
- * Simplified dispatch list.
-
- For convenience purposes, the dispatch list should allow
- lists instead of binaries. The lists can be converted to
- binary by Cowboy at listener initialization.
-
- There has also been discussion on allowing the dispatch
- list to be hierarchical.
-
- * Add Transport:secure/0.
+ * Add Transport:secure/0
Currently Cowboy checks if a connection is secure by
checking if its name is 'ssl'. This isn't a very modular
diff --git a/ebin/.gitignore b/ebin/.gitignore
deleted file mode 100644
index e69de29..0000000
--- a/ebin/.gitignore
+++ /dev/null
diff --git a/examples/README.md b/examples/README.md
index c0e1f41..f2b0c64 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,9 +1,15 @@
Cowboy Examples
===============
+ * [basic_auth](./examples/basic_auth):
+ basic HTTP authorization with REST
+
* [chunked_hello_world](./examples/chunked_hello_world):
demonstrates chunked data transfer with two one-second delays
+ * [compress_response](./examples/compress_response)
+ send a response body compressed if the client supports it
+
* [cookie](./examples/cookie):
set cookies from server and client side
diff --git a/examples/basic_auth/README.md b/examples/basic_auth/README.md
new file mode 100644
index 0000000..38ae9a2
--- /dev/null
+++ b/examples/basic_auth/README.md
@@ -0,0 +1,43 @@
+Cowboy Basic Authorization Rest Hello World
+===========================================
+
+To compile this example you need rebar in your PATH.
+
+Type the following command:
+```
+$ rebar get-deps compile
+```
+
+You can then start the Erlang node with the following command:
+```
+./start.sh
+```
+
+Then run any given command or point your browser to the indicated URL.
+
+Examples
+--------
+
+### Get 401
+``` bash
+$ curl -i http://localhost:8080
+HTTP/1.1 401 Unauthorized
+connection: keep-alive
+server: Cowboy
+date: Sun, 20 Jan 2013 14:10:27 GMT
+content-length: 0
+www-authenticate: Restricted
+```
+
+### Get 200
+``` bash
+$ curl -i -u "Alladin:open sesame" http://localhost:8080
+HTTP/1.1 200 OK
+connection: keep-alive
+server: Cowboy
+date: Sun, 20 Jan 2013 14:11:12 GMT
+content-length: 16
+content-type: text/plain
+
+Hello, Alladin!
+```
diff --git a/examples/basic_auth/rebar.config b/examples/basic_auth/rebar.config
new file mode 100644
index 0000000..6ad3062
--- /dev/null
+++ b/examples/basic_auth/rebar.config
@@ -0,0 +1,4 @@
+{deps, [
+ {cowboy, ".*",
+ {git, "git://github.com/extend/cowboy.git", "master"}}
+]}.
diff --git a/examples/basic_auth/src/basic_auth.app.src b/examples/basic_auth/src/basic_auth.app.src
new file mode 100644
index 0000000..cbf4ea1
--- /dev/null
+++ b/examples/basic_auth/src/basic_auth.app.src
@@ -0,0 +1,15 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+{application, basic_auth, [
+ {description, "Cowboy Basic HTTP Authorization example."},
+ {vsn, "1"},
+ {modules, []},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib,
+ cowboy
+ ]},
+ {mod, {basic_auth_app, []}},
+ {env, []}
+]}.
diff --git a/examples/basic_auth/src/basic_auth.erl b/examples/basic_auth/src/basic_auth.erl
new file mode 100644
index 0000000..9294c77
--- /dev/null
+++ b/examples/basic_auth/src/basic_auth.erl
@@ -0,0 +1,14 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(basic_auth).
+
+%% API.
+-export([start/0]).
+
+%% API.
+
+start() ->
+ ok = application:start(crypto),
+ ok = application:start(ranch),
+ ok = application:start(cowboy),
+ ok = application:start(basic_auth).
diff --git a/examples/basic_auth/src/basic_auth_app.erl b/examples/basic_auth/src/basic_auth_app.erl
new file mode 100644
index 0000000..24c766e
--- /dev/null
+++ b/examples/basic_auth/src/basic_auth_app.erl
@@ -0,0 +1,25 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+%% @private
+-module(basic_auth_app).
+-behaviour(application).
+
+%% API.
+-export([start/2]).
+-export([stop/1]).
+
+%% API.
+
+start(_Type, _Args) ->
+ Dispatch = cowboy_router:compile([
+ {'_', [
+ {"/", toppage_handler, []}
+ ]}
+ ]),
+ {ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
+ {env, [{dispatch, Dispatch}]}
+ ]),
+ basic_auth_sup:start_link().
+
+stop(_State) ->
+ ok.
diff --git a/examples/basic_auth/src/basic_auth_sup.erl b/examples/basic_auth/src/basic_auth_sup.erl
new file mode 100644
index 0000000..6219b5f
--- /dev/null
+++ b/examples/basic_auth/src/basic_auth_sup.erl
@@ -0,0 +1,23 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+%% @private
+-module(basic_auth_sup).
+-behaviour(supervisor).
+
+%% API.
+-export([start_link/0]).
+
+%% supervisor.
+-export([init/1]).
+
+%% API.
+
+-spec start_link() -> {ok, pid()}.
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% supervisor.
+
+init([]) ->
+ Procs = [],
+ {ok, {{one_for_one, 10, 10}, Procs}}.
diff --git a/examples/basic_auth/src/toppage_handler.erl b/examples/basic_auth/src/toppage_handler.erl
new file mode 100644
index 0000000..94383d4
--- /dev/null
+++ b/examples/basic_auth/src/toppage_handler.erl
@@ -0,0 +1,32 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+%% @doc Basic authorization Hello world handler.
+-module(toppage_handler).
+
+-export([init/3]).
+-export([content_types_provided/2]).
+-export([is_authorized/2]).
+-export([hello_to_text/2]).
+
+init(_Transport, _Req, []) ->
+ {upgrade, protocol, cowboy_rest}.
+
+
+is_authorized(Req, S) ->
+ {ok, Auth, Req1} = cowboy_req:parse_header(<<"authorization">>, Req),
+ case Auth of
+ {<<"basic">>, {User = <<"Alladin">>, <<"open sesame">>}} ->
+ {true, Req1, User};
+ _ ->
+ {{false, <<"Restricted">>}, Req1, S}
+ end.
+
+content_types_provided(Req, State) ->
+ {[
+ {<<"text/plain">>, hello_to_text}
+ ], Req, State}.
+
+
+hello_to_text(Req, User) ->
+ {<< <<"Hello, ">>/binary, User/binary, <<"!\n">>/binary >>, Req, User}.
+
diff --git a/examples/basic_auth/start.sh b/examples/basic_auth/start.sh
new file mode 100755
index 0000000..9e8a30b
--- /dev/null
+++ b/examples/basic_auth/start.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+erl -pa ebin deps/*/ebin -s basic_auth \
+ -eval "io:format(\"Get 401: curl -i http://localhost:8080~n\")." \
+ -eval "io:format(\"Get 200: curl -i -u \\\"Alladin:open sesame\\\" http://localhost:8080~n\")."
diff --git a/examples/chunked_hello_world/src/chunked_hello_world_app.erl b/examples/chunked_hello_world/src/chunked_hello_world_app.erl
index 41efd06..0032d01 100644
--- a/examples/chunked_hello_world/src/chunked_hello_world_app.erl
+++ b/examples/chunked_hello_world/src/chunked_hello_world_app.erl
@@ -11,13 +11,13 @@
%% API.
start(_Type, _Args) ->
- Dispatch = [
+ Dispatch = cowboy_router:compile([
{'_', [
- {[], toppage_handler, []}
+ {"/", toppage_handler, []}
]}
- ],
+ ]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
- {dispatch, Dispatch}
+ {env, [{dispatch, Dispatch}]}
]),
chunked_hello_world_sup:start_link().
diff --git a/examples/chunked_hello_world/src/toppage_handler.erl b/examples/chunked_hello_world/src/toppage_handler.erl
index 0838832..b6f2d04 100644
--- a/examples/chunked_hello_world/src/toppage_handler.erl
+++ b/examples/chunked_hello_world/src/toppage_handler.erl
@@ -5,7 +5,7 @@
-export([init/3]).
-export([handle/2]).
--export([terminate/2]).
+-export([terminate/3]).
init(_Transport, Req, []) ->
{ok, Req, undefined}.
@@ -19,5 +19,5 @@ handle(Req, State) ->
ok = cowboy_req:chunk("Chunked!\r\n", Req2),
{ok, Req2, State}.
-terminate(_Req, _State) ->
+terminate(_Reason, _Req, _State) ->
ok.
diff --git a/examples/compress_response/README.md b/examples/compress_response/README.md
new file mode 100644
index 0000000..8afbe65
--- /dev/null
+++ b/examples/compress_response/README.md
@@ -0,0 +1,62 @@
+Cowboy Compress Response
+========================
+
+To compile this example you need rebar in your PATH.
+
+Type the following command:
+```
+$ rebar get-deps compile
+```
+
+You can then start the Erlang node with the following command:
+```
+./start.sh
+```
+
+Then point your browser to the indicated URL.
+
+Example
+-------
+
+``` bash
+$ curl -i http://localhost:8080
+HTTP/1.1 200 OK
+connection: keep-alive
+server: Cowboy
+date: Mon, 07 Jan 2013 18:42:29 GMT
+content-length: 909
+
+A cowboy is an animal herder who tends cattle on ranches in North America,
+traditionally on horseback, and often performs a multitude of other ranch-
+related tasks. The historic American cowboy of the late 19th century arose
+from the vaquero traditions of northern Mexico and became a figure of special
+significance and legend. A subtype, called a wrangler, specifically tends the
+horses used to work cattle. In addition to ranch work, some cowboys work for
+or participate in rodeos. Cowgirls, first defined as such in the late 19th
+century, had a less-well documented historical role, but in the modern world
+have established the ability to work at virtually identical tasks and obtained
+considerable respect for their achievements. There are also cattle handlers
+in many other parts of the world, particularly South America and Australia,
+who perform work similar to the cowboy in their respective nations.
+
+$ curl -i --compressed http://localhost:8080
+HTTP/1.1 200 OK
+connection: keep-alive
+server: Cowboy
+date: Mon, 07 Jan 2013 18:42:30 GMT
+content-encoding: gzip
+content-length: 510
+
+A cowboy is an animal herder who tends cattle on ranches in North America,
+traditionally on horseback, and often performs a multitude of other ranch-
+related tasks. The historic American cowboy of the late 19th century arose
+from the vaquero traditions of northern Mexico and became a figure of special
+significance and legend. A subtype, called a wrangler, specifically tends the
+horses used to work cattle. In addition to ranch work, some cowboys work for
+or participate in rodeos. Cowgirls, first defined as such in the late 19th
+century, had a less-well documented historical role, but in the modern world
+have established the ability to work at virtually identical tasks and obtained
+considerable respect for their achievements. There are also cattle handlers
+in many other parts of the world, particularly South America and Australia,
+who perform work similar to the cowboy in their respective nations.
+```
diff --git a/examples/compress_response/rebar.config b/examples/compress_response/rebar.config
new file mode 100644
index 0000000..6ad3062
--- /dev/null
+++ b/examples/compress_response/rebar.config
@@ -0,0 +1,4 @@
+{deps, [
+ {cowboy, ".*",
+ {git, "git://github.com/extend/cowboy.git", "master"}}
+]}.
diff --git a/examples/compress_response/src/compress_response.app.src b/examples/compress_response/src/compress_response.app.src
new file mode 100644
index 0000000..3512084
--- /dev/null
+++ b/examples/compress_response/src/compress_response.app.src
@@ -0,0 +1,15 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+{application, compress_response, [
+ {description, "Cowboy Compress Response example."},
+ {vsn, "1"},
+ {modules, []},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib,
+ cowboy
+ ]},
+ {mod, {compress_response_app, []}},
+ {env, []}
+]}.
diff --git a/examples/compress_response/src/compress_response.erl b/examples/compress_response/src/compress_response.erl
new file mode 100644
index 0000000..ac2636c
--- /dev/null
+++ b/examples/compress_response/src/compress_response.erl
@@ -0,0 +1,14 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(compress_response).
+
+%% API.
+-export([start/0]).
+
+%% API.
+
+start() ->
+ ok = application:start(crypto),
+ ok = application:start(ranch),
+ ok = application:start(cowboy),
+ ok = application:start(compress_response).
diff --git a/examples/compress_response/src/compress_response_app.erl b/examples/compress_response/src/compress_response_app.erl
new file mode 100644
index 0000000..b36dcbd
--- /dev/null
+++ b/examples/compress_response/src/compress_response_app.erl
@@ -0,0 +1,26 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+%% @private
+-module(compress_response_app).
+-behaviour(application).
+
+%% API.
+-export([start/2]).
+-export([stop/1]).
+
+%% API.
+
+start(_Type, _Args) ->
+ Dispatch = cowboy_router:compile([
+ {'_', [
+ {"/", toppage_handler, []}
+ ]}
+ ]),
+ {ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
+ {compress, true},
+ {env, [{dispatch, Dispatch}]}
+ ]),
+ compress_response_sup:start_link().
+
+stop(_State) ->
+ ok.
diff --git a/examples/compress_response/src/compress_response_sup.erl b/examples/compress_response/src/compress_response_sup.erl
new file mode 100644
index 0000000..d1bc312
--- /dev/null
+++ b/examples/compress_response/src/compress_response_sup.erl
@@ -0,0 +1,23 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+%% @private
+-module(compress_response_sup).
+-behaviour(supervisor).
+
+%% API.
+-export([start_link/0]).
+
+%% supervisor.
+-export([init/1]).
+
+%% API.
+
+-spec start_link() -> {ok, pid()}.
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% supervisor.
+
+init([]) ->
+ Procs = [],
+ {ok, {{one_for_one, 10, 10}, Procs}}.
diff --git a/examples/compress_response/src/toppage_handler.erl b/examples/compress_response/src/toppage_handler.erl
new file mode 100644
index 0000000..3558a9c
--- /dev/null
+++ b/examples/compress_response/src/toppage_handler.erl
@@ -0,0 +1,31 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+%% @doc Compress response handler.
+-module(toppage_handler).
+
+-export([init/3]).
+-export([handle/2]).
+-export([terminate/3]).
+
+init(_Transport, Req, []) ->
+ {ok, Req, undefined}.
+
+handle(Req, State) ->
+ BigBody =
+<<"A cowboy is an animal herder who tends cattle on ranches in North America,
+traditionally on horseback, and often performs a multitude of other ranch-
+related tasks. The historic American cowboy of the late 19th century arose
+from the vaquero traditions of northern Mexico and became a figure of special
+significance and legend. A subtype, called a wrangler, specifically tends the
+horses used to work cattle. In addition to ranch work, some cowboys work for
+or participate in rodeos. Cowgirls, first defined as such in the late 19th
+century, had a less-well documented historical role, but in the modern world
+have established the ability to work at virtually identical tasks and obtained
+considerable respect for their achievements. There are also cattle handlers
+in many other parts of the world, particularly South America and Australia,
+who perform work similar to the cowboy in their respective nations.\n">>,
+ {ok, Req2} = cowboy_req:reply(200, [], BigBody, Req),
+ {ok, Req2, State}.
+
+terminate(_Reason, _Req, _State) ->
+ ok.
diff --git a/examples/compress_response/start.sh b/examples/compress_response/start.sh
new file mode 100755
index 0000000..2e79031
--- /dev/null
+++ b/examples/compress_response/start.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+erl -pa ebin deps/*/ebin -s compress_response \
+ -eval "io:format(\"Point your browser at http://localhost:8080~n\")."
diff --git a/examples/cookie/src/cookie_app.erl b/examples/cookie/src/cookie_app.erl
index 195d6b6..91d1b95 100644
--- a/examples/cookie/src/cookie_app.erl
+++ b/examples/cookie/src/cookie_app.erl
@@ -11,13 +11,13 @@
%% API.
start(_Type, _Args) ->
- Dispatch = [
+ Dispatch = cowboy_router:compile([
{'_', [
{'_', toppage_handler, []}
]}
- ],
+ ]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
- {dispatch, Dispatch}
+ {env, [{dispatch, Dispatch}]}
]),
cookie_sup:start_link().
diff --git a/examples/cookie/src/toppage_handler.erl b/examples/cookie/src/toppage_handler.erl
index 783cda6..b107d5a 100644
--- a/examples/cookie/src/toppage_handler.erl
+++ b/examples/cookie/src/toppage_handler.erl
@@ -5,7 +5,7 @@
-export([init/3]).
-export([handle/2]).
--export([terminate/2]).
+-export([terminate/3]).
init(_Transport, Req, []) ->
{ok, Req, undefined}.
@@ -25,5 +25,5 @@ handle(Req, State) ->
Body, Req4),
{ok, Req5, State}.
-terminate(_Req, _State) ->
+terminate(_Reason, _Req, _State) ->
ok.
diff --git a/examples/echo_get/src/echo_get_app.erl b/examples/echo_get/src/echo_get_app.erl
index b9551f0..d661e9c 100644
--- a/examples/echo_get/src/echo_get_app.erl
+++ b/examples/echo_get/src/echo_get_app.erl
@@ -11,13 +11,13 @@
%% API.
start(_Type, _Args) ->
- Dispatch = [
+ Dispatch = cowboy_router:compile([
{'_', [
- {[], toppage_handler, []}
+ {"/", toppage_handler, []}
]}
- ],
+ ]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
- {dispatch, Dispatch}
+ {env, [{dispatch, Dispatch}]}
]),
echo_get_sup:start_link().
diff --git a/examples/echo_get/src/toppage_handler.erl b/examples/echo_get/src/toppage_handler.erl
index 86433cb..c604bae 100644
--- a/examples/echo_get/src/toppage_handler.erl
+++ b/examples/echo_get/src/toppage_handler.erl
@@ -5,7 +5,7 @@
-export([init/3]).
-export([handle/2]).
--export([terminate/2]).
+-export([terminate/3]).
init(_Transport, Req, []) ->
{ok, Req, undefined}.
@@ -25,5 +25,5 @@ echo(_, _, Req) ->
%% Method not allowed.
cowboy_req:reply(405, Req).
-terminate(_Req, _State) ->
+terminate(_Reason, _Req, _State) ->
ok.
diff --git a/examples/echo_post/src/echo_post_app.erl b/examples/echo_post/src/echo_post_app.erl
index 93f3bd5..7d86c53 100644
--- a/examples/echo_post/src/echo_post_app.erl
+++ b/examples/echo_post/src/echo_post_app.erl
@@ -11,13 +11,13 @@
%% API.
start(_Type, _Args) ->
- Dispatch = [
+ Dispatch = cowboy_router:compile([
{'_', [
- {[], toppage_handler, []}
+ {"/", toppage_handler, []}
]}
- ],
+ ]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
- {dispatch, Dispatch}
+ {env, [{dispatch, Dispatch}]}
]),
echo_post_sup:start_link().
diff --git a/examples/echo_post/src/toppage_handler.erl b/examples/echo_post/src/toppage_handler.erl
index 808ba8e..6831c78 100644
--- a/examples/echo_post/src/toppage_handler.erl
+++ b/examples/echo_post/src/toppage_handler.erl
@@ -5,16 +5,16 @@
-export([init/3]).
-export([handle/2]).
--export([terminate/2]).
+-export([terminate/3]).
init(_Transport, Req, []) ->
{ok, Req, undefined}.
handle(Req, State) ->
{Method, Req2} = cowboy_req:method(Req),
- {HasBody, Req3} = cowboy_req:has_body(Req2),
- {ok, Req4} = maybe_echo(Method, HasBody, Req3),
- {ok, Req4, State}.
+ HasBody = cowboy_req:has_body(Req2),
+ {ok, Req3} = maybe_echo(Method, HasBody, Req2),
+ {ok, Req3, State}.
maybe_echo(<<"POST">>, true, Req) ->
{ok, PostVals, Req2} = cowboy_req:body_qs(Req),
@@ -32,5 +32,5 @@ echo(Echo, Req) ->
cowboy_req:reply(200,
[{<<"content-encoding">>, <<"utf-8">>}], Echo, Req).
-terminate(_Req, _State) ->
+terminate(_Reason, _Req, _State) ->
ok.
diff --git a/examples/hello_world/src/hello_world_app.erl b/examples/hello_world/src/hello_world_app.erl
index 1cb15ab..eb938d3 100644
--- a/examples/hello_world/src/hello_world_app.erl
+++ b/examples/hello_world/src/hello_world_app.erl
@@ -11,13 +11,13 @@
%% API.
start(_Type, _Args) ->
- Dispatch = [
+ Dispatch = cowboy_router:compile([
{'_', [
- {[], toppage_handler, []}
+ {"/", toppage_handler, []}
]}
- ],
+ ]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
- {dispatch, Dispatch}
+ {env, [{dispatch, Dispatch}]}
]),
hello_world_sup:start_link().
diff --git a/examples/hello_world/src/toppage_handler.erl b/examples/hello_world/src/toppage_handler.erl
index 55b5323..4124b5a 100644
--- a/examples/hello_world/src/toppage_handler.erl
+++ b/examples/hello_world/src/toppage_handler.erl
@@ -5,7 +5,7 @@
-export([init/3]).
-export([handle/2]).
--export([terminate/2]).
+-export([terminate/3]).
init(_Transport, Req, []) ->
{ok, Req, undefined}.
@@ -14,5 +14,5 @@ handle(Req, State) ->
{ok, Req2} = cowboy_req:reply(200, [], <<"Hello world!">>, Req),
{ok, Req2, State}.
-terminate(_Req, _State) ->
+terminate(_Reason, _Req, _State) ->
ok.
diff --git a/examples/rest_hello_world/src/rest_hello_world_app.erl b/examples/rest_hello_world/src/rest_hello_world_app.erl
index 510dbb1..a662c3d 100644
--- a/examples/rest_hello_world/src/rest_hello_world_app.erl
+++ b/examples/rest_hello_world/src/rest_hello_world_app.erl
@@ -11,13 +11,13 @@
%% API.
start(_Type, _Args) ->
- Dispatch = [
+ Dispatch = cowboy_router:compile([
{'_', [
- {[], toppage_handler, []}
+ {"/", toppage_handler, []}
]}
- ],
+ ]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
- {dispatch, Dispatch}
+ {env, [{dispatch, Dispatch}]}
]),
rest_hello_world_sup:start_link().
diff --git a/examples/static/README.md b/examples/static/README.md
index ef46312..78f5338 100644
--- a/examples/static/README.md
+++ b/examples/static/README.md
@@ -42,3 +42,8 @@ $ curl -sLO http://localhost:8080/test.txt
$ cat test.txt
If you read this then the static file server works!
```
+
+HTML5 Video Example
+-------------------
+
+Open http://localhost:8080/video.html in your favorite browser.
diff --git a/examples/static/priv/small.mp4 b/examples/static/priv/small.mp4
new file mode 100644
index 0000000..1fc4788
--- /dev/null
+++ b/examples/static/priv/small.mp4
Binary files differ
diff --git a/examples/static/priv/small.ogv b/examples/static/priv/small.ogv
new file mode 100644
index 0000000..6409d6e
--- /dev/null
+++ b/examples/static/priv/small.ogv
Binary files differ
diff --git a/examples/static/priv/video.html b/examples/static/priv/video.html
new file mode 100644
index 0000000..eca63ee
--- /dev/null
+++ b/examples/static/priv/video.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<body>
+ <h1>HTML5 Video Example</h1>
+ <video controls>
+ <source src="small.ogv" type="video/ogg"/>
+ <source src="small.mp4" type="video/mp4"/>
+ </video>
+ <p>Videos taken from <a href="http://techslides.com/sample-webm-ogg-and-mp4-video-files-for-html5/">TechSlides</a></p>
+</body>
+</html>
diff --git a/examples/static/src/static_app.erl b/examples/static/src/static_app.erl
index 16ef554..a2b9c31 100644
--- a/examples/static/src/static_app.erl
+++ b/examples/static/src/static_app.erl
@@ -11,16 +11,16 @@
%% API.
start(_Type, _Args) ->
- Dispatch = [
+ Dispatch = cowboy_router:compile([
{'_', [
- {['...'], cowboy_static, [
+ {"/[...]", cowboy_static, [
{directory, {priv_dir, static, []}},
{mimetypes, {fun mimetypes:path_to_mimes/2, default}}
]}
]}
- ],
+ ]),
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
- {dispatch, Dispatch}
+ {env, [{dispatch, Dispatch}]}
]),
static_sup:start_link().
diff --git a/examples/static/start.sh b/examples/static/start.sh
index ab15739..bc67846 100755
--- a/examples/static/start.sh
+++ b/examples/static/start.sh
@@ -1,3 +1,4 @@
#!/bin/sh
erl -pa ebin deps/*/ebin -s static \
- -eval "io:format(\"Point your browser at http://localhost:8080/test.txt~n\")."
+ -eval "io:format(\"Point your browser at http://localhost:8080/test.txt~n\")." \
+ -eval "io:format(\"Point your browser at http://localhost:8080/video.html~n\")."
diff --git a/guide/handlers.md b/guide/handlers.md
new file mode 100644
index 0000000..e2c1264
--- /dev/null
+++ b/guide/handlers.md
@@ -0,0 +1,55 @@
+Handlers
+========
+
+Purpose
+-------
+
+Handlers are Erlang modules that represent a resource.
+
+Handlers must process the request and send a reply. The nature of the
+reply will vary between handlers.
+
+Different kinds of handlers can be combined in a single module. This
+allows a module to handle both websocket and long-polling code in a
+single place, for example.
+
+Protocol upgrades
+-----------------
+
+Cowboy features many different handlers: HTTP handlers, loop handlers,
+websocket handlers, REST handlers and static handlers. All of them
+have a common entry point: the `init/3` function.
+
+By default, Cowboy considers your handler to be an HTTP handler.
+
+To switch to a different protocol, like, for example, Websocket,
+you must perform a protocol upgrade. This is done by returning
+a protocol upgrade tuple at the end of `init/3`.
+
+The following snippet upgrades the handler to `my_protocol`.
+
+``` erlang
+init(_Any, _Req, _Opts) ->
+ {upgrade, protocol, my_protocol}.
+```
+
+Cowboy comes with two protocol upgrades: `cowboy_rest` and
+`cowboy_websocket`. Use these values in place of `my_protocol`
+to use them.
+
+Custom protocol upgrades
+------------------------
+
+The `my_protocol` module above will be used for further processing
+of the request. It requires only one callback, `upgrade/4`.
+
+It receives the request object, the middleware environment, and
+the handler this request has been routed to along with its options.
+
+``` erlang
+upgrade(Req, Env, Handler, HandlerOpts) ->
+ %% ...
+```
+
+This callback is expected to behave like any middleware. Please
+see the corresponding chapter for more information.
diff --git a/guide/hooks.md b/guide/hooks.md
new file mode 100644
index 0000000..d4b520a
--- /dev/null
+++ b/guide/hooks.md
@@ -0,0 +1,77 @@
+Hooks
+=====
+
+On request
+----------
+
+The `onrequest` hook is called as soon as Cowboy finishes fetching
+the request headers. It occurs before any other processing, including
+routing. It can be used to perform any modification needed on the
+request object before continuing with the processing. If a reply is
+sent inside this hook, then Cowboy will move on to the next request,
+skipping any subsequent handling.
+
+This hook is a function that takes a request object as argument,
+and returns a request object. This function MUST NOT crash. Cowboy
+will not send any reply if a crash occurs in this function.
+
+You can specify the `onrequest` hook when creating the listener,
+inside the request options.
+
+``` erlang
+cowboy:start_http(my_http_listener, 100,
+ [{port, 8080}],
+ [
+ {env, [{dispatch, Dispatch}]},
+ {onrequest, fun ?MODULE:debug_hook/1}
+ ]
+).
+```
+
+The following hook function prints the request object everytime a
+request is received. This can be useful for debugging, for example.
+
+``` erlang
+debug_hook(Req) ->
+ erlang:display(Req),
+ Req.
+```
+
+Make sure to always return the last request object obtained.
+
+On response
+-----------
+
+The `onresponse` hook is called right before sending the response
+to the socket. It can be used for the purposes of logging responses,
+or for modifying the response headers or body. The best example is
+providing custom error pages.
+
+Note that like the `onrequest` hook, this function MUST NOT crash.
+Cowboy may or may not send a reply if this function crashes.
+
+You can specify the `onresponse` hook when creating the listener also.
+
+``` erlang
+cowboy:start_http(my_http_listener, 100,
+ [{port, 8080}],
+ [
+ {env, [{dispatch, Dispatch}]},
+ {onresponse, fun ?MODULE:custom_404_hook/4}
+ ]
+).
+```
+
+The following hook function will provide a custom body for 404 errors
+when it has not been provided before, and will let Cowboy proceed with
+the default response otherwise.
+
+``` erlang
+custom_404_hook(404, Headers, <<>>, Req) ->
+ {ok, Req2} = cowboy_req:reply(404, Headers, <<"404 Not Found.">>, Req),
+ Req2;
+custom_404_hook(_, _, _, Req) ->
+ Req.
+```
+
+Again, make sure to always return the last request object obtained.
diff --git a/guide/http_handlers.md b/guide/http_handlers.md
new file mode 100644
index 0000000..aba0e06
--- /dev/null
+++ b/guide/http_handlers.md
@@ -0,0 +1,27 @@
+HTTP handlers
+=============
+
+Purpose
+-------
+
+HTTP handlers are the simplest Cowboy module to handle a request.
+
+Usage
+-----
+
+You need to implement three callbacks for HTTP handlers. The first,
+`init/3`, is common to all handlers. In the context of HTTP handlers
+this should be used for any initialization needs.
+
+The second callback, `handle/2`, is where most of your code should
+be. As the name explains, this is where you handle the request.
+
+The last callback, `terminate/3`, will be empty most of the time.
+It's used for any needed cleanup. If you used the process dictionary,
+timers, monitors then you most likely want to stop them in this
+callback, as Cowboy might end up reusing this process for subsequent
+requests. Please see the Internals chapter for more information.
+
+Of course the general advice is to not use the process dictionary,
+and that any operation requiring reception of messages should be
+done in a loop handler, documented in its own chapter.
diff --git a/guide/internals.md b/guide/internals.md
new file mode 100644
index 0000000..0f8adc2
--- /dev/null
+++ b/guide/internals.md
@@ -0,0 +1,79 @@
+Internals
+=========
+
+Architecture
+------------
+
+Cowboy is a lightweight HTTP server.
+
+It is built on top of Ranch. Please see the Ranch guide for more
+informations.
+
+It uses only one process per connection. The process where your
+code runs is the process controlling the socket. Using one process
+instead of two allows for lower memory usage.
+
+It uses binaries. Binaries are more efficient than lists for
+representing strings because they take less memory space. Processing
+performance can vary depending on the operation. Binaries are known
+for generally getting a great boost if the code is compiled natively.
+Please see the HiPE documentation for more details.
+
+Because querying for the current date and time can be expensive,
+Cowboy generates one `Date` header value every second, shares it
+to all other processes, which then simply copy it in the response.
+This allows compliance with HTTP/1.1 with no actual performance loss.
+
+One process for many requests
+-----------------------------
+
+As previously mentioned, Cowboy only use one process per connection.
+Because there can be more than one request per connection with the
+keepalive feature of HTTP/1.1, that means the same process will be
+used to handle many requests.
+
+Because of this, you are expected to make sure your process cleans
+up before terminating the handling of the current request. This may
+include cleaning up the process dictionary, timers, monitoring and
+more.
+
+Lowercase header names
+----------------------
+
+For consistency reasons it has been chosen to convert all header names
+to lowercase binary strings. This prevents the programmer from making
+case mistakes, and is possible because header names are case insensitive.
+
+This works fine for the large majority of clients. However, some badly
+implemented clients, especially ones found in corporate code or closed
+source products, may not handle header names in a case insensitive manner.
+This means that when Cowboy returns lowercase header names, these clients
+will not find the headers they are looking for.
+
+A simple way to solve this is to create an `onresponse` hook that will
+format the header names with the expected case.
+
+``` erlang
+capitalize_hook(Status, Headers, Body, Req) ->
+ Headers2 = [{cowboy_bstr:capitalize_token(N), V}
+ || {N, V} <- Headers],
+ {ok, Req2} = cowboy_req:reply(Status, Headers2, Body, Req),
+ Req2.
+```
+
+Improving performance
+---------------------
+
+By default the maximum number of active connections is set to a
+generally accepted big enough number. This is meant to prevent having
+too many processes performing potentially heavy work and slowing
+everything else down, or taking up all the memory.
+
+Disabling this feature, by setting the `{max_connections, infinity}`
+protocol option, would give you greater performance when you are
+only processing short-lived requests.
+
+Another option is to define platform-specific socket options that
+are known to improve their efficiency.
+
+Please see the Ranch guide for more information.
diff --git a/guide/introduction.md b/guide/introduction.md
index 871e243..c7f48e2 100644
--- a/guide/introduction.md
+++ b/guide/introduction.md
@@ -77,8 +77,8 @@ Dispatch = [
],
%% Name, NbAcceptors, TransOpts, ProtoOpts
cowboy:start_http(my_http_listener, 100,
- [{port, 8080}],
- [{dispatch, Dispatch}]
+ [{port, 8080}],
+ [{env, [{dispatch, Dispatch}]}]
).
```
@@ -87,7 +87,7 @@ handlers, Websocket handlers, REST handlers and static handlers. Their
usage is documented in the respective sections of the guide.
Most applications use the plain HTTP handler, which has three callback
-functions: init/3, handle/2 and terminate/2. Following is an example of
+functions: init/3, handle/2 and terminate/3. Following is an example of
a simple handler module.
``` erlang
@@ -96,7 +96,7 @@ a simple handler module.
-export([init/3]).
-export([handle/2]).
--export([terminate/2]).
+-export([terminate/3]).
init({tcp, http}, Req, Opts) ->
{ok, Req, undefined_state}.
@@ -105,10 +105,10 @@ handle(Req, State) ->
{ok, Req2} = cowboy_req:reply(200, [], <<"Hello World!">>, Req),
{ok, Req2, State}.
-terminate(Req, State) ->
+terminate(Reason, Req, State) ->
ok.
```
The `Req` variable above is the Req object, which allows the developer
-to obtain informations about the request and to perform a reply. Its usage
+to obtain information about the request and to perform a reply. Its usage
is explained in its respective section of the guide.
diff --git a/guide/loop_handlers.md b/guide/loop_handlers.md
new file mode 100644
index 0000000..c3d1891
--- /dev/null
+++ b/guide/loop_handlers.md
@@ -0,0 +1,62 @@
+Loop handlers
+=============
+
+Purpose
+-------
+
+Loop handlers are a special kind of HTTP handlers used when the
+response can not be sent right away. The handler enters instead
+a receive loop waiting for the right message before it can send
+a response.
+
+They are most useful when performing long-polling operations or
+when using server-sent events.
+
+While the same can be accomplished using plain HTTP handlers,
+it is recommended to use loop handlers because they are well-tested
+and allow using built-in features like hibernation and timeouts.
+
+Usage
+-----
+
+Loop handlers are used for requests where a response might not
+be immediately available, but where you would like to keep the
+connection open for a while in case the response arrives. The
+most known example of such practice is known as long-polling.
+
+Loop handlers can also be used for requests where a response is
+partially available and you need to stream the response body
+while the connection is open. The most known example of such
+practice is known as server-sent events.
+
+Loop handlers essentially wait for one or more Erlang messages
+and feed these messages to the `info/3` callback. It also features
+the `init/3` and `terminate/3` callbacks which work the same as
+for plain HTTP handlers.
+
+The following handler waits for a message `{reply, Body}` before
+sending a response. If this message doesn't arrive within 60
+seconds, it gives up and a `204 No Content` will be replied.
+It also hibernates the process to save memory while waiting for
+this message.
+
+``` erlang
+-module(my_loop_handler).
+-behaviour(cowboy_loop_handler).
+
+-export([init/3]).
+-export([info/3]).
+-export([terminate/3]).
+
+init({tcp, http}, Req, Opts) ->
+ {loop, Req, undefined_state, 60000, hibernate}.
+
+info({reply, Body}, Req, State) ->
+ {ok, Req2} = cowboy_req:reply(200, [], Body, Req),
+ {ok, Req2, State};
+info(Message, Req, State) ->
+ {loop, Req, State, hibernate}.
+
+terminate(Reason, Req, State) ->
+ ok.
+```
diff --git a/guide/middlewares.md b/guide/middlewares.md
new file mode 100644
index 0000000..0ab6dc2
--- /dev/null
+++ b/guide/middlewares.md
@@ -0,0 +1,72 @@
+Middlewares
+===========
+
+Purpose
+-------
+
+Cowboy delegates the request processing to middleware components.
+By default, two middlewares are defined, for the routing and handling
+of the request, as is detailed in most of this guide.
+
+Middlewares give you complete control over how requests are to be
+processed. You can add your own middlewares to the mix or completely
+change the chain of middlewares as needed.
+
+Cowboy will execute all middlewares in the given order, unless one
+of them decides to stop processing.
+
+Usage
+-----
+
+Middlewares only need to implement a single callback: `execute/2`.
+It is defined in the `cowboy_middleware` behavior.
+
+This callback has two arguments. The first is the `Req` object.
+The second is the environment.
+
+Middlewares can return one of four different values:
+ * `{ok, Req, Env}` to continue the request processing
+ * `{suspend, Module, Function, Args}` to hibernate
+ * `{halt, Req}` to stop processing and move on to the next request
+ * `{error, StatusCode, Req}` to reply an error and close the socket
+
+Of note is that when hibernating, processing will resume on the given
+MFA, discarding all previous stacktrace. Make sure you keep the `Req`
+and `Env` in the arguments of this MFA for later use.
+
+If an error happens during middleware processing, Cowboy will not try
+to send an error back to the socket, the process will just crash. It
+is up to the middleware to make sure that a reply is sent if something
+goes wrong.
+
+Configuration
+-------------
+
+The middleware environment is defined as the `env` protocol option.
+In the previous chapters we saw it briefly when we needed to pass
+the routing information. It is a list of tuples with the first
+element being an atom and the second any Erlang term.
+
+Two values in the environment are reserved:
+ * `listener` contains the pid of the listener process
+ * `result` contains the result of the processing
+
+The `listener` value is always defined. The `result` value can be
+set by any middleware. If set to anything other than `ok`, Cowboy
+will not process any subsequent requests on this connection.
+
+The middlewares that come with Cowboy may define or require other
+environment values to perform.
+
+Routing middleware
+------------------
+
+The routing middleware requires the `dispatch` value. If routing
+succeeds, it will put the handler name and options in the `handler`
+and `handler_opts` values of the environment, respectively.
+
+Handler middleware
+------------------
+
+The handler middleware requires the `handler` and `handler_opts`
+values. It puts the result of the request handling into `result`.
diff --git a/guide/req.md b/guide/req.md
new file mode 100644
index 0000000..e13d3a5
--- /dev/null
+++ b/guide/req.md
@@ -0,0 +1,205 @@
+Request object
+==============
+
+Purpose
+-------
+
+The request object is a special variable that can be used
+to interact with a request, extracting information from it
+or modifying it, and sending a response.
+
+It's a special variable because it contains both immutable
+and mutable state. This means that some operations performed
+on the request object will always return the same result,
+while others will not. For example, obtaining request headers
+can be repeated safely. Obtaining the request body can only
+be done once, as it is read directly from the socket.
+
+With few exceptions, all calls to the `cowboy_req` module
+will return an updated request object. You MUST use the new
+request object instead of the old one for all subsequent
+operations.
+
+Request
+-------
+
+Cowboy allows you to retrieve a lot of information about
+the request. All these calls return a `{Value, Req}` tuple,
+with `Value` the requested value and `Req` the updated
+request object.
+
+The following access functions are defined in `cowboy_req`:
+
+ * `method/1`: the request method (`<<"GET">>`, `<<"POST">>`...)
+ * `version/1`: the HTTP version (`{1,0}` or `{1,1}`)
+ * `peer/1`: the peer address and port number
+ * `peer_addr/1`: the peer address guessed using the request headers
+ * `host/1`: the hostname requested
+ * `host_info/1`: the result of the `[...]` match on the host
+ * `port/1`: the port number used for the connection
+ * `path/1`: the path requested
+ * `path_info/1`: the result of the `[...]` match on the path
+ * `qs/1`: the entire query string unmodified
+ * `qs_val/{2,3}`: the value for the requested query string key
+ * `qs_vals/1`: all key/values found in the query string
+ * `fragment/1`: the fragment part of the URL (e.g. `#nav-links`)
+ * `host_url/1`: the requested URL without the path, qs and fragment
+ * `url/1`: the requested URL
+ * `binding/{2,3}`: the value for the requested binding found during routing
+ * `bindings/1`: all key/values found during routing
+ * `header/{2,3}`: the value for the requested header name
+ * `headers/1`: all headers name/value
+ * `cookie/{2,3}`: the value for the requested cookie name
+ * `cookies/1`: all cookies name/value
+ * `meta/{2,3}`: the meta information for the requested key
+
+All the functions above that can take two or three arguments
+take an optional third argument for the default value if
+none is found. Otherwise it will return `undefined`.
+
+In addition, Cowboy allows you to parse headers using the
+`parse_header/{2,3}` function, which takes a header name
+as lowercase binary, the request object, and an optional
+default value. It returns `{ok, ParsedValue, Req}` if it
+could be parsed, `{undefined, RawValue, Req}` if Cowboy
+doesn't know this header, and `{error, badarg}` if Cowboy
+encountered an error while trying to parse it.
+
+Finally, Cowboy allows you to set request meta information
+using the `set_meta/3` function, which takes a name, a value
+and the request object and returns the latter modified.
+
+Request body
+------------
+
+Cowboy will not read the request body until you ask it to.
+If you don't, then Cowboy will simply discard it. It will
+not take extra memory space until you start reading it.
+
+Cowboy has a few utility functions for dealing with the
+request body.
+
+The function `has_body/1` will return whether the request
+contains a body. Note that some clients may not send the
+right headers while still sending a body, but as Cowboy has
+no way of detecting it this function will return `false`.
+
+The function `body_length/1` retrieves the size of the
+request body. If the body is compressed, the value returned
+here is the compressed size. If a `Transfer-Encoding` header
+was passed in the request, then Cowboy will return a size
+of `undefined`, as it has no way of knowing it.
+
+If you know the request contains a body, and that it is
+of appropriate size, then you can read it directly with
+either `body/1` or `body_qs/1`. Otherwise, you will want
+to stream it with `stream_body/1` and `skip_body/1`, with
+the streaming process optionally initialized using `init_stream/4`.
+
+Multipart request body
+----------------------
+
+Cowboy provides facilities for dealing with multipart bodies.
+They are typically used for uploading files. You can use two
+functions to process these bodies, `multipart_data/1` and
+`multipart_skip/1`.
+
+Response
+--------
+
+You can send a response by calling the `reply/{2,3,4}` function.
+It takes the status code for the response (usually `200`),
+an optional list of headers, an optional body and the request
+object.
+
+The following snippet sends a simple response with no headers
+specified but with a body.
+
+``` erlang
+{ok, Req2} = cowboy_req:reply(200, [], "Hello world!", Req).
+```
+
+If this is the only line in your handler then make sure to return
+the `Req2` variable to Cowboy so it can know you replied.
+
+If you want to send HTML you'll need to specify the `Content-Type`
+header so the client can properly interpret it.
+
+``` erlang
+{ok, Req2} = cowboy_req:reply(200,
+ [{<<"content-type">>, <<"text/html">>}],
+ "<html><head>Hello world!</head><body><p>Hats off!</p></body></html>",
+ Req).
+```
+
+You only need to make sure to follow conventions and to use a
+lowercase header name.
+
+Chunked response
+----------------
+
+You can also send chunked responses using `chunked_reply/{2,3}`.
+Chunked responses allow you to send the body in chunks of various
+sizes. It is the recommended way of performing streaming if the
+client supports it.
+
+You must first initiate the response by calling the aforementioned
+function, then you can call `chunk/2` as many times as needed.
+The following snippet sends a body in three chunks.
+
+``` erlang
+{ok, Req2} = cowboy_req:chunked_reply(200, Req),
+ok = cowboy_req:chunk("Hello...", Req2),
+ok = cowboy_req:chunk("chunked...", Req2),
+ok = cowboy_req:chunk("world!!", Req2).
+```
+
+As you can see the call to `chunk/2` does not return a modified
+request object. It may return an error, however, so you should
+make sure that you match the return value on `ok`.
+
+Response preconfiguration
+-------------------------
+
+Cowboy allows you to set response cookies, headers or body
+in advance without having to send the response at the same time.
+Then, when you decide to send it, all these informations will be
+built into the resulting response.
+
+Some of the functions available for this purpose also give you
+additional functionality, like `set_resp_cookie/4` which will build
+the appropriate `Set-Cookie` header, or `set_resp_body_fun/{2,3}`
+which allows you to stream the response body.
+
+Note that any value given directly to `reply/{2,3,4}` will
+override all preset values. This means for example that you
+can set a default body and then override it when you decide
+to send a reply.
+
+Closing the connection
+----------------------
+
+HTTP/1.1 keep-alive allows clients to send more than one request
+on the same connection. This can be useful for speeding up the
+loading of webpages, but is not required. You can tell Cowboy
+explicitly that you want to close the connection by setting the
+`Connection` header to `close`.
+
+``` erlang
+{ok, Req2} = cowboy_req:reply(200,
+ [{<<"connection">>, <<"close">>}],
+ Req).
+```
+
+Reducing the memory footprint
+-----------------------------
+
+When you are done reading information from the request object
+and know you are not going to access it anymore, for example
+when using long-polling or Websocket, you can use the `compact/1`
+function to remove most of the data from the request object and
+free memory.
+
+``` erlang
+Req2 = cowboy_req:compact(Req).
+```
diff --git a/guide/rest_handlers.md b/guide/rest_handlers.md
new file mode 100644
index 0000000..df5f841
--- /dev/null
+++ b/guide/rest_handlers.md
@@ -0,0 +1,28 @@
+REST handlers
+=============
+
+Purpose
+-------
+
+REST is a set of constraints that, when applied to HTTP, dictates how
+resources must behave. It is the recommended way to handle requests
+with Cowboy.
+
+REST is implemented in Cowboy as a protocol upgrade. Once upgraded,
+the request is handled as a state machine with many optional callbacks
+describing the resource and modifying the machine's behavior.
+
+Flow diagram
+------------
+
+@todo Add the beautiful flow diagram here.
+
+Callbacks
+---------
+
+@todo Describe the callbacks.
+
+Usage
+-----
+
+@todo Explain how to use them.
diff --git a/guide/routing.md b/guide/routing.md
new file mode 100644
index 0000000..9d5c5af
--- /dev/null
+++ b/guide/routing.md
@@ -0,0 +1,244 @@
+Routing
+=======
+
+Purpose
+-------
+
+Cowboy does nothing by default.
+
+To make Cowboy useful, you need to map URLs to Erlang modules that will
+handle the requests. This is called routing.
+
+When Cowboy receives a request, it tries to match the requested host and
+path to the resources given in the dispatch rules. If it matches, then
+the associated Erlang code will be executed.
+
+Routing rules are given per host. Cowboy will first match on the host,
+and then try to find a matching path.
+
+Routes need to be compiled before they can be used by Cowboy.
+
+Structure
+---------
+
+The general structure for the routes is defined as follow.
+
+``` erlang
+Routes = [Host1, Host2, ... HostN].
+```
+
+Each host contains matching rules for the host along with optional
+constraints, and a list of routes for the path component.
+
+``` erlang
+Host1 = {HostMatch, PathsList}.
+Host2 = {HostMatch, Constraints, PathsList}.
+```
+
+The list of routes for the path component is defined similar to the
+list of hosts.
+
+``` erlang
+PathsList = [Path1, Path2, ... PathN].
+```
+
+Finally, each path contains matching rules for the path along with
+optional constraints, and gives us the handler module to be used
+along with options that will be given to it on initialization.
+
+``` erlang
+Path1 = {PathMatch, Handler, Opts}.
+Path2 = {PathMatch, Constraints, Handler, Opts}.
+```
+
+Continue reading to learn more about the match syntax and the optional
+constraints.
+
+Match syntax
+------------
+
+The match syntax is used to associate host names and paths with their
+respective handlers.
+
+The match syntax is the same for host and path with a few subtleties.
+Indeed, the segments separator is different, and the host is matched
+starting from the last segment going to the first. All examples will
+feature both host and path match rules and explain the differences
+when encountered.
+
+Excluding special values that we will explain at the end of this section,
+the simplest match value is a host or a path. It can be given as either
+a `string()` or a `binary()`.
+
+``` erlang
+PathMatch1 = "/".
+PathMatch2 = "/path/to/resource".
+
+HostMatch1 = "cowboy.example.org".
+```
+
+As you can see, all paths defined this way must start with a slash
+character. Note that these two paths are identical as far as routing
+is concerned.
+
+``` erlang
+PathMatch2 = "/path/to/resource".
+PathMatch3 = "/path/to/resource/".
+```
+
+Hosts with and without a trailing dot are equivalent for routing.
+Similarly, hosts with and without a leading dot are also equivalent.
+
+``` erlang
+HostMatch1 = "cowboy.example.org".
+HostMatch2 = "cowboy.example.org.".
+HostMatch3 = ".cowboy.example.org".
+```
+
+It is possible to extract segments of the host and path and to store
+the values in the `Req` object for later use. We call these kind of
+values bindings.
+
+The syntax for bindings is very simple. A segment that begins with
+the `:` character means that what follows until the end of the segment
+is the name of the binding in which the segment value will be stored.
+
+``` erlang
+PathMatch = "/hats/:name/prices".
+HostMatch = ":subdomain.example.org".
+```
+
+If these two end up matching when routing, you will end up with two
+bindings defined, `subdomain` and `name`, each containing the
+segment value where they were defined. For example, the URL
+`http://test.example.org/hats/wild_cowboy_legendary/prices` will
+result in having the value `test` bound to the name `subdomain`
+and the value `wild_cowboy_legendary` bound to the name `name`.
+They can later be retrieved using `cowboy_req:binding/{2,3}`. The
+binding name must be given as an atom.
+
+There is a special binding name you can use to mimic the underscore
+variable in Erlang. Any match against the `_` binding will succeed
+but the data will be discarded. This is especially useful for
+matching against many domain names in one go.
+
+``` erlang
+HostMatch = "ninenines.:_".
+```
+
+Similarly, it is possible to have optional segments. Anything
+between brackets is optional.
+
+``` erlang
+PathMatch = "/hats/[page/:number]".
+HostMatch = "[www.]ninenines.eu".
+```
+
+You can also have imbricated optional segments.
+
+``` erlang
+PathMatch = "/hats/[page/[:number]]".
+```
+
+You can retrieve the rest of the host or path using `[...]`.
+In the case of hosts it will match anything before, in the case
+of paths anything after the previously matched segments. It is
+a special case of optional segments, in that it can have
+zero, one or many segments. You can then find the segments using
+`cowboy_req:host_info/1` and `cowboy_req:path_info/1` respectively.
+They will be represented as a list of segments.
+
+``` erlang
+PathMatch = "/hats/[...]".
+HostMatch = "[...]ninenines.eu".
+```
+
+If a binding appears twice in the routing rules, then the match
+will succeed only if they share the same value. This copies the
+Erlang pattern matching behavior.
+
+``` erlang
+PathMatch = "/hats/:name/:name".
+```
+
+This is also true when an optional segment is present. In this
+case the two values must be identical only if the segment is
+available.
+
+``` erlang
+PathMatch = "/hats/:name/[:name]".
+```
+
+If a binding is defined in both the host and path, then they must
+also share the same value.
+
+``` erlang
+PathMatch = "/:user/[...]".
+HostMatch = ":user.github.com".
+```
+
+Finally, there are two special match values that can be used. The
+first is the atom `'_'` which will match any host or path.
+
+``` erlang
+PathMatch = '_'.
+HostMatch = '_'.
+```
+
+The second is the special host match `"*"` which will match the
+wildcard path, generally used alongside the `OPTIONS` method.
+
+``` erlang
+HostMatch = "*".
+```
+
+Constraints
+-----------
+
+After the matching has completed, the resulting bindings can be tested
+against a set of constraints. Constraints are only tested when the
+binding is defined. They run in the order you defined them. The match
+will succeed only if they all succeed.
+
+They are always given as a two or three elements tuple, where the first
+element is the name of the binding, the second element is the constraint's
+name, and the optional third element is the constraint's arguments.
+
+The following constraints are currently defined:
+
+ * {Name, int}
+ * {Name, function, fun ((Value) -> true | {true, NewValue} | false)}
+
+The `int` constraint will check if the binding is a binary string
+representing an integer, and if it is, will convert the value to integer.
+
+The `function` constraint will pass the binding value to a user specified
+function that receives the binary value as its only argument and must
+return whether it fulfills the constraint, optionally modifying the value.
+The value thus returned can be of any type.
+
+Note that constraint functions SHOULD be pure and MUST NOT crash.
+
+Compilation
+-----------
+
+The structure defined in this chapter needs to be compiled before it is
+passed to Cowboy. This allows Cowboy to efficiently lookup the correct
+handler to run instead of having to parse the routes repeatedly.
+
+This can be done with a simple call to `cowboy_router:compile/1`.
+
+``` erlang
+{ok, Routes} = cowboy_router:compile([
+ %% {HostMatch, list({PathMatch, Handler, Opts})}
+ {'_', [{'_', my_handler, []}]}
+]),
+%% Name, NbAcceptors, TransOpts, ProtoOpts
+cowboy:start_http(my_http_listener, 100,
+ [{port, 8080}],
+ [{env, [{routes, Routes}]}]
+).
+```
+
+Note that this function will return `{error, badarg}` if the structure
+given is incorrect.
diff --git a/guide/static_handlers.md b/guide/static_handlers.md
new file mode 100644
index 0000000..f87515a
--- /dev/null
+++ b/guide/static_handlers.md
@@ -0,0 +1,30 @@
+Static handlers
+===============
+
+Purpose
+-------
+
+Static handlers are a built-in REST handler for serving files. They
+are available as a convenience and provide fast file serving with
+proper cache handling.
+
+Usage
+-----
+
+Static handlers are pre-written REST handlers. They only need
+to be specified in the routing information with the proper options.
+
+The following example routing serves all files found in the
+`priv_dir/static/` directory of the application. It uses a
+mimetypes library to figure out the files' content types.
+
+``` erlang
+Dispatch = [
+ {'_', [
+ {['...'], cowboy_static, [
+ {directory, {priv_dir, static, []}},
+ {mimetypes, {fun mimetypes:path_to_mimes/2, default}}
+ ]}
+ ]}
+].
+```
diff --git a/guide/toc.md b/guide/toc.md
index b57b92e..2f8fa36 100644
--- a/guide/toc.md
+++ b/guide/toc.md
@@ -6,43 +6,54 @@ Cowboy User Guide
* Prerequisites
* Conventions
* Getting started
- * Routing
+ * [Routing](routing.md)
* Purpose
* Dispatch list
* Match rules
* Bindings
* Constraints
- * Handlers
+ * [Handlers](handlers.md)
* Purpose
* Protocol upgrades
- * HTTP handlers
+ * Custom protocol upgrades
+ * [HTTP handlers](http_handlers.md)
* Purpose
- * Callbacks
* Usage
- * Loop handlers
+ * [Loop handlers](loop_handlers.md)
* Purpose
- * Callbacks
* Usage
- * Websocket handlers
+ * [Websocket handlers](ws_handlers.md)
* Purpose
- * Callbacks
* Usage
- * REST handlers
+ * [REST handlers](rest_handlers.md)
* Purpose
* Flow diagram
* Callbacks
* Usage
- * Static handlers
+ * [Static handlers](static_handlers.md)
* Purpose
* Usage
- * Request object
+ * [Request object](req.md)
* Purpose
* Request
* Request body
- * Reply
- * Hooks
+ * Multipart request body
+ * Response
+ * Chunked response
+ * Response preconfiguration
+ * Closing the connection
+ * Reducing the memory footprint
+ * [Hooks](hooks.md)
* On request
* On response
- * Internals
+ * [Middlewares](middlewares.md)
+ * Purpose
+ * Usage
+ * Configuration
+ * Routing middleware
+ * Handler middleware
+ * [Internals](internals.md)
* Architecture
- * Efficiency considerations
+ * One process for many requests
+ * Lowercase header names
+ * Improving performance
diff --git a/guide/ws_handlers.md b/guide/ws_handlers.md
new file mode 100644
index 0000000..c1e551e
--- /dev/null
+++ b/guide/ws_handlers.md
@@ -0,0 +1,75 @@
+Websocket handlers
+==================
+
+Purpose
+-------
+
+Websocket is an extension to HTTP to emulate plain TCP connections
+between the user's browser and the server. Requests that are upgraded
+are then handled by websocket handlers.
+
+Both sides of the socket can send data at any time asynchronously.
+
+Websocket is an IETF standard. Cowboy supports the standard and all
+the drafts that were previously implemented by browsers. Websocket
+is implemented by most browsers today, although for backward
+compatibility reasons a solution like [Bullet](https://github.com/extend/bullet)
+might be preferred.
+
+Usage
+-----
+
+Websocket handlers are a bridge between the client and your system.
+They can receive data from the client, through `websocket_handle/3`,
+or from the system, through `websocket_info/3`. It is up to the
+handler to decide to process this data, and optionally send a reply
+to the client.
+
+The first thing to do to be able to handle websockets is to tell
+Cowboy that it should upgrade the connection to use the Websocket
+protocol, as follow.
+
+``` erlang
+init({tcp, http}, Req, Opts) ->
+ {upgrade, protocol, cowboy_websocket}.
+```
+
+Cowboy will then switch the protocol and call `websocket_init`,
+followed by zero or more calls to `websocket_data` and
+`websocket_info`. Then, when the connection is shutting down,
+`websocket_terminate` will be called.
+
+The following handler sends a message every second. It also echoes
+back what it receives.
+
+``` erlang
+-module(my_ws_handler).
+-behaviour(cowboy_websocket_handler).
+
+-export([init/3]).
+-export([websocket_init/3]).
+-export([websocket_handle/3]).
+-export([websocket_info/3]).
+-export([websocket_terminate/3]).
+
+init({tcp, http}, Req, Opts) ->
+ {upgrade, protocol, cowboy_websocket}.
+
+websocket_init(TransportName, Req, _Opts) ->
+ erlang:start_timer(1000, self(), <<"Hello!">>),
+ {ok, Req, undefined_state}.
+
+websocket_handle({text, Msg}, Req, State) ->
+ {reply, {text, << "That's what she said! ", Msg/binary >>}, Req, State};
+websocket_handle(_Data, Req, State) ->
+ {ok, Req, State}.
+
+websocket_info({timeout, _Ref, Msg}, Req, State) ->
+ erlang:start_timer(1000, self(), <<"How' you doin'?">>),
+ {reply, {text, Msg}, Req, State};
+websocket_info(_Info, Req, State) ->
+ {ok, Req, State}.
+
+websocket_terminate(_Reason, _Req, _State) ->
+ ok.
+```
diff --git a/rebar.config b/rebar.config
index ef634de..ba92ee5 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,9 +1,3 @@
{deps, [
- {ranch, "0\\.6\\.0.*", {git, "git://github.com/extend/ranch.git", "0.6.0"}}
-]}.
-{erl_opts, [
-%% bin_opt_info,
-%% warn_missing_spec,
- warnings_as_errors,
- warn_export_all
+ {ranch, ".*", {git, "git://github.com/extend/ranch.git", "0.6.1"}}
]}.
diff --git a/rebar.tests.config b/rebar.tests.config
deleted file mode 100644
index 128f069..0000000
--- a/rebar.tests.config
+++ /dev/null
@@ -1,8 +0,0 @@
-{cover_enabled, true}.
-{deps, [
- {proper, ".*",
- {git, "git://github.com/manopapad/proper.git", "master"}},
- {ranch, "0\\.6\\.0.*", {git, "git://github.com/extend/ranch.git", "0.6.0"}}
-]}.
-{eunit_opts, [verbose, {report, {eunit_surefire, [{dir, "."}]}}]}.
-{erl_opts, []}.
diff --git a/src/cowboy.app.src b/src/cowboy.app.src
index d32262e..59fa8fe 100644
--- a/src/cowboy.app.src
+++ b/src/cowboy.app.src
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy.erl b/src/cowboy.erl
index 9e4a66a..79dbb71 100644
--- a/src/cowboy.erl
+++ b/src/cowboy.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_app.erl b/src/cowboy_app.erl
index 180d400..b46ba1d 100644
--- a/src/cowboy_app.erl
+++ b/src/cowboy_app.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_bstr.erl b/src/cowboy_bstr.erl
index 6e5b353..bc6818f 100644
--- a/src/cowboy_bstr.erl
+++ b/src/cowboy_bstr.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -16,16 +16,40 @@
-module(cowboy_bstr).
%% Binary strings.
+-export([capitalize_token/1]).
-export([to_lower/1]).
%% Characters.
-export([char_to_lower/1]).
-export([char_to_upper/1]).
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-endif.
+
+%% @doc Capitalize a token.
+%%
+%% The first letter and all letters after a dash are capitalized.
+%% This is the form seen for header names in the HTTP/1.1 RFC and
+%% others. Note that using this form isn't required, as header name
+%% are case insensitive, and it is only provided for use with eventual
+%% badly implemented clients.
+-spec capitalize_token(B) -> B when B::binary().
+capitalize_token(B) ->
+ capitalize_token(B, true, <<>>).
+capitalize_token(<<>>, _, Acc) ->
+ Acc;
+capitalize_token(<< $-, Rest/bits >>, _, Acc) ->
+ capitalize_token(Rest, true, << Acc/binary, $- >>);
+capitalize_token(<< C, Rest/bits >>, true, Acc) ->
+ capitalize_token(Rest, false, << Acc/binary, (char_to_upper(C)) >>);
+capitalize_token(<< C, Rest/bits >>, false, Acc) ->
+ capitalize_token(Rest, false, << Acc/binary, (char_to_lower(C)) >>).
+
%% @doc Convert a binary string to lowercase.
--spec to_lower(binary()) -> binary().
-to_lower(L) ->
- << << (char_to_lower(C)) >> || << C >> <= L >>.
+-spec to_lower(B) -> B when B::binary().
+to_lower(B) ->
+ << << (char_to_lower(C)) >> || << C >> <= B >>.
%% @doc Convert [A-Z] characters to lowercase.
%% @end
@@ -88,3 +112,22 @@ char_to_upper($x) -> $X;
char_to_upper($y) -> $Y;
char_to_upper($z) -> $Z;
char_to_upper(Ch) -> Ch.
+
+%% Tests.
+
+-ifdef(TEST).
+
+capitalize_token_test_() ->
+ %% {Header, Result}
+ Tests = [
+ {<<"heLLo-woRld">>, <<"Hello-World">>},
+ {<<"Sec-Websocket-Version">>, <<"Sec-Websocket-Version">>},
+ {<<"Sec-WebSocket-Version">>, <<"Sec-Websocket-Version">>},
+ {<<"sec-websocket-version">>, <<"Sec-Websocket-Version">>},
+ {<<"SEC-WEBSOCKET-VERSION">>, <<"Sec-Websocket-Version">>},
+ {<<"Sec-WebSocket--Version">>, <<"Sec-Websocket--Version">>},
+ {<<"Sec-WebSocket---Version">>, <<"Sec-Websocket---Version">>}
+ ],
+ [{H, fun() -> R = capitalize_token(H) end} || {H, R} <- Tests].
+
+-endif.
diff --git a/src/cowboy_client.erl b/src/cowboy_client.erl
index fee6793..4d958b1 100644
--- a/src/cowboy_client.erl
+++ b/src/cowboy_client.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2012-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl
index b439bb1..71bcb21 100644
--- a/src/cowboy_clock.erl
+++ b/src/cowboy_clock.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -71,7 +71,7 @@ rfc1123() ->
rfc1123(DateTime) ->
update_rfc1123(<<>>, undefined, DateTime).
-%% @doc Return the current date and time formatted according to RFC-2109.
+%% @doc Return the given date and time formatted according to RFC-2109.
%%
%% This format is used in the <em>set-cookie</em> header sent with
%% HTTP responses.
diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl
deleted file mode 100644
index ef6e8ac..0000000
--- a/src/cowboy_dispatcher.erl
+++ /dev/null
@@ -1,291 +0,0 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
-%% Copyright (c) 2011, Anthony Ramine <[email protected]>
-%%
-%% Permission to use, copy, modify, and/or distribute this software for any
-%% purpose with or without fee is hereby granted, provided that the above
-%% copyright notice and this permission notice appear in all copies.
-%%
-%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-%% @doc Dispatch requests according to a hostname and path.
--module(cowboy_dispatcher).
-
-%% API.
--export([match/3]).
-
--type bindings() :: [{atom(), binary()}].
--type tokens() :: [binary()].
--type match_rule() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()].
--type dispatch_path() :: [{match_rule(), module(), any()}].
--type dispatch_rule() :: {Host::match_rule(), Path::dispatch_path()}.
--type dispatch_rules() :: [dispatch_rule()].
-
--export_type([bindings/0]).
--export_type([tokens/0]).
--export_type([dispatch_rules/0]).
-
--ifdef(TEST).
--include_lib("eunit/include/eunit.hrl").
--endif.
-
-%% API.
-
-%% @doc Match hostname tokens and path tokens against dispatch rules.
-%%
-%% It is typically used for matching tokens for the hostname and path of
-%% the request against a global dispatch rule for your listener.
-%%
-%% Dispatch rules are a list of <em>{Hostname, PathRules}</em> tuples, with
-%% <em>PathRules</em> being a list of <em>{Path, HandlerMod, HandlerOpts}</em>.
-%%
-%% <em>Hostname</em> and <em>Path</em> are match rules and can be either the
-%% atom <em>'_'</em>, which matches everything, `<<"*">>', which match the
-%% wildcard path, or a list of tokens.
-%%
-%% Each token can be either a binary, the atom <em>'_'</em>,
-%% the atom '...' or a named atom. A binary token must match exactly,
-%% <em>'_'</em> matches everything for a single token, <em>'...'</em> matches
-%% everything for the rest of the tokens and a named atom will bind the
-%% corresponding token value and return it.
-%%
-%% The list of hostname tokens is reversed before matching. For example, if
-%% we were to match "www.ninenines.eu", we would first match "eu", then
-%% "ninenines", then "www". This means that in the context of hostnames,
-%% the <em>'...'</em> atom matches properly the lower levels of the domain
-%% as would be expected.
-%%
-%% When a result is found, this function will return the handler module and
-%% options found in the dispatch list, a key-value list of bindings and
-%% the tokens that were matched by the <em>'...'</em> atom for both the
-%% hostname and path.
--spec match(dispatch_rules(), Host::binary() | tokens(), Path::binary())
- -> {ok, module(), any(), bindings(),
- HostInfo::undefined | tokens(),
- PathInfo::undefined | tokens()}
- | {error, notfound, host} | {error, notfound, path}
- | {error, badrequest, path}.
-match([], _, _) ->
- {error, notfound, host};
-match([{'_', PathMatchs}|_Tail], _, Path) ->
- match_path(PathMatchs, undefined, Path, []);
-match([{HostMatch, PathMatchs}|Tail], Tokens, Path)
- when is_list(Tokens) ->
- case list_match(Tokens, lists:reverse(HostMatch), []) of
- false ->
- match(Tail, Tokens, Path);
- {true, Bindings, undefined} ->
- match_path(PathMatchs, undefined, Path, Bindings);
- {true, Bindings, HostInfo} ->
- match_path(PathMatchs, lists:reverse(HostInfo),
- Path, Bindings)
- end;
-match(Dispatch, Host, Path) ->
- match(Dispatch, split_host(Host), Path).
-
--spec match_path(dispatch_path(),
- HostInfo::undefined | tokens(), binary() | tokens(), bindings())
- -> {ok, module(), any(), bindings(),
- HostInfo::undefined | tokens(),
- PathInfo::undefined | tokens()}
- | {error, notfound, path} | {error, badrequest, path}.
-match_path([], _, _, _) ->
- {error, notfound, path};
-match_path([{'_', Handler, Opts}|_Tail], HostInfo, _, Bindings) ->
- {ok, Handler, Opts, Bindings, HostInfo, undefined};
-match_path([{<<"*">>, Handler, Opts}|_Tail], HostInfo, <<"*">>, Bindings) ->
- {ok, Handler, Opts, Bindings, HostInfo, undefined};
-match_path([{PathMatch, Handler, Opts}|Tail], HostInfo, Tokens,
- Bindings) when is_list(Tokens) ->
- case list_match(Tokens, PathMatch, []) of
- false ->
- match_path(Tail, HostInfo, Tokens, Bindings);
- {true, PathBinds, PathInfo} ->
- {ok, Handler, Opts, Bindings ++ PathBinds, HostInfo, PathInfo}
- end;
-match_path(_Dispatch, _HostInfo, badrequest, _Bindings) ->
- {error, badrequest, path};
-match_path(Dispatch, HostInfo, Path, Bindings) ->
- match_path(Dispatch, HostInfo, split_path(Path), Bindings).
-
-%% Internal.
-
-%% @doc Split a hostname into a list of tokens.
--spec split_host(binary()) -> tokens().
-split_host(Host) ->
- split_host(Host, []).
-
-split_host(Host, Acc) ->
- case binary:match(Host, <<".">>) of
- nomatch when Host =:= <<>> ->
- Acc;
- nomatch ->
- [Host|Acc];
- {Pos, _} ->
- << Segment:Pos/binary, _:8, Rest/bits >> = Host,
- false = byte_size(Segment) == 0,
- split_host(Rest, [Segment|Acc])
- end.
-
-%% @doc Split a path into a list of path segments.
-%%
-%% Following RFC2396, this function may return path segments containing any
-%% character, including <em>/</em> if, and only if, a <em>/</em> was escaped
-%% and part of a path segment.
--spec split_path(binary()) -> tokens().
-split_path(<< $/, Path/bits >>) ->
- split_path(Path, []);
-split_path(_) ->
- badrequest.
-
-split_path(Path, Acc) ->
- try
- case binary:match(Path, <<"/">>) of
- nomatch when Path =:= <<>> ->
- lists:reverse([cowboy_http:urldecode(S) || S <- Acc]);
- nomatch ->
- lists:reverse([cowboy_http:urldecode(S) || S <- [Path|Acc]]);
- {Pos, _} ->
- << Segment:Pos/binary, _:8, Rest/bits >> = Path,
- split_path(Rest, [Segment|Acc])
- end
- catch
- error:badarg ->
- badrequest
- end.
-
--spec list_match(tokens(), match_rule(), bindings())
- -> {true, bindings(), undefined | tokens()} | false.
-%% Atom '...' matches any trailing path, stop right now.
-list_match(List, ['...'], Binds) ->
- {true, Binds, List};
-%% Atom '_' matches anything, continue.
-list_match([_E|Tail], ['_'|TailMatch], Binds) ->
- list_match(Tail, TailMatch, Binds);
-%% Both values match, continue.
-list_match([E|Tail], [E|TailMatch], Binds) ->
- list_match(Tail, TailMatch, Binds);
-%% Bind E to the variable name V and continue.
-list_match([E|Tail], [V|TailMatch], Binds) when is_atom(V) ->
- list_match(Tail, TailMatch, [{V, E}|Binds]);
-%% Match complete.
-list_match([], [], Binds) ->
- {true, Binds, undefined};
-%% Values don't match, stop.
-list_match(_List, _Match, _Binds) ->
- false.
-
-%% Tests.
-
--ifdef(TEST).
-
-split_host_test_() ->
- %% {Host, Result}
- Tests = [
- {<<"">>, []},
- {<<"*">>, [<<"*">>]},
- {<<"cowboy.ninenines.eu">>,
- [<<"eu">>, <<"ninenines">>, <<"cowboy">>]},
- {<<"ninenines.eu">>,
- [<<"eu">>, <<"ninenines">>]},
- {<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>,
- [<<"z">>, <<"y">>, <<"x">>, <<"w">>, <<"v">>, <<"u">>, <<"t">>,
- <<"s">>, <<"r">>, <<"q">>, <<"p">>, <<"o">>, <<"n">>, <<"m">>,
- <<"l">>, <<"k">>, <<"j">>, <<"i">>, <<"h">>, <<"g">>, <<"f">>,
- <<"e">>, <<"d">>, <<"c">>, <<"b">>, <<"a">>]}
- ],
- [{H, fun() -> R = split_host(H) end} || {H, R} <- Tests].
-
-split_path_test_() ->
- %% {Path, Result, QueryString}
- Tests = [
- {<<"/">>, []},
- {<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>]},
- {<<"/users">>, [<<"users">>]},
- {<<"/users/42/friends">>, [<<"users">>, <<"42">>, <<"friends">>]},
- {<<"/users/a+b/c%21d">>, [<<"users">>, <<"a b">>, <<"c!d">>]}
- ],
- [{P, fun() -> R = split_path(P) end} || {P, R} <- Tests].
-
-match_test_() ->
- Dispatch = [
- {[<<"www">>, '_', <<"ninenines">>, <<"eu">>], [
- {[<<"users">>, '_', <<"mails">>], match_any_subdomain_users, []}
- ]},
- {[<<"ninenines">>, <<"eu">>], [
- {[<<"users">>, id, <<"friends">>], match_extend_users_friends, []},
- {'_', match_extend, []}
- ]},
- {[<<"ninenines">>, var], [
- {[<<"threads">>, var], match_duplicate_vars,
- [we, {expect, two}, var, here]}
- ]},
- {[<<"erlang">>, ext], [
- {'_', match_erlang_ext, []}
- ]},
- {'_', [
- {[<<"users">>, id, <<"friends">>], match_users_friends, []},
- {'_', match_any, []}
- ]}
- ],
- %% {Host, Path, Result}
- Tests = [
- {<<"any">>, <<"/">>, {ok, match_any, [], []}},
- {<<"www.any.ninenines.eu">>, <<"/users/42/mails">>,
- {ok, match_any_subdomain_users, [], []}},
- {<<"www.ninenines.eu">>, <<"/users/42/mails">>,
- {ok, match_any, [], []}},
- {<<"www.ninenines.eu">>, <<"/">>,
- {ok, match_any, [], []}},
- {<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>,
- {error, notfound, path}},
- {<<"ninenines.eu">>, <<"/">>,
- {ok, match_extend, [], []}},
- {<<"ninenines.eu">>, <<"/users/42/friends">>,
- {ok, match_extend_users_friends, [], [{id, <<"42">>}]}},
- {<<"erlang.fr">>, '_',
- {ok, match_erlang_ext, [], [{ext, <<"fr">>}]}},
- {<<"any">>, <<"/users/444/friends">>,
- {ok, match_users_friends, [], [{id, <<"444">>}]}},
- {<<"ninenines.fr">>, <<"/threads/987">>,
- {ok, match_duplicate_vars, [we, {expect, two}, var, here],
- [{var, <<"fr">>}, {var, <<"987">>}]}}
- ],
- [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
- {ok, Handler, Opts, Binds, undefined, undefined}
- = match(Dispatch, H, P)
- end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests].
-
-match_info_test_() ->
- Dispatch = [
- {[<<"www">>, <<"ninenines">>, <<"eu">>], [
- {[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], match_path, []}
- ]},
- {['...', <<"ninenines">>, <<"eu">>], [
- {'_', match_any, []}
- ]}
- ],
- Tests = [
- {<<"ninenines.eu">>, <<"/">>,
- {ok, match_any, [], [], [], undefined}},
- {<<"bugs.ninenines.eu">>, <<"/">>,
- {ok, match_any, [], [], [<<"bugs">>], undefined}},
- {<<"cowboy.bugs.ninenines.eu">>, <<"/">>,
- {ok, match_any, [], [], [<<"cowboy">>, <<"bugs">>], undefined}},
- {<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>,
- {ok, match_path, [], [], undefined, []}},
- {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>,
- {ok, match_path, [], [], undefined, [<<"path_info">>]}},
- {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>,
- {ok, match_path, [], [], undefined, [<<"foo">>, <<"bar">>]}}
- ],
- [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
- R = match(Dispatch, H, P)
- end} || {H, P, R} <- Tests].
-
--endif.
diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl
new file mode 100644
index 0000000..7ed7db3
--- /dev/null
+++ b/src/cowboy_handler.erl
@@ -0,0 +1,220 @@
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% @doc Handler middleware.
+%%
+%% Execute the handler given by the <em>handler</em> and <em>handler_opts</em>
+%% environment values. The result of this execution is added to the
+%% environment under the <em>result</em> value.
+%%
+%% @see cowboy_http_handler
+-module(cowboy_handler).
+-behaviour(cowboy_middleware).
+
+-export([execute/2]).
+-export([handler_loop/4]).
+
+-record(state, {
+ env :: cowboy_middleware:env(),
+ hibernate = false :: boolean(),
+ loop_timeout = infinity :: timeout(),
+ loop_timeout_ref :: undefined | reference(),
+ resp_sent = false :: boolean()
+}).
+
+%% @private
+-spec execute(Req, Env)
+ -> {ok, Req, Env} | {error, 500, Req}
+ | {suspend, ?MODULE, handler_loop, [any()]}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+execute(Req, Env) ->
+ {_, Handler} = lists:keyfind(handler, 1, Env),
+ {_, HandlerOpts} = lists:keyfind(handler_opts, 1, Env),
+ handler_init(Req, #state{env=Env}, Handler, HandlerOpts).
+
+-spec handler_init(Req, #state{}, module(), any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {error, 500, Req} | {suspend, module(), function(), [any()]}
+ when Req::cowboy_req:req().
+handler_init(Req, State, Handler, HandlerOpts) ->
+ Transport = cowboy_req:get(transport, Req),
+ try Handler:init({Transport:name(), http}, Req, HandlerOpts) of
+ {ok, Req2, HandlerState} ->
+ handler_handle(Req2, State, Handler, HandlerState);
+ {loop, Req2, HandlerState} ->
+ handler_before_loop(Req2, State#state{hibernate=false},
+ Handler, HandlerState);
+ {loop, Req2, HandlerState, hibernate} ->
+ handler_before_loop(Req2, State#state{hibernate=true},
+ Handler, HandlerState);
+ {loop, Req2, HandlerState, Timeout} ->
+ handler_before_loop(Req2, State#state{loop_timeout=Timeout},
+ Handler, HandlerState);
+ {loop, Req2, HandlerState, Timeout, hibernate} ->
+ handler_before_loop(Req2, State#state{
+ hibernate=true, loop_timeout=Timeout}, Handler, HandlerState);
+ {shutdown, Req2, HandlerState} ->
+ terminate_request(Req2, State, Handler, HandlerState,
+ {normal, shutdown});
+ %% @todo {upgrade, transport, Module}
+ {upgrade, protocol, Module} ->
+ upgrade_protocol(Req, State, Handler, HandlerOpts, Module);
+ {upgrade, protocol, Module, Req2, HandlerOpts2} ->
+ upgrade_protocol(Req2, State, Handler, HandlerOpts2, Module)
+ catch Class:Reason ->
+ error_logger:error_msg(
+ "** Cowboy handler ~p terminating in ~p/~p~n"
+ " for the reason ~p:~p~n"
+ "** Options were ~p~n"
+ "** Request was ~p~n"
+ "** Stacktrace: ~p~n~n",
+ [Handler, init, 3, Class, Reason, HandlerOpts,
+ cowboy_req:to_list(Req), erlang:get_stacktrace()]),
+ error_terminate(Req, State)
+ end.
+
+-spec upgrade_protocol(Req, #state{}, module(), any(), module())
+ -> {ok, Req, Env}
+ | {suspend, module(), atom(), any()}
+ | {halt, Req}
+ | {error, cowboy_http:status(), Req}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+upgrade_protocol(Req, #state{env=Env},
+ Handler, HandlerOpts, Module) ->
+ Module:upgrade(Req, Env, Handler, HandlerOpts).
+
+-spec handler_handle(Req, #state{}, module(), any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {error, 500, Req}
+ when Req::cowboy_req:req().
+handler_handle(Req, State, Handler, HandlerState) ->
+ try Handler:handle(Req, HandlerState) of
+ {ok, Req2, HandlerState2} ->
+ terminate_request(Req2, State, Handler, HandlerState2,
+ {normal, shutdown})
+ catch Class:Reason ->
+ error_logger:error_msg(
+ "** Cowboy handler ~p terminating in ~p/~p~n"
+ " for the reason ~p:~p~n"
+ "** Handler state was ~p~n"
+ "** Request was ~p~n"
+ "** Stacktrace: ~p~n~n",
+ [Handler, handle, 2, Class, Reason, HandlerState,
+ cowboy_req:to_list(Req), erlang:get_stacktrace()]),
+ handler_terminate(Req, Handler, HandlerState, Reason),
+ error_terminate(Req, State)
+ end.
+
+%% We don't listen for Transport closes because that would force us
+%% to receive data and buffer it indefinitely.
+-spec handler_before_loop(Req, #state{}, module(), any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {error, 500, Req} | {suspend, module(), function(), [any()]}
+ when Req::cowboy_req:req().
+handler_before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) ->
+ State2 = handler_loop_timeout(State),
+ {suspend, ?MODULE, handler_loop,
+ [Req, State2#state{hibernate=false}, Handler, HandlerState]};
+handler_before_loop(Req, State, Handler, HandlerState) ->
+ State2 = handler_loop_timeout(State),
+ handler_loop(Req, State2, Handler, HandlerState).
+
+%% Almost the same code can be found in cowboy_websocket.
+-spec handler_loop_timeout(#state{}) -> #state{}.
+handler_loop_timeout(State=#state{loop_timeout=infinity}) ->
+ State#state{loop_timeout_ref=undefined};
+handler_loop_timeout(State=#state{loop_timeout=Timeout,
+ loop_timeout_ref=PrevRef}) ->
+ _ = case PrevRef of undefined -> ignore; PrevRef ->
+ erlang:cancel_timer(PrevRef) end,
+ TRef = erlang:start_timer(Timeout, self(), ?MODULE),
+ State#state{loop_timeout_ref=TRef}.
+
+%% @private
+-spec handler_loop(Req, #state{}, module(), any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {error, 500, Req} | {suspend, module(), function(), [any()]}
+ when Req::cowboy_req:req().
+handler_loop(Req, State=#state{loop_timeout_ref=TRef}, Handler, HandlerState) ->
+ receive
+ {cowboy_req, resp_sent} ->
+ handler_loop(Req, State#state{resp_sent=true},
+ Handler, HandlerState);
+ {timeout, TRef, ?MODULE} ->
+ terminate_request(Req, State, Handler, HandlerState,
+ {normal, timeout});
+ {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
+ handler_loop(Req, State, Handler, HandlerState);
+ Message ->
+ handler_call(Req, State, Handler, HandlerState, Message)
+ end.
+
+-spec handler_call(Req, #state{}, module(), any(), any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {error, 500, Req} | {suspend, module(), function(), [any()]}
+ when Req::cowboy_req:req().
+handler_call(Req, State, Handler, HandlerState, Message) ->
+ try Handler:info(Message, Req, HandlerState) of
+ {ok, Req2, HandlerState2} ->
+ terminate_request(Req2, State, Handler, HandlerState2,
+ {normal, shutdown});
+ {loop, Req2, HandlerState2} ->
+ handler_before_loop(Req2, State, Handler, HandlerState2);
+ {loop, Req2, HandlerState2, hibernate} ->
+ handler_before_loop(Req2, State#state{hibernate=true},
+ Handler, HandlerState2)
+ catch Class:Reason ->
+ error_logger:error_msg(
+ "** Cowboy handler ~p terminating in ~p/~p~n"
+ " for the reason ~p:~p~n"
+ "** Handler state was ~p~n"
+ "** Request was ~p~n"
+ "** Stacktrace: ~p~n~n",
+ [Handler, info, 3, Class, Reason, HandlerState,
+ cowboy_req:to_list(Req), erlang:get_stacktrace()]),
+ handler_terminate(Req, Handler, HandlerState, Reason),
+ error_terminate(Req, State)
+ end.
+
+-spec terminate_request(Req, #state{}, module(), any(),
+ {normal, timeout | shutdown} | {error, atom()}) ->
+ {ok, Req, cowboy_middleware:env()} when Req::cowboy_req:req().
+terminate_request(Req, #state{env=Env}, Handler, HandlerState, Reason) ->
+ HandlerRes = handler_terminate(Req, Handler, HandlerState, Reason),
+ {ok, Req, [{result, HandlerRes}|Env]}.
+
+-spec handler_terminate(cowboy_req:req(), module(), any(),
+ {normal, timeout | shutdown} | {error, atom()}) -> ok.
+handler_terminate(Req, Handler, HandlerState, Reason) ->
+ try
+ Handler:terminate(Reason, cowboy_req:lock(Req), HandlerState)
+ catch Class:Reason2 ->
+ error_logger:error_msg(
+ "** Cowboy handler ~p terminating in ~p/~p~n"
+ " for the reason ~p:~p~n"
+ "** Handler state was ~p~n"
+ "** Request was ~p~n"
+ "** Stacktrace: ~p~n~n",
+ [Handler, terminate, 3, Class, Reason2, HandlerState,
+ cowboy_req:to_list(Req), erlang:get_stacktrace()])
+ end.
+
+%% Only send an error reply if there is no resp_sent message.
+-spec error_terminate(Req, #state{})
+ -> {error, 500, Req} | {halt, Req} when Req::cowboy_req:req().
+error_terminate(Req, #state{resp_sent=true}) ->
+ %% Close the connection, but do not attempt sending a reply.
+ {halt, cowboy_req:set([{connection, close}, {resp_state, done}], Req)};
+error_terminate(Req, _) ->
+ {error, 500, Req}.
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index 66383cb..a78e090 100644
--- a/src/cowboy_http.erl
+++ b/src/cowboy_http.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
@@ -36,6 +36,7 @@
-export([token/2]).
-export([token_ci/2]).
-export([quoted_string/2]).
+-export([authorization/2]).
%% Decoding.
-export([te_chunked/2]).
@@ -801,25 +802,72 @@ qvalue(<< C, Rest/binary >>, Fun, Q, M)
qvalue(Data, Fun, Q, _M) ->
Fun(Data, Q).
+%% @doc Parse authorization value according rfc 2617.
+%% Only Basic authorization is supported so far.
+-spec authorization(binary(), binary()) -> {binary(), any()} | {error, badarg}.
+authorization(UserPass, Type = <<"basic">>) ->
+ cowboy_http:whitespace(UserPass,
+ fun(D) ->
+ authorization_basic_userid(base64:mime_decode(D),
+ fun(Rest, Userid) ->
+ authorization_basic_password(Rest,
+ fun(Password) ->
+ {Type, {Userid, Password}}
+ end)
+ end)
+ end);
+authorization(String, Type) ->
+ {Type, String}.
+
+%% @doc Parse user credentials.
+-spec authorization_basic_userid(binary(), fun()) -> any().
+authorization_basic_userid(Data, Fun) ->
+ authorization_basic_userid(Data, Fun, <<>>).
+
+authorization_basic_userid(<<>>, _Fun, _Acc) ->
+ {error, badarg};
+authorization_basic_userid(<<C, _Rest/binary>>, _Fun, Acc)
+ when C < 32; C =:= 127; (C =:=$: andalso Acc =:= <<>>) ->
+ {error, badarg};
+authorization_basic_userid(<<$:, Rest/binary>>, Fun, Acc) ->
+ Fun(Rest, Acc);
+authorization_basic_userid(<<C, Rest/binary>>, Fun, Acc) ->
+ authorization_basic_userid(Rest, Fun, <<Acc/binary, C>>).
+
+-spec authorization_basic_password(binary(), fun()) -> any().
+authorization_basic_password(Data, Fun) ->
+ authorization_basic_password(Data, Fun, <<>>).
+
+authorization_basic_password(<<>>, _Fun, <<>>) ->
+ {error, badarg};
+authorization_basic_password(<<C, _Rest/binary>>, _Fun, _Acc)
+ when C < 32; C=:= 127 ->
+ {error, badarg};
+authorization_basic_password(<<>>, Fun, Acc) ->
+ Fun(Acc);
+authorization_basic_password(<<C, Rest/binary>>, Fun, Acc) ->
+ authorization_basic_password(Rest, Fun, <<Acc/binary, C>>).
+
%% Decoding.
%% @doc Decode a stream of chunks.
--spec te_chunked(binary(), {non_neg_integer(), non_neg_integer()})
- -> more | {ok, binary(), {non_neg_integer(), non_neg_integer()}}
- | {ok, binary(), binary(), {non_neg_integer(), non_neg_integer()}}
- | {done, non_neg_integer(), binary()} | {error, badarg}.
-te_chunked(<<>>, _) ->
- more;
+-spec te_chunked(Bin, TransferState)
+ -> more | {more, non_neg_integer(), Bin, TransferState}
+ | {ok, Bin, TransferState} | {ok, Bin, Bin, TransferState}
+ | {done, non_neg_integer(), Bin} | {error, badarg}
+ when Bin::binary(), TransferState::{non_neg_integer(), non_neg_integer()}.
te_chunked(<< "0\r\n\r\n", Rest/binary >>, {0, Streamed}) ->
{done, Streamed, Rest};
te_chunked(Data, {0, Streamed}) ->
%% @todo We are expecting an hex size, not a general token.
token(Data,
- fun (Rest, _) when byte_size(Rest) < 4 ->
- more;
- (<< "\r\n", Rest/binary >>, BinLen) ->
+ fun (<< "\r\n", Rest/binary >>, BinLen) ->
Len = list_to_integer(binary_to_list(BinLen), 16),
te_chunked(Rest, {Len, Streamed});
+ %% Chunk size shouldn't take too many bytes,
+ %% don't try to stream forever.
+ (Rest, _) when byte_size(Rest) < 16 ->
+ more;
(_, _) ->
{error, badarg}
end);
@@ -827,13 +875,12 @@ te_chunked(Data, {ChunkRem, Streamed}) when byte_size(Data) >= ChunkRem + 2 ->
<< Chunk:ChunkRem/binary, "\r\n", Rest/binary >> = Data,
{ok, Chunk, Rest, {0, Streamed + byte_size(Chunk)}};
te_chunked(Data, {ChunkRem, Streamed}) ->
- Size = byte_size(Data),
- {ok, Data, {ChunkRem - Size, Streamed + Size}}.
+ {more, ChunkRem + 2, Data, {ChunkRem, Streamed}}.
%% @doc Decode an identity stream.
--spec te_identity(binary(), {non_neg_integer(), non_neg_integer()})
- -> {ok, binary(), {non_neg_integer(), non_neg_integer()}}
- | {done, binary(), non_neg_integer(), binary()}.
+-spec te_identity(Bin, TransferState)
+ -> {ok, Bin, TransferState} | {done, Bin, non_neg_integer(), Bin}
+ when Bin::binary(), TransferState::{non_neg_integer(), non_neg_integer()}.
te_identity(Data, {Streamed, Total})
when Streamed + byte_size(Data) < Total ->
{ok, Data, {Streamed + byte_size(Data), Total}};
@@ -1294,4 +1341,15 @@ urlencode_test_() ->
?_assertEqual(<<"%ff+">>, urlencode(<<255, " ">>))
].
+http_authorization_test_() ->
+ [?_assertEqual({<<"basic">>, {<<"Alladin">>, <<"open sesame">>}},
+ authorization(<<"QWxsYWRpbjpvcGVuIHNlc2FtZQ==">>, <<"basic">>)),
+ ?_assertEqual({error, badarg},
+ authorization(<<"dXNlcm5hbWUK">>, <<"basic">>)),
+ ?_assertEqual({error, badarg},
+ authorization(<<"_[]@#$%^&*()-AA==">>, <<"basic">>)),
+ ?_assertEqual({error, badarg},
+ authorization(<<"dXNlcjpwYXNzCA==">>, <<"basic">>)) %% user:pass\010
+ ].
+
-endif.
diff --git a/src/cowboy_http_handler.erl b/src/cowboy_http_handler.erl
index d686f30..0393153 100644
--- a/src/cowboy_http_handler.erl
+++ b/src/cowboy_http_handler.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -22,8 +22,8 @@
%% <em>handle/2</em> allows you to handle the request. It receives the
%% state previously defined.
%%
-%% <em>terminate/2</em> allows you to clean up. It receives the state
-%% previously defined.
+%% <em>terminate/3</em> allows you to clean up. It receives the
+%% termination reason and the state previously defined.
%%
%% There is no required operation to perform in any of these callbacks
%% other than returning the proper values. Make sure you always return
@@ -33,6 +33,9 @@
-type opts() :: any().
-type state() :: any().
+-type terminate_reason() :: {normal, shutdown}
+ | {normal, timeout} %% Only occurs in loop handlers.
+ | {error, atom()}.
-callback init({atom(), http}, Req, opts())
-> {ok, Req, state()}
@@ -42,7 +45,8 @@
| {loop, Req, state(), timeout(), hibernate}
| {shutdown, Req, state()}
| {upgrade, protocol, module()}
+ | {upgrade, protocol, module(), Req, opts()}
when Req::cowboy_req:req().
-callback handle(Req, State) -> {ok, Req, State}
when Req::cowboy_req:req(), State::state().
--callback terminate(cowboy_req:req(), state()) -> ok.
+-callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok.
diff --git a/src/cowboy_loop_handler.erl b/src/cowboy_loop_handler.erl
index 5ff86cf..f8d008f 100644
--- a/src/cowboy_loop_handler.erl
+++ b/src/cowboy_loop_handler.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -23,8 +23,8 @@
%% receive. It receives the message and the state previously defined.
%% It can decide to stop the receive loop or continue receiving.
%%
-%% <em>terminate/2</em> allows you to clean up. It receives the state
-%% previously defined.
+%% <em>terminate/3</em> allows you to clean up. It receives the
+%% termination reason and the state previously defined.
%%
%% There is no required operation to perform in any of these callbacks
%% other than returning the proper values. Make sure you always return
@@ -39,6 +39,9 @@
-type opts() :: any().
-type state() :: any().
+-type terminate_reason() :: {normal, shutdown}
+ | {normal, timeout}
+ | {error, atom()}.
-callback init({atom(), http}, Req, opts())
-> {ok, Req, state()}
@@ -48,10 +51,11 @@
| {loop, Req, state(), timeout(), hibernate}
| {shutdown, Req, state()}
| {upgrade, protocol, module()}
+ | {upgrade, protocol, module(), Req, opts()}
when Req::cowboy_req:req().
-callback info(any(), Req, State)
-> {ok, Req, State}
| {loop, Req, State}
| {loop, Req, State, hibernate}
when Req::cowboy_req:req(), State::state().
--callback terminate(cowboy_req:req(), state()) -> ok.
+-callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok.
diff --git a/src/cowboy_middleware.erl b/src/cowboy_middleware.erl
new file mode 100644
index 0000000..0c1ca77
--- /dev/null
+++ b/src/cowboy_middleware.erl
@@ -0,0 +1,36 @@
+%% Copyright (c) 2013, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% @doc Behaviour for middlewares.
+%%
+%% Only one function needs to be implemented, <em>execute/2</em>.
+%% It receives the Req and the environment and returns them
+%% optionally modified. It can decide to stop the processing with
+%% or without an error. It is also possible to hibernate the process
+%% if needed.
+%%
+%% A middleware can perform any operation. Make sure you always return
+%% the last modified Req so that Cowboy has the up to date information
+%% about the request.
+-module(cowboy_middleware).
+
+-type env() :: [{atom(), any()}].
+-export_type([env/0]).
+
+-callback execute(Req, Env)
+ -> {ok, Req, Env}
+ | {suspend, module(), atom(), any()}
+ | {halt, Req}
+ | {error, cowboy_http:status(), Req}
+ when Req::cowboy_req:req(), Env::env().
diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl
index 7344d1f..b479fa9 100644
--- a/src/cowboy_protocol.erl
+++ b/src/cowboy_protocol.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
@@ -17,7 +17,10 @@
%%
%% The available options are:
%% <dl>
-%% <dt>dispatch</dt><dd>The dispatch list for this protocol.</dd>
+%% <dt>compress</dt><dd>Whether to automatically compress the response
+%% body when the conditions are met. Disabled by default.</dd>
+%% <dt>env</dt><dd>The environment passed and optionally modified
+%% by middlewares.</dd>
%% <dt>max_empty_lines</dt><dd>Max number of empty lines before a request.
%% Defaults to 5.</dd>
%% <dt>max_header_name_length</dt><dd>Max length allowed for header names.
@@ -27,23 +30,22 @@
%% <dt>max_headers</dt><dd>Max number of headers allowed.
%% Defaults to 100.</dd>
%% <dt>max_keepalive</dt><dd>Max number of requests allowed in a single
-%% keep-alive session. Defaults to infinity.</dd>
+%% keep-alive session. Defaults to 100.</dd>
%% <dt>max_request_line_length</dt><dd>Max length allowed for the request
%% line. Defaults to 4096.</dd>
+%% <dt>middlewares</dt><dd>The list of middlewares to execute when a
+%% request is received.</dd>
%% <dt>onrequest</dt><dd>Optional fun that allows Req interaction before
%% any dispatching is done. Host info, path info and bindings are thus
%% not available at this point.</dd>
%% <dt>onresponse</dt><dd>Optional fun that allows replacing a response
%% sent by the application.</dd>
-%% <dt>timeout</dt><dd>Time in milliseconds before an idle
-%% connection is closed. Defaults to 5000 milliseconds.</dd>
+%% <dt>timeout</dt><dd>Time in milliseconds a client has to send the
+%% full request line and headers. Defaults to 5000 milliseconds.</dd>
%% </dl>
%%
%% Note that there is no need to monitor these processes when using Cowboy as
%% an application as it already supervises them under the listener supervisor.
-%%
-%% @see cowboy_dispatcher
-%% @see cowboy_http_handler
-module(cowboy_protocol).
%% API.
@@ -52,20 +54,20 @@
%% Internal.
-export([init/4]).
-export([parse_request/3]).
--export([handler_loop/4]).
+-export([resume/6]).
-type onrequest_fun() :: fun((Req) -> Req).
-type onresponse_fun() ::
fun((cowboy_http:status(), cowboy_http:headers(), iodata(), Req) -> Req).
-
-export_type([onrequest_fun/0]).
-export_type([onresponse_fun/0]).
-record(state, {
- listener :: pid(),
socket :: inet:socket(),
transport :: module(),
- dispatch :: cowboy_dispatcher:dispatch_rules(),
+ middlewares :: [module()],
+ compress :: boolean(),
+ env :: cowboy_middleware:env(),
onrequest :: undefined | onrequest_fun(),
onresponse = undefined :: undefined | onresponse_fun(),
max_empty_lines :: non_neg_integer(),
@@ -76,9 +78,7 @@
max_header_value_length :: non_neg_integer(),
max_headers :: non_neg_integer(),
timeout :: timeout(),
- hibernate = false :: boolean(),
- loop_timeout = infinity :: timeout(),
- loop_timeout_ref :: undefined | reference()
+ until :: non_neg_integer() | infinity
}).
%% API.
@@ -102,24 +102,34 @@ get_value(Key, Opts, Default) ->
%% @private
-spec init(pid(), inet:socket(), module(), any()) -> ok.
init(ListenerPid, Socket, Transport, Opts) ->
- Dispatch = get_value(dispatch, Opts, []),
+ Compress = get_value(compress, Opts, false),
MaxEmptyLines = get_value(max_empty_lines, Opts, 5),
MaxHeaderNameLength = get_value(max_header_name_length, Opts, 64),
MaxHeaderValueLength = get_value(max_header_value_length, Opts, 4096),
MaxHeaders = get_value(max_headers, Opts, 100),
- MaxKeepalive = get_value(max_keepalive, Opts, infinity),
+ MaxKeepalive = get_value(max_keepalive, Opts, 100),
MaxRequestLineLength = get_value(max_request_line_length, Opts, 4096),
+ Middlewares = get_value(middlewares, Opts, [cowboy_router, cowboy_handler]),
+ Env = [{listener, ListenerPid}|get_value(env, Opts, [])],
OnRequest = get_value(onrequest, Opts, undefined),
OnResponse = get_value(onresponse, Opts, undefined),
Timeout = get_value(timeout, Opts, 5000),
ok = ranch:accept_ack(ListenerPid),
- wait_request(<<>>, #state{listener=ListenerPid, socket=Socket,
- transport=Transport, dispatch=Dispatch,
+ wait_request(<<>>, #state{socket=Socket, transport=Transport,
+ middlewares=Middlewares, compress=Compress, env=Env,
max_empty_lines=MaxEmptyLines, max_keepalive=MaxKeepalive,
max_request_line_length=MaxRequestLineLength,
max_header_name_length=MaxHeaderNameLength,
max_header_value_length=MaxHeaderValueLength, max_headers=MaxHeaders,
- timeout=Timeout, onrequest=OnRequest, onresponse=OnResponse}, 0).
+ onrequest=OnRequest, onresponse=OnResponse,
+ timeout=Timeout, until=until(Timeout)}, 0).
+
+-spec until(timeout()) -> non_neg_integer() | infinity.
+until(infinity) ->
+ infinity;
+until(Timeout) ->
+ {Me, S, Mi} = os:timestamp(),
+ Me * 1000000000 + S * 1000 + Mi div 1000 + Timeout.
%% Request parsing.
%%
@@ -128,10 +138,24 @@ init(ListenerPid, Socket, Transport, Opts) ->
%% right after the header parsing is finished and the code becomes
%% more interesting past that point.
+-spec recv(inet:socket(), module(), non_neg_integer() | infinity)
+ -> {ok, binary()} | {error, closed | timeout | atom()}.
+recv(Socket, Transport, infinity) ->
+ Transport:recv(Socket, 0, infinity);
+recv(Socket, Transport, Until) ->
+ {Me, S, Mi} = os:timestamp(),
+ Now = Me * 1000000000 + S * 1000 + Mi div 1000,
+ Timeout = Until - Now,
+ if Timeout < 0 ->
+ {error, timeout};
+ true ->
+ Transport:recv(Socket, 0, Timeout)
+ end.
+
-spec wait_request(binary(), #state{}, non_neg_integer()) -> ok.
wait_request(Buffer, State=#state{socket=Socket, transport=Transport,
- timeout=Timeout}, ReqEmpty) ->
- case Transport:recv(Socket, 0, Timeout) of
+ until=Until}, ReqEmpty) ->
+ case recv(Socket, Transport, Until) of
{ok, Data} ->
parse_request(<< Buffer/binary, Data/binary >>, State, ReqEmpty);
{error, _} ->
@@ -222,8 +246,8 @@ wait_header(_, State=#state{max_headers=MaxHeaders}, _, _, _, _, _, Headers)
when length(Headers) >= MaxHeaders ->
error_terminate(400, State);
wait_header(Buffer, State=#state{socket=Socket, transport=Transport,
- timeout=Timeout}, M, P, Q, F, V, H) ->
- case Transport:recv(Socket, 0, Timeout) of
+ until=Until}, M, P, Q, F, V, H) ->
+ case recv(Socket, Transport, Until) of
{ok, Data} ->
parse_header(<< Buffer/binary, Data/binary >>,
State, M, P, Q, F, V, H);
@@ -294,9 +318,9 @@ parse_hd_name_ws(<< C, Rest/bits >>, S, M, P, Q, F, V, H, Name) ->
end.
wait_hd_before_value(Buffer, State=#state{
- socket=Socket, transport=Transport, timeout=Timeout},
+ socket=Socket, transport=Transport, until=Until},
M, P, Q, F, V, H, N) ->
- case Transport:recv(Socket, 0, Timeout) of
+ case recv(Socket, Transport, Until) of
{ok, Data} ->
parse_hd_before_value(<< Buffer/binary, Data/binary >>,
State, M, P, Q, F, V, H, N);
@@ -326,9 +350,9 @@ parse_hd_before_value(Buffer, State=#state{
%% to change the other arguments' position and trigger costy
%% operations for no reasons.
wait_hd_value(_, State=#state{
- socket=Socket, transport=Transport, timeout=Timeout},
+ socket=Socket, transport=Transport, until=Until},
M, P, Q, F, V, H, N, SoFar) ->
- case Transport:recv(Socket, 0, Timeout) of
+ case recv(Socket, Transport, Until) of
{ok, Data} ->
parse_hd_value(Data, State, M, P, Q, F, V, H, N, SoFar);
{error, timeout} ->
@@ -341,9 +365,9 @@ wait_hd_value(_, State=#state{
%% to check for multilines allows us to avoid a few tests in
%% the critical path, but forces us to have a special function.
wait_hd_value_nl(_, State=#state{
- socket=Socket, transport=Transport, timeout=Timeout},
+ socket=Socket, transport=Transport, until=Until},
M, P, Q, F, V, Headers, Name, SoFar) ->
- case Transport:recv(Socket, 0, Timeout) of
+ case recv(Socket, Transport, Until) of
{ok, << C, Data/bits >>} when C =:= $\s; C =:= $\t ->
parse_hd_value(Data, State, M, P, Q, F, V, Headers, Name, SoFar);
{ok, Data} ->
@@ -437,197 +461,86 @@ parse_host(<< C, Rest/bits >>, Acc) ->
request(Buffer, State=#state{socket=Socket, transport=Transport,
req_keepalive=ReqKeepalive, max_keepalive=MaxKeepalive,
- onresponse=OnResponse},
+ compress=Compress, onresponse=OnResponse},
Method, Path, Query, Fragment, Version, Headers, Host, Port) ->
Req = cowboy_req:new(Socket, Transport, Method, Path, Query, Fragment,
Version, Headers, Host, Port, Buffer, ReqKeepalive < MaxKeepalive,
- OnResponse),
- onrequest(Req, State, Host).
+ Compress, OnResponse),
+ onrequest(Req, State).
%% Call the global onrequest callback. The callback can send a reply,
%% in which case we consider the request handled and move on to the next
%% one. Note that since we haven't dispatched yet, we don't know the
%% handler, host_info, path_info or bindings yet.
--spec onrequest(cowboy_req:req(), #state{}, binary()) -> ok.
-onrequest(Req, State=#state{onrequest=undefined}, Host) ->
- dispatch(Req, State, Host, cowboy_req:get(path, Req));
-onrequest(Req, State=#state{onrequest=OnRequest}, Host) ->
+-spec onrequest(cowboy_req:req(), #state{}) -> ok.
+onrequest(Req, State=#state{onrequest=undefined}) ->
+ execute(Req, State);
+onrequest(Req, State=#state{onrequest=OnRequest}) ->
Req2 = OnRequest(Req),
case cowboy_req:get(resp_state, Req2) of
- waiting -> dispatch(Req2, State, Host, cowboy_req:get(path, Req2));
+ waiting -> execute(Req2, State);
_ -> next_request(Req2, State, ok)
end.
--spec dispatch(cowboy_req:req(), #state{}, binary(), binary()) -> ok.
-dispatch(Req, State=#state{dispatch=Dispatch}, Host, Path) ->
- case cowboy_dispatcher:match(Dispatch, Host, Path) of
- {ok, Handler, Opts, Bindings, HostInfo, PathInfo} ->
- Req2 = cowboy_req:set_bindings(HostInfo, PathInfo, Bindings, Req),
- handler_init(Req2, State, Handler, Opts);
- {error, notfound, host} ->
- error_terminate(400, Req, State);
- {error, badrequest, path} ->
- error_terminate(400, Req, State);
- {error, notfound, path} ->
- error_terminate(404, Req, State)
- end.
+-spec execute(cowboy_req:req(), #state{}) -> ok.
+execute(Req, State=#state{middlewares=Middlewares, env=Env}) ->
+ execute(Req, State, Env, Middlewares).
--spec handler_init(cowboy_req:req(), #state{}, module(), any()) -> ok.
-handler_init(Req, State=#state{transport=Transport}, Handler, Opts) ->
- try Handler:init({Transport:name(), http}, Req, Opts) of
- {ok, Req2, HandlerState} ->
- handler_handle(Req2, State, Handler, HandlerState);
- {loop, Req2, HandlerState} ->
- handler_before_loop(Req2, State#state{hibernate=false},
- Handler, HandlerState);
- {loop, Req2, HandlerState, hibernate} ->
- handler_before_loop(Req2, State#state{hibernate=true},
- Handler, HandlerState);
- {loop, Req2, HandlerState, Timeout} ->
- handler_before_loop(Req2, State#state{loop_timeout=Timeout},
- Handler, HandlerState);
- {loop, Req2, HandlerState, Timeout, hibernate} ->
- handler_before_loop(Req2, State#state{
- hibernate=true, loop_timeout=Timeout}, Handler, HandlerState);
- {shutdown, Req2, HandlerState} ->
- handler_terminate(Req2, Handler, HandlerState);
- %% @todo {upgrade, transport, Module}
- {upgrade, protocol, Module} ->
- upgrade_protocol(Req, State, Handler, Opts, Module);
- {upgrade, protocol, Module, Req2, Opts2} ->
- upgrade_protocol(Req2, State, Handler, Opts2, Module)
- catch Class:Reason ->
- error_terminate(500, Req, State),
- error_logger:error_msg(
- "** Cowboy handler ~p terminating in ~p/~p~n"
- " for the reason ~p:~p~n"
- "** Options were ~p~n"
- "** Request was ~p~n"
- "** Stacktrace: ~p~n~n",
- [Handler, init, 3, Class, Reason, Opts,
- cowboy_req:to_list(Req), erlang:get_stacktrace()])
- end.
-
--spec upgrade_protocol(cowboy_req:req(), #state{}, module(), any(), module())
+-spec execute(cowboy_req:req(), #state{}, cowboy_middleware:env(), [module()])
-> ok.
-upgrade_protocol(Req, State=#state{listener=ListenerPid},
- Handler, Opts, Module) ->
- case Module:upgrade(ListenerPid, Handler, Opts, Req) of
- {UpgradeRes, Req2} -> next_request(Req2, State, UpgradeRes);
- _Any -> terminate(State)
+execute(Req, State, Env, []) ->
+ next_request(Req, State, get_value(result, Env, ok));
+execute(Req, State, Env, [Middleware|Tail]) ->
+ case Middleware:execute(Req, Env) of
+ {ok, Req2, Env2} ->
+ execute(Req2, State, Env2, Tail);
+ {suspend, Module, Function, Args} ->
+ erlang:hibernate(?MODULE, resume,
+ [State, Env, Tail, Module, Function, Args]);
+ {halt, Req2} ->
+ next_request(Req2, State, ok);
+ {error, Code, Req2} ->
+ error_terminate(Code, Req2, State)
end.
--spec handler_handle(cowboy_req:req(), #state{}, module(), any()) -> ok.
-handler_handle(Req, State, Handler, HandlerState) ->
- try Handler:handle(Req, HandlerState) of
- {ok, Req2, HandlerState2} ->
- terminate_request(Req2, State, Handler, HandlerState2)
- catch Class:Reason ->
- error_logger:error_msg(
- "** Cowboy handler ~p terminating in ~p/~p~n"
- " for the reason ~p:~p~n"
- "** Handler state was ~p~n"
- "** Request was ~p~n"
- "** Stacktrace: ~p~n~n",
- [Handler, handle, 2, Class, Reason, HandlerState,
- cowboy_req:to_list(Req), erlang:get_stacktrace()]),
- handler_terminate(Req, Handler, HandlerState),
- error_terminate(500, Req, State)
+-spec resume(#state{}, cowboy_middleware:env(), [module()],
+ module(), module(), [any()]) -> ok.
+resume(State, Env, Tail, Module, Function, Args) ->
+ case apply(Module, Function, Args) of
+ {ok, Req2, Env2} ->
+ execute(Req2, State, Env2, Tail);
+ {suspend, Module2, Function2, Args2} ->
+ erlang:hibernate(?MODULE, resume,
+ [State, Env, Tail, Module2, Function2, Args2]);
+ {halt, Req2} ->
+ next_request(Req2, State, ok);
+ {error, Code, Req2} ->
+ error_terminate(Code, Req2, State)
end.
-%% We don't listen for Transport closes because that would force us
-%% to receive data and buffer it indefinitely.
--spec handler_before_loop(cowboy_req:req(), #state{}, module(), any()) -> ok.
-handler_before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) ->
- State2 = handler_loop_timeout(State),
- catch erlang:hibernate(?MODULE, handler_loop,
- [Req, State2#state{hibernate=false}, Handler, HandlerState]),
- ok;
-handler_before_loop(Req, State, Handler, HandlerState) ->
- State2 = handler_loop_timeout(State),
- handler_loop(Req, State2, Handler, HandlerState).
-
-%% Almost the same code can be found in cowboy_websocket.
--spec handler_loop_timeout(#state{}) -> #state{}.
-handler_loop_timeout(State=#state{loop_timeout=infinity}) ->
- State#state{loop_timeout_ref=undefined};
-handler_loop_timeout(State=#state{loop_timeout=Timeout,
- loop_timeout_ref=PrevRef}) ->
- _ = case PrevRef of undefined -> ignore; PrevRef ->
- erlang:cancel_timer(PrevRef) end,
- TRef = erlang:start_timer(Timeout, self(), ?MODULE),
- State#state{loop_timeout_ref=TRef}.
-
-%% @private
--spec handler_loop(cowboy_req:req(), #state{}, module(), any()) -> ok.
-handler_loop(Req, State=#state{loop_timeout_ref=TRef}, Handler, HandlerState) ->
- receive
- {timeout, TRef, ?MODULE} ->
- terminate_request(Req, State, Handler, HandlerState);
- {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
- handler_loop(Req, State, Handler, HandlerState);
- Message ->
- handler_call(Req, State, Handler, HandlerState, Message)
- end.
-
--spec handler_call(cowboy_req:req(), #state{}, module(), any(), any()) -> ok.
-handler_call(Req, State, Handler, HandlerState, Message) ->
- try Handler:info(Message, Req, HandlerState) of
- {ok, Req2, HandlerState2} ->
- terminate_request(Req2, State, Handler, HandlerState2);
- {loop, Req2, HandlerState2} ->
- handler_before_loop(Req2, State, Handler, HandlerState2);
- {loop, Req2, HandlerState2, hibernate} ->
- handler_before_loop(Req2, State#state{hibernate=true},
- Handler, HandlerState2)
- catch Class:Reason ->
- error_logger:error_msg(
- "** Cowboy handler ~p terminating in ~p/~p~n"
- " for the reason ~p:~p~n"
- "** Handler state was ~p~n"
- "** Request was ~p~n"
- "** Stacktrace: ~p~n~n",
- [Handler, info, 3, Class, Reason, HandlerState,
- cowboy_req:to_list(Req), erlang:get_stacktrace()]),
- handler_terminate(Req, Handler, HandlerState),
- error_terminate(500, Req, State)
- end.
-
--spec handler_terminate(cowboy_req:req(), module(), any()) -> ok.
-handler_terminate(Req, Handler, HandlerState) ->
- try
- Handler:terminate(cowboy_req:lock(Req), HandlerState)
- catch Class:Reason ->
- error_logger:error_msg(
- "** Cowboy handler ~p terminating in ~p/~p~n"
- " for the reason ~p:~p~n"
- "** Handler state was ~p~n"
- "** Request was ~p~n"
- "** Stacktrace: ~p~n~n",
- [Handler, terminate, 2, Class, Reason, HandlerState,
- cowboy_req:to_list(Req), erlang:get_stacktrace()])
- end.
-
--spec terminate_request(cowboy_req:req(), #state{}, module(), any()) -> ok.
-terminate_request(Req, State, Handler, HandlerState) ->
- HandlerRes = handler_terminate(Req, Handler, HandlerState),
- next_request(Req, State, HandlerRes).
-
-spec next_request(cowboy_req:req(), #state{}, any()) -> ok.
-next_request(Req, State=#state{req_keepalive=Keepalive}, HandlerRes) ->
+next_request(Req, State=#state{req_keepalive=Keepalive, timeout=Timeout},
+ HandlerRes) ->
cowboy_req:ensure_response(Req, 204),
- {BodyRes, [Buffer, Connection]} = case cowboy_req:skip_body(Req) of
- {ok, Req2} -> {ok, cowboy_req:get([buffer, connection], Req2)};
- {error, _} -> {close, [<<>>, close]}
- end,
- %% Flush the resp_sent message before moving on.
- receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
- case {HandlerRes, BodyRes, Connection} of
- {ok, ok, keepalive} ->
- ?MODULE:parse_request(Buffer, State#state{
- req_keepalive=Keepalive + 1}, 0);
- _Closed ->
- terminate(State)
+ %% If we are going to close the connection,
+ %% we do not want to attempt to skip the body.
+ case cowboy_req:get(connection, Req) of
+ close ->
+ terminate(State);
+ _ ->
+ Buffer = case cowboy_req:skip_body(Req) of
+ {ok, Req2} -> cowboy_req:get(buffer, Req2);
+ _ -> close
+ end,
+ %% Flush the resp_sent message before moving on.
+ receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
+ if HandlerRes =:= ok, Buffer =/= close ->
+ ?MODULE:parse_request(Buffer,
+ State#state{req_keepalive=Keepalive + 1,
+ until=until(Timeout)}, 0);
+ true ->
+ terminate(State)
+ end
end.
%% Only send an error reply if there is no resp_sent message.
@@ -644,13 +557,13 @@ error_terminate(Code, Req, State) ->
%% Only send an error reply if there is no resp_sent message.
-spec error_terminate(cowboy_http:status(), #state{}) -> ok.
error_terminate(Code, State=#state{socket=Socket, transport=Transport,
- onresponse=OnResponse}) ->
+ compress=Compress, onresponse=OnResponse}) ->
receive
{cowboy_req, resp_sent} -> ok
after 0 ->
_ = cowboy_req:reply(Code, cowboy_req:new(Socket, Transport,
<<"GET">>, <<>>, <<>>, <<>>, {1, 1}, [], <<>>, undefined,
- <<>>, false, OnResponse)),
+ <<>>, false, Compress, OnResponse)),
ok
end,
terminate(State).
diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl
index 4a9e1a7..5cb7aa3 100644
--- a/src/cowboy_req.erl
+++ b/src/cowboy_req.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
@@ -42,7 +42,7 @@
-module(cowboy_req).
%% Request API.
--export([new/13]).
+-export([new/14]).
-export([method/1]).
-export([version/1]).
-export([peer/1]).
@@ -89,6 +89,7 @@
-export([set_resp_cookie/4]).
-export([set_resp_header/3]).
-export([set_resp_body/2]).
+-export([set_resp_body_fun/2]).
-export([set_resp_body_fun/3]).
-export([has_resp_header/2]).
-export([has_resp_body/1]).
@@ -111,7 +112,6 @@
-export([compact/1]).
-export([lock/1]).
-export([to_list/1]).
--export([transport/1]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@@ -123,7 +123,7 @@
-type cookie_opts() :: [cookie_option()].
-export_type([cookie_opts/0]).
--type resp_body_fun() :: fun(() -> {sent, non_neg_integer()}).
+-type resp_body_fun() :: fun((inet:socket(), module()) -> ok).
-record(http_req, {
%% Transport.
@@ -137,14 +137,14 @@
version = {1, 1} :: cowboy_http:version(),
peer = undefined :: undefined | {inet:ip_address(), inet:port_number()},
host = undefined :: undefined | binary(),
- host_info = undefined :: undefined | cowboy_dispatcher:tokens(),
+ host_info = undefined :: undefined | cowboy_router:tokens(),
port = undefined :: undefined | inet:port_number(),
path = undefined :: binary(),
- path_info = undefined :: undefined | cowboy_dispatcher:tokens(),
+ path_info = undefined :: undefined | cowboy_router:tokens(),
qs = undefined :: binary(),
qs_vals = undefined :: undefined | list({binary(), binary() | true}),
fragment = undefined :: binary(),
- bindings = undefined :: undefined | cowboy_dispatcher:bindings(),
+ bindings = undefined :: undefined | cowboy_router:bindings(),
headers = [] :: cowboy_http:headers(),
p_headers = [] :: [any()], %% @todo Improve those specs.
cookies = undefined :: undefined | [{binary(), binary()}],
@@ -156,12 +156,15 @@
buffer = <<>> :: binary(),
%% Response.
+ resp_compress = false :: boolean(),
resp_state = waiting :: locked | waiting | chunks | done,
resp_headers = [] :: cowboy_http:headers(),
- resp_body = <<>> :: iodata() | {non_neg_integer(), resp_body_fun()},
+ resp_body = <<>> :: iodata() | resp_body_fun()
+ | {non_neg_integer(), resp_body_fun()},
%% Functions.
- onresponse = undefined :: undefined | cowboy_protocol:onresponse_fun()
+ onresponse = undefined :: undefined | already_called
+ | cowboy_protocol:onresponse_fun()
}).
-opaque req() :: #http_req{}.
@@ -178,16 +181,16 @@
%% in an optimized way and add the parsed value to p_headers' cache.
-spec new(inet:socket(), module(), binary(), binary(), binary(), binary(),
cowboy_http:version(), cowboy_http:headers(), binary(),
- inet:port_number() | undefined, binary(), boolean(),
+ inet:port_number() | undefined, binary(), boolean(), boolean(),
undefined | cowboy_protocol:onresponse_fun())
-> req().
new(Socket, Transport, Method, Path, Query, Fragment,
Version, Headers, Host, Port, Buffer, CanKeepalive,
- OnResponse) ->
+ Compress, OnResponse) ->
Req = #http_req{socket=Socket, transport=Transport, pid=self(),
method=Method, path=Path, qs=Query, fragment=Fragment, version=Version,
headers=Headers, host=Host, port=Port, buffer=Buffer,
- onresponse=OnResponse},
+ resp_compress=Compress, onresponse=OnResponse},
case CanKeepalive and (Version =:= {1, 1}) of
false ->
Req#http_req{connection=close};
@@ -253,7 +256,7 @@ host(Req) ->
%% @doc Return the extra host information obtained from partially matching
%% the hostname using <em>'...'</em>.
-spec host_info(Req)
- -> {cowboy_dispatcher:tokens() | undefined, Req} when Req::req().
+ -> {cowboy_router:tokens() | undefined, Req} when Req::req().
host_info(Req) ->
{Req#http_req.host_info, Req}.
@@ -270,7 +273,7 @@ path(Req) ->
%% @doc Return the extra path information obtained from partially matching
%% the patch using <em>'...'</em>.
-spec path_info(Req)
- -> {cowboy_dispatcher:tokens() | undefined, Req} when Req::req().
+ -> {cowboy_router:tokens() | undefined, Req} when Req::req().
path_info(Req) ->
{Req#http_req.path_info, Req}.
@@ -438,6 +441,11 @@ parse_header(Name, Req, Default) when Name =:= <<"accept-language">> ->
fun (Value) ->
cowboy_http:nonempty_list(Value, fun cowboy_http:language_range/2)
end);
+parse_header(Name, Req, Default) when Name =:= <<"authorization">> ->
+ parse_header(Name, Req, Default,
+ fun (Value) ->
+ cowboy_http:token_ci(Value, fun cowboy_http:authorization/2)
+ end);
parse_header(Name, Req, Default) when Name =:= <<"content-length">> ->
parse_header(Name, Req, Default, fun cowboy_http:digits/1);
parse_header(Name, Req, Default) when Name =:= <<"content-type">> ->
@@ -456,6 +464,11 @@ parse_header(Name, Req, Default)
when Name =:= <<"if-modified-since">>;
Name =:= <<"if-unmodified-since">> ->
parse_header(Name, Req, Default, fun cowboy_http:http_date/1);
+parse_header(Name, Req, Default) when Name =:= <<"sec-websocket-protocol">> ->
+ parse_header(Name, Req, Default,
+ fun (Value) ->
+ cowboy_http:nonempty_list(Value, fun cowboy_http:token/2)
+ end);
%% @todo Extension parameters.
parse_header(Name, Req, Default) when Name =:= <<"transfer-encoding">> ->
parse_header(Name, Req, Default,
@@ -548,11 +561,10 @@ set_meta(Name, Value, Req=#http_req{meta=Meta}) ->
%% Request Body API.
%% @doc Return whether the request message has a body.
--spec has_body(Req) -> {boolean(), Req} when Req::req().
+-spec has_body(cowboy_req:req()) -> boolean().
has_body(Req) ->
- Has = lists:keymember(<<"content-length">>, 1, Req#http_req.headers) orelse
- lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers),
- {Has, Req}.
+ lists:keymember(<<"content-length">>, 1, Req#http_req.headers) orelse
+ lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers).
%% @doc Return the request message body length, if known.
%%
@@ -632,17 +644,18 @@ stream_body(Req=#http_req{buffer=Buffer, body_state={stream, _, _, _}})
when Buffer =/= <<>> ->
transfer_decode(Buffer, Req#http_req{buffer= <<>>});
stream_body(Req=#http_req{body_state={stream, _, _, _}}) ->
- stream_body_recv(Req);
+ stream_body_recv(0, Req);
stream_body(Req=#http_req{body_state=done}) ->
{done, Req}.
--spec stream_body_recv(Req)
+-spec stream_body_recv(non_neg_integer(), Req)
-> {ok, binary(), Req} | {error, atom()} when Req::req().
-stream_body_recv(Req=#http_req{
+stream_body_recv(Length, Req=#http_req{
transport=Transport, socket=Socket, buffer=Buffer}) ->
%% @todo Allow configuring the timeout.
- case Transport:recv(Socket, 0, 5000) of
- {ok, Data} -> transfer_decode(<< Buffer/binary, Data/binary >>, Req);
+ case Transport:recv(Socket, Length, 5000) of
+ {ok, Data} -> transfer_decode(<< Buffer/binary, Data/binary >>,
+ Req#http_req{buffer= <<>>});
{error, Reason} -> {error, Reason}
end.
@@ -660,7 +673,10 @@ transfer_decode(Data, Req=#http_req{
{stream, TransferDecode, TransferState2, ContentDecode}});
%% @todo {header(s) for chunked
more ->
- stream_body_recv(Req#http_req{buffer=Data});
+ stream_body_recv(0, Req#http_req{buffer=Data});
+ {more, Length, Rest, TransferState2} ->
+ stream_body_recv(Length, Req#http_req{buffer=Rest, body_state=
+ {stream, TransferDecode, TransferState2, ContentDecode}});
{done, Length, Rest} ->
Req2 = transfer_decode_done(Length, Rest, Req),
{done, Req2};
@@ -721,7 +737,6 @@ skip_body(Req) ->
%% @doc Return the full body sent with the request, parsed as an
%% application/x-www-form-urlencoded string. Essentially a POST query string.
-%% @todo We need an option to limit the size of the body for QS too.
-spec body_qs(Req)
-> {ok, [{binary(), binary() | true}], Req} | {error, atom()}
when Req::req().
@@ -758,7 +773,6 @@ multipart_data(Req=#http_req{multipart={Length, Cont}}) ->
multipart_data(Req=#http_req{body_state=done}) ->
{eof, Req}.
-%% @todo Typespecs.
multipart_data(Req, Length, {headers, Headers, Cont}) ->
{headers, Headers, Req#http_req{multipart={Length, Cont}}};
multipart_data(Req, Length, {body, Data, Cont}) ->
@@ -822,20 +836,33 @@ set_resp_header(Name, Value, Req=#http_req{resp_headers=RespHeaders}) ->
set_resp_body(Body, Req) ->
Req#http_req{resp_body=Body}.
+%% @doc Add a body stream function to the response.
+%%
+%% The body set here is ignored if the response is later sent using
+%% anything other than reply/2 or reply/3.
+%%
+%% Setting a response stream function without a length means that the
+%% body will be sent until the connection is closed. Cowboy will make
+%% sure that the connection is closed with no extra step required.
+%%
+%% To inform the client that a body has been sent with this request,
+%% Cowboy will add a "Transfer-Encoding: identity" header to the
+%% response.
+-spec set_resp_body_fun(resp_body_fun(), Req) -> Req when Req::req().
+set_resp_body_fun(StreamFun, Req) ->
+ Req#http_req{resp_body=StreamFun}.
+
%% @doc Add a body function to the response.
%%
-%% The response body may also be set to a content-length - stream-function pair.
-%% If the response body is of this type normal response headers will be sent.
-%% After the response headers has been sent the body function is applied.
-%% The body function is expected to write the response body directly to the
-%% socket using the transport module.
+%% The body set here is ignored if the response is later sent using
+%% anything other than reply/2 or reply/3.
%%
-%% If the body function crashes while writing the response body or writes fewer
-%% bytes than declared the behaviour is undefined. The body set here is ignored
-%% if the response is later sent using anything other than `reply/2' or
-%% `reply/3'.
+%% Cowboy will call the given response stream function after sending the
+%% headers. This function must send the specified number of bytes to the
+%% socket it will receive as argument.
%%
-%% @see cowboy_req:transport/1.
+%% If the body function crashes while writing the response body or writes
+%% fewer bytes than declared the behaviour is undefined.
-spec set_resp_body_fun(non_neg_integer(), resp_body_fun(), Req)
-> Req when Req::req().
set_resp_body_fun(StreamLen, StreamFun, Req) ->
@@ -848,6 +875,8 @@ has_resp_header(Name, #http_req{resp_headers=RespHeaders}) ->
%% @doc Return whether a body has been set for the response.
-spec has_resp_body(req()) -> boolean().
+has_resp_body(#http_req{resp_body=RespBody}) when is_function(RespBody) ->
+ true;
has_resp_body(#http_req{resp_body={Length, _}}) ->
Length > 0;
has_resp_body(#http_req{resp_body=RespBody}) ->
@@ -876,35 +905,93 @@ reply(Status, Headers, Req=#http_req{resp_body=Body}) ->
iodata() | {non_neg_integer() | resp_body_fun()}, Req)
-> {ok, Req} when Req::req().
reply(Status, Headers, Body, Req=#http_req{
+ socket=Socket, transport=Transport,
version=Version, connection=Connection,
- method=Method, resp_state=waiting, resp_headers=RespHeaders}) ->
+ method=Method, resp_compress=Compress,
+ resp_state=waiting, resp_headers=RespHeaders}) ->
RespConn = response_connection(Headers, Connection),
HTTP11Headers = case Version of
{1, 1} -> [{<<"connection">>, atom_to_connection(Connection)}];
_ -> []
end,
case Body of
+ BodyFun when is_function(BodyFun) ->
+ %% We stream the response body until we close the connection.
+ {RespType, Req2} = response(Status, Headers, RespHeaders, [
+ {<<"connection">>, <<"close">>},
+ {<<"date">>, cowboy_clock:rfc1123()},
+ {<<"server">>, <<"Cowboy">>},
+ {<<"transfer-encoding">>, <<"identity">>}
+ ], <<>>, Req#http_req{connection=close}),
+ if RespType =/= hook, Method =/= <<"HEAD">> ->
+ BodyFun(Socket, Transport);
+ true -> ok
+ end;
{ContentLength, BodyFun} ->
+ %% We stream the response body for ContentLength bytes.
{RespType, Req2} = response(Status, Headers, RespHeaders, [
{<<"content-length">>, integer_to_list(ContentLength)},
{<<"date">>, cowboy_clock:rfc1123()},
{<<"server">>, <<"Cowboy">>}
|HTTP11Headers], <<>>, Req),
- if RespType =/= hook, Method =/= <<"HEAD">> -> BodyFun();
+ if RespType =/= hook, Method =/= <<"HEAD">> ->
+ BodyFun(Socket, Transport);
true -> ok
end;
+ _ when Compress ->
+ Req2 = reply_may_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method);
_ ->
- {_, Req2} = response(Status, Headers, RespHeaders, [
- {<<"content-length">>, integer_to_list(iolist_size(Body))},
- {<<"date">>, cowboy_clock:rfc1123()},
- {<<"server">>, <<"Cowboy">>}
- |HTTP11Headers],
- case Method of <<"HEAD">> -> <<>>; _ -> Body end,
- Req)
+ Req2 = reply_no_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method, iolist_size(Body))
end,
{ok, Req2#http_req{connection=RespConn, resp_state=done,
resp_headers=[], resp_body= <<>>}}.
+reply_may_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method) ->
+ BodySize = iolist_size(Body),
+ {ok, Encodings, Req2}
+ = cowboy_req:parse_header(<<"accept-encoding">>, Req),
+ CanGzip = (BodySize > 300)
+ andalso (false =:= lists:keyfind(<<"content-encoding">>,
+ 1, Headers))
+ andalso (false =:= lists:keyfind(<<"content-encoding">>,
+ 1, RespHeaders))
+ andalso (false =:= lists:keyfind(<<"transfer-encoding">>,
+ 1, Headers))
+ andalso (false =:= lists:keyfind(<<"transfer-encoding">>,
+ 1, RespHeaders))
+ andalso (Encodings =/= undefined)
+ andalso (false =/= lists:keyfind(<<"gzip">>, 1, Encodings)),
+ case CanGzip of
+ true ->
+ GzBody = zlib:gzip(Body),
+ {_, Req3} = response(Status, Headers, RespHeaders, [
+ {<<"content-length">>, integer_to_list(byte_size(GzBody))},
+ {<<"content-encoding">>, <<"gzip">>},
+ {<<"date">>, cowboy_clock:rfc1123()},
+ {<<"server">>, <<"Cowboy">>}
+ |HTTP11Headers],
+ case Method of <<"HEAD">> -> <<>>; _ -> GzBody end,
+ Req2),
+ Req3;
+ false ->
+ reply_no_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method, BodySize)
+ end.
+
+reply_no_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method, BodySize) ->
+ {_, Req2} = response(Status, Headers, RespHeaders, [
+ {<<"content-length">>, integer_to_list(BodySize)},
+ {<<"date">>, cowboy_clock:rfc1123()},
+ {<<"server">>, <<"Cowboy">>}
+ |HTTP11Headers],
+ case Method of <<"HEAD">> -> <<>>; _ -> Body end,
+ Req),
+ Req2.
+
%% @equiv chunked_reply(Status, [], Req)
-spec chunked_reply(cowboy_http:status(), Req) -> {ok, Req} when Req::req().
chunked_reply(Status, Req) ->
@@ -1044,8 +1131,8 @@ set([{transport, Val}|Tail], Req) -> set(Tail, Req#http_req{transport=Val});
set([{version, Val}|Tail], Req) -> set(Tail, Req#http_req{version=Val}).
%% @private
--spec set_bindings(cowboy_dispatcher:tokens(), cowboy_dispatcher:tokens(),
- cowboy_dispatcher:bindings(), Req) -> Req when Req::req().
+-spec set_bindings(cowboy_router:tokens(), cowboy_router:tokens(),
+ cowboy_router:bindings(), Req) -> Req when Req::req().
set_bindings(HostInfo, PathInfo, Bindings, Req) ->
Req#http_req{host_info=HostInfo, path_info=PathInfo,
bindings=Bindings}.
@@ -1077,18 +1164,6 @@ lock(Req) ->
to_list(Req) ->
lists:zip(record_info(fields, http_req), tl(tuple_to_list(Req))).
-%% @doc Return the transport module and socket associated with a request.
-%%
-%% This exposes the same socket interface used internally by the HTTP protocol
-%% implementation to developers that needs low level access to the socket.
-%%
-%% It is preferred to use this in conjuction with the stream function support
-%% in `set_resp_body_fun/3' if this is used to write a response body directly
-%% to the socket. This ensures that the response headers are set correctly.
--spec transport(req()) -> {ok, module(), inet:socket()}.
-transport(#http_req{transport=Transport, socket=Socket}) ->
- {ok, Transport, Socket}.
-
%% Internal.
-spec response(cowboy_http:status(), cowboy_http:headers(),
@@ -1097,13 +1172,17 @@ transport(#http_req{transport=Transport, socket=Socket}) ->
response(Status, Headers, RespHeaders, DefaultHeaders, Body, Req=#http_req{
socket=Socket, transport=Transport, version=Version,
pid=ReqPid, onresponse=OnResponse}) ->
- FullHeaders = response_merge_headers(Headers, RespHeaders, DefaultHeaders),
+ FullHeaders = case OnResponse of
+ already_called -> Headers;
+ _ -> response_merge_headers(Headers, RespHeaders, DefaultHeaders)
+ end,
Req2 = case OnResponse of
+ already_called -> Req;
undefined -> Req;
OnResponse -> OnResponse(Status, FullHeaders, Body,
%% Don't call 'onresponse' from the hook itself.
Req#http_req{resp_headers=[], resp_body= <<>>,
- onresponse=undefined})
+ onresponse=already_called})
end,
ReplyType = case Req2#http_req.resp_state of
waiting ->
diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl
index 06655a4..a49d622 100644
--- a/src/cowboy_rest.erl
+++ b/src/cowboy_rest.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -23,6 +23,7 @@
-export([upgrade/4]).
-record(state, {
+ env :: cowboy_middleware:env(),
method = undefined :: binary(),
%% Handler.
@@ -54,31 +55,31 @@
%% You do not need to call this function manually. To upgrade to the REST
%% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em>
%% in your <em>cowboy_http_handler:init/3</em> handler function.
--spec upgrade(pid(), module(), any(), Req)
- -> {ok, Req} | close when Req::cowboy_req:req().
-upgrade(_ListenerPid, Handler, Opts, Req) ->
+-spec upgrade(Req, Env, module(), any())
+ -> {ok, Req, Env} | {error, 500, Req}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+upgrade(Req, Env, Handler, HandlerOpts) ->
try
Method = cowboy_req:get(method, Req),
case erlang:function_exported(Handler, rest_init, 2) of
true ->
- case Handler:rest_init(Req, Opts) of
+ case Handler:rest_init(Req, HandlerOpts) of
{ok, Req2, HandlerState} ->
- service_available(Req2, #state{method=Method,
+ service_available(Req2, #state{env=Env, method=Method,
handler=Handler, handler_state=HandlerState})
end;
false ->
- service_available(Req, #state{method=Method,
+ service_available(Req, #state{env=Env, method=Method,
handler=Handler})
end
catch Class:Reason ->
- PLReq = cowboy_req:to_list(Req),
error_logger:error_msg(
"** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n** Options were ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, rest_init, 2, Class, Reason, Opts, PLReq, erlang:get_stacktrace()]),
- {ok, _Req2} = cowboy_req:reply(500, Req),
- close
+ [Handler, rest_init, 2, Class, Reason, HandlerOpts,
+ cowboy_req:to_list(Req), erlang:get_stacktrace()]),
+ {error, 500, Req}
end.
service_available(Req, State) ->
@@ -90,7 +91,8 @@ known_methods(Req, State=#state{method=Method}) ->
no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">>;
Method =:= <<"POST">>; Method =:= <<"PUT">>;
Method =:= <<"DELETE">>; Method =:= <<"TRACE">>;
- Method =:= <<"CONNECT">>; Method =:= <<"OPTIONS">> ->
+ Method =:= <<"CONNECT">>; Method =:= <<"OPTIONS">>;
+ Method =:= <<"PATCH">> ->
next(Req, State, fun uri_too_long/2);
no_call ->
next(Req, State, 501);
@@ -643,6 +645,8 @@ method(Req, State=#state{method= <<"POST">>}) ->
post_is_create(Req, State);
method(Req, State=#state{method= <<"PUT">>}) ->
is_conflict(Req, State);
+method(Req, State=#state{method= <<"PATCH">>}) ->
+ patch_resource(Req, State);
method(Req, State=#state{method=Method})
when Method =:= <<"GET">>; Method =:= <<"HEAD">> ->
set_resp_body(Req, State);
@@ -666,6 +670,8 @@ post_is_create(Req, State) ->
%% (including the leading /).
create_path(Req, State) ->
case call(Req, State, create_path) of
+ no_call ->
+ put_resource(Req, State, fun created_path/2);
{halt, Req2, HandlerState} ->
terminate(Req2, State#state{handler_state=HandlerState});
{Path, Req2, HandlerState} ->
@@ -677,6 +683,23 @@ create_path(Req, State) ->
State2, 303)
end.
+%% Called after content_types_accepted is called for POST methods
+%% when create_path did not exist. Expects the full path to
+%% be returned and MUST exist in the case that create_path
+%% does not.
+created_path(Req, State) ->
+ case call(Req, State, created_path) of
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {Path, Req2, HandlerState} ->
+ {HostURL, Req3} = cowboy_req:host_url(Req2),
+ State2 = State#state{handler_state=HandlerState},
+ Req4 = cowboy_req:set_resp_header(
+ <<"Location">>, << HostURL/binary, Path/binary >>, Req3),
+ respond(cowboy_req:set_meta(put_path, Path, Req4),
+ State2, 303)
+ end.
+
%% process_post should return true when the POST body could be processed
%% and false when it hasn't, in which case a 500 error is sent.
process_post(Req, State) ->
@@ -707,6 +730,9 @@ put_resource(Req, State) ->
%% may be different from the request path, and is stored as request metadata.
%% It is always defined past this point. It can be retrieved as demonstrated:
%% {PutPath, Req2} = cowboy_req:meta(put_path, Req)
+%%
+%%content_types_accepted SHOULD return a different list
+%% for each HTTP method.
put_resource(Req, State, OnTrue) ->
case call(Req, State, content_types_accepted) of
no_call ->
@@ -721,6 +747,27 @@ put_resource(Req, State, OnTrue) ->
choose_content_type(Req3, State2, OnTrue, ContentType, CTA2)
end.
+%% content_types_accepted should return a list of media types and their
+%% associated callback functions in the same format as content_types_provided.
+%%
+%% The callback will then be called and is expected to process the content
+%% pushed to the resource in the request body.
+%%
+%% content_types_accepted SHOULD return a different list
+%% for each HTTP method.
+patch_resource(Req, State) ->
+ case call(Req, State, content_types_accepted) of
+ no_call ->
+ respond(Req, State, 415);
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {CTM, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ {ok, ContentType, Req3}
+ = cowboy_req:parse_header(<<"content-type">>, Req2),
+ choose_content_type(Req3, State2, 204, ContentType, CTM)
+ end.
+
%% The special content type '*' will always match. It can be used as a
%% catch-all content type for accepting any kind of request content.
%% Note that because it will always match, it should be the last of the
@@ -738,15 +785,14 @@ choose_content_type(Req,
"function ~p/~p was not exported~n"
"** Request was ~p~n** State was ~p~n~n",
[Handler, Fun, 2, cowboy_req:to_list(Req), HandlerState]),
- {ok, _} = cowboy_req:reply(500, Req),
- close;
- {halt, Req2, HandlerState} ->
- terminate(Req2, State#state{handler_state=HandlerState});
- {true, Req2, HandlerState} ->
- State2 = State#state{handler_state=HandlerState},
+ {error, 500, Req};
+ {halt, Req2, HandlerState2} ->
+ terminate(Req2, State#state{handler_state=HandlerState2});
+ {true, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
next(Req2, State2, OnTrue);
- {false, Req2, HandlerState} ->
- State2 = State#state{handler_state=HandlerState},
+ {false, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
respond(Req2, State2, 422)
end;
choose_content_type(Req, State, OnTrue, ContentType, [_Any|Tail]) ->
@@ -790,15 +836,16 @@ set_resp_body(Req, State=#state{handler=Handler, handler_state=HandlerState,
"function ~p/~p was not exported~n"
"** Request was ~p~n** State was ~p~n~n",
[Handler, Fun, 2, cowboy_req:to_list(Req5), HandlerState]),
- {ok, _} = cowboy_req:reply(500, Req5),
- close;
- {halt, Req6, HandlerState} ->
- terminate(Req6, State4#state{handler_state=HandlerState});
- {Body, Req6, HandlerState} ->
- State5 = State4#state{handler_state=HandlerState},
+ {error, 500, Req5};
+ {halt, Req6, HandlerState2} ->
+ terminate(Req6, State4#state{handler_state=HandlerState2});
+ {Body, Req6, HandlerState2} ->
+ State5 = State4#state{handler_state=HandlerState2},
Req7 = case Body of
- {stream, Len, Fun1} ->
- cowboy_req:set_resp_body_fun(Len, Fun1, Req6);
+ {stream, StreamFun} ->
+ cowboy_req:set_resp_body_fun(StreamFun, Req6);
+ {stream, Len, StreamFun} ->
+ cowboy_req:set_resp_body_fun(Len, StreamFun, Req6);
_Contents ->
cowboy_req:set_resp_body(Body, Req6)
end,
@@ -845,12 +892,6 @@ generate_etag(Req, State=#state{etag=undefined}) ->
case call(Req, State, generate_etag) of
no_call ->
{undefined, Req, State#state{etag=no_call}};
- %% Previously the return value from the generate_etag/2 callback was set
- %% as the value of the ETag header in the response. Therefore the only
- %% valid return type was `binary()'. If a handler returns a `binary()'
- %% it must be mapped to the expected type or it'll always fail to
- %% compare equal to any entity tags present in the request headers.
- %% @todo Remove support for binary return values after 0.6.
{Etag, Req2, HandlerState} when is_binary(Etag) ->
[Etag2] = cowboy_http:entity_tag_match(Etag),
{Etag2, Req2, State#state{handler_state=HandlerState, etag=Etag2}};
@@ -915,10 +956,11 @@ respond(Req, State, StatusCode) ->
{ok, Req2} = cowboy_req:reply(StatusCode, Req),
terminate(Req2, State).
-terminate(Req, #state{handler=Handler, handler_state=HandlerState}) ->
+terminate(Req, #state{env=Env, handler=Handler,
+ handler_state=HandlerState}) ->
case erlang:function_exported(Handler, rest_terminate, 2) of
true -> ok = Handler:rest_terminate(
cowboy_req:lock(Req), HandlerState);
false -> ok
end,
- {ok, Req}.
+ {ok, Req, [{result, ok}|Env]}.
diff --git a/src/cowboy_router.erl b/src/cowboy_router.erl
new file mode 100644
index 0000000..a4597ed
--- /dev/null
+++ b/src/cowboy_router.erl
@@ -0,0 +1,565 @@
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% @doc Routing middleware.
+%%
+%% Resolve the handler to be used for the request based on the
+%% routing information found in the <em>dispatch</em> environment value.
+%% When found, the handler module and associated data are added to
+%% the environment as the <em>handler</em> and <em>handler_opts</em> values
+%% respectively.
+%%
+%% If the route cannot be found, processing stops with either
+%% a 400 or a 404 reply.
+-module(cowboy_router).
+-behaviour(cowboy_middleware).
+
+-export([compile/1]).
+-export([execute/2]).
+
+-type bindings() :: [{atom(), binary()}].
+-type tokens() :: [binary()].
+-export_type([bindings/0]).
+-export_type([tokens/0]).
+
+-type constraints() :: [{atom(), int}
+ | {atom(), function, fun ((binary()) -> true | {true, any()} | false)}].
+-export_type([constraints/0]).
+
+-type route_match() :: binary() | string().
+-type route_path() :: {Path::route_match(), Handler::module(), Opts::any()}
+ | {Path::route_match(), constraints(), Handler::module(), Opts::any()}.
+-type route_rule() :: {Host::route_match(), Paths::[route_path()]}
+ | {Host::route_match(), constraints(), Paths::[route_path()]}.
+-opaque routes() :: [route_rule()].
+-export_type([routes/0]).
+
+-type dispatch_match() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()].
+-type dispatch_path() :: {dispatch_match(), module(), any()}.
+-type dispatch_rule() :: {Host::dispatch_match(), Paths::[dispatch_path()]}.
+-opaque dispatch_rules() :: [dispatch_rule()].
+-export_type([dispatch_rules/0]).
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-endif.
+
+%% @doc Compile a list of routes into the dispatch format used
+%% by Cowboy's routing.
+-spec compile(routes()) -> dispatch_rules().
+compile(Routes) ->
+ compile(Routes, []).
+
+compile([], Acc) ->
+ lists:reverse(Acc);
+compile([{Host, Paths}|Tail], Acc) ->
+ compile([{Host, [], Paths}|Tail], Acc);
+compile([{HostMatch, Constraints, Paths}|Tail], Acc) ->
+ HostRules = case HostMatch of
+ '_' -> '_';
+ _ -> compile_host(HostMatch)
+ end,
+ PathRules = compile_paths(Paths, []),
+ Hosts = case HostRules of
+ '_' -> [{'_', Constraints, PathRules}];
+ _ -> [{R, Constraints, PathRules} || R <- HostRules]
+ end,
+ compile(Tail, Hosts ++ Acc).
+
+compile_host(HostMatch) when is_list(HostMatch) ->
+ compile_host(unicode:characters_to_binary(HostMatch));
+compile_host(HostMatch) when is_binary(HostMatch) ->
+ compile_rules(HostMatch, $., [], [], <<>>).
+
+compile_paths([], Acc) ->
+ lists:reverse(Acc);
+compile_paths([{PathMatch, Handler, Opts}|Tail], Acc) ->
+ compile_paths([{PathMatch, [], Handler, Opts}|Tail], Acc);
+compile_paths([{PathMatch, Constraints, Handler, Opts}|Tail], Acc)
+ when is_list(PathMatch) ->
+ compile_paths([{unicode:characters_to_binary(PathMatch),
+ Constraints, Handler, Opts}|Tail], Acc);
+compile_paths([{'_', Constraints, Handler, Opts}|Tail], Acc) ->
+ compile_paths(Tail, [{'_', Constraints, Handler, Opts}] ++ Acc);
+compile_paths([{<< $/, PathMatch/binary >>, Constraints, Handler, Opts}|Tail],
+ Acc) ->
+ PathRules = compile_rules(PathMatch, $/, [], [], <<>>),
+ Paths = [{lists:reverse(R), Constraints, Handler, Opts} || R <- PathRules],
+ compile_paths(Tail, Paths ++ Acc).
+
+compile_rules(<<>>, _, Segments, Rules, <<>>) ->
+ [Segments|Rules];
+compile_rules(<<>>, _, Segments, Rules, Acc) ->
+ [[Acc|Segments]|Rules];
+compile_rules(<< S, Rest/binary >>, S, Segments, Rules, <<>>) ->
+ compile_rules(Rest, S, Segments, Rules, <<>>);
+compile_rules(<< S, Rest/binary >>, S, Segments, Rules, Acc) ->
+ compile_rules(Rest, S, [Acc|Segments], Rules, <<>>);
+compile_rules(<< $:, Rest/binary >>, S, Segments, Rules, <<>>) ->
+ {NameBin, Rest2} = compile_binding(Rest, S, <<>>),
+ Name = binary_to_atom(NameBin, utf8),
+ compile_rules(Rest2, S, Segments, Rules, Name);
+compile_rules(<< $:, _/binary >>, _, _, _, _) ->
+ erlang:error(badarg);
+compile_rules(<< $[, $., $., $., $], Rest/binary >>, S, Segments, Rules, Acc)
+ when Acc =:= <<>> ->
+ compile_rules(Rest, S, ['...'|Segments], Rules, Acc);
+compile_rules(<< $[, $., $., $., $], Rest/binary >>, S, Segments, Rules, Acc) ->
+ compile_rules(Rest, S, ['...', Acc|Segments], Rules, Acc);
+compile_rules(<< $[, S, Rest/binary >>, S, Segments, Rules, Acc) ->
+ compile_brackets(Rest, S, [Acc|Segments], Rules);
+compile_rules(<< $[, Rest/binary >>, S, Segments, Rules, <<>>) ->
+ compile_brackets(Rest, S, Segments, Rules);
+%% Open bracket in the middle of a segment.
+compile_rules(<< $[, _/binary >>, _, _, _, _) ->
+ erlang:error(badarg);
+%% Missing an open bracket.
+compile_rules(<< $], _/binary >>, _, _, _, _) ->
+ erlang:error(badarg);
+compile_rules(<< C, Rest/binary >>, S, Segments, Rules, Acc) ->
+ compile_rules(Rest, S, Segments, Rules, << Acc/binary, C >>).
+
+%% Everything past $: until $. or $[ or $] or end of binary
+%% is the binding name.
+compile_binding(<<>>, _, <<>>) ->
+ erlang:error(badarg);
+compile_binding(Rest = <<>>, _, Acc) ->
+ {Acc, Rest};
+compile_binding(Rest = << C, _/binary >>, S, Acc)
+ when C =:= S; C =:= $[; C =:= $] ->
+ {Acc, Rest};
+compile_binding(<< C, Rest/binary >>, S, Acc) ->
+ compile_binding(Rest, S, << Acc/binary, C >>).
+
+compile_brackets(Rest, S, Segments, Rules) ->
+ {Bracket, Rest2} = compile_brackets_split(Rest, <<>>, 0),
+ Rules1 = compile_rules(Rest2, S, Segments, [], <<>>),
+ Rules2 = compile_rules(<< Bracket/binary, Rest2/binary >>,
+ S, Segments, [], <<>>),
+ Rules ++ Rules2 ++ Rules1.
+
+%% Missing a close bracket.
+compile_brackets_split(<<>>, _, _) ->
+ erlang:error(badarg);
+%% Make sure we don't confuse the closing bracket we're looking for.
+compile_brackets_split(<< C, Rest/binary >>, Acc, N) when C =:= $[ ->
+ compile_brackets_split(Rest, << Acc/binary, C >>, N + 1);
+compile_brackets_split(<< C, Rest/binary >>, Acc, N) when C =:= $], N > 0 ->
+ compile_brackets_split(Rest, << Acc/binary, C >>, N - 1);
+%% That's the right one.
+compile_brackets_split(<< $], Rest/binary >>, Acc, 0) ->
+ {Acc, Rest};
+compile_brackets_split(<< C, Rest/binary >>, Acc, N) ->
+ compile_brackets_split(Rest, << Acc/binary, C >>, N).
+
+%% @private
+-spec execute(Req, Env)
+ -> {ok, Req, Env} | {error, 400 | 404, Req}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+execute(Req, Env) ->
+ {_, Dispatch} = lists:keyfind(dispatch, 1, Env),
+ [Host, Path] = cowboy_req:get([host, path], Req),
+ case match(Dispatch, Host, Path) of
+ {ok, Handler, HandlerOpts, Bindings, HostInfo, PathInfo} ->
+ Req2 = cowboy_req:set_bindings(HostInfo, PathInfo, Bindings, Req),
+ {ok, Req2, [{handler, Handler}, {handler_opts, HandlerOpts}|Env]};
+ {error, notfound, host} ->
+ {error, 400, Req};
+ {error, badrequest, path} ->
+ {error, 400, Req};
+ {error, notfound, path} ->
+ {error, 404, Req}
+ end.
+
+%% Internal.
+
+%% @doc Match hostname tokens and path tokens against dispatch rules.
+%%
+%% It is typically used for matching tokens for the hostname and path of
+%% the request against a global dispatch rule for your listener.
+%%
+%% Dispatch rules are a list of <em>{Hostname, PathRules}</em> tuples, with
+%% <em>PathRules</em> being a list of <em>{Path, HandlerMod, HandlerOpts}</em>.
+%%
+%% <em>Hostname</em> and <em>Path</em> are match rules and can be either the
+%% atom <em>'_'</em>, which matches everything, `<<"*">>', which match the
+%% wildcard path, or a list of tokens.
+%%
+%% Each token can be either a binary, the atom <em>'_'</em>,
+%% the atom '...' or a named atom. A binary token must match exactly,
+%% <em>'_'</em> matches everything for a single token, <em>'...'</em> matches
+%% everything for the rest of the tokens and a named atom will bind the
+%% corresponding token value and return it.
+%%
+%% The list of hostname tokens is reversed before matching. For example, if
+%% we were to match "www.ninenines.eu", we would first match "eu", then
+%% "ninenines", then "www". This means that in the context of hostnames,
+%% the <em>'...'</em> atom matches properly the lower levels of the domain
+%% as would be expected.
+%%
+%% When a result is found, this function will return the handler module and
+%% options found in the dispatch list, a key-value list of bindings and
+%% the tokens that were matched by the <em>'...'</em> atom for both the
+%% hostname and path.
+-spec match(dispatch_rules(), Host::binary() | tokens(), Path::binary())
+ -> {ok, module(), any(), bindings(),
+ HostInfo::undefined | tokens(),
+ PathInfo::undefined | tokens()}
+ | {error, notfound, host} | {error, notfound, path}
+ | {error, badrequest, path}.
+match([], _, _) ->
+ {error, notfound, host};
+%% If the host is '_' then there can be no constraints.
+match([{'_', [], PathMatchs}|_Tail], _, Path) ->
+ match_path(PathMatchs, undefined, Path, []);
+match([{HostMatch, Constraints, PathMatchs}|Tail], Tokens, Path)
+ when is_list(Tokens) ->
+ case list_match(Tokens, HostMatch, []) of
+ false ->
+ match(Tail, Tokens, Path);
+ {true, Bindings, HostInfo} ->
+ HostInfo2 = case HostInfo of
+ undefined -> undefined;
+ _ -> lists:reverse(HostInfo)
+ end,
+ case check_constraints(Constraints, Bindings) of
+ {ok, Bindings2} ->
+ match_path(PathMatchs, HostInfo2, Path, Bindings2);
+ nomatch ->
+ match(Tail, Tokens, Path)
+ end
+ end;
+match(Dispatch, Host, Path) ->
+ match(Dispatch, split_host(Host), Path).
+
+-spec match_path([dispatch_path()],
+ HostInfo::undefined | tokens(), binary() | tokens(), bindings())
+ -> {ok, module(), any(), bindings(),
+ HostInfo::undefined | tokens(),
+ PathInfo::undefined | tokens()}
+ | {error, notfound, path} | {error, badrequest, path}.
+match_path([], _, _, _) ->
+ {error, notfound, path};
+%% If the path is '_' then there can be no constraints.
+match_path([{'_', [], Handler, Opts}|_Tail], HostInfo, _, Bindings) ->
+ {ok, Handler, Opts, Bindings, HostInfo, undefined};
+match_path([{<<"*">>, _Constraints, Handler, Opts}|_Tail], HostInfo, <<"*">>, Bindings) ->
+ {ok, Handler, Opts, Bindings, HostInfo, undefined};
+match_path([{PathMatch, Constraints, Handler, Opts}|Tail], HostInfo, Tokens,
+ Bindings) when is_list(Tokens) ->
+ case list_match(Tokens, PathMatch, Bindings) of
+ false ->
+ match_path(Tail, HostInfo, Tokens, Bindings);
+ {true, PathBinds, PathInfo} ->
+ case check_constraints(Constraints, PathBinds) of
+ {ok, PathBinds2} ->
+ {ok, Handler, Opts, PathBinds2, HostInfo, PathInfo};
+ nomatch ->
+ match_path(Tail, HostInfo, Tokens, Bindings)
+ end
+ end;
+match_path(_Dispatch, _HostInfo, badrequest, _Bindings) ->
+ {error, badrequest, path};
+match_path(Dispatch, HostInfo, Path, Bindings) ->
+ match_path(Dispatch, HostInfo, split_path(Path), Bindings).
+
+check_constraints([], Bindings) ->
+ {ok, Bindings};
+check_constraints([Constraint|Tail], Bindings) ->
+ Name = element(1, Constraint),
+ case lists:keyfind(Name, 1, Bindings) of
+ false ->
+ check_constraints(Tail, Bindings);
+ {_, Value} ->
+ case check_constraint(Constraint, Value) of
+ true ->
+ check_constraints(Tail, Bindings);
+ {true, Value2} ->
+ Bindings2 = lists:keyreplace(Name, 1, Bindings,
+ {Name, Value2}),
+ check_constraints(Tail, Bindings2);
+ false ->
+ nomatch
+ end
+ end.
+
+check_constraint({_, int}, Value) ->
+ try {true, list_to_integer(binary_to_list(Value))}
+ catch _:_ -> false
+ end;
+check_constraint({_, function, Fun}, Value) ->
+ Fun(Value).
+
+%% @doc Split a hostname into a list of tokens.
+-spec split_host(binary()) -> tokens().
+split_host(Host) ->
+ split_host(Host, []).
+
+split_host(Host, Acc) ->
+ case binary:match(Host, <<".">>) of
+ nomatch when Host =:= <<>> ->
+ Acc;
+ nomatch ->
+ [Host|Acc];
+ {Pos, _} ->
+ << Segment:Pos/binary, _:8, Rest/bits >> = Host,
+ false = byte_size(Segment) == 0,
+ split_host(Rest, [Segment|Acc])
+ end.
+
+%% @doc Split a path into a list of path segments.
+%%
+%% Following RFC2396, this function may return path segments containing any
+%% character, including <em>/</em> if, and only if, a <em>/</em> was escaped
+%% and part of a path segment.
+-spec split_path(binary()) -> tokens().
+split_path(<< $/, Path/bits >>) ->
+ split_path(Path, []);
+split_path(_) ->
+ badrequest.
+
+split_path(Path, Acc) ->
+ try
+ case binary:match(Path, <<"/">>) of
+ nomatch when Path =:= <<>> ->
+ lists:reverse([cowboy_http:urldecode(S) || S <- Acc]);
+ nomatch ->
+ lists:reverse([cowboy_http:urldecode(S) || S <- [Path|Acc]]);
+ {Pos, _} ->
+ << Segment:Pos/binary, _:8, Rest/bits >> = Path,
+ split_path(Rest, [Segment|Acc])
+ end
+ catch
+ error:badarg ->
+ badrequest
+ end.
+
+-spec list_match(tokens(), dispatch_match(), bindings())
+ -> {true, bindings(), undefined | tokens()} | false.
+%% Atom '...' matches any trailing path, stop right now.
+list_match(List, ['...'], Binds) ->
+ {true, Binds, List};
+%% Atom '_' matches anything, continue.
+list_match([_E|Tail], ['_'|TailMatch], Binds) ->
+ list_match(Tail, TailMatch, Binds);
+%% Both values match, continue.
+list_match([E|Tail], [E|TailMatch], Binds) ->
+ list_match(Tail, TailMatch, Binds);
+%% Bind E to the variable name V and continue,
+%% unless V was already defined and E isn't identical to the previous value.
+list_match([E|Tail], [V|TailMatch], Binds) when is_atom(V) ->
+ case lists:keyfind(V, 1, Binds) of
+ {_, E} ->
+ list_match(Tail, TailMatch, Binds);
+ {_, _} ->
+ false;
+ false ->
+ list_match(Tail, TailMatch, [{V, E}|Binds])
+ end;
+%% Match complete.
+list_match([], [], Binds) ->
+ {true, Binds, undefined};
+%% Values don't match, stop.
+list_match(_List, _Match, _Binds) ->
+ false.
+
+%% Tests.
+
+-ifdef(TEST).
+
+compile_test_() ->
+ %% {Routes, Result}
+ Tests = [
+ %% Match any host and path.
+ {[{'_', [{'_', h, o}]}],
+ [{'_', [], [{'_', [], h, o}]}]},
+ {[{"cowboy.example.org",
+ [{"/", ha, oa}, {"/path/to/resource", hb, ob}]}],
+ [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [
+ {[], [], ha, oa},
+ {[<<"path">>, <<"to">>, <<"resource">>], [], hb, ob}]}]},
+ {[{'_', [{"/path/to/resource/", h, o}]}],
+ [{'_', [], [{[<<"path">>, <<"to">>, <<"resource">>], [], h, o}]}]},
+ {[{"cowboy.example.org.", [{'_', h, o}]}],
+ [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]},
+ {[{".cowboy.example.org", [{'_', h, o}]}],
+ [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]},
+ {[{":subdomain.example.org", [{"/hats/:name/prices", h, o}]}],
+ [{[<<"org">>, <<"example">>, subdomain], [], [
+ {[<<"hats">>, name, <<"prices">>], [], h, o}]}]},
+ {[{"ninenines.:_", [{"/hats/:_", h, o}]}],
+ [{['_', <<"ninenines">>], [], [{[<<"hats">>, '_'], [], h, o}]}]},
+ {[{"[www.]ninenines.eu",
+ [{"/horses", h, o}, {"/hats/[page/:number]", h, o}]}], [
+ {[<<"eu">>, <<"ninenines">>], [], [
+ {[<<"horses">>], [], h, o},
+ {[<<"hats">>], [], h, o},
+ {[<<"hats">>, <<"page">>, number], [], h, o}]},
+ {[<<"eu">>, <<"ninenines">>, <<"www">>], [], [
+ {[<<"horses">>], [], h, o},
+ {[<<"hats">>], [], h, o},
+ {[<<"hats">>, <<"page">>, number], [], h, o}]}]},
+ {[{'_', [{"/hats/[page/[:number]]", h, o}]}], [{'_', [], [
+ {[<<"hats">>], [], h, o},
+ {[<<"hats">>, <<"page">>], [], h, o},
+ {[<<"hats">>, <<"page">>, number], [], h, o}]}]},
+ {[{"[...]ninenines.eu", [{"/hats/[...]", h, o}]}],
+ [{[<<"eu">>, <<"ninenines">>, '...'], [], [
+ {[<<"hats">>, '...'], [], h, o}]}]}
+ ],
+ [{lists:flatten(io_lib:format("~p", [Rt])),
+ fun() -> Rs = compile(Rt) end} || {Rt, Rs} <- Tests].
+
+split_host_test_() ->
+ %% {Host, Result}
+ Tests = [
+ {<<"">>, []},
+ {<<"*">>, [<<"*">>]},
+ {<<"cowboy.ninenines.eu">>,
+ [<<"eu">>, <<"ninenines">>, <<"cowboy">>]},
+ {<<"ninenines.eu">>,
+ [<<"eu">>, <<"ninenines">>]},
+ {<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>,
+ [<<"z">>, <<"y">>, <<"x">>, <<"w">>, <<"v">>, <<"u">>, <<"t">>,
+ <<"s">>, <<"r">>, <<"q">>, <<"p">>, <<"o">>, <<"n">>, <<"m">>,
+ <<"l">>, <<"k">>, <<"j">>, <<"i">>, <<"h">>, <<"g">>, <<"f">>,
+ <<"e">>, <<"d">>, <<"c">>, <<"b">>, <<"a">>]}
+ ],
+ [{H, fun() -> R = split_host(H) end} || {H, R} <- Tests].
+
+split_path_test_() ->
+ %% {Path, Result, QueryString}
+ Tests = [
+ {<<"/">>, []},
+ {<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>]},
+ {<<"/users">>, [<<"users">>]},
+ {<<"/users/42/friends">>, [<<"users">>, <<"42">>, <<"friends">>]},
+ {<<"/users/a+b/c%21d">>, [<<"users">>, <<"a b">>, <<"c!d">>]}
+ ],
+ [{P, fun() -> R = split_path(P) end} || {P, R} <- Tests].
+
+match_test_() ->
+ Dispatch = [
+ {[<<"eu">>, <<"ninenines">>, '_', <<"www">>], [], [
+ {[<<"users">>, '_', <<"mails">>], [], match_any_subdomain_users, []}
+ ]},
+ {[<<"eu">>, <<"ninenines">>], [], [
+ {[<<"users">>, id, <<"friends">>], [], match_extend_users_friends, []},
+ {'_', [], match_extend, []}
+ ]},
+ {[var, <<"ninenines">>], [], [
+ {[<<"threads">>, var], [], match_duplicate_vars,
+ [we, {expect, two}, var, here]}
+ ]},
+ {[ext, <<"erlang">>], [], [
+ {'_', [], match_erlang_ext, []}
+ ]},
+ {'_', [], [
+ {[<<"users">>, id, <<"friends">>], [], match_users_friends, []},
+ {'_', [], match_any, []}
+ ]}
+ ],
+ %% {Host, Path, Result}
+ Tests = [
+ {<<"any">>, <<"/">>, {ok, match_any, [], []}},
+ {<<"www.any.ninenines.eu">>, <<"/users/42/mails">>,
+ {ok, match_any_subdomain_users, [], []}},
+ {<<"www.ninenines.eu">>, <<"/users/42/mails">>,
+ {ok, match_any, [], []}},
+ {<<"www.ninenines.eu">>, <<"/">>,
+ {ok, match_any, [], []}},
+ {<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>,
+ {error, notfound, path}},
+ {<<"ninenines.eu">>, <<"/">>,
+ {ok, match_extend, [], []}},
+ {<<"ninenines.eu">>, <<"/users/42/friends">>,
+ {ok, match_extend_users_friends, [], [{id, <<"42">>}]}},
+ {<<"erlang.fr">>, '_',
+ {ok, match_erlang_ext, [], [{ext, <<"fr">>}]}},
+ {<<"any">>, <<"/users/444/friends">>,
+ {ok, match_users_friends, [], [{id, <<"444">>}]}}
+ ],
+ [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
+ {ok, Handler, Opts, Binds, undefined, undefined}
+ = match(Dispatch, H, P)
+ end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests].
+
+match_info_test_() ->
+ Dispatch = [
+ {[<<"eu">>, <<"ninenines">>, <<"www">>], [], [
+ {[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], [], match_path, []}
+ ]},
+ {[<<"eu">>, <<"ninenines">>, '...'], [], [
+ {'_', [], match_any, []}
+ ]}
+ ],
+ Tests = [
+ {<<"ninenines.eu">>, <<"/">>,
+ {ok, match_any, [], [], [], undefined}},
+ {<<"bugs.ninenines.eu">>, <<"/">>,
+ {ok, match_any, [], [], [<<"bugs">>], undefined}},
+ {<<"cowboy.bugs.ninenines.eu">>, <<"/">>,
+ {ok, match_any, [], [], [<<"cowboy">>, <<"bugs">>], undefined}},
+ {<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>,
+ {ok, match_path, [], [], undefined, []}},
+ {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>,
+ {ok, match_path, [], [], undefined, [<<"path_info">>]}},
+ {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>,
+ {ok, match_path, [], [], undefined, [<<"foo">>, <<"bar">>]}}
+ ],
+ [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
+ R = match(Dispatch, H, P)
+ end} || {H, P, R} <- Tests].
+
+match_constraints_test() ->
+ Dispatch = [{'_', [],
+ [{[<<"path">>, value], [{value, int}], match, []}]}],
+ {ok, _, [], [{value, 123}], _, _} = match(Dispatch,
+ <<"ninenines.eu">>, <<"/path/123">>),
+ {ok, _, [], [{value, 123}], _, _} = match(Dispatch,
+ <<"ninenines.eu">>, <<"/path/123/">>),
+ {error, notfound, path} = match(Dispatch,
+ <<"ninenines.eu">>, <<"/path/NaN/">>),
+ Dispatch2 = [{'_', [],
+ [{[<<"path">>, username], [{username, function,
+ fun(Value) -> Value =:= cowboy_bstr:to_lower(Value) end}],
+ match, []}]}],
+ {ok, _, [], [{username, <<"essen">>}], _, _} = match(Dispatch2,
+ <<"ninenines.eu">>, <<"/path/essen">>),
+ {error, notfound, path} = match(Dispatch2,
+ <<"ninenines.eu">>, <<"/path/ESSEN">>),
+ ok.
+
+match_same_bindings_test() ->
+ Dispatch = [{[same, same], [], [{'_', [], match, []}]}],
+ {ok, _, [], [{same, <<"eu">>}], _, _} = match(Dispatch,
+ <<"eu.eu">>, <<"/">>),
+ {error, notfound, host} = match(Dispatch,
+ <<"ninenines.eu">>, <<"/">>),
+ Dispatch2 = [{[<<"eu">>, <<"ninenines">>, user], [],
+ [{[<<"path">>, user], [], match, []}]}],
+ {ok, _, [], [{user, <<"essen">>}], _, _} = match(Dispatch2,
+ <<"essen.ninenines.eu">>, <<"/path/essen">>),
+ {ok, _, [], [{user, <<"essen">>}], _, _} = match(Dispatch2,
+ <<"essen.ninenines.eu">>, <<"/path/essen/">>),
+ {error, notfound, path} = match(Dispatch2,
+ <<"essen.ninenines.eu">>, <<"/path/notessen">>),
+ Dispatch3 = [{'_', [], [{[same, same], [], match, []}]}],
+ {ok, _, [], [{same, <<"path">>}], _, _} = match(Dispatch3,
+ <<"ninenines.eu">>, <<"/path/path">>),
+ {error, notfound, path} = match(Dispatch3,
+ <<"ninenines.eu">>, <<"/path/to">>),
+ ok.
+
+-endif.
diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl
index 55d01c7..373ea52 100644
--- a/src/cowboy_static.erl
+++ b/src/cowboy_static.erl
@@ -289,7 +289,7 @@ forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=State) ->
-spec last_modified(Req, #state{})
-> {calendar:datetime(), Req, #state{}} when Req::cowboy_req:req().
last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) ->
- {Modified, Req, State}.
+ {erlang:localtime_to_universaltime(Modified), Req, State}.
%% @private Generate the ETag header value for this file.
@@ -321,8 +321,14 @@ content_types_provided(Req, #state{filepath=Filepath,
-spec file_contents(cowboy_req:req(), #state{}) -> tuple().
file_contents(Req, #state{filepath=Filepath,
fileinfo={ok, #file_info{size=Filesize}}}=State) ->
- {ok, Transport, Socket} = cowboy_req:transport(Req),
- Writefile = fun() -> Transport:sendfile(Socket, Filepath) end,
+ Writefile = fun(Socket, Transport) ->
+ %% Transport:sendfile/2 may return {error, closed}
+ %% if the connection is closed while sending the file.
+ case Transport:sendfile(Socket, Filepath) of
+ {ok, _} -> ok;
+ {error, closed} -> ok
+ end
+ end,
{{stream, Filesize, Writefile}, Req, State}.
diff --git a/src/cowboy_sup.erl b/src/cowboy_sup.erl
index 00fcc5e..0e4e59a 100644
--- a/src/cowboy_sup.erl
+++ b/src/cowboy_sup.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl
index 8c02ac7..debb69f 100644
--- a/src/cowboy_websocket.erl
+++ b/src/cowboy_websocket.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -12,7 +12,10 @@
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-%% @doc WebSocket protocol implementation.
+%% @doc Websocket protocol implementation.
+%%
+%% Cowboy supports versions 7 through 17 of the Websocket drafts.
+%% It also supports RFC6455, the proposed standard for Websocket.
-module(cowboy_websocket).
%% API.
@@ -21,52 +24,52 @@
%% Internal.
-export([handler_loop/4]).
+-type close_code() :: 1000..4999.
+-export_type([close_code/0]).
+
-type frame() :: close | ping | pong
| {text | binary | close | ping | pong, binary()}
- | {close, 1000..4999, binary()}.
+ | {close, close_code(), binary()}.
-export_type([frame/0]).
-type opcode() :: 0 | 1 | 2 | 8 | 9 | 10.
-type mask_key() :: 0..16#ffffffff.
-
-%% The websocket_data/4 function may be called multiple times for a message.
-%% The websocket_dispatch/4 function is only called once for each message.
--type frag_state() ::
- undefined | %% no fragmentation has been seen.
- {nofin, opcode()} | %% first fragment has been seen.
- {nofin, opcode(), binary()} | %% first fragment has been unmasked.
- {fin, opcode(), binary()}. %% last fragment has been seen.
+-type frag_state() :: undefined
+ | {nofin, opcode(), binary()} | {fin, opcode(), binary()}.
-record(state, {
+ env :: cowboy_middleware:env(),
socket = undefined :: inet:socket(),
transport = undefined :: module(),
- version :: 0 | 7 | 8 | 13,
handler :: module(),
- opts :: any(),
- challenge = undefined :: undefined | binary() | {binary(), binary()},
+ handler_opts :: any(),
+ key = undefined :: undefined | binary(),
timeout = infinity :: timeout(),
timeout_ref = undefined :: undefined | reference(),
messages = undefined :: undefined | {atom(), atom(), atom()},
hibernate = false :: boolean(),
- eop :: undefined | tuple(), %% hixie-76 specific.
- origin = undefined :: undefined | binary(), %% hixie-76 specific.
- frag_state = undefined :: frag_state()
+ frag_state = undefined :: frag_state(),
+ utf8_state = <<>> :: binary()
}).
-%% @doc Upgrade a HTTP request to the WebSocket protocol.
+%% @doc Upgrade an HTTP request to the Websocket protocol.
%%
-%% You do not need to call this function manually. To upgrade to the WebSocket
+%% You do not need to call this function manually. To upgrade to the Websocket
%% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em>
%% in your <em>cowboy_http_handler:init/3</em> handler function.
--spec upgrade(pid(), module(), any(), cowboy_req:req()) -> closed.
-upgrade(ListenerPid, Handler, Opts, Req) ->
+-spec upgrade(Req, Env, module(), any())
+ -> {ok, Req, Env} | {error, 400, Req}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+upgrade(Req, Env, Handler, HandlerOpts) ->
+ {_, ListenerPid} = lists:keyfind(listener, 1, Env),
ranch_listener:remove_connection(ListenerPid),
- {ok, Transport, Socket} = cowboy_req:transport(Req),
- State = #state{socket=Socket, transport=Transport,
- handler=Handler, opts=Opts},
+ [Socket, Transport] = cowboy_req:get([socket, transport], Req),
+ State = #state{env=Env, socket=Socket, transport=Transport,
+ handler=Handler, handler_opts=HandlerOpts},
case catch websocket_upgrade(State, Req) of
{ok, State2, Req2} -> handler_init(State2, Req2);
- {'EXIT', _Reason} -> upgrade_error(Req)
+ {'EXIT', _Reason} -> upgrade_error(Req, Env)
end.
-spec websocket_upgrade(#state{}, Req)
@@ -79,41 +82,21 @@ websocket_upgrade(State, Req) ->
{ok, [<<"websocket">>], Req3}
= cowboy_req:parse_header(<<"upgrade">>, Req2),
{Version, Req4} = cowboy_req:header(<<"sec-websocket-version">>, Req3),
- websocket_upgrade(Version, State, Req4).
-
-%% @todo Handle the Sec-Websocket-Protocol header.
-%% @todo Reply a proper error, don't die, if a required header is undefined.
--spec websocket_upgrade(undefined | <<_:8>>, #state{}, Req)
- -> {ok, #state{}, Req} when Req::cowboy_req:req().
-%% No version given. Assuming hixie-76 draft.
-%%
-%% We need to wait to send a reply back before trying to read the
-%% third part of the challenge key, because proxies will wait for
-%% a reply before sending it. Therefore we calculate the challenge
-%% key only in websocket_handshake/3.
-websocket_upgrade(undefined, State, Req) ->
- {Origin, Req2} = cowboy_req:header(<<"origin">>, Req),
- {Key1, Req3} = cowboy_req:header(<<"sec-websocket-key1">>, Req2),
- {Key2, Req4} = cowboy_req:header(<<"sec-websocket-key2">>, Req3),
- false = lists:member(undefined, [Origin, Key1, Key2]),
- EOP = binary:compile_pattern(<< 255 >>),
- {ok, State#state{version=0, origin=Origin, challenge={Key1, Key2},
- eop=EOP}, cowboy_req:set_meta(websocket_version, 0, Req4)};
-%% Versions 7 and 8. Implementation follows the hybi 7 through 17 drafts.
-websocket_upgrade(Version, State, Req)
- when Version =:= <<"7">>; Version =:= <<"8">>;
- Version =:= <<"13">> ->
- {Key, Req2} = cowboy_req:header(<<"sec-websocket-key">>, Req),
- false = Key =:= undefined,
- Challenge = hybi_challenge(Key),
IntVersion = list_to_integer(binary_to_list(Version)),
- {ok, State#state{version=IntVersion, challenge=Challenge},
- cowboy_req:set_meta(websocket_version, IntVersion, Req2)}.
-
--spec handler_init(#state{}, cowboy_req:req()) -> closed.
-handler_init(State=#state{transport=Transport, handler=Handler, opts=Opts},
- Req) ->
- try Handler:websocket_init(Transport:name(), Req, Opts) of
+ true = (IntVersion =:= 7) orelse (IntVersion =:= 8)
+ orelse (IntVersion =:= 13),
+ {Key, Req5} = cowboy_req:header(<<"sec-websocket-key">>, Req4),
+ false = Key =:= undefined,
+ {ok, State#state{key=Key},
+ cowboy_req:set_meta(websocket_version, IntVersion, Req5)}.
+
+-spec handler_init(#state{}, Req)
+ -> {ok, Req, cowboy_middleware:env()} | {error, 400, Req}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+handler_init(State=#state{env=Env, transport=Transport,
+ handler=Handler, handler_opts=HandlerOpts}, Req) ->
+ try Handler:websocket_init(Transport:name(), Req, HandlerOpts) of
{ok, Req2, HandlerState} ->
websocket_handshake(State, Req2, HandlerState);
{ok, Req2, HandlerState, hibernate} ->
@@ -127,60 +110,36 @@ handler_init(State=#state{transport=Transport, handler=Handler, opts=Opts},
hibernate=true}, Req2, HandlerState);
{shutdown, Req2} ->
cowboy_req:ensure_response(Req2, 400),
- closed
+ {ok, Req2, [{result, closed}|Env]}
catch Class:Reason ->
- upgrade_error(Req),
error_logger:error_msg(
"** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n** Options were ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, websocket_init, 3, Class, Reason, Opts,
- cowboy_req:to_list(Req),erlang:get_stacktrace()])
+ [Handler, websocket_init, 3, Class, Reason, HandlerOpts,
+ cowboy_req:to_list(Req),erlang:get_stacktrace()]),
+ upgrade_error(Req, Env)
end.
--spec upgrade_error(cowboy_req:req()) -> closed.
-upgrade_error(Req) ->
+%% Only send an error reply if there is no resp_sent message.
+-spec upgrade_error(Req, Env) -> {ok, Req, Env} | {error, 400, Req}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+upgrade_error(Req, Env) ->
receive
- {cowboy_req, resp_sent} -> closed
+ {cowboy_req, resp_sent} ->
+ {ok, Req, [{result, closed}|Env]}
after 0 ->
- _ = cowboy_req:reply(400, [], [], Req),
- closed
+ {error, 400, Req}
end.
--spec websocket_handshake(#state{}, cowboy_req:req(), any()) -> closed.
-websocket_handshake(State=#state{socket=Socket, transport=Transport,
- version=0, origin=Origin, challenge={Key1, Key2}},
- Req, HandlerState) ->
- {<< "http", Location/binary >>, Req1} = cowboy_req:url(Req),
- {ok, Req2} = cowboy_req:upgrade_reply(
- <<"101 WebSocket Protocol Handshake">>,
- [{<<"upgrade">>, <<"WebSocket">>},
- {<<"sec-websocket-location">>, << "ws", Location/binary >>},
- {<<"sec-websocket-origin">>, Origin}],
- Req1),
- %% Flush the resp_sent message before moving on.
- receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
- %% We replied with a proper response. Proxies should be happy enough,
- %% we can now read the 8 last bytes of the challenge keys and send
- %% the challenge response directly to the socket.
- %%
- %% We use a trick here to read exactly 8 bytes of the body regardless
- %% of what's in the buffer.
- {ok, Req3} = cowboy_req:init_stream(
- fun cowboy_http:te_identity/2, {0, 8},
- fun cowboy_http:ce_identity/1, Req2),
- case cowboy_req:body(Req3) of
- {ok, Key3, Req4} ->
- Challenge = hixie76_challenge(Key1, Key2, Key3),
- Transport:send(Socket, Challenge),
- handler_before_loop(State#state{messages=Transport:messages()},
- Req4, HandlerState, <<>>);
- _Any ->
- %% If an error happened reading the body, stop there.
- handler_terminate(State, Req3, HandlerState, {error, closed})
- end;
-websocket_handshake(State=#state{transport=Transport, challenge=Challenge},
+-spec websocket_handshake(#state{}, Req, any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+websocket_handshake(State=#state{transport=Transport, key=Key},
Req, HandlerState) ->
+ Challenge = base64:encode(crypto:sha(
+ << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>)),
{ok, Req2} = cowboy_req:upgrade_reply(
101,
[{<<"upgrade">>, <<"websocket">>},
@@ -189,17 +148,19 @@ websocket_handshake(State=#state{transport=Transport, challenge=Challenge},
%% Flush the resp_sent message before moving on.
receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
State2 = handler_loop_timeout(State),
- handler_before_loop(State2#state{messages=Transport:messages()},
- Req2, HandlerState, <<>>).
+ handler_before_loop(State2#state{key=undefined,
+ messages=Transport:messages()}, Req2, HandlerState, <<>>).
--spec handler_before_loop(#state{}, cowboy_req:req(), any(), binary()) -> closed.
+-spec handler_before_loop(#state{}, Req, any(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
handler_before_loop(State=#state{
socket=Socket, transport=Transport, hibernate=true},
Req, HandlerState, SoFar) ->
Transport:setopts(Socket, [{active, once}]),
- catch erlang:hibernate(?MODULE, handler_loop,
- [State#state{hibernate=false}, Req, HandlerState, SoFar]),
- closed;
+ {suspend, ?MODULE, handler_loop,
+ [State#state{hibernate=false}, Req, HandlerState, SoFar]};
handler_before_loop(State=#state{socket=Socket, transport=Transport},
Req, HandlerState, SoFar) ->
Transport:setopts(Socket, [{active, once}]),
@@ -215,10 +176,12 @@ handler_loop_timeout(State=#state{timeout=Timeout, timeout_ref=PrevRef}) ->
State#state{timeout_ref=TRef}.
%% @private
--spec handler_loop(#state{}, cowboy_req:req(), any(), binary()) -> closed.
-handler_loop(State=#state{
- socket=Socket, messages={OK, Closed, Error}, timeout_ref=TRef},
- Req, HandlerState, SoFar) ->
+-spec handler_loop(#state{}, Req, any(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+handler_loop(State=#state{socket=Socket, messages={OK, Closed, Error},
+ timeout_ref=TRef}, Req, HandlerState, SoFar) ->
receive
{OK, Socket, Data} ->
State2 = handler_loop_timeout(State),
@@ -237,191 +200,298 @@ handler_loop(State=#state{
SoFar, websocket_info, Message, fun handler_before_loop/4)
end.
--spec websocket_data(#state{}, cowboy_req:req(), any(), binary()) -> closed.
-%% No more data.
-websocket_data(State, Req, HandlerState, <<>>) ->
- handler_before_loop(State, Req, HandlerState, <<>>);
-%% hixie-76 close frame.
-websocket_data(State=#state{version=0}, Req, HandlerState,
- << 255, 0, _Rest/binary >>) ->
- websocket_close(State, Req, HandlerState, {normal, closed});
-%% hixie-76 data frame. We only support the frame type 0, same as the specs.
-websocket_data(State=#state{version=0, eop=EOP}, Req, HandlerState,
- Data = << 0, _/binary >>) ->
- case binary:match(Data, EOP) of
- {Pos, 1} ->
- Pos2 = Pos - 1,
- << 0, Payload:Pos2/binary, 255, Rest/bits >> = Data,
- handler_call(State, Req, HandlerState,
- Rest, websocket_handle, {text, Payload}, fun websocket_data/4);
- nomatch ->
- %% @todo We probably should allow limiting frame length.
- handler_before_loop(State, Req, HandlerState, Data)
- end;
-%% incomplete hybi data frame.
-websocket_data(State=#state{version=Version}, Req, HandlerState, Data)
- when Version =/= 0, byte_size(Data) =:= 1 ->
- handler_before_loop(State, Req, HandlerState, Data);
-%% 7 bit payload length prefix exists
+%% All frames passing through this function are considered valid,
+%% with the only exception of text and close frames with a payload
+%% which may still contain errors.
+-spec websocket_data(#state{}, Req, any(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+%% RSV bits MUST be 0 unless an extension is negotiated
+%% that defines meanings for non-zero values.
+websocket_data(State, Req, HandlerState, << _:1, Rsv:3, _/bits >>)
+ when Rsv =/= 0 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Invalid opcode. Note that these opcodes may be used by extensions.
+websocket_data(State, Req, HandlerState, << _:4, Opcode:4, _/bits >>)
+ when Opcode > 2, Opcode =/= 8, Opcode =/= 9, Opcode =/= 10 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Control frames MUST NOT be fragmented.
+websocket_data(State, Req, HandlerState, << 0:1, _:3, Opcode:4, _/bits >>)
+ when Opcode >= 8 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% A frame MUST NOT use the zero opcode unless fragmentation was initiated.
+websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
+ << _:4, 0:4, _/bits >>) ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Non-control opcode when expecting control message or next fragment.
+websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState,
+ << _:4, Opcode:4, _/bits >>)
+ when Opcode =/= 0, Opcode < 8 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Close control frame length MUST be 0 or >= 2.
+websocket_data(State, Req, HandlerState, << _:4, 8:4, _:1, 1:7, _/bits >>) ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Close control frame with incomplete close code. Need more data.
websocket_data(State, Req, HandlerState,
- << Fin:1, Rsv:3, Opcode:4, Mask:1, PayloadLen:7, Rest/bits >>
- = Data) when PayloadLen < 126 ->
+ Data = << _:4, 8:4, 1:1, Len:7, _/bits >>)
+ when Len > 1, byte_size(Data) < 8 ->
+ handler_before_loop(State, Req, HandlerState, Data);
+%% 7 bits payload length.
+websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
+ Len:7, MaskKey:32, Rest/bits >>)
+ when Len < 126 ->
websocket_data(State, Req, HandlerState,
- Fin, Rsv, Opcode, Mask, PayloadLen, Rest, Data);
-%% 7+16 bits payload length prefix exists
-websocket_data(State, Req, HandlerState,
- << Fin:1, Rsv:3, Opcode:4, Mask:1, 126:7, PayloadLen:16, Rest/bits >>
- = Data) when PayloadLen > 125 ->
+ Opcode, Len, MaskKey, Rest, Fin);
+%% 16 bits payload length.
+websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
+ 126:7, Len:16, MaskKey:32, Rest/bits >>)
+ when Len > 125, Opcode < 8 ->
websocket_data(State, Req, HandlerState,
- Fin, Rsv, Opcode, Mask, PayloadLen, Rest, Data);
-%% 7+16 bits payload length prefix missing
-websocket_data(State, Req, HandlerState,
- << _Fin:1, _Rsv:3, _Opcode:4, _Mask:1, 126:7, Rest/bits >>
- = Data) when byte_size(Rest) < 2 ->
- handler_before_loop(State, Req, HandlerState, Data);
-%% 7+64 bits payload length prefix exists
-websocket_data(State, Req, HandlerState,
- << Fin:1, Rsv:3, Opcode:4, Mask:1, 127:7, 0:1, PayloadLen:63,
- Rest/bits >> = Data) when PayloadLen > 16#FFFF ->
+ Opcode, Len, MaskKey, Rest, Fin);
+%% 63 bits payload length.
+websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
+ 127:7, 0:1, Len:63, MaskKey:32, Rest/bits >>)
+ when Len > 16#ffff, Opcode < 8 ->
websocket_data(State, Req, HandlerState,
- Fin, Rsv, Opcode, Mask, PayloadLen, Rest, Data);
-%% 7+64 bits payload length prefix missing
-websocket_data(State, Req, HandlerState,
- << _Fin:1, _Rsv:3, _Opcode:4, _Mask:1, 127:7, Rest/bits >>
- = Data) when byte_size(Rest) < 8 ->
- handler_before_loop(State, Req, HandlerState, Data);
-%% invalid payload length prefix.
-websocket_data(State, Req, HandlerState, _Data) ->
- websocket_close(State, Req, HandlerState, {error, badframe}).
-
--spec websocket_data(#state{}, cowboy_req:req(), any(), non_neg_integer(),
- non_neg_integer(), non_neg_integer(), non_neg_integer(),
- non_neg_integer(), binary(), binary()) -> closed.
-%% A fragmented message MUST start a non-zero opcode.
-websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
- _Fin=0, _Rsv=0, _Opcode=0, _Mask, _PayloadLen, _Rest, _Buffer) ->
+ Opcode, Len, MaskKey, Rest, Fin);
+%% When payload length is over 63 bits, the most significant bit MUST be 0.
+websocket_data(State, Req, HandlerState, << _:8, 1:1, 127:7, 1:1, _/bits >>) ->
websocket_close(State, Req, HandlerState, {error, badframe});
-%% A control message MUST NOT be fragmented.
-websocket_data(State, Req, HandlerState, _Fin=0, _Rsv=0, Opcode, _Mask,
- _PayloadLen, _Rest, _Buffer) when Opcode >= 8 ->
+%% All frames sent from the client to the server are masked.
+websocket_data(State, Req, HandlerState, << _:8, 0:1, _/bits >>) ->
websocket_close(State, Req, HandlerState, {error, badframe});
-%% The opcode is only included in the first message fragment.
-websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
- _Fin=0, _Rsv=0, Opcode, Mask, PayloadLen, Rest, Data) ->
- websocket_before_unmask(
- State#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- Data, Rest, 0, Mask, PayloadLen);
-%% non-control opcode when expecting control message or next fragment.
-websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState, _Fin,
- _Rsv=0, Opcode, _Mask, _Ln, _Rest, _Data) when Opcode > 0, Opcode < 8 ->
+%% For the next two clauses, it can be one of the following:
+%%
+%% * The minimal number of bytes MUST be used to encode the length
+%% * All control frames MUST have a payload length of 125 bytes or less
+websocket_data(State, Req, HandlerState, << _:9, 126:7, _:48, _/bits >>) ->
websocket_close(State, Req, HandlerState, {error, badframe});
-%% If the first message fragment was incomplete, retry unmasking.
-websocket_data(State=#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- _Fin=0, _Rsv=0, Opcode, Mask, PayloadLen, Rest, Data) ->
- websocket_before_unmask(
- State#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- Data, Rest, 0, Mask, PayloadLen);
-%% if the opcode is zero and the fin flag is zero, unmask and await next.
-websocket_data(State=#state{frag_state={nofin, _Opcode, _Payloads}}, Req,
- HandlerState, _Fin=0, _Rsv=0, _Opcode2=0, Mask, PayloadLen, Rest,
- Data) ->
- websocket_before_unmask(
- State, Req, HandlerState, Data, Rest, 0, Mask, PayloadLen);
-%% when the last fragment is seen. Update the fragmentation status.
-websocket_data(State=#state{frag_state={nofin, Opcode, Payloads}}, Req,
- HandlerState, _Fin=1, _Rsv=0, _Opcode=0, Mask, PayloadLen, Rest,
- Data) ->
- websocket_before_unmask(
- State#state{frag_state={fin, Opcode, Payloads}},
- Req, HandlerState, Data, Rest, 0, Mask, PayloadLen);
-%% control messages MUST NOT use 7+16 bits or 7+64 bits payload length prefixes
-websocket_data(State, Req, HandlerState, _Fin, _Rsv, Opcode, _Mask, PayloadLen,
- _Rest, _Data) when Opcode >= 8, PayloadLen > 125 ->
- websocket_close(State, Req, HandlerState, {error, badframe});
-%% unfragmented message. unmask and dispatch the message.
-websocket_data(State=#state{version=Version}, Req, HandlerState, _Fin=1, _Rsv=0,
- Opcode, Mask, PayloadLen, Rest, Data) when Version =/= 0 ->
- websocket_before_unmask(
- State, Req, HandlerState, Data, Rest, Opcode, Mask, PayloadLen);
-%% Something was wrong with the frame. Close the connection.
-websocket_data(State, Req, HandlerState, _Fin, _Rsv, _Opcode, _Mask,
- _PayloadLen, _Rest, _Data) ->
- websocket_close(State, Req, HandlerState, {error, badframe}).
-
-%% hybi routing depending on whether unmasking is needed.
--spec websocket_before_unmask(#state{}, cowboy_req:req(), any(), binary(),
- binary(), opcode(), 0 | 1, non_neg_integer() | undefined) -> closed.
-websocket_before_unmask(State, Req, HandlerState, Data,
- Rest, Opcode, Mask, PayloadLen) ->
- case {Mask, PayloadLen} of
- {0, 0} ->
- websocket_dispatch(State, Req, HandlerState, Rest, Opcode, <<>>);
- {1, N} when N + 4 > byte_size(Rest); N =:= undefined ->
- %% @todo We probably should allow limiting frame length.
- handler_before_loop(State, Req, HandlerState, Data);
- {1, _N} ->
- << MaskKey:32, Payload:PayloadLen/binary, Rest2/bits >> = Rest,
- websocket_unmask(State, Req, HandlerState, Rest2,
- Opcode, Payload, MaskKey)
- end.
-
-%% hybi unmasking.
--spec websocket_unmask(#state{}, cowboy_req:req(), any(), binary(),
- opcode(), binary(), mask_key()) -> closed.
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, Payload, MaskKey) ->
- websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, Payload, MaskKey, <<>>).
-
--spec websocket_unmask(#state{}, cowboy_req:req(), any(), binary(),
- opcode(), binary(), mask_key(), binary()) -> closed.
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:32, Rest/bits >>, MaskKey, Acc) ->
+websocket_data(State, Req, HandlerState, << _:9, 127:7, _:96, _/bits >>) ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Need more data.
+websocket_data(State, Req, HandlerState, Data) ->
+ handler_before_loop(State, Req, HandlerState, Data).
+
+%% Initialize or update fragmentation state.
+-spec websocket_data(#state{}, Req, any(),
+ opcode(), non_neg_integer(), mask_key(), binary(), 0 | 1)
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+%% The opcode is only included in the first frame fragment.
+websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
+ Opcode, Len, MaskKey, Data, 0) ->
+ websocket_payload(State#state{frag_state={nofin, Opcode, <<>>}},
+ Req, HandlerState, 0, Len, MaskKey, <<>>, Data);
+%% Subsequent frame fragments.
+websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState,
+ 0, Len, MaskKey, Data, 0) ->
+ websocket_payload(State, Req, HandlerState,
+ 0, Len, MaskKey, <<>>, Data);
+%% Final frame fragment.
+websocket_data(State=#state{frag_state={nofin, Opcode, SoFar}},
+ Req, HandlerState, 0, Len, MaskKey, Data, 1) ->
+ websocket_payload(State#state{frag_state={fin, Opcode, SoFar}},
+ Req, HandlerState, 0, Len, MaskKey, <<>>, Data);
+%% Unfragmented frame.
+websocket_data(State, Req, HandlerState, Opcode, Len, MaskKey, Data, 1) ->
+ websocket_payload(State, Req, HandlerState,
+ Opcode, Len, MaskKey, <<>>, Data).
+
+-spec websocket_payload(#state{}, Req, any(),
+ opcode(), non_neg_integer(), mask_key(), binary(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+%% Close control frames with a payload MUST contain a valid close code.
+websocket_payload(State, Req, HandlerState,
+ Opcode=8, Len, MaskKey, <<>>, << MaskedCode:2/binary, Rest/bits >>) ->
+ Unmasked = << Code:16 >> = websocket_unmask(MaskedCode, MaskKey, <<>>),
+ if Code < 1000; Code =:= 1004; Code =:= 1005; Code =:= 1006;
+ (Code > 1011) and (Code < 3000); Code > 4999 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+ true ->
+ websocket_payload(State, Req, HandlerState,
+ Opcode, Len - 2, MaskKey, Unmasked, Rest)
+ end;
+%% Text frames and close control frames MUST have a payload that is valid UTF-8.
+websocket_payload(State=#state{utf8_state=Incomplete},
+ Req, HandlerState, Opcode, Len, MaskKey, Unmasked, Data)
+ when (byte_size(Data) < Len) andalso ((Opcode =:= 1) orelse
+ ((Opcode =:= 8) andalso (Unmasked =/= <<>>))) ->
+ Unmasked2 = websocket_unmask(Data,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
+ case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
+ false ->
+ websocket_close(State, Req, HandlerState, {error, badencoding});
+ Utf8State ->
+ websocket_payload_loop(State#state{utf8_state=Utf8State},
+ Req, HandlerState, Opcode, Len - byte_size(Data), MaskKey,
+ << Unmasked/binary, Unmasked2/binary >>)
+ end;
+websocket_payload(State=#state{utf8_state=Incomplete},
+ Req, HandlerState, Opcode, Len, MaskKey, Unmasked, Data)
+ when Opcode =:= 1; (Opcode =:= 8) and (Unmasked =/= <<>>) ->
+ << End:Len/binary, Rest/bits >> = Data,
+ Unmasked2 = websocket_unmask(End,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
+ case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
+ <<>> ->
+ websocket_dispatch(State#state{utf8_state= <<>>},
+ Req, HandlerState, Rest, Opcode,
+ << Unmasked/binary, Unmasked2/binary >>);
+ _ ->
+ websocket_close(State, Req, HandlerState, {error, badencoding})
+ end;
+%% Fragmented text frames may cut payload in the middle of UTF-8 codepoints.
+websocket_payload(State=#state{frag_state={_, 1, _}, utf8_state=Incomplete},
+ Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, Data)
+ when byte_size(Data) < Len ->
+ Unmasked2 = websocket_unmask(Data,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
+ case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
+ false ->
+ websocket_close(State, Req, HandlerState, {error, badencoding});
+ Utf8State ->
+ websocket_payload_loop(State#state{utf8_state=Utf8State},
+ Req, HandlerState, Opcode, Len - byte_size(Data), MaskKey,
+ << Unmasked/binary, Unmasked2/binary >>)
+ end;
+websocket_payload(State=#state{frag_state={Fin, 1, _}, utf8_state=Incomplete},
+ Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, Data) ->
+ << End:Len/binary, Rest/bits >> = Data,
+ Unmasked2 = websocket_unmask(End,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
+ case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
+ <<>> ->
+ websocket_dispatch(State#state{utf8_state= <<>>},
+ Req, HandlerState, Rest, Opcode,
+ << Unmasked/binary, Unmasked2/binary >>);
+ Utf8State when is_binary(Utf8State), Fin =:= nofin ->
+ websocket_dispatch(State#state{utf8_state=Utf8State},
+ Req, HandlerState, Rest, Opcode,
+ << Unmasked/binary, Unmasked2/binary >>);
+ _ ->
+ websocket_close(State, Req, HandlerState, {error, badencoding})
+ end;
+%% Other frames have a binary payload.
+websocket_payload(State, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked, Data)
+ when byte_size(Data) < Len ->
+ Unmasked2 = websocket_unmask(Data,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), Unmasked),
+ websocket_payload_loop(State, Req, HandlerState,
+ Opcode, Len - byte_size(Data), MaskKey, Unmasked2);
+websocket_payload(State, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked, Data) ->
+ << End:Len/binary, Rest/bits >> = Data,
+ Unmasked2 = websocket_unmask(End,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), Unmasked),
+ websocket_dispatch(State, Req, HandlerState, Rest, Opcode, Unmasked2).
+
+-spec websocket_unmask(B, mask_key(), B) -> B when B::binary().
+websocket_unmask(<<>>, _, Unmasked) ->
+ Unmasked;
+websocket_unmask(<< O:32, Rest/bits >>, MaskKey, Acc) ->
T = O bxor MaskKey,
- websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, Rest, MaskKey, << Acc/binary, T:32 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:24 >>, MaskKey, Acc) ->
+ websocket_unmask(Rest, MaskKey, << Acc/binary, T:32 >>);
+websocket_unmask(<< O:24 >>, MaskKey, Acc) ->
<< MaskKey2:24, _:8 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, << Acc/binary, T:24 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:16 >>, MaskKey, Acc) ->
+ << Acc/binary, T:24 >>;
+websocket_unmask(<< O:16 >>, MaskKey, Acc) ->
<< MaskKey2:16, _:16 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, << Acc/binary, T:16 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:8 >>, MaskKey, Acc) ->
+ << Acc/binary, T:16 >>;
+websocket_unmask(<< O:8 >>, MaskKey, Acc) ->
<< MaskKey2:8, _:24 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, << Acc/binary, T:8 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, <<>>, _MaskKey, Acc) ->
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, Acc).
+ << Acc/binary, T:8 >>.
+
+%% Because we unmask on the fly we need to continue from the right mask byte.
+-spec rotate_mask_key(mask_key(), non_neg_integer()) -> mask_key().
+rotate_mask_key(MaskKey, UnmaskedLen) ->
+ Left = UnmaskedLen rem 4,
+ Right = 4 - Left,
+ (MaskKey bsl (Left * 8)) + (MaskKey bsr (Right * 8)).
+
+%% Returns <<>> if the argument is valid UTF-8, false if not,
+%% or the incomplete part of the argument if we need more data.
+-spec is_utf8(binary()) -> false | binary().
+is_utf8(Valid = <<>>) ->
+ Valid;
+is_utf8(<< _/utf8, Rest/binary >>) ->
+ is_utf8(Rest);
+%% 2 bytes. Codepages C0 and C1 are invalid; fail early.
+is_utf8(<< 2#1100000:7, _/bits >>) ->
+ false;
+is_utf8(Incomplete = << 2#110:3, _:5 >>) ->
+ Incomplete;
+%% 3 bytes.
+is_utf8(Incomplete = << 2#1110:4, _:4 >>) ->
+ Incomplete;
+is_utf8(Incomplete = << 2#1110:4, _:4, 2#10:2, _:6 >>) ->
+ Incomplete;
+%% 4 bytes. Codepage F4 may have invalid values greater than 0x10FFFF.
+is_utf8(<< 2#11110100:8, 2#10:2, High:6, _/bits >>) when High >= 2#10000 ->
+ false;
+is_utf8(Incomplete = << 2#11110:5, _:3 >>) ->
+ Incomplete;
+is_utf8(Incomplete = << 2#11110:5, _:3, 2#10:2, _:6 >>) ->
+ Incomplete;
+is_utf8(Incomplete = << 2#11110:5, _:3, 2#10:2, _:6, 2#10:2, _:6 >>) ->
+ Incomplete;
+%% Invalid.
+is_utf8(_) ->
+ false.
+
+-spec websocket_payload_loop(#state{}, Req, any(),
+ opcode(), non_neg_integer(), mask_key(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+websocket_payload_loop(State=#state{socket=Socket, transport=Transport,
+ messages={OK, Closed, Error}, timeout_ref=TRef},
+ Req, HandlerState, Opcode, Len, MaskKey, Unmasked) ->
+ Transport:setopts(Socket, [{active, once}]),
+ receive
+ {OK, Socket, Data} ->
+ State2 = handler_loop_timeout(State),
+ websocket_payload(State2, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked, Data);
+ {Closed, Socket} ->
+ handler_terminate(State, Req, HandlerState, {error, closed});
+ {Error, Socket, Reason} ->
+ handler_terminate(State, Req, HandlerState, {error, Reason});
+ {timeout, TRef, ?MODULE} ->
+ websocket_close(State, Req, HandlerState, {normal, timeout});
+ {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
+ websocket_payload_loop(State, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked);
+ Message ->
+ handler_call(State, Req, HandlerState,
+ <<>>, websocket_info, Message,
+ fun (State2, Req2, HandlerState2, _) ->
+ websocket_payload_loop(State2, Req2, HandlerState2,
+ Opcode, Len, MaskKey, Unmasked)
+ end)
+ end.
-%% hybi dispatching.
--spec websocket_dispatch(#state{}, cowboy_req:req(), any(), binary(),
- opcode(), binary()) -> closed.
-%% First frame of a fragmented message unmasked. Expect intermediate or last.
-websocket_dispatch(State=#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- RemainingData, 0, Payload) ->
- websocket_data(State#state{frag_state={nofin, Opcode, Payload}},
- Req, HandlerState, RemainingData);
-%% Intermediate frame of a fragmented message unmasked. Add payload to buffer.
-websocket_dispatch(State=#state{frag_state={nofin, Opcode, Payloads}}, Req,
- HandlerState, RemainingData, 0, Payload) ->
+-spec websocket_dispatch(#state{}, Req, any(), binary(), opcode(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+%% Continuation frame.
+websocket_dispatch(State=#state{frag_state={nofin, Opcode, SoFar}},
+ Req, HandlerState, RemainingData, 0, Payload) ->
websocket_data(State#state{frag_state={nofin, Opcode,
- <<Payloads/binary, Payload/binary>>}}, Req, HandlerState,
- RemainingData);
-%% Last frame of a fragmented message unmasked. Dispatch to handler.
-websocket_dispatch(State=#state{frag_state={fin, Opcode, Payloads}}, Req,
- HandlerState, RemainingData, 0, Payload) ->
+ << SoFar/binary, Payload/binary >>}}, Req, HandlerState, RemainingData);
+%% Last continuation frame.
+websocket_dispatch(State=#state{frag_state={fin, Opcode, SoFar}},
+ Req, HandlerState, RemainingData, 0, Payload) ->
websocket_dispatch(State#state{frag_state=undefined}, Req, HandlerState,
- RemainingData, Opcode, <<Payloads/binary, Payload/binary>>);
+ RemainingData, Opcode, << SoFar/binary, Payload/binary >>);
%% Text frame.
websocket_dispatch(State, Req, HandlerState, RemainingData, 1, Payload) ->
handler_call(State, Req, HandlerState, RemainingData,
@@ -431,13 +501,15 @@ websocket_dispatch(State, Req, HandlerState, RemainingData, 2, Payload) ->
handler_call(State, Req, HandlerState, RemainingData,
websocket_handle, {binary, Payload}, fun websocket_data/4);
%% Close control frame.
-%% @todo Handle the optional Payload.
-websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, _Payload) ->
- websocket_close(State, Req, HandlerState, {normal, closed});
+websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, <<>>) ->
+ websocket_close(State, Req, HandlerState, {remote, closed});
+websocket_dispatch(State, Req, HandlerState, _RemainingData, 8,
+ << Code:16, Payload/bits >>) ->
+ websocket_close(State, Req, HandlerState, {remote, Code, Payload});
%% Ping control frame. Send a pong back and forward the ping to the handler.
websocket_dispatch(State=#state{socket=Socket, transport=Transport},
Req, HandlerState, RemainingData, 9, Payload) ->
- Len = hybi_payload_length(byte_size(Payload)),
+ Len = payload_length_to_binary(byte_size(Payload)),
Transport:send(Socket, << 1:1, 0:3, 10:4, 0:1, Len/bits, Payload/binary >>),
handler_call(State, Req, HandlerState, RemainingData,
websocket_handle, {ping, Payload}, fun websocket_data/4);
@@ -446,10 +518,12 @@ websocket_dispatch(State, Req, HandlerState, RemainingData, 10, Payload) ->
handler_call(State, Req, HandlerState, RemainingData,
websocket_handle, {pong, Payload}, fun websocket_data/4).
--spec handler_call(#state{}, cowboy_req:req(), any(), binary(),
- atom(), any(), fun()) -> closed.
-handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
- RemainingData, Callback, Message, NextState) ->
+-spec handler_call(#state{}, Req, any(), binary(), atom(), any(), fun())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+handler_call(State=#state{handler=Handler, handler_opts=HandlerOpts}, Req,
+ HandlerState, RemainingData, Callback, Message, NextState) ->
try Handler:Callback(Message, Req, HandlerState) of
{ok, Req2, HandlerState2} ->
NextState(State, Req2, HandlerState2, RemainingData);
@@ -515,7 +589,7 @@ handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
" for the reason ~p:~p~n** Message was ~p~n"
"** Options were ~p~n** Handler state was ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, Callback, 3, Class, Reason, Message, Opts,
+ [Handler, Callback, 3, Class, Reason, Message, HandlerOpts,
HandlerState, PLReq, erlang:get_stacktrace()]),
websocket_close(State, Req, HandlerState, {error, handler})
end.
@@ -528,13 +602,6 @@ websocket_opcode(pong) -> 10.
-spec websocket_send(frame(), #state{})
-> ok | shutdown | {error, atom()}.
-%% hixie-76 text frame.
-websocket_send({text, Payload}, #state{
- socket=Socket, transport=Transport, version=0}) ->
- Transport:send(Socket, [0, Payload, 255]);
-%% Ignore all unknown frame types for compatibility with hixie 76.
-websocket_send(_Any, #state{version=0}) ->
- ok;
websocket_send(Type, #state{socket=Socket, transport=Transport})
when Type =:= close ->
Opcode = websocket_opcode(Type),
@@ -554,7 +621,7 @@ websocket_send({Type = close, StatusCode, Payload}, #state{
Len = 2 + iolist_size(Payload),
%% Control packets must not be > 125 in length.
true = Len =< 125,
- BinLen = hybi_payload_length(Len),
+ BinLen = payload_length_to_binary(Len),
Transport:send(Socket,
[<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits, StatusCode:16 >>, Payload]),
shutdown;
@@ -567,7 +634,7 @@ websocket_send({Type, Payload}, #state{socket=Socket, transport=Transport}) ->
true ->
true
end,
- BinLen = hybi_payload_length(Len),
+ BinLen = payload_length_to_binary(Len),
Transport:send(Socket,
[<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits >>, Payload]).
@@ -582,20 +649,32 @@ websocket_send_many([Frame|Tail], State) ->
Error -> Error
end.
--spec websocket_close(#state{}, cowboy_req:req(), any(), {atom(), atom()})
- -> closed.
-websocket_close(State=#state{socket=Socket, transport=Transport, version=0},
- Req, HandlerState, Reason) ->
- Transport:send(Socket, << 255, 0 >>),
- handler_terminate(State, Req, HandlerState, Reason);
+-spec websocket_close(#state{}, Req, any(),
+ {atom(), atom()} | {remote, close_code(), binary()})
+ -> {ok, Req, cowboy_middleware:env()}
+ when Req::cowboy_req:req().
websocket_close(State=#state{socket=Socket, transport=Transport},
Req, HandlerState, Reason) ->
- Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>),
+ case Reason of
+ {normal, _} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>);
+ {error, badframe} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1002:16 >>);
+ {error, badencoding} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1007:16 >>);
+ {error, handler} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1011:16 >>);
+ {remote, closed} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>);
+ {remote, Code, _} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, Code:16 >>)
+ end,
handler_terminate(State, Req, HandlerState, Reason).
--spec handler_terminate(#state{}, cowboy_req:req(),
- any(), atom() | {atom(), atom()}) -> closed.
-handler_terminate(#state{handler=Handler, opts=Opts},
+-spec handler_terminate(#state{}, Req, any(), atom() | {atom(), atom()})
+ -> {ok, Req, cowboy_middleware:env()}
+ when Req::cowboy_req:req().
+handler_terminate(#state{env=Env, handler=Handler, handler_opts=HandlerOpts},
Req, HandlerState, TerminateReason) ->
try
Handler:websocket_terminate(TerminateReason, Req, HandlerState)
@@ -606,35 +685,14 @@ handler_terminate(#state{handler=Handler, opts=Opts},
" for the reason ~p:~p~n** Initial reason was ~p~n"
"** Options were ~p~n** Handler state was ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, websocket_terminate, 3, Class, Reason, TerminateReason, Opts,
- HandlerState, PLReq, erlang:get_stacktrace()])
+ [Handler, websocket_terminate, 3, Class, Reason, TerminateReason,
+ HandlerOpts, HandlerState, PLReq, erlang:get_stacktrace()])
end,
- closed.
-
-%% hixie-76 specific.
-
--spec hixie76_challenge(binary(), binary(), binary()) -> binary().
-hixie76_challenge(Key1, Key2, Key3) ->
- IntKey1 = hixie76_key_to_integer(Key1),
- IntKey2 = hixie76_key_to_integer(Key2),
- erlang:md5(<< IntKey1:32, IntKey2:32, Key3/binary >>).
-
--spec hixie76_key_to_integer(binary()) -> integer().
-hixie76_key_to_integer(Key) ->
- Number = list_to_integer([C || << C >> <= Key, C >= $0, C =< $9]),
- Spaces = length([C || << C >> <= Key, C =:= 32]),
- Number div Spaces.
-
-%% hybi specific.
-
--spec hybi_challenge(binary()) -> binary().
-hybi_challenge(Key) ->
- Bin = << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>,
- base64:encode(crypto:sha(Bin)).
+ {ok, Req, [{result, closed}|Env]}.
--spec hybi_payload_length(0..16#7fffffffffffffff)
+-spec payload_length_to_binary(0..16#7fffffffffffffff)
-> << _:7 >> | << _:23 >> | << _:71 >>.
-hybi_payload_length(N) ->
+payload_length_to_binary(N) ->
case N of
N when N =< 125 -> << N:7 >>;
N when N =< 16#ffff -> << 126:7, N:16 >>;
diff --git a/src/cowboy_websocket_handler.erl b/src/cowboy_websocket_handler.erl
index 6d7f9de..bd2ed5a 100644
--- a/src/cowboy_websocket_handler.erl
+++ b/src/cowboy_websocket_handler.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -50,7 +50,7 @@
-type opts() :: any().
-type state() :: any().
--type terminate_reason() :: {normal, closed}
+-type terminate_reason() :: {normal, shutdown}
| {normal, timeout}
| {error, closed}
| {error, badframe}
diff --git a/test/autobahn_SUITE.erl b/test/autobahn_SUITE.erl
index 9ae9d7a..61cf631 100644
--- a/test/autobahn_SUITE.erl
+++ b/test/autobahn_SUITE.erl
@@ -64,7 +64,7 @@ end_per_suite(_Config) ->
init_per_group(autobahn, Config) ->
Port = 33080,
cowboy:start_http(autobahn, 100, [{port, Port}], [
- {dispatch, init_dispatch()}
+ {env, [{dispatch, init_dispatch()}]}
]),
[{port, Port}|Config].
@@ -75,8 +75,8 @@ end_per_group(Listener, _Config) ->
%% Dispatch configuration.
init_dispatch() ->
- [{[<<"localhost">>], [
- {[<<"echo">>], websocket_echo_handler, []}]}].
+ cowboy_router:compile([{"localhost", [
+ {"/echo", websocket_echo_handler, []}]}]).
%% autobahn cases
@@ -92,7 +92,7 @@ run_tests(Config) ->
_ -> ok
end,
{ok, IndexHTML} = file:read_file(IndexFile),
- case binary:match(IndexHTML, <<"Fail">>) of
- {_, _} -> erlang:error(failed);
- nomatch -> ok
+ case length(binary:matches(IndexHTML, <<"case_failed">>)) > 2 of
+ true -> erlang:error(failed);
+ false -> ok
end.
diff --git a/test/autobahn_SUITE_data/test.py b/test/autobahn_SUITE_data/test.py
index c528c64..19c7669 100755
--- a/test/autobahn_SUITE_data/test.py
+++ b/test/autobahn_SUITE_data/test.py
@@ -10,7 +10,7 @@ AB_TESTS_PRIV = os.getenv("AB_TESTS_PRIV")
VIRTUALENV_URL = 'https://raw.github.com/pypa/virtualenv/master/virtualenv.py'
VIRTUALENV_BIN = os.path.join(AB_TESTS_ENV, "virtualenv.py")
-PIP_BIN = os.path.join(AB_TESTS_ENV, "bin", "pip")
+INSTALL_BIN = os.path.join(AB_TESTS_ENV, "bin", "easy_install")
def activate_env(env):
@@ -29,7 +29,7 @@ def install_env(env):
subprocess.check_call(["curl", "-sS", VIRTUALENV_URL, "-o", VIRTUALENV_BIN])
subprocess.check_call(["python", VIRTUALENV_BIN, env])
activate_env(env)
- subprocess.check_call([PIP_BIN, "install", "AutobahnTestSuite"])
+ subprocess.check_call([INSTALL_BIN, "http://pypi.python.org/packages/2.7/a/autobahntestsuite/autobahntestsuite-0.5.2-py2.7.egg#md5=f7480d4ca6ce4954ac05f59778de4bda"])
def client_config():
"""
diff --git a/test/chunked_handler.erl b/test/chunked_handler.erl
index 38305fd..e486afe 100644
--- a/test/chunked_handler.erl
+++ b/test/chunked_handler.erl
@@ -2,16 +2,18 @@
-module(chunked_handler).
-behaviour(cowboy_http_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3, handle/2, terminate/3]).
init({_Transport, http}, Req, _Opts) ->
{ok, Req, undefined}.
handle(Req, State) ->
{ok, Req2} = cowboy_req:chunked_reply(200, Req),
+ timer:sleep(100),
cowboy_req:chunk("chunked_handler\r\n", Req2),
+ timer:sleep(100),
cowboy_req:chunk("works fine!", Req2),
{ok, Req2, State}.
-terminate(_Req, _State) ->
+terminate(_, _, _) ->
ok.
diff --git a/cover.spec b/test/cover.spec
index 9dba11c..9dba11c 100644
--- a/cover.spec
+++ b/test/cover.spec
diff --git a/test/eunit_SUITE.erl b/test/eunit_SUITE.erl
new file mode 100644
index 0000000..a460890
--- /dev/null
+++ b/test/eunit_SUITE.erl
@@ -0,0 +1,31 @@
+%% Copyright (c) 2013, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(eunit_SUITE).
+
+-include_lib("common_test/include/ct.hrl").
+
+%% ct.
+-export([all/0]).
+
+%% Tests.
+-export([eunit/1]).
+
+%% ct.
+
+all() ->
+ [eunit].
+
+eunit(_) ->
+ ok = eunit:test({application, cowboy}).
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index c90e585..afe62c3 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
@@ -45,21 +45,26 @@
-export([nc_zero/1]).
-export([onrequest/1]).
-export([onrequest_reply/1]).
+-export([onresponse_capitalize/1]).
-export([onresponse_crash/1]).
-export([onresponse_reply/1]).
-export([pipeline/1]).
-export([rest_bad_accept/1]).
+-export([rest_created_path/1]).
-export([rest_expires/1]).
-export([rest_keepalive/1]).
-export([rest_keepalive_post/1]).
-export([rest_missing_get_callbacks/1]).
-export([rest_missing_put_callbacks/1]).
-export([rest_nodelete/1]).
+-export([rest_patch/1]).
-export([rest_resource_etags/1]).
-export([rest_resource_etags_if_none_match/1]).
-export([set_resp_body/1]).
-export([set_resp_header/1]).
-export([set_resp_overwrite/1]).
+-export([slowloris/1]).
+-export([slowloris2/1]).
-export([static_attribute_etag/1]).
-export([static_function_etag/1]).
-export([static_mimetypes_function/1]).
@@ -68,14 +73,24 @@
-export([static_test_file/1]).
-export([static_test_file_css/1]).
-export([stream_body_set_resp/1]).
+-export([stream_body_set_resp_close/1]).
-export([te_chunked/1]).
+-export([te_chunked_chopped/1]).
-export([te_chunked_delayed/1]).
-export([te_identity/1]).
%% ct.
all() ->
- [{group, http}, {group, https}, {group, onrequest}, {group, onresponse}].
+ [
+ {group, http},
+ {group, https},
+ {group, http_compress},
+ {group, https_compress},
+ {group, onrequest},
+ {group, onresponse},
+ {group, onresponse_capitalize}
+ ].
groups() ->
Tests = [
@@ -98,17 +113,21 @@ groups() ->
nc_zero,
pipeline,
rest_bad_accept,
+ rest_created_path,
rest_expires,
rest_keepalive,
rest_keepalive_post,
rest_missing_get_callbacks,
rest_missing_put_callbacks,
rest_nodelete,
+ rest_patch,
rest_resource_etags,
rest_resource_etags_if_none_match,
set_resp_body,
set_resp_header,
set_resp_overwrite,
+ slowloris,
+ slowloris2,
static_attribute_etag,
static_function_etag,
static_mimetypes_function,
@@ -117,13 +136,17 @@ groups() ->
static_test_file,
static_test_file_css,
stream_body_set_resp,
+ stream_body_set_resp_close,
te_chunked,
+ te_chunked_chopped,
te_chunked_delayed,
te_identity
],
[
{http, [], Tests},
{https, [], Tests},
+ {http_compress, [], Tests},
+ {https_compress, [], Tests},
{onrequest, [], [
onrequest,
onrequest_reply
@@ -131,11 +154,13 @@ groups() ->
{onresponse, [], [
onresponse_crash,
onresponse_reply
+ ]},
+ {onresponse_capitalize, [], [
+ onresponse_capitalize
]}
].
init_per_suite(Config) ->
- application:start(inets),
application:start(crypto),
application:start(ranch),
application:start(cowboy),
@@ -145,7 +170,6 @@ end_per_suite(_Config) ->
application:stop(cowboy),
application:stop(ranch),
application:stop(crypto),
- application:stop(inets),
ok.
init_per_group(http, Config) ->
@@ -153,7 +177,7 @@ init_per_group(http, Config) ->
Transport = ranch_tcp,
Config1 = init_static_dir(Config),
{ok, _} = cowboy:start_http(http, 100, [{port, Port}], [
- {dispatch, init_dispatch(Config1)},
+ {env, [{dispatch, init_dispatch(Config1)}]},
{max_keepalive, 50},
{timeout, 500}
]),
@@ -172,18 +196,51 @@ init_per_group(https, Config) ->
application:start(public_key),
application:start(ssl),
{ok, _} = cowboy:start_https(https, 100, Opts ++ [{port, Port}], [
- {dispatch, init_dispatch(Config1)},
+ {env, [{dispatch, init_dispatch(Config1)}]},
{max_keepalive, 50},
{timeout, 500}
]),
{ok, Client} = cowboy_client:init(Opts),
[{scheme, <<"https">>}, {port, Port}, {opts, Opts},
{transport, Transport}, {client, Client}|Config1];
-init_per_group(onrequest, Config) ->
+init_per_group(http_compress, Config) ->
Port = 33082,
Transport = ranch_tcp,
+ Config1 = init_static_dir(Config),
+ {ok, _} = cowboy:start_http(http_compress, 100, [{port, Port}], [
+ {compress, true},
+ {env, [{dispatch, init_dispatch(Config1)}]},
+ {max_keepalive, 50},
+ {timeout, 500}
+ ]),
+ {ok, Client} = cowboy_client:init([]),
+ [{scheme, <<"http">>}, {port, Port}, {opts, []},
+ {transport, Transport}, {client, Client}|Config1];
+init_per_group(https_compress, Config) ->
+ Port = 33083,
+ Transport = ranch_ssl,
+ Opts = [
+ {certfile, ?config(data_dir, Config) ++ "cert.pem"},
+ {keyfile, ?config(data_dir, Config) ++ "key.pem"},
+ {password, "cowboy"}
+ ],
+ Config1 = init_static_dir(Config),
+ application:start(public_key),
+ application:start(ssl),
+ {ok, _} = cowboy:start_https(https_compress, 100, Opts ++ [{port, Port}], [
+ {compress, true},
+ {env, [{dispatch, init_dispatch(Config1)}]},
+ {max_keepalive, 50},
+ {timeout, 500}
+ ]),
+ {ok, Client} = cowboy_client:init(Opts),
+ [{scheme, <<"https">>}, {port, Port}, {opts, Opts},
+ {transport, Transport}, {client, Client}|Config1];
+init_per_group(onrequest, Config) ->
+ Port = 33084,
+ Transport = ranch_tcp,
{ok, _} = cowboy:start_http(onrequest, 100, [{port, Port}], [
- {dispatch, init_dispatch(Config)},
+ {env, [{dispatch, init_dispatch(Config)}]},
{max_keepalive, 50},
{onrequest, fun onrequest_hook/1},
{timeout, 500}
@@ -192,25 +249,37 @@ init_per_group(onrequest, Config) ->
[{scheme, <<"http">>}, {port, Port}, {opts, []},
{transport, Transport}, {client, Client}|Config];
init_per_group(onresponse, Config) ->
- Port = 33083,
+ Port = 33085,
Transport = ranch_tcp,
{ok, _} = cowboy:start_http(onresponse, 100, [{port, Port}], [
- {dispatch, init_dispatch(Config)},
+ {env, [{dispatch, init_dispatch(Config)}]},
{max_keepalive, 50},
{onresponse, fun onresponse_hook/4},
{timeout, 500}
]),
{ok, Client} = cowboy_client:init([]),
[{scheme, <<"http">>}, {port, Port}, {opts, []},
+ {transport, Transport}, {client, Client}|Config];
+init_per_group(onresponse_capitalize, Config) ->
+ Port = 33086,
+ Transport = ranch_tcp,
+ {ok, _} = cowboy:start_http(onresponse_capitalize, 100, [{port, Port}], [
+ {env, [{dispatch, init_dispatch(Config)}]},
+ {max_keepalive, 50},
+ {onresponse, fun onresponse_capitalize_hook/4},
+ {timeout, 500}
+ ]),
+ {ok, Client} = cowboy_client:init([]),
+ [{scheme, <<"http">>}, {port, Port}, {opts, []},
{transport, Transport}, {client, Client}|Config].
-end_per_group(https, Config) ->
+end_per_group(Group, Config) when Group =:= https; Group =:= https_compress ->
cowboy:stop_listener(https),
application:stop(ssl),
application:stop(public_key),
end_static_dir(Config),
ok;
-end_per_group(http, Config) ->
+end_per_group(Group, Config) when Group =:= http; Group =:= http_compress ->
cowboy:stop_listener(http),
end_static_dir(Config);
end_per_group(Name, _) ->
@@ -220,54 +289,60 @@ end_per_group(Name, _) ->
%% Dispatch configuration.
init_dispatch(Config) ->
- [
- {[<<"localhost">>], [
- {[<<"chunked_response">>], chunked_handler, []},
- {[<<"init_shutdown">>], http_handler_init_shutdown, []},
- {[<<"long_polling">>], http_handler_long_polling, []},
- {[<<"headers">>, <<"dupe">>], http_handler,
+ cowboy_router:compile([
+ {"localhost", [
+ {"/chunked_response", chunked_handler, []},
+ {"/init_shutdown", http_handler_init_shutdown, []},
+ {"/long_polling", http_handler_long_polling, []},
+ {"/headers/dupe", http_handler,
[{headers, [{<<"connection">>, <<"close">>}]}]},
- {[<<"set_resp">>, <<"header">>], http_handler_set_resp,
+ {"/set_resp/header", http_handler_set_resp,
[{headers, [{<<"vary">>, <<"Accept">>}]}]},
- {[<<"set_resp">>, <<"overwrite">>], http_handler_set_resp,
+ {"/set_resp/overwrite", http_handler_set_resp,
[{headers, [{<<"server">>, <<"DesireDrive/1.0">>}]}]},
- {[<<"set_resp">>, <<"body">>], http_handler_set_resp,
+ {"/set_resp/body", http_handler_set_resp,
[{body, <<"A flameless dance does not equal a cycle">>}]},
- {[<<"stream_body">>, <<"set_resp">>], http_handler_stream_body,
+ {"/stream_body/set_resp", http_handler_stream_body,
[{reply, set_resp}, {body, <<"stream_body_set_resp">>}]},
- {[<<"static">>, '...'], cowboy_static,
+ {"/stream_body/set_resp_close",
+ http_handler_stream_body, [
+ {reply, set_resp_close},
+ {body, <<"stream_body_set_resp_close">>}]},
+ {"/static/[...]", cowboy_static,
[{directory, ?config(static_dir, Config)},
{mimetypes, [{<<".css">>, [<<"text/css">>]}]}]},
- {[<<"static_mimetypes_function">>, '...'], cowboy_static,
+ {"/static_mimetypes_function/[...]", cowboy_static,
[{directory, ?config(static_dir, Config)},
{mimetypes, {fun(Path, data) when is_binary(Path) ->
[<<"text/html">>] end, data}}]},
- {[<<"handler_errors">>], http_handler_errors, []},
- {[<<"static_attribute_etag">>, '...'], cowboy_static,
+ {"/handler_errors", http_handler_errors, []},
+ {"/static_attribute_etag/[...]", cowboy_static,
[{directory, ?config(static_dir, Config)},
{etag, {attributes, [filepath, filesize, inode, mtime]}}]},
- {[<<"static_function_etag">>, '...'], cowboy_static,
+ {"/static_function_etag/[...]", cowboy_static,
[{directory, ?config(static_dir, Config)},
{etag, {fun static_function_etag/2, etag_data}}]},
- {[<<"static_specify_file">>, '...'], cowboy_static,
+ {"/static_specify_file/[...]", cowboy_static,
[{directory, ?config(static_dir, Config)},
{mimetypes, [{<<".css">>, [<<"text/css">>]}]},
{file, <<"test_file.css">>}]},
- {[<<"multipart">>], http_handler_multipart, []},
- {[<<"echo">>, <<"body">>], http_handler_echo_body, []},
- {[<<"bad_accept">>], rest_simple_resource, []},
- {[<<"simple">>], rest_simple_resource, []},
- {[<<"forbidden_post">>], rest_forbidden_resource, [true]},
- {[<<"simple_post">>], rest_forbidden_resource, [false]},
- {[<<"missing_get_callbacks">>], rest_missing_callbacks, []},
- {[<<"missing_put_callbacks">>], rest_missing_callbacks, []},
- {[<<"nodelete">>], rest_nodelete_resource, []},
- {[<<"resetags">>], rest_resource_etags, []},
- {[<<"rest_expires">>], rest_expires, []},
- {[<<"loop_timeout">>], http_handler_loop_timeout, []},
- {[], http_handler, []}
+ {"/multipart", http_handler_multipart, []},
+ {"/echo/body", http_handler_echo_body, []},
+ {"/bad_accept", rest_simple_resource, []},
+ {"/simple", rest_simple_resource, []},
+ {"/forbidden_post", rest_forbidden_resource, [true]},
+ {"/simple_post", rest_forbidden_resource, [false]},
+ {"/missing_get_callbacks", rest_missing_callbacks, []},
+ {"/missing_put_callbacks", rest_missing_callbacks, []},
+ {"/nodelete", rest_nodelete_resource, []},
+ {"/patch", rest_patch_resource, []},
+ {"/created_path", rest_created_path_resource, []},
+ {"/resetags", rest_resource_etags, []},
+ {"/rest_expires", rest_expires, []},
+ {"/loop_timeout", http_handler_loop_timeout, []},
+ {"/", http_handler, []}
]}
- ].
+ ]).
init_static_dir(Config) ->
Dir = filename:join(?config(priv_dir, Config), "static"),
@@ -408,10 +483,16 @@ check_status(Config) ->
{Ret, URL}
end || {Status, URL} <- Tests].
-%% @todo Convert to cowboy_client.
chunked_response(Config) ->
- {ok, {{"HTTP/1.1", 200, "OK"}, _, "chunked_handler\r\nworks fine!"}}
- = httpc:request(binary_to_list(build_url("/chunked_response", Config))).
+ Client = ?config(client, Config),
+ {ok, Client2} = cowboy_client:request(<<"GET">>,
+ build_url("/chunked_response", Config), Client),
+ {ok, 200, Headers, Client3} = cowboy_client:response(Client2),
+ true = lists:keymember(<<"transfer-encoding">>, 1, Headers),
+ {ok, Transport, Socket} = cowboy_client:transport(Client3),
+ {ok, <<"11\r\nchunked_handler\r\n\r\nB\r\nworks fine!\r\n0\r\n\r\n">>}
+ = Transport:recv(Socket, 44, 1000),
+ {error, closed} = cowboy_client:response(Client3).
%% Check if sending requests whose size is around the MTU breaks something.
echo_body(Config) ->
@@ -503,8 +584,8 @@ http10_hostless(Config) ->
ranch:start_listener(Name, 5,
?config(transport, Config), ?config(opts, Config) ++ [{port, Port10}],
cowboy_protocol, [
- {dispatch, [{'_', [
- {[<<"http1.0">>, <<"hostless">>], http_handler, []}]}]},
+ {env, [{dispatch, cowboy_router:compile([
+ {'_', [{"/http1.0/hostless", http_handler, []}]}])}]},
{max_keepalive, 50},
{timeout, 500}]
),
@@ -517,7 +598,8 @@ keepalive_max(Config) ->
URL = build_url("/", Config),
ok = keepalive_max_loop(Client, URL, 50).
-keepalive_max_loop(_, _, 0) ->
+keepalive_max_loop(Client, _, 0) ->
+ {error, closed} = cowboy_client:response(Client),
ok;
keepalive_max_loop(Client, URL, N) ->
Headers = [{<<"connection">>, <<"keep-alive">>}],
@@ -536,7 +618,8 @@ keepalive_nl(Config) ->
URL = build_url("/", Config),
ok = keepalive_nl_loop(Client, URL, 10).
-keepalive_nl_loop(_, _, 0) ->
+keepalive_nl_loop(Client, _, 0) ->
+ {error, closed} = cowboy_client:response(Client),
ok;
keepalive_nl_loop(Client, URL, N) ->
Headers = [{<<"connection">>, <<"keep-alive">>}],
@@ -619,6 +702,21 @@ onrequest_hook(Req) ->
Req3
end.
+onresponse_capitalize(Config) ->
+ Client = ?config(client, Config),
+ {ok, Client2} = cowboy_client:request(<<"GET">>,
+ build_url("/", Config), Client),
+ {ok, Transport, Socket} = cowboy_client:transport(Client2),
+ {ok, Data} = Transport:recv(Socket, 0, 1000),
+ false = nomatch =:= binary:match(Data, <<"Content-Length">>).
+
+%% Hook for the above onresponse_capitalize test.
+onresponse_capitalize_hook(Status, Headers, Body, Req) ->
+ Headers2 = [{cowboy_bstr:capitalize_token(N), V}
+ || {N, V} <- Headers],
+ {ok, Req2} = cowboy_req:reply(Status, Headers2, Body, Req),
+ Req2.
+
onresponse_crash(Config) ->
Client = ?config(client, Config),
{ok, Client2} = cowboy_client:request(<<"GET">>,
@@ -668,6 +766,18 @@ rest_bad_accept(Config) ->
Client),
{ok, 400, _, _} = cowboy_client:response(Client2).
+rest_created_path(Config) ->
+ Headers = [{<<"content-type">>, <<"text/plain">>}],
+ Body = <<"Whatever">>,
+ Client = ?config(client, Config),
+ URL = build_url("/created_path", Config),
+ {ok, Client2} = cowboy_client:request(<<"POST">>, URL, Headers,
+ Body, Client),
+ {ok, 303, ResHeaders, _} = cowboy_client:response(Client2),
+ {<<"location">>, _Location} =
+ lists:keyfind(<<"location">>, 1, ResHeaders),
+ ok.
+
rest_expires(Config) ->
Client = ?config(client, Config),
{ok, Client2} = cowboy_client:request(<<"GET">>,
@@ -742,6 +852,21 @@ rest_nodelete(Config) ->
build_url("/nodelete", Config), Client),
{ok, 500, _, _} = cowboy_client:response(Client2).
+rest_patch(Config) ->
+ Tests = [
+ {204, [{<<"content-type">>, <<"text/plain">>}], <<"whatever">>},
+ {422, [{<<"content-type">>, <<"text/plain">>}], <<"false">>},
+ {400, [{<<"content-type">>, <<"text/plain">>}], <<"halt">>},
+ {415, [{<<"content-type">>, <<"application/json">>}], <<"bad_content_type">>}
+ ],
+ Client = ?config(client, Config),
+ _ = [begin
+ {ok, Client2} = cowboy_client:request(<<"PATCH">>,
+ build_url("/patch", Config), Headers, Body, Client),
+ {ok, Status, _, _} = cowboy_client:response(Client2),
+ ok
+ end || {Status, Headers, Body} <- Tests].
+
rest_resource_get_etag(Config, Type) ->
rest_resource_get_etag(Config, Type, []).
@@ -806,6 +931,34 @@ set_resp_overwrite(Config) ->
{<<"server">>, <<"DesireDrive/1.0">>}
= lists:keyfind(<<"server">>, 1, Headers).
+slowloris(Config) ->
+ Client = ?config(client, Config),
+ Transport = ?config(transport, Config),
+ {ok, Client2} = cowboy_client:connect(
+ Transport, "localhost", ?config(port, Config), Client),
+ try
+ [begin
+ {ok, _} = cowboy_client:raw_request([C], Client2),
+ receive after 25 -> ok end
+ end || C <- "GET / HTTP/1.1\r\nHost: localhost\r\n"
+ "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US)\r\n"
+ "Cookie: name=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n\r\n"],
+ error(failure)
+ catch error:{badmatch, _} ->
+ ok
+ end.
+
+slowloris2(Config) ->
+ Client = ?config(client, Config),
+ Transport = ?config(transport, Config),
+ {ok, Client2} = cowboy_client:connect(
+ Transport, "localhost", ?config(port, Config), Client),
+ {ok, _} = cowboy_client:raw_request("GET / HTTP/1.1\r\n", Client2),
+ receive after 300 -> ok end,
+ {ok, _} = cowboy_client:raw_request("Host: localhost\r\n", Client2),
+ receive after 300 -> ok end,
+ {ok, 408, _, _} = cowboy_client:response(Client2).
+
static_attribute_etag(Config) ->
Client = ?config(client, Config),
{ok, Client2} = cowboy_client:request(<<"GET">>,
@@ -892,6 +1045,22 @@ stream_body_set_resp(Config) ->
{ok, <<"stream_body_set_resp">>, _}
= cowboy_client:response_body(Client3).
+stream_body_set_resp_close(Config) ->
+ Client = ?config(client, Config),
+ {ok, Client2} = cowboy_client:request(<<"GET">>,
+ build_url("/stream_body/set_resp_close", Config), Client),
+ {ok, 200, _, Client3} = cowboy_client:response(Client2),
+ {ok, Transport, Socket} = cowboy_client:transport(Client3),
+ case element(7, Client3) of
+ <<"stream_body_set_resp_close">> ->
+ ok;
+ Buffer ->
+ {ok, Rest} = Transport:recv(Socket, 26 - byte_size(Buffer), 1000),
+ <<"stream_body_set_resp_close">> = << Buffer/binary, Rest/binary >>,
+ ok
+ end,
+ {error, closed} = Transport:recv(Socket, 0, 1000).
+
te_chunked(Config) ->
Client = ?config(client, Config),
Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
@@ -903,6 +1072,21 @@ te_chunked(Config) ->
{ok, 200, _, Client3} = cowboy_client:response(Client2),
{ok, Body, _} = cowboy_client:response_body(Client3).
+te_chunked_chopped(Config) ->
+ Client = ?config(client, Config),
+ Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
+ Body2 = iolist_to_binary(body_to_chunks(50, Body, [])),
+ {ok, Client2} = cowboy_client:request(<<"GET">>,
+ build_url("/echo/body", Config),
+ [{<<"transfer-encoding">>, <<"chunked">>}], Client),
+ {ok, Transport, Socket} = cowboy_client:transport(Client2),
+ _ = [begin
+ ok = Transport:send(Socket, << C >>),
+ ok = timer:sleep(10)
+ end || << C >> <= Body2],
+ {ok, 200, _, Client3} = cowboy_client:response(Client2),
+ {ok, Body, _} = cowboy_client:response_body(Client3).
+
te_chunked_delayed(Config) ->
Client = ?config(client, Config),
Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
diff --git a/test/http_handler.erl b/test/http_handler.erl
index e569adb..e1f1665 100644
--- a/test/http_handler.erl
+++ b/test/http_handler.erl
@@ -2,7 +2,7 @@
-module(http_handler).
-behaviour(cowboy_http_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3, handle/2, terminate/3]).
-record(state, {headers, body}).
@@ -15,5 +15,5 @@ handle(Req, State=#state{headers=Headers, body=Body}) ->
{ok, Req2} = cowboy_req:reply(200, Headers, Body, Req),
{ok, Req2, State}.
-terminate(_Req, _State) ->
+terminate(_, _, _) ->
ok.
diff --git a/test/http_handler_echo_body.erl b/test/http_handler_echo_body.erl
index e4b1ee0..31595d5 100644
--- a/test/http_handler_echo_body.erl
+++ b/test/http_handler_echo_body.erl
@@ -2,18 +2,18 @@
-module(http_handler_echo_body).
-behaviour(cowboy_http_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3, handle/2, terminate/3]).
init({_, http}, Req, _) ->
{ok, Req, undefined}.
handle(Req, State) ->
- {true, Req1} = cowboy_req:has_body(Req),
- {ok, Body, Req2} = cowboy_req:body(Req1),
+ true = cowboy_req:has_body(Req),
+ {ok, Body, Req2} = cowboy_req:body(Req),
{Size, Req3} = cowboy_req:body_length(Req2),
Size = byte_size(Body),
{ok, Req4} = cowboy_req:reply(200, [], Body, Req3),
{ok, Req4, State}.
-terminate(_, _) ->
+terminate(_, _, _) ->
ok.
diff --git a/test/http_handler_errors.erl b/test/http_handler_errors.erl
index 30cbaeb..2d1066c 100644
--- a/test/http_handler_errors.erl
+++ b/test/http_handler_errors.erl
@@ -2,7 +2,7 @@
-module(http_handler_errors).
-behaviour(cowboy_http_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3, handle/2, terminate/3]).
init({_Transport, http}, Req, _Opts) ->
{Case, Req1} = cowboy_req:qs_val(<<"case">>, Req),
@@ -36,5 +36,5 @@ handle(Req, <<"handle_after_reply">> = Case) ->
{ok, _Req1} = cowboy_req:reply(200, [], "http_handler_crashes", Req),
erlang:error(Case).
-terminate(_Req, _State) ->
+terminate(_, _, _) ->
ok.
diff --git a/test/http_handler_init_shutdown.erl b/test/http_handler_init_shutdown.erl
index edea1a0..fd01983 100644
--- a/test/http_handler_init_shutdown.erl
+++ b/test/http_handler_init_shutdown.erl
@@ -2,7 +2,7 @@
-module(http_handler_init_shutdown).
-behaviour(cowboy_http_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3, handle/2, terminate/3]).
init({_Transport, http}, Req, _Opts) ->
{ok, Req2} = cowboy_req:reply(<<"666 Init Shutdown Testing">>,
@@ -13,5 +13,5 @@ handle(Req, State) ->
{ok, Req2} = cowboy_req:reply(200, [], "Hello world!", Req),
{ok, Req2, State}.
-terminate(_Req, _State) ->
+terminate(_, _, _) ->
ok.
diff --git a/test/http_handler_long_polling.erl b/test/http_handler_long_polling.erl
index d61d697..763e1fe 100644
--- a/test/http_handler_long_polling.erl
+++ b/test/http_handler_long_polling.erl
@@ -2,7 +2,7 @@
-module(http_handler_long_polling).
-behaviour(cowboy_http_handler).
--export([init/3, handle/2, info/3, terminate/2]).
+-export([init/3, handle/2, info/3, terminate/3]).
init({_Transport, http}, Req, _Opts) ->
erlang:send_after(500, self(), timeout),
@@ -18,5 +18,5 @@ info(timeout, Req, State) ->
erlang:send_after(500, self(), timeout),
{loop, Req, State - 1, hibernate}.
-terminate(_Req, _State) ->
+terminate({normal, shutdown}, _, _) ->
ok.
diff --git a/test/http_handler_loop_timeout.erl b/test/http_handler_loop_timeout.erl
index c9bb15f..0155b1e 100644
--- a/test/http_handler_loop_timeout.erl
+++ b/test/http_handler_loop_timeout.erl
@@ -2,7 +2,7 @@
-module(http_handler_loop_timeout).
-behaviour(cowboy_loop_handler).
--export([init/3, info/3, terminate/2]).
+-export([init/3, info/3, terminate/3]).
init({_, http}, Req, _) ->
erlang:send_after(1000, self(), error_timeout),
@@ -12,5 +12,5 @@ info(error_timeout, Req, State) ->
{ok, Req2} = cowboy_req:reply(500, Req),
{ok, Req2, State}.
-terminate(_, _) ->
+terminate({normal, timeout}, _, _) ->
ok.
diff --git a/test/http_handler_multipart.erl b/test/http_handler_multipart.erl
index 850574f..8209535 100644
--- a/test/http_handler_multipart.erl
+++ b/test/http_handler_multipart.erl
@@ -2,7 +2,7 @@
-module(http_handler_multipart).
-behaviour(cowboy_http_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3, handle/2, terminate/3]).
init({_Transport, http}, Req, []) ->
{ok, Req, {}}.
@@ -12,7 +12,7 @@ handle(Req, State) ->
{ok, Req3} = cowboy_req:reply(200, [], term_to_binary(Result), Req2),
{ok, Req3, State}.
-terminate(_Req, _State) ->
+terminate(_, _, _) ->
ok.
acc_multipart(Req) ->
diff --git a/test/http_handler_set_resp.erl b/test/http_handler_set_resp.erl
index 70ddf79..d00d72a 100644
--- a/test/http_handler_set_resp.erl
+++ b/test/http_handler_set_resp.erl
@@ -2,7 +2,7 @@
-module(http_handler_set_resp).
-behaviour(cowboy_http_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3, handle/2, terminate/3]).
init({_Transport, http}, Req, Opts) ->
Headers = proplists:get_value(headers, Opts, []),
@@ -27,5 +27,5 @@ handle(Req, State) ->
end
end.
-terminate(_Req, _State) ->
+terminate(_, _, _) ->
ok.
diff --git a/test/http_handler_stream_body.erl b/test/http_handler_stream_body.erl
index feb4f78..5e42fa7 100644
--- a/test/http_handler_stream_body.erl
+++ b/test/http_handler_stream_body.erl
@@ -2,7 +2,7 @@
-module(http_handler_stream_body).
-behaviour(cowboy_http_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3, handle/2, terminate/3]).
-record(state, {headers, body, reply}).
@@ -12,13 +12,17 @@ init({_Transport, http}, Req, Opts) ->
Reply = proplists:get_value(reply, Opts),
{ok, Req, #state{headers=Headers, body=Body, reply=Reply}}.
-handle(Req, State=#state{headers=_Headers, body=Body, reply=set_resp}) ->
- {ok, Transport, Socket} = cowboy_req:transport(Req),
- SFun = fun() -> Transport:send(Socket, Body), sent end,
- SLen = iolist_size(Body),
- Req2 = cowboy_req:set_resp_body_fun(SLen, SFun, Req),
+handle(Req, State=#state{headers=_Headers, body=Body, reply=Reply}) ->
+ SFun = fun(Socket, Transport) -> Transport:send(Socket, Body) end,
+ Req2 = case Reply of
+ set_resp ->
+ SLen = iolist_size(Body),
+ cowboy_req:set_resp_body_fun(SLen, SFun, Req);
+ set_resp_close ->
+ cowboy_req:set_resp_body_fun(SFun, Req)
+ end,
{ok, Req3} = cowboy_req:reply(200, Req2),
{ok, Req3, State}.
-terminate(_Req, _State) ->
+terminate(_, _, _) ->
ok.
diff --git a/test/rest_created_path_resource.erl b/test/rest_created_path_resource.erl
new file mode 100644
index 0000000..5ad8cfc
--- /dev/null
+++ b/test/rest_created_path_resource.erl
@@ -0,0 +1,35 @@
+-module(rest_created_path_resource).
+-export([init/3]).
+-export([allowed_methods/2]).
+-export([content_types_provided/2]).
+-export([get_text_plain/2]).
+-export([post_is_create/2]).
+-export([content_types_accepted/2]).
+-export([post_text_plain/2]).
+-export([created_path/2]).
+
+init(_Transport, _Req, _Opts) ->
+ {upgrade, protocol, cowboy_rest}.
+
+allowed_methods(Req, State) ->
+{[<<"HEAD">>, <<"GET">>, <<"POST">>], Req, State}.
+
+content_types_provided(Req, State) ->
+ {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}.
+
+get_text_plain(Req, State) ->
+ {<<"This is REST!">>, Req, State}.
+
+post_is_create(Req, State) ->
+ {true, Req, State}.
+
+content_types_accepted(Req, State) ->
+ {[{{<<"text">>, <<"plain">>, []}, post_text_plain}], Req, State}.
+
+post_text_plain(Req, State) ->
+ {true, Req, State}.
+
+created_path(Req, State) ->
+ {<<"/created">>, Req, State}.
+
+
diff --git a/test/rest_patch_resource.erl b/test/rest_patch_resource.erl
new file mode 100644
index 0000000..e265f6f
--- /dev/null
+++ b/test/rest_patch_resource.erl
@@ -0,0 +1,34 @@
+-module(rest_patch_resource).
+-export([init/3, allowed_methods/2, content_types_provided/2, get_text_plain/2,
+ content_types_accepted/2, patch_text_plain/2]).
+
+init(_Transport, _Req, _Opts) ->
+ {upgrade, protocol, cowboy_rest}.
+
+allowed_methods(Req, State) ->
+ {[<<"HEAD">>, <<"GET">>, <<"PATCH">>], Req, State}.
+
+content_types_provided(Req, State) ->
+ {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}.
+
+get_text_plain(Req, State) ->
+ {<<"This is REST!">>, Req, State}.
+
+content_types_accepted(Req, State) ->
+ case cowboy_req:method(Req) of
+ {<<"PATCH">>, Req0} ->
+ {[{{<<"text">>, <<"plain">>, []}, patch_text_plain}], Req0, State};
+ {_, Req0} ->
+ {[], Req0, State}
+ end.
+
+patch_text_plain(Req, State) ->
+ case cowboy_req:body(Req) of
+ {ok, <<"halt">>, Req0} ->
+ {ok, Req1} = cowboy_req:reply(400, Req0),
+ {halt, Req1, State};
+ {ok, <<"false">>, Req0} ->
+ {false, Req0, State};
+ {ok, _Body, Req0} ->
+ {true, Req0, State}
+ end.
diff --git a/test/websocket_echo_handler.erl b/test/websocket_echo_handler.erl
index 926b51d..21b0116 100644
--- a/test/websocket_echo_handler.erl
+++ b/test/websocket_echo_handler.erl
@@ -1,21 +1,14 @@
%% Feel free to use, reuse and abuse the code in this file.
-module(websocket_echo_handler).
--behaviour(cowboy_http_handler).
-behaviour(cowboy_websocket_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3]).
-export([websocket_init/3, websocket_handle/3,
websocket_info/3, websocket_terminate/3]).
init(_Any, _Req, _Opts) ->
{upgrade, protocol, cowboy_websocket}.
-handle(_Req, _State) ->
- exit(badarg).
-
-terminate(_Req, _State) ->
- exit(badarg).
-
websocket_init(_TransportName, Req, _Opts) ->
Req2 = cowboy_req:compact(Req),
{ok, Req2, undefined}.
diff --git a/test/websocket_handler.erl b/test/websocket_handler.erl
index caf4828..a9863ae 100644
--- a/test/websocket_handler.erl
+++ b/test/websocket_handler.erl
@@ -1,21 +1,14 @@
%% Feel free to use, reuse and abuse the code in this file.
-module(websocket_handler).
--behaviour(cowboy_http_handler).
-behaviour(cowboy_websocket_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3]).
-export([websocket_init/3, websocket_handle/3,
websocket_info/3, websocket_terminate/3]).
init(_Any, _Req, _Opts) ->
{upgrade, protocol, cowboy_websocket}.
-handle(_Req, _State) ->
- exit(badarg).
-
-terminate(_Req, _State) ->
- exit(badarg).
-
websocket_init(_TransportName, Req, _Opts) ->
erlang:start_timer(1000, self(), <<"websocket_init">>),
Req2 = cowboy_req:compact(Req),
diff --git a/test/websocket_handler_init_shutdown.erl b/test/websocket_handler_init_shutdown.erl
index 5fdfba3..7ccea05 100644
--- a/test/websocket_handler_init_shutdown.erl
+++ b/test/websocket_handler_init_shutdown.erl
@@ -1,21 +1,14 @@
%% Feel free to use, reuse and abuse the code in this file.
-module(websocket_handler_init_shutdown).
--behaviour(cowboy_http_handler).
-behaviour(cowboy_websocket_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3]).
-export([websocket_init/3, websocket_handle/3,
websocket_info/3, websocket_terminate/3]).
init(_Any, _Req, _Opts) ->
{upgrade, protocol, cowboy_websocket}.
-handle(_Req, _State) ->
- exit(badarg).
-
-terminate(_Req, _State) ->
- exit(badarg).
-
websocket_init(_TransportName, Req, _Opts) ->
{ok, Req2} = cowboy_req:reply(403, Req),
{shutdown, Req2}.
diff --git a/test/ws_SUITE.erl b/test/ws_SUITE.erl
index 34befda..06d4b3e 100644
--- a/test/ws_SUITE.erl
+++ b/test/ws_SUITE.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
@@ -63,7 +63,6 @@ groups() ->
[{ws, [], BaseTests}].
init_per_suite(Config) ->
- application:start(inets),
application:start(crypto),
application:start(ranch),
application:start(cowboy),
@@ -73,13 +72,12 @@ end_per_suite(_Config) ->
application:stop(cowboy),
application:stop(ranch),
application:stop(crypto),
- application:stop(inets),
ok.
init_per_group(ws, Config) ->
Port = 33080,
cowboy:start_http(ws, 100, [{port, Port}], [
- {dispatch, init_dispatch()}
+ {env, [{dispatch, init_dispatch()}]}
]),
[{port, Port}|Config].
@@ -90,42 +88,39 @@ end_per_group(Listener, _Config) ->
%% Dispatch configuration.
init_dispatch() ->
- [
- {[<<"localhost">>], [
- {[<<"websocket">>], websocket_handler, []},
- {[<<"ws_echo_handler">>], websocket_echo_handler, []},
- {[<<"ws_init_shutdown">>], websocket_handler_init_shutdown, []},
- {[<<"ws_send_many">>], ws_send_many_handler, [
+ cowboy_router:compile([
+ {"localhost", [
+ {"/websocket", websocket_handler, []},
+ {"/ws_echo_handler", websocket_echo_handler, []},
+ {"/ws_init_shutdown", websocket_handler_init_shutdown, []},
+ {"/ws_send_many", ws_send_many_handler, [
{sequence, [
{text, <<"one">>},
{text, <<"two">>},
{text, <<"seven!">>}]}
]},
- {[<<"ws_send_close">>], ws_send_many_handler, [
+ {"/ws_send_close", ws_send_many_handler, [
{sequence, [
{text, <<"send">>},
close,
{text, <<"won't be received">>}]}
]},
- {[<<"ws_send_close_payload">>], ws_send_many_handler, [
+ {"/ws_send_close_payload", ws_send_many_handler, [
{sequence, [
{text, <<"send">>},
{close, 1001, <<"some text!">>},
{text, <<"won't be received">>}]}
]},
- {[<<"ws_timeout_hibernate">>], ws_timeout_hibernate_handler, []},
- {[<<"ws_timeout_cancel">>], ws_timeout_cancel_handler, []},
- {[<<"ws_upgrade_with_opts">>], ws_upgrade_with_opts_handler,
+ {"/ws_timeout_hibernate", ws_timeout_hibernate_handler, []},
+ {"/ws_timeout_cancel", ws_timeout_cancel_handler, []},
+ {"/ws_upgrade_with_opts", ws_upgrade_with_opts_handler,
<<"failure">>}
]}
- ].
+ ]).
%% ws and wss.
-%% This test makes sure the code works even if we wait for a reply
-%% before sending the third challenge key in the GET body.
-%%
-%% This ensures that Cowboy will work fine with proxies on hixie.
+%% We do not support hixie76 anymore.
ws0(Config) ->
{port, Port} = lists:keyfind(port, 1, Config),
{ok, Socket} = gen_tcp:connect("localhost", Port,
@@ -140,34 +135,8 @@ ws0(Config) ->
"Sec-Websocket-Key2: 1711 M;4\\74 80<6\r\n"
"\r\n"),
{ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
- {ok, {http_response, {1, 1}, 101, "WebSocket Protocol Handshake"}, Rest}
- = erlang:decode_packet(http, Handshake, []),
- [Headers, <<>>] = websocket_headers(
- erlang:decode_packet(httph, Rest, []), []),
- {'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers),
- {'Upgrade', "WebSocket"} = lists:keyfind('Upgrade', 1, Headers),
- {"sec-websocket-location", "ws://localhost/websocket"}
- = lists:keyfind("sec-websocket-location", 1, Headers),
- {"sec-websocket-origin", "http://localhost"}
- = lists:keyfind("sec-websocket-origin", 1, Headers),
- ok = gen_tcp:send(Socket, <<15,245,8,18,2,204,133,33>>),
- {ok, Body} = gen_tcp:recv(Socket, 0, 6000),
- <<169,244,191,103,146,33,149,59,74,104,67,5,99,118,171,236>> = Body,
- ok = gen_tcp:send(Socket, << 0, "client_msg", 255 >>),
- {ok, << 0, "client_msg", 255 >>} = gen_tcp:recv(Socket, 0, 6000),
- {ok, << 0, "websocket_init", 255 >>} = gen_tcp:recv(Socket, 0, 6000),
- {ok, << 0, "websocket_handle", 255 >>} = gen_tcp:recv(Socket, 0, 6000),
- {ok, << 0, "websocket_handle", 255 >>} = gen_tcp:recv(Socket, 0, 6000),
- {ok, << 0, "websocket_handle", 255 >>} = gen_tcp:recv(Socket, 0, 6000),
- %% We try to send another HTTP request to make sure
- %% the server closed the request.
- ok = gen_tcp:send(Socket, [
- << 255, 0 >>, %% Close websocket command.
- "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" %% Server should ignore it.
- ]),
- {ok, << 255, 0 >>} = gen_tcp:recv(Socket, 0, 6000),
- {error, closed} = gen_tcp:recv(Socket, 0, 6000),
- ok.
+ {ok, {http_response, {1, 1}, 400, _}, _}
+ = erlang:decode_packet(http, Handshake, []).
ws8(Config) ->
{port, Port} = lists:keyfind(port, 1, Config),
@@ -203,9 +172,9 @@ ws8(Config) ->
= gen_tcp:recv(Socket, 0, 6000),
{ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
= gen_tcp:recv(Socket, 0, 6000),
- ok = gen_tcp:send(Socket, << 1:1, 0:3, 9:4, 0:8 >>), %% ping
+ ok = gen_tcp:send(Socket, << 1:1, 0:3, 9:4, 1:1, 0:7, 0:32 >>), %% ping
{ok, << 1:1, 0:3, 10:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), %% pong
- ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), %% close
+ ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), %% close
{ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.
@@ -254,7 +223,7 @@ ws8_single_bytes(Config) ->
ok = gen_tcp:send(Socket, << 16#81 >>), %% send one byte
ok = timer:sleep(100), %% sleep for a period
ok = gen_tcp:send(Socket, << 16#85 >>), %% send another and so on
- ok = timer:sleep(100),
+ ok = timer:sleep(100),
ok = gen_tcp:send(Socket, << 16#37 >>),
ok = timer:sleep(100),
ok = gen_tcp:send(Socket, << 16#fa >>),
@@ -282,9 +251,9 @@ ws8_single_bytes(Config) ->
= gen_tcp:recv(Socket, 0, 6000),
{ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
= gen_tcp:recv(Socket, 0, 6000),
- ok = gen_tcp:send(Socket, << 1:1, 0:3, 9:4, 0:8 >>), %% ping
+ ok = gen_tcp:send(Socket, << 1:1, 0:3, 9:4, 1:1, 0:7, 0:32 >>), %% ping
{ok, << 1:1, 0:3, 10:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), %% pong
- ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), %% close
+ ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), %% close
{ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.
@@ -317,7 +286,7 @@ ws13(Config) ->
{ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>}
= gen_tcp:recv(Socket, 0, 6000),
%% binary (empty)
- ok = gen_tcp:send(Socket, << 1:1, 0:3, 2:4, 0:8 >>),
+ ok = gen_tcp:send(Socket, << 1:1, 0:3, 2:4, 1:1, 0:7, 0:32 >>),
{ok, << 1:1, 0:3, 2:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
%% binary
ok = gen_tcp:send(Socket, << 16#82, 16#85, 16#37, 16#fa, 16#21, 16#3d,
@@ -333,9 +302,9 @@ ws13(Config) ->
= gen_tcp:recv(Socket, 0, 6000),
{ok, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
= gen_tcp:recv(Socket, 0, 6000),
- ok = gen_tcp:send(Socket, << 1:1, 0:3, 9:4, 0:8 >>), %% ping
+ ok = gen_tcp:send(Socket, << 1:1, 0:3, 9:4, 1:1, 0:7, 0:32 >>), %% ping
{ok, << 1:1, 0:3, 10:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), %% pong
- ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), %% close
+ ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), %% close
{ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.
@@ -425,7 +394,7 @@ ws_send_many(Config) ->
<< 1:1, 0:3, 1:4, 0:1, 3:7, "one",
1:1, 0:3, 1:4, 0:1, 3:7, "two",
1:1, 0:3, 1:4, 0:1, 6:7, "seven!" >> = Many,
- ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), %% close
+ ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), %% close
{ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.
@@ -479,8 +448,7 @@ ws_text_fragments(Config) ->
<< 16#9f >>, << 16#4d >>, << 16#51 >>, << 16#58 >>]),
{ok, << 1:1, 0:3, 1:4, 0:1, 15:7, "HelloHelloHello" >>}
= gen_tcp:recv(Socket, 0, 6000),
-
- ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), %% close
+ ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), %% close
{ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.
@@ -507,7 +475,7 @@ ws_timeout_hibernate(Config) ->
{'Upgrade', "websocket"} = lists:keyfind('Upgrade', 1, Headers),
{"sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
= lists:keyfind("sec-websocket-accept", 1, Headers),
- {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
+ {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000),
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.
@@ -534,7 +502,7 @@ ws_timeout_cancel(Config) ->
{'Upgrade', "websocket"} = lists:keyfind('Upgrade', 1, Headers),
{"sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
= lists:keyfind("sec-websocket-accept", 1, Headers),
- {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
+ {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000),
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.
@@ -547,41 +515,28 @@ ws_timeout_reset(Config) ->
"GET /ws_timeout_cancel HTTP/1.1\r\n"
"Host: localhost\r\n"
"Connection: Upgrade\r\n"
- "Upgrade: WebSocket\r\n"
- "Origin: http://localhost\r\n"
- "Sec-Websocket-Key1: Y\" 4 1Lj!957b8@0H756!i\r\n"
- "Sec-Websocket-Key2: 1711 M;4\\74 80<6\r\n"
+ "Upgrade: websocket\r\n"
+ "Sec-WebSocket-Origin: http://localhost\r\n"
+ "Sec-Websocket-Version: 13\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
"\r\n"]),
{ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
- {ok, {http_response, {1, 1}, 101, "WebSocket Protocol Handshake"}, Rest}
+ {ok, {http_response, {1, 1}, 101, "Switching Protocols"}, Rest}
= erlang:decode_packet(http, Handshake, []),
[Headers, <<>>] = websocket_headers(
erlang:decode_packet(httph, Rest, []), []),
{'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers),
- {'Upgrade', "WebSocket"} = lists:keyfind('Upgrade', 1, Headers),
- {"sec-websocket-location", "ws://localhost/ws_timeout_cancel"}
- = lists:keyfind("sec-websocket-location", 1, Headers),
- {"sec-websocket-origin", "http://localhost"}
- = lists:keyfind("sec-websocket-origin", 1, Headers),
- ok = gen_tcp:send(Socket, <<15,245,8,18,2,204,133,33>>),
- {ok, Body} = gen_tcp:recv(Socket, 0, 6000),
- <<169,244,191,103,146,33,149,59,74,104,67,5,99,118,171,236>> = Body,
- ok = gen_tcp:send(Socket, << 0, "msg sent", 255 >>),
- {ok, << 0, "msg sent", 255 >>}
- = gen_tcp:recv(Socket, 0, 6000),
- ok = timer:sleep(500),
- ok = gen_tcp:send(Socket, << 0, "msg sent", 255 >>),
- {ok, << 0, "msg sent", 255 >>}
- = gen_tcp:recv(Socket, 0, 6000),
- ok = timer:sleep(500),
- ok = gen_tcp:send(Socket, << 0, "msg sent", 255 >>),
- {ok, << 0, "msg sent", 255 >>}
- = gen_tcp:recv(Socket, 0, 6000),
- ok = timer:sleep(500),
- ok = gen_tcp:send(Socket, << 0, "msg sent", 255 >>),
- {ok, << 0, "msg sent", 255 >>}
- = gen_tcp:recv(Socket, 0, 6000),
- {ok, << 255, 0 >>} = gen_tcp:recv(Socket, 0, 6000),
+ {'Upgrade', "websocket"} = lists:keyfind('Upgrade', 1, Headers),
+ {"sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
+ = lists:keyfind("sec-websocket-accept", 1, Headers),
+ [begin
+ ok = gen_tcp:send(Socket, << 16#81, 16#85, 16#37, 16#fa, 16#21, 16#3d,
+ 16#7f, 16#9f, 16#4d, 16#51, 16#58 >>),
+ {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>}
+ = gen_tcp:recv(Socket, 0, 6000),
+ ok = timer:sleep(500)
+ end || _ <- [1, 2, 3, 4]],
+ {ok, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>} = gen_tcp:recv(Socket, 0, 6000),
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.
@@ -609,7 +564,7 @@ ws_upgrade_with_opts(Config) ->
= lists:keyfind("sec-websocket-accept", 1, Headers),
{ok, Response} = gen_tcp:recv(Socket, 9, 6000),
<< 1:1, 0:3, 1:4, 0:1, 7:7, "success" >> = Response,
- ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), %% close
+ ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), %% close
{ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
ok.
diff --git a/test/ws_timeout_cancel_handler.erl b/test/ws_timeout_cancel_handler.erl
index ee75d9b..68b0468 100644
--- a/test/ws_timeout_cancel_handler.erl
+++ b/test/ws_timeout_cancel_handler.erl
@@ -1,21 +1,14 @@
%% Feel free to use, reuse and abuse the code in this file.
-module(ws_timeout_cancel_handler).
--behaviour(cowboy_http_handler).
-behaviour(cowboy_websocket_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3]).
-export([websocket_init/3, websocket_handle/3,
websocket_info/3, websocket_terminate/3]).
init(_Any, _Req, _Opts) ->
{upgrade, protocol, cowboy_websocket}.
-handle(_Req, _State) ->
- exit(badarg).
-
-terminate(_Req, _State) ->
- exit(badarg).
-
websocket_init(_TransportName, Req, _Opts) ->
erlang:start_timer(500, self(), should_not_cancel_timer),
{ok, Req, undefined, 1000}.
diff --git a/test/ws_timeout_hibernate_handler.erl b/test/ws_timeout_hibernate_handler.erl
index ac6ee4f..41b9edd 100644
--- a/test/ws_timeout_hibernate_handler.erl
+++ b/test/ws_timeout_hibernate_handler.erl
@@ -1,21 +1,14 @@
%% Feel free to use, reuse and abuse the code in this file.
-module(ws_timeout_hibernate_handler).
--behaviour(cowboy_http_handler).
-behaviour(cowboy_websocket_handler).
--export([init/3, handle/2, terminate/2]).
+-export([init/3]).
-export([websocket_init/3, websocket_handle/3,
websocket_info/3, websocket_terminate/3]).
init(_Any, _Req, _Opts) ->
{upgrade, protocol, cowboy_websocket}.
-handle(_Req, _State) ->
- exit(badarg).
-
-terminate(_Req, _State) ->
- exit(badarg).
-
websocket_init(_TransportName, Req, _Opts) ->
{ok, Req, undefined, 1000, hibernate}.