13 Jan

serving files through a script

One thing I need to do while building the multi-user version of webme is to convert it so file references such as /f/photos/an_image.jpg get transparently converted so they serve correctly, even though the actual file may be located somewhere entirely else.

While writing the code for that, I started with a much simpler idea – make sure that files such as /f/photos/an_image.jpg are served through a script, instead of by the web server. This allows you to do stuff such as:

  • User-based permissions on a per-file basis.
  • Load the file from outside the web-root. This has the added advantage that your users can upload potentially dangerous files such as PHP scripts, but the scripts will not be run because they are not accessible directly from a URL.
  • Log in a database that the file was downloaded.

So, there are two steps involved here – first you need to internally rewrite the URL from /f/photos/an_image.jpg to something like /common/get_file.php?filename=photos/an_image.jpg, and the second step is to write the /common/get_file.php script (well, in your case, it involves copy/paste. In mine, it involved writing it 😉 ).

In the root .htaccess file, add this:

RewriteEngine on
RewriteRule ^f/(.*)$ /common/get_file.php?filename=$1 [QSA,L]

And the get_file.php script is this:

<?php

require '../common.php';

if(!isset($_REQUEST['filename']))exit;
$file=BASEDIR.'f/'.$_REQUEST['filename'];
if(strpos($file,'..')!==false || strpos($file,'/.')!==false)exit;

if(!file_exists($file) || !is_file($file))exit;

$force_download=isset($_REQUEST['force_download']);

header('Content-Description: File Transfer');
if($force_download){
  header('Content-Type: application/octet-stream');
  header('Content-Disposition: attachment; filename='.basename($file));
}
else{
  header('Content-Type: '.get_mimetype(preg_replace('/.*\./','',$file)));
}
header('Content-Transfer-Encoding: binary');
if($force_download){
  header('Expires: 0');
  header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
  header('Pragma: public');
}
else{
  header('Cache-Control: max-age = 2592000');
  header('Expires-Active: On');
  header('Expires: Fri, 1 Jan 2500 01:01:01 GMT');
  header('Pragma:');
}
header('Content-Length: ' . filesize($file));
ob_clean();
flush();
readfile($file);

