Skip to content

Commit

Permalink
Handle fragmented http messages using erlang:decode_packet
Browse files Browse the repository at this point in the history
  • Loading branch information
madtrick committed Jan 25, 2014
1 parent f9f29dd commit 3d2dee7
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 92 deletions.
164 changes: 118 additions & 46 deletions src/wsock_http.erl
Expand Up @@ -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}.
Expand Down Expand Up @@ -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, <<?CTRL>>, [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),
Expand Down
23 changes: 3 additions & 20 deletions test/spec/wsock_handshake_spec.erl
Expand Up @@ -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),

Expand All @@ -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)
Expand Down Expand Up @@ -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),
Expand Down
60 changes: 34 additions & 26 deletions test/spec/wsock_http_spec.erl
Expand Up @@ -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),

Expand All @@ -160,34 +156,38 @@ 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),

assert_that(Response, is({error, malformed_request}))
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),

Expand All @@ -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).

0 comments on commit 3d2dee7

Please sign in to comment.