File Coverage

blib/lib/Mojolicious/Plugin/WebFinger.pm
Criterion Covered Total %
statement 82 156 52.5
branch 27 124 21.7
condition 27 75 36.0
subroutine 9 15 60.0
pod 1 1 100.0
total 146 371 39.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::WebFinger;
2 1     1   1110 use Mojo::Base 'Mojolicious::Plugin';
  1         3  
  1         8  
3 1     1   235 use Mojo::Util 'url_escape';
  1         10  
  1         50  
4 1     1   6 use Mojo::URL;
  1         2  
  1         8  
5              
6             # Todo:
7             # - Make callback non-blocking aware
8             # - Support 307 Temporary Redirect as described in the spec
9             # - Support simple startup defintion, like
10             # plugin WebFinger => {
11             # 'akron@sojolicious' => {
12             # describedby => {
13             # type => 'application/rdf+xml',
14             # href => 'http://sojolicio.us/akron.foaf'
15             # }
16             # }
17             # };
18              
19             our $VERSION = '0.12';
20              
21              
22             my $WK_PATH = '/.well-known/webfinger';
23              
24             # Register Plugin
25             sub register {
26 1     1 1 42 my ($plugin, $mojo, $param) = @_;
27              
28             # Plugin parameter
29 1   50     4 $param ||= {};
30              
31             # Load parameter from Config file
32 1 50       10 if (my $config_param = $mojo->config('WebFinger')) {
33 0         0 $param = { %$param, %$config_param };
34             };
35              
36             # Load HostMeta if not already loaded.
37             # This automatically loads XRD,
38             # Util::Endpoint and Util::Callback plugins.
39 1 50       27 unless (exists $mojo->renderer->helpers->{hostmeta}) {
40 1         22 $mojo->plugin('HostMeta');
41             };
42              
43             # Check for 'prepare_webfinger' and 'fetch_webfinger' callback
44 1         29915 $mojo->callback(
45             [qw/fetch_webfinger prepare_webfinger/],
46             $param,
47             -once
48             );
49              
50             # Get seconds to expiration
51 1         127 my $seconds = (60 * 60 * 24 * 10);
52 1 50 33     7 if ($param->{expires} && $param->{expires} =~ /^\d+$/) {
53 0         0 $seconds = delete $param->{expires};
54             };
55              
56             # Establish WebFinger Route
57 1         4 my $wfr = $mojo->routes->any($WK_PATH);
58              
59             # Establish endpoint
60 1         365 $wfr->endpoint(
61             webfinger => {
62             query => [
63             'resource' => '{uri}',
64             'rel' => '{rel?}',
65             'format' => '{format?}'
66             ]
67             });
68              
69             # Response to webfinger request
70             $wfr->to(
71             cb => sub {
72 5     5   94099 my $c = shift;
73              
74             # Check for security
75 5 50 33     27 if ($param->{secure} && !$c->req->is_secure) {
76              
77             # Bad request - only https allowed!
78 0         0 return $c->render(status => 400);
79             };
80              
81             # Get resource parameter
82 5         18 my $res = $c->param('resource');
83              
84             # Delete invalid parameters
85 5 50 33     1764 if (!$res || $res eq '{uri}') {
86              
87             # Bad request - no resource defined
88 0         0 return $c->render(status => 400);
89             };
90              
91             # Set standard format
92 5 50 50     22 unless ($c->stash('format') || scalar $c->param('_format') || scalar $c->param('format')) {
      100        
93 3         425 $c->stash(format => 'jrd');
94             };
95              
96             # Normalize the resource
97 5         248 my ($acct, $host, $nres) = _normalize_resource($c, $res);
98              
99             # Set host to local
100 5   50     22 $host ||= $c->req->url->base->host || 'localhost';
      66        
101              
102             # Bad request - no resource defined
103 5 50       68 return $c->render(status => 400) unless $nres;
104              
105             # Check for 'prepare_webfinger' callback
106 5 100       25 if ($c->callback(prepare_webfinger => $nres)) {
107              
108             # The response body is already rendered
109 3 50       106 return if $c->res->body;
110              
111             # Create new xrd document
112 3         146 my $xrd = _serve_webfinger($c, $acct, $nres, $res);
113              
114             # Seconds given
115 3 50       13 if ($xrd) {
116              
117 3         21 my $expires;
118 3 50 33     14 unless ($expires = $xrd->expires && $seconds) {
119 3         820 $expires = $xrd->expires( time + $seconds);
120             };
121              
122             # Expires set
123 3 50       21817 if ($expires) {
124              
125             # Set cache control
126 3         33 my $headers = $c->res->headers;
127 3         91 $headers->cache_control(
128             "public, max-age=$seconds"
129             );
130              
131             # Set expires header
132 3         32 $headers->expires( $xrd->expires );
133             };
134             };
135              
136             # Server xrd document
137 3         3133 return $c->reply->xrd($xrd, $res);
138             };
139              
140             # No valid xrd document is existing for this resource
141 2         76 return $c->reply->xrd(undef, $res);
142             }
143 1         44 );
144              
145             # Add Route to Host-Meta - exactly once
146             $mojo->hook(
147             prepare_hostmeta => sub {
148 1     1   4748 my ($c, $hostmeta) = @_;
149              
150             # Add JRD link
151 1         11 $hostmeta->link(lrdd => {
152             type => 'application/jrd+json',
153             template => $c->endpoint(
154             webfinger => {
155             '?' => undef
156             }
157             )
158             });
159              
160             # Add XRD link
161 1         4110 $hostmeta->link(lrdd => {
162             type => 'application/xrd+xml',
163             template => $c->endpoint(
164             webfinger => {
165             format => 'xrd',
166             '?' => undef
167             }
168             )
169             });
170 1         36 });
171              
172             # webfinger helper
173 1         16 $mojo->helper(
174             webfinger => \&_fetch_webfinger
175             );
176             };
177              
178              
179             # Fetch webfinger resource
180             sub _fetch_webfinger {
181 2     2   21743 my $c = shift;
182              
183 2         7 my ($acct, $res, $nres, $host);
184              
185              
186             # Request with host information
187 2 50 33     14 if ($_[1] && !ref($_[1]) && index($_[1], '-') != 0) {
      33        
188 0         0 $host = shift;
189 0         0 $nres = shift;
190             }
191              
192             # Get host information from resource
193             else {
194 2         5 $res = shift;
195 2         9 ($acct, $host, $nres) = _normalize_resource($c, $res);
196             };
197              
198             # Trim tail
199 2   33     10 pop while @_ && !defined $_[-1];
200              
201             # Get flags
202 2         5 my %flag;
203 2   33     10 while (defined $_[-1] && index($_[-1], '-') == 0) {
204 0         0 $flag{ pop() } = 1;
205             };
206              
207             # Optimize flags for known services
208 2 50 66     12 if ($host && $host =~ /(?:gmail|yahoo|mozilla)\.(?:com|org|net)$/i) {
209 0 0       0 $flag{-old} = 1 unless $flag{-modern};
210             };
211              
212             # Get callback
213 2 50 33     11 my $cb = defined $_[-1] && ref $_[-1] eq 'CODE' ? pop : undef;
214              
215             # Get header information for requests
216 2         26 my $header = {};
217 2 0 33     13 if ($_[0] && ref $_[0] && ref($_[0]) eq 'HASH') {
      33        
218 0         0 $header = shift;
219             };
220              
221             # Get relation information
222 2         3 my $rel = shift;
223              
224             # If local, serve local
225 2 50 50     14 if (!$host ||
      66        
226             ($host eq ($c->req->url->base->host || 'localhost'))) {
227              
228 2 50       36 if ($c->callback(prepare_webfinger => $nres)) {
229              
230             # Serve local xrd document
231 2         64 my $xrd = _serve_webfinger($c, $acct, $nres, $res);
232              
233             # Return values
234 2 50       18 return $cb ? $cb->($xrd, Mojo::Headers->new) : (
    50          
235             wantarray ? ($xrd, Mojo::Headers->new) : $xrd
236             );
237             }
238             else {
239 0 0       0 return $cb ? $cb->() : undef;
240             }
241             };
242              
243             # Check cache
244 0         0 my ($xrd, $headers) = $c->callback(
245             fetch_webfinger => ($host, $nres, $header)
246             );
247              
248             # Store unchanged normalized resource
249 0         0 $res = $nres;
250              
251             # Delete resource
252 0         0 $nres =~ s/^acct://;
253              
254             # xrd document exists
255 0 0       0 if ($xrd) {
256              
257             # Filter relations
258 0 0       0 $xrd = $xrd->filter_rel( $rel ) if $rel;
259              
260             # Set headers to default
261 0 0 0     0 $headers ||= Mojo::Headers->new if $cb || wantarray;
      0        
262              
263             # Return cached webfinger document
264             # Return values
265 0 0       0 return $cb ? $cb->($xrd, $headers) : (
    0          
266             wantarray ? ($xrd, $headers) : $xrd
267             );
268             };
269              
270             # Not found
271 0 0 0     0 return ($cb ? $cb->() : undef) unless $host && $res;
    0          
272              
273             # Set secure value
274 0         0 my $secure;
275 0 0 0     0 if (exists $flag{-secure} || exists $flag{-modern}) {
276 0         0 $secure = 1;
277             };
278              
279             # Modern webfinger path
280 0         0 my $path = '//' . $host . $WK_PATH . '?resource=' . url_escape $nres;
281 0 0       0 $path = 'https:' . $path if $secure;
282              
283             # Non-blocking
284 0 0       0 if ($cb) {
285              
286             # Initialize delay array
287 0         0 my @delay;
288              
289             # If modern is allowed
290 0 0       0 unless (exists $flag{-old}) {
291              
292             # push to delay array
293             push(
294             @delay,
295              
296             # Step 1
297             sub {
298 0     0   0 my $delay = shift;
299              
300             # Retrieve from modern path
301 0         0 $c->get_xrd(
302             $path => $header => $delay->begin
303             );
304             },
305              
306             # Step 2
307             sub {
308 0     0   0 my ($delay, $xrd, $headers) = @_;
309              
310             # Document found
311 0 0       0 if ($xrd) {
312              
313             # Hook for caching
314 0         0 $c->app->plugins->emit_hook(
315             after_fetching_webfinger => (
316             $c, $host, $res, $xrd, $headers
317             ));
318              
319             # Filter based on relations
320 0 0       0 $xrd = $xrd->filter_rel($rel) if $rel;
321              
322             # Successful
323 0         0 return $cb->($xrd, $headers);
324             };
325              
326             # No more discovery
327 0 0       0 return $cb->() if exists $flag{-modern};
328              
329             # Next step
330 0         0 $delay->begin->();
331 0         0 });
332             };
333              
334             # Old Host-Meta discovery
335             push(
336             @delay,
337              
338             # Step 3
339             sub {
340 0     0   0 my $delay = shift;
341              
342 0         0 my @param = (
343             $host,
344             $header,
345             ['lrdd'],
346             $delay->begin(0,1)
347             );
348              
349 0 0       0 push @param, '-secure' if $secure;
350              
351             # Host-Meta with lrdd
352 0         0 $c->hostmeta( @param );
353             },
354              
355             # Step 4
356             sub {
357             # Host-Meta document
358 0     0   0 my ($delay, $xrd) = @_;
359              
360             # Host-Meta is expired
361 0 0 0     0 return $cb->() if !$xrd || $xrd->expired;
362              
363             # Prepare lrdd
364 0 0       0 my $template = _get_lrdd($xrd) or return $cb->();
365              
366             # Interpolate template
367 0         0 my $lrdd = $c->endpoint($template => {
368             uri => $nres,
369             '?' => undef
370             });
371              
372             # Get lrdd
373 0         0 $c->get_xrd($lrdd => $header => $delay->begin(0,1))
374             },
375              
376             # Step 5
377             sub {
378 0     0   0 my $delay = shift;
379 0         0 my ($xrd, $headers) = @_;
380              
381             # No lrdd xrd document found
382 0 0       0 return $cb->() unless $xrd;
383              
384             # Hook for caching
385 0         0 $c->app->plugins->emit_hook(
386             after_fetching_webfinger => (
387             $c, $host, $res, $xrd, $headers
388             ));
389              
390             # Filter based on relations
391 0 0       0 $xrd = $xrd->filter_rel($rel) if $rel;
392              
393             # Successful
394 0         0 return $cb->($xrd, $headers);
395 0         0 });
396              
397             # Create delay
398 0         0 my $delay = Mojo::IOLoop->delay(@delay);
399              
400             # Start IOLoop if not running
401 0 0       0 $delay->wait unless Mojo::IOLoop->is_running;
402              
403 0         0 return;
404             };
405              
406             # Blocking
407             # Modern discovery
408 0 0       0 unless (exists $flag{-old}) {
409              
410             # Retrieve from modern path
411 0         0 ($xrd, $headers) = $c->get_xrd($path => $header);
412             };
413              
414             # Not found yet
415 0 0       0 unless ($xrd) {
416              
417             # No further discovery
418 0 0       0 return if exists $flag{-modern};
419              
420             # Host-Meta and lrdd
421 0 0       0 $xrd = $c->hostmeta(
    0          
422             $host,
423             $header,
424             ['lrdd'],
425             ($secure ? '-secure' : undef)
426             ) or return;
427              
428             # Todo: support header expiration date
429 0 0       0 return if $xrd->expired;
430              
431             # Find 'lrdd' link
432 0 0       0 my $template = _get_lrdd($xrd) or return;
433              
434             # Interpolate template
435 0         0 my $lrdd = $c->endpoint(
436             $template => {
437             uri => $nres,
438             '?' => undef
439             });
440              
441             # Retrieve based on lrdd
442 0 0       0 ($xrd, $headers) = $c->get_xrd($lrdd => $header) or return;
443             };
444              
445             # Hook for caching
446 0         0 $c->app->plugins->emit_hook(
447             after_fetching_webfinger => (
448             $c, $host, $res, $xrd, $headers
449             ));
450              
451             # Filter based on relations
452 0 0       0 $xrd = $xrd->filter_rel($rel) if $rel;
453              
454             # Return
455 0 0       0 return wantarray ? ($xrd, $headers) : $xrd;
456             };
457              
458              
459             # Serve webfinger
460             sub _serve_webfinger {
461 5     5   11 my $c = shift;
462 5         16 my ($acct, $nres, $res) = @_;
463              
464             # No normalized resource
465 5 50       25 return unless $nres;
466              
467             # No resource given
468 5   33     14 $res ||= $nres;
469              
470             # Create new XRD document
471 5         27 my $xrd = $c->new_xrd;
472              
473             # Set Subject
474 5         1085 $xrd->subject($res);
475              
476             # Set Alias
477 5 100       20184 $xrd->alias($nres) if $res ne $nres;
478              
479             # Run hook
480 5         4148 $c->app->plugins->emit_hook(
481             before_serving_webfinger => ($c, $nres, $xrd)
482             );
483              
484             # Filter relations
485 5 50       19329 $xrd = $xrd->filter_rel($c->every_param('rel')) if $c->param('rel');
486              
487             # Return webfinger document
488 5         743 return $xrd;
489             };
490              
491              
492             # Normalize resource
493             sub _normalize_resource {
494 7     7   22 my ($c, $res) = @_;
495 7 50       22 return unless $res;
496              
497             # Resource is qualified
498 7 50 66     59 if (index($res, 'acct:') != 0 and $res =~ /^[^:]+:/) {
499              
500 0 0       0 return $res unless wantarray;
501              
502             # Check host
503 0         0 my $url = Mojo::URL->new($res);
504              
505             # Get host information
506 0         0 my $host = $url->host;
507              
508             # Return array
509 0 0       0 return (undef, $host, $res) if wantarray;
510             };
511              
512             # Delete scheme if exists
513 7         43 $res =~ s/^acct://i;
514              
515             # Split user from domain
516 7         46 my ($acct, $host) = split '@', lc $res;
517              
518             # Create norm writing
519 7         30 my $norm = 'acct:' . $acct . '@';
520              
521             # Use request host if no host is given
522 7   50     36 $norm .= ($host || $c->req->url->base->host || 'localhost');
523              
524 7 50       104 return wantarray ? ($acct, $host, $norm) : $norm;
525             };
526              
527              
528             # Get lrdd
529             sub _get_lrdd {
530 0     0     my $xrd = shift;
531              
532             # Find 'lrdd' link
533 0 0         my $lrdd = $xrd->link('lrdd') or return;
534              
535             # Get template
536 0 0         $lrdd->attr('template') or return;
537             };
538              
539              
540             1;
541              
542              
543             __END__