CloudFederationProviderManager.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Federation;
  8. use NCU\Security\Signature\ISignatureManager;
  9. use OC\AppFramework\Http;
  10. use OC\OCM\OCMSignatoryManager;
  11. use OCP\App\IAppManager;
  12. use OCP\Federation\Exceptions\ProviderDoesNotExistsException;
  13. use OCP\Federation\ICloudFederationNotification;
  14. use OCP\Federation\ICloudFederationProvider;
  15. use OCP\Federation\ICloudFederationProviderManager;
  16. use OCP\Federation\ICloudFederationShare;
  17. use OCP\Federation\ICloudIdManager;
  18. use OCP\Http\Client\IClient;
  19. use OCP\Http\Client\IClientService;
  20. use OCP\Http\Client\IResponse;
  21. use OCP\IAppConfig;
  22. use OCP\IConfig;
  23. use OCP\OCM\Exceptions\OCMProviderException;
  24. use OCP\OCM\IOCMDiscoveryService;
  25. use Psr\Log\LoggerInterface;
  26. /**
  27. * Class Manager
  28. *
  29. * Manage Cloud Federation Providers
  30. *
  31. * @package OC\Federation
  32. */
  33. class CloudFederationProviderManager implements ICloudFederationProviderManager {
  34. /** @var array list of available cloud federation providers */
  35. private array $cloudFederationProvider = [];
  36. public function __construct(
  37. private IConfig $config,
  38. private IAppManager $appManager,
  39. private IAppConfig $appConfig,
  40. private IClientService $httpClientService,
  41. private ICloudIdManager $cloudIdManager,
  42. private IOCMDiscoveryService $discoveryService,
  43. private readonly ISignatureManager $signatureManager,
  44. private readonly OCMSignatoryManager $signatoryManager,
  45. private LoggerInterface $logger,
  46. ) {
  47. }
  48. /**
  49. * Registers an callback function which must return an cloud federation provider
  50. *
  51. * @param string $resourceType which resource type does the provider handles
  52. * @param string $displayName user facing name of the federated share provider
  53. * @param callable $callback
  54. */
  55. public function addCloudFederationProvider($resourceType, $displayName, callable $callback) {
  56. $this->cloudFederationProvider[$resourceType] = [
  57. 'resourceType' => $resourceType,
  58. 'displayName' => $displayName,
  59. 'callback' => $callback,
  60. ];
  61. }
  62. /**
  63. * remove cloud federation provider
  64. *
  65. * @param string $providerId
  66. */
  67. public function removeCloudFederationProvider($providerId) {
  68. unset($this->cloudFederationProvider[$providerId]);
  69. }
  70. /**
  71. * get a list of all cloudFederationProviders
  72. *
  73. * @return array [resourceType => ['resourceType' => $resourceType, 'displayName' => $displayName, 'callback' => callback]]
  74. */
  75. public function getAllCloudFederationProviders() {
  76. return $this->cloudFederationProvider;
  77. }
  78. /**
  79. * get a specific cloud federation provider
  80. *
  81. * @param string $resourceType
  82. * @return ICloudFederationProvider
  83. * @throws ProviderDoesNotExistsException
  84. */
  85. public function getCloudFederationProvider($resourceType) {
  86. if (isset($this->cloudFederationProvider[$resourceType])) {
  87. return call_user_func($this->cloudFederationProvider[$resourceType]['callback']);
  88. } else {
  89. throw new ProviderDoesNotExistsException($resourceType);
  90. }
  91. }
  92. /**
  93. * @deprecated 29.0.0 - Use {@see sendCloudShare()} instead and handle errors manually
  94. */
  95. public function sendShare(ICloudFederationShare $share) {
  96. $cloudID = $this->cloudIdManager->resolveCloudId($share->getShareWith());
  97. try {
  98. try {
  99. $response = $this->postOcmPayload($cloudID->getRemote(), '/shares', json_encode($share->getShare()));
  100. } catch (OCMProviderException) {
  101. return false;
  102. }
  103. if ($response->getStatusCode() === Http::STATUS_CREATED) {
  104. $result = json_decode($response->getBody(), true);
  105. return (is_array($result)) ? $result : [];
  106. }
  107. } catch (\Exception $e) {
  108. $this->logger->debug($e->getMessage(), ['exception' => $e]);
  109. // if flat re-sharing is not supported by the remote server
  110. // we re-throw the exception and fall back to the old behaviour.
  111. // (flat re-shares has been introduced in Nextcloud 9.1)
  112. if ($e->getCode() === Http::STATUS_INTERNAL_SERVER_ERROR) {
  113. throw $e;
  114. }
  115. }
  116. return false;
  117. }
  118. /**
  119. * @param ICloudFederationShare $share
  120. * @return IResponse
  121. * @throws OCMProviderException
  122. */
  123. public function sendCloudShare(ICloudFederationShare $share): IResponse {
  124. $cloudID = $this->cloudIdManager->resolveCloudId($share->getShareWith());
  125. $client = $this->httpClientService->newClient();
  126. try {
  127. return $this->postOcmPayload($cloudID->getRemote(), '/shares', json_encode($share->getShare()), $client);
  128. } catch (\Throwable $e) {
  129. $this->logger->error('Error while sending share to federation server: ' . $e->getMessage(), ['exception' => $e]);
  130. try {
  131. return $client->getResponseFromThrowable($e);
  132. } catch (\Throwable $e) {
  133. throw new OCMProviderException($e->getMessage(), $e->getCode(), $e);
  134. }
  135. }
  136. }
  137. /**
  138. * @param string $url
  139. * @param ICloudFederationNotification $notification
  140. * @return array|false
  141. * @deprecated 29.0.0 - Use {@see sendCloudNotification()} instead and handle errors manually
  142. */
  143. public function sendNotification($url, ICloudFederationNotification $notification) {
  144. try {
  145. try {
  146. $response = $this->postOcmPayload($url, '/notifications', json_encode($notification->getMessage()));
  147. } catch (OCMProviderException) {
  148. return false;
  149. }
  150. if ($response->getStatusCode() === Http::STATUS_CREATED) {
  151. $result = json_decode($response->getBody(), true);
  152. return (is_array($result)) ? $result : [];
  153. }
  154. } catch (\Exception $e) {
  155. // log the error and return false
  156. $this->logger->error('error while sending notification for federated share: ' . $e->getMessage(), ['exception' => $e]);
  157. }
  158. return false;
  159. }
  160. /**
  161. * @param string $url
  162. * @param ICloudFederationNotification $notification
  163. * @return IResponse
  164. * @throws OCMProviderException
  165. */
  166. public function sendCloudNotification(string $url, ICloudFederationNotification $notification): IResponse {
  167. $client = $this->httpClientService->newClient();
  168. try {
  169. return $this->postOcmPayload($url, '/notifications', json_encode($notification->getMessage()), $client);
  170. } catch (\Throwable $e) {
  171. $this->logger->error('Error while sending notification to federation server: ' . $e->getMessage(), ['exception' => $e]);
  172. try {
  173. return $client->getResponseFromThrowable($e);
  174. } catch (\Throwable $e) {
  175. throw new OCMProviderException($e->getMessage(), $e->getCode(), $e);
  176. }
  177. }
  178. }
  179. /**
  180. * check if the new cloud federation API is ready to be used
  181. *
  182. * @return bool
  183. */
  184. public function isReady() {
  185. return $this->appManager->isEnabledForUser('cloud_federation_api');
  186. }
  187. /**
  188. * @param string $cloudId
  189. * @param string $uri
  190. * @param string $payload
  191. *
  192. * @return IResponse
  193. * @throws OCMProviderException
  194. */
  195. private function postOcmPayload(string $cloudId, string $uri, string $payload, ?IClient $client = null): IResponse {
  196. $ocmProvider = $this->discoveryService->discover($cloudId);
  197. $uri = $ocmProvider->getEndPoint() . '/' . ltrim($uri, '/');
  198. $client = $client ?? $this->httpClientService->newClient();
  199. return $client->post($uri, $this->prepareOcmPayload($uri, $payload));
  200. }
  201. /**
  202. * @param string $uri
  203. * @param string $payload
  204. *
  205. * @return array
  206. */
  207. private function prepareOcmPayload(string $uri, string $payload): array {
  208. $payload = array_merge($this->getDefaultRequestOptions(), ['body' => $payload]);
  209. if ($this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED, lazy: true) &&
  210. $this->signatoryManager->getRemoteSignatory($this->signatureManager->extractIdentityFromUri($uri)) === null) {
  211. return $payload;
  212. }
  213. if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
  214. $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
  215. $this->signatoryManager,
  216. $payload,
  217. 'post', $uri
  218. );
  219. }
  220. return $signedPayload ?? $payload;
  221. }
  222. private function getDefaultRequestOptions(): array {
  223. return [
  224. 'headers' => ['content-type' => 'application/json'],
  225. 'timeout' => 10,
  226. 'connect_timeout' => 10,
  227. 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false),
  228. ];
  229. }
  230. }