%% Copyright (c) 2013-2018, Loïc Hoguin %% %% 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.