%% Copyright (c) 2013-2015, Loïc Hoguin <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_spdy).
%% Zstream.
-export([deflate_init/0]).
-export([inflate_init/0]).
%% Parse.
-export([split/1]).
-export([parse/2]).
%% Build.
-export([data/3]).
-export([syn_stream/12]).
-export([syn_reply/6]).
-export([rst_stream/2]).
-export([settings/2]).
-export([ping/1]).
-export([goaway/2]).
%% @todo headers
%% @todo window_update
-include("cow_spdy.hrl").
%% Zstream.
deflate_init() ->
Zdef = zlib:open(),
ok = zlib:deflateInit(Zdef),
_ = zlib:deflateSetDictionary(Zdef, ?ZDICT),
Zdef.
inflate_init() ->
Zinf = zlib:open(),
ok = zlib:inflateInit(Zinf),
Zinf.
%% Parse.
split(Data = << _:40, Length:24, _/bits >>)
when byte_size(Data) >= Length + 8 ->
Length2 = Length + 8,
<< Frame:Length2/binary, Rest/bits >> = Data,
{true, Frame, Rest};
split(_) ->
false.
parse(<< 0:1, StreamID:31, 0:7, IsFinFlag:1, _:24, Data/bits >>, _) ->
{data, StreamID, from_flag(IsFinFlag), Data};
parse(<< 1:1, 3:15, 1:16, 0:6, IsUnidirectionalFlag:1, IsFinFlag:1,
_:25, StreamID:31, _:1, AssocToStreamID:31, Priority:3, _:5,
0:8, Rest/bits >>, Zinf) ->
case parse_headers(Rest, Zinf) of
{ok, Headers, [{<<":host">>, Host}, {<<":method">>, Method},
{<<":path">>, Path}, {<<":scheme">>, Scheme},
{<<":version">>, Version}]} ->
{syn_stream, StreamID, AssocToStreamID, from_flag(IsFinFlag),
from_flag(IsUnidirectionalFlag), Priority, Method,
Scheme, Host, Path, Version, Headers};
_ ->
{error, badprotocol}
end;
parse(<< 1:1, 3:15, 2:16, 0:7, IsFinFlag:1, _:25,
StreamID:31, Rest/bits >>, Zinf) ->
case parse_headers(Rest, Zinf) of
{ok, Headers, [{<<":status">>, Status}, {<<":version">>, Version}]} ->
{syn_reply, StreamID, from_flag(IsFinFlag),
Status, Version, Headers};
_ ->
{error, badprotocol}
end;
parse(<< 1:1, 3:15, 3:16, 0:8, _:56, StatusCode:32 >>, _)
when StatusCode =:= 0; StatusCode > 11 ->
{error, badprotocol};
parse(<< 1:1, 3:15, 3:16, 0:8, _:25, StreamID:31, StatusCode:32 >>, _) ->
Status = case StatusCode of
1 -> protocol_error;
2 -> invalid_stream;
3 -> refused_stream;
4 -> unsupported_version;
5 -> cancel;
6 -> internal_error;
7 -> flow_control_error;
8 -> stream_in_use;
9 -> stream_already_closed;
10 -> invalid_credentials;
11 -> frame_too_large
end,
{rst_stream, StreamID, Status};
parse(<< 1:1, 3:15, 4:16, 0:7, ClearSettingsFlag:1, _:24,
NbEntries:32, Rest/bits >>, _) ->
try
Settings = [begin
Is0 = 0,
Key = case ID of
1 -> upload_bandwidth;
2 -> download_bandwidth;
3 -> round_trip_time;
4 -> max_concurrent_streams;
5 -> current_cwnd;
6 -> download_retrans_rate;
7 -> initial_window_size;
8 -> client_certificate_vector_size
end,
{Key, Value, from_flag(PersistFlag), from_flag(WasPersistedFlag)}
end || << Is0:6, WasPersistedFlag:1, PersistFlag:1,
ID:24, Value:32 >> <= Rest],
NbEntries = length(Settings),
{settings, from_flag(ClearSettingsFlag), Settings}
catch _:_ ->
{error, badprotocol}
end;
parse(<< 1:1, 3:15, 6:16, 0:8, _:24, PingID:32 >>, _) ->
{ping, PingID};
parse(<< 1:1, 3:15, 7:16, 0:8, _:56, StatusCode:32 >>, _)
when StatusCode > 2 ->
{error, badprotocol};
parse(<< 1:1, 3:15, 7:16, 0:8, _:25, LastGoodStreamID:31,
StatusCode:32 >>, _) ->
Status = case StatusCode of
0 -> ok;
1 -> protocol_error;
2 -> internal_error
end,
{goaway, LastGoodStreamID, Status};
parse(<< 1:1, 3:15, 8:16, 0:7, IsFinFlag:1, _:25, StreamID:31,
Rest/bits >>, Zinf) ->
case parse_headers(Rest, Zinf) of
{ok, Headers, []} ->
{headers, StreamID, from_flag(IsFinFlag), Headers};
_ ->
{error, badprotocol}
end;
parse(<< 1:1, 3:15, 9:16, 0:8, _:57, 0:31 >>, _) ->
{error, badprotocol};
parse(<< 1:1, 3:15, 9:16, 0:8, _:25, StreamID:31,
_:1, DeltaWindowSize:31 >>, _) ->
{window_update, StreamID, DeltaWindowSize};
parse(_, _) ->
{error, badprotocol}.
parse_headers(Data, Zinf) ->
[<< NbHeaders:32, Rest/bits >>] = inflate(Zinf, Data),
parse_headers(Rest, NbHeaders, [], []).
parse_headers(<<>>, 0, Headers, SpHeaders) ->
{ok, lists:reverse(Headers), lists:sort(SpHeaders)};
parse_headers(<<>>, _, _, _) ->
error;
parse_headers(_, 0, _, _) ->
error;
parse_headers(<< 0:32, _/bits >>, _, _, _) ->
error;
parse_headers(<< L1:32, Key:L1/binary, L2:32, Value:L2/binary, Rest/bits >>,
NbHeaders, Acc, SpAcc) ->
case Key of
<< $:, _/bits >> ->
parse_headers(Rest, NbHeaders - 1, Acc,
lists:keystore(Key, 1, SpAcc, {Key, Value}));
_ ->
parse_headers(Rest, NbHeaders - 1, [{Key, Value}|Acc], SpAcc)
end.
inflate(Zinf, Data) ->
try
zlib:inflate(Zinf, Data)
catch _:_ ->
ok = zlib:inflateSetDictionary(Zinf, ?ZDICT),
zlib:inflate(Zinf, <<>>)
end.
from_flag(0) -> false;
from_flag(1) -> true.
%% Build.
data(StreamID, IsFin, Data) ->
IsFinFlag = to_flag(IsFin),
Length = iolist_size(Data),
[<< 0:1, StreamID:31, 0:7, IsFinFlag:1, Length:24 >>, Data].
syn_stream(Zdef, StreamID, AssocToStreamID, IsFin, IsUnidirectional,
Priority, Method, Scheme, Host, Path, Version, Headers) ->
IsFinFlag = to_flag(IsFin),
IsUnidirectionalFlag = to_flag(IsUnidirectional),
HeaderBlock = build_headers(Zdef, [
{<<":method">>, Method},
{<<":scheme">>, Scheme},
{<<":host">>, Host},
{<<":path">>, Path},
{<<":version">>, Version}
|Headers]),
Length = 10 + iolist_size(HeaderBlock),
[<< 1:1, 3:15, 1:16, 0:6, IsUnidirectionalFlag:1, IsFinFlag:1,
Length:24, 0:1, StreamID:31, 0:1, AssocToStreamID:31,
Priority:3, 0:5, 0:8 >>, HeaderBlock].
syn_reply(Zdef, StreamID, IsFin, Status, Version, Headers) ->
IsFinFlag = to_flag(IsFin),
HeaderBlock = build_headers(Zdef, [
{<<":status">>, Status},
{<<":version">>, Version}
|Headers]),
Length = 4 + iolist_size(HeaderBlock),
[<< 1:1, 3:15, 2:16, 0:7, IsFinFlag:1, Length:24,
0:1, StreamID:31 >>, HeaderBlock].
rst_stream(StreamID, Status) ->
StatusCode = case Status of
protocol_error -> 1;
invalid_stream -> 2;
refused_stream -> 3;
unsupported_version -> 4;
cancel -> 5;
internal_error -> 6;
flow_control_error -> 7;
stream_in_use -> 8;
stream_already_closed -> 9;
invalid_credentials -> 10;
frame_too_large -> 11
end,
<< 1:1, 3:15, 3:16, 0:8, 8:24,
0:1, StreamID:31, StatusCode:32 >>.
settings(ClearSettingsFlag, Settings) ->
IsClearSettingsFlag = to_flag(ClearSettingsFlag),
NbEntries = length(Settings),
Entries = [begin
IsWasPersistedFlag = to_flag(WasPersistedFlag),
IsPersistFlag = to_flag(PersistFlag),
ID = case Key of
upload_bandwidth -> 1;
download_bandwidth -> 2;
round_trip_time -> 3;
max_concurrent_streams -> 4;
current_cwnd -> 5;
download_retrans_rate -> 6;
initial_window_size -> 7;
client_certificate_vector_size -> 8
end,
<< 0:6, IsWasPersistedFlag:1, IsPersistFlag:1, ID:24, Value:32 >>
end || {Key, Value, WasPersistedFlag, PersistFlag} <- Settings],
Length = 4 + iolist_size(Entries),
[<< 1:1, 3:15, 4:16, 0:7, IsClearSettingsFlag:1, Length:24,
NbEntries:32 >>, Entries].
ping(PingID) ->
<< 1:1, 3:15, 6:16, 0:8, 4:24, PingID:32 >>.
goaway(LastGoodStreamID, Status) ->
StatusCode = case Status of
ok -> 0;
protocol_error -> 1;
internal_error -> 2
end,
<< 1:1, 3:15, 7:16, 0:8, 8:24,
0:1, LastGoodStreamID:31, StatusCode:32 >>.
%% @todo headers
%% @todo window_update
-ifdef(TEST).
settings_frame_test() ->
ClearSettingsFlag = false,
Settings = [{max_concurrent_streams,1000,false,false},
{initial_window_size,10485760,false,false}],
Bin = list_to_binary(cow_spdy:settings(ClearSettingsFlag, Settings)),
P = cow_spdy:parse(Bin, undefined),
P = {settings, ClearSettingsFlag, Settings},
ok.
-endif.
build_headers(Zdef, Headers) ->
DedupedHeaders = dedupe_headers(Headers, []),
NbHeaders = length(DedupedHeaders),
Headers2 = [begin
L1 = iolist_size(Key),
L2 = iolist_size(Value),
[<< L1:32 >>, Key, << L2:32 >>, Value]
end || {Key, Value} <- DedupedHeaders],
zlib:deflate(Zdef, [<< NbHeaders:32 >>, Headers2], full).
dedupe_headers([], Acc) ->
lists:reverse(Acc);
dedupe_headers([{Key, Value}|Headers], Acc) ->
Acc2 = append_header_value(Key, Value, Acc, []),
dedupe_headers(Headers, Acc2).
append_header_value(Key, Value, [], Acc) ->
[{Key, Value}|Acc];
append_header_value(Key, Value, [{Key, PrevValue}|Rest], Acc) ->
[{Key, [PrevValue, 0, Value]}|Rest] ++ Acc;
append_header_value(Key, Value, [Header|Headers], Acc) ->
append_header_value(Key, Value, Headers, [Header|Acc]).
-ifdef(TEST).
dedupe_headers_test_() ->
Tests = [
{[{<<"set-cookie">>, <<"session=123">>}, {<<"set-cookie">>, <<"other=456">>}, {<<"content-type">>, <<"text/html">>}],
[{<<"set-cookie">>, [<<"session=123">>, 0, <<"other=456">>]}, {<<"content-type">>, <<"text/html">>}]}
],
[fun() -> D = dedupe_headers(R, []) end || {R, D} <- Tests].
-endif.
to_flag(false) -> 0;
to_flag(true) -> 1.