File Coverage

blib/lib/Plack/App/Prerender.pm
Criterion Covered Total %
statement 41 109 37.6
branch 0 42 0.0
condition 0 14 0.0
subroutine 14 16 87.5
pod 2 2 100.0
total 57 183 31.1


line stmt bran cond sub pod time code
1             package Plack::App::Prerender;
2              
3             # ABSTRACT: a simple prerendering proxy for Plack
4              
5 1     1   627500 use v5.10.1;
  1         9  
6 1     1   5 use strict;
  1         1  
  1         17  
7 1     1   3 use warnings;
  1         2  
  1         60  
8              
9             our $VERSION = 'v0.2.0';
10              
11 1     1   6 use parent qw/ Plack::Component /;
  1         2  
  1         9  
12              
13 1     1   3677 use Encode qw/ encode /;
  1         2  
  1         42  
14 1     1   5 use HTTP::Headers;
  1         2  
  1         54  
15 1     1   6 use HTTP::Request;
  1         2  
  1         31  
16 1     1   5 use HTTP::Status qw/ :constants /;
  1         1  
  1         406  
17 1     1   424 use Plack::Request;
  1         44120  
  1         31  
18 1     1   7 use Plack::Util;
  1         3  
  1         23  
19 1     1   376 use Plack::Util::Accessor qw/ mech rewrite cache max_age request response wait /;
  1         219  
  1         6  
20 1     1   477 use Ref::Util qw/ is_coderef is_plain_arrayref /;
  1         1392  
  1         62  
21 1     1   427 use Time::Seconds qw/ ONE_HOUR /;
  1         1118  
  1         46  
22 1     1   6 use WWW::Mechanize::Chrome;
  1         2  
  1         597  
