314 lines
9.1 KiB
Erlang
314 lines
9.1 KiB
Erlang
%% Copyright (c) 2013-2023, Loïc Hoguin <essen@ninenines.eu>
|
|
%%
|
|
%% 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].
|
|
|
|
-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.
|
|
|
|
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
|
|
|
|
build_headers(Zdef, Headers) ->
|
|
Headers1 = merge_headers(lists:sort(Headers), []),
|
|
NbHeaders = length(Headers1),
|
|
Headers2 = [begin
|
|
L1 = iolist_size(Key),
|
|
L2 = iolist_size(Value),
|
|
[<< L1:32 >>, Key, << L2:32 >>, Value]
|
|
end || {Key, Value} <- Headers1],
|
|
zlib:deflate(Zdef, [<< NbHeaders:32 >>, Headers2], full).
|
|
|
|
merge_headers([], Acc) ->
|
|
lists:reverse(Acc);
|
|
merge_headers([{Name, Value1}, {Name, Value2}|Tail], Acc) ->
|
|
merge_headers([{Name, [Value1, 0, Value2]}|Tail], Acc);
|
|
merge_headers([Head|Tail], Acc) ->
|
|
merge_headers(Tail, [Head|Acc]).
|
|
|
|
-ifdef(TEST).
|
|
merge_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 = merge_headers(R, []) end || {R, D} <- Tests].
|
|
-endif.
|
|
|
|
to_flag(false) -> 0;
|
|
to_flag(true) -> 1.
|