diff options
-rw-r--r-- | include/http.hrl | 26 | ||||
-rw-r--r-- | src/cowboy_dispatcher.erl | 165 | ||||
-rw-r--r-- | src/cowboy_http_protocol.erl | 90 | ||||
-rw-r--r-- | src/cowboy_http_req.erl | 201 | ||||
-rw-r--r-- | src/cowboy_http_websocket.erl | 43 | ||||
-rw-r--r-- | test/http_SUITE.erl | 8 |
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, []} ]} ]. |