-
Notifications
You must be signed in to change notification settings - Fork 5
Debugging how Nextcloud access remote shares. #373
Comments
Nextcloud doesn't do anything until accepting the share. ownCloud logs also show no trace of accessing. After clicking on accept on Nextcloud: ownCloud access logs:
Nextcloud access logs:
|
Nextcloud behaviour is different from what Giuseppe has seen via CernBox: CernBox logs:
|
I found what happens when Nextcloud removes the CernBox share from the db without any warnings. A detailed report in next comment. |
the file with hardcoded API version: |
The function that checks share info: Specific ownCloud detection probe: A function that removes shares from db: |
Following all your findings, and as also discussed with @labkode, I would rather propose you @MahdiBaghbani and @michielbdejong a different approach. AFAIU you can intercept in Nextcloud the "click" from the frontend that requests to browse a federated share, and redirect it to wherever you like. This intercept may well require a patch in Nextcloud core, that's fine. If you hook an action of the nc-sciencemesh app, then you can implement the action by going to the remote reva using the OCM access, as already documented (see e.g. https://github.com/cs3org/OCM-API#share-access). This has multiple advantages:
Let's discuss that tomorrow over gitter. |
Nextcloud code flow for accepting a remote share. When the share arrives and you decide to accept the share, this function will be called: /**
* accept server-to-server share
*
* @param int $id
* @return bool True if the share could be accepted, false otherwise
*/
public
function acceptShare($id)
{
$share = $this->getShare($id);
$result = false;
if ($share) {
\OC_Util::setupFS($this->uid);
$shareFolder = Helper::getShareFolder(null, $this->uid);
$mountPoint = Files::buildNotExistingFileName($shareFolder, $share['name']);
$mountPoint = Filesystem::normalizePath($mountPoint);
$hash = md5($mountPoint);
$userShareAccepted = false;
if ((int)$share['share_type'] === IShare::TYPE_USER) {
$acceptShare = $this->connection->prepare('
UPDATE `*PREFIX*share_external`
SET `accepted` = ?,
`mountpoint` = ?,
`mountpoint_hash` = ?
WHERE `id` = ? AND `user` = ?');
$userShareAccepted = $acceptShare->execute([1, $mountPoint, $hash, $id, $this->uid]);
} else {
$parentId = (int)$share['parent'];
if ($parentId !== -1) {
// this is the sub-share
$subshare = $share;
} else {
$subshare = $this->fetchUserShare($id, $this->uid);
}
if ($subshare !== null) {
try {
$acceptShare = $this->connection->prepare('
UPDATE `*PREFIX*share_external`
SET `accepted` = ?,
`mountpoint` = ?,
`mountpoint_hash` = ?
WHERE `id` = ? AND `user` = ?');
$acceptShare->execute([1, $mountPoint, $hash, $subshare['id'], $this->uid]);
$result = true;
} catch (Exception $e) {
$this->logger->emergency('Could not update share', ['exception' => $e]);
$result = false;
}
} else {
try {
$this->writeShareToDb(
$share['remote'],
$share['share_token'],
$share['password'],
$share['name'],
$share['owner'],
$this->uid,
$mountPoint, $hash, 1,
$share['remote_id'],
$id,
$share['share_type']);
$result = true;
} catch (Exception $e) {
$this->logger->emergency('Could not create share', ['exception' => $e]);
$result = false;
}
}
}
if ($userShareAccepted !== false) {
$this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'accept');
$event = new FederatedShareAddedEvent($share['remote']);
$this->eventDispatcher->dispatchTyped($event);
$this->eventDispatcher->dispatchTyped(new Files\Events\InvalidateMountCacheEvent($this->userManager->get($this->uid)));
$result = true;
}
}
// Make sure the user has no notification for something that does not exist anymore.
$this->processNotification($id);
return $result;
} This line is important, it calls the end API endpoint to notify the remote server about the share being accepted: $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'accept'); This function is here: /**
* inform remote server whether server-to-server share was accepted/declined
*
* @param string $remote
* @param string $token
* @param string $remoteId Share id on the remote host
* @param string $feedback
* @return boolean
*/
private function sendFeedbackToRemote($remote, $token, $remoteId, $feedback) {
$result = $this->tryOCMEndPoint($remote, $token, $remoteId, $feedback);
if (is_array($result)) {
return true;
}
$federationEndpoints = $this->discoveryService->discover($remote, 'FEDERATED_SHARING');
$endpoint = isset($federationEndpoints['share']) ? $federationEndpoints['share'] : '/ocs/v2.php/cloud/shares';
$url = rtrim($remote, '/') . $endpoint . '/' . $remoteId . '/' . $feedback . '?format=' . Share::RESPONSE_FORMAT;
$fields = ['token' => $token];
$client = $this->clientService->newClient();
try {
$response = $client->post(
$url,
[
'body' => $fields,
'connect_timeout' => 10,
]
);
} catch (\Exception $e) {
return false;
}
$status = json_decode($response->getBody(), true);
return ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200);
} At this line, it tries the ocm endpoint first: $result = $this->tryOCMEndPoint($remote, $token, $remoteId, $feedback); Which is this function: /**
* try send accept message to ocm end-point
*
* @param string $remoteDomain
* @param string $token
* @param string $remoteId id of the share
* @param string $feedback
* @return array|false
*/
protected function tryOCMEndPoint($remoteDomain, $token, $remoteId, $feedback) {
switch ($feedback) {
case 'accept':
$notification = $this->cloudFederationFactory->getCloudFederationNotification();
$notification->setMessage(
'SHARE_ACCEPTED',
'file',
$remoteId,
[
'sharedSecret' => $token,
'message' => 'Recipient accept the share'
]
);
return $this->cloudFederationProviderManager->sendNotification($remoteDomain, $notification);
case 'decline':
$notification = $this->cloudFederationFactory->getCloudFederationNotification();
$notification->setMessage(
'SHARE_DECLINED',
'file',
$remoteId,
[
'sharedSecret' => $token,
'message' => 'Recipient declined the share'
]
);
return $this->cloudFederationProviderManager->sendNotification($remoteDomain, $notification);
}
return false;
} Then in this line, it calls the send notification function: return $this->cloudFederationProviderManager->sendNotification($remoteDomain, $notification); Which is located here: /**
* @param string $url
* @param ICloudFederationNotification $notification
* @return array|false
*/
public function sendNotification($url, ICloudFederationNotification $notification) {
$ocmEndPoint = $this->getOCMEndPoint($url);
if (empty($ocmEndPoint)) {
return false;
}
$client = $this->httpClientService->newClient();
try {
$response = $client->post($ocmEndPoint . '/notifications', [
'body' => json_encode($notification->getMessage()),
'headers' => ['content-type' => 'application/json'],
'timeout' => 10,
'connect_timeout' => 10,
]);
if ($response->getStatusCode() === Http::STATUS_CREATED) {
$result = json_decode($response->getBody(), true);
return (is_array($result)) ? $result : [];
}
} catch (\Exception $e) {
// log the error and return false
$this->logger->error('error while sending notification for federated share: ' . $e->getMessage(), ['exception' => $e]);
}
return false;
} In above function it calls this function first: $ocmEndPoint = $this->getOCMEndPoint($url); /**
* check if server supports the new OCM api and ask for the correct end-point
*
* @param string $url full base URL of the cloud server
* @return string
*/
protected function getOCMEndPoint($url) {
if (isset($this->ocmEndPoints[$url])) {
return $this->ocmEndPoints[$url];
}
$client = $this->httpClientService->newClient();
try {
$response = $client->get($url . '/ocm-provider/', ['timeout' => 10, 'connect_timeout' => 10]);
} catch (\Exception $e) {
$this->ocmEndPoints[$url] = '';
return '';
}
$result = $response->getBody();
$result = json_decode($result, true);
$supportedVersion = isset($result['apiVersion']) && $result['apiVersion'] === $this->supportedAPIVersion;
if (isset($result['endPoint']) && $supportedVersion) {
$this->ocmEndPoints[$url] = $result['endPoint'];
return $result['endPoint'];
}
$this->ocmEndPoints[$url] = '';
return '';
} This is the function responsible for the first ocm-provider call in both ownCloud and CERNBox $response = $client->get($url . '/ocm-provider/', ['timeout' => 10, 'connect_timeout' => 10] ownCloud:
CERNBox
Now the first culprit responsible for failing to access the share from CERNBox is here in line:
It checks private $supportedAPIVersion = '1.0-proposal1'; When the version doesn't match, Nextcloud will simply return an empty string: return ''; Then the return false; This where @glpatcern wants to know about: /** @var array cache OCM end-points */
private $ocmEndPoints = []; It uses cache 😄 if it finds ocm endpoint add it to the cache: if (isset($result['endPoint']) && $supportedVersion) {
$this->ocmEndPoints[$url] = $result['endPoint'];
return $result['endPoint'];
} if not it will add an empty string: this->ocmEndPoints[$url] = ''; and in consecutive calls, it just looks at the cache: if (isset($this->ocmEndPoints[$url])) {
return $this->ocmEndPoints[$url];
} Now back to the debugging: $response = $client->post($ocmEndPoint . '/notifications', [
'body' => json_encode($notification->getMessage()),
'headers' => ['content-type' => 'application/json'],
'timeout' => 10,
'connect_timeout' => 10,
]); which maps to this ownCloud access log:
|
this function is being called here: class Scanner extends \OC\Files\Cache\Scanner {
/** @var \OCA\Files_Sharing\External\Storage */
protected $storage;
public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
// Disable locking for federated shares
parent::scan($path, $recursive, $reuse, false);
}
/**
* Scan a single file and store it in the cache.
* If an exception happened while accessing the external storage,
* the storage will be checked for availability and removed
* if it is not available any more.
*
* @param string $file file to scan
* @param int $reuseExisting
* @param int $parentId
* @param array | null $cacheData existing data in the cache for the file to be scanned
* @param bool $lock set to false to disable getting an additional read lock during scanning
* @return array | null an array of metadata of the scanned file
*/
public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
try {
return parent::scanFile($file, $reuseExisting, $parentId, $cacheData, $lock, $data);
} catch (ForbiddenException $e) {
$this->storage->checkStorageAvailability();
} catch (NotFoundException $e) {
// if the storage isn't found, the call to
// checkStorageAvailable() will verify it and remove it
// if appropriate
$this->storage->checkStorageAvailability();
} catch (StorageInvalidException $e) {
$this->storage->checkStorageAvailability();
} catch (StorageNotAvailableException $e) {
$this->storage->checkStorageAvailability();
}
}
} |
Nextcloud accessing ownCloud is like this:
first a 401 then 207 but it doesn't happen when trying to access CERNBox. just single 401. |
After fixing API version with Giuseppe we came to new logs in Nextcloud and CERNBox: Nextcloud extra logs:
CERNBox logs:
|
This problem:
Has been fixed by Giuseppe returning the STATUS_CREATED (201) to the notification. This is what Nextcloud sends to
CERNBox logs:
as seen in new logs, Nextcloud doesn't go to this anymore:
yet the problem still exists:
as seen
So after Giuseppe added CERNBox
CERNBox logs:
now it uses the right endpoint for PROPFIND but fails again on PROPFIND and doesn't go to the second PROPFIND with token. Problems:
|
On dev-stock run NRRO scenario.
when sharing from OC (marie) to NC (einstein):
OC access logs:
OC Reva logs:
NC access logs:
NC Reva Logs:
The text was updated successfully, but these errors were encountered: