1<?php 2/** 3 * @author Björn Schießle <bjoern@schiessle.org> 4 * @author Joas Schilling <coding@schilljs.com> 5 * @author Lukas Reschke <lukas@statuscode.ch> 6 * @author Thomas Müller <thomas.mueller@tmit.eu> 7 * @author Vincent Petry <pvince81@owncloud.com> 8 * 9 * @copyright Copyright (c) 2018, ownCloud GmbH 10 * @license AGPL-3.0 11 * 12 * This code is free software: you can redistribute it and/or modify 13 * it under the terms of the GNU Affero General Public License, version 3, 14 * as published by the Free Software Foundation. 15 * 16 * This program is distributed in the hope that it will be useful, 17 * but WITHOUT ANY WARRANTY; without even the implied warranty of 18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 * GNU Affero General Public License for more details. 20 * 21 * You should have received a copy of the GNU Affero General Public License, version 3, 22 * along with this program. If not, see <http://www.gnu.org/licenses/> 23 * 24 */ 25 26namespace OCA\FederatedFileSharing; 27 28use OCA\FederatedFileSharing\Ocm\NotificationManager; 29use OCP\AppFramework\Http; 30use OCP\BackgroundJob\IJobList; 31use OCP\Http\Client\IClientService; 32use OCP\IConfig; 33use GuzzleHttp\Exception\ClientException; 34 35class Notifications { 36 public const RESPONSE_FORMAT = 'json'; // default response format for ocs calls 37 38 /** @var AddressHandler */ 39 private $addressHandler; 40 41 /** @var IClientService */ 42 private $httpClientService; 43 44 /** @var DiscoveryManager */ 45 private $discoveryManager; 46 47 /** @var NotificationManager */ 48 private $notificationManager; 49 50 /** @var IJobList */ 51 private $jobList; 52 53 /** @var IConfig */ 54 private $config; 55 56 /** 57 * @param AddressHandler $addressHandler 58 * @param IClientService $httpClientService 59 * @param DiscoveryManager $discoveryManager 60 * @param IJobList $jobList 61 * @param IConfig $config 62 */ 63 public function __construct( 64 AddressHandler $addressHandler, 65 IClientService $httpClientService, 66 DiscoveryManager $discoveryManager, 67 NotificationManager $notificationManager, 68 IJobList $jobList, 69 IConfig $config 70 ) { 71 $this->addressHandler = $addressHandler; 72 $this->httpClientService = $httpClientService; 73 $this->discoveryManager = $discoveryManager; 74 $this->notificationManager = $notificationManager; 75 $this->jobList = $jobList; 76 $this->config = $config; 77 } 78 79 /** 80 * send server-to-server share to remote server 81 * 82 * @param Address $shareWithAddress 83 * @param Address $ownerAddress 84 * @param Address $sharedByAddress 85 * @param string $token 86 * @param string $name 87 * @param string $remote_id 88 * 89 * @return bool|array true if successful or status information 90 * 91 * @throws \OC\HintException 92 * @throws \OC\ServerNotAvailableException 93 * @throws \Exception 94 */ 95 public function sendRemoteShare( 96 Address $shareWithAddress, 97 Address $ownerAddress, 98 Address $sharedByAddress, 99 $token, 100 $name, 101 $remote_id 102 ) { 103 $remoteShareSuccess = false; 104 if ($shareWithAddress->getUserId() && $shareWithAddress->getCloudId()) { 105 $remoteShareSuccess = $this->sendOcmRemoteShare( 106 $shareWithAddress, 107 $ownerAddress, 108 $sharedByAddress, 109 $token, 110 $name, 111 $remote_id 112 ); 113 if (!$remoteShareSuccess) { 114 $remoteShareSuccess = $this->sendPreOcmRemoteShare( 115 $shareWithAddress, 116 $ownerAddress, 117 $sharedByAddress, 118 $token, 119 $name, 120 $remote_id 121 ); 122 } 123 } 124 if ($remoteShareSuccess === true) { 125 \OC_Hook::emit( 126 'OCP\Share', 127 'federated_share_added', 128 ['server' => $shareWithAddress->getHostName()] 129 ); 130 } 131 return $remoteShareSuccess; 132 } 133 134 /** 135 * ask owner to re-share the file with the given user 136 * 137 * @param string $token 138 * @param int $id remote Id 139 * @param int $shareId internal share Id 140 * @param string $remote remote address of the owner 141 * @param string $shareWith 142 * @param int $permission 143 * @return bool|array 144 * @throws \Exception 145 */ 146 public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission) { 147 $data = [ 148 'shareWith' => $shareWith, 149 'senderId' => $shareId 150 ]; 151 $ocmNotification = $this->notificationManager->convertToOcmFileNotification($id, $token, 'reshare', $data); 152 $ocmFields = $ocmNotification->toArray(); 153 154 $url = \rtrim( 155 $this->addressHandler->removeProtocolFromUrl($remote), 156 '/' 157 ); 158 $result = $this->tryHttpPostToShareEndpoint($url, '/notifications', $ocmFields, true); 159 if (isset($result['statusCode']) && $result['statusCode'] === Http::STATUS_CREATED) { 160 $response = \json_decode($result['result'], true); 161 if (\is_array($response) && isset($response['sharedSecret'], $response['providerId'])) { 162 return [ 163 $response['sharedSecret'], 164 $response['providerId'] 165 ]; 166 } 167 return true; 168 } 169 170 $fields = [ 171 'shareWith' => $shareWith, 172 'token' => $token, 173 'permission' => $permission, 174 'remoteId' => $shareId 175 ]; 176 177 $url = $this->addressHandler->removeProtocolFromUrl($remote); 178 $result = $this->tryHttpPostToShareEndpoint(\rtrim($url, '/'), '/' . $id . '/reshare', $fields); 179 $status = \json_decode($result['result'], true); 180 181 $httpRequestSuccessful = $result['success']; 182 $validToken = isset($status['ocs']['data']['token']) && \is_string($status['ocs']['data']['token']); 183 $validRemoteId = isset($status['ocs']['data']['remoteId']); 184 185 if ($httpRequestSuccessful && $this->isOcsStatusOk($status) && $validToken && $validRemoteId) { 186 return [ 187 $status['ocs']['data']['token'], 188 $status['ocs']['data']['remoteId'] 189 ]; 190 } 191 192 return false; 193 } 194 195 /** 196 * send server-to-server unshare to remote server 197 * 198 * @param string $remote url 199 * @param int $id share id 200 * @param string $token 201 * @return bool 202 */ 203 public function sendRemoteUnShare($remote, $id, $token) { 204 return $this->sendUpdateToRemote($remote, $id, $token, 'unshare'); 205 } 206 207 /** 208 * send server-to-server unshare to remote server 209 * 210 * @param string $remote url 211 * @param int $id share id 212 * @param string $token 213 * @return bool 214 */ 215 public function sendRevokeShare($remote, $id, $token) { 216 return $this->sendUpdateToRemote($remote, $id, $token, 'revoke'); 217 } 218 219 /** 220 * send notification to remote server if the permissions was changed 221 * 222 * @param string $remote 223 * @param string $remoteId 224 * @param string $token 225 * @param int $permissions 226 * @return bool 227 */ 228 public function sendPermissionChange($remote, $remoteId, $token, $permissions) { 229 return $this->sendUpdateToRemote($remote, $remoteId, $token, 'permissions', ['permissions' => $permissions]); 230 } 231 232 /** 233 * forward accept reShare to remote server 234 * 235 * @param string $remote 236 * @param string $remoteId 237 * @param string $token 238 */ 239 public function sendAcceptShare($remote, $remoteId, $token) { 240 $this->sendUpdateToRemote($remote, $remoteId, $token, 'accept'); 241 } 242 243 /** 244 * forward decline reShare to remote server 245 * 246 * @param string $remote 247 * @param string $remoteId 248 * @param string $token 249 */ 250 public function sendDeclineShare($remote, $remoteId, $token) { 251 $this->sendUpdateToRemote($remote, $remoteId, $token, 'decline'); 252 } 253 254 /** 255 * inform remote server whether server-to-server share was accepted/declined 256 * 257 * @param string $remote 258 * @param string $remoteId Share id on the remote host 259 * @param string $token 260 * @param string $action possible actions: 261 * accept, decline, unshare, revoke, permissions 262 * @param array $data 263 * @param int $try 264 * 265 * @return boolean 266 * 267 * @throws \Exception 268 */ 269 public function sendUpdateToRemote($remote, $remoteId, $token, $action, $data = [], $try = 0) { 270 $ocmNotification = $this->notificationManager->convertToOcmFileNotification($remoteId, $token, $action, $data); 271 $ocmFields = $ocmNotification->toArray(); 272 $url = \rtrim( 273 $this->addressHandler->removeProtocolFromUrl($remote), 274 '/' 275 ); 276 $result = $this->tryHttpPostToShareEndpoint($url, '/notifications', $ocmFields, true); 277 if (isset($result['statusCode']) && $result['statusCode'] === Http::STATUS_CREATED) { 278 return true; 279 } 280 281 $fields = ['token' => $token]; 282 foreach ($data as $key => $value) { 283 $fields[$key] = $value; 284 } 285 286 $url = \rtrim( 287 $this->addressHandler->removeProtocolFromUrl($remote), 288 '/' 289 ); 290 $result = $this->tryHttpPostToShareEndpoint($url, '/' . $remoteId . '/' . $action, $fields); 291 $status = \json_decode($result['result'], true); 292 293 if ($result['success'] && $this->isOcsStatusOk($status)) { 294 return true; 295 } elseif ($try === 0) { 296 // only add new job on first try 297 $this->jobList->add( 298 'OCA\FederatedFileSharing\BackgroundJob\RetryJob', 299 [ 300 'remote' => $remote, 301 'remoteId' => $remoteId, 302 'token' => $token, 303 'action' => $action, 304 'data' => \json_encode($data), 305 'try' => $try, 306 'lastRun' => $this->getTimestamp() 307 ] 308 ); 309 } 310 311 return false; 312 } 313 314 /** 315 * return current timestamp 316 * 317 * @return int 318 */ 319 protected function getTimestamp() { 320 return \time(); 321 } 322 323 /** 324 * try http post first with https and then with http as a fallback 325 * 326 * @param string $remoteDomain 327 * @param string $urlSuffix 328 * @param array $fields post parameters 329 * @param bool $useOcm send request to OCM endpoint instead of OCS 330 * 331 * @return array 332 * 333 * @throws \Exception 334 */ 335 protected function tryHttpPostToShareEndpoint($remoteDomain, $urlSuffix, array $fields, $useOcm = false) { 336 $client = $this->httpClientService->newClient(); 337 $protocol = 'https://'; 338 $result = [ 339 'success' => false, 340 'result' => '', 341 ]; 342 $try = 0; 343 344 while ($result['success'] === false && $try < 2) { 345 try { 346 if ($useOcm) { 347 $endpoint = $this->discoveryManager->getOcmShareEndpoint($protocol . $remoteDomain); 348 $endpoint .= $urlSuffix; 349 } else { 350 $relativePath = $this->discoveryManager->getShareEndpoint($protocol . $remoteDomain); 351 $endpoint = $protocol . $remoteDomain . $relativePath . $urlSuffix . '?format=' . self::RESPONSE_FORMAT; 352 } 353 354 $options = [ 355 'timeout' => 10, 356 'connect_timeout' => 10, 357 ]; 358 $sendAs = $useOcm === true ? 'json' : 'body'; 359 $options[$sendAs] = $fields; 360 $response = $client->post($endpoint, $options); 361 $result['result'] = $response->getBody(); 362 $result['statusCode'] = $response->getStatusCode(); 363 $result['success'] = true; 364 break; 365 } catch (ClientException $e) { 366 // this exceptions happens for http error code 40x which the server sends 367 // if any data of the request is bad, ie. the username does not exist. 368 // In that case we want to retrieve an error message. 369 $response = $e->getResponse(); 370 $result['result'] = $response->getBody(); 371 $try++; 372 } catch (\Exception $e) { 373 // if flat re-sharing is not supported by the remote server 374 // we re-throw the exception and fall back to the old behaviour. 375 // (flat re-shares has been introduced in ownCloud 9.1) 376 if ($e->getCode() === Http::STATUS_INTERNAL_SERVER_ERROR) { 377 throw $e; 378 } 379 380 $allowHttpFallback = $this->config->getSystemValue('sharing.federation.allowHttpFallback', false) === true; 381 if (!$allowHttpFallback) { 382 break; 383 } 384 $try++; 385 $protocol = 'http://'; 386 } 387 } 388 389 return $result; 390 } 391 392 /** 393 * @param Address $shareWithAddress 394 * @param Address $ownerAddress 395 * @param Address $sharedByAddress 396 * @param $token 397 * @param $name 398 * @param $remote_id 399 * @return bool 400 * @throws \Exception 401 */ 402 protected function sendOcmRemoteShare(Address $shareWithAddress, Address $ownerAddress, Address $sharedByAddress, $token, $name, $remote_id) { 403 $fields = [ 404 'shareWith' => $shareWithAddress->getCloudId(), 405 'name' => $name, 406 'providerId' => (string) $remote_id, 407 'owner' => $ownerAddress->getCloudId(), 408 'ownerDisplayName' => $ownerAddress->getDisplayName(), 409 'sender' => $sharedByAddress->getCloudId(), 410 'senderDisplayName' => $sharedByAddress->getDisplayName(), 411 'shareType' => 'user', 412 'resourceType' => 'file', 413 'protocol' => [ 414 'name' => 'webdav', 415 'options' => [ 416 'sharedSecret' => $token 417 ] 418 ] 419 ]; 420 421 $url = $shareWithAddress->getHostName(); 422 $result = $this->tryHttpPostToShareEndpoint($url, '/shares', $fields, true); 423 424 if (isset($result['statusCode']) && $result['statusCode'] === Http::STATUS_CREATED) { 425 return true; 426 } 427 return false; 428 } 429 430 /** 431 * @param Address $shareWithAddress 432 * @param Address $ownerAddress 433 * @param Address $sharedByAddress 434 * @param $token 435 * @param $name 436 * @param $remote_id 437 * @return array|bool 438 * @throws \Exception 439 */ 440 protected function sendPreOcmRemoteShare(Address $shareWithAddress, Address $ownerAddress, Address $sharedByAddress, $token, $name, $remote_id) { 441 $fields = [ 442 'shareWith' => $shareWithAddress->getUserId(), 443 'token' => $token, 444 'name' => $name, 445 'remoteId' => $remote_id, 446 'owner' => $ownerAddress->getUserId(), 447 'ownerFederatedId' => $ownerAddress->getCloudId(), 448 'sharedBy' => $sharedByAddress->getUserId(), 449 'sharedByFederatedId' => $sharedByAddress->getUserId(), 450 'remote' => $this->addressHandler->generateRemoteURL(), 451 ]; 452 $url = $shareWithAddress->getHostName(); 453 $result = $this->tryHttpPostToShareEndpoint($url, '', $fields); 454 $status = \json_decode($result['result'], true); 455 if ($result['success'] && $this->isOcsStatusOk($status)) { 456 return true; 457 } 458 return $status; 459 } 460 461 /** 462 * Validate ocs response - 100 or 200 means success 463 * 464 * @param array $status 465 * 466 * @return bool 467 */ 468 private function isOcsStatusOk($status) { 469 return \in_array($status['ocs']['meta']['statuscode'], [100, 200]); 470 } 471} 472