Current section

Files

Jump to
gramps src gramps.erl
Raw

src/gramps.erl

-module(gramps).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-export([frame_from_message/1, frame_to_bytes_builder/2, to_text_frame/2, to_binary_frame/2, get_messages/2, aggregate_frames/3, read_response/1, read_request/1, parse_websocket_key/1]).
-export_type([data_frame/0, control_frame/0, frame/0, frame_parse_error/0, parsed_frame/0, sha_hash/0, packet_type/0, decode_packet_error/0, uri_packet/0, decoded_packet/0]).
-type data_frame() :: {text_frame, integer(), bitstring()} |
{binary_frame, integer(), bitstring()}.
-type control_frame() :: {close_frame, integer(), bitstring()} |
{ping_frame, integer(), bitstring()} |
{pong_frame, integer(), bitstring()}.
-type frame() :: {data, data_frame()} |
{control, control_frame()} |
{continuation, integer(), bitstring()}.
-type frame_parse_error() :: {need_more_data, bitstring()} | invalid_frame.
-type parsed_frame() :: {complete, frame()} | {incomplete, frame()}.
-type sha_hash() :: sha.
-type packet_type() :: httph_bin | http_bin.
-type decode_packet_error() :: {more, integer()} | {http_error, binary()}.
-type uri_packet() :: {abs_path, binary()} |
{absolute_uri, gleam@http:scheme(), binary(), integer(), binary()}.
-type decoded_packet() :: {http_request,
binary(),
uri_packet(),
{integer(), integer()}} |
{http_response, {integer(), integer()}, integer(), binary()} |
{http_header, integer(), gleam@dynamic:dynamic_(), binary(), binary()} |
http_eoh.
-spec unmask_data(bitstring(), list(bitstring()), integer(), bitstring()) -> bitstring().
unmask_data(Data, Masks, Index, Resp) ->
case Data of
<<Masked:8/bitstring, Rest/bitstring>> ->
_assert_subject = gleam@list:at(Masks, Index rem 4),
{ok, Mask_value} = case _assert_subject of
{ok, _} -> _assert_subject;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Assertion pattern match failed"/utf8>>,
value => _assert_fail,
module => <<"gramps"/utf8>>,
function => <<"unmask_data"/utf8>>,
line => 41})
end,
Unmasked = crypto:exor(Mask_value, Masked),
unmask_data(
Rest,
Masks,
Index + 1,
<<Resp/bitstring, Unmasked/bitstring>>
);
_ ->
Resp
end.
-spec frame_from_message(bitstring()) -> {ok, {parsed_frame(), bitstring()}} |
{error, frame_parse_error()}.
frame_from_message(Message) ->
case Message of
<<Complete:1,
_:3,
Opcode:4/integer,
Mask:1,
Payload_length:7/integer,
Rest/bitstring>> ->
Payload_size = case Payload_length of
126 ->
16;
127 ->
64;
_ ->
0
end,
Masked = case Mask of
1 ->
true;
0 ->
false;
_ ->
erlang:error(#{gleam_error => panic,
message => <<"Somehow a bit wasn't 0 or 1"/utf8>>,
module => <<"gramps"/utf8>>,
function => <<"frame_from_message"/utf8>>,
line => 79})
end,
case {Masked, Rest} of
{true,
<<Length:Payload_size/integer,
Mask1:1/binary,
Mask2:1/binary,
Mask3:1/binary,
Mask4:1/binary,
Rest@1/bitstring>>} ->
Payload_byte_size = case Length of
0 ->
Payload_length;
N ->
N
end,
case Rest@1 of
<<Payload:Payload_byte_size/binary, Rest@2/bitstring>> ->
Data = unmask_data(
Payload,
[Mask1, Mask2, Mask3, Mask4],
0,
<<>>
),
_pipe = case Opcode of
0 ->
{ok, {continuation, Payload_length, Data}};
1 ->
{ok,
{data,
{text_frame, Payload_length, Data}}};
2 ->
{ok,
{data,
{binary_frame, Payload_length, Data}}};
8 ->
{ok,
{control,
{close_frame, Payload_length, Data}}};
9 ->
{ok,
{control,
{ping_frame, Payload_length, Data}}};
10 ->
{ok,
{control,
{pong_frame, Payload_length, Data}}};
_ ->
{error, invalid_frame}
end,
gleam@result:then(
_pipe,
fun(Frame) -> case Complete of
1 ->
{ok, {{complete, Frame}, Rest@2}};
0 ->
{ok, {{incomplete, Frame}, Rest@2}};
_ ->
{error, invalid_frame}
end end
);
_ ->
{error, {need_more_data, Message}}
end;
{false, <<Length@1:Payload_size/integer, Rest@3/bitstring>>} ->
Payload_byte_size@1 = case Length@1 of
0 ->
Payload_length;
N@1 ->
N@1
end,
case Rest@3 of
<<Payload@1:Payload_byte_size@1/binary,
Rest@4/bitstring>> ->
_pipe@1 = case Opcode of
0 ->
{ok,
{continuation,
Payload_length,
Payload@1}};
1 ->
{ok,
{data,
{text_frame,
Payload_length,
Payload@1}}};
2 ->
{ok,
{data,
{binary_frame,
Payload_length,
Payload@1}}};
8 ->
{ok,
{control,
{close_frame,
Payload_length,
Payload@1}}};
9 ->
{ok,
{control,
{ping_frame,
Payload_length,
Payload@1}}};
10 ->
{ok,
{control,
{pong_frame,
Payload_length,
Payload@1}}};
_ ->
{error, invalid_frame}
end,
gleam@result:then(
_pipe@1,
fun(Frame@1) -> case Complete of
1 ->
{ok, {{complete, Frame@1}, Rest@4}};
0 ->
{ok,
{{incomplete, Frame@1}, Rest@4}};
_ ->
{error, invalid_frame}
end end
);
_ ->
{error, {need_more_data, Message}}
end;
{_, _} ->
{error, invalid_frame}
end;
_ ->
{error, invalid_frame}
end.
-spec make_frame(
integer(),
integer(),
bitstring(),
gleam@option:option(bitstring())
) -> gleam@bytes_builder:bytes_builder().
make_frame(Opcode, Length, Payload, Mask) ->
Length_section = case Length of
Length@1 when Length@1 > 65535 ->
<<127:7, Length@1:64/integer>>;
Length@2 when Length@2 >= 126 ->
<<126:7, Length@2:16/integer>>;
_ ->
<<Length:7>>
end,
Masked = case gleam@option:is_some(Mask) of
true ->
1;
false ->
0
end,
Mask_key = gleam@option:unwrap(Mask, <<>>),
_pipe = <<1:1,
0:3,
Opcode:4,
Masked:1,
Length_section/bitstring,
Mask_key/bitstring,
Payload/bitstring>>,
gleam_stdlib:wrap_list(_pipe).
-spec frame_to_bytes_builder(frame(), gleam@option:option(bitstring())) -> gleam@bytes_builder:bytes_builder().
frame_to_bytes_builder(Frame, Mask) ->
case Frame of
{data, {text_frame, Payload_length, Payload}} ->
make_frame(1, Payload_length, Payload, Mask);
{control, {close_frame, Payload_length@1, Payload@1}} ->
make_frame(8, Payload_length@1, Payload@1, Mask);
{data, {binary_frame, Payload_length@2, Payload@2}} ->
make_frame(2, Payload_length@2, Payload@2, Mask);
{control, {pong_frame, Payload_length@3, Payload@3}} ->
make_frame(10, Payload_length@3, Payload@3, Mask);
{control, {ping_frame, Payload_length@4, Payload@4}} ->
make_frame(9, Payload_length@4, Payload@4, Mask);
{continuation, Length, Payload@5} ->
make_frame(0, Length, Payload@5, Mask)
end.
-spec to_text_frame(binary(), boolean()) -> gleam@bytes_builder:bytes_builder().
to_text_frame(Data, Mask) ->
Msg = gleam_stdlib:identity(Data),
Size = erlang:byte_size(Msg),
{Maybe_masked_data, Mask@2} = case Mask of
true ->
Mask@1 = crypto:strong_rand_bytes(4),
<<Mask1:1/binary, Mask2:1/binary, Mask3:1/binary, Mask4:1/binary>> = case Mask@1 of
<<_:1/binary, _:1/binary, _:1/binary, _:1/binary>> -> Mask@1;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Assertion pattern match failed"/utf8>>,
value => _assert_fail,
module => <<"gramps"/utf8>>,
function => <<"to_text_frame"/utf8>>,
line => 219})
end,
{unmask_data(Msg, [Mask1, Mask2, Mask3, Mask4], 0, <<>>),
{some, Mask@1}};
_ ->
{Msg, none}
end,
frame_to_bytes_builder(
{data, {text_frame, Size, Maybe_masked_data}},
Mask@2
).
-spec to_binary_frame(bitstring(), boolean()) -> gleam@bytes_builder:bytes_builder().
to_binary_frame(Data, Mask) ->
Size = erlang:byte_size(Data),
{Maybe_masked_data, Mask@2} = case Mask of
true ->
Mask@1 = crypto:strong_rand_bytes(4),
<<Mask1:1/binary, Mask2:1/binary, Mask3:1/binary, Mask4:1/binary>> = case Mask@1 of
<<_:1/binary, _:1/binary, _:1/binary, _:1/binary>> -> Mask@1;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Assertion pattern match failed"/utf8>>,
value => _assert_fail,
module => <<"gramps"/utf8>>,
function => <<"to_binary_frame"/utf8>>,
line => 237})
end,
{unmask_data(Data, [Mask1, Mask2, Mask3, Mask4], 0, <<>>),
{some, Mask@1}};
_ ->
{Data, none}
end,
frame_to_bytes_builder(
{data, {binary_frame, Size, Maybe_masked_data}},
Mask@2
).
-spec get_messages(bitstring(), list(parsed_frame())) -> {list(parsed_frame()),
bitstring()}.
get_messages(Data, Frames) ->
case frame_from_message(Data) of
{ok, {Frame, <<>>}} ->
{gleam@list:reverse([Frame | Frames]), <<>>};
{ok, {Frame@1, Rest}} ->
get_messages(Rest, [Frame@1 | Frames]);
{error, {need_more_data, Rest@1}} ->
{Frames, Rest@1};
{error, invalid_frame} ->
{Frames, Data}
end.
-spec append_frame(frame(), integer(), bitstring()) -> frame().
append_frame(Left, Length, Data) ->
case Left of
{data, {text_frame, Len, Payload}} ->
{data,
{text_frame,
Len + Length,
<<Payload/bitstring, Data/bitstring>>}};
{data, {binary_frame, Len@1, Payload@1}} ->
{data,
{binary_frame,
Len@1 + Length,
<<Payload@1/bitstring, Data/bitstring>>}};
{control, {close_frame, Len@2, Payload@2}} ->
{control,
{close_frame,
Len@2 + Length,
<<Payload@2/bitstring, Data/bitstring>>}};
{control, {ping_frame, Len@3, Payload@3}} ->
{control,
{ping_frame,
Len@3 + Length,
<<Payload@3/bitstring, Data/bitstring>>}};
{control, {pong_frame, Len@4, Payload@4}} ->
{control,
{pong_frame,
Len@4 + Length,
<<Payload@4/bitstring, Data/bitstring>>}};
{continuation, _, _} ->
Left
end.
-spec aggregate_frames(
list(parsed_frame()),
gleam@option:option(frame()),
list(frame())
) -> {ok, list(frame())} | {error, nil}.
aggregate_frames(Frames, Previous, Joined) ->
case {Frames, Previous} of
{[], _} ->
{ok, gleam@list:reverse(Joined)};
{[{complete, {continuation, Length, Data}} | Rest], {some, Prev}} ->
Next = append_frame(Prev, Length, Data),
aggregate_frames(Rest, none, [Next | Joined]);
{[{incomplete, {continuation, Length@1, Data@1}} | Rest@1],
{some, Prev@1}} ->
Next@1 = append_frame(Prev@1, Length@1, Data@1),
aggregate_frames(Rest@1, {some, Next@1}, Joined);
{[{incomplete, Frame} | Rest@2], none} ->
aggregate_frames(Rest@2, {some, Frame}, Joined);
{[{complete, Frame@1} | Rest@3], none} ->
aggregate_frames(Rest@3, none, [Frame@1 | Joined]);
{_, _} ->
{error, nil}
end.
-spec get_headers(bitstring(), list({binary(), binary()})) -> {ok,
{list({binary(), binary()}), bitstring()}} |
{error, decode_packet_error()}.
get_headers(Data, Headers) ->
case gramps_ffi:decode_packet(httph_bin, Data, []) of
{ok, {http_eoh, Rest}} ->
{ok, {Headers, Rest}};
{ok, {{http_header, _, _, Field, Value}, Rest@1}} ->
get_headers(
Rest@1,
[{gleam@string:lowercase(Field), Value} | Headers]
);
{ok, Val} ->
{error,
{http_error,
<<"Expected only headers but got request/response: "/utf8,
(gleam@string:inspect(Val))/binary>>}};
{error, Reason} ->
{error, Reason}
end.
-spec read_response(bitstring()) -> {ok,
{gleam@http@response:response(nil), bitstring()}} |
{error, decode_packet_error()}.
read_response(Data) ->
case gramps_ffi:decode_packet(http_bin, Data, []) of
{ok, {{http_response, _, Status, _}, Rest}} ->
case get_headers(Rest, []) of
{ok, {Headers, Rest@1}} ->
{ok, {{response, Status, Headers, nil}, Rest@1}};
{error, Reason} ->
{error, Reason}
end;
{error, Reason@1} ->
{error, Reason@1};
_ ->
{error, {http_error, <<"Unexpected data"/utf8>>}}
end.
-spec read_request(bitstring()) -> {ok,
{gleam@http@request:request(nil), bitstring()}} |
{error, decode_packet_error()}.
read_request(Data) ->
case gramps_ffi:decode_packet(http_bin, Data, []) of
{ok, {{http_request, Method, Uri, _}, Rest}} ->
case get_headers(Rest, []) of
{ok, {Headers, Rest@1}} ->
Host = begin
_pipe = Headers,
_pipe@1 = gleam@list:key_find(_pipe, <<"host"/utf8>>),
gleam@result:unwrap(_pipe@1, <<""/utf8>>)
end,
{Scheme@1, Host@2, Port@1, Path@2} = case Uri of
{absolute_uri, Scheme, Host@1, Port, Path} ->
{Scheme, Host@1, {some, Port}, Path};
{abs_path, Path@1} ->
{http, Host, none, Path@1}
end,
{Path@3, Query} = begin
_pipe@2 = Path@2,
_pipe@3 = gleam@string:split_once(_pipe@2, <<"?"/utf8>>),
_pipe@4 = gleam@result:map(
_pipe@3,
fun(Pair) ->
{erlang:element(1, Pair),
{some, erlang:element(2, Pair)}}
end
),
gleam@result:unwrap(_pipe@4, {Path@2, none})
end,
Method@1 = gleam@http:method_from_dynamic(
gleam@dynamic:from(Method)
),
{ok,
{{request,
gleam@result:unwrap(Method@1, get),
Headers,
nil,
Scheme@1,
Host@2,
Port@1,
Path@3,
Query},
Rest@1}};
{error, Reason} ->
{error, Reason}
end;
{error, Reason@1} ->
{error, Reason@1};
Err ->
{error,
{http_error,
<<"Unexpected data: "/utf8,
(gleam@string:inspect(Err))/binary>>}}
end.
-spec parse_websocket_key(binary()) -> binary().
parse_websocket_key(Key) ->
_pipe = Key,
_pipe@1 = gleam@string:append(
_pipe,
<<"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"/utf8>>
),
_pipe@2 = crypto:hash(sha, _pipe@1),
base64:encode(_pipe@2).