Add fole check endpoint & Update dependencies
This commit is contained in:
@@ -9,7 +9,6 @@ use Illuminate\Support\Facades\Storage;
|
|||||||
|
|
||||||
class ApiController extends Controller
|
class ApiController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Filesize of File
|
* Get Filesize of File
|
||||||
*/
|
*/
|
||||||
@@ -43,6 +42,38 @@ class ApiController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a storage path exists
|
||||||
|
*/
|
||||||
|
public function checkPath(string $folderhash, string $timehash): \Illuminate\Http\JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$path = Crypt::decryptString($folderhash);
|
||||||
|
$time = Crypt::decryptString($timehash);
|
||||||
|
} catch (DecryptException $e) {
|
||||||
|
return response()->json(['valid' => false, 'error' => 'Invalid Token!'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Carbon::now() > Carbon::parse($time)) {
|
||||||
|
return response()->json(['valid' => false, 'error' => 'Link has expired!'], 410);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defensive: reject traversal; only allow the known prefixes / year dirs.
|
||||||
|
if (str_contains($path, '..')) {
|
||||||
|
return response()->json(['valid' => false, 'error' => 'Invalid path!'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Storage::exists($path)) {
|
||||||
|
return response()->json(['valid' => false, 'error' => 'File not found!'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'valid' => true,
|
||||||
|
'type' => Storage::directoryExists($path) ? 'directory' : 'file',
|
||||||
|
'size' => Storage::directoryExists($path) ? null : Storage::size($path),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download File
|
* Download File
|
||||||
*/
|
*/
|
||||||
@@ -82,7 +113,8 @@ class ApiController extends Controller
|
|||||||
200,
|
200,
|
||||||
$headers
|
$headers
|
||||||
);
|
);
|
||||||
} catch (DecryptException $e) { }
|
} catch (DecryptException $e) {
|
||||||
|
}
|
||||||
|
|
||||||
return view('error', ['error' => 'Invalid Token!']);
|
return view('error', ['error' => 'Invalid Token!']);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -29,7 +29,7 @@ class Kernel extends HttpKernel
|
|||||||
* @var array<string, array<int, class-string|string>>
|
* @var array<string, array<int, class-string|string>>
|
||||||
*/
|
*/
|
||||||
protected $middlewareGroups = [
|
protected $middlewareGroups = [
|
||||||
'web' => []
|
'web' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +40,6 @@ class Kernel extends HttpKernel
|
|||||||
* @var array<string, class-string|string>
|
* @var array<string, class-string|string>
|
||||||
*/
|
*/
|
||||||
protected $middlewareAliases = [
|
protected $middlewareAliases = [
|
||||||
|
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -5,19 +5,19 @@
|
|||||||
"keywords": ["laravel", "framework"],
|
"keywords": ["laravel", "framework"],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1",
|
"php": "^8.2",
|
||||||
"guzzlehttp/guzzle": "^7.2",
|
"guzzlehttp/guzzle": "^7.8.1",
|
||||||
"laravel/framework": "^10.10",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/sanctum": "^3.2",
|
"laravel/sanctum": "^4.2",
|
||||||
"laravel/tinker": "^2.8"
|
"laravel/tinker": "^2.10"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.9.1",
|
"fakerphp/faker": "^1.24.0",
|
||||||
"laravel/pint": "^1.0",
|
"laravel/pint": "^1.18",
|
||||||
"laravel/sail": "^1.18",
|
"laravel/sail": "^1.18",
|
||||||
"mockery/mockery": "^1.4.4",
|
"mockery/mockery": "^1.4.4",
|
||||||
"nunomaduro/collision": "^7.0",
|
"nunomaduro/collision": "^8.1",
|
||||||
"phpunit/phpunit": "^10.1",
|
"phpunit/phpunit": "^11.4",
|
||||||
"spatie/laravel-ignition": "^2.0"
|
"spatie/laravel-ignition": "^2.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
|||||||
Generated
+2168
-1487
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'guard' => 'web',
|
||||||
|
'passwords' => 'users',
|
||||||
|
],
|
||||||
|
|
||||||
|
'guards' => [
|
||||||
|
'web' => [
|
||||||
|
'driver' => 'session',
|
||||||
|
'provider' => 'users',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'providers' => [
|
||||||
|
'users' => [
|
||||||
|
'driver' => 'eloquent',
|
||||||
|
'model' => App\Models\User::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'passwords' => [
|
||||||
|
'users' => [
|
||||||
|
'provider' => 'users',
|
||||||
|
'table' => 'password_reset_tokens',
|
||||||
|
'expire' => 60,
|
||||||
|
'throttle' => 60,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'password_timeout' => 10800,
|
||||||
|
|
||||||
|
];
|
||||||
@@ -16,3 +16,5 @@ use Illuminate\Support\Facades\Route;
|
|||||||
|
|
||||||
Route::get('/download/{folderhash}/{timehash}', [ApiController::class, 'download']);
|
Route::get('/download/{folderhash}/{timehash}', [ApiController::class, 'download']);
|
||||||
Route::get('/getSize/{folderhash}/{timehash}', [ApiController::class, 'getFileSize']);
|
Route::get('/getSize/{folderhash}/{timehash}', [ApiController::class, 'getFileSize']);
|
||||||
|
Route::get('/check/{folderhash}/{timehash}', [ApiController::class, 'checkPath'])
|
||||||
|
->middleware('throttle:120,1');
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Console\Kernel;
|
||||||
|
|
||||||
|
trait CreatesApplication
|
||||||
|
{
|
||||||
|
public function createApplication()
|
||||||
|
{
|
||||||
|
$app = require __DIR__.'/../bootstrap/app.php';
|
||||||
|
|
||||||
|
$app->make(Kernel::class)->bootstrap();
|
||||||
|
|
||||||
|
return $app;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CheckPathTest extends TestCase
|
||||||
|
{
|
||||||
|
private function tokenize(string $value): string
|
||||||
|
{
|
||||||
|
return Crypt::encryptString($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_valid_file_returns_size(): void
|
||||||
|
{
|
||||||
|
Storage::fake('local');
|
||||||
|
Storage::put('hentai-1080p/2026/Title/E01.mp4', 'fake-content');
|
||||||
|
|
||||||
|
$folder = $this->tokenize('hentai-1080p/2026/Title/E01.mp4');
|
||||||
|
$time = $this->tokenize(now()->addHours(6)->toDateTimeString());
|
||||||
|
|
||||||
|
$this->getJson("/check/{$folder}/{$time}")
|
||||||
|
->assertOk()
|
||||||
|
->assertJson([
|
||||||
|
'valid' => true,
|
||||||
|
'type' => 'file',
|
||||||
|
])
|
||||||
|
->assertJsonPath('size', strlen('fake-content'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_valid_directory_is_recognized(): void
|
||||||
|
{
|
||||||
|
Storage::fake('local');
|
||||||
|
Storage::put('2026/Title/E01/720/manifest.mpd', 'fake-manifest');
|
||||||
|
|
||||||
|
$folder = $this->tokenize('2026/Title');
|
||||||
|
$time = $this->tokenize(now()->addHours(6)->toDateTimeString());
|
||||||
|
|
||||||
|
$this->getJson("/check/{$folder}/{$time}")
|
||||||
|
->assertOk()
|
||||||
|
->assertJson([
|
||||||
|
'valid' => true,
|
||||||
|
'type' => 'directory',
|
||||||
|
'size' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_missing_path_returns_404(): void
|
||||||
|
{
|
||||||
|
Storage::fake('local');
|
||||||
|
|
||||||
|
$folder = $this->tokenize('hentai/2026/Title/E01.mp4');
|
||||||
|
$time = $this->tokenize(now()->addHours(6)->toDateTimeString());
|
||||||
|
|
||||||
|
$this->getJson("/check/{$folder}/{$time}")
|
||||||
|
->assertStatus(404)
|
||||||
|
->assertJson(['valid' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_expired_token_returns_410(): void
|
||||||
|
{
|
||||||
|
Storage::fake('local');
|
||||||
|
Storage::put('hentai/2026/Title/E01.mp4', 'fake-content');
|
||||||
|
|
||||||
|
$folder = $this->tokenize('hentai/2026/Title/E01.mp4');
|
||||||
|
$time = $this->tokenize(now()->subMinute()->toDateTimeString());
|
||||||
|
|
||||||
|
$this->getJson("/check/{$folder}/{$time}")
|
||||||
|
->assertStatus(410)
|
||||||
|
->assertJson(['valid' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_invalid_token_returns_422(): void
|
||||||
|
{
|
||||||
|
Storage::fake('local');
|
||||||
|
|
||||||
|
$this->getJson('/check/not-a-token/not-a-token')
|
||||||
|
->assertStatus(422)
|
||||||
|
->assertJson(['valid' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_path_traversal_is_rejected(): void
|
||||||
|
{
|
||||||
|
Storage::fake('local');
|
||||||
|
|
||||||
|
$folder = $this->tokenize('../../etc/passwd');
|
||||||
|
$time = $this->tokenize(now()->addHours(6)->toDateTimeString());
|
||||||
|
|
||||||
|
$this->getJson("/check/{$folder}/{$time}")
|
||||||
|
->assertStatus(422)
|
||||||
|
->assertJson(['valid' => false]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
|
|
||||||
|
abstract class TestCase extends BaseTestCase
|
||||||
|
{
|
||||||
|
use CreatesApplication;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user