Skip to content

Instantly share code, notes, and snippets.

@KarelWintersky
Created April 14, 2023 10:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KarelWintersky/532c93d936c8ad06b4dfe3fff206cb3a to your computer and use it in GitHub Desktop.
Save KarelWintersky/532c93d936c8ad06b4dfe3fff206cb3a to your computer and use it in GitHub Desktop.
Download proxy with logging
<?php
/**
* ?file=STORAGE:file.jpg
* or
* ?file=file.jpg
*
* @todo: separate logs for different buckets?
* @todo: testing
*/
const LOGFILE_OK = __DIR__ . '/download-success.log';
const LOGFILE_ERROR = __DIR__ . '/download-errors.log';
const LOGFILE_SEPARATOR = '|';
$buckets = [
'_' => __DIR__ . '/frontend/images/',
'1st' => __DIR__ . '/files1/'
];
/**
* Функции объявлены как методы класса для простоты и инкапсуляции
*/
class FileDownloader {
public static function httpNotFound() {
http_response_code(404);
header("HTTP/1.0 404 Not Found");
header('Content-type: text/html');
die('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html lang="ru"><head><title>Storage proxy reporting: 404 Not Found</title></head><body><h1>Not Found</h1><p>The requested URL was not found on this server.</p></body></html>');
}
public static function getMimeType($file) {
$types = [
'7z' => 'application/x-7z-compressed',
'doc' => 'application/msword',
'docm' => 'application/vnd.ms-word.document.macroenabled.12',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'gif' => 'image/gif',
'htm' => 'text/html',
'html' => 'text/html',
'jpe' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'pdf' => 'application/pdf',
'png' => 'image/png',
];
$ext = pathinfo($file, PATHINFO_EXTENSION);
return array_key_exists($ext, $types) ? $types[$ext] : "application/force-download";
}
public static function getIP() {
if (PHP_SAPI === 'cli') { return '127.0.0.1'; }
if (!isset ($_SERVER['REMOTE_ADDR'])) { return NULL; }
if (array_key_exists("HTTP_X_FORWARDED_FOR", $_SERVER)) {
$http_x_forwarded_for = explode(",", $_SERVER["HTTP_X_FORWARDED_FOR"]);
$client_ip = trim(end($http_x_forwarded_for));
if (filter_var($client_ip, FILTER_VALIDATE_IP)) {
return $client_ip;
}
}
return filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP) ? $_SERVER['REMOTE_ADDR'] : NULL;
}
public static function fWriteLog(string $file_with_path, string $file_name, string $bucket) {
if (empty(LOGFILE_OK)) {
return;
}
$fl = fopen(LOGFILE_OK, "w+");
$fl_stat = stat(LOGFILE_OK);
if ($fl_stat === false) {
syslog(LOG_WARNING, __FILE__ . " reports: error creating log file " . LOGFILE_OK);
return;
}
if ($fl_stat['size'] == 0) {
fputcsv($fl, ["datetime", "ip", "bucket", "file"], LOGFILE_SEPARATOR);
}
fputcsv($fl, [
date('c'),
self::getIP(),
$bucket,
$file_name
], LOGFILE_SEPARATOR);
fclose($fl);
}
public static function fWriteErrorLog(string $file, string $bucket, string $message) {
if (empty(LOGFILE_ERROR)) {
return;
}
$fl = fopen(LOGFILE_ERROR, "w+");
$fl_stat = stat(LOGFILE_ERROR);
if ($fl_stat === false) {
syslog(LOG_WARNING, __FILE__ . " reports: error creating log file " . LOGFILE_ERROR);
return;
}
if ($fl_stat['size'] == 0) {
fputcsv($fl, ["datetime", "ip", "bucket", "file", "message"], LOGFILE_SEPARATOR);
}
fputcsv($fl, [
date('c'),
self::getIP(),
$bucket,
$file,
$message
], LOGFILE_SEPARATOR);
fclose($fl);
}
}
class FileDownloaderException extends RuntimeException { }
try {
$file_name = '';
$bucket = '';
if (empty($_GET['file'])) {
throw new FileDownloaderException("Empty request");
}
$arg = $_GET['file'];
$arg_parts = explode(':', $arg);
if (count($arg_parts) == 2) {
$bucket = $arg_parts[0];
$file_name = $arg_parts[1];
} elseif (count($arg_parts) == 1) {
$bucket = '_';
$file_name = $arg_parts[0];
} else {
throw new FileDownloaderException("Incorrect request format");
}
if (!array_key_exists($bucket, $buckets)) {
throw new FileDownloaderException("Bucket not exist");
}
$file_with_path = $buckets[ $bucket ] . $file_name;
if (!file_exists($file_with_path) || !is_readable($file_with_path)) {
throw new FileDownloaderException("File not exists or not readable");
}
$file_mimetype = FileDownloader::getMimeType($file_with_path);
$file_size = stat($file_with_path);
if ($file_size === false) {
throw new FileDownloaderException("Invalid file size");
}
$file_size = $file_size['size'];
$file_pointer = fopen($file_with_path, "rb");
if ($file_pointer === false) {
throw new FileDownloaderException("Can't open file for reading");
}
// looks like file exist. so, log and out file
header("Pragma: public");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: private", false);
header("Content-Type: {$file_mimetype}");
header("Content-Disposition: attachment; filename=\"{$file_name}\";");
header("Content-Transfer-Encoding: binary");
header("Content-Length: {$file_size}");
@ob_clean();
rewind($file_pointer);
fpassthru($file_pointer);
FileDownloader::fWriteLog($file_with_path, $file_name, $bucket);
} catch (FileDownloaderException $e) {
FileDownloader::fWriteErrorLog($file_name, $bucket, $e->getMessage());
FileDownloader::httpNotFound();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment