diff options
-rw-r--r-- | AUTHORS | 7 | ||||
-rw-r--r-- | CHANGELOG.md | 55 | ||||
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | examples/elixir_hello_world/mix.exs | 2 | ||||
-rw-r--r-- | examples/rest_pastebin/src/toppage_handler.erl | 15 | ||||
-rw-r--r-- | guide/middlewares.md | 2 | ||||
-rw-r--r-- | guide/req.md | 7 | ||||
-rw-r--r-- | guide/resources.md | 17 | ||||
-rw-r--r-- | guide/rest_handlers.md | 84 | ||||
-rw-r--r-- | guide/toc.md | 4 | ||||
-rw-r--r-- | rebar.config | 2 | ||||
-rw-r--r-- | src/cowboy.app.src | 2 | ||||
-rw-r--r-- | src/cowboy_protocol.erl | 14 | ||||
-rw-r--r-- | src/cowboy_req.erl | 130 | ||||
-rw-r--r-- | src/cowboy_rest.erl | 120 | ||||
-rw-r--r-- | src/cowboy_websocket.erl | 4 | ||||
-rw-r--r-- | test/http_SUITE.erl | 15 | ||||
-rw-r--r-- | test/rest_created_path_resource.erl | 35 | ||||
-rw-r--r-- | test/rest_forbidden_resource.erl | 15 |
19 files changed, 265 insertions, 267 deletions
@@ -3,19 +3,19 @@ Cowboy is available thanks to the work of: Loïc Hoguin Magnus Klaar Anthony Ramine -Tom Burdick Adam Cammack +Tom Burdick Ali Sabil Paul Oliver +Slava Yurin Yurii Rashkovskii Andrew Majorov +James Fish Josh Toft -Slava Yurin Steven Gravell Andrew Thompson Hunter Morris Ivan Lisenkov -James Fish Vladimir Dronnikov 0x00F6 0xAX @@ -52,4 +52,5 @@ Unix1 alisdair sullivan dbmercer derdesign +rambocoder serge diff --git a/CHANGELOG.md b/CHANGELOG.md index fa81c74..825d820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,61 @@ CHANGELOG ========= +0.8.3 +----- + + * Remove init_stream/5, add stream_body/2 + + It's better to allow configuring the streamed chunk size on + a per chunk basis. Also easier to use. + + * Update Ranch to 0.8.0 + + Much faster. Also improved stability. + +0.8.2 +----- + + * Add error_hook and ssl_hello_world example + + * Greatly improve the performance of body reading operations + + The streamed chunk size is now configurable through the new + function cowboy_req:init_stream/5. + + * Add cowboy_req:body/2 and cowboy_req:body_qs/2 + + These functions take an additional argument indicating the + maximum size of the body. They will return {error, badlength} + if the size is too large, or {error, chunked} if the body + was sent using the chunked Transfer-Encoding and its size + cannot be determined. + + The function body/1 is now an alias to body/2 with a maximum + body size of 8MB. Likewise, the function body_qs/1 is an alias + of body_qs/2 with a maximum body size of 16KB. + + * Properly handle explicit identity Transfer-Encoding in body_length/1 + + * Small but noticeable performance improvement in the critical path + + We stopped using binary:match/2 in favor of custom functions. + This makes Cowboy 0.5ms faster per request. + + * Prevent loop handlers from awakening after sending a response + + * Optimize cowboy_static initialization code + + * Make path checks in cowboy_static cross-platform + + * Allow '*' for REST content types parameters in content_types_provided + + * Fix cowboy_router types + + * Update Ranch to 0.6.2; adds support for two new SSL options + + * Improve documentation + 0.8.1 ----- @@ -1,7 +1,7 @@ # See LICENSE for licensing information. PROJECT = cowboy -RANCH_VSN = 0.6.2 +RANCH_VSN = 0.8.0 ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \ +warn_shadow_vars +warn_obsolete_guard # +bin_opt_info +warn_missing_spec diff --git a/examples/elixir_hello_world/mix.exs b/examples/elixir_hello_world/mix.exs index 065024d..9f113a6 100644 --- a/examples/elixir_hello_world/mix.exs +++ b/examples/elixir_hello_world/mix.exs @@ -14,7 +14,7 @@ defmodule ElixirHelloWorld.Mixfile do end defp deps do - [ {:ranch, github: "extend/ranch", tag: "0.6.2"}, + [ {:ranch, github: "extend/ranch", tag: "0.8.0"}, {:cowboy, github: "extend/cowboy"} ] end end diff --git a/examples/rest_pastebin/src/toppage_handler.erl b/examples/rest_pastebin/src/toppage_handler.erl index 5e904d9..a6a0829 100644 --- a/examples/rest_pastebin/src/toppage_handler.erl +++ b/examples/rest_pastebin/src/toppage_handler.erl @@ -9,8 +9,6 @@ -export([content_types_provided/2]). -export([content_types_accepted/2]). -export([resource_exists/2]). --export([post_is_create/2]). --export([create_path/2]). %% Callback Callbacks -export([create_paste/2]). @@ -47,17 +45,16 @@ resource_exists(Req, _State) -> end end. -post_is_create(Req, State) -> - {true, Req, State}. - -create_path(Req, State) -> - {<<$/, (new_paste_id())/binary>>, Req, State}. - create_paste(Req, State) -> {<<$/, PasteID/binary>>, Req2} = cowboy_req:meta(put_path, Req), {ok, [{<<"paste">>, Paste}], Req3} = cowboy_req:body_qs(Req2), ok = file:write_file(full_path(PasteID), Paste), - {true, Req3, State}. + case cowboy_req:method(Req3) of + {<<"POST">>, Req4} -> + {<<$/, (new_paste_id())/binary>>, Req4, State}; + {_, Req4} -> + {true, Req4, State} + end. paste_html(Req, index) -> {read_file("index.html"), Req, index}; diff --git a/guide/middlewares.md b/guide/middlewares.md index b5f055c..a1f1f8d 100644 --- a/guide/middlewares.md +++ b/guide/middlewares.md @@ -48,7 +48,7 @@ the routing information. It is a list of tuples with the first element being an atom and the second any Erlang term. Two values in the environment are reserved: - * `listener` contains the pid of the listener process + * `listener` contains the name of the listener * `result` contains the result of the processing The `listener` value is always defined. The `result` value can be diff --git a/guide/req.md b/guide/req.md index f627737..aa3bf4b 100644 --- a/guide/req.md +++ b/guide/req.md @@ -33,7 +33,6 @@ The following access functions are defined in `cowboy_req`: * `method/1`: the request method (`<<"GET">>`, `<<"POST">>`...) * `version/1`: the HTTP version (`{1,0}` or `{1,1}`) * `peer/1`: the peer address and port number - * `peer_addr/1`: the peer address guessed using the request headers * `host/1`: the hostname requested * `host_info/1`: the result of the `[...]` match on the host * `port/1`: the port number used for the connection @@ -104,9 +103,9 @@ and `body_qs/2` will return `{error, badlength}`. If the request contains chunked body, `body/1`, `body/2`, `body_qs/1` and `body_qs/2` will return `{error, chunked}`. If you get either of the above two errors, you will want to -handle the body of the request using `stream_body/1` and -`skip_body/1`, with the streaming process optionally -initialized using `init_stream/4` or `init_stream/5`. +handle the body of the request using `stream_body/1`, +`stream_body/2` and `skip_body/1`, with the streaming process +optionally initialized using `init_stream/4`. Multipart request body ---------------------- diff --git a/guide/resources.md b/guide/resources.md new file mode 100644 index 0000000..1d1b18a --- /dev/null +++ b/guide/resources.md @@ -0,0 +1,17 @@ +Resources +========= + +Frameworks +---------- + + * Please send a pull request! + +Helper libraries +---------------- + + * Please send a pull request! + +Articles +-------- + + * Please send a pull request! diff --git a/guide/rest_handlers.md b/guide/rest_handlers.md index 62f0ba1..1eccc65 100644 --- a/guide/rest_handlers.md +++ b/guide/rest_handlers.md @@ -25,9 +25,89 @@ Not done yet. Feel free to use the one that is currently being worked on. Callbacks --------- -Please see the Webmachine documentation at this time. +All callbacks are optional. Some may become mandatory depending +on what other defined callbacks return. The flow diagram should +be a pretty good resource to determine which callbacks you need. + +When the request starts being processed, Cowboy will call the +`rest_init/2` function if it is defined, with the Req object +and the handler options as arguments. This function must return +`{ok, Req, State}` where `State` is the handler's state that all +subsequent callbacks will receive. + +At the end of every request, the special callback `rest_terminate/2` +will be called if it is defined. It cannot be used to send a reply, +and must always return `ok`. + +All other callbacks are resource callbacks. They all take two +arguments, the Req object and the State, and return a three-element +tuple of the form `{Value, Req, State}`. + +The following table summarizes the callbacks and their default values. +If the callback isn't defined, then the default value will be used. +Please look at the flow diagram to find out the result of each return +value. + +All callbacks can also return `{halt, Req, State}` to stop execution +of the request, at which point `rest_terminate/2` will be called. + +In the following table, "skip" means the callback is entirely skipped +if it is undefined, moving directly to the next step. Similarly, an +empty column means there is no default value for this callback. + +| Callback name | Default value | +| ---------------------- | ------------------------- | +| allowed_methods | `[<<"GET">>, <<"HEAD">>]` | +| allow_missing_post | `true` | +| charsets_provided | skip | +| content_types_accepted | | +| content_types_provided | | +| delete_completed | `true` | +| delete_resource | `false` | +| expires | `undefined` | +| forbidden | `false` | +| generate_etag | `undefined` | +| is_authorized | `true` | +| is_conflict | `false` | +| known_content_type | `true` | +| known_methods | `[<<"GET">>, <<"HEAD">>, <<"POST">>, <<"PUT">>, <<"PATCH">>, <<"DELETE">>, <<"OPTIONS">>]` | +| languages_provided | skip | +| last_modified | `undefined` | +| malformed_request | `false` | +| moved_permanently | `false` | +| moved_temporarily | `false` | +| multiple_choices | `false` | +| options | | +| previously_existed | `false` | +| resource_exists | `true` | +| service_available | `true` | +| uri_too_long | `false` | +| valid_content_headers | `true` | +| valid_entity_length | `true` | +| variances | `[]` | + +As you can see, Cowboy tries to move on with the request whenever +possible by using well thought out default values. + +In addition to these, there can be any number of user-defined +callbacks that are specified through `content_types_accepted/2` +and `content_types_provided/2`. They can take any name, however +it is recommended to use a separate prefix for the callbacks of +each function. For example, `from_html` and `to_html` indicate +in the first case that we're accepting a resource given as HTML, +and in the second case that we send one as HTML. Usage ----- -Please see the examples at this time. +Like Websocket, REST is a sub-protocol of HTTP. It therefore +requires a protocol upgrade. + +``` erlang +init({tcp, http}, Req, Opts) -> + {upgrade, protocol, cowboy_rest}. +``` + +Cowboy will then switch to the REST protocol and start executing +the flow diagram, starting from `rest_init/2` if it's defined, +and ending with `rest_terminate/2` also if defined. diff --git a/guide/toc.md b/guide/toc.md index 2f0d914..6909bed 100644 --- a/guide/toc.md +++ b/guide/toc.md @@ -58,3 +58,7 @@ Cowboy User Guide * One process for many requests * Lowercase header names * Improving performance + * [Resources](resources.md) + * Frameworks + * Helper libraries + * Articles diff --git a/rebar.config b/rebar.config index 64f8d0e..0ddc628 100644 --- a/rebar.config +++ b/rebar.config @@ -1,3 +1,3 @@ {deps, [ - {ranch, ".*", {git, "git://github.com/extend/ranch.git", "0.6.2"}} + {ranch, ".*", {git, "git://github.com/extend/ranch.git", "0.8.0"}} ]}. diff --git a/src/cowboy.app.src b/src/cowboy.app.src index e6309f9..d224c5d 100644 --- a/src/cowboy.app.src +++ b/src/cowboy.app.src @@ -17,7 +17,7 @@ {description, "Small, fast, modular HTTP server."}, {sub_description, "Cowboy is also a socket acceptor pool, " "able to accept connections for any kind of TCP protocol."}, - {vsn, "0.8.1"}, + {vsn, "0.8.3"}, {modules, []}, {registered, [cowboy_clock, cowboy_sup]}, {applications, [ diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl index 78e3b78..be351b7 100644 --- a/src/cowboy_protocol.erl +++ b/src/cowboy_protocol.erl @@ -84,9 +84,9 @@ %% API. %% @doc Start an HTTP protocol process. --spec start_link(pid(), inet:socket(), module(), any()) -> {ok, pid()}. -start_link(ListenerPid, Socket, Transport, Opts) -> - Pid = spawn_link(?MODULE, init, [ListenerPid, Socket, Transport, Opts]), +-spec start_link(any(), inet:socket(), module(), any()) -> {ok, pid()}. +start_link(Ref, Socket, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]), {ok, Pid}. %% Internal. @@ -100,8 +100,8 @@ get_value(Key, Opts, Default) -> end. %% @private --spec init(pid(), inet:socket(), module(), any()) -> ok. -init(ListenerPid, Socket, Transport, Opts) -> +-spec init(any(), inet:socket(), module(), any()) -> ok. +init(Ref, Socket, Transport, Opts) -> Compress = get_value(compress, Opts, false), MaxEmptyLines = get_value(max_empty_lines, Opts, 5), MaxHeaderNameLength = get_value(max_header_name_length, Opts, 64), @@ -110,11 +110,11 @@ init(ListenerPid, Socket, Transport, Opts) -> MaxKeepalive = get_value(max_keepalive, Opts, 100), MaxRequestLineLength = get_value(max_request_line_length, Opts, 4096), Middlewares = get_value(middlewares, Opts, [cowboy_router, cowboy_handler]), - Env = [{listener, ListenerPid}|get_value(env, Opts, [])], + Env = [{listener, Ref}|get_value(env, Opts, [])], OnRequest = get_value(onrequest, Opts, undefined), OnResponse = get_value(onresponse, Opts, undefined), Timeout = get_value(timeout, Opts, 5000), - ok = ranch:accept_ack(ListenerPid), + ok = ranch:accept_ack(Ref), wait_request(<<>>, #state{socket=Socket, transport=Transport, middlewares=Middlewares, compress=Compress, env=Env, max_empty_lines=MaxEmptyLines, max_keepalive=MaxKeepalive, diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 966e463..4ec42f9 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -46,7 +46,6 @@ -export([method/1]). -export([version/1]). -export([peer/1]). --export([peer_addr/1]). -export([host/1]). -export([host_info/1]). -export([port/1]). @@ -78,8 +77,8 @@ -export([has_body/1]). -export([body_length/1]). -export([init_stream/4]). --export([init_stream/5]). -export([stream_body/1]). +-export([stream_body/2]). -export([skip_body/1]). -export([body/1]). -export([body/2]). @@ -155,8 +154,8 @@ meta = [] :: [{atom(), any()}], %% Request body. - body_state = waiting :: waiting | done | {stream, - non_neg_integer(), non_neg_integer(), fun(), any(), fun()}, + body_state = waiting :: waiting | done + | {stream, non_neg_integer(), fun(), any(), fun()}, multipart = undefined :: undefined | {non_neg_integer(), fun()}, buffer = <<>> :: binary(), @@ -230,29 +229,6 @@ version(Req) -> peer(Req) -> {Req#http_req.peer, Req}. -%% @doc Returns the peer address calculated from headers. --spec peer_addr(Req) -> {inet:ip_address(), Req} when Req::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 host binary string. -spec host(Req) -> {binary(), Req} when Req::req(). host(Req) -> @@ -426,61 +402,61 @@ parse_header_default(_Name) -> undefined. -spec parse_header(binary(), Req, any()) -> {ok, any(), Req} | {undefined, binary(), Req} | {error, badarg} when Req::req(). -parse_header(Name, Req, Default) when Name =:= <<"accept">> -> +parse_header(Name = <<"accept">>, Req, Default) -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:list(Value, fun cowboy_http:media_range/2) end); -parse_header(Name, Req, Default) when Name =:= <<"accept-charset">> -> +parse_header(Name = <<"accept-charset">>, Req, Default) -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:nonempty_list(Value, fun cowboy_http:conneg/2) end); -parse_header(Name, Req, Default) when Name =:= <<"accept-encoding">> -> +parse_header(Name = <<"accept-encoding">>, Req, Default) -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:list(Value, fun cowboy_http:conneg/2) end); -parse_header(Name, Req, Default) when Name =:= <<"accept-language">> -> +parse_header(Name = <<"accept-language">>, Req, Default) -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:nonempty_list(Value, fun cowboy_http:language_range/2) end); -parse_header(Name, Req, Default) when Name =:= <<"authorization">> -> +parse_header(Name = <<"authorization">>, Req, Default) -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:token_ci(Value, fun cowboy_http:authorization/2) end); -parse_header(Name, Req, Default) when Name =:= <<"content-length">> -> +parse_header(Name = <<"content-length">>, Req, Default) -> parse_header(Name, Req, Default, fun cowboy_http:digits/1); -parse_header(Name, Req, Default) when Name =:= <<"content-type">> -> +parse_header(Name = <<"content-type">>, Req, Default) -> parse_header(Name, Req, Default, fun cowboy_http:content_type/1); parse_header(Name = <<"cookie">>, Req, Default) -> parse_header(Name, Req, Default, fun cowboy_http:cookie_list/1); -parse_header(Name, Req, Default) when Name =:= <<"expect">> -> +parse_header(Name = <<"expect">>, Req, Default) -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:nonempty_list(Value, fun cowboy_http:expectation/2) end); parse_header(Name, Req, Default) - when Name =:= <<"if-match">>; Name =:= <<"if-none-match">> -> + when Name =:= <<"if-match">>; + Name =:= <<"if-none-match">> -> parse_header(Name, Req, Default, fun cowboy_http:entity_tag_match/1); parse_header(Name, Req, Default) when Name =:= <<"if-modified-since">>; Name =:= <<"if-unmodified-since">> -> parse_header(Name, Req, Default, fun cowboy_http:http_date/1); -parse_header(Name, Req, Default) when Name =:= <<"sec-websocket-protocol">> -> +parse_header(Name, Req, Default) + when Name =:= <<"sec-websocket-protocol">>; + Name =:= <<"x-forwarded-for">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:nonempty_list(Value, fun cowboy_http:token/2) end); %% @todo Extension parameters. -parse_header(Name, Req, Default) when Name =:= <<"transfer-encoding">> -> - parse_header(Name, Req, Default, - fun (Value) -> - cowboy_http:nonempty_list(Value, fun cowboy_http:token_ci/2) - end); -parse_header(Name, Req, Default) when Name =:= <<"upgrade">> -> +parse_header(Name, Req, Default) + when Name =:= <<"transfer-encoding">>; + Name =:= <<"upgrade">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:nonempty_list(Value, fun cowboy_http:token_ci/2) @@ -566,7 +542,7 @@ set_meta(Name, Value, Req=#http_req{meta=Meta}) -> %% Request Body API. %% @doc Return whether the request message has a body. --spec has_body(cowboy_req:req()) -> boolean(). +-spec has_body(req()) -> boolean(). has_body(Req) -> case lists:keyfind(<<"content-length">>, 1, Req#http_req.headers) of {_, <<"0">>} -> @@ -591,17 +567,11 @@ body_length(Req) -> {undefined, Req2} end. -%% @equiv init_stream(1000000, TransferDecode, TransferState, ContentDecode, Req) --spec init_stream(fun(), any(), fun(), Req) - -> {ok, Req} when Req::req(). -init_stream(TransferDecode, TransferState, ContentDecode, Req) -> - init_stream(1000000, TransferDecode, TransferState, ContentDecode, Req). - %% @doc Initialize body streaming and set custom decoding functions. %% %% Calling this function is optional. It should only be used if you %% need to override the default behavior of Cowboy. Otherwise you -%% should call stream_body/1 directly. +%% should call stream_body/{1,2} directly. %% %% Two decodings happen. First a decoding function is applied to the %% transferred data, and then another is applied to the actual content. @@ -613,27 +583,36 @@ init_stream(TransferDecode, TransferState, ContentDecode, Req) -> %% Content encoding is generally used for compression. %% %% Standard encodings can be found in cowboy_http. --spec init_stream(non_neg_integer(), fun(), any(), fun(), Req) +-spec init_stream(fun(), any(), fun(), Req) -> {ok, Req} when Req::req(). -init_stream(MaxLength, TransferDecode, TransferState, ContentDecode, Req) -> +init_stream(TransferDecode, TransferState, ContentDecode, Req) -> {ok, Req#http_req{body_state= - {stream, 0, MaxLength, TransferDecode, TransferState, ContentDecode}}}. + {stream, 0, TransferDecode, TransferState, ContentDecode}}}. + +%% @equiv stream_body(1000000, Req) +-spec stream_body(Req) -> {ok, binary(), Req} + | {done, Req} | {error, atom()} when Req::req(). +stream_body(Req) -> + stream_body(1000000, Req). %% @doc Stream the request's body. %% %% This is the most low level function to read the request body. %% -%% In most cases, if they weren't defined before using stream_body/4, +%% In most cases, if they weren't defined before using init_stream/4, %% this function will guess which transfer and content encodings were %% used for building the request body, and configure the decoding %% functions that will be used when streaming. %% %% It then starts streaming the body, returning {ok, Data, Req} %% for each streamed part, and {done, Req} when it's finished streaming. --spec stream_body(Req) -> {ok, binary(), Req} +%% +%% You can limit the size of the chunks being returned by using the +%% second argument which is the size in bytes. It defaults to 1000000 bytes. +-spec stream_body(non_neg_integer(), Req) -> {ok, binary(), Req} | {done, Req} | {error, atom()} when Req::req(). -stream_body(Req=#http_req{body_state=waiting, - version=Version, transport=Transport, socket=Socket}) -> +stream_body(MaxLength, Req=#http_req{body_state=waiting, version=Version, + transport=Transport, socket=Socket}) -> {ok, ExpectHeader, Req1} = parse_header(<<"expect">>, Req), case ExpectHeader of [<<"100-continue">>] -> @@ -645,8 +624,8 @@ stream_body(Req=#http_req{body_state=waiting, end, case parse_header(<<"transfer-encoding">>, Req1) of {ok, [<<"chunked">>], Req2} -> - stream_body(Req2#http_req{body_state= - {stream, 0, 1000000, + stream_body(MaxLength, Req2#http_req{body_state= + {stream, 0, fun cowboy_http:te_chunked/2, {0, 0}, fun cowboy_http:ce_identity/1}}); {ok, [<<"identity">>], Req2} -> @@ -655,25 +634,25 @@ stream_body(Req=#http_req{body_state=waiting, 0 -> {done, Req3#http_req{body_state=done}}; Length -> - stream_body(Req3#http_req{body_state= - {stream, Length, 1000000, + stream_body(MaxLength, Req3#http_req{body_state= + {stream, Length, fun cowboy_http:te_identity/2, {0, Length}, fun cowboy_http:ce_identity/1}}) end end; -stream_body(Req=#http_req{body_state=done}) -> +stream_body(_, Req=#http_req{body_state=done}) -> {done, Req}; -stream_body(Req=#http_req{buffer=Buffer}) +stream_body(_, Req=#http_req{buffer=Buffer}) when Buffer =/= <<>> -> transfer_decode(Buffer, Req#http_req{buffer= <<>>}); -stream_body(Req) -> - stream_body_recv(Req). +stream_body(MaxLength, Req) -> + stream_body_recv(MaxLength, Req). --spec stream_body_recv(Req) +-spec stream_body_recv(non_neg_integer(), Req) -> {ok, binary(), Req} | {error, atom()} when Req::req(). -stream_body_recv(Req=#http_req{ +stream_body_recv(MaxLength, Req=#http_req{ transport=Transport, socket=Socket, buffer=Buffer, - body_state={stream, Length, MaxLength, _, _, _}}) -> + body_state={stream, Length, _, _, _}}) -> %% @todo Allow configuring the timeout. case Transport:recv(Socket, min(Length, MaxLength), 5000) of {ok, Data} -> transfer_decode(<< Buffer/binary, Data/binary >>, @@ -683,20 +662,20 @@ stream_body_recv(Req=#http_req{ -spec transfer_decode(binary(), Req) -> {ok, binary(), Req} | {error, atom()} when Req::req(). -transfer_decode(Data, Req=#http_req{body_state={stream, _, MaxLength, +transfer_decode(Data, Req=#http_req{body_state={stream, _, TransferDecode, TransferState, ContentDecode}}) -> case TransferDecode(Data, TransferState) of {ok, Data2, Rest, TransferState2} -> content_decode(ContentDecode, Data2, - Req#http_req{buffer=Rest, body_state={stream, 0, MaxLength, + Req#http_req{buffer=Rest, body_state={stream, 0, TransferDecode, TransferState2, ContentDecode}}); %% @todo {header(s) for chunked more -> - stream_body_recv(Req#http_req{buffer=Data, body_state={stream, - 0, MaxLength, TransferDecode, TransferState, ContentDecode}}); + stream_body_recv(0, Req#http_req{buffer=Data, body_state={stream, + 0, TransferDecode, TransferState, ContentDecode}}); {more, Length, Data2, TransferState2} -> content_decode(ContentDecode, Data2, - Req#http_req{body_state={stream, Length, MaxLength, + Req#http_req{body_state={stream, Length, TransferDecode, TransferState2, ContentDecode}}); {done, Length, Rest} -> Req2 = transfer_decode_done(Length, Rest, Req), @@ -1010,8 +989,7 @@ reply(Status, Headers, Body, Req=#http_req{ reply_may_compress(Status, Headers, Body, Req, RespHeaders, HTTP11Headers, Method) -> BodySize = iolist_size(Body), - {ok, Encodings, Req2} - = cowboy_req:parse_header(<<"accept-encoding">>, Req), + {ok, Encodings, Req2} = parse_header(<<"accept-encoding">>, Req), CanGzip = (BodySize > 300) andalso (false =:= lists:keyfind(<<"content-encoding">>, 1, Headers)) diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index cb4fffb..526f102 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -96,9 +96,8 @@ known_methods(Req, State=#state{method=Method}) -> 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">>; - Method =:= <<"PATCH">> -> + Method =:= <<"PATCH">>; Method =:= <<"DELETE">>; + Method =:= <<"OPTIONS">> -> next(Req, State, fun uri_too_long/2); no_call -> next(Req, State, 501); @@ -236,8 +235,8 @@ content_types_provided(Req, State) -> normalize_content_types({ContentType, Callback}) when is_binary(ContentType) -> {cowboy_http:content_type(ContentType), Callback}; -normalize_content_types(Provided) -> - Provided. +normalize_content_types(Normalized) -> + Normalized. prioritize_accept(Accept) -> lists:sort( @@ -728,16 +727,18 @@ 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). + expect(Req, State, allow_missing_post, true, fun post_resource/2, OnFalse). + +post_resource(Req, State) -> + accept_resource(Req, State, 204). method(Req, State=#state{method= <<"DELETE">>}) -> delete_resource(Req, State); -method(Req, State=#state{method= <<"POST">>}) -> - post_is_create(Req, State); method(Req, State=#state{method= <<"PUT">>}) -> is_conflict(Req, State); -method(Req, State=#state{method= <<"PATCH">>}) -> - patch_resource(Req, State); +method(Req, State=#state{method=Method}) + when Method =:= <<"POST">>; Method =:= <<"PATCH">> -> + accept_resource(Req, State, 204); method(Req, State=#state{method=Method}) when Method =:= <<"GET">>; Method =:= <<"HEAD">> -> set_resp_body_etag(Req, State); @@ -752,79 +753,21 @@ delete_resource(Req, State) -> 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, State) -> - case call(Req, State, create_path) of - no_call -> - put_resource(Req, State, fun created_path/2); - {halt, Req2, HandlerState} -> - terminate(Req2, State#state{handler_state=HandlerState}); - {Path, Req2, HandlerState} -> - {HostURL, Req3} = cowboy_req:host_url(Req2), - State2 = State#state{handler_state=HandlerState}, - Req4 = cowboy_req:set_resp_header( - <<"location">>, << HostURL/binary, Path/binary >>, Req3), - put_resource(cowboy_req:set_meta(put_path, Path, Req4), - State2, 303) - end. - -%% Called after content_types_accepted is called for POST methods -%% when create_path did not exist. Expects the full path to -%% be returned and MUST exist in the case that create_path -%% does not. -created_path(Req, State) -> - case call(Req, State, created_path) of - {halt, Req2, HandlerState} -> - terminate(Req2, State#state{handler_state=HandlerState}); - {Path, Req2, HandlerState} -> - {HostURL, Req3} = cowboy_req:host_url(Req2), - State2 = State#state{handler_state=HandlerState}, - Req4 = cowboy_req:set_resp_header( - <<"location">>, << HostURL/binary, Path/binary >>, Req3), - respond(cowboy_req:set_meta(put_path, Path, Req4), - State2, 303) - end. - -%% process_post should return true when the POST body could be processed -%% and false when it hasn't, in which case a 500 error is sent. -process_post(Req, State) -> - case call(Req, State, process_post) of - {halt, Req2, HandlerState} -> - terminate(Req2, State#state{handler_state=HandlerState}); - {true, Req2, HandlerState} -> - State2 = State#state{handler_state=HandlerState}, - next(Req2, State2, fun is_new_resource/2); - {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, State) -> - Path = cowboy_req:get(path, Req), - put_resource(cowboy_req:set_meta(put_path, Path, Req), - State, fun is_new_resource/2). + accept_resource(Req, 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_req:meta(put_path, Req) +%% pushed to the resource in the request body. %% -%%content_types_accepted SHOULD return a different list +%% content_types_accepted SHOULD return a different list %% for each HTTP method. -put_resource(Req, State, OnTrue) -> +accept_resource(Req, State, OnTrue) -> case call(Req, State, content_types_accepted) of no_call -> respond(Req, State, 415); @@ -838,27 +781,6 @@ put_resource(Req, State, OnTrue) -> choose_content_type(Req3, State2, OnTrue, ContentType, CTA2) end. -%% content_types_accepted should return a list of media types and their -%% associated callback functions in the same format as content_types_provided. -%% -%% The callback will then be called and is expected to process the content -%% pushed to the resource in the request body. -%% -%% content_types_accepted SHOULD return a different list -%% for each HTTP method. -patch_resource(Req, State) -> - case call(Req, State, content_types_accepted) of - no_call -> - respond(Req, State, 415); - {halt, Req2, HandlerState} -> - terminate(Req2, State#state{handler_state=HandlerState}); - {CTM, Req2, HandlerState} -> - State2 = State#state{handler_state=HandlerState}, - {ok, ContentType, Req3} - = cowboy_req:parse_header(<<"content-type">>, Req2), - choose_content_type(Req3, State2, 204, ContentType, CTM) - end. - %% The special content type '*' will always match. It can be used as a %% catch-all content type for accepting any kind of request content. %% Note that because it will always match, it should be the last of the @@ -880,9 +802,8 @@ choose_content_type(Req, State, OnTrue, choose_content_type(Req, State, OnTrue, ContentType, [_Any|Tail]) -> choose_content_type(Req, State, OnTrue, ContentType, Tail). -process_content_type(Req, - State=#state{handler=Handler, handler_state=HandlerState}, - OnTrue, Fun) -> +process_content_type(Req, State=#state{method=Method, + handler=Handler, handler_state=HandlerState}, OnTrue, Fun) -> case call(Req, State, Fun) of no_call -> error_logger:error_msg( @@ -898,7 +819,12 @@ process_content_type(Req, next(Req2, State2, OnTrue); {false, Req2, HandlerState2} -> State2 = State#state{handler_state=HandlerState2}, - respond(Req2, State2, 422) + respond(Req2, State2, 422); + {ResURL, Req2, HandlerState2} when Method =:= <<"POST">> -> + State2 = State#state{handler_state=HandlerState2}, + Req3 = cowboy_req:set_resp_header( + <<"location">>, ResURL, Req2), + respond(Req3, State2, 303) end. %% Whether we created a new resource, either through PUT or POST. diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl index 7bb5e74..b5075c0 100644 --- a/src/cowboy_websocket.erl +++ b/src/cowboy_websocket.erl @@ -63,8 +63,8 @@ | {suspend, module(), atom(), [any()]} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). upgrade(Req, Env, Handler, HandlerOpts) -> - {_, ListenerPid} = lists:keyfind(listener, 1, Env), - ranch_listener:remove_connection(ListenerPid), + {_, Ref} = lists:keyfind(listener, 1, Env), + ranch:remove_connection(Ref), [Socket, Transport] = cowboy_req:get([socket, transport], Req), State = #state{env=Env, socket=Socket, transport=Transport, handler=Handler, handler_opts=HandlerOpts}, diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 911efb8..e33e19a 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -54,7 +54,6 @@ -export([pipeline/1]). -export([pipeline_long_polling/1]). -export([rest_bad_accept/1]). --export([rest_created_path/1]). -export([rest_expires/1]). -export([rest_keepalive/1]). -export([rest_keepalive_post/1]). @@ -124,7 +123,6 @@ groups() -> pipeline, pipeline_long_polling, rest_bad_accept, - rest_created_path, rest_expires, rest_keepalive, rest_keepalive_post, @@ -364,7 +362,6 @@ init_dispatch(Config) -> {"/missing_put_callbacks", rest_missing_callbacks, []}, {"/nodelete", rest_nodelete_resource, []}, {"/patch", rest_patch_resource, []}, - {"/created_path", rest_created_path_resource, []}, {"/resetags", rest_resource_etags, []}, {"/rest_expires", rest_expires, []}, {"/loop_timeout", http_handler_loop_timeout, []}, @@ -882,18 +879,6 @@ rest_bad_accept(Config) -> Client), {ok, 400, _, _} = cowboy_client:response(Client2). -rest_created_path(Config) -> - Headers = [{<<"content-type">>, <<"text/plain">>}], - Body = <<"Whatever">>, - Client = ?config(client, Config), - URL = build_url("/created_path", Config), - {ok, Client2} = cowboy_client:request(<<"POST">>, URL, Headers, - Body, Client), - {ok, 303, ResHeaders, _} = cowboy_client:response(Client2), - {<<"location">>, _Location} = - lists:keyfind(<<"location">>, 1, ResHeaders), - ok. - rest_expires(Config) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, diff --git a/test/rest_created_path_resource.erl b/test/rest_created_path_resource.erl deleted file mode 100644 index 5ad8cfc..0000000 --- a/test/rest_created_path_resource.erl +++ /dev/null @@ -1,35 +0,0 @@ --module(rest_created_path_resource). --export([init/3]). --export([allowed_methods/2]). --export([content_types_provided/2]). --export([get_text_plain/2]). --export([post_is_create/2]). --export([content_types_accepted/2]). --export([post_text_plain/2]). --export([created_path/2]). - -init(_Transport, _Req, _Opts) -> - {upgrade, protocol, cowboy_rest}. - -allowed_methods(Req, State) -> -{[<<"HEAD">>, <<"GET">>, <<"POST">>], Req, State}. - -content_types_provided(Req, State) -> - {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. - -get_text_plain(Req, State) -> - {<<"This is REST!">>, Req, State}. - -post_is_create(Req, State) -> - {true, Req, State}. - -content_types_accepted(Req, State) -> - {[{{<<"text">>, <<"plain">>, []}, post_text_plain}], Req, State}. - -post_text_plain(Req, State) -> - {true, Req, State}. - -created_path(Req, State) -> - {<<"/created">>, Req, State}. - - diff --git a/test/rest_forbidden_resource.erl b/test/rest_forbidden_resource.erl index 63aac7e..287ff62 100644 --- a/test/rest_forbidden_resource.erl +++ b/test/rest_forbidden_resource.erl @@ -1,7 +1,7 @@ -module(rest_forbidden_resource). -export([init/3, rest_init/2, allowed_methods/2, forbidden/2, content_types_provided/2, content_types_accepted/2, - post_is_create/2, create_path/2, to_text/2, from_text/2]). + to_text/2, from_text/2]). init(_Transport, _Req, _Opts) -> {upgrade, protocol, cowboy_rest}. @@ -23,18 +23,9 @@ content_types_provided(Req, State) -> content_types_accepted(Req, State) -> {[{{<<"text">>, <<"plain">>, []}, from_text}], Req, State}. -post_is_create(Req, State) -> - {true, Req, State}. - -create_path(Req, State) -> - {Path, Req2} = cowboy_req:path(Req), - {Path, Req2, State}. - to_text(Req, State) -> {<<"This is REST!">>, Req, State}. from_text(Req, State) -> - {true, Req, State}. - - - + {Path, Req2} = cowboy_req:path(Req), + {Path, Req2, State}. |