Skip to content

Commit 0b9c6bd

Browse files
committed
Proxy file downloads through the panel rather than having to get creative with download tokens
1 parent 78ccdf9 commit 0b9c6bd

File tree

4 files changed

+77
-42
lines changed

4 files changed

+77
-42
lines changed

app/Http/Controllers/Api/Client/Servers/FileController.php

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,48 @@
22

33
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
44

5-
use Carbon\Carbon;
6-
use Ramsey\Uuid\Uuid;
75
use Illuminate\Http\Response;
86
use Pterodactyl\Models\Server;
9-
use Illuminate\Http\JsonResponse;
107
use GuzzleHttp\Exception\TransferException;
8+
use Illuminate\Contracts\Routing\ResponseFactory;
119
use Pterodactyl\Repositories\Wings\DaemonFileRepository;
1210
use Pterodactyl\Transformers\Daemon\FileObjectTransformer;
13-
use Illuminate\Contracts\Cache\Repository as CacheRepository;
1411
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
1512
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
1613
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest;
1714
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest;
1815
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest;
1916
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest;
2017
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest;
21-
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DownloadFileRequest;
2218
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest;
2319
use Pterodactyl\Http\Requests\Api\Client\Servers\Files\WriteFileContentRequest;
2420

2521
class FileController extends ClientApiController
2622
{
2723
/**
28-
* @var \Illuminate\Contracts\Cache\Factory
24+
* @var \Pterodactyl\Repositories\Wings\DaemonFileRepository
2925
*/
30-
private $cache;
26+
private $fileRepository;
3127

3228
/**
33-
* @var \Pterodactyl\Repositories\Wings\DaemonFileRepository
29+
* @var \Illuminate\Contracts\Routing\ResponseFactory
3430
*/
35-
private $fileRepository;
31+
private $responseFactory;
3632

3733
/**
3834
* FileController constructor.
3935
*
36+
* @param \Illuminate\Contracts\Routing\ResponseFactory $responseFactory
4037
* @param \Pterodactyl\Repositories\Wings\DaemonFileRepository $fileRepository
41-
* @param \Illuminate\Contracts\Cache\Repository $cache
4238
*/
43-
public function __construct(DaemonFileRepository $fileRepository, CacheRepository $cache)
44-
{
39+
public function __construct(
40+
ResponseFactory $responseFactory,
41+
DaemonFileRepository $fileRepository
42+
) {
4543
parent::__construct();
4644

47-
$this->cache = $cache;
4845
$this->fileRepository = $fileRepository;
46+
$this->responseFactory = $responseFactory;
4947
}
5048

5149
/**
@@ -91,6 +89,39 @@ public function getFileContents(GetFileContentsRequest $request, Server $server)
9189
);
9290
}
9391

92+
/**
93+
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\GetFileContentsRequest $request
94+
* @param \Pterodactyl\Models\Server $server
95+
* @return \Symfony\Component\HttpFoundation\StreamedResponse
96+
*
97+
* @throws \Exception
98+
*/
99+
public function download(GetFileContentsRequest $request, Server $server)
100+
{
101+
set_time_limit(0);
102+
103+
$request = $this->fileRepository->setServer($server)->streamContent(
104+
$request->get('file')
105+
);
106+
107+
$body = $request->getBody();
108+
109+
preg_match('/filename=(?<name>.*)$/', $request->getHeaderLine('Content-Disposition'), $matches);
110+
111+
return $this->responseFactory->streamDownload(
112+
function () use ($body) {
113+
while (! $body->eof()) {
114+
echo $body->read(128);
115+
}
116+
},
117+
$matches['name'] ?? 'download',
118+
[
119+
'Content-Type' => $request->getHeaderLine('Content-Type'),
120+
'Content-Length' => $request->getHeaderLine('Content-Length'),
121+
]
122+
);
123+
}
124+
94125
/**
95126
* Writes the contents of the specified file to the server.
96127
*
@@ -171,27 +202,4 @@ public function delete(DeleteFileRequest $request, Server $server): Response
171202

172203
return Response::create('', Response::HTTP_NO_CONTENT);
173204
}
174-
175-
/**
176-
* Configure a reference to a file to download in the cache so that when the
177-
* user hits the Daemon and it verifies with the Panel they'll actually be able
178-
* to download that file.
179-
*
180-
* Returns the token that needs to be used when downloading the file.
181-
*
182-
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\DownloadFileRequest $request
183-
* @param \Pterodactyl\Models\Server $server
184-
* @return \Illuminate\Http\JsonResponse
185-
* @throws \Exception
186-
*/
187-
public function download(DownloadFileRequest $request, Server $server): JsonResponse
188-
{
189-
$token = Uuid::uuid4()->toString();
190-
191-
$this->cache->put(
192-
'Server:Downloads:' . $token, ['server' => $server->uuid, 'path' => $request->route()->parameter('file')], Carbon::now()->addMinutes(5)
193-
);
194-
195-
return JsonResponse::create(['token' => $token]);
196-
}
197205
}

app/Repositories/Wings/DaemonFileRepository.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,29 @@ public function getContent(string $path, int $notLargerThan = null): string
5757
return $response->getBody()->__toString();
5858
}
5959

60+
/**
61+
* Returns a stream of a file's contents back to the calling function to allow
62+
* proxying the request through the Panel rather than needing a direct call to
63+
* the Daemon in order to work.
64+
*
65+
* @param string $path
66+
* @return \Psr\Http\Message\ResponseInterface
67+
*/
68+
public function streamContent(string $path): ResponseInterface
69+
{
70+
Assert::isInstanceOf($this->server, Server::class);
71+
72+
$response = $this->getHttpClient()->get(
73+
sprintf('/api/servers/%s/files/contents', $this->server->uuid),
74+
[
75+
'query' => ['file' => $path, 'download' => true],
76+
'stream' => true,
77+
]
78+
);
79+
80+
return $response;
81+
}
82+
6083
/**
6184
* Save new contents to a given file. This works for both creating and updating
6285
* a file.

resources/scripts/components/server/files/FileDropdownMenu.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { join } from 'path';
1313
import deleteFile from '@/api/server/files/deleteFile';
1414
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
1515
import copyFile from '@/api/server/files/copyFile';
16-
import { httpErrorToHuman } from '@/api/http';
16+
import http, { httpErrorToHuman } from '@/api/http';
1717

1818
type ModalType = 'rename' | 'move';
1919

@@ -69,6 +69,10 @@ export default ({ uuid }: { uuid: string }) => {
6969
});
7070
};
7171

72+
const doDownload = () => {
73+
window.location = `/api/client/servers/${server.uuid}/files/download?file=${join(directory, file.name)}` as unknown as Location;
74+
};
75+
7276
useEffect(() => {
7377
menuVisible
7478
? document.addEventListener('click', windowListener)
@@ -138,7 +142,10 @@ export default ({ uuid }: { uuid: string }) => {
138142
<FontAwesomeIcon icon={faCopy} className={'text-xs'}/>
139143
<span className={'ml-2'}>Copy</span>
140144
</div>
141-
<div className={'hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded'}>
145+
<div
146+
className={'hover:text-neutral-700 p-2 flex items-center hover:bg-neutral-100 rounded'}
147+
onClick={() => doDownload()}
148+
>
142149
<FontAwesomeIcon icon={faFileDownload} className={'text-xs'}/>
143150
<span className={'ml-2'}>Download</span>
144151
</div>

routes/api-client.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,12 @@
4646
Route::group(['prefix' => '/files'], function () {
4747
Route::get('/list', 'Servers\FileController@listDirectory')->name('api.client.servers.files.list');
4848
Route::get('/contents', 'Servers\FileController@getFileContents')->name('api.client.servers.files.contents');
49+
Route::get('/download', 'Servers\FileController@download');
4950
Route::put('/rename', 'Servers\FileController@renameFile')->name('api.client.servers.files.rename');
5051
Route::post('/copy', 'Servers\FileController@copyFile')->name('api.client.servers.files.copy');
5152
Route::post('/write', 'Servers\FileController@writeFileContents')->name('api.client.servers.files.write');
5253
Route::post('/delete', 'Servers\FileController@delete')->name('api.client.servers.files.delete');
5354
Route::post('/create-folder', 'Servers\FileController@createFolder')->name('api.client.servers.files.create-folder');
54-
55-
Route::post('/download/{file}', 'Servers\FileController@download')
56-
->where('file', '.*')
57-
->name('api.client.servers.files.download');
5855
});
5956

6057
Route::group(['prefix' => '/network'], function () {

0 commit comments

Comments
 (0)