function get_mimetype($f) {
    $mimetypes = array('ez'=>'application/andrew-inset', 'hqx'=>'application/mac-binhex40', 'cpt'=>'application/mac-compactpro', 'doc'=>'application/msword', 'bin'=>'application/octet-stream', 'dms'=>'application/octet-stream', 'lha'=>'application/octet-stream', 'lzh'=>'application/octet-stream', 'exe'=>'application/octet-stream', 'class'=>'application/octet-stream', 'so'=>'application/octet-stream', 'dll'=>'application/octet-stream', 'oda'=>'application/oda', 'pdf'=>'application/pdf', 'ai'=>'application/postscript', 'eps'=>'application/postscript', 'ps'=>'application/postscript', 'smi'=>'application/smil', 'smil'=>'application/smil', 'mif'=>'application/vnd.mif', 'xls'=>'application/vnd.ms-excel', 'ppt'=>'application/vnd.ms-powerpoint', 'wbxml'=>'application/vnd.wap.wbxml', 'wmlc'=>'application/vnd.wap.wmlc', 'wmlsc'=>'application/vnd.wap.wmlscriptc', 'bcpio'=>'application/x-bcpio', 'vcd'=>'application/x-cdlink', 'pgn'=>'application/x-chess-pgn', 'cpio'=>'application/x-cpio', 'csh'=>'application/x-csh', 'dcr'=>'application/x-director', 'dir'=>'application/x-director', 'dxr'=>'application/x-director', 'dvi'=>'application/x-dvi', 'spl'=>'application/x-futuresplash', 'gtar'=>'application/x-gtar', 'hdf'=>'application/x-hdf', 'js'=>'application/x-javascript', 'skp'=>'application/x-koan', 'skd'=>'application/x-koan', 'skt'=>'application/x-koan', 'skm'=>'application/x-koan', 'latex'=>'application/x-latex', 'nc'=>'application/x-netcdf', 'cdf'=>'application/x-netcdf', 'sh'=>'application/x-sh', 'shar'=>'application/x-shar', 'swf'=>'application/x-shockwave-flash', 'sit'=>'application/x-stuffit', 'sv4cpio'=>'application/x-sv4cpio', 'sv4crc'=>'application/x-sv4crc', 'tar'=>'application/x-tar', 'tcl'=>'application/x-tcl', 'tex'=>'application/x-tex', 'texinfo'=>'application/x-texinfo', 'texi'=>'application/x-texinfo', 't'=>'application/x-troff', 'tr'=>'application/x-troff', 'roff'=>'application/x-troff', 'man'=>'application/x-troff-man', 'me'=>'application/x-troff-me', 'ms'=>'application/x-troff-ms', 'ustar'=>'application/x-ustar', 'src'=>'application/x-wais-source', 'xhtml'=>'application/xhtml+xml', 'xht'=>'application/xhtml+xml', 'zip'=>'application/zip', 'au'=>'audio/basic', 'snd'=>'audio/basic', 'mid'=>'audio/midi', 'midi'=>'audio/midi', 'kar'=>'audio/midi', 'mpga'=>'audio/mpeg', 'mp2'=>'audio/mpeg', 'mp3'=>'audio/mpeg', 'aif'=>'audio/x-aiff', 'aiff'=>'audio/x-aiff', 'aifc'=>'audio/x-aiff', 'm3u'=>'audio/x-mpegurl', 'ram'=>'audio/x-pn-realaudio', 'rm'=>'audio/x-pn-realaudio', 'rpm'=>'audio/x-pn-realaudio-plugin', 'ra'=>'audio/x-realaudio', 'wav'=>'audio/x-wav', 'pdb'=>'chemical/x-pdb', 'xyz'=>'chemical/x-xyz', 'bmp'=>'image/bmp', 'gif'=>'image/gif', 'ief'=>'image/ief', 'jpeg'=>'image/jpeg', 'jpg'=>'image/jpeg', 'jpe'=>'image/jpeg', 'png'=>'image/png', 'tiff'=>'image/tiff', 'tif'=>'image/tiff', 'djvu'=>'image/vnd.djvu', 'djv'=>'image/vnd.djvu', 'wbmp'=>'image/vnd.wap.wbmp', 'ras'=>'image/x-cmu-raster', 'pnm'=>'image/x-portable-anymap', 'pbm'=>'image/x-portable-bitmap', 'pgm'=>'image/x-portable-graymap', 'ppm'=>'image/x-portable-pixmap', 'rgb'=>'image/x-rgb', 'xbm'=>'image/x-xbitmap', 'xpm'=>'image/x-xpixmap', 'xwd'=>'image/x-xwindowdump', 'igs'=>'model/iges', 'iges'=>'model/iges', 'msh'=>'model/mesh', 'mesh'=>'model/mesh', 'silo'=>'model/mesh', 'wrl'=>'model/vrml', 'vrml'=>'model/vrml', 'css'=>'text/css', 'html'=>'text/html', 'htm'=>'text/html', 'asc'=>'text/plain', 'txt'=>'text/plain', 'rtx'=>'text/richtext', 'rtf'=>'text/rtf', 'sgml'=>'text/sgml', 'sgm'=>'text/sgml', 'tsv'=>'text/tab-separated-values', 'wml'=>'text/vnd.wap.wml', 'wmls'=>'text/vnd.wap.wmlscript', 'etx'=>'text/x-setext', 'xsl'=>'text/xml', 'xml'=>'text/xml', 'mpeg'=>'video/mpeg', 'mpg'=>'video/mpeg', 'mpe'=>'video/mpeg', 'qt'=>'video/quicktime', 'mov'=>'video/quicktime', 'mxu'=>'video/vnd.mpegurl', 'avi'=>'video/x-msvideo', 'movie'=>'video/x-sgi-movie', 'ice'=>'x-conference/x-cooltalk');
    $extension = preg_replace('/.*\./', '', $f);
    if (isset($mimetypes[$extension]))return $mimetypes[$extension];
    return 'unknown/mimetype';
}

The only bit you might want to change above is to remove the require '../common.php';, and replace BASEDIR with your own DOCUMENT_ROOT address (with a trailing ‘/’).

As an added bonus, if you add a ?force_download to the address of the file you want to download, then it will be forced into a download, instead of being handled by the browser. For example, /f/photos/an_image.jpg?force_download.

Right. That’s enough explanation. I’ve got work to do.

4 thoughts on “serving files through a script

  1. Richard, could you give an example of how my code is insecure?

    I read the article you linked, but it appears to be nothing to do with what I’m doing here – your example is about including (executing) PHP files, but mine is about allowing files to be downloaded.

  2. Pingback: klog » Blog Archive » hosting multiple sites from the same CMS engine

Comments are closed.