Lexicon-driven ATProto AppView in Gleam — port of HappyView
0

Configure Feed

Select the types of activity you want to include in your feed.

gleamview / src / gleamview_ffi.erl
8.1 kB 250 lines
1-module(gleamview_ffi). 2-export([ 3 open_browser/1, 4 oauth_can_bind/1, 5 oauth_wait_callback/2, 6 dpop_generate_keypair/0, 7 dpop_sign_jwt/3, 8 base64url_encode/1, 9 base64url_decode/1, 10 random_bytes/1, 11 sha256/1 12]). 13 14 15reason_bin(Reason) -> 16 unicode:characters_to_binary(io_lib:format("~p", [Reason])). 17 18%% --------------------------------------------------------------------------- 19%% OAuth / DPoP helpers 20%% --------------------------------------------------------------------------- 21 22open_browser(Url) when is_binary(Url) -> 23 Target = binary_to_list(Url), 24 Cmds = 25 case os:type() of 26 {unix, darwin} -> [{"open", [Target]}]; 27 {win32, _} -> [{"cmd", ["/c", "start", "", Target]}]; 28 _ -> [{"xdg-open", [Target]}] 29 end, 30 open_browser_try(Cmds). 31 32open_browser_try([]) -> 33 {error, <<"no browser opener available">>}; 34open_browser_try([{Cmd, Args} | Rest]) -> 35 case os:find_executable(Cmd) of 36 false -> 37 open_browser_try(Rest); 38 Path -> 39 try 40 Port = open_port( 41 {spawn_executable, Path}, 42 [{args, Args}, hide, binary, exit_status] 43 ), 44 spawn(fun() -> drain_port(Port) end), 45 {ok, nil} 46 catch 47 _:_ -> open_browser_try(Rest) 48 end 49 end. 50 51drain_port(Port) -> 52 receive 53 {Port, {exit_status, _}} -> ok; 54 {Port, _} -> drain_port(Port) 55 after 5000 -> 56 catch port_close(Port), 57 ok 58 end. 59 60%% Probe whether we can bind 127.0.0.1:Port (closes immediately). 61oauth_can_bind(Port) when is_integer(Port) -> 62 case 63 gen_tcp:listen(Port, [ 64 binary, 65 {packet, raw}, 66 {active, false}, 67 {reuseaddr, true}, 68 {ip, {127, 0, 0, 1}}, 69 {backlog, 1} 70 ]) 71 of 72 {ok, Listen} -> 73 gen_tcp:close(Listen), 74 true; 75 {error, _} -> 76 false 77 end. 78 79%% Listen on 127.0.0.1:Port, wait for one HTTP request, return query string. 80%% TimeoutMs is the overall wait budget. 81oauth_wait_callback(Port, TimeoutMs) when is_integer(Port), is_integer(TimeoutMs) -> 82 case 83 gen_tcp:listen(Port, [ 84 binary, 85 {packet, raw}, 86 {active, false}, 87 {reuseaddr, true}, 88 {ip, {127, 0, 0, 1}}, 89 {backlog, 1} 90 ]) 91 of 92 {error, Reason} -> 93 {error, reason_bin(Reason)}; 94 {ok, Listen} -> 95 try 96 case gen_tcp:accept(Listen, TimeoutMs) of 97 {error, Reason} -> 98 {error, reason_bin(Reason)}; 99 {ok, Sock} -> 100 case recv_http_request(Sock, <<>>, TimeoutMs) of 101 {error, Reason} -> 102 gen_tcp:close(Sock), 103 {error, Reason}; 104 {ok, PathQuery} -> 105 Body = 106 <<"HTTP/1.1 200 OK\r\n", 107 "content-type: text/plain; charset=utf-8\r\n", 108 "connection: close\r\n", 109 "content-length: 48\r\n", 110 "\r\n", 111 "Authentication complete. You can close this window.">>, 112 _ = gen_tcp:send(Sock, Body), 113 gen_tcp:close(Sock), 114 {ok, PathQuery} 115 end 116 end 117 after 118 gen_tcp:close(Listen) 119 end 120 end. 121 122recv_http_request(Sock, Acc, TimeoutMs) -> 123 case gen_tcp:recv(Sock, 0, min(TimeoutMs, 30_000)) of 124 {error, Reason} -> 125 {error, reason_bin(Reason)}; 126 {ok, Data} -> 127 Buf = <<Acc/binary, Data/binary>>, 128 case binary:match(Buf, <<"\r\n\r\n">>) of 129 nomatch when byte_size(Buf) > 65536 -> 130 {error, <<"HTTP request too large">>}; 131 nomatch -> 132 recv_http_request(Sock, Buf, TimeoutMs); 133 {Pos, _} -> 134 Header = binary:part(Buf, 0, Pos), 135 case parse_request_line(Header) of 136 {error, _} = E -> E; 137 {ok, PathQuery} -> {ok, PathQuery} 138 end 139 end 140 end. 141 142parse_request_line(Header) -> 143 case binary:split(Header, <<"\r\n">>) of 144 [Line | _] -> 145 case binary:split(Line, <<" ">>, [global]) of 146 [_Method, PathQuery | _] -> {ok, PathQuery}; 147 _ -> {error, <<"malformed request line">>} 148 end; 149 _ -> 150 {error, <<"empty request">>} 151 end. 152 153%% Generate P-256 keypair for DPoP. Returns {ok, {D, X, Y}} as base64url binaries. 154dpop_generate_keypair() -> 155 {Pub, Priv} = crypto:generate_key(ecdh, secp256r1), 156 <<4, X:32/binary, Y:32/binary>> = Pub, 157 D = 158 case Priv of 159 <<D0:32/binary>> -> D0; 160 _ when is_binary(Priv), byte_size(Priv) =< 32 -> 161 pad32(Priv); 162 _ -> 163 error({bad_priv, Priv}) 164 end, 165 {ok, {base64url_encode(D), base64url_encode(X), base64url_encode(Y)}}. 166 167%% Sign JWT signing-input (header.payload) with ES256 using private key D (base64url). 168%% Public X,Y also base64url — used only for header jwk construction by Gleam. 169%% Returns base64url signature (R||S, 64 bytes). 170dpop_sign_jwt(SigningInput, DB64, _XYIgnored) when is_binary(SigningInput), is_binary(DB64) -> 171 try 172 D = base64url_decode_strict(DB64), 173 D32 = pad32(D), 174 Der = crypto:sign(ecdsa, sha256, SigningInput, [D32, secp256r1]), 175 Raw = der_ecdsa_to_raw(Der), 176 {ok, base64url_encode(Raw)} 177 catch 178 C:E:S -> 179 {error, reason_bin({C, E, S})} 180 end. 181 182base64url_encode(Bin) when is_binary(Bin) -> 183 Enc = base64:encode(Bin, #{mode => urlsafe, padding => false}), 184 case is_binary(Enc) of 185 true -> Enc; 186 false -> unicode:characters_to_binary(Enc) 187 end. 188 189base64url_decode(Bin) when is_binary(Bin) -> 190 try 191 {ok, base64url_decode_strict(Bin)} 192 catch 193 _:_ -> {error, nil} 194 end. 195 196base64url_decode_strict(Bin) -> 197 %% OTP base64 urlsafe may need padding restored 198 Padded = pad_b64(Bin), 199 Decoded = base64:decode(Padded, #{mode => urlsafe}), 200 case Decoded of 201 B when is_binary(B) -> B; 202 {ok, B} when is_binary(B) -> B; 203 Other -> error({bad_base64, Other}) 204 end. 205 206pad_b64(Bin) -> 207 case byte_size(Bin) rem 4 of 208 0 -> Bin; 209 2 -> <<Bin/binary, "==">>; 210 3 -> <<Bin/binary, "=">>; 211 1 -> <<Bin/binary, "===">> 212 end. 213 214random_bytes(N) when is_integer(N), N > 0 -> 215 crypto:strong_rand_bytes(N). 216 217sha256(Bin) when is_binary(Bin) -> 218 crypto:hash(sha256, Bin). 219 220pad32(Bin) when byte_size(Bin) =:= 32 -> 221 Bin; 222pad32(Bin) when byte_size(Bin) < 32 -> 223 Pad = 32 - byte_size(Bin), 224 <<0:(Pad * 8), Bin/binary>>; 225pad32(Bin) when byte_size(Bin) > 32 -> 226 binary:part(Bin, byte_size(Bin) - 32, 32). 227 228%% Convert DER ECDSA signature to IEEE P1363 R||S (32+32 bytes). 229der_ecdsa_to_raw(<<16#30, _Len, Rest0/binary>>) -> 230 {R, Rest1} = der_parse_int(Rest0), 231 {S, _} = der_parse_int(Rest1), 232 <<(pad32(R))/binary, (pad32(S))/binary>>; 233der_ecdsa_to_raw(Other) -> 234 error({bad_der_sig, Other}). 235 236der_parse_int(<<16#02, Len, Rest/binary>>) when Len > 0, byte_size(Rest) >= Len -> 237 <<Int:Len/binary, Tail/binary>> = Rest, 238 %% INTEGER may have a leading 0x00 for positive values with high bit set 239 Clean = strip_der_int_padding(Int), 240 {Clean, Tail}; 241der_parse_int(Other) -> 242 error({bad_der_int, Other}). 243 244strip_der_int_padding(<<0, RestInt/binary>>) when byte_size(RestInt) > 0 -> 245 case binary:first(RestInt) band 16#80 of 246 0 -> <<0, RestInt/binary>>; 247 _ -> RestInt 248 end; 249strip_der_int_padding(Int) -> 250 Int.