Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions ci/apiv2/hashtopolis.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,22 @@ def _helper_request(self, helper_uri, payload):
else:
return self.resp_to_json(r)

def _helper_get_request_file(self, helper_uri, payload, range=None):
self.authenticate()
uri = self._api_endpoint + self._model_uri + helper_uri
headers = self._headers
if range:
headers["Range"] = range

logging.debug(f"Sending GET request to {uri}, with params:{payload}")
r = requests.get(uri, headers=headers, params=payload)
if range is None:
assert r.status_code == 200
else:
assert r.status_code == 206
logging.debug(f"received file contents: \n {r.text}")
return r.text

def _test_authentication(self, username, password):
auth_uri = self._api_endpoint + '/auth/token'
auth = (username, password)
Expand Down Expand Up @@ -898,6 +914,11 @@ def import_cracked_hashes(self, hashlist, source_data, separator):
response = self._helper_request("importCrackedHashes", payload)
return response['data']

def get_file(self, file, range=None):
payload = {
'file': file.id
}
return self._helper_get_request_file("getFile", payload, range)

def recount_file_lines(self, file):
payload = {
Expand Down
14 changes: 14 additions & 0 deletions ci/apiv2/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,17 @@ def test_recount_wordlist(self):
file = helper.recount_file_lines(file=model_obj)

self.assertEqual(file.lineCount, 3)

def test_helper_get_file(self):
model_obj = self.create_test_object()

helper = Helper()
file_data = helper.get_file(file=model_obj)
self.assertEqual(file_data, "12345678\n123456\nprincess\n")

def test_range_request_get_file(self):
model_obj = self.create_test_object()

helper = Helper()
file_data = helper.get_file(file=model_obj, range="bytes=9-15")
self.assertEqual(file_data, "123456\n")
1 change: 1 addition & 0 deletions src/api/v2/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ public function process(Request $request, RequestHandler $handler): Response {
require __DIR__ . "/../../inc/apiv2/helper/exportCrackedHashes.routes.php";
require __DIR__ . "/../../inc/apiv2/helper/exportLeftHashes.routes.php";
require __DIR__ . "/../../inc/apiv2/helper/exportWordlist.routes.php";
require __DIR__ . "/../../inc/apiv2/helper/getFile.routes.php";
require __DIR__ . "/../../inc/apiv2/helper/importCrackedHashes.routes.php";
require __DIR__ . "/../../inc/apiv2/helper/importFile.routes.php";
require __DIR__ . "/../../inc/apiv2/helper/purgeTask.routes.php";
Expand Down
175 changes: 175 additions & 0 deletions src/inc/apiv2/helper/getFile.routes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php
use DBA\File;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use DBA\Factory;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpForbiddenException;

require_once(dirname(__FILE__) . "/../common/AbstractHelperAPI.class.php");

class getFileHelperAPI extends AbstractHelperAPI {
public static function getBaseUri(): string {
return "/api/v2/helper/getFile";
}

public static function getAvailableMethods(): array {
return ['GET'];
}

public function getRequiredPermissions(string $method): array {
return [File::PERM_READ];
}

public function actionPost(array $data): object|array|null
{
assert(False, "GetFile has no POST");
}

public function validateFile($request, $file_id) {
if (!is_numeric($file_id)) {
throw new HTException("Invalid file id given: " . $file_id);
}
$file = Factory::getFileFactory()->get($file_id);
if (!$file) {
throw new HttpNotFoundException($request, "No file with id: " . $file_id);
}
$filename = Factory::getStoredValueFactory()->get(DDirectories::FILES)->getVal() . "/" . $file->getFilename();
//checks below should never trigger
if (!file_exists($filename)) {
throw new HttpNotFoundException($request, "File not found at filesystem");
}
if (!is_readable($filename)) {
throw new HttpForbiddenException($request, "Not allowed to read file");
}

return $filename;
}

/**
* Handles HTTP range requests for partial conten delivery
*
* This method processes the `Range` header from the HTTP request
* to determine the start and end byte positions for the response,
* ensuring the range is valid and updates the file pointer accordingly.
*
* @param int &$start A reference to the starting byte of the range. This value will be updated.
* @param int &$end A reference to the ending byte of the range. This value will be updated.
* @param int &$size The total size of the content in bytes.
* @param resource &$fp A file pointer resource to seek to the correct position for the range.
* @return bool Returns `true` if the range request is valid and successfully processed, or `false` otherwise.
*
* @throws InvalidArgumentException If the `Range` header is malformed.
*
* @note This function assumes the presence of the `HTTP_RANGE` header in the `$_SERVER` superglobal.
*/
protected function handleRangeRequest(int &$start, int &$end, int &$size, &$fp): bool {

$c_start = $start;
$c_end = $end;

list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);

if (strpos($range, ',') !== false) {
return false;
}
if ($range == '-') {
$c_start = $size - substr($range, 1);
}
else {
$range = explode('-', $range);
$c_start = $range[0];
if ((isset($range[1]) && is_numeric($range[1]))) {
$c_end = $range[1];
}
else {
$c_end = $size;
}
}
if ($c_end > $end) {
$c_end = $end;
}
if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
return false;
}
$start = $c_start;
$end = $c_end;
fseek($fp, $start);
return true;
}

public function handleGet(Request $request, Response $response): Response {
$this->preCommon($request);
$file_id = intval($request->getQueryParams()['file']);

$filename = $this->validateFile($request, $file_id);

$size = Util::filesize($filename);
$lastModified = filemtime($filename);

$etag = md5($lastModified . $size);
$ifNoneMatch = $request->getHeaderLine('If-None-Match');
if ($ifNoneMatch === $etag) {
return $response->withStatus(304);
}

$exp = explode(".", $filename);
if ($exp[sizeof($exp) - 1] == '7z') {
$contentType = "application/x-7z-compressed";
} else {
$contentType = "application/force-download";
}
$fp = @fopen($filename, "rb");

if (!$fp) {
throw new HttpForbiddenException($request, "Can't open the file");
}

$start = 0; // Start byte
$end = $size - 1; // End byte

$status = 200;
if (isset($_SERVER['HTTP_RANGE'])) {
if(!$this->handleRangeRequest($start, $end, $size, $fp)) {
fclose($fp);
return $response->withStatus(416)
->withHeader("Content-Range", "bytes $start-$end/$size");
} else {
$status = 206;
}
}

$length = $end - $start + 1; //content-length
$buffer = 1024 * 100;
$stream = $response->getBody();
while (!feof($fp) && ($p = ftell($fp)) <= $end) {
if ($p + $buffer > $end) {
$buffer = $end - $p + 1;
}
$stream->write(fread($fp, $buffer));
}
fclose($fp);

return $response->withStatus($status)
->withHeader("Content-Type", $contentType)
->withHeader("Content-Description", $filename)
->withHeader("Content-Disposition", "attachment; filename=\"" . $filename . "\"")
->withHeader("Accept-Ranges", "Byte")
->withHeader("Content-Range", "bytes $start-$end/$size")
->withHeader("Content-Length", $length)
->withHeader("ETag", $etag);
}

static public function register($app): void
{
$baseUri = getFileHelperAPI::getBaseUri();

/* Allow CORS preflight requests */
$app->options($baseUri, function (Request $request, Response $response): Response {
return $response;
});
$app->get($baseUri, "getFileHelperAPI:handleGet");
}
}

getFileHelperAPI::register($app);