From bd6b17e7993a5f94c067f203b24fb6ad04fd8033 Mon Sep 17 00:00:00 2001 From: Andrei Nesterov Date: Sat, 2 Jul 2016 18:22:56 +0300 Subject: [PATCH] Add CORS example --- examples/README.asciidoc | 3 + examples/cors_hello_world/Makefile | 8 + examples/cors_hello_world/README.asciidoc | 159 ++++++++++++++++++ examples/cors_hello_world/relx.config | 2 + .../src/cors_hello_world_app.erl | 26 +++ .../src/cors_hello_world_middleware.erl | 48 ++++++ .../src/cors_hello_world_sup.erl | 23 +++ .../cors_hello_world/src/toppage_handler.erl | 12 ++ 8 files changed, 281 insertions(+) create mode 100644 examples/cors_hello_world/Makefile create mode 100644 examples/cors_hello_world/README.asciidoc create mode 100644 examples/cors_hello_world/relx.config create mode 100644 examples/cors_hello_world/src/cors_hello_world_app.erl create mode 100644 examples/cors_hello_world/src/cors_hello_world_middleware.erl create mode 100644 examples/cors_hello_world/src/cors_hello_world_sup.erl create mode 100644 examples/cors_hello_world/src/toppage_handler.erl diff --git a/examples/README.asciidoc b/examples/README.asciidoc index 7e5c6e02..f687fe79 100644 --- a/examples/README.asciidoc +++ b/examples/README.asciidoc @@ -9,6 +9,9 @@ * link:cookie[]: set cookies from server and client side +* link:cors_hello_world[]: + simplest CORS middleware example + * link:echo_get[]: parse and echo a GET query string diff --git a/examples/cors_hello_world/Makefile b/examples/cors_hello_world/Makefile new file mode 100644 index 00000000..c78dd24c --- /dev/null +++ b/examples/cors_hello_world/Makefile @@ -0,0 +1,8 @@ +PROJECT = cors_hello_world +PROJECT_DESCRIPTION = Cowboy CORS Hello World example +PROJECT_VERSION = 1 + +DEPS = cowboy +dep_cowboy_commit = master + +include ../../erlang.mk diff --git a/examples/cors_hello_world/README.asciidoc b/examples/cors_hello_world/README.asciidoc new file mode 100644 index 00000000..321cf38f --- /dev/null +++ b/examples/cors_hello_world/README.asciidoc @@ -0,0 +1,159 @@ += Hello world example + +To try this example, you need GNU `make` and `git` in your PATH. + +To build and run the example, use the following command: + +[source,bash] +$ make run + +== HTTP/1.1 example output + +[source,bash] +---- +$ curl -i \ + -XOPTIONS \ + -H'Origin:http://example.org' \ + -H'Access-Control-Request-Method:GET' \ + -H'Access-Control-Request-Headers:Authorization' \ + 'http://localhost:8080' +HTTP/1.1 200 OK +access-control-allow-headers: Authorization +access-control-allow-methods: GET, PUT +access-control-allow-origin: http://example.org +access-control-max-age: 0 +content-length: 0 +date: Sat, 02 Jul 2016 14:56:30 GMT +server: Cowboy +vary: Origin + +$ curl -i \ + -XGET \ + -H'Origin:http://example.org' \ + 'http://localhost:8080' +HTTP/1.1 200 OK +access-control-allow-origin: http://example.org +content-length: 12 +content-type: text/plain +date: Sat, 02 Jul 2016 14:46:42 GMT +server: Cowboy +vary: Origin + +Hello world! +---- + +== HTTP/2 example output + +[source,bash] +---- +$ nghttp -v \ + -H':method: OPTIONS' \ + -H'Origin:http://example.org' \ + -H'Access-Control-Request-Method:GET' \ + -H'Access-Control-Request-Headers:Authorization' \ + 'http://localhost:8080' +[ 0.002] Connected +[ 0.002] send SETTINGS frame + (niv=2) + [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] + [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] +[ 0.002] send PRIORITY frame + (dep_stream_id=0, weight=201, exclusive=0) +[ 0.002] send PRIORITY frame + (dep_stream_id=0, weight=101, exclusive=0) +[ 0.002] send PRIORITY frame + (dep_stream_id=0, weight=1, exclusive=0) +[ 0.002] send PRIORITY frame + (dep_stream_id=7, weight=1, exclusive=0) +[ 0.002] send PRIORITY frame + (dep_stream_id=3, weight=1, exclusive=0) +[ 0.002] send HEADERS frame + ; END_STREAM | END_HEADERS | PRIORITY + (padlen=0, dep_stream_id=11, weight=16, exclusive=0) + ; Open new stream + :method: OPTIONS + :path: / + :scheme: http + :authority: localhost:8080 + accept: */* + accept-encoding: gzip, deflate + user-agent: nghttp2/1.11.1 + origin: http://example.org + access-control-request-method: GET + access-control-request-headers: Authorization +[ 0.007] recv SETTINGS frame + (niv=0) +[ 0.007] recv SETTINGS frame + ; ACK + (niv=0) +[ 0.007] send SETTINGS frame + ; ACK + (niv=0) +[ 0.007] recv (stream_id=13) :status: 200 +[ 0.007] recv (stream_id=13) access-control-allow-headers: Authorization +[ 0.007] recv (stream_id=13) access-control-allow-methods: GET, PUT +[ 0.007] recv (stream_id=13) access-control-allow-origin: http://example.org +[ 0.007] recv (stream_id=13) access-control-max-age: 0 +[ 0.007] recv (stream_id=13) content-length: 0 +[ 0.008] recv (stream_id=13) date: Sat, 02 Jul 2016 15:06:07 GMT +[ 0.008] recv (stream_id=13) server: Cowboy +[ 0.008] recv (stream_id=13) vary: Origin +[ 0.008] recv HEADERS frame + ; END_STREAM | END_HEADERS + (padlen=0) + ; First response header +[ 0.008] send GOAWAY frame + (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) + +$ nghttp -v \ + -H':method:GET' \ + -H'Origin:http://example.org' \ + 'http://localhost:8080' +[ 0.002] Connected +[ 0.002] send SETTINGS frame + (niv=2) + [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] + [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] +[ 0.002] send PRIORITY frame + (dep_stream_id=0, weight=201, exclusive=0) +[ 0.002] send PRIORITY frame + (dep_stream_id=0, weight=101, exclusive=0) +[ 0.002] send PRIORITY frame + (dep_stream_id=0, weight=1, exclusive=0) +[ 0.002] send PRIORITY frame + (dep_stream_id=7, weight=1, exclusive=0) +[ 0.002] send PRIORITY frame + (dep_stream_id=3, weight=1, exclusive=0) +[ 0.002] send HEADERS frame + ; END_STREAM | END_HEADERS | PRIORITY + (padlen=0, dep_stream_id=11, weight=16, exclusive=0) + ; Open new stream + :method: GET + :path: / + :scheme: http + :authority: localhost:8080 + accept: */* + accept-encoding: gzip, deflate + user-agent: nghttp2/1.11.1 + origin: http://example.org +[ 0.004] recv SETTINGS frame + (niv=0) +[ 0.005] recv SETTINGS frame + ; ACK + (niv=0) +[ 0.005] recv (stream_id=13) :status: 200 +[ 0.005] recv (stream_id=13) access-control-allow-origin: http://example.org +[ 0.005] recv (stream_id=13) content-length: 12 +[ 0.005] recv (stream_id=13) content-type: text/plain +[ 0.005] recv (stream_id=13) date: Sat, 02 Jul 2016 15:07:01 GMT +[ 0.005] recv (stream_id=13) server: Cowboy +[ 0.005] recv (stream_id=13) vary: Origin +[ 0.005] recv HEADERS frame + ; END_HEADERS + (padlen=0) + ; First response header +Hello world![ 0.009] recv DATA frame + ; END_STREAM +[ 0.009] send GOAWAY frame + (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[]) +---- diff --git a/examples/cors_hello_world/relx.config b/examples/cors_hello_world/relx.config new file mode 100644 index 00000000..2fd8b98e --- /dev/null +++ b/examples/cors_hello_world/relx.config @@ -0,0 +1,2 @@ +{release, {cors_hello_world_example, "1"}, [cors_hello_world]}. +{extended_start_script, true}. diff --git a/examples/cors_hello_world/src/cors_hello_world_app.erl b/examples/cors_hello_world/src/cors_hello_world_app.erl new file mode 100644 index 00000000..b40331db --- /dev/null +++ b/examples/cors_hello_world/src/cors_hello_world_app.erl @@ -0,0 +1,26 @@ +%% Feel free to use, reuse and abuse the code in this file. + +%% @private +-module(cors_hello_world_app). +-behaviour(application). + +%% API. +-export([start/2]). +-export([stop/1]). + +%% API. + +start(_Type, _Args) -> + Dispatch = cowboy_router:compile([ + {'_', [ + {"/", toppage_handler, []} + ]} + ]), + {ok, _} = cowboy:start_clear(http, 100, [{port, 8080}], #{ + middlewares => [cors_hello_world_middleware, cowboy_router, cowboy_handler], + env => #{dispatch => Dispatch} + }), + cors_hello_world_sup:start_link(). + +stop(_State) -> + ok. diff --git a/examples/cors_hello_world/src/cors_hello_world_middleware.erl b/examples/cors_hello_world/src/cors_hello_world_middleware.erl new file mode 100644 index 00000000..5c96f4a1 --- /dev/null +++ b/examples/cors_hello_world/src/cors_hello_world_middleware.erl @@ -0,0 +1,48 @@ +%% Feel free to use, reuse and abuse the code in this file. + +%% @private +-module(cors_hello_world_middleware). + +%% API. +-export([execute/2]). + +%% API. + +-spec execute(Req, Env) -> {ok | stop, Req, Env} when Req :: cowboy_req:req(), Env :: any(). +execute(#{headers := #{<<"origin">> := HeaderVal}} = Req, Env) -> + %%AllowedOrigins = '*', + AllowedOrigins = [ + {<<"http">>, <<"example.org">>, 80}, + {<<"https">>, <<"example.org">>, 443} + ], + + %% NOTE: we assume we always deal with single origin + [Val] = cow_http_hd:parse_origin(HeaderVal), + case check_origin(Val, AllowedOrigins) of + true -> handle_cors_request(HeaderVal, Req, Env); + _ -> {ok, Req, Env} + end; +execute(Req, Env) -> + {ok, Req, Env}. + +-spec handle_cors_request(binary(), cowboy_req:req(), any()) -> cowboy_req:req(). +handle_cors_request(Origin, #{method := Method} = Req, Env) -> + Req2 = cowboy_req:set_resp_header(<<"access-control-allow-origin">>, Origin, Req), + Req3 = cowboy_req:set_resp_header(<<"vary">>, <<"Origin">>, Req2), + case Method of + <<"OPTIONS">> -> + Req4 = cowboy_req:set_resp_header(<<"access-control-allow-methods">>, <<"GET, PUT">>, Req3), + Req5 = cowboy_req:set_resp_header(<<"access-control-allow-headers">>, <<"Authorization">>, Req4), + Req6 = cowboy_req:set_resp_header(<<"access-control-max-age">>, <<"0">>, Req5), + Req7 = cowboy_req:reply(200, Req6), + {stop, Req7}; + _ -> + {ok, Req3, Env} + end. + +-spec check_origin(Origin, [Origin] | '*') -> boolean() when Origin :: {binary(), binary(), 0..65535} | reference(). +check_origin(Val, '*') when is_reference(Val) -> true; +check_origin(_, '*') -> true; +check_origin(Val, Val) -> true; +check_origin(Val, L) when is_list(L) -> lists:member(Val, L); +check_origin(_, _) -> false. diff --git a/examples/cors_hello_world/src/cors_hello_world_sup.erl b/examples/cors_hello_world/src/cors_hello_world_sup.erl new file mode 100644 index 00000000..7ed3d209 --- /dev/null +++ b/examples/cors_hello_world/src/cors_hello_world_sup.erl @@ -0,0 +1,23 @@ +%% Feel free to use, reuse and abuse the code in this file. + +%% @private +-module(cors_hello_world_sup). +-behaviour(supervisor). + +%% API. +-export([start_link/0]). + +%% supervisor. +-export([init/1]). + +%% API. + +-spec start_link() -> {ok, pid()}. +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% supervisor. + +init([]) -> + Procs = [], + {ok, {{one_for_one, 10, 10}, Procs}}. diff --git a/examples/cors_hello_world/src/toppage_handler.erl b/examples/cors_hello_world/src/toppage_handler.erl new file mode 100644 index 00000000..eb95bf3f --- /dev/null +++ b/examples/cors_hello_world/src/toppage_handler.erl @@ -0,0 +1,12 @@ +%% Feel free to use, reuse and abuse the code in this file. + +%% @doc Hello world handler. +-module(toppage_handler). + +-export([init/2]). + +init(Req, Opts) -> + cowboy_req:reply(200, #{ + <<"content-type">> => <<"text/plain">> + }, <<"Hello world!">>, Req), + {ok, Req, Opts}.