perlsky is a Perl 5 implementation of an AT Protocol Personal Data Server.
13
fork

Configure Feed

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

Resolve non-local handles through appview

alice 111e3e72 2c1f61d9

+166 -4
+35 -4
lib/ATProto/PDS/API/Builtins.pm
··· 7 7 8 8 use Exporter 'import'; 9 9 use Mojo::JSON qw(false true); 10 + use Mojo::URL; 11 + use Mojo::UserAgent; 10 12 11 13 use ATProto::PDS::Identity qw(account_did_doc normalize_handle service_did service_did_doc); 12 14 ··· 50 52 } 51 53 52 54 my $service_handle = lc($c->config_value('service_handle_domain', 'localhost')); 55 + if ($handle eq $service_handle) { 56 + return { 57 + did => service_did($c->app->settings), 58 + }; 59 + } 60 + 61 + if (my $did = _resolve_remote_handle_via_appview($c, $handle)) { 62 + return { did => $did }; 63 + } 64 + 53 65 die { 54 66 status => 404, 55 67 error => 'HandleNotFound', 56 68 message => "No DID found for handle $handle", 57 - } unless $handle eq $service_handle; 58 - 59 - return { 60 - did => service_did($c->app->settings), 61 69 }; 62 70 }); 63 71 ··· 114 122 }, 115 123 }; 116 124 }); 125 + } 126 + 127 + sub _resolve_remote_handle_via_appview ($c, $handle) { 128 + my $origin = $c->config_value('bsky_appview_url', 'https://api.bsky.app'); 129 + return undef unless defined $origin && length $origin; 130 + 131 + state %ua_for_origin; 132 + my $ua = $ua_for_origin{$origin} //= do { 133 + my $client = Mojo::UserAgent->new(max_redirects => 0); 134 + $client->request_timeout(15); 135 + $client->inactivity_timeout(15); 136 + $client; 137 + }; 138 + 139 + my $url = Mojo::URL->new($origin)->path('/xrpc/com.atproto.identity.resolveHandle')->query(handle => $handle); 140 + my $tx = eval { $ua->get($url) }; 141 + return undef if $@ || !$tx; 142 + 143 + my $res = $tx->result; 144 + return undef unless ($res->code // 0) == 200; 145 + my $json = $res->json; 146 + return undef unless ref($json) eq 'HASH' && defined($json->{did}) && length($json->{did}); 147 + return $json->{did}; 117 148 } 118 149 119 150 sub _same_did ($left, $right) {
+131
t/remote-handle-resolution.t
··· 1 + use v5.34; 2 + use warnings; 3 + 4 + use Config (); 5 + use File::Path qw(remove_tree); 6 + use File::Spec; 7 + use FindBin qw($Bin); 8 + use IO::Socket::INET; 9 + use Test::More; 10 + use Time::HiRes qw(sleep); 11 + 12 + BEGIN { 13 + require lib; 14 + my $root = File::Spec->rel2abs(File::Spec->catdir($Bin, '..')); 15 + lib->import( 16 + File::Spec->catdir($root, 'lib'), 17 + File::Spec->catdir($root, 'local', 'lib', 'perl5'), 18 + File::Spec->catdir($root, 'local', 'lib', 'perl5', $Config::Config{archname}), 19 + ); 20 + } 21 + 22 + use Mojo::Server::Daemon; 23 + use Mojo::UserAgent; 24 + use Mojolicious; 25 + use Test::Mojo; 26 + use ATProto::PDS; 27 + 28 + my @mock_pids; 29 + END { 30 + my $status = $?; 31 + kill 'TERM', @mock_pids if @mock_pids; 32 + waitpid($_, 0) for @mock_pids; 33 + $? = $status; 34 + } 35 + 36 + my $root = File::Spec->rel2abs(File::Spec->catdir($Bin, '..')); 37 + my $tmp = File::Spec->catdir($root, 'data', 'tmp-tests', 'remote-handle-resolution'); 38 + remove_tree($tmp) if -d $tmp; 39 + 40 + my $remote_handle = 'alice.mosphere.at'; 41 + my $remote_did = 'did:plc:pkktelaqretqiz2bddzzlv3t'; 42 + 43 + my $appview_app = Mojolicious->new; 44 + $appview_app->routes->get('/ready')->to(cb => sub { 45 + my ($c) = @_; 46 + $c->render(text => 'ok'); 47 + }); 48 + $appview_app->routes->get('/xrpc/com.atproto.identity.resolveHandle')->to(cb => sub { 49 + my ($c) = @_; 50 + my $handle = lc($c->param('handle') // ''); 51 + return $c->render(json => { did => $remote_did }) if $handle eq $remote_handle; 52 + $c->render(status => 404, json => { 53 + error => 'HandleNotFound', 54 + message => "No DID found for handle $handle", 55 + }); 56 + }); 57 + 58 + my $appview_url = _start_mock_server($appview_app); 59 + 60 + my $app = ATProto::PDS->new( 61 + project_root => $root, 62 + settings => { 63 + base_url => 'http://127.0.0.1:7755', 64 + service_did_method => 'did:web', 65 + service_handle_domain => 'perlsky.example.test', 66 + jwt_secret => 'remote-handle-secret', 67 + data_dir => $tmp, 68 + db_path => File::Spec->catfile($tmp, 'perlsky.sqlite'), 69 + bsky_appview_url => $appview_url, 70 + }, 71 + ); 72 + my $t = Test::Mojo->new($app); 73 + 74 + $t->get_ok("/xrpc/com.atproto.identity.resolveHandle?handle=$remote_handle") 75 + ->status_is(200) 76 + ->json_is('/did' => $remote_did); 77 + 78 + $t->get_ok('/xrpc/com.atproto.identity.resolveHandle?handle=missing.example.test') 79 + ->status_is(404) 80 + ->json_is('/error' => 'HandleNotFound'); 81 + 82 + done_testing; 83 + 84 + sub _start_mock_server { 85 + my ($mock_app) = @_; 86 + my $port = _find_free_port(); 87 + my $pid = fork(); 88 + die 'fork failed' unless defined $pid; 89 + 90 + if ($pid == 0) { 91 + my $daemon = Mojo::Server::Daemon->new( 92 + app => $mock_app, 93 + listen => ["http://127.0.0.1:$port"], 94 + silent => 1, 95 + ); 96 + $daemon->run; 97 + exit 0; 98 + } 99 + 100 + push @mock_pids, $pid; 101 + my $url = "http://127.0.0.1:$port"; 102 + _wait_for_ready($url); 103 + return $url; 104 + } 105 + 106 + sub _wait_for_ready { 107 + my ($base_url) = @_; 108 + my $ua = Mojo::UserAgent->new(max_redirects => 0); 109 + for (1 .. 100) { 110 + my $ok = eval { 111 + my $tx = $ua->get("$base_url/ready"); 112 + my $res = $tx->result; 113 + return ($res->code // 0) == 200; 114 + }; 115 + return 1 if $ok; 116 + sleep 0.05; 117 + } 118 + die "mock server did not become ready at $base_url"; 119 + } 120 + 121 + sub _find_free_port { 122 + my $sock = IO::Socket::INET->new( 123 + LocalAddr => '127.0.0.1', 124 + LocalPort => 0, 125 + Proto => 'tcp', 126 + Listen => 5, 127 + ) or die "unable to allocate port: $!"; 128 + my $port = $sock->sockport; 129 + close $sock; 130 + return $port; 131 + }