From 3c1e3a10fec8f9f91bd54a10dc735eb58efe7481 Mon Sep 17 00:00:00 2001 From: Dominik Hebeler <dominik@suma-ev.de> Date: Thu, 14 Jan 2021 14:08:36 +0100 Subject: [PATCH] added download route --- app/Http/Controllers/DownloadController.php | 103 ++++++++++++++++++++ app/Http/Controllers/ProxyController.php | 31 +++--- resources/views/errors/413.blade.php | 6 +- routes/web.php | 7 +- 4 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 app/Http/Controllers/DownloadController.php diff --git a/app/Http/Controllers/DownloadController.php b/app/Http/Controllers/DownloadController.php new file mode 100644 index 0000000..65f021e --- /dev/null +++ b/app/Http/Controllers/DownloadController.php @@ -0,0 +1,103 @@ +<?php + +namespace App\Http\Controllers; + +use Illuminate\Http\Request; +use Carbon\Carbon; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Log; + +class DownloadController extends Controller +{ + // How many hours is a Download link valid + const DOWNLOADLINKVALIDHOURS = 1; + + public function redirector(Request $request) + { + return redirect(route('download', [ + "url" => $request->input('url', 'https://metager.de'), + "valid-until" => $request->input('valid-until', ''), + "password" => $request->input('password', '') + ])); + } + + public function download(Request $request) + { + $url = $request->input("url"); + + $headers = get_headers($url, 1); + + $filename = basename($url); + + # From the headers we need to remove the first Element since it's the status code: + $status = $headers[0]; + $status = intval(preg_split("/\s+/si", $status)[1]); + array_forget($headers, 0); + + # Add the Filename if it's not set: + if (!isset($headers["Content-Disposition"])) { + $headers["Content-Disposition"] = "inline; filename=\"" . $filename . "\""; + } elseif (preg_match("/filename=\"{0,1}(.*?)(\"|\s|$)/", $headers["Content-Disposition"], $matches)) { + $filename = $matches[1]; + } + + $response = new StreamedResponse(function () use ($url) { + # We are gonna stream a large file + $wh = fopen('php://output', 'r+'); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_BUFFERSIZE, 256); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_LOW_SPEED_LIMIT, 50000); + curl_setopt($ch, CURLOPT_LOW_SPEED_TIME, 5); + curl_setopt($ch, CURLOPT_FILE, $wh); // Data will be sent to our stream ;-) + + curl_exec($ch); + + curl_close($ch); + + // Don't forget to close the "file" / stream + fclose($wh); + }, 200, $headers); + $response->send(); + return $response; + } + + public static function generateDownloadLinkParameters($url) + { + $validUntil = self::generateValidUntilDate(); + $password = self::generatePassword($url, $validUntil); + return [ + "url" => $url, + "valid-until" => $validUntil, + "password" => $password + ]; + } + + /** + * This function generates a Date/Time String which is used by our Download Controller + * to check if a download link is valid. + * This Date/Time is used in the password hash, too to make sure it is not altered + */ + private static function generateValidUntilDate() + { + $validUntil = Carbon::now()->setTimezone("UTC"); + $validUntil->addHours(self::DOWNLOADLINKVALIDHOURS); + + return $validUntil->format("d-m-Y H:i:s P"); + } + + /** + * This function generates the password for Download Links + * The password is an hmac with the proxy password. + * Algo is SHA256 + * Data is $url . $validUntil + * When verifying the password we can verify integrity of the supplied valid-until argument + */ + private static function generatePassword($url, $validUntil) + { + return hash_hmac("sha256", $url . $validUntil, env("PROXY_PASSWORD", "unsecure_password")); + } +} diff --git a/app/Http/Controllers/ProxyController.php b/app/Http/Controllers/ProxyController.php index ccd63e9..0d0888b 100644 --- a/app/Http/Controllers/ProxyController.php +++ b/app/Http/Controllers/ProxyController.php @@ -12,6 +12,7 @@ use URL; use Illuminate\Support\Facades\Redis; use App\Console\Commands\RequestFetcher; use App\Models\HttpParser; +use Carbon\Carbon; class ProxyController extends Controller { @@ -174,16 +175,19 @@ class ProxyController extends Controller Redis::rpush(RequestFetcher::FETCHQUEUE_KEY, $mission); $answer = Redis::brpoplpush($hash, $hash, 10); Redis::expire($hash, 15); - if($answer){ + if ($answer) { $answer = json_decode($answer, true); } } else { $answer = Cache::get($hash); } - if(!empty($answer["error"])){ - if($answer["error"] === CURLE_ABORTED_BY_CALLBACK){ - return response(view("errors.413"), 413); + if (!empty($answer["error"])) { + if ($answer["error"] === CURLE_ABORTED_BY_CALLBACK) { + return response(view("errors.413")->with([ + "url" => $targetUrl, + "valid-until" => Carbond::now()->add("1h")->format('d-m-Y'), + ]), 413); } } @@ -202,7 +206,12 @@ class ProxyController extends Controller if (isset($answer["headers"]["content-disposition"])) { // File Downloads aren't working anymore within an IFrame. // We will show the user a page to download the File - return response(view("errors.413"), 413); + $postData = \App\Http\Controllers\DownloadController::generateDownloadLinkParameters($targetUrl); + return response(view("errors.413")->with([ + "url" => $postData["url"], + "validuntil" => $postData["valid-until"], + "password" => $postData["password"] + ]), 413); } $body = base64_decode($answer["body"]); switch ($contentType) { @@ -264,13 +273,14 @@ class ProxyController extends Controller ->withHeaders($answer["headers"]); } - public function streamFile(Request $request, $password, $id, $url){ + public function streamFile(Request $request, $password, $id, $url) + { /** * Forms of proxied webpages might aswell Post to this URL * Those need to be denied */ $check = md5(env('PROXY_PASSWORD') . date('dmy') . $request->ip()); - if(!$request->filled("force-download") && $request->input("check", "") === $check){ + if (!$request->filled("force-download") && $request->input("check", "") === $check) { abort(405); } $targetUrl = str_replace("<<SLASH>>", "/", $url); @@ -295,11 +305,11 @@ class ProxyController extends Controller # Add the Filename if it's not set: if (!isset($headers["Content-Disposition"])) { $headers["Content-Disposition"] = "inline; filename=\"" . $filename . "\""; - }elseif(preg_match("/filename=\"{0,1}(.*?)(\"|\s|$)/", $headers["Content-Disposition"], $matches)){ + } elseif (preg_match("/filename=\"{0,1}(.*?)(\"|\s|$)/", $headers["Content-Disposition"], $matches)) { $filename = $matches[1]; } - return response()->streamDownload(function() use ($url){ + return response()->streamDownload(function () use ($url) { # We are gonna stream a large file $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); @@ -308,7 +318,7 @@ class ProxyController extends Controller curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_LOW_SPEED_LIMIT, 50000); curl_setopt($ch, CURLOPT_LOW_SPEED_TIME, 5); - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data){ + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) { echo($data); }); @@ -316,7 +326,6 @@ class ProxyController extends Controller curl_close($ch); }, $filename, $headers); - } public function proxifyUrl($url, $password = null, $key, $topLevel) diff --git a/resources/views/errors/413.blade.php b/resources/views/errors/413.blade.php index 5248567..28cb935 100644 --- a/resources/views/errors/413.blade.php +++ b/resources/views/errors/413.blade.php @@ -31,8 +31,10 @@ <body> <div> <p>The File size of the requested resource exceeds our configured file size limit. If you want you can chose to download this file. Your request will stay anonymous but we will not try to anonymize the contents.</p> - <form method="post" target="_top"> - <input type="hidden" name="force-download" value="{{ md5(env('PROXY_PASSWORD') . date('dmy') . Request::ip()) }}"> + <form method="GET" action="{{ route('download') }}" target="_top"> + <input type="hidden" name="url" value="{{ $url }}"> + <input type="hidden" name="valid-until" value="{{ $validuntil }}"> + <input type="hidden" name="password" value="{{ $password }}"> <button type="submit">Datei herunterladen</button> </form> </div> diff --git a/routes/web.php b/routes/web.php index 28c7971..e83b457 100644 --- a/routes/web.php +++ b/routes/web.php @@ -18,6 +18,10 @@ Route::get('healthz', function () { ->header('Content-Type', 'text/plain'); }); +Route::group(['prefix' => 'download'], function () { + Route::get('/', 'DownloadController@download')->name("download"); +}); + Route::get('/', function () { if (env("APP_ENV", "") !== "production") { return view("development"); @@ -44,6 +48,7 @@ Route::get('{password}/{url}', 'ProxyController@proxyPage')->middleware('throttl Route::get('proxy/{password}/{id}/{url}', 'ProxyController@proxy')->middleware('checkpw:true'); Route::post('proxy/{password}/{id}/{url}', 'ProxyController@streamFile')->middleware('checkpw:true'); + Route::post('/{url}', function ($url) { abort(405); -}); \ No newline at end of file +}); -- GitLab