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
38 changes: 36 additions & 2 deletions lib/Controller/SignalingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,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 @@ -115,12 +137,16 @@ 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'])]
#[RequestHeader(name: 'talk-recording-random', description: 'Random seed used to generate the request checksum', indirect: true)]
#[RequestHeader(name: 'talk-recording-checksum', description: 'Checksum over the request body to verify authenticity from the recording backend', indirect: true)]
#[RequestHeader(name: 'talk-sipbridge-random', description: 'Random seed used to generate the request checksum', indirect: true)]
#[RequestHeader(name: 'talk-sipbridge-checksum', description: 'Checksum over the room token to verify authenticity from the SIP bridge', indirect: true)]
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 @@ -130,6 +156,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;
} elseif ($this->serverSession->get('app_api') === true) {
// Live transcription ex-app
$isRecordingRequest = true;
Expand Down Expand Up @@ -157,9 +191,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 || $isRecordingRequest) {
} elseif ($this->userId !== null || $isRecordingRequest || $isSIPBridgeRequest) {
// Mobile clients and admin setup check use the neutral point
// Same for live-transcription
// Same for live-transcription and SIP bridge
$room = null;
} else {
throw new RoomNotFoundException();
Expand Down
16 changes: 16 additions & 0 deletions openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -18768,6 +18768,22 @@
"type": "string"
}
},
{
"name": "talk-sipbridge-random",
"in": "header",
"description": "Random seed used to generate the request checksum",
"schema": {
"type": "string"
}
},
{
"name": "talk-sipbridge-checksum",
"in": "header",
"description": "Checksum over the room token to verify authenticity from the SIP bridge",
"schema": {
"type": "string"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
Expand Down
16 changes: 16 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -18673,6 +18673,22 @@
"type": "string"
}
},
{
"name": "talk-sipbridge-random",
"in": "header",
"description": "Random seed used to generate the request checksum",
"schema": {
"type": "string"
}
},
{
"name": "talk-sipbridge-checksum",
"in": "header",
"description": "Checksum over the room token to verify authenticity from the SIP bridge",
"schema": {
"type": "string"
}
},
{
"name": "OCS-APIRequest",
"in": "header",
Expand Down
4 changes: 4 additions & 0 deletions src/types/openapi/openapi-full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9677,6 +9677,10 @@ export interface operations {
"talk-recording-random"?: string;
/** @description Checksum over the request body to verify authenticity from the recording backend */
"talk-recording-checksum"?: string;
/** @description Random seed used to generate the request checksum */
"talk-sipbridge-random"?: string;
/** @description Checksum over the room token to verify authenticity from the SIP bridge */
"talk-sipbridge-checksum"?: string;
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
Expand Down
4 changes: 4 additions & 0 deletions src/types/openapi/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9139,6 +9139,10 @@ export interface operations {
"talk-recording-random"?: string;
/** @description Checksum over the request body to verify authenticity from the recording backend */
"talk-recording-checksum"?: string;
/** @description Random seed used to generate the request checksum */
"talk-sipbridge-random"?: string;
/** @description Checksum over the room token to verify authenticity from the SIP bridge */
"talk-sipbridge-checksum"?: string;
/** @description Required to be true for the API request to pass */
"OCS-APIRequest": boolean;
};
Expand Down
82 changes: 82 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\Config\IUserConfig;
Expand Down Expand Up @@ -1447,4 +1448,85 @@ 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 => '',
});

$this->serverSession->method('get')->willReturn(null);

$result = $this->controller->getSettings();
$this->assertSame(Http::STATUS_OK, $result->getStatus());
}
}
Loading