-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Allow app to register a download provider #34956
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,154 @@ | ||||||||
| <?php | ||||||||
|
|
||||||||
| declare(strict_types=1); | ||||||||
|
|
||||||||
| /** | ||||||||
| * @copyright Copyright (c) 2022 Louis Chmn <[email protected]> | ||||||||
| * | ||||||||
| * @author Louis Chmn <[email protected]> | ||||||||
| * | ||||||||
| * @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\Controller; | ||||||||
|
|
||||||||
| use OC\AppFramework\Bootstrap\Coordinator; | ||||||||
| use OCP\AppFramework\Http\ZipResponse; | ||||||||
| use OCP\AppFramework\Controller; | ||||||||
| use OCP\Files\File; | ||||||||
| use OCP\Files\Folder; | ||||||||
| use OCP\Files\IFileDownloadProvider; | ||||||||
| use OCP\Files\Node; | ||||||||
| use OCP\IRequest; | ||||||||
| use Psr\Log\LoggerInterface; | ||||||||
|
|
||||||||
| class DownloadController extends Controller { | ||||||||
| private Coordinator $coordinator; | ||||||||
| private LoggerInterface $logger; | ||||||||
|
|
||||||||
| public function __construct( | ||||||||
| string $appName, | ||||||||
| IRequest $request, | ||||||||
| Coordinator $coordinator, | ||||||||
| LoggerInterface $logger | ||||||||
| ) { | ||||||||
| parent::__construct($appName, $request); | ||||||||
|
|
||||||||
| $this->request = $request; | ||||||||
| $this->coordinator = $coordinator; | ||||||||
| $this->logger = $logger; | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * @NoCSRFRequired | ||||||||
| * @PublicPage | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Please make use of the rate limit then, so guests can not DDoS the server: server/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php Lines 42 to 43 in 6312c0d
Should also add brute force protection and log attempts for guessing share tokens
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't there be a way to indicate failed guess attempts for brute force protection ? Like in:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes for brute force protection is For rate limiting this is not needed as rate limiting always kicks in. |
||||||||
| * @UserRateThrottle(limit=5, period=100) | ||||||||
| * @AnonRateThrottle(limit=1, period=100) | ||||||||
| * @BruteForceProtection(action='download_files') | ||||||||
| */ | ||||||||
| public function index(string $files): ZipResponse { | ||||||||
| $response = new ZipResponse($this->request, 'download'); | ||||||||
|
|
||||||||
| /** @var string[] */ | ||||||||
| $files = json_decode($files); | ||||||||
artonge marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
|
||||||||
| if (count($files) === 0) { | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we have an upper limit as well? Server can easily run into a timeout anyway.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Difficult to predict from the number of files. Also, what to do if we reach a timeout ? Because this prevent users from downloading their files. We can maybe create a background job that will make the file available at a specific URL. Would checking for the Same thing with the size. Not sure how the zip is created, but if this is in memory, then this will lead to OOM. So when the size reaches |
||||||||
| return $response; | ||||||||
| } | ||||||||
|
|
||||||||
| [$firstPrefix,] = explode('/', $files[0], 2); | ||||||||
| $commonPrefix = $firstPrefix; | ||||||||
| foreach ($files as $filePath) { | ||||||||
| $commonPrefix = $this->getCommonPrefix($filePath, $commonPrefix); | ||||||||
| } | ||||||||
|
|
||||||||
| foreach ($files as $filePath) { | ||||||||
| $node = null; | ||||||||
|
|
||||||||
| foreach ($this->getProviders() as $provider) { | ||||||||
| try { | ||||||||
| $node = $provider->getNode($filePath); | ||||||||
| if ($node !== null) { | ||||||||
| break; | ||||||||
| } | ||||||||
| } catch (\Throwable $ex) { | ||||||||
| $providerClass = $provider::class; | ||||||||
|
||||||||
| $this->logger->warning("Error while getting file content from $providerClass", ['exception' => $ex]); | ||||||||
|
||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| if ($node === null) { | ||||||||
| continue; | ||||||||
| } | ||||||||
|
|
||||||||
| $this->addNode($response, $node, substr($filePath, strlen($commonPrefix))); | ||||||||
| } | ||||||||
|
|
||||||||
| return $response; | ||||||||
| } | ||||||||
|
|
||||||||
| private function getCommonPrefix(string $str1, string $str2): string { | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the goal of this function? It looks very dangerous and I don't think it does what you think it does?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's to prevent having a zip file with this kind of hierarchy: files/
├─ userId/
│ ├─ folderName/
│ │ ├─ theFileIWant.txtBut only theFileIWant.txtNote that However, your concern is still valid. The best way would be to be able to split the path and then compare the parts. But I am not sure that we have a proper way to do that in PHP. Any tips ?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||
| $explodedStr1 = explode('/', $str1); | ||||||||
| $explodedStr2 = explode('/', $str2); | ||||||||
|
|
||||||||
| for ($i = 0; $i < count($explodedStr1); $i++) { | ||||||||
| if (!isset($explodedStr2[$i]) || $explodedStr1[$i] !== $explodedStr2[$i]) { | ||||||||
| $i--; | ||||||||
| break; | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| if ($i < 0) { | ||||||||
| return ''; | ||||||||
| } else { | ||||||||
| return implode(array_slice($explodedStr1, 0, $i)); | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| private function addNode(ZipResponse $response, Node $node, string $path): void { | ||||||||
| if ($node instanceof File) { | ||||||||
| $response->addResource($node->fopen('r'), $path, $node->getSize()); | ||||||||
| } | ||||||||
|
|
||||||||
| if ($node instanceof Folder) { | ||||||||
| foreach ($node->getDirectoryListing() as $subnode) { | ||||||||
| $this->addNode($response, $subnode, $path.'/'.$subnode->getName()); | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * @return IFileDownloadProvider[] | ||||||||
| */ | ||||||||
| private function getProviders() { | ||||||||
| /** @var IFileDownloadProvider[] */ | ||||||||
| $providers = []; | ||||||||
|
|
||||||||
| $context = $this->coordinator->getRegistrationContext(); | ||||||||
| if ($context === null) { | ||||||||
| throw new \Exception("Can't get download providers"); | ||||||||
| } | ||||||||
|
|
||||||||
| $providerRegistrations = $context->getFileDownloadProviders(); | ||||||||
|
|
||||||||
| foreach ($providerRegistrations as $registration) { | ||||||||
| $providers[] = \OCP\Server::get($registration->getService()); | ||||||||
| } | ||||||||
|
|
||||||||
| return $providers; | ||||||||
| } | ||||||||
| } | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * @copyright Copyright (c) 2022 Louis Chmn <[email protected]> | ||
| * | ||
| * @author Louis Chmn <[email protected]> | ||
| * | ||
| * @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\Provider; | ||
|
|
||
| use OCP\Files\Folder; | ||
| use OCP\Files\Node; | ||
| use OCP\Files\IFileDownloadProvider; | ||
|
|
||
| class FileDownloadProvider implements IFileDownloadProvider { | ||
| private ?Folder $userFolder; | ||
|
|
||
| public function __construct( | ||
| ?Folder $userFolder | ||
| ) { | ||
| $this->userFolder = $userFolder; | ||
| } | ||
|
|
||
| public function getNode(string $davPath): ?Node { | ||
| if (!str_starts_with($davPath, "files/")) { | ||
| return null; | ||
| } | ||
|
|
||
| if ($this->userFolder === null) { | ||
| return null; | ||
| } | ||
|
|
||
| /** @var ?string */ | ||
| $userId = explode('/', $davPath, 3)[1] ?? null; | ||
| if (is_null($userId) || $userId !== $this->userFolder->getOwner()->getUID()) { | ||
|
||
| return null; | ||
| } | ||
|
|
||
| $filePath = substr($davPath, strlen("files/$userId")); | ||
|
|
||
| return $this->userFolder->get($filePath); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.