tags:

views:

170

answers:

7

I found some posts related here but, nothing right on.. I need to serve up an image (humm) "correctly" and using as little resources as possible. I was working on a sub (below) but, is not too resource friendly just by the fact I use CGI. That is just my assumption though. I am a Perl newbie but, I like it better than php.

The query would be generated by "somescript.pl?img=image.png"

#!/usr/bin/perl -Tw
use strict;
use warnings;
use CGI;

#I should drop warnings after all is said and done. Also name my vars generically. Right?
#I dont know if this query method will work or is even the best method.
$query = new CGI;
my @img = $query->param;
if ( $_ eq "img" ) { my $file = $query->param($_); }
if ( $_ ne "img" ) {    ## I will send to an error sub that serves up a error image
}

# Prob a one liner to take care of the above. Not within my ability though.
# Still figuring all this out here.. Very verbose sorry...
# I will strip everything but lowercase alpha and the "."
# with s =~ /[something like A-Z] I will look it up //g;
# Still.. prob all above will fit in a one liner by a PERL guru!

# Below is related to -Taint, I was told this is important to use the -T.
$ENV{PATH} = "bin:/usr/bin";
delete( $ENV{qw(IFS CDPATH BASH_ENV ENV)} );

# now I will grab the images extension.
my $ext = ( $file =~ m/[^.]+$/ )[0];

#I was informed to use the "three" but, I am unsure what that means.
# My attempt based on my reading many posts here.

my $length = ( stat($file) )[10];
my $image  = do {
    local $/ = undef;
    print "Content-type: image/$ext\n";
    print "Content-length: $length \n\n";
    binmode STDOUT;
    open( FH, "<", $file ) || die "Could not find $file: $!";
    my $buffer = "";
    while ( read( FH, $buffer, 10240 ) ) {
        print $buffer;
    }
    close(FH);
};

As you can see, my attempt here is obviously a beginners.

I have found great advice here in stack overflow. I thank everyone past and present.

+4  A: 

Have a look at the way it is done in Apachegallery

http://search.cpan.org/~legart/Apache-Gallery-1.0RC3/lib/Apache/Gallery.pm

It's using Imlib2 and it is quite efficient, including advanced features as resizing and rotate on the fly and using a shared disk cache.

kriss
+1  A: 

I think you're missing something here:

