mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
Switch the HTTP protocol to use binary packets instead of lists.
The server now does a single recv (or more, but only if needed) which is then sent to erlang:decode_packet/3 multiple times. Since most requests are smaller than the default MTU on many platforms, we benefit from this greatly. In the case of requests with a body, the server usually read at least part of the body on the first recv. This is bufferized properly and used when later retrieving the body. In the case of pipelined requests, we can end up reading many requests in a single recv, which are then handled properly using only the buffer containing the received data.
This commit is contained in:
parent
6c1f73c53c
commit
29e71cf4da
6 changed files with 288 additions and 243 deletions
|
@ -15,11 +15,11 @@
|
||||||
-include_lib("kernel/include/inet.hrl").
|
-include_lib("kernel/include/inet.hrl").
|
||||||
|
|
||||||
-type http_method() :: 'OPTIONS' | 'GET' | 'HEAD'
|
-type http_method() :: 'OPTIONS' | 'GET' | 'HEAD'
|
||||||
| 'POST' | 'PUT' | 'DELETE' | 'TRACE' | string().
|
| 'POST' | 'PUT' | 'DELETE' | 'TRACE' | binary().
|
||||||
-type http_uri() :: '*' | {absoluteURI, http | https, Host::string(),
|
-type http_uri() :: '*' | {absoluteURI, http | https, Host::binary(),
|
||||||
Port::integer() | undefined, Path::string()}
|
Port::integer() | undefined, Path::binary()}
|
||||||
| {scheme, Scheme::string(), string()}
|
| {scheme, Scheme::binary(), binary()}
|
||||||
| {abs_path, string()} | string().
|
| {abs_path, binary()} | binary().
|
||||||
-type http_version() :: {Major::integer(), Minor::integer()}.
|
-type http_version() :: {Major::integer(), Minor::integer()}.
|
||||||
-type http_header() :: 'Cache-Control' | 'Connection' | 'Date' | 'Pragma'
|
-type http_header() :: 'Cache-Control' | 'Connection' | 'Date' | 'Pragma'
|
||||||
| 'Transfer-Encoding' | 'Upgrade' | 'Via' | 'Accept' | 'Accept-Charset'
|
| 'Transfer-Encoding' | 'Upgrade' | 'Via' | 'Accept' | 'Accept-Charset'
|
||||||
|
@ -33,10 +33,10 @@
|
||||||
| 'Content-Md5' | 'Content-Range' | 'Content-Type' | 'Etag'
|
| 'Content-Md5' | 'Content-Range' | 'Content-Type' | 'Etag'
|
||||||
| 'Expires' | 'Last-Modified' | 'Accept-Ranges' | 'Set-Cookie'
|
| 'Expires' | 'Last-Modified' | 'Accept-Ranges' | 'Set-Cookie'
|
||||||
| 'Set-Cookie2' | 'X-Forwarded-For' | 'Cookie' | 'Keep-Alive'
|
| 'Set-Cookie2' | 'X-Forwarded-For' | 'Cookie' | 'Keep-Alive'
|
||||||
| 'Proxy-Connection' | string().
|
| 'Proxy-Connection' | binary().
|
||||||
-type http_headers() :: list({http_header(), string()}).
|
-type http_headers() :: list({http_header(), binary()}).
|
||||||
%% -type http_cookies() :: term(). %% @todo
|
%% -type http_cookies() :: term(). %% @todo
|
||||||
-type http_status() :: non_neg_integer() | string().
|
-type http_status() :: non_neg_integer() | binary().
|
||||||
|
|
||||||
-record(http_req, {
|
-record(http_req, {
|
||||||
%% Transport.
|
%% Transport.
|
||||||
|
@ -49,18 +49,20 @@
|
||||||
version = {1, 1} :: http_version(),
|
version = {1, 1} :: http_version(),
|
||||||
peer = undefined :: undefined | {Address::ip_address(), Port::ip_port()},
|
peer = undefined :: undefined | {Address::ip_address(), Port::ip_port()},
|
||||||
host = undefined :: undefined | cowboy_dispatcher:path_tokens(),
|
host = undefined :: undefined | cowboy_dispatcher:path_tokens(),
|
||||||
raw_host = undefined :: undefined | string(),
|
raw_host = undefined :: undefined | binary(),
|
||||||
port = undefined :: undefined | ip_port(),
|
port = undefined :: undefined | ip_port(),
|
||||||
path = undefined :: undefined | '*' | cowboy_dispatcher:path_tokens(),
|
path = undefined :: undefined | '*' | cowboy_dispatcher:path_tokens(),
|
||||||
raw_path = undefined :: undefined | string(),
|
raw_path = undefined :: undefined | binary(),
|
||||||
qs_vals = undefined :: undefined | list({Name::string(), Value::string() | true}),
|
qs_vals = undefined :: undefined
|
||||||
raw_qs = undefined :: undefined | string(),
|
| list({Name::binary(), Value::binary() | true}),
|
||||||
|
raw_qs = undefined :: undefined | binary(),
|
||||||
bindings = undefined :: undefined | cowboy_dispatcher:bindings(),
|
bindings = undefined :: undefined | cowboy_dispatcher:bindings(),
|
||||||
headers = [] :: http_headers(),
|
headers = [] :: http_headers(),
|
||||||
%% cookies = undefined :: undefined | http_cookies() %% @todo
|
%% cookies = undefined :: undefined | http_cookies() %% @todo
|
||||||
|
|
||||||
%% Request body.
|
%% Request body.
|
||||||
body_state = waiting :: waiting | done,
|
body_state = waiting :: waiting | done,
|
||||||
|
buffer = <<>> :: binary(),
|
||||||
|
|
||||||
%% Response.
|
%% Response.
|
||||||
resp_state = locked :: locked | waiting | done
|
resp_state = locked :: locked | waiting | done
|
||||||
|
|
|
@ -15,9 +15,9 @@
|
||||||
-module(cowboy_dispatcher).
|
-module(cowboy_dispatcher).
|
||||||
-export([split_host/1, split_path/1, match/3]). %% API.
|
-export([split_host/1, split_path/1, match/3]). %% API.
|
||||||
|
|
||||||
-type bindings() :: list({Key::atom(), Value::string()}).
|
-type bindings() :: list({Key::atom(), Value::binary()}).
|
||||||
-type path_tokens() :: list(nonempty_string()).
|
-type path_tokens() :: list(binary()).
|
||||||
-type match_rule() :: '_' | '*' | list(string() | '_' | atom()).
|
-type match_rule() :: '_' | '*' | list(binary() | '_' | atom()).
|
||||||
-type dispatch_rule() :: {Host::match_rule(), list({Path::match_rule(),
|
-type dispatch_rule() :: {Host::match_rule(), list({Path::match_rule(),
|
||||||
Handler::module(), Opts::term()})}.
|
Handler::module(), Opts::term()})}.
|
||||||
-type dispatch_rules() :: list(dispatch_rule()).
|
-type dispatch_rules() :: list(dispatch_rule()).
|
||||||
|
@ -29,25 +29,34 @@
|
||||||
|
|
||||||
%% API.
|
%% API.
|
||||||
|
|
||||||
-spec split_host(Host::string())
|
-spec split_host(Host::binary())
|
||||||
-> {Tokens::path_tokens(), Host::string(), Port::undefined | ip_port()}.
|
-> {Tokens::path_tokens(), RawHost::binary(), Port::undefined | ip_port()}.
|
||||||
|
split_host(<<>>) ->
|
||||||
|
{[], <<>>, undefined};
|
||||||
split_host(Host) ->
|
split_host(Host) ->
|
||||||
case string:chr(Host, $:) of
|
case binary:split(Host, <<":">>) of
|
||||||
0 -> {string:tokens(Host, "."), Host, undefined};
|
[Host] ->
|
||||||
N ->
|
{binary:split(Host, <<".">>, [global, trim]), Host, undefined};
|
||||||
{Host2, [$:|Port]} = lists:split(N - 1, Host),
|
[Host2, Port] ->
|
||||||
{string:tokens(Host2, "."), Host2, list_to_integer(Port)}
|
{binary:split(Host2, <<".">>, [global, trim]), Host2,
|
||||||
|
list_to_integer(binary_to_list(Port))}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec split_path(Path::string())
|
-spec split_path(Path::binary())
|
||||||
-> {Tokens::path_tokens(), Path::string(), Qs::string()}.
|
-> {Tokens::path_tokens(), RawPath::binary(), Qs::binary()}.
|
||||||
split_path(Path) ->
|
split_path(Path) ->
|
||||||
case string:chr(Path, $?) of
|
case binary:split(Path, <<"?">>) of
|
||||||
0 ->
|
[Path] -> {do_split_path(Path, <<"/">>), Path, <<>>};
|
||||||
{string:tokens(Path, "/"), Path, []};
|
[<<>>, Qs] -> {[], <<>>, Qs};
|
||||||
N ->
|
[Path2, Qs] -> {do_split_path(Path2, <<"/">>), Path2, Qs}
|
||||||
{Path2, [$?|Qs]} = lists:split(N - 1, Path),
|
end.
|
||||||
{string:tokens(Path2, "/"), Path2, Qs}
|
|
||||||
|
-spec do_split_path(RawPath::binary(), Separator::binary())
|
||||||
|
-> Tokens::path_tokens().
|
||||||
|
do_split_path(RawPath, Separator) ->
|
||||||
|
case binary:split(RawPath, Separator, [global, trim]) of
|
||||||
|
[<<>>|Path] -> Path;
|
||||||
|
Path -> Path
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec match(Host::path_tokens(), Path::path_tokens(),
|
-spec match(Host::path_tokens(), Path::path_tokens(),
|
||||||
|
@ -122,33 +131,40 @@ list_match([], [], Binds) ->
|
||||||
split_host_test_() ->
|
split_host_test_() ->
|
||||||
%% {Host, Result}
|
%% {Host, Result}
|
||||||
Tests = [
|
Tests = [
|
||||||
{"", {[], "", undefined}},
|
{<<"">>, {[], <<"">>, undefined}},
|
||||||
{".........", {[], ".........", undefined}},
|
{<<".........">>, {[], <<".........">>, undefined}},
|
||||||
{"*", {["*"], "*", undefined}},
|
{<<"*">>, {[<<"*">>], <<"*">>, undefined}},
|
||||||
{"cowboy.dev-extend.eu", {["cowboy", "dev-extend", "eu"],
|
{<<"cowboy.dev-extend.eu">>,
|
||||||
"cowboy.dev-extend.eu", undefined}},
|
{[<<"cowboy">>, <<"dev-extend">>, <<"eu">>],
|
||||||
{"dev-extend..eu",
|
<<"cowboy.dev-extend.eu">>, undefined}},
|
||||||
{["dev-extend", "eu"], "dev-extend..eu", undefined}},
|
{<<"dev-extend..eu">>,
|
||||||
{"dev-extend.eu", {["dev-extend", "eu"], "dev-extend.eu", undefined}},
|
{[<<"dev-extend">>, <<>>, <<"eu">>],
|
||||||
{"dev-extend.eu:8080", {["dev-extend", "eu"], "dev-extend.eu", 8080}},
|
<<"dev-extend..eu">>, undefined}},
|
||||||
{"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z",
|
{<<"dev-extend.eu">>,
|
||||||
{["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
|
{[<<"dev-extend">>, <<"eu">>], <<"dev-extend.eu">>, undefined}},
|
||||||
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"],
|
{<<"dev-extend.eu:8080">>,
|
||||||
"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z", undefined}}
|
{[<<"dev-extend">>, <<"eu">>], <<"dev-extend.eu">>, 8080}},
|
||||||
|
{<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>,
|
||||||
|
{[<<"a">>, <<"b">>, <<"c">>, <<"d">>, <<"e">>, <<"f">>, <<"g">>,
|
||||||
|
<<"h">>, <<"i">>, <<"j">>, <<"k">>, <<"l">>, <<"m">>, <<"n">>,
|
||||||
|
<<"o">>, <<"p">>, <<"q">>, <<"r">>, <<"s">>, <<"t">>, <<"u">>,
|
||||||
|
<<"v">>, <<"w">>, <<"x">>, <<"y">>, <<"z">>],
|
||||||
|
<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>,
|
||||||
|
undefined}}
|
||||||
],
|
],
|
||||||
[{H, fun() -> R = split_host(H) end} || {H, R} <- Tests].
|
[{H, fun() -> R = split_host(H) end} || {H, R} <- Tests].
|
||||||
|
|
||||||
split_host_fail_test_() ->
|
split_host_fail_test_() ->
|
||||||
Tests = [
|
Tests = [
|
||||||
"dev-extend.eu:owns",
|
<<"dev-extend.eu:owns">>,
|
||||||
"dev-extend.eu: owns",
|
<<"dev-extend.eu: owns">>,
|
||||||
"dev-extend.eu:42fun",
|
<<"dev-extend.eu:42fun">>,
|
||||||
"dev-extend.eu: 42fun",
|
<<"dev-extend.eu: 42fun">>,
|
||||||
"dev-extend.eu:42 fun",
|
<<"dev-extend.eu:42 fun">>,
|
||||||
"dev-extend.eu:fun 42",
|
<<"dev-extend.eu:fun 42">>,
|
||||||
"dev-extend.eu: 42",
|
<<"dev-extend.eu: 42">>,
|
||||||
":owns",
|
<<":owns">>,
|
||||||
":42 fun"
|
<<":42 fun">>
|
||||||
],
|
],
|
||||||
[{H, fun() -> case catch split_host(H) of
|
[{H, fun() -> case catch split_host(H) of
|
||||||
{'EXIT', _Reason} -> ok
|
{'EXIT', _Reason} -> ok
|
||||||
|
@ -157,58 +173,61 @@ split_host_fail_test_() ->
|
||||||
split_path_test_() ->
|
split_path_test_() ->
|
||||||
%% {Path, Result, QueryString}
|
%% {Path, Result, QueryString}
|
||||||
Tests = [
|
Tests = [
|
||||||
{"?", [], "", ""},
|
{<<"?">>, [], <<"">>, <<"">>},
|
||||||
{"???", [], "", "??"},
|
{<<"???">>, [], <<"">>, <<"??">>},
|
||||||
{"/", [], "/", ""},
|
{<<"/">>, [], <<"/">>, <<"">>},
|
||||||
{"/users", ["users"], "/users", ""},
|
{<<"/users">>, [<<"users">>], <<"/users">>, <<"">>},
|
||||||
{"/users?", ["users"], "/users", ""},
|
{<<"/users?">>, [<<"users">>], <<"/users">>, <<"">>},
|
||||||
{"/users?a", ["users"], "/users", "a"},
|
{<<"/users?a">>, [<<"users">>], <<"/users">>, <<"a">>},
|
||||||
{"/users/42/friends?a=b&c=d&e=notsure?whatever",
|
{<<"/users/42/friends?a=b&c=d&e=notsure?whatever">>,
|
||||||
["users", "42", "friends"],
|
[<<"users">>, <<"42">>, <<"friends">>],
|
||||||
"/users/42/friends", "a=b&c=d&e=notsure?whatever"}
|
<<"/users/42/friends">>, <<"a=b&c=d&e=notsure?whatever">>}
|
||||||
],
|
],
|
||||||
[{P, fun() -> {R, RawP, Qs} = split_path(P) end} || {P, R, RawP, Qs} <- Tests].
|
[{P, fun() -> {R, RawP, Qs} = split_path(P) end}
|
||||||
|
|| {P, R, RawP, Qs} <- Tests].
|
||||||
|
|
||||||
match_test_() ->
|
match_test_() ->
|
||||||
Dispatch = [
|
Dispatch = [
|
||||||
{["www", '_', "dev-extend", "eu"], [
|
{[<<"www">>, '_', <<"dev-extend">>, <<"eu">>], [
|
||||||
{["users", '_', "mails"], match_any_subdomain_users, []}
|
{[<<"users">>, '_', <<"mails">>], match_any_subdomain_users, []}
|
||||||
]},
|
]},
|
||||||
{["dev-extend", "eu"], [
|
{[<<"dev-extend">>, <<"eu">>], [
|
||||||
{["users", id, "friends"], match_extend_users_friends, []},
|
{[<<"users">>, id, <<"friends">>], match_extend_users_friends, []},
|
||||||
{'_', match_extend, []}
|
{'_', match_extend, []}
|
||||||
]},
|
]},
|
||||||
{["dev-extend", var], [
|
{[<<"dev-extend">>, var], [
|
||||||
{["threads", var], match_duplicate_vars,
|
{[<<"threads">>, var], match_duplicate_vars,
|
||||||
[we, {expect, two}, var, here]}
|
[we, {expect, two}, var, here]}
|
||||||
]},
|
]},
|
||||||
{["erlang", ext], [
|
{[<<"erlang">>, ext], [
|
||||||
{'_', match_erlang_ext, []}
|
{'_', match_erlang_ext, []}
|
||||||
]},
|
]},
|
||||||
{'_', [
|
{'_', [
|
||||||
{["users", id, "friends"], match_users_friends, []},
|
{[<<"users">>, id, <<"friends">>], match_users_friends, []},
|
||||||
{'_', match_any, []}
|
{'_', match_any, []}
|
||||||
]}
|
]}
|
||||||
],
|
],
|
||||||
%% {Host, Path, Result}
|
%% {Host, Path, Result}
|
||||||
Tests = [
|
Tests = [
|
||||||
{["any"], [], {ok, match_any, [], []}},
|
{[<<"any">>], [], {ok, match_any, [], []}},
|
||||||
{["www", "any", "dev-extend", "eu"], ["users", "42", "mails"],
|
{[<<"www">>, <<"any">>, <<"dev-extend">>, <<"eu">>],
|
||||||
|
[<<"users">>, <<"42">>, <<"mails">>],
|
||||||
{ok, match_any_subdomain_users, [], []}},
|
{ok, match_any_subdomain_users, [], []}},
|
||||||
{["www", "dev-extend", "eu"], ["users", "42", "mails"],
|
{[<<"www">>, <<"dev-extend">>, <<"eu">>],
|
||||||
{ok, match_any, [], []}},
|
[<<"users">>, <<"42">>, <<"mails">>], {ok, match_any, [], []}},
|
||||||
{["www", "dev-extend", "eu"], [], {ok, match_any, [], []}},
|
{[<<"www">>, <<"dev-extend">>, <<"eu">>], [], {ok, match_any, [], []}},
|
||||||
{["www", "any", "dev-extend", "eu"], ["not_users", "42", "mails"],
|
{[<<"www">>, <<"any">>, <<"dev-extend">>, <<"eu">>],
|
||||||
{error, notfound, path}},
|
[<<"not_users">>, <<"42">>, <<"mails">>], {error, notfound, path}},
|
||||||
{["dev-extend", "eu"], [], {ok, match_extend, [], []}},
|
{[<<"dev-extend">>, <<"eu">>], [], {ok, match_extend, [], []}},
|
||||||
{["dev-extend", "eu"], ["users", "42", "friends"],
|
{[<<"dev-extend">>, <<"eu">>], [<<"users">>, <<"42">>, <<"friends">>],
|
||||||
{ok, match_extend_users_friends, [], [{id, "42"}]}},
|
{ok, match_extend_users_friends, [], [{id, <<"42">>}]}},
|
||||||
{["erlang", "fr"], '_', {ok, match_erlang_ext, [], [{ext, "fr"}]}},
|
{[<<"erlang">>, <<"fr">>], '_',
|
||||||
{["any"], ["users", "444", "friends"],
|
{ok, match_erlang_ext, [], [{ext, <<"fr">>}]}},
|
||||||
{ok, match_users_friends, [], [{id, "444"}]}},
|
{[<<"any">>], [<<"users">>, <<"444">>, <<"friends">>],
|
||||||
{["dev-extend", "fr"], ["threads", "987"],
|
{ok, match_users_friends, [], [{id, <<"444">>}]}},
|
||||||
|
{[<<"dev-extend">>, <<"fr">>], [<<"threads">>, <<"987">>],
|
||||||
{ok, match_duplicate_vars, [we, {expect, two}, var, here],
|
{ok, match_duplicate_vars, [we, {expect, two}, var, here],
|
||||||
[{var, "fr"}, {var, "987"}]}}
|
[{var, <<"fr">>}, {var, <<"987">>}]}}
|
||||||
],
|
],
|
||||||
[{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
|
[{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
|
||||||
R = match(H, P, Dispatch)
|
R = match(H, P, Dispatch)
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
-module(cowboy_http_protocol).
|
-module(cowboy_http_protocol).
|
||||||
-export([start_link/3]). %% API.
|
-export([start_link/3]). %% API.
|
||||||
-export([init/3, wait_request/1]). %% FSM.
|
-export([init/3, parse_request/1]). %% FSM.
|
||||||
|
|
||||||
-include("include/http.hrl").
|
-include("include/http.hrl").
|
||||||
|
|
||||||
|
@ -26,7 +26,8 @@
|
||||||
req_empty_lines = 0 :: integer(),
|
req_empty_lines = 0 :: integer(),
|
||||||
max_empty_lines :: integer(),
|
max_empty_lines :: integer(),
|
||||||
timeout :: timeout(),
|
timeout :: timeout(),
|
||||||
connection = keepalive :: keepalive | close
|
connection = keepalive :: keepalive | close,
|
||||||
|
buffer = <<>> :: binary()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
%% API.
|
%% API.
|
||||||
|
@ -47,11 +48,21 @@ init(Socket, Transport, Opts) ->
|
||||||
wait_request(#state{socket=Socket, transport=Transport,
|
wait_request(#state{socket=Socket, transport=Transport,
|
||||||
dispatch=Dispatch, max_empty_lines=MaxEmptyLines, timeout=Timeout}).
|
dispatch=Dispatch, max_empty_lines=MaxEmptyLines, timeout=Timeout}).
|
||||||
|
|
||||||
|
-spec parse_request(State::#state{}) -> ok.
|
||||||
|
%% @todo Use decode_packet options to limit length?
|
||||||
|
parse_request(State=#state{buffer=Buffer}) ->
|
||||||
|
case erlang:decode_packet(http_bin, Buffer, []) of
|
||||||
|
{ok, Request, Rest} -> request(Request, State#state{buffer=Rest});
|
||||||
|
{more, _Length} -> wait_request(State);
|
||||||
|
{error, _Reason} -> error_response(400, State)
|
||||||
|
end.
|
||||||
|
|
||||||
-spec wait_request(State::#state{}) -> ok.
|
-spec wait_request(State::#state{}) -> ok.
|
||||||
wait_request(State=#state{socket=Socket, transport=Transport, timeout=T}) ->
|
wait_request(State=#state{socket=Socket, transport=Transport,
|
||||||
Transport:setopts(Socket, [{packet, http}]),
|
timeout=T, buffer=Buffer}) ->
|
||||||
case Transport:recv(Socket, 0, T) of
|
case Transport:recv(Socket, 0, T) of
|
||||||
{ok, Request} -> request(Request, State);
|
{ok, Data} -> parse_request(State#state{
|
||||||
|
buffer= << Buffer/binary, Data/binary >>});
|
||||||
{error, timeout} -> error_terminate(408, State);
|
{error, timeout} -> error_terminate(408, State);
|
||||||
{error, closed} -> terminate(State)
|
{error, closed} -> terminate(State)
|
||||||
end.
|
end.
|
||||||
|
@ -67,41 +78,50 @@ request({http_request, Method, {abs_path, AbsPath}, Version},
|
||||||
State=#state{socket=Socket, transport=Transport}) ->
|
State=#state{socket=Socket, transport=Transport}) ->
|
||||||
{Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath),
|
{Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath),
|
||||||
ConnAtom = version_to_connection(Version),
|
ConnAtom = version_to_connection(Version),
|
||||||
wait_header(#http_req{socket=Socket, transport=Transport,
|
parse_header(#http_req{socket=Socket, transport=Transport,
|
||||||
connection=ConnAtom, method=Method, version=Version,
|
connection=ConnAtom, method=Method, version=Version,
|
||||||
path=Path, raw_path=RawPath, raw_qs=Qs},
|
path=Path, raw_path=RawPath, raw_qs=Qs},
|
||||||
State#state{connection=ConnAtom});
|
State#state{connection=ConnAtom});
|
||||||
request({http_request, Method, '*', Version},
|
request({http_request, Method, '*', Version},
|
||||||
State=#state{socket=Socket, transport=Transport}) ->
|
State=#state{socket=Socket, transport=Transport}) ->
|
||||||
ConnAtom = version_to_connection(Version),
|
ConnAtom = version_to_connection(Version),
|
||||||
wait_header(#http_req{socket=Socket, transport=Transport,
|
parse_header(#http_req{socket=Socket, transport=Transport,
|
||||||
connection=ConnAtom, method=Method, version=Version,
|
connection=ConnAtom, method=Method, version=Version,
|
||||||
path='*', raw_path="*", raw_qs=[]},
|
path='*', raw_path= <<"*">>, raw_qs= <<>>},
|
||||||
State#state{connection=ConnAtom});
|
State#state{connection=ConnAtom});
|
||||||
request({http_request, _Method, _URI, _Version}, State) ->
|
request({http_request, _Method, _URI, _Version}, State) ->
|
||||||
error_terminate(501, State);
|
error_terminate(501, State);
|
||||||
request({http_error, "\r\n"},
|
request({http_error, <<"\r\n">>},
|
||||||
State=#state{req_empty_lines=N, max_empty_lines=N}) ->
|
State=#state{req_empty_lines=N, max_empty_lines=N}) ->
|
||||||
error_terminate(400, State);
|
error_terminate(400, State);
|
||||||
request({http_error, "\r\n"}, State=#state{req_empty_lines=N}) ->
|
request({http_error, <<"\r\n">>}, State=#state{req_empty_lines=N}) ->
|
||||||
wait_request(State#state{req_empty_lines=N + 1});
|
parse_request(State#state{req_empty_lines=N + 1});
|
||||||
request({http_error, _Any}, State) ->
|
request({http_error, _Any}, State) ->
|
||||||
error_terminate(400, State).
|
error_terminate(400, State).
|
||||||
|
|
||||||
|
-spec parse_header(Req::#http_req{}, State::#state{}) -> ok.
|
||||||
|
parse_header(Req, State=#state{buffer=Buffer}) ->
|
||||||
|
case erlang:decode_packet(httph_bin, Buffer, []) of
|
||||||
|
{ok, Header, Rest} -> header(Header, Req, State#state{buffer=Rest});
|
||||||
|
{more, _Length} -> wait_header(Req, State);
|
||||||
|
{error, _Reason} -> error_response(400, State)
|
||||||
|
end.
|
||||||
|
|
||||||
-spec wait_header(Req::#http_req{}, State::#state{}) -> ok.
|
-spec wait_header(Req::#http_req{}, State::#state{}) -> ok.
|
||||||
wait_header(Req, State=#state{socket=Socket,
|
wait_header(Req, State=#state{socket=Socket,
|
||||||
transport=Transport, timeout=T}) ->
|
transport=Transport, timeout=T, buffer=Buffer}) ->
|
||||||
case Transport:recv(Socket, 0, T) of
|
case Transport:recv(Socket, 0, T) of
|
||||||
{ok, Header} -> header(Header, Req, State);
|
{ok, Data} -> parse_header(Req, State#state{
|
||||||
|
buffer= << Buffer/binary, Data/binary >>});
|
||||||
{error, timeout} -> error_terminate(408, State);
|
{error, timeout} -> error_terminate(408, State);
|
||||||
{error, closed} -> terminate(State)
|
{error, closed} -> terminate(State)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec header({http_header, I::integer(), Field::http_header(), R::term(),
|
-spec header({http_header, I::integer(), Field::http_header(), R::term(),
|
||||||
Value::string()} | http_eoh, Req::#http_req{}, State::#state{}) -> ok.
|
Value::binary()} | http_eoh, Req::#http_req{}, State::#state{}) -> ok.
|
||||||
header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{
|
header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{
|
||||||
transport=Transport, host=undefined}, State) ->
|
transport=Transport, host=undefined}, State) ->
|
||||||
RawHost2 = string_to_lower(RawHost),
|
RawHost2 = binary_to_lower(RawHost),
|
||||||
case catch cowboy_dispatcher:split_host(RawHost2) of
|
case catch cowboy_dispatcher:split_host(RawHost2) of
|
||||||
{Host, RawHost3, undefined} ->
|
{Host, RawHost3, undefined} ->
|
||||||
Port = default_port(Transport:name()),
|
Port = default_port(Transport:name()),
|
||||||
|
@ -115,21 +135,21 @@ header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{
|
||||||
end;
|
end;
|
||||||
%% Ignore Host headers if we already have it.
|
%% Ignore Host headers if we already have it.
|
||||||
header({http_header, _I, 'Host', _R, _V}, Req, State) ->
|
header({http_header, _I, 'Host', _R, _V}, Req, State) ->
|
||||||
wait_header(Req, State);
|
parse_header(Req, State);
|
||||||
header({http_header, _I, 'Connection', _R, Connection}, Req, State) ->
|
header({http_header, _I, 'Connection', _R, Connection}, Req, State) ->
|
||||||
ConnAtom = connection_to_atom(Connection),
|
ConnAtom = connection_to_atom(Connection),
|
||||||
wait_header(Req#http_req{connection=ConnAtom,
|
parse_header(Req#http_req{connection=ConnAtom,
|
||||||
headers=[{'Connection', Connection}|Req#http_req.headers]},
|
headers=[{'Connection', Connection}|Req#http_req.headers]},
|
||||||
State#state{connection=ConnAtom});
|
State#state{connection=ConnAtom});
|
||||||
header({http_header, _I, Field, _R, Value}, Req, State) ->
|
header({http_header, _I, Field, _R, Value}, Req, State) ->
|
||||||
wait_header(Req#http_req{headers=[{Field, Value}|Req#http_req.headers]},
|
parse_header(Req#http_req{headers=[{Field, Value}|Req#http_req.headers]},
|
||||||
State);
|
State);
|
||||||
%% The Host header is required.
|
%% The Host header is required.
|
||||||
header(http_eoh, #http_req{host=undefined}, State) ->
|
header(http_eoh, #http_req{host=undefined}, State) ->
|
||||||
error_terminate(400, State);
|
error_terminate(400, State);
|
||||||
header(http_eoh, Req, State) ->
|
header(http_eoh, Req, State=#state{buffer=Buffer}) ->
|
||||||
handler_init(Req, State);
|
handler_init(Req#http_req{buffer=Buffer}, State#state{buffer= <<>>});
|
||||||
header({http_error, _String}, _Req, State) ->
|
header({http_error, _Bin}, _Req, State) ->
|
||||||
error_terminate(500, State).
|
error_terminate(500, State).
|
||||||
|
|
||||||
-spec dispatch(Req::#http_req{}, State::#state{}) -> ok.
|
-spec dispatch(Req::#http_req{}, State::#state{}) -> ok.
|
||||||
|
@ -139,7 +159,7 @@ dispatch(Req=#http_req{host=Host, path=Path},
|
||||||
%% things like url rewriting.
|
%% things like url rewriting.
|
||||||
case cowboy_dispatcher:match(Host, Path, Dispatch) of
|
case cowboy_dispatcher:match(Host, Path, Dispatch) of
|
||||||
{ok, Handler, Opts, Binds} ->
|
{ok, Handler, Opts, Binds} ->
|
||||||
wait_header(Req#http_req{bindings=Binds},
|
parse_header(Req#http_req{bindings=Binds},
|
||||||
State#state{handler={Handler, Opts}});
|
State#state{handler={Handler, Opts}});
|
||||||
{error, notfound, host} ->
|
{error, notfound, host} ->
|
||||||
error_terminate(400, State);
|
error_terminate(400, State);
|
||||||
|
@ -173,14 +193,17 @@ handler_loop(HandlerState, Req, State=#state{handler={Handler, _Opts}}) ->
|
||||||
|
|
||||||
-spec handler_terminate(HandlerState::term(), Req::#http_req{},
|
-spec handler_terminate(HandlerState::term(), Req::#http_req{},
|
||||||
State::#state{}) -> ok.
|
State::#state{}) -> ok.
|
||||||
handler_terminate(HandlerState, Req, State=#state{handler={Handler, _Opts}}) ->
|
handler_terminate(HandlerState, Req=#http_req{buffer=Buffer},
|
||||||
|
State=#state{handler={Handler, _Opts}}) ->
|
||||||
HandlerRes = (catch Handler:terminate(
|
HandlerRes = (catch Handler:terminate(
|
||||||
Req#http_req{resp_state=locked}, HandlerState)),
|
Req#http_req{resp_state=locked}, HandlerState)),
|
||||||
BodyRes = ensure_body_processed(Req),
|
BodyRes = ensure_body_processed(Req),
|
||||||
ensure_response(Req, State),
|
ensure_response(Req, State),
|
||||||
case {HandlerRes, BodyRes, State#state.connection} of
|
case {HandlerRes, BodyRes, State#state.connection} of
|
||||||
{ok, ok, keepalive} -> ?MODULE:wait_request(State);
|
{ok, ok, keepalive} ->
|
||||||
_Closed -> terminate(State)
|
?MODULE:parse_request(State#state{buffer=Buffer});
|
||||||
|
_Closed ->
|
||||||
|
terminate(State)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec ensure_body_processed(Req::#http_req{}) -> ok | close.
|
-spec ensure_body_processed(Req::#http_req{}) -> ok | close.
|
||||||
|
@ -226,14 +249,14 @@ terminate(#state{socket=Socket, transport=Transport}) ->
|
||||||
version_to_connection({1, 1}) -> keepalive;
|
version_to_connection({1, 1}) -> keepalive;
|
||||||
version_to_connection(_Any) -> close.
|
version_to_connection(_Any) -> close.
|
||||||
|
|
||||||
-spec connection_to_atom(Connection::string()) -> keepalive | close.
|
-spec connection_to_atom(Connection::binary()) -> keepalive | close.
|
||||||
connection_to_atom("keep-alive") ->
|
connection_to_atom(<<"keep-alive">>) ->
|
||||||
keepalive;
|
keepalive;
|
||||||
connection_to_atom("close") ->
|
connection_to_atom(<<"close">>) ->
|
||||||
close;
|
close;
|
||||||
connection_to_atom(Connection) ->
|
connection_to_atom(Connection) ->
|
||||||
case string_to_lower(Connection) of
|
case binary_to_lower(Connection) of
|
||||||
"close" -> close;
|
<<"close">> -> close;
|
||||||
_Any -> keepalive
|
_Any -> keepalive
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -241,11 +264,10 @@ connection_to_atom(Connection) ->
|
||||||
default_port(ssl) -> 443;
|
default_port(ssl) -> 443;
|
||||||
default_port(_) -> 80.
|
default_port(_) -> 80.
|
||||||
|
|
||||||
%% More efficient implementation of string:to_lower.
|
|
||||||
%% We are excluding a few characters on purpose.
|
%% We are excluding a few characters on purpose.
|
||||||
-spec string_to_lower(string()) -> string().
|
-spec binary_to_lower(binary()) -> binary().
|
||||||
string_to_lower(L) ->
|
binary_to_lower(L) ->
|
||||||
[char_to_lower(C) || C <- L].
|
<< << (char_to_lower(C)) >> || << C >> <= L >>.
|
||||||
|
|
||||||
%% We gain noticeable speed by matching each value directly.
|
%% We gain noticeable speed by matching each value directly.
|
||||||
-spec char_to_lower(char()) -> char().
|
-spec char_to_lower(char()) -> char().
|
||||||
|
|
|
@ -59,7 +59,7 @@ peer(Req) ->
|
||||||
host(Req) ->
|
host(Req) ->
|
||||||
{Req#http_req.host, Req}.
|
{Req#http_req.host, Req}.
|
||||||
|
|
||||||
-spec raw_host(Req::#http_req{}) -> {RawHost::string(), Req::#http_req{}}.
|
-spec raw_host(Req::#http_req{}) -> {RawHost::binary(), Req::#http_req{}}.
|
||||||
raw_host(Req) ->
|
raw_host(Req) ->
|
||||||
{Req#http_req.raw_host, Req}.
|
{Req#http_req.raw_host, Req}.
|
||||||
|
|
||||||
|
@ -72,18 +72,18 @@ port(Req) ->
|
||||||
path(Req) ->
|
path(Req) ->
|
||||||
{Req#http_req.path, Req}.
|
{Req#http_req.path, Req}.
|
||||||
|
|
||||||
-spec raw_path(Req::#http_req{}) -> {RawPath::string(), Req::#http_req{}}.
|
-spec raw_path(Req::#http_req{}) -> {RawPath::binary(), Req::#http_req{}}.
|
||||||
raw_path(Req) ->
|
raw_path(Req) ->
|
||||||
{Req#http_req.raw_path, Req}.
|
{Req#http_req.raw_path, Req}.
|
||||||
|
|
||||||
-spec qs_val(Name::string(), Req::#http_req{})
|
-spec qs_val(Name::binary(), Req::#http_req{})
|
||||||
-> {Value::string() | true | undefined, Req::#http_req{}}.
|
-> {Value::binary() | true | undefined, Req::#http_req{}}.
|
||||||
%% @equiv qs_val(Name, Req) -> qs_val(Name, Req, undefined)
|
%% @equiv qs_val(Name, Req) -> qs_val(Name, Req, undefined)
|
||||||
qs_val(Name, Req) ->
|
qs_val(Name, Req) ->
|
||||||
qs_val(Name, Req, undefined).
|
qs_val(Name, Req, undefined).
|
||||||
|
|
||||||
-spec qs_val(Name::string(), Req::#http_req{}, Default)
|
-spec qs_val(Name::binary(), Req::#http_req{}, Default)
|
||||||
-> {Value::string() | true | Default, Req::#http_req{}}
|
-> {Value::binary() | true | Default, Req::#http_req{}}
|
||||||
when Default::term().
|
when Default::term().
|
||||||
qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined}, Default) ->
|
qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined}, Default) ->
|
||||||
QsVals = parse_qs(RawQs),
|
QsVals = parse_qs(RawQs),
|
||||||
|
@ -95,25 +95,25 @@ qs_val(Name, Req, Default) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec qs_vals(Req::#http_req{})
|
-spec qs_vals(Req::#http_req{})
|
||||||
-> {list({Name::string(), Value::string() | true}), Req::#http_req{}}.
|
-> {list({Name::binary(), Value::binary() | true}), Req::#http_req{}}.
|
||||||
qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined}) ->
|
qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined}) ->
|
||||||
QsVals = parse_qs(RawQs),
|
QsVals = parse_qs(RawQs),
|
||||||
qs_vals(Req#http_req{qs_vals=QsVals});
|
qs_vals(Req#http_req{qs_vals=QsVals});
|
||||||
qs_vals(Req=#http_req{qs_vals=QsVals}) ->
|
qs_vals(Req=#http_req{qs_vals=QsVals}) ->
|
||||||
{QsVals, Req}.
|
{QsVals, Req}.
|
||||||
|
|
||||||
-spec raw_qs(Req::#http_req{}) -> {RawQs::string(), Req::#http_req{}}.
|
-spec raw_qs(Req::#http_req{}) -> {RawQs::binary(), Req::#http_req{}}.
|
||||||
raw_qs(Req) ->
|
raw_qs(Req) ->
|
||||||
{Req#http_req.raw_qs, Req}.
|
{Req#http_req.raw_qs, Req}.
|
||||||
|
|
||||||
-spec binding(Name::atom(), Req::#http_req{})
|
-spec binding(Name::atom(), Req::#http_req{})
|
||||||
-> {Value::string() | undefined, Req::#http_req{}}.
|
-> {Value::binary() | undefined, Req::#http_req{}}.
|
||||||
%% @equiv binding(Name, Req) -> binding(Name, Req, undefined)
|
%% @equiv binding(Name, Req) -> binding(Name, Req, undefined)
|
||||||
binding(Name, Req) ->
|
binding(Name, Req) ->
|
||||||
binding(Name, Req, undefined).
|
binding(Name, Req, undefined).
|
||||||
|
|
||||||
-spec binding(Name::atom(), Req::#http_req{}, Default)
|
-spec binding(Name::atom(), Req::#http_req{}, Default)
|
||||||
-> {Value::string() | Default, Req::#http_req{}} when Default::term().
|
-> {Value::binary() | Default, Req::#http_req{}} when Default::term().
|
||||||
binding(Name, Req, Default) ->
|
binding(Name, Req, Default) ->
|
||||||
case lists:keyfind(Name, 1, Req#http_req.bindings) of
|
case lists:keyfind(Name, 1, Req#http_req.bindings) of
|
||||||
{Name, Value} -> {Value, Req};
|
{Name, Value} -> {Value, Req};
|
||||||
|
@ -121,18 +121,18 @@ binding(Name, Req, Default) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec bindings(Req::#http_req{})
|
-spec bindings(Req::#http_req{})
|
||||||
-> {list({Name::atom(), Value::string()}), Req::#http_req{}}.
|
-> {list({Name::atom(), Value::binary()}), Req::#http_req{}}.
|
||||||
bindings(Req) ->
|
bindings(Req) ->
|
||||||
{Req#http_req.bindings, Req}.
|
{Req#http_req.bindings, Req}.
|
||||||
|
|
||||||
-spec header(Name::atom() | string(), Req::#http_req{})
|
-spec header(Name::atom() | binary(), Req::#http_req{})
|
||||||
-> {Value::string() | undefined, Req::#http_req{}}.
|
-> {Value::binary() | undefined, Req::#http_req{}}.
|
||||||
%% @equiv header(Name, Req) -> header(Name, Req, undefined)
|
%% @equiv header(Name, Req) -> header(Name, Req, undefined)
|
||||||
header(Name, Req) ->
|
header(Name, Req) ->
|
||||||
header(Name, Req, undefined).
|
header(Name, Req, undefined).
|
||||||
|
|
||||||
-spec header(Name::atom() | string(), Req::#http_req{}, Default)
|
-spec header(Name::atom() | binary(), Req::#http_req{}, Default)
|
||||||
-> {Value::string() | Default, Req::#http_req{}} when Default::term().
|
-> {Value::binary() | Default, Req::#http_req{}} when Default::term().
|
||||||
header(Name, Req, Default) ->
|
header(Name, Req, Default) ->
|
||||||
case lists:keyfind(Name, 1, Req#http_req.headers) of
|
case lists:keyfind(Name, 1, Req#http_req.headers) of
|
||||||
{Name, Value} -> {Value, Req};
|
{Name, Value} -> {Value, Req};
|
||||||
|
@ -154,26 +154,28 @@ body(Req) ->
|
||||||
case Length of
|
case Length of
|
||||||
undefined -> {error, badarg};
|
undefined -> {error, badarg};
|
||||||
_Any ->
|
_Any ->
|
||||||
Length2 = list_to_integer(Length),
|
Length2 = list_to_integer(binary_to_list(Length)),
|
||||||
body(Length2, Req2)
|
body(Length2, Req2)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
%% @todo We probably want to configure the timeout.
|
%% @todo We probably want to configure the timeout.
|
||||||
-spec body(Length::non_neg_integer(), Req::#http_req{})
|
-spec body(Length::non_neg_integer(), Req::#http_req{})
|
||||||
-> {ok, Body::binary(), Req::#http_req{}} | {error, Reason::atom()}.
|
-> {ok, Body::binary(), Req::#http_req{}} | {error, Reason::atom()}.
|
||||||
|
body(Length, Req=#http_req{body_state=waiting, buffer=Buffer})
|
||||||
|
when Length =:= byte_size(Buffer) ->
|
||||||
|
{ok, Buffer, Req#http_req{body_state=done, buffer= <<>>}};
|
||||||
body(Length, Req=#http_req{socket=Socket, transport=Transport,
|
body(Length, Req=#http_req{socket=Socket, transport=Transport,
|
||||||
body_state=waiting}) ->
|
body_state=waiting, buffer=Buffer}) when Length > byte_size(Buffer) ->
|
||||||
Transport:setopts(Socket, [{packet, raw}]),
|
case Transport:recv(Socket, Length - byte_size(Buffer), 5000) of
|
||||||
case Transport:recv(Socket, Length, 5000) of
|
{ok, Body} -> {ok, << Buffer/binary, Body/binary >>, Req#http_req{body_state=done, buffer= <<>>}};
|
||||||
{ok, Body} -> {ok, Body, Req#http_req{body_state=done}};
|
|
||||||
{error, Reason} -> {error, Reason}
|
{error, Reason} -> {error, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec body_qs(Req::#http_req{})
|
-spec body_qs(Req::#http_req{})
|
||||||
-> {list({Name::string(), Value::string() | true}), Req::#http_req{}}.
|
-> {list({Name::binary(), Value::binary() | true}), Req::#http_req{}}.
|
||||||
body_qs(Req) ->
|
body_qs(Req) ->
|
||||||
{ok, Body, Req2} = body(Req),
|
{ok, Body, Req2} = body(Req),
|
||||||
{parse_qs(binary_to_list(Body)), Req2}.
|
{parse_qs(Body), Req2}.
|
||||||
|
|
||||||
%% Response API.
|
%% Response API.
|
||||||
|
|
||||||
|
@ -182,91 +184,90 @@ body_qs(Req) ->
|
||||||
reply(Code, Headers, Body, Req=#http_req{socket=Socket,
|
reply(Code, Headers, Body, Req=#http_req{socket=Socket,
|
||||||
transport=Transport, connection=Connection,
|
transport=Transport, connection=Connection,
|
||||||
resp_state=waiting}) ->
|
resp_state=waiting}) ->
|
||||||
StatusLine = ["HTTP/1.1 ", status(Code), "\r\n"],
|
StatusLine = <<"HTTP/1.1 ", (status(Code))/binary, "\r\n">>,
|
||||||
DefaultHeaders = [
|
DefaultHeaders = [
|
||||||
{"Connection", atom_to_connection(Connection)},
|
{<<"Connection">>, atom_to_connection(Connection)},
|
||||||
{"Content-Length", integer_to_list(iolist_size(Body))}
|
{<<"Content-Length">>, list_to_binary(integer_to_list(iolist_size(Body)))}
|
||||||
],
|
],
|
||||||
Headers2 = lists:keysort(1, Headers),
|
Headers2 = lists:keysort(1, Headers),
|
||||||
Headers3 = lists:ukeymerge(1, Headers2, DefaultHeaders),
|
Headers3 = lists:ukeymerge(1, Headers2, DefaultHeaders),
|
||||||
Headers4 = [[Key, ": ", Value, "\r\n"] || {Key, Value} <- Headers3],
|
Headers4 = [<< Key/binary, ": ", Value/binary, "\r\n">> || {Key, Value} <- Headers3],
|
||||||
Transport:send(Socket, [StatusLine, Headers4, "\r\n", Body]),
|
Transport:send(Socket, [StatusLine, Headers4, <<"\r\n">>, Body]),
|
||||||
{ok, Req#http_req{resp_state=done}}.
|
{ok, Req#http_req{resp_state=done}}.
|
||||||
|
|
||||||
%% Internal.
|
%% Internal.
|
||||||
|
|
||||||
-spec parse_qs(Qs::string()) -> list({Name::string(), Value::string() | true}).
|
-spec parse_qs(Qs::binary()) -> list({Name::binary(), Value::binary() | true}).
|
||||||
|
parse_qs(<<>>) ->
|
||||||
|
[];
|
||||||
parse_qs(Qs) ->
|
parse_qs(Qs) ->
|
||||||
Tokens = string:tokens(Qs, "&"),
|
Tokens = binary:split(Qs, <<"&">>, [global, trim]),
|
||||||
[case string:chr(Token, $=) of
|
[case binary:split(Token, <<"=">>) of
|
||||||
0 ->
|
[Token] -> {Token, true};
|
||||||
{Token, true};
|
[Name, Value] -> {Name, Value}
|
||||||
N ->
|
|
||||||
{Name, [$=|Value]} = lists:split(N - 1, Token),
|
|
||||||
{Name, Value}
|
|
||||||
end || Token <- Tokens].
|
end || Token <- Tokens].
|
||||||
|
|
||||||
-spec atom_to_connection(Atom::keepalive | close) -> string().
|
-spec atom_to_connection(Atom::keepalive | close) -> binary().
|
||||||
atom_to_connection(keepalive) ->
|
atom_to_connection(keepalive) ->
|
||||||
"keep-alive";
|
<<"keep-alive">>;
|
||||||
atom_to_connection(close) ->
|
atom_to_connection(close) ->
|
||||||
"close".
|
<<"close">>.
|
||||||
|
|
||||||
-spec status(Code::http_status()) -> string().
|
-spec status(Code::http_status()) -> binary().
|
||||||
status(100) -> "100 Continue";
|
status(100) -> <<"100 Continue">>;
|
||||||
status(101) -> "101 Switching Protocols";
|
status(101) -> <<"101 Switching Protocols">>;
|
||||||
status(102) -> "102 Processing";
|
status(102) -> <<"102 Processing">>;
|
||||||
status(200) -> "200 OK";
|
status(200) -> <<"200 OK">>;
|
||||||
status(201) -> "201 Created";
|
status(201) -> <<"201 Created">>;
|
||||||
status(202) -> "202 Accepted";
|
status(202) -> <<"202 Accepted">>;
|
||||||
status(203) -> "203 Non-Authoritative Information";
|
status(203) -> <<"203 Non-Authoritative Information">>;
|
||||||
status(204) -> "204 No Content";
|
status(204) -> <<"204 No Content">>;
|
||||||
status(205) -> "205 Reset Content";
|
status(205) -> <<"205 Reset Content">>;
|
||||||
status(206) -> "206 Partial Content";
|
status(206) -> <<"206 Partial Content">>;
|
||||||
status(207) -> "207 Multi-Status";
|
status(207) -> <<"207 Multi-Status">>;
|
||||||
status(226) -> "226 IM Used";
|
status(226) -> <<"226 IM Used">>;
|
||||||
status(300) -> "300 Multiple Choices";
|
status(300) -> <<"300 Multiple Choices">>;
|
||||||
status(301) -> "301 Moved Permanently";
|
status(301) -> <<"301 Moved Permanently">>;
|
||||||
status(302) -> "302 Found";
|
status(302) -> <<"302 Found">>;
|
||||||
status(303) -> "303 See Other";
|
status(303) -> <<"303 See Other">>;
|
||||||
status(304) -> "304 Not Modified";
|
status(304) -> <<"304 Not Modified">>;
|
||||||
status(305) -> "305 Use Proxy";
|
status(305) -> <<"305 Use Proxy">>;
|
||||||
status(306) -> "306 Switch Proxy";
|
status(306) -> <<"306 Switch Proxy">>;
|
||||||
status(307) -> "307 Temporary Redirect";
|
status(307) -> <<"307 Temporary Redirect">>;
|
||||||
status(400) -> "400 Bad Request";
|
status(400) -> <<"400 Bad Request">>;
|
||||||
status(401) -> "401 Unauthorized";
|
status(401) -> <<"401 Unauthorized">>;
|
||||||
status(402) -> "402 Payment Required";
|
status(402) -> <<"402 Payment Required">>;
|
||||||
status(403) -> "403 Forbidden";
|
status(403) -> <<"403 Forbidden">>;
|
||||||
status(404) -> "404 Not Found";
|
status(404) -> <<"404 Not Found">>;
|
||||||
status(405) -> "405 Method Not Allowed";
|
status(405) -> <<"405 Method Not Allowed">>;
|
||||||
status(406) -> "406 Not Acceptable";
|
status(406) -> <<"406 Not Acceptable">>;
|
||||||
status(407) -> "407 Proxy Authentication Required";
|
status(407) -> <<"407 Proxy Authentication Required">>;
|
||||||
status(408) -> "408 Request Timeout";
|
status(408) -> <<"408 Request Timeout">>;
|
||||||
status(409) -> "409 Conflict";
|
status(409) -> <<"409 Conflict">>;
|
||||||
status(410) -> "410 Gone";
|
status(410) -> <<"410 Gone">>;
|
||||||
status(411) -> "411 Length Required";
|
status(411) -> <<"411 Length Required">>;
|
||||||
status(412) -> "412 Precondition Failed";
|
status(412) -> <<"412 Precondition Failed">>;
|
||||||
status(413) -> "413 Request Entity Too Large";
|
status(413) -> <<"413 Request Entity Too Large">>;
|
||||||
status(414) -> "414 Request-URI Too Long";
|
status(414) -> <<"414 Request-URI Too Long">>;
|
||||||
status(415) -> "415 Unsupported Media Type";
|
status(415) -> <<"415 Unsupported Media Type">>;
|
||||||
status(416) -> "416 Requested Range Not Satisfiable";
|
status(416) -> <<"416 Requested Range Not Satisfiable">>;
|
||||||
status(417) -> "417 Expectation Failed";
|
status(417) -> <<"417 Expectation Failed">>;
|
||||||
status(418) -> "418 I'm a teapot";
|
status(418) -> <<"418 I'm a teapot">>;
|
||||||
status(422) -> "422 Unprocessable Entity";
|
status(422) -> <<"422 Unprocessable Entity">>;
|
||||||
status(423) -> "423 Locked";
|
status(423) -> <<"423 Locked">>;
|
||||||
status(424) -> "424 Failed Dependency";
|
status(424) -> <<"424 Failed Dependency">>;
|
||||||
status(425) -> "425 Unordered Collection";
|
status(425) -> <<"425 Unordered Collection">>;
|
||||||
status(426) -> "426 Upgrade Required";
|
status(426) -> <<"426 Upgrade Required">>;
|
||||||
status(500) -> "500 Internal Server Error";
|
status(500) -> <<"500 Internal Server Error">>;
|
||||||
status(501) -> "501 Not Implemented";
|
status(501) -> <<"501 Not Implemented">>;
|
||||||
status(502) -> "502 Bad Gateway";
|
status(502) -> <<"502 Bad Gateway">>;
|
||||||
status(503) -> "503 Service Unavailable";
|
status(503) -> <<"503 Service Unavailable">>;
|
||||||
status(504) -> "504 Gateway Timeout";
|
status(504) -> <<"504 Gateway Timeout">>;
|
||||||
status(505) -> "505 HTTP Version Not Supported";
|
status(505) -> <<"505 HTTP Version Not Supported">>;
|
||||||
status(506) -> "506 Variant Also Negotiates";
|
status(506) -> <<"506 Variant Also Negotiates">>;
|
||||||
status(507) -> "507 Insufficient Storage";
|
status(507) -> <<"507 Insufficient Storage">>;
|
||||||
status(510) -> "510 Not Extended";
|
status(510) -> <<"510 Not Extended">>;
|
||||||
status(L) when is_list(L) -> L.
|
status(B) when is_binary(B) -> B.
|
||||||
|
|
||||||
%% Tests.
|
%% Tests.
|
||||||
|
|
||||||
|
@ -275,12 +276,12 @@ status(L) when is_list(L) -> L.
|
||||||
parse_qs_test_() ->
|
parse_qs_test_() ->
|
||||||
%% {Qs, Result}
|
%% {Qs, Result}
|
||||||
Tests = [
|
Tests = [
|
||||||
{"", []},
|
{<<"">>, []},
|
||||||
{"a=b", [{"a", "b"}]},
|
{<<"a=b">>, [{<<"a">>, <<"b">>}]},
|
||||||
{"aaa=bbb", [{"aaa", "bbb"}]},
|
{<<"aaa=bbb">>, [{<<"aaa">>, <<"bbb">>}]},
|
||||||
{"a&b", [{"a", true}, {"b", true}]},
|
{<<"a&b">>, [{<<"a">>, true}, {<<"b">>, true}]},
|
||||||
{"a=b&c&d=e", [{"a", "b"}, {"c", true}, {"d", "e"}]},
|
{<<"a=b&c&d=e">>, [{<<"a">>, <<"b">>}, {<<"c">>, true}, {<<"d">>, <<"e">>}]},
|
||||||
{"a=b=c=d=e&f=g", [{"a", "b=c=d=e"}, {"f", "g"}]}
|
{<<"a=b=c=d=e&f=g">>, [{<<"a">>, <<"b=c=d=e">>}, {<<"f">>, <<"g">>}]}
|
||||||
],
|
],
|
||||||
[{Qs, fun() -> R = parse_qs(Qs) end} || {Qs, R} <- Tests].
|
[{Qs, fun() -> R = parse_qs(Qs) end} || {Qs, R} <- Tests].
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
-record(state, {
|
-record(state, {
|
||||||
handler :: module(),
|
handler :: module(),
|
||||||
opts :: term(),
|
opts :: term(),
|
||||||
origin = undefined :: undefined | string(),
|
origin = undefined :: undefined | binary(),
|
||||||
challenge = undefined :: undefined | binary(),
|
challenge = undefined :: undefined | binary(),
|
||||||
timeout = infinity :: timeout(),
|
timeout = infinity :: timeout(),
|
||||||
messages = undefined :: undefined | {atom(), atom(), atom()}
|
messages = undefined :: undefined | {atom(), atom(), atom()}
|
||||||
|
@ -35,28 +35,27 @@ upgrade(Handler, Opts, Req) ->
|
||||||
|
|
||||||
-spec websocket_upgrade(State::#state{}, Req::#http_req{})
|
-spec websocket_upgrade(State::#state{}, Req::#http_req{})
|
||||||
-> {ok, State::#state{}, Req::#http_req{}}.
|
-> {ok, State::#state{}, Req::#http_req{}}.
|
||||||
websocket_upgrade(State, Req=#http_req{socket=Socket, transport=Transport}) ->
|
websocket_upgrade(State, Req) ->
|
||||||
{"Upgrade", Req2} = cowboy_http_req:header('Connection', Req),
|
{<<"Upgrade">>, Req2} = cowboy_http_req:header('Connection', Req),
|
||||||
{"WebSocket", Req3} = cowboy_http_req:header('Upgrade', Req2),
|
{<<"WebSocket">>, Req3} = cowboy_http_req:header('Upgrade', Req2),
|
||||||
{Origin, Req4} = cowboy_http_req:header("Origin", Req3),
|
{Origin, Req4} = cowboy_http_req:header(<<"Origin">>, Req3),
|
||||||
{Key1, Req5} = cowboy_http_req:header("Sec-Websocket-Key1", Req4),
|
{Key1, Req5} = cowboy_http_req:header(<<"Sec-Websocket-Key1">>, Req4),
|
||||||
{Key2, Req6} = cowboy_http_req:header("Sec-Websocket-Key2", Req5),
|
{Key2, Req6} = cowboy_http_req:header(<<"Sec-Websocket-Key2">>, Req5),
|
||||||
false = lists:member(undefined, [Origin, Key1, Key2]),
|
false = lists:member(undefined, [Origin, Key1, Key2]),
|
||||||
Transport:setopts(Socket, [binary]),
|
|
||||||
{ok, Key3, Req7} = cowboy_http_req:body(8, Req6),
|
{ok, Key3, Req7} = cowboy_http_req:body(8, Req6),
|
||||||
Challenge = challenge(Key1, Key2, Key3),
|
Challenge = challenge(Key1, Key2, Key3),
|
||||||
{ok, State#state{origin=Origin, challenge=Challenge}, Req7}.
|
{ok, State#state{origin=Origin, challenge=Challenge}, Req7}.
|
||||||
|
|
||||||
-spec challenge(Key1::string(), Key2::string(), Key3::binary()) -> binary().
|
-spec challenge(Key1::binary(), Key2::binary(), Key3::binary()) -> binary().
|
||||||
challenge(Key1, Key2, Key3) ->
|
challenge(Key1, Key2, Key3) ->
|
||||||
IntKey1 = key_to_integer(Key1),
|
IntKey1 = key_to_integer(Key1),
|
||||||
IntKey2 = key_to_integer(Key2),
|
IntKey2 = key_to_integer(Key2),
|
||||||
erlang:md5(<< IntKey1:32, IntKey2:32, Key3/binary >>).
|
erlang:md5(<< IntKey1:32, IntKey2:32, Key3/binary >>).
|
||||||
|
|
||||||
-spec key_to_integer(Key::string()) -> integer().
|
-spec key_to_integer(Key::binary()) -> integer().
|
||||||
key_to_integer(Key) ->
|
key_to_integer(Key) ->
|
||||||
Number = list_to_integer([C || C <- Key, C >= $0, C =< $9]),
|
Number = list_to_integer([C || << C >> <= Key, C >= $0, C =< $9]),
|
||||||
Spaces = length([C || C <- Key, C =:= 32]),
|
Spaces = length([C || << C >> <= Key, C =:= 32]),
|
||||||
Number div Spaces.
|
Number div Spaces.
|
||||||
|
|
||||||
-spec handler_init(State::#state{}, Req::#http_req{}) -> ok.
|
-spec handler_init(State::#state{}, Req::#http_req{}) -> ok.
|
||||||
|
@ -85,21 +84,23 @@ websocket_handshake(State=#state{origin=Origin, challenge=Challenge},
|
||||||
raw_path=Path}, HandlerState) ->
|
raw_path=Path}, HandlerState) ->
|
||||||
Location = websocket_location(Transport:name(), Host, Port, Path),
|
Location = websocket_location(Transport:name(), Host, Port, Path),
|
||||||
{ok, Req2} = cowboy_http_req:reply(
|
{ok, Req2} = cowboy_http_req:reply(
|
||||||
"101 WebSocket Protocol Handshake",
|
<<"101 WebSocket Protocol Handshake">>,
|
||||||
[{"Connection", "Upgrade"},
|
[{<<"Connection">>, <<"Upgrade">>},
|
||||||
{"Upgrade", "WebSocket"},
|
{<<"Upgrade">>, <<"WebSocket">>},
|
||||||
{"Sec-WebSocket-Location", Location},
|
{<<"Sec-WebSocket-Location">>, Location},
|
||||||
{"Sec-WebSocket-Origin", Origin}],
|
{<<"Sec-WebSocket-Origin">>, Origin}],
|
||||||
Challenge, Req#http_req{resp_state=waiting}),
|
Challenge, Req#http_req{resp_state=waiting}),
|
||||||
handler_loop(State#state{messages=Transport:messages()},
|
handler_loop(State#state{messages=Transport:messages()},
|
||||||
Req2, HandlerState, <<>>).
|
Req2, HandlerState, <<>>).
|
||||||
|
|
||||||
-spec websocket_location(TransName::atom(), Host::string(),
|
-spec websocket_location(TransportName::atom(), Host::binary(),
|
||||||
Port::ip_port(), Path::string()) -> string().
|
Port::ip_port(), Path::binary()) -> binary().
|
||||||
websocket_location(ssl, Host, Port, Path) ->
|
websocket_location(ssl, Host, Port, Path) ->
|
||||||
"wss://" ++ Host ++ ":" ++ integer_to_list(Port) ++ Path;
|
<< "wss://", Host/binary, ":",
|
||||||
|
(list_to_binary(integer_to_list(Port)))/binary, Path/binary >>;
|
||||||
websocket_location(_Any, Host, Port, Path) ->
|
websocket_location(_Any, Host, Port, Path) ->
|
||||||
"ws://" ++ Host ++ ":" ++ integer_to_list(Port) ++ Path.
|
<< "ws://", Host/binary, ":",
|
||||||
|
(list_to_binary(integer_to_list(Port)))/binary, Path/binary >>.
|
||||||
|
|
||||||
-spec handler_loop(State::#state{}, Req::#http_req{},
|
-spec handler_loop(State::#state{}, Req::#http_req{},
|
||||||
HandlerState::term(), SoFar::binary()) -> ok.
|
HandlerState::term(), SoFar::binary()) -> ok.
|
||||||
|
|
|
@ -76,10 +76,10 @@ end_per_group(https, _Config) ->
|
||||||
|
|
||||||
init_http_dispatch() ->
|
init_http_dispatch() ->
|
||||||
[
|
[
|
||||||
{["localhost"], [
|
{[<<"localhost">>], [
|
||||||
{["websocket"], websocket_handler, []},
|
{[<<"websocket">>], websocket_handler, []},
|
||||||
{["headers", "dupe"], http_handler,
|
{[<<"headers">>, <<"dupe">>], http_handler,
|
||||||
[{headers, [{"Connection", "close"}]}]},
|
[{headers, [{<<"Connection">>, <<"close">>}]}]},
|
||||||
{[], http_handler, []}
|
{[], http_handler, []}
|
||||||
]}
|
]}
|
||||||
].
|
].
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue