Skip to content

Commit f2fbc91

Browse files
authored
Merge pull request #4902 from nextcloud/feat/wopi-proof
feat(wopi): WOPI proof
2 parents e8696ff + 91ddbf2 commit f2fbc91

File tree

10 files changed

+734
-4
lines changed

10 files changed

+734
-4
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"ext-json": "*",
1818
"ext-simplexml": "*",
1919
"mikehaertl/php-pdftk": "^0.13.1",
20-
"league/commonmark": "^2.7"
20+
"league/commonmark": "^2.7",
21+
"phpseclib/phpseclib": "^3.0"
2122
},
2223
"require-dev": {
2324
"roave/security-advisories": "dev-master",

composer.lock

Lines changed: 228 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/Exceptions/WopiException.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
/**
3+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
4+
* SPDX-License-Identifier: AGPL-3.0-or-later
5+
*/
6+
namespace OCA\Richdocuments\Exceptions;
7+
8+
class WopiException extends \Exception {
9+
}

lib/Middleware/WOPIMiddleware.php

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@
1414
use OCA\Richdocuments\Db\WopiMapper;
1515
use OCA\Richdocuments\Exceptions\ExpiredTokenException;
1616
use OCA\Richdocuments\Exceptions\UnknownTokenException;
17+
use OCA\Richdocuments\Exceptions\WopiException;
1718
use OCA\Richdocuments\Helper;
19+
use OCA\Richdocuments\Service\DiscoveryService;
20+
use OCA\Richdocuments\Service\ProofKeyService;
1821
use OCP\AppFramework\Http;
1922
use OCP\AppFramework\Http\JSONResponse;
2023
use OCP\AppFramework\Http\Response;
2124
use OCP\AppFramework\Middleware;
2225
use OCP\Files\NotPermittedException;
2326
use OCP\IConfig;
2427
use OCP\IRequest;
28+
use OCP\IURLGenerator;
2529
use Psr\Log\LoggerInterface;
2630
use ReflectionClass;
2731
use ReflectionMethod;
@@ -30,9 +34,12 @@
3034
class WOPIMiddleware extends Middleware {
3135
public function __construct(
3236
private IConfig $config,
37+
private IURLGenerator $urlGenerator,
3338
private IRequest $request,
39+
private DiscoveryService $discoveryService,
3440
private WopiMapper $wopiMapper,
3541
private LoggerInterface $logger,
42+
private ProofKeyService $proofKeyService,
3643
private bool $isWOPIRequest = false,
3744
) {
3845
}
@@ -56,13 +63,52 @@ public function beforeController($controller, $methodName) {
5663
return;
5764
}
5865

59-
if (strpos($this->request->getRequestUri(), 'wopi/settings/upload') !== false) {
66+
if (str_contains($this->request->getRequestUri(), '/wopi/settings/upload')) {
6067
return;
6168
}
6269

6370
try {
64-
$fileId = $this->request->getParam('fileId');
6571
$accessToken = $this->request->getParam('access_token');
72+
$isWopiSettingsUrl = str_contains($this->request->getRequestUri(), '/wopi/settings');
73+
74+
if (!$isWopiSettingsUrl) {
75+
$wopiProof = $this->request->getHeader('X-WOPI-Proof');
76+
$wopiProofOld = $this->request->getHeader('X-WOPI-ProofOld');
77+
$hasProofKey = $this->discoveryService->hasProofKey();
78+
79+
// This could mean the discovery cache needs to be updated
80+
// e.g. if Collabora sends a WOPI proof but the cached discovery
81+
// says there is not one, then we should re-fetch it
82+
if ($hasProofKey !== (bool)$wopiProof) {
83+
$this->discoveryService->fetch();
84+
$hasProofKey = $this->discoveryService->hasProofKey();
85+
}
86+
87+
if ($hasProofKey) {
88+
$wopiTimestamp = $this->request->getHeader('X-WOPI-TimeStamp');
89+
$wopiTimestampIsOld = $this->proofKeyService->isOldTimestamp((int)$wopiTimestamp);
90+
91+
if ($wopiTimestampIsOld) {
92+
throw new WopiException('X-WOPI-TimeStamp header is older than 20 minutes');
93+
}
94+
95+
$url = $this->urlGenerator->getBaseUrl() . $this->request->getRequestUri();
96+
97+
$isProofValid = $this->proofKeyService->isProofValid(
98+
$accessToken,
99+
$url,
100+
$wopiTimestamp,
101+
$wopiProof,
102+
$wopiProofOld
103+
);
104+
105+
if (!$isProofValid) {
106+
throw new WopiException('Invalid WOPI proof');
107+
}
108+
}
109+
}
110+
111+
$fileId = $this->request->getParam('fileId');
66112
[$fileId, ,] = Helper::parseFileId($fileId);
67113
$wopi = $this->wopiMapper->getWopiForToken($accessToken);
68114
if ((int)$fileId !== $wopi->getFileid() && (int)$fileId !== $wopi->getTemplateId()) {
@@ -75,6 +121,11 @@ public function beforeController($controller, $methodName) {
75121
$this->logger->info('Invalid token for WOPI access', [ 'exception' => $e ]);
76122
}
77123
throw new NotPermittedException();
124+
} catch (WopiException $e) {
125+
$this->logger->error('WOPI error: ' . $e->getMessage(), [
126+
'exception' => $e,
127+
]);
128+
throw new WopiException();
78129
} catch (\Exception $e) {
79130
$this->logger->error('Failed to validate WOPI access', [ 'exception' => $e ]);
80131
throw new NotPermittedException();
@@ -84,6 +135,10 @@ public function beforeController($controller, $methodName) {
84135
}
85136

86137
public function afterException($controller, $methodName, \Exception $exception): Response {
138+
if ($exception instanceof WopiException && $controller instanceof WopiController) {
139+
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
140+
}
141+
87142
if ($exception instanceof NotPermittedException && $controller instanceof WopiController) {
88143
return new JSONResponse([], Http::STATUS_FORBIDDEN);
89144
}

0 commit comments

Comments
 (0)