Skip to content

Commit

Permalink
Merge pull request #11698 from nextcloud/bugfix/11272/check-host-on-join
Browse files Browse the repository at this point in the history
feat(federation): Proxy the Talk-Hash header on Join and Capabilities so clients are aware of …
  • Loading branch information
nickvergessen authored Mar 1, 2024
2 parents f4ce578 + 2343f89 commit 3e26411
Show file tree
Hide file tree
Showing 9 changed files with 479 additions and 13 deletions.
2 changes: 2 additions & 0 deletions appinfo/routes/routesRoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
['name' => 'Room#setAllAttendeesPermissions', 'url' => '/api/{apiVersion}/room/{token}/attendees/permissions/all', 'verb' => 'PUT', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::joinRoom() */
['name' => 'Room#joinRoom', 'url' => '/api/{apiVersion}/room/{token}/participants/active', 'verb' => 'POST', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::joinFederatedRoom() */
['name' => 'Room#joinFederatedRoom', 'url' => '/api/{apiVersion}/room/{token}/federation/active', 'verb' => 'POST', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::resendInvitations() */
['name' => 'Room#resendInvitations', 'url' => '/api/{apiVersion}/room/{token}/participants/resend-invitations', 'verb' => 'POST', 'requirements' => $requirementsWithToken],
/** @see \OCA\Talk\Controller\RoomController::leaveRoom() */
Expand Down
99 changes: 87 additions & 12 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ private function validateSIPBridgeRequest(string $token): bool {
/**
* @return TalkRoom
*/
protected function formatRoom(Room $room, ?Participant $currentParticipant, ?array $statuses = null, bool $isSIPBridgeRequest = false, bool $isListingBreakoutRooms = false): array {
protected function formatRoom(Room $room, ?Participant $currentParticipant, ?array $statuses = null, bool $isSIPBridgeRequest = false, bool $isListingBreakoutRooms = false, array $remoteRoomData = []): array {
return $this->roomFormatter->formatRoom(
$this->getResponseFormat(),
$this->commonReadMessages,
Expand Down Expand Up @@ -1449,7 +1449,7 @@ public function setPassword(string $password): DataResponse {
* @param string $token Token of the room
* @param string $password Password of the room
* @param bool $force Create a new session if necessary
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|DataResponse<Http::STATUS_CONFLICT, array{sessionId: string, inCall: int, lastPing: int}, array{}>
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{X-Nextcloud-Talk-Proxy-Hash?: string}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|DataResponse<Http::STATUS_CONFLICT, array{sessionId: string, inCall: int, lastPing: int}, array{}>
*
* 200: Room joined successfully
* 403: Joining room is not allowed
Expand Down Expand Up @@ -1521,9 +1521,34 @@ public function joinRoom(string $token, string $password = '', bool $force = tru
}
}

$headers = [];
if ($room->getRemoteServer() !== '') {
$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,
];
} else {
$result = $this->roomService->verifyPassword($room, (string) $this->session->getPasswordForRoom($token));
}

$user = $this->userManager->get($this->userId);
try {
$result = $this->roomService->verifyPassword($room, (string) $this->session->getPasswordForRoom($token));
if ($user instanceof IUser) {
$participant = $this->participantService->joinRoom($this->roomService, $room, $user, $password, $result['result']);
$this->participantService->generatePinForParticipant($room, $participant);
Expand Down Expand Up @@ -1551,7 +1576,51 @@ public function joinRoom(string $token, string $password = '', bool $force = tru
$this->sessionService->updateLastPing($session, $this->timeFactory->getTime());
}

return new DataResponse($this->formatRoom($room, $participant));
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
*
* @param string $token Token of the room
* @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
* 404: Room not found
*/
#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
#[PublicPage]
#[BruteForceProtection(action: 'talkRoomToken')]
#[BruteForceProtection(action: 'talkFederationAccess')]
public function joinFederatedRoom(string $token): DataResponse {
if (!$this->federationAuthenticator->isFederationRequest()) {
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => 'talkRoomToken']);
return $response;
}

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

// 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) {
$response = new DataResponse(null, Http::STATUS_NOT_FOUND);
$response->throttle(['token' => $token, 'action' => 'talkFederationAccess']);
return $response;
}
}

/**
Expand Down Expand Up @@ -1771,9 +1840,8 @@ public function leaveRoom(string $token): DataResponse {
$this->session->removeSessionForRoom($token);

try {
$isTalkFederation = $this->request->getHeader('X-Nextcloud-Federation');
// The participant is just joining, so enforce to not load any session
if (!$isTalkFederation) {
if (!$this->federationAuthenticator->isFederationRequest()) {
$room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId);
$participant = $this->participantService->getParticipantBySession($room, $sessionId);
} else {
Expand Down Expand Up @@ -2155,23 +2223,30 @@ public function setMessageExpiration(int $seconds): DataResponse {
/**
* Get capabilities for a room
*
* @return DataResponse<Http::STATUS_OK, TalkCapabilities|array<empty>, array{X-Nextcloud-Talk-Hash: string}>
* @return DataResponse<Http::STATUS_OK, TalkCapabilities|array<empty>, array{X-Nextcloud-Talk-Hash?: string, X-Nextcloud-Talk-Proxy-Hash?: string}>
*
* 200: Get capabilities successfully
*/
#[FederationSupported]
#[PublicPage]
#[RequireParticipant]
public function getCapabilities(): DataResponse {
$headers = [];
if ($this->room->getRemoteServer()) {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\RoomController::class);
return $proxy->getCapabilities($this->room, $this->participant);
$response = $proxy->getCapabilities($this->room, $this->participant);

$data = $response->getData();
if ($response->getHeaders()['X-Nextcloud-Talk-Hash']) {
$headers['X-Nextcloud-Talk-Proxy-Hash'] = $response->getHeaders()['X-Nextcloud-Talk-Hash'];
}
} else {
$capabilities = $this->capabilities->getCapabilities();
$data = $capabilities['spreed'] ?? [];
$headers['X-Nextcloud-Talk-Hash'] = sha1(json_encode($capabilities));
}

$capabilities = $this->capabilities->getCapabilities();
return new DataResponse($capabilities['spreed'] ?? [], Http::STATUS_OK, [
'X-Nextcloud-Talk-Hash' => sha1(json_encode($capabilities)),
]);
return new DataResponse($data, Http::STATUS_OK, $headers);
}
}
31 changes: 30 additions & 1 deletion lib/Federation/Proxy/TalkV1/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,33 @@ public function getParticipants(Room $room, Participant $participant, bool $incl
return new DataResponse($data, Http::STATUS_OK, $headers);
}

/**
* @see \OCA\Talk\Controller\RoomController::joinFederatedRoom()
*
* @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, array<empty>, array{X-Nextcloud-Talk-Proxy-Hash: string}>
* @throws CannotReachRemoteException
*
* 200: Federated user is still part of the room
* 404: Room not found
*/
public function joinFederatedRoom(Room $room, Participant $participant): DataResponse {
$proxy = $this->proxy->post(
$participant->getAttendee()->getInvitedCloudId(),
$participant->getAttendee()->getAccessToken(),
$room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v4/room/' . $room->getRemoteToken() . '/federation/active',
);

$statusCode = $proxy->getStatusCode();
if (!in_array($statusCode, [Http::STATUS_OK, Http::STATUS_NOT_FOUND], true)) {
$this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode());
throw new CannotReachRemoteException();
}

$headers = ['X-Nextcloud-Talk-Proxy-Hash' => $proxy->getHeader('X-Nextcloud-Talk-Hash')];

return new DataResponse([], $statusCode, $headers);
}

/**
* @see \OCA\Talk\Controller\RoomController::getCapabilities()
*
Expand All @@ -98,7 +125,9 @@ public function getCapabilities(Room $room, Participant $participant): DataRespo
/** @var TalkCapabilities|array<empty> $data */
$data = $this->proxy->getOCSData($proxy);

$headers = ['X-Nextcloud-Talk-Hash' => $proxy->getHeader('X-Nextcloud-Talk-Hash')];
$headers = [
'X-Nextcloud-Talk-Hash' => $proxy->getHeader('X-Nextcloud-Talk-Hash'),
];

return new DataResponse($data, Http::STATUS_OK, $headers);
}
Expand Down
119 changes: 119 additions & 0 deletions openapi-federation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,125 @@
}
}
}
},
"/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/federation/active": {
"post": {
"operationId": "room-join-federated-room",
"summary": "Fake join a room on the host server to verify the federated user is still part of it",
"tags": [
"room"
],
"security": [
{},
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "apiVersion",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"v4"
],
"default": "v4"
}
},
{
"name": "token",
"in": "path",
"description": "Token of the room",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-z0-9]{4,30}$"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
"description": "Required to be true for the API request to pass",
"required": true,
"schema": {
"type": "boolean",
"default": true
}
}
],
"responses": {
"200": {
"description": "Federated user is still part of the room",
"headers": {
"X-Nextcloud-Talk-Hash": {
"schema": {
"type": "string"
}
}
},
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
},
"404": {
"description": "Room not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"nullable": true
}
}
}
}
}
}
}
}
}
}
}
},
"tags": []
Expand Down
Loading

0 comments on commit 3e26411

Please sign in to comment.