Skip to content

Commit

Permalink
Merge pull request #12604 from nextcloud/add-support-for-external-sig…
Browse files Browse the repository at this point in the history
…naling-federation

Add support for external signaling federation
  • Loading branch information
nickvergessen authored Jul 24, 2024
2 parents ff12362 + d891573 commit 8d68d25
Show file tree
Hide file tree
Showing 34 changed files with 1,289 additions and 136 deletions.
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* 🌉 **Sync with other chat solutions** With [Matterbridge](https://github.com/42wim/matterbridge/) being integrated in Talk, you can easily sync a lot of other chat solutions to Nextcloud Talk and vice-versa.
]]></description>

<version>20.0.0-dev.7</version>
<version>20.0.0-dev.8</version>
<licence>agpl</licence>

<author>Daniel Calviño Sánchez</author>
Expand Down
2 changes: 2 additions & 0 deletions appinfo/routes/routesRoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
['name' => 'Room#resendInvitations', 'url' => '/api/{apiVersion}/room/{token}/participants/resend-invitations', 'verb' => 'POST', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::leaveRoom() */
['name' => 'Room#leaveRoom', 'url' => '/api/{apiVersion}/room/{token}/participants/active', 'verb' => 'DELETE', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::leaveFederatedRoom() */
['name' => 'Room#leaveFederatedRoom', 'url' => '/api/{apiVersion}/room/{token}/federation/active', 'verb' => 'DELETE', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::setSessionState() */
['name' => 'Room#setSessionState', 'url' => '/api/{apiVersion}/room/{token}/participants/state', 'verb' => 'PUT', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::promoteModerator() */
Expand Down
1 change: 1 addition & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,4 @@
## 20
* `ban-v1` - Whether the API to ban attendees is available
* `mention-permissions` - Whether non-moderators are allowed to mention `@all`
* `federation-v2` - Whether federated session ids are used and calls are possible with federation
1 change: 1 addition & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class Capabilities implements IPublicCapability {
'silent-send-state',
'chat-read-last',
'federation-v1',
'federation-v2',
'ban-v1',
'chat-reference-id',
'mention-permissions',
Expand Down
7 changes: 6 additions & 1 deletion lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use OCP\AppFramework\Services\IAppConfig;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\ICloudIdManager;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IURLGenerator;
Expand Down Expand Up @@ -46,6 +47,7 @@ public function __construct(
private ISecureRandom $secureRandom,
private IGroupManager $groupManager,
private IUserManager $userManager,
private ICloudIdManager $cloudIdManager,
private IURLGenerator $urlGenerator,
protected ITimeFactory $timeFactory,
private IEventDispatcher $dispatcher,
Expand Down Expand Up @@ -572,7 +574,8 @@ public function getSignalingUserData(IUser $user): array {
}

/**
* @param string|null $userId
* @param string|null $userId if given, the id of a user in this instance or
* a cloud id.
* @return string
*/
private function getSignalingTicketV2(?string $userId): string {
Expand All @@ -586,6 +589,8 @@ private function getSignalingTicketV2(?string $userId): string {
if ($user instanceof IUser) {
$data['sub'] = $user->getUID();
$data['userdata'] = $this->getSignalingUserData($user);
} elseif (!empty($userId) && $this->cloudIdManager->isValidCloudId($userId)) {
$data['sub'] = $userId;
}

$alg = $this->getSignalingTokenAlgorithm();
Expand Down
164 changes: 95 additions & 69 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1506,33 +1506,16 @@ public function setPassword(string $password): DataResponse {
* 409: Session already exists
*/
#[PublicPage]
#[BruteForceProtection(action: 'talkFederationAccess')]
#[BruteForceProtection(action: 'talkRoomPassword')]
#[BruteForceProtection(action: 'talkRoomToken')]
public function joinRoom(string $token, string $password = '', bool $force = true): DataResponse {
$sessionId = $this->session->getSessionForRoom($token);
$isTalkFederation = $this->federationAuthenticator->isFederationRequest();
try {
// The participant is just joining, so enforce to not load any session
if (!$isTalkFederation) {
$action = 'talkRoomToken';
$room = $this->manager->getRoomForUserByToken($token, $this->userId, null);
} else {
$action = 'talkFederationAccess';
try {
$room = $this->federationAuthenticator->getRoom();
} catch (RoomNotFoundException) {
$room = $this->manager->getRoomByRemoteAccess(
$token,
Attendee::ACTOR_FEDERATED_USERS,
$this->federationAuthenticator->getCloudId(),
$this->federationAuthenticator->getAccessToken(),
);
}
}
$room = $this->manager->getRoomForUserByToken($token, $this->userId, null);
} catch (RoomNotFoundException $e) {
$response = new DataResponse([], Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => $action]);
$response->throttle(['token' => $token, 'action' => 'talkRoomToken']);
return $response;
}

Expand Down Expand Up @@ -1581,22 +1564,6 @@ public function joinRoom(string $token, string $password = '', bool $force = tru

$headers = [];
if ($room->isFederatedConversation()) {
$participant = $this->participantService->getParticipant($room, $this->userId);

/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController::class);
$response = $proxy->joinFederatedRoom($room, $participant);

if ($response->getStatus() === Http::STATUS_NOT_FOUND) {
$this->participantService->removeAttendee($room, $participant, AAttendeeRemovedEvent::REASON_REMOVED);
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

$proxyHeaders = $response->getHeaders();
if (isset($proxyHeaders['X-Nextcloud-Talk-Proxy-Hash'])) {
$headers['X-Nextcloud-Talk-Proxy-Hash'] = $proxyHeaders['X-Nextcloud-Talk-Proxy-Hash'];
}

// Skip password checking
$result = [
'result' => true,
Expand All @@ -1610,8 +1577,6 @@ public function joinRoom(string $token, string $password = '', bool $force = tru
if ($user instanceof IUser) {
$participant = $this->participantService->joinRoom($this->roomService, $room, $user, $password, $result['result']);
$this->participantService->generatePinForParticipant($room, $participant);
} elseif ($isTalkFederation) {
$participant = $this->participantService->joinRoomAsFederatedUser($room, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId());
} else {
$participant = $this->participantService->joinRoomAsNewGuest($this->roomService, $room, $password, $result['result'], $previousParticipant);
$this->session->setGuestActorIdForRoom($room->getToken(), $participant->getAttendee()->getActorId());
Expand All @@ -1626,7 +1591,7 @@ public function joinRoom(string $token, string $password = '', bool $force = tru
return $response;
} catch (UnauthorizedException $e) {
$response = new DataResponse([], Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => $action]);
$response->throttle(['token' => $token, 'action' => 'talkRoomToken']);
return $response;
}

Expand All @@ -1637,23 +1602,49 @@ public function joinRoom(string $token, string $password = '', bool $force = tru
$this->sessionService->updateLastPing($session, $this->timeFactory->getTime());
}

if ($room->isFederatedConversation()) {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController::class);

try {
$response = $proxy->joinFederatedRoom($room, $participant);
} catch (CannotReachRemoteException $e) {
$this->participantService->leaveRoomAsSession($room, $participant);

throw $e;
}

if ($response->getStatus() === Http::STATUS_NOT_FOUND) {
$this->participantService->removeAttendee($room, $participant, AAttendeeRemovedEvent::REASON_REMOVED);
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

$proxyHeaders = $response->getHeaders();
if (isset($proxyHeaders['X-Nextcloud-Talk-Proxy-Hash'])) {
$headers['X-Nextcloud-Talk-Proxy-Hash'] = $proxyHeaders['X-Nextcloud-Talk-Proxy-Hash'];
}
}

return new DataResponse($this->formatRoom($room, $participant), Http::STATUS_OK, $headers);
}

/**
* Fake join a room on the host server to verify the federated user is still part of it
* Join room on the host server using the session id of the federated user.
*
* The session id can be null only for requests from Talk < 20.
*
* @param string $token Token of the room
* @param string $sessionId Federated session id to join with
* @return DataResponse<Http::STATUS_OK, array<empty>, array{X-Nextcloud-Talk-Hash: string}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Federated user is still part of the room
* 200: Federated user joined the room
* 404: Room not found
*/
#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
#[PublicPage]
#[BruteForceProtection(action: 'talkRoomToken')]
#[BruteForceProtection(action: 'talkFederationAccess')]
public function joinFederatedRoom(string $token): DataResponse {
public function joinFederatedRoom(string $token, ?string $sessionId): DataResponse {
if (!$this->federationAuthenticator->isFederationRequest()) {
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => 'talkRoomToken']);
Expand All @@ -1662,22 +1653,26 @@ public function joinFederatedRoom(string $token): DataResponse {

try {
try {
$this->federationAuthenticator->getRoom();
$room = $this->federationAuthenticator->getRoom();
} catch (RoomNotFoundException) {
$this->manager->getRoomByRemoteAccess(
$room = $this->manager->getRoomByRemoteAccess(
$token,
Attendee::ACTOR_FEDERATED_USERS,
$this->federationAuthenticator->getCloudId(),
$this->federationAuthenticator->getAccessToken(),
);
}

if ($sessionId != null) {
$participant = $this->participantService->joinRoomAsFederatedUser($room, Attendee::ACTOR_FEDERATED_USERS, $this->federationAuthenticator->getCloudId(), $sessionId);
}

// Let the clients know if they need to reload capabilities
$capabilities = $this->capabilities->getCapabilities();
return new DataResponse([], Http::STATUS_OK, [
'X-Nextcloud-Talk-Hash' => sha1(json_encode($capabilities)),
]);
} catch (RoomNotFoundException) {
} catch (RoomNotFoundException|UnauthorizedException) {
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => 'talkFederationAccess']);
return $response;
Expand Down Expand Up @@ -1902,33 +1897,64 @@ public function leaveRoom(string $token): DataResponse {
$this->session->removeSessionForRoom($token);

try {
// The participant is just joining, so enforce to not load any session
if (!$this->federationAuthenticator->isFederationRequest()) {
$room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId);
$participant = $this->participantService->getParticipantBySession($room, $sessionId);
} else {
try {
$room = $this->federationAuthenticator->getRoom();
} catch (RoomNotFoundException) {
$room = $this->manager->getRoomByRemoteAccess(
$token,
Attendee::ACTOR_FEDERATED_USERS,
$this->federationAuthenticator->getCloudId(),
$this->federationAuthenticator->getAccessToken(),
);
}
$room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId);
$participant = $this->participantService->getParticipantBySession($room, $sessionId);

try {
$participant = $this->federationAuthenticator->getParticipant();
} catch (ParticipantNotFoundException) {
$participant = $this->participantService->getParticipantByActor(
$room,
Attendee::ACTOR_FEDERATED_USERS,
$this->federationAuthenticator->getCloudId(),
);
$this->federationAuthenticator->authenticated($room, $participant);
}
if ($room->isFederatedConversation()) {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController::class);
$response = $proxy->leaveFederatedRoom($room, $participant);
}

$this->participantService->leaveRoomAsSession($room, $participant);
} catch (RoomNotFoundException|ParticipantNotFoundException) {
}

return new DataResponse();
}

/**
* Leave room on the host server using the session id of the federated user.
*
* @param string $token Token of the room
* @param string $sessionId Federated session id to leave with
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Successfully left the room
* 404: Room not found (non-federation request)
*/
#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
#[PublicPage]
#[BruteForceProtection(action: 'talkRoomToken')]
public function leaveFederatedRoom(string $token, string $sessionId): DataResponse {
if (!$this->federationAuthenticator->isFederationRequest()) {
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => 'talkRoomToken']);
return $response;
}

try {
try {
$room = $this->federationAuthenticator->getRoom();
} catch (RoomNotFoundException) {
$room = $this->manager->getRoomByRemoteAccess(
$token,
Attendee::ACTOR_FEDERATED_USERS,
$this->federationAuthenticator->getCloudId(),
$this->federationAuthenticator->getAccessToken(),
);
}

try {
$participant = $this->federationAuthenticator->getParticipant();
} catch (ParticipantNotFoundException) {
$participant = $this->participantService->getParticipantBySession(
$room,
$sessionId,
);
$this->federationAuthenticator->authenticated($room, $participant);
}

$this->participantService->leaveRoomAsSession($room, $participant);
} catch (RoomNotFoundException|ParticipantNotFoundException) {
}
Expand Down
Loading

0 comments on commit 8d68d25

Please sign in to comment.