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.

Support public handle host validation

alice 0dddad8b 91878674

+61 -4
+29
docs/DEPLOYMENT.md
··· 169 169 170 170 For public user handles you also need a matching wildcard-capable site or on-demand TLS path for `*.pds.example.com`. 171 171 172 + One practical Caddy pattern is on-demand TLS restricted to domains that `perlsky` approves: 173 + 174 + ```caddy 175 + { 176 + on_demand_tls { 177 + ask http://127.0.0.1:7755/_allow-cert 178 + } 179 + } 180 + 181 + pds.example.com { 182 + encode gzip 183 + reverse_proxy 127.0.0.1:7755 184 + } 185 + 186 + https:// { 187 + tls { 188 + on_demand 189 + } 190 + 191 + @perlsky_handles host *.pds.example.com 192 + handle @perlsky_handles { 193 + encode gzip 194 + reverse_proxy 127.0.0.1:7755 195 + } 196 + } 197 + ``` 198 + 199 + This still requires wildcard DNS or per-handle DNS records so public ACME validation can reach the server. 200 + 172 201 A minimal nginx site looks like: 173 202 174 203 ```nginx
+15 -1
lib/ATProto/PDS.pm
··· 15 15 use ATProto::PDS::API::Server qw(register_server_handlers); 16 16 use ATProto::PDS::API::Sync qw(register_sync_handlers); 17 17 use ATProto::PDS::Crawlers; 18 - use ATProto::PDS::Identity qw(account_did_doc service_did); 18 + use ATProto::PDS::Identity qw(account_did_doc normalize_handle service_did); 19 19 use ATProto::PDS::LexiconCatalog qw(endpoint_catalog); 20 20 use ATProto::PDS::LexiconRegistry; 21 21 use ATProto::PDS::Metrics; ··· 148 148 149 149 $c->res->headers->content_type('text/plain; version=0.0.4; charset=utf-8'); 150 150 $c->render(data => $c->app->metrics->render_prometheus); 151 + }); 152 + 153 + $routes->get('/_allow-cert')->to(cb => sub ($c) { 154 + my $domain = lc($c->param('domain') // q()); 155 + my $suffix = lc($c->config_value('service_handle_domain', 'localhost')); 156 + my $hostname = lc($c->config_value('hostname', $suffix)); 157 + my $allowed = length($domain) 158 + && ( 159 + $domain eq $hostname 160 + || defined normalize_handle($domain, $suffix, { no_append => 1 }) 161 + ); 162 + 163 + return $c->render(status => 403, text => 'forbidden') unless $allowed; 164 + $c->render(text => 'ok'); 151 165 }); 152 166 153 167 $routes->get('/.well-known/did.json')->to(cb => sub ($c) {
+17 -3
t/app-routes.t
··· 39 39 ->json_is('/availableUserDomains/0' => 'localhost') 40 40 ->json_like('/did' => qr/\Adid:web:/); 41 41 42 + my $suffix = time . int(rand(1_000_000)); 43 + my $routeprobe_handle = "routeprobe-$suffix.localhost"; 44 + 42 45 $t->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 43 - handle => 'routeprobe.localhost', 44 - email => 'routeprobe@example.com', 46 + handle => $routeprobe_handle, 47 + email => "routeprobe-$suffix\@example.com", 45 48 password => 'hunter42', 46 49 })->status_is(200); 47 50 48 51 my $routeprobe_did = $t->tx->res->json->{did}; 49 52 50 - $t->get_ok('/.well-known/atproto-did' => { Host => 'routeprobe.localhost' }) 53 + $t->get_ok('/.well-known/atproto-did' => { Host => $routeprobe_handle }) 51 54 ->status_is(200) 52 55 ->content_type_like(qr{text/plain}) 53 56 ->content_is($routeprobe_did); 54 57 55 58 $t->get_ok('/.well-known/atproto-did' => { Host => 'missing.localhost' }) 56 59 ->status_is(404); 60 + 61 + $t->get_ok("/_allow-cert?domain=$routeprobe_handle") 62 + ->status_is(200) 63 + ->content_is('ok'); 64 + 65 + $t->get_ok('/_allow-cert?domain=localhost') 66 + ->status_is(200) 67 + ->content_is('ok'); 68 + 69 + $t->get_ok('/_allow-cert?domain=example.com') 70 + ->status_is(403); 57 71 58 72 $t->post_ok('/xrpc/com.atproto.repo.createRecord' => json => {}) 59 73 ->status_is(404)