Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 34 additions & 1 deletion lib/Controller/SignalingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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('')) {
Expand All @@ -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();
Expand All @@ -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();
Expand Down
80 changes: 80 additions & 0 deletions tests/php/Controller/SignalingControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
Loading