Skip to content

Commit

Permalink
switch to registration via POST
Browse files Browse the repository at this point in the history
- download relevant data is sent per POST, a token is received
- download is started by GET and the received token
- solves the disadvantages from XHR-only download
- job to cleanup download tokens

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
  • Loading branch information
blizzz committed Mar 17, 2021
1 parent 323fb60 commit 699d9e8
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 19 deletions.
1 change: 1 addition & 0 deletions apps/files/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<job>OCA\Files\BackgroundJob\ScanFiles</job>
<job>OCA\Files\BackgroundJob\DeleteOrphanedItems</job>
<job>OCA\Files\BackgroundJob\CleanupFileLocks</job>
<job>OCA\Files\BackgroundJob\CleanupDownloadTokens</job>
<job>OCA\Files\BackgroundJob\CleanupDirectEditingTokens</job>
</background-jobs>

Expand Down
7 changes: 6 additions & 1 deletion apps/files/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,15 @@
'url' => '/ajax/getstoragestats.php',
'verb' => 'GET',
],
[
'name' => 'ajax#registerDownload',
'url' => '/registerDownload',
'verb' => 'POST',
],
[
'name' => 'ajax#download',
'url' => '/ajax/download.php',
'verb' => 'POST',
'verb' => 'GET',
],
[
'name' => 'API#toggleShowFolder',
Expand Down
1 change: 1 addition & 0 deletions apps/files/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@

class Application extends App implements IBootstrap {
public const APP_ID = 'files';
public const DL_TOKEN_PREFIX = 'dlToken_';

public function __construct(array $urlParams = []) {
parent::__construct(self::APP_ID, $urlParams);
Expand Down
60 changes: 60 additions & 0 deletions apps/files/lib/BackgroundJob/CleanupDownloadTokens.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\files\lib\BackgroundJob;

use OC\BackgroundJob\TimedJob;
use OCA\Files\AppInfo\Application;
use OCP\IConfig;

class CleanupDownloadTokens extends TimedJob {
private const INTERVAL_MINUTES = 24 * 60;
/** @var IConfig */
private $config;

public function __construct(IConfig $config) {
$this->interval = self::INTERVAL_MINUTES;
$this->config = $config;
}

protected function run($argument) {
$appKeys = $this->config->getAppKeys(Application::APP_ID);
foreach ($appKeys as $key) {
if (strpos($key, Application::DL_TOKEN_PREFIX) !== 0) {
continue;
}
$dataStr = $this->config->getAppValue(Application::APP_ID, $key, '');
if ($dataStr === '') {
$this->config->deleteAppValue(Application::APP_ID, $key);
continue;
}
$data = \json_decode($dataStr, true);
if (!isset($data['lastActivity']) || (time() - $data['lastActivity']) > 24 * 60 * 2) {
// deletes tokens that have not seen activity for 2 days
// the period is chosen to allow continue of downloads with network interruptions in minde
$this->config->deleteAppValue(Application::APP_ID, $key);
}
}
}
}
67 changes: 61 additions & 6 deletions apps/files/lib/Controller/AjaxController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,40 @@
namespace OCA\Files\Controller;

use OC_Files;
use OCA\Files\AppInfo\Application;
use OCA\Files\Helper;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\Security\ISecureRandom;
use function json_decode;
use function json_encode;

class AjaxController extends Controller {
/** @var ISession */
private $session;
/** @var IConfig */
private $config;

public function __construct(string $appName, IRequest $request, ISession $session) {
/** @var ISecureRandom */
private $secureRandom;

public function __construct(
string $appName,
IRequest $request,
ISession $session,
IConfig $config,
ISecureRandom $secureRandom
) {
parent::__construct($appName, $request);
$this->session = $session;
$this->request = $request;
$this->config = $config;
$this->secureRandom = $secureRandom;
}

/**
Expand All @@ -67,26 +86,62 @@ public function getStorageStats(string $dir = '/'): JSONResponse {
/**
* @NoAdminRequired
*/
public function download($files, string $dir = '', string $downloadStartSecret = '') {
public function registerDownload($files, string $dir = '', string $downloadStartSecret = '') {
if (is_string($files)) {
$files = [$files];
} elseif (!is_array($files)) {
throw new \InvalidArgumentException('Invalid argument for files');
}

$attempts = 0;
do {
if ($attempts === 10) {
throw new \RuntimeException('Failed to create unique download token');
}
$token = $this->secureRandom->generate(15);
$attempts++;
} while ($this->config->getAppValue(Application::APP_ID, Application::DL_TOKEN_PREFIX . $token, '') !== '');

$this->config->setAppValue(
Application::APP_ID,
Application::DL_TOKEN_PREFIX . $token,
json_encode([
'files' => $files,
'dir' => $dir,
'downloadStartSecret' => $downloadStartSecret,
'lastActivity' => time()
])
);

return new JSONResponse(['token' => $token]);
}

/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function download(string $token) {
$dataStr = $this->config->getAppValue(Application::APP_ID, Application::DL_TOKEN_PREFIX . $token, '');
if ($dataStr === '') {
return new NotFoundResponse();
}
$this->session->close();

if (strlen($downloadStartSecret) <= 32
&& (preg_match('!^[a-zA-Z0-9]+$!', $downloadStartSecret) === 1)
$data = json_decode($dataStr, true);
$data['lastActivity'] = time();
$this->config->setAppValue(Application::APP_ID, Application::DL_TOKEN_PREFIX . $token, json_encode($data));

if (strlen($data['downloadStartSecret']) <= 32
&& (preg_match('!^[a-zA-Z0-9]+$!', $data['downloadStartSecret']) === 1)
) {
setcookie('ocDownloadStarted', $downloadStartSecret, time() + 20, '/');
setcookie('ocDownloadStarted', $data['downloadStartSecret'], time() + 20, '/');
}

$serverParams = [ 'head' => $this->request->getMethod() === 'HEAD' ];
if (isset($_SERVER['HTTP_RANGE'])) {
$serverParams['range'] = $this->request->getHeader('Range');
}

OC_Files::get($dir, $files, $serverParams);
OC_Files::get($data['dir'], $data['files'], $serverParams);
}
}
21 changes: 9 additions & 12 deletions apps/files/src/services/Download.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,29 @@
*
*/

import fileDownload from 'js-file-download'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'

export default class Download {

export
const
async get(files, dir, downloadStartSecret) {
await axios.post(
generateUrl('apps/files/ajax/download.php'),
get(files, dir, downloadStartSecret) {
axios.post(
generateUrl('apps/files/registerDownload'),
{
files,
dir,
downloadStartSecret,
},
{
responseType: 'blob',
}
).then(res => {
const fileNameMatch = res.headers['content-disposition'].match(/filename="(.+)"/)
let fileName = ''
if (fileNameMatch.length === 2) {
fileName = fileNameMatch[1]
if (res.status === 200 && res.data.token) {
const dlUrl = generateUrl(
'apps/files/ajax/download.php?token={token}',
{ token: res.data.token }
)
OC.redirect(dlUrl)
}
fileDownload(res.data, fileName)
})
}

Expand Down

0 comments on commit 699d9e8

Please sign in to comment.