File Coverage

blib/lib/MojoX/UserAgent.pm
Criterion Covered Total %
statement 14 15 93.3
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 19 20 95.0


line stmt bran cond sub pod time code
1             # Copyright (C) 2009, Pascal Gaudette.
2              
3             package MojoX::UserAgent;
4              
5 4     4   73476 use warnings;
  4         9  
  4         180  
6 4     4   23 use strict;
  4         5  
  4         145  
7              
8 4     4   20 use base 'Mojo::Base';
  4         7  
  4         2939  
9              
10 4     4   35574 use Carp 'croak';
  4         10  
  4         281  
11              
12 4     4   3645 use Mojo 0.991250;
  4         1455723  
  0            
13              
14             use Mojo::URL;
15             use Mojo::Transaction::Pipeline;
16             use Mojo::Client;
17             use Mojo::Cookie;
18             use MojoX::UserAgent::Transaction;
19             use MojoX::UserAgent::CookieJar;
20              
21             our $VERSION = '0.21';
22              
23             __PACKAGE__->attr('allow_post_redirect', 1);
24              
25             __PACKAGE__->attr(
26             'agent' => "Mozilla/5.0 (compatible; MojoX::UserAgent/$VERSION)");
27              
28             __PACKAGE__->attr('app');
29              
30             __PACKAGE__->attr('cookie_jar' => sub { MojoX::UserAgent::CookieJar->new });
31              
32             __PACKAGE__->attr(
33             'default_done_cb' => sub {
34             return sub {
35             my ($self, $tx) = @_;
36             my $url = $tx->hops ? $tx->original_req->url : $tx->req->url;
37             print "$url done in " . $tx->hops . " hops.\n";
38             };
39             }
40             );
41              
42             __PACKAGE__->attr('default_headers');
43              
44             __PACKAGE__->attr('follow_redirects' => 1);
45              
46             # pipeline_method: 'none' / 'horizontal' / 'vertical'
47             __PACKAGE__->attr('pipeline_method' => 'none');
48              
49             __PACKAGE__->attr('redirect_limit' => 10);
50              
51             __PACKAGE__->attr('validate_cookie_paths' => 0);
52              
53              
54             __PACKAGE__->attr('_count' => 0);
55              
56             __PACKAGE__->attr('_client' => sub { Mojo::Client->new });
57              
58             __PACKAGE__->attr('_maxconnections' => 3);
59             __PACKAGE__->attr('_maxpipereqs' => 4);
60              
61             __PACKAGE__->attr('_active' => sub { {} });
62             __PACKAGE__->attr('_ondeck' => sub { {} });
63              
64              
65             # Subroutine declarations
66             sub _add_pipe_no;
67             sub _add_pipe_h;
68             sub _add_pipe_v;
69              
70             __PACKAGE__->attr(
71             '_add_methods' => sub {
72             { 'none' => \&_add_pipe_no,
73             'horizontal' => \&_add_pipe_h,
74             'vertical' => \&_add_pipe_v,
75             };
76             }
77             );
78              
79             # Subroutine declarations
80             sub _find_finished_nopipe;
81             sub _find_finished_pipe;
82              
83             __PACKAGE__->attr(
84             '_find_finished' => sub {
85             { 'none' => \&_find_finished_nopipe,
86             'horizontal' => \&_find_finished_pipe,
87             'vertical' => \&_find_finished_pipe
88             };
89             }
90             );
91              
92             sub new {
93             my $self = shift->SUPER::new();
94             $self->_client->keep_alive_timeout(30);
95             return $self;
96             }
97              
98             sub cookies_for_url {
99             my $self = shift;
100              
101             my $resp_cookies = $self->cookie_jar->cookies_for_url(@_);
102              
103             return [] unless @{$resp_cookies};
104              
105             # now make request cookies
106             my @req_cookies = ();
107             for my $rc (@{$resp_cookies}) {
108             my $cookie = Mojo::Cookie::Request->new;
109             $cookie->name($rc->name);
110             $cookie->value($rc->value);
111             $cookie->path($rc->path);
112             $cookie->version($rc->version) if defined $rc->version;
113              
114             push @req_cookies, $cookie;
115             }
116              
117             return [@req_cookies];
118             }
119              
120             sub crank_all {
121             my $self = shift;
122              
123             my $active_count = 0;
124             for my $id (keys %{$self->_active}) {
125             $active_count += $self->crank_dest($id);
126             }
127             return $active_count;
128             }
129              
130             sub crank_dest {
131             my $self = shift;
132             my $dest = shift;
133              
134             # Update the active queue
135             my $active = $self->_update_active($dest);
136              
137             return 0 unless (@{$active}); # nothing currently active for this host:port
138              
139             $self->app ? $self->_spin_app($active) : $self->_spin($active);
140              
141             my @still_active;
142             my @finished;
143              
144             # Use appropriate sub to find finished txs based on pipelining method
145             my $ff_sub = $self->_find_finished->{$self->pipeline_method};
146             $self->$ff_sub($active, \@still_active, \@finished);
147              
148             for my $tx (@finished) {
149              
150             # TODO: need to check for tx errors here!
151             $self->{_count}++;
152              
153             # Check for cookies
154             $self->_extract_cookies($tx);
155              
156             # Check for redirect
157             my $redirect = 0;
158             my $method;
159             my $location;
160              
161             if ( $tx->res->is_status_class(300)
162             && $self->follow_redirects
163             && $tx->hops < $self->redirect_limit
164             && ($location = $tx->res->headers->header('Location')))
165             {
166              
167             # Presumably 304 (not modified) shouldn't include
168             # a Location so shouldn't come in here...
169             my $code = $tx->res->code;
170              
171             if ($code == 301 || $code == 302 || $code == 307) {
172             if ($tx->req->method eq 'GET' || $tx->req->method eq 'HEAD') {
173             $redirect = 1;
174             $method = $tx->req->method;
175             }
176             elsif ($self->allow_post_redirect) {
177              
178             # This setting allows automated POST redirection to a GET
179             # for a different resource. This goes against the
180             # current HTTP/1.1 specification, but appears to be
181             # most browsers' default behavior...
182             $redirect = 1;
183             $method = 'GET';
184             }
185             }
186             elsif ($code == 303) {
187             $redirect = 1;
188             $method = 'GET';
189             }
190             elsif ($code == 305) {
191              
192             # Set up a proxied request (TODO)
193             $tx->error('Proxy support not yet implemented');
194             }
195             else {
196              
197             # unknown 3xx response... what to do?
198             $tx->error('Unknown 3xx response');
199             }
200              
201             }
202              
203              
204             if ($redirect) {
205              
206             my $newurl = Mojo::URL->new();
207             $newurl->parse($location);
208             my $oldurl = $tx->req->url;
209              
210             # Deal with relative redirection
211             $newurl->scheme($oldurl->scheme) unless $newurl->is_abs;
212             $newurl->authority($oldurl->authority) unless $newurl->is_abs;
213              
214             unless ($newurl->path =~ m{^/}) {
215             $newurl->path($oldurl->path->append($newurl->path));
216             }
217              
218             my $new_tx = MojoX::UserAgent::Transaction->new(
219             { url => $newurl,
220             method => $method,
221             id => $tx->id,
222             hops => $tx->hops + 1,
223             callback => $tx->done_cb,
224             ua => $self,
225             original_req => (
226             $tx->original_req
227             ? $tx->original_req
228             : $tx->req
229             )
230             }
231             );
232             $self->spool($new_tx);
233              
234             }
235             else {
236              
237             # Invoke Callback
238             $tx->done_cb->($self, $tx);
239             }
240             }
241              
242             # Put those not finished back into the active array for this host:port
243             push @{$active}, @still_active;
244              
245             return scalar @{$active};
246             }
247              
248             sub get {
249             my $self = shift;
250             my $url = shift;
251             my $cb = shift || $self->default_done_cb;
252              
253             my $tx = MojoX::UserAgent::Transaction->new(
254             { url => $url,
255             callback => $cb,
256             ua => $self
257             }
258             );
259             $self->spool($tx);
260             1;
261             }
262              
263             sub is_idle {
264             my $self = shift;
265              
266             return (!(scalar keys %{$self->_active})
267             && !(scalar keys %{$self->_ondeck}));
268             }
269              
270             sub maxconnections {
271             my $self = shift;
272             my $value = shift;
273              
274             return $self->_maxconnections unless $value;
275              
276             $self->is_idle
277             ? return $self->_maxconnections($value)
278             : return $self->_maxconnections;
279             }
280              
281             sub maxpipereqs {
282             my $self = shift;
283             my $value = shift;
284              
285             return $self->_maxpipereqs unless $value;
286              
287             $self->is_idle
288             ? return $self->_maxpipereqs($value)
289             : return $self->_maxpipereqs;
290             }
291              
292             sub post {
293             my $self = shift;
294             my $url = shift;
295             my $cb = shift || $self->default_done_cb;
296              
297             my $tx = MojoX::UserAgent::Transaction->new(
298             { method => 'POST',
299             url => $url,
300             callback => $cb,
301             ua => $self
302             }
303             );
304             $self->spool($tx);
305             1;
306             }
307              
308             sub run_all {
309             my $self = shift;
310              
311             while (1) {
312             $self->crank_all;
313             last if $self->is_idle;
314             }
315             }
316              
317             sub spool {
318             my $self = shift;
319             my $new_transactions = (ref $_[0] eq 'ARRAY') ? shift : [@_];
320              
321             for my $tx (@{$new_transactions}) {
322             my ($scheme, $host, $port) = $tx->client_info;
323              
324             my $id = "$host:$port";
325             if (my $ondeck = $self->_ondeck->{$id}) {
326             push @{$ondeck}, $tx;
327             }
328             else {
329             $self->_ondeck->{$id} = [$tx];
330             $self->_active->{$id} = [];
331             }
332             }
333             1;
334             }
335              
336             sub _extract_cookies {
337             my ($self, $tx) = @_;
338              
339             my $cookies = $tx->res->cookies;
340              
341             if (@{$cookies}) {
342             my $cleared = $self->_scrub_cookies($tx, $cookies);
343             $self->cookie_jar->store($cleared) if @{$cleared};
344             }
345              
346              
347             1;
348             }
349              
350             sub _find_finished_pipe {
351             my ($self, $active, $still_active, $finished) = @_;
352              
353             while (my $tx = shift @{$active}) {
354             if ($tx->is_finished) {
355              
356             # if it's a pipeline, we must unpack
357             if (ref $tx eq 'Mojo::Transaction::Pipeline') {
358             while (my $inner = shift @{$tx->finished}) {
359             push @{$finished}, $inner;
360             }
361              
362             # We must also unpack from the other two queues...
363             while (my $inner =
364             (shift @{$tx->inactive} || shift @{$tx->active}))
365             {
366             unless ($inner->has_error) {
367             $tx->has_error
368             ? $inner->error($tx->error)
369             : $inner->error('Something weird happened');
370             }
371             push @{$finished}, $inner;
372             }
373              
374             }
375             else {
376             push @{$finished}, $tx;
377             }
378             }
379             else {
380             # if it's a pipeline, look for finished single transactions within
381             if (ref $tx eq 'Mojo::Transaction::Pipeline') {
382             while (my $inner = shift @{$tx->finished}) {
383             push @{$finished}, $inner;
384             }
385             }
386              
387             push @{$still_active}, $tx;
388             }
389             }
390              
391             }
392              
393             sub _find_finished_nopipe {
394             my ($self, $active, $still_active, $finished) = @_;
395              
396             while (my $tx = shift @{$active}) {
397             $tx->is_finished
398             ? push @{$finished}, $tx
399             : push @{$still_active}, $tx;
400             }
401              
402             }
403              
404             sub _add_pipe_h_or_v() {
405             my ($self, $h_or_v, $slots, $ondeck, $active) = @_;
406              
407             my $queue_max = $slots * $self->maxpipereqs;
408              
409             my @stage;
410             my $i=0;
411             my $j=0;
412             my $queued=0;
413              
414             while ($queued < $queue_max && @{$ondeck}) {
415              
416             $stage[$i] = [] unless $stage[$i];
417              
418             $stage[$i]->[$j] = shift @{$ondeck};
419             $queued++;
420              
421             if ($h_or_v) {
422              
423             # Vertical
424             $j++;
425             if ($j == $self->maxpipereqs) {
426             $j = 0;
427             $i++;
428             }
429             }
430             else {
431              
432             # Horizontal
433             $i++;
434             if ($i == $slots) {
435             $i = 0;
436             $j++;
437             }
438             }
439             }
440              
441             foreach my $slot (@stage) {
442             if (scalar @{$slot} == 1) {
443             push @{$active}, $slot->[0];
444             }
445             else {
446             my $pipe = Mojo::Transaction::Pipeline->new(@{$slot});
447             push @{$active}, $pipe;
448             }
449             }
450             }
451              
452             sub _add_pipe_h {
453             my $self= shift;
454              
455             $self->_add_pipe_h_or_v(0, @_);
456              
457             }
458              
459             sub _add_pipe_no {
460             my ($self, $slots, $ondeck, $active) = @_;
461              
462             my $i=0;
463             while ($i<$slots && @{$ondeck}) {
464             push @{$active}, (shift @{$ondeck});
465             $i++;
466             }
467             }
468              
469             sub _add_pipe_v() {
470             my $self= shift;
471              
472             $self->_add_pipe_h_or_v(1, @_);
473              
474             }
475              
476             sub _scrub_cookies {
477             my $self = shift;
478             my $tx = shift;
479             my $cookies = shift;
480              
481             my @cleared = ();
482              
483             for my $cookie (@{$cookies}) {
484              
485             # Domain check
486             if ($cookie->domain) {
487              
488             my $domain = $cookie->domain;
489             my $host = $tx->req->url->host;
490              
491             # strip any leading dot
492             $cookie->domain($domain) if ($domain =~ s/^\.//);
493              
494             unless ( $domain =~ m{[\w\-]+\.[\w\-]+$}x
495             && ($host =~ s/\.$domain$//x || $host =~ s/^$domain$//x)
496             && $host !~ m{\.})
497             {
498              
499             # Note that in theory we should add to this a refusal if
500             # the domain matches one of these:
501             # http://publicsuffix.org/list/
502             next;
503             }
504             }
505             else {
506             $cookie->domain($tx->req->url->host);
507             }
508              
509             # Port check
510             if ($cookie->port) {
511              
512             # Should be comma separated list of numbers
513             next unless $cookie->port =~ m/^[\d\,]+$/;
514             }
515              
516             # Clean max-age
517             if ($cookie->max_age) {
518              
519             # Integer number - only digits
520             next unless $cookie->max_age =~ m/^\d+$/;
521             }
522              
523             # Path check
524             if ($cookie->path) {
525              
526             # Should be a prefix of the request URI
527             if ($self->validate_cookie_paths) {
528             my $cpath = $cookie->path;
529             next unless ($tx->req->url->path =~ m/^$cpath/);
530             }
531             }
532             else {
533             $cookie->path($tx->req->url->path);
534             }
535              
536             push @cleared, $cookie;
537             }
538             return \@cleared;
539             }
540              
541             sub _spin {
542             my $self = shift;
543             my $txs = shift;
544              
545             $self->_client->spin(@{$txs});
546             }
547             sub _spin_app {
548             my $self = shift;
549             my $txs = shift;
550              
551             #can only spin one so pick at random
552             my $tx = $txs->[int(rand(scalar @{$txs}))];
553             $self->_client->spin_app($self->{app}, $tx);
554             }
555              
556             sub _update_active {
557             my $self = shift;
558             my $dest = shift;
559              
560             my $ondeck = $self->_ondeck->{$dest};
561             my $active = $self->_active->{$dest};
562              
563             my $on_count = scalar @{$ondeck};
564             my $act_count = scalar @{$active};
565              
566             if (!$act_count && !$on_count) {
567             # nothing active or ondeck for this host/port: delete hash entries
568             delete $self->_ondeck->{$dest};
569             delete $self->_active->{$dest};
570             return [];
571             }
572              
573             if (@{$ondeck} && $act_count < $self->maxconnections) {
574              
575             # Use appropriate method to add to the active queue
576             my $slots = $self->maxconnections - $act_count;
577             my $add_sub = $self->_add_methods->{$self->pipeline_method};
578             $self->$add_sub($slots, $ondeck, $active);
579             }
580              
581             return $active;
582             }
583              
584              
585             1;
586             __END__