diff options
44 files changed, 3274 insertions, 432 deletions
@@ -6,8 +6,8 @@ Anthony Ramine Adam Cammack Tom Burdick Ali Sabil -Paul Oliver James Fish +Paul Oliver Slava Yurin Yurii Rashkovskii Andrew Majorov diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9b92b..478a9a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,56 @@ CHANGELOG ========= +0.8.5 +----- + + * Add the Cowboy Function Reference + + Everything documented in the function reference is the API + that will make it to Cowboy 1.0. + + * Use erlang.mk + + The project is of course still compatible with rebar + and can be used as a dependency just fine. + + * Update Ranch to 0.8.3 + + * Remove cowboy_req:fragment/1 + + No well-written client is sending the fragment with the URL. + + * Add cowboy_req:set_resp_body_fun(chunked, Fun, Req) + + * Improve various typespecs + + * Change the return value of cowboy_req:version/1 + + We now have 'HTTP/1.1' instead of {1, 1} and 'HTTP/1.0' + instead of {1, 0}. + + * Change the return value of REST accept callbacks + + The Path return value becomes {true, Path}. + + * Change the return value of REST charsets_provided/2 + + It was incorrectly expecting a list of tuples instead of + a list of charsets. + + * Move various types to the cowboy module + * cowboy_http:version() to cowboy:http_version() + * cowboy_http:headers() to cowboy:http_headers() + * cowboy_http:status() to cowboy:http_status() + * cowboy_protocol:onrequest_fun() to cowboy:onrequest_fun() + * cowboy_protocol:onresponse_fun() to cowboy:onresponse_fun() + + * Add type cowboy_protocol:opts() + + * Fix a REST bug with the OPTIONS method + + * Fix a REST bug where iso-8859-1 would be incoditionally selected + 0.8.4 ----- @@ -1,4 +1,4 @@ -Copyright (c) 2011-2012, Loïc Hoguin <[email protected]> +Copyright (c) 2011-2013, Loïc Hoguin <[email protected]> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -1,105 +1,28 @@ # See LICENSE for licensing information. PROJECT = cowboy -RANCH_VSN = 0.8.1 -ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \ - +warn_shadow_vars +warn_obsolete_guard # +bin_opt_info +warn_missing_spec -DEPS_DIR ?= $(CURDIR)/deps -export DEPS_DIR +# Options. -# Makefile tweaks. - -V ?= 0 - -appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src; -appsrc_verbose = $(appsrc_verbose_$(V)) - -erlc_verbose_0 = @echo " ERLC " $(?F); -erlc_verbose = $(erlc_verbose_$(V)) - -gen_verbose_0 = @echo " GEN " $@; -gen_verbose = $(gen_verbose_$(V)) - -.PHONY: all clean-all app clean deps clean-deps docs clean-docs \ - build-tests tests autobahn build-plt dialyze - -# Application. - -all: deps app - -clean-all: clean clean-deps clean-docs - $(gen_verbose) rm -rf .$(PROJECT).plt $(DEPS_DIR) logs - -MODULES = $(shell ls src/*.erl | sed 's/src\///;s/\.erl/,/' | sed '$$s/.$$//') - -app: ebin/$(PROJECT).app - $(appsrc_verbose) cat src/$(PROJECT).app.src \ - | sed 's/{modules, \[\]}/{modules, \[$(MODULES)\]}/' \ - > ebin/$(PROJECT).app - -COMPILE_FIRST = src/cowboy_middleware.erl src/cowboy_sub_protocol.erl - -ebin/$(PROJECT).app: src/*.erl - @mkdir -p ebin/ - $(erlc_verbose) erlc -v $(ERLC_OPTS) -o ebin/ -pa ebin/ \ - $(COMPILE_FIRST) $? - -clean: - $(gen_verbose) rm -rf ebin/ test/*.beam erl_crash.dump +COMPILE_FIRST = cowboy_middleware cowboy_sub_protocol +CT_SUITES = eunit http spdy ws +PLT_APPS = crypto public_key ssl # Dependencies. -$(DEPS_DIR)/ranch: - @mkdir -p $(DEPS_DIR) - git clone -n -- https://github.com/extend/ranch.git $(DEPS_DIR)/ranch - cd $(DEPS_DIR)/ranch ; git checkout -q $(RANCH_VSN) - -deps: $(DEPS_DIR)/ranch - @$(MAKE) -C $(DEPS_DIR)/ranch - -clean-deps: - -@$(MAKE) -C $(DEPS_DIR)/ranch clean - -# Documentation. - -docs: clean-docs - $(gen_verbose) erl -noshell \ - -eval 'edoc:application($(PROJECT), ".", []), init:stop().' +DEPS = ranch +TEST_DEPS = ct_helper +dep_ranch = https://github.com/extend/ranch.git 0.8.3 +dep_ct_helper = https://github.com/extend/ct_helper.git master -clean-docs: - $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info +# Standard targets. -# Tests. +include erlang.mk -build-tests: - $(gen_verbose) erlc -v $(ERLC_OPTS) \ - -o test/ test/*.erl test/*/*.erl -pa ebin/ +# Extra targets. -CT_RUN = ct_run \ - -no_auto_compile \ - -noshell \ - -pa ebin $(DEPS_DIR)/*/ebin \ - -dir test \ - -logdir logs -# -cover test/cover.spec - -tests: ERLC_OPTS += -DTEST=1 +'{parse_transform, eunit_autoexport}' -tests: clean clean-deps deps app build-tests - @mkdir -p logs/ - @$(CT_RUN) -suite eunit_SUITE http_SUITE ws_SUITE - $(gen_verbose) rm -f test/*.beam +.PHONY: autobahn autobahn: clean clean-deps deps app build-tests @mkdir -p logs/ @$(CT_RUN) -suite autobahn_SUITE - -# Dialyzer. - -build-plt: deps app - @dialyzer --build_plt --output_plt .$(PROJECT).plt \ - --apps erts kernel stdlib crypto public_key ssl $(DEPS_DIR)/ranch - -dialyze: - @dialyzer --src src --plt .$(PROJECT).plt --no_native \ - -Werror_handling -Wrace_conditions -Wunmatched_returns # -Wunderspecs @@ -21,9 +21,9 @@ No parameterized module. No process dictionary. **Clean** Erlang code. Getting Started --------------- - * [Read the guide](http://ninenines.eu/docs/en/cowboy/HEAD/guide/introduction) + * [Read the guide](http://ninenines.eu/docs/en/cowboy/HEAD/guide) + * [Check the manual](http://ninenines.eu/docs/en/cowboy/HEAD/manual) * Look at the examples in the `examples/` directory - * Build API documentation with `make docs`; open `doc/index.html` Support ------- @@ -3,58 +3,39 @@ ROADMAP This document explains in as much details as possible the list of planned changes and work to be done on the Cowboy -server. It is non-exhaustive and subject to change. Items -are not ordered. +server. It is intended to be exhaustive but some elements +might still be missing. - * Add and improve examples +All the following items must be done before Cowboy 1.0 is +released. - * Improve user guide + * Parse support for all standard HTTP/1.1 headers - We need feedback to improve the guide. + * Support for multipart requests and responses - * Add and improve tests + * Convenience API for extracting query string and body + information, similar to PHP's $_GET, $_POST and $_FILES - Amongst the areas less tested there is protocol upgrades - and the REST handler. + * Add Range support to REST - While eunit and ct tests are fine, some parts of the - code could benefit from PropEr tests. - - * Continuous performance testing - - Initially dubbed the Horse project, Cowboy could benefit - from a continuous performance testing tool that would - allow us to easily compare the impact of the changes we - are introducing, similar to what the Phoronix test suite - allows. - - Depending on the test it may be interesting to compare - Cowboy to other servers and eventually take ideas from - the servers that outperform Cowboy for the task being tested. - - * Full HTTP/1.1 support - - * Improved HTTP/1.0 support - - Most of the work on Cowboy has been done with HTTP/1.1 - in mind. But there is still a need for HTTP/1.0 code in - Cowboy. The server code should be reviewed and tested - to ensure compatibility with remaining HTTP/1.0 products. + * SPDY support - * Continue improving the REST API + We are only interested in supporting existing + implementations, not the full protocol, as this + protocol has been abandoned in favor of HTTP/2.0 - * SPDY support + * Complete the user guide -The following items pertain to Ranch. +The following items pertain to Ranch, but are equally important. * Resizing the acceptor pool We should be able to add more acceptors to a pool but also - to remove some of them as needed. + to remove some of them as needed * Add Transport:secure/0 Currently Cowboy checks if a connection is secure by checking if its name is 'ssl'. This isn't a very modular solution, adding an API function that returns whether - a connection is secure would fix that issue. + a connection is secure would fix that issue diff --git a/erlang.mk b/erlang.mk new file mode 100644 index 0000000..ecd4d54 --- /dev/null +++ b/erlang.mk @@ -0,0 +1,135 @@ +# 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. + +# Verbosity and tweaks. + +V ?= 0 + +appsrc_verbose_0 = @echo " APP " $(PROJECT).app.src; +appsrc_verbose = $(appsrc_verbose_$(V)) + +erlc_verbose_0 = @echo " ERLC " $(?F); +erlc_verbose = $(erlc_verbose_$(V)) + +gen_verbose_0 = @echo " GEN " $@; +gen_verbose = $(gen_verbose_$(V)) + +.PHONY: all clean-all app clean deps clean-deps docs clean-docs \ + build-tests tests build-plt dialyze + +# Deps directory. + +DEPS_DIR ?= $(CURDIR)/deps +export DEPS_DIR + +ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(DEPS)) +ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS)) + +# Application. + +ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \ + +warn_shadow_vars +warn_obsolete_guard # +bin_opt_info +warn_missing_spec +COMPILE_FIRST ?= +COMPILE_FIRST_PATHS = $(addprefix src/,$(addsuffix .erl,$(COMPILE_FIRST))) + +all: deps app + +clean-all: clean clean-deps clean-docs + $(gen_verbose) rm -rf .$(PROJECT).plt $(DEPS_DIR) logs + +MODULES = $(shell ls src/*.erl | sed 's/src\///;s/\.erl/,/' | sed '$$s/.$$//') + +app: ebin/$(PROJECT).app + $(appsrc_verbose) cat src/$(PROJECT).app.src \ + | sed 's/{modules, \[\]}/{modules, \[$(MODULES)\]}/' \ + > ebin/$(PROJECT).app + +ebin/$(PROJECT).app: src/*.erl + @mkdir -p ebin/ + $(erlc_verbose) ERL_LIBS=deps erlc -v $(ERLC_OPTS) -o ebin/ -pa ebin/ \ + $(COMPILE_FIRST_PATHS) $? + +clean: + $(gen_verbose) rm -rf ebin/ test/*.beam erl_crash.dump + +# Dependencies. + +define get_dep = + @mkdir -p $(DEPS_DIR) + git clone -n -- $(word 1,$(dep_$(1))) $(DEPS_DIR)/$(1) + cd $(DEPS_DIR)/$(1) ; git checkout -q $(word 2,$(dep_$(1))) +endef + +define dep_target = +$(DEPS_DIR)/$(1): + $(call get_dep,$(1)) +endef + +$(foreach dep,$(DEPS),$(eval $(call dep_target,$(dep)))) + +deps: $(ALL_DEPS_DIRS) + @for dep in $(ALL_DEPS_DIRS) ; do $(MAKE) -C $$dep; done + +clean-deps: + @for dep in $(ALL_DEPS_DIRS) ; do $(MAKE) -C $$dep clean; done + +# Documentation. + +docs: clean-docs + $(gen_verbose) erl -noshell \ + -eval 'edoc:application($(PROJECT), ".", []), init:stop().' + +clean-docs: + $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info + +# Tests. + +$(foreach dep,$(TEST_DEPS),$(eval $(call dep_target,$(dep)))) + +build-test-deps: $(ALL_TEST_DEPS_DIRS) + @for dep in $(ALL_TEST_DEPS_DIRS) ; do $(MAKE) -C $$dep; done + +build-tests: build-test-deps + $(gen_verbose) erlc -v $(ERLC_OPTS) -o test/ \ + $(wildcard test/*.erl test/*/*.erl) -pa ebin/ + +CT_RUN = ct_run \ + -no_auto_compile \ + -noshell \ + -pa ebin $(DEPS_DIR)/*/ebin \ + -dir test \ + -logdir logs +# -cover test/cover.spec + +CT_SUITES ?= +CT_SUITES_FULL = $(addsuffix _SUITE,$(CT_SUITES)) + +tests: ERLC_OPTS += -DTEST=1 +'{parse_transform, eunit_autoexport}' +tests: clean deps app build-tests + @mkdir -p logs/ + @$(CT_RUN) -suite $(CT_SUITES_FULL) + $(gen_verbose) rm -f test/*.beam + +# Dialyzer. + +PLT_APPS ?= +DIALYZER_OPTS ?= -Werror_handling -Wrace_conditions \ + -Wunmatched_returns # -Wunderspecs + +build-plt: deps app + @dialyzer --build_plt --output_plt .$(PROJECT).plt \ + --apps erts kernel stdlib $(PLT_APPS) $(ALL_DEPS_DIRS) + +dialyze: + @dialyzer --src src --plt .$(PROJECT).plt --no_native $(DIALYZER_OPTS) diff --git a/examples/elixir_hello_world/mix.exs b/examples/elixir_hello_world/mix.exs index aab48e8..b2e3dd3 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.8.1"}, + [ {:ranch, github: "extend/ranch", tag: "0.8.3"}, {:cowboy, github: "extend/cowboy"} ] end end diff --git a/guide/req.md b/guide/req.md index aa3bf4b..96f72b9 100644 --- a/guide/req.md +++ b/guide/req.md @@ -31,7 +31,7 @@ request object. The following access functions are defined in `cowboy_req`: * `method/1`: the request method (`<<"GET">>`, `<<"POST">>`...) - * `version/1`: the HTTP version (`{1,0}` or `{1,1}`) + * `version/1`: the HTTP version (`'HTTP/1.0'` or `'HTTP/1.1'`) * `peer/1`: the peer address and port number * `host/1`: the hostname requested * `host_info/1`: the result of the `[...]` match on the host @@ -41,8 +41,7 @@ The following access functions are defined in `cowboy_req`: * `qs/1`: the entire query string unmodified * `qs_val/{2,3}`: the value for the requested query string key * `qs_vals/1`: all key/values found in the query string - * `fragment/1`: the fragment part of the URL (e.g. `#nav-links`) - * `host_url/1`: the requested URL without the path, qs and fragment + * `host_url/1`: the requested URL without the path and query string * `url/1`: the requested URL * `binding/{2,3}`: the value for the requested binding found during routing * `bindings/1`: all key/values found during routing diff --git a/manual/cowboy.md b/manual/cowboy.md new file mode 100644 index 0000000..ebfe615 --- /dev/null +++ b/manual/cowboy.md @@ -0,0 +1,89 @@ +cowboy +====== + +The `cowboy` module provides convenience functions for +manipulating Ranch listeners. + +Types +----- + +### http_headers() = [{binary(), iodata()}] + +> HTTP headers as a list of key/values. + +### http_status() = non_neg_integer() | binary() + +> HTTP status. +> +> A binary status can be used to set a custom message. + +### http_version() = 'HTTP/1.1' | 'HTTP/1.0' + +> HTTP version. + +### onrequest_fun() = fun((cowboy_req:req()) -> cowboy_req:req()) + +> Fun called immediately after receiving a request. +> +> It can perform any operation on the `Req` object, including +> reading the request body or replying. If a reply is sent, +> the processing of the request ends here, before any middleware +> is executed. + +### onresponse_fun() = fun((http_status(), http_headers(), + iodata(), cowboy_req:req()) -> cowboy_req:req()) + +> Fun called immediately before sending the response. +> +> It can perform any operation on the `Req` object, including +> reading the request body or replying. If a reply is sent, it +> overrides the reply initially sent. The callback will not be +> called again for the new reply. + +Exports +------- + +### start_http(Ref, NbAcceptors, TransOpts, ProtoOpts) -> {ok, pid()} + +> Types: +> * Ref = ranch:ref() +> * NbAcceptors = non_neg_integer() +> * TransOpts = ranch_tcp:opts() +> * ProtoOpts = cowboy_protocol:opts() +> +> Start listening for HTTP connections. Returns the pid for this +> listener's supervisor. + +### start_https(Ref, NbAcceptors, TransOpts, ProtoOpts) -> {ok, pid()} + +> Types: +> * Ref = ranch:ref() +> * NbAcceptors = non_neg_integer() +> * TransOpts = ranch_ssl:opts() +> * ProtoOpts = cowboy_protocol:opts() +> +> Start listening for HTTPS connections. Returns the pid for this +> listener's supervisor. + +### stop_listener(Ref) -> ok + +> Types: +> * Ref = ranch:ref() +> +> Stop a previously started listener. + +### set_env(Ref, Name, Value) -> ok + +> Types: +> * Ref = ranch:ref() +> * Name = atom() +> * Value = any() +> +> Set or update an environment value for an already running listener. +> This will take effect on all subsequent connections. + +See also +-------- + +The [Ranch guide](http://ninenines.eu/docs/en/ranch/HEAD/guide) +provides detailed information about how listeners work. diff --git a/manual/cowboy_app.md b/manual/cowboy_app.md new file mode 100644 index 0000000..5311109 --- /dev/null +++ b/manual/cowboy_app.md @@ -0,0 +1,25 @@ +The Cowboy Application +====================== + +Small, fast, modular HTTP server. + +Dependencies +------------ + +The `cowboy` application uses the Erlang applications `ranch` +for listening and accepting TCP connections, and `crypto` +for establishing Websocket connections. These dependencies must +be loaded for the `cowboy` application to work. In an embedded +environment this means that they need to be started with the +`application:start/{1,2}` function before the `cowboy` +application is started. + +The `cowboy` application also uses the Erlang applications +`public_key` and `ssl` when listening for HTTPS connections. +These are started automatically if they weren't before. + +Environment +----------- + +The `cowboy` application does not define any application +environment configuration parameters. diff --git a/manual/cowboy_handler.md b/manual/cowboy_handler.md new file mode 100644 index 0000000..8d13492 --- /dev/null +++ b/manual/cowboy_handler.md @@ -0,0 +1,25 @@ +cowboy_handler +============== + +The `cowboy_handler` middleware executes the handler passed +through the environment values `handler` and `handler_opts`, +and add the result of this execution to the environment as +the value `result`, indicating that the request has been +handled and received a response. + +Environment input: + * handler = module() + * handler_opts = any() + +Environment output: + * result = ok + +Types +----- + +None. + +Exports +------- + +None. diff --git a/manual/cowboy_http_handler.md b/manual/cowboy_http_handler.md new file mode 100644 index 0000000..9d283e7 --- /dev/null +++ b/manual/cowboy_http_handler.md @@ -0,0 +1,57 @@ +cowboy_http_handler +=================== + +The `cowboy_http_handler` behaviour defines the interface used +by plain HTTP handlers. + +Unless noted otherwise, the callbacks will be executed sequentially. + +Types +----- + +None. + +Callbacks +--------- + +### init({TransportName, ProtocolName}, Req, Opts) + -> {ok, Req, State} | {shutdown, Req, State} + +> Types: +> * TransportName = tcp | ssl | atom() +> * ProtocolName = http | atom() +> * Req = cowboy_req:req() +> * Opts = any() +> * State = any() +> +> Initialize the state for this request. +> +> The `shutdown` return value can be used to skip the `handle/2` +> call entirely. + +### handle(Req, State) -> {ok, Req, State} + +> Types: +> * Req = cowboy_req:req() +> * State = any() +> +> Handle the request. +> +> This callback is where the request is handled and a response +> should be sent. If a response is not sent, Cowboy will send +> a `204 No Content` response automatically. + +### terminate(Reason, Req, State) -> ok + +> Types: +> * Reason = {normal, shutdown} | {error, atom()} +> * Req = cowboy_req:req() +> * State = any() +> +> Perform any necessary cleanup of the state. +> +> This callback should release any resource currently in use, +> clear any active timer and reset the process to its original +> state, as it might be reused for future requests sent on the +> same connection. Typical plain HTTP handlers rarely need to +> use it. diff --git a/manual/cowboy_loop_handler.md b/manual/cowboy_loop_handler.md new file mode 100644 index 0000000..ccbb9b0 --- /dev/null +++ b/manual/cowboy_loop_handler.md @@ -0,0 +1,91 @@ +cowboy_loop_handler +=================== + +The `cowboy_loop_handler` behaviour defines the interface used +by HTTP handlers that do not send a response directly, instead +requiring a receive loop to process Erlang messages. + +This interface is best fit for long-polling types of requests. + +The `init/3` callback will always be called, followed by zero +or more calls to `info/3`. The `terminate/3` will always be +called last. + +Types +----- + +None. + +Callbacks +--------- + +### init({TransportName, ProtocolName}, Req, Opts) + -> {loop, Req, State} + | {loop, Req, State, hibernate} + | {loop, Req, State, Timeout} + | {loop, Req, State, Timeout, hibernate} + | {shutdown, Req, State} + +> Types: +> * TransportName = tcp | ssl | atom() +> * ProtocolName = http | atom() +> * Req = cowboy_req:req() +> * Opts = any() +> * State = any() +> * Timeout = timeout() +> +> Initialize the state for this request. +> +> This callback will typically be used to register this process +> to an event manager or a message queue in order to receive +> the messages the handler wants to process. +> +> The receive loop will run for a duration of up to `Timeout` +> milliseconds after it last received data from the socket, +> at which point it will stop and send a `204 No Content` reply. +> By default this value is set to `infinity`. It is recommended +> to either set this value or ensure by any other mechanism +> that the handler will be closed after a certain period of +> inactivity. +> +> The `hibernate` option will hibernate the process until it +> starts receiving messages. +> +> The `shutdown` return value can be used to skip the receive +> loop entirely. + +### info(Info, Req, State) -> {ok, Req, State} | {loop, Req, State} + | {loop, Req, State, hibernate} + +> Types: +> * Info = any() +> * Req = cowboy_req:req() +> * State = any() +> +> Handle the Erlang message received. +> +> This function will be called every time an Erlang message +> has been received. The message can be any Erlang term. +> +> The `ok` return value can be used to stop the receive loop, +> typically because a response has been sent. +> +> The `hibernate` option will hibernate the process until +> it receives another message. + +### terminate(Reason, Req, State) -> ok + +> Types: +> * Reason = {normal, shutdown} | {normal, timeout} | {error, closed} | {error, overflow} | {error, atom()} +> * Req = cowboy_req:req() +> * State = any() +> +> Perform any necessary cleanup of the state. +> +> This callback will typically unregister from any event manager +> or message queue it registered to in `init/3`. +> +> This callback should release any resource currently in use, +> clear any active timer and reset the process to its original +> state, as it might be reused for future requests sent on the +> same connection. diff --git a/manual/cowboy_middleware.md b/manual/cowboy_middleware.md new file mode 100644 index 0000000..dd28ff8 --- /dev/null +++ b/manual/cowboy_middleware.md @@ -0,0 +1,56 @@ +cowboy_middleware +================= + +The `cowboy_middleware` behaviour defines the interface used +by Cowboy middleware modules. + +Middlewares process the request sequentially in the order they +are configured. + +Types +----- + +### env() = [{atom(), any()}] + +> The environment variable. +> +> One is created for every request. It is passed to each +> middleware module executed and subsequently returned, +> optionally with its contents modified. + +Callbacks +--------- + +### execute(Req, Env) + -> {ok, Req, Env} + | {suspend, Module, Function, Args} + | {halt, Req} + | {error, StatusCode, Req} + +> Types: +> * Req = cowboy_req:req() +> * Env = env() +> * Module = module() +> * Function = atom() +> * Args = [any()] +> * StatusCode = cowboy:http_status() +> +> Execute the middleware. +> +> The `ok` return value indicates that everything went well +> and that Cowboy should continue processing the request. A +> response may or may not have been sent. +> +> The `suspend` return value will hibernate the process until +> an Erlang message is received. Note that when resuming, any +> previous stacktrace information will be gone. +> +> The `halt` return value stops Cowboy from doing any further +> processing of the request, even if there are middlewares +> that haven't been executed yet. The connection may be left +> open to receive more requests from the client. +> +> The `error` return value sends an error response identified +> by the `StatusCode` and then proceeds to terminate the +> connection. Middlewares that haven't been executed yet +> will not be called. diff --git a/manual/cowboy_protocol.md b/manual/cowboy_protocol.md new file mode 100644 index 0000000..86aee9d --- /dev/null +++ b/manual/cowboy_protocol.md @@ -0,0 +1,65 @@ +cowboy_protocol +=============== + +The `cowboy_protocol` module implements HTTP/1.1 and HTTP/1.0 +as a Ranch protocol. + +Types +----- + +### opts() = [{compress, boolean()} + | {env, cowboy_middleware:env()} + | {max_empty_lines, non_neg_integer()} + | {max_header_name_length, non_neg_integer()} + | {max_header_value_length, non_neg_integer()} + | {max_headers, non_neg_integer()} + | {max_keepalive, non_neg_integer()} + | {max_request_line_length, non_neg_integer()} + | {middlewares, [module()]} + | {onrequest, cowboy:onrequest_fun()} + | {onresponse, cowboy:onresponse_fun()} + | {timeout, timeout()}] + +> Configuration for the HTTP protocol handler. +> +> This configuration is passed to Cowboy when starting listeners +> using `cowboy:start_http/4` or `cowboy:start_https/4` functions. +> +> It can be updated without restarting listeners using the +> Ranch functions `ranch:get_protocol_options/1` and +> `ranch:set_protocol_options/2`. + +Option descriptions +------------------- + +The default value is given next to the option name. + + - compress (false) + - When enabled, Cowboy will attempt to compress the response body. + - env ([{listener, Ref}]) + - Initial middleware environment. + - max_empty_lines (5) + - Maximum number of empty lines before a request. + - max_header_name_length (64) + - Maximum length of header names. + - max_header_value_length (4096) + - Maximum length of header values. + - max_headers (100) + - Maximum number of headers allowed per request. + - max_keepalive (100) + - Maximum number of requests allowed per connection. + - max_request_line_length (4096) + - Maximum length of the request line. + - middlewares ([cowboy_router, cowboy_handler]) + - List of middlewares to execute for every requests. + - onrequest (undefined) + - Fun called every time a request is received. + - onresponse (undefined) + - Fun called every time a response is sent. + - timeout (5000) + - Time in ms with no requests before Cowboy closes the connection. + +Exports +------- + +None. diff --git a/manual/cowboy_req.md b/manual/cowboy_req.md new file mode 100644 index 0000000..f10120a --- /dev/null +++ b/manual/cowboy_req.md @@ -0,0 +1,597 @@ +cowboy_req +========== + +The `cowboy_req` module provides functions to access, manipulate +and respond to requests. + +The functions in this module follow patterns for their return types, +based on the kind of function. + + * access: `{Value, Req}` + * action: `{Result, Req} | {Result, Value, Req} | {error, atom()}` + * modification: `Req` + * question: `boolean()` + +The only exception is the `chunk/2` function which may return `ok`. + +Whenever `Req` is returned, you must use this returned value and +ignore any previous you may have had. This value contains various +state informations which are necessary for Cowboy to do some lazy +evaluation or cache results where appropriate. + +Types +----- + +### cookie_opts() = [{max_age, non_neg_integer()} + | {domain, binary()} | {path, binary()} + | {secure, boolean()} | {http_only, boolean()}] + +> Cookie options. + +### req() - opaque to the user + +> The `Req` object. +> +> All functions in this module receive a `Req` as argument, +> and most of them return a new object labelled `Req2` in +> the function descriptions below. + +Request related exports +----------------------- + +### binding(Name, Req) -> binding(Name, Req, undefined) +### binding(Name, Req, Default) -> {Value, Req2} + +> Types: +> * Name = atom() +> * Default = any() +> * Value = binary() | Default +> +> Return the value for the given binding. + +### bindings(Req) -> {[{Name, Value}], Req2} + +> Types: +> * Name = atom() +> * Value = binary() +> +> Return all bindings. + +### cookie(Name, Req) -> cookie(Name, Req, undefined) +### cookie(Name, Req, Default) -> {Value, Req2} + +> Types: +> * Name = binary() +> * Default = any() +> * Value = binary() | Default +> +> Return the value for the given cookie. +> +> Cookie names are case sensitive. + +### cookies(Req) -> {[{Name, Value}], Req2} + +> Types: +> * Name = binary() +> * Value = binary() +> +> Return all cookies. + +### header(Name, Req) -> header(Name, Req, undefined) +### header(Name, Req, Default) -> {Value, Req2} + +> Types: +> * Name = binary() +> * Default = any() +> * Value = binary() | Default +> +> Return the value for the given header. +> +> While header names are case insensitive, this function expects +> the name to be a lowercase binary. + +### headers(Req) -> {Headers, Req2} + +> Types: +> * Headers = cowboy:http_headers() +> +> Return all headers. + +### host(Req) -> {Host, Req2} + +> Types: +> * Host = binary() +> +> Return the requested host. + +### host_info(Req) -> {HostInfo, Req2} + +> Types: +> * HostInfo = cowboy_router:tokens() | undefined +> +> Return the extra tokens from matching against `...` during routing. + +### host_url(Req) -> {HostURL, Req2} + +> Types: +> * HostURL = binary() | undefined +> +> Return the requested URL excluding the path component. +> +> This function will always return `undefined` until the +> `cowboy_router` middleware has been executed. This includes +> the `onrequest` hook. + +### meta(Name, Req) -> meta(Name, Req, undefined) +### meta(Name, Req, Default) -> {Value, Req2} + +> Types: +> * Name = atom() +> * Default = any() +> * Value = any() +> +> Return metadata about the request. + +### method(Req) -> {Method, Req2} + +> Types: +> * Method = binary() +> +> Return the method. +> +> Methods are case sensitive. Standard methods are always uppercase. + +### parse_header(Name, Req) -> +### parse_header(Name, Req, Default) -> {ok, ParsedValue, Req2} + | {undefined, Value, Req2} | {error, badarg} + +> Types: +> * Name = binary() +> * Default = any() +> * ParsedValue - see below +> * Value = any() +> +> Parse the given header. +> +> While header names are case insensitive, this function expects +> the name to be a lowercase binary. +> +> The `parse_header/2` function will call `parser_header/3` with a +> different default value depending on the header being parsed. The +> following table summarizes the default values used. +> +> | Header name | Default value | +> | ----------------- | ------------------ | +> | transfer-encoding | `[<<"identity">>]` | +> | Any other header | `undefined` | +> +> The parsed value differs depending on the header being parsed. The +> following table summarizes the different types returned. +> +> | Header name | Type | +> | ---------------------- | ------------------------------------------------- | +> | accept | `[{{Type, SubType, Params}, Quality, AcceptExt}]` | +> | accept-charset | `[{Charset, Quality}]` | +> | accept-encoding | `[{Encoding, Quality}]` | +> | accept-language | `[{LanguageTag, Quality}]` | +> | authorization | `{AuthType, Credentials}` | +> | content-length | `non_neg_integer()` | +> | content-type | `{Type, SubType, Params}` | +> | cookie | `[{binary(), binary()}]` | +> | expect | `[Expect | {Expect, ExpectValue, Params}]` | +> | if-match | `'*' | [{weak | strong, OpaqueTag}]` | +> | if-modified-since | `calendar:datetime()` | +> | if-none-match | `'*' | [{weak | strong, OpaqueTag}]` | +> | if-unmodified-since | `calendar:datetime()` | +> | range | `{Unit, [Range]}` | +> | sec-websocket-protocol | `[binary()]` | +> | transfer-encoding | `[binary()]` | +> | upgrade | `[binary()]` | +> | x-forwarded-for | `[binary()]` | +> +> Types for the above table: +> * Type = SubType = Charset = Encoding = LanguageTag = binary() +> * AuthType = Expect = OpaqueTag = Unit = binary() +> * Params = [{binary(), binary()}] +> * Quality = 0..1000 +> * AcceptExt = [{binary(), binary()} | binary()] +> * Credentials - see below +> * Range = {non_neg_integer(), non_neg_integer() | infinity} | neg_integer() +> +> 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. +> +> The headers accept, accept-encoding and cookie headers can return +> an empty list. Others will return `{error, badarg}` if the header +> value is empty. +> +> The authorization header parsing code currently only supports basic +> HTTP authentication. The `Credentials` type is thus `{Username, Password}` +> with `Username` and `Password` being `binary()`. +> +> The range header value `Range` can take three forms: +> * `{From, To}`: from `From` to `To` units +> * `{From, infinity}`: everything after `From` units +> * `-Final`: the final `Final` units +> +> An `undefined` tuple will be returned if Cowboy doesn't know how +> to parse the requested header. + +### path(Req) -> {Path, Req2} + +> Types: +> * Path = binary() +> +> Return the requested path. + +### path_info(Req) -> {PathInfo, Req2} + +> Types: +> * PathInfo = cowboy_router:tokens() | undefined +> +> Return the extra tokens from matching against `...` during routing. + +### peer(Req) -> {Peer, Req2} + +> Types: +> * Peer = {inet:ip_address(), inet:port_number()} +> +> Return the client's IP address and port number. + +### port(Req) -> {Port, Req2} + +> Types: +> * Port = inet:port_number() +> +> Return the request's port. +> +> The port returned by this function is obtained by parsing +> the host header. It may be different than the actual port +> the client used to connect to the Cowboy server. + +### qs(Req) -> {QueryString, Req2} + +> Types: +> * QueryString = binary() +> +> Return the request's query string. + +### qs_val(Name, Req) -> qs_val(Name, Req, undefined) +### qs_val(Name, Req, Default) -> {Value, Req2} + +> Types: +> * Name = binary() +> * Default = any() +> * Value = binary() | true +> +> Return a value from the request's query string. +> +> The value `true` will be returned when the name was found +> in the query string without an associated value. + +### qs_vals(Req) -> {[{Name, Value}], Req2} + +> Types: +> * Name = binary() +> * Value = binary() | true +> +> Return the request's query string as a list of tuples. +> +> The value `true` will be returned when the name was found +> in the query string without an associated value. + +### set_meta(Name, Value, Req) -> Req2 + +> Types: +> * Name = atom() +> * Value = any() +> +> Set metadata about the request. +> +> An existing value will be overwritten. + +### url(Req) -> {URL, Req2} + +> Types: +> * URL = binary() | undefined +> +> Return the requested URL. +> +> This function will always return `undefined` until the +> `cowboy_router` middleware has been executed. This includes +> the `onrequest` hook. + +### version(Req) -> {Version, Req2} + +> Types: +> * Version = cowboy:http_version() +> +> Return the HTTP version used for this request. + +Request body related exports +---------------------------- + +### body(Req) -> body(8000000, Req) +### body(MaxLength, Req) -> {ok, Data, Req2} | {error, Reason} + +> Types: +> * MaxLength = non_neg_integer() | infinity +> * Data = binary() +> * Reason = chunked | badlength | atom() +> +> Return the request body. +> +> This function will return `{error, chunked}` if the request +> body was sent using the chunked transfer-encoding. It will +> also return `{error, badlength}` if the length of the body +> exceeds the given `MaxLength`, which is 8MB by default. + +### body_length(Req) -> {Length, Req2} + +> Types: +> * Length = non_neg_integer() | undefined +> +> Return the length of the request body. +> +> The length will only be returned if the request does not +> use any transfer-encoding and if the content-length header +> is present. + +### body_qs(Req) -> body_qs(16000, Req) +### body_qs(MaxLength, Req) -> {ok, [{Name, Value}], Req2} | {error, Reason} + +> Types: +> * MaxLength = non_neg_integer() | infinity +> * Name = binary() +> * Value = binary() | true +> * Reason = chunked | badlength | atom() +> +> Return the request body as a list of tuples. +> +> This function will parse the body assuming the content-type +> application/x-www-form-urlencoded, commonly used for the +> query string. +> +> This function will return `{error, chunked}` if the request +> body was sent using the chunked transfer-encoding. It will +> also return `{error, badlength}` if the length of the body +> exceeds the given `MaxLength`, which is 16KB by default. + +### has_body(Req) -> boolean() + +> Return whether the request has a body. + +### init_stream(TransferDecode, TransferState, ContentDecode, Req) -> {ok, Req2} + +> Types: +> * TransferDecode = fun((Encoded, TransferState) -> OK | More | Done | {error, Reason}) +> * Encoded = Decoded = Rest = binary() +> * TransferState = any() +> * OK = {ok, Decoded, Rest, TransferState} +> * More = more | {more, Length, Decoded, TransferState} +> * Done = {done, TotalLength, Rest} | {done, Decoded, TotalLength, Rest} +> * Length = TotalLength = non_neg_integer() +> * ContentDecode = fun((Encoded) -> {ok, Decoded} | {error, Reason}) +> * Reason = atom() +> +> Initialize streaming of the request body. +> +> This function can be used to specify what function to use +> for decoding the request body, generally specified in the +> transfer-encoding and content-encoding request headers. +> +> Cowboy will properly handle chunked transfer-encoding by +> default. You do not need to call this function if you do +> not need to decode other encodings, `stream_body/{1,2}` +> will perform all the required initialization when it is +> called the first time. + +### skip_body(Req) -> {ok, Req2} | {error, Reason} + +> Types: +> * Reason = atom() +> +> Skip the request body. +> +> This function will skip the body even if it was partially +> read before. + +### stream_body(Req) -> stream_body(1000000, Req) +### stream_body(MaxSegmentSize, Req) -> {ok, Data, Req2} + | {done, Req2} | {error, Reason} + +> Types: +> * MaxSegmentSize = non_neg_integer() +> * Data = binary() +> * Reason = atom() +> +> Stream the request body. +> +> This function will return a segment of the request body +> with a size of up to `MaxSegmentSize`, or 1MB by default. +> This function can be called repeatedly until a `done` tuple +> is returned, indicating the body has been fully received. +> +> Cowboy will properly handle chunked transfer-encoding by +> default. If any other transfer-encoding or content-encoding +> has been used for the request, custom decoding functions +> can be used. They must be specified using `init_stream/4`. +> +> After the body has been streamed fully, Cowboy will remove +> the transfer-encoding header from the `Req` object, and add +> the content-length header if it wasn't already there. + +Response related exports +------------------------ + +### chunk(Data, Req) -> ok | {error, Reason} + +> Types: +> * Data = iodata() +> * Reason = atom() +> +> Send a chunk of data. +> +> This function should be called as many times as needed +> to send data chunks after calling `chunked_reply/{2,3}`. +> +> When the method is HEAD, no data will actually be sent. +> +> If the request uses HTTP/1.0, the data is sent directly +> without wrapping it in an HTTP/1.1 chunk, providing +> compatibility with older clients. + +### chunked_reply(StatusCode, Req) -> chunked_reply(StatusCode, [], Req) +### chunked_reply(StatusCode, Headers, Req) -> {ok, Req2} + +> Types: +> * StatusCode = cowboy:http_status() +> * Headers = cowboy:http_headers() +> +> Send a response using chunked transfer-encoding. +> +> This function effectively sends the response status line +> and headers to the client. +> +> This function will not send any body set previously. After +> this call the handler must use the `chunk/2` function +> repeatedly to send the body in as many chunks as needed. +> +> If the request uses HTTP/1.0, the data is sent directly +> without wrapping it in an HTTP/1.1 chunk, providing +> compatibility with older clients. + +### delete_resp_header(Name, Req) -> Req2 + +> Types: +> * Name = binary() +> +> Delete the given response header. +> +> While header names are case insensitive, this function expects +> the name to be a lowercase binary. + +### has_resp_body(Req) -> boolean() + +> Return whether a response body has been set. +> +> This function will return false if a response body has +> been set with a length of 0. + +### has_resp_header(Name, Req) -> boolean() + +> Types: +> * Name = binary() +> +> Return whether the given response header has been set. +> +> While header names are case insensitive, this function expects +> the name to be a lowercase binary. + +### reply(StatusCode, Req) -> reply(StatusCode, [], Req) +### reply(StatusCode, Headers, Req) - see below +### reply(StatusCode, Headers, Body, Req) -> {ok, Req2} + +> Types: +> * StatusCode = cowboy:http_status() +> * Headers = cowboy:http_headers() +> * Body = iodata() +> +> Send a response. +> +> This function effectively sends the response status line, +> headers and body to the client, in a single send function +> call. +> +> The `reply/2` and `reply/3` functions will send the body +> set previously, if any. The `reply/4` function overrides +> any body set previously and sends `Body` instead. +> +> If a body function was set, and `reply/2` or `reply/3` was +> used, it will be called before returning. +> +> No more data can be sent to the client after this function +> returns. + +### set_resp_body(Body, Req) -> Req2 + +> Types: +> * Body = iodata() +> +> Set a response body. +> +> This body will not be sent if `chunked_reply/{2,3}` or +> `reply/4` is used, as they override it. + +### set_resp_body_fun(Fun, Req) -> Req2 +### set_resp_body_fun(Length, Fun, Req) -> Req2 + +> Types: +> * Fun = fun((Socket, Transport) -> ok) +> * Socket = inet:socket() +> * Transport = module() +> * Length = non_neg_integer() +> +> Set a fun for sending the response body. +> +> If a `Length` is provided, it will be sent in the +> content-length header in the response. It is recommended +> to set the length if it can be known in advance. +> +> This function will only be called if the response is sent +> using the `reply/2` or `reply/3` function. +> +> The fun will receive the Ranch `Socket` and `Transport` as +> arguments. Only send and sendfile operations are supported. + +### set_resp_body_fun(chunked, Fun, Req) -> Req2 + +> Types: +> * Fun = fun((ChunkFun) -> ok) +> * ChunkFun = fun((iodata()) -> ok | {error, atom()}) +> +> Set a fun for sending the response body using chunked transfer-encoding. +> +> This function will only be called if the response is sent +> using the `reply/2` or `reply/3` function. +> +> The fun will receive another fun as argument. This fun is to +> be used to send chunks in a similar way to the `chunk/2` function, +> except the fun only takes one argument, the data to be sent in +> the chunk. + +### set_resp_cookie(Name, Value, Opts, Req) -> Req2 + +> Types: +> * Name = iodata() +> * Value = iodata() +> * Opts = cookie_opts() +> +> Set a cookie in the response. +> +> Cookie names are case sensitive. + +### set_resp_header(Name, Value, Req) -> Req2 + +> Types: +> * Name = binary() +> * Value = iodata() +> +> Set a response header. +> +> You should use `set_resp_cookie/4` instead of this function +> to set cookies. + +Misc. exports +------------- + +### compact(Req) -> Req2 + +> Remove any non-essential data from the `Req` object. +> +> Long-lived connections usually only need to manipulate the +> `Req` object at initialization. Compacting allows saving up +> memory by discarding extraneous information. diff --git a/manual/cowboy_rest.md b/manual/cowboy_rest.md new file mode 100644 index 0000000..4d5862a --- /dev/null +++ b/manual/cowboy_rest.md @@ -0,0 +1,552 @@ +cowboy_rest +=========== + +The `cowboy_rest` module implements REST semantics on top of +the HTTP protocol. + +This module cannot be described as a behaviour due to most of +the callbacks it defines being optional. It has the same +semantics as a behaviour otherwise. + +The only mandatory callback is `init/3`, needed to perform +the protocol upgrade. + +Types +----- + +None. + +Meta values +----------- + +### charset + +> Type: binary() +> +> Negotiated charset. +> +> This value may not be defined if no charset was negotiated. + +### language + +> Type: binary() +> +> Negotiated language. +> +> This value may not be defined if no language was negotiated. + +### media_type + +> Type: {binary(), binary(), '*' | [{binary(), binary()}]} +> +> Negotiated media-type. +> +> The media-type is the content-type, excluding the charset. +> +> This value is always defined after the call to +> `content_types_provided/2`. + +Callbacks +--------- + +### init({TransportName, ProtocolName}, Req, Opts) + -> {upgrade, protocol, cowboy_rest} + | {upgrade, protocol, cowboy_rest, Req, Opts} + +> Types: +> * TransportName = tcp | ssl | atom() +> * ProtocolName = http | atom() +> * Req = cowboy_req:req() +> * Opts = any() +> +> Upgrade the protocol to `cowboy_rest`. +> +> This is the only mandatory callback. + +### rest_init(Req, Opts) -> {ok, Req, State} + +> Types: +> * Req = cowboy_req:req() +> * Opts = any() +> * State = any() +> +> Initialize the state for this request. + +### rest_terminate(Req, State) -> ok + +> Types: +> * Req = cowboy_req:req() +> * State = any() +> +> Perform any necessary cleanup of the state. +> +> This callback should release any resource currently in use, +> clear any active timer and reset the process to its original +> state, as it might be reused for future requests sent on the +> same connection. + +### Callback(Req, State) -> {Value, Req, State} | {halt, Req, State} + +> Types: +> * Callback - one of the REST callbacks described below +> * Req = cowboy_req:req() +> * State = any() +> * Value - see the REST callbacks description below +> +> Please see the REST callbacks description below for details +> on the `Value` type, the default value if the callback is +> not defined, and more general information on when the +> callback is called and what its intended use is. +> +> The `halt` tuple can be returned to stop REST processing. +> It is up to the resource code to send a reply before that, +> otherwise a `204 No Content` will be sent. + +REST callbacks description +-------------------------- + +### allowed_methods + +> * Methods: all +> * Value type: [binary()] +> * Default value: [<<"GET">>, <<"HEAD">>, <<"OPTIONS">>] +> +> Return the list of allowed methods. +> +> Methods are case sensitive. Standard methods are always uppercase. + +### allow_missing_post + +> * Methods: POST +> * Value type: boolean() +> * Default value: true +> +> Return whether POST is allowed when the resource doesn't exist. +> +> Returning `true` here means that a new resource will be +> created. The URL to the created resource should also be +> returned from the `AcceptResource` callback. + +### charsets_provided + +> * Methods: GET, HEAD, POST, PUT, PATCH, DELETE +> * Value type: [binary()] +> * Skip to the next step if undefined +> +> Return the list of charsets the resource provides. +> +> The list must be ordered in order of preference. +> +> If the accept-charset header was not sent, the first charset +> in the list will be selected. Otherwise Cowboy will select +> the most appropriate charset from the list. +> +> The chosen charset will be set in the `Req` object as the meta +> value `charset`. +> +> While charsets are case insensitive, this callback is expected +> to return them as lowercase binary. + +### content_types_accepted + +> * Methods: POST, PUT, PATCH +> * No default +> +> Types: +> * Value = [{binary() | {Type, SubType, Params}, AcceptResource}] +> * Type = SubType = binary() +> * Params = '*' | [{binary(), binary()}] +> * AcceptResource = atom() +> +> Return the list of content-types the resource accepts. +> +> The list must be ordered in order of preference. +> +> Each content-type can be given either as a binary string or as +> a tuple containing the type, subtype and parameters. +> +> 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. +> +> This function will be called for POST, PUT and PATCH requests. +> It is entirely possible to define different callbacks for different +> methods if the handling of the request differs. Simply verify +> what the method is with `cowboy_req:method/1` and return a +> different list for each methods. +> +> The `AcceptResource` value is the name of the callback that will +> be called if the content-type matches. It is defined as follow. +> +> * Value type: true | {true, URL} | false +> * No default +> +> Process the request body. +> +> This function should create or update the resource with the +> information contained in the request body. This information +> may be full or partial depending on the request method. +> +> If the request body was processed successfully, `true` or +> `{true, URL}` may be returned. If an URL is provided, the +> response will redirect the client to the location of the +> resource. +> +> If a response body must be sent, the appropriate media-type, charset +> and language can be retrieved using the `cowboy_req:meta/{2,3}` +> functions. The respective keys are `media_type`, `charset` +> and `language`. The body can be set using `cowboy_req:set_resp_body/2`. + +### content_types_provided + +> * Methods: GET, HEAD +> * Default value: [{{<<"text">>, <<"html">>, '*'}, to_html}] +> +> Types: +> * Value = [{binary() | {Type, SubType, Params}, ProvideResource}] +> * Type = SubType = binary() +> * Params = '*' | [{binary(), binary()}] +> * ProvideResource = atom() +> +> Return the list of content-types the resource provides. +> +> The list must be ordered in order of preference. +> +> Each content-type can be given either as a binary string or as +> a tuple containing the type, subtype and parameters. +> +> 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. +> +> The `ProvideResource` value is the name of the callback that will +> be called if the content-type matches. It is defined as follow. +> +> * Value type: iodata() | {stream, Fun} | {stream, Len, Fun} | {chunked, ChunkedFun} +> * No default +> +> Return the response body. +> +> The response body may be provided directly or through a fun. +> If a fun tuple is returned, the appropriate `set_resp_body_fun` +> function will be called. Please refer to the documentation for +> these functions for more information about the types. +> +> The call to this callback happens a good time after the call to +> `content_types_provided/2`, when it is time to start rendering +> the response body. + +### delete_completed + +> * Methods: DELETE +> * Value type: boolean() +> * Default value: true +> +> Return whether the delete action has been completed. +> +> This function should return `false` if there is no guarantee +> that the resource gets deleted immediately from the system, +> including from any internal cache. +> +> When this function returns `false`, a `202 Accepted` +> response will be sent instead of a `200 OK` or `204 No Content`. + +### delete_resource + +> * Methods: DELETE +> * Value type: boolean() +> * Default value: false +> +> Delete the resource. +> +> The value returned indicates if the action was successful, +> regardless of whether the resource is immediately deleted +> from the system. + +### expires + +> * Methods: GET, HEAD +> * Value type: calendar:datetime() | undefined +> * Default value: undefined +> +> Return the date of expiration of the resource. +> +> This date will be sent as the value of the expires header. + +### forbidden + +> * Methods: all +> * Value type: boolean() +> * Default value: false +> +> Return whether access to the resource is forbidden. +> +> A `403 Forbidden` response will be sent if this +> function returns `true`. This status code means that +> access is forbidden regardless of authentication, +> and that the request shouldn't be repeated. + +### generate_etag + +> * Methods: GET, HEAD, POST, PUT, PATCH, DELETE +> * 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. + +### is_authorized + +> * Methods: all +> * Value type: true | {false, AuthHeader} +> * Default value: true +> +> Return whether the user is authorized to perform the action. +> +> This function should be used to perform any necessary +> authentication of the user before attempting to perform +> any action on the resource. +> +> If the authentication fails, the value returned will be sent +> as the value for the www-authenticate header in the +> `401 Unauthorized` response. + +### is_conflict + +> * Methods: PUT +> * Value type: boolean() +> * Default value: false +> +> Return whether the put action results in a conflict. +> +> A `409 Conflict` response will be sent if this function +> returns `true`. + +### known_content_type + +> * Methods: all +> * Value type: boolean() +> * Default value: true +> +> Return whether the content-type is known. +> +> This function determines if the server understands the +> content-type, regardless of its use by the resource. + +### known_methods + +> * Methods: all +> * Value type: [binary()] +> * Default value: [<<"GET">>, <<"HEAD">>, <<"POST">>, <<"PUT">>, <<"PATCH">>, <<"DELETE">>, <<"OPTIONS">>] +> +> Return the list of known methods. +> +> The full list of methods known by the server should be +> returned, regardless of their use in the resource. +> +> The default value lists the methods Cowboy knows and +> implement in `cowboy_rest`. +> +> Methods are case sensitive. Standard methods are always uppercase. + +### languages_provided + +> * Methods: GET, HEAD, POST, PUT, PATCH, DELETE +> * Value type: [binary()] +> * Skip to the next step if undefined +> +> Return the list of languages the resource provides. +> +> The list must be ordered in order of preference. +> +> If the accept-language header was not sent, the first language +> in the list will be selected. Otherwise Cowboy will select +> the most appropriate language from the list. +> +> The chosen language will be set in the `Req` object as the meta +> value `language`. +> +> While languages are case insensitive, this callback is expected +> to return them as lowercase binary. + +### last_modified + +> * Methods: GET, HEAD, POST, PUT, PATCH, DELETE +> * Value type: calendar:datetime() +> * Default value: undefined +> +> Return the date of last modification of the resource. +> +> This date will be used to test against the if-modified-since +> and if-unmodified-since headers, and sent as the last-modified +> header in the response of GET and HEAD requests. + +### malformed_request + +> * Methods: all +> * Value type: boolean() +> * Default value: false +> +> Return whether the request is malformed. +> +> Cowboy has already performed all the necessary checks +> by the time this function is called, so few resources +> are expected to implement it. +> +> The check is to be done on the request itself, not on +> the request body, which is processed later. + +### moved_permanently + +> * Methods: GET, HEAD, POST, PUT, PATCH, DELETE +> * Value type: {true, URL} | false +> * Default value: false +> +> Return whether the resource was permanently moved. +> +> If it was, its new URL is also returned and sent in the +> location header in the response. + +### moved_temporarily + +> * Methods: GET, HEAD, POST, PATCH, DELETE +> * Value type: {true, URL} | false +> * Default value: false +> +> Return whether the resource was temporarily moved. +> +> If it was, its new URL is also returned and sent in the +> location header in the response. + +### multiple_choices + +> * Methods: GET, HEAD, POST, PUT, PATCH, DELETE +> * Value type: boolean() +> * Default value: false +> +> Return whether there are multiple representations of the resource. +> +> This function should be used to inform the client if there +> are different representations of the resource, for example +> different content-type. If this function returns `true`, +> the response body should include information about these +> different representations using `cowboy_req:set_resp_body/2`. +> The content-type of the response should be the one previously +> negociated and that can be obtained by calling +> `cowboy_req:meta(media_type, Req)`. + +### options + +> * Methods: OPTIONS +> * Value type: ok +> * Default value: ok +> +> Handle a request for information. +> +> The response should inform the client the communication +> options available for this resource. +> +> By default, Cowboy will send a `200 OK` response with the +> allow header set. + +### previously_existed + +> * Methods: GET, HEAD, POST, PATCH, DELETE +> * Value type: boolean() +> * Default value: false +> +> Return whether the resource existed previously. + +### resource_exists + +> * Methods: GET, HEAD, POST, PUT, PATCH, DELETE +> * Value type: boolean() +> * Default value: true +> +> Return whether the resource exists. +> +> If it exists, conditional headers will be tested before +> attempting to perform the action. Otherwise, Cowboy will +> check if the resource previously existed first. + +### service_available + +> * Methods: all +> * Value type: boolean() +> * Default value: true +> +> Return whether the service is available. +> +> This function can be used to test that all relevant backend +> systems are up and able to handle requests. +> +> A `503 Service Unavailable` response will be sent if this +> function returns `false`. + +### uri_too_long + +> * Methods: all +> * Value type: boolean() +> * Default value: false +> +> Return whether the requested URI is too long. +> +> Cowboy has already performed all the necessary checks +> by the time this function is called, so few resources +> are expected to implement it. +> +> A `414 Request-URI Too Long` response will be sent if this +> function returns `true`. + +### valid_content_headers + +> * Methods: all +> * Value type: boolean() +> * Default value: true +> +> Return whether the content-* headers are valid. +> +> This also applies to the transfer-encoding header. This +> function must return `false` for any unknown content-* +> headers, or if the headers can't be understood. The +> function `cowboy_req:parse_header/2` can be used to +> quickly check the headers can be parsed. +> +> A `501 Not Implemented` response will be sent if this +> function returns `false`. + +### valid_entity_length + +> * Methods: all +> * Value type: boolean() +> * Default value: true +> +> Return whether the request body length is within acceptable boundaries. +> +> A `413 Request Entity Too Large` response will be sent if this +> function returns `false`. + +### variances + +> * Methods: GET, HEAD, POST, PUT, PATCH, DELETE +> * Value type: [binary()] +> * Default value: [] +> +> Return the list of headers that affect the representation of the resource. +> +> These request headers return the same resource but with different +> parameters, like another language or a different content-type. +> +> Cowboy will automatically add the accept, accept-language and +> accept-charset headers to the list if the respective functions +> were defined in the resource. +> +> This operation is performed right before the `resource_exists/2` +> callback. All responses past that point will contain the vary +> header which holds this list. diff --git a/manual/cowboy_router.md b/manual/cowboy_router.md new file mode 100644 index 0000000..1c6dc04 --- /dev/null +++ b/manual/cowboy_router.md @@ -0,0 +1,68 @@ +cowboy_router +============= + +The `cowboy_router` middleware maps the requested host and +path to the handler to be used for processing the request. +It uses the dispatch rules compiled from the routes given +to the `compile/1` function for this purpose. It adds the +handler name and options to the environment as the values +`handler` and `handler_opts` respectively. + +Environment input: + * dispatch = dispatch_rules() + +Environment output: + * handler = module() + * handler_opts = any() + +Types +----- + +### bindings() = [{atom(), binary()}] + +> List of bindings found during routing. + +### constraints() = [IntConstraint | FunConstraint] + +> Types: +> * IntConstraint = {atom(), int} +> * FunConstraint = {atom(), function, Fun} +> * Fun = fun((binary()) -> true | {true, any()} | false) +> +> List of constraints to apply to the bindings. +> +> The int constraint will convert the binding to an integer. +> The fun constraint allows writing custom code for checking +> the bindings. Returning a new value from that fun allows +> replacing the current binding with a new value. + +### dispatch_rules() - opaque to the user + +> Rules for dispatching request used by Cowboy. + +### routes() = [{Host, Paths} | {Host, constraints(), Paths}] + +> Types: +> * Host = Path = '_' | iodata() +> * Paths = [{Path, Handler, Opts} | {Path, constraints(), Handler, Opts}] +> * Handler = module() +> * Opts = any() +> +> Human readable list of routes mapping hosts and paths to handlers. +> +> The syntax for routes is defined in the user guide. + +### tokens() = [binary()] + +> List of host_info and path_info tokens found during routing. + +Exports +------- + +### compile(Routes) -> Dispatch + +> Types: +> * Routes = routes() +> * Dispatch = dispatch_rules() +> +> Compile the routes for use by Cowboy. diff --git a/manual/cowboy_sub_protocol.md b/manual/cowboy_sub_protocol.md new file mode 100644 index 0000000..a8ecae1 --- /dev/null +++ b/manual/cowboy_sub_protocol.md @@ -0,0 +1,34 @@ +cowboy_sub_protocol +=================== + +The `cowboy_sub_protocol` behaviour defines the interface used +by modules that implement a protocol on top of HTTP. + +Types +----- + +None. + +Callbacks +--------- + +### upgrade(Req, Env, Handler, Opts) + -> {ok, Req, Env} + | {suspend, Module, Function, Args} + | {halt, Req} + | {error, StatusCode, Req} + +> Types: +> * Req = cowboy_req:req() +> * Env = env() +> * Handler = module() +> * Opts = any() +> * Module = module() +> * Function = atom() +> * Args = [any()] +> * StatusCode = cowboy:http_status() +> +> Upgrade the protocol. +> +> Please refer to the `cowboy_middleware` manual for a +> description of the return values. diff --git a/manual/cowboy_websocket.md b/manual/cowboy_websocket.md new file mode 100644 index 0000000..ae3ca1b --- /dev/null +++ b/manual/cowboy_websocket.md @@ -0,0 +1,34 @@ +cowboy_websocket +================ + +The `cowboy_websocket` module implements the Websocket protocol. + +The callbacks for websocket handlers are defined in the manual +for the `cowboy_websocket_handler` behaviour. + +Types +----- + +### close_code() = 1000..4999 + +> Reason for closing the connection. + +### frame() = close | ping | pong + | {text | binary | close | ping | pong, iodata()} + | {close, close_code(), iodata()} + +> Frames that can be sent to the client. + +Meta values +----------- + +### websocket_version + +> Type: 7 | 8 | 13 +> +> The version of the Websocket protocol being used. + +Exports +------- + +None. diff --git a/manual/cowboy_websocket_handler.md b/manual/cowboy_websocket_handler.md new file mode 100644 index 0000000..f0480b1 --- /dev/null +++ b/manual/cowboy_websocket_handler.md @@ -0,0 +1,131 @@ +cowboy_websocket_handler +======================== + +The `cowboy_websocket_handler` behaviour defines the interface used +by Websocket handlers. + +The `init/3` and `websocket_init/3` callbacks will always be called, +followed by zero or more calls to `websocket_handle/3` and +`websocket_info/3`. The `websocket_terminate/3` will always +be called last. + +Types +----- + +None. + +Callbacks +--------- + +### init({TransportName, ProtocolName}, Req, Opts) + -> {upgrade, protocol, cowboy_websocket} + | {upgrade, protocol, cowboy_websocket, Req, Opts} + +> Types: +> * TransportName = tcp | ssl | atom() +> * ProtocolName = http | atom() +> * Req = cowboy_req:req() +> * Opts = any() +> +> Upgrade the protocol to `cowboy_websocket`. + +### websocket_init(TransportName, Req, Opts) + -> {ok, Req, State} + | {ok, Req, State, hibernate} + | {ok, Req, State, Timeout} + | {ok, Req, State, Timeout, hibernate} + | {shutdown, Req} + +> Types: +> * TransportName = tcp | ssl | atom() +> * Req = cowboy_req:req() +> * Opts = any() +> * State = any() +> * Timeout = timeout() +> +> Initialize the state for this session. +> +> This function is called before the upgrade to Websocket occurs. +> It can be used to negotiate Websocket protocol extensions +> with the client. It will typically be used to register this process +> to an event manager or a message queue in order to receive +> the messages the handler wants to process. +> +> The connection will stay up for a duration of up to `Timeout` +> milliseconds after it last received data from the socket, +> at which point it will stop and close the connection. +> By default this value is set to `infinity`. It is recommended +> to either set this value or ensure by any other mechanism +> that the handler will be closed after a certain period of +> inactivity. +> +> The `hibernate` option will hibernate the process until it +> starts receiving either data from the Websocket connection +> or Erlang messages. +> +> The `shutdown` return value can be used to close the connection +> before upgrading to Websocket. + +### websocket_handle(InFrame, Req, State) + -> {ok, Req, State} + | {ok, Req, State, hibernate} + | {reply, OutFrame | [OutFrame], Req, State} + | {reply, OutFrame | [OutFrame], Req, State, hibernate} + | {shutdown, Req, State} + +> Types: +> * InFrame = {text | binary | ping | pong, binary()} +> * Req = cowboy_req:req() +> * State = any() +> * OutFrame = cowboy_websocket:frame() +> +> Handle the data received from the Websocket connection. +> +> This function will be called every time data is received +> from the Websocket connection. +> +> The `shutdown` return value can be used to close the +> connection. A close reply will also result in the connection +> being closed. +> +> The `hibernate` option will hibernate the process until +> it receives new data from the Websocket connection or an +> Erlang message. + +### websocket_info(Info, Req, State) + -> {ok, Req, State} + | {ok, Req, State, hibernate} + | {reply, OutFrame | [OutFrame], Req, State} + | {reply, OutFrame | [OutFrame], Req, State, hibernate} + | {shutdown, Req, State} + +> Types: +> * Info = any() +> * Req = cowboy_req:req() +> * State = any() +> * OutFrame = cowboy_websocket:frame() +> +> Handle the Erlang message received. +> +> This function will be called every time an Erlang message +> has been received. The message can be any Erlang term. +> +> The `shutdown` return value can be used to close the +> connection. A close reply will also result in the connection +> being closed. +> +> The `hibernate` option will hibernate the process until +> it receives another message or new data from the Websocket +> connection. + +### websocket_terminate(Reason, Req, State) -> ok + +> Types: +> * Reason = {normal, shutdown | timeout} | {remote, closed} | {remote, cowboy_websocket:close_code(), binary()} | {error, badencoding | badframe | closed | atom()} +> * Req = cowboy_req:req() +> * State = any() +> +> Perform any necessary cleanup of the state. +> +> The connection will be closed and the process stopped right +> after this call. diff --git a/manual/toc.md b/manual/toc.md new file mode 100644 index 0000000..d05696e --- /dev/null +++ b/manual/toc.md @@ -0,0 +1,18 @@ +Cowboy Function Reference +========================= + +The function reference documents the public interface of Cowboy. + + * [The Cowboy Application](cowboy_app.md) + * [cowboy](cowboy.md) + * [cowboy_handler](cowboy_handler.md) + * [cowboy_http_handler](cowboy_http_handler.md) + * [cowboy_loop_handler](cowboy_loop_handler.md) + * [cowboy_middleware](cowboy_middleware.md) + * [cowboy_protocol](cowboy_protocol.md) + * [cowboy_req](cowboy_req.md) + * [cowboy_rest](cowboy_rest.md) + * [cowboy_router](cowboy_router.md) + * [cowboy_sub_protocol](cowboy_sub_protocol.md) + * [cowboy_websocket](cowboy_websocket.md) + * [cowboy_websocket_handler](cowboy_websocket_handler.md) diff --git a/rebar.config b/rebar.config index 7367e6d..bab6fa3 100644 --- a/rebar.config +++ b/rebar.config @@ -1,3 +1,3 @@ {deps, [ - {ranch, ".*", {git, "git://github.com/extend/ranch.git", "0.8.1"}} + {ranch, ".*", {git, "git://github.com/extend/ranch.git", "0.8.3"}} ]}. diff --git a/src/cowboy.app.src b/src/cowboy.app.src index 92dd124..e9cfcb8 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.4"}, + {vsn, "0.8.5"}, {modules, []}, {registered, [cowboy_clock, cowboy_sup]}, {applications, [ diff --git a/src/cowboy.erl b/src/cowboy.erl index 257172d..16445e1 100644 --- a/src/cowboy.erl +++ b/src/cowboy.erl @@ -17,25 +17,56 @@ -export([start_http/4]). -export([start_https/4]). +-export([start_spdy/4]). -export([stop_listener/1]). -export([set_env/3]). +-type http_headers() :: [{binary(), iodata()}]. +-export_type([http_headers/0]). + +-type http_status() :: non_neg_integer() | binary(). +-export_type([http_status/0]). + +-type http_version() :: 'HTTP/1.1' | 'HTTP/1.0'. +-export_type([http_version/0]). + +-type onrequest_fun() :: fun((Req) -> Req). +-export_type([onrequest_fun/0]). + +-type onresponse_fun() :: + fun((http_status(), http_headers(), iodata(), Req) -> Req). +-export_type([onresponse_fun/0]). + %% @doc Start an HTTP listener. --spec start_http(any(), non_neg_integer(), any(), any()) -> {ok, pid()}. +-spec start_http(ranch:ref(), non_neg_integer(), ranch_tcp:opts(), + cowboy_protocol:opts()) -> {ok, pid()}. start_http(Ref, NbAcceptors, TransOpts, ProtoOpts) when is_integer(NbAcceptors), NbAcceptors > 0 -> ranch:start_listener(Ref, NbAcceptors, ranch_tcp, TransOpts, cowboy_protocol, ProtoOpts). %% @doc Start an HTTPS listener. --spec start_https(any(), non_neg_integer(), any(), any()) -> {ok, pid()}. +-spec start_https(ranch:ref(), non_neg_integer(), ranch_ssl:opts(), + cowboy_protocol:opts()) -> {ok, pid()}. 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(any(), non_neg_integer(), any(), any()) -> {ok, pid()}. +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(any()) -> ok. +-spec stop_listener(ranch:ref()) -> ok. stop_listener(Ref) -> ranch:stop_listener(Ref). @@ -44,7 +75,7 @@ stop_listener(Ref) -> %% Allows you to update live an environment value used by middlewares. %% This function is primarily intended to simplify updating the dispatch %% list used for routing. --spec set_env(any(), atom(), any()) -> ok. +-spec set_env(ranch:ref(), atom(), any()) -> ok. set_env(Ref, Name, Value) -> Opts = ranch:get_protocol_options(Ref), {_, Env} = lists:keyfind(env, 1, Opts), diff --git a/src/cowboy_client.erl b/src/cowboy_client.erl index 4d958b1..b5f96b3 100644 --- a/src/cowboy_client.erl +++ b/src/cowboy_client.erl @@ -40,7 +40,7 @@ timeout = 5000 :: timeout(), %% @todo Configurable. buffer = <<>> :: binary(), connection = keepalive :: keepalive | close, - version = {1, 1} :: cowboy_http:version(), + version = 'HTTP/1.1' :: cowboy:http_version(), response_body = undefined :: undefined | non_neg_integer() }). @@ -91,7 +91,7 @@ request(Method, URL, Headers, Body, Client=#client{ wait -> connect(Transport, Host, Port, Client); request -> {ok, Client} end, - VersionBin = cowboy_http:version_to_binary(Version), + VersionBin = atom_to_binary(Version, latin1), %% @todo do keepalive too, allow override... Headers2 = [ {<<"host">>, FullHost}, @@ -173,7 +173,7 @@ stream_status(Client=#client{state=State, buffer=Buffer}) when State =:= request -> case binary:split(Buffer, <<"\r\n">>) of [Line, Rest] -> - parse_status(Client#client{state=response, buffer=Rest}, Line); + parse_version(Client#client{state=response, buffer=Rest}, Line); _ -> case recv(Client) of {ok, Data} -> @@ -184,11 +184,13 @@ stream_status(Client=#client{state=State, buffer=Buffer}) end end. -parse_status(Client, << "HTTP/", High, ".", Low, " ", - S3, S2, S1, " ", StatusStr/binary >>) - when High >= $0, High =< $9, Low >= $0, Low =< $9, - S3 >= $0, S3 =< $9, S2 >= $0, S2 =< $9, S1 >= $0, S1 =< $9 -> - Version = {High - $0, Low - $0}, +parse_version(Client, << "HTTP/1.1 ", Rest/binary >>) -> + parse_status(Client, Rest, 'HTTP/1.1'); +parse_version(Client, << "HTTP/1.0 ", Rest/binary >>) -> + parse_status(Client, Rest, 'HTTP/1.0'). + +parse_status(Client, << S3, S2, S1, " ", StatusStr/binary >>, Version) + when S3 >= $0, S3 =< $9, S2 >= $0, S2 =< $9, S1 >= $0, S1 =< $9 -> Status = (S3 - $0) * 100 + (S2 - $0) * 10 + S1 - $0, {ok, Status, StatusStr, Client#client{version=Version}}. diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl index e040554..2074b4e 100644 --- a/src/cowboy_handler.erl +++ b/src/cowboy_handler.erl @@ -105,7 +105,7 @@ handler_init(Req, State, Handler, HandlerOpts) -> -> {ok, Req, Env} | {suspend, module(), atom(), any()} | {halt, Req} - | {error, cowboy_http:status(), Req} + | {error, cowboy:http_status(), Req} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). upgrade_protocol(Req, #state{env=Env}, Handler, HandlerOpts, Module) -> diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index f889b52..af60dd9 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -46,21 +46,12 @@ %% Interpretation. -export([cookie_to_iodata/3]). --export([version_to_binary/1]). -export([urldecode/1]). -export([urldecode/2]). -export([urlencode/1]). -export([urlencode/2]). -export([x_www_form_urlencoded/1]). --type version() :: {Major::non_neg_integer(), Minor::non_neg_integer()}. --type headers() :: [{binary(), iodata()}]. --type status() :: non_neg_integer() | binary(). - --export_type([version/0]). --export_type([headers/0]). --export_type([status/0]). - %% Parsing. %% @doc Parse a non-empty list of the given type. @@ -802,7 +793,7 @@ qvalue(Data, Fun, Q, _M) -> %% Only Basic authorization is supported so far. -spec authorization(binary(), binary()) -> {binary(), any()} | {error, badarg}. authorization(UserPass, Type = <<"basic">>) -> - cowboy_http:whitespace(UserPass, + whitespace(UserPass, fun(D) -> authorization_basic_userid(base64:mime_decode(D), fun(Rest, Userid) -> @@ -813,7 +804,7 @@ authorization(UserPass, Type = <<"basic">>) -> end) end); authorization(String, Type) -> - cowboy_http:whitespace(String, fun(Rest) -> {Type, Rest} end). + whitespace(String, fun(Rest) -> {Type, Rest} end). %% @doc Parse user credentials. -spec authorization_basic_userid(binary(), fun()) -> any(). @@ -849,12 +840,12 @@ authorization_basic_password(<<C, Rest/binary>>, Fun, Acc) -> Unit :: binary(), Range :: {non_neg_integer(), non_neg_integer() | infinity} | neg_integer(). range(Data) -> - cowboy_http:token_ci(Data, fun range/2). + token_ci(Data, fun range/2). range(Data, Token) -> whitespace(Data, fun(<<"=", Rest/binary>>) -> - case cowboy_http:list(Rest, fun range_beginning/2) of + case list(Rest, fun range_beginning/2) of {error, badarg} -> {error, badarg}; Ranges -> @@ -1001,11 +992,6 @@ cookie_to_iodata(Name, Value, Opts) -> [Name, <<"=">>, Value, <<"; Version=1">>, MaxAgeBin, DomainBin, PathBin, SecureBin, HttpOnlyBin]. -%% @doc Convert an HTTP version tuple to its binary form. --spec version_to_binary(version()) -> binary(). -version_to_binary({1, 1}) -> <<"HTTP/1.1">>; -version_to_binary({1, 0}) -> <<"HTTP/1.0">>. - %% @doc Decode a URL encoded binary. %% @equiv urldecode(Bin, crash) -spec urldecode(binary()) -> binary(). diff --git a/src/cowboy_http_handler.erl b/src/cowboy_http_handler.erl index 0393153..3ad8f88 100644 --- a/src/cowboy_http_handler.erl +++ b/src/cowboy_http_handler.erl @@ -35,6 +35,8 @@ -type state() :: any(). -type terminate_reason() :: {normal, shutdown} | {normal, timeout} %% Only occurs in loop handlers. + | {error, closed} %% Only occurs in loop handlers. + | {error, overflow} %% Only occurs in loop handlers. | {error, atom()}. -callback init({atom(), http}, Req, opts()) diff --git a/src/cowboy_loop_handler.erl b/src/cowboy_loop_handler.erl index f8d008f..af49e57 100644 --- a/src/cowboy_loop_handler.erl +++ b/src/cowboy_loop_handler.erl @@ -41,6 +41,8 @@ -type state() :: any(). -type terminate_reason() :: {normal, shutdown} | {normal, timeout} + | {error, closed} + | {error, overflow} | {error, atom()}. -callback init({atom(), http}, Req, opts()) diff --git a/src/cowboy_middleware.erl b/src/cowboy_middleware.erl index 0c1ca77..40c9407 100644 --- a/src/cowboy_middleware.erl +++ b/src/cowboy_middleware.erl @@ -30,7 +30,7 @@ -callback execute(Req, Env) -> {ok, Req, Env} - | {suspend, module(), atom(), any()} + | {suspend, module(), atom(), [any()]} | {halt, Req} - | {error, cowboy_http:status(), Req} + | {error, cowboy:http_status(), Req} when Req::cowboy_req:req(), Env::env(). diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl index be351b7..b42f524 100644 --- a/src/cowboy_protocol.erl +++ b/src/cowboy_protocol.erl @@ -54,13 +54,22 @@ %% Internal. -export([init/4]). -export([parse_request/3]). +-export([parse_host/2]). -export([resume/6]). --type onrequest_fun() :: fun((Req) -> Req). --type onresponse_fun() :: - fun((cowboy_http:status(), cowboy_http:headers(), iodata(), Req) -> Req). --export_type([onrequest_fun/0]). --export_type([onresponse_fun/0]). +-type opts() :: [{compress, boolean()} + | {env, cowboy_middleware:env()} + | {max_empty_lines, non_neg_integer()} + | {max_header_name_length, non_neg_integer()} + | {max_header_value_length, non_neg_integer()} + | {max_headers, non_neg_integer()} + | {max_keepalive, non_neg_integer()} + | {max_request_line_length, non_neg_integer()} + | {middlewares, [module()]} + | {onrequest, cowboy:onrequest_fun()} + | {onresponse, cowboy:onresponse_fun()} + | {timeout, timeout()}]. +-export_type([opts/0]). -record(state, { socket :: inet:socket(), @@ -68,8 +77,8 @@ middlewares :: [module()], compress :: boolean(), env :: cowboy_middleware:env(), - onrequest :: undefined | onrequest_fun(), - onresponse = undefined :: undefined | onresponse_fun(), + onrequest :: undefined | cowboy:onrequest_fun(), + onresponse = undefined :: undefined | cowboy:onresponse_fun(), max_empty_lines :: non_neg_integer(), req_keepalive = 1 :: non_neg_integer(), max_keepalive :: non_neg_integer(), @@ -84,7 +93,7 @@ %% API. %% @doc Start an HTTP protocol process. --spec start_link(any(), inet:socket(), module(), any()) -> {ok, pid()}. +-spec start_link(ranch:ref(), inet:socket(), module(), opts()) -> {ok, pid()}. start_link(Ref, Socket, Transport, Opts) -> Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]), {ok, Pid}. @@ -100,7 +109,7 @@ get_value(Key, Opts, Default) -> end. %% @private --spec init(any(), inet:socket(), module(), any()) -> ok. +-spec init(ranch:ref(), inet:socket(), module(), opts()) -> ok. init(Ref, Socket, Transport, Opts) -> Compress = get_value(compress, Opts, false), MaxEmptyLines = get_value(max_empty_lines, Opts, 5), @@ -202,7 +211,7 @@ parse_method(<< C, Rest/bits >>, State, SoFar) -> parse_uri(<< $\r, _/bits >>, State, _) -> error_terminate(400, State); parse_uri(<< "* ", Rest/bits >>, State, Method) -> - parse_version(Rest, State, Method, <<"*">>, <<>>, <<>>); + parse_version(Rest, State, Method, <<"*">>, <<>>); parse_uri(<< "http://", Rest/bits >>, State, Method) -> parse_uri_skip_host(Rest, State, Method); parse_uri(<< "https://", Rest/bits >>, State, Method) -> @@ -220,61 +229,61 @@ parse_uri_skip_host(<< C, Rest/bits >>, State, Method) -> parse_uri_path(<< C, Rest/bits >>, State, Method, SoFar) -> case C of $\r -> error_terminate(400, State); - $\s -> parse_version(Rest, State, Method, SoFar, <<>>, <<>>); + $\s -> parse_version(Rest, State, Method, SoFar, <<>>); $? -> parse_uri_query(Rest, State, Method, SoFar, <<>>); - $# -> parse_uri_fragment(Rest, State, Method, SoFar, <<>>, <<>>); + $# -> skip_uri_fragment(Rest, State, Method, SoFar, <<>>); _ -> parse_uri_path(Rest, State, Method, << SoFar/binary, C >>) end. parse_uri_query(<< C, Rest/bits >>, S, M, P, SoFar) -> case C of $\r -> error_terminate(400, S); - $\s -> parse_version(Rest, S, M, P, SoFar, <<>>); - $# -> parse_uri_fragment(Rest, S, M, P, SoFar, <<>>); + $\s -> parse_version(Rest, S, M, P, SoFar); + $# -> skip_uri_fragment(Rest, S, M, P, SoFar); _ -> parse_uri_query(Rest, S, M, P, << SoFar/binary, C >>) end. -parse_uri_fragment(<< C, Rest/bits >>, S, M, P, Q, SoFar) -> +skip_uri_fragment(<< C, Rest/bits >>, S, M, P, Q) -> case C of $\r -> error_terminate(400, S); - $\s -> parse_version(Rest, S, M, P, Q, SoFar); - _ -> parse_uri_fragment(Rest, S, M, P, Q, << SoFar/binary, C >>) + $\s -> parse_version(Rest, S, M, P, Q); + _ -> skip_uri_fragment(Rest, S, M, P, Q) end. -parse_version(<< "HTTP/1.1\r\n", Rest/bits >>, S, M, P, Q, F) -> - parse_header(Rest, S, M, P, Q, F, {1, 1}, []); -parse_version(<< "HTTP/1.0\r\n", Rest/bits >>, S, M, P, Q, F) -> - parse_header(Rest, S, M, P, Q, F, {1, 0}, []); -parse_version(_, State, _, _, _, _) -> +parse_version(<< "HTTP/1.1\r\n", Rest/bits >>, S, M, P, Q) -> + parse_header(Rest, S, M, P, Q, 'HTTP/1.1', []); +parse_version(<< "HTTP/1.0\r\n", Rest/bits >>, S, M, P, Q) -> + parse_header(Rest, S, M, P, Q, 'HTTP/1.0', []); +parse_version(_, State, _, _, _) -> error_terminate(505, State). %% Stop receiving data if we have more than allowed number of headers. -wait_header(_, State=#state{max_headers=MaxHeaders}, _, _, _, _, _, Headers) +wait_header(_, State=#state{max_headers=MaxHeaders}, _, _, _, _, Headers) when length(Headers) >= MaxHeaders -> error_terminate(400, State); wait_header(Buffer, State=#state{socket=Socket, transport=Transport, - until=Until}, M, P, Q, F, V, H) -> + until=Until}, M, P, Q, V, H) -> case recv(Socket, Transport, Until) of {ok, Data} -> parse_header(<< Buffer/binary, Data/binary >>, - State, M, P, Q, F, V, H); + State, M, P, Q, V, H); {error, timeout} -> error_terminate(408, State); {error, _} -> terminate(State) end. -parse_header(<< $\r, $\n, Rest/bits >>, S, M, P, Q, F, V, Headers) -> - request(Rest, S, M, P, Q, F, V, lists:reverse(Headers)); +parse_header(<< $\r, $\n, Rest/bits >>, S, M, P, Q, V, Headers) -> + request(Rest, S, M, P, Q, V, lists:reverse(Headers)); parse_header(Buffer, State=#state{max_header_name_length=MaxLength}, - M, P, Q, F, V, H) -> + M, P, Q, V, H) -> case match_colon(Buffer, 0) of nomatch when byte_size(Buffer) > MaxLength -> error_terminate(400, State); nomatch -> - wait_header(Buffer, State, M, P, Q, F, V, H); + wait_header(Buffer, State, M, P, Q, V, H); _ -> - parse_hd_name(Buffer, State, M, P, Q, F, V, H, <<>>) + parse_hd_name(Buffer, State, M, P, Q, V, H, <<>>) end. match_colon(<< $:, _/bits >>, N) -> @@ -290,73 +299,73 @@ match_colon(_, _) -> %% ... Sorry for your eyes. %% %% But let's be honest, that's still pretty readable. -parse_hd_name(<< C, Rest/bits >>, S, M, P, Q, F, V, H, SoFar) -> +parse_hd_name(<< C, Rest/bits >>, S, M, P, Q, V, H, SoFar) -> case C of - $: -> parse_hd_before_value(Rest, S, M, P, Q, F, V, H, SoFar); - $\s -> parse_hd_name_ws(Rest, S, M, P, Q, F, V, H, SoFar); - $\t -> parse_hd_name_ws(Rest, S, M, P, Q, F, V, H, SoFar); - $A -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $a >>); - $B -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $b >>); - $C -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $c >>); - $D -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $d >>); - $E -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $e >>); - $F -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $f >>); - $G -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $g >>); - $H -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $h >>); - $I -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $i >>); - $J -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $j >>); - $K -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $k >>); - $L -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $l >>); - $M -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $m >>); - $N -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $n >>); - $O -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $o >>); - $P -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $p >>); - $Q -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $q >>); - $R -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $r >>); - $S -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $s >>); - $T -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $t >>); - $U -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $u >>); - $V -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $v >>); - $W -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $w >>); - $X -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $x >>); - $Y -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $y >>); - $Z -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $z >>); - C -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, C >>) + $: -> parse_hd_before_value(Rest, S, M, P, Q, V, H, SoFar); + $\s -> parse_hd_name_ws(Rest, S, M, P, Q, V, H, SoFar); + $\t -> parse_hd_name_ws(Rest, S, M, P, Q, V, H, SoFar); + $A -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $a >>); + $B -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $b >>); + $C -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $c >>); + $D -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $d >>); + $E -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $e >>); + $F -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $f >>); + $G -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $g >>); + $H -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $h >>); + $I -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $i >>); + $J -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $j >>); + $K -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $k >>); + $L -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $l >>); + $M -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $m >>); + $N -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $n >>); + $O -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $o >>); + $P -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $p >>); + $Q -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $q >>); + $R -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $r >>); + $S -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $s >>); + $T -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $t >>); + $U -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $u >>); + $V -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $v >>); + $W -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $w >>); + $X -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $x >>); + $Y -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $y >>); + $Z -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, $z >>); + C -> parse_hd_name(Rest, S, M, P, Q, V, H, << SoFar/binary, C >>) end. -parse_hd_name_ws(<< C, Rest/bits >>, S, M, P, Q, F, V, H, Name) -> +parse_hd_name_ws(<< C, Rest/bits >>, S, M, P, Q, V, H, Name) -> case C of - $\s -> parse_hd_name_ws(Rest, S, M, P, Q, F, V, H, Name); - $\t -> parse_hd_name_ws(Rest, S, M, P, Q, F, V, H, Name); - $: -> parse_hd_before_value(Rest, S, M, P, Q, F, V, H, Name) + $\s -> parse_hd_name_ws(Rest, S, M, P, Q, V, H, Name); + $\t -> parse_hd_name_ws(Rest, S, M, P, Q, V, H, Name); + $: -> parse_hd_before_value(Rest, S, M, P, Q, V, H, Name) end. wait_hd_before_value(Buffer, State=#state{ socket=Socket, transport=Transport, until=Until}, - M, P, Q, F, V, H, N) -> + M, P, Q, V, H, N) -> case recv(Socket, Transport, Until) of {ok, Data} -> parse_hd_before_value(<< Buffer/binary, Data/binary >>, - State, M, P, Q, F, V, H, N); + State, M, P, Q, V, H, N); {error, timeout} -> error_terminate(408, State); {error, _} -> terminate(State) end. -parse_hd_before_value(<< $\s, Rest/bits >>, S, M, P, Q, F, V, H, N) -> - parse_hd_before_value(Rest, S, M, P, Q, F, V, H, N); -parse_hd_before_value(<< $\t, Rest/bits >>, S, M, P, Q, F, V, H, N) -> - parse_hd_before_value(Rest, S, M, P, Q, F, V, H, N); +parse_hd_before_value(<< $\s, Rest/bits >>, S, M, P, Q, V, H, N) -> + parse_hd_before_value(Rest, S, M, P, Q, V, H, N); +parse_hd_before_value(<< $\t, Rest/bits >>, S, M, P, Q, V, H, N) -> + parse_hd_before_value(Rest, S, M, P, Q, V, H, N); parse_hd_before_value(Buffer, State=#state{ - max_header_value_length=MaxLength}, M, P, Q, F, V, H, N) -> + max_header_value_length=MaxLength}, M, P, Q, V, H, N) -> case match_eol(Buffer, 0) of nomatch when byte_size(Buffer) > MaxLength -> error_terminate(400, State); nomatch -> - wait_hd_before_value(Buffer, State, M, P, Q, F, V, H, N); + wait_hd_before_value(Buffer, State, M, P, Q, V, H, N); _ -> - parse_hd_value(Buffer, State, M, P, Q, F, V, H, N, <<>>) + parse_hd_value(Buffer, State, M, P, Q, V, H, N, <<>>) end. %% We completely ignore the first argument which is always @@ -365,10 +374,10 @@ parse_hd_before_value(Buffer, State=#state{ %% operations for no reasons. wait_hd_value(_, State=#state{ socket=Socket, transport=Transport, until=Until}, - M, P, Q, F, V, H, N, SoFar) -> + M, P, Q, V, H, N, SoFar) -> case recv(Socket, Transport, Until) of {ok, Data} -> - parse_hd_value(Data, State, M, P, Q, F, V, H, N, SoFar); + parse_hd_value(Data, State, M, P, Q, V, H, N, SoFar); {error, timeout} -> error_terminate(408, State); {error, _} -> @@ -380,51 +389,51 @@ wait_hd_value(_, State=#state{ %% the critical path, but forces us to have a special function. wait_hd_value_nl(_, State=#state{ socket=Socket, transport=Transport, until=Until}, - M, P, Q, F, V, Headers, Name, SoFar) -> + M, P, Q, V, Headers, Name, SoFar) -> case recv(Socket, Transport, Until) of {ok, << C, Data/bits >>} when C =:= $\s; C =:= $\t -> - parse_hd_value(Data, State, M, P, Q, F, V, Headers, Name, SoFar); + parse_hd_value(Data, State, M, P, Q, V, Headers, Name, SoFar); {ok, Data} -> - parse_header(Data, State, M, P, Q, F, V, [{Name, SoFar}|Headers]); + parse_header(Data, State, M, P, Q, V, [{Name, SoFar}|Headers]); {error, timeout} -> error_terminate(408, State); {error, _} -> terminate(State) end. -parse_hd_value(<< $\r, Rest/bits >>, S, M, P, Q, F, V, Headers, Name, SoFar) -> +parse_hd_value(<< $\r, Rest/bits >>, S, M, P, Q, V, Headers, Name, SoFar) -> case Rest of << $\n >> -> - wait_hd_value_nl(<<>>, S, M, P, Q, F, V, Headers, Name, SoFar); + wait_hd_value_nl(<<>>, S, M, P, Q, V, Headers, Name, SoFar); << $\n, C, Rest2/bits >> when C =:= $\s; C =:= $\t -> - parse_hd_value(Rest2, S, M, P, Q, F, V, Headers, Name, SoFar); + parse_hd_value(Rest2, S, M, P, Q, V, Headers, Name, SoFar); << $\n, Rest2/bits >> -> - parse_header(Rest2, S, M, P, Q, F, V, [{Name, SoFar}|Headers]) + parse_header(Rest2, S, M, P, Q, V, [{Name, SoFar}|Headers]) end; -parse_hd_value(<< C, Rest/bits >>, S, M, P, Q, F, V, H, N, SoFar) -> - parse_hd_value(Rest, S, M, P, Q, F, V, H, N, << SoFar/binary, C >>); +parse_hd_value(<< C, Rest/bits >>, S, M, P, Q, V, H, N, SoFar) -> + parse_hd_value(Rest, S, M, P, Q, V, H, N, << SoFar/binary, C >>); parse_hd_value(<<>>, State=#state{max_header_value_length=MaxLength}, - _, _, _, _, _, _, _, SoFar) when byte_size(SoFar) > MaxLength -> + _, _, _, _, _, _, SoFar) when byte_size(SoFar) > MaxLength -> error_terminate(400, State); -parse_hd_value(<<>>, S, M, P, Q, F, V, H, N, SoFar) -> - wait_hd_value(<<>>, S, M, P, Q, F, V, H, N, SoFar). +parse_hd_value(<<>>, S, M, P, Q, V, H, N, SoFar) -> + wait_hd_value(<<>>, S, M, P, Q, V, H, N, SoFar). -request(B, State=#state{transport=Transport}, M, P, Q, F, Version, Headers) -> +request(B, State=#state{transport=Transport}, M, P, Q, Version, Headers) -> case lists:keyfind(<<"host">>, 1, Headers) of - false when Version =:= {1, 1} -> + false when Version =:= 'HTTP/1.1' -> error_terminate(400, State); false -> - request(B, State, M, P, Q, F, Version, Headers, + request(B, State, M, P, Q, Version, Headers, <<>>, default_port(Transport:name())); {_, RawHost} -> case catch parse_host(RawHost, <<>>) of {'EXIT', _} -> error_terminate(400, State); {Host, undefined} -> - request(B, State, M, P, Q, F, Version, Headers, + request(B, State, M, P, Q, Version, Headers, Host, default_port(Transport:name())); {Host, Port} -> - request(B, State, M, P, Q, F, Version, Headers, + request(B, State, M, P, Q, Version, Headers, Host, Port) end end. @@ -476,11 +485,11 @@ parse_host(<< C, Rest/bits >>, Acc) -> request(Buffer, State=#state{socket=Socket, transport=Transport, req_keepalive=ReqKeepalive, max_keepalive=MaxKeepalive, compress=Compress, onresponse=OnResponse}, - Method, Path, Query, Fragment, Version, Headers, Host, Port) -> + Method, Path, Query, Version, Headers, Host, Port) -> case Transport:peername(Socket) of {ok, Peer} -> Req = cowboy_req:new(Socket, Transport, Peer, Method, Path, - Query, Fragment, Version, Headers, Host, Port, Buffer, + Query, Version, Headers, Host, Port, Buffer, ReqKeepalive < MaxKeepalive, Compress, OnResponse), onrequest(Req, State); {error, _} -> @@ -565,7 +574,7 @@ next_request(Req, State=#state{req_keepalive=Keepalive, timeout=Timeout}, end. %% Only send an error reply if there is no resp_sent message. --spec error_terminate(cowboy_http:status(), cowboy_req:req(), #state{}) -> ok. +-spec error_terminate(cowboy:http_status(), cowboy_req:req(), #state{}) -> ok. error_terminate(Code, Req, State) -> receive {cowboy_req, resp_sent} -> ok @@ -576,14 +585,14 @@ error_terminate(Code, Req, State) -> terminate(State). %% Only send an error reply if there is no resp_sent message. --spec error_terminate(cowboy_http:status(), #state{}) -> ok. +-spec error_terminate(cowboy:http_status(), #state{}) -> ok. error_terminate(Code, State=#state{socket=Socket, transport=Transport, compress=Compress, onresponse=OnResponse}) -> receive {cowboy_req, resp_sent} -> ok after 0 -> _ = cowboy_req:reply(Code, cowboy_req:new(Socket, Transport, - undefined, <<"GET">>, <<>>, <<>>, <<>>, {1, 1}, [], <<>>, + undefined, <<"GET">>, <<>>, <<>>, 'HTTP/1.1', [], <<>>, undefined, <<>>, false, Compress, OnResponse)), ok end, diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 5b8157d..093663c 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -41,7 +41,7 @@ -module(cowboy_req). %% Request API. --export([new/15]). +-export([new/14]). -export([method/1]). -export([version/1]). -export([peer/1]). @@ -54,7 +54,6 @@ -export([qs_val/2]). -export([qs_val/3]). -export([qs_vals/1]). --export([fragment/1]). -export([host_url/1]). -export([url/1]). -export([binding/2]). @@ -121,20 +120,30 @@ -type cookie_opts() :: [cookie_option()]. -export_type([cookie_opts/0]). --type resp_body_fun() :: fun((inet:socket(), module()) -> ok). +-type content_decode_fun() :: fun((binary()) + -> {ok, binary()} + | {error, atom()}). +-type transfer_decode_fun() :: fun((binary(), any()) + -> {ok, binary(), binary(), any()} + | more | {more, non_neg_integer(), binary(), any()} + | {done, non_neg_integer(), binary()} + | {done, binary(), non_neg_integer(), binary()} + | {error, atom()}). + +-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, %% Request. pid = undefined :: pid(), method = <<"GET">> :: binary(), - version = {1, 1} :: cowboy_http:version(), + version = 'HTTP/1.1' :: cowboy:http_version(), peer = undefined :: undefined | {inet:ip_address(), inet:port_number()}, host = undefined :: undefined | binary(), host_info = undefined :: undefined | cowboy_router:tokens(), @@ -143,30 +152,29 @@ path_info = undefined :: undefined | cowboy_router:tokens(), qs = undefined :: binary(), qs_vals = undefined :: undefined | list({binary(), binary() | true}), - fragment = undefined :: binary(), bindings = undefined :: undefined | cowboy_router:bindings(), - headers = [] :: cowboy_http:headers(), + headers = [] :: cowboy:http_headers(), p_headers = [] :: [any()], %% @todo Improve those specs. cookies = undefined :: undefined | [{binary(), binary()}], meta = [] :: [{atom(), any()}], %% Request body. - body_state = waiting :: waiting | done - | {stream, non_neg_integer(), fun(), any(), fun()}, + body_state = waiting :: waiting | done | {stream, non_neg_integer(), + transfer_decode_fun(), any(), content_decode_fun()}, multipart = undefined :: undefined | {non_neg_integer(), fun()}, buffer = <<>> :: binary(), %% Response. resp_compress = false :: boolean(), resp_state = waiting :: locked | waiting | chunks | done, - resp_headers = [] :: cowboy_http:headers(), + resp_headers = [] :: cowboy:http_headers(), resp_body = <<>> :: iodata() | resp_body_fun() | {non_neg_integer(), resp_body_fun()} | {chunked, resp_chunked_fun()}, %% Functions. onresponse = undefined :: undefined | already_called - | cowboy_protocol:onresponse_fun() + | cowboy:onresponse_fun() }). -opaque req() :: #http_req{}. @@ -181,21 +189,21 @@ %% %% 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(), binary(), - cowboy_http:version(), cowboy_http:headers(), binary(), + binary(), binary(), binary(), + cowboy:http_version(), cowboy:http_headers(), binary(), inet:port_number() | undefined, binary(), boolean(), boolean(), - undefined | cowboy_protocol:onresponse_fun()) + undefined | cowboy:onresponse_fun()) -> req(). -new(Socket, Transport, Peer, Method, Path, Query, Fragment, +new(Socket, Transport, Peer, Method, Path, Query, Version, Headers, Host, Port, Buffer, CanKeepalive, Compress, OnResponse) -> Req = #http_req{socket=Socket, transport=Transport, pid=self(), peer=Peer, - method=Method, path=Path, qs=Query, fragment=Fragment, version=Version, + method=Method, path=Path, qs=Query, version=Version, headers=Headers, host=Host, port=Port, buffer=Buffer, resp_compress=Compress, onresponse=OnResponse}, - case CanKeepalive and (Version =:= {1, 1}) of + case CanKeepalive and (Version =:= 'HTTP/1.1') of false -> Req#http_req{connection=close}; true -> @@ -216,13 +224,13 @@ method(Req) -> {Req#http_req.method, Req}. %% @doc Return the HTTP version used for the request. --spec version(Req) -> {cowboy_http:version(), Req} when Req::req(). +-spec version(Req) -> {cowboy:http_version(), Req} when Req::req(). version(Req) -> {Req#http_req.version, Req}. %% @doc Return the peer address and port number of the remote host. -spec peer(Req) - -> {undefined | {inet:ip_address(), inet:port_number()}, Req} + -> {{inet:ip_address(), inet:port_number()}, Req} when Req::req(). peer(Req) -> {Req#http_req.peer, Req}. @@ -289,11 +297,6 @@ qs_vals(Req=#http_req{qs=RawQs, qs_vals=undefined}) -> qs_vals(Req=#http_req{qs_vals=QsVals}) -> {QsVals, Req}. -%% @doc Return the raw fragment directly taken from the request. --spec fragment(Req) -> {binary(), Req} when Req::req(). -fragment(Req) -> - {Req#http_req.fragment, Req}. - %% @doc Return the request URL as a binary without the path and query string. %% %% The URL includes the scheme, host and port only. @@ -316,7 +319,7 @@ host_url(Req=#http_req{transport=Transport, host=Host, port=Port}) -> %% @doc Return the full request URL as a binary. %% -%% The URL includes the scheme, host, port, path, query string and fragment. +%% The URL includes the scheme, host, port, path and query string. -spec url(Req) -> {undefined | binary(), Req} when Req::req(). url(Req=#http_req{}) -> {HostURL, Req2} = host_url(Req), @@ -324,16 +327,12 @@ url(Req=#http_req{}) -> url(undefined, Req=#http_req{}) -> {undefined, Req}; -url(HostURL, Req=#http_req{path=Path, qs=QS, fragment=Fragment}) -> +url(HostURL, Req=#http_req{path=Path, qs=QS}) -> QS2 = case QS of <<>> -> <<>>; _ -> << "?", QS/binary >> end, - Fragment2 = case Fragment of - <<>> -> <<>>; - _ -> << "#", Fragment/binary >> - end, - {<< HostURL/binary, Path/binary, QS2/binary, Fragment2/binary >>, Req}. + {<< HostURL/binary, Path/binary, QS2/binary >>, Req}. %% @equiv binding(Name, Req, undefined) -spec binding(atom(), Req) -> {binary() | undefined, Req} when Req::req(). @@ -371,7 +370,7 @@ header(Name, Req, Default) -> end. %% @doc Return the full list of headers. --spec headers(Req) -> {cowboy_http:headers(), Req} when Req::req(). +-spec headers(Req) -> {cowboy:http_headers(), Req} when Req::req(). headers(Req) -> {Req#http_req.headers, Req}. @@ -480,14 +479,14 @@ parse_header(Name, Req=#http_req{p_headers=PHeaders}, Default, Fun) -> %% @equiv cookie(Name, Req, undefined) -spec cookie(binary(), Req) - -> {binary() | true | undefined, Req} when Req::req(). + -> {binary() | undefined, Req} when Req::req(). cookie(Name, Req) when is_binary(Name) -> cookie(Name, Req, undefined). %% @doc Return the cookie value for the given key, or a default if %% missing. -spec cookie(binary(), Req, Default) - -> {binary() | true | Default, Req} when Req::req(), Default::any(). + -> {binary() | Default, Req} when Req::req(), Default::any(). cookie(Name, Req=#http_req{cookies=undefined}, Default) when is_binary(Name) -> case parse_header(<<"cookie">>, Req) of {ok, undefined, Req2} -> @@ -502,7 +501,7 @@ cookie(Name, Req, Default) -> end. %% @doc Return the full list of cookie values. --spec cookies(Req) -> {list({binary(), binary() | true}), Req} when Req::req(). +-spec cookies(Req) -> {list({binary(), binary()}), Req} when Req::req(). cookies(Req=#http_req{cookies=undefined}) -> case parse_header(<<"cookie">>, Req) of {ok, undefined, Req2} -> @@ -583,7 +582,7 @@ body_length(Req) -> %% Content encoding is generally used for compression. %% %% Standard encodings can be found in cowboy_http. --spec init_stream(fun(), any(), fun(), Req) +-spec init_stream(transfer_decode_fun(), any(), content_decode_fun(), Req) -> {ok, Req} when Req::req(). init_stream(TransferDecode, TransferState, ContentDecode, Req) -> {ok, Req#http_req{body_state= @@ -608,7 +607,7 @@ stream_body(Req) -> %% for each streamed part, and {done, Req} when it's finished streaming. %% %% 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. +%% first 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(MaxLength, Req=#http_req{body_state=waiting, version=Version, @@ -616,7 +615,7 @@ stream_body(MaxLength, Req=#http_req{body_state=waiting, version=Version, {ok, ExpectHeader, Req1} = parse_header(<<"expect">>, Req), case ExpectHeader of [<<"100-continue">>] -> - HTTPVer = cowboy_http:version_to_binary(Version), + HTTPVer = atom_to_binary(Version, latin1), Transport:send(Socket, << HTTPVer/binary, " ", (status(100))/binary, "\r\n\r\n" >>); undefined -> @@ -702,7 +701,7 @@ transfer_decode_done(Length, Rest, Req=#http_req{ headers=Headers3, p_headers=PHeaders3}. %% @todo Probably needs a Rest. --spec content_decode(fun(), binary(), Req) +-spec content_decode(content_decode_fun(), binary(), Req) -> {ok, binary(), Req} | {error, atom()} when Req::req(). content_decode(ContentDecode, Data, Req) -> case ContentDecode(Data) of @@ -787,11 +786,8 @@ body_qs(MaxBodyLength, Req) -> %% this function returns <em>{headers, Headers}</em> followed by a sequence of %% <em>{body, Data}</em> tuples and finally <em>end_of_part</em>. When there %% is no part to parse anymore, <em>eof</em> is returned. -%% -%% If the request Content-Type is not a multipart one, <em>{error, badarg}</em> -%% is returned. -spec multipart_data(Req) - -> {headers, cowboy_http:headers(), Req} | {body, binary(), Req} + -> {headers, cowboy:http_headers(), Req} | {body, binary(), Req} | {end_of_part | eof, Req} when Req::req(). multipart_data(Req=#http_req{body_state=waiting}) -> {ok, {<<"multipart">>, _SubType, Params}, Req2} = @@ -921,7 +917,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}) -> @@ -929,18 +925,18 @@ delete_resp_header(Name, Req=#http_req{resp_headers=RespHeaders}) -> Req#http_req{resp_headers=RespHeaders2}. %% @equiv reply(Status, [], [], Req) --spec reply(cowboy_http:status(), Req) -> {ok, Req} when Req::req(). +-spec reply(cowboy:http_status(), Req) -> {ok, Req} when Req::req(). reply(Status, Req=#http_req{resp_body=Body}) -> reply(Status, [], Body, Req). %% @equiv reply(Status, Headers, [], Req) --spec reply(cowboy_http:status(), cowboy_http:headers(), Req) +-spec reply(cowboy:http_status(), cowboy:http_headers(), Req) -> {ok, Req} when Req::req(). reply(Status, Headers, Req=#http_req{resp_body=Body}) -> reply(Status, Headers, Body, Req). %% @doc Send a reply to the client. --spec reply(cowboy_http:status(), cowboy_http:headers(), +-spec reply(cowboy:http_status(), cowboy:http_headers(), iodata() | {non_neg_integer() | resp_body_fun()}, Req) -> {ok, Req} when Req::req(). reply(Status, Headers, Body, Req=#http_req{ @@ -948,20 +944,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 - {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 @@ -974,13 +980,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 - {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), @@ -988,7 +993,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 @@ -1005,7 +1010,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) -> @@ -1051,13 +1056,13 @@ reply_no_compress(Status, Headers, Body, Req, Req2. %% @equiv chunked_reply(Status, [], Req) --spec chunked_reply(cowboy_http:status(), Req) -> {ok, Req} when Req::req(). +-spec chunked_reply(cowboy:http_status(), Req) -> {ok, Req} when Req::req(). chunked_reply(Status, Req) -> chunked_reply(Status, [], Req). %% @doc Initiate the sending of a chunked reply to the client. %% @see cowboy_req:chunk/2 --spec chunked_reply(cowboy_http:status(), cowboy_http:headers(), Req) +-spec chunked_reply(cowboy:http_status(), cowboy:http_headers(), Req) -> {ok, Req} when Req::req(). chunked_reply(Status, Headers, Req) -> {_, Req2} = chunked_response(Status, Headers, Req), @@ -1069,18 +1074,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={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) +-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), @@ -1088,7 +1109,7 @@ upgrade_reply(Status, Headers, Req=#http_req{ %% @doc Ensure the response has been sent fully. %% @private --spec ensure_response(req(), cowboy_http:status()) -> ok. +-spec ensure_response(req(), cowboy:http_status()) -> ok. %% The response has already been fully sent to the client. ensure_response(#http_req{resp_state=done}, _) -> ok; @@ -1100,11 +1121,10 @@ ensure_response(Req=#http_req{resp_state=waiting}, Status) -> %% Terminate the chunked body for HTTP/1.1 only. ensure_response(#http_req{method= <<"HEAD">>, resp_state=chunks}, _) -> ok; -ensure_response(#http_req{version={1, 0}, resp_state=chunks}, _) -> +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. @@ -1126,7 +1146,6 @@ g(body_state, #http_req{body_state=Ret}) -> Ret; g(buffer, #http_req{buffer=Ret}) -> Ret; g(connection, #http_req{connection=Ret}) -> Ret; g(cookies, #http_req{cookies=Ret}) -> Ret; -g(fragment, #http_req{fragment=Ret}) -> Ret; g(headers, #http_req{headers=Ret}) -> Ret; g(host, #http_req{host=Ret}) -> Ret; g(host_info, #http_req{host_info=Ret}) -> Ret; @@ -1157,7 +1176,6 @@ set([{body_state, Val}|Tail], Req) -> set(Tail, Req#http_req{body_state=Val}); set([{buffer, Val}|Tail], Req) -> set(Tail, Req#http_req{buffer=Val}); set([{connection, Val}|Tail], Req) -> set(Tail, Req#http_req{connection=Val}); set([{cookies, Val}|Tail], Req) -> set(Tail, Req#http_req{cookies=Val}); -set([{fragment, Val}|Tail], Req) -> set(Tail, Req#http_req{fragment=Val}); set([{headers, Val}|Tail], Req) -> set(Tail, Req#http_req{headers=Val}); set([{host, Val}|Tail], Req) -> set(Tail, Req#http_req{host=Val}); set([{host_info, Val}|Tail], Req) -> set(Tail, Req#http_req{host_info=Val}); @@ -1216,14 +1234,23 @@ to_list(Req) -> %% Internal. --spec chunked_response(cowboy_http:status(), cowboy_http:headers(), 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), HTTP11Headers = case Version of - {1, 1} -> [ + 'HTTP/1.1' -> [ {<<"connection">>, atom_to_connection(Connection)}, {<<"transfer-encoding">>, <<"chunked">>}]; _ -> [] @@ -1235,8 +1262,8 @@ chunked_response(Status, Headers, Req=#http_req{ {RespType, Req2#http_req{connection=RespConn, resp_state=chunks, resp_headers=[], resp_body= <<>>}}. --spec response(cowboy_http:status(), cowboy_http:headers(), - cowboy_http:headers(), cowboy_http:headers(), iodata(), Req) +-spec response(cowboy:http_status(), cowboy:http_headers(), + 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, @@ -1245,22 +1272,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 = cowboy_http:version_to_binary(Version), + 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; _ -> @@ -1268,7 +1305,7 @@ response(Status, Headers, RespHeaders, DefaultHeaders, Body, Req=#http_req{ end, {ReplyType, Req2}. --spec response_connection(cowboy_http:headers(), keepalive | close) +-spec response_connection(cowboy:http_headers(), keepalive | close) -> keepalive | close. response_connection([], Connection) -> Connection; @@ -1281,16 +1318,16 @@ response_connection([{Name, Value}|Tail], Connection) -> response_connection(Tail, Connection) end. --spec response_merge_headers(cowboy_http:headers(), cowboy_http:headers(), - cowboy_http:headers()) -> cowboy_http:headers(). +-spec response_merge_headers(cowboy:http_headers(), cowboy:http_headers(), + cowboy:http_headers()) -> cowboy:http_headers(). response_merge_headers(Headers, RespHeaders, DefaultHeaders) -> Headers2 = [{Key, Value} || {Key, Value} <- Headers], merge_headers( merge_headers(Headers2, RespHeaders), DefaultHeaders). --spec merge_headers(cowboy_http:headers(), cowboy_http:headers()) - -> cowboy_http:headers(). +-spec merge_headers(cowboy:http_headers(), cowboy:http_headers()) + -> cowboy:http_headers(). %% Merge headers by prepending the tuples in the second list to the %% first list. It also handles Set-Cookie properly, which supports @@ -1377,7 +1414,7 @@ connection_to_atom([<<"close">>|_]) -> connection_to_atom([_|Tail]) -> connection_to_atom(Tail). --spec status(cowboy_http:status()) -> binary(). +-spec status(cowboy:http_status()) -> binary(). status(100) -> <<"100 Continue">>; status(101) -> <<"101 Switching Protocols">>; status(102) -> <<"102 Processing">>; @@ -1444,38 +1481,28 @@ status(B) when is_binary(B) -> B. url_test() -> {undefined, _} = url(#http_req{transport=ranch_tcp, host= <<>>, port= undefined, - path= <<>>, qs= <<>>, fragment= <<>>, pid=self()}), + path= <<>>, qs= <<>>, pid=self()}), {<<"http://localhost/path">>, _ } = url(#http_req{transport=ranch_tcp, host= <<"localhost">>, port=80, - path= <<"/path">>, qs= <<>>, fragment= <<>>, pid=self()}), + path= <<"/path">>, qs= <<>>, pid=self()}), {<<"http://localhost:443/path">>, _} = url(#http_req{transport=ranch_tcp, host= <<"localhost">>, port=443, - path= <<"/path">>, qs= <<>>, fragment= <<>>, pid=self()}), + path= <<"/path">>, qs= <<>>, pid=self()}), {<<"http://localhost:8080/path">>, _} = url(#http_req{transport=ranch_tcp, host= <<"localhost">>, port=8080, - path= <<"/path">>, qs= <<>>, fragment= <<>>, pid=self()}), + path= <<"/path">>, qs= <<>>, pid=self()}), {<<"http://localhost:8080/path?dummy=2785">>, _} = url(#http_req{transport=ranch_tcp, host= <<"localhost">>, port=8080, - path= <<"/path">>, qs= <<"dummy=2785">>, fragment= <<>>, - pid=self()}), - {<<"http://localhost:8080/path?dummy=2785#fragment">>, _} = - url(#http_req{transport=ranch_tcp, host= <<"localhost">>, port=8080, - path= <<"/path">>, qs= <<"dummy=2785">>, fragment= <<"fragment">>, - pid=self()}), + path= <<"/path">>, qs= <<"dummy=2785">>, pid=self()}), {<<"https://localhost/path">>, _} = url(#http_req{transport=ranch_ssl, host= <<"localhost">>, port=443, - path= <<"/path">>, qs= <<>>, fragment= <<>>, pid=self()}), + path= <<"/path">>, qs= <<>>, pid=self()}), {<<"https://localhost:8443/path">>, _} = url(#http_req{transport=ranch_ssl, host= <<"localhost">>, port=8443, - path= <<"/path">>, qs= <<>>, fragment= <<>>, pid=self()}), + path= <<"/path">>, qs= <<>>, pid=self()}), {<<"https://localhost:8443/path?dummy=2785">>, _} = url(#http_req{transport=ranch_ssl, host= <<"localhost">>, port=8443, - path= <<"/path">>, qs= <<"dummy=2785">>, fragment= <<>>, - pid=self()}), - {<<"https://localhost:8443/path?dummy=2785#fragment">>, _} = - url(#http_req{transport=ranch_ssl, host= <<"localhost">>, port=8443, - path= <<"/path">>, qs= <<"dummy=2785">>, fragment= <<"fragment">>, - pid=self()}), + path= <<"/path">>, qs= <<"dummy=2785">>, pid=self()}), ok. parse_connection_test_() -> diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index 0913b26..ecbe7bc 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -45,7 +45,7 @@ language_a :: undefined | binary(), %% Charset. - charsets_p = [] :: [{binary(), integer()}], + charsets_p = [] :: [binary()], charset_a :: undefined | binary(), %% Whether the resource exists. @@ -412,8 +412,7 @@ charsets_provided(Req, State) -> cowboy_req:parse_header(<<"accept-charset">>, Req2), case AcceptCharset of undefined -> - set_content_type(Req3, State2#state{ - charset_a=element(1, hd(CP))}); + set_content_type(Req3, State2#state{charset_a=hd(CP)}); AcceptCharset -> AcceptCharset2 = prioritize_charsets(AcceptCharset), choose_charset(Req3, State2, AcceptCharset2) @@ -433,7 +432,11 @@ prioritize_charsets(AcceptCharsets) -> end, AcceptCharsets), case lists:keymember(<<"*">>, 1, AcceptCharsets2) of true -> AcceptCharsets2; - false -> [{<<"iso-8859-1">>, 1000}|AcceptCharsets2] + false -> + case lists:keymember(<<"iso-8859-1">>, 1, AcceptCharsets2) of + true -> AcceptCharsets2; + false -> [{<<"iso-8859-1">>, 1000}|AcceptCharsets2] + end end. choose_charset(Req, State, []) -> @@ -443,7 +446,7 @@ choose_charset(Req, State=#state{charsets_p=CP}, [Charset|Tail]) -> match_charset(Req, State, Accept, [], _Charset) -> choose_charset(Req, State, Accept); -match_charset(Req, State, _Accept, [{Provided, _}|_], {Provided, _}) -> +match_charset(Req, State, _Accept, [Provided|_], {Provided, _}) -> set_content_type(Req, State#state{charset_a=Provided}); match_charset(Req, State, Accept, [_|Tail], Charset) -> match_charset(Req, State, Accept, Tail, Charset). @@ -848,7 +851,7 @@ process_content_type(Req, State=#state{method=Method, {false, Req2, HandlerState2} -> State2 = State#state{handler_state=HandlerState2}, respond(Req2, State2, 422); - {ResURL, Req2, HandlerState2} when Method =:= <<"POST">> -> + {{true, ResURL}, Req2, HandlerState2} when Method =:= <<"POST">> -> State2 = State#state{handler_state=HandlerState2}, Req3 = cowboy_req:set_resp_header( <<"location">>, ResURL, Req2), diff --git a/src/cowboy_spdy.erl b/src/cowboy_spdy.erl new file mode 100644 index 0000000..ba02706 --- /dev/null +++ b/src/cowboy_spdy.erl @@ -0,0 +1,544 @@ +%% 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]). + +-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}); + {'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, << _:38, 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, << _:65, 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, 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(<< 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]). + +%% 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, sendfile + +name() -> + spdy. + +send(Socket, Data) -> + stream_data(Socket, Data). 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_sub_protocol.erl b/src/cowboy_sub_protocol.erl index 0b231d3..26ccd7e 100644 --- a/src/cowboy_sub_protocol.erl +++ b/src/cowboy_sub_protocol.erl @@ -31,7 +31,7 @@ -callback upgrade(Req, Env, module(), any()) -> {ok, Req, Env} - | {suspend, module(), atom(), any()} + | {suspend, module(), atom(), [any()]} | {halt, Req} - | {error, cowboy_http:status(), Req} + | {error, cowboy:http_status(), Req} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl index b5075c0..3667797 100644 --- a/src/cowboy_websocket.erl +++ b/src/cowboy_websocket.erl @@ -29,8 +29,8 @@ -export_type([close_code/0]). -type frame() :: close | ping | pong - | {text | binary | close | ping | pong, binary()} - | {close, close_code(), binary()}. + | {text | binary | close | ping | pong, iodata()} + | {close, close_code(), iodata()}. -export_type([frame/0]). -type opcode() :: 0 | 1 | 2 | 8 | 9 | 10. diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 98d4376..21cdd4b 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -209,11 +209,8 @@ init_per_group(http, Config) -> {transport, Transport}, {client, Client}|Config1]; init_per_group(https, Config) -> Transport = ranch_ssl, - Opts = [ - {certfile, ?config(data_dir, Config) ++ "ssl/cert.pem"}, - {keyfile, ?config(data_dir, Config) ++ "ssl/key.pem"}, - {password, "cowboy"} - ], + {_, Cert, Key} = ct_helper:make_certs(), + Opts = [{cert, Cert}, {key, Key}], Config1 = init_static_dir(Config), application:start(public_key), application:start(ssl), @@ -241,11 +238,8 @@ init_per_group(http_compress, Config) -> {transport, Transport}, {client, Client}|Config1]; init_per_group(https_compress, Config) -> Transport = ranch_ssl, - Opts = [ - {certfile, ?config(data_dir, Config) ++ "ssl/cert.pem"}, - {keyfile, ?config(data_dir, Config) ++ "ssl/key.pem"}, - {password, "cowboy"} - ], + {_, Cert, Key} = ct_helper:make_certs(), + Opts = [{cert, Cert}, {key, Key}], Config1 = init_static_dir(Config), application:start(public_key), application:start(ssl), diff --git a/test/http_SUITE_data/rest_forbidden_resource.erl b/test/http_SUITE_data/rest_forbidden_resource.erl index 287ff62..920ba31 100644 --- a/test/http_SUITE_data/rest_forbidden_resource.erl +++ b/test/http_SUITE_data/rest_forbidden_resource.erl @@ -28,4 +28,4 @@ to_text(Req, State) -> from_text(Req, State) -> {Path, Req2} = cowboy_req:path(Req), - {Path, Req2, State}. + {{true, Path}, Req2, State}. diff --git a/test/http_SUITE_data/ssl/cert.pem b/test/http_SUITE_data/ssl/cert.pem deleted file mode 100644 index a772007..0000000 --- a/test/http_SUITE_data/ssl/cert.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICKTCCAZICCQCl9gdHk5NqUjANBgkqhkiG9w0BAQUFADBZMQswCQYDVQQGEwJB -VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTEwNDA4MTMxNTE3WhcN -MTEwNTA4MTMxNTE3WjBZMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0 -ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAls -b2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOjgFPS0dP4d8F1e -bNJPB+kAjM2FyTZGmkFCLUYONTPrdGOUIHL/UOGtU22BQzlskE+a6/j2Kg72tm8x -4X7yf+6s7CdRe086idNx9+GymZ64ZTnly33rD3AJffbBeWHwT2e9fuBeFk9WGC8v -kqECFZyqf7+znS0o48oBNcx3ePB5AgMBAAEwDQYJKoZIhvcNAQEFBQADgYEASTkv -oHuZyO8DgT8bIE6W3yM2fvlNshkhh7Thgpf32qQoVOxRU9EF0KpuJCCAHQHQNQlI -nf9Zc4UzOrLhxZBGocNhkkn4WLw2ysto/7+/+9xHah0M0l4auHLQagVLCoOsHUn2 -JX+A2NrbvuX5wnUrZGOdgY70tvMBeU/xLtp3af8= ------END CERTIFICATE----- diff --git a/test/http_SUITE_data/ssl/key.pem b/test/http_SUITE_data/ssl/key.pem deleted file mode 100644 index 0b699cc..0000000 --- a/test/http_SUITE_data/ssl/key.pem +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -Proc-Type: 4,ENCRYPTED -DEK-Info: DES-EDE3-CBC,F11262DB77BB804C - -jOJ+ft/dihIxz7CTuuK47fCTGdX7xMLANmA7mRg8y9OYhNZQiCz5GjcWLqe0NNl5 -qXPW0uvT/9B5O9o21Y2i/CKU1BqRLuXHXDsjHg7RGaSH6wIavWt+lR+I1sjieFbX -VByK1KHXjEU704DEILKJIA9gVzoYAgMzo+FTw2e/2jusXntxk8HXyF5zKTzjHBtI -NQGweJqTmfZjX3SgPP4Co/ShrA6fUG0uTp1HwbByJnwtAeT3xWJrAD4QSn7+qrlv -3qmEIqVXsvLrfZRY1WZ4uIsbLK8wkvxboSIoIK55VV9R2zRbwQULon6QJwKYujAr -J2WUYkHHQOMpaAzUmalaT+8GUt8/A1oSK4BdiSZywsMMm46/hDadXBzFg+dPL5g2 -Td+7/L0S6tUVWq4+YBp5EalZH6VQ4cqPYDJZUZ9xt6+yY7V5748lSdA7cHCROnbG -bKbSW9WbF7MPDHCjvCAfq+s1dafHJgyIOlMg2bm7V8eHWAA0xKQ/o7i5EyEyaKYR -UXGeAf+KfXcclEZ77v2RCXZvd6ceWkifm59qWv/3TCYaHiS2Aa3lVToMKTwYzzXQ -p5X5os6wv3IAi2nGyAIOoSDisdHmFteZNXNQsw0n3XCAYfsNMk+r5/r5YqDffURH -c8SMOCP4BIPoZ/abi/gnEntGqsx1YALg0aosHwHGDJ/l+QJC6u6PZk310YzRw4GL -K9+wscFgEub2OO+R83Vkfesj4tYzgOjab7+92a/soHdW0zhGejlvehODOgNZ6NUG -MPQlT+qpF9Jh5IThYXupXXFzJzQe3O/qVXy89m69JGa+AWRvbu+M/A== ------END RSA PRIVATE KEY----- diff --git a/test/spdy_SUITE.erl b/test/spdy_SUITE.erl new file mode 100644 index 0000000..df29281 --- /dev/null +++ b/test/spdy_SUITE.erl @@ -0,0 +1,163 @@ +%% 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), + Config. + +end_per_suite(_Config) -> + 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(_) -> + cowboy_router:compile([ + {"localhost", [ + {"/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"}, + {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]. |