aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--include/http.hrl26
-rw-r--r--src/cowboy_dispatcher.erl165
-rw-r--r--src/cowboy_http_protocol.erl90
-rw-r--r--src/cowboy_http_req.erl201
-rw-r--r--src/cowboy_http_websocket.erl43
-rw-r--r--test/http_SUITE.erl8
6 files changed, 289 insertions, 244 deletions
diff --git a/include/http.hrl b/include/http.hrl
index 52eeb12..e6c37a9 100644
--- a/include/http.hrl
+++ b/include/http.hrl
@@ -15,11 +15,11 @@
-include_lib("kernel/include/inet.hrl").
-type http_method() :: 'OPTIONS' | 'GET' | 'HEAD'
- | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | string().
--type http_uri() :: '*' | {absoluteURI, http | https, Host::string(),
- Port::integer() | undefined, Path::string()}
- | {scheme, Scheme::string(), string()}
- | {abs_path, string()} | string().
+ | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | binary().
+-type http_uri() :: '*' | {absoluteURI, http | https, Host::binary(),
+ Port::integer() | undefined, Path::binary()}
+ | {scheme, Scheme::binary(), binary()}
+ | {abs_path, binary()} | binary().
-type http_version() :: {Major::integer(), Minor::integer()}.
-type http_header() :: 'Cache-Control' | 'Connection' | 'Date' | 'Pragma'
| 'Transfer-Encoding' | 'Upgrade' | 'Via' | 'Accept' | 'Accept-Charset'
@@ -33,10 +33,10 @@
| 'Content-Md5' | 'Content-Range' | 'Content-Type' | 'Etag'
| 'Expires' | 'Last-Modified' | 'Accept-Ranges' | 'Set-Cookie'
| 'Set-Cookie2' | 'X-Forwarded-For' | 'Cookie' | 'Keep-Alive'
- | 'Proxy-Connection' | string().
--type http_headers() :: list({http_header(), string()}).
+ | 'Proxy-Connection' | binary().
+-type http_headers() :: list({http_header(), binary()}).
%% -type http_cookies() :: term(). %% @todo
--type http_status() :: non_neg_integer() | string().
+-type http_status() :: non_neg_integer() | binary().
-record(http_req, {
%% Transport.
@@ -49,18 +49,20 @@
version = {1, 1} :: http_version(),
peer = undefined :: undefined | {Address::ip_address(), Port::ip_port()},
host = undefined :: undefined | cowboy_dispatcher:path_tokens(),
- raw_host = undefined :: undefined | string(),
+ raw_host = undefined :: undefined | binary(),
port = undefined :: undefined | ip_port(),
path = undefined :: undefined | '*' | cowboy_dispatcher:path_tokens(),
- raw_path = undefined :: undefined | string(),
- qs_vals = undefined :: undefined | list({Name::string(), Value::string() | true}),
- raw_qs = undefined :: undefined | string(),
+ raw_path = undefined :: undefined | binary(),
+ qs_vals = undefined :: undefined
+ | list({Name::binary(), Value::binary() | true}),
+ raw_qs = undefined :: undefined | binary(),
bindings = undefined :: undefined | cowboy_dispatcher:bindings(),
headers = [] :: http_headers(),
%% cookies = undefined :: undefined | http_cookies() %% @todo
%% Request body.
body_state = waiting :: waiting | done,
+ buffer = <<>> :: binary(),
%% Response.
resp_state = locked :: locked | waiting | done
diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl
index 4769da0..f540cd5 100644
--- a/src/cowboy_dispatcher.erl
+++ b/src/cowboy_dispatcher.erl
@@ -15,9 +15,9 @@
-module(cowboy_dispatcher).
-export([split_host/1, split_path/1, match/3]). %% API.
--type bindings() :: list({Key::atom(), Value::string()}).
--type path_tokens() :: list(nonempty_string()).
--type match_rule() :: '_' | '*' | list(string() | '_' | atom()).
+-type bindings() :: list({Key::atom(), Value::binary()}).
+-type path_tokens() :: list(binary()).
+-type match_rule() :: '_' | '*' | list(binary() | '_' | atom()).
-type dispatch_rule() :: {Host::match_rule(), list({Path::match_rule(),
Handler::module(), Opts::term()})}.
-type dispatch_rules() :: list(dispatch_rule()).
@@ -29,25 +29,34 @@
%% API.
--spec split_host(Host::string())
- -> {Tokens::path_tokens(), Host::string(), Port::undefined | ip_port()}.
+-spec split_host(Host::binary())
+ -> {Tokens::path_tokens(), RawHost::binary(), Port::undefined | ip_port()}.
+split_host(<<>>) ->
+ {[], <<>>, undefined};
split_host(Host) ->
- case string:chr(Host, $:) of
- 0 -> {string:tokens(Host, "."), Host, undefined};
- N ->
- {Host2, [$:|Port]} = lists:split(N - 1, Host),
- {string:tokens(Host2, "."), Host2, list_to_integer(Port)}
+ case binary:split(Host, <<":">>) of
+ [Host] ->
+ {binary:split(Host, <<".">>, [global, trim]), Host, undefined};
+ [Host2, Port] ->
+ {binary:split(Host2, <<".">>, [global, trim]), Host2,
+ list_to_integer(binary_to_list(Port))}
end.
--spec split_path(Path::string())
- -> {Tokens::path_tokens(), Path::string(), Qs::string()}.
+-spec split_path(Path::binary())
+ -> {Tokens::path_tokens(), RawPath::binary(), Qs::binary()}.
split_path(Path) ->
- case string:chr(Path, $?) of
- 0 ->
- {string:tokens(Path, "/"), Path, []};
- N ->
- {Path2, [$?|Qs]} = lists:split(N - 1, Path),
- {string:tokens(Path2, "/"), Path2, Qs}
+ case binary:split(Path, <<"?">>) of
+ [Path] -> {do_split_path(Path, <<"/">>), Path, <<>>};
+ [<<>>, Qs] -> {[], <<>>, Qs};
+ [Path2, Qs] -> {do_split_path(Path2, <<"/">>), Path2, Qs}
+ end.
+
+-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.
-spec match(Host::path_tokens(), Path::path_tokens(),
@@ -122,33 +131,40 @@ list_match([], [], Binds) ->
split_host_test_() ->
%% {Host, Result}
Tests = [
- {"", {[], "", undefined}},
- {".........", {[], ".........", undefined}},
- {"*", {["*"], "*", undefined}},
- {"cowboy.dev-extend.eu", {["cowboy", "dev-extend", "eu"],
- "cowboy.dev-extend.eu", undefined}},
- {"dev-extend..eu",
- {["dev-extend", "eu"], "dev-extend..eu", undefined}},
- {"dev-extend.eu", {["dev-extend", "eu"], "dev-extend.eu", undefined}},
- {"dev-extend.eu:8080", {["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}}
+ {<<"">>, {[], <<"">>, undefined}},
+ {<<".........">>, {[], <<".........">>, undefined}},
+ {<<"*">>, {[<<"*">>], <<"*">>, undefined}},
+ {<<"cowboy.dev-extend.eu">>,
+ {[<<"cowboy">>, <<"dev-extend">>, <<"eu">>],
+ <<"cowboy.dev-extend.eu">>, undefined}},
+ {<<"dev-extend..eu">>,
+ {[<<"dev-extend">>, <<>>, <<"eu">>],
+ <<"dev-extend..eu">>, undefined}},
+ {<<"dev-extend.eu">>,
+ {[<<"dev-extend">>, <<"eu">>], <<"dev-extend.eu">>, undefined}},
+ {<<"dev-extend.eu:8080">>,
+ {[<<"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].
split_host_fail_test_() ->
Tests = [
- "dev-extend.eu:owns",
- "dev-extend.eu: owns",
- "dev-extend.eu:42fun",
- "dev-extend.eu: 42fun",
- "dev-extend.eu:42 fun",
- "dev-extend.eu:fun 42",
- "dev-extend.eu: 42",
- ":owns",
- ":42 fun"
+ <<"dev-extend.eu:owns">>,
+ <<"dev-extend.eu: owns">>,
+ <<"dev-extend.eu:42fun">>,
+ <<"dev-extend.eu: 42fun">>,
+ <<"dev-extend.eu:42 fun">>,
+ <<"dev-extend.eu:fun 42">>,
+ <<"dev-extend.eu: 42">>,
+ <<":owns">>,
+ <<":42 fun">>
],
[{H, fun() -> case catch split_host(H) of
{'EXIT', _Reason} -> ok
@@ -157,58 +173,61 @@ split_host_fail_test_() ->
split_path_test_() ->
%% {Path, Result, QueryString}
Tests = [
- {"?", [], "", ""},
- {"???", [], "", "??"},
- {"/", [], "/", ""},
- {"/users", ["users"], "/users", ""},
- {"/users?", ["users"], "/users", ""},
- {"/users?a", ["users"], "/users", "a"},
- {"/users/42/friends?a=b&c=d&e=notsure?whatever",
- ["users", "42", "friends"],
- "/users/42/friends", "a=b&c=d&e=notsure?whatever"}
+ {<<"?">>, [], <<"">>, <<"">>},
+ {<<"???">>, [], <<"">>, <<"??">>},
+ {<<"/">>, [], <<"/">>, <<"">>},
+ {<<"/users">>, [<<"users">>], <<"/users">>, <<"">>},
+ {<<"/users?">>, [<<"users">>], <<"/users">>, <<"">>},
+ {<<"/users?a">>, [<<"users">>], <<"/users">>, <<"a">>},
+ {<<"/users/42/friends?a=b&c=d&e=notsure?whatever">>,
+ [<<"users">>, <<"42">>, <<"friends">>],
+ <<"/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_() ->
Dispatch = [
- {["www", '_', "dev-extend", "eu"], [
- {["users", '_', "mails"], match_any_subdomain_users, []}
+ {[<<"www">>, '_', <<"dev-extend">>, <<"eu">>], [
+ {[<<"users">>, '_', <<"mails">>], match_any_subdomain_users, []}
]},
- {["dev-extend", "eu"], [
- {["users", id, "friends"], match_extend_users_friends, []},
+ {[<<"dev-extend">>, <<"eu">>], [
+ {[<<"users">>, id, <<"friends">>], match_extend_users_friends, []},
{'_', match_extend, []}
]},
- {["dev-extend", var], [
- {["threads", var], match_duplicate_vars,
+ {[<<"dev-extend">>, var], [
+ {[<<"threads">>, var], match_duplicate_vars,
[we, {expect, two}, var, here]}
]},
- {["erlang", ext], [
+ {[<<"erlang">>, ext], [
{'_', match_erlang_ext, []}
]},
{'_', [
- {["users", id, "friends"], match_users_friends, []},
+ {[<<"users">>, id, <<"friends">>], match_users_friends, []},
{'_', match_any, []}
]}
],
%% {Host, Path, Result}
Tests = [
- {["any"], [], {ok, match_any, [], []}},
- {["www", "any", "dev-extend", "eu"], ["users", "42", "mails"],
+ {[<<"any">>], [], {ok, match_any, [], []}},
+ {[<<"www">>, <<"any">>, <<"dev-extend">>, <<"eu">>],
+ [<<"users">>, <<"42">>, <<"mails">>],
{ok, match_any_subdomain_users, [], []}},
- {["www", "dev-extend", "eu"], ["users", "42", "mails"],
- {ok, match_any, [], []}},
- {["www", "dev-extend", "eu"], [], {ok, match_any, [], []}},
- {["www", "any", "dev-extend", "eu"], ["not_users", "42", "mails"],
- {error, notfound, path}},
- {["dev-extend", "eu"], [], {ok, match_extend, [], []}},
- {["dev-extend", "eu"], ["users", "42", "friends"],
- {ok, match_extend_users_friends, [], [{id, "42"}]}},
- {["erlang", "fr"], '_', {ok, match_erlang_ext, [], [{ext, "fr"}]}},
- {["any"], ["users", "444", "friends"],
- {ok, match_users_friends, [], [{id, "444"}]}},
- {["dev-extend", "fr"], ["threads", "987"],
+ {[<<"www">>, <<"dev-extend">>, <<"eu">>],
+ [<<"users">>, <<"42">>, <<"mails">>], {ok, match_any, [], []}},
+ {[<<"www">>, <<"dev-extend">>, <<"eu">>], [], {ok, match_any, [], []}},
+ {[<<"www">>, <<"any">>, <<"dev-extend">>, <<"eu">>],
+ [<<"not_users">>, <<"42">>, <<"mails">>], {error, notfound, path}},
+ {[<<"dev-extend">>, <<"eu">>], [], {ok, match_extend, [], []}},
+ {[<<"dev-extend">>, <<"eu">>], [<<"users">>, <<"42">>, <<"friends">>],
+ {ok, match_extend_users_friends, [], [{id, <<"42">>}]}},
+ {[<<"erlang">>, <<"fr">>], '_',
+ {ok, match_erlang_ext, [], [{ext, <<"fr">>}]}},
+ {[<<"any">>], [<<"users">>, <<"444">>, <<"friends">>],
+ {ok, match_users_friends, [], [{id, <<"444">>}]}},
+ {[<<"dev-extend">>, <<"fr">>], [<<"threads">>, <<"987">>],
{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() ->
R = match(H, P, Dispatch)
diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl
index 602ea6a..221e316 100644
--- a/src/cowboy_http_protocol.erl
+++ b/src/cowboy_http_protocol.erl
@@ -14,7 +14,7 @@
-module(cowboy_http_protocol).
-export([start_link/3]). %% API.
--export([init/3, wait_request/1]). %% FSM.
+-export([init/3, parse_request/1]). %% FSM.
-include("include/http.hrl").
@@ -26,7 +26,8 @@
req_empty_lines = 0 :: integer(),
max_empty_lines :: integer(),
timeout :: timeout(),
- connection = keepalive :: keepalive | close
+ connection = keepalive :: keepalive | close,
+ buffer = <<>> :: binary()
}).
%% API.
@@ -47,11 +48,21 @@ init(Socket, Transport, Opts) ->
wait_request(#state{socket=Socket, transport=Transport,
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.
-wait_request(State=#state{socket=Socket, transport=Transport, timeout=T}) ->
- Transport:setopts(Socket, [{packet, http}]),
+wait_request(State=#state{socket=Socket, transport=Transport,
+ timeout=T, buffer=Buffer}) ->
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, closed} -> terminate(State)
end.
@@ -67,41 +78,50 @@ request({http_request, Method, {abs_path, AbsPath}, Version},
State=#state{socket=Socket, transport=Transport}) ->
{Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath),
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,
path=Path, raw_path=RawPath, raw_qs=Qs},
State#state{connection=ConnAtom});
request({http_request, Method, '*', Version},
State=#state{socket=Socket, transport=Transport}) ->
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,
- path='*', raw_path="*", raw_qs=[]},
+ path='*', raw_path= <<"*">>, raw_qs= <<>>},
State#state{connection=ConnAtom});
request({http_request, _Method, _URI, _Version}, 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}) ->
error_terminate(400, State);
-request({http_error, "\r\n"}, State=#state{req_empty_lines=N}) ->
- wait_request(State#state{req_empty_lines=N + 1});
+request({http_error, <<"\r\n">>}, State=#state{req_empty_lines=N}) ->
+ parse_request(State#state{req_empty_lines=N + 1});
request({http_error, _Any}, 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.
wait_header(Req, State=#state{socket=Socket,
- transport=Transport, timeout=T}) ->
+ transport=Transport, timeout=T, buffer=Buffer}) ->
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, closed} -> terminate(State)
end.
-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{
transport=Transport, host=undefined}, State) ->
- RawHost2 = string_to_lower(RawHost),
+ RawHost2 = binary_to_lower(RawHost),
case catch cowboy_dispatcher:split_host(RawHost2) of
{Host, RawHost3, undefined} ->
Port = default_port(Transport:name()),
@@ -115,21 +135,21 @@ header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{
end;
%% Ignore Host headers if we already have it.
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) ->
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]},
State#state{connection=ConnAtom});
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);
%% The Host header is required.
header(http_eoh, #http_req{host=undefined}, State) ->
error_terminate(400, State);
-header(http_eoh, Req, State) ->
- handler_init(Req, State);
-header({http_error, _String}, _Req, State) ->
+header(http_eoh, Req, State=#state{buffer=Buffer}) ->
+ handler_init(Req#http_req{buffer=Buffer}, State#state{buffer= <<>>});
+header({http_error, _Bin}, _Req, State) ->
error_terminate(500, State).
-spec dispatch(Req::#http_req{}, State::#state{}) -> ok.
@@ -139,7 +159,7 @@ dispatch(Req=#http_req{host=Host, path=Path},
%% things like url rewriting.
case cowboy_dispatcher:match(Host, Path, Dispatch) of
{ok, Handler, Opts, Binds} ->
- wait_header(Req#http_req{bindings=Binds},
+ parse_header(Req#http_req{bindings=Binds},
State#state{handler={Handler, Opts}});
{error, notfound, host} ->
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{},
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(
Req#http_req{resp_state=locked}, HandlerState)),
BodyRes = ensure_body_processed(Req),
ensure_response(Req, State),
case {HandlerRes, BodyRes, State#state.connection} of
- {ok, ok, keepalive} -> ?MODULE:wait_request(State);
- _Closed -> terminate(State)
+ {ok, ok, keepalive} ->
+ ?MODULE:parse_request(State#state{buffer=Buffer});
+ _Closed ->
+ terminate(State)
end.
-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(_Any) -> close.
--spec connection_to_atom(Connection::string()) -> keepalive | close.
-connection_to_atom("keep-alive") ->
+-spec connection_to_atom(Connection::binary()) -> keepalive | close.
+connection_to_atom(<<"keep-alive">>) ->
keepalive;
-connection_to_atom("close") ->
+connection_to_atom(<<"close">>) ->
close;
connection_to_atom(Connection) ->
- case string_to_lower(Connection) of
- "close" -> close;
+ case binary_to_lower(Connection) of
+ <<"close">> -> close;
_Any -> keepalive
end.
@@ -241,11 +264,10 @@ connection_to_atom(Connection) ->
default_port(ssl) -> 443;
default_port(_) -> 80.
-%% More efficient implementation of string:to_lower.
%% We are excluding a few characters on purpose.
--spec string_to_lower(string()) -> string().
-string_to_lower(L) ->
- [char_to_lower(C) || C <- L].
+-spec binary_to_lower(binary()) -> binary().
+binary_to_lower(L) ->
+ << << (char_to_lower(C)) >> || << C >> <= L >>.
%% We gain noticeable speed by matching each value directly.
-spec char_to_lower(char()) -> char().
diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl
index f0fc15c..e2239eb 100644
--- a/src/cowboy_http_req.erl
+++ b/src/cowboy_http_req.erl
@@ -59,7 +59,7 @@ peer(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) ->
{Req#http_req.raw_host, Req}.
@@ -72,18 +72,18 @@ port(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) ->
{Req#http_req.raw_path, Req}.
--spec qs_val(Name::string(), Req::#http_req{})
- -> {Value::string() | true | undefined, Req::#http_req{}}.
+-spec qs_val(Name::binary(), Req::#http_req{})
+ -> {Value::binary() | true | undefined, Req::#http_req{}}.
%% @equiv qs_val(Name, Req) -> qs_val(Name, Req, undefined)
qs_val(Name, Req) ->
qs_val(Name, Req, undefined).
--spec qs_val(Name::string(), Req::#http_req{}, Default)
- -> {Value::string() | true | Default, Req::#http_req{}}
+-spec qs_val(Name::binary(), Req::#http_req{}, Default)
+ -> {Value::binary() | true | Default, Req::#http_req{}}
when Default::term().
qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined}, Default) ->
QsVals = parse_qs(RawQs),
@@ -95,25 +95,25 @@ qs_val(Name, Req, Default) ->
end.
-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}) ->
QsVals = parse_qs(RawQs),
qs_vals(Req#http_req{qs_vals=QsVals});
qs_vals(Req=#http_req{qs_vals=QsVals}) ->
{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) ->
{Req#http_req.raw_qs, 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)
binding(Name, Req) ->
binding(Name, Req, undefined).
-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) ->
case lists:keyfind(Name, 1, Req#http_req.bindings) of
{Name, Value} -> {Value, Req};
@@ -121,18 +121,18 @@ binding(Name, Req, Default) ->
end.
-spec bindings(Req::#http_req{})
- -> {list({Name::atom(), Value::string()}), Req::#http_req{}}.
+ -> {list({Name::atom(), Value::binary()}), Req::#http_req{}}.
bindings(Req) ->
{Req#http_req.bindings, Req}.
--spec header(Name::atom() | string(), Req::#http_req{})
- -> {Value::string() | undefined, Req::#http_req{}}.
+-spec header(Name::atom() | binary(), Req::#http_req{})
+ -> {Value::binary() | undefined, Req::#http_req{}}.
%% @equiv header(Name, Req) -> header(Name, Req, undefined)
header(Name, Req) ->
header(Name, Req, undefined).
--spec header(Name::atom() | string(), Req::#http_req{}, Default)
- -> {Value::string() | Default, Req::#http_req{}} when Default::term().
+-spec header(Name::atom() | binary(), Req::#http_req{}, Default)
+ -> {Value::binary() | Default, Req::#http_req{}} when Default::term().
header(Name, Req, Default) ->
case lists:keyfind(Name, 1, Req#http_req.headers) of
{Name, Value} -> {Value, Req};
@@ -154,26 +154,28 @@ body(Req) ->
case Length of
undefined -> {error, badarg};
_Any ->
- Length2 = list_to_integer(Length),
+ Length2 = list_to_integer(binary_to_list(Length)),
body(Length2, Req2)
end.
%% @todo We probably want to configure the timeout.
-spec body(Length::non_neg_integer(), Req::#http_req{})
-> {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_state=waiting}) ->
- Transport:setopts(Socket, [{packet, raw}]),
- case Transport:recv(Socket, Length, 5000) of
- {ok, Body} -> {ok, Body, Req#http_req{body_state=done}};
+ body_state=waiting, buffer=Buffer}) when Length > byte_size(Buffer) ->
+ case Transport:recv(Socket, Length - byte_size(Buffer), 5000) of
+ {ok, Body} -> {ok, << Buffer/binary, Body/binary >>, Req#http_req{body_state=done, buffer= <<>>}};
{error, Reason} -> {error, Reason}
end.
-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) ->
{ok, Body, Req2} = body(Req),
- {parse_qs(binary_to_list(Body)), Req2}.
+ {parse_qs(Body), Req2}.
%% Response API.
@@ -182,91 +184,90 @@ body_qs(Req) ->
reply(Code, Headers, Body, Req=#http_req{socket=Socket,
transport=Transport, connection=Connection,
resp_state=waiting}) ->
- StatusLine = ["HTTP/1.1 ", status(Code), "\r\n"],
+ StatusLine = <<"HTTP/1.1 ", (status(Code))/binary, "\r\n">>,
DefaultHeaders = [
- {"Connection", atom_to_connection(Connection)},
- {"Content-Length", integer_to_list(iolist_size(Body))}
+ {<<"Connection">>, atom_to_connection(Connection)},
+ {<<"Content-Length">>, list_to_binary(integer_to_list(iolist_size(Body)))}
],
Headers2 = lists:keysort(1, Headers),
Headers3 = lists:ukeymerge(1, Headers2, DefaultHeaders),
- Headers4 = [[Key, ": ", Value, "\r\n"] || {Key, Value} <- Headers3],
- Transport:send(Socket, [StatusLine, Headers4, "\r\n", Body]),
+ Headers4 = [<< Key/binary, ": ", Value/binary, "\r\n">> || {Key, Value} <- Headers3],
+ Transport:send(Socket, [StatusLine, Headers4, <<"\r\n">>, Body]),
{ok, Req#http_req{resp_state=done}}.
%% 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) ->
- Tokens = string:tokens(Qs, "&"),
- [case string:chr(Token, $=) of
- 0 ->
- {Token, true};
- N ->
- {Name, [$=|Value]} = lists:split(N - 1, Token),
- {Name, Value}
+ Tokens = binary:split(Qs, <<"&">>, [global, trim]),
+ [case binary:split(Token, <<"=">>) of
+ [Token] -> {Token, true};
+ [Name, Value] -> {Name, Value}
end || Token <- Tokens].
--spec atom_to_connection(Atom::keepalive | close) -> string().
+-spec atom_to_connection(Atom::keepalive | close) -> binary().
atom_to_connection(keepalive) ->
- "keep-alive";
+ <<"keep-alive">>;
atom_to_connection(close) ->
- "close".
-
--spec status(Code::http_status()) -> string().
-status(100) -> "100 Continue";
-status(101) -> "101 Switching Protocols";
-status(102) -> "102 Processing";
-status(200) -> "200 OK";
-status(201) -> "201 Created";
-status(202) -> "202 Accepted";
-status(203) -> "203 Non-Authoritative Information";
-status(204) -> "204 No Content";
-status(205) -> "205 Reset Content";
-status(206) -> "206 Partial Content";
-status(207) -> "207 Multi-Status";
-status(226) -> "226 IM Used";
-status(300) -> "300 Multiple Choices";
-status(301) -> "301 Moved Permanently";
-status(302) -> "302 Found";
-status(303) -> "303 See Other";
-status(304) -> "304 Not Modified";
-status(305) -> "305 Use Proxy";
-status(306) -> "306 Switch Proxy";
-status(307) -> "307 Temporary Redirect";
-status(400) -> "400 Bad Request";
-status(401) -> "401 Unauthorized";
-status(402) -> "402 Payment Required";
-status(403) -> "403 Forbidden";
-status(404) -> "404 Not Found";
-status(405) -> "405 Method Not Allowed";
-status(406) -> "406 Not Acceptable";
-status(407) -> "407 Proxy Authentication Required";
-status(408) -> "408 Request Timeout";
-status(409) -> "409 Conflict";
-status(410) -> "410 Gone";
-status(411) -> "411 Length Required";
-status(412) -> "412 Precondition Failed";
-status(413) -> "413 Request Entity Too Large";
-status(414) -> "414 Request-URI Too Long";
-status(415) -> "415 Unsupported Media Type";
-status(416) -> "416 Requested Range Not Satisfiable";
-status(417) -> "417 Expectation Failed";
-status(418) -> "418 I'm a teapot";
-status(422) -> "422 Unprocessable Entity";
-status(423) -> "423 Locked";
-status(424) -> "424 Failed Dependency";
-status(425) -> "425 Unordered Collection";
-status(426) -> "426 Upgrade Required";
-status(500) -> "500 Internal Server Error";
-status(501) -> "501 Not Implemented";
-status(502) -> "502 Bad Gateway";
-status(503) -> "503 Service Unavailable";
-status(504) -> "504 Gateway Timeout";
-status(505) -> "505 HTTP Version Not Supported";
-status(506) -> "506 Variant Also Negotiates";
-status(507) -> "507 Insufficient Storage";
-status(510) -> "510 Not Extended";
-status(L) when is_list(L) -> L.
+ <<"close">>.
+
+-spec status(Code::http_status()) -> binary().
+status(100) -> <<"100 Continue">>;
+status(101) -> <<"101 Switching Protocols">>;
+status(102) -> <<"102 Processing">>;
+status(200) -> <<"200 OK">>;
+status(201) -> <<"201 Created">>;
+status(202) -> <<"202 Accepted">>;
+status(203) -> <<"203 Non-Authoritative Information">>;
+status(204) -> <<"204 No Content">>;
+status(205) -> <<"205 Reset Content">>;
+status(206) -> <<"206 Partial Content">>;
+status(207) -> <<"207 Multi-Status">>;
+status(226) -> <<"226 IM Used">>;
+status(300) -> <<"300 Multiple Choices">>;
+status(301) -> <<"301 Moved Permanently">>;
+status(302) -> <<"302 Found">>;
+status(303) -> <<"303 See Other">>;
+status(304) -> <<"304 Not Modified">>;
+status(305) -> <<"305 Use Proxy">>;
+status(306) -> <<"306 Switch Proxy">>;
+status(307) -> <<"307 Temporary Redirect">>;
+status(400) -> <<"400 Bad Request">>;
+status(401) -> <<"401 Unauthorized">>;
+status(402) -> <<"402 Payment Required">>;
+status(403) -> <<"403 Forbidden">>;
+status(404) -> <<"404 Not Found">>;
+status(405) -> <<"405 Method Not Allowed">>;
+status(406) -> <<"406 Not Acceptable">>;
+status(407) -> <<"407 Proxy Authentication Required">>;
+status(408) -> <<"408 Request Timeout">>;
+status(409) -> <<"409 Conflict">>;
+status(410) -> <<"410 Gone">>;
+status(411) -> <<"411 Length Required">>;
+status(412) -> <<"412 Precondition Failed">>;
+status(413) -> <<"413 Request Entity Too Large">>;
+status(414) -> <<"414 Request-URI Too Long">>;
+status(415) -> <<"415 Unsupported Media Type">>;
+status(416) -> <<"416 Requested Range Not Satisfiable">>;
+status(417) -> <<"417 Expectation Failed">>;
+status(418) -> <<"418 I'm a teapot">>;
+status(422) -> <<"422 Unprocessable Entity">>;
+status(423) -> <<"423 Locked">>;
+status(424) -> <<"424 Failed Dependency">>;
+status(425) -> <<"425 Unordered Collection">>;
+status(426) -> <<"426 Upgrade Required">>;
+status(500) -> <<"500 Internal Server Error">>;
+status(501) -> <<"501 Not Implemented">>;
+status(502) -> <<"502 Bad Gateway">>;
+status(503) -> <<"503 Service Unavailable">>;
+status(504) -> <<"504 Gateway Timeout">>;
+status(505) -> <<"505 HTTP Version Not Supported">>;
+status(506) -> <<"506 Variant Also Negotiates">>;
+status(507) -> <<"507 Insufficient Storage">>;
+status(510) -> <<"510 Not Extended">>;
+status(B) when is_binary(B) -> B.
%% Tests.
@@ -275,12 +276,12 @@ status(L) when is_list(L) -> L.
parse_qs_test_() ->
%% {Qs, Result}
Tests = [
- {"", []},
- {"a=b", [{"a", "b"}]},
- {"aaa=bbb", [{"aaa", "bbb"}]},
- {"a&b", [{"a", true}, {"b", true}]},
- {"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">>, [{<<"a">>, <<"b">>}]},
+ {<<"aaa=bbb">>, [{<<"aaa">>, <<"bbb">>}]},
+ {<<"a&b">>, [{<<"a">>, true}, {<<"b">>, true}]},
+ {<<"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">>}]}
],
[{Qs, fun() -> R = parse_qs(Qs) end} || {Qs, R} <- Tests].
diff --git a/src/cowboy_http_websocket.erl b/src/cowboy_http_websocket.erl
index 0150a6f..4e12358 100644
--- a/src/cowboy_http_websocket.erl
+++ b/src/cowboy_http_websocket.erl
@@ -20,7 +20,7 @@
-record(state, {
handler :: module(),
opts :: term(),
- origin = undefined :: undefined | string(),
+ origin = undefined :: undefined | binary(),
challenge = undefined :: undefined | binary(),
timeout = infinity :: timeout(),
messages = undefined :: undefined | {atom(), atom(), atom()}
@@ -35,28 +35,27 @@ upgrade(Handler, Opts, Req) ->
-spec websocket_upgrade(State::#state{}, Req::#http_req{})
-> {ok, State::#state{}, Req::#http_req{}}.
-websocket_upgrade(State, Req=#http_req{socket=Socket, transport=Transport}) ->
- {"Upgrade", Req2} = cowboy_http_req:header('Connection', Req),
- {"WebSocket", Req3} = cowboy_http_req:header('Upgrade', Req2),
- {Origin, Req4} = cowboy_http_req:header("Origin", Req3),
- {Key1, Req5} = cowboy_http_req:header("Sec-Websocket-Key1", Req4),
- {Key2, Req6} = cowboy_http_req:header("Sec-Websocket-Key2", Req5),
+websocket_upgrade(State, Req) ->
+ {<<"Upgrade">>, Req2} = cowboy_http_req:header('Connection', Req),
+ {<<"WebSocket">>, Req3} = cowboy_http_req:header('Upgrade', Req2),
+ {Origin, Req4} = cowboy_http_req:header(<<"Origin">>, Req3),
+ {Key1, Req5} = cowboy_http_req:header(<<"Sec-Websocket-Key1">>, Req4),
+ {Key2, Req6} = cowboy_http_req:header(<<"Sec-Websocket-Key2">>, Req5),
false = lists:member(undefined, [Origin, Key1, Key2]),
- Transport:setopts(Socket, [binary]),
{ok, Key3, Req7} = cowboy_http_req:body(8, Req6),
Challenge = challenge(Key1, Key2, Key3),
{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) ->
IntKey1 = key_to_integer(Key1),
IntKey2 = key_to_integer(Key2),
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) ->
- Number = list_to_integer([C || C <- Key, C >= $0, C =< $9]),
- Spaces = length([C || C <- Key, C =:= 32]),
+ Number = list_to_integer([C || << C >> <= Key, C >= $0, C =< $9]),
+ Spaces = length([C || << C >> <= Key, C =:= 32]),
Number div Spaces.
-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) ->
Location = websocket_location(Transport:name(), Host, Port, Path),
{ok, Req2} = cowboy_http_req:reply(
- "101 WebSocket Protocol Handshake",
- [{"Connection", "Upgrade"},
- {"Upgrade", "WebSocket"},
- {"Sec-WebSocket-Location", Location},
- {"Sec-WebSocket-Origin", Origin}],
+ <<"101 WebSocket Protocol Handshake">>,
+ [{<<"Connection">>, <<"Upgrade">>},
+ {<<"Upgrade">>, <<"WebSocket">>},
+ {<<"Sec-WebSocket-Location">>, Location},
+ {<<"Sec-WebSocket-Origin">>, Origin}],
Challenge, Req#http_req{resp_state=waiting}),
handler_loop(State#state{messages=Transport:messages()},
Req2, HandlerState, <<>>).
--spec websocket_location(TransName::atom(), Host::string(),
- Port::ip_port(), Path::string()) -> string().
+-spec websocket_location(TransportName::atom(), Host::binary(),
+ Port::ip_port(), Path::binary()) -> binary().
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) ->
- "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{},
HandlerState::term(), SoFar::binary()) -> ok.
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index 21681be..c04c3d8 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -76,10 +76,10 @@ end_per_group(https, _Config) ->
init_http_dispatch() ->
[
- {["localhost"], [
- {["websocket"], websocket_handler, []},
- {["headers", "dupe"], http_handler,
- [{headers, [{"Connection", "close"}]}]},
+ {[<<"localhost">>], [
+ {[<<"websocket">>], websocket_handler, []},
+ {[<<"headers">>, <<"dupe">>], http_handler,
+ [{headers, [{<<"Connection">>, <<"close">>}]}]},
{[], http_handler, []}
]}
].