1 # This module is part of debbugs, and is released
2 # under the terms of the GPL version 2, or any later version. See the
3 # file README and COPYING for more information.
4 # Copyright 2013 by Don Armstrong <don@donarmstrong.com>.
6 package Debbugs::Libravatar;
10 Debbugs::Libravatar -- Libravatar service handler (mod_perl)
14 <Location /libravatar>
15 SetHandler perl-script
16 PerlResponseHandler Debbugs::Libravatar
21 Debbugs::Libravatar is a libravatar service handler which will serve
22 libravatar requests. It also contains utility routines which are used
23 by the libravatar.cgi script for those who do not have mod_perl.
33 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
34 use Exporter qw(import);
36 use Debbugs::Config qw(:config);
37 use Debbugs::Common qw(:lock);
40 use Debbugs::CGI qw(cgi_parameters);
41 use Digest::MD5 qw(md5_hex);
42 use File::Temp qw(tempfile);
49 ($VERSION) = q$Revision$ =~ /^Revision:\s+([^\s+])/;
50 $DEBUG = 0 unless defined $DEBUG;
53 %EXPORT_TAGS = (libravatar => [qw(retrieve_libravatar cache_location)]
56 Exporter::export_ok_tags(keys %EXPORT_TAGS);
57 $EXPORT_TAGS{all} = [@EXPORT_OK];
65 =item retrieve_libravatar
67 $cache_location = retrieve_libravatar(location => $cache_location,
68 email => lc($param{email}),
71 Returns the cache location where a specific avatar can be loaded. If
72 there isn't a matching avatar, or there is an error, returns undef.
77 sub retrieve_libravatar{
88 my $cache_location = $param{location};
90 $cache_location =~ s/\.[^\.\/]+$//;
91 # take out a lock on the cache location so that if another request
92 # is made while we are serving this one, we don't do double work
93 my ($fh,$lockfile,$errors) =
94 simple_filelock($cache_location.'.lock',20,0.5);
98 # figure out if the cache is now valid; if it is, return the
101 ($temp_location, $timestamp) = cache_location(email => $param{email});
103 return ($temp_location,$timestamp);
106 require LWP::UserAgent;
108 my $dest_type = 'png';
110 my $uri = libravatar_url(email => $param{email},
113 my $ua = LWP::UserAgent->new(agent => 'Debbugs libravatar service (not Mozilla)',
115 $ua->from($config{maintainer});
116 # if we don't get an avatar within 10 seconds, return so we
117 # don't block forever
119 # if the avatar is bigger than 30K, we don't want it either
120 $ua->max_size(30*1024);
121 $ua->default_header('Accept' => 'image/*');
122 my $r = $ua->get($uri);
123 if (not $r->is_success()) {
124 if ($r->code != 404) {
125 die "Not successful in request";
127 # No avatar - cache a negative result
128 if ($config{libravatar_default_image} =~ m/\.(png|jpg)$/) {
131 system('cp', '-laf', $config{libravatar_default_image}, $cache_location.'.'.$dest_type) == 0
132 or die("Cannot copy $config{libravatar_default_image}");
133 # Returns from eval {}
137 my $aborted = $r->header('Client-Aborted');
138 # if we exceeded max size, I'm not sure if we'll be
139 # successfull or not, but regardless, there will be a
140 # Client-Aborted header. Stop here if that header is defined.
141 die "Client aborted header" if defined $aborted;
142 my $type = $r->header('Content-Type');
143 # if there's no content type, or it's not one we like, we won't
144 # bother going further
146 die "Wrong content type" if not $type =~ m{^image/([^/]+)$};
147 $dest_type = $type_mapping{$1};
148 die "No dest type" if not defined $dest_type;
150 # undo any content encoding
151 $r->decode() or die "Unable to decode content encoding";
152 # ok, now we need to convert it from whatever it is into a
153 # format that we actually like
154 my ($temp_fh,$temp_fn) = tempfile() or
155 die "Unable to create temporary file";
157 print {$temp_fh} $r->content() or
158 die "Unable to print to temp file";
160 die "Unable to close temp file";
161 ### Figure out the actual type from the file
162 $magic = File::LibMagic->new() if not defined $magic;
163 $type = $magic->checktype_filename(abs_path($temp_fn));
164 die "Wrong content type ($type)" if not $type =~ m{^image/([^/;]+)(?:;|$)};
165 $dest_type = $type_mapping{$1};
166 die "No dest type for ($1)" if not defined $dest_type;
167 ### resize all images to 80x80 and strip comments out of
168 ### them. If convert has a bug, it would be possible for
169 ### this to be an attack vector, but hopefully minimizing
170 ### the size above, and requiring proper mime types will
171 ### minimize that slightly. Doing this will at least make
172 ### it harder for malicious web images to harm our users
173 system('convert','-resize','80x80',
176 $cache_location.'.'.$dest_type) == 0 or
177 die "convert file failed";
181 unlink($cache_location.'.'.$dest_type) if -e $cache_location.'.'.$dest_type;
182 unlink($temp_fn) if -e $temp_fn;
183 die "Unable to convert image";
187 # there was some kind of error; return undef and unlock the
189 simple_unlockfile($fh,$lockfile);
192 simple_unlockfile($fh,$lockfile);
193 $timestamp = (stat($cache_location.'.'.$dest_type))[9];
194 return ($cache_location.'.'.$dest_type,$timestamp);
197 sub blocked_libravatar {
198 my ($email,$md5sum) = @_;
200 for my $blocker (@{$config{libravatar_blacklist}||[]}) {
201 for my $element ($email,$md5sum) {
202 next unless defined $element;
204 if ($element =~ /$blocker/) {
213 # Returns ($path, $timestamp)
214 # - For blocked images, $path will be undef
215 # - If $timestamp is 0 (and $path is not undef), the image should
220 if (exists $param{md5sum}) {
221 $md5sum = $param{md5sum};
222 }elsif (exists $param{email}) {
223 $md5sum = md5_hex(lc($param{email}));
225 croak("cache_location must be called with one of md5sum or email");
227 return (undef, 0) if blocked_libravatar($param{email},$md5sum);
228 my $cache_dir = $param{cache_dir} // $config{libravatar_cache_dir};
229 $stem = $cache_dir.'/'.$md5sum;
230 for my $ext ('.png', '.jpg', '') {
231 my $path = $stem.$ext;
233 my $timestamp = (time - (stat(_))[9] < 60*60) ? (stat(_))[9] : 0;
234 return ($path, $timestamp);
240 ## the following is mod_perl specific
243 if (exists $ENV{MOD_PERL_API_VERSION}) {
244 if ($ENV{MOD_PERL_API_VERSION} == 2) {
245 require Apache2::RequestIO;
246 require Apache2::RequestRec;
247 require Apache2::RequestUtil;
248 require Apache2::Const;
251 APR::Const->import(-compile => qw(FINFO_NORM));
252 Apache2::Const->import(-compile => qw(OK DECLINED FORBIDDEN NOT_FOUND HTTP_NOT_MODIFIED));
254 die "Unsupported mod perl api; mod_perl 2.0.0 or later is required";
260 die "Calling handler only makes sense if this is running under mod_perl" unless exists $ENV{MOD_PERL_API_VERSION};
261 my $r = shift or Apache2::RequestUtil->request;
263 # we only want GET or HEAD requests
264 unless ($r->method eq 'HEAD' or $r->method eq 'GET') {
265 return Apache2::Const::DECLINED();
267 $r->headers_out->{"X-Powered-By"} = "Debbugs libravatar";
270 # subtract out location
271 my $location = $r->location();
272 my ($email) = $uri =~ m/\Q$location\E\/?(.*)$/;
273 if (not length $email) {
274 return Apache2::Const::NOT_FOUND();
276 my $q = CGI::Simple->new();
277 my %param = cgi_parameters(query => $q,
278 single => [qw(avatar)],
279 default => {avatar => 'yes',
282 if ($param{avatar} ne 'yes' or not defined $email or not length $email) {
283 serve_cache_mod_perl('',$r);
284 return Apache2::Const::DECLINED();
286 # figure out what the md5sum of the e-mail is.
287 my ($cache_location, $timestamp) = cache_location(email => $email);
288 # if we've got it, and it's less than one hour old, return it.
290 serve_cache_mod_perl($cache_location,$r);
291 return Apache2::Const::DECLINED();
293 ($cache_location,$timestamp) =
294 retrieve_libravatar(location => $cache_location,
297 if (not defined $cache_location) {
298 # failure, serve the default image
299 serve_cache_mod_perl('',$r,$timestamp);
300 return Apache2::Const::DECLINED();
302 serve_cache_mod_perl($cache_location,$r,$timestamp);
303 return Apache2::Const::DECLINED();
309 sub serve_cache_mod_perl {
310 my ($cache_location,$r,$timestamp) = @_;
311 if (not defined $cache_location or not length $cache_location) {
312 # serve the default image
313 $cache_location = $config{libravatar_default_image};
315 $magic = File::LibMagic->new() if not defined $magic;
317 return Apache2::Const::DECLINED() if not defined $magic;
319 $r->content_type($magic->checktype_filename(abs_path($cache_location)));
321 $r->filename($cache_location);
323 $r->finfo(APR::Finfo::stat($cache_location, APR::Const::FINFO_NORM(), $r->pool));