diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index b9557b4a5f0..3c41bcde046 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -99,6 +99,28 @@ private function validateRecordingBackendRequest(string $data): bool { } } + /** + * Check if the current request is coming from an allowed SIP bridge. + * + * The bridge sends the custom header "Talk-SIPBridge-Random" containing + * at least 32 bytes random data, and the header "Talk-SIPBridge-Checksum", + * which is the SHA256-HMAC of the random data and the room token, + * calculated with the shared secret from the configuration. + * + * @param string $data Room token (or empty string when no token is present) + * @return bool + */ + private function validateSIPBridgeRequest(string $data): bool { + $random = $this->request->getHeader('Talk-SIPBridge-Random'); + $checksum = $this->request->getHeader('Talk-SIPBridge-Checksum'); + $secret = $this->talkConfig->getSIPSharedSecret(); + try { + return $this->checksumVerificationService->validateRequest($random, $checksum, $secret, $data); + } catch (UnauthorizedException) { + return false; + } + } + /** * Get the signaling settings * @@ -112,10 +134,12 @@ private function validateRecordingBackendRequest(string $data): bool { #[PublicPage] #[BruteForceProtection(action: 'talkRoomToken')] #[BruteForceProtection(action: 'talkRecordingSecret')] + #[BruteForceProtection(action: 'talkSipBridgeSecret')] #[BruteForceProtection(action: 'talkFederationAccess')] #[OpenAPI(tags: ['internal_signaling', 'external_signaling'])] public function getSettings(string $token = ''): DataResponse { $isRecordingRequest = false; + $isSIPBridgeRequest = false; if (!empty($this->request->getHeader('Talk-Recording-Random')) || !empty($this->request->getHeader('Talk-Recording-Checksum'))) { if (!$this->validateRecordingBackendRequest('')) { @@ -125,6 +149,14 @@ public function getSettings(string $token = ''): DataResponse { } $isRecordingRequest = true; + } elseif (!empty($this->request->getHeader('Talk-SIPBridge-Random')) || !empty($this->request->getHeader('Talk-SIPBridge-Checksum'))) { + if (!$this->validateSIPBridgeRequest($token)) { + $response = new DataResponse(null, Http::STATUS_UNAUTHORIZED); + $response->throttle(['action' => 'talkSipBridgeSecret']); + return $response; + } + + $isSIPBridgeRequest = true; } $isTalkFederation = $this->federationAuthenticator->isFederationRequest(); @@ -149,8 +181,9 @@ public function getSettings(string $token = ''): DataResponse { $this->federationAuthenticator->authenticated($room, $participant); } elseif ($token !== '') { $room = $this->manager->getRoomForUserByToken($token, $this->userId); - } elseif ($this->userId !== null) { + } elseif ($this->userId !== null || $isSIPBridgeRequest) { // Mobile clients and admin setup check use the neutral point + // Same for SIP bridge $room = null; } else { throw new RoomNotFoundException(); diff --git a/tests/php/Controller/SignalingControllerTest.php b/tests/php/Controller/SignalingControllerTest.php index af3da3b21b2..5799bf67bac 100644 --- a/tests/php/Controller/SignalingControllerTest.php +++ b/tests/php/Controller/SignalingControllerTest.php @@ -29,6 +29,7 @@ use OCA\Talk\Signaling\Messages; use OCA\Talk\TalkSession; use OCP\App\IAppManager; +use OCP\AppFramework\Http; use OCP\AppFramework\Services\IAppConfig; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; @@ -1443,4 +1444,83 @@ public function testLeaveRoomWithOldSession(): void { $participant = $participantService->getParticipant($room, $this->userId, $newSessionId); $this->assertEquals($newSessionId, $participant->getSession()->getSessionId()); } + + private const SIP_BRIDGE_SECRET = 'MySIPSecretValueMySIPSecretValue1234'; + + private function sipBridgeChecksum(string $data, string $random): string { + return hash_hmac('sha256', $random . $data, self::SIP_BRIDGE_SECRET); + } + + private function setUpSIPBridgeConfig(): void { + $this->config = $this->createMock(Config::class); + $this->config->method('getSIPSharedSecret')->willReturn(self::SIP_BRIDGE_SECRET); + $this->userId = null; + $this->recreateSignalingController(); + } + + public function testGetSettingsUnauthenticatedWithoutToken(): void { + $this->userId = null; + $this->recreateSignalingController(); + + $this->request->method('getHeader')->willReturn(''); + + $result = $this->controller->getSettings(); + $this->assertSame(Http::STATUS_NOT_FOUND, $result->getStatus()); + } + + public function testGetSettingsSIPBridgeInvalidChecksum(): void { + $this->setUpSIPBridgeConfig(); + + $random = 'afb6b872ab03e3376b31bf0af601067222ff7990335ca02d327071b73c0119c6'; + $this->request->method('getHeader') + ->willReturnCallback(fn (string $header): string => match ($header) { + 'Talk-SIPBridge-Random' => $random, + 'Talk-SIPBridge-Checksum' => 'invalid-checksum', + default => '', + }); + + $result = $this->controller->getSettings(); + $this->assertSame(Http::STATUS_UNAUTHORIZED, $result->getStatus()); + } + + public function testGetSettingsSIPBridgeShortRandom(): void { + $this->setUpSIPBridgeConfig(); + + $random = 'tooshort'; + $checksum = $this->sipBridgeChecksum('', $random); + $this->request->method('getHeader') + ->willReturnCallback(fn (string $header): string => match ($header) { + 'Talk-SIPBridge-Random' => $random, + 'Talk-SIPBridge-Checksum' => $checksum, + default => '', + }); + + $result = $this->controller->getSettings(); + $this->assertSame(Http::STATUS_UNAUTHORIZED, $result->getStatus()); + } + + public function testGetSettingsSIPBridgeValidNoToken(): void { + $this->config = $this->createMock(Config::class); + $this->config->method('getSIPSharedSecret')->willReturn(self::SIP_BRIDGE_SECRET); + $this->config->method('getStunServers')->willReturn([]); + $this->config->method('getTurnSettings')->willReturn([]); + $this->config->method('getSignalingMode')->willReturn(Config::SIGNALING_INTERNAL); + $this->config->method('getHideSignalingWarning')->willReturn(false); + $this->config->method('isSIPConfigured')->willReturn(false); + $this->signalingManager->method('getSignalingServerLinkForConversation')->willReturn(''); + $this->userId = null; + $this->recreateSignalingController(); + + $random = 'afb6b872ab03e3376b31bf0af601067222ff7990335ca02d327071b73c0119c6'; + $checksum = $this->sipBridgeChecksum('', $random); + $this->request->method('getHeader') + ->willReturnCallback(fn (string $header): string => match ($header) { + 'Talk-SIPBridge-Random' => $random, + 'Talk-SIPBridge-Checksum' => $checksum, + default => '', + }); + + $result = $this->controller->getSettings(); + $this->assertSame(Http::STATUS_OK, $result->getStatus()); + } }