Skip to content

Commit

Permalink
Bug/999 download file via new api (#1130)
Browse files Browse the repository at this point in the history
* FEAT added getFile helper endpoint to download files in new API

* FEAT add etag support on getFile

* cleaned up getFile helper

* FEAT added test for getFile helper

* FEAT added tests for Range request in getFile helper

* Fixed code style suggestion in ci/apiv2/hashtopolis.py
  • Loading branch information
jessevz authored Nov 21, 2024
1 parent dbeb8a9 commit fd5ba50
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 0 deletions.
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);

0 comments on commit fd5ba50

Please sign in to comment.