diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | manual/cowboy_req.md | 9 | ||||
-rw-r--r-- | manual/cowboy_rest.md | 16 | ||||
-rw-r--r-- | src/cowboy.erl | 18 | ||||
-rw-r--r-- | src/cowboy_http.erl | 20 | ||||
-rw-r--r-- | src/cowboy_protocol.erl | 1 | ||||
-rw-r--r-- | src/cowboy_req.erl | 121 | ||||
-rw-r--r-- | src/cowboy_rest.erl | 6 | ||||
-rw-r--r-- | src/cowboy_spdy.erl | 587 | ||||
-rw-r--r-- | src/cowboy_spdy.hrl | 181 | ||||
-rw-r--r-- | src/cowboy_static.erl | 4 | ||||
-rw-r--r-- | test/http_SUITE.erl | 97 | ||||
-rw-r--r-- | test/http_SUITE_data/rest_post_charset_resource.erl | 15 | ||||
-rw-r--r-- | test/spdy_SUITE.erl | 171 |
14 files changed, 1139 insertions, 109 deletions
@@ -5,7 +5,7 @@ PROJECT = cowboy # Options. COMPILE_FIRST = cowboy_middleware cowboy_sub_protocol -CT_SUITES = eunit http ws +CT_SUITES = eunit http spdy ws PLT_APPS = crypto public_key ssl # Dependencies. diff --git a/manual/cowboy_req.md b/manual/cowboy_req.md index f10120a..8a765dc 100644 --- a/manual/cowboy_req.md +++ b/manual/cowboy_req.md @@ -176,7 +176,7 @@ Request related exports > | accept-language | `[{LanguageTag, Quality}]` | > | authorization | `{AuthType, Credentials}` | > | content-length | `non_neg_integer()` | -> | content-type | `{Type, SubType, Params}` | +> | content-type | `{Type, SubType, ContentTypeParams}` | > | cookie | `[{binary(), binary()}]` | > | expect | `[Expect | {Expect, ExpectValue, Params}]` | > | if-match | `'*' | [{weak | strong, OpaqueTag}]` | @@ -192,7 +192,7 @@ Request related exports > Types for the above table: > * Type = SubType = Charset = Encoding = LanguageTag = binary() > * AuthType = Expect = OpaqueTag = Unit = binary() -> * Params = [{binary(), binary()}] +> * Params = ContentTypeParams = [{binary(), binary()}] > * Quality = 0..1000 > * AcceptExt = [{binary(), binary()} | binary()] > * Credentials - see below @@ -201,8 +201,9 @@ Request related exports > The cookie names and values, the values of the sec-websocket-protocol > and x-forwarded-for headers, the values in `AcceptExt` and `Params`, > the authorization `Credentials`, the `ExpectValue` and `OpaqueTag` -> are case sensitive. All other values are case insensitive and -> will be returned as lowercase. +> are case sensitive. All values in `ContentTypeParams` are case sensitive +> except the value of the charset parameter, which is case insensitive. +> All other values are case insensitive and will be returned as lowercase. > > The headers accept, accept-encoding and cookie headers can return > an empty list. Others will return `{error, badarg}` if the header diff --git a/manual/cowboy_rest.md b/manual/cowboy_rest.md index b7890e4..110e224 100644 --- a/manual/cowboy_rest.md +++ b/manual/cowboy_rest.md @@ -168,7 +168,9 @@ REST callbacks description > Cowboy will select the most appropriate content-type from the list. > If any parameter is acceptable, then the tuple form should be used > with parameters set to `'*'`. If the parameters value is set to `[]` -> only content-type values with no parameters will be accepted. +> only content-type values with no parameters will be accepted. All +> parameter values are treated in a case sensitive manner except the +> `charset` parameter, if present, which is case insensitive. > > This function will be called for POST, PUT and PATCH requests. > It is entirely possible to define different callbacks for different @@ -207,7 +209,7 @@ REST callbacks description > * Value = [{binary() | {Type, SubType, Params}, ProvideResource}] > * Type = SubType = binary() > * Params = '*' | [{binary(), binary()}] -> * AcceptResource = atom() +> * ProvideResource = atom() > > Return the list of content-types the resource provides. > @@ -219,7 +221,9 @@ REST callbacks description > Cowboy will select the most appropriate content-type from the list. > If any parameter is acceptable, then the tuple form should be used > with parameters set to `'*'`. If the parameters value is set to `[]` -> only content-type values with no parameters will be accepted. +> only content-type values with no parameters will be accepted. All +> parameter values are treated in a case sensitive manner except the +> `charset` parameter, if present, which is case insensitive. > > The `ProvideResource` value is the name of the callback that will > be called if the content-type matches. It is defined as follow. @@ -291,12 +295,16 @@ REST callbacks description ### generate_etag > * Methods: GET, HEAD, POST, PUT, PATCH, DELETE -> * Value type: binary() +> * Value type: binary() | {weak | strong, binary()} > * Default value: undefined > > Return the entity tag of the resource. > > This value will be sent as the value of the etag header. +> +> If a binary is returned, then the value will be parsed +> to the tuple form automatically. The value must be in +> the same format as the etag header, including quotes. ### is_authorized diff --git a/src/cowboy.erl b/src/cowboy.erl index f343f41..abc7911 100644 --- a/src/cowboy.erl +++ b/src/cowboy.erl @@ -17,6 +17,7 @@ -export([start_http/4]). -export([start_https/4]). +-export([start_spdy/4]). -export([stop_listener/1]). -export([set_env/3]). @@ -38,7 +39,7 @@ %% @doc Start an HTTP listener. -spec start_http(ranch:ref(), non_neg_integer(), ranch_tcp:opts(), - cowboy_protocol:opts()) -> {ok, pid()}. + cowboy_protocol:opts()) -> {ok, pid()} | {error, any()}. start_http(Ref, NbAcceptors, TransOpts, ProtoOpts) when is_integer(NbAcceptors), NbAcceptors > 0 -> ranch:start_listener(Ref, NbAcceptors, @@ -46,12 +47,25 @@ start_http(Ref, NbAcceptors, TransOpts, ProtoOpts) %% @doc Start an HTTPS listener. -spec start_https(ranch:ref(), non_neg_integer(), ranch_ssl:opts(), - cowboy_protocol:opts()) -> {ok, pid()}. + cowboy_protocol:opts()) -> {ok, pid()} | {error, any()}. start_https(Ref, NbAcceptors, TransOpts, ProtoOpts) when is_integer(NbAcceptors), NbAcceptors > 0 -> ranch:start_listener(Ref, NbAcceptors, ranch_ssl, TransOpts, cowboy_protocol, ProtoOpts). +%% @doc Start a SPDY listener. +-spec start_spdy(ranch:ref(), non_neg_integer(), ranch_ssl:opts(), + cowboy_spdy:opts()) -> {ok, pid()} | {error, any()}. +start_spdy(Ref, NbAcceptors, TransOpts, ProtoOpts) + when is_integer(NbAcceptors), NbAcceptors > 0 -> + TransOpts2 = [ + {connection_type, supervisor}, + {next_protocols_advertised, + [<<"spdy/3">>, <<"http/1.1">>, <<"http/1.0">>]} + |TransOpts], + ranch:start_listener(Ref, NbAcceptors, + ranch_ssl, TransOpts2, cowboy_spdy, ProtoOpts). + %% @doc Stop a listener. -spec stop_listener(ranch:ref()) -> ok. stop_listener(Ref) -> diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index af60dd9..d2bdf3b 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -162,14 +162,26 @@ cookie_value(<< C, Rest/binary >>, Fun, Acc) -> cookie_value(Rest, Fun, << Acc/binary, C >>). %% @doc Parse a content type. +%% +%% We lowercase the charset header as we know it's case insensitive. -spec content_type(binary()) -> any(). content_type(Data) -> media_type(Data, fun (Rest, Type, SubType) -> - params(Rest, - fun (<<>>, Params) -> {Type, SubType, Params}; - (_Rest2, _) -> {error, badarg} - end) + params(Rest, + fun (<<>>, Params) -> + case lists:keyfind(<<"charset">>, 1, Params) of + false -> + {Type, SubType, Params}; + {_, Charset} -> + Charset2 = cowboy_bstr:to_lower(Charset), + Params2 = lists:keyreplace(<<"charset">>, + 1, Params, {<<"charset">>, Charset2}), + {Type, SubType, Params2} + end; + (_Rest2, _) -> + {error, badarg} + end) end). %% @doc Parse a media range. diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl index 06c5446..b42f524 100644 --- a/src/cowboy_protocol.erl +++ b/src/cowboy_protocol.erl @@ -54,6 +54,7 @@ %% Internal. -export([init/4]). -export([parse_request/3]). +-export([parse_host/2]). -export([resume/6]). -type opts() :: [{compress, boolean()} diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index d0f2a35..0e1c8a7 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -130,13 +130,13 @@ | {done, binary(), non_neg_integer(), binary()} | {error, atom()}). --type resp_body_fun() :: fun((inet:socket(), module()) -> ok). +-type resp_body_fun() :: fun((any(), module()) -> ok). -type send_chunk_fun() :: fun((iodata()) -> ok | {error, atom()}). -type resp_chunked_fun() :: fun((send_chunk_fun()) -> ok). -record(http_req, { %% Transport. - socket = undefined :: undefined | inet:socket(), + socket = undefined :: any(), transport = undefined :: undefined | module(), connection = keepalive :: keepalive | close, @@ -189,7 +189,7 @@ %% %% Since we always need to parse the Connection header, we do it %% in an optimized way and add the parsed value to p_headers' cache. --spec new(inet:socket(), module(), +-spec new(any(), module(), undefined | {inet:ip_address(), inet:port_number()}, binary(), binary(), binary(), cowboy:http_version(), cowboy:http_headers(), binary(), @@ -492,7 +492,10 @@ cookie(Name, Req=#http_req{cookies=undefined}, Default) when is_binary(Name) -> {ok, undefined, Req2} -> {Default, Req2#http_req{cookies=[]}}; {ok, Cookies, Req2} -> - cookie(Name, Req2#http_req{cookies=Cookies}, Default) + cookie(Name, Req2#http_req{cookies=Cookies}, Default); + %% Flash player incorrectly sends an empty Cookie header. + {error, badarg} -> + {Default, Req#http_req{cookies=[]}} end; cookie(Name, Req, Default) -> case lists:keyfind(Name, 1, Req#http_req.cookies) of @@ -507,7 +510,10 @@ cookies(Req=#http_req{cookies=undefined}) -> {ok, undefined, Req2} -> {[], Req2#http_req{cookies=[]}}; {ok, Cookies, Req2} -> - cookies(Req2#http_req{cookies=Cookies}) + cookies(Req2#http_req{cookies=Cookies}); + %% Flash player incorrectly sends an empty Cookie header. + {error, badarg} -> + {[], Req#http_req{cookies=[]}} end; cookies(Req=#http_req{cookies=Cookies}) -> {Cookies, Req}. @@ -917,7 +923,7 @@ has_resp_body(#http_req{resp_body={Length, _}}) -> has_resp_body(#http_req{resp_body=RespBody}) -> iolist_size(RespBody) > 0. -%% Remove a header previously set for the response. +%% @doc Remove a header previously set for the response. -spec delete_resp_header(binary(), Req) -> Req when Req::req(). delete_resp_header(Name, Req=#http_req{resp_headers=RespHeaders}) -> @@ -944,20 +950,30 @@ reply(Status, Headers, Body, Req=#http_req{ version=Version, connection=Connection, method=Method, resp_compress=Compress, resp_state=waiting, resp_headers=RespHeaders}) -> - HTTP11Headers = case Version of - 'HTTP/1.1' -> [{<<"connection">>, atom_to_connection(Connection)}]; - _ -> [] + HTTP11Headers = if + Transport =/= cowboy_spdy, Version =:= 'HTTP/1.1' -> + [{<<"connection">>, atom_to_connection(Connection)}]; + true -> + [] end, Req3 = case Body of BodyFun when is_function(BodyFun) -> %% We stream the response body until we close the connection. RespConn = close, - {RespType, Req2} = response(Status, Headers, RespHeaders, [ - {<<"connection">>, <<"close">>}, - {<<"date">>, cowboy_clock:rfc1123()}, - {<<"server">>, <<"Cowboy">>}, - {<<"transfer-encoding">>, <<"identity">>} - ], <<>>, Req), + {RespType, Req2} = if + Transport =:= cowboy_spdy -> + response(Status, Headers, RespHeaders, [ + {<<"date">>, cowboy_clock:rfc1123()}, + {<<"server">>, <<"Cowboy">>} + ], stream, Req); + true -> + response(Status, Headers, RespHeaders, [ + {<<"connection">>, <<"close">>}, + {<<"date">>, cowboy_clock:rfc1123()}, + {<<"server">>, <<"Cowboy">>}, + {<<"transfer-encoding">>, <<"identity">>} + ], <<>>, Req) + end, if RespType =/= hook, Method =/= <<"HEAD">> -> BodyFun(Socket, Transport); true -> ok @@ -970,13 +986,12 @@ reply(Status, Headers, Body, Req=#http_req{ ChunkFun = fun(IoData) -> chunk(IoData, Req2) end, BodyFun(ChunkFun), %% Terminate the chunked body for HTTP/1.1 only. - _ = case Version of - 'HTTP/1.0' -> ok; - _ -> Transport:send(Socket, <<"0\r\n\r\n">>) + case Version of + 'HTTP/1.0' -> Req2; + _ -> last_chunk(Req2) end; - true -> ok - end, - Req2; + true -> Req2 + end; {ContentLength, BodyFun} -> %% We stream the response body for ContentLength bytes. RespConn = response_connection(Headers, Connection), @@ -984,7 +999,7 @@ reply(Status, Headers, Body, Req=#http_req{ {<<"content-length">>, integer_to_list(ContentLength)}, {<<"date">>, cowboy_clock:rfc1123()}, {<<"server">>, <<"Cowboy">>} - |HTTP11Headers], <<>>, Req), + |HTTP11Headers], stream, Req), if RespType =/= hook, Method =/= <<"HEAD">> -> BodyFun(Socket, Transport); true -> ok @@ -1001,7 +1016,7 @@ reply(Status, Headers, Body, Req=#http_req{ RespHeaders, HTTP11Headers, Method, iolist_size(Body)), Req2#http_req{connection=RespConn} end, - {ok, Req3#http_req{resp_state=done,resp_headers=[], resp_body= <<>>}}. + {ok, Req3#http_req{resp_state=done, resp_headers=[], resp_body= <<>>}}. reply_may_compress(Status, Headers, Body, Req, RespHeaders, HTTP11Headers, Method) -> @@ -1065,18 +1080,34 @@ chunked_reply(Status, Headers, Req) -> -spec chunk(iodata(), req()) -> ok | {error, atom()}. chunk(_Data, #http_req{method= <<"HEAD">>}) -> ok; -chunk(Data, #http_req{socket=Socket, transport=Transport, version='HTTP/1.0'}) -> +chunk(Data, #http_req{socket=Socket, transport=cowboy_spdy, + resp_state=chunks}) -> + cowboy_spdy:stream_data(Socket, Data); +chunk(Data, #http_req{socket=Socket, transport=Transport, + resp_state=chunks, version='HTTP/1.0'}) -> Transport:send(Socket, Data); -chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) -> +chunk(Data, #http_req{socket=Socket, transport=Transport, + resp_state=chunks}) -> Transport:send(Socket, [integer_to_list(iolist_size(Data), 16), <<"\r\n">>, Data, <<"\r\n">>]). +%% @doc Finish the chunked reply. +%% @todo If ever made public, need to send nothing if HEAD. +-spec last_chunk(Req) -> Req when Req::req(). +last_chunk(Req=#http_req{socket=Socket, transport=cowboy_spdy}) -> + _ = cowboy_spdy:stream_close(Socket), + Req#http_req{resp_state=done}; +last_chunk(Req=#http_req{socket=Socket, transport=Transport}) -> + _ = Transport:send(Socket, <<"0\r\n\r\n">>), + Req#http_req{resp_state=done}. + %% @doc Send an upgrade reply. %% @private -spec upgrade_reply(cowboy:http_status(), cowboy:http_headers(), Req) -> {ok, Req} when Req::req(). -upgrade_reply(Status, Headers, Req=#http_req{ - resp_state=waiting, resp_headers=RespHeaders}) -> +upgrade_reply(Status, Headers, Req=#http_req{transport=Transport, + resp_state=waiting, resp_headers=RespHeaders}) + when Transport =/= cowboy_spdy -> {_, Req2} = response(Status, Headers, RespHeaders, [ {<<"connection">>, <<"Upgrade">>} ], <<>>, Req), @@ -1098,9 +1129,8 @@ ensure_response(#http_req{method= <<"HEAD">>, resp_state=chunks}, _) -> ok; ensure_response(#http_req{version='HTTP/1.0', resp_state=chunks}, _) -> ok; -ensure_response(#http_req{socket=Socket, transport=Transport, - resp_state=chunks}, _) -> - Transport:send(Socket, <<"0\r\n\r\n">>), +ensure_response(Req=#http_req{resp_state=chunks}, _) -> + _ = last_chunk(Req), ok. %% Private setter/getter API. @@ -1213,6 +1243,15 @@ to_list(Req) -> -spec chunked_response(cowboy:http_status(), cowboy:http_headers(), Req) -> {normal | hook, Req} when Req::req(). chunked_response(Status, Headers, Req=#http_req{ + transport=cowboy_spdy, resp_state=waiting, + resp_headers=RespHeaders}) -> + {RespType, Req2} = response(Status, Headers, RespHeaders, [ + {<<"date">>, cowboy_clock:rfc1123()}, + {<<"server">>, <<"Cowboy">>} + ], stream, Req), + {RespType, Req2#http_req{resp_state=chunks, + resp_headers=[], resp_body= <<>>}}; +chunked_response(Status, Headers, Req=#http_req{ version=Version, connection=Connection, resp_state=waiting, resp_headers=RespHeaders}) -> RespConn = response_connection(Headers, Connection), @@ -1230,7 +1269,7 @@ chunked_response(Status, Headers, Req=#http_req{ resp_headers=[], resp_body= <<>>}}. -spec response(cowboy:http_status(), cowboy:http_headers(), - cowboy:http_headers(), cowboy:http_headers(), iodata(), Req) + cowboy:http_headers(), cowboy:http_headers(), stream | iodata(), Req) -> {normal | hook, Req} when Req::req(). response(Status, Headers, RespHeaders, DefaultHeaders, Body, Req=#http_req{ socket=Socket, transport=Transport, version=Version, @@ -1239,22 +1278,32 @@ response(Status, Headers, RespHeaders, DefaultHeaders, Body, Req=#http_req{ already_called -> Headers; _ -> response_merge_headers(Headers, RespHeaders, DefaultHeaders) end, + Body2 = case Body of stream -> <<>>; _ -> Body end, Req2 = case OnResponse of already_called -> Req; undefined -> Req; - OnResponse -> OnResponse(Status, FullHeaders, Body, - %% Don't call 'onresponse' from the hook itself. - Req#http_req{resp_headers=[], resp_body= <<>>, - onresponse=already_called}) + OnResponse -> + OnResponse(Status, FullHeaders, Body2, + %% Don't call 'onresponse' from the hook itself. + Req#http_req{resp_headers=[], resp_body= <<>>, + onresponse=already_called}) end, ReplyType = case Req2#http_req.resp_state of + waiting when Transport =:= cowboy_spdy, Body =:= stream -> + cowboy_spdy:stream_reply(Socket, status(Status), FullHeaders), + ReqPid ! {?MODULE, resp_sent}, + normal; + waiting when Transport =:= cowboy_spdy -> + cowboy_spdy:reply(Socket, status(Status), FullHeaders, Body), + ReqPid ! {?MODULE, resp_sent}, + normal; waiting -> HTTPVer = atom_to_binary(Version, latin1), StatusLine = << HTTPVer/binary, " ", (status(Status))/binary, "\r\n" >>, HeaderLines = [[Key, <<": ">>, Value, <<"\r\n">>] || {Key, Value} <- FullHeaders], - Transport:send(Socket, [StatusLine, HeaderLines, <<"\r\n">>, Body]), + Transport:send(Socket, [StatusLine, HeaderLines, <<"\r\n">>, Body2]), ReqPid ! {?MODULE, resp_sent}, normal; _ -> diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index 5f96b53..34bfce1 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -145,6 +145,9 @@ allowed_methods(Req, State=#state{method=Method}) -> end end. +method_not_allowed(Req, State, []) -> + Req2 = cowboy_req:set_resp_header(<<"allow">>, <<>>, Req), + respond(Req2, State, 405); method_not_allowed(Req, State, Methods) -> << ", ", Allow/binary >> = << << ", ", M/binary >> || M <- Methods >>, Req2 = cowboy_req:set_resp_header(<<"allow">>, Allow, Req), @@ -186,6 +189,9 @@ valid_entity_length(Req, State) -> %% you should do it directly in the options/2 call using set_resp_headers. options(Req, State=#state{allowed_methods=Methods, method= <<"OPTIONS">>}) -> case call(Req, State, options) of + no_call when Methods =:= [] -> + Req2 = cowboy_req:set_resp_header(<<"allow">>, <<>>, Req), + respond(Req2, State, 200); no_call -> << ", ", Allow/binary >> = << << ", ", M/binary >> || M <- Methods >>, diff --git a/src/cowboy_spdy.erl b/src/cowboy_spdy.erl new file mode 100644 index 0000000..182e6da --- /dev/null +++ b/src/cowboy_spdy.erl @@ -0,0 +1,587 @@ +%% Copyright (c) 2013, Loïc Hoguin <[email protected]> +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% @doc SPDY protocol handler. +%% +%% The available options are: +%% <dl> +%% </dl> +%% +%% Note that there is no need to monitor these processes when using Cowboy as +%% an application as it already supervises them under the listener supervisor. +-module(cowboy_spdy). + +%% API. +-export([start_link/4]). + +%% Internal. +-export([init/5]). +-export([system_continue/3]). +-export([system_terminate/4]). +-export([system_code_change/4]). + +%% Internal request process. +-export([request_init/9]). +-export([resume/5]). +-export([reply/4]). +-export([stream_reply/3]). +-export([stream_data/2]). +-export([stream_close/1]). + +%% Internal transport functions. +-export([name/0]). +-export([send/2]). +-export([sendfile/2]). + +-record(child, { + streamid :: non_neg_integer(), + pid :: pid(), + input = nofin :: fin | nofin, + output = nofin :: fin | nofin +}). + +-record(state, { + parent = undefined :: pid(), + socket, + transport, + buffer = <<>> :: binary(), + middlewares, + env, + onrequest, + onresponse, + peer, + zdef, + zinf, + last_streamid = 0 :: non_neg_integer(), + children = [] :: [#child{}] +}). + +-record(special_headers, { + method, + path, + version, + host, + scheme %% @todo We don't use it. +}). + +-type opts() :: []. +-export_type([opts/0]). + +-include("cowboy_spdy.hrl"). + +%% API. + +%% @doc Start a SPDY protocol process. +-spec start_link(any(), inet:socket(), module(), any()) -> {ok, pid()}. +start_link(Ref, Socket, Transport, Opts) -> + proc_lib:start_link(?MODULE, init, + [self(), Ref, Socket, Transport, Opts]). + +%% Internal. + +%% @doc Faster alternative to proplists:get_value/3. +%% @private +get_value(Key, Opts, Default) -> + case lists:keyfind(Key, 1, Opts) of + {_, Value} -> Value; + _ -> Default + end. + +%% @private +-spec init(pid(), ranch:ref(), inet:socket(), module(), opts()) -> ok. +init(Parent, Ref, Socket, Transport, Opts) -> + process_flag(trap_exit, true), + ok = proc_lib:init_ack(Parent, {ok, self()}), + {ok, Peer} = Transport:peername(Socket), + Middlewares = get_value(middlewares, Opts, [cowboy_router, cowboy_handler]), + Env = [{listener, Ref}|get_value(env, Opts, [])], + OnRequest = get_value(onrequest, Opts, undefined), + OnResponse = get_value(onresponse, Opts, undefined), + Zdef = zlib:open(), + ok = zlib:deflateInit(Zdef), + _ = zlib:deflateSetDictionary(Zdef, ?ZDICT), + Zinf = zlib:open(), + ok = zlib:inflateInit(Zinf), + ok = ranch:accept_ack(Ref), + loop(#state{parent=Parent, socket=Socket, transport=Transport, + middlewares=Middlewares, env=Env, onrequest=OnRequest, + onresponse=OnResponse, peer=Peer, zdef=Zdef, zinf=Zinf}). + +loop(State=#state{parent=Parent, socket=Socket, transport=Transport, + buffer=Buffer, children=Children}) -> + {OK, Closed, Error} = Transport:messages(), + Transport:setopts(Socket, [{active, once}]), + receive + {OK, Socket, Data} -> + Data2 = << Buffer/binary, Data/binary >>, + case Data2 of + << _:40, Length:24, _/bits >> + when byte_size(Data2) >= Length + 8 -> + Length2 = Length + 8, + << Frame:Length2/binary, Rest/bits >> = Data2, + control_frame(State#state{buffer=Rest}, Frame); + Rest -> + loop(State#state{buffer=Rest}) + end; + {Closed, Socket} -> + terminate(State); + {Error, Socket, _Reason} -> + terminate(State); + {reply, {Pid, StreamID}, Status, Headers} + when Pid =:= self() -> + Child = #child{output=nofin} = lists:keyfind(StreamID, + #child.streamid, Children), + syn_reply(State, fin, StreamID, Status, Headers), + Children2 = lists:keyreplace(StreamID, + #child.streamid, Children, Child#child{output=fin}), + loop(State#state{children=Children2}); + {reply, {Pid, StreamID}, Status, Headers, Body} + when Pid =:= self() -> + Child = #child{output=nofin} = lists:keyfind(StreamID, + #child.streamid, Children), + syn_reply(State, nofin, StreamID, Status, Headers), + data(State, fin, StreamID, Body), + Children2 = lists:keyreplace(StreamID, + #child.streamid, Children, Child#child{output=fin}), + loop(State#state{children=Children2}); + {stream_reply, {Pid, StreamID}, Status, Headers} + when Pid =:= self() -> + #child{output=nofin} = lists:keyfind(StreamID, + #child.streamid, Children), + syn_reply(State, nofin, StreamID, Status, Headers), + loop(State); + {stream_data, {Pid, StreamID}, Data} + when Pid =:= self() -> + #child{output=nofin} = lists:keyfind(StreamID, + #child.streamid, Children), + data(State, nofin, StreamID, Data), + loop(State); + {stream_close, {Pid, StreamID}} + when Pid =:= self() -> + Child = #child{output=nofin} = lists:keyfind(StreamID, + #child.streamid, Children), + data(State, fin, StreamID), + Children2 = lists:keyreplace(StreamID, + #child.streamid, Children, Child#child{output=fin}), + loop(State#state{children=Children2}); + {sendfile, {Pid, StreamID}, Filepath} + when Pid =:= self() -> + Child = #child{output=nofin} = lists:keyfind(StreamID, + #child.streamid, Children), + data_from_file(State, StreamID, Filepath), + Children2 = lists:keyreplace(StreamID, + #child.streamid, Children, Child#child{output=fin}), + loop(State#state{children=Children2}); + {'EXIT', Parent, Reason} -> + exit(Reason); + {'EXIT', Pid, _} -> + Children2 = lists:keydelete(Pid, #child.pid, Children), + loop(State#state{children=Children2}); + {system, From, Request} -> + sys:handle_system_msg(Request, From, Parent, ?MODULE, [], State); + %% Calls from the supervisor module. + {'$gen_call', {To, Tag}, which_children} -> + Children = [{?MODULE, Pid, worker, [?MODULE]} + || #child{pid=Pid} <- Children], + To ! {Tag, Children}, + loop(State); + {'$gen_call', {To, Tag}, count_children} -> + NbChildren = length(Children), + Counts = [{specs, 1}, {active, NbChildren}, + {supervisors, 0}, {workers, NbChildren}], + To ! {Tag, Counts}, + loop(State); + {'$gen_call', {To, Tag}, _} -> + To ! {Tag, {error, ?MODULE}}, + loop(State) + after 60000 -> + goaway(State, ok), + terminate(State) + end. + +system_continue(_, _, State) -> + loop(State). + +-spec system_terminate(any(), _, _, _) -> no_return(). +system_terminate(Reason, _, _, _) -> + exit(Reason). + +system_code_change(Misc, _, _, _) -> + {ok, Misc}. + +%% We do not support SYN_STREAM with FLAG_UNIDIRECTIONAL set. +control_frame(State, << 1:1, 3:15, 1:16, _:6, 1:1, _:26, + StreamID:31, _/bits >>) -> + rst_stream(State, StreamID, internal_error), + loop(State); +%% We do not support Associated-To-Stream-ID and CREDENTIAL Slot. +control_frame(State, << 1:1, 3:15, 1:16, _:33, StreamID:31, _:1, + AssocToStreamID:31, _:8, Slot:8, _/bits >>) + when AssocToStreamID =/= 0; Slot =/= 0 -> + rst_stream(State, StreamID, internal_error), + loop(State); +%% SYN_STREAM +%% +%% Erlang does not allow us to control the priority of processes +%% so we ignore that value entirely. +control_frame(State=#state{middlewares=Middlewares, env=Env, + onrequest=OnRequest, onresponse=OnResponse, peer=Peer, + zinf=Zinf, children=Children}, + << 1:1, 3:15, 1:16, Flags:8, _:25, StreamID:31, + _:32, _Priority:3, _:13, Rest/bits >>) -> + IsFin = case Flags of + 1 -> fin; + 0 -> nofin + end, + [<< NbHeaders:32, Rest2/bits >>] = try + zlib:inflate(Zinf, Rest) + catch _:_ -> + ok = zlib:inflateSetDictionary(Zinf, ?ZDICT), + zlib:inflate(Zinf, <<>>) + end, + case syn_stream_headers(Rest2, NbHeaders, [], #special_headers{}) of + {ok, Headers, Special} -> + Pid = spawn_link(?MODULE, request_init, + [self(), StreamID, Peer, Headers, + OnRequest, OnResponse, Env, Middlewares, Special]), + loop(State#state{last_streamid=StreamID, + children=[#child{streamid=StreamID, pid=Pid, + input=IsFin, output=nofin}|Children]}); + {error, badname} -> + rst_stream(State, StreamID, protocol_error), + loop(State#state{last_streamid=StreamID}); + {error, special} -> + rst_stream(State, StreamID, protocol_error), + loop(State#state{last_streamid=StreamID}) + end; +%% SYN_REPLY +control_frame(State, << 1:1, 3:15, 2:16, _/bits >>) -> + error_logger:error_msg("Ignored SYN_REPLY control frame~n"), + loop(State); +%% RST_STREAM +control_frame(State, << 1:1, 3:15, 3:16, _Flags:8, _Length:24, + _:1, _StreamID:31, StatusCode:32 >>) -> + Status = case StatusCode of + 1 -> protocol_error; + 2 -> invalid_stream; + 3 -> refused_stream; + 4 -> unsupported_version; + 5 -> cancel; + 6 -> internal_error; + 7 -> flow_control_error; + 8 -> stream_in_use; + 9 -> stream_already_closed; + 10 -> invalid_credentials; + 11 -> frame_too_large + end, + error_logger:error_msg("Received RST_STREAM control frame: ~p~n", [Status]), + %% @todo Stop StreamID. + loop(State); +%% SETTINGS +control_frame(State, << 1:1, 3:15, 4:16, 0:8, _:24, + NbEntries:32, Rest/bits >>) -> + Settings = [begin + Name = case ID of + 1 -> upload_bandwidth; + 2 -> download_bandwidth; + 3 -> round_trip_time; + 4 -> max_concurrent_streams; + 5 -> current_cwnd; + 6 -> download_retrans_rate; + 7 -> initial_window_size; + 8 -> client_certificate_vector_size + end, + {Flags, Name, Value} + end || << Flags:8, ID:24, Value:32 >> <= Rest], + if + NbEntries =/= length(Settings) -> + goaway(State, protocol_error), + terminate(State); + true -> + error_logger:error_msg("Ignored SETTINGS control frame: ~p~n", + [Settings]), + loop(State) + end; +%% PING initiated by the server; ignore, we don't send any +control_frame(State, << 1:1, 3:15, 6:16, 0:8, 4:24, PingID:32 >>) + when PingID rem 2 =:= 0 -> + error_logger:error_msg("Ignored PING control frame: ~p~n", [PingID]), + loop(State); +%% PING initiated by the client; send it back +control_frame(State=#state{socket=Socket, transport=Transport}, + Data = << 1:1, 3:15, 6:16, 0:8, 4:24, _:32 >>) -> + Transport:send(Socket, Data), + loop(State); +%% GOAWAY +control_frame(State, << 1:1, 3:15, 7:16, _/bits >>) -> + error_logger:error_msg("Ignored GOAWAY control frame~n"), + loop(State); +%% HEADERS +control_frame(State, << 1:1, 3:15, 8:16, _/bits >>) -> + error_logger:error_msg("Ignored HEADERS control frame~n"), + loop(State); +%% WINDOW_UPDATE +control_frame(State, << 1:1, 3:15, 9:16, 0:8, _/bits >>) -> + error_logger:error_msg("Ignored WINDOW_UPDATE control frame~n"), + loop(State); +%% CREDENTIAL +control_frame(State, << 1:1, 3:15, 10:16, _/bits >>) -> + error_logger:error_msg("Ignored CREDENTIAL control frame~n"), + loop(State); +%% ??? +control_frame(State, _) -> + goaway(State, protocol_error), + terminate(State). + +%% @todo We must wait for the children to finish here, +%% but only up to N milliseconds. Then we shutdown. +terminate(_State) -> + ok. + +syn_stream_headers(<<>>, 0, Acc, Special=#special_headers{ + method=Method, path=Path, version=Version, host=Host, scheme=Scheme}) -> + if + Method =:= undefined; Path =:= undefined; Version =:= undefined; + Host =:= undefined; Scheme =:= undefined -> + {error, special}; + true -> + {ok, lists:reverse(Acc), Special} + end; +syn_stream_headers(<< 0:32, _Rest/bits >>, _NbHeaders, _Acc, _Special) -> + {error, badname}; +syn_stream_headers(<< NameLen:32, Rest/bits >>, NbHeaders, Acc, Special) -> + << Name:NameLen/binary, ValueLen:32, Rest2/bits >> = Rest, + << Value:ValueLen/binary, Rest3/bits >> = Rest2, + case Name of + <<":host">> -> + syn_stream_headers(Rest3, NbHeaders - 1, + [{<<"host">>, Value}|Acc], + Special#special_headers{host=Value}); + <<":method">> -> + syn_stream_headers(Rest3, NbHeaders - 1, Acc, + Special#special_headers{method=Value}); + <<":path">> -> + syn_stream_headers(Rest3, NbHeaders - 1, Acc, + Special#special_headers{path=Value}); + <<":version">> -> + syn_stream_headers(Rest3, NbHeaders - 1, Acc, + Special#special_headers{version=Value}); + <<":scheme">> -> + syn_stream_headers(Rest3, NbHeaders - 1, Acc, + Special#special_headers{scheme=Value}); + _ -> + syn_stream_headers(Rest3, NbHeaders - 1, + [{Name, Value}|Acc], Special) + end. + +syn_reply(#state{socket=Socket, transport=Transport, zdef=Zdef}, + IsFin, StreamID, Status, Headers) -> + Headers2 = [{<<":status">>, Status}, + {<<":version">>, <<"HTTP/1.1">>}|Headers], + NbHeaders = length(Headers2), + HeaderBlock = [begin + NameLen = byte_size(Name), + ValueLen = iolist_size(Value), + [<< NameLen:32, Name/binary, ValueLen:32 >>, Value] + end || {Name, Value} <- Headers2], + HeaderBlock2 = [<< NbHeaders:32 >>, HeaderBlock], + HeaderBlock3 = zlib:deflate(Zdef, HeaderBlock2, full), + Flags = case IsFin of + fin -> 1; + nofin -> 0 + end, + Len = 4 + iolist_size(HeaderBlock3), + Transport:send(Socket, [ + << 1:1, 3:15, 2:16, Flags:8, Len:24, 0:1, StreamID:31 >>, + HeaderBlock3]). + +rst_stream(#state{socket=Socket, transport=Transport}, StreamID, Status) -> + StatusCode = case Status of + protocol_error -> 1; +%% invalid_stream -> 2; +%% refused_stream -> 3; +%% unsupported_version -> 4; +%% cancel -> 5; + internal_error -> 6 +%% flow_control_error -> 7; +%% stream_in_use -> 8; +%% stream_already_closed -> 9; +%% invalid_credentials -> 10; +%% frame_too_large -> 11 + end, + Transport:send(Socket, << 1:1, 3:15, 3:16, 0:8, 8:24, + 0:1, StreamID:31, StatusCode:32 >>). + +goaway(#state{socket=Socket, transport=Transport, last_streamid=LastStreamID}, + Status) -> + StatusCode = case Status of + ok -> 0; + protocol_error -> 1 +%% internal_error -> 2 + end, + Transport:send(Socket, << 1:1, 3:15, 7:16, 0:8, 8:24, + 0:1, LastStreamID:31, StatusCode:32 >>). + +data(#state{socket=Socket, transport=Transport}, fin, StreamID) -> + Transport:send(Socket, << 0:1, StreamID:31, 1:8, 0:24 >>). + +data(#state{socket=Socket, transport=Transport}, IsFin, StreamID, Data) -> + Flags = case IsFin of + fin -> 1; + nofin -> 0 + end, + Len = iolist_size(Data), + Transport:send(Socket, [ + << 0:1, StreamID:31, Flags:8, Len:24 >>, + Data]). + +data_from_file(#state{socket=Socket, transport=Transport}, + StreamID, Filepath) -> + {ok, IoDevice} = file:open(Filepath, [read, binary, raw]), + data_from_file(Socket, Transport, StreamID, IoDevice). + +data_from_file(Socket, Transport, StreamID, IoDevice) -> + case file:read(IoDevice, 16#1fff) of + eof -> + _ = Transport:send(Socket, << 0:1, StreamID:31, 1:8, 0:24 >>), + ok; + {ok, Data} -> + Len = byte_size(Data), + Data2 = [<< 0:1, StreamID:31, 0:8, Len:24 >>, Data], + case Transport:send(Socket, Data2) of + ok -> + data_from_file(Socket, Transport, StreamID, IoDevice); + {error, _} -> + ok + end + end. + +%% Request process. + +request_init(Parent, StreamID, Peer, + Headers, OnRequest, OnResponse, Env, Middlewares, + #special_headers{method=Method, path=Path, version=Version, + host=Host}) -> + Version2 = parse_version(Version), + {Host2, Port} = cowboy_protocol:parse_host(Host, <<>>), + {Path2, Query} = parse_path(Path, <<>>), + Req = cowboy_req:new({Parent, StreamID}, ?MODULE, Peer, + Method, Path2, Query, Version2, Headers, + Host2, Port, <<>>, true, false, OnResponse), + case OnRequest of + undefined -> + execute(Req, Env, Middlewares); + _ -> + Req2 = OnRequest(Req), + case cowboy_req:get(resp_state, Req2) of + waiting -> execute(Req2, Env, Middlewares); + _ -> ok + end + end. + +parse_version(<<"HTTP/1.1">>) -> + 'HTTP/1.1'; +parse_version(<<"HTTP/1.0">>) -> + 'HTTP/1.0'. + +parse_path(<<>>, Path) -> + {Path, <<>>}; +parse_path(<< $?, Rest/binary >>, Path) -> + parse_query(Rest, Path, <<>>); +parse_path(<< C, Rest/binary >>, SoFar) -> + parse_path(Rest, << SoFar/binary, C >>). + +parse_query(<<>>, Path, Query) -> + {Path, Query}; +parse_query(<< C, Rest/binary >>, Path, SoFar) -> + parse_query(Rest, Path, << SoFar/binary, C >>). + +-spec execute(cowboy_req:req(), cowboy_middleware:env(), [module()]) + -> ok. +execute(Req, _, []) -> + cowboy_req:ensure_response(Req, 204); +execute(Req, Env, [Middleware|Tail]) -> + case Middleware:execute(Req, Env) of + {ok, Req2, Env2} -> + execute(Req2, Env2, Tail); + {suspend, Module, Function, Args} -> + erlang:hibernate(?MODULE, resume, + [Env, Tail, Module, Function, Args]); + {halt, Req2} -> + cowboy_req:ensure_response(Req2, 204); + {error, Code, Req2} -> + error_terminate(Code, Req2) + end. + +%% @private +-spec resume(cowboy_middleware:env(), [module()], + module(), module(), [any()]) -> ok. +resume(Env, Tail, Module, Function, Args) -> + case apply(Module, Function, Args) of + {ok, Req2, Env2} -> + execute(Req2, Env2, Tail); + {suspend, Module2, Function2, Args2} -> + erlang:hibernate(?MODULE, resume, + [Env, Tail, Module2, Function2, Args2]); + {halt, Req2} -> + cowboy_req:ensure_response(Req2, 204); + {error, Code, Req2} -> + error_terminate(Code, Req2) + end. + +%% Only send an error reply if there is no resp_sent message. +-spec error_terminate(cowboy:http_status(), cowboy_req:req()) -> ok. +error_terminate(Code, Req) -> + receive + {cowboy_req, resp_sent} -> ok + after 0 -> + _ = cowboy_req:reply(Code, Req), + ok + end. + +%% Reply functions used by cowboy_req. + +reply(Socket = {Pid, _}, Status, Headers, Body) -> + _ = case iolist_size(Body) of + 0 -> Pid ! {reply, Socket, Status, Headers}; + _ -> Pid ! {reply, Socket, Status, Headers, Body} + end, + ok. + +stream_reply(Socket = {Pid, _}, Status, Headers) -> + _ = Pid ! {stream_reply, Socket, Status, Headers}, + ok. + +stream_data(Socket = {Pid, _}, Data) -> + _ = Pid ! {stream_data, Socket, Data}, + ok. + +stream_close(Socket = {Pid, _}) -> + _ = Pid ! {stream_close, Socket}, + ok. + +%% Internal transport functions. +%% @todo recv + +name() -> + spdy. + +send(Socket, Data) -> + stream_data(Socket, Data). + +%% We don't wait for the result of the actual sendfile call, +%% therefore we can't know how much was actually sent. +sendfile(Socket = {Pid, _}, Filepath) -> + _ = Pid ! {sendfile, Socket, Filepath}, + {ok, undefined}. diff --git a/src/cowboy_spdy.hrl b/src/cowboy_spdy.hrl new file mode 100644 index 0000000..9637b1c --- /dev/null +++ b/src/cowboy_spdy.hrl @@ -0,0 +1,181 @@ +%% Zlib dictionary. + +-define(ZDICT, << + 16#00, 16#00, 16#00, 16#07, 16#6f, 16#70, 16#74, 16#69, + 16#6f, 16#6e, 16#73, 16#00, 16#00, 16#00, 16#04, 16#68, + 16#65, 16#61, 16#64, 16#00, 16#00, 16#00, 16#04, 16#70, + 16#6f, 16#73, 16#74, 16#00, 16#00, 16#00, 16#03, 16#70, + 16#75, 16#74, 16#00, 16#00, 16#00, 16#06, 16#64, 16#65, + 16#6c, 16#65, 16#74, 16#65, 16#00, 16#00, 16#00, 16#05, + 16#74, 16#72, 16#61, 16#63, 16#65, 16#00, 16#00, 16#00, + 16#06, 16#61, 16#63, 16#63, 16#65, 16#70, 16#74, 16#00, + 16#00, 16#00, 16#0e, 16#61, 16#63, 16#63, 16#65, 16#70, + 16#74, 16#2d, 16#63, 16#68, 16#61, 16#72, 16#73, 16#65, + 16#74, 16#00, 16#00, 16#00, 16#0f, 16#61, 16#63, 16#63, + 16#65, 16#70, 16#74, 16#2d, 16#65, 16#6e, 16#63, 16#6f, + 16#64, 16#69, 16#6e, 16#67, 16#00, 16#00, 16#00, 16#0f, + 16#61, 16#63, 16#63, 16#65, 16#70, 16#74, 16#2d, 16#6c, + 16#61, 16#6e, 16#67, 16#75, 16#61, 16#67, 16#65, 16#00, + 16#00, 16#00, 16#0d, 16#61, 16#63, 16#63, 16#65, 16#70, + 16#74, 16#2d, 16#72, 16#61, 16#6e, 16#67, 16#65, 16#73, + 16#00, 16#00, 16#00, 16#03, 16#61, 16#67, 16#65, 16#00, + 16#00, 16#00, 16#05, 16#61, 16#6c, 16#6c, 16#6f, 16#77, + 16#00, 16#00, 16#00, 16#0d, 16#61, 16#75, 16#74, 16#68, + 16#6f, 16#72, 16#69, 16#7a, 16#61, 16#74, 16#69, 16#6f, + 16#6e, 16#00, 16#00, 16#00, 16#0d, 16#63, 16#61, 16#63, + 16#68, 16#65, 16#2d, 16#63, 16#6f, 16#6e, 16#74, 16#72, + 16#6f, 16#6c, 16#00, 16#00, 16#00, 16#0a, 16#63, 16#6f, + 16#6e, 16#6e, 16#65, 16#63, 16#74, 16#69, 16#6f, 16#6e, + 16#00, 16#00, 16#00, 16#0c, 16#63, 16#6f, 16#6e, 16#74, + 16#65, 16#6e, 16#74, 16#2d, 16#62, 16#61, 16#73, 16#65, + 16#00, 16#00, 16#00, 16#10, 16#63, 16#6f, 16#6e, 16#74, + 16#65, 16#6e, 16#74, 16#2d, 16#65, 16#6e, 16#63, 16#6f, + 16#64, 16#69, 16#6e, 16#67, 16#00, 16#00, 16#00, 16#10, + 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, 16#74, 16#2d, + 16#6c, 16#61, 16#6e, 16#67, 16#75, 16#61, 16#67, 16#65, + 16#00, 16#00, 16#00, 16#0e, 16#63, 16#6f, 16#6e, 16#74, + 16#65, 16#6e, 16#74, 16#2d, 16#6c, 16#65, 16#6e, 16#67, + 16#74, 16#68, 16#00, 16#00, 16#00, 16#10, 16#63, 16#6f, + 16#6e, 16#74, 16#65, 16#6e, 16#74, 16#2d, 16#6c, 16#6f, + 16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00, + 16#00, 16#0b, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, + 16#74, 16#2d, 16#6d, 16#64, 16#35, 16#00, 16#00, 16#00, + 16#0d, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, 16#74, + 16#2d, 16#72, 16#61, 16#6e, 16#67, 16#65, 16#00, 16#00, + 16#00, 16#0c, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, + 16#74, 16#2d, 16#74, 16#79, 16#70, 16#65, 16#00, 16#00, + 16#00, 16#04, 16#64, 16#61, 16#74, 16#65, 16#00, 16#00, + 16#00, 16#04, 16#65, 16#74, 16#61, 16#67, 16#00, 16#00, + 16#00, 16#06, 16#65, 16#78, 16#70, 16#65, 16#63, 16#74, + 16#00, 16#00, 16#00, 16#07, 16#65, 16#78, 16#70, 16#69, + 16#72, 16#65, 16#73, 16#00, 16#00, 16#00, 16#04, 16#66, + 16#72, 16#6f, 16#6d, 16#00, 16#00, 16#00, 16#04, 16#68, + 16#6f, 16#73, 16#74, 16#00, 16#00, 16#00, 16#08, 16#69, + 16#66, 16#2d, 16#6d, 16#61, 16#74, 16#63, 16#68, 16#00, + 16#00, 16#00, 16#11, 16#69, 16#66, 16#2d, 16#6d, 16#6f, + 16#64, 16#69, 16#66, 16#69, 16#65, 16#64, 16#2d, 16#73, + 16#69, 16#6e, 16#63, 16#65, 16#00, 16#00, 16#00, 16#0d, + 16#69, 16#66, 16#2d, 16#6e, 16#6f, 16#6e, 16#65, 16#2d, + 16#6d, 16#61, 16#74, 16#63, 16#68, 16#00, 16#00, 16#00, + 16#08, 16#69, 16#66, 16#2d, 16#72, 16#61, 16#6e, 16#67, + 16#65, 16#00, 16#00, 16#00, 16#13, 16#69, 16#66, 16#2d, + 16#75, 16#6e, 16#6d, 16#6f, 16#64, 16#69, 16#66, 16#69, + 16#65, 16#64, 16#2d, 16#73, 16#69, 16#6e, 16#63, 16#65, + 16#00, 16#00, 16#00, 16#0d, 16#6c, 16#61, 16#73, 16#74, + 16#2d, 16#6d, 16#6f, 16#64, 16#69, 16#66, 16#69, 16#65, + 16#64, 16#00, 16#00, 16#00, 16#08, 16#6c, 16#6f, 16#63, + 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00, 16#00, + 16#0c, 16#6d, 16#61, 16#78, 16#2d, 16#66, 16#6f, 16#72, + 16#77, 16#61, 16#72, 16#64, 16#73, 16#00, 16#00, 16#00, + 16#06, 16#70, 16#72, 16#61, 16#67, 16#6d, 16#61, 16#00, + 16#00, 16#00, 16#12, 16#70, 16#72, 16#6f, 16#78, 16#79, + 16#2d, 16#61, 16#75, 16#74, 16#68, 16#65, 16#6e, 16#74, + 16#69, 16#63, 16#61, 16#74, 16#65, 16#00, 16#00, 16#00, + 16#13, 16#70, 16#72, 16#6f, 16#78, 16#79, 16#2d, 16#61, + 16#75, 16#74, 16#68, 16#6f, 16#72, 16#69, 16#7a, 16#61, + 16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00, 16#00, 16#05, + 16#72, 16#61, 16#6e, 16#67, 16#65, 16#00, 16#00, 16#00, + 16#07, 16#72, 16#65, 16#66, 16#65, 16#72, 16#65, 16#72, + 16#00, 16#00, 16#00, 16#0b, 16#72, 16#65, 16#74, 16#72, + 16#79, 16#2d, 16#61, 16#66, 16#74, 16#65, 16#72, 16#00, + 16#00, 16#00, 16#06, 16#73, 16#65, 16#72, 16#76, 16#65, + 16#72, 16#00, 16#00, 16#00, 16#02, 16#74, 16#65, 16#00, + 16#00, 16#00, 16#07, 16#74, 16#72, 16#61, 16#69, 16#6c, + 16#65, 16#72, 16#00, 16#00, 16#00, 16#11, 16#74, 16#72, + 16#61, 16#6e, 16#73, 16#66, 16#65, 16#72, 16#2d, 16#65, + 16#6e, 16#63, 16#6f, 16#64, 16#69, 16#6e, 16#67, 16#00, + 16#00, 16#00, 16#07, 16#75, 16#70, 16#67, 16#72, 16#61, + 16#64, 16#65, 16#00, 16#00, 16#00, 16#0a, 16#75, 16#73, + 16#65, 16#72, 16#2d, 16#61, 16#67, 16#65, 16#6e, 16#74, + 16#00, 16#00, 16#00, 16#04, 16#76, 16#61, 16#72, 16#79, + 16#00, 16#00, 16#00, 16#03, 16#76, 16#69, 16#61, 16#00, + 16#00, 16#00, 16#07, 16#77, 16#61, 16#72, 16#6e, 16#69, + 16#6e, 16#67, 16#00, 16#00, 16#00, 16#10, 16#77, 16#77, + 16#77, 16#2d, 16#61, 16#75, 16#74, 16#68, 16#65, 16#6e, + 16#74, 16#69, 16#63, 16#61, 16#74, 16#65, 16#00, 16#00, + 16#00, 16#06, 16#6d, 16#65, 16#74, 16#68, 16#6f, 16#64, + 16#00, 16#00, 16#00, 16#03, 16#67, 16#65, 16#74, 16#00, + 16#00, 16#00, 16#06, 16#73, 16#74, 16#61, 16#74, 16#75, + 16#73, 16#00, 16#00, 16#00, 16#06, 16#32, 16#30, 16#30, + 16#20, 16#4f, 16#4b, 16#00, 16#00, 16#00, 16#07, 16#76, + 16#65, 16#72, 16#73, 16#69, 16#6f, 16#6e, 16#00, 16#00, + 16#00, 16#08, 16#48, 16#54, 16#54, 16#50, 16#2f, 16#31, + 16#2e, 16#31, 16#00, 16#00, 16#00, 16#03, 16#75, 16#72, + 16#6c, 16#00, 16#00, 16#00, 16#06, 16#70, 16#75, 16#62, + 16#6c, 16#69, 16#63, 16#00, 16#00, 16#00, 16#0a, 16#73, + 16#65, 16#74, 16#2d, 16#63, 16#6f, 16#6f, 16#6b, 16#69, + 16#65, 16#00, 16#00, 16#00, 16#0a, 16#6b, 16#65, 16#65, + 16#70, 16#2d, 16#61, 16#6c, 16#69, 16#76, 16#65, 16#00, + 16#00, 16#00, 16#06, 16#6f, 16#72, 16#69, 16#67, 16#69, + 16#6e, 16#31, 16#30, 16#30, 16#31, 16#30, 16#31, 16#32, + 16#30, 16#31, 16#32, 16#30, 16#32, 16#32, 16#30, 16#35, + 16#32, 16#30, 16#36, 16#33, 16#30, 16#30, 16#33, 16#30, + 16#32, 16#33, 16#30, 16#33, 16#33, 16#30, 16#34, 16#33, + 16#30, 16#35, 16#33, 16#30, 16#36, 16#33, 16#30, 16#37, + 16#34, 16#30, 16#32, 16#34, 16#30, 16#35, 16#34, 16#30, + 16#36, 16#34, 16#30, 16#37, 16#34, 16#30, 16#38, 16#34, + 16#30, 16#39, 16#34, 16#31, 16#30, 16#34, 16#31, 16#31, + 16#34, 16#31, 16#32, 16#34, 16#31, 16#33, 16#34, 16#31, + 16#34, 16#34, 16#31, 16#35, 16#34, 16#31, 16#36, 16#34, + 16#31, 16#37, 16#35, 16#30, 16#32, 16#35, 16#30, 16#34, + 16#35, 16#30, 16#35, 16#32, 16#30, 16#33, 16#20, 16#4e, + 16#6f, 16#6e, 16#2d, 16#41, 16#75, 16#74, 16#68, 16#6f, + 16#72, 16#69, 16#74, 16#61, 16#74, 16#69, 16#76, 16#65, + 16#20, 16#49, 16#6e, 16#66, 16#6f, 16#72, 16#6d, 16#61, + 16#74, 16#69, 16#6f, 16#6e, 16#32, 16#30, 16#34, 16#20, + 16#4e, 16#6f, 16#20, 16#43, 16#6f, 16#6e, 16#74, 16#65, + 16#6e, 16#74, 16#33, 16#30, 16#31, 16#20, 16#4d, 16#6f, + 16#76, 16#65, 16#64, 16#20, 16#50, 16#65, 16#72, 16#6d, + 16#61, 16#6e, 16#65, 16#6e, 16#74, 16#6c, 16#79, 16#34, + 16#30, 16#30, 16#20, 16#42, 16#61, 16#64, 16#20, 16#52, + 16#65, 16#71, 16#75, 16#65, 16#73, 16#74, 16#34, 16#30, + 16#31, 16#20, 16#55, 16#6e, 16#61, 16#75, 16#74, 16#68, + 16#6f, 16#72, 16#69, 16#7a, 16#65, 16#64, 16#34, 16#30, + 16#33, 16#20, 16#46, 16#6f, 16#72, 16#62, 16#69, 16#64, + 16#64, 16#65, 16#6e, 16#34, 16#30, 16#34, 16#20, 16#4e, + 16#6f, 16#74, 16#20, 16#46, 16#6f, 16#75, 16#6e, 16#64, + 16#35, 16#30, 16#30, 16#20, 16#49, 16#6e, 16#74, 16#65, + 16#72, 16#6e, 16#61, 16#6c, 16#20, 16#53, 16#65, 16#72, + 16#76, 16#65, 16#72, 16#20, 16#45, 16#72, 16#72, 16#6f, + 16#72, 16#35, 16#30, 16#31, 16#20, 16#4e, 16#6f, 16#74, + 16#20, 16#49, 16#6d, 16#70, 16#6c, 16#65, 16#6d, 16#65, + 16#6e, 16#74, 16#65, 16#64, 16#35, 16#30, 16#33, 16#20, + 16#53, 16#65, 16#72, 16#76, 16#69, 16#63, 16#65, 16#20, + 16#55, 16#6e, 16#61, 16#76, 16#61, 16#69, 16#6c, 16#61, + 16#62, 16#6c, 16#65, 16#4a, 16#61, 16#6e, 16#20, 16#46, + 16#65, 16#62, 16#20, 16#4d, 16#61, 16#72, 16#20, 16#41, + 16#70, 16#72, 16#20, 16#4d, 16#61, 16#79, 16#20, 16#4a, + 16#75, 16#6e, 16#20, 16#4a, 16#75, 16#6c, 16#20, 16#41, + 16#75, 16#67, 16#20, 16#53, 16#65, 16#70, 16#74, 16#20, + 16#4f, 16#63, 16#74, 16#20, 16#4e, 16#6f, 16#76, 16#20, + 16#44, 16#65, 16#63, 16#20, 16#30, 16#30, 16#3a, 16#30, + 16#30, 16#3a, 16#30, 16#30, 16#20, 16#4d, 16#6f, 16#6e, + 16#2c, 16#20, 16#54, 16#75, 16#65, 16#2c, 16#20, 16#57, + 16#65, 16#64, 16#2c, 16#20, 16#54, 16#68, 16#75, 16#2c, + 16#20, 16#46, 16#72, 16#69, 16#2c, 16#20, 16#53, 16#61, + 16#74, 16#2c, 16#20, 16#53, 16#75, 16#6e, 16#2c, 16#20, + 16#47, 16#4d, 16#54, 16#63, 16#68, 16#75, 16#6e, 16#6b, + 16#65, 16#64, 16#2c, 16#74, 16#65, 16#78, 16#74, 16#2f, + 16#68, 16#74, 16#6d, 16#6c, 16#2c, 16#69, 16#6d, 16#61, + 16#67, 16#65, 16#2f, 16#70, 16#6e, 16#67, 16#2c, 16#69, + 16#6d, 16#61, 16#67, 16#65, 16#2f, 16#6a, 16#70, 16#67, + 16#2c, 16#69, 16#6d, 16#61, 16#67, 16#65, 16#2f, 16#67, + 16#69, 16#66, 16#2c, 16#61, 16#70, 16#70, 16#6c, 16#69, + 16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#2f, 16#78, + 16#6d, 16#6c, 16#2c, 16#61, 16#70, 16#70, 16#6c, 16#69, + 16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#2f, 16#78, + 16#68, 16#74, 16#6d, 16#6c, 16#2b, 16#78, 16#6d, 16#6c, + 16#2c, 16#74, 16#65, 16#78, 16#74, 16#2f, 16#70, 16#6c, + 16#61, 16#69, 16#6e, 16#2c, 16#74, 16#65, 16#78, 16#74, + 16#2f, 16#6a, 16#61, 16#76, 16#61, 16#73, 16#63, 16#72, + 16#69, 16#70, 16#74, 16#2c, 16#70, 16#75, 16#62, 16#6c, + 16#69, 16#63, 16#70, 16#72, 16#69, 16#76, 16#61, 16#74, + 16#65, 16#6d, 16#61, 16#78, 16#2d, 16#61, 16#67, 16#65, + 16#3d, 16#67, 16#7a, 16#69, 16#70, 16#2c, 16#64, 16#65, + 16#66, 16#6c, 16#61, 16#74, 16#65, 16#2c, 16#73, 16#64, + 16#63, 16#68, 16#63, 16#68, 16#61, 16#72, 16#73, 16#65, + 16#74, 16#3d, 16#75, 16#74, 16#66, 16#2d, 16#38, 16#63, + 16#68, 16#61, 16#72, 16#73, 16#65, 16#74, 16#3d, 16#69, + 16#73, 16#6f, 16#2d, 16#38, 16#38, 16#35, 16#39, 16#2d, + 16#31, 16#2c, 16#75, 16#74, 16#66, 16#2d, 16#2c, 16#2a, + 16#2c, 16#65, 16#6e, 16#71, 16#3d, 16#30, 16#2e >>). diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl index fd5654e..d144dd3 100644 --- a/src/cowboy_static.erl +++ b/src/cowboy_static.erl @@ -233,7 +233,7 @@ rest_init(Req, Opts) -> end. rest_init(Req, Opts, Filepath) -> - Fileinfo = file:read_file_info(Filepath), + Fileinfo = file:read_file_info(Filepath, [{time, universal}]), Mimetypes = case lists:keyfind(mimetypes, 1, Opts) of false -> {fun path_to_mimetypes/2, []}; {_, {{M, F}, E}} -> {fun M:F/2, E}; @@ -290,7 +290,7 @@ forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=State) -> -spec last_modified(Req, #state{}) -> {calendar:datetime(), Req, #state{}} when Req::cowboy_req:req(). last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) -> - {erlang:localtime_to_universaltime(Modified), Req, 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 diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 21cdd4b..2d7f420 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -64,6 +64,7 @@ -export([rest_options_default/1]). -export([rest_param_all/1]). -export([rest_patch/1]). +-export([rest_post_charset/1]). -export([rest_postonly/1]). -export([rest_resource_etags/1]). -export([rest_resource_etags_if_none_match/1]). @@ -138,6 +139,7 @@ groups() -> rest_options_default, rest_param_all, rest_patch, + rest_post_charset, rest_postonly, rest_resource_etags, rest_resource_etags_if_none_match, @@ -187,9 +189,13 @@ init_per_suite(Config) -> application:start(crypto), application:start(ranch), application:start(cowboy), - Config. + Dir = ?config(priv_dir, Config) ++ "/static", + ct_helper:create_static_dir(Dir), + [{static_dir, Dir}|Config]. -end_per_suite(_Config) -> +end_per_suite(Config) -> + Dir = ?config(static_dir, Config), + ct_helper:delete_static_dir(Dir), application:stop(cowboy), application:stop(ranch), application:stop(crypto), @@ -197,62 +203,58 @@ end_per_suite(_Config) -> init_per_group(http, Config) -> Transport = ranch_tcp, - Config1 = init_static_dir(Config), {ok, _} = cowboy:start_http(http, 100, [{port, 0}], [ - {env, [{dispatch, init_dispatch(Config1)}]}, + {env, [{dispatch, init_dispatch(Config)}]}, {max_keepalive, 50}, {timeout, 500} ]), Port = ranch:get_port(http), {ok, Client} = cowboy_client:init([]), [{scheme, <<"http">>}, {port, Port}, {opts, []}, - {transport, Transport}, {client, Client}|Config1]; + {transport, Transport}, {client, Client}|Config]; init_per_group(https, Config) -> Transport = ranch_ssl, {_, Cert, Key} = ct_helper:make_certs(), Opts = [{cert, Cert}, {key, Key}], - Config1 = init_static_dir(Config), application:start(public_key), application:start(ssl), {ok, _} = cowboy:start_https(https, 100, Opts ++ [{port, 0}], [ - {env, [{dispatch, init_dispatch(Config1)}]}, + {env, [{dispatch, init_dispatch(Config)}]}, {max_keepalive, 50}, {timeout, 500} ]), Port = ranch:get_port(https), {ok, Client} = cowboy_client:init(Opts), [{scheme, <<"https">>}, {port, Port}, {opts, Opts}, - {transport, Transport}, {client, Client}|Config1]; + {transport, Transport}, {client, Client}|Config]; init_per_group(http_compress, Config) -> Transport = ranch_tcp, - Config1 = init_static_dir(Config), {ok, _} = cowboy:start_http(http_compress, 100, [{port, 0}], [ {compress, true}, - {env, [{dispatch, init_dispatch(Config1)}]}, + {env, [{dispatch, init_dispatch(Config)}]}, {max_keepalive, 50}, {timeout, 500} ]), Port = ranch:get_port(http_compress), {ok, Client} = cowboy_client:init([]), [{scheme, <<"http">>}, {port, Port}, {opts, []}, - {transport, Transport}, {client, Client}|Config1]; + {transport, Transport}, {client, Client}|Config]; init_per_group(https_compress, Config) -> Transport = ranch_ssl, {_, Cert, Key} = ct_helper:make_certs(), Opts = [{cert, Cert}, {key, Key}], - Config1 = init_static_dir(Config), application:start(public_key), application:start(ssl), {ok, _} = cowboy:start_https(https_compress, 100, Opts ++ [{port, 0}], [ {compress, true}, - {env, [{dispatch, init_dispatch(Config1)}]}, + {env, [{dispatch, init_dispatch(Config)}]}, {max_keepalive, 50}, {timeout, 500} ]), Port = ranch:get_port(https_compress), {ok, Client} = cowboy_client:init(Opts), [{scheme, <<"https">>}, {port, Port}, {opts, Opts}, - {transport, Transport}, {client, Client}|Config1]; + {transport, Transport}, {client, Client}|Config]; init_per_group(onrequest, Config) -> Transport = ranch_tcp, {ok, _} = cowboy:start_http(onrequest, 100, [{port, 0}], [ @@ -301,15 +303,11 @@ init_per_group(set_env, Config) -> [{scheme, <<"http">>}, {port, Port}, {opts, []}, {transport, Transport}, {client, Client}|Config]. -end_per_group(Group, Config) when Group =:= https; Group =:= https_compress -> - cowboy:stop_listener(https), +end_per_group(Name, _) when Name =:= https; Name =:= https_compress -> + cowboy:stop_listener(Name), application:stop(ssl), application:stop(public_key), - end_static_dir(Config), ok; -end_per_group(Group, Config) when Group =:= http; Group =:= http_compress -> - cowboy:stop_listener(http), - end_static_dir(Config); end_per_group(Name, _) -> cowboy:stop_listener(Name), ok. @@ -357,7 +355,7 @@ init_dispatch(Config) -> {"/static_specify_file/[...]", cowboy_static, [{directory, ?config(static_dir, Config)}, {mimetypes, [{<<".css">>, [<<"text/css">>]}]}, - {file, <<"test_file.css">>}]}, + {file, <<"style.css">>}]}, {"/multipart", http_multipart, []}, {"/echo/body", http_echo_body, []}, {"/echo/body_qs", http_body_qs, []}, @@ -370,6 +368,7 @@ init_dispatch(Config) -> {"/missing_get_callbacks", rest_missing_callbacks, []}, {"/missing_put_callbacks", rest_missing_callbacks, []}, {"/nodelete", rest_nodelete_resource, []}, + {"/post_charset", rest_post_charset_resource, []}, {"/postonly", rest_postonly_resource, []}, {"/patch", rest_patch_resource, []}, {"/resetags", rest_resource_etags, []}, @@ -381,29 +380,6 @@ init_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. - %% Convenience functions. quick_raw(Data, Config) -> @@ -513,9 +489,9 @@ check_status(Config) -> {400, "/static/%2f"}, {400, "/static/%2e"}, {400, "/static/%2e%2e"}, - {403, "/static/test_dir"}, - {403, "/static/test_dir/"}, - {403, "/static/test_noread"}, + {403, "/static/directory"}, + {403, "/static/directory/"}, + {403, "/static/unreadable"}, {404, "/not/found"}, {404, "/static/not_found"}, {500, "/handler_errors?case=handler_before_reply"}, @@ -999,6 +975,15 @@ rest_patch(Config) -> ok end || {Status, Headers, Body} <- Tests]. +rest_post_charset(Config) -> + Client = ?config(client, Config), + Headers = [ + {<<"content-type">>, <<"text/plain;charset=UTF-8">>} + ], + {ok, Client2} = cowboy_client:request(<<"POST">>, + build_url("/post_charset", Config), Headers, "12345", Client), + {ok, 204, _, _} = cowboy_client:response(Client2). + rest_postonly(Config) -> Client = ?config(client, Config), Headers = [ @@ -1114,9 +1099,9 @@ slowloris2(Config) -> static_attribute_etag(Config) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, - build_url("/static_attribute_etag/test.html", Config), Client), + build_url("/static_attribute_etag/index.html", Config), Client), {ok, Client3} = cowboy_client:request(<<"GET">>, - build_url("/static_attribute_etag/test.html", Config), Client2), + build_url("/static_attribute_etag/index.html", Config), Client2), {ok, 200, Headers1, Client4} = cowboy_client:response(Client3), {ok, 200, Headers2, _} = cowboy_client:response(Client4), {<<"etag">>, ETag1} = lists:keyfind(<<"etag">>, 1, Headers1), @@ -1127,9 +1112,9 @@ static_attribute_etag(Config) -> static_function_etag(Config) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, - build_url("/static_function_etag/test.html", Config), Client), + build_url("/static_function_etag/index.html", Config), Client), {ok, Client3} = cowboy_client:request(<<"GET">>, - build_url("/static_function_etag/test.html", Config), Client2), + build_url("/static_function_etag/index.html", Config), Client2), {ok, 200, Headers1, Client4} = cowboy_client:response(Client3), {ok, 200, Headers2, _} = cowboy_client:response(Client4), {<<"etag">>, ETag1} = lists:keyfind(<<"etag">>, 1, Headers1), @@ -1150,7 +1135,7 @@ static_function_etag(Arguments, etag_data) -> static_mimetypes_function(Config) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, - build_url("/static_mimetypes_function/test.html", Config), Client), + build_url("/static_mimetypes_function/index.html", Config), Client), {ok, 200, Headers, _} = cowboy_client:response(Client2), {<<"content-type">>, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers). @@ -1162,7 +1147,7 @@ static_specify_file(Config) -> {ok, 200, Headers, Client3} = cowboy_client:response(Client2), {<<"content-type">>, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), - {ok, <<"test_file.css\n">>, _} = cowboy_client:response_body(Client3). + {ok, <<"body{color:red}\n">>, _} = cowboy_client:response_body(Client3). static_specify_file_catchall(Config) -> Client = ?config(client, Config), @@ -1171,12 +1156,12 @@ static_specify_file_catchall(Config) -> {ok, 200, Headers, Client3} = cowboy_client:response(Client2), {<<"content-type">>, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), - {ok, <<"test_file.css\n">>, _} = cowboy_client:response_body(Client3). + {ok, <<"body{color:red}\n">>, _} = cowboy_client:response_body(Client3). static_test_file(Config) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, - build_url("/static/test_file", Config), Client), + build_url("/static/unknown", Config), Client), {ok, 200, Headers, _} = cowboy_client:response(Client2), {<<"content-type">>, <<"application/octet-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers). @@ -1184,7 +1169,7 @@ static_test_file(Config) -> static_test_file_css(Config) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, - build_url("/static/test_file.css", Config), Client), + build_url("/static/style.css", Config), Client), {ok, 200, Headers, _} = cowboy_client:response(Client2), {<<"content-type">>, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers). diff --git a/test/http_SUITE_data/rest_post_charset_resource.erl b/test/http_SUITE_data/rest_post_charset_resource.erl new file mode 100644 index 0000000..9ccfa61 --- /dev/null +++ b/test/http_SUITE_data/rest_post_charset_resource.erl @@ -0,0 +1,15 @@ +-module(rest_post_charset_resource). +-export([init/3, allowed_methods/2, content_types_accepted/2, from_text/2]). + +init(_Transport, _Req, _Opts) -> + {upgrade, protocol, cowboy_rest}. + +allowed_methods(Req, State) -> + {[<<"POST">>], Req, State}. + +content_types_accepted(Req, State) -> + {[{{<<"text">>, <<"plain">>, [{<<"charset">>, <<"utf-8">>}]}, + from_text}], Req, State}. + +from_text(Req, State) -> + {true, Req, State}. diff --git a/test/spdy_SUITE.erl b/test/spdy_SUITE.erl new file mode 100644 index 0000000..1089991 --- /dev/null +++ b/test/spdy_SUITE.erl @@ -0,0 +1,171 @@ +%% Copyright (c) 2013, Loïc Hoguin <[email protected]> +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(spdy_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include("../src/cowboy_spdy.hrl"). + +%% ct. +-export([all/0]). +-export([groups/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_group/2]). +-export([end_per_group/2]). + +%% Tests. +-export([check_status/1]). + +%% ct. + +all() -> + [{group, spdy}]. + +groups() -> + [{spdy, [], [ + check_status + ]}]. + +init_per_suite(Config) -> + application:start(crypto), + application:start(ranch), + application:start(cowboy), + application:start(public_key), + application:start(ssl), + Dir = ?config(priv_dir, Config) ++ "/static", + ct_helper:create_static_dir(Dir), + [{static_dir, Dir}|Config]. + +end_per_suite(Config) -> + Dir = ?config(static_dir, Config), + ct_helper:delete_static_dir(Dir), + application:stop(ssl), + application:stop(public_key), + application:stop(cowboy), + application:stop(ranch), + application:stop(crypto), + ok. + +init_per_group(Name, Config) -> + {_, Cert, Key} = ct_helper:make_certs(), + Opts = [{cert, Cert}, {key, Key}], + {ok, _} = cowboy:start_spdy(Name, 100, Opts ++ [{port, 0}], [ + {env, [{dispatch, init_dispatch(Config)}]} + ]), + Port = ranch:get_port(Name), + [{port, Port}|Config]. + +end_per_group(Name, _) -> + cowboy:stop_listener(Name), + ok. + +%% Dispatch configuration. + +init_dispatch(Config) -> + cowboy_router:compile([ + {"localhost", [ + {"/static/[...]", cowboy_static, + [{directory, ?config(static_dir, Config)}, + {mimetypes, [{<<".css">>, [<<"text/css">>]}]}]}, + {"/chunked", http_chunked, []}, + {"/", http_handler, []} + ]} + ]). + +%% Convenience functions. + +quick_get(Host, Path, ExpectedFlags, Config) -> + {_, Port} = lists:keyfind(port, 1, Config), + {ok, Socket} = ssl:connect("localhost", Port, [ + binary, {active, false}, + {client_preferred_next_protocols, client, [<<"spdy/3">>]} + ]), + {Zdef, Zinf} = zlib_init(), + ReqHeaders = headers_encode(Zdef, [ + {<<":method">>, <<"GET">>}, + {<<":path">>, list_to_binary(Path)}, + {<<":version">>, <<"HTTP/1.1">>}, + {<<":host">>, list_to_binary(Host)}, + {<<":scheme">>, <<"https">>} + ]), + ReqLength = 10 + byte_size(ReqHeaders), + StreamID = 1, + ok = ssl:send(Socket, << 1:1, 3:15, 1:16, 0:8, ReqLength:24, + 0:1, StreamID:31, 0:1, 0:31, 0:3, 0:5, 0:8, ReqHeaders/binary >>), + {ok, Packet} = ssl:recv(Socket, 0, 1000), + << 1:1, 3:15, 2:16, Flags:8, RespLength:24, + _:1, StreamID:31, RespHeaders/bits >> = Packet, + Flags = ExpectedFlags, + RespLength = 4 + byte_size(RespHeaders), + [<< NbHeaders:32, Rest/bits >>] = try + zlib:inflate(Zinf, RespHeaders) + catch _:_ -> + ok = zlib:inflateSetDictionary(Zinf, ?ZDICT), + zlib:inflate(Zinf, <<>>) + end, + RespHeaders2 = headers_decode(Zinf, Rest, []), + NbHeaders = length(RespHeaders2), + {_, << Status:3/binary, _/bits >>} + = lists:keyfind(<<":status">>, 1, RespHeaders2), + StatusCode = list_to_integer(binary_to_list(Status)), + ok = ssl:close(Socket), + zlib_terminate(Zdef, Zinf), + {StatusCode, RespHeaders2}. + +zlib_init() -> + Zdef = zlib:open(), + ok = zlib:deflateInit(Zdef), + _ = zlib:deflateSetDictionary(Zdef, ?ZDICT), + Zinf = zlib:open(), + ok = zlib:inflateInit(Zinf), + {Zdef, Zinf}. + +zlib_terminate(Zdef, Zinf) -> + zlib:close(Zdef), + zlib:close(Zinf). + +headers_encode(Zdef, Headers) -> + NbHeaders = length(Headers), + Headers2 = << << (begin + SizeN = byte_size(N), + SizeV = byte_size(V), + << SizeN:32, N/binary, SizeV:32, V/binary >> + end)/binary >> || {N, V} <- Headers >>, + Headers3 = << NbHeaders:32, Headers2/binary >>, + iolist_to_binary(zlib:deflate(Zdef, Headers3, full)). + +headers_decode(_, <<>>, Acc) -> + lists:reverse(Acc); +headers_decode(Zinf, << SizeN:32, Rest/bits >>, Acc) -> + << Name:SizeN/binary, SizeV:32, Rest2/bits >> = Rest, + << Value:SizeV/binary, Rest3/bits >> = Rest2, + headers_decode(Zinf, Rest3, [{Name, Value}|Acc]). + +%% Tests. + +check_status(Config) -> + Tests = [ + {200, nofin, "localhost", "/"}, + {200, nofin, "localhost", "/chunked"}, + {200, nofin, "localhost", "/static/style.css"}, + {400, fin, "bad-host", "/"}, + {400, fin, "localhost", "bad-path"}, + {404, fin, "localhost", "/this/path/does/not/exist"} + ], + _ = [{Status, Fin, Host, Path} = begin + RespFlags = case Fin of fin -> 1; nofin -> 0 end, + {Ret, _} = quick_get(Host, Path, RespFlags, Config), + {Ret, Fin, Host, Path} + end || {Status, Fin, Host, Path} <- Tests]. |