aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml3
-rw-r--r--AUTHORS13
-rw-r--r--CHANGELOG.md213
-rw-r--r--Makefile5
-rw-r--r--README.md58
-rw-r--r--include/http.hrl20
-rw-r--r--rebar.config2
-rw-r--r--src/cowboy.app.src2
-rw-r--r--src/cowboy.erl11
-rw-r--r--src/cowboy_acceptor.erl2
-rw-r--r--src/cowboy_app.erl2
-rw-r--r--src/cowboy_clock.erl26
-rw-r--r--src/cowboy_cookies.erl7
-rw-r--r--src/cowboy_dispatcher.erl20
-rw-r--r--src/cowboy_http.erl240
-rw-r--r--src/cowboy_http_protocol.erl102
-rw-r--r--src/cowboy_http_req.erl252
-rw-r--r--src/cowboy_http_rest.erl872
-rw-r--r--src/cowboy_http_static.erl461
-rw-r--r--src/cowboy_http_websocket.erl149
-rw-r--r--src/cowboy_protocol.erl6
-rw-r--r--test/http_SUITE.erl556
-rw-r--r--test/http_handler_errors.erl40
-rw-r--r--test/http_handler_multipart.erl2
-rw-r--r--test/http_handler_set_resp.erl33
-rw-r--r--test/http_handler_stream_body.erl24
-rw-r--r--test/rest_simple_resource.erl12
-rw-r--r--test/websocket_handler.erl2
-rw-r--r--test/websocket_handler_init_shutdown.erl2
-rw-r--r--test/ws_SUITE.erl318
30 files changed, 2914 insertions, 541 deletions
diff --git a/.travis.yml b/.travis.yml
index 9f9d89b..f04becf 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,7 @@
language: erlang
otp_release:
+ - R15B
- R14B04
- R14B03
- R14B02
- - R14B01
- - R14B
script: "make tests"
diff --git a/AUTHORS b/AUTHORS
index 29630e6..a07a69d 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -2,8 +2,17 @@ Cowboy is available thanks to the work of:
Loïc Hoguin
Anthony Ramine
+Magnus Klaar
+Paul Oliver
+Steven Gravell
Tom Burdick
-Hans Ulrich Niedermann
Hunter Morris
-Steven Gravell
Yurii Rashkovskii
+Ali Sabil
+Hans Ulrich Niedermann
+Jesper Louis Andersen
+Mathieu Lecarme
+Max Lapshin
+Michiel Hakvoort
+Ori Bar
+Alisdair Sullivan
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..a4b815b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,213 @@
+CHANGELOG
+=========
+
+0.4.0
+-----
+
+* Set the cowboy_listener process priority to high
+
+ As it is the central process used by all incoming requests
+ we need to set its priority to high to avoid timeouts that
+ would happen otherwise when reaching a huge number of
+ concurrent requests.
+
+* Add cowboy:child_spec/6 for embedding in other applications
+
+* Add cowboy_http_rest, an experimental REST protocol support
+
+ Based on the Webmachine diagram and documentation. It is a
+ new implementation, not a port, therefore a few changes have
+ been made. However all the callback names are the same and
+ should behave similarly to Webmachine.
+
+ There is currently no documentation other than the Webmachine
+ resource documentation and the comments found in cowboy_http_rest,
+ which itself should be fairly easy to read and understand.
+
+* Add cowboy_http_static, an experimental static file handler
+
+ Makes use of the aforementioned REST protocol support to
+ deliver files with proper content type and cache headers.
+
+ Note that this uses the new file:sendfile support when
+ appropriate, which currently requires the VM to be started
+ with the +A option defined, else errors may randomly appear.
+
+* Add cowboy_bstr module for binary strings related functions
+
+* Add cowboy_http module for HTTP parsing functions
+
+ This module so far contains various functions for HTTP header
+ parsing along with URL encoding and decoding.
+
+* Remove quoted from the default dependencies
+
+ This should make Cowboy much easier to compile and use by default.
+ It is of course still possible to use quoted as your URL decoding
+ library in Cowboy thanks to the newly added urldecode option.
+
+* Fix supervisor spec for non dynamic modules to allow upgrades to complete
+
+* Add cowboy:accept_ack/1 for a cleaner handling of the shoot message
+
+ Before, when the listener accepted a connection, the newly created
+ process was waiting for a message containing the atom 'shoot' before
+ proceeding. This has been replaced by the cowboy:accept_ack/1 function.
+
+ This function should be used where 'shoot' was received because the
+ contents of the message have changed (and could change again in the
+ distant future).
+
+* Update binary parsing expressions to avoid hype crashes
+
+ More specifically, /bits was replaced by /binary.
+
+* Rename the type cowboy_dispatcher:path_tokens/0 to tokens/0
+
+* Remove the cowboy_clock:date/0, time/0 and datetime/0 types
+
+ The calendar module exports those same types properly since R14B04.
+
+* Add cacertfile configuration option to cowboy_ssl_transport
+
+* Add cowboy_protocol behaviour
+
+* Remove -Wbehaviours dialyzer option unavailable in R15B
+
+* Many tests and specs improvements
+
+### cowboy_http_req
+
+* Fix a crash when reading the request body
+
+* Add parse_header/2 and parse_header/3
+
+ The following headers can now be semantically parsed: Connection, Accept,
+ Accept-Charset, Accept-Encoding, Accept-Language, Content-Length,
+ Content-Type, If-Match, If-None-Match, If-Modified-Since,
+ If-Unmodified-Since, Upgrade
+
+* Add set_resp_header/3, set_resp_cookie/4 and set_resp_body/2
+
+ These functions allow handlers to set response headers and body
+ without having to reply directly.
+
+* Add set_resp_body_fun/3
+
+ This function allows handlers to stream the body of the response
+ using the given fun. The size of the response must be known beforehand.
+
+* Add transport/1 to obtain the transport and socket for the request
+
+ This allows handlers to have low-level socket access in those cases
+ where they do need it, like when streaming a response body with
+ set_resp_body_fun/3.
+
+* Add peer_addr/1
+
+ This function tries to guess the real peer IP based on the HTTP
+ headers received.
+
+* Add meta/2 and meta/3 to save useful protocol information
+
+ Currently used to save the Websocket protocol version currently used,
+ and to save request information in the REST protocol handler.
+
+* Add reply/2 and reply/3 aliases to reply/4
+
+* Add upgrade_reply/3 for protocol upgrades
+
+### cowboy_http_protocol
+
+* Add the {urldecode, fun urldecode/2} option
+
+ Added when quoted was removed from the default build. Can be used to
+ tell Cowboy to use quoted or any other URL decoding routine.
+
+* Add the max_keepalive option
+
+* Add the max_line_length option
+
+* Allow HTTP handlers to stop during init/3
+
+ To do so they can return {shutdown, Req, State}.
+
+* Add loops support in HTTP handlers for proper long-polling support
+
+ A loop can be entered by returning either of {loop, Req, State},
+ {loop, Req, State, hibernate}, {loop, Req, State, Timeout} or
+ {loop, Req, State, Timeout, hibernate} from init/3.
+
+ Loops are useful when we cannot reply immediately and instead
+ are waiting for an Erlang message to be able to complete the request,
+ as would typically be done for long-polling.
+
+ Loop support in the protocol means that timeouts and hibernating
+ are well tested and handled so you can use those options without
+ worrying. It is recommended to set the timeout option.
+
+ When a loop is started, handle/2 will never be called so it does
+ not need to be defined. When the request process receives an Erlang
+ message, it will call the info/3 function with the message as the
+ first argument.
+
+ Like in OTP, you do need to set timeout and hibernate again when
+ returning from info/3 to enable them until the next call.
+
+* Fix the sending of 500 errors when handlers crash
+
+ Now we send an error response when no response has been sent,
+ and do nothing more than close the connection if anything
+ did get sent.
+
+* Fix a crash when the server is sent HTTP responses
+
+* Fix HTTP timeouts handling when the Request-Line wasn't received
+
+* Fix the handling of the max number of empty lines between requests
+
+* Fix the handling of HEAD requests
+
+* Fix HTTP/1.0 Host header handling
+
+* Reply status 400 if we receive an unexpected value or error for headers
+
+* Properly close when the application sends "Connection: close" header
+
+* Close HTTP connections on all errors
+
+* Improve the error message for HTTP handlers
+
+### cowboy_http_websocket
+
+* Add websocket support for all versions up to RFC 6455
+
+ Support isn't perfect yet according to the specifications, but
+ is working against all currently known client implementations.
+
+* Allow websocket_init/3 to return with the hibernate option set
+
+* Add {shutdown, Req} return value to websocket_init/3 to fail an upgrade
+
+* Fix websocket timeout handling
+
+* Fix error messages: wrong callback name was reported on error
+
+* Fix byte-by-byte websocket handling
+
+* Fix an issue when using hixie-76 with certain proxies
+
+* Fix a crash in the hixie-76 handshake
+
+* Fix the handshake when SSL is used on port 443
+
+* Fix a crash in the handshake when cowboy_http_req:compact/1 is used
+
+* Fix handshake when a query string is present
+
+* Fix a crash when the Upgrade header contains more than one token
+
+0.2.0
+-----
+
+* Initial release.
diff --git a/Makefile b/Makefile
index 8ea1eb4..e5524f4 100644
--- a/Makefile
+++ b/Makefile
@@ -22,15 +22,14 @@ eunit:
@$(REBAR) eunit skip_deps=true
ct:
- @$(REBAR) ct
+ @$(REBAR) ct skip_deps=true
build-plt:
@$(DIALYZER) --build_plt --output_plt .cowboy_dialyzer.plt \
--apps kernel stdlib sasl inets crypto public_key ssl
dialyze:
- @$(DIALYZER) --src src --plt .cowboy_dialyzer.plt \
- -Wbehaviours -Werror_handling \
+ @$(DIALYZER) --src src --plt .cowboy_dialyzer.plt -Werror_handling \
-Wrace_conditions -Wunmatched_returns # -Wunderspecs
docs:
diff --git a/README.md b/README.md
index 2e5edcb..ce769ba 100644
--- a/README.md
+++ b/README.md
@@ -94,7 +94,6 @@ Following is an example of a "Hello World!" HTTP handler.
``` erlang
-module(my_handler).
--behaviour(cowboy_http_handler).
-export([init/3, handle/2, terminate/2]).
init({tcp, http}, Req, Opts) ->
@@ -108,6 +107,46 @@ 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. An 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_http_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.
+
+**Note**: versions prior to `0.4.0` used the
+[quoted](https://github.com/klaar/quoted.erl) library instead of the built in
+`cowboy_http:urldecode/2` function. If you want to retain this you must add it
+as a dependency to your application and add the following cowboy_http_protocol
+option:
+
+``` erlang
+ {urldecode, {fun quoted:from_url/2, quoted:make([])}}
+```
+
Continue reading to learn how to dispatch rules and handle requests.
Dispatch rules
@@ -179,21 +218,13 @@ Websocket would look like this:
``` erlang
-module(my_ws_handler).
--behaviour(cowboy_http_handler).
--behaviour(cowboy_http_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({tcp, http}, Req, Opts) ->
{upgrade, protocol, cowboy_http_websocket}.
-handle(Req, State) ->
- error(foo). %% Will never be called.
-
-terminate(Req, State) ->
- error(foo). %% Same for that one.
-
websocket_init(TransportName, Req, _Opts) ->
erlang:start_timer(1000, self(), <<"Hello!">>),
{ok, Req, undefined_state}.
@@ -236,9 +267,10 @@ is the pid to the listener's gen_server, managing the connections. Socket is of
course the client socket; Transport is the module name of the chosen transport
handler and Opts is protocol options defined when starting the listener.
-After initializing your protocol, it is recommended to wait to receive a message
-containing the atom 'shoot', as it will ensure Cowboy has been able to fully
-initialize the socket. Anything you do past this point is up to you!
+After initializing your protocol, it is recommended to call the
+function cowboy:accept_ack/1 with the ListenerPid as argument,
+as it will ensure Cowboy has been able to fully initialize the socket.
+Anything you do past this point is up to you!
If you need to change some socket options, like enabling raw mode for example,
you can call the <em>Transport:setopts/2</em> function. It is the protocol's
diff --git a/include/http.hrl b/include/http.hrl
index 3b4e938..a10b120 100644
--- a/include/http.hrl
+++ b/include/http.hrl
@@ -33,16 +33,11 @@
| 'Expires' | 'Last-Modified' | 'Accept-Ranges' | 'Set-Cookie'
| 'Set-Cookie2' | 'X-Forwarded-For' | 'Cookie' | 'Keep-Alive'
| 'Proxy-Connection' | binary().
--type http_headers() :: list({http_header(), binary()}).
+-type http_headers() :: list({http_header(), iodata()}).
-type http_cookies() :: list({binary(), binary()}).
-type http_status() :: non_neg_integer() | binary().
-
-%% @todo Improve this type.
--type multipart_data() ::
- {headers, http_headers()} |
- {data, binary()} |
- end_of_part |
- eof.
+-type http_resp_body() :: iodata() | {non_neg_integer(),
+ fun(() -> {sent, non_neg_integer()})}.
-record(http_req, {
%% Transport.
@@ -51,6 +46,7 @@
connection = keepalive :: keepalive | close,
%% Request.
+ pid = undefined :: pid(),
method = 'GET' :: http_method(),
version = {1, 1} :: http_version(),
peer = undefined :: undefined | {inet:ip_address(), inet:ip_port()},
@@ -67,6 +63,7 @@
headers = [] :: http_headers(),
p_headers = [] :: [any()], %% @todo Improve those specs.
cookies = undefined :: undefined | http_cookies(),
+ meta = [] :: [{atom(), any()}],
%% Request body.
body_state = waiting :: waiting | done |
@@ -74,5 +71,10 @@
buffer = <<>> :: binary(),
%% Response.
- resp_state = waiting :: locked | waiting | chunks | done
+ resp_state = waiting :: locked | waiting | chunks | done,
+ resp_headers = [] :: http_headers(),
+ resp_body = <<>> :: http_resp_body(),
+
+ %% Functions.
+ urldecode :: {fun((binary(), T) -> binary()), T}
}).
diff --git a/rebar.config b/rebar.config
index fe95b2c..82d1fca 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,7 +1,5 @@
{cover_enabled, true}.
{deps, [
- {quoted, "1.2.*",
- {git, "git://github.com/klaar/quoted.erl.git", {tag, "1.2.0"}}},
{proper, "1.0",
{git, "git://github.com/manopapad/proper.git", {tag, "v1.0"}}}
]}.
diff --git a/src/cowboy.app.src b/src/cowboy.app.src
index 264607f..9b3ee50 100644
--- a/src/cowboy.app.src
+++ b/src/cowboy.app.src
@@ -14,7 +14,7 @@
{application, cowboy, [
{description, "Small, fast, modular HTTP server."},
- {vsn, "0.3.0"},
+ {vsn, "0.5.0"},
{modules, []},
{registered, [cowboy_clock, cowboy_sup]},
{applications, [
diff --git a/src/cowboy.erl b/src/cowboy.erl
index 9b07921..6defeea 100644
--- a/src/cowboy.erl
+++ b/src/cowboy.erl
@@ -15,7 +15,7 @@
%% @doc Cowboy API to start and stop listeners.
-module(cowboy).
--export([start_listener/6, stop_listener/1, child_spec/6]).
+-export([start_listener/6, stop_listener/1, child_spec/6, accept_ack/1]).
%% @doc Start a listener for the given transport and protocol.
%%
@@ -61,6 +61,7 @@ stop_listener(Ref) ->
end.
%% @doc Return a child spec suitable for embedding.
+%%
%% When you want to embed cowboy in another application, you can use this
%% function to create a <em>ChildSpec</em> suitable for use in a supervisor.
%% The parameters are the same as in <em>start_listener/6</em> but rather
@@ -74,3 +75,11 @@ child_spec(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts)
{{cowboy_listener_sup, Ref}, {cowboy_listener_sup, start_link, [
NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts
]}, permanent, 5000, supervisor, [cowboy_listener_sup]}.
+
+%% @doc Acknowledge the accepted connection.
+%%
+%% Effectively used to make sure the socket control has been given to
+%% the protocol process before starting to use it.
+-spec accept_ack(pid()) -> ok.
+accept_ack(ListenerPid) ->
+ receive {shoot, ListenerPid} -> ok end.
diff --git a/src/cowboy_acceptor.erl b/src/cowboy_acceptor.erl
index f2b603e..4cb9fa7 100644
--- a/src/cowboy_acceptor.erl
+++ b/src/cowboy_acceptor.erl
@@ -40,7 +40,7 @@ acceptor(LSocket, Transport, Protocol, Opts, MaxConns, ListenerPid, ReqsSup) ->
Transport:controlling_process(CSocket, Pid),
{ok, NbConns} = cowboy_listener:add_connection(ListenerPid,
default, Pid),
- Pid ! shoot,
+ Pid ! {shoot, ListenerPid},
limit_reqs(ListenerPid, NbConns, MaxConns);
{error, timeout} ->
ignore;
diff --git a/src/cowboy_app.erl b/src/cowboy_app.erl
index 0ff08f0..c7cefe4 100644
--- a/src/cowboy_app.erl
+++ b/src/cowboy_app.erl
@@ -46,7 +46,7 @@ profile_output() ->
consider_profiling() ->
case application:get_env(profile) of
{ok, true} ->
- eprof:start(),
+ {ok, _Pid} = eprof:start(),
eprof:start_profiling([self()]);
_ ->
not_profiling
diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl
index 3597bdd..c699f4f 100644
--- a/src/cowboy_clock.erl
+++ b/src/cowboy_clock.erl
@@ -25,23 +25,8 @@
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]). %% gen_server.
-%% @todo Use calendar types whenever they get exported.
--type year() :: non_neg_integer().
--type month() :: 1..12.
--type day() :: 1..31.
--type hour() :: 0..23.
--type minute() :: 0..59.
--type second() :: 0..59.
--type daynum() :: 1..7.
-
--type date() :: {year(), month(), day()}.
--type time() :: {hour(), minute(), second()}.
-
--type datetime() :: {date(), time()}.
--export_type([date/0, time/0, datetime/0]).
-
-record(state, {
- universaltime = undefined :: undefined | datetime(),
+ universaltime = undefined :: undefined | calendar:datetime(),
rfc1123 = <<>> :: binary(),
tref = undefined :: undefined | timer:tref()
}).
@@ -74,7 +59,7 @@ rfc1123() ->
%%
%% This format is used in the <em>'Set-Cookie'</em> header sent with
%% HTTP responses.
--spec rfc2109(datetime()) -> binary().
+-spec rfc2109(calendar:datetime()) -> binary().
rfc2109(LocalTime) ->
{{YYYY,MM,DD},{Hour,Min,Sec}} =
case calendar:local_time_to_universal_time_dst(LocalTime) of
@@ -145,7 +130,8 @@ code_change(_OldVsn, State, _Extra) ->
%% Internal.
--spec update_rfc1123(binary(), undefined | datetime(), datetime()) -> binary().
+-spec update_rfc1123(binary(), undefined | calendar:datetime(),
+ calendar:datetime()) -> binary().
update_rfc1123(Bin, Now, Now) ->
Bin;
update_rfc1123(<< Keep:23/binary, _/bits >>,
@@ -184,7 +170,7 @@ pad_int(X) when X < 10 ->
pad_int(X) ->
list_to_binary(integer_to_list(X)).
--spec weekday(daynum()) -> <<_:24>>.
+-spec weekday(1..7) -> <<_:24>>.
weekday(1) -> <<"Mon">>;
weekday(2) -> <<"Tue">>;
weekday(3) -> <<"Wed">>;
@@ -193,7 +179,7 @@ weekday(5) -> <<"Fri">>;
weekday(6) -> <<"Sat">>;
weekday(7) -> <<"Sun">>.
--spec month(month()) -> <<_:24>>.
+-spec month(1..12) -> <<_:24>>.
month( 1) -> <<"Jan">>;
month( 2) -> <<"Feb">>;
month( 3) -> <<"Mar">>;
diff --git a/src/cowboy_cookies.erl b/src/cowboy_cookies.erl
index 9c6c4c3..6818a86 100644
--- a/src/cowboy_cookies.erl
+++ b/src/cowboy_cookies.erl
@@ -23,7 +23,7 @@
-type kv() :: {Name::binary(), Value::binary()}.
-type kvlist() :: [kv()].
-type cookie_option() :: {max_age, integer()}
- | {local_time, {cowboy_clock:date(), cowboy_clock:time()}}
+ | {local_time, calendar:datetime()}
| {domain, binary()} | {path, binary()}
| {secure, true | false} | {http_only, true | false}.
-export_type([kv/0, kvlist/0, cookie_option/0]).
@@ -171,13 +171,12 @@ quote(V0) ->
V
end.
--spec add_seconds(integer(), cowboy_clock:datetime())
- -> cowboy_clock:datetime().
+-spec add_seconds(integer(), calendar:datetime()) -> calendar:datetime().
add_seconds(Secs, LocalTime) ->
Greg = calendar:datetime_to_gregorian_seconds(LocalTime),
calendar:gregorian_seconds_to_datetime(Greg + Secs).
--spec age_to_cookie_date(integer(), cowboy_clock:datetime()) -> binary().
+-spec age_to_cookie_date(integer(), calendar:datetime()) -> binary().
age_to_cookie_date(Age, LocalTime) ->
cowboy_clock:rfc2109(add_seconds(Age, LocalTime)).
diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl
index 67ea34b..22f6e1e 100644
--- a/src/cowboy_dispatcher.erl
+++ b/src/cowboy_dispatcher.erl
@@ -16,7 +16,7 @@
%% @doc Dispatch requests according to a hostname and path.
-module(cowboy_dispatcher).
--export([split_host/1, split_path/1, match/3]). %% API.
+-export([split_host/1, split_path/2, match/3]). %% API.
-type bindings() :: list({atom(), binary()}).
-type tokens() :: list(binary()).
@@ -50,21 +50,22 @@ split_host(Host) ->
%% 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(), binary(), binary()}.
-split_path(Path) ->
+-spec split_path(binary(), fun((binary()) -> binary())) ->
+ {tokens(), binary(), binary()}.
+split_path(Path, URLDec) ->
case binary:split(Path, <<"?">>) of
- [Path] -> {do_split_path(Path, <<"/">>), Path, <<>>};
+ [Path] -> {do_split_path(Path, <<"/">>, URLDec), Path, <<>>};
[<<>>, Qs] -> {[], <<>>, Qs};
- [Path2, Qs] -> {do_split_path(Path2, <<"/">>), Path2, Qs}
+ [Path2, Qs] -> {do_split_path(Path2, <<"/">>, URLDec), Path2, Qs}
end.
--spec do_split_path(binary(), <<_:8>>) -> tokens().
-do_split_path(RawPath, Separator) ->
+-spec do_split_path(binary(), <<_:8>>, fun((binary()) -> binary())) -> tokens().
+do_split_path(RawPath, Separator, URLDec) ->
EncodedPath = case binary:split(RawPath, Separator, [global, trim]) of
[<<>>|Path] -> Path;
Path -> Path
end,
- [quoted:from_url(Token) || Token <- EncodedPath].
+ [URLDec(Token) || Token <- EncodedPath].
%% @doc Match hostname tokens and path tokens against dispatch rules.
%%
@@ -224,7 +225,8 @@ split_path_test_() ->
[<<"users">>, <<"a b">>, <<"c!d">>],
<<"/users/a+b/c%21d">>, <<"e+f=g+h">>}
],
- [{P, fun() -> {R, RawP, Qs} = split_path(P) end}
+ URLDecode = fun(Bin) -> cowboy_http:urldecode(Bin, crash) end,
+ [{P, fun() -> {R, RawP, Qs} = split_path(P, URLDecode) end}
|| {P, R, RawP, Qs} <- Tests].
match_test_() ->
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index 6404379..7c1a2d3 100644
--- a/src/cowboy_http.erl
+++ b/src/cowboy_http.erl
@@ -23,7 +23,8 @@
whitespace/2, digits/1, token/2, token_ci/2, quoted_string/2]).
%% Interpretation.
--export([connection_to_atom/1]).
+-export([connection_to_atom/1, urldecode/1, urldecode/2, urlencode/1,
+ urlencode/2]).
-include("include/http.hrl").
-include_lib("eunit/include/eunit.hrl").
@@ -57,11 +58,11 @@ list(Data, Fun) ->
list(Data, Fun, Acc) ->
whitespace(Data,
fun (<<>>) -> Acc;
- (<< $,, Rest/bits >>) -> list(Rest, Fun, Acc);
+ (<< $,, Rest/binary >>) -> list(Rest, Fun, Acc);
(Rest) -> Fun(Rest,
fun (D, I) -> whitespace(D,
fun (<<>>) -> [I|Acc];
- (<< $,, R/bits >>) -> list(R, Fun, [I|Acc]);
+ (<< $,, R/binary >>) -> list(R, Fun, [I|Acc]);
(_Any) -> {error, badarg}
end)
end)
@@ -80,7 +81,7 @@ content_type(Data) ->
-> any().
content_type_params(Data, Fun, Acc) ->
whitespace(Data,
- fun (<< $;, Rest/bits >>) -> content_type_param(Rest, Fun, Acc);
+ fun (<< $;, Rest/binary >>) -> content_type_param(Rest, Fun, Acc);
(<<>>) -> Fun(lists:reverse(Acc));
(_Rest) -> {error, badarg}
end).
@@ -92,7 +93,7 @@ content_type_param(Data, Fun, Acc) ->
fun (Rest) ->
token_ci(Rest,
fun (_Rest2, <<>>) -> {error, badarg};
- (<< $=, Rest2/bits >>, Attr) ->
+ (<< $=, Rest2/binary >>, Attr) ->
word(Rest2,
fun (Rest3, Value) ->
content_type_params(Rest3, Fun,
@@ -114,7 +115,7 @@ media_range(Data, Fun) ->
[{binary(), binary()}]) -> any().
media_range_params(Data, Fun, Type, SubType, Acc) ->
whitespace(Data,
- fun (<< $;, Rest/bits >>) ->
+ fun (<< $;, Rest/binary >>) ->
whitespace(Rest,
fun (Rest2) ->
media_range_param_attr(Rest2, Fun, Type, SubType, Acc)
@@ -127,7 +128,7 @@ media_range_params(Data, Fun, Type, SubType, Acc) ->
media_range_param_attr(Data, Fun, Type, SubType, Acc) ->
token_ci(Data,
fun (_Rest, <<>>) -> {error, badarg};
- (<< $=, Rest/bits >>, Attr) ->
+ (<< $=, Rest/binary >>, Attr) ->
media_range_param_value(Rest, Fun, Type, SubType, Acc, Attr)
end).
@@ -150,7 +151,7 @@ media_range_param_value(Data, Fun, Type, SubType, Acc, Attr) ->
media_type(Data, Fun) ->
token_ci(Data,
fun (_Rest, <<>>) -> {error, badarg};
- (<< $/, Rest/bits >>, Type) ->
+ (<< $/, Rest/binary >>, Type) ->
token_ci(Rest,
fun (_Rest2, <<>>) -> {error, badarg};
(Rest2, SubType) -> Fun(Rest2, Type, SubType)
@@ -163,7 +164,7 @@ media_type(Data, Fun) ->
[{binary(), binary()} | binary()]) -> any().
accept_ext(Data, Fun, Type, SubType, Params, Quality, Acc) ->
whitespace(Data,
- fun (<< $;, Rest/bits >>) ->
+ fun (<< $;, Rest/binary >>) ->
whitespace(Rest,
fun (Rest2) ->
accept_ext_attr(Rest2, Fun,
@@ -180,7 +181,7 @@ accept_ext(Data, Fun, Type, SubType, Params, Quality, Acc) ->
accept_ext_attr(Data, Fun, Type, SubType, Params, Quality, Acc) ->
token_ci(Data,
fun (_Rest, <<>>) -> {error, badarg};
- (<< $=, Rest/bits >>, Attr) ->
+ (<< $=, Rest/binary >>, Attr) ->
accept_ext_value(Rest, Fun, Type, SubType, Params,
Quality, Acc, Attr);
(Rest, Attr) ->
@@ -213,7 +214,7 @@ conneg(Data, Fun) ->
%% @doc Parse a language range, followed by an optional quality value.
-spec language_range(binary(), fun()) -> any().
-language_range(<< $*, Rest/bits >>, Fun) ->
+language_range(<< $*, Rest/binary >>, Fun) ->
language_range_ret(Rest, Fun, '*');
language_range(Data, Fun) ->
language_tag(Data,
@@ -221,7 +222,7 @@ language_range(Data, Fun) ->
language_range_ret(Rest, Fun, LanguageTag)
end).
--spec language_range_ret(binary(), fun(), '*' | {binary(), binary()}) -> any().
+-spec language_range_ret(binary(), fun(), '*' | {binary(), [binary()]}) -> any().
language_range_ret(Data, Fun, LanguageTag) ->
maybe_qparam(Data,
fun (Rest, Quality) ->
@@ -233,10 +234,10 @@ language_tag(Data, Fun) ->
alpha(Data,
fun (_Rest, Tag) when byte_size(Tag) =:= 0; byte_size(Tag) > 8 ->
{error, badarg};
- (<< $-, Rest/bits >>, Tag) ->
+ (<< $-, Rest/binary >>, Tag) ->
language_subtag(Rest, Fun, Tag, []);
(Rest, Tag) ->
- Fun(Rest, {Tag, []})
+ Fun(Rest, Tag)
end).
-spec language_subtag(binary(), fun(), binary(), [binary()]) -> any().
@@ -244,16 +245,18 @@ language_subtag(Data, Fun, Tag, Acc) ->
alpha(Data,
fun (_Rest, SubTag) when byte_size(SubTag) =:= 0;
byte_size(SubTag) > 8 -> {error, badarg};
- (<< $-, Rest/bits >>, SubTag) ->
+ (<< $-, Rest/binary >>, SubTag) ->
language_subtag(Rest, Fun, Tag, [SubTag|Acc]);
(Rest, SubTag) ->
- Fun(Rest, {Tag, lists:reverse([SubTag|Acc])})
+ %% Rebuild the full tag now that we know it's correct
+ Sub = << << $-, S/binary >> || S <- lists:reverse([SubTag|Acc]) >>,
+ Fun(Rest, << Tag/binary, Sub/binary >>)
end).
-spec maybe_qparam(binary(), fun()) -> any().
maybe_qparam(Data, Fun) ->
whitespace(Data,
- fun (<< $;, Rest/bits >>) ->
+ fun (<< $;, Rest/binary >>) ->
whitespace(Rest,
fun (Rest2) ->
qparam(Rest2, Fun)
@@ -264,12 +267,12 @@ maybe_qparam(Data, Fun) ->
%% @doc Parse a quality parameter string (for example q=0.500).
-spec qparam(binary(), fun()) -> any().
-qparam(<< Q, $=, Data/bits >>, Fun) when Q =:= $q; Q =:= $Q ->
+qparam(<< Q, $=, Data/binary >>, Fun) when Q =:= $q; Q =:= $Q ->
qvalue(Data, Fun).
%% @doc Parse either a list of entity tags or a "*".
-spec entity_tag_match(binary()) -> any().
-entity_tag_match(<< $*, Rest/bits >>) ->
+entity_tag_match(<< $*, Rest/binary >>) ->
whitespace(Rest,
fun (<<>>) -> '*';
(_Any) -> {error, badarg}
@@ -279,7 +282,7 @@ entity_tag_match(Data) ->
%% @doc Parse an entity-tag.
-spec entity_tag(binary(), fun()) -> any().
-entity_tag(<< "W/", Rest/bits >>, Fun) ->
+entity_tag(<< "W/", Rest/binary >>, Fun) ->
opaque_tag(Rest, Fun, weak);
entity_tag(Data, Fun) ->
opaque_tag(Data, Fun, strong).
@@ -320,11 +323,11 @@ http_date(Data) ->
-spec rfc1123_date(binary()) -> any().
rfc1123_date(Data) ->
wkday(Data,
- fun (<< ", ", Rest/bits >>, _WkDay) ->
+ fun (<< ", ", Rest/binary >>, _WkDay) ->
date1(Rest,
- fun (<< " ", Rest2/bits >>, Date) ->
+ fun (<< " ", Rest2/binary >>, Date) ->
time(Rest2,
- fun (<< " GMT", Rest3/bits >>, Time) ->
+ fun (<< " GMT", Rest3/binary >>, Time) ->
http_date_ret(Rest3, {Date, Time});
(_Any, _Time) ->
{error, badarg}
@@ -344,11 +347,11 @@ rfc1123_date(Data) ->
%% in the past (this helps solve the "year 2000" problem).
rfc850_date(Data) ->
weekday(Data,
- fun (<< ", ", Rest/bits >>, _WeekDay) ->
+ fun (<< ", ", Rest/binary >>, _WeekDay) ->
date2(Rest,
- fun (<< " ", Rest2/bits >>, Date) ->
+ fun (<< " ", Rest2/binary >>, Date) ->
time(Rest2,
- fun (<< " GMT", Rest3/bits >>, Time) ->
+ fun (<< " GMT", Rest3/binary >>, Time) ->
http_date_ret(Rest3, {Date, Time});
(_Any, _Time) ->
{error, badarg}
@@ -364,11 +367,11 @@ rfc850_date(Data) ->
-spec asctime_date(binary()) -> any().
asctime_date(Data) ->
wkday(Data,
- fun (<< " ", Rest/bits >>, _WkDay) ->
+ fun (<< " ", Rest/binary >>, _WkDay) ->
date3(Rest,
- fun (<< " ", Rest2/bits >>, PartialDate) ->
+ fun (<< " ", Rest2/binary >>, PartialDate) ->
time(Rest2,
- fun (<< " ", Rest3/bits >>, Time) ->
+ fun (<< " ", Rest3/binary >>, Time) ->
asctime_year(Rest3,
PartialDate, Time);
(_Any, _Time) ->
@@ -382,7 +385,7 @@ asctime_date(Data) ->
end).
-spec asctime_year(binary(), tuple(), tuple()) -> any().
-asctime_year(<< Y1, Y2, Y3, Y4, Rest/bits >>, {Month, Day}, Time)
+asctime_year(<< Y1, Y2, Y3, Y4, Rest/binary >>, {Month, Day}, Time)
when Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9,
Y3 >= $0, Y3 =< $9, Y4 >= $0, Y4 =< $9 ->
Year = (Y1 - $0) * 1000 + (Y2 - $0) * 100 + (Y3 - $0) * 10 + (Y4 - $0),
@@ -402,9 +405,9 @@ http_date_ret(Data, DateTime = {Date, _Time}) ->
%% We never use it, pretty much just checks the wkday is right.
-spec wkday(binary(), fun()) -> any().
-wkday(<< WkDay:3/binary, Rest/bits >>, Fun)
- when WkDay =:= <<"Mon">>; WkDay =:= "Tue"; WkDay =:= "Wed";
- WkDay =:= <<"Thu">>; WkDay =:= "Fri"; WkDay =:= "Sat";
+wkday(<< WkDay:3/binary, Rest/binary >>, Fun)
+ when WkDay =:= <<"Mon">>; WkDay =:= <<"Tue">>; WkDay =:= <<"Wed">>;
+ WkDay =:= <<"Thu">>; WkDay =:= <<"Fri">>; WkDay =:= <<"Sat">>;
WkDay =:= <<"Sun">> ->
Fun(Rest, WkDay);
wkday(_Any, _Fun) ->
@@ -430,7 +433,7 @@ weekday(_Any, _Fun) ->
{error, badarg}.
-spec date1(binary(), fun()) -> any().
-date1(<< D1, D2, " ", M:3/binary, " ", Y1, Y2, Y3, Y4, Rest/bits >>, Fun)
+date1(<< D1, D2, " ", M:3/binary, " ", Y1, Y2, Y3, Y4, Rest/binary >>, Fun)
when D1 >= $0, D1 =< $9, D2 >= $0, D2 =< $9,
Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9,
Y3 >= $0, Y3 =< $9, Y4 >= $0, Y4 =< $9 ->
@@ -448,7 +451,7 @@ date1(_Data, _Fun) ->
{error, badarg}.
-spec date2(binary(), fun()) -> any().
-date2(<< D1, D2, "-", M:3/binary, "-", Y1, Y2, Rest/bits >>, Fun)
+date2(<< D1, D2, "-", M:3/binary, "-", Y1, Y2, Rest/binary >>, Fun)
when D1 >= $0, D1 =< $9, D2 >= $0, D2 =< $9,
Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9 ->
case month(M) of
@@ -470,7 +473,7 @@ date2(_Data, _Fun) ->
{error, badarg}.
-spec date3(binary(), fun()) -> any().
-date3(<< M:3/binary, " ", D1, D2, Rest/bits >>, Fun)
+date3(<< M:3/binary, " ", D1, D2, Rest/binary >>, Fun)
when (D1 >= $0 andalso D1 =< $3) orelse D1 =:= $\s,
D2 >= $0, D2 =< $9 ->
case month(M) of
@@ -502,7 +505,7 @@ month(<<"Dec">>) -> 12;
month(_Any) -> {error, badarg}.
-spec time(binary(), fun()) -> any().
-time(<< H1, H2, ":", M1, M2, ":", S1, S2, Rest/bits >>, Fun)
+time(<< H1, H2, ":", M1, M2, ":", S1, S2, Rest/binary >>, Fun)
when H1 >= $0, H1 =< $2, H2 >= $0, H2 =< $9,
M1 >= $0, M1 =< $5, M2 >= $0, M2 =< $9,
S1 >= $0, S1 =< $5, S2 >= $0, S2 =< $9 ->
@@ -521,7 +524,7 @@ time(<< H1, H2, ":", M1, M2, ":", S1, S2, Rest/bits >>, Fun)
%% @doc Skip whitespace.
-spec whitespace(binary(), fun()) -> any().
-whitespace(<< C, Rest/bits >>, Fun)
+whitespace(<< C, Rest/binary >>, Fun)
when C =:= $\s; C =:= $\t ->
whitespace(Rest, Fun);
whitespace(Data, Fun) ->
@@ -541,14 +544,14 @@ digits(Data) ->
end).
-spec digits(binary(), fun()) -> any().
-digits(<< C, Rest/bits >>, Fun)
+digits(<< C, Rest/binary >>, Fun)
when C >= $0, C =< $9 ->
digits(Rest, Fun, C - $0);
digits(_Data, _Fun) ->
{error, badarg}.
-spec digits(binary(), fun(), non_neg_integer()) -> any().
-digits(<< C, Rest/bits >>, Fun, Acc)
+digits(<< C, Rest/binary >>, Fun, Acc)
when C >= $0, C =< $9 ->
digits(Rest, Fun, Acc * 10 + (C - $0));
digits(Data, Fun, Acc) ->
@@ -564,7 +567,7 @@ alpha(Data, Fun) ->
-spec alpha(binary(), fun(), binary()) -> any().
alpha(<<>>, Fun, Acc) ->
Fun(<<>>, Acc);
-alpha(<< C, Rest/bits >>, Fun, Acc)
+alpha(<< C, Rest/binary >>, Fun, Acc)
when C >= $a andalso C =< $z;
C >= $A andalso C =< $Z ->
C2 = cowboy_bstr:char_to_lower(C),
@@ -574,7 +577,7 @@ alpha(Data, Fun, Acc) ->
%% @doc Parse either a token or a quoted string.
-spec word(binary(), fun()) -> any().
-word(Data = << $", _/bits >>, Fun) ->
+word(Data = << $", _/binary >>, Fun) ->
quoted_string(Data, Fun);
word(Data, Fun) ->
token(Data,
@@ -597,47 +600,47 @@ token(Data, Fun) ->
-spec token(binary(), fun(), ci | cs, binary()) -> any().
token(<<>>, Fun, _Case, Acc) ->
Fun(<<>>, Acc);
-token(Data = << C, _Rest/bits >>, Fun, _Case, Acc)
+token(Data = << C, _Rest/binary >>, Fun, _Case, Acc)
when C =:= $(; C =:= $); C =:= $<; C =:= $>; C =:= $@;
C =:= $,; C =:= $;; C =:= $:; C =:= $\\; C =:= $";
C =:= $/; C =:= $[; C =:= $]; C =:= $?; C =:= $=;
C =:= ${; C =:= $}; C =:= $\s; C =:= $\t;
C < 32; C =:= 127 ->
Fun(Data, Acc);
-token(<< C, Rest/bits >>, Fun, Case = ci, Acc) ->
+token(<< C, Rest/binary >>, Fun, Case = ci, Acc) ->
C2 = cowboy_bstr:char_to_lower(C),
token(Rest, Fun, Case, << Acc/binary, C2 >>);
-token(<< C, Rest/bits >>, Fun, Case, Acc) ->
+token(<< C, Rest/binary >>, Fun, Case, Acc) ->
token(Rest, Fun, Case, << Acc/binary, C >>).
%% @doc Parse a quoted string.
-spec quoted_string(binary(), fun()) -> any().
-quoted_string(<< $", Rest/bits >>, Fun) ->
+quoted_string(<< $", Rest/binary >>, Fun) ->
quoted_string(Rest, Fun, <<>>).
-spec quoted_string(binary(), fun(), binary()) -> any().
quoted_string(<<>>, _Fun, _Acc) ->
{error, badarg};
-quoted_string(<< $", Rest/bits >>, Fun, Acc) ->
+quoted_string(<< $", Rest/binary >>, Fun, Acc) ->
Fun(Rest, Acc);
-quoted_string(<< $\\, C, Rest/bits >>, Fun, Acc) ->
+quoted_string(<< $\\, C, Rest/binary >>, Fun, Acc) ->
quoted_string(Rest, Fun, << Acc/binary, C >>);
-quoted_string(<< C, Rest/bits >>, Fun, Acc) ->
+quoted_string(<< C, Rest/binary >>, Fun, Acc) ->
quoted_string(Rest, Fun, << Acc/binary, C >>).
%% @doc Parse a quality value.
-spec qvalue(binary(), fun()) -> any().
-qvalue(<< $0, $., Rest/bits >>, Fun) ->
+qvalue(<< $0, $., Rest/binary >>, Fun) ->
qvalue(Rest, Fun, 0, 100);
-qvalue(<< $0, Rest/bits >>, Fun) ->
+qvalue(<< $0, Rest/binary >>, Fun) ->
Fun(Rest, 0);
-qvalue(<< $1, $., $0, $0, $0, Rest/bits >>, Fun) ->
+qvalue(<< $1, $., $0, $0, $0, Rest/binary >>, Fun) ->
Fun(Rest, 1000);
-qvalue(<< $1, $., $0, $0, Rest/bits >>, Fun) ->
+qvalue(<< $1, $., $0, $0, Rest/binary >>, Fun) ->
Fun(Rest, 1000);
-qvalue(<< $1, $., $0, Rest/bits >>, Fun) ->
+qvalue(<< $1, $., $0, Rest/binary >>, Fun) ->
Fun(Rest, 1000);
-qvalue(<< $1, Rest/bits >>, Fun) ->
+qvalue(<< $1, Rest/binary >>, Fun) ->
Fun(Rest, 1000);
qvalue(_Data, _Fun) ->
{error, badarg}.
@@ -645,7 +648,7 @@ qvalue(_Data, _Fun) ->
-spec qvalue(binary(), fun(), integer(), 1 | 10 | 100) -> any().
qvalue(Data, Fun, Q, 0) ->
Fun(Data, Q);
-qvalue(<< C, Rest/bits >>, Fun, Q, M)
+qvalue(<< C, Rest/binary >>, Fun, Q, M)
when C >= $0, C =< $9 ->
qvalue(Rest, Fun, Q + (C - $0) * M, M div 10);
qvalue(Data, Fun, Q, _M) ->
@@ -668,6 +671,91 @@ connection_to_atom([<<"close">>|_Tail]) ->
connection_to_atom([_Any|Tail]) ->
connection_to_atom(Tail).
+%% @doc Decode a URL encoded binary.
+%% @equiv urldecode(Bin, crash)
+-spec urldecode(binary()) -> binary().
+urldecode(Bin) when is_binary(Bin) ->
+ urldecode(Bin, <<>>, crash).
+
+%% @doc Decode a URL encoded binary.
+%% The second argument specifies how to handle percent characters that are not
+%% followed by two valid hex characters. Use `skip' to ignore such errors,
+%% if `crash' is used the function will fail with the reason `badarg'.
+-spec urldecode(binary(), crash | skip) -> binary().
+urldecode(Bin, OnError) when is_binary(Bin) ->
+ urldecode(Bin, <<>>, OnError).
+
+-spec urldecode(binary(), binary(), crash | skip) -> binary().
+urldecode(<<$%, H, L, Rest/binary>>, Acc, OnError) ->
+ G = unhex(H),
+ M = unhex(L),
+ if G =:= error; M =:= error ->
+ case OnError of skip -> ok; crash -> erlang:error(badarg) end,
+ urldecode(<<H, L, Rest/binary>>, <<Acc/binary, $%>>, OnError);
+ true ->
+ urldecode(Rest, <<Acc/binary, (G bsl 4 bor M)>>, OnError)
+ end;
+urldecode(<<$%, Rest/binary>>, Acc, OnError) ->
+ case OnError of skip -> ok; crash -> erlang:error(badarg) end,
+ urldecode(Rest, <<Acc/binary, $%>>, OnError);
+urldecode(<<$+, Rest/binary>>, Acc, OnError) ->
+ urldecode(Rest, <<Acc/binary, $ >>, OnError);
+urldecode(<<C, Rest/binary>>, Acc, OnError) ->
+ urldecode(Rest, <<Acc/binary, C>>, OnError);
+urldecode(<<>>, Acc, _OnError) ->
+ Acc.
+
+-spec unhex(byte()) -> byte() | error.
+unhex(C) when C >= $0, C =< $9 -> C - $0;
+unhex(C) when C >= $A, C =< $F -> C - $A + 10;
+unhex(C) when C >= $a, C =< $f -> C - $a + 10;
+unhex(_) -> error.
+
+
+%% @doc URL encode a string binary.
+%% @equiv urlencode(Bin, [])
+-spec urlencode(binary()) -> binary().
+urlencode(Bin) ->
+ urlencode(Bin, []).
+
+%% @doc URL encode a string binary.
+%% The `noplus' option disables the default behaviour of quoting space
+%% characters, `\s', as `+'. The `upper' option overrides the default behaviour
+%% of writing hex numbers using lowecase letters to using uppercase letters
+%% instead.
+-spec urlencode(binary(), [noplus|upper]) -> binary().
+urlencode(Bin, Opts) ->
+ Plus = not proplists:get_value(noplus, Opts, false),
+ Upper = proplists:get_value(upper, Opts, false),
+ urlencode(Bin, <<>>, Plus, Upper).
+
+-spec urlencode(binary(), binary(), boolean(), boolean()) -> binary().
+urlencode(<<C, Rest/binary>>, Acc, P=Plus, U=Upper) ->
+ if C >= $0, C =< $9 -> urlencode(Rest, <<Acc/binary, C>>, P, U);
+ C >= $A, C =< $Z -> urlencode(Rest, <<Acc/binary, C>>, P, U);
+ C >= $a, C =< $z -> urlencode(Rest, <<Acc/binary, C>>, P, U);
+ C =:= $.; C =:= $-; C =:= $~; C =:= $_ ->
+ urlencode(Rest, <<Acc/binary, C>>, P, U);
+ C =:= $ , Plus ->
+ urlencode(Rest, <<Acc/binary, $+>>, P, U);
+ true ->
+ H = C band 16#F0 bsr 4, L = C band 16#0F,
+ H1 = if Upper -> tohexu(H); true -> tohexl(H) end,
+ L1 = if Upper -> tohexu(L); true -> tohexl(L) end,
+ urlencode(Rest, <<Acc/binary, $%, H1, L1>>, P, U)
+ end;
+urlencode(<<>>, Acc, _Plus, _Upper) ->
+ Acc.
+
+-spec tohexu(byte()) -> byte().
+tohexu(C) when C < 10 -> $0 + C;
+tohexu(C) when C < 17 -> $A + C - 10.
+
+-spec tohexl(byte()) -> byte().
+tohexl(C) when C < 10 -> $0 + C;
+tohexl(C) when C < 17 -> $a + C - 10.
+
+
%% Tests.
-ifdef(TEST).
@@ -687,16 +775,16 @@ nonempty_language_range_list_test_() ->
%% {Value, Result}
Tests = [
{<<"da, en-gb;q=0.8, en;q=0.7">>, [
- {{<<"da">>, []}, 1000},
- {{<<"en">>, [<<"gb">>]}, 800},
- {{<<"en">>, []}, 700}
+ {<<"da">>, 1000},
+ {<<"en-gb">>, 800},
+ {<<"en">>, 700}
]},
{<<"en, en-US, en-cockney, i-cherokee, x-pig-latin">>, [
- {{<<"en">>, []}, 1000},
- {{<<"en">>, [<<"us">>]}, 1000},
- {{<<"en">>, [<<"cockney">>]}, 1000},
- {{<<"i">>, [<<"cherokee">>]}, 1000},
- {{<<"x">>, [<<"pig">>, <<"latin">>]}, 1000}
+ {<<"en">>, 1000},
+ {<<"en-us">>, 1000},
+ {<<"en-cockney">>, 1000},
+ {<<"i-cherokee">>, 1000},
+ {<<"x-pig-latin">>, 1000}
]}
],
[{V, fun() -> R = nonempty_list(V, fun language_range/2) end}
@@ -834,4 +922,28 @@ digits_test_() ->
],
[{V, fun() -> R = digits(V) end} || {V, R} <- Tests].
+urldecode_test_() ->
+ U = fun urldecode/2,
+ [?_assertEqual(<<" ">>, U(<<"%20">>, crash)),
+ ?_assertEqual(<<" ">>, U(<<"+">>, crash)),
+ ?_assertEqual(<<0>>, U(<<"%00">>, crash)),
+ ?_assertEqual(<<255>>, U(<<"%fF">>, crash)),
+ ?_assertEqual(<<"123">>, U(<<"123">>, crash)),
+ ?_assertEqual(<<"%i5">>, U(<<"%i5">>, skip)),
+ ?_assertEqual(<<"%5">>, U(<<"%5">>, skip)),
+ ?_assertError(badarg, U(<<"%i5">>, crash)),
+ ?_assertError(badarg, U(<<"%5">>, crash))
+ ].
+
+urlencode_test_() ->
+ U = fun urlencode/2,
+ [?_assertEqual(<<"%ff%00">>, U(<<255,0>>, [])),
+ ?_assertEqual(<<"%FF%00">>, U(<<255,0>>, [upper])),
+ ?_assertEqual(<<"+">>, U(<<" ">>, [])),
+ ?_assertEqual(<<"%20">>, U(<<" ">>, [noplus])),
+ ?_assertEqual(<<"aBc">>, U(<<"aBc">>, [])),
+ ?_assertEqual(<<".-~_">>, U(<<".-~_">>, [])),
+ ?_assertEqual(<<"%ff+">>, urlencode(<<255, " ">>))
+ ].
+
-endif.
diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl
index c76c607..cd951d1 100644
--- a/src/cowboy_http_protocol.erl
+++ b/src/cowboy_http_protocol.erl
@@ -22,6 +22,9 @@
%% Defaults to 5.</dd>
%% <dt>timeout</dt><dd>Time in milliseconds before an idle
%% connection is closed. Defaults to 5000 milliseconds.</dd>
+%% <dt>urldecode</dt><dd>Function and options argument to use when decoding
+%% URL encoded strings. Defaults to `{fun cowboy_http:urldecode/2, crash}'.
+%% </dd>
%% </dl>
%%
%% Note that there is no need to monitor these processes when using Cowboy as
@@ -44,8 +47,11 @@
transport :: module(),
dispatch :: cowboy_dispatcher:dispatch_rules(),
handler :: {module(), any()},
+ urldecode :: {fun((binary(), T) -> binary()), T},
req_empty_lines = 0 :: integer(),
max_empty_lines :: integer(),
+ req_keepalive = 1 :: integer(),
+ max_keepalive :: integer(),
max_line_length :: integer(),
timeout :: timeout(),
buffer = <<>> :: binary(),
@@ -69,12 +75,16 @@ start_link(ListenerPid, Socket, Transport, Opts) ->
init(ListenerPid, Socket, Transport, Opts) ->
Dispatch = proplists:get_value(dispatch, Opts, []),
MaxEmptyLines = proplists:get_value(max_empty_lines, Opts, 5),
+ MaxKeepalive = proplists:get_value(max_keepalive, Opts, infinity),
MaxLineLength = proplists:get_value(max_line_length, Opts, 4096),
Timeout = proplists:get_value(timeout, Opts, 5000),
- receive shoot -> ok end,
+ URLDecDefault = {fun cowboy_http:urldecode/2, crash},
+ URLDec = proplists:get_value(urldecode, Opts, URLDecDefault),
+ ok = cowboy:accept_ack(ListenerPid),
wait_request(#state{listener=ListenerPid, socket=Socket, transport=Transport,
dispatch=Dispatch, max_empty_lines=MaxEmptyLines,
- max_line_length=MaxLineLength, timeout=Timeout}).
+ max_keepalive=MaxKeepalive, max_line_length=MaxLineLength,
+ timeout=Timeout, urldecode=URLDec}).
%% @private
-spec parse_request(#state{}) -> ok | none().
@@ -100,24 +110,24 @@ wait_request(State=#state{socket=Socket, transport=Transport,
-spec request({http_request, http_method(), http_uri(),
http_version()}, #state{}) -> ok | none().
-%% @todo We probably want to handle some things differently between versions.
request({http_request, _Method, _URI, Version}, State)
when Version =/= {1, 0}, Version =/= {1, 1} ->
error_terminate(505, State);
-%% @todo We need to cleanup the URI properly.
request({http_request, Method, {abs_path, AbsPath}, Version},
- State=#state{socket=Socket, transport=Transport}) ->
- {Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath),
+ State=#state{socket=Socket, transport=Transport,
+ urldecode={URLDecFun, URLDecArg}=URLDec}) ->
+ URLDecode = fun(Bin) -> URLDecFun(Bin, URLDecArg) end,
+ {Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath, URLDecode),
ConnAtom = version_to_connection(Version),
parse_header(#http_req{socket=Socket, transport=Transport,
- connection=ConnAtom, method=Method, version=Version,
- path=Path, raw_path=RawPath, raw_qs=Qs}, State);
+ connection=ConnAtom, pid=self(), method=Method, version=Version,
+ path=Path, raw_path=RawPath, raw_qs=Qs, urldecode=URLDec}, State);
request({http_request, Method, '*', Version},
- State=#state{socket=Socket, transport=Transport}) ->
+ State=#state{socket=Socket, transport=Transport, urldecode=URLDec}) ->
ConnAtom = version_to_connection(Version),
parse_header(#http_req{socket=Socket, transport=Transport,
- connection=ConnAtom, method=Method, version=Version,
- path='*', raw_path= <<"*">>, raw_qs= <<>>}, State);
+ connection=ConnAtom, pid=self(), method=Method, version=Version,
+ path='*', raw_path= <<"*">>, raw_qs= <<>>, urldecode=URLDec}, State);
request({http_request, _Method, _URI, _Version}, State) ->
error_terminate(501, State);
request({http_error, <<"\r\n">>},
@@ -125,7 +135,7 @@ request({http_error, <<"\r\n">>},
error_terminate(400, State);
request({http_error, <<"\r\n">>}, State=#state{req_empty_lines=N}) ->
parse_request(State#state{req_empty_lines=N + 1});
-request({http_error, _Any}, State) ->
+request(_Any, State) ->
error_terminate(400, State).
-spec parse_header(#http_req{}, #state{}) -> ok | none().
@@ -191,15 +201,17 @@ header(http_eoh, Req=#http_req{version={1, 0}, transport=Transport,
port=Port, buffer=Buffer}, State#state{buffer= <<>>});
header(http_eoh, Req, State=#state{buffer=Buffer}) ->
handler_init(Req#http_req{buffer=Buffer}, State#state{buffer= <<>>});
-header({http_error, _Bin}, _Req, State) ->
- error_terminate(500, State).
+header(_Any, _Req, State) ->
+ error_terminate(400, State).
-spec dispatch(fun((#http_req{}, #state{}) -> ok),
#http_req{}, #state{}) -> ok | none().
dispatch(Next, Req=#http_req{host=Host, path=Path},
State=#state{dispatch=Dispatch}) ->
- %% @todo We probably want to filter the Host and Path here to allow
- %% things like url rewriting.
+ %% @todo We should allow a configurable chain of handlers here to
+ %% allow things like url rewriting, site-wide authentication,
+ %% optional dispatching, and more. It would default to what
+ %% we are doing so far.
case cowboy_dispatcher:match(Host, Path, Dispatch) of
{ok, Handler, Opts, Binds, HostInfo, PathInfo} ->
Next(Req#http_req{host_info=HostInfo, path_info=PathInfo,
@@ -211,8 +223,8 @@ dispatch(Next, Req=#http_req{host=Host, path=Path},
end.
-spec handler_init(#http_req{}, #state{}) -> ok | none().
-handler_init(Req, State=#state{listener=ListenerPid,
- transport=Transport, handler={Handler, Opts}}) ->
+handler_init(Req, State=#state{transport=Transport,
+ handler={Handler, Opts}}) ->
try Handler:init({Transport:name(), http}, Req, Opts) of
{ok, Req2, HandlerState} ->
handler_handle(HandlerState, Req2, State);
@@ -231,7 +243,7 @@ handler_init(Req, State=#state{listener=ListenerPid,
handler_terminate(HandlerState, Req2, State);
%% @todo {upgrade, transport, Module}
{upgrade, protocol, Module} ->
- Module:upgrade(ListenerPid, Handler, Opts, Req)
+ upgrade_protocol(Req, State, Module)
catch Class:Reason ->
error_terminate(500, State),
error_logger:error_msg(
@@ -242,11 +254,19 @@ handler_init(Req, State=#state{listener=ListenerPid,
[Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()])
end.
+-spec upgrade_protocol(#http_req{}, #state{}, atom()) -> ok | none().
+upgrade_protocol(Req, State=#state{listener=ListenerPid,
+ handler={Handler, Opts}}, Module) ->
+ case Module:upgrade(ListenerPid, Handler, Opts, Req) of
+ {UpgradeRes, Req2} -> next_request(Req2, State, UpgradeRes);
+ _Any -> terminate(State)
+ end.
+
-spec handler_handle(any(), #http_req{}, #state{}) -> ok | none().
handler_handle(HandlerState, Req, State=#state{handler={Handler, Opts}}) ->
try Handler:handle(Req, HandlerState) of
{ok, Req2, HandlerState2} ->
- next_request(HandlerState2, Req2, State)
+ terminate_request(HandlerState2, Req2, State)
catch Class:Reason ->
error_logger:error_msg(
"** Handler ~p terminating in handle/2~n"
@@ -256,7 +276,7 @@ handler_handle(HandlerState, Req, State=#state{handler={Handler, Opts}}) ->
[Handler, Class, Reason, Opts,
HandlerState, Req, erlang:get_stacktrace()]),
handler_terminate(HandlerState, Req, State),
- terminate(State)
+ error_terminate(500, State)
end.
%% We don't listen for Transport closes because that would force us
@@ -286,7 +306,7 @@ handler_loop_timeout(State=#state{loop_timeout=Timeout,
handler_loop(HandlerState, Req, State=#state{loop_timeout_ref=TRef}) ->
receive
{?MODULE, timeout, TRef} ->
- next_request(HandlerState, Req, State);
+ terminate_request(HandlerState, Req, State);
{?MODULE, timeout, OlderTRef} when is_reference(OlderTRef) ->
handler_loop(HandlerState, Req, State);
Message ->
@@ -298,7 +318,7 @@ handler_call(HandlerState, Req, State=#state{handler={Handler, Opts}},
Message) ->
try Handler:info(Message, Req, HandlerState) of
{ok, Req2, HandlerState2} ->
- next_request(HandlerState2, Req2, State);
+ terminate_request(HandlerState2, Req2, State);
{loop, Req2, HandlerState2} ->
handler_before_loop(HandlerState2, Req2, State);
{loop, Req2, HandlerState2, hibernate} ->
@@ -311,7 +331,9 @@ handler_call(HandlerState, Req, State=#state{handler={Handler, Opts}},
"** Options were ~p~n** Handler state was ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
[Handler, Class, Reason, Opts,
- HandlerState, Req, erlang:get_stacktrace()])
+ HandlerState, Req, erlang:get_stacktrace()]),
+ handler_terminate(HandlerState, Req, State),
+ error_terminate(500, State)
end.
-spec handler_terminate(any(), #http_req{}, #state{}) -> ok.
@@ -328,16 +350,24 @@ handler_terminate(HandlerState, Req, #state{handler={Handler, Opts}}) ->
HandlerState, Req, erlang:get_stacktrace()])
end.
--spec next_request(any(), #http_req{}, #state{}) -> ok | none().
-next_request(HandlerState, Req=#http_req{connection=Conn, buffer=Buffer},
- State) ->
+-spec terminate_request(any(), #http_req{}, #state{}) -> ok | none().
+terminate_request(HandlerState, Req, State) ->
HandlerRes = handler_terminate(HandlerState, Req, State),
- BodyRes = ensure_body_processed(Req),
+ next_request(Req, State, HandlerRes).
+
+-spec next_request(#http_req{}, #state{}, any()) -> ok | none().
+next_request(Req=#http_req{connection=Conn, buffer=Buffer},
+ State=#state{req_keepalive=Keepalive, max_keepalive=MaxKeepalive},
+ HandlerRes) ->
RespRes = ensure_response(Req),
+ BodyRes = ensure_body_processed(Req),
+ %% Flush the resp_sent message before moving on.
+ receive {cowboy_http_req, resp_sent} -> ok after 0 -> ok end,
case {HandlerRes, BodyRes, RespRes, Conn} of
- {ok, ok, ok, keepalive} ->
+ {ok, ok, ok, keepalive} when Keepalive < MaxKeepalive ->
?MODULE:parse_request(State#state{
- buffer=Buffer, req_empty_lines=0});
+ buffer=Buffer, req_empty_lines=0,
+ req_keepalive=Keepalive + 1});
_Closed ->
terminate(State)
end.
@@ -372,11 +402,17 @@ ensure_response(#http_req{socket=Socket, transport=Transport,
Transport:send(Socket, <<"0\r\n\r\n">>),
close.
+%% Only send an error reply if there is no resp_sent message.
-spec error_terminate(http_status(), #state{}) -> ok.
error_terminate(Code, State=#state{socket=Socket, transport=Transport}) ->
- _ = cowboy_http_req:reply(Code, [], [], #http_req{
- socket=Socket, transport=Transport,
- connection=close, resp_state=waiting}),
+ receive
+ {cowboy_http_req, resp_sent} -> ok
+ after 0 ->
+ _ = cowboy_http_req:reply(Code, #http_req{
+ socket=Socket, transport=Transport,
+ connection=close, pid=self(), resp_state=waiting}),
+ ok
+ end,
terminate(State).
-spec terminate(#state{}) -> ok.
diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl
index f850e52..aa30d2c 100644
--- a/src/cowboy_http_req.erl
+++ b/src/cowboy_http_req.erl
@@ -22,32 +22,32 @@
-module(cowboy_http_req).
-export([
- method/1, version/1, peer/1,
+ method/1, version/1, peer/1, peer_addr/1,
host/1, host_info/1, raw_host/1, port/1,
path/1, path_info/1, raw_path/1,
qs_val/2, qs_val/3, qs_vals/1, raw_qs/1,
binding/2, binding/3, bindings/1,
header/2, header/3, headers/1,
parse_header/2, parse_header/3,
- cookie/2, cookie/3, cookies/1
+ cookie/2, cookie/3, cookies/1,
+ meta/2, meta/3
]). %% Request API.
-export([
- body/1, body/2, body_qs/1
-]). %% Request Body API.
-
--export([
+ body/1, body/2, body_qs/1,
multipart_data/1, multipart_skip/1
-]). %% Request Multipart API.
+]). %% Request Body API.
-export([
+ set_resp_cookie/4, set_resp_header/3, set_resp_body/2,
+ set_resp_body_fun/3, has_resp_header/2, has_resp_body/1,
reply/2, reply/3, reply/4,
chunked_reply/2, chunked_reply/3, chunk/2,
upgrade_reply/3
]). %% Response API.
-export([
- compact/1
+ compact/1, transport/1
]). %% Misc API.
-include("include/http.hrl").
@@ -73,6 +73,29 @@ peer(Req=#http_req{socket=Socket, transport=Transport, peer=undefined}) ->
peer(Req) ->
{Req#http_req.peer, Req}.
+%% @doc Returns the peer address calculated from headers.
+-spec peer_addr(#http_req{}) -> {inet:ip_address(), #http_req{}}.
+peer_addr(Req = #http_req{}) ->
+ {RealIp, Req1} = header(<<"X-Real-Ip">>, Req),
+ {ForwardedForRaw, Req2} = header(<<"X-Forwarded-For">>, Req1),
+ {{PeerIp, _PeerPort}, Req3} = peer(Req2),
+ ForwardedFor = case ForwardedForRaw of
+ undefined ->
+ undefined;
+ ForwardedForRaw ->
+ case re:run(ForwardedForRaw, "^(?<first_ip>[^\\,]+)",
+ [{capture, [first_ip], binary}]) of
+ {match, [FirstIp]} -> FirstIp;
+ _Any -> undefined
+ end
+ end,
+ {ok, PeerAddr} = if
+ is_binary(RealIp) -> inet_parse:address(binary_to_list(RealIp));
+ is_binary(ForwardedFor) -> inet_parse:address(binary_to_list(ForwardedFor));
+ true -> {ok, PeerIp}
+ end,
+ {PeerAddr, Req3}.
+
%% @doc Return the tokens for the hostname requested.
-spec host(#http_req{}) -> {cowboy_dispatcher:tokens(), #http_req{}}.
host(Req) ->
@@ -126,9 +149,9 @@ qs_val(Name, Req) when is_binary(Name) ->
%% missing.
-spec qs_val(binary(), #http_req{}, Default)
-> {binary() | true | Default, #http_req{}} when Default::any().
-qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined}, Default)
- when is_binary(Name) ->
- QsVals = parse_qs(RawQs),
+qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined,
+ urldecode={URLDecFun, URLDecArg}}, Default) when is_binary(Name) ->
+ QsVals = parse_qs(RawQs, fun(Bin) -> URLDecFun(Bin, URLDecArg) end),
qs_val(Name, Req#http_req{qs_vals=QsVals}, Default);
qs_val(Name, Req, Default) ->
case lists:keyfind(Name, 1, Req#http_req.qs_vals) of
@@ -138,8 +161,9 @@ qs_val(Name, Req, Default) ->
%% @doc Return the full list of query string values.
-spec qs_vals(#http_req{}) -> {list({binary(), binary() | true}), #http_req{}}.
-qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined}) ->
- QsVals = parse_qs(RawQs),
+qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined,
+ urldecode={URLDecFun, URLDecArg}}) ->
+ QsVals = parse_qs(RawQs, fun(Bin) -> URLDecFun(Bin, URLDecArg) end),
qs_vals(Req#http_req{qs_vals=QsVals});
qs_vals(Req=#http_req{qs_vals=QsVals}) ->
{QsVals, Req}.
@@ -204,13 +228,7 @@ parse_header(Name, Req=#http_req{p_headers=PHeaders}) ->
%% @doc Default values for semantic header parsing.
-spec parse_header_default(http_header()) -> any().
-parse_header_default('Accept') -> [];
-parse_header_default('Accept-Charset') -> [];
-parse_header_default('Accept-Encoding') -> [];
-parse_header_default('Accept-Language') -> [];
parse_header_default('Connection') -> [];
-parse_header_default('If-Match') -> '*';
-parse_header_default('If-None-Match') -> '*';
parse_header_default(_Name) -> undefined.
%% @doc Semantically parse headers.
@@ -265,6 +283,11 @@ parse_header(Name, Req, Default)
fun (Value) ->
cowboy_http:http_date(Value)
end);
+parse_header(Name, Req, Default) when Name =:= 'Upgrade' ->
+ parse_header(Name, Req, Default,
+ fun (Value) ->
+ cowboy_http:nonempty_list(Value, fun cowboy_http:token_ci/2)
+ end);
parse_header(Name, Req, Default) ->
{Value, Req2} = header(Name, Req, Default),
{undefined, Value, Req2}.
@@ -319,11 +342,29 @@ cookies(Req=#http_req{cookies=undefined}) ->
cookies(Req=#http_req{cookies=Cookies}) ->
{Cookies, Req}.
+%% @equiv meta(Name, Req, undefined)
+-spec meta(atom(), #http_req{}) -> {any() | undefined, #http_req{}}.
+meta(Name, Req) ->
+ meta(Name, Req, undefined).
+
+%% @doc Return metadata information about the request.
+%%
+%% Metadata information varies from one protocol to another. Websockets
+%% would define the protocol version here, while REST would use it to
+%% indicate which media type, language and charset were retained.
+-spec meta(atom(), #http_req{}, any()) -> {any(), #http_req{}}.
+meta(Name, Req, Default) ->
+ case lists:keyfind(Name, 1, Req#http_req.meta) of
+ {Name, Value} -> {Value, Req};
+ false -> {Default, Req}
+ end.
+
%% Request Body API.
%% @doc Return the full body sent with the request, or <em>{error, badarg}</em>
%% if no <em>Content-Length</em> is available.
%% @todo We probably want to allow a max length.
+%% @todo Add multipart support to this function.
-spec body(#http_req{}) -> {ok, binary(), #http_req{}} | {error, atom()}.
body(Req) ->
{Length, Req2} = cowboy_http_req:parse_header('Content-Length', Req),
@@ -343,12 +384,11 @@ body(Req) ->
-spec body(non_neg_integer(), #http_req{})
-> {ok, binary(), #http_req{}} | {error, atom()}.
body(Length, Req=#http_req{body_state=waiting, buffer=Buffer})
- when Length =< byte_size(Buffer) ->
+ when is_integer(Length) andalso Length =< byte_size(Buffer) ->
<< Body:Length/binary, Rest/bits >> = Buffer,
{ok, Body, Req#http_req{body_state=done, buffer=Rest}};
body(Length, Req=#http_req{socket=Socket, transport=Transport,
- body_state=waiting, buffer=Buffer})
- when is_integer(Length) andalso Length > byte_size(Buffer) ->
+ body_state=waiting, buffer=Buffer}) ->
case Transport:recv(Socket, Length - byte_size(Buffer), 5000) of
{ok, Body} -> {ok, << Buffer/binary, Body/binary >>,
Req#http_req{body_state=done, buffer= <<>>}};
@@ -358,9 +398,9 @@ body(Length, Req=#http_req{socket=Socket, transport=Transport,
%% @doc Return the full body sent with the reqest, parsed as an
%% application/x-www-form-urlencoded string. Essentially a POST query string.
-spec body_qs(#http_req{}) -> {list({binary(), binary() | true}), #http_req{}}.
-body_qs(Req) ->
+body_qs(Req=#http_req{urldecode={URLDecFun, URLDecArg}}) ->
{ok, Body, Req2} = body(Req),
- {parse_qs(Body), Req2}.
+ {parse_qs(Body, fun(Bin) -> URLDecFun(Bin, URLDecArg) end), Req2}.
%% Multipart Request API.
@@ -373,7 +413,9 @@ body_qs(Req) ->
%%
%% If the request Content-Type is not a multipart one, <em>{error, badarg}</em>
%% is returned.
--spec multipart_data(#http_req{}) -> {multipart_data(), #http_req{}}.
+-spec multipart_data(#http_req{})
+ -> {{headers, http_headers()} | {data, binary()} | end_of_part | eof,
+ #http_req{}}.
multipart_data(Req=#http_req{body_state=waiting}) ->
{{<<"multipart">>, _SubType, Params}, Req2} =
parse_header('Content-Type', Req),
@@ -427,35 +469,95 @@ multipart_skip(Req) ->
%% Response API.
+%% @doc Add a cookie header to the response.
+-spec set_resp_cookie(binary(), binary(), [cowboy_cookies:cookie_option()],
+ #http_req{}) -> {ok, #http_req{}}.
+set_resp_cookie(Name, Value, Options, Req) ->
+ {HeaderName, HeaderValue} = cowboy_cookies:cookie(Name, Value, Options),
+ set_resp_header(HeaderName, HeaderValue, Req).
+
+%% @doc Add a header to the response.
+-spec set_resp_header(http_header(), iodata(), #http_req{})
+ -> {ok, #http_req{}}.
+set_resp_header(Name, Value, Req=#http_req{resp_headers=RespHeaders}) ->
+ NameBin = header_to_binary(Name),
+ {ok, Req#http_req{resp_headers=[{NameBin, Value}|RespHeaders]}}.
+
+%% @doc Add a body to the response.
+%%
+%% The body set here is ignored if the response is later sent using
+%% anything other than reply/2 or reply/3. The response body is expected
+%% to be a binary or an iolist.
+-spec set_resp_body(iodata(), #http_req{}) -> {ok, #http_req{}}.
+set_resp_body(Body, Req) ->
+ {ok, Req#http_req{resp_body=Body}}.
+
+
+%% @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.
+%%
+%% 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'.
+%%
+%% @see cowboy_http_req:transport/1.
+-spec set_resp_body_fun(non_neg_integer(), fun(() -> {sent, non_neg_integer()}),
+ #http_req{}) -> {ok, #http_req{}}.
+set_resp_body_fun(StreamLen, StreamFun, Req) ->
+ {ok, Req#http_req{resp_body={StreamLen, StreamFun}}}.
+
+
+%% @doc Return whether the given header has been set for the response.
+-spec has_resp_header(http_header(), #http_req{}) -> boolean().
+has_resp_header(Name, #http_req{resp_headers=RespHeaders}) ->
+ NameBin = header_to_binary(Name),
+ lists:keymember(NameBin, 1, RespHeaders).
+
+%% @doc Return whether a body has been set for the response.
+-spec has_resp_body(#http_req{}) -> boolean().
+has_resp_body(#http_req{resp_body={Length, _}}) ->
+ Length > 0;
+has_resp_body(#http_req{resp_body=RespBody}) ->
+ iolist_size(RespBody) > 0.
+
%% @equiv reply(Status, [], [], Req)
-spec reply(http_status(), #http_req{}) -> {ok, #http_req{}}.
-reply(Status, Req) ->
- reply(Status, [], [], Req).
+reply(Status, Req=#http_req{resp_body=Body}) ->
+ reply(Status, [], Body, Req).
%% @equiv reply(Status, Headers, [], Req)
-spec reply(http_status(), http_headers(), #http_req{}) -> {ok, #http_req{}}.
-reply(Status, Headers, Req) ->
- reply(Status, Headers, [], Req).
+reply(Status, Headers, Req=#http_req{resp_body=Body}) ->
+ reply(Status, Headers, Body, Req).
%% @doc Send a reply to the client.
-spec reply(http_status(), http_headers(), iodata(), #http_req{})
-> {ok, #http_req{}}.
reply(Status, Headers, Body, Req=#http_req{socket=Socket,
- transport=Transport, connection=Connection,
- method=Method, resp_state=waiting}) ->
+ transport=Transport, connection=Connection, pid=ReqPid,
+ method=Method, resp_state=waiting, resp_headers=RespHeaders}) ->
RespConn = response_connection(Headers, Connection),
- Head = response_head(Status, Headers, [
+ ContentLen = case Body of {CL, _} -> CL; _ -> iolist_size(Body) end,
+ Head = response_head(Status, Headers, RespHeaders, [
{<<"Connection">>, atom_to_connection(Connection)},
- {<<"Content-Length">>,
- list_to_binary(integer_to_list(iolist_size(Body)))},
+ {<<"Content-Length">>, integer_to_list(ContentLen)},
{<<"Date">>, cowboy_clock:rfc1123()},
{<<"Server">>, <<"Cowboy">>}
]),
- case Method of
- 'HEAD' -> Transport:send(Socket, Head);
- _ -> Transport:send(Socket, [Head, Body])
+ case {Method, Body} of
+ {'HEAD', _} -> Transport:send(Socket, Head);
+ {_, {_, StreamFun}} -> Transport:send(Socket, Head), StreamFun();
+ {_, _} -> Transport:send(Socket, [Head, Body])
end,
- {ok, Req#http_req{connection=RespConn, resp_state=done}}.
+ ReqPid ! {?MODULE, resp_sent},
+ {ok, Req#http_req{connection=RespConn, resp_state=done,
+ resp_headers=[], resp_body= <<>>}}.
%% @equiv chunked_reply(Status, [], Req)
-spec chunked_reply(http_status(), #http_req{}) -> {ok, #http_req{}}.
@@ -466,17 +568,20 @@ chunked_reply(Status, Req) ->
%% @see cowboy_http_req:chunk/2
-spec chunked_reply(http_status(), http_headers(), #http_req{})
-> {ok, #http_req{}}.
-chunked_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport,
- connection=Connection, resp_state=waiting}) ->
+chunked_reply(Status, Headers, Req=#http_req{socket=Socket,
+ transport=Transport, connection=Connection, pid=ReqPid,
+ resp_state=waiting, resp_headers=RespHeaders}) ->
RespConn = response_connection(Headers, Connection),
- Head = response_head(Status, Headers, [
+ Head = response_head(Status, Headers, RespHeaders, [
{<<"Connection">>, atom_to_connection(Connection)},
{<<"Transfer-Encoding">>, <<"chunked">>},
{<<"Date">>, cowboy_clock:rfc1123()},
{<<"Server">>, <<"Cowboy">>}
]),
Transport:send(Socket, Head),
- {ok, Req#http_req{connection=RespConn, resp_state=chunks}}.
+ ReqPid ! {?MODULE, resp_sent},
+ {ok, Req#http_req{connection=RespConn, resp_state=chunks,
+ resp_headers=[], resp_body= <<>>}}.
%% @doc Send a chunk of data.
%%
@@ -489,15 +594,17 @@ chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) ->
<<"\r\n">>, Data, <<"\r\n">>]).
%% @doc Send an upgrade reply.
+%% @private
-spec upgrade_reply(http_status(), http_headers(), #http_req{})
-> {ok, #http_req{}}.
upgrade_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport,
- resp_state=waiting}) ->
- Head = response_head(Status, Headers, [
+ pid=ReqPid, resp_state=waiting, resp_headers=RespHeaders}) ->
+ Head = response_head(Status, Headers, RespHeaders, [
{<<"Connection">>, <<"Upgrade">>}
]),
Transport:send(Socket, Head),
- {ok, Req#http_req{resp_state=done}}.
+ ReqPid ! {?MODULE, resp_sent},
+ {ok, Req#http_req{resp_state=done, resp_headers=[], resp_body= <<>>}}.
%% Misc API.
@@ -510,18 +617,32 @@ upgrade_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport,
compact(Req) ->
Req#http_req{host=undefined, host_info=undefined, path=undefined,
path_info=undefined, qs_vals=undefined,
- bindings=undefined, headers=[]}.
+ bindings=undefined, headers=[],
+ p_headers=[], cookies=[]}.
+
+%% @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(#http_req{}) -> {ok, module(), inet:socket()}.
+transport(#http_req{transport=Transport, socket=Socket}) ->
+ {ok, Transport, Socket}.
%% Internal.
--spec parse_qs(binary()) -> list({binary(), binary() | true}).
-parse_qs(<<>>) ->
+-spec parse_qs(binary(), fun((binary()) -> binary())) ->
+ list({binary(), binary() | true}).
+parse_qs(<<>>, _URLDecode) ->
[];
-parse_qs(Qs) ->
+parse_qs(Qs, URLDecode) ->
Tokens = binary:split(Qs, <<"&">>, [global, trim]),
[case binary:split(Token, <<"=">>) of
- [Token] -> {quoted:from_url(Token), true};
- [Name, Value] -> {quoted:from_url(Name), quoted:from_url(Value)}
+ [Token] -> {URLDecode(Token), true};
+ [Name, Value] -> {URLDecode(Name), URLDecode(Value)}
end || Token <- Tokens].
-spec response_connection(http_headers(), keepalive | close)
@@ -545,15 +666,27 @@ response_connection_parse(ReplyConn) ->
Tokens = cowboy_http:nonempty_list(ReplyConn, fun cowboy_http:token/2),
cowboy_http:connection_to_atom(Tokens).
--spec response_head(http_status(), http_headers(), http_headers()) -> iolist().
-response_head(Status, Headers, DefaultHeaders) ->
+-spec response_head(http_status(), http_headers(), http_headers(),
+ http_headers()) -> iolist().
+response_head(Status, Headers, RespHeaders, DefaultHeaders) ->
StatusLine = <<"HTTP/1.1 ", (status(Status))/binary, "\r\n">>,
Headers2 = [{header_to_binary(Key), Value} || {Key, Value} <- Headers],
- Headers3 = lists:keysort(1, Headers2),
- Headers4 = lists:ukeymerge(1, Headers3, DefaultHeaders),
- Headers5 = [[Key, <<": ">>, Value, <<"\r\n">>]
- || {Key, Value} <- Headers4],
- [StatusLine, Headers5, <<"\r\n">>].
+ Headers3 = merge_headers(
+ merge_headers(Headers2, RespHeaders),
+ DefaultHeaders),
+ Headers4 = [[Key, <<": ">>, Value, <<"\r\n">>]
+ || {Key, Value} <- Headers3],
+ [StatusLine, Headers4, <<"\r\n">>].
+
+-spec merge_headers(http_headers(), http_headers()) -> http_headers().
+merge_headers(Headers, []) ->
+ Headers;
+merge_headers(Headers, [{Name, Value}|Tail]) ->
+ Headers2 = case lists:keymember(Name, 1, Headers) of
+ true -> Headers;
+ false -> Headers ++ [{Name, Value}]
+ end,
+ merge_headers(Headers2, Tail).
-spec atom_to_connection(keepalive) -> <<_:80>>;
(close) -> <<_:40>>.
@@ -689,6 +822,7 @@ parse_qs_test_() ->
{<<"a=b=c=d=e&f=g">>, [{<<"a">>, <<"b=c=d=e">>}, {<<"f">>, <<"g">>}]},
{<<"a+b=c+d">>, [{<<"a b">>, <<"c d">>}]}
],
- [{Qs, fun() -> R = parse_qs(Qs) end} || {Qs, R} <- Tests].
+ URLDecode = fun cowboy_http:urldecode/1,
+ [{Qs, fun() -> R = parse_qs(Qs, URLDecode) end} || {Qs, R} <- Tests].
-endif.
diff --git a/src/cowboy_http_rest.erl b/src/cowboy_http_rest.erl
new file mode 100644
index 0000000..35f82e3
--- /dev/null
+++ b/src/cowboy_http_rest.erl
@@ -0,0 +1,872 @@
+%% Copyright (c) 2011, 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 Experimental REST protocol implementation.
+%%
+%% Based on the Webmachine Diagram from Alan Dean and Justin Sheehy, which
+%% can be found in the Webmachine source tree, and on the Webmachine
+%% documentation available at http://wiki.basho.com/Webmachine.html
+%% at the time of writing.
+-module(cowboy_http_rest).
+-export([upgrade/4]).
+
+-record(state, {
+ %% Handler.
+ handler :: atom(),
+ handler_state :: any(),
+
+ %% Media type.
+ content_types_p = [] ::
+ [{{binary(), binary(), [{binary(), binary()}]}, atom()}],
+ content_type_a :: undefined
+ | {{binary(), binary(), [{binary(), binary()}]}, atom()},
+
+ %% Language.
+ languages_p = [] :: [binary()],
+ language_a :: undefined | binary(),
+
+ %% Charset.
+ charsets_p = [] :: [binary()],
+ charset_a :: undefined | binary(),
+
+ %% Cached resource calls.
+ etag :: undefined | no_call | binary(),
+ last_modified :: undefined | no_call | calendar:datetime(),
+ expires :: undefined | no_call | calendar:datetime()
+}).
+
+-include("include/http.hrl").
+
+%% @doc Upgrade a HTTP request to the REST protocol.
+%%
+%% 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(), #http_req{}) -> {ok, #http_req{}}.
+upgrade(_ListenerPid, Handler, Opts, Req) ->
+ try
+ case erlang:function_exported(Handler, rest_init, 2) of
+ true ->
+ case Handler:rest_init(Req, Opts) of
+ {ok, Req2, HandlerState} ->
+ service_available(Req2, #state{handler=Handler,
+ handler_state=HandlerState})
+ end;
+ false ->
+ service_available(Req, #state{handler=Handler})
+ end
+ catch Class:Reason ->
+ error_logger:error_msg(
+ "** Handler ~p terminating in rest_init/3~n"
+ " for the reason ~p:~p~n** Options were ~p~n"
+ "** Request was ~p~n** Stacktrace: ~p~n~n",
+ [Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()]),
+ {ok, _Req2} = cowboy_http_req:reply(500, Req),
+ ok
+ end.
+
+service_available(Req, State) ->
+ expect(Req, State, service_available, true, fun known_methods/2, 503).
+
+%% known_methods/2 should return a list of atoms or binary methods.
+known_methods(Req=#http_req{method=Method}, State) ->
+ case call(Req, State, known_methods) of
+ no_call when Method =:= 'HEAD'; Method =:= 'GET'; Method =:= 'POST';
+ Method =:= 'PUT'; Method =:= 'DELETE'; Method =:= 'TRACE';
+ Method =:= 'CONNECT'; Method =:= 'OPTIONS' ->
+ next(Req, State, fun uri_too_long/2);
+ no_call ->
+ next(Req, State, 501);
+ {List, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
+ case lists:member(Method, List) of
+ true -> next(Req2, State2, fun uri_too_long/2);
+ false -> next(Req2, State2, 501)
+ end
+ end.
+
+uri_too_long(Req, State) ->
+ expect(Req, State, uri_too_long, false, fun allowed_methods/2, 414).
+
+%% allowed_methods/2 should return a list of atoms or binary methods.
+allowed_methods(Req=#http_req{method=Method}, State) ->
+ case call(Req, State, allowed_methods) of
+ no_call when Method =:= 'HEAD'; Method =:= 'GET' ->
+ next(Req, State, fun malformed_request/2);
+ no_call ->
+ method_not_allowed(Req, State, ['GET', 'HEAD']);
+ {List, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
+ case lists:member(Method, List) of
+ true -> next(Req2, State2, fun malformed_request/2);
+ false -> method_not_allowed(Req2, State2, List)
+ end
+ end.
+
+method_not_allowed(Req, State, Methods) ->
+ {ok, Req2} = cowboy_http_req:set_resp_header(
+ <<"Allow">>, method_not_allowed_build(Methods, []), Req),
+ respond(Req2, State, 405).
+
+method_not_allowed_build([], []) ->
+ <<>>;
+method_not_allowed_build([], [_Ignore|Acc]) ->
+ lists:reverse(Acc);
+method_not_allowed_build([Method|Tail], Acc) when is_atom(Method) ->
+ Method2 = list_to_binary(atom_to_list(Method)),
+ method_not_allowed_build(Tail, [<<", ">>, Method2|Acc]);
+method_not_allowed_build([Method|Tail], Acc) ->
+ method_not_allowed_build(Tail, [<<", ">>, Method|Acc]).
+
+malformed_request(Req, State) ->
+ expect(Req, State, malformed_request, false, fun is_authorized/2, 400).
+
+%% is_authorized/2 should return true or {false, WwwAuthenticateHeader}.
+is_authorized(Req, State) ->
+ case call(Req, State, is_authorized) of
+ no_call ->
+ forbidden(Req, State);
+ {true, Req2, HandlerState2} ->
+ forbidden(Req2, State#state{handler_state=HandlerState2});
+ {{false, AuthHead}, Req2, HandlerState2} ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Www-Authenticate">>, AuthHead, Req2),
+ respond(Req3, State#state{handler_state=HandlerState2}, 401)
+ end.
+
+forbidden(Req, State) ->
+ expect(Req, State, forbidden, false, fun valid_content_headers/2, 403).
+
+valid_content_headers(Req, State) ->
+ expect(Req, State, valid_content_headers, true,
+ fun known_content_type/2, 501).
+
+known_content_type(Req, State) ->
+ expect(Req, State, known_content_type, true,
+ fun valid_entity_length/2, 413).
+
+valid_entity_length(Req, State) ->
+ expect(Req, State, valid_entity_length, true, fun options/2, 413).
+
+%% If you need to add additional headers to the response at this point,
+%% you should do it directly in the options/2 call using set_resp_headers.
+options(Req=#http_req{method='OPTIONS'}, State) ->
+ {ok, Req2, HandlerState2} = call(Req, State, options),
+ respond(Req2, State#state{handler_state=HandlerState2}, 200);
+options(Req, State) ->
+ content_types_provided(Req, State).
+
+%% content_types_provided/2 should return a list of content types and their
+%% associated callback function as a tuple: {{Type, SubType, Params}, Fun}.
+%% Type and SubType are the media type as binary. Params is a list of
+%% Key/Value tuple, with Key and Value a binary. Fun is the name of the
+%% callback that will be used to return the content of the response. It is
+%% given as an atom.
+%%
+%% An example of such return value would be:
+%% {{<<"text">>, <<"html">>, []}, to_html}
+%%
+%% Note that it is also possible to return a binary content type that will
+%% then be parsed by Cowboy. However note that while this may make your
+%% resources a little more readable, this is a lot less efficient. An example
+%% of such a return value would be:
+%% {<<"text/html">>, to_html}
+content_types_provided(Req=#http_req{meta=Meta}, State) ->
+ case call(Req, State, content_types_provided) of
+ no_call ->
+ not_acceptable(Req, State);
+ {[], Req2, HandlerState} ->
+ not_acceptable(Req2, State#state{handler_state=HandlerState});
+ {CTP, Req2, HandlerState} ->
+ CTP2 = [normalize_content_types_provided(P) || P <- CTP],
+ State2 = State#state{
+ handler_state=HandlerState, content_types_p=CTP2},
+ {Accept, Req3} = cowboy_http_req:parse_header('Accept', Req2),
+ case Accept of
+ undefined ->
+ {PMT, _Fun} = HeadCTP = hd(CTP2),
+ languages_provided(
+ Req3#http_req{meta=[{media_type, PMT}|Meta]},
+ State2#state{content_type_a=HeadCTP});
+ Accept ->
+ Accept2 = prioritize_accept(Accept),
+ choose_media_type(Req3, State2, Accept2)
+ end
+ end.
+
+normalize_content_types_provided({ContentType, Handler})
+ when is_binary(ContentType) ->
+ {cowboy_http:content_type(ContentType), Handler};
+normalize_content_types_provided(Provided) ->
+ Provided.
+
+prioritize_accept(Accept) ->
+ lists:sort(
+ fun ({MediaTypeA, Quality, _AcceptParamsA},
+ {MediaTypeB, Quality, _AcceptParamsB}) ->
+ %% Same quality, check precedence in more details.
+ prioritize_mediatype(MediaTypeA, MediaTypeB);
+ ({_MediaTypeA, QualityA, _AcceptParamsA},
+ {_MediaTypeB, QualityB, _AcceptParamsB}) ->
+ %% Just compare the quality.
+ QualityA > QualityB
+ end, Accept).
+
+%% Media ranges can be overridden by more specific media ranges or
+%% specific media types. If more than one media range applies to a given
+%% type, the most specific reference has precedence.
+%%
+%% We always choose B over A when we can't decide between the two.
+prioritize_mediatype({TypeA, SubTypeA, ParamsA}, {TypeB, SubTypeB, ParamsB}) ->
+ case TypeB of
+ TypeA ->
+ case SubTypeB of
+ SubTypeA -> length(ParamsA) > length(ParamsB);
+ <<"*">> -> true;
+ _Any -> false
+ end;
+ <<"*">> -> true;
+ _Any -> false
+ end.
+
+%% Ignoring the rare AcceptParams. Not sure what should be done about them.
+choose_media_type(Req, State, []) ->
+ not_acceptable(Req, State);
+choose_media_type(Req, State=#state{content_types_p=CTP},
+ [MediaType|Tail]) ->
+ match_media_type(Req, State, Tail, CTP, MediaType).
+
+match_media_type(Req, State, Accept, [], _MediaType) ->
+ choose_media_type(Req, State, Accept);
+match_media_type(Req, State, Accept, CTP,
+ MediaType = {{<<"*">>, <<"*">>, _Params_A}, _QA, _APA}) ->
+ match_media_type_params(Req, State, Accept, CTP, MediaType);
+match_media_type(Req, State, Accept,
+ CTP = [{{Type, SubType_P, _PP}, _Fun}|_Tail],
+ MediaType = {{Type, SubType_A, _PA}, _QA, _APA})
+ when SubType_P =:= SubType_A; SubType_A =:= <<"*">> ->
+ match_media_type_params(Req, State, Accept, CTP, MediaType);
+match_media_type(Req, State, Accept, [_Any|Tail], MediaType) ->
+ match_media_type(Req, State, Accept, Tail, MediaType).
+
+match_media_type_params(Req=#http_req{meta=Meta}, State, Accept,
+ [Provided = {PMT = {_TP, _STP, Params_P}, _Fun}|Tail],
+ MediaType = {{_TA, _STA, Params_A}, _QA, _APA}) ->
+ case lists:sort(Params_P) =:= lists:sort(Params_A) of
+ true ->
+ languages_provided(Req#http_req{meta=[{media_type, PMT}|Meta]},
+ State#state{content_type_a=Provided});
+ false ->
+ match_media_type(Req, State, Accept, Tail, MediaType)
+ end.
+
+%% languages_provided should return a list of binary values indicating
+%% which languages are accepted by the resource.
+%%
+%% @todo I suppose we should also ask the resource if it wants to
+%% set a language itself or if it wants it to be automatically chosen.
+languages_provided(Req, State) ->
+ case call(Req, State, languages_provided) of
+ no_call ->
+ charsets_provided(Req, State);
+ {[], Req2, HandlerState2} ->
+ not_acceptable(Req2, State#state{handler_state=HandlerState2});
+ {LP, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2, languages_p=LP},
+ {AcceptLanguage, Req3} =
+ cowboy_http_req:parse_header('Accept-Language', Req2),
+ case AcceptLanguage of
+ undefined ->
+ set_language(Req3, State2#state{language_a=hd(LP)});
+ AcceptLanguage ->
+ AcceptLanguage2 = prioritize_languages(AcceptLanguage),
+ choose_language(Req3, State2, AcceptLanguage2)
+ end
+ end.
+
+%% A language-range matches a language-tag if it exactly equals the tag,
+%% or if it exactly equals a prefix of the tag such that the first tag
+%% character following the prefix is "-". The special range "*", if
+%% present in the Accept-Language field, matches every tag not matched
+%% by any other range present in the Accept-Language field.
+%%
+%% @todo The last sentence probably means we should always put '*'
+%% at the end of the list.
+prioritize_languages(AcceptLanguages) ->
+ lists:sort(
+ fun ({_TagA, QualityA}, {_TagB, QualityB}) ->
+ QualityA > QualityB
+ end, AcceptLanguages).
+
+choose_language(Req, State, []) ->
+ not_acceptable(Req, State);
+choose_language(Req, State=#state{languages_p=LP}, [Language|Tail]) ->
+ match_language(Req, State, Tail, LP, Language).
+
+match_language(Req, State, Accept, [], _Language) ->
+ choose_language(Req, State, Accept);
+match_language(Req, State, _Accept, [Provided|_Tail], {'*', _Quality}) ->
+ set_language(Req, State#state{language_a=Provided});
+match_language(Req, State, _Accept, [Provided|_Tail], {Provided, _Quality}) ->
+ set_language(Req, State#state{language_a=Provided});
+match_language(Req, State, Accept, [Provided|Tail],
+ Language = {Tag, _Quality}) ->
+ Length = byte_size(Tag),
+ case Provided of
+ << Tag:Length/binary, $-, _Any/bits >> ->
+ set_language(Req, State#state{language_a=Provided});
+ _Any ->
+ match_language(Req, State, Accept, Tail, Language)
+ end.
+
+set_language(Req=#http_req{meta=Meta}, State=#state{language_a=Language}) ->
+ {ok, Req2} = cowboy_http_req:set_resp_header(
+ <<"Content-Language">>, Language, Req),
+ charsets_provided(Req2#http_req{meta=[{language, Language}|Meta]}, State).
+
+%% charsets_provided should return a list of binary values indicating
+%% which charsets are accepted by the resource.
+charsets_provided(Req, State) ->
+ case call(Req, State, charsets_provided) of
+ no_call ->
+ set_content_type(Req, State);
+ {[], Req2, HandlerState2} ->
+ not_acceptable(Req2, State#state{handler_state=HandlerState2});
+ {CP, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2, charsets_p=CP},
+ {AcceptCharset, Req3} =
+ cowboy_http_req:parse_header('Accept-Charset', Req2),
+ case AcceptCharset of
+ undefined ->
+ set_content_type(Req3, State2#state{charset_a=hd(CP)});
+ AcceptCharset ->
+ AcceptCharset2 = prioritize_charsets(AcceptCharset),
+ choose_charset(Req3, State2, AcceptCharset2)
+ end
+ end.
+
+%% The special value "*", if present in the Accept-Charset field,
+%% matches every character set (including ISO-8859-1) which is not
+%% mentioned elsewhere in the Accept-Charset field. If no "*" is present
+%% in an Accept-Charset field, then all character sets not explicitly
+%% mentioned get a quality value of 0, except for ISO-8859-1, which gets
+%% a quality value of 1 if not explicitly mentioned.
+prioritize_charsets(AcceptCharsets) ->
+ AcceptCharsets2 = lists:sort(
+ fun ({_CharsetA, QualityA}, {_CharsetB, QualityB}) ->
+ QualityA > QualityB
+ end, AcceptCharsets),
+ case lists:keymember(<<"*">>, 1, AcceptCharsets2) of
+ true -> AcceptCharsets2;
+ false -> [{<<"iso-8859-1">>, 1000}|AcceptCharsets2]
+ end.
+
+choose_charset(Req, State, []) ->
+ not_acceptable(Req, State);
+choose_charset(Req, State=#state{charsets_p=CP}, [Charset|Tail]) ->
+ match_charset(Req, State, Tail, CP, Charset).
+
+match_charset(Req, State, Accept, [], _Charset) ->
+ choose_charset(Req, State, Accept);
+match_charset(Req, State, _Accept, [Provided|_Tail],
+ {Provided, _Quality}) ->
+ set_content_type(Req, State#state{charset_a=Provided});
+match_charset(Req, State, Accept, [_Provided|Tail], Charset) ->
+ match_charset(Req, State, Accept, Tail, Charset).
+
+set_content_type(Req=#http_req{meta=Meta}, State=#state{
+ content_type_a={{Type, SubType, Params}, _Fun},
+ charset_a=Charset}) ->
+ ParamsBin = set_content_type_build_params(Params, []),
+ ContentType = [Type, <<"/">>, SubType, ParamsBin],
+ ContentType2 = case Charset of
+ undefined -> ContentType;
+ Charset -> [ContentType, <<"; charset=">>, Charset]
+ end,
+ {ok, Req2} = cowboy_http_req:set_resp_header(
+ <<"Content-Type">>, ContentType2, Req),
+ encodings_provided(Req2#http_req{meta=[{charset, Charset}|Meta]}, State).
+
+set_content_type_build_params([], []) ->
+ <<>>;
+set_content_type_build_params([], Acc) ->
+ lists:reverse(Acc);
+set_content_type_build_params([{Attr, Value}|Tail], Acc) ->
+ set_content_type_build_params(Tail, [[Attr, <<"=">>, Value], <<";">>|Acc]).
+
+%% @todo Match for identity as we provide nothing else for now.
+%% @todo Don't forget to set the Content-Encoding header when we reply a body
+%% and the found encoding is something other than identity.
+encodings_provided(Req, State) ->
+ variances(Req, State).
+
+not_acceptable(Req, State) ->
+ respond(Req, State, 406).
+
+%% variances/2 should return a list of headers that will be added
+%% to the Vary response header. The Accept, Accept-Language,
+%% Accept-Charset and Accept-Encoding headers do not need to be
+%% specified.
+%%
+%% @todo Do Accept-Encoding too when we handle it.
+%% @todo Does the order matter?
+variances(Req, State=#state{content_types_p=CTP,
+ languages_p=LP, charsets_p=CP}) ->
+ Variances = case CTP of
+ [] -> [];
+ [_] -> [];
+ [_|_] -> [<<"Accept">>]
+ end,
+ Variances2 = case LP of
+ [] -> Variances;
+ [_] -> Variances;
+ [_|_] -> [<<"Accept-Language">>|Variances]
+ end,
+ Variances3 = case CP of
+ [] -> Variances2;
+ [_] -> Variances2;
+ [_|_] -> [<<"Accept-Charset">>|Variances2]
+ end,
+ {Variances4, Req3, State2} = case call(Req, State, variances) of
+ no_call ->
+ {Variances3, Req, State};
+ {HandlerVariances, Req2, HandlerState} ->
+ {Variances3 ++ HandlerVariances, Req2,
+ State#state{handler_state=HandlerState}}
+ end,
+ case [[<<", ">>, V] || V <- Variances4] of
+ [] ->
+ resource_exists(Req3, State2);
+ [[<<", ">>, H]|Variances5] ->
+ {ok, Req4} = cowboy_http_req:set_resp_header(
+ <<"Variances">>, [H|Variances5], Req3),
+ resource_exists(Req4, State2)
+ end.
+
+resource_exists(Req, State) ->
+ expect(Req, State, resource_exists, true,
+ fun if_match_exists/2, fun if_match_musnt_exist/2).
+
+if_match_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-Match', Req) of
+ {undefined, Req2} ->
+ if_unmodified_since_exists(Req2, State);
+ {'*', Req2} ->
+ if_unmodified_since_exists(Req2, State);
+ {ETagsList, Req2} ->
+ if_match(Req2, State, ETagsList)
+ end.
+
+if_match(Req, State, EtagsList) ->
+ {Etag, Req2, State2} = generate_etag(Req, State),
+ case Etag of
+ no_call ->
+ precondition_failed(Req2, State2);
+ Etag ->
+ case lists:member(Etag, EtagsList) of
+ true -> if_unmodified_since_exists(Req2, State2);
+ false -> precondition_failed(Req2, State2)
+ end
+ end.
+
+if_match_musnt_exist(Req, State) ->
+ case cowboy_http_req:header('If-Match', Req) of
+ {undefined, Req2} -> is_put_to_missing_resource(Req2, State);
+ {_Any, Req2} -> precondition_failed(Req2, State)
+ end.
+
+if_unmodified_since_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-Unmodified-Since', Req) of
+ {undefined, Req2} ->
+ if_none_match_exists(Req2, State);
+ {{error, badarg}, Req2} ->
+ if_none_match_exists(Req2, State);
+ {IfUnmodifiedSince, Req2} ->
+ if_unmodified_since(Req2, State, IfUnmodifiedSince)
+ end.
+
+%% If LastModified is the atom 'no_call', we continue.
+if_unmodified_since(Req, State, IfUnmodifiedSince) ->
+ {LastModified, Req2, State2} = last_modified(Req, State),
+ case LastModified > IfUnmodifiedSince of
+ true -> precondition_failed(Req2, State2);
+ false -> if_none_match_exists(Req2, State2)
+ end.
+
+if_none_match_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-None-Match', Req) of
+ {undefined, Req2} ->
+ if_modified_since_exists(Req2, State);
+ {'*', Req2} ->
+ precondition_is_head_get(Req2, State);
+ {EtagsList, Req2} ->
+ if_none_match(Req2, State, EtagsList)
+ end.
+
+if_none_match(Req, State, EtagsList) ->
+ {Etag, Req2, State2} = generate_etag(Req, State),
+ case Etag of
+ no_call ->
+ precondition_failed(Req2, State2);
+ Etag ->
+ case lists:member(Etag, EtagsList) of
+ true -> precondition_is_head_get(Req2, State2);
+ false -> if_modified_since_exists(Req2, State2)
+ end
+ end.
+
+precondition_is_head_get(Req=#http_req{method=Method}, State)
+ when Method =:= 'HEAD'; Method =:= 'GET' ->
+ not_modified(Req, State);
+precondition_is_head_get(Req, State) ->
+ precondition_failed(Req, State).
+
+if_modified_since_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-Modified-Since', Req) of
+ {undefined, Req2} ->
+ method(Req2, State);
+ {{error, badarg}, Req2} ->
+ method(Req2, State);
+ {IfModifiedSince, Req2} ->
+ if_modified_since_now(Req2, State, IfModifiedSince)
+ end.
+
+if_modified_since_now(Req, State, IfModifiedSince) ->
+ case IfModifiedSince > erlang:universaltime() of
+ true -> method(Req, State);
+ false -> if_modified_since(Req, State, IfModifiedSince)
+ end.
+
+if_modified_since(Req, State, IfModifiedSince) ->
+ {LastModified, Req2, State2} = last_modified(Req, State),
+ case LastModified of
+ no_call ->
+ method(Req2, State2);
+ LastModified ->
+ case LastModified > IfModifiedSince of
+ true -> method(Req2, State2);
+ false -> not_modified(Req2, State2)
+ end
+ end.
+
+not_modified(Req=#http_req{resp_headers=RespHeaders}, State) ->
+ RespHeaders2 = lists:keydelete(<<"Content-Type">>, 1, RespHeaders),
+ Req2 = Req#http_req{resp_headers=RespHeaders2},
+ {Req3, State2} = set_resp_etag(Req2, State),
+ {Req4, State3} = set_resp_expires(Req3, State2),
+ respond(Req4, State3, 304).
+
+precondition_failed(Req, State) ->
+ respond(Req, State, 412).
+
+is_put_to_missing_resource(Req=#http_req{method='PUT'}, State) ->
+ moved_permanently(Req, State, fun is_conflict/2);
+is_put_to_missing_resource(Req, State) ->
+ previously_existed(Req, State).
+
+%% moved_permanently/2 should return either false or {true, Location}
+%% with Location the full new URI of the resource.
+moved_permanently(Req, State, OnFalse) ->
+ case call(Req, State, moved_permanently) of
+ {{true, Location}, Req2, HandlerState2} ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Location">>, Location, Req2),
+ respond(Req3, State#state{handler_state=HandlerState2}, 301);
+ {false, Req2, HandlerState2} ->
+ OnFalse(Req2, State#state{handler_state=HandlerState2});
+ no_call ->
+ OnFalse(Req, State)
+ end.
+
+previously_existed(Req, State) ->
+ expect(Req, State, previously_existed, false,
+ fun (R, S) -> is_post_to_missing_resource(R, S, 404) end,
+ fun (R, S) -> moved_permanently(R, S, fun moved_temporarily/2) end).
+
+%% moved_temporarily/2 should return either false or {true, Location}
+%% with Location the full new URI of the resource.
+moved_temporarily(Req, State) ->
+ case call(Req, State, moved_temporarily) of
+ {{true, Location}, Req2, HandlerState2} ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Location">>, Location, Req2),
+ respond(Req3, State#state{handler_state=HandlerState2}, 307);
+ {false, Req2, HandlerState2} ->
+ is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState2}, 410);
+ no_call ->
+ is_post_to_missing_resource(Req, State, 410)
+ end.
+
+is_post_to_missing_resource(Req=#http_req{method='POST'}, State, OnFalse) ->
+ allow_missing_post(Req, State, OnFalse);
+is_post_to_missing_resource(Req, State, OnFalse) ->
+ respond(Req, State, OnFalse).
+
+allow_missing_post(Req, State, OnFalse) ->
+ expect(Req, State, allow_missing_post, true, fun post_is_create/2, OnFalse).
+
+method(Req=#http_req{method='DELETE'}, State) ->
+ delete_resource(Req, State);
+method(Req=#http_req{method='POST'}, State) ->
+ post_is_create(Req, State);
+method(Req=#http_req{method='PUT'}, State) ->
+ is_conflict(Req, State);
+method(Req, State) ->
+ set_resp_body(Req, State).
+
+%% delete_resource/2 should start deleting the resource and return.
+delete_resource(Req, State) ->
+ expect(Req, State, delete_resource, true, fun delete_completed/2, 500).
+
+%% delete_completed/2 indicates whether the resource has been deleted yet.
+delete_completed(Req, State) ->
+ expect(Req, State, delete_completed, true, fun has_resp_body/2, 202).
+
+%% post_is_create/2 indicates whether the POST method can create new resources.
+post_is_create(Req, State) ->
+ expect(Req, State, post_is_create, false, fun process_post/2, fun create_path/2).
+
+%% When the POST method can create new resources, create_path/2 will be called
+%% and is expected to return the full path to the new resource
+%% (including the leading /).
+create_path(Req=#http_req{meta=Meta}, State) ->
+ case call(Req, State, create_path) of
+ {Path, Req2, HandlerState} ->
+ Location = create_path_location(Req2, Path),
+ State2 = State#state{handler_state=HandlerState},
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Location">>, Location, Req2),
+ put_resource(Req3#http_req{meta=[{put_path, Path}|Meta]},
+ State2, 303)
+ end.
+
+create_path_location(#http_req{transport=Transport, raw_host=Host,
+ port=Port}, Path) ->
+ TransportName = Transport:name(),
+ << (create_path_location_protocol(TransportName))/binary, "://",
+ Host/binary, (create_path_location_port(TransportName, Port))/binary,
+ Path/binary >>.
+
+create_path_location_protocol(ssl) -> <<"https">>;
+create_path_location_protocol(_) -> <<"http">>.
+
+create_path_location_port(ssl, 443) ->
+ <<>>;
+create_path_location_port(tcp, 80) ->
+ <<>>;
+create_path_location_port(_, Port) ->
+ <<":", (list_to_binary(integer_to_list(Port)))/binary>>.
+
+%% 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) ->
+ case call(Req, State, process_post) of
+ {true, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ next(Req2, State2, 201);
+ {false, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ respond(Req2, State2, 500)
+ end.
+
+is_conflict(Req, State) ->
+ expect(Req, State, is_conflict, false, fun put_resource/2, 409).
+
+put_resource(Req=#http_req{raw_path=RawPath, meta=Meta}, State) ->
+ Req2 = Req#http_req{meta=[{put_path, RawPath}|Meta]},
+ put_resource(Req2, State, fun is_new_resource/2).
+
+%% 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. The path to the new resource
+%% 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_http_req:meta(put_path, Req)
+put_resource(Req, State, OnTrue) ->
+ case call(Req, State, content_types_accepted) of
+ no_call ->
+ respond(Req, State, 415);
+ {CTA, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
+ {ContentType, Req3}
+ = cowboy_http_req:parse_header('Content-Type', Req2),
+ choose_content_type(Req3, State2, OnTrue, ContentType, CTA)
+ end.
+
+choose_content_type(Req, State, _OnTrue, _ContentType, []) ->
+ respond(Req, State, 415);
+choose_content_type(Req, State, OnTrue, ContentType,
+ [{Accepted, Fun}|_Tail]) when ContentType =:= Accepted ->
+ case call(Req, State, Fun) of
+ {true, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ next(Req2, State2, OnTrue);
+ {false, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ respond(Req2, State2, 500)
+ end;
+choose_content_type(Req, State, OnTrue, ContentType, [_Any|Tail]) ->
+ choose_content_type(Req, State, OnTrue, ContentType, Tail).
+
+%% Whether we created a new resource, either through PUT or POST.
+%% This is easily testable because we would have set the Location
+%% header by this point if we did so.
+is_new_resource(Req, State) ->
+ case cowboy_http_req:has_resp_header(<<"Location">>, Req) of
+ true -> respond(Req, State, 201);
+ false -> has_resp_body(Req, State)
+ end.
+
+has_resp_body(Req, State) ->
+ case cowboy_http_req:has_resp_body(Req) of
+ true -> multiple_choices(Req, State);
+ false -> respond(Req, State, 204)
+ end.
+
+%% Set the response headers and call the callback found using
+%% content_types_provided/2 to obtain the request body and add
+%% it to the response.
+set_resp_body(Req=#http_req{method=Method},
+ State=#state{content_type_a={_Type, Fun}})
+ when Method =:= 'GET'; Method =:= 'HEAD' ->
+ {Req2, State2} = set_resp_etag(Req, State),
+ {LastModified, Req3, State3} = last_modified(Req2, State2),
+ case LastModified of
+ LastModified when is_atom(LastModified) ->
+ Req4 = Req3;
+ LastModified ->
+ LastModifiedStr = httpd_util:rfc1123_date(LastModified),
+ {ok, Req4} = cowboy_http_req:set_resp_header(
+ <<"Last-Modified">>, LastModifiedStr, Req3)
+ end,
+ {Req5, State4} = set_resp_expires(Req4, State3),
+ case call(Req5, State4, Fun) of
+ {Body, Req6, HandlerState} ->
+ State5 = State4#state{handler_state=HandlerState},
+ {ok, Req7} = case Body of
+ {stream, Len, Fun1} ->
+ cowboy_http_req:set_resp_body_fun(Len, Fun1, Req6);
+ _Contents ->
+ cowboy_http_req:set_resp_body(Body, Req6)
+ end,
+ multiple_choices(Req7, State5)
+ end;
+set_resp_body(Req, State) ->
+ multiple_choices(Req, State).
+
+multiple_choices(Req, State) ->
+ expect(Req, State, multiple_choices, false, 200, 300).
+
+%% Response utility functions.
+
+set_resp_etag(Req, State) ->
+ {Etag, Req2, State2} = generate_etag(Req, State),
+ case Etag of
+ undefined ->
+ {Req2, State2};
+ Etag ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Etag">>, Etag, Req2),
+ {Req3, State2}
+ end.
+
+set_resp_expires(Req, State) ->
+ {Expires, Req2, State2} = expires(Req, State),
+ case Expires of
+ Expires when is_atom(Expires) ->
+ {Req2, State2};
+ Expires ->
+ ExpiresStr = httpd_util:rfc1123_date(Expires),
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Expires">>, ExpiresStr, Req2),
+ {Req3, State2}
+ end.
+
+%% Info retrieval. No logic.
+
+generate_etag(Req, State=#state{etag=no_call}) ->
+ {undefined, Req, State};
+generate_etag(Req, State=#state{etag=undefined}) ->
+ case call(Req, State, generate_etag) of
+ no_call ->
+ {undefined, Req, State#state{etag=no_call}};
+ {Etag, Req2, HandlerState2} ->
+ {Etag, Req2, State#state{handler_state=HandlerState2, etag=Etag}}
+ end;
+generate_etag(Req, State=#state{etag=Etag}) ->
+ {Etag, Req, State}.
+
+last_modified(Req, State=#state{last_modified=no_call}) ->
+ {undefined, Req, State};
+last_modified(Req, State=#state{last_modified=undefined}) ->
+ case call(Req, State, last_modified) of
+ no_call ->
+ {undefined, Req, State#state{last_modified=no_call}};
+ {LastModified, Req2, HandlerState2} ->
+ {LastModified, Req2, State#state{handler_state=HandlerState2,
+ last_modified=LastModified}}
+ end;
+last_modified(Req, State=#state{last_modified=LastModified}) ->
+ {LastModified, Req, State}.
+
+expires(Req, State=#state{expires=no_call}) ->
+ {undefined, Req, State};
+expires(Req, State=#state{expires=undefined}) ->
+ case call(Req, State, expires) of
+ no_call ->
+ {undefined, Req, State#state{expires=no_call}};
+ {Expires, Req2, HandlerState2} ->
+ {Expires, Req2, State#state{handler_state=HandlerState2,
+ expires=Expires}}
+ end;
+expires(Req, State=#state{expires=Expires}) ->
+ {Expires, Req, State}.
+
+%% REST primitives.
+
+expect(Req, State, Callback, Expected, OnTrue, OnFalse) ->
+ case call(Req, State, Callback) of
+ no_call ->
+ next(Req, State, OnTrue);
+ {Expected, Req2, HandlerState2} ->
+ next(Req2, State#state{handler_state=HandlerState2}, OnTrue);
+ {_Unexpected, Req2, HandlerState2} ->
+ next(Req2, State#state{handler_state=HandlerState2}, OnFalse)
+ end.
+
+call(Req, #state{handler=Handler, handler_state=HandlerState}, Fun) ->
+ case erlang:function_exported(Handler, Fun, 2) of
+ true -> Handler:Fun(Req, HandlerState);
+ false -> no_call
+ end.
+
+next(Req, State, Next) when is_function(Next) ->
+ Next(Req, State);
+next(Req, State, StatusCode) when is_integer(StatusCode) ->
+ respond(Req, State, StatusCode).
+
+%% @todo Allow some sort of callback for custom error pages.
+respond(Req, State, StatusCode) ->
+ {ok, Req2} = cowboy_http_req:reply(StatusCode, Req),
+ terminate(Req2, State).
+
+terminate(Req, #state{handler=Handler, handler_state=HandlerState}) ->
+ case erlang:function_exported(Handler, rest_terminate, 2) of
+ true -> ok = Handler:rest_terminate(
+ Req#http_req{resp_state=locked}, HandlerState);
+ false -> ok
+ end,
+ {ok, Req}.
diff --git a/src/cowboy_http_static.erl b/src/cowboy_http_static.erl
new file mode 100644
index 0000000..3e3cb9e
--- /dev/null
+++ b/src/cowboy_http_static.erl
@@ -0,0 +1,461 @@
+%% Copyright (c) 2011, Magnus Klaar <[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 Static resource handler.
+%%
+%% This built in HTTP handler provides a simple file serving capability for
+%% cowboy applications. It should be considered an experimental feature because
+%% of it's dependency on the experimental REST handler. It's recommended to be
+%% used for small or temporary environments where it is not preferrable to set
+%% up a second server just to serve files.
+%%
+%% If this handler is used the Erlang node running the cowboy application must
+%% be configured to use an async thread pool. This is configured by adding the
+%% `+A $POOL_SIZE' argument to the `erl' command used to start the node. See
+%% <a href="http://erlang.org/pipermail/erlang-bugs/2012-January/002720.html">
+%% this reply</a> from the OTP team to erlang-bugs
+%%
+%% == Base configuration ==
+%%
+%% The handler must be configured with a request path prefix to serve files
+%% under and the path to a directory to read files from. The request path prefix
+%% is defined in the path pattern of the cowboy dispatch rule for the handler.
+%% The request path pattern must end with a ``'...''' token.
+%% The directory path can be set to either an absolute or relative path in the
+%% form of a list or binary string representation of a file system path. A list
+%% of binary path segments, as is used throughout cowboy, is also a valid
+%% directory path.
+%%
+%% The directory path can also be set to a relative path within the `priv/'
+%% directory of an application. This is configured by setting the value of the
+%% directory option to a tuple of the form `{priv_dir, Application, Relpath}'.
+%%
+%% ==== Examples ====
+%% ```
+%% %% Serve files from /var/www/ under http://example.com/static/
+%% {[<<"static">>, '...'], cowboy_http_static,
+%% [{directory, "/var/www"}]}
+%%
+%% %% Serve files from the current working directory under http://example.com/static/
+%% {[<<"static">>, '...'], cowboy_http_static,
+%% [{directory, <<"./">>}]}
+%%
+%% %% Serve files from cowboy/priv/www under http://example.com/
+%% {['...'], cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, [<<"www">>]}}]}
+%% '''
+%%
+%% == Content type configuration ==
+%%
+%% By default the content type of all static resources will be set to
+%% `application/octet-stream'. This can be overriden by supplying a list
+%% of filename extension to mimetypes pairs in the `mimetypes' option.
+%% The filename extension should be a binary string including the leading dot.
+%% The mimetypes must be of a type that the `cowboy_http_rest' protocol can
+%% handle.
+%%
+%% The <a href="https://github.com/spawngrid/mimetypes">spawngrid/mimetypes</a>
+%% application, or an arbitrary function accepting the path to the file being
+%% served, can also be used to generate the list of content types for a static
+%% file resource. The function used must accept an additional argument after
+%% the file path argument.
+%%
+%% ==== Example ====
+%% ```
+%% %% Use a static list of content types.
+%% {[<<"static">>, '...'], cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, []}},
+%% {mimetypes, [
+%% {<<".css">>, [<<"text/css">>]},
+%% {<<".js">>, [<<"application/javascript">>]}]}]}
+%%
+%% %% Use the default database in the mimetypes application.
+%% {[<<"static">>, '...', cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, []}},
+%% {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]]}
+%% '''
+%%
+%% == ETag Header Function ==
+%%
+%% The default behaviour of the static file handler is to not generate ETag
+%% headers. This is because generating ETag headers based on file metadata
+%% causes different servers in a cluster to generate different ETag values for
+%% the same file unless the metadata is also synced. Generating strong ETags
+%% based on the contents of a file is currently out of scope for this module.
+%%
+%% The default behaviour can be overridden to generate an ETag header based on
+%% a combination of the file path, file size, inode and mtime values. If the
+%% option value is a list of attribute names tagged with `attributes' a hex
+%% encoded CRC32 checksum of the attribute values are used as the ETag header
+%% value.
+%%
+%% If a strong ETag is required a user defined function for generating the
+%% header value can be supplied. The function must accept a proplist of the
+%% file attributes as the first argument and a second argument containing any
+%% additional data that the function requires. The function must return a
+%% `binary()' or `undefined'.
+%%
+%% ==== Examples ====
+%% ```
+%% %% A value of default is equal to not specifying the option.
+%% {[<<"static">>, '...', cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, []}},
+%% {etag, default}]]}
+%%
+%% %% Use all avaliable ETag function arguments to generate a header value.
+%% {[<<"static">>, '...', cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, []}},
+%% {etag, {attributes, [filepath, filesize, inode, mtime]}}]]}
+%%
+%% %% Use a user defined function to generate a strong ETag header value.
+%% {[<<"static">>, '...', cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, []}},
+%% {etag, {fun generate_strong_etag/2, strong_etag_extra}}]]}
+%%
+%% generate_strong_etag(Arguments, strong_etag_extra) ->
+%% {_, Filepath} = lists:keyfind(filepath, 1, Arguments),
+%% {_, _Filesize} = lists:keyfind(filesize, 1, Arguments),
+%% {_, _INode} = lists:keyfind(inode, 1, Arguments),
+%% {_, _Modified} = lists:keyfind(mtime, 1, Arguments),
+%% ChecksumCommand = lists:flatten(io_lib:format("sha1sum ~s", [Filepath])),
+%% [Checksum|_] = string:tokens(os:cmd(ChecksumCommand), " "),
+%% iolist_to_binary(Checksum).
+%% '''
+-module(cowboy_http_static).
+
+%% include files
+-include("http.hrl").
+-include_lib("kernel/include/file.hrl").
+
+%% cowboy_http_protocol callbacks
+-export([init/3]).
+
+%% cowboy_http_rest callbacks
+-export([rest_init/2, allowed_methods/2, malformed_request/2,
+ resource_exists/2, forbidden/2, last_modified/2, generate_etag/2,
+ content_types_provided/2, file_contents/2]).
+
+%% internal
+-export([path_to_mimetypes/2]).
+
+%% types
+-type dirpath() :: string() | binary() | [binary()].
+-type dirspec() :: dirpath() | {priv, atom(), dirpath()}.
+-type mimedef() :: {binary(), binary(), [{binary(), binary()}]}.
+-type etagarg() :: {filepath, binary()} | {mtime, cowboy_clock:datetime()}
+ | {inode, non_neg_integer()} | {filesize, non_neg_integer()}.
+
+%% handler state
+-record(state, {
+ filepath :: binary() | error,
+ fileinfo :: {ok, #file_info{}} | {error, _} | error,
+ mimetypes :: {fun((binary(), T) -> [mimedef()]), T} | undefined,
+ etag_fun :: {fun(([etagarg()], T) -> undefined | binary()), T}}).
+
+
+%% @private Upgrade from HTTP handler to REST handler.
+init({_Transport, http}, _Req, _Opts) ->
+ {upgrade, protocol, cowboy_http_rest}.
+
+
+%% @private Set up initial state of REST handler.
+-spec rest_init(#http_req{}, list()) -> {ok, #http_req{}, #state{}}.
+rest_init(Req, Opts) ->
+ Directory = proplists:get_value(directory, Opts),
+ Directory1 = directory_path(Directory),
+ Mimetypes = proplists:get_value(mimetypes, Opts, []),
+ Mimetypes1 = case Mimetypes of
+ {_, _} -> Mimetypes;
+ [] -> {fun path_to_mimetypes/2, []};
+ [_|_] -> {fun path_to_mimetypes/2, Mimetypes}
+ end,
+ ETagFunction = case proplists:get_value(etag, Opts) of
+ default -> {fun no_etag_function/2, undefined};
+ undefined -> {fun no_etag_function/2, undefined};
+ {attributes, Attrs} -> {fun attr_etag_function/2, Attrs};
+ {_, _}=EtagFunction1 -> EtagFunction1
+ end,
+ {Filepath, Req1} = cowboy_http_req:path_info(Req),
+ State = case check_path(Filepath) of
+ error ->
+ #state{filepath=error, fileinfo=error, mimetypes=undefined,
+ etag_fun=ETagFunction};
+ ok ->
+ Filepath1 = join_paths(Directory1, Filepath),
+ Fileinfo = file:read_file_info(Filepath1),
+ #state{filepath=Filepath1, fileinfo=Fileinfo, mimetypes=Mimetypes1,
+ etag_fun=ETagFunction}
+ end,
+ {ok, Req1, State}.
+
+
+%% @private Only allow GET and HEAD requests on files.
+-spec allowed_methods(#http_req{}, #state{}) ->
+ {[atom()], #http_req{}, #state{}}.
+allowed_methods(Req, State) ->
+ {['GET', 'HEAD'], Req, State}.
+
+%% @private
+-spec malformed_request(#http_req{}, #state{}) ->
+ {boolean(), #http_req{}, #state{}}.
+malformed_request(Req, #state{filepath=error}=State) ->
+ {true, Req, State};
+malformed_request(Req, State) ->
+ {false, Req, State}.
+
+
+%% @private Check if the resource exists under the document root.
+-spec resource_exists(#http_req{}, #state{}) ->
+ {boolean(), #http_req{}, #state{}}.
+resource_exists(Req, #state{fileinfo={error, _}}=State) ->
+ {false, Req, State};
+resource_exists(Req, #state{fileinfo={ok, Fileinfo}}=State) ->
+ {Fileinfo#file_info.type =:= regular, Req, State}.
+
+
+%% @private
+%% Access to a file resource is forbidden if it exists and the local node does
+%% not have permission to read it. Directory listings are always forbidden.
+-spec forbidden(#http_req{}, #state{}) -> {boolean(), #http_req{}, #state{}}.
+forbidden(Req, #state{fileinfo={_, #file_info{type=directory}}}=State) ->
+ {true, Req, State};
+forbidden(Req, #state{fileinfo={error, eacces}}=State) ->
+ {true, Req, State};
+forbidden(Req, #state{fileinfo={error, _}}=State) ->
+ {false, Req, State};
+forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=State) ->
+ {not (Access =:= read orelse Access =:= read_write), Req, State}.
+
+
+%% @private Read the time a file system system object was last modified.
+-spec last_modified(#http_req{}, #state{}) ->
+ {cowboy_clock:datetime(), #http_req{}, #state{}}.
+last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) ->
+ {Modified, Req, State}.
+
+
+%% @private Generate the ETag header value for this file.
+%% The ETag header value is only generated if the resource is a file that
+%% exists in document root.
+-spec generate_etag(#http_req{}, #state{}) ->
+ {undefined | binary(), #http_req{}, #state{}}.
+generate_etag(Req, #state{fileinfo={_, #file_info{type=regular, inode=INode,
+ mtime=Modified, size=Filesize}}, filepath=Filepath,
+ etag_fun={ETagFun, ETagData}}=State) ->
+ ETagArgs = [
+ {filepath, Filepath}, {filesize, Filesize},
+ {inode, INode}, {mtime, Modified}],
+ {ETagFun(ETagArgs, ETagData), Req, State};
+generate_etag(Req, State) ->
+ {undefined, Req, State}.
+
+
+%% @private Return the content type of a file.
+-spec content_types_provided(#http_req{}, #state{}) -> tuple().
+content_types_provided(Req, #state{filepath=Filepath,
+ mimetypes={MimetypesFun, MimetypesData}}=State) ->
+ Mimetypes = [{T, file_contents}
+ || T <- MimetypesFun(Filepath, MimetypesData)],
+ {Mimetypes, Req, State}.
+
+
+%% @private Return a function that writes a file directly to the socket.
+-spec file_contents(#http_req{}, #state{}) -> tuple().
+file_contents(Req, #state{filepath=Filepath,
+ fileinfo={ok, #file_info{size=Filesize}}}=State) ->
+ {ok, Transport, Socket} = cowboy_http_req:transport(Req),
+ Writefile = content_function(Transport, Socket, Filepath),
+ {{stream, Filesize, Writefile}, Req, State}.
+
+
+%% @private Return a function writing the contents of a file to a socket.
+%% The function returns the number of bytes written to the socket to enable
+%% the calling function to determine if the expected number of bytes were
+%% written to the socket.
+-spec content_function(module(), inet:socket(), binary()) ->
+ fun(() -> {sent, non_neg_integer()}).
+content_function(Transport, Socket, Filepath) ->
+ %% `file:sendfile/2' will only work with the `cowboy_tcp_transport'
+ %% transport module. SSL or future SPDY transports that require the
+ %% content to be encrypted or framed as the content is sent.
+ case erlang:function_exported(file, sendfile, 2) of
+ false ->
+ fun() -> sfallback(Transport, Socket, Filepath) end;
+ _ when Transport =/= cowboy_tcp_transport ->
+ fun() -> sfallback(Transport, Socket, Filepath) end;
+ true ->
+ fun() -> sendfile(Socket, Filepath) end
+ end.
+
+
+%% @private Sendfile fallback function.
+-spec sfallback(module(), inet:socket(), binary()) -> {sent, non_neg_integer()}.
+sfallback(Transport, Socket, Filepath) ->
+ {ok, File} = file:open(Filepath, [read,binary,raw]),
+ sfallback(Transport, Socket, File, 0).
+
+-spec sfallback(module(), inet:socket(), file:io_device(),
+ non_neg_integer()) -> {sent, non_neg_integer()}.
+sfallback(Transport, Socket, File, Sent) ->
+ case file:read(File, 16#1FFF) of
+ eof ->
+ ok = file:close(File),
+ {sent, Sent};
+ {ok, Bin} ->
+ ok = Transport:send(Socket, Bin),
+ sfallback(Transport, Socket, File, Sent + byte_size(Bin))
+ end.
+
+
+%% @private Wrapper for sendfile function.
+-spec sendfile(inet:socket(), binary()) -> {sent, non_neg_integer()}.
+sendfile(Socket, Filepath) ->
+ {ok, Sent} = file:sendfile(Filepath, Socket),
+ {sent, Sent}.
+
+-spec directory_path(dirspec()) -> dirpath().
+directory_path({priv_dir, App, []}) ->
+ priv_dir_path(App);
+directory_path({priv_dir, App, [H|_]=Path}) when is_integer(H) ->
+ filename:join(priv_dir_path(App), Path);
+directory_path({priv_dir, App, [H|_]=Path}) when is_binary(H) ->
+ filename:join(filename:split(priv_dir_path(App)) ++ Path);
+directory_path({priv_dir, App, Path}) when is_binary(Path) ->
+ filename:join(priv_dir_path(App), Path);
+directory_path(Path) ->
+ Path.
+
+
+%% @private Validate a request path for unsafe characters.
+%% There is no way to escape special characters in a filesystem path.
+-spec check_path(Path::[binary()]) -> ok | error.
+check_path([]) -> ok;
+check_path([<<"">>|_T]) -> error;
+check_path([<<".">>|_T]) -> error;
+check_path([<<"..">>|_T]) -> error;
+check_path([H|T]) ->
+ case binary:match(H, <<"/">>) of
+ {_, _} -> error;
+ nomatch -> check_path(T)
+ end.
+
+
+%% @private Join the the directory and request paths.
+-spec join_paths(dirpath(), [binary()]) -> binary().
+join_paths([H|_]=Dirpath, Filepath) when is_integer(H) ->
+ filename:join(filename:split(Dirpath) ++ Filepath);
+join_paths([H|_]=Dirpath, Filepath) when is_binary(H) ->
+ filename:join(Dirpath ++ Filepath);
+join_paths(Dirpath, Filepath) when is_binary(Dirpath) ->
+ filename:join([Dirpath] ++ Filepath);
+join_paths([], Filepath) ->
+ filename:join(Filepath).
+
+
+%% @private Return the path to the priv/ directory of an application.
+-spec priv_dir_path(atom()) -> string().
+priv_dir_path(App) ->
+ case code:priv_dir(App) of
+ {error, bad_name} -> priv_dir_mod(App);
+ Dir -> Dir
+ end.
+
+-spec priv_dir_mod(atom()) -> string().
+priv_dir_mod(Mod) ->
+ case code:which(Mod) of
+ File when not is_list(File) -> "../priv";
+ File -> filename:join([filename:dirname(File),"../priv"])
+ end.
+
+
+%% @private Use application/octet-stream as the default mimetype.
+%% If a list of extension - mimetype pairs are provided as the mimetypes
+%% an attempt to find the mimetype using the file extension. If no match
+%% is found the default mimetype is returned.
+-spec path_to_mimetypes(binary(), [{binary(), [mimedef()]}]) ->
+ [mimedef()].
+path_to_mimetypes(Filepath, Extensions) when is_binary(Filepath) ->
+ Ext = filename:extension(Filepath),
+ case Ext of
+ <<>> -> default_mimetype();
+ _Ext -> path_to_mimetypes_(Ext, Extensions)
+ end.
+
+-spec path_to_mimetypes_(binary(), [{binary(), [mimedef()]}]) -> [mimedef()].
+path_to_mimetypes_(Ext, Extensions) ->
+ case lists:keyfind(Ext, 1, Extensions) of
+ {_, MTs} -> MTs;
+ _Unknown -> default_mimetype()
+ end.
+
+-spec default_mimetype() -> [mimedef()].
+default_mimetype() ->
+ [{<<"application">>, <<"octet-stream">>, []}].
+
+
+%% @private Do not send ETag headers in the default configuration.
+-spec no_etag_function([etagarg()], undefined) -> undefined.
+no_etag_function(_Args, undefined) ->
+ undefined.
+
+%% @private A simple alternative is to send an ETag based on file attributes.
+-type fileattr() :: filepath | filesize | mtime | inode.
+-spec attr_etag_function([etagarg()], [fileattr()]) -> binary().
+attr_etag_function(Args, Attrs) ->
+ attr_etag_function(Args, Attrs, []).
+
+-spec attr_etag_function([etagarg()], [fileattr()], [binary()]) -> binary().
+attr_etag_function(_Args, [], Acc) ->
+ list_to_binary(integer_to_list(erlang:crc32(Acc), 16));
+attr_etag_function(Args, [H|T], Acc) ->
+ {_, Value} = lists:keyfind(H, 1, Args),
+ attr_etag_function(Args, T, [term_to_binary(Value)|Acc]).
+
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-define(_eq(E, I), ?_assertEqual(E, I)).
+
+check_path_test_() ->
+ C = fun check_path/1,
+ [?_eq(error, C([<<>>])),
+ ?_eq(ok, C([<<"abc">>])),
+ ?_eq(error, C([<<".">>])),
+ ?_eq(error, C([<<"..">>])),
+ ?_eq(error, C([<<"/">>]))
+ ].
+
+join_paths_test_() ->
+ P = fun join_paths/2,
+ [?_eq(<<"a">>, P([], [<<"a">>])),
+ ?_eq(<<"a/b/c">>, P(<<"a/b">>, [<<"c">>])),
+ ?_eq(<<"a/b/c">>, P("a/b", [<<"c">>])),
+ ?_eq(<<"a/b/c">>, P([<<"a">>, <<"b">>], [<<"c">>]))
+ ].
+
+directory_path_test_() ->
+ P = fun directory_path/1,
+ PL = fun(I) -> length(filename:split(P(I))) end,
+ Base = PL({priv_dir, cowboy, []}),
+ [?_eq(Base + 1, PL({priv_dir, cowboy, "a"})),
+ ?_eq(Base + 1, PL({priv_dir, cowboy, <<"a">>})),
+ ?_eq(Base + 1, PL({priv_dir, cowboy, [<<"a">>]})),
+ ?_eq(Base + 2, PL({priv_dir, cowboy, "a/b"})),
+ ?_eq(Base + 2, PL({priv_dir, cowboy, <<"a/b">>})),
+ ?_eq(Base + 2, PL({priv_dir, cowboy, [<<"a">>, <<"b">>]})),
+ ?_eq("a/b", P("a/b"))
+ ].
+
+
+-endif.
diff --git a/src/cowboy_http_websocket.erl b/src/cowboy_http_websocket.erl
index 08a0c90..0f0204c 100644
--- a/src/cowboy_http_websocket.erl
+++ b/src/cowboy_http_websocket.erl
@@ -30,9 +30,9 @@
%% <li>Firefox 6</li>
%% </ul>
%%
-%% Version 8 is supported by the following browsers:
+%% Version 8+ is supported by the following browsers:
%% <ul>
-%% <li>Firefox 7</li>
+%% <li>Firefox 7+</li>
%% <li>Chrome 14+</li>
%% </ul>
-module(cowboy_http_websocket).
@@ -64,7 +64,7 @@
%% 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(), #http_req{}) -> ok | none().
+-spec upgrade(pid(), module(), any(), #http_req{}) -> closed | none().
upgrade(ListenerPid, Handler, Opts, Req) ->
cowboy_listener:move_connection(ListenerPid, websocket, self()),
case catch websocket_upgrade(#state{handler=Handler, opts=Opts}, Req) of
@@ -72,16 +72,13 @@ upgrade(ListenerPid, Handler, Opts, Req) ->
{'EXIT', _Reason} -> upgrade_error(Req)
end.
-%% @todo We need a function to properly parse headers according to their ABNF,
-%% instead of having ugly code like this case here.
-%% @todo Upgrade is a list of products and should be parsed as such.
-spec websocket_upgrade(#state{}, #http_req{}) -> {ok, #state{}, #http_req{}}.
websocket_upgrade(State, Req) ->
{ConnTokens, Req2}
= cowboy_http_req:parse_header('Connection', Req),
true = lists:member(<<"upgrade">>, ConnTokens),
- {WS, Req3} = cowboy_http_req:header('Upgrade', Req2),
- <<"websocket">> = cowboy_bstr:to_lower(WS),
+ %% @todo Should probably send a 426 if the Upgrade header is missing.
+ {[<<"websocket">>], Req3} = cowboy_http_req:parse_header('Upgrade', Req2),
{Version, Req4} = cowboy_http_req:header(<<"Sec-Websocket-Version">>, Req3),
websocket_upgrade(Version, State, Req4).
@@ -95,25 +92,26 @@ websocket_upgrade(State, Req) ->
%% 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) ->
+websocket_upgrade(undefined, State, Req=#http_req{meta=Meta}) ->
{Origin, Req2} = cowboy_http_req:header(<<"Origin">>, Req),
{Key1, Req3} = cowboy_http_req:header(<<"Sec-Websocket-Key1">>, Req2),
{Key2, Req4} = cowboy_http_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}, Req4};
+ eop=EOP}, Req4#http_req{meta=[{websocket_version, 0}|Meta]}};
%% Versions 7 and 8. Implementation follows the hybi 7 through 17 drafts.
-websocket_upgrade(Version, State, Req)
+websocket_upgrade(Version, State, Req=#http_req{meta=Meta})
when Version =:= <<"7">>; Version =:= <<"8">>;
Version =:= <<"13">> ->
{Key, Req2} = cowboy_http_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}, Req2}.
+ {ok, State#state{version=IntVersion, challenge=Challenge},
+ Req2#http_req{meta=[{websocket_version, IntVersion}|Meta]}}.
--spec handler_init(#state{}, #http_req{}) -> ok | none().
+-spec handler_init(#state{}, #http_req{}) -> closed | none().
handler_init(State=#state{handler=Handler, opts=Opts},
Req=#http_req{transport=Transport}) ->
try Handler:websocket_init(Transport:name(), Req, Opts) of
@@ -139,31 +137,27 @@ handler_init(State=#state{handler=Handler, opts=Opts},
[Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()])
end.
--spec upgrade_error(#http_req{}) -> ok.
+-spec upgrade_error(#http_req{}) -> closed.
upgrade_error(Req) ->
- {ok, Req2} = cowboy_http_req:reply(400, [], [],
+ {ok, _Req2} = cowboy_http_req:reply(400, [], [],
Req#http_req{resp_state=waiting}),
- upgrade_terminate(Req2).
+ closed.
%% @see cowboy_http_protocol:ensure_response/1
--spec upgrade_denied(#http_req{}) -> ok.
-upgrade_denied(Req=#http_req{resp_state=done}) ->
- upgrade_terminate(Req);
+-spec upgrade_denied(#http_req{}) -> closed.
+upgrade_denied(#http_req{resp_state=done}) ->
+ closed;
upgrade_denied(Req=#http_req{resp_state=waiting}) ->
- {ok, Req2} = cowboy_http_req:reply(400, [], [], Req),
- upgrade_terminate(Req2);
-upgrade_denied(Req=#http_req{method='HEAD', resp_state=chunks}) ->
- upgrade_terminate(Req);
-upgrade_denied(Req=#http_req{socket=Socket, transport=Transport,
+ {ok, _Req2} = cowboy_http_req:reply(400, [], [], Req),
+ closed;
+upgrade_denied(#http_req{method='HEAD', resp_state=chunks}) ->
+ closed;
+upgrade_denied(#http_req{socket=Socket, transport=Transport,
resp_state=chunks}) ->
Transport:send(Socket, <<"0\r\n\r\n">>),
- upgrade_terminate(Req).
+ closed.
--spec upgrade_terminate(#http_req{}) -> ok.
-upgrade_terminate(#http_req{socket=Socket, transport=Transport}) ->
- Transport:close(Socket).
-
--spec websocket_handshake(#state{}, #http_req{}, any()) -> ok | none().
+-spec websocket_handshake(#state{}, #http_req{}, any()) -> closed | none().
websocket_handshake(State=#state{version=0, origin=Origin,
challenge={Key1, Key2}}, Req=#http_req{socket=Socket,
transport=Transport, raw_host=Host, port=Port,
@@ -175,14 +169,20 @@ websocket_handshake(State=#state{version=0, origin=Origin,
{<<"Sec-Websocket-Location">>, Location},
{<<"Sec-Websocket-Origin">>, Origin}],
Req#http_req{resp_state=waiting}),
+ %% Flush the resp_sent message before moving on.
+ receive {cowboy_http_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.
- {ok, Key3, Req3} = cowboy_http_req:body(8, Req2),
- Challenge = hixie76_challenge(Key1, Key2, Key3),
- Transport:send(Socket, Challenge),
- handler_before_loop(State#state{messages=Transport:messages()},
- Req3, HandlerState, <<>>);
+ case cowboy_http_req:body(8, Req2) of
+ {ok, Key3, Req3} ->
+ Challenge = hixie76_challenge(Key1, Key2, Key3),
+ Transport:send(Socket, Challenge),
+ handler_before_loop(State#state{messages=Transport:messages()},
+ Req3, HandlerState, <<>>);
+ _Any ->
+ closed %% If an error happened reading the body, stop there.
+ end;
websocket_handshake(State=#state{challenge=Challenge},
Req=#http_req{transport=Transport}, HandlerState) ->
{ok, Req2} = cowboy_http_req:upgrade_reply(
@@ -190,10 +190,12 @@ websocket_handshake(State=#state{challenge=Challenge},
[{<<"Upgrade">>, <<"websocket">>},
{<<"Sec-Websocket-Accept">>, Challenge}],
Req#http_req{resp_state=waiting}),
+ %% Flush the resp_sent message before moving on.
+ receive {cowboy_http_req, resp_sent} -> ok after 0 -> ok end,
handler_before_loop(State#state{messages=Transport:messages()},
Req2, HandlerState, <<>>).
--spec handler_before_loop(#state{}, #http_req{}, any(), binary()) -> ok | none().
+-spec handler_before_loop(#state{}, #http_req{}, any(), binary()) -> closed | none().
handler_before_loop(State=#state{hibernate=true},
Req=#http_req{socket=Socket, transport=Transport},
HandlerState, SoFar) ->
@@ -218,7 +220,7 @@ handler_loop_timeout(State=#state{timeout=Timeout, timeout_ref=PrevRef}) ->
State#state{timeout_ref=TRef}.
%% @private
--spec handler_loop(#state{}, #http_req{}, any(), binary()) -> ok | none().
+-spec handler_loop(#state{}, #http_req{}, any(), binary()) -> closed | none().
handler_loop(State=#state{messages={OK, Closed, Error}, timeout_ref=TRef},
Req=#http_req{socket=Socket}, HandlerState, SoFar) ->
receive
@@ -238,17 +240,17 @@ handler_loop(State=#state{messages={OK, Closed, Error}, timeout_ref=TRef},
SoFar, websocket_info, Message, fun handler_before_loop/4)
end.
--spec websocket_data(#state{}, #http_req{}, any(), binary()) -> ok | none().
+-spec websocket_data(#state{}, #http_req{}, any(), binary()) -> closed | none().
%% 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/bits >>) ->
+ << 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, _/bits >>) ->
+ Data = << 0, _/binary >>) ->
case binary:match(Data, EOP) of
{Pos, 1} ->
Pos2 = Pos - 1,
@@ -268,38 +270,54 @@ websocket_data(State=#state{version=Version}, Req, HandlerState, Data)
websocket_data(State=#state{version=Version}, Req, HandlerState, Data)
when Version =/= 0 ->
<< 1:1, 0:3, Opcode:4, Mask:1, PayloadLen:7, Rest/bits >> = Data,
- {PayloadLen2, Rest2} = case {PayloadLen, Rest} of
- {126, << L:16, R/bits >>} -> {L, R};
- {126, Rest} -> {undefined, Rest};
- {127, << 0:1, L:63, R/bits >>} -> {L, R};
- {127, Rest} -> {undefined, Rest};
- {PayloadLen, Rest} -> {PayloadLen, Rest}
- end,
- case {Mask, PayloadLen2} of
+ case {PayloadLen, Rest} of
+ {126, _} when Opcode >= 8 -> websocket_close(
+ State, Req, HandlerState, {error, protocol});
+ {127, _} when Opcode >= 8 -> websocket_close(
+ State, Req, HandlerState, {error, protocol});
+ {126, << L:16, R/bits >>} -> websocket_before_unmask(
+ State, Req, HandlerState, Data, R, Opcode, Mask, L);
+ {126, Rest} -> websocket_before_unmask(
+ State, Req, HandlerState, Data, Rest, Opcode, Mask, undefined);
+ {127, << 0:1, L:63, R/bits >>} -> websocket_before_unmask(
+ State, Req, HandlerState, Data, R, Opcode, Mask, L);
+ {127, Rest} -> websocket_before_unmask(
+ State, Req, HandlerState, Data, Rest, Opcode, Mask, undefined);
+ {PayloadLen, Rest} -> websocket_before_unmask(
+ State, Req, HandlerState, Data, Rest, Opcode, Mask, PayloadLen)
+ end;
+%% Something was wrong with the frame. Close the connection.
+websocket_data(State, Req, HandlerState, _Bad) ->
+ websocket_close(State, Req, HandlerState, {error, badframe}).
+
+%% hybi routing depending on whether unmasking is needed.
+-spec websocket_before_unmask(#state{}, #http_req{}, any(), binary(),
+ binary(), opcode(), 0 | 1, non_neg_integer() | undefined)
+ -> closed | none().
+websocket_before_unmask(State, Req, HandlerState, Data,
+ Rest, Opcode, Mask, PayloadLen) ->
+ case {Mask, PayloadLen} of
{0, 0} ->
- websocket_dispatch(State, Req, HandlerState, Rest2, Opcode, <<>>);
- {1, N} when N + 4 > byte_size(Rest2); N =:= undefined ->
+ 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:PayloadLen2/binary, Rest3/bits >> = Rest2,
- websocket_unmask(State, Req, HandlerState, Rest3,
+ << MaskKey:32, Payload:PayloadLen/binary, Rest2/bits >> = Rest,
+ websocket_unmask(State, Req, HandlerState, Rest2,
Opcode, Payload, MaskKey)
- end;
-%% Something was wrong with the frame. Close the connection.
-websocket_data(State, Req, HandlerState, _Bad) ->
- websocket_close(State, Req, HandlerState, {error, badframe}).
+ end.
%% hybi unmasking.
-spec websocket_unmask(#state{}, #http_req{}, any(), binary(),
- opcode(), binary(), mask_key()) -> ok | none().
+ opcode(), binary(), mask_key()) -> closed | none().
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, Payload, MaskKey) ->
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, Payload, MaskKey, <<>>).
-spec websocket_unmask(#state{}, #http_req{}, any(), binary(),
- opcode(), binary(), mask_key(), binary()) -> ok | none().
+ opcode(), binary(), mask_key(), binary()) -> closed | none().
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, << O:32, Rest/bits >>, MaskKey, Acc) ->
T = O bxor MaskKey,
@@ -330,7 +348,7 @@ websocket_unmask(State, Req, HandlerState, RemainingData,
%% hybi dispatching.
-spec websocket_dispatch(#state{}, #http_req{}, any(), binary(),
- opcode(), binary()) -> ok | none().
+ opcode(), binary()) -> closed | none().
%% @todo Fragmentation.
%~ websocket_dispatch(State, Req, HandlerState, RemainingData, 0, Payload) ->
%% Text frame.
@@ -358,7 +376,7 @@ websocket_dispatch(State, Req, HandlerState, RemainingData, 10, Payload) ->
websocket_handle, {pong, Payload}, fun websocket_data/4).
-spec handler_call(#state{}, #http_req{}, any(), binary(),
- atom(), any(), fun()) -> ok | none().
+ atom(), any(), fun()) -> closed | none().
handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
RemainingData, Callback, Message, NextState) ->
try Handler:Callback(Message, Req, HandlerState) of
@@ -387,7 +405,7 @@ handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
websocket_close(State, Req, HandlerState, {error, handler})
end.
--spec websocket_send(binary(), #state{}, #http_req{}) -> ok | ignore.
+-spec websocket_send(binary(), #state{}, #http_req{}) -> closed | ignore.
%% hixie-76 text frame.
websocket_send({text, Payload}, #state{version=0},
#http_req{socket=Socket, transport=Transport}) ->
@@ -407,21 +425,19 @@ websocket_send({Type, Payload}, _State,
Transport:send(Socket, [<< 1:1, 0:3, Opcode:4, 0:1, Len/bits >>,
Payload]).
--spec websocket_close(#state{}, #http_req{}, any(), {atom(), atom()}) -> ok.
+-spec websocket_close(#state{}, #http_req{}, any(), {atom(), atom()}) -> closed.
websocket_close(State=#state{version=0}, Req=#http_req{socket=Socket,
transport=Transport}, HandlerState, Reason) ->
Transport:send(Socket, << 255, 0 >>),
- Transport:close(Socket),
handler_terminate(State, Req, HandlerState, Reason);
%% @todo Send a Payload? Using Reason is usually good but we're quite careless.
websocket_close(State, Req=#http_req{socket=Socket,
transport=Transport}, HandlerState, Reason) ->
Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>),
- Transport:close(Socket),
handler_terminate(State, Req, HandlerState, Reason).
-spec handler_terminate(#state{}, #http_req{},
- any(), atom() | {atom(), atom()}) -> ok.
+ any(), atom() | {atom(), atom()}) -> closed.
handler_terminate(#state{handler=Handler, opts=Opts},
Req, HandlerState, TerminateReason) ->
try
@@ -434,7 +450,8 @@ handler_terminate(#state{handler=Handler, opts=Opts},
"** Request was ~p~n** Stacktrace: ~p~n~n",
[Handler, Class, Reason, TerminateReason, Opts,
HandlerState, Req, erlang:get_stacktrace()])
- end.
+ end,
+ closed.
%% hixie-76 specific.
diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl
index 9dc35d9..34bb1a1 100644
--- a/src/cowboy_protocol.erl
+++ b/src/cowboy_protocol.erl
@@ -24,9 +24,9 @@
%% starting the listener. The <em>start_link/4</em> function must follow
%% the supervisor start function specification.
%%
-%% After initializing your protocol, it is recommended to wait to
-%% receive a message containing the atom 'shoot', as it will ensure
-%% Cowboy has been able to fully initialize the socket.
+%% After initializing your protocol, it is recommended to call the
+%% function cowboy:accept_ack/1 with the ListenerPid as argument,
+%% as it will ensure Cowboy has been able to fully initialize the socket.
%% Anything you do past this point is up to you!
%%
%% If you need to change some socket options, like enabling raw mode
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index 21bac1f..22ebb51 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -20,24 +20,34 @@
-export([all/0, groups/0, init_per_suite/1, end_per_suite/1,
init_per_group/2, end_per_group/2]). %% ct.
-export([chunked_response/1, headers_dupe/1, headers_huge/1,
- keepalive_nl/1, multipart/1, nc_rand/1, nc_zero/1, pipeline/1, raw/1,
- ws0/1, ws8/1, ws8_single_bytes/1, ws8_init_shutdown/1,
- ws13/1, ws_timeout_hibernate/1]). %% http.
--export([http_200/1, http_404/1]). %% http and https.
+ keepalive_nl/1, max_keepalive/1, nc_rand/1, nc_zero/1,
+ pipeline/1, raw/1, set_resp_header/1, set_resp_overwrite/1,
+ set_resp_body/1, stream_body_set_resp/1, response_as_req/1,
+ static_mimetypes_function/1, static_attribute_etag/1,
+ static_function_etag/1, multipart/1]). %% http.
+-export([http_200/1, http_404/1, handler_errors/1,
+ file_200/1, file_403/1, dir_403/1, file_404/1,
+ file_400/1]). %% http and https.
-export([http_10_hostless/1]). %% misc.
+-export([rest_simple/1, rest_keepalive/1]). %% rest.
%% ct.
all() ->
- [{group, http}, {group, https}, {group, misc}].
+ [{group, http}, {group, https}, {group, misc}, {group, rest}].
groups() ->
- BaseTests = [http_200, http_404],
+ BaseTests = [http_200, http_404, handler_errors,
+ file_200, file_403, dir_403, file_404, file_400],
[{http, [], [chunked_response, headers_dupe, headers_huge,
- keepalive_nl, nc_rand, nc_zero, pipeline, raw,
- ws0, ws8, ws8_single_bytes, ws8_init_shutdown, ws13,
- ws_timeout_hibernate, multipart] ++ BaseTests},
- {https, [], BaseTests}, {misc, [], [http_10_hostless]}].
+ keepalive_nl, max_keepalive, nc_rand, nc_zero, pipeline, raw,
+ set_resp_header, set_resp_overwrite,
+ set_resp_body, response_as_req, stream_body_set_resp,
+ static_mimetypes_function, static_attribute_etag,
+ static_function_etag, multipart] ++ BaseTests},
+ {https, [], BaseTests},
+ {misc, [], [http_10_hostless]},
+ {rest, [], [rest_simple, rest_keepalive]}].
init_per_suite(Config) ->
application:start(inets),
@@ -51,13 +61,16 @@ end_per_suite(_Config) ->
init_per_group(http, Config) ->
Port = 33080,
+ Config1 = init_static_dir(Config),
cowboy:start_listener(http, 100,
cowboy_tcp_transport, [{port, Port}],
- cowboy_http_protocol, [{dispatch, init_http_dispatch()}]
+ cowboy_http_protocol, [{max_keepalive, 50},
+ {dispatch, init_http_dispatch(Config1)}]
),
- [{scheme, "http"}, {port, Port}|Config];
+ [{scheme, "http"}, {port, Port}|Config1];
init_per_group(https, Config) ->
Port = 33081,
+ Config1 = init_static_dir(Config),
application:start(crypto),
application:start(public_key),
application:start(ssl),
@@ -66,9 +79,9 @@ init_per_group(https, Config) ->
cowboy_ssl_transport, [
{port, Port}, {certfile, DataDir ++ "cert.pem"},
{keyfile, DataDir ++ "key.pem"}, {password, "cowboy"}],
- cowboy_http_protocol, [{dispatch, init_https_dispatch()}]
+ cowboy_http_protocol, [{dispatch, init_https_dispatch(Config1)}]
),
- [{scheme, "https"}, {port, Port}|Config];
+ [{scheme, "https"}, {port, Port}|Config1];
init_per_group(misc, Config) ->
Port = 33082,
cowboy:start_listener(misc, 100,
@@ -76,38 +89,93 @@ init_per_group(misc, Config) ->
cowboy_http_protocol, [{dispatch, [{'_', [
{[], http_handler, []}
]}]}]),
+ [{port, Port}|Config];
+init_per_group(rest, Config) ->
+ Port = 33083,
+ cowboy:start_listener(reset, 100,
+ cowboy_tcp_transport, [{port, Port}],
+ cowboy_http_protocol, [{dispatch, [{'_', [
+ {[<<"simple">>], rest_simple_resource, []}
+ ]}]}]),
[{port, Port}|Config].
-end_per_group(https, _Config) ->
+end_per_group(https, Config) ->
cowboy:stop_listener(https),
application:stop(ssl),
application:stop(public_key),
application:stop(crypto),
+ end_static_dir(Config),
ok;
+end_per_group(http, Config) ->
+ cowboy:stop_listener(http),
+ end_static_dir(Config);
end_per_group(Listener, _Config) ->
cowboy:stop_listener(Listener),
ok.
%% Dispatch configuration.
-init_http_dispatch() ->
+init_http_dispatch(Config) ->
[
{[<<"localhost">>], [
{[<<"chunked_response">>], chunked_handler, []},
- {[<<"websocket">>], websocket_handler, []},
- {[<<"ws_timeout_hibernate">>], ws_timeout_hibernate_handler, []},
- {[<<"ws_init_shutdown">>], websocket_handler_init_shutdown, []},
{[<<"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,
+ [{headers, [{<<"Vary">>, <<"Accept">>}]}]},
+ {[<<"set_resp">>, <<"overwrite">>], http_handler_set_resp,
+ [{headers, [{<<"Server">>, <<"DesireDrive/1.0">>}]}]},
+ {[<<"set_resp">>, <<"body">>], http_handler_set_resp,
+ [{body, <<"A flameless dance does not equal a cycle">>}]},
+ {[<<"stream_body">>, <<"set_resp">>], http_handler_stream_body,
+ [{reply, set_resp}, {body, <<"stream_body_set_resp">>}]},
+ {[<<"static">>, '...'], cowboy_http_static,
+ [{directory, ?config(static_dir, Config)},
+ {mimetypes, [{<<".css">>, [<<"text/css">>]}]}]},
+ {[<<"static_mimetypes_function">>, '...'], cowboy_http_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_http_static,
+ [{directory, ?config(static_dir, Config)},
+ {etag, {attributes, [filepath, filesize, inode, mtime]}}]},
+ {[<<"static_function_etag">>, '...'], cowboy_http_static,
+ [{directory, ?config(static_dir, Config)},
+ {etag, {fun static_function_etag/2, etag_data}}]},
{[<<"multipart">>], http_handler_multipart, []},
{[], http_handler, []}
]}
].
-init_https_dispatch() ->
- init_http_dispatch().
+init_https_dispatch(Config) ->
+ init_http_dispatch(Config).
+
+
+init_static_dir(Config) ->
+ Dir = filename:join(?config(priv_dir, Config), "static"),
+ Level1 = fun(Name) -> filename:join(Dir, Name) end,
+ ok = file:make_dir(Dir),
+ ok = file:write_file(Level1("test_file"), "test_file\n"),
+ ok = file:write_file(Level1("test_file.css"), "test_file.css\n"),
+ ok = file:write_file(Level1("test_noread"), "test_noread\n"),
+ ok = file:change_mode(Level1("test_noread"), 8#0333),
+ ok = file:write_file(Level1("test.html"), "test.html\n"),
+ ok = file:make_dir(Level1("test_dir")),
+ [{static_dir, Dir}|Config].
+
+end_static_dir(Config) ->
+ Dir = ?config(static_dir, Config),
+ Level1 = fun(Name) -> filename:join(Dir, Name) end,
+ ok = file:delete(Level1("test_file")),
+ ok = file:delete(Level1("test_file.css")),
+ ok = file:delete(Level1("test_noread")),
+ ok = file:delete(Level1("test.html")),
+ ok = file:del_dir(Level1("test_dir")),
+ ok = file:del_dir(Dir),
+ Config.
%% http.
@@ -136,7 +204,7 @@ keepalive_nl(Config) ->
{port, Port} = lists:keyfind(port, 1, Config),
{ok, Socket} = gen_tcp:connect("localhost", Port,
[binary, {active, false}, {packet, raw}]),
- ok = keepalive_nl_loop(Socket, 100),
+ ok = keepalive_nl_loop(Socket, 10),
ok = gen_tcp:close(Socket).
keepalive_nl_loop(_Socket, 0) ->
@@ -150,6 +218,26 @@ keepalive_nl_loop(Socket, N) ->
ok = gen_tcp:send(Socket, "\r\n"), %% extra nl
keepalive_nl_loop(Socket, N - 1).
+max_keepalive(Config) ->
+ {port, Port} = lists:keyfind(port, 1, Config),
+ {ok, Socket} = gen_tcp:connect("localhost", Port,
+ [binary, {active, false}, {packet, raw}]),
+ ok = max_keepalive_loop(Socket, 50),
+ {error, closed} = gen_tcp:recv(Socket, 0, 1000).
+
+max_keepalive_loop(_Socket, 0) ->
+ ok;
+max_keepalive_loop(Socket, N) ->
+ ok = gen_tcp:send(Socket, "GET / HTTP/1.1\r\n"
+ "Host: localhost\r\nConnection: keep-alive\r\n\r\n"),
+ {ok, Data} = gen_tcp:recv(Socket, 0, 6000),
+ {0, 12} = binary:match(Data, <<"HTTP/1.1 200">>),
+ case N of
+ 1 -> {_, _} = binary:match(Data, <<"Connection: close">>);
+ N -> nomatch = binary:match(Data, <<"Connection: close">>)
+ end,
+ keepalive_nl_loop(Socket, N - 1).
+
multipart(Config) ->
Url = build_url("/multipart", Config),
Body = <<
@@ -237,6 +325,40 @@ raw_req(Packet, Config) ->
gen_tcp:close(Socket),
{Packet, Res}.
+%% Send a raw request. Return the response code and the full response.
+raw_resp(Request, Config) ->
+ {port, Port} = lists:keyfind(port, 1, Config),
+ Transport = case ?config(scheme, Config) of
+ "http" -> gen_tcp;
+ "https" -> ssl
+ end,
+ {ok, Socket} = Transport:connect("localhost", Port,
+ [binary, {active, false}, {packet, raw}]),
+ ok = Transport:send(Socket, Request),
+ {StatusCode, Response} = case recv_loop(Transport, Socket, <<>>) of
+ {ok, << "HTTP/1.1 ", Str:24/bits, _Rest/bits >> = Bin} ->
+ {list_to_integer(binary_to_list(Str)), Bin};
+ {ok, Bin} ->
+ {badresp, Bin};
+ {error, Reason} ->
+ {Reason, <<>>}
+ end,
+ Transport:close(Socket),
+ {Response, StatusCode}.
+
+recv_loop(Transport, Socket, Acc) ->
+ case Transport:recv(Socket, 0, 6000) of
+ {ok, Data} ->
+ recv_loop(Transport, Socket, <<Acc/binary, Data/binary>>);
+ {error, closed} ->
+ ok = Transport:close(Socket),
+ {ok, Acc};
+ {error, Reason} ->
+ {error, Reason}
+ end.
+
+
+
raw(Config) ->
Huge = [$0 || _N <- lists:seq(1, 5000)],
Tests = [
@@ -263,241 +385,130 @@ raw(Config) ->
[{Packet, StatusCode} = raw_req(Packet, Config)
|| {Packet, StatusCode} <- Tests].
-%% 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.
-ws0(Config) ->
- {port, Port} = lists:keyfind(port, 1, Config),
- {ok, Socket} = gen_tcp:connect("localhost", Port,
- [binary, {active, false}, {packet, raw}]),
- ok = gen_tcp:send(Socket,
- "GET /websocket 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"
- "\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),
- ok = gen_tcp:send(Socket, << 255, 0 >>),
- {ok, << 255, 0 >>} = gen_tcp:recv(Socket, 0, 6000),
- {error, closed} = gen_tcp:recv(Socket, 0, 6000),
- ok.
-
-ws8(Config) ->
+set_resp_header(Config) ->
{port, Port} = lists:keyfind(port, 1, Config),
{ok, Socket} = gen_tcp:connect("localhost", Port,
[binary, {active, false}, {packet, raw}]),
- ok = gen_tcp:send(Socket, [
- "GET /websocket HTTP/1.1\r\n"
- "Host: localhost\r\n"
- "Connection: Upgrade\r\n"
- "Upgrade: websocket\r\n"
- "Sec-WebSocket-Origin: http://localhost\r\n"
- "Sec-WebSocket-Version: 8\r\n"
- "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
- "\r\n"]),
- {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
- {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-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
- = lists:keyfind("sec-websocket-accept", 1, Headers),
- 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, << 1:1, 0:3, 1:4, 0:1, 14:7, "websocket_init" >>}
- = 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, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
- = 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, << 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, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
- {error, closed} = gen_tcp:recv(Socket, 0, 6000),
- ok.
-
-ws8_single_bytes(Config) ->
- {port, Port} = lists:keyfind(port, 1, Config),
- {ok, Socket} = gen_tcp:connect("localhost", Port,
- [binary, {active, false}, {packet, raw}]),
- ok = gen_tcp:send(Socket, [
- "GET /websocket HTTP/1.1\r\n"
- "Host: localhost\r\n"
- "Connection: Upgrade\r\n"
- "Upgrade: websocket\r\n"
- "Sec-WebSocket-Origin: http://localhost\r\n"
- "Sec-WebSocket-Version: 8\r\n"
- "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
- "\r\n"]),
- {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
- {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-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
- = lists:keyfind("sec-websocket-accept", 1, Headers),
- 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 = gen_tcp:send(Socket, << 16#37 >>),
- ok = timer:sleep(100),
- ok = gen_tcp:send(Socket, << 16#fa >>),
- ok = timer:sleep(100),
- ok = gen_tcp:send(Socket, << 16#21 >>),
- ok = timer:sleep(100),
- ok = gen_tcp:send(Socket, << 16#3d >>),
- ok = timer:sleep(100),
- ok = gen_tcp:send(Socket, << 16#7f >>),
- ok = timer:sleep(100),
- ok = gen_tcp:send(Socket, << 16#9f >>),
- ok = timer:sleep(100),
- ok = gen_tcp:send(Socket, << 16#4d >>),
- ok = timer:sleep(100),
- ok = gen_tcp:send(Socket, << 16#51 >>),
- ok = timer:sleep(100),
- ok = gen_tcp:send(Socket, << 16#58 >>),
- {ok, << 1:1, 0:3, 1:4, 0:1, 14:7, "websocket_init" >>}
- = gen_tcp:recv(Socket, 0, 6000),
- {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>}
- = 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, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
- = 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, << 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, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
- {error, closed} = gen_tcp:recv(Socket, 0, 6000),
- ok.
-
-ws_timeout_hibernate(Config) ->
- {port, Port} = lists:keyfind(port, 1, Config),
- {ok, Socket} = gen_tcp:connect("localhost", Port,
- [binary, {active, false}, {packet, raw}]),
- ok = gen_tcp:send(Socket, [
- "GET /ws_timeout_hibernate HTTP/1.1\r\n"
- "Host: localhost\r\n"
- "Connection: Upgrade\r\n"
- "Upgrade: websocket\r\n"
- "Sec-WebSocket-Origin: http://localhost\r\n"
- "Sec-WebSocket-Version: 8\r\n"
- "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
- "\r\n"]),
- {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
- {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-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),
- {error, closed} = gen_tcp:recv(Socket, 0, 6000),
- ok.
+ ok = gen_tcp:send(Socket, "GET /set_resp/header HTTP/1.1\r\n"
+ "Host: localhost\r\nConnection: close\r\n\r\n"),
+ {ok, Data} = gen_tcp:recv(Socket, 0, 6000),
+ {_, _} = binary:match(Data, <<"Vary: Accept">>),
+ {_, _} = binary:match(Data, <<"Set-Cookie: ">>).
-ws8_init_shutdown(Config) ->
+set_resp_overwrite(Config) ->
{port, Port} = lists:keyfind(port, 1, Config),
{ok, Socket} = gen_tcp:connect("localhost", Port,
[binary, {active, false}, {packet, raw}]),
- ok = gen_tcp:send(Socket, [
- "GET /ws_init_shutdown HTTP/1.1\r\n"
- "Host: localhost\r\n"
- "Connection: Upgrade\r\n"
- "Upgrade: websocket\r\n"
- "Sec-WebSocket-Origin: http://localhost\r\n"
- "Sec-WebSocket-Version: 8\r\n"
- "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
- "\r\n"]),
- {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
- {ok, {http_response, {1, 1}, 403, "Forbidden"}, _Rest}
- = erlang:decode_packet(http, Handshake, []),
- {error, closed} = gen_tcp:recv(Socket, 0, 6000),
- ok.
+ ok = gen_tcp:send(Socket, "GET /set_resp/overwrite HTTP/1.1\r\n"
+ "Host: localhost\r\nConnection: close\r\n\r\n"),
+ {ok, Data} = gen_tcp:recv(Socket, 0, 6000),
+ {_Start, _Length} = binary:match(Data, <<"Server: DesireDrive/1.0">>).
-ws13(Config) ->
+set_resp_body(Config) ->
{port, Port} = lists:keyfind(port, 1, Config),
{ok, Socket} = gen_tcp:connect("localhost", Port,
[binary, {active, false}, {packet, raw}]),
- ok = gen_tcp:send(Socket, [
- "GET /websocket HTTP/1.1\r\n"
- "Host: localhost\r\n"
- "Connection: Upgrade\r\n"
- "Origin: http://localhost\r\n"
- "Sec-WebSocket-Version: 13\r\n"
- "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
- "Upgrade: websocket\r\n"
- "\r\n"]),
- {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
- {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-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
- = lists:keyfind("sec-websocket-accept", 1, Headers),
- 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, << 1:1, 0:3, 1:4, 0:1, 14:7, "websocket_init" >>}
- = 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, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
- = 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, << 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, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
- {error, closed} = gen_tcp:recv(Socket, 0, 6000),
- ok.
-
-websocket_headers({ok, http_eoh, Rest}, Acc) ->
- [Acc, Rest];
-websocket_headers({ok, {http_header, _I, Key, _R, Value}, Rest}, Acc) ->
- F = fun(S) when is_atom(S) -> S; (S) -> string:to_lower(S) end,
- websocket_headers(erlang:decode_packet(httph, Rest, []),
- [{F(Key), Value}|Acc]).
+ ok = gen_tcp:send(Socket, "GET /set_resp/body HTTP/1.1\r\n"
+ "Host: localhost\r\nConnection: close\r\n\r\n"),
+ {ok, Data} = gen_tcp:recv(Socket, 0, 6000),
+ {_Start, _Length} = binary:match(Data, <<"\r\n\r\n"
+ "A flameless dance does not equal a cycle">>).
+
+response_as_req(Config) ->
+ Packet =
+"HTTP/1.0 302 Found
+Location: http://www.google.co.il/
+Cache-Control: private
+Content-Type: text/html; charset=UTF-8
+Set-Cookie: PREF=ID=568f67013d4a7afa:FF=0:TM=1323014101:LM=1323014101:S=XqctDWC65MzKT0zC; expires=Tue, 03-Dec-2013 15:55:01 GMT; path=/; domain=.google.com
+Date: Sun, 04 Dec 2011 15:55:01 GMT
+Server: gws
+Content-Length: 221
+X-XSS-Protection: 1; mode=block
+X-Frame-Options: SAMEORIGIN
+
+<HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">
+<TITLE>302 Moved</TITLE></HEAD><BODY>
+<H1>302 Moved</H1>
+The document has moved
+<A HREF=\"http://www.google.co.il/\">here</A>.
+</BODY></HTML>",
+ {Packet, 400} = raw_req(Packet, Config).
+
+stream_body_set_resp(Config) ->
+ {Packet, 200} = raw_resp(
+ "GET /stream_body/set_resp HTTP/1.1\r\n"
+ "Host: localhost\r\nConnection: close\r\n\r\n", Config),
+ {_Start, _Length} = binary:match(Packet, <<"stream_body_set_resp">>).
+
+static_mimetypes_function(Config) ->
+ TestURL = build_url("/static_mimetypes_function/test.html", Config),
+ {ok, {{"HTTP/1.1", 200, "OK"}, Headers1, "test.html\n"}} =
+ httpc:request(TestURL),
+ "text/html" = ?config("content-type", Headers1).
+
+handler_errors(Config) ->
+ Request = fun(Case) ->
+ raw_resp(["GET /handler_errors?case=", Case, " HTTP/1.1\r\n",
+ "Host: localhost\r\n\r\n"], Config) end,
+
+ {_Packet1, 500} = Request("init_before_reply"),
+
+ {Packet2, 200} = Request("init_after_reply"),
+ nomatch = binary:match(Packet2, <<"HTTP/1.1 500">>),
+
+ {Packet3, 200} = Request("init_reply_handle_error"),
+ nomatch = binary:match(Packet3, <<"HTTP/1.1 500">>),
+
+ {_Packet4, 500} = Request("handle_before_reply"),
+
+ {Packet5, 200} = Request("handle_after_reply"),
+ nomatch = binary:match(Packet5, <<"HTTP/1.1 500">>),
+
+ {Packet6, 200} = raw_resp([
+ "GET / HTTP/1.1\r\n",
+ "Host: localhost\r\n",
+ "Connection: keep-alive\r\n\r\n",
+ "GET /handler_errors?case=handle_after_reply\r\n",
+ "Host: localhost\r\n\r\n"], Config),
+ nomatch = binary:match(Packet6, <<"HTTP/1.1 500">>),
+
+ {Packet7, 200} = raw_resp([
+ "GET / HTTP/1.1\r\n",
+ "Host: localhost\r\n",
+ "Connection: keep-alive\r\n\r\n",
+ "GET /handler_errors?case=handle_before_reply HTTP/1.1\r\n",
+ "Host: localhost\r\n\r\n"], Config),
+ {{_, _}, _} = {binary:match(Packet7, <<"HTTP/1.1 500">>), Packet7},
+
+ done.
+
+static_attribute_etag(Config) ->
+ TestURL = build_url("/static_attribute_etag/test.html", Config),
+ {ok, {{"HTTP/1.1", 200, "OK"}, Headers1, "test.html\n"}} =
+ httpc:request(TestURL),
+ false = ?config("etag", Headers1) =:= undefined,
+ {ok, {{"HTTP/1.1", 200, "OK"}, Headers2, "test.html\n"}} =
+ httpc:request(TestURL),
+ true = ?config("etag", Headers1) =:= ?config("etag", Headers2).
+
+static_function_etag(Config) ->
+ TestURL = build_url("/static_function_etag/test.html", Config),
+ {ok, {{"HTTP/1.1", 200, "OK"}, Headers1, "test.html\n"}} =
+ httpc:request(TestURL),
+ false = ?config("etag", Headers1) =:= undefined,
+ {ok, {{"HTTP/1.1", 200, "OK"}, Headers2, "test.html\n"}} =
+ httpc:request(TestURL),
+ true = ?config("etag", Headers1) =:= ?config("etag", Headers2).
+
+static_function_etag(Arguments, etag_data) ->
+ {_, Filepath} = lists:keyfind(filepath, 1, Arguments),
+ {_, _Filesize} = lists:keyfind(filesize, 1, Arguments),
+ {_, _INode} = lists:keyfind(inode, 1, Arguments),
+ {_, _Modified} = lists:keyfind(mtime, 1, Arguments),
+ ChecksumCommand = lists:flatten(io_lib:format("sha1sum ~s", [Filepath])),
+ [Checksum|_] = string:tokens(os:cmd(ChecksumCommand), " "),
+ iolist_to_binary(Checksum).
%% http and https.
@@ -514,8 +525,61 @@ http_404(Config) ->
{ok, {{"HTTP/1.1", 404, "Not Found"}, _Headers, _Body}} =
httpc:request(build_url("/not/found", Config)).
+file_200(Config) ->
+ {ok, {{"HTTP/1.1", 200, "OK"}, Headers, "test_file\n"}} =
+ httpc:request(build_url("/static/test_file", Config)),
+ "application/octet-stream" = ?config("content-type", Headers),
+
+ {ok, {{"HTTP/1.1", 200, "OK"}, Headers1, "test_file.css\n"}} =
+ httpc:request(build_url("/static/test_file.css", Config)),
+ "text/css" = ?config("content-type", Headers1).
+
+file_403(Config) ->
+ {ok, {{"HTTP/1.1", 403, "Forbidden"}, _Headers, _Body}} =
+ httpc:request(build_url("/static/test_noread", Config)).
+
+dir_403(Config) ->
+ {ok, {{"HTTP/1.1", 403, "Forbidden"}, _Headers, _Body}} =
+ httpc:request(build_url("/static/test_dir", Config)),
+ {ok, {{"HTTP/1.1", 403, "Forbidden"}, _Headers, _Body}} =
+ httpc:request(build_url("/static/test_dir/", Config)).
+
+file_404(Config) ->
+ {ok, {{"HTTP/1.1", 404, "Not Found"}, _Headers, _Body}} =
+ httpc:request(build_url("/static/not_found", Config)).
+
+file_400(Config) ->
+ {ok, {{"HTTP/1.1", 400, "Bad Request"}, _Headers, _Body}} =
+ httpc:request(build_url("/static/%2f", Config)),
+ {ok, {{"HTTP/1.1", 400, "Bad Request"}, _Headers1, _Body1}} =
+ httpc:request(build_url("/static/%2e", Config)),
+ {ok, {{"HTTP/1.1", 400, "Bad Request"}, _Headers2, _Body2}} =
+ httpc:request(build_url("/static/%2e%2e", Config)).
%% misc.
http_10_hostless(Config) ->
Packet = "GET / HTTP/1.0\r\n\r\n",
{Packet, 200} = raw_req(Packet, Config).
+
+%% rest.
+
+rest_simple(Config) ->
+ Packet = "GET /simple HTTP/1.1\r\nHost: localhost\r\n\r\n",
+ {Packet, 200} = raw_req(Packet, Config).
+
+rest_keepalive(Config) ->
+ {port, Port} = lists:keyfind(port, 1, Config),
+ {ok, Socket} = gen_tcp:connect("localhost", Port,
+ [binary, {active, false}, {packet, raw}]),
+ ok = rest_keepalive_loop(Socket, 100),
+ ok = gen_tcp:close(Socket).
+
+rest_keepalive_loop(_Socket, 0) ->
+ ok;
+rest_keepalive_loop(Socket, N) ->
+ ok = gen_tcp:send(Socket, "GET /simple HTTP/1.1\r\n"
+ "Host: localhost\r\nConnection: keep-alive\r\n\r\n"),
+ {ok, Data} = gen_tcp:recv(Socket, 0, 6000),
+ {0, 12} = binary:match(Data, <<"HTTP/1.1 200">>),
+ nomatch = binary:match(Data, <<"Connection: close">>),
+ rest_keepalive_loop(Socket, N - 1).
diff --git a/test/http_handler_errors.erl b/test/http_handler_errors.erl
new file mode 100644
index 0000000..1c23207
--- /dev/null
+++ b/test/http_handler_errors.erl
@@ -0,0 +1,40 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(http_handler_errors).
+-behaviour(cowboy_http_handler).
+-export([init/3, handle/2, terminate/2]).
+
+init({_Transport, http}, Req, _Opts) ->
+ {Case, Req1} = cowboy_http_req:qs_val(<<"case">>, Req),
+ case_init(Case, Req1).
+
+case_init(<<"init_before_reply">> = Case, _Req) ->
+ erlang:error(Case);
+
+case_init(<<"init_after_reply">> = Case, Req) ->
+ {ok, _Req1} = cowboy_http_req:reply(200, [], "http_handler_crashes", Req),
+ erlang:error(Case);
+
+case_init(<<"init_reply_handle_error">> = Case, Req) ->
+ {ok, Req1} = cowboy_http_req:reply(200, [], "http_handler_crashes", Req),
+ {ok, Req1, Case};
+
+case_init(<<"handle_before_reply">> = Case, Req) ->
+ {ok, Req, Case};
+
+case_init(<<"handle_after_reply">> = Case, Req) ->
+ {ok, Req, Case}.
+
+
+handle(_Req, <<"init_reply_handle_error">> = Case) ->
+ erlang:error(Case);
+
+handle(_Req, <<"handle_before_reply">> = Case) ->
+ erlang:error(Case);
+
+handle(Req, <<"handle_after_reply">> = Case) ->
+ {ok, _Req1} = cowboy_http_req:reply(200, [], "http_handler_crashes", Req),
+ erlang:error(Case).
+
+terminate(_Req, _State) ->
+ ok.
diff --git a/test/http_handler_multipart.erl b/test/http_handler_multipart.erl
index 773b61e..f5f7919 100644
--- a/test/http_handler_multipart.erl
+++ b/test/http_handler_multipart.erl
@@ -10,7 +10,7 @@ init({_Transport, http}, Req, []) ->
handle(Req, State) ->
{Result, Req2} = acc_multipart(Req, []),
{ok, Req3} = cowboy_http_req:reply(200, [], term_to_binary(Result), Req2),
- {ok, Req, State}.
+ {ok, Req3, State}.
terminate(_Req, _State) ->
ok.
diff --git a/test/http_handler_set_resp.erl b/test/http_handler_set_resp.erl
new file mode 100644
index 0000000..83d48c0
--- /dev/null
+++ b/test/http_handler_set_resp.erl
@@ -0,0 +1,33 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(http_handler_set_resp).
+-behaviour(cowboy_http_handler).
+-export([init/3, handle/2, terminate/2]).
+
+init({_Transport, http}, Req, Opts) ->
+ Headers = proplists:get_value(headers, Opts, []),
+ Body = proplists:get_value(body, Opts, <<"http_handler_set_resp">>),
+ {ok, Req2} = lists:foldl(fun({Name, Value}, {ok, R}) ->
+ cowboy_http_req:set_resp_header(Name, Value, R)
+ end, {ok, Req}, Headers),
+ {ok, Req3} = cowboy_http_req:set_resp_body(Body, Req2),
+ {ok, Req4} = cowboy_http_req:set_resp_header(
+ <<"X-Cowboy-Test">>, <<"ok">>, Req3),
+ {ok, Req5} = cowboy_http_req:set_resp_cookie(
+ <<"cake">>, <<"lie">>, [], Req4),
+ {ok, Req5, undefined}.
+
+handle(Req, State) ->
+ case cowboy_http_req:has_resp_header(<<"X-Cowboy-Test">>, Req) of
+ false -> {ok, Req, State};
+ true ->
+ case cowboy_http_req:has_resp_body(Req) of
+ false -> {ok, Req, State};
+ true ->
+ {ok, Req2} = cowboy_http_req:reply(200, Req),
+ {ok, Req2, State}
+ end
+ end.
+
+terminate(_Req, _State) ->
+ ok.
diff --git a/test/http_handler_stream_body.erl b/test/http_handler_stream_body.erl
new file mode 100644
index 0000000..c90f746
--- /dev/null
+++ b/test/http_handler_stream_body.erl
@@ -0,0 +1,24 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(http_handler_stream_body).
+-behaviour(cowboy_http_handler).
+-export([init/3, handle/2, terminate/2]).
+
+-record(state, {headers, body, reply}).
+
+init({_Transport, http}, Req, Opts) ->
+ Headers = proplists:get_value(headers, Opts, []),
+ Body = proplists:get_value(body, Opts, "http_handler_stream_body"),
+ 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_http_req:transport(Req),
+ SFun = fun() -> Transport:send(Socket, Body), sent end,
+ SLen = iolist_size(Body),
+ {ok, Req2} = cowboy_http_req:set_resp_body_fun(SLen, SFun, Req),
+ {ok, Req3} = cowboy_http_req:reply(200, Req2),
+ {ok, Req3, State}.
+
+terminate(_Req, _State) ->
+ ok.
diff --git a/test/rest_simple_resource.erl b/test/rest_simple_resource.erl
new file mode 100644
index 0000000..e2c573c
--- /dev/null
+++ b/test/rest_simple_resource.erl
@@ -0,0 +1,12 @@
+-module(rest_simple_resource).
+-export([init/3, content_types_provided/2, get_text_plain/2]).
+
+init(_Transport, _Req, _Opts) ->
+ {upgrade, protocol, cowboy_http_rest}.
+
+content_types_provided(Req, State) ->
+ {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}.
+
+get_text_plain(Req, State) ->
+ {<<"This is REST!">>, Req, State}.
+
diff --git a/test/websocket_handler.erl b/test/websocket_handler.erl
index 0cfc8f3..abb4967 100644
--- a/test/websocket_handler.erl
+++ b/test/websocket_handler.erl
@@ -23,6 +23,8 @@ websocket_init(_TransportName, Req, _Opts) ->
websocket_handle({text, Data}, Req, State) ->
{reply, {text, Data}, Req, State};
+websocket_handle({binary, Data}, Req, State) ->
+ {reply, {binary, Data}, Req, State};
websocket_handle(_Frame, Req, State) ->
{ok, Req, State}.
diff --git a/test/websocket_handler_init_shutdown.erl b/test/websocket_handler_init_shutdown.erl
index 2d52cbd..aa9e056 100644
--- a/test/websocket_handler_init_shutdown.erl
+++ b/test/websocket_handler_init_shutdown.erl
@@ -17,7 +17,7 @@ terminate(_Req, _State) ->
exit(badarg).
websocket_init(_TransportName, Req, _Opts) ->
- Req2 = cowboy_http_req:reply(403, Req),
+ {ok, Req2} = cowboy_http_req:reply(403, Req),
{shutdown, Req2}.
websocket_handle(_Frame, _Req, _State) ->
diff --git a/test/ws_SUITE.erl b/test/ws_SUITE.erl
new file mode 100644
index 0000000..136833f
--- /dev/null
+++ b/test/ws_SUITE.erl
@@ -0,0 +1,318 @@
+%% Copyright (c) 2011, 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(ws_SUITE).
+
+-include_lib("common_test/include/ct.hrl").
+
+-export([all/0, groups/0, init_per_suite/1, end_per_suite/1,
+ init_per_group/2, end_per_group/2]). %% ct.
+-export([ws0/1, ws8/1, ws8_single_bytes/1, ws8_init_shutdown/1,
+ ws13/1, ws_timeout_hibernate/1]). %% ws.
+
+%% ct.
+
+all() ->
+ [{group, ws}].
+
+groups() ->
+ BaseTests = [ws0, ws8, ws8_single_bytes, ws8_init_shutdown, ws13,
+ ws_timeout_hibernate],
+ [{ws, [], BaseTests}].
+
+init_per_suite(Config) ->
+ application:start(inets),
+ application:start(cowboy),
+ Config.
+
+end_per_suite(_Config) ->
+ application:stop(cowboy),
+ application:stop(inets),
+ ok.
+
+init_per_group(ws, Config) ->
+ Port = 33080,
+ cowboy:start_listener(ws, 100,
+ cowboy_tcp_transport, [{port, Port}],
+ cowboy_http_protocol, [{dispatch, init_dispatch()}]
+ ),
+ [{port, Port}|Config].
+
+end_per_group(Listener, _Config) ->
+ cowboy:stop_listener(Listener),
+ ok.
+
+%% Dispatch configuration.
+
+init_dispatch() ->
+ [
+ {[<<"localhost">>], [
+ {[<<"websocket">>], websocket_handler, []},
+ {[<<"ws_timeout_hibernate">>], ws_timeout_hibernate_handler, []},
+ {[<<"ws_init_shutdown">>], websocket_handler_init_shutdown, []}
+ ]}
+ ].
+
+%% 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.
+ws0(Config) ->
+ {port, Port} = lists:keyfind(port, 1, Config),
+ {ok, Socket} = gen_tcp:connect("localhost", Port,
+ [binary, {active, false}, {packet, raw}]),
+ ok = gen_tcp:send(Socket,
+ "GET /websocket 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"
+ "\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.
+
+ws8(Config) ->
+ {port, Port} = lists:keyfind(port, 1, Config),
+ {ok, Socket} = gen_tcp:connect("localhost", Port,
+ [binary, {active, false}, {packet, raw}]),
+ ok = gen_tcp:send(Socket, [
+ "GET /websocket HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Connection: Upgrade\r\n"
+ "Upgrade: websocket\r\n"
+ "Sec-WebSocket-Origin: http://localhost\r\n"
+ "Sec-WebSocket-Version: 8\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ "\r\n"]),
+ {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
+ {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-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
+ = lists:keyfind("sec-websocket-accept", 1, Headers),
+ 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, << 1:1, 0:3, 1:4, 0:1, 14:7, "websocket_init" >>}
+ = 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, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
+ = 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, << 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, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
+ {error, closed} = gen_tcp:recv(Socket, 0, 6000),
+ ok.
+
+ws8_single_bytes(Config) ->
+ {port, Port} = lists:keyfind(port, 1, Config),
+ {ok, Socket} = gen_tcp:connect("localhost", Port,
+ [binary, {active, false}, {packet, raw}]),
+ ok = gen_tcp:send(Socket, [
+ "GET /websocket HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Connection: Upgrade\r\n"
+ "Upgrade: websocket\r\n"
+ "Sec-WebSocket-Origin: http://localhost\r\n"
+ "Sec-WebSocket-Version: 8\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ "\r\n"]),
+ {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
+ {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-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
+ = lists:keyfind("sec-websocket-accept", 1, Headers),
+ 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 = gen_tcp:send(Socket, << 16#37 >>),
+ ok = timer:sleep(100),
+ ok = gen_tcp:send(Socket, << 16#fa >>),
+ ok = timer:sleep(100),
+ ok = gen_tcp:send(Socket, << 16#21 >>),
+ ok = timer:sleep(100),
+ ok = gen_tcp:send(Socket, << 16#3d >>),
+ ok = timer:sleep(100),
+ ok = gen_tcp:send(Socket, << 16#7f >>),
+ ok = timer:sleep(100),
+ ok = gen_tcp:send(Socket, << 16#9f >>),
+ ok = timer:sleep(100),
+ ok = gen_tcp:send(Socket, << 16#4d >>),
+ ok = timer:sleep(100),
+ ok = gen_tcp:send(Socket, << 16#51 >>),
+ ok = timer:sleep(100),
+ ok = gen_tcp:send(Socket, << 16#58 >>),
+ {ok, << 1:1, 0:3, 1:4, 0:1, 14:7, "websocket_init" >>}
+ = gen_tcp:recv(Socket, 0, 6000),
+ {ok, << 1:1, 0:3, 1:4, 0:1, 5:7, "Hello" >>}
+ = 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, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
+ = 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, << 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, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
+ {error, closed} = gen_tcp:recv(Socket, 0, 6000),
+ ok.
+
+ws_timeout_hibernate(Config) ->
+ {port, Port} = lists:keyfind(port, 1, Config),
+ {ok, Socket} = gen_tcp:connect("localhost", Port,
+ [binary, {active, false}, {packet, raw}]),
+ ok = gen_tcp:send(Socket, [
+ "GET /ws_timeout_hibernate HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Connection: Upgrade\r\n"
+ "Upgrade: websocket\r\n"
+ "Sec-WebSocket-Origin: http://localhost\r\n"
+ "Sec-WebSocket-Version: 8\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ "\r\n"]),
+ {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
+ {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-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),
+ {error, closed} = gen_tcp:recv(Socket, 0, 6000),
+ ok.
+
+ws8_init_shutdown(Config) ->
+ {port, Port} = lists:keyfind(port, 1, Config),
+ {ok, Socket} = gen_tcp:connect("localhost", Port,
+ [binary, {active, false}, {packet, raw}]),
+ ok = gen_tcp:send(Socket, [
+ "GET /ws_init_shutdown HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Connection: Upgrade\r\n"
+ "Upgrade: websocket\r\n"
+ "Sec-WebSocket-Origin: http://localhost\r\n"
+ "Sec-WebSocket-Version: 8\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ "\r\n"]),
+ {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
+ {ok, {http_response, {1, 1}, 403, "Forbidden"}, _Rest}
+ = erlang:decode_packet(http, Handshake, []),
+ {error, closed} = gen_tcp:recv(Socket, 0, 6000),
+ ok.
+
+ws13(Config) ->
+ {port, Port} = lists:keyfind(port, 1, Config),
+ {ok, Socket} = gen_tcp:connect("localhost", Port,
+ [binary, {active, false}, {packet, raw}]),
+ ok = gen_tcp:send(Socket, [
+ "GET /websocket HTTP/1.1\r\n"
+ "Host: localhost\r\n"
+ "Connection: Upgrade\r\n"
+ "Origin: http://localhost\r\n"
+ "Sec-WebSocket-Version: 13\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+ "Upgrade: websocket\r\n"
+ "\r\n"]),
+ {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
+ {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-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
+ = lists:keyfind("sec-websocket-accept", 1, Headers),
+ %% text
+ 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),
+ %% binary (empty)
+ ok = gen_tcp:send(Socket, << 1:1, 0:3, 2:4, 0:8 >>),
+ {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,
+ 16#7f, 16#9f, 16#4d, 16#51, 16#58 >>),
+ {ok, << 1:1, 0:3, 2:4, 0:1, 5:7, "Hello" >>}
+ = gen_tcp:recv(Socket, 0, 6000),
+ %% Receives.
+ {ok, << 1:1, 0:3, 1:4, 0:1, 14:7, "websocket_init" >>}
+ = 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, << 1:1, 0:3, 1:4, 0:1, 16:7, "websocket_handle" >>}
+ = 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, << 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, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
+ {error, closed} = gen_tcp:recv(Socket, 0, 6000),
+ ok.
+
+websocket_headers({ok, http_eoh, Rest}, Acc) ->
+ [Acc, Rest];
+websocket_headers({ok, {http_header, _I, Key, _R, Value}, Rest}, Acc) ->
+ F = fun(S) when is_atom(S) -> S; (S) -> string:to_lower(S) end,
+ websocket_headers(erlang:decode_packet(httph, Rest, []),
+ [{F(Key), Value}|Acc]).