From 635b0717931df907ee8015a42ad0ed1fcdf967c4 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Mon, 23 Sep 2013 07:47:40 +0200 Subject: Implement rangeDownload() as driver and provide sendfile implementations for Nginx and Lighttpd * The rangeDownload() function has been moved to libraries/Ddownload/drivers/Ddownload_php.php * The nginx and lighttpd drivers can be set via $config['download_driver'] Signed-off-by: Pierre Schmitz --- application/config/config.php | 16 +++ application/controllers/file.php | 6 +- application/helpers/filebin_helper.php | 99 ------------------ application/libraries/Ddownload/Ddownload.php | 34 +++++++ .../Ddownload/drivers/Ddownload_lighttpd.php | 27 +++++ .../Ddownload/drivers/Ddownload_nginx.php | 30 ++++++ .../libraries/Ddownload/drivers/Ddownload_php.php | 111 +++++++++++++++++++++ 7 files changed, 222 insertions(+), 101 deletions(-) create mode 100644 application/libraries/Ddownload/Ddownload.php create mode 100644 application/libraries/Ddownload/drivers/Ddownload_lighttpd.php create mode 100644 application/libraries/Ddownload/drivers/Ddownload_nginx.php create mode 100644 application/libraries/Ddownload/drivers/Ddownload_php.php (limited to 'application') diff --git a/application/config/config.php b/application/config/config.php index 5d6ea5d1f..54870b37b 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -416,6 +416,22 @@ $config['auth_fluxbb'] = array( // possible values: production, development $config['environment'] = "production"; +// This sets the download implementation. Possible values are php, nginx and lighttpd +// The nginx and lighttpd drivers make use of the server's sendfile feature. +$config['download_driver'] = 'php'; +// The lighttpd driver requires the following directive to be set in your fastcgi.server configuration: +// "allow-x-send-file" => "enable" +// See http://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModFastCGI#X-Sendfile +// +// When using the nginx download driver you need to define an internal location +// from which nginx will serve your uploads: +// location ^~ /protected-uploads/ { +// internal; +// alias /; +// } +// See http://wiki.nginx.org/X-accel +$config['download_nginx_location'] = '/protected-uploads'; + if (file_exists(FCPATH.'application/config/config-local.php')) { include FCPATH.'application/config/config-local.php'; } diff --git a/application/controllers/file.php b/application/controllers/file.php index b02cdd8ce..6ac0bc28c 100644 --- a/application/controllers/file.php +++ b/application/controllers/file.php @@ -98,10 +98,12 @@ class File extends MY_Controller { exit(); } + $this->load->driver("ddownload"); + // user wants the plain file if ($lexer == 'plain') { handle_etag($etag); - rangeDownload($file, $filedata["filename"], "text/plain"); + $this->ddownload->serveFile($file, $filedata["filename"], "text/plain"); exit(); } @@ -122,7 +124,7 @@ class File extends MY_Controller { header("$header_name: allow 'none'; img-src *; media-src *; font-src *; style-src * 'unsafe-inline'; script-src 'none'; object-src *; frame-src 'none'; "); } handle_etag($etag); - rangeDownload($file, $filedata["filename"], $filedata["mimetype"]); + $this->ddownload->serveFile($file, $filedata["filename"], $filedata["mimetype"]); exit(); } diff --git a/application/helpers/filebin_helper.php b/application/helpers/filebin_helper.php index 6e4c84e43..71ce7e6ca 100644 --- a/application/helpers/filebin_helper.php +++ b/application/helpers/filebin_helper.php @@ -20,105 +20,6 @@ function format_bytes($size) } } -// Original source: http://www.phpfreaks.com/forums/index.php?topic=198274.msg895468#msg895468 -function rangeDownload($file, $filename, $type) -{ - $fp = @fopen($file, 'r'); - - $size = filesize($file); // File size - $length = $size; // Content length - $start = 0; // Start byte - $end = $size - 1; // End byte - // Now that we've gotten so far without errors we send the accept range header - /* At the moment we only support single ranges. - * Multiple ranges requires some more work to ensure it works correctly - * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 - * - * Multirange support annouces itself with: - * header('Accept-Ranges: bytes'); - * - * Multirange content must be sent with multipart/byteranges mediatype, - * (mediatype = mimetype) - * as well as a boundry header to indicate the various chunks of data. - */ - header("Accept-Ranges: 0-$length"); - // header('Accept-Ranges: bytes'); - // multipart/byteranges - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 - if (isset($_SERVER['HTTP_RANGE'])) - { - $c_start = $start; - $c_end = $end; - // Extract the range string - list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); - // Make sure the client hasn't sent us a multibyte range - if (strpos($range, ',') !== false) - { - // (?) Shoud this be issued here, or should the first - // range be used? Or should the header be ignored and - // we output the whole content? - header('HTTP/1.1 416 Requested Range Not Satisfiable'); - header("Content-Range: bytes $start-$end/$size"); - // (?) Echo some info to the client? - exit; - } - // If the range starts with an '-' we start from the beginning - // If not, we forward the file pointer - // And make sure to get the end byte if spesified - if ($range{0} == '-') - { - // The n-number of the last bytes is requested - $c_start = $size - substr($range, 1); - } - else - { - $range = explode('-', $range); - $c_start = $range[0]; - $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size; - } - /* Check the range and make sure it's treated according to the specs. - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - */ - // End bytes can not be larger than $end. - $c_end = ($c_end > $end) ? $end : $c_end; - // Validate the requested range and return an error if it's not correct. - if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) - { - header('HTTP/1.1 416 Requested Range Not Satisfiable'); - header("Content-Range: bytes $start-$end/$size"); - // (?) Echo some info to the client? - exit; - } - $start = $c_start; - $end = $c_end; - $length = $end - $start + 1; // Calculate new content length - fseek($fp, $start); - header('HTTP/1.1 206 Partial Content'); - // Notify the client the byte range we'll be outputting - header("Content-Range: bytes $start-$end/$size"); - } - header("Content-Length: $length"); - header("Content-disposition: inline; filename=\"".$filename."\"\n"); - header("Content-Type: ".$type."\n"); - - // Start buffered download - $buffer = 1024 * 8; - while(!feof($fp) && ($p = ftell($fp)) <= $end) - { - if ($p + $buffer > $end) - { - // In case we're only outputtin a chunk, make sure we don't - // read past the length - $buffer = $end - $p + 1; - } - set_time_limit(0); // Reset time limit for big files - echo fread($fp, $buffer); - flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. - } - - fclose($fp); -} - function even_odd($reset = false) { static $counter = 1; diff --git a/application/libraries/Ddownload/Ddownload.php b/application/libraries/Ddownload/Ddownload.php new file mode 100644 index 000000000..808dfe776 --- /dev/null +++ b/application/libraries/Ddownload/Ddownload.php @@ -0,0 +1,34 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +abstract class Ddownload_Driver extends CI_Driver { + + abstract public function serveFile($file, $filename, $type); +} + +class Ddownload extends CI_Driver_Library { + + protected $_adapter = null; + + protected $valid_drivers = array( + 'ddownload_php', 'ddownload_nginx', 'ddownload_lighttpd' + ); + + function __construct() + { + $CI =& get_instance(); + + $this->_adapter = $CI->config->item('download_driver'); + } + + public function serveFile($file, $filename, $type) + { + $this->{$this->_adapter}->serveFile($file, $filename, $type); + } +} diff --git a/application/libraries/Ddownload/drivers/Ddownload_lighttpd.php b/application/libraries/Ddownload/drivers/Ddownload_lighttpd.php new file mode 100644 index 000000000..31db4d340 --- /dev/null +++ b/application/libraries/Ddownload/drivers/Ddownload_lighttpd.php @@ -0,0 +1,27 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Ddownload_lighttpd extends Ddownload_Driver { + + public function serveFile($file, $filename, $type) + { + $CI =& get_instance(); + $upload_path = $CI->config->item('upload_path'); + + if (strpos($file, $upload_path) !== 0) { + show_error('Invalid file path'); + return; + } + + header('Content-disposition: inline; filename="'.$filename."\"\n"); + header('Content-Type: '.$type."\n"); + header('X-Sendfile: '.$file."\n"); + } + +} diff --git a/application/libraries/Ddownload/drivers/Ddownload_nginx.php b/application/libraries/Ddownload/drivers/Ddownload_nginx.php new file mode 100644 index 000000000..5fb6ffa87 --- /dev/null +++ b/application/libraries/Ddownload/drivers/Ddownload_nginx.php @@ -0,0 +1,30 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Ddownload_nginx extends Ddownload_Driver { + + public function serveFile($file, $filename, $type) + { + $CI =& get_instance(); + $upload_path = $CI->config->item('upload_path'); + $download_location = $CI->config->item('download_nginx_location'); + + if (strpos($file, $upload_path) === 0) { + $file_path = substr($file, strlen($upload_path)); + } else { + show_error('Invalid file path'); + return; + } + + header('Content-disposition: inline; filename="'.$filename."\"\n"); + header('Content-Type: '.$type."\n"); + header('X-Accel-Redirect: '.$download_location.$file_path."\n"); + } + +} diff --git a/application/libraries/Ddownload/drivers/Ddownload_php.php b/application/libraries/Ddownload/drivers/Ddownload_php.php new file mode 100644 index 000000000..344db53f0 --- /dev/null +++ b/application/libraries/Ddownload/drivers/Ddownload_php.php @@ -0,0 +1,111 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Ddownload_php extends Ddownload_Driver { + + // Original source: http://www.phpfreaks.com/forums/index.php?topic=198274.msg895468#msg895468 + public function serveFile($file, $filename, $type) + { + $fp = @fopen($file, 'r'); + + $size = filesize($file); // File size + $length = $size; // Content length + $start = 0; // Start byte + $end = $size - 1; // End byte + // Now that we've gotten so far without errors we send the accept range header + /* At the moment we only support single ranges. + * Multiple ranges requires some more work to ensure it works correctly + * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 + * + * Multirange support annouces itself with: + * header('Accept-Ranges: bytes'); + * + * Multirange content must be sent with multipart/byteranges mediatype, + * (mediatype = mimetype) + * as well as a boundry header to indicate the various chunks of data. + */ + header("Accept-Ranges: 0-$length"); + // header('Accept-Ranges: bytes'); + // multipart/byteranges + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 + if (isset($_SERVER['HTTP_RANGE'])) + { + $c_start = $start; + $c_end = $end; + // Extract the range string + list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); + // Make sure the client hasn't sent us a multibyte range + if (strpos($range, ',') !== false) + { + // (?) Shoud this be issued here, or should the first + // range be used? Or should the header be ignored and + // we output the whole content? + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes $start-$end/$size"); + // (?) Echo some info to the client? + exit; + } + // If the range starts with an '-' we start from the beginning + // If not, we forward the file pointer + // And make sure to get the end byte if spesified + if ($range{0} == '-') + { + // The n-number of the last bytes is requested + $c_start = $size - substr($range, 1); + } + else + { + $range = explode('-', $range); + $c_start = $range[0]; + $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size; + } + /* Check the range and make sure it's treated according to the specs. + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + */ + // End bytes can not be larger than $end. + $c_end = ($c_end > $end) ? $end : $c_end; + // Validate the requested range and return an error if it's not correct. + if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) + { + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes $start-$end/$size"); + // (?) Echo some info to the client? + exit; + } + $start = $c_start; + $end = $c_end; + $length = $end - $start + 1; // Calculate new content length + fseek($fp, $start); + header('HTTP/1.1 206 Partial Content'); + // Notify the client the byte range we'll be outputting + header("Content-Range: bytes $start-$end/$size"); + } + header("Content-Length: $length"); + header("Content-disposition: inline; filename=\"".$filename."\"\n"); + header("Content-Type: ".$type."\n"); + + // Start buffered download + $buffer = 1024 * 8; + while(!feof($fp) && ($p = ftell($fp)) <= $end) + { + if ($p + $buffer > $end) + { + // In case we're only outputtin a chunk, make sure we don't + // read past the length + $buffer = $end - $p + 1; + } + set_time_limit(0); // Reset time limit for big files + echo fread($fp, $buffer); + flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. + } + + fclose($fp); + } + +} -- cgit v1.2.3-24-g4f1b