1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 20:22:42 +01:00

Add protected download route with signed route signatures

This commit is contained in:
David Bomba 2023-07-07 14:56:43 +10:00
parent 71523ecde3
commit fb37fc40a3
7 changed files with 135 additions and 34 deletions

View File

@ -11,10 +11,12 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\Export\StoreExportRequest; use Illuminate\Support\Str;
use App\Jobs\Company\CompanyExport;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use App\Utils\Traits\MakesHash;
use App\Jobs\Company\CompanyExport;
use Illuminate\Support\Facades\Cache;
use App\Http\Requests\Export\StoreExportRequest;
class ExportController extends BaseController class ExportController extends BaseController
{ {
@ -54,8 +56,12 @@ class ExportController extends BaseController
*/ */
public function index(StoreExportRequest $request) public function index(StoreExportRequest $request)
{ {
CompanyExport::dispatch(auth()->user()->getCompany(), auth()->user()); $hash = Str::uuid();
$url = \Illuminate\Support\Facades\URL::temporarySignedRoute('protected_download', now()->addHour(), ['hash' => $hash]);
Cache::put($hash, $url, now()->addHour());
return response()->json(['message' => 'Processing'], 200); CompanyExport::dispatch(auth()->user()->getCompany(), auth()->user(), $hash);
return response()->json(['message' => 'Processing', 'url' => $url], 200);
} }
} }

View File

@ -0,0 +1,41 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Jobs\Util\UnlinkFile;
use App\Exceptions\SystemError;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
class ProtectedDownloadController extends BaseController
{
public function index(Request $request)
{
$hashed_path = Cache::pull($request->hash);
if (!$hashed_path) {
throw new SystemError('File no longer available', 404);
abort(404, 'File no longer available');
}
UnlinkFile::dispatch(config('filesystems.default'), $hashed_path)->delay(now()->addSeconds(10));
return response()->streamDownload(function () use ($hashed_path) {
echo Storage::get($hashed_path);
}, basename($hashed_path), []);
}
}

View File

@ -41,6 +41,7 @@ use App\Http\Middleware\VerifyCsrfToken;
use App\Http\Middleware\ContactTokenAuth; use App\Http\Middleware\ContactTokenAuth;
use Illuminate\Auth\Middleware\Authorize; use Illuminate\Auth\Middleware\Authorize;
use App\Http\Middleware\SetDbByCompanyKey; use App\Http\Middleware\SetDbByCompanyKey;
use App\Http\Middleware\ValidateSignature;
use App\Http\Middleware\PasswordProtection; use App\Http\Middleware\PasswordProtection;
use App\Http\Middleware\ClientPortalEnabled; use App\Http\Middleware\ClientPortalEnabled;
use App\Http\Middleware\CheckClientExistence; use App\Http\Middleware\CheckClientExistence;
@ -49,16 +50,13 @@ use Illuminate\Http\Middleware\SetCacheHeaders;
use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\Middleware\StartSession;
use App\Http\Middleware\CheckForMaintenanceMode; use App\Http\Middleware\CheckForMaintenanceMode;
use App\Http\Middleware\RedirectIfAuthenticated; use App\Http\Middleware\RedirectIfAuthenticated;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Routing\Middleware\ValidateSignature;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified; use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\View\Middleware\ShareErrorsFromSession; use Illuminate\View\Middleware\ShareErrorsFromSession;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Routing\Middleware\ThrottleRequestsWithRedis;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
class Kernel extends HttpKernel class Kernel extends HttpKernel

View File

@ -0,0 +1,49 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\Exceptions\InvalidSignatureException;
class ValidateSignature
{
/**
* The names of the parameters that should be ignored.
*
* @var array<int, string>
*/
protected $ignore = [
'q'
];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $relative
* @return \Illuminate\Http\Response
*
* @throws \Illuminate\Routing\Exceptions\InvalidSignatureException
*/
public function handle($request, Closure $next, $relative = null)
{
$ignore = property_exists($this, 'except') ? $this->except : $this->ignore;
if ($request->hasValidSignatureWhileIgnoring($ignore, $relative !== 'relative')) {
return $next($request);
}
throw new InvalidSignatureException;
}
}

View File

