aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/BulkUpload
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib/BulkUpload')
-rw-r--r--apps/dav/lib/BulkUpload/BulkUploadPlugin.php61
-rw-r--r--apps/dav/lib/BulkUpload/MultipartRequestParser.php111
2 files changed, 85 insertions, 87 deletions
diff --git a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
index bb6baf48b56..d4faf3764e1 100644
--- a/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
+++ b/apps/dav/lib/BulkUpload/BulkUploadPlugin.php
@@ -1,47 +1,27 @@
<?php
+
/**
- * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
- *
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\BulkUpload;
+use OCA\DAV\Connector\Sabre\MtimeSanitizer;
+use OCP\AppFramework\Http;
+use OCP\Files\DavUtil;
+use OCP\Files\Folder;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
-use OCP\Files\Folder;
-use OCP\AppFramework\Http;
-use OCA\DAV\Connector\Sabre\MtimeSanitizer;
class BulkUploadPlugin extends ServerPlugin {
-
- /** @var Folder */
- private $userFolder;
-
- /** @var LoggerInterface */
- private $logger;
-
- public function __construct(Folder $userFolder, LoggerInterface $logger) {
- $this->userFolder = $userFolder;
- $this->logger = $logger;
+ public function __construct(
+ private Folder $userFolder,
+ private LoggerInterface $logger,
+ ) {
}
/**
@@ -60,11 +40,11 @@ class BulkUploadPlugin extends ServerPlugin {
*/
public function httpPost(RequestInterface $request, ResponseInterface $response): bool {
// Limit bulk upload to the /dav/bulk endpoint
- if ($request->getPath() !== "bulk") {
+ if ($request->getPath() !== 'bulk') {
return true;
}
- $multiPartParser = new MultipartRequestParser($request);
+ $multiPartParser = new MultipartRequestParser($request, $this->logger);
$writtenFiles = [];
while (!$multiPartParser->isAtLastBoundary()) {
@@ -74,7 +54,7 @@ class BulkUploadPlugin extends ServerPlugin {
// Return early if an error occurs during parsing.
$this->logger->error($e->getMessage());
$response->setStatus(Http::STATUS_BAD_REQUEST);
- $response->setBody(json_encode($writtenFiles));
+ $response->setBody(json_encode($writtenFiles, JSON_THROW_ON_ERROR));
return false;
}
@@ -90,22 +70,25 @@ class BulkUploadPlugin extends ServerPlugin {
$node = $this->userFolder->newFile($headers['x-file-path'], $content);
$node->touch($mtime);
+ $node = $this->userFolder->getFirstNodeById($node->getId());
$writtenFiles[$headers['x-file-path']] = [
- "error" => false,
- "etag" => $node->getETag(),
+ 'error' => false,
+ 'etag' => $node->getETag(),
+ 'fileid' => DavUtil::getDavFileId($node->getId()),
+ 'permissions' => DavUtil::getDavPermissions($node),
];
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['path' => $headers['x-file-path']]);
$writtenFiles[$headers['x-file-path']] = [
- "error" => true,
- "message" => $e->getMessage(),
+ 'error' => true,
+ 'message' => $e->getMessage(),
];
}
}
$response->setStatus(Http::STATUS_OK);
- $response->setBody(json_encode($writtenFiles));
+ $response->setBody(json_encode($writtenFiles, JSON_THROW_ON_ERROR));
return false;
}
diff --git a/apps/dav/lib/BulkUpload/MultipartRequestParser.php b/apps/dav/lib/BulkUpload/MultipartRequestParser.php
index 7554447fc93..50f8cff76ba 100644
--- a/apps/dav/lib/BulkUpload/MultipartRequestParser.php
+++ b/apps/dav/lib/BulkUpload/MultipartRequestParser.php
@@ -1,32 +1,18 @@
<?php
+
/**
- * @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
- *
- * @author Louis Chemineau <louis@chmn.me>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * 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, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\BulkUpload;
-use Sabre\HTTP\RequestInterface;
+use OCP\AppFramework\Http;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\LengthRequired;
-use OCP\AppFramework\Http;
+use Sabre\HTTP\RequestInterface;
class MultipartRequestParser {
@@ -34,15 +20,18 @@ class MultipartRequestParser {
private $stream;
/** @var string */
- private $boundary = "";
+ private $boundary = '';
/** @var string */
- private $lastBoundary = "";
+ private $lastBoundary = '';
/**
* @throws BadRequest
*/
- public function __construct(RequestInterface $request) {
+ public function __construct(
+ RequestInterface $request,
+ protected LoggerInterface $logger,
+ ) {
$stream = $request->getBody();
$contentType = $request->getHeader('Content-Type');
@@ -51,14 +40,14 @@ class MultipartRequestParser {
}
if ($contentType === null) {
- throw new BadRequest("Content-Type can not be null");
+ throw new BadRequest('Content-Type can not be null');
}
$this->stream = $stream;
$boundary = $this->parseBoundaryFromHeaders($contentType);
- $this->boundary = '--'.$boundary."\r\n";
- $this->lastBoundary = '--'.$boundary."--\r\n";
+ $this->boundary = '--' . $boundary . "\r\n";
+ $this->lastBoundary = '--' . $boundary . "--\r\n";
}
/**
@@ -69,16 +58,22 @@ class MultipartRequestParser {
*/
private function parseBoundaryFromHeaders(string $contentType): string {
try {
+ if (!str_contains($contentType, ';')) {
+ throw new \InvalidArgumentException('No semicolon in header');
+ }
[$mimeType, $boundary] = explode(';', $contentType);
+ if (!str_contains($boundary, '=')) {
+ throw new \InvalidArgumentException('No equal in boundary header');
+ }
[$boundaryKey, $boundaryValue] = explode('=', $boundary);
} catch (\Exception $e) {
- throw new BadRequest("Error while parsing boundary in Content-Type header.", Http::STATUS_BAD_REQUEST, $e);
+ throw new BadRequest('Error while parsing boundary in Content-Type header.', Http::STATUS_BAD_REQUEST, $e);
}
$boundaryValue = trim($boundaryValue);
// Remove potential quotes around boundary value.
- if (substr($boundaryValue, 0, 1) == '"' && substr($boundaryValue, -1) == '"') {
+ if (str_starts_with($boundaryValue, '"') && str_ends_with($boundaryValue, '"')) {
$boundaryValue = substr($boundaryValue, 1, -1);
}
@@ -108,7 +103,7 @@ class MultipartRequestParser {
$seekBackResult = fseek($this->stream, -$expectedContentLength, SEEK_CUR);
if ($seekBackResult === -1) {
- throw new Exception("Unknown error while seeking content", Http::STATUS_INTERNAL_SERVER_ERROR);
+ throw new Exception('Unknown error while seeking content', Http::STATUS_INTERNAL_SERVER_ERROR);
}
return $expectedContent === $content;
@@ -146,7 +141,10 @@ class MultipartRequestParser {
$headers = $this->readPartHeaders();
- $content = $this->readPartContent($headers["content-length"], $headers["x-file-md5"]);
+ $length = (int)$headers['content-length'];
+
+ $this->validateHash($length, $headers['x-file-md5'] ?? '', $headers['oc-checksum'] ?? '');
+ $content = $this->readPartContent($length);
return [$headers, $content];
}
@@ -158,7 +156,7 @@ class MultipartRequestParser {
*/
private function readBoundary(): string {
if (!$this->isAtBoundary()) {
- throw new BadRequest("Boundary not found where it should be.");
+ throw new BadRequest('Boundary not found where it should be.');
}
return fread($this->stream, strlen($this->boundary));
@@ -179,6 +177,11 @@ class MultipartRequestParser {
throw new Exception('An error occurred while reading headers of a part');
}
+ if (!str_contains($line, ':')) {
+ $this->logger->error('Header missing ":" on bulk request: ' . json_encode($line));
+ throw new Exception('An error occurred while reading headers of a part', Http::STATUS_BAD_REQUEST);
+ }
+
try {
[$key, $value] = explode(':', $line, 2);
$headers[strtolower(trim($key))] = trim($value);
@@ -187,12 +190,13 @@ class MultipartRequestParser {
}
}
- if (!isset($headers["content-length"])) {
- throw new LengthRequired("The Content-Length header must not be null.");
+ if (!isset($headers['content-length'])) {
+ throw new LengthRequired('The Content-Length header must not be null.');
}
- if (!isset($headers["x-file-md5"])) {
- throw new BadRequest("The X-File-MD5 header must not be null.");
+ // TODO: Drop $md5 condition when the latest desktop client that uses it is no longer supported.
+ if (!isset($headers['x-file-md5']) && !isset($headers['oc-checksum'])) {
+ throw new BadRequest('The hash headers must not be null.');
}
return $headers;
@@ -204,21 +208,19 @@ class MultipartRequestParser {
* @throws Exception
* @throws BadRequest
*/
- private function readPartContent(int $length, string $md5): string {
- $computedMd5 = $this->computeMd5Hash($length);
-
- if ($md5 !== $computedMd5) {
- throw new BadRequest("Computed md5 hash is incorrect.");
+ private function readPartContent(int $length): string {
+ if ($length === 0) {
+ $content = '';
+ } else {
+ $content = stream_get_line($this->stream, $length);
}
- $content = stream_get_line($this->stream, $length);
-
if ($content === false) {
throw new Exception("Fail to read part's content.");
}
- if (feof($this->stream)) {
- throw new Exception("Unexpected EOF while reading stream.");
+ if ($length !== 0 && feof($this->stream)) {
+ throw new Exception('Unexpected EOF while reading stream.');
}
// Read '\r\n'.
@@ -228,12 +230,25 @@ class MultipartRequestParser {
}
/**
- * Compute the MD5 hash of the next x bytes.
+ * Compute the MD5 or checksum hash of the next x bytes.
+ * TODO: Drop $md5 argument when the latest desktop client that uses it is no longer supported.
*/
- private function computeMd5Hash(int $length): string {
- $context = hash_init('md5');
+ private function validateHash(int $length, string $fileMd5Header, string $checksumHeader): void {
+ if ($checksumHeader !== '') {
+ [$algorithm, $hash] = explode(':', $checksumHeader, 2);
+ } elseif ($fileMd5Header !== '') {
+ $algorithm = 'md5';
+ $hash = $fileMd5Header;
+ } else {
+ throw new BadRequest('No hash provided.');
+ }
+
+ $context = hash_init($algorithm);
hash_update_stream($context, $this->stream, $length);
fseek($this->stream, -$length, SEEK_CUR);
- return hash_final($context);
+ $computedHash = hash_final($context);
+ if ($hash !== $computedHash) {
+ throw new BadRequest("Computed $algorithm hash is incorrect ($computedHash).");
+ }
}
}