From a61868f0a3497949a6c20071d3f96c93bcdafb4e Mon Sep 17 00:00:00 2001 From: Ben Murphy Date: Sun, 16 Oct 2011 11:51:30 +0100 Subject: [PATCH] initial commit --- .gitignore | 7 + Makefile | 10 + README.md | 73 ++++ include/espdy_frame.hrl | 53 +++ include/espdy_server.hrl | 1 + rebar | Bin 0 -> 108325 bytes rebar.config | 6 + server.crt | 14 + server.key | 15 + src/espdy.app.src | 8 + src/espdy_frame.erl | 308 ++++++++++++++ src/espdy_mock_socket.erl | 30 ++ src/espdy_server.erl | 799 +++++++++++++++++++++++++++++++++++++ src/espdy_ssl_socket.erl | 27 ++ src/espdy_tcp_socket.erl | 27 ++ src/espdy_test_server.erl | 133 ++++++ test/espdy_frame_test.erl | 131 ++++++ test/espdy_server_test.erl | 706 ++++++++++++++++++++++++++++++++ test_server | 1 + 19 files changed, 2349 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 include/espdy_frame.hrl create mode 100644 include/espdy_server.hrl create mode 100755 rebar create mode 100644 rebar.config create mode 100644 server.crt create mode 100644 server.key create mode 100644 src/espdy.app.src create mode 100644 src/espdy_frame.erl create mode 100644 src/espdy_mock_socket.erl create mode 100644 src/espdy_server.erl create mode 100644 src/espdy_ssl_socket.erl create mode 100644 src/espdy_tcp_socket.erl create mode 100644 src/espdy_test_server.erl create mode 100644 test/espdy_frame_test.erl create mode 100644 test/espdy_server_test.erl create mode 100755 test_server diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0b8e040 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.eunit +*.beam +deps +ebin +log +TEST-*xml + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e5a790f --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +compile: + ./rebar compile + +eunit: + ./rebar skip_deps=true eunit + +run: + erl -pa ebin -pa deps/lager/ebin -s espdy_test_server + +.PHONY: eunit compile diff --git a/README.md b/README.md new file mode 100644 index 0000000..c22598b --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +#Status + +This project is still very alpha. Enough of the protocol has been implemented to view pages in google chrome. Check out espdy_test_server.erl + +#Running +/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --use-spdy=ssl +make compile +./test_server +http://localhost:8443 + +# {active, false} socket like API +This makes the code much more complicated :( and I'm wondering if it is worth having. If you use a callback module or {active, true} style message sending +then the code becomes much simpler. + +However, {active, false} gives us flow control (with spdy/3) and really simple integration with erlang web frameworks that use tcp like sockets and do recv to process POSTs. + +We also support multiple concurrent send() or recv() operations on the same stream which makes the code a little bit more complicated. I'm not sure how useful this behaviour is because you have to make sure you are send()'ing and recv()'ing atomic blocks for it to be useful otherwise you might interleave messages corrupting the stream. gen_tcp supports this behaviour. i'm not sure if the ssl module supports this behaviour. + +#Issues + +## NPN Support in Erlang + +There is no NPN support for SSL in Erlang. I'm currently working on a patch to fix this. Whether Erlang +would accept a patch for a draft extensions is another question. + +## Write Queueing + +We queue up writes in a write process. If the reader at the other end is not reading data fast enough then our write process will start consuming lots of memory. We need to kill off connections that are misbehaving. send() calls block until the write has returned so there is _some_ flow control on the send() side. once the os buffer has filled up send() will start blocking. however, we will send data in response to PING and protocol errors and it is possible a misbehaving client could trick us into allocating lots of memory for the write queue. + +## Read Buffering + +There is no flow control in spdy draft 2. When we receive data we add it to a buffer. We keep adding data to the buffer. If we receive data faster than we are recv()'ing it then we can start chewing up memory. The plan is to add a max_recv_buffer setting and to stop reading from the socket if this limit is met. + +## Goaway Implementation + +We don't correctly send {error, closed_not_processed} in a lot of places. This hasn't been implemented in send/recv. Also, we kill the gen_server too early and a client may receive {error, closed} because the gen_server is not around instead of {error, closed_not_processed}. + +## No settings + +Have not implemented sending or receiving of the settings frame. + +## Partial implementation of headers + +No support for receiving header frames. I'm not sure how this would look in the api :( + +## StreamId limit + +No support for stopping connections when the StreamId limit has been reached + +## Not checking version + +We are currently not checking the version field on control or data frames :( + +## No backlimit for accepts + +We continue to add connections to the accept buffer without bound. + +## Communicating errors back to the client + +If a client isn't send'ing or recv'ing on a stream then they won't receive error notifications about previous failed sends. I think this is how unix +tcp works so i don't think it is that terrible :). However, the spdy http protocol is the client writes to the server and then half-closes, then the +server writes to the client and then half-closes. There is no server waiting for an ack from the client. If the servers response to the client is not +received it will not be aware of this. I'm not sure how big of a problem this is. Generally http servers don't care if they fail to deliver a response +to the client. + +## No chunking of sends + +If you do a large send() then it will block all the other channels using the spdy socket. We should probably chunk sends() to a reasonable value. + +# Code duplication + +There is some code duplication between espdy_server and espdy_frame for control frame decoding + diff --git a/include/espdy_frame.hrl b/include/espdy_frame.hrl new file mode 100644 index 0000000..c4a6616 --- /dev/null +++ b/include/espdy_frame.hrl @@ -0,0 +1,53 @@ +-define(BYTE(X), (X):8/unsigned-big-integer). +-define(UINT16(X), (X):16/unsigned-big-integer). +-define(UINT24(X), (X):24/unsigned-big-integer). +-define(UINT32(X), (X):32/unsigned-big-integer). +-define(UINT64(X), (X):64/unsigned-big-integer). + +-define(NOOP, 5). +-define(PING, 6). +-define(SYN_STREAM, 1). +-define(SYN_REPLY, 2). +-define(RST_STREAM, 3). +-define(HEADERS, 8). +-define(GOAWAY, 7). + +-define(SPDY_VERSION, 2). +-define(CONTROL_FRAME, 1:1/unsigned-big-integer). +-define(DATA_FRAME, 0:1/unsigned-big-integer). +-define(TYPE(X), (X):16/unsigned-big-integer). +-define(VERSION(X), (X):15/unsigned-big-integer). +-define(LENGTH(X), ?UINT24(X)). +-define(FLAGS(X), ?BYTE(X)). +-define(RESERVED_BIT, _:1). +-define(MAKE_RESERVED_BIT, 0:1). +-define(STREAM_ID(X), (X):31/unsigned-big-integer). +-define(PRIORITY(X), (X):2/unsigned-big-integer). +-define(UNUSED(X), _:(X)). +-define(MAKE_UNUSED(X), 0:(X)). + +-define(UNUSED_SYN_REPLY, 16). +-define(UNUSED_SYN_STREAM, 14). +-define(UNUSED_HEADERS, 16). + +-define(FLAG_FIN, 1). +-define(FLAG_UNIDIRECTIONAL, 2). + +%% RST_STREAM STATUSES +-define(PROTOCOL_ERROR, 1). +-define(INVALID_STREAM, 2). +-define(REFUSED_STREAM, 3). +-define(UNSUPPORTED_VERSION, 4). +-define(CANCEL, 5). +-define(FLOW_CONTROL_ERROR, 6). +-define(STREAM_IN_USE, 7). +-define(STREAM_ALREADY_CLOSED, 8). + +-record(control_frame, {version, type, flags, data}). +-record(rst_stream, {stream_id, status}). +-record(headers, {headers, stream_id}). +-record(data_frame, {stream_id, flags, data}). +-record(syn_reply, {stream_id, flags, headers}). +-record(syn_stream, {stream_id, flags, headers, associated_stream_id}). +-record(goaway, {last_good_stream_id}). +-record(noop, {}). diff --git a/include/espdy_server.hrl b/include/espdy_server.hrl new file mode 100644 index 0000000..b7d7643 --- /dev/null +++ b/include/espdy_server.hrl @@ -0,0 +1 @@ +-record(espdy_socket, {pid, stream_id}). diff --git a/rebar b/rebar new file mode 100755 index 0000000000000000000000000000000000000000..c3e01cc123ac180e1c41f383eeae23275294ace5 GIT binary patch literal 108325 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 s)Pdlh2^+xwrg-2acqRfOQ6&)}LGIZMc6>) -> + decode_frame(Buffer); +read_frame(<<>>, Data) -> + decode_frame(Data); +read_frame(Buffer, Data) -> + decode_frame(<>). + +decode_frame(<>) -> + {#control_frame{version = Version, type = Type, flags = Flags, data = Data}, Rest}; +decode_frame(<>) -> + {#data_frame{stream_id = StreamID, flags = Flags, data = Data}, Rest}; +decode_frame(Data) -> + Data. + +decode_exactly_one_frame(Data) -> + {Frame, <<>>} = decode_frame(Data), + Frame. + + +decode_control_frame(Frame) -> + ZLib = initialize_zlib_for_inflate(), + ControlFrame = decode_control_frame(ZLib, Frame), + zlib:close(ZLib), + ControlFrame. + + +decode_control_frame(_ZLib, #control_frame{type = ?GOAWAY, data = + <>}) -> + #goaway{last_good_stream_id = LastGoodStreamId}; + +decode_control_frame(_ZLib, #control_frame{type = ?RST_STREAM, data = + <>}) -> + #rst_stream{stream_id = StreamId, status = StatusCode}; + + +decode_control_frame(_ZLib, #control_frame{type = ?NOOP, data = <<>>}) -> + #noop{}; + +decode_control_frame(ZLib, #control_frame{type = ?HEADERS, data = + <>}) -> + Headers = decode_name_value_header_block(ZLib, NameValueHeaderBlock), + #headers{stream_id = StreamId, headers = Headers}; + +decode_control_frame(ZLib, #control_frame{flags = Flags, type = ?SYN_REPLY, data = + <>}) -> + Headers = decode_name_value_header_block(ZLib, NameValueHeaderBlock), + #syn_reply{flags = Flags, stream_id = StreamId, headers = Headers}; + +decode_control_frame(ZLib, #control_frame{flags = Flags, type = ?SYN_STREAM, data = + <>}) -> + Headers = decode_name_value_header_block(ZLib, NameValueHeaderBlock), + #syn_stream{flags = Flags, stream_id = StreamId, headers = Headers, associated_stream_id = AssociatedToStreamId}. + +validate_valid_values(Pairs) -> + fold_error(fun validate_value/1, Pairs). + +validate_value({_Name, <<>>}) -> + {error, empty_value}; +validate_value({_Name, <<0, _Rest/binary>>}) -> + {error, value_starts_with_null}; +validate_value({_Name, Value}) -> + case binary:last(Value) =:= 0 of + true -> {error, value_ends_with_null}; + _ -> + case binary:match(Value, <<0,0>>) of + nomatch -> ok; + _ -> {error, empty_value} + end + end. + + +validate_no_dups(Pairs) -> + SortedPairs = lists:ukeysort(1, Pairs), + case length(SortedPairs) =:= length(Pairs) of + true -> ok; + false -> {error, duplicate_header_names} + end. + +expand_headers(Pairs) -> + lists:map(fun expand_header/1, Pairs). + +expand_header({Name, <<>>}) -> + {Name, []}; +expand_header({Name, Value}) -> + Values = binary:split(Value, <<0>>), + {Name, Values}. + +chain_validations([], _Data) -> + ok; +chain_validations([F|Rest], Data) -> + case F(Data) of + ok -> chain_validations(Rest, Data); + Error -> Error + end. + +fold_error(_Fun, []) -> + ok; +fold_error(Fun, [H|Rest]) -> + case Fun(H) of + ok -> fold_error(Fun, Rest); + Error -> Error + end. + +validate_headers_names_valid(Pairs) -> + fold_error(fun validate_headers_name_valid/1, Pairs). + +validate_headers_name_valid({<<>>, _Value}) -> + {error, empty_header_name}; +validate_headers_name_valid(_) -> + ok. + +make_headers(Pairs) -> + case chain_validations([fun validate_no_dups/1, fun validate_valid_values/1, fun validate_headers_names_valid/1], Pairs) of + ok -> + expand_headers(Pairs); + Error -> Error + end. + +decode_name_value_header_block(ZLib, Binary) -> + Iodata = + try + zlib:inflate(ZLib, Binary) + catch + error : {need_dictionary,3751956914} -> + zlib:inflateSetDictionary(ZLib, dictionary()), + zlib:inflate(ZLib, []) + end, + decode_name_value_header_block(list_to_binary(Iodata)). + +encode_name_value_header_block(ZLib, NameValuePairs) -> + Binary = encode_name_value_header_block(NameValuePairs), + zlib:deflate(ZLib, Binary, sync). + +encode_name_value_header_block(NameValuesPairs) -> + lists:foldl(fun (E, Acc) -> <> end, + <>, + NameValuesPairs). + +encode_name_values({Name, Values}) -> + EncodedValues = encode_values(Values), + <>. + +encode_values(Values) -> + lists:foldl( + fun (<<>>, _) -> + throw(empty_header_value); + (E, <<>>) -> + <>; + (E, Acc) -> + <> + end, + <<>>, Values). + +decode_name_value_header_block(<>) -> + make_headers(decode_name_value(Repeats, NumberOfNameValuePairs, [])). + +decode_name_value(_Binary, 0, Acc) -> + Acc; +decode_name_value(Binary0, N, Acc) -> + {Value, Binary} = decode_name_value(Binary0), + decode_name_value(Binary, N - 1, [Value | Acc]). + +decode_name_value(<>) -> + {{Name, Value}, Rest}. + +dictionary() -> + <<"optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-" + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi" + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser" + "-agent10010120020120220320420520630030130230330430530630740040140240340440" + "5406407408409410411412413414415416417500501502503504505accept-rangesageeta" + "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic" + "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran" + "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati" + "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo" + "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe" + "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic" + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1" + ".1statusversionurl", 0>>. + + +initialize_zlib_for_deflate() -> + Z = zlib:open(), + zlib:deflateInit(Z, default, deflated, 15, 8, default), + zlib:deflateSetDictionary(Z, dictionary()), + Z. + +initialize_zlib_for_inflate() -> + Z = zlib:open(), + zlib:inflateInit(Z, 15), + Z. + +encode_data_frame(StreamId, Flags, Data) -> + <>. + +encode_control_frame(Type, Flags, Data) -> + <>. + +encode_noop() -> + encode_control_frame(?NOOP, 0, <<>>). + +encode_goaway(LastStreamId) -> + encode_control_frame(?GOAWAY, 0, <>). + +encode_ping(ID) -> + encode_control_frame(?PING, 0, <>). + +encode_rst_stream(StreamId, StatusCode) -> + encode_control_frame(?RST_STREAM, 0, + <<0:1, ?STREAM_ID(StreamId), + ?UINT32(StatusCode)>>). + +encode_syn_stream_with_raw_uncompressed_name_value_pairs(Flags, StreamId, AssociatedToStreamId, Priority, RawBlock) -> + ZLib = initialize_zlib_for_deflate(), + NameValueHeaderBlock = list_to_binary(zlib:deflate(ZLib, RawBlock, sync)), + encode_syn_stream_with_raw_name_value_pairs(Flags, StreamId, AssociatedToStreamId, Priority, NameValueHeaderBlock). + +encode_syn_stream_with_raw_name_value_pairs(Flags, StreamId, AssociatedToStreamId, Priority, NameValueHeaderBlock) -> + encode_control_frame(?SYN_STREAM, Flags, + <>). + +encode_syn_stream(Flags, StreamId, AssociatedToStreamId, Priority, NameValuePairs) -> + ZLib = initialize_zlib_for_deflate(), + Frame = encode_syn_stream(ZLib, Flags, StreamId, AssociatedToStreamId, Priority, NameValuePairs), + zlib:close(ZLib), + Frame. + +encode_syn_stream(ZLib, Flags, StreamId, AssociatedToStreamId, Priority, NameValuePairs) -> + NameValueHeaderBlock = list_to_binary(encode_name_value_header_block(ZLib, NameValuePairs)), + encode_syn_stream_with_raw_name_value_pairs(Flags, StreamId, AssociatedToStreamId, Priority, NameValueHeaderBlock). + +encode_syn_reply(StreamId, Flags, Headers) -> + ZLib = initialize_zlib_for_deflate(), + encode_syn_reply(StreamId, Flags, Headers), + zlib:close(ZLib). + +encode_syn_reply(ZLib, StreamId, Flags, Headers) -> + NameValueHeaderBlock = list_to_binary(encode_name_value_header_block(ZLib, Headers)), + Frame = << + ?MAKE_RESERVED_BIT, ?STREAM_ID(StreamId), + ?MAKE_UNUSED(?UNUSED_SYN_REPLY), + NameValueHeaderBlock/binary>>, + encode_control_frame(?SYN_REPLY, Flags, Frame). + +encode_headers(StreamId, Flags, Headers) -> + ZLib = initialize_zlib_for_deflate(), + Frame = encode_headers(ZLib, StreamId, Flags, Headers), + zlib:close(ZLib), + Frame. + +encode_headers_with_raw_uncompressed_name_value_pairs(ZLib, StreamId, Flags, Headers) -> + encode_headers_with_compressed_name_value_header_block(StreamId, Flags, list_to_binary(zlib:deflate(ZLib, Headers, sync))). + +encode_headers_with_raw_uncompressed_name_value_pairs(StreamId, Flags, Headers) -> + ZLib = initialize_zlib_for_deflate(), + Frame = encode_headers_with_raw_uncompressed_name_value_pairs(StreamId, Flags, Headers), + zlib:close(ZLib), + Frame. + +encode_headers(ZLib, StreamId, Flags, Headers) -> + NameValueHeaderBlock = list_to_binary(encode_name_value_header_block(ZLib, Headers)), + encode_headers_with_compressed_name_value_header_block(StreamId, Flags, NameValueHeaderBlock). + +encode_headers_with_compressed_name_value_header_block(StreamId, Flags, NameValueHeaderBlock) -> + Frame = << + ?MAKE_RESERVED_BIT, ?STREAM_ID(StreamId), + ?MAKE_UNUSED(?UNUSED_HEADERS), + NameValueHeaderBlock/binary>>, + encode_control_frame(?HEADERS, Flags, Frame). \ No newline at end of file diff --git a/src/espdy_mock_socket.erl b/src/espdy_mock_socket.erl new file mode 100644 index 0000000..58c6fd9 --- /dev/null +++ b/src/espdy_mock_socket.erl @@ -0,0 +1,30 @@ +-module(espdy_mock_socket, [PacketReceiver]). + +-export([send/1, close/0, shutdown/0, setopts/1, controlling_process/1, data_tag/0, error_tag/0, close_tag/0]). + +close_tag() -> + tcp_closed. + +error_tag() -> + tcp_error. + +data_tag() -> + tcp. + +send(Packet) -> + PacketReceiver ! {packet, self(), Packet}, + receive + {mock_packet_result, Result} -> Result + end. + +close() -> + ok. + +setopts(Options) -> + ok. + +shutdown() -> + ok. + +controlling_process(Pid) -> + ok. \ No newline at end of file diff --git a/src/espdy_server.erl b/src/espdy_server.erl new file mode 100644 index 0000000..17dc2d2 --- /dev/null +++ b/src/espdy_server.erl @@ -0,0 +1,799 @@ +-module(espdy_server). +-compile([{parse_transform, lager_transform}]). +-behaviour(gen_server). + +-include("espdy_frame.hrl"). +-include("espdy_server.hrl"). + +-export([start_link/2]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, socket_writer/1, graceful_close/1]). + + +-export([accept/1, + connect/1, + connect/3, + connect_for_push/3, + send_headers/2, + send/2, + close/1, + send_and_close/2, + recv/3, recv/2, + statistics/1, + stream_id/1]). + +% TESTING API ONLY. NOT SAFE TO USE +-export([async_accept/1, async_send_headers/2, async_send/2, async_close/1, async_send_and_close/2, + async_recv/3, async_recv/2]). + +make_socket(Pid, StreamId) -> + #espdy_socket{pid = Pid, stream_id = StreamId}. + +accept(Pid) -> + case send_call(Pid, accept) of + {ok, StreamId, Headers} -> {ok, make_socket(Pid, StreamId), Headers}; + Error -> Error + end. + +connect(Pid) -> + connect(Pid, false, []). + +connect_for_push(#espdy_socket{pid = Pid, stream_id = StreamId}, SendFin, Headers) -> + connect(Pid, SendFin, Headers, StreamId). + +connect(Pid, SendFin, Headers) -> + connect(Pid, SendFin, Headers, 0). + +connect(Pid, SendFin, Headers, AssociatedStreamId) -> + case send_call(Pid, {connect, Headers, SendFin, AssociatedStreamId}) of + {ok, StreamId} -> {ok, make_socket(Pid, StreamId)}; + Error -> Error + end. + +graceful_close(Pid) -> + gen_server:cast(Pid, graceful_close). + +send_headers(#espdy_socket{pid = Pid, stream_id = StreamId}, Headers) -> + send_call(Pid, {send_headers, StreamId, Headers}). + +send(#espdy_socket{pid = Pid, stream_id = StreamId}, Data) -> + send_call(Pid, {send, StreamId, Data}). + +send_and_close(#espdy_socket{pid = Pid, stream_id = StreamId}, Data) -> + send_call(Pid, {send_and_close, StreamId, Data}). + +close(#espdy_socket{pid = Pid, stream_id = StreamId}) -> + send_call(Pid, {close, StreamId}). + +recv(#espdy_socket{pid = Pid, stream_id = StreamId}, Length, Timeout) -> + send_call(Pid, {recv, StreamId, Length, Timeout}). + + + +recv(Socket, Length) -> + recv(Socket, Length, infinity). + +statistics(Pid) -> + send_call(Pid, statistics). + +stream_id(#espdy_socket{stream_id = StreamId}) -> + StreamId. + +async_accept(Pid) -> + send_async(Pid, accept). + +async_send(#espdy_socket{pid = Pid, stream_id = StreamId}, Data) -> + send_async(Pid, {send, StreamId, Data}). + +async_recv(#espdy_socket{pid = Pid, stream_id = StreamId}, Length, Timeout) -> + send_async(Pid, {recv, StreamId, Length, Timeout}). + +async_recv(Socket, Length) -> + async_recv(Socket, Length, infinity). + +async_send_headers(#espdy_socket{pid = Pid, stream_id = StreamId}, Headers) -> + send_async(Pid, {send_headers, StreamId, Headers}). + +async_close(#espdy_socket{pid = Pid, stream_id = StreamId}) -> + send_async(Pid, {close, StreamId}). + +async_send_and_close(#espdy_socket{pid = Pid, stream_id = StreamId}, Data) -> + send_async(Pid, {send_and_close, StreamId, Data}). + +send_async(Pid, Msg) -> + Ref = make_ref(), + Pid ! {'$gen_call', {self(), Ref}, Msg}, + Ref. + +send_call(Pid, Request) -> + try + gen_server:call(Pid, Request, infinity) + catch + % not sure if this is too broad... + exit:_ -> + {error, closed} + end. + +-record(state, { + last_stream_accepted = 0, + next_syn_stream_id, + closing = false, + other_side_closing = false, + data_tag, + close_tag, + role, + socket, + socket_writer = undefined, + zlib_read_nv_context = undefined, + zlib_write_nv_context = undefined, + read_buffer = <<>>, + streams = dict:new(), + accept_buffer = [], + waiting_acceptors = []}). + +-record(stream, { + + read_open = true, + write_closing = false, + write_open = true, + read_buffer = <<>>, + recv_waiters = [], + send_waiters = [], + send_header_waiters = [], + syn_reply_sent = false}). + +stream_to_proplist(#stream{} = Rec) -> + lists:zip(record_info(fields, stream), tl(tuple_to_list(Rec))). + +debug_streams(#state{streams = Streams}) -> + lists:map(fun({N, S}) -> {N, stream_to_proplist(S) } end, dict:to_list(Streams)). + +start_link(Socket, Role) -> + case gen_server:start_link(espdy_server, [Socket, Role], []) of + {ok, Pid} = Result -> + ok = Socket:controlling_process(Pid), + ok = gen_server:call(Pid, start), + Result; + Error -> Error + end. + +initial_stream_id(server) -> + 2; +initial_stream_id(client) -> + 1. + +init([Socket, Role]) -> + {ok, #state{ + next_syn_stream_id = initial_stream_id(Role), + data_tag = Socket:data_tag(), + close_tag = Socket:close_tag(), + socket = Socket, + socket_writer = spawn_link(?MODULE, socket_writer, [Socket]), + role = Role, + zlib_read_nv_context = espdy_frame:initialize_zlib_for_inflate(), + zlib_write_nv_context = espdy_frame:initialize_zlib_for_deflate() + }}. + +socket_writer(Socket) -> + receive + {send, Data, Callback} -> + lager:debug("Sending Data ~p", [Data]), + Callback(Socket:send(Data)), + socket_writer(Socket); + _ -> + socket_writer(Socket) + end. + +socket_write(State, Data) -> + socket_write(State, Data, fun(_) -> ok end). + +socket_write(State, Data, CallbackFun) -> + (State#state.socket_writer) ! {send, Data, CallbackFun}. + +split_at_or_at_end(N, Binary) when byte_size(Binary) > N -> + split_at(N, Binary); +split_at_or_at_end(_N, Binary) -> + {Binary, <<>>}. + +split_at(N, Binary) -> + case Binary of + <> -> {Bytes, Rest} + end. + +handle_receive_need_more_data(_StreamId, #stream{read_open = false}, _Length, _From, State) -> + {reply, {error, closed}, State}; +handle_receive_need_more_data(StreamId, StreamRecord, Length, From, State) -> + RecvWaiters = StreamRecord#stream.recv_waiters, + NewStreams = dict:store(StreamId, StreamRecord#stream{recv_waiters = [{From, Length} | RecvWaiters]}, State#state.streams), + {noreply, State#state{streams = NewStreams}}. + +handle_receive(StreamId, StreamRecord, Length, From, State) -> + ReadBuffer = StreamRecord#stream.read_buffer, + case byte_size(ReadBuffer) >= Length of + true -> + {First, Rest} = split_at(Length, ReadBuffer), + NewStreams = dict:store(StreamId, StreamRecord#stream{read_buffer = Rest}, State#state.streams), + {reply, First, State#state{streams = NewStreams}}; + _ -> + handle_receive_need_more_data(StreamId, StreamRecord, Length, From, State) + end. + +write_headers(State, StreamId, Stream, Data, From) -> + Self = self(), + + WriteFun = fun(Result) -> + case Result of + ok -> + Self ! {header_finished, {ok, StreamId}}; + Reason -> + Self ! {write_error, Reason} + end + end, + + socket_write(State, Data, WriteFun), + Stream#stream{syn_reply_sent = true, send_header_waiters = Stream#stream.send_header_waiters ++ [From]}. + + + + +handle_send_headers(State, StreamId, #stream{syn_reply_sent = true} = StreamRecord, Headers, From) -> + Packet = espdy_frame:encode_headers(State#state.zlib_write_nv_context, StreamId, 0, Headers), + write_headers(State, StreamId, StreamRecord, Packet, From); + +handle_send_headers(State, StreamId, #stream{syn_reply_sent = false} = StreamRecord, Headers, From) -> + SynReply = espdy_frame:encode_syn_reply(State#state.zlib_write_nv_context, StreamId, 0, Headers), + write_headers(State, StreamId, StreamRecord, SynReply, From). + +ensure_syn_reply_sent(_State, _StreamId, #stream{syn_reply_sent = true} = Stream) -> + Stream; +ensure_syn_reply_sent(State, StreamId, Stream) -> + Data = espdy_frame:encode_syn_reply(State#state.zlib_write_nv_context, StreamId, 0, []), + WriteFun = fun(_Result) -> + ok % TODO: handle write error + end, + + socket_write(State, Data, WriteFun), + Stream#stream{syn_reply_sent = true}. + +write_flags(true) -> + ?FLAG_FIN; +write_flags(_) -> + 0. + +update_write_closing_flag(_Stream, true) -> + true; +update_write_closing_flag(#stream{write_closing = Closing}, _Fin) -> + Closing. + +perform_write(State, StreamId, Stream0, Buffer, Fin, From) -> + Stream = ensure_syn_reply_sent(State, StreamId, Stream0), + perform_write_prime(State, StreamId, Stream, Buffer, Fin, From). + +perform_write_prime(State, StreamId, Stream, BufferToSend, Fin, From) -> + Self = self(), + Data = espdy_frame:encode_data_frame(StreamId, write_flags(Fin), BufferToSend), + + WriteFun = fun(Result) -> + + case Result of + ok -> + Self ! {write_finished, {ok, StreamId, Fin, true}}; + Reason -> + Self ! {write_error, Reason} + end + end, + + socket_write(State, Data, WriteFun), + Stream#stream{write_closing = update_write_closing_flag(Stream, Fin), send_waiters = Stream#stream.send_waiters ++ [From]}. + + +handle_send(StreamId, StreamRecord, Packet, Fin, From, State) -> + StreamRecord1 = perform_write(State, StreamId, StreamRecord, Packet, Fin, From), + update_stream(StreamId, StreamRecord1, State). + +update_stream(StreamId, StreamRecord, State) -> + NewStreams = dict:store(StreamId, StreamRecord, State#state.streams), + State#state{streams = NewStreams}. + +handle_accept(_From, #state{closing = true} = State) -> + {reply, {error, closed}, State}; +handle_accept(_From, #state{accept_buffer = [{StreamId, Headers} | Rest]} = State) -> + {reply, {ok, StreamId, Headers}, State#state{accept_buffer = Rest}}; +handle_accept(From, State) -> + {noreply, State#state{waiting_acceptors = State#state.waiting_acceptors ++ [From]}}. + +send_fin(StreamId, Stream, State) -> + Data = espdy_frame:encode_data_frame(StreamId, ?FLAG_FIN, <<>>), + Self = self(), + WriteFun = fun(Result) -> + + case Result of + ok -> + Self ! {write_finished, {ok, StreamId, true, false}}; + Reason -> + Self ! {write_error, Reason} + end + end, + + socket_write(State, Data, WriteFun), + + Stream#stream{write_closing = true}. + +handle_close(StreamId, Stream, State) -> + send_fin(StreamId, Stream, State). + +syn_flags(WithFin, StreamId) -> + syn_flags_fin(WithFin) bor syn_flags_uni(StreamId). + +syn_flags_fin(true) -> + ?FLAG_FIN; +syn_flags_fin(_WithFin) -> + 0. + +syn_flags_uni(0) -> + 0; +syn_flags_uni(_) -> + ?FLAG_UNIDIRECTIONAL. + +handle_connect(Headers, WithFin, AssociatedStreamId, _From, State) -> + StreamId = State#state.next_syn_stream_id, + + Frame = espdy_frame:encode_syn_stream(State#state.zlib_write_nv_context, syn_flags(WithFin, AssociatedStreamId), StreamId, AssociatedStreamId, 0, Headers), + lager:debug("Sending connect frame StreamId: ~p AssociatedStreamId: ~p SynFlags: ~p Headers: ~p", [StreamId, AssociatedStreamId, syn_flags(WithFin, AssociatedStreamId), Headers]), + + socket_write(State, Frame), + % TODO: FIX fully closed streams here + State1 = update_stream(StreamId, #stream{syn_reply_sent = true, write_open = (not WithFin), read_open = (AssociatedStreamId =:= 0)}, State), + {reply, {ok, StreamId}, State1#state{next_syn_stream_id = StreamId + 2}}. + + +run_with_stream(StreamId, State, Function) -> + case dict:find(StreamId, State#state.streams) of + error -> + lager:debug("Stream not found ~p ~p", [StreamId, debug_streams(State)]), + {reply, {error, closed}, State}; + {ok, StreamRecord} -> + Function(StreamRecord) + end. + +is_write_open(#stream{write_closing = false, write_open = true}) -> + true; +is_write_open(_) -> + false. + +run_with_stream_write_open(StreamId, State, Function) -> + run_with_stream(StreamId, State, + fun(Stream) -> + case is_write_open(Stream) of + true -> Function(Stream); + _ -> {reply, {error, closed}, State} + end + end). + + + +is_closing(#state{closing = true}) -> + true; +is_closing(#state{other_side_closing = true}) -> + true; +is_closing(_) -> + false. + +handle_call({connect, Headers, WithFin, AssociatedStreamId}, From, State) -> + case is_closing(State) of + true -> + {reply, {error, closed_not_processed}, State}; + _ -> + handle_connect(Headers, WithFin, AssociatedStreamId, From, State) + end; +handle_call({close, StreamId}, _From, State) -> + run_with_stream(StreamId, State, fun(StreamRecord) -> + NewStream = handle_close(StreamId, StreamRecord, State), + {reply, ok, update_stream(StreamId, NewStream, State)} + end); + +handle_call(statistics, _From, State) -> + lager:debug("Stream status for statistics ~p", [debug_streams(State)]), + {reply, get_statistics(State), State}; + +handle_call(accept, From, State) -> + handle_accept(From, State); + +handle_call({send_headers, StreamId, Headers}, From, State) -> + run_with_stream_write_open(StreamId, State, fun(StreamRecord) -> + NewStream = handle_send_headers(State, StreamId, StreamRecord, Headers, From), + {noreply, update_stream(StreamId, NewStream, State)} + end); + +handle_call({send_and_close, StreamId, Binary}, From, State) -> + run_with_stream_write_open(StreamId, State, fun(StreamRecord) -> + {noreply, handle_send(StreamId, StreamRecord, Binary, true, From, State)} + end); +handle_call({send, StreamId, Binary}, From, State) -> + run_with_stream_write_open(StreamId, State, fun(StreamRecord) -> + {noreply, handle_send(StreamId, StreamRecord, Binary, false, From, State)} + end); + +handle_call({recv, StreamId, Length, _Timeout}, From, State) -> + run_with_stream(StreamId, State, + fun(StreamRecord) -> + handle_receive(StreamId, StreamRecord, Length, From, State) + end); + +handle_call(start, _From, State) -> + (State#state.socket):setopts([{active, once}, {packet, raw}, {mode, binary}]), + {reply, ok, State}; + +handle_call(Call, _From, State) -> + lager:debug("Received invalid call ~p", [Call]), + {reply, {error, unknown_call}, State}. + +handle_cast(graceful_close, #state{closing = true} = State) -> + {noreply, State}; +handle_cast(graceful_close, #state{closing = false} = State) -> + socket_write(State, espdy_frame:encode_goaway(State#state.last_stream_accepted)), + case dict:size(State#state.streams) of + 0 -> + {stop, normal, State}; + _ -> + {noreply, State#state{closing = true}} + end; + + +handle_cast(_Call, State) -> + {noreply, State}. + +is_ping_reply(ID, Role) -> + (ID rem 2 =:= 0) =:= (Role =:= server). + +handle_ping_reply(ID, State) -> + lager:debug("Received PING reply ~p", [ID]), + State. + +handle_ping(ID, State) -> + lager:debug("Received PING ~p", [ID]), + socket_write(State, espdy_frame:encode_ping(ID)), + State. + + +stream_error_for_open_stream(StreamId, Stream, Status, State) -> + socket_write(State, espdy_frame:encode_rst_stream(StreamId, Status)), + stream_abnormally_closed(StreamId, Stream, State). + +stream_error(StreamId, Status, State) -> + socket_write(State, espdy_frame:encode_rst_stream(StreamId, Status)), + State. + +is_flag_fin(Flags) -> + (Flags band ?FLAG_FIN) =:= ?FLAG_FIN. + +send_closed(From) -> + lager:debug("Sending {error, closed} to ~p ", [From]), + gen_server:reply(From, {error, closed}). + +stream_abnormally_closed(StreamId, Stream, State) -> + stream_read_closed(Stream), + stream_write_abnormally_closed(Stream), + NewStreams = dict:erase(StreamId, State#state.streams), + State#state{streams = NewStreams}. + +stream_write_abnormally_closed(Stream) -> + lists:foreach(fun send_closed/1, Stream#stream.send_waiters). + +stream_read_closed(Stream) -> + [send_closed(From) || {From, _Length} <- Stream#stream.recv_waiters]. + + + +add_stream_data(Stream, Data) -> + ReadBuffer = Stream#stream.read_buffer, + lager:debug("Reply To Receivers ~p ~p ~n", [Data, Stream#stream.recv_waiters]), + {NewBuffer, NewReceivers} = reply_to_receivers(<>, lists:reverse(Stream#stream.recv_waiters)), + Stream#stream{read_buffer = NewBuffer, recv_waiters = NewReceivers}. + +reply_to_receivers(Buffer, []) -> + {Buffer, []}; +reply_to_receivers(Binary, [{From, Length} | Rest] = Receivers) -> + case Binary of + <> -> + lager:debug("Reply To Receiver ~p ~p ~n", [From, Packet]), + gen_server:reply(From, Packet), + reply_to_receivers(RestOfBuffer, Rest); + _ -> + {Binary, Receivers} + end. + +update_open_stream(Stream, Flags) -> + case is_flag_fin(Flags) of + true -> + lager:debug("Closing Stream", []), + stream_read_closed(Stream), + Stream#stream{read_open = false, recv_waiters = []}; + _ -> + Stream + end. + + +handle_valid_data_frame(StreamId, #stream{read_open = false} = Stream, _Flags, _Data, State) -> + stream_error_for_open_stream(StreamId, Stream, ?INVALID_STREAM, State); + +handle_valid_data_frame(StreamId, Stream, Flags, Data, State) -> + Stream1 = add_stream_data(Stream, Data), + Stream2 = update_open_stream(Stream1, Flags), + check_if_stream_is_fully_closed_and_update_stream(StreamId, Stream2, State). + +new_stream(StreamId, Headers, #state{waiting_acceptors = [H|Rest]} = State) -> + gen_server:reply(H, {ok, StreamId, Headers}), + State#state{waiting_acceptors = Rest}; +new_stream(StreamId, Headers, State) -> + State#state{accept_buffer = State#state.accept_buffer ++ [{StreamId, Headers}]}. + +expected_stream_modulus(#state{role = server}) -> + 1; +expected_stream_modulus(#state{role = client}) -> + 0. + +can_accept_stream(State) -> + State#state.closing =:= false. + +is_valid_stream_id(State, StreamId) -> + (StreamId =/= 0) and (expected_stream_modulus(State) =:= (StreamId rem 2)) and (State#state.last_stream_accepted < StreamId). + +check_headers(StreamId, {error, Reason}, _Flags, State) -> + lager:debug("Receive invalid header ~p", [Reason]), + stream_error(StreamId, ?PROTOCOL_ERROR, State); + +check_headers(StreamId, Headers, Flags, State) -> + NewStreams = dict:store(StreamId, #stream{read_open = not is_flag_fin(Flags)}, State#state.streams), + new_stream(StreamId, Headers, State#state{streams = NewStreams, last_stream_accepted = StreamId}). + +accept_stream(State, StreamId, Flags, Headers) -> + + case is_valid_stream_id(State, StreamId) of + true -> + check_headers(StreamId, Headers, Flags, State); + false -> + stream_error(StreamId, ?PROTOCOL_ERROR, State), + State + end. + +refuse_stream(State, StreamId) -> + stream_error(StreamId, ?REFUSED_STREAM, State), + State. + +with_stream(StreamId, State, Fun) -> + case dict:find(StreamId, State#state.streams) of + error -> + lager:debug("Stream Error: INVALID STREAM", []), + stream_error(StreamId, ?INVALID_STREAM, State); + {ok, Stream} -> + Fun(Stream) + end. + +validate_header_frame(Stream, Headers) -> + case {Headers, Stream#stream.read_open} of + {{error, _Reason}, _} -> + {error, ?PROTOCOL_ERROR}; + {_, false } -> + {error, ?INVALID_STREAM}; + _ -> + ok + end. + +handle_frame(#control_frame{type = ?GOAWAY, data = <>}, State) -> + State#state{other_side_closing = true}; +handle_frame(#control_frame{type = ?HEADERS, data = + <>}, State) -> + %% must always decode headers + Headers = espdy_frame:decode_name_value_header_block(State#state.zlib_read_nv_context, NameValueHeaderBlock), + lager:debug("Receive headers ~p", [Headers]), + + with_stream(StreamId, State, fun(Stream) -> + case validate_header_frame(Stream, Headers) of + {error, Status} -> + stream_error(StreamId, Status, State); + _ -> + lager:debug("Received valid headers but we ignore headers..."), + State + end + end); + +handle_frame(#control_frame{type = ?NOOP}, State) -> + State; + +handle_frame(#control_frame{type = ?PING, data = <>}, State) -> + case is_ping_reply(ID, State#state.role) of + true -> handle_ping_reply(ID, State); + _ -> handle_ping(ID, State) + end; + +handle_frame(#control_frame{flags = Flags, type = ?SYN_STREAM, data = + <>}, State) -> + + lager:debug("Received SYN STREAM for ~p with NameValueHeaderBlock size ~p", [StreamId, byte_size(NameValueHeaderBlock)]), + % always need to decode headers to stop streams getting of sync + + Headers = espdy_frame:decode_name_value_header_block(State#state.zlib_read_nv_context, NameValueHeaderBlock), + + case can_accept_stream(State) of + true -> + accept_stream(State, StreamId, Flags, Headers); + false -> + refuse_stream(State, StreamId) + end; + +handle_frame(#control_frame{type = ?RST_STREAM, data = + <>}, State) -> + case dict:find(StreamId, State#state.streams) of + error -> + lager:debug("Received RST_STREAM for unknown stream: ~p (~p)", [StreamId, StatusCode]), + State; + {ok, Stream} -> + lager:debug("Received RST_STREAM for stream: ~p (~p)", [StreamId, StatusCode]), + stream_abnormally_closed(StreamId, Stream, State) + end; + +handle_frame(#data_frame{stream_id = StreamId, flags = Flags, data = Data}, State) -> + with_stream(StreamId, State, fun(Stream) -> + handle_valid_data_frame(StreamId, Stream, Flags, Data, State) + end); + +handle_frame(Frame, State) -> + lager:debug("Received Unknown Frame: ~p", [Frame]), + State. + +handle_data(Data, State0) -> + case espdy_frame:read_frame(State0#state.read_buffer, Data) of + {Frame, NewBuffer} -> + lager:debug("Received frame ~p", [Frame]), + State = handle_frame(Frame, State0), + handle_data(<<>>, State#state{read_buffer = NewBuffer}); + NewBuffer -> + lager:debug("Buffering... ~p", [NewBuffer]), + (State0#state.socket):setopts([{active, once}]), + State0#state{read_buffer = NewBuffer} + end. + +notify_write_finished(Stream, true) -> + case Stream#stream.send_waiters of + [Top | Rest] -> + gen_server:reply(Top, ok), + Stream#stream{send_waiters = Rest}; + _ -> + %WTF + Stream + end; + +notify_write_finished(Stream, false) -> + Stream. + +stream_finished(StreamId, _Stream, State) -> + lager:debug("Stream fully closed ~p", [StreamId]), + State#state{streams = dict:erase(StreamId, State#state.streams)}. + +check_if_stream_is_fully_closed_and_update_stream(StreamId, #stream{read_open = false, write_open = false} = Stream, State) -> + stream_finished(StreamId, Stream, State); +check_if_stream_is_fully_closed_and_update_stream(StreamId, Stream, State) -> + update_stream(StreamId, Stream, State). + +check_for_graceful_shutdown(State) -> + case (State#state.closing =:= true) and (dict:size(State#state.streams) =:= 0) of + true -> + lager:debug("scheduled shutdown", []), + {stop, normal, State}; + _ -> + {noreply, State} + end. + +handle_write_finished(StreamId, Stream0, Fin, Notify, State) -> + Stream1 = notify_write_finished(Stream0, Notify), + Stream2 = case Fin of + true -> + Stream1#stream{write_open = false}; + _ -> + Stream1 + end, + + check_if_stream_is_fully_closed_and_update_stream(StreamId, Stream2, State). + +handle_header_finished(StreamId, Stream, State) -> + NewStream = case Stream#stream.send_header_waiters of + [Top | Rest] -> + gen_server:reply(Top, ok), + Stream#stream{send_header_waiters = Rest}; + _ -> + %WTF + Stream + end, + + update_stream(StreamId, NewStream, State). + + +handle_info({header_finished, {ok, StreamId}}, State) -> + lager:debug("header_finished ~p", [StreamId]), + case dict:find(StreamId, State#state.streams) of + {ok, Stream} -> + {noreply, handle_header_finished(StreamId, Stream, State)}; + _ -> + lager:debug("Received header_finished notification for stream that no longer exists ~p", [StreamId]), + {noreply, State} + end; +handle_info({write_error, Reason}, State) -> + lager:debug("Received write_error notification ~p", [Reason]), + {stop, normal, State}; + +handle_info({write_finished, {ok, StreamId, Fin, Notify}}, State) -> + lager:debug("write_finished ~p", [StreamId]), + case dict:find(StreamId, State#state.streams) of + {ok, Stream} -> + State1 = handle_write_finished(StreamId, Stream, Fin, Notify, State), + lager:debug("Stream status ~p", [debug_streams(State1)]), + check_for_graceful_shutdown(State1); + _ -> + lager:debug("Received write_finished notification for stream that no longer exists ~p", [StreamId]), + {noreply, State} + end; + +handle_info({CloseTag, _Socket}, #state{close_tag = CloseTag} = State0) -> + lager:debug("Received closed ~p", [self()]), + %% mmmm... i think this is wrong. need to add tests for half closed tcp + + {stop, normal, State0}; + +handle_info({DataTag, _Socket, Data}, #state{data_tag = DataTag} = State0) -> + lager:debug("Received data ~p Buffer ~p ~n", [Data, State0#state.read_buffer]), + State = handle_data(Data, State0), + lager:debug("Stream status ~p", [debug_streams(State)]), + check_for_graceful_shutdown(State); + + +handle_info(Msg, State) -> + lager:debug("Received unknown message ~p", [Msg]), + {noreply, State}. + +clean_up_listeners(Stream) -> + stream_read_closed(Stream), + stream_write_abnormally_closed(Stream). + +dict_for_each_value(Fun, Dict) -> + dict:fold( + fun(_K, V, Acc) -> + Fun(V), + Acc + end, 0, Dict). + +clean_up_all_listeners(State) -> + dict_for_each_value(fun clean_up_listeners/1, State#state.streams). + + +get_statistics(State) -> + [RO, WO, BO, Z] = dict:fold( + fun (_K, #stream{read_open = true} = Stream, [ReadOpen, WriteOpen, BothOpen, Zombies]) -> + case is_write_open(Stream) of + true -> + [ReadOpen, WriteOpen, BothOpen + 1, Zombies]; + _ -> + [ReadOpen + 1, WriteOpen, BothOpen, Zombies] + end; + (_K, #stream{read_open = false} = Stream , [ReadOpen, WriteOpen, BothOpen, Zombies]) -> + case is_write_open(Stream) of + true -> + [ReadOpen, WriteOpen + 1, BothOpen, Zombies]; + _ -> + [ReadOpen, WriteOpen, BothOpen, Zombies + 1] + end + end, [0, 0, 0, 0], State#state.streams), + [{read_open, RO}, {write_open, WO}, {both_open, BO}, {zombies, Z}]. + + +terminate(_Reason, State) -> + zlib:close(State#state.zlib_read_nv_context), + zlib:close(State#state.zlib_write_nv_context), + clean_up_all_listeners(State), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. \ No newline at end of file diff --git a/src/espdy_ssl_socket.erl b/src/espdy_ssl_socket.erl new file mode 100644 index 0000000..0209b0f --- /dev/null +++ b/src/espdy_ssl_socket.erl @@ -0,0 +1,27 @@ +-module(espdy_ssl_socket, [Socket]). + +-export([send/1, close/0, shutdown/0, setopts/1, controlling_process/1, data_tag/0, close_tag/0, error_tag/0]). + +data_tag() -> + ssl. + +close_tag() -> + ssl_closed. + +error_tag() -> + ssl_error. + +send(Packet) -> + ssl:send(Socket, Packet). + +close() -> + ssl:close(Socket). + +setopts(Options) -> + ssl:setopts(Socket, Options). + +shutdown() -> + ssl:shutdown(Socket). + +controlling_process(Pid) -> + ssl:controlling_process(Socket, Pid). \ No newline at end of file diff --git a/src/espdy_tcp_socket.erl b/src/espdy_tcp_socket.erl new file mode 100644 index 0000000..6cf9d08 --- /dev/null +++ b/src/espdy_tcp_socket.erl @@ -0,0 +1,27 @@ +-module(espdy_tcp_socket, [Socket]). + +-export([send/1, close/0, shutdown/0, setopts/1, controlling_process/1, data_tag/0, error_tag/0, close_tag/0]). + +close_tag() -> + tcp_closed. + +error_tag() -> + tcp_error. + +data_tag() -> + tcp. + +send(Packet) -> + gen_tcp:send(Socket, Packet). + +close() -> + gen_tcp:close(Socket). + +setopts(Options) -> + inet:setopts(Socket, Options). + +shutdown() -> + gen_tcp:shutdown(Socket). + +controlling_process(Pid) -> + gen_tcp:controlling_process(Socket, Pid). \ No newline at end of file diff --git a/src/espdy_test_server.erl b/src/espdy_test_server.erl new file mode 100644 index 0000000..3dbff65 --- /dev/null +++ b/src/espdy_test_server.erl @@ -0,0 +1,133 @@ +-module(espdy_test_server). +-compile([{parse_transform, lager_transform}]). + +-export([start/0]). + +start() -> + lager:start(), + lager:set_loglevel(lager_console_backend, debug), + crypto:start(), + ssl:start(), + {ok, Listen} = ssl:listen(8443, [ + {keyfile, "server.key"}, + {certfile, "server.crt"}, + {reuseaddr, true}, + {ssl_imp, new}, + binary, + {packet, raw}, + {active, false} + ]), + + lager:info("Listening on port 80", []), + + loop(Listen). + +loop(Listen) -> + case ssl:transport_accept(Listen) of + {ok, Socket} -> + case ssl:ssl_accept(Socket) of + ok -> + spawn(fun() -> loop(Listen) end), + accept(Socket); + {error, Reason} -> + lager:info("Error ssl_accept socket", [Reason]), + loop(Listen) + end; + {error, Reason} -> + lager:info("Error accepting socket", [Reason]), + loop(Listen) + end. + +accept(Socket) -> + lager:info("Accepted Socket ~p", [Socket]), + {ok, Pid} = espdy_server:start_link(espdy_ssl_socket:new(Socket), server), + espdy_loop(Pid). + +espdy_loop(Pid) -> + {ok, Socket, Headers} = espdy_server:accept(Pid), + spawn(fun() -> espdy_loop(Pid) end), + espdy_accept(Socket, Headers). + +get_header(Header, Headers) -> + + case lists:keyfind(Header, 1, Headers) of + {_, [Url]} -> {ok, Url}; + _ -> notfound + end. + +espdy_accept(Socket, Headers) -> + io:format(user, "Headers ~p", [Headers]), + ResponseHeaders = [ + {<<"status">>, [<<"200 OK">>]}, + {<<"version">>, [<<"HTTP/1.1">>]}, + {<<"Set-Cookie">>, [<<"cookie1=value1">>, <<"cookie2=value2">>]} + ], + + + case get_header(<<"url">>, Headers) of + {ok, <<"/style.css">>} -> + timer:sleep(1000), + espdy_server:send_headers(Socket, ResponseHeaders), + espdy_server:send(Socket, css_file1()), + espdy_server:close(Socket); + {ok, <<"/style2.css">>} -> + espdy_server:send_headers(Socket, ResponseHeaders), + timer:sleep(500), + espdy_server:send(Socket, css_file2()), + espdy_server:close(Socket); + {ok, <<"/">>} -> + espdy_server:send_headers(Socket, ResponseHeaders), + {ok, Scheme} = get_header(<<"scheme">>, Headers), + {ok, Host} = get_header(<<"host">>, Headers), + Url = list_to_binary([Scheme, "://", Host, "/style3.css"]), + + PushHeaders = [{<<"status">>, <<"200 OK">>}, {<<"version">>, <<"HTTP/1.1">>}, {<<"url">>, Url}, {<<"content-type">>, <<"text/css">>}], + {ok, PushSocket} = espdy_server:connect_for_push(Socket, false, simple_headers(PushHeaders)), + espdy_server:send_and_close(PushSocket, css_file3()), + espdy_server:send_and_close(Socket, html_page()); + _ -> + espdy_server:send_headers(Socket, simple_headers([{<<"status">>, <<"404 Not Found">>}, {<<"version">>, <<"HTTP/1.1">>}])), + espdy_server:close(Socket) + end. + +simple_header({Name, Value}) -> + {Name, [Value]}. + +simple_headers(Headers) -> + lists:map(fun simple_header/1, Headers). + +css_file1() -> + <<".css1 {}">>. +css_file2() -> + <<".css2 {}">>. +css_file3() -> + <<".css3 {}">>. + + +html_page() -> + <<"" + "" + " " + "" + "" + "hello from erlang" + "" + "">>. + diff --git a/test/espdy_frame_test.erl b/test/espdy_frame_test.erl new file mode 100644 index 0000000..877443b --- /dev/null +++ b/test/espdy_frame_test.erl @@ -0,0 +1,131 @@ +-module(espdy_frame_test). +-include_lib("eunit/include/eunit.hrl"). +-include("espdy_frame.hrl"). + +decode_name_value_header_block_test() -> + Headers = espdy_frame:decode_name_value_header_block(<< + ?UINT16(2), + ?UINT16(5), "name1", ?UINT16(4), "valu", + ?UINT16(4), "nam2", ?UINT16(3), "val" + >>), + ?assertEqual(lists:sort([{<<"name1">>, [<<"valu">>]}, {<<"nam2">>, [<<"val">>]}]), lists:sort(Headers)). + +decode_name_value_header_block_with_empty_value_test() -> + Headers = espdy_frame:decode_name_value_header_block(<< + ?UINT16(1), + ?UINT16(5), "name1", ?UINT16(0) + >>), + ?assertMatch({error, _Reason}, Headers). + +decode_name_value_header_block_with_multiple_values_test() -> + Headers = espdy_frame:decode_name_value_header_block(<< + ?UINT16(1), + ?UINT16(5), "name1", ?UINT16(3), "h", 0, "i" + >>), + ?assertEqual([{<<"name1">>, [<<"h">>, <<"i">>]}], Headers). + +decode_name_value_when_starting_with_null_value_test() -> + Headers = espdy_frame:decode_name_value_header_block(<< + ?UINT16(1), + ?UINT16(5), "name1", ?UINT16(2), 0, "h" + >>), + ?assertMatch({error, _Reason}, Headers). + +decode_name_value_when_ending_with_null_value_test() -> + Headers = espdy_frame:decode_name_value_header_block(<< + ?UINT16(1), + ?UINT16(5), "name1", ?UINT16(2), "h", 0 + >>), + ?assertMatch({error, _Reason}, Headers). + +decode_name_value_when_ending_with_null_value2_test() -> + Headers = espdy_frame:decode_name_value_header_block(<< + ?UINT16(1), + ?UINT16(5), "name1", ?UINT16(4), "foo", 0 + >>), + ?assertMatch({error, _Reason}, Headers). + +decode_name_value_with_empty_value_test() -> + Headers = espdy_frame:decode_name_value_header_block(<< + ?UINT16(1), + ?UINT16(5), "name1", ?UINT16(4), "h", 0, 0, "i" + >>), + ?assertMatch({error, _Reason}, Headers). + +decode_name_value_with_empty_header_name_test() -> + Headers = espdy_frame:decode_name_value_header_block(<< + ?UINT16(1), + ?UINT16(0), ?UINT16(5), "hello" + >>), + ?assertMatch({error, _Reason}, Headers). + +encode_name_value_header_block_test() -> + Frame = espdy_frame:encode_name_value_header_block([{<<"hello">>, [<<"world">>]}, {<<"foo">>, [<<"one">>, <<"two">>]}]), + Expected = <>, + ?assertEqual(Expected, Frame). + +encode_name_value_header_block_with_empty_test() -> + Frame = espdy_frame:encode_name_value_header_block([{<<"hello">>, []}]), + Expected = <>, + ?assertEqual(Expected, Frame). + +encode_name_value_header_block_with_empty_value_test() -> + ?assertException(throw, empty_header_value, espdy_frame:encode_name_value_header_block([{<<"hello">>, [<<>>]}])). + + +encode_then_decode_compressed_name_value_header_block_test() -> + ZLibDeflate = espdy_frame:initialize_zlib_for_deflate(), + Frame = list_to_binary(espdy_frame:encode_name_value_header_block(ZLibDeflate, [{<<"host">>, [<<"hostname">>]}])), + ZLibInflate = espdy_frame:initialize_zlib_for_inflate(), + Headers = espdy_frame:decode_name_value_header_block(ZLibInflate, Frame), + ?assertEqual([{<<"host">>, [<<"hostname">>]}], Headers). + + +decode_ping_frame_test() -> + Frame = espdy_frame:encode_ping(1), + {DecodedFrame, Data} = espdy_frame:read_frame(<<>>, Frame), + ?assertEqual(<<>>, Data), + ?assertEqual(#control_frame{version = ?SPDY_VERSION, type = ?PING, flags = 0, data = <<0,0,0,1>>}, DecodedFrame). + +decode_split_ping_frame_test() -> + Frame = espdy_frame:encode_ping(1), + <> = Frame, + {DecodedFrame, Data} = espdy_frame:read_frame(Part1, Part2), + ?assertEqual(<<>>, Data), + ?assertEqual(#control_frame{version = ?SPDY_VERSION, type = ?PING, flags = 0, data = <<0,0,0,1>>}, DecodedFrame). + +chrome_frame_test() -> + Buffer = <<128,2,0,1,1,0,1,31,0,0,0,1,0,0,0,0,0,0,56,234,223,162,81,178,98,224,102,96,131,164,23,6,123,184,11,117,48,44,214,174,64,23,205,205,177,46,180,53,208,179,212,209,210,215,2,179,44,24,248,80,115,44,131,156,103,176,63,212,61,58,96,7,129,213,153,235,64,212,27,51,240,163,229,105,6,65,144,139,117,160,78,214,41,78,73,206,128,171,129,37,3,6,190,212,60,221,208,96,157,212,60,168,165,188,40,137,141,129,19,26,36,182,6,12,44,160,220,207,192,9,74,34,57,96,38,91,46,176,192,201,79,97,96,118,119,13,97,96,43,6,106,203,77,5,170,42,41,41,96,96,6,133,5,163,62,3,23,34,3,51,148,250,230,87,101,230,228,36,234,155,234,25,40,104,0,228,155,152,156,153,87,146,95,156,97,173,224,9,76,83,57,10,64,1,5,255,96,133,8,5,67,131,120,243,120,3,77,5,71,96,240,164,134,167,38,121,103,150,232,155,26,155,234,25,42,104,120,123,132,248,250,232,40,228,100,102,167,42,184,167,38,103,231,107,42,56,103,0,203,165,84,125,67,19,61,160,235,129,170,128,101,131,66,112,98,90,98,81,38,68,19,3,59,52,118,24,56,96,145,6,0,0,0,255,255>> , + {Frame, <<>>} = espdy_frame:decode_frame(Buffer), + DecodedFrame = espdy_frame:decode_control_frame(Frame). + +encode_two_syn_replies_test() -> + Headers = [ + {<<"status">>, [<<"200 OK">>]}, + {<<"version">>, [<<"HTTP/1.1">>]} + ], + + ZLib = espdy_frame:initialize_zlib_for_deflate(), + Syn1 = espdy_frame:encode_syn_reply(ZLib, 1, 0, Headers), + + Syn2 = espdy_frame:encode_syn_reply(ZLib, 3, 0, Headers), + + Inflate = espdy_frame:initialize_zlib_for_inflate(), + + DecodeFrame1 = espdy_frame:decode_exactly_one_frame(Syn1), + Syn1Decoded = espdy_frame:decode_control_frame(Inflate, DecodeFrame1), + + DecodeFrame2 = espdy_frame:decode_exactly_one_frame(Syn2), + Syn2Decoded = espdy_frame:decode_control_frame(Inflate, DecodeFrame2). + +decode_our_headers_test() -> + ZLib = espdy_frame:initialize_zlib_for_inflate(), + Buffer = <<128,2,0,2,0,0,0,39,0,0,0,1,0,0,120,187,223,162,81,178,98,96,98,96,131,8,50,176,1,19,171,130,191,55,3,59,84,154,129,3,166,11,0,0,0,255,255>>, + Frame = espdy_frame:decode_exactly_one_frame(Buffer), + DecodedFrame = espdy_frame:decode_control_frame(ZLib, Frame), + + Buffer2 = <<128,2,0,2,0,0,0,14,0,0,0,3,0,0,34,74,17,0,0,0,255,255>>, + Frame2= espdy_frame:decode_exactly_one_frame(Buffer2), + DecodedFrame2 = espdy_frame:decode_control_frame(ZLib, Frame2). + + diff --git a/test/espdy_server_test.erl b/test/espdy_server_test.erl new file mode 100644 index 0000000..fc7fb9f --- /dev/null +++ b/test/espdy_server_test.erl @@ -0,0 +1,706 @@ +-module(espdy_server_test). +-compile([{parse_transform, lager_transform}]). + +-include_lib("eunit/include/eunit.hrl"). +-include("espdy_frame.hrl"). +-include("espdy_server.hrl"). + +-define(TEST(X), {setup, local, fun setup/0, fun tear_down/1, {atom_to_list(X), fun X/0}}). + +% there has to be an easier way of doing this... right ? + +packet_sim_test_() -> + {spawn, + [ + ?TEST(close_after_send_tst), + ?TEST(syn_reply_tst), + ?TEST(send_tst), + ?TEST(rst_while_waiting_for_read_tst), + ?TEST(rst_read_tst), + ?TEST(flag_fin_read_ok_tst), + ?TEST(flag_fin_tst), + ?TEST(flag_fin_during_tst), + ?TEST(syn_stream_tst), + ?TEST(syn_stream_buffer_tst), + ?TEST(syn_stream_blocking_tst), + ?TEST(server_ping_tst), + ?TEST(client_ping_tst), + ?TEST(server_receive_ping_reply_tst), + ?TEST(client_receive_ping_reply_tst), + ?TEST(statistics_tst), + ?TEST(statistics_read_open_tst), + ?TEST(handle_accept_queue_tst), + ?TEST(handle_receive_write_after_close_from_other_side_tst), + ?TEST(handle_non_existant_data_frame_tst), + ?TEST(handle_write_finished_for_stream_that_no_longer_exists_tst), + ?TEST(handle_header_finished_for_stream_that_no_longer_exists_tst), + ?TEST(receive_closed_when_the_underlying_connection_is_closed_beneath_us_tst), + ?TEST(send_headers_tst), + ?TEST(handle_split_frame_tst), + ?TEST(noop_tst), + ?TEST(receive_protocol_error_for_invalid_syn_tst), + ?TEST(receive_protocol_error_for_invalid_syn_when_zero_tst), + ?TEST(receive_protocol_error_for_invalid_syn_when_going_backwards_tst), + ?TEST(graceful_close_closes_server_when_there_are_no_connections_tst), + ?TEST(graceful_close_closes_server_when_last_connection_is_closed_tst), + ?TEST(graceful_close_close_twice_tst), + ?TEST(unknown_cast_tst), + ?TEST(unknown_call_tst), + ?TEST(statistics_zombie_tst), + ?TEST(zero_length_name_receives_rst_tst), + ?TEST(zero_length_value_receives_rst_tst), + ?TEST(zero_length_multi_value_receives_rst_tst), + ?TEST(zero_length_name_receives_rst_when_receiving_headers_tst), + ?TEST(receive_rst_after_sending_on_closed_channel_tst), + ?TEST(refuse_new_connections_after_receiving_goaway_tst), + ?TEST(refuse_new_connects_after_sending_goaway_tst), + ?TEST(refuse_new_accepts_after_sending_goaway_tst), + ?TEST(connect_and_recv_tst), + ?TEST(connect_for_push_and_get_error_closed_when_recving_tst), + ?TEST(connect_and_send_syn_fin_tst) + + ] + } + . + +setup() -> + application:start(lager), + lager:set_loglevel(lager_console_backend, error). + +tear_down(_) -> + check_for_messages(). + +check_for_messages() -> + receive + Msg -> + lager:info("Received message ~p", [Msg]), + ?assert(false) + after 100 -> + ok + end. + +start_socket(Role) -> + Socket = espdy_mock_socket:new(self()), + {ok, Pid} = espdy_server:start_link(Socket, Role), + Pid. + +send_syn_stream(ZLib, Pid, StreamId, Headers) -> + Frame = espdy_frame:encode_syn_stream(ZLib, 0, StreamId, 0, 0, Headers), + send_frame(Pid, Frame). + +send_syn_stream(ZLib, Pid, StreamId) -> + send_syn_stream(ZLib, Pid, StreamId, []). + +send_syn_stream(Pid, StreamId) -> + Frame = espdy_frame:encode_syn_stream(0, StreamId, 0, 0, []), + send_frame(Pid, Frame). + +start_channel(ZLib, Pid, N) -> + send_syn_stream(ZLib, Pid, N), + {ok, Socket, _Headers} = espdy_server:accept(Pid), + Socket. + +start_channel(Pid, N) -> + send_syn_stream(Pid, N), + {ok, Socket, _Headers} = espdy_server:accept(Pid), + Socket. + +start_channel_one(Pid) -> + start_channel(Pid, 1). + +start_channel_one() -> + start_channel_one(start_socket(server)). + + +receive_packet() -> + receive + {packet, PacketPid, Packet} -> + PacketPid ! {mock_packet_result, ok}, + timer:sleep(50) % DODGY HACK + end, + Packet. + +receive_data_frame() -> + receive + {packet, PacketPid, Packet} -> PacketPid ! {mock_packet_result, ok} + end, + Frame = espdy_frame:decode_exactly_one_frame(Packet), + ?assertMatch(#data_frame{}, Frame), + Frame. + +receive_control_frame() -> + receive + {packet, PacketPid, Packet} -> PacketPid ! {mock_packet_result, ok} + end, + Frame = espdy_frame:decode_exactly_one_frame(Packet), + ?assertMatch(#control_frame{}, Frame), + Frame. + + + +send_frame(#espdy_socket{pid = Pid}, Packet) -> + Pid ! {tcp, undefined, Packet}; + +send_frame(Pid, Packet) -> + Pid ! {tcp, undefined, Packet}. + +assert_dont_receive_packet() -> + receive + {packet, _, _} -> ?assert(false) + after 10 -> + ok + end. + +receive_response(Ref) -> + receive + {Ref, Resp} -> ok + end, + Resp. + + + +statistics_zombie_tst() -> + Pid = start_socket(server), + Socket = start_channel_one(Pid), + + send_frame(Socket, espdy_frame:encode_data_frame(1, ?FLAG_FIN, <<>>)), + + Ref1 = espdy_server:async_send_and_close(Socket, <<"hello">>), + ?assertMatch(#control_frame{type = ?SYN_REPLY}, receive_control_frame()), + + % ZOOOMBIIIESSS. this probably shouldn't be a zombie. but the idea is we need to keep + % streams around until they have written all their crap out. + ?assertEqual([{read_open, 0}, {write_open, 0}, {both_open, 0}, {zombies, 1}], espdy_server:statistics(Pid)), + ?assertMatch(#data_frame{flags = ?FLAG_FIN}, receive_data_frame()), + ok = receive_response(Ref1). + + +statistics_read_open_tst() -> + Pid = start_socket(server), + Socket = start_channel_one(Pid), + + ?assertEqual([{read_open, 0}, {write_open, 0}, {both_open, 1}, {zombies, 0}], espdy_server:statistics(Pid)), + + Ref1 = espdy_server:async_send_and_close(Socket, <<"hello">>), + ?assertMatch(#control_frame{type = ?SYN_REPLY}, receive_control_frame()), + ?assertMatch(#data_frame{flags = ?FLAG_FIN}, receive_data_frame()), + ok = receive_response(Ref1), + + ?assertEqual([{read_open, 1}, {write_open, 0}, {both_open, 0}, {zombies, 0}], espdy_server:statistics(Pid)). + + +statistics_tst() -> + Pid = start_socket(server), + Socket = start_channel_one(Pid), + + ?assertEqual([{read_open, 0}, {write_open, 0}, {both_open, 1}, {zombies, 0}], espdy_server:statistics(Pid)), + + send_frame(Socket, espdy_frame:encode_data_frame(1, ?FLAG_FIN, <<>>)), + + ?assertEqual([{read_open, 0}, {write_open, 1}, {both_open, 0}, {zombies, 0}], espdy_server:statistics(Pid)), + + Ref1 = espdy_server:async_send_and_close(Socket, <<"hello">>), + + ?assertMatch(#control_frame{type = ?SYN_REPLY}, receive_control_frame()), + ?assertMatch(#data_frame{flags = ?FLAG_FIN}, receive_data_frame()), + + ok = receive_response(Ref1), + + ?assertEqual([{read_open, 0}, {write_open, 0}, {both_open, 0}, {zombies, 0}], espdy_server:statistics(Pid)) + + . + +close_after_send_tst() -> + + Socket = start_channel_one(), + Ref1 = espdy_server:async_send(Socket, <<"hello">>), + + %SYN_REPLY + receive_control_frame(), + + receive_data_frame(), + ok = receive_response(Ref1), + + Ref2 = espdy_server:async_close(Socket), + + DataFrame = receive_data_frame(), + ?assertEqual(?FLAG_FIN, DataFrame#data_frame.flags), + + ok = receive_response(Ref2). + + +syn_reply_tst() -> + Headers = [{<<"foo">>, [<<"bar">>]}], + + Socket = start_channel_one(), + Ref1 = espdy_server:async_send_headers(Socket, Headers), + ControlFrame = receive_control_frame(), + SynReply = espdy_frame:decode_control_frame(ControlFrame), + ?assertEqual(1, SynReply#syn_reply.stream_id), + ?assertEqual(Headers, SynReply#syn_reply.headers), + ?assertEqual(0, SynReply#syn_reply.flags), + + ok = receive_response(Ref1). + +send_tst() -> + + Socket = start_channel_one(), + Ref1 = espdy_server:async_send(Socket, <<"helloworld">>), + Ref2 = espdy_server:async_send(Socket, <<"lollercopter">>), + + ControlFrame = receive_control_frame(), + SynReply = espdy_frame:decode_control_frame(ControlFrame), + ?assertEqual(#syn_reply{stream_id = 1, headers = [], flags = 0}, SynReply), + + DataFrame = receive_data_frame(), + ?assertEqual(1, DataFrame#data_frame.stream_id), + ?assertEqual(<<"helloworld">>, DataFrame#data_frame.data), + + ?assertEqual(#data_frame{stream_id = 1, data = <<"lollercopter">>, flags = 0}, receive_data_frame()), + + ok = receive_response(Ref1), + ok = receive_response(Ref2). + +rst_while_waiting_for_read_tst() -> + + Socket = start_channel_one(), + DataFrame = espdy_frame:encode_data_frame(1, 0, <<"1234">>), + RstFrame = espdy_frame:encode_rst_stream(1, ?PROTOCOL_ERROR), + + send_frame(Socket, DataFrame), + + Ref1 = espdy_server:async_recv(Socket, 6), + + send_frame(Socket, RstFrame), + + {error, closed} = receive_response(Ref1). + +rst_read_tst() -> + + Socket = start_channel_one(), + + DataFrame = espdy_frame:encode_data_frame(1, 0, <<"12345">>), + + send_frame(Socket, DataFrame), + RstFrame = espdy_frame:encode_rst_stream(1, ?PROTOCOL_ERROR), + + send_frame(Socket, RstFrame), + + ?assertEqual({error, closed}, espdy_server:recv(Socket, 4)). + + +flag_fin_read_ok_tst() -> + + Socket = start_channel_one(), + DataFrame = espdy_frame:encode_data_frame(1, ?FLAG_FIN, <<"1234">>), + send_frame(Socket, DataFrame), + + ?assertEqual(<<"1234">>, espdy_server:recv(Socket, 4)). + +flag_fin_tst() -> + + Socket = start_channel_one(), + DataFrame = espdy_frame:encode_data_frame(1, ?FLAG_FIN, <<"1234">>), + send_frame(Socket, DataFrame), + + ?assertEqual({error, closed}, espdy_server:recv(Socket, 6)). + +flag_fin_during_tst() -> + + Socket = start_channel_one(), + DataFrame = espdy_frame:encode_data_frame(1, 0, <<"1234">>), + send_frame(Socket, DataFrame), + Ref1 = espdy_server:async_recv(Socket, 6), + + DataFrame2 = espdy_frame:encode_data_frame(1, ?FLAG_FIN, <<"5">>), + send_frame(Socket, DataFrame2), + ?assertEqual({error, closed}, receive_response(Ref1)). + +syn_stream_tst() -> + + Socket = start_channel_one(), + DataFrame = espdy_frame:encode_data_frame(1, 0, <<"helloxworld">>), + send_frame(Socket, DataFrame), + + Binary = espdy_server:recv(Socket, 6), + ?assertEqual(<<"hellox">>, Binary), + Binary2 = espdy_server:recv(Socket, 5), + ?assertEqual(<<"world">>, Binary2). + +syn_stream_buffer_tst() -> + + Socket = start_channel_one(), + + DataFrame = espdy_frame:encode_data_frame(1, 0, <<"1234">>), + DataFrame2 = espdy_frame:encode_data_frame(1, 0, <<"5678">>), + send_frame(Socket, DataFrame), + send_frame(Socket, DataFrame2), + + Binary = espdy_server:recv(Socket, 6), + ?assertEqual(<<"123456">>, Binary), + Binary2 = espdy_server:recv(Socket, 2), + ?assertEqual(<<"78">>, Binary2). + +syn_stream_blocking_tst() -> + + Socket = start_channel_one(), + + Ref1 = espdy_server:async_recv(Socket, 6), + Ref2 = espdy_server:async_recv(Socket, 5), + + DataFrame = espdy_frame:encode_data_frame(1, 0, <<"helloxworld">>), + + send_frame(Socket, DataFrame), + + ?assertEqual(<<"hellox">>, receive_response(Ref1)), + ?assertEqual(<<"world">>, receive_response(Ref2)). + + +receive_accept(Pid, Ref) -> + {ok, StreamId, Headers} = receive_response(Ref), + {ok, #espdy_socket{pid = Pid, stream_id = StreamId}, Headers}. + +handle_accept_queue_tst() -> + Socket = start_socket(server), + + Ref1 = espdy_server:async_accept(Socket), + Ref2 = espdy_server:async_accept(Socket), + + ZLib = espdy_frame:initialize_zlib_for_deflate(), + send_syn_stream(ZLib, Socket, 1), + send_syn_stream(ZLib, Socket, 3), + zlib:close(ZLib), + + {ok, Socket1, []} = receive_accept(Socket, Ref1), + ?assertEqual(1, espdy_server:stream_id(Socket1)), + {ok, Socket2, []} = receive_accept(Socket, Ref2), + ?assertEqual(3, espdy_server:stream_id(Socket2)). + +receive_rst_stream(StreamId, Status) -> + ControlFrame = receive_control_frame(), + ?assertMatch(#control_frame{type = ?RST_STREAM}, ControlFrame), + ?assertMatch(#rst_stream{stream_id = StreamId, status = Status}, espdy_frame:decode_control_frame(ControlFrame)). + +handle_receive_write_after_close_from_other_side_tst() -> + Socket = start_channel_one(), + send_frame(Socket, espdy_frame:encode_data_frame(1, ?FLAG_FIN, <<>>)), + send_frame(Socket, espdy_frame:encode_data_frame(1, ?FLAG_FIN, <<>>)), + + receive_rst_stream(1, ?INVALID_STREAM), + + {error, closed} = espdy_server:send(Socket, <<"hello">>). + +handle_non_existant_data_frame_tst() -> + Socket = start_channel_one(), + send_frame(Socket, espdy_frame:encode_data_frame(2, ?FLAG_FIN, <<>>)), + receive_rst_stream(2, ?INVALID_STREAM). + + +handle_write_finished_for_stream_that_no_longer_exists_tst() -> + % not sure if this is actually possible.. but we have tests for it... + Socket = start_socket(server), + + Socket ! {write_finished, {ok, 1, false, true}}. + +handle_header_finished_for_stream_that_no_longer_exists_tst() -> + % not sure if this is actually possible.. but we have tests for it... + Socket = start_socket(server), + + Socket ! {header_finished, {ok, 1}}. + +tcp_close(#espdy_socket{pid = Pid}) -> + Pid ! {tcp_closed, undefined}. + +receive_closed_when_the_underlying_connection_is_closed_beneath_us_tst() -> + Socket = start_channel_one(), + Ref1 = espdy_server:async_recv(Socket, 5), + tcp_close(Socket), + {error, closed} = receive_response(Ref1). + +receive_syn_reply(ZLib) -> + ControlFrame = receive_control_frame(), + SynReply = espdy_frame:decode_control_frame(ZLib, ControlFrame), + ?assertMatch(#syn_reply{}, SynReply), + SynReply. + +receive_headers(ZLib) -> + ControlFrame = receive_control_frame(), + Headers = espdy_frame:decode_control_frame(ZLib, ControlFrame), + ?assertMatch(#headers{}, Headers), + Headers. + +send_headers_tst() -> + Socket = start_channel_one(), + FirstHeaders = [{<<"foo">>, [<<"bar">>]}], + Ref1 = espdy_server:async_send_headers(Socket, FirstHeaders), + ZLib = espdy_frame:initialize_zlib_for_inflate(), + ?assertMatch(#syn_reply{headers = FirstHeaders}, receive_syn_reply(ZLib)), + + SecondHeaders = [{<<"bar">>, [<<"foo">>]}], + Ref2 = espdy_server:async_send_headers(Socket, SecondHeaders), + ?assertMatch(#headers{headers = SecondHeaders}, receive_headers(ZLib)), + + zlib:close(ZLib), + + ok = receive_response(Ref1), + ok = receive_response(Ref2). + +handle_split_frame_tst() -> + Socket = start_socket(server), + Frame = espdy_frame:encode_syn_stream(0, 1, 0, 0, []), + <> = Frame, + send_frame(Socket, Part1), + send_frame(Socket, Part2), + {ok, _StreamSock, []} = espdy_server:accept(Socket). + +noop_tst() -> + Socket = start_socket(server), + Noop = espdy_frame:encode_noop(), + send_frame(Socket, Noop). + +receive_protocol_error_for_invalid_syn_tst() -> + Socket = start_socket(server), + ZLib = espdy_frame:initialize_zlib_for_deflate(), + Headers = [{<<"hello">>, [<<"world">>]}], + + send_syn_stream(ZLib, Socket, 2, Headers), + + + + ControlFrame = receive_control_frame(), + RstStream = espdy_frame:decode_control_frame(ControlFrame), + ?assertMatch(#rst_stream{stream_id = 2, status = ?PROTOCOL_ERROR}, RstStream), + + send_syn_stream(ZLib, Socket, 3, Headers), + + {ok, Socket1, Headers} = espdy_server:accept(Socket). + +receive_protocol_error_for_invalid_syn_when_zero_tst() -> + Socket = start_socket(client), + Frame = espdy_frame:encode_syn_stream(0, 0, 0, 0, []), + send_frame(Socket, Frame), + + ControlFrame = receive_control_frame(), + RstStream = espdy_frame:decode_control_frame(ControlFrame), + ?assertMatch(#rst_stream{stream_id = 0, status = ?PROTOCOL_ERROR}, RstStream). + + +receive_protocol_error_for_invalid_syn_when_going_backwards_tst() -> + Socket = start_socket(server), + ZLib = espdy_frame:initialize_zlib_for_deflate(), + Socket3 = start_channel(ZLib, Socket, 3), + send_syn_stream(ZLib, Socket, 1, []), + + ControlFrame = receive_control_frame(), + RstStream = espdy_frame:decode_control_frame(ControlFrame), + ?assertMatch(#rst_stream{stream_id = 1, status = ?PROTOCOL_ERROR}, RstStream). + +zero_length_name_receives_rst_tst() -> + Socket = start_socket(server), + Frame = espdy_frame:encode_syn_stream_with_raw_uncompressed_name_value_pairs(0, 1, 0, 0, <>), + send_frame(Socket, Frame), + RstStream = espdy_frame:decode_control_frame(receive_control_frame()), + ?assertMatch(#rst_stream{stream_id = 1, status = ?PROTOCOL_ERROR}, RstStream). + +zero_length_value_receives_rst_tst() -> + Socket = start_socket(server), + Frame = espdy_frame:encode_syn_stream_with_raw_uncompressed_name_value_pairs(0, 1, 0, 0, <>), + send_frame(Socket, Frame), + RstStream = espdy_frame:decode_control_frame(receive_control_frame()), + ?assertMatch(#rst_stream{stream_id = 1, status = ?PROTOCOL_ERROR}, RstStream). + +zero_length_multi_value_receives_rst_tst() -> + Socket = start_socket(server), + Frame = espdy_frame:encode_syn_stream_with_raw_uncompressed_name_value_pairs(0, 1, 0, 0, <>), + send_frame(Socket, Frame), + RstStream = espdy_frame:decode_control_frame(receive_control_frame()), + ?assertMatch(#rst_stream{stream_id = 1, status = ?PROTOCOL_ERROR}, RstStream). + + +zero_length_name_receives_rst_when_receiving_headers_tst() -> + ZLib = espdy_frame:initialize_zlib_for_deflate(), + Socket = start_socket(server), + send_syn_stream(ZLib, Socket, 1), + + Frame = espdy_frame:encode_headers_with_raw_uncompressed_name_value_pairs(ZLib, 1, 0, <>), + send_frame(Socket, Frame), + RstStream = espdy_frame:decode_control_frame(receive_control_frame()), + ?assertMatch(#rst_stream{stream_id = 1, status = ?PROTOCOL_ERROR}, RstStream). + +% receive_headers_tst() -> +% ZLib = espdy_frame:initialize_zlib_for_deflate(), +% Socket = start_socket(server), +% Headers = [{<<"name">>, [<<"value">>]}], +% send_syn_stream(ZLib, Socket, 1), +% {ok, Socket1, _} = espdy_server:accept(Socket), +% send_frame(Socket, espdy_frame:encode_headers(ZLib, 1, 0, Headers)), +% +% ?assertMatch({header, Headers}, espdy_server:recv_header_or_data(Socket1)). + +graceful_close_closes_server_when_there_are_no_connections_tst() -> + Socket = start_socket(server), + espdy_server:graceful_close(Socket), + ControlFrame = receive_control_frame(), + GoAway = espdy_frame:decode_control_frame(ControlFrame), + ?assertMatch(#goaway{last_good_stream_id = 0}, GoAway), + ?assertEqual({error, closed}, espdy_server:statistics(Socket)). + +assert_cant_start_channel_with_refused(ZLib, Socket, N) -> + send_syn_stream(ZLib, Socket, 3), + ControlFrame = receive_control_frame(), + RstStream = espdy_frame:decode_control_frame(ControlFrame), + ?assertMatch(#rst_stream{stream_id = N, status = ?REFUSED_STREAM}, RstStream). + +graceful_close_close_twice_tst() -> + Socket = start_socket(server), + Socket1 = start_channel(Socket, 1), + espdy_server:graceful_close(Socket), + espdy_server:graceful_close(Socket), + ControlFrame = receive_control_frame(), + GoAway = espdy_frame:decode_control_frame(ControlFrame), + ?assertMatch(#goaway{last_good_stream_id = 1}, GoAway). + +graceful_close_closes_server_when_last_connection_is_closed_tst() -> + ZLib = espdy_frame:initialize_zlib_for_deflate(), + Msg1 = <<"omgdoesitwork?">>, + Socket = start_socket(server), + Socket1 = start_channel(ZLib, Socket, 1), + espdy_server:graceful_close(Socket), + ControlFrame = receive_control_frame(), + GoAway = espdy_frame:decode_control_frame(ControlFrame), + ?assertMatch(#goaway{last_good_stream_id = 1}, GoAway), + + assert_cant_start_channel_with_refused(ZLib, Socket, 3), + + send_frame(Socket1, espdy_frame:encode_data_frame(1, ?FLAG_FIN, Msg1)), + ?assertEqual(Msg1, espdy_server:recv(Socket1, byte_size(Msg1))), + + Ref1 = espdy_server:async_close(Socket1), + receive_packet(), + ok = receive_response(Ref1), + + ?assertEqual({error, closed}, espdy_server:statistics(Socket)). + +receive_rst_after_sending_on_closed_channel_tst() -> + ZLib = espdy_frame:initialize_zlib_for_deflate(), + Socket = start_socket(server), + Headers = [{<<"name">>, [<<"value">>]}], + send_syn_stream(ZLib, Socket, 1), + + {ok, Socket1, _} = espdy_server:accept(Socket), + + send_frame(Socket, espdy_frame:encode_data_frame(1, ?FLAG_FIN, <<>>)), + send_frame(Socket, espdy_frame:encode_headers(ZLib, 1, 0, Headers)), + + ?assertMatch(#rst_stream{stream_id = 1, status = ?INVALID_STREAM}, espdy_frame:decode_control_frame(receive_control_frame())), + + send_syn_stream(ZLib, Socket, 3, Headers), + + {ok, Socket2, Headers} = espdy_server:accept(Socket) + + + % this can happen naturally not just from broken clients because there is a race + % between cancelling/refusing a stream and the other party being aware of this + % + % ie: refuse a stream and then a headers frame pops down. + % we always need to make sure we decode headers with zlib no matter what or the zlib + % dictionaries will get out of sync which will break ALL subsequent SYN or HEADER + % frames. + + . + + +refuse_new_connects_after_sending_goaway_tst() -> + Socket = start_socket(server), + Channel = start_channel_one(Socket), + espdy_server:graceful_close(Socket), + + GoAway = receive_control_frame(), + + + ?assertMatch({error, closed_not_processed}, espdy_server:connect(Socket)). + + +refuse_new_accepts_after_sending_goaway_tst() -> + Socket = start_socket(server), + Channel = start_channel_one(Socket), + espdy_server:graceful_close(Socket), + + GoAway = receive_control_frame(), + + ?assertMatch({error, closed}, espdy_server:accept(Socket)). + +refuse_new_connections_after_receiving_goaway_tst() -> + Socket = start_socket(server), + Channel = start_channel_one(Socket), + send_frame(Socket, espdy_frame:encode_goaway(1)), + + + ?assertMatch({error, closed_not_processed}, espdy_server:connect(Socket)). + +connect_and_recv_tst() -> + Socket = start_socket(server), + {ok, Channel} = espdy_server:connect(Socket), + SynStream = espdy_frame:decode_control_frame(receive_control_frame()), + ?assertMatch(#syn_stream{associated_stream_id = 0, stream_id = 2, flags = 0, headers = []}, SynStream), + send_frame(Socket, espdy_frame:encode_data_frame(Channel#espdy_socket.stream_id, 0, <<"hello">>)), + ?assertEqual(<<"hello">>, espdy_server:recv(Channel, 5)). + +connect_and_send_syn_fin_tst() -> + Socket = start_socket(server), + Headers = [{<<"hello">>, [<<"world">>]}], + {ok, Channel} = espdy_server:connect(Socket, true, Headers), + SynStream = espdy_frame:decode_control_frame(receive_control_frame()), + ?assertMatch(#syn_stream{associated_stream_id = 0, stream_id = 2, flags = ?FLAG_FIN, headers = Headers}, SynStream), + ?assertEqual({error, closed}, espdy_server:send(Channel, <<"more_data">>)). + +connect_for_push_and_get_error_closed_when_recving_tst() -> + Socket = start_socket(server), + ChannelOne = start_channel_one(Socket), + Headers = [{<<"hello">>, [<<"world">>]}], + + {ok, Channel} = espdy_server:connect_for_push(ChannelOne, false, Headers), + SynStream = espdy_frame:decode_control_frame(receive_control_frame()), + ?assertMatch(#syn_stream{associated_stream_id = 1, stream_id = 2, flags = ?FLAG_UNIDIRECTIONAL, headers = Headers}, SynStream), + ?assertEqual({error, closed}, espdy_server:recv(Channel, <<"more_data">>)). + +run_ping(Role, ID) -> + Socket = start_socket(Role), + Frame = espdy_frame:encode_ping(ID), + send_frame(Socket, Frame), + + ?assertEqual(Frame, receive_packet()). + +run_receive_ping(Role, ID) -> + Socket = start_socket(Role), + Frame = espdy_frame:encode_ping(ID), + send_frame(Socket, Frame), + + assert_dont_receive_packet(). + + +server_ping_tst() -> + + run_ping(server, 1). + +client_ping_tst() -> + + run_ping(client, 2). + +server_receive_ping_reply_tst() -> + + run_receive_ping(server, 2). + +client_receive_ping_reply_tst() -> + + run_receive_ping(client, 1). + +unknown_cast_tst() -> + gen_server:cast(start_socket(server), lols). + +unknown_call_tst() -> + ?assertEqual({error, unknown_call}, gen_server:call(start_socket(server), lols, infinity)). + +code_change_test() -> + ?assertEqual({ok, state}, espdy_server:code_change(old_vsn, state, extra)). + \ No newline at end of file diff --git a/test_server b/test_server new file mode 100755 index 0000000..f6cd1ca --- /dev/null +++ b/test_server @@ -0,0 +1 @@ +erl -pa ebin -pa deps/lager/ebin -s espdy_test_server