diff options
96 files changed, 3586 insertions, 1540 deletions
@@ -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 @@ -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. @@ -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 Binary files differnew file mode 100644 index 0000000..1fc4788 --- /dev/null +++ b/examples/static/priv/small.mp4 diff --git a/examples/static/priv/small.ogv b/examples/static/priv/small.ogv Binary files differnew file mode 100644 index 0000000..6409d6e --- /dev/null +++ b/examples/static/priv/small.ogv 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}. |