diff options
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" @@ -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. @@ -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: @@ -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]). |