@ -11,39 +11,39 @@
namespace App\Jobs\Company; namespace App\Jobs\Company;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\DownloadBackup;
use App\Models\Company;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\Models\PurchaseOrderInvitation;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoiceInvitation;
use App\Models\User; use App\Models\User;
use App\Models\VendorContact;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Models\Company;
use App\Libraries\MultiDB;
use Illuminate\Support\Str;
use App\Mail\DownloadBackup;
use App\Jobs\Util\UnlinkFile;
use App\Models\VendorContact;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use App\Models\QuoteInvitation;
use App\Utils\Traits\MakesHash;
use App\Models\CreditInvitation;
use App\Jobs\Mail\NinjaMailerJob;
use App\Models\InvoiceInvitation;
use Illuminate\Support\Facades\App;
use App\Jobs\Mail\NinjaMailerObject;
use Illuminate\Support\Facades\Cache;
use Illuminate\Queue\SerializesModels;
use App\Models\PurchaseOrderInvitation;
use Illuminate\Support\Facades\Storage;
use Illuminate\Queue\InteractsWithQueue;
use App\Models\RecurringInvoiceInvitation;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Storage;
class CompanyExport implements ShouldQueue class CompanyExport implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
public $company; private $export_format = 'json';
private $export_format;
private $export_data = []; private $export_data = [];
public $user;
/** /**
* Create a new job instance. * Create a new job instance.
@ -52,11 +52,8 @@ class CompanyExport implements ShouldQueue
* @param User $user * @param User $user
* @param string $custom_token_name * @param string $custom_token_name
*/ */
public function __construct(Company $company, User $user, $export_format = 'json') public function __construct(public Company $company, private User $user, public string $hash)
{ {
$this->company = $company;
$this->user = $user;
$this->export_format = $export_format;
} }
/** /**
@ -467,6 +464,11 @@ class CompanyExport implements ShouldQueue
} }
$storage_file_path = Storage::disk(config('filesystems.default'))->url('backups/'.$file_name); $storage_file_path = Storage::disk(config('filesystems.default'))->url('backups/'.$file_name);
$storage_path = Storage::disk(config('filesystems.default'))->path('backups/'.$file_name);
$url = Cache::get($this->hash);
Cache::put($this->hash, $storage_path, now()->addHour());
App::forgetInstance('translator'); App::forgetInstance('translator');
$t = app('translator'); $t = app('translator');
@ -475,13 +477,15 @@ class CompanyExport implements ShouldQueue
$company_reference = Company::find($this->company->id); $company_reference = Company::find($this->company->id);
$nmo = new NinjaMailerObject; $nmo = new NinjaMailerObject;
$nmo->mailable = new DownloadBackup($storage_file_path, $company_reference); $nmo->mailable = new DownloadBackup($url, $company_reference);
$nmo->to_user = $this->user; $nmo->to_user = $this->user;
$nmo->company = $company_reference; $nmo->company = $company_reference;
$nmo->settings = $this->company->settings; $nmo->settings = $this->company->settings;
NinjaMailerJob::dispatch($nmo, true); NinjaMailerJob::dispatch($nmo, true);
UnlinkFile::dispatch(config('filesystems.default'), $storage_path)->delay(now()->addHours(1));
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
sleep(3); sleep(3);
unlink($zip_path); unlink($zip_path);

View File

@ -1,5 +1,5 @@
@extends('portal.ninja2020.layout.error') @extends('portal.ninja2020.layout.error')
@section('title', __($title) ?? 'Server Error') @section('title', $title ?? 'Error')
@section('code', __($code) ?? '500') @section('code', __($code) ?? '500')
@section('message', __($message) ?? 'System Error') @section('message', __($message) ?? 'System Error')

View File

@ -101,6 +101,7 @@ use App\Http\Controllers\Support\Messages\SendingController;
use App\Http\Controllers\Reports\ClientSalesReportController; use App\Http\Controllers\Reports\ClientSalesReportController;
use App\Http\Controllers\Reports\InvoiceItemReportController; use App\Http\Controllers\Reports\InvoiceItemReportController;
use App\Http\Controllers\PaymentNotificationWebhookController; use App\Http\Controllers\PaymentNotificationWebhookController;
use App\Http\Controllers\ProtectedDownloadController;
use App\Http\Controllers\Reports\ProductSalesReportController; use App\Http\Controllers\Reports\ProductSalesReportController;
use App\Http\Controllers\Reports\ClientBalanceReportController; use App\Http\Controllers\Reports\ClientBalanceReportController;
use App\Http\Controllers\Reports\ClientContactReportController; use App\Http\Controllers\Reports\ClientContactReportController;
@ -401,4 +402,6 @@ Route::post('api/v1/yodlee/data_updates', [YodleeController::class, 'dataUpdates
Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshUpdatesWebhook'])->middleware('throttle:100,1'); Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshUpdatesWebhook'])->middleware('throttle:100,1');
Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1'); Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1');
Route::get('api/v1/protected_download/{hash}', [ProtectedDownloadController::class, 'index'])->name('protected_download')->middleware('signed')->middleware('throttle:300,1');
Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404'); Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404');