5
5
*/
6
6
namespace OCA \CloudFederationAPI \Controller ;
7
7
8
+ use NCU \Security \Signature \Exceptions \IncomingRequestException ;
9
+ use NCU \Security \Signature \Exceptions \SignatoryNotFoundException ;
10
+ use NCU \Security \Signature \Exceptions \SignatureException ;
11
+ use NCU \Security \Signature \Exceptions \SignatureNotFoundException ;
12
+ use NCU \Security \Signature \ISignatureManager ;
13
+ use NCU \Security \Signature \Model \IIncomingSignedRequest ;
14
+ use OC \OCM \OCMSignatoryManager ;
8
15
use OCA \CloudFederationAPI \Config ;
9
16
use OCA \CloudFederationAPI \ResponseDefinitions ;
10
17
use OCP \AppFramework \Controller ;
22
29
use OCP \Federation \ICloudFederationFactory ;
23
30
use OCP \Federation \ICloudFederationProviderManager ;
24
31
use OCP \Federation \ICloudIdManager ;
32
+ use OCP \IAppConfig ;
25
33
use OCP \IGroupManager ;
26
34
use OCP \IRequest ;
27
35
use OCP \IURLGenerator ;
28
36
use OCP \IUserManager ;
29
37
use OCP \Share \Exceptions \ShareNotFound ;
38
+ use OCP \Share \IProviderFactory ;
39
+ use OCP \Share \IShare ;
30
40
use OCP \Util ;
31
41
use Psr \Log \LoggerInterface ;
32
42
@@ -50,8 +60,12 @@ public function __construct(
50
60
private IURLGenerator $ urlGenerator ,
51
61
private ICloudFederationProviderManager $ cloudFederationProviderManager ,
52
62
private Config $ config ,
63
+ private readonly IAppConfig $ appConfig ,
53
64
private ICloudFederationFactory $ factory ,
54
65
private ICloudIdManager $ cloudIdManager ,
66
+ private readonly ISignatureManager $ signatureManager ,
67
+ private readonly OCMSignatoryManager $ signatoryManager ,
68
+ private readonly IProviderFactory $ shareProviderFactory ,
55
69
) {
56
70
parent ::__construct ($ appName , $ request );
57
71
}
@@ -81,11 +95,20 @@ public function __construct(
81
95
#[NoCSRFRequired]
82
96
#[BruteForceProtection(action: 'receiveFederatedShare ' )]
83
97
public function addShare ($ shareWith , $ name , $ description , $ providerId , $ owner , $ ownerDisplayName , $ sharedBy , $ sharedByDisplayName , $ protocol , $ shareType , $ resourceType ) {
98
+ try {
99
+ // if request is signed and well signed, no exception are thrown
100
+ // if request is not signed and host is known for not supporting signed request, no exception are thrown
101
+ $ signedRequest = $ this ->getSignedRequest ();
102
+ $ this ->confirmSignedOrigin ($ signedRequest , 'owner ' , $ owner );
103
+ } catch (IncomingRequestException $ e ) {
104
+ $ this ->logger ->warning ('incoming request exception ' , ['exception ' => $ e ]);
105
+ return new JSONResponse (['message ' => $ e ->getMessage (), 'validationErrors ' => []], Http::STATUS_BAD_REQUEST );
106
+ }
107
+
84
108
// check if all required parameters are set
85
109
if ($ shareWith === null ||
86
110
$ name === null ||
87
111
$ providerId === null ||
88
- $ owner === null ||
89
112
$ resourceType === null ||
90
113
$ shareType === null ||
91
114
!is_array ($ protocol ) ||
@@ -208,6 +231,16 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $
208
231
#[PublicPage]
209
232
#[BruteForceProtection(action: 'receiveFederatedShareNotification ' )]
210
233
public function receiveNotification ($ notificationType , $ resourceType , $ providerId , ?array $ notification ) {
234
+ try {
235
+ // if request is signed and well signed, no exception are thrown
236
+ // if request is not signed and host is known for not supporting signed request, no exception are thrown
237
+ $ signedRequest = $ this ->getSignedRequest ();
238
+ $ this ->confirmShareOrigin ($ signedRequest , $ notification ['sharedSecret ' ] ?? '' );
239
+ } catch (IncomingRequestException $ e ) {
240
+ $ this ->logger ->warning ('incoming request exception ' , ['exception ' => $ e ]);
241
+ return new JSONResponse (['message ' => $ e ->getMessage (), 'validationErrors ' => []], Http::STATUS_BAD_REQUEST );
242
+ }
243
+
211
244
// check if all required parameters are set
212
245
if ($ notificationType === null ||
213
246
$ resourceType === null ||
@@ -286,4 +319,124 @@ private function mapUid($uid) {
286
319
287
320
return $ uid ;
288
321
}
322
+
323
+
324
+ /**
325
+ * returns signed request if available.
326
+ * throw an exception:
327
+ * - if request is signed, but wrongly signed
328
+ * - if request is not signed but instance is configured to only accept signed ocm request
329
+ *
330
+ * @return IIncomingSignedRequest|null null if remote does not (and never did) support signed request
331
+ * @throws IncomingRequestException
332
+ */
333
+ private function getSignedRequest (): ?IIncomingSignedRequest {
334
+ try {
335
+ return $ this ->signatureManager ->getIncomingSignedRequest ($ this ->signatoryManager );
336
+ } catch (SignatureNotFoundException |SignatoryNotFoundException $ e ) {
337
+ // remote does not support signed request.
338
+ // currently we still accept unsigned request until lazy appconfig
339
+ // core.enforce_signed_ocm_request is set to true (default: false)
340
+ if ($ this ->appConfig ->getValueBool ('core ' , OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED , lazy: true )) {
341
+ $ this ->logger ->notice ('ignored unsigned request ' , ['exception ' => $ e ]);
342
+ throw new IncomingRequestException ('Unsigned request ' );
343
+ }
344
+ } catch (SignatureException $ e ) {
345
+ $ this ->logger ->notice ('wrongly signed request ' , ['exception ' => $ e ]);
346
+ throw new IncomingRequestException ('Invalid signature ' );
347
+ }
348
+ return null ;
349
+ }
350
+
351
+
352
+ /**
353
+ * confirm that the value related to $key entry from the payload is in format userid@hostname
354
+ * and compare hostname with the origin of the signed request.
355
+ *
356
+ * If request is not signed, we still verify that the hostname from the extracted value does,
357
+ * actually, not support signed request
358
+ *
359
+ * @param IIncomingSignedRequest|null $signedRequest
360
+ * @param string $key entry from data available in data
361
+ * @param string $value value itself used in case request is not signed
362
+ *
363
+ * @throws IncomingRequestException
364
+ */
365
+ private function confirmSignedOrigin (?IIncomingSignedRequest $ signedRequest , string $ key , string $ value ): void {
366
+ if ($ signedRequest === null ) {
367
+ $ instance = $ this ->getHostFromFederationId ($ value );
368
+ try {
369
+ $ this ->signatureManager ->searchSignatory ($ instance );
370
+ throw new IncomingRequestException ('instance is supposed to sign its request ' );
371
+ } catch (SignatoryNotFoundException ) {
372
+ return ;
373
+ }
374
+ }
375
+
376
+ $ body = json_decode ($ signedRequest ->getBody (), true ) ?? [];
377
+ $ entry = trim ($ body [$ key ] ?? '' , '@ ' );
378
+ if ($ this ->getHostFromFederationId ($ entry ) !== $ signedRequest ->getOrigin ()) {
379
+ throw new IncomingRequestException ('share initiation from different instance ' );
380
+ }
381
+ }
382
+
383
+
384
+ /**
385
+ * confirm that the value related to share token is in format userid@hostname
386
+ * and compare hostname with the origin of the signed request.
387
+ *
388
+ * If request is not signed, we still verify that the hostname from the extracted value does,
389
+ * actually, not support signed request
390
+ *
391
+ * @param IIncomingSignedRequest|null $signedRequest
392
+ * @param string $token
393
+ *
394
+ * @return void
395
+ * @throws IncomingRequestException
396
+ */
397
+ private function confirmShareOrigin (?IIncomingSignedRequest $ signedRequest , string $ token ): void {
398
+ if ($ token === '' ) {
399
+ throw new BadRequestException (['sharedSecret ' ]);
400
+ }
401
+
402
+ $ provider = $ this ->shareProviderFactory ->getProviderForType (IShare::TYPE_REMOTE );
403
+ $ share = $ provider ->getShareByToken ($ token );
404
+ $ entry = $ share ->getSharedWith ();
405
+
406
+ $ instance = $ this ->getHostFromFederationId ($ entry );
407
+ if ($ signedRequest === null ) {
408
+ try {
409
+ $ this ->signatureManager ->searchSignatory ($ instance );
410
+ throw new IncomingRequestException ('instance is supposed to sign its request ' );
411
+ } catch (SignatoryNotFoundException ) {
412
+ return ;
413
+ }
414
+ } elseif ($ instance !== $ signedRequest ->getOrigin ()) {
415
+ throw new IncomingRequestException ('token sharedWith from different instance ' );
416
+ }
417
+ }
418
+
419
+ /**
420
+ * @param string $entry
421
+ * @return string
422
+ * @throws IncomingRequestException
423
+ */
424
+ private function getHostFromFederationId (string $ entry ): string {
425
+ if (!str_contains ($ entry , '@ ' )) {
426
+ throw new IncomingRequestException ('entry does not contains @ ' );
427
+ }
428
+ [, $ rightPart ] = explode ('@ ' , $ entry , 2 );
429
+
430
+ $ host = parse_url ($ rightPart , PHP_URL_HOST );
431
+ $ port = parse_url ($ rightPart , PHP_URL_PORT );
432
+ if ($ port !== null && $ port !== false ) {
433
+ $ host .= ': ' . $ port ;
434
+ }
435
+
436
+ if (is_string ($ host ) && $ host !== '' ) {
437
+ return $ host ;
438
+ }
439
+
440
+ throw new IncomingRequestException ('host is empty ' );
441
+ }
289
442
}
0 commit comments