File Coverage

blib/lib/Net/OpenID/URIFetch.pm
Criterion Covered Total %
statement 47 102 46.0
branch 0 22 0.0
condition 0 11 0.0
subroutine 15 20 75.0
pod 0 1 0.0
total 62 156 39.7


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             =head1 NAME
4              
5             Net::OpenID::URIFetch - fetch and cache content from HTTP URLs
6              
7             =head1 VERSION
8              
9             version 1.20
10              
11             =head1 DESCRIPTION
12              
13             This is roughly based on Ben Trott's URI::Fetch module, but
14             URI::Fetch doesn't cache enough headers that Yadis can be implemented
15             with it, so this is a lame copy altered to allow Yadis support.
16              
17             Hopefully one day URI::Fetch can be modified to do what we need and
18             this can go away.
19              
20             This module is tailored to the needs of Net::OpenID::Consumer and probably
21             isn't much use outside of it. See URI::Fetch for a more general module.
22              
23             =cut
24              
25             package Net::OpenID::URIFetch;
26             $Net::OpenID::URIFetch::VERSION = '1.20';
27 2     2   14474 use HTTP::Request;
  2         32098  
  2         44  
28 2     2   737 use HTTP::Status;
  2         5339  
  2         366  
29 2     2   10 use strict;
  2         1  
  2         33  
30 2     2   6 use warnings;
  2         1  
  2         31  
31 2     2   6 use Carp();
  2         2  
  2         33  
32              
33 2     2   5 use constant URI_OK => 200;
  2         2  
  2         124  
34 2     2   6 use constant URI_MOVED_PERMANENTLY => 301;
  2         2  
  2         64  
35 2     2   5 use constant URI_NOT_MODIFIED => 304;
  2         2  
  2         79  
36 2     2   6 use constant URI_GONE => 410;
  2         2  
  2         791  
37              
38             # Fetch a document, either from cache or from a server
39             # URI -- location of document
40             # CONSUMER -- where to find user-agent and cache
41             # CONTENT_HOOK -- applied to freshly-retrieved document
42             # to normalize it into some particular format/structure
43             # PREFIX -- used as part of the cache key, distinguishes
44             # different content formats and must change whenever
45             # CONTENT_HOOK is switched to a new format; this way,
46             # cache entries from a previous run of this server that
47             # are using a different content format will not kill us.
48             sub fetch {
49 0     0 0   my ($class, $uri, $consumer, $content_hook, $prefix) = @_;
50 0   0       $prefix ||= '';
51              
52 0 0         if ($uri eq 'x-xrds-location') {
53 0           Carp::confess("Buh?");
54             }
55              
56 0           my $ua = $consumer->ua;
57 0           my $cache = $consumer->cache;
58 0           my $ref;
59              
60 0           my $cache_key = "URIFetch:${prefix}:${uri}";
61              
62 0 0         if ($cache) {
63 0 0         if (my $blob = $cache->get($cache_key)) {
64 0           $ref = Storable::thaw($blob);
65             }
66             }
67             my $cached_response = sub {
68             return Net::OpenID::URIFetch::Response->new(
69             status => 200,
70             content => $ref->{Content},
71             last_modified => $ref->{LastModified},
72             headers => $ref->{Headers},
73             final_uri => $ref->{FinalURI},
74 0     0     );
75 0           };
76              
77             # We just serve anything from the last 60 seconds right out of the cache,
78             # thus avoiding doing several requests to the same URL when we do
79             # Yadis, then HTML discovery.
80             # TODO: Make this tunable?
81 0 0 0       if ($ref && $ref->{CacheTime} > (time() - 60)) {
82 0           $consumer->_debug("Cache HIT for $uri");
83 0           return $cached_response->();
84             }
85             else {
86 0           $consumer->_debug("Cache MISS for $uri");
87             }
88              
89 0           my $req = HTTP::Request->new(GET => $uri);
90 0           $req->header('Accept-Encoding', scalar HTTP::Message::decodable());
91 0 0         if ($ref) {
92 0 0         if (my $etag = ($ref->{Headers}->{etag})) {
93 0           $req->header('If-None-Match', $etag);
94             }
95 0 0         if (my $ts = $ref->{LastModified}) {
96 0           $req->if_modified_since($ts);
97             }
98             }
99              
100 0           my $res = $ua->request($req);
101              
102             # There are only a few headers that OpenID/Yadis care about
103 0           my @useful_headers = qw(last-modified etag content-type x-yadis-location x-xrds-location);
104              
105 0           my %response_fields;
106              
107 0 0         if ($res->code == HTTP::Status::RC_NOT_MODIFIED()) {
108 0           $consumer->_debug("Server says it's not modified. Serving from cache.");
109 0           return $cached_response->();
110             }
111             else {
112 0           my $final_uri = $res->request->uri->as_string();
113 0           my $final_cache_key = "URIFetch:${prefix}:${final_uri}";
114              
115 0   0       my $content = $res->decoded_content # Decode content-encoding and charset
116             || $res->decoded_content(charset => 'none') # Decode content-encoding
117             || $res->content; # Undecoded content
118              
119 0 0         if ($content_hook) {
120 0           $content_hook->(\$content);
121             }
122              
123 0           my $headers = {};
124 0           foreach my $k (@useful_headers) {
125 0           $headers->{$k} = $res->header($k);
126             }
127              
128 0           my $ret = Net::OpenID::URIFetch::Response->new(
129             status => $res->code,
130             last_modified => $res->last_modified,
131             content => $content,
132             headers => $headers,
133             final_uri => $final_uri,
134             );
135              
136 0 0 0       if ($cache && $res->code == 200) {
137 0           my $cache_data = {
138             LastModified => $ret->last_modified,
139             Headers => $ret->headers,
140             Content => $ret->content,
141             CacheTime => time(),
142             FinalURI => $final_uri,
143             };
144 0           my $cache_blob = Storable::freeze($cache_data);
145 0           $cache->set($final_cache_key, $cache_blob);
146 0           $cache->set($cache_key, $cache_blob);
147             }
148              
149 0           return $ret;
150             }
151              
152             }
153              
154             package Net::OpenID::URIFetch::Response;
155             $Net::OpenID::URIFetch::Response::VERSION = '1.20';
156 2     2   13 use strict;
  2         2  
  2         36  
157 2     2   6 use constant FIELDS => [qw(final_uri status content headers last_modified)];
  2         0  
  2         90  
158 2     2   951 use fields @{FIELDS()};
  2         1968  
  2         3  
  2         8  
159 2     2   123 use Carp();
  2         2  
  2         170  
160              
161             sub new {
162 0     0     my ($class, %opts) = @_;
163 0           my $self = fields::new($class);
164 0           @{$self}{@{FIELDS()}} = delete @opts{@{FIELDS()}};
  0            
  0            
  0            
165 0 0         Carp::croak("Unknown option(s): " . join(", ", keys %opts)) if %opts;
166 0           return $self;
167             }
168              
169             BEGIN {
170 2     2   2 foreach my $field_name (@{FIELDS()}) {
  2         5  
171 2     2   6 no strict 'refs';
  2         1  
  2         101  
172 10         112 *{__PACKAGE__ . '::' . $field_name}
173 10     0   26 = sub { return $_[0]->{$field_name}; };
  0            
174             }
175             }
176              
177             sub header {
178 0     0     return $_[0]->{headers}{lc($_[1])};
179             }
180              
181             1;