Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for external signaling federation #12604

Merged
merged 17 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
nickvergessen marked this conversation as resolved.
Show resolved Hide resolved
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(
nickvergessen marked this conversation as resolved.
Show resolved Hide resolved
$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')]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[BruteForceProtection(action: 'talkRoomToken')]
#[BruteForceProtection(action: 'talkRoomToken')]
#[BruteForceProtection(action: 'talkFederationAccess')]

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throttle with action: talkFederationAccess when the credentials where not valid

}
Expand Down
Loading
Loading