my @img = $query->param;
if ( $_ eq "img" ) { my $file = $query->param($_); }
if ( $_ ne "img" ) {    ## error }

$_ is uninitialized. I think you wanted to say:

my @img = $query->param;
foreach (@img) {
  if ( $_ eq "img" ) { my $file = $query->param($_); }
  if ( $_ ne "img" ) {    ##  error }
}

or for better readability and maintainability

my @img = $query->param;
foreach my $param (@img) {
  if ( $param eq "img" ) { my $file = $query->param($param); }
  if ( $param ne "img" ) {    ## error }
}


For another thing, you probably want to use

( stat($file) )[7];

and not

( stat($file) )[10];

to get the length of a file. (stat $file)[10] will give you the file's change time.

mobrule
How about `my $file = $query->param("img"); if (grep $_ ne "img", $query->param) { unexpected param error }` ? Looping over a collection just to pull one known value out is a bit of an anti-pattern :)
hobbs
Thanks guys! I dont know how I lost the foreach, it is in my original code. Let me go over all these responses here. No-one commented on my use of taint yet. I guess it is ok as it sits.. stat is something I will do more reading on. Thanks again for pointing that out!
Jim_Bo
@hobbs nice one... Thanks!
Jim_Bo
+5  A: 
  1. If you're going to use extension as a substitute for MIME-type, then you'd better name all your JPEG images .jpeg and not .jpg! File::MMagic or File::MimeInfo would make better general-purpose solutions.
  2. (stat $file)[10] isn't the content-length, it's the ctime, which is worthless to you. (stat $file)[7] works, but -s $file works just as well and it's obvious to any Perl programmer what it does without having to consult the stat manual. (2a: use -s on the filehandle once you open it instead of the filename to avoid racing against file replacement.)
  3. I can access any file on the filesystem that's readable by the user the CGI runs as, e.g. image.pl?image=../../../../../../../etc/passwd. I'd suggest specifically mentioning the images directory so that you're not dependent on the getcwd, and using File::Spec->no_upwards and File::Spec->catfile to build a pathname that can only be inside the images directory.
  4. It's not really good form for a CGI to die if it's avoidable. If the file isn't found, return a 404 status. If the request is illegal, return a 400 or 403 status, etc.
  5. Your URLs would be nicer if you used path_info to allow image.pl/foo.png instead of image.pl?img=foo.png.
  6. Unless you add some more logic, the images you serve up won't be cached by the client.
  7. Man, these are stacking up. Have you considered finding some code that's already written for the purpose instead of writing your own?
hobbs
I have jpe?g covered in my htaccess but not here, excellent catch. I forgot to put the if ($ext eq "jpg") {$ext = "jpeg"} part it the code I posted here.
Jim_Bo
Yeah I blew it on the [10] my bad. Htaccess is how the script is being called. The image tag is normal so, my friend can show off his images on sites that don't allow query strings in the tags.
Jim_Bo
But, I set up my htaccess to hit the script and serve up the image when there is a condition existent in the images path. A reversed engineered hotlinking rule basically. The fact that I don't even need a query string is true. THANK YOU I should do that but, I was stuck on how I would grab that url so I can split on the /'s if needed. path_info may be exactly the clue I need to solve that. I will look into File::Spec big time.. I wanted to avoid modules but, to no avail.
Jim_Bo
I tried to find a snippet to do this but, all had no security and were just very basic. I like to build my own so I can learn. I won't even use a snippet I don't understand. Thanks for your informative response. Further guidance is always appreciated!
Jim_Bo
Yes, die will not be there either. I should have not posted that here. I actually want to display an alternate image if file not found.
Jim_Bo
@Jim_Bo, don't avoid modules. Modules are your friends. Especially the core ones, like File::Spec.
daotoad
exactly. Using CPAN modules so you don't have to reinvent the wheel is one of the FEATURES of perl :)
Karel Bílek
Yes, but do I need actually to load a module for this task? Speed, security and the least amount of resources is the aim. It should be fairly simple to serve up an image from a script. Seemingly, I am incorrect. I use modules frequently and embrace the ease. I imagined there would even be a module I could install that does exactly what I am looking for. So many folks serve up images via script but, I am finding no "standard(ish)" real world example outside of just the basics. I honestly never had so much trouble with a 20 line or less snippet. I am just clueless newbie I guess!
Jim_Bo
+1  A: 

The simplest way to serve an image is by using the file handling that is probably already included in your webserver.

You can also add authentication by using an .htaccess file (if you're using Apache).

Peter Stuifzand
Yes, thank you, you seem to know what I am up to. This (htaccess) is how the script is being called. The image tag is normal so, he can show off his images on sites that don't allow query strings in the tags. But, I set up my htaccess to hit the script and serve up the image when there is a condition existent in the images path. A reversed engineered hotlinking rule basically. So, now I am stuck on the actual script that will serve it up...
Jim_Bo
+1  A: 

I would only change a few things.

First, replace the block of code after the first comment block with this:

my $query = new CGI;
my $file = $query->params('img');

Your code to get the file extension doesn't work for me. This does:

my ($ext) = $file =~ m/\.([^\.]+)$/;

I do not understand the use of "my $image = do {...". It just doesn't seem necessary.

Since you're using the CGI module already, use it to do your headers for you:

print $query->header(
    -type => 'image/' . $ext,
    -Content_length => $length,
    );

The way you're reading the file and writing it back out looks functionally perfect.

I have several additional comments & suggestions. First, your code is extremely insecure. It's nice that you're thinking about taint mode, but you're not doing anything about the filename passed in from your client. What if they passed "/etc/passwd", for example? Another is that you might as well open your image file (after doing more security checks) before sending the HTTP headers. This would allow you to send a reasonable error back to the client (404?), rather than just dying. Use CGI's "header" method to make that easy. You can still write something to STDERR if you like.

That's all I can think of just now. I hope this is enough to get you going.

Rob F
Thank you! Yes, die was not to be used. I want to serve up a default image if file not found. Still figuring that one out. The htacces file is what is actually calling the image script. The rule sends it to my script if the images path meets a specific condition. That way my friend can use an normal image tag to show off his artwork images on places that may not allow query strings in image tags. I need help on the security "upgrade" you mentioned. Thanks again for your guidance...
Jim_Bo
+1  A: 

Ok, here are some revisions based on everyones input.. I wish I could check everyones answer!

I have not tested this but, work in progress.

I am calling this script via my htaccess file:

So,

<img src="http://somesite.com/publicimages/image.png"&gt;

Gets sent to my script via htaccess file...

RewriteRule ^publicimages/(.*).(gif|jpe?g|png)$ image.pl?img=$1 [L,NC]

Which will (hopefully) go to my script...

#!/usr/bin/perl -T
use strict;
use CGI;
BEGIN{$SIG{__DIE__} = \&FatalErr}

my $file = $query->param("img");
if (grep $_ ne "img", $query->param) { &FatalErr; }

$ENV{PATH} = "bin:/usr/bin";
delete ($ENV{qw(IFS CDPATH BASH_ENV ENV)});


my ($ext) = $file =~ m/\.([^\.]+)$/;
   if ($ext eq "jpg") {$ext = "jpeg"} 

my $length = (stat($file))[7];

local $/ = undef;
print $query->header(
    -type => 'image/' . $ext,
    -Content_length => $length,
);
  binmode STDOUT;
  open (FH,"<", $file) || DIE;
  my $buffer = "";
     while (read(FH, $buffer, 10240)) {
       print $buffer;
     close(FH);
};

sub FatalErr {
print $query->header(
    -type => 'image/gif',
    -Content_length => $length,
print "errorimage.gif";
}

Please help me improve this. It will help others I am sure... I have not tested this either.

I intend to implement a hit counter as well for each time an image is served up. That would write to a flatfile located under the root dir or the most efficient method but, that is another question I guess!

Should I not exit(0); at the end as well??

Sorry, I am learning...

I would like to beef up security big time too so...

Thanks again for everyones help.

Jim_Bo
I registered today so I can vote up everyone who has helped me. I just need to get my rep up so I can do so....
Jim_Bo
daotoad
Thanks, I need to sanitize quite a bit for sure. I will try to figure out how to perform that redirect on that FatalError sub. Another learning curve. I will check out perl tidy and critic. Anything that will help me learn how to do this correctly. I am however, confused about the extension as list context statement you made. Could you explain? Thanks for your help....
Jim_Bo
You should expand your question, not clarify it in an answer.
brian d foy
Yes, sorry. I just realized I could edit my original question after responses were posted (unlike many forums). Newbie here, learning. Thanks for pointing that out.. ;-)
Jim_Bo
@daotoad I just started using PERL::CRITIC today on your advise. I love it! Talk about a time saver. So far, most my codes pass Harsh and all pass Stern. I will test my revised question script thoroughly before I post it in an edit above. Hopefully, I will create a good generic image serving snippet that can be used by many people. I cannot find one anywhere that boasts any security.
Jim_Bo
A: 

I'm not sure what you are trying to do, but it looks like it would be much easier to handle this without Perl and CGI. Which server are you using? I'd prefer to fix this up with a rewrite rule in Apache.

I've never been a fan of gatekeeper scripts, though. Maybe if you can say why you are trying to do this we can come up with a good solution (and not just a better one :).

brian d foy