23              
24             # RECOMMEND PREREQ: CHI
25             # RECOMMEND PREREQ: Log::Log4perl
26             # RECOMMEND PREREQ: Ref::Util::XS
27              
28              
29             sub prepare_app {
30 0     0 1   my ($self) = @_;
31              
32 0 0         unless ($self->mech) {
33              
34 0           my $mech = WWW::Mechanize::Chrome->new(
35             headless => 1,
36             separate_session => 1,
37             );
38              
39 0           $self->mech($mech);
40              
41             }
42              
43 0 0         unless ($self->request) {
44             $self->request(
45             {
46             'User-Agent' => 'X-Forwarded-User-Agent',
47 0           ( map { $_ => $_ } qw/
  0            
48             X-Forwarded-For
49             X-Forwarded-Host
50             X-Forwarded-Port
51             X-Forwarded-Proto
52             /
53             ),
54             }
55             );
56             }
57 0 0         if (is_plain_arrayref($self->request)) {
58 0           $self->request( { map { $_ => $_ } @{ $self->request } } );
  0            
  0            
59             }
60              
61              
62 0 0         unless ($self->response) {
63             $self->response(
64             {
65 0           ( map { $_ => $_ } qw/
  0            
66             Content-Type
67             Expires
68             Last-Modified
69             /
70             ),
71             }
72             );
73             }
74 0 0         if (is_plain_arrayref($self->response)) {
75 0           $self->response( { map { $_ => $_ } @{ $self->response } } );
  0            
  0            
76             }
77              
78 0 0         unless ($self->max_age) {
79 0           $self->max_age( ONE_HOUR );
80             }
81             }
82              
83             sub call {
84 0     0 1   my ($self, $env) = @_;
85              
86 0           my $req = Plack::Request->new($env);
87              
88 0   0       my $method = $req->method // '';
89 0 0         unless ($method eq "GET") {
90 0           return [ HTTP_METHOD_NOT_ALLOWED, [], [] ];
91             }
92              
93 0           my $path_query = $env->{REQUEST_URI};
94              
95 0           my $base = $self->rewrite;
96 0 0         my $url = is_coderef($base)
97             ? $base->($path_query, $env)
98             : $base . $path_query;
99              
100 0   0       $url //= [ HTTP_BAD_REQUEST, [], [] ];
101 0 0         return $url if (is_plain_arrayref($url));
102              
103 0           my $cache = $self->cache;
104 0 0         my $data = $cache ? $cache->get($path_query) : undef;
105 0 0         if (defined $data) {
106              
107 0           return $data;
108              
109             }
110             else {
111              
112 0           my $mech = $self->mech;
113 0           $mech->reset_headers;
114              
115 0           my $req_head = $req->headers;
116 0           for my $field (keys %{ $self->request }) {
  0            
117 0   0       my $value = $req_head->header($field) // next;
118 0 0         my $send = $self->request->{$field} or next;
119 0 0         $send = $field if $send eq "1";
120 0           $mech->add_header( $send => $value );
121             }
122              
123 0           my $res = $mech->get( $url );
124              
125 0 0         if (my $count = $self->wait) {
126 0           while ($mech->infinite_scroll(1)) {
127 0 0         last if $count-- < 0;
128             }
129             }
130              
131 0           my $body = encode("UTF-8", $mech->content);
132              
133 0           my $head = $res->headers;
134 0           my $h = Plack::Util::headers([ 'X-Renderer' => __PACKAGE__ ]);
135 0           for my $field (keys %{ $self->response }) {
  0            
136 0   0       my $value = $head->header($field) // next;
137 0           $value =~ tr/\n/ /;
138 0 0         my $send = $self->response->{$field} or next;
139 0 0         $send = $field if $send eq "1";
140 0           $h->set( $send => $value );
141             }
142              
143 0 0         if ($res->code == HTTP_OK) {
144              
145 0           $data = [ HTTP_OK, $h->headers, [$body] ];
146              
147 0 0         if ($cache) {
148 0           my $age;
149 0 0         if (my $value = $head->header("Cache-Control")) {
150 0           ($age) = $value =~ /(?:s\-)?max-age=([0-9]+)\b/;
151 0 0 0       if ($age && $age > $self->max_age) {
152 0           $age = $self->max_age;
153             }
154             }
155              
156 0   0       $cache->set( $path_query, $data, $age // $self->max_age );
157             }
158              
159 0           return $data;
160              
161             }
162             else {
163              
164 0           return [ $res->code, $h->headers, [$body] ];
165              
166             }
167             }
168              
169             }
170              
171              
172             1;
173              
174             __END__
175              
176             =pod
177              
178             =encoding UTF-8
179              
180             =head1 NAME
181              
182             Plack::App::Prerender - a simple prerendering proxy for Plack
183              
184             =head1 VERSION
185              
186             version v0.2.0
187              
188             =head1 SYNOPSIS
189              
190             use CHI;
191             use Log::Log4perl qw/ :easy /;
192             use Plack::App::Prerender;
193              
194             my $cache = CHI->new(
195             driver => 'File',
196             root_dir => '/tmp/test-chi',
197             );
198              
199             Log::Log4perl->easy_init($ERROR);
200              
201             my $app = Plack::App::Prerender->new(
202             rewrite => "http://www.example.com",
203             cache => $cache,
204             wait => 10,
205             )->to_app;
206              
207             =head1 DESCRIPTION
208              
209             This is a PSGI application that acts as a simple prerendering proxy
210             for websites using Chrone.
211              
212             This only supports GET requests, as this is intended as a proxy for
213             search engines that do not support AJAX-generated content.
214              
215             =head1 ATTRIBUTES
216              
217             =head2 mech
218              
219             A L<WWW::Mechanize::Chrome> object. If omitted, a headless instance of
220             Chrome will be launched.
221              
222             If you want to specify alternative options, you chould create your own
223             instance of WWW::Mechanize::Chrome and pass it to the constructor.
224              
225             =head2 rewrite
226              
227             This can either be a base URL prefix string, or a code reference that
228             takes the PSGI C<REQUEST_URI> and environment hash as arguments, and
229             returns a full URL to pass to L</mech>.
230              
231             If the code reference returns C<undef>, then the request will abort
232             with an HTTP 400.
233              
234             If the code reference returns an array reference, then it assumes the
235             request is a Plack response and simply returns it.
236              
237             This can be used for simple request validation. For example,
238              
239             use Robots::Validate v0.2.0;
240              
241             sub validator {
242             my ($path, $env) = @_;
243              
244             state $rv = Robots::Validate->new();
245              
246             unless ( $rv->validate( $env ) ) {
247             if (my $logger = $env->{'psgix.logger'}) {
248             $logger->( { level => 'warn', message => 'not a bot!' } );
249             }
250             return [ 403, [], [] ];
251             }
252              
253             ...
254             }
255              
256             =head2 cache
257              
258             This is the cache handling interface. See L<CHI>.
259              
260             If no cache is specified (v0.2.0), then the result will not be cached.
261              
262             =head2 max_age
263              
264             This is the maximum time (in seconds) to cache content. If the page
265             returns a C<Cache-Control> header with a C<max-age>, then that will be
266             used instead.
267              
268             =head2 request
269              
270             This is a hash reference (since v0.2.0) of request headers to pass
271             through the proxy. The keys are the request header fieldss, and the
272             values are the headers that will be passed to the L</rewrite> URL.
273              
274             Values of C<1> will be a synonym for the same header, and false values
275             will mean that the header is skipped.
276              
277             An array reference can be used to simply pass through a list of
278             headers unchanged.
279              
280             It will default to the following headers:
281              
282             =over
283              
284             =item C<X-Forwarded-For>
285              
286             =item C<X-Forwarded-Host>
287              
288             =item C<X-Forwarded-Port>
289              
290             =item C<X-Forwarded-Proto>
291              
292             =back
293              
294             The C<User-Agent> is forwarded as C<X-Forwarded-User-Agent>.
295              
296             =head2 response
297              
298             This is a hash reference (since v0.2.0) of request headers to return
299             from the proxy. The keys are the response header fields, and the
300             values are the headers that will be returned from the proxy.
301              
302             Values of C<1> will be a synonym for the same header, and false values
303             will mean that the header is skipped.
304              
305             An array reference can be used to simply pass through a list of
306             headers unchanged.
307              
308             It will default to the following headers:
309              
310             =over
311              
312             =item C<Content-Type>
313              
314             =item C<Expires>
315              
316             =item C<Last-Modified>
317              
318             =back
319              
320             =head2 wait
321              
322             The number of seconds to wait for new content to be loaded.
323              
324             =head1 LIMITATIONS
325              
326             This does not support cache invalidation or screenshot rendering.
327              
328             This only does the bare minimum necessary for proxying requests. You
329             may need additional middleware for reverse proxies, logging, or
330             security filtering.
331              
332             =head1 SEE ALSO
333              
334             L<Plack>
335              
336             L<WWW::Mechanize::Chrome>
337              
338             Rendertron L<https://github.com/GoogleChrome/rendertron>
339              
340             =head1 SOURCE
341              
342             The development version is on github at L<https://github.com/robrwo/perl-Plack-App-Prerender>
343             and may be cloned from L<git://github.com/robrwo/perl-Plack-App-Prerender.git>
344              
345             =head1 BUGS
346              
347             Please report any bugs or feature requests on the bugtracker website
348             L<https://github.com/robrwo/perl-Plack-App-Prerender/issues>
349              
350             When submitting a bug or request, please include a test-file or a
351             patch to an existing test-file that illustrates the bug or desired
352             feature.
353              
354             =head1 AUTHOR
355              
356             Robert Rothenberg <rrwo@cpan.org>
357              
358             =head1 COPYRIGHT AND LICENSE
359              
360             This software is Copyright (c) 2020 by Robert Rothenberg.
361              
362             This is free software, licensed under:
363              
364             The Artistic License 2.0 (GPL Compatible)
365              
366             =cut