RoundCube Webmail
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

208 lines
5.9 KiB

<?php
/*
+-----------------------------------------------------------------------+
| Roundcube Webmail IMAP Client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| This is the public entry point for HTTP requests regarding static |
| content. |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* @const array Supported file types/extensions. If file type is not on the list
* it will have to be served by custom code in your plugin.
*/
const SUPPORTED_TYPES = [
'avif' => 'image/avif',
'css' => 'text/css',
'gif' => 'image/gif',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'html' => 'text/html',
'ico' => 'image/x-icon',
'js' => 'text/javascript',
'json' => 'application/json',
'less' => 'text/less',
'mp3' => 'audio/mpeg',
'png' => 'image/png',
'pdf' => 'application/pdf',
'svg' => 'image/svg+xml',
'tiff' => 'image/tiff',
'wav' => 'audio/wav',
'webp' => 'image/webp',
'woff' => 'font/woff',
'woff2' => 'font/woff2',
];
/**
* @const array Path prefixes to look for the requested files
*/
const ALLOWED_PATHS = [
'installer/',
'plugins/',
'program/',
'skins/',
];
define('INSTALL_PATH', realpath(__DIR__ . '/..') . '/');
$path = validateStaticFile($_SERVER['PATH_INFO']);
if (!$path) {
http_response_code(404);
exit;
}
serveStaticFile($path);
/**
* Validate that the file exists and can be served
*
* @param string $path File location
*
* @return ?string Verified and resolved file location
*/
function validateStaticFile(string $path): ?string
{
$path = trim($path, "/ \t\r\n");
// Remove query params from the path (e.g. cache buster)
$path = preg_replace('/[?&].*$/', '', $path);
// Potential hack attempts, don't allow ".."
if (strpos($path, '..') !== false) {
return null;
}
$ext = pathinfo($path, \PATHINFO_EXTENSION);
// Only supported file types
if (empty($ext) || !isset(SUPPORTED_TYPES[strtolower($ext)])) {
return null;
}
// Ignore some sensitive files
if (preg_match('/(README.*|CHANGELOG.*|SECURITY.*|meta\.json|composer\..*)/', $path)) {
return null;
}
$found = false;
foreach (ALLOWED_PATHS as $prefix) {
if (str_starts_with($path, $prefix) && !preg_match('~skins/.+/templates/~', $path)) {
$found = true;
break;
}
}
if (!$found) {
return null;
}
$path = realpath(INSTALL_PATH . $path);
if ($path === false) {
return null;
}
return $path;
}
/**
* Serve a file.
*
* @param string $path File location
*/
function serveStaticFile($path): void
{
$lastModifiedTime = filemtime($path);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T', $lastModifiedTime));
if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $lastModifiedTime) {
http_response_code(304); // "Not Modified"
return;
}
$size = filesize($path);
$fp = fopen($path, 'r');
$range = [0, $size - 1];
if (isset($_SERVER['HTTP_RANGE'])) {
// $valid = preg_match('^bytes=\d*-\d*(,\d*-\d*)*$', $_SERVER['HTTP_RANGE']);
if (!str_starts_with($_SERVER['HTTP_RANGE'], 'bytes=')) {
http_response_code(416); // "Range Not Satisfiable"
header('Content-Range: bytes */' . $size); // Required in 416.
return;
}
$ranges = explode(',', substr($_SERVER['HTTP_RANGE'], 6));
$range = explode('-', $ranges[0]); // TODO: only support the first range now.
if ($range[0] === '') {
$range[0] = 0;
}
if ($range[1] === '') {
$range[1] = $size - 1;
}
if ($range[0] >= 0 && ($range[1] <= $size - 1) && $range[0] <= $range[1]) {
http_response_code(206); // "Partial Content"
header('Content-Range: bytes ' . sprintf('%u-%u/%u', $range[0], $range[1], $size));
} else {
http_response_code(416); // "Range Not Satisfiable"
header('Content-Range: bytes */' . $size);
return;
}
}
$contentLength = $range[1] - $range[0] + 1;
$ext = pathinfo($path, \PATHINFO_EXTENSION);
$headers = [
'Accept-Ranges' => 'bytes',
'Content-Length' => $contentLength,
'Content-Type' => SUPPORTED_TYPES[strtolower($ext)],
// 'Content-Disposition: attachment; filename="xxxxx"',
'Cache-Control' => 'public, max-age=604800',
'Expires' => gmdate('D, d M Y H:i:s \G\M\T', time() + 30 * 86400),
];
foreach ($headers as $k => $v) {
header("{$k}: {$v}", true);
}
if ($range[0] > 0) {
fseek($fp, $range[0]);
}
$sentSize = 0;
while (!feof($fp) && (connection_status() === \CONNECTION_NORMAL)) {
$readingSize = $contentLength - $sentSize;
$readingSize = min($readingSize, 512 * 1024);
if ($readingSize <= 0) {
break;
}
$data = fread($fp, $readingSize);
if ($data === false) {
break;
}
$sentSize += strlen($data);
echo $data;
flush();
}
fclose($fp);
}