From 3d2dee751726e2bfa9cce86ac8a4083d558092fe Mon Sep 17 00:00:00 2001 From: Farruco Sanjurjo Date: Sat, 25 Jan 2014 10:00:09 +0100 Subject: [PATCH] Handle fragmented http messages using erlang:decode_packet --- src/wsock_http.erl | 164 +++++++++++++++++++++-------- test/spec/wsock_handshake_spec.erl | 23 +--- test/spec/wsock_http_spec.erl | 60 ++++++----- 3 files changed, 155 insertions(+), 92 deletions(-) diff --git a/src/wsock_http.erl b/src/wsock_http.erl index dccc923..1493d86 100644 --- a/src/wsock_http.erl +++ b/src/wsock_http.erl @@ -24,20 +24,22 @@ -spec decode(Data::binary(), Type::request | response) -> {ok,#http_message{}} | {error, malformed_request}. decode(Data, Type) -> - [StartLine | Headers] = split(Data), - StartLineProcessed = process_startline(StartLine, Type), - HeadersProcessed = process_headers(Headers), - - case {StartLineProcessed, HeadersProcessed} of - {{error, _}, _} -> - {error, malformed_request}; - {_, {error, _}} -> + case process_startline(Data, Type) of + fragmented -> + fragmented_http_message; + {error, _} -> {error, malformed_request}; - {{ok, StartLineList}, {ok, HeaderList}} -> - {ok, wsock_http:build(Type, StartLineList, HeaderList)} + {ok, StartlineFields, Rest} -> + case process_headers(Rest) of + fragmented -> + fragmented_http_message; + {error, _} -> + {error, malformed_request}; + {ok, HeadersFields} -> + {ok, wsock_http:build(Type, StartlineFields, HeadersFields)} + end end. - -spec build(Type::atom(), StartLine::list({atom(), string()}), Headers::list({string(), string()})) -> #http_message{}. build(Type, StartLine, Headers) -> #http_message{type = Type, start_line = StartLine, headers = Headers}. @@ -81,51 +83,121 @@ get_header_value_case_insensitive(Key, [{Name, Value} | Tail]) -> %============= % Helpers %============= --spec split(Data::binary()) -> list(binary()). -split(Data)-> - Fragments = lists:map(fun(Element) -> - re:replace(Element, <<"^\n*\s*|\n*\s*$">>, <<"">>, [global, {return, binary}]) - end, binary:split(Data, <>, [trim, global])), - - lists:filter(fun(Element) -> - <<>> =/= Element - end, Fragments). - --spec process_startline(StartLine::binary(), Type:: request | response) -> {ok, list({atom(), string()})} | {error, nomatch}. +-spec ensure_string(Data :: list()) -> list() + ; (Data :: binary()) -> list() + ; (Data :: atom()) -> list(). +ensure_string(Data) when is_list(Data) -> Data; +ensure_string(Data) when is_binary(Data) -> erlang:binary_to_list(Data); +ensure_string(Data) when is_integer(Data) -> erlang:integer_to_list(Data); +ensure_string(Data) -> erlang:atom_to_list(Data). + +-spec process_startline( + StartLine::binary(), + Type:: request | response + ) -> + fragmented | + {ok, list({atom(), string()}), binary()} | + {error, term()}. process_startline(StartLine, request) -> - process_startline(StartLine, "(GET)\s+([\S/])\s+HTTP\/([0-9]\.[0-9])", [method, resource, version]); + decode_http_message(http_bin, start_line, StartLine); process_startline(StartLine, response) -> - process_startline(StartLine, "HTTP/([0-9]\.[0-9])\s([0-9]{3,3})\s([a-zA-z0-9 ]+)", [version, status, reason]). - --spec process_startline(StartLine::binary(), Regexp::list(), Keys::list(atom())) -> {ok, list({atom(), string()})} | {error, nomatch}. -process_startline(StartLine, Regexp, Keys) -> - case regexp_run(Regexp, StartLine) of - {match, [_ | Matchs]} -> - {ok , lists:zip(Keys, Matchs)}; - nomatch -> {error, nomatch} + decode_http_message(http_bin, status_line, StartLine). + +-spec decode_http_message( + Type :: atom(), + Chunk :: atom(), + Data :: binary() + ) -> + fragmented | + {error, invalid_http_message} | + {error, unexpected_http_message} | + {ok, [{method, string()} | [{resource, string()} | {version, string()}]], binary()} | + {ok, [{version, string()} | [{status, string()} | {reason, string()}]], binary()} | + {ok, {string(), string()}, binary()} | + ok. +decode_http_message(Type, Chunk, Data) -> + case erlang:decode_packet(Type, Data, []) of + {more, _} -> + fragmented; + {error, _} -> + {error, invalid_http_message}; + {ok, {http_error, _}} -> + {error, invalid_http_message}; + {ok, {http_request, Method, Resource, Version}, Rest} when Chunk == start_line -> + {ok, [{method, ensure_string(Method)}, {resource, process_http_uri(Resource)}, {version, process_http_version(Version)}], Rest}; + {ok, {http_response, Version, Status, Reason}, Rest} when Chunk == status_line -> + {ok, [{version, process_http_version(Version)}, {status, ensure_string(Status)}, {reason, ensure_string(Reason)}], Rest}; + {ok, {http_header, _, Field, _, Value}, Rest} when Chunk == header-> + {ok, {ensure_string(Field), ensure_string(Value)}, Rest}; + {ok, http_eoh, _} when Chunk == header -> + ok; + _ -> + {error, unexpected_http_message} end. --spec regexp_run(Regexp::list(), String::binary()) -> {match, list()} | nomatch. -regexp_run(Regexp, String) -> - re:run(String, Regexp, [{capture, all, list}, caseless]). - +-spec process_http_uri( + '*' + ) -> string() + ; + ({ + absoluteURI, + Protocol :: http | http, + Host :: string() | binary(), + Port :: inet:port_number() | undefined, + Path :: string() | binary() + }) -> string() + ; + ({ + scheme, + Scheme :: string() | binary(), + string() | binary() + }) -> string() + ; + ({ + abs_path, + string() | binary() + }) -> string(). +process_http_uri('*') -> + "*"; +process_http_uri({absoluteURI, Protocol, Host, Port, Path}) -> + PortString = case Port of + undefined -> ""; + Number -> ":" ++ ensure_string(Number) + end, + + ensure_string(Protocol) ++ "://" ++ ensure_string(Host) ++ PortString ++ "/" ++ ensure_string(Path) ; +process_http_uri({scheme, Scheme, Path}) -> + ensure_string(Scheme) ++ Path; +process_http_uri({abs_path, Path}) -> + ensure_string(Path); +process_http_uri(Uri) -> + ensure_string(Uri). + +-spec process_http_version({Major :: pos_integer(), Minor :: pos_integer()}) -> string(). +process_http_version({Major, Minor}) -> + ensure_string(Major) ++ "." ++ ensure_string(Minor). -spec process_headers(Headers::list(binary())) -> {ok, list({string(), string()})} | {error, nomatch}. process_headers(Headers) -> process_headers(Headers, []). --spec process_headers(Headers::list(binary()), Acc::list({list(), list()})) -> {ok, list({string(), string()})} | {error, nomatch}. -process_headers([Header | Tail], Acc) -> - case regexp_run("([!-9;-~]+)\s*:\s*(.+)", Header) of - {match, [_Match, HeaderName, HeaderValue]} -> - process_headers(Tail, [{string:strip(HeaderName), HeaderValue} | Acc]); - nomatch -> - {error, nomatch} - end; - -process_headers([], Acc) -> - {ok, Acc}. +-spec process_headers( + Headers::binary(), + Acc::list({list(), list()}) +) -> + fragmented | + {ok, list({string(), string()})} | + {error, invalid_http_message}. +process_headers(Data, Acc) -> + case decode_http_message(httph_bin, header, Data) of + {ok, Header, Rest} -> + process_headers(Rest, [Header | Acc]); + ok -> + {ok, Acc}; + Other -> + Other + end. encode_message(StartlineExpr, StartlineFields, Headers) -> SL = build_start_line(StartlineExpr, StartlineFields), diff --git a/test/spec/wsock_handshake_spec.erl b/test/spec/wsock_handshake_spec.erl index 377bcf2..3e7a0cc 100644 --- a/test/spec/wsock_handshake_spec.erl +++ b/test/spec/wsock_handshake_spec.erl @@ -22,13 +22,7 @@ spec() -> describe("wsock_handshake", fun() -> describe("handle_open", fun() -> it("should handle a open-handshake request from the client", fun() -> - BinRequest = list_to_binary(["GET / HTTP/1.1\r\n - Host : server.example.org\r\n - Upgrade : websocket\r\n - Connection : Upgrade\r\n - Sec-WebSocket-Key : AQIDBAUGBwgJCgsMDQ4PEA==\r\n - Sec-WebSocket-Version : 13\r\n\r\n - "]), + BinRequest = list_to_binary(["GET / HTTP/1.1\r\nHost : server.example.org\r\nUpgrade : websocket\r\nConnection : Upgrade\r\nSec-WebSocket-Key : AQIDBAUGBwgJCgsMDQ4PEA==\r\nSec-WebSocket-Version : 13\r\n\r\n"]), {ok, Message} = wsock_http:decode(BinRequest, request), {ok,Response} = wsock_handshake:handle_open(Message), @@ -37,12 +31,7 @@ spec() -> end), it("should return an error if the request isn't valid", fun() -> %Missing sec-websocket-key header - BinRequest = list_to_binary(["GET / HTTP/1.1\r\n - Host : server.example.org\r\n - Upgrade : websocket\r\n - Connection : Upgrade\r\n - Sec-WebSocket-Version : 13\r\n\r\n - "]), + BinRequest = list_to_binary(["GET / HTTP/1.1\r\nHost : server.example.org\r\nUpgrade : websocket\r\nConnection : Upgrade\r\nSec-WebSocket-Version : 13\r\n\r\n"]), {ok, Message} = wsock_http:decode(BinRequest, request), {error, invalid_handshake_opening} = wsock_handshake:handle_open(Message) end) @@ -97,13 +86,7 @@ spec() -> {ok, OpenHandShake} = wsock_handshake:open(Resource, Host, Port), Key = wsock_http:get_header_value("sec-websocket-key", OpenHandShake#handshake.message), - BinResponse = list_to_binary(["HTTP/1.1 101 Switch Protocols\r\n - Upgrade: websocket\r\n - Connection: upgrade\r\n - Sec-Websocket-Accept: ", fake_sec_websocket_accept(Key), "\r\n", - "Header-A: A\r\n - Header-C: 123123\r\n - Header-D: D\r\n\r\n"]), + BinResponse = list_to_binary(["HTTP/1.1 101 Switch Protocols\r\nUpgrade: websocket\r\nConnection: upgrade\r\nSec-Websocket-Accept: ", fake_sec_websocket_accept(Key), "\r\n","Header-A: A\r\nHeader-C: 123123\r\nHeader-D: D\r\n\r\n"]), {ok, Response} = wsock_http:decode(BinResponse, response), {ok, Handshake} = wsock_handshake:handle_response(Response, OpenHandShake), diff --git a/test/spec/wsock_http_spec.erl b/test/spec/wsock_http_spec.erl index 902464a..97c5b29 100644 --- a/test/spec/wsock_http_spec.erl +++ b/test/spec/wsock_http_spec.erl @@ -137,11 +137,7 @@ spec() -> describe("decode", fun()-> describe("requests", fun() -> it("should return a http_message record of type request", fun() -> - Data = <<"GET / HTTP/1.1\r\n - Host : www.example.org\r\n - Upgrade : websocket\r\n - Sec-WebSocket-Key : ----\r\n - Header-D: D\r\n\r\n">>, + Data = <<"GET / HTTP/1.1\r\nHost : www.example.org\r\nUpgrade : websocket\r\nSec-WebSocket-Key : ----\r\nHeader-D: D\r\n\r\n">>, {ok, Message} = wsock_http:decode(Data, request), @@ -160,11 +156,7 @@ spec() -> end), it("should return an error if the message startline is malformed", fun() -> %Missing HTTP method - Data = <<" / HTTP/1.1\r\n - Host : www.example.org\r\n - Upgrade : websocket\r\n - Sec-WebSocket-Key : ----\r\n - Header-D: D\r\n\r\n">>, + Data = <<" / HTTP/1.1\r\nHost : www.example.org\r\nUpgrade : websocket\r\nSec-WebSocket-Key : ----\r\nHeader-D: D\r\n\r\n">>, Response = wsock_http:decode(Data, request), @@ -172,22 +164,30 @@ spec() -> end ), it("should return an error if some header is malformed", fun() -> %missing ":" in Upgrade header - Data = <<"GET / HTTP/1.1\r\n - Host : www.example.org\r\n - Upgrade websocket\r\n - Header-D: D\r\n\r\n">>, + Data = <<"GET / HTTP/1.1\r\nHost : www.example.org\r\nUpgrade websocket\r\nHeader-D: D\r\n\r\n">>, Response = wsock_http:decode(Data, request), assert_that(Response, is({error, malformed_request})) + end), + describe("should handle fragmented http requests", fun() -> + it("should handle a request with only a chunk of the startline", fun() -> + Data = <<"GET / ">>, + + Response = wsock_http:decode(Data, request), + assert_that(Response, is(fragmented_http_message)) + end), + it("should handle a request with fragmented headers", fun() -> + Data = <<"GET / HTTP/1.1\r\nHost">>, + + Response = wsock_http:decode(Data, request), + assert_that(Response, is(fragmented_http_message)) + end) end) end), describe("responses", fun() -> it("should return a http_message record of type response", fun() -> - Data = <<"HTTP/1.1 205 Reset Content\r\n - Header-A: A\r\n - Header-C: dGhlIHNhbXBsZSBub25jZQ==\r\n - Header-D: D\r\n\r\n">>, + Data = <<"HTTP/1.1 205 Reset Content\r\nHeader-A: A\r\nHeader-C: dGhlIHNhbXBsZSBub25jZQ==\r\nHeader-D: D\r\n\r\n">>, {ok, Message} = wsock_http:decode(Data, response), @@ -202,24 +202,32 @@ describe("responses", fun() -> assert_that(wsock_http:get_header_value("header-d", Message), is("D")) end), it("should return an error if the message startline is malformed", fun() -> - Data = <<"HTTP/1.1 Reset Content\r\n - Header-A: A\r\n - Header-C: dGhlIHNhbXBsZSBub25jZQ==\r\n - Header-D: D\r\n\r\n">>, + Data = <<"HTTP/1.1 Reset Content\r\nHeader-A: A\r\nHeader-C: dGhlIHNhbXBsZSBub25jZQ==\r\nHeader-D: D\r\n\r\n">>, Response = wsock_http:decode(Data, response), assert_that(Response, is({error, malformed_request})) end), it("should return an error if some header is malformed", fun() -> - Data = <<"HTTP/1.1 205 Reset Content\r\n - Header-A: A\r\n - Header-C dGhlIHNhbXBsZSBub25jZQ==\r\n - Header-D: D\r\n\r\n">>, + Data = <<"HTTP/1.1 205 Reset Content\r\nHeader-A: A\r\nHeader-C dGhlIHNhbXBsZSBub25jZQ==\r\nHeader-D: D\r\n\r\n">>, Response = wsock_http:decode(Data, response), assert_that(Response, is({error, malformed_request})) + end), + describe("should handle fragmented http responses", fun() -> + it("should handle a response with only a chunk of the status line", fun() -> + Data = <<"HTTP/1.1 205">>, + + Response = wsock_http:decode(Data, response), + assert_that(Response, is(fragmented_http_message)) + end), + it("should handle a response with fragmented headers", fun() -> + Data = <<"HTTP/1.1 200 OK\r\nContent-type">>, + + Response = wsock_http:decode(Data, response), + assert_that(Response, is(fragmented_http_message)) + end) end) end) end).