Fix URKL encoding hellscape; closes #2664 closes #2663

This commit is contained in:
Dane Everitt 2020-11-06 20:47:03 -08:00
parent 009f9c297d
commit 625fd92130
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
9 changed files with 32 additions and 17 deletions

View file

@ -6,6 +6,7 @@ use Carbon\CarbonImmutable;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use Pterodactyl\Services\Nodes\NodeJWTService; use Pterodactyl\Services\Nodes\NodeJWTService;
use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Contracts\Routing\ResponseFactory;
use Pterodactyl\Repositories\Wings\DaemonFileRepository; use Pterodactyl\Repositories\Wings\DaemonFileRepository;
@ -70,7 +71,7 @@ class FileController extends ClientApiController
{ {
$contents = $this->fileRepository $contents = $this->fileRepository
->setServer($server) ->setServer($server)
->getDirectory(urlencode(urldecode($request->get('directory') ?? '/'))); ->getDirectory($this->encode($request->get('directory') ?? '/'));
return $this->fractal->collection($contents) return $this->fractal->collection($contents)
->transformWith($this->getTransformer(FileObjectTransformer::class)) ->transformWith($this->getTransformer(FileObjectTransformer::class))
@ -91,7 +92,7 @@ class FileController extends ClientApiController
{ {
return new Response( return new Response(
$this->fileRepository->setServer($server)->getContent( $this->fileRepository->setServer($server)->getContent(
urlencode(urldecode($request->get('file'))), config('pterodactyl.files.max_edit_size') $this->encode($request->get('file')), config('pterodactyl.files.max_edit_size')
), ),
Response::HTTP_OK, Response::HTTP_OK,
['Content-Type' => 'text/plain'] ['Content-Type' => 'text/plain']
@ -113,7 +114,7 @@ class FileController extends ClientApiController
$token = $this->jwtService $token = $this->jwtService
->setExpiresAt(CarbonImmutable::now()->addMinutes(15)) ->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
->setClaims([ ->setClaims([
'file_path' => $request->get('file'), 'file_path' => rawurldecode($request->get('file')),
'server_uuid' => $server->uuid, 'server_uuid' => $server->uuid,
]) ])
->handle($server->node, $request->user()->id . $server->uuid); ->handle($server->node, $request->user()->id . $server->uuid);
@ -142,7 +143,7 @@ class FileController extends ClientApiController
public function write(WriteFileContentRequest $request, Server $server): JsonResponse public function write(WriteFileContentRequest $request, Server $server): JsonResponse
{ {
$this->fileRepository->setServer($server)->putContent( $this->fileRepository->setServer($server)->putContent(
$request->get('file'), $this->encode($request->get('file')),
$request->getContent() $request->getContent()
); );
@ -261,4 +262,18 @@ class FileController extends ClientApiController
return new JsonResponse([], Response::HTTP_NO_CONTENT); return new JsonResponse([], Response::HTTP_NO_CONTENT);
} }
/**
* Encodes a given file name & path in a format that should work for a good majority
* of file names without too much confusing logic.
*
* @param string $path
* @return string
*/
private function encode(string $path): string
{
return Collection::make(explode('/', rawurldecode($path)))->map(function ($value) {
return rawurlencode($value);
})->join('/');
}
} }

View file

@ -3,7 +3,7 @@ import http from '@/api/http';
export default (server: string, file: string): Promise<string> => { export default (server: string, file: string): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http.get(`/api/client/servers/${server}/files/contents`, { http.get(`/api/client/servers/${server}/files/contents`, {
params: { file: file.split('/').map(item => encodeURIComponent(item)).join('/') }, params: { file: encodeURI(decodeURI(file)) },
transformResponse: res => res, transformResponse: res => res,
responseType: 'text', responseType: 'text',
}) })

View file

@ -17,7 +17,7 @@ export interface FileObject {
export default async (uuid: string, directory?: string): Promise<FileObject[]> => { export default async (uuid: string, directory?: string): Promise<FileObject[]> => {
const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, { const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, {
params: { directory: directory?.split('/').map(item => encodeURIComponent(item)).join('/') }, params: { directory: encodeURI(directory ?? '/') },
}); });
return (data.data || []).map(rawDataToFileObject); return (data.data || []).map(rawDataToFileObject);

View file

@ -2,7 +2,7 @@ import http from '@/api/http';
export default async (uuid: string, file: string, content: string): Promise<void> => { export default async (uuid: string, file: string, content: string): Promise<void> => {
await http.post(`/api/client/servers/${uuid}/files/write`, content, { await http.post(`/api/client/servers/${uuid}/files/write`, content, {
params: { file }, params: { file: encodeURI(decodeURI(file)) },
headers: { headers: {
'Content-Type': 'text/plain', 'Content-Type': 'text/plain',
}, },

View file

@ -61,7 +61,7 @@ export default () => {
setLoading(true); setLoading(true);
clearFlashes('files:view'); clearFlashes('files:view');
fetchFileContent() fetchFileContent()
.then(content => saveFileContents(uuid, encodeURIComponent(name || hash.replace(/^#/, '')), content)) .then(content => saveFileContents(uuid, name || hash.replace(/^#/, ''), content))
.then(() => { .then(() => {
if (name) { if (name) {
history.push(`/server/${id}/files/edit#/${name}`); history.push(`/server/${id}/files/edit#/${name}`);

View file

@ -33,10 +33,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
.filter(directory => !!directory) .filter(directory => !!directory)
.map((directory, index, dirs) => { .map((directory, index, dirs) => {
if (!withinFileEditor && index === dirs.length - 1) { if (!withinFileEditor && index === dirs.length - 1) {
return { name: decodeURIComponent(encodeURIComponent(directory)) }; return { name: directory };
} }
return { name: decodeURIComponent(encodeURIComponent(directory)), path: `/${dirs.slice(0, index + 1).join('/')}` }; return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` };
}); });
const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => { const onSelectAllClick = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -79,7 +79,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
} }
{file && {file &&
<React.Fragment> <React.Fragment>
<span css={tw`px-1 text-neutral-300`}>{decodeURIComponent(encodeURIComponent(file))}</span> <span css={tw`px-1 text-neutral-300`}>{decodeURI(file)}</span>
</React.Fragment> </React.Fragment>
} }
</div> </div>

View file

@ -36,7 +36,7 @@ export default () => {
useEffect(() => { useEffect(() => {
clearFlashes('files'); clearFlashes('files');
setSelectedFiles([]); setSelectedFiles([]);
setDirectory(hash.length > 0 ? hash : '/'); setDirectory(hash.length > 0 ? decodeURI(hash) : '/');
}, [ hash ]); }, [ hash ]);
useEffect(() => { useEffect(() => {

View file

@ -24,6 +24,8 @@ const Clickable: React.FC<{ file: FileObject }> = memo(({ file, children }) => {
const history = useHistory(); const history = useHistory();
const match = useRouteMatch(); const match = useRouteMatch();
const destination = cleanDirectoryPath(`${directory}/${file.name}`).split('/').map(v => encodeURI(v)).join('/');
const onRowClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { const onRowClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
// Don't rely on the onClick to work with the generated URL. Because of the way this // Don't rely on the onClick to work with the generated URL. Because of the way this
// component re-renders you'll get redirected into a nested directory structure since // component re-renders you'll get redirected into a nested directory structure since
@ -32,7 +34,7 @@ const Clickable: React.FC<{ file: FileObject }> = memo(({ file, children }) => {
// Just trust me future me, leave this be. // Just trust me future me, leave this be.
if (!file.isFile) { if (!file.isFile) {
e.preventDefault(); e.preventDefault();
history.push(`#${cleanDirectoryPath(`${directory}/${file.name}`)}`); history.push(`#${destination}`);
} }
}; };
@ -43,7 +45,7 @@ const Clickable: React.FC<{ file: FileObject }> = memo(({ file, children }) => {
</div> </div>
: :
<NavLink <NavLink
to={`${match.url}/${file.isFile ? 'edit/' : ''}#${cleanDirectoryPath(`${directory}/${file.name}`)}`} to={`${match.url}/${file.isFile ? 'edit/' : ''}#${destination}`}
css={tw`flex flex-1 text-neutral-300 no-underline p-3 overflow-hidden truncate`} css={tw`flex flex-1 text-neutral-300 no-underline p-3 overflow-hidden truncate`}
onClick={onRowClick} onClick={onRowClick}
> >

View file

@ -92,9 +92,7 @@ export default ({ className }: WithClassname) => {
<span css={tw`text-neutral-200`}>This directory will be created as</span> <span css={tw`text-neutral-200`}>This directory will be created as</span>
&nbsp;/home/container/ &nbsp;/home/container/
<span css={tw`text-cyan-200`}> <span css={tw`text-cyan-200`}>
{decodeURIComponent(encodeURIComponent( {join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, '')}
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
))}
</span> </span>
</p> </p>
<div css={tw`flex justify-end`}> <div css={tw`flex justify-end`}>