From 2343f89a6c0642964f0f8bba581cc83ba7e59329 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 1 Mar 2024 14:17:02 +0100 Subject: [PATCH] feat(federation): Proxy the Talk-Hash header so clients are aware of changes Signed-off-by: Joas Schilling --- appinfo/routes/routesRoomController.php | 2 + lib/Controller/RoomController.php | 99 +++++++++++-- .../TalkV1/Controller/RoomController.php | 31 ++++- openapi-federation.json | 119 ++++++++++++++++ openapi-full.json | 131 ++++++++++++++++++ openapi.json | 12 ++ src/types/openapi/openapi-federation.ts | 45 ++++++ src/types/openapi/openapi-full.ts | 49 +++++++ src/types/openapi/openapi.ts | 4 + 9 files changed, 479 insertions(+), 13 deletions(-) diff --git a/appinfo/routes/routesRoomController.php b/appinfo/routes/routesRoomController.php index 1da2316a857..ede5727ff54 100644 --- a/appinfo/routes/routesRoomController.php +++ b/appinfo/routes/routesRoomController.php @@ -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() */ diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index eb2263a86f6..90e792285c9 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -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, @@ -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|DataResponse, array{}>|DataResponse + * @return DataResponse|DataResponse, array{}>|DataResponse * * 200: Room joined successfully * 403: Joining room is not allowed @@ -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); @@ -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, array{X-Nextcloud-Talk-Hash: string}>|DataResponse + * + * 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; + } } /** @@ -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 { @@ -2155,7 +2223,7 @@ public function setMessageExpiration(int $seconds): DataResponse { /** * Get capabilities for a room * - * @return DataResponse, array{X-Nextcloud-Talk-Hash: string}> + * @return DataResponse, array{X-Nextcloud-Talk-Hash?: string, X-Nextcloud-Talk-Proxy-Hash?: string}> * * 200: Get capabilities successfully */ @@ -2163,15 +2231,22 @@ public function setMessageExpiration(int $seconds): DataResponse { #[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); } } diff --git a/lib/Federation/Proxy/TalkV1/Controller/RoomController.php b/lib/Federation/Proxy/TalkV1/Controller/RoomController.php index 29180f30fe1..73ef617b754 100644 --- a/lib/Federation/Proxy/TalkV1/Controller/RoomController.php +++ b/lib/Federation/Proxy/TalkV1/Controller/RoomController.php @@ -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, 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() * @@ -98,7 +125,9 @@ public function getCapabilities(Room $room, Participant $participant): DataRespo /** @var TalkCapabilities|array $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); } diff --git a/openapi-federation.json b/openapi-federation.json index 94f1cc13e13..ed3d8cf2802 100644 --- a/openapi-federation.json +++ b/openapi-federation.json @@ -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": [] diff --git a/openapi-full.json b/openapi-full.json index 79512b6a4e8..195e2659e92 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -13330,6 +13330,13 @@ "responses": { "200": { "description": "Room joined successfully", + "headers": { + "X-Nextcloud-Talk-Proxy-Hash": { + "schema": { + "type": "string" + } + } + }, "content": { "application/json": { "schema": { @@ -15243,6 +15250,11 @@ "schema": { "type": "string" } + }, + "X-Nextcloud-Talk-Proxy-Hash": { + "schema": { + "type": "string" + } } }, "content": { @@ -18303,6 +18315,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 + } + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/recording/backend": { "post": { "operationId": "recording-backend", diff --git a/openapi.json b/openapi.json index 93b882007eb..e7b9d229213 100644 --- a/openapi.json +++ b/openapi.json @@ -13435,6 +13435,13 @@ "responses": { "200": { "description": "Room joined successfully", + "headers": { + "X-Nextcloud-Talk-Proxy-Hash": { + "schema": { + "type": "string" + } + } + }, "content": { "application/json": { "schema": { @@ -15348,6 +15355,11 @@ "schema": { "type": "string" } + }, + "X-Nextcloud-Talk-Proxy-Hash": { + "schema": { + "type": "string" + } } }, "content": { diff --git a/src/types/openapi/openapi-federation.ts b/src/types/openapi/openapi-federation.ts index c34cbe8f98b..9ab19f8d777 100644 --- a/src/types/openapi/openapi-federation.ts +++ b/src/types/openapi/openapi-federation.ts @@ -29,6 +29,10 @@ export type paths = { */ get: operations["federation-get-shares"]; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/federation/active": { + /** Fake join a room on the host server to verify the federated user is still part of it */ + post: operations["room-join-federated-room"]; + }; }; export type webhooks = Record; @@ -396,4 +400,45 @@ export type operations = { }; }; }; + /** Fake join a room on the host server to verify the federated user is still part of it */ + "room-join-federated-room": { + parameters: { + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + /** @description Token of the room */ + token: string; + }; + }; + responses: { + /** @description Federated user is still part of the room */ + 200: { + headers: { + "X-Nextcloud-Talk-Hash"?: string; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Room not found */ + 404: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; }; diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 711bce6fba2..a2e752054c1 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -468,6 +468,10 @@ export type paths = { */ get: operations["federation-get-shares"]; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/room/{token}/federation/active": { + /** Fake join a room on the host server to verify the federated user is still part of it */ + post: operations["room-join-federated-room"]; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/recording/backend": { /** Update the recording status as a backend */ post: operations["recording-backend"]; @@ -4547,6 +4551,9 @@ export type operations = { responses: { /** @description Room joined successfully */ 200: { + headers: { + "X-Nextcloud-Talk-Proxy-Hash"?: string; + }; content: { "application/json": { ocs: { @@ -5208,6 +5215,7 @@ export type operations = { 200: { headers: { "X-Nextcloud-Talk-Hash"?: string; + "X-Nextcloud-Talk-Proxy-Hash"?: string; }; content: { "application/json": { @@ -6221,6 +6229,47 @@ export type operations = { }; }; }; + /** Fake join a room on the host server to verify the federated user is still part of it */ + "room-join-federated-room": { + parameters: { + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + /** @description Token of the room */ + token: string; + }; + }; + responses: { + /** @description Federated user is still part of the room */ + 200: { + headers: { + "X-Nextcloud-Talk-Hash"?: string; + }; + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + /** @description Room not found */ + 404: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: unknown; + }; + }; + }; + }; + }; + }; /** Update the recording status as a backend */ "recording-backend": { parameters: { diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 98e6ce34317..c20dd956870 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -4466,6 +4466,9 @@ export type operations = { responses: { /** @description Room joined successfully */ 200: { + headers: { + "X-Nextcloud-Talk-Proxy-Hash"?: string; + }; content: { "application/json": { ocs: { @@ -5127,6 +5130,7 @@ export type operations = { 200: { headers: { "X-Nextcloud-Talk-Hash"?: string; + "X-Nextcloud-Talk-Proxy-Hash"?: string; }; content: { "application/json": {