The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

=head1 NAME

Net::OpenID::URIFetch - fetch and cache content from HTTP URLs

=head1 DESCRIPTION

This is roughly based on Ben Trott's URI::Fetch module, but
URI::Fetch doesn't cache enough headers that Yadis can be implemented
with it, so this is a lame copy altered to allow Yadis support.

Hopefully one day URI::Fetch can be modified to do what we need and
this can go away.

This module is tailored to the needs of Net::OpenID::Consumer and probably
isn't much use outside of it. See URI::Fetch for a more general module.

=cut

package Net::OpenID::URIFetch;

use HTTP::Request;
use HTTP::Status;
use strict;
use warnings;
use Carp;

our $HAS_ZLIB;
BEGIN {
    $HAS_ZLIB = eval "use Compress::Zlib (); 1;";
}

use constant URI_OK                => 200;
use constant URI_MOVED_PERMANENTLY => 301;
use constant URI_NOT_MODIFIED      => 304;
use constant URI_GONE              => 410;

sub fetch {
    my ($class, $uri, $consumer, $content_hook) = @_;

    if ($uri eq 'x-xrds-location') {
        Carp::confess("Buh?");
    }

    my $ua = $consumer->ua;
    my $cache = $consumer->cache;
    my $ref;

    # By prefixing the cache key, we can ensure we won't
    # get left-over cache items from older versions of Consumer
    # that used URI::Fetch.
    my $cache_key = 'URIFetch:'.$uri;

    if ($cache) {
        if (my $blob = $cache->get($cache_key)) {
            $ref = Storable::thaw($blob);
        }
    }

    # We just serve anything from the last 60 seconds right out of the cache,
    # thus avoiding doing several requests to the same URL when we do
    # Yadis, then HTML discovery.
    # TODO: Make this tunable?
    if ($ref && $ref->{CacheTime} > (time() - 60)) {
        $consumer->_debug("Cache HIT for $uri");
        return Net::OpenID::URIFetch::Response->new(
            status => 200,
            content => $ref->{Content},
            headers => $ref->{Headers},
            final_uri => $ref->{FinalURI},
        );
    }
    else {
        $consumer->_debug("Cache MISS for $uri");
    }

    my $req = HTTP::Request->new(GET => $uri);
    if ($HAS_ZLIB) {
        $req->header('Accept-Encoding', 'gzip');
    }
    if ($ref) {
        if (my $etag = ($ref->{Headers}->{etag})) {
            $req->header('If-None-Match', $etag);
        }
        if (my $ts = ($ref->{Headers}->{'last-modified'})) {
            $req->if_modified_since($ts);
        }
    }

    my $res = $ua->request($req);

    # There are only a few headers that OpenID/Yadis care about
    my @useful_headers = qw(last-modified etag content-type x-yadis-location x-xrds-location);

    my %response_fields;

    if ($res->code == HTTP::Status::RC_NOT_MODIFIED()) {
        $consumer->_debug("Server says it's not modified. Serving from cache.");
        return Net::OpenID::URIFetch::Response->new(
            status => 200,
            content => $ref->{Content},
            headers => $ref->{Headers},
            final_uri => $ref->{FinalURI},
        );
    }
    else {
        my $content = $res->content;
        my $final_uri = $res->request->uri->as_string();
        my $final_cache_key = "URIFetch:".$final_uri;

        if ($res->content_encoding && $res->content_encoding eq 'gzip') {
            $content = Compress::Zlib::memGunzip($content);
        }

        if ($content_hook) {
            $content_hook->(\$content);
        }

        my $headers = {};
        foreach my $k (@useful_headers) {
            $headers->{$k} = $res->header($k);
        }

        my $ret = Net::OpenID::URIFetch::Response->new(
            status => $res->code,
            content => $content,
            headers => $headers,
            final_uri => $final_uri,
        );

        if ($cache && $res->code == 200) {
            my $cache_data = {
                Headers => $ret->headers,
                Content => $ret->content,
                CacheTime => time(),
                FinalURI => $final_uri,
            };
            my $cache_blob = Storable::freeze($cache_data);
            $cache->set($final_cache_key, $cache_blob);
            $cache->set($cache_key, $cache_blob);
        }

        return $ret;
    }

}

package Net::OpenID::URIFetch::Response;

sub new {
    my ($class, %opts) = @_;

    my $self = {};
    $self->{final_uri} = delete($opts{final_uri});
    $self->{status} = delete($opts{status});
    $self->{content} = delete($opts{content});
    $self->{headers} = delete($opts{headers});

    return bless $self, $class;
}

sub final_uri {
    return $_[0]->{final_uri};
}

sub status {
    return $_[0]->{status};
}

sub content {
    return $_[0]->{content};
}

sub headers {
    return $_[0]->{headers};
}

sub header {
    return $_[0]->{headers}{lc($_[1])};
}

1;