Gleam CLI client for Tangled (clone of aly.codes/tg)
1-module(gtg_ffi).
2-export([
3 halt/1,
4 unix_seconds/0,
5 unix_micros/0,
6 gzip/1,
7 gunzip/1,
8 get_cwd/0,
9 format_rfc3339/1,
10 ed25519_generate/0,
11 ed25519_public/1,
12 ed25519_sign/2,
13 lookup_txt/1,
14 wait_http_callback/2
15]).
16
17halt(Code) ->
18 erlang:halt(Code).
19
20unix_seconds() ->
21 erlang:system_time(second).
22
23unix_micros() ->
24 erlang:system_time(microsecond).
25
26gzip(Data) when is_binary(Data) ->
27 zlib:gzip(Data).
28
29gunzip(Data) when is_binary(Data) ->
30 try
31 {ok, zlib:gunzip(Data)}
32 catch
33 _:_ -> {error, <<"gunzip failed">>}
34 end.
35
36get_cwd() ->
37 case file:get_cwd() of
38 {ok, Cwd} -> {ok, unicode:characters_to_binary(Cwd)};
39 {error, Reason} -> {error, unicode:characters_to_binary(io_lib:format("~p", [Reason]))}
40 end.
41
42format_rfc3339(Secs) when is_integer(Secs) ->
43 {{Y, Mo, D}, {H, Mi, S}} = calendar:system_time_to_universal_time(Secs, second),
44 list_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ", [Y, Mo, D, H, Mi, S])).
45
46%% {PublicKey :: binary(), PrivateSeed :: binary()} — both 32 bytes.
47ed25519_generate() ->
48 {Pub, Priv} = crypto:generate_key(eddsa, ed25519),
49 Seed =
50 case byte_size(Priv) of
51 32 -> Priv;
52 64 -> binary:part(Priv, 0, 32);
53 _ -> Priv
54 end,
55 {Pub, Seed}.
56
57ed25519_public(Seed) when is_binary(Seed), byte_size(Seed) =:= 32 ->
58 {Pub, _} = crypto:generate_key(eddsa, ed25519, Seed),
59 Pub;
60ed25519_public(Priv) when is_binary(Priv), byte_size(Priv) =:= 64 ->
61 Seed = binary:part(Priv, 0, 32),
62 {Pub, _} = crypto:generate_key(eddsa, ed25519, Seed),
63 Pub.
64
65ed25519_sign(Msg, Seed) when is_binary(Msg), is_binary(Seed) ->
66 crypto:sign(eddsa, none, Msg, [Seed, ed25519]).
67
68lookup_txt(NameBin) when is_binary(NameBin) ->
69 Name = unicode:characters_to_list(NameBin),
70 case inet_res:lookup(Name, in, txt) of
71 Records when is_list(Records) ->
72 lists:flatmap(fun txt_strings/1, Records);
73 _ ->
74 []
75 end.
76
77txt_strings([H | _] = L) when is_list(H) ->
78 [list_to_binary(lists:flatten(L))];
79txt_strings(Txt) when is_list(Txt) ->
80 [list_to_binary(Txt)];
81txt_strings(Bin) when is_binary(Bin) ->
82 [Bin];
83txt_strings(_) ->
84 [].
85
86%% Accept one HTTP request on 127.0.0.1:Port and return the raw query string
87%% from the request target (everything after '?'), or {error, ReasonBin}.
88wait_http_callback(Port, TimeoutMs) when is_integer(Port), is_integer(TimeoutMs) ->
89 Opts = [
90 binary,
91 {packet, raw},
92 {active, false},
93 {reuseaddr, true},
94 {ip, {127, 0, 0, 1}}
95 ],
96 case gen_tcp:listen(Port, Opts) of
97 {ok, Listen} ->
98 case gen_tcp:accept(Listen, TimeoutMs) of
99 {ok, Sock} ->
100 case gen_tcp:recv(Sock, 0, TimeoutMs) of
101 {ok, Data} ->
102 Reply =
103 <<"HTTP/1.1 200 OK\r\n",
104 "Content-Type: text/html; charset=utf-8\r\n",
105 "Connection: close\r\n",
106 "Content-Length: 62\r\n",
107 "\r\n",
108 "<html><body>Authenticated. You can close this tab.</body></html>">>,
109 _ = gen_tcp:send(Sock, Reply),
110 gen_tcp:close(Sock),
111 gen_tcp:close(Listen),
112 case extract_query(Data) of
113 {ok, Qs} -> {ok, Qs};
114 error -> {error, <<"could not parse callback request">>}
115 end;
116 {error, Reason} ->
117 gen_tcp:close(Sock),
118 gen_tcp:close(Listen),
119 {error, reason_bin(Reason)}
120 end;
121 {error, Reason} ->
122 gen_tcp:close(Listen),
123 {error, reason_bin(Reason)}
124 end;
125 {error, Reason} ->
126 {error, reason_bin(Reason)}
127 end.
128
129extract_query(Data) when is_binary(Data) ->
130 %% First line: METHOD SP target SP HTTP/…
131 case binary:split(Data, <<"\r\n">>) of
132 [Line | _] ->
133 case binary:split(Line, <<" ">>, [global]) of
134 [_, Target | _] ->
135 case binary:split(Target, <<"?">>) of
136 [_, Qs] -> {ok, Qs};
137 _ -> {ok, <<>>}
138 end;
139 _ ->
140 error
141 end;
142 _ ->
143 error
144 end.
145
146reason_bin(Reason) ->
147 unicode:characters_to_binary(io_lib:format("~p", [Reason])).