summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/capabilities.md1
-rw-r--r--docs/internal-signaling.md1
-rw-r--r--lib/Capabilities.php5
-rw-r--r--lib/Config.php124
-rw-r--r--lib/Controller/SignalingController.php20
-rw-r--r--lib/Signaling/Manager.php3
-rw-r--r--src/utils/signaling.js58
-rw-r--r--src/utils/webrtc/index.js6
-rw-r--r--tests/php/CapabilitiesTest.php17
-rw-r--r--tests/php/ConfigTest.php214
-rw-r--r--tests/php/Controller/SignalingControllerTest.php25
-rw-r--r--tests/php/Signaling/BackendNotifierTest.php3
12 files changed, 401 insertions, 76 deletions
diff --git a/docs/capabilities.md b/docs/capabilities.md
index 311ffbcc1..9659e7428 100644
--- a/docs/capabilities.md
+++ b/docs/capabilities.md
@@ -104,3 +104,4 @@ title: Capabilities
* `talk-polls` - Polls feature is available to use in the chat
* `config => call => enabled` - Whether calling is enabled on the instance or not
* `config => signaling => session-ping-limit` - Number of sessions the HPB is allowed to ping in the same request
+* `config => signaling => hello-v2-token-key` - Public key to use when verifying JWT auth tokens for hello V2.
diff --git a/docs/internal-signaling.md b/docs/internal-signaling.md
index fc75d4e29..000cb7c31 100644
--- a/docs/internal-signaling.md
+++ b/docs/internal-signaling.md
@@ -26,6 +26,7 @@
| `stunservers` | array | v3 | STUN servers |
| `turnservers` | array | v3 | TURN servers |
| `sipDialinInfo` | string | v2 | Generic SIP dial-in information for this conversation (admin free text containing the phone number etc) |
+| `helloAuthParams` | array | v3 | Parameters of the different `hello` versions for the external signaling server. |
- STUN server
diff --git a/lib/Capabilities.php b/lib/Capabilities.php
index f0a84e74e..c388347be 100644
--- a/lib/Capabilities.php
+++ b/lib/Capabilities.php
@@ -150,6 +150,11 @@ class Capabilities implements IPublicCapability {
$capabilities['config']['conversations']['can-create'] = $user instanceof IUser && !$this->talkConfig->isNotAllowedToCreateConversations($user);
+ $pubKey = $this->talkConfig->getSignalingTokenPublicKey();
+ if ($pubKey) {
+ $capabilities['config']['signaling']['hello-v2-token-key'] = $pubKey;
+ }
+
if ($this->serverConfig->getAppValue('spreed', 'has_reference_id', 'no') === 'yes') {
$capabilities['features'][] = 'chat-reference-id';
}
diff --git a/lib/Config.php b/lib/Config.php
index 1628aa1cd..b3dda8d85 100644
--- a/lib/Config.php
+++ b/lib/Config.php
@@ -23,12 +23,16 @@ declare(strict_types=1);
namespace OCA\Talk;
+use Firebase\JWT\JWT;
+
use OCP\AppFramework\Utility\ITimeFactory;
use OCA\Talk\Events\GetTurnServersEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IGroupManager;
+use OCP\IURLGenerator;
use OCP\IUser;
+use OCP\IUserManager;
use OCP\Security\ISecureRandom;
class Config {
@@ -36,9 +40,14 @@ class Config {
public const SIGNALING_EXTERNAL = 'external';
public const SIGNALING_CLUSTER_CONVERSATION = 'conversation_cluster';
+ public const SIGNALING_TICKET_V1 = 1;
+ public const SIGNALING_TICKET_V2 = 2;
+
protected IConfig $config;
protected ITimeFactory $timeFactory;
private IGroupManager $groupManager;
+ private IUserManager $userManager;
+ private IURLGenerator $urlGenerator;
private ISecureRandom $secureRandom;
private IEventDispatcher $dispatcher;
@@ -47,11 +56,15 @@ class Config {
public function __construct(IConfig $config,
ISecureRandom $secureRandom,
IGroupManager $groupManager,
+ IUserManager $userManager,
+ IURLGenerator $urlGenerator,
ITimeFactory $timeFactory,
IEventDispatcher $dispatcher) {
$this->config = $config;
$this->secureRandom = $secureRandom;
$this->groupManager = $groupManager;
+ $this->userManager = $userManager;
+ $this->urlGenerator = $urlGenerator;
$this->timeFactory = $timeFactory;
$this->dispatcher = $dispatcher;
}
@@ -340,10 +353,26 @@ class Config {
}
/**
+ * @param int $version
* @param string $userId
* @return string
*/
- public function getSignalingTicket(?string $userId): string {
+ public function getSignalingTicket(int $version, ?string $userId): string {
+ switch ($version) {
+ case self::SIGNALING_TICKET_V1:
+ return $this->getSignalingTicketV1($userId);
+ case self::SIGNALING_TICKET_V2:
+ return $this->getSignalingTicketV2($userId);
+ default:
+ return $this->getSignalingTicketV1($userId);
+ }
+ }
+
+ /**
+ * @param string $userId
+ * @return string
+ */
+ private function getSignalingTicketV1(?string $userId): string {
if (empty($userId)) {
$secret = $this->config->getAppValue('spreed', 'signaling_ticket_secret');
} else {
@@ -369,6 +398,99 @@ class Config {
return $data . ':' . $hash;
}
+ private function ensureSignalingTokenKeys(string $alg): void {
+ $secret = $this->config->getAppValue('spreed', 'signaling_token_privkey_' . strtolower($alg));
+ if ($secret) {
+ return;
+ }
+
+ if (substr($alg, 0, 2) === 'ES') {
+ $privKey = openssl_pkey_new([
+ 'curve_name' => 'prime256v1',
+ 'private_key_type' => OPENSSL_KEYTYPE_EC,
+ ]);
+ $pubKey = openssl_pkey_get_details($privKey);
+ $public = $pubKey['key'];
+ if (!openssl_pkey_export($privKey, $secret)) {
+ throw new \Exception('Could not export private key');
+ }
+ } elseif (substr($alg, 0, 2) === 'RS') {
+ $privKey = openssl_pkey_new([
+ 'private_key_bits' => 2048,
+ 'private_key_type' => OPENSSL_KEYTYPE_RSA,
+ ]);
+ $pubKey = openssl_pkey_get_details($privKey);
+ $public = $pubKey['key'];
+ if (!openssl_pkey_export($privKey, $secret)) {
+ throw new \Exception('Could not export private key');
+ }
+ } elseif ($alg === 'EdDSA') {
+ $privKey = sodium_crypto_sign_keypair();
+ $public = base64_encode(sodium_crypto_sign_publickey($privKey));
+ $secret = base64_encode(sodium_crypto_sign_secretkey($privKey));
+ } else {
+ throw new \Exception('Unsupported algorithm ' . $alg);
+ }
+
+ $this->config->setAppValue('spreed', 'signaling_token_privkey_' . strtolower($alg), $secret);
+ $this->config->setAppValue('spreed', 'signaling_token_pubkey_' . strtolower($alg), $public);
+ }
+
+ public function getSignalingTokenAlgorithm(): string {
+ return $this->config->getAppValue('spreed', 'signaling_token_alg', 'ES256');
+ }
+
+ public function getSignalingTokenPrivateKey(?string $alg = null): string {
+ if (!$alg) {
+ $alg = $this->getSignalingTokenAlgorithm();
+ }
+ $this->ensureSignalingTokenKeys($alg);
+
+ return $this->config->getAppValue('spreed', 'signaling_token_privkey_' . strtolower($alg));
+ }
+
+ public function getSignalingTokenPublicKey(?string $alg = null): string {
+ if (!$alg) {
+ $alg = $this->getSignalingTokenAlgorithm();
+ }
+ $this->ensureSignalingTokenKeys($alg);
+
+ return $this->config->getAppValue('spreed', 'signaling_token_pubkey_' . strtolower($alg));
+ }
+
+ /**
+ * @param IUser $user
+ * @return array
+ */
+ public function getSignalingUserData(IUser $user): array {
+ return [
+ 'displayname' => $user->getDisplayName(),
+ ];
+ }
+
+ /**
+ * @param string $userId
+ * @return string
+ */
+ private function getSignalingTicketV2(?string $userId): string {
+ $timestamp = $this->timeFactory->getTime();
+ $data = [
+ 'iss' => $this->urlGenerator->getAbsoluteURL(''),
+ 'iat' => $timestamp,
+ 'exp' => $timestamp + 60, // Valid for 1 minute.
+ ];
+ $user = !empty($userId) ? $this->userManager->get($userId) : null;
+ if ($user instanceof IUser) {
+ $data['sub'] = $user->getUID();
+ $data['userdata'] = $this->getSignalingUserData($user);
+ }
+
+ $alg = $this->getSignalingTokenAlgorithm();
+ $secret = $this->getSignalingTokenPrivateKey($alg);
+ $token = JWT::encode($data, $secret, $alg);
+ return $token;
+ }
+
/**
* @param string $userId
* @param string $ticket
diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php
index 40a2ac3e5..20954020f 100644
--- a/lib/Controller/SignalingController.php
+++ b/lib/Controller/SignalingController.php
@@ -46,6 +46,7 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\Exception;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Http\Client\IClientService;
+use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\IUser;
@@ -59,6 +60,7 @@ class SignalingController extends OCSController {
public const EVENT_BACKEND_SIGNALING_ROOMS = self::class . '::signalingBackendRoom';
+ private IConfig $serverConfig;
private Config $talkConfig;
private \OCA\Talk\Signaling\Manager $signalingManager;
private TalkSession $session;
@@ -76,6 +78,7 @@ class SignalingController extends OCSController {
public function __construct(string $appName,
IRequest $request,
+ IConfig $serverConfig,
Config $talkConfig,
\OCA\Talk\Signaling\Manager $signalingManager,
TalkSession $session,
@@ -91,6 +94,7 @@ class SignalingController extends OCSController {
LoggerInterface $logger,
?string $UserId) {
parent::__construct($appName, $request);
+ $this->serverConfig = $serverConfig;
$this->talkConfig = $talkConfig;
$this->signalingManager = $signalingManager;
$this->session = $session;
@@ -160,12 +164,22 @@ class SignalingController extends OCSController {
$signalingMode = $this->talkConfig->getSignalingMode();
$signaling = $this->signalingManager->getSignalingServerLinkForConversation($room);
+ $helloAuthParams = [
+ '1.0' => [
+ 'userid' => $this->userId,
+ 'ticket' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V1, $this->userId),
+ ],
+ '2.0' => [
+ 'token' => $this->talkConfig->getSignalingTicket(Config::SIGNALING_TICKET_V2, $this->userId),
+ ],
+ ];
$data = [
'signalingMode' => $signalingMode,
'userId' => $this->userId,
'hideWarning' => $signaling !== '' || $this->talkConfig->getHideSignalingWarning(),
'server' => $signaling,
- 'ticket' => $this->talkConfig->getSignalingTicket($this->userId),
+ 'ticket' => $helloAuthParams['1.0']['ticket'],
+ 'helloAuthParams' => $helloAuthParams,
'stunservers' => $stun,
'turnservers' => $turn,
'sipDialinInfo' => $this->talkConfig->isSIPConfigured() ? $this->talkConfig->getDialInInfo() : '',
@@ -546,9 +560,7 @@ class SignalingController extends OCSController {
];
if (!empty($userId)) {
$response['auth']['userid'] = $user->getUID();
- $response['auth']['user'] = [
- 'displayname' => $user->getDisplayName(),
- ];
+ $response['auth']['user'] = $this->talkConfig->getSignalingUserData($user);
}
$this->logger->debug('Validated signaling ticket for {user}', [
'user' => !empty($userId) ? $userId : '(guests)',
diff --git a/lib/Signaling/Manager.php b/lib/Signaling/Manager.php
index b69561343..87f472900 100644
--- a/lib/Signaling/Manager.php
+++ b/lib/Signaling/Manager.php
@@ -54,7 +54,8 @@ class Manager {
$features = explode(',', $featureHeader);
$features = array_map('trim', $features);
return in_array('audio-video-permissions', $features, true)
- && in_array('incall-all', $features, true);
+ && in_array('incall-all', $features, true)
+ && in_array('hello-v2', $features, true);
}
public function getSignalingServerLinkForConversation(?Room $room): string {
diff --git a/src/utils/signaling.js b/src/utils/signaling.js
index 6c711961d..56c98faaf 100644
--- a/src/utils/signaling.js
+++ b/src/utils/signaling.js
@@ -590,6 +590,7 @@ function Standalone(settings, urls) {
url = url.slice(0, -1)
}
this.url = url + '/spreed'
+ this.welcomeTimeoutMs = 3000
this.initialReconnectIntervalMs = 1000
this.maxReconnectIntervalMs = 16000
this.reconnectIntervalMs = this.initialReconnectIntervalMs
@@ -658,7 +659,11 @@ Signaling.Standalone.prototype.connect = function() {
this.signalingConnectionWarning = null
}
this.reconnectIntervalMs = this.initialReconnectIntervalMs
- this.sendHello()
+ if (this.settings.helloAuthParams['2.0']) {
+ this.waitForWelcomeTimeout = setTimeout(this.welcomeTimeout.bind(this), this.welcomeTimeoutMs)
+ } else {
+ this.sendHello()
+ }
}.bind(this)
this.socket.onerror = function(event) {
console.error('Error', event)
@@ -713,6 +718,9 @@ Signaling.Standalone.prototype.connect = function() {
this._trigger('onBeforeReceiveMessage', [data])
const message = {}
switch (data.type) {
+ case 'welcome':
+ this.welcomeReceived(data)
+ break
case 'hello':
if (!id) {
// Only process if not received as result of our "hello".
@@ -744,10 +752,17 @@ Signaling.Standalone.prototype.connect = function() {
this._trigger('message', [message])
break
case 'error':
- if (data.error.code === 'processing_failed') {
+ switch (data.error.code) {
+ case 'processing_failed':
console.error('An error occurred processing the signaling message, please ask your server administrator to check the log file')
- } else {
+ break
+ case 'token_expired':
+ console.info('The signaling token is expired, need to update settings')
+ this._trigger('updateSettings')
+ break
+ default:
console.error('Ignore unknown error', data)
+ break
}
break
default:
@@ -760,6 +775,30 @@ Signaling.Standalone.prototype.connect = function() {
}.bind(this)
}
+Signaling.Standalone.prototype.welcomeReceived = function(data) {
+ console.debug('Welcome received', data)
+ if (this.waitForWelcomeTimeout !== null) {
+ clearTimeout(this.waitForWelcomeTimeout)
+ this.waitForWelcomeTimeout = null
+ }
+
+ this.features = {}
+ let i
+ if (data.welcome && data.welcome.features) {
+ const features = data.welcome.features
+ for (i = 0; i < features.length; i++) {
+ this.features[features[i]] = true
+ }
+ }
+
+ this.sendHello()
+}
+
+Signaling.Standalone.prototype.welcomeTimeout = function() {
+ console.warn('No welcome received, assuming old-style signaling server')
+ this.sendHello()
+}
+
Signaling.Standalone.prototype.sendBye = function() {
if (this.connected) {
this.doSend({
@@ -901,16 +940,19 @@ Signaling.Standalone.prototype.sendHello = function() {
// Already reconnected with a new session.
this._forceReconnect = false
const url = generateOcsUrl('apps/spreed/api/v3/signaling/backend')
+ let helloVersion
+ if (this.hasFeature('hello-v2') && this.settings.helloAuthParams['2.0']) {
+ helloVersion = '2.0'
+ } else {
+ helloVersion = '1.0'
+ }
msg = {
type: 'hello',
hello: {
- version: '1.0',
+ version: helloVersion,
auth: {
url,
- params: {
- userid: this.settings.userId,
- ticket: this.settings.ticket,
- },
+ params: this.settings.helloAuthParams[helloVersion],
},
},
}
diff --git a/src/utils/webrtc/index.js b/src/utils/webrtc/index.js
index 5840e3795..2f2db261f 100644
--- a/src/utils/webrtc/index.js
+++ b/src/utils/webrtc/index.js
@@ -101,6 +101,12 @@ async function connectSignaling(token) {
if (!signaling) {
signaling = Signaling.createConnection(settings)
+ signaling.on('updateSettings', async function() {
+ const settings = await getSignalingSettings(token)
+ console.debug('Received updated settings', settings)
+ signaling.settings = settings
+ })
+
}
tokensInSignaling[token] = true
diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php
index 39017081b..3c050c089 100644
--- a/tests/php/CapabilitiesTest.php
+++ b/tests/php/CapabilitiesTest.php
@@ -316,4 +316,21 @@ class CapabilitiesTest extends TestCase {
$this->assertInstanceOf(IPublicCapability::class, $capabilities);
$this->assertSame([], $capabilities->getCapabilities());
}
+
+ public function testCapabilitiesHelloV2Key(): void {
+ $capabilities = new Capabilities(
+ $this->serverConfig,
+ $this->talkConfig,
+ $this->commentsManager,
+ $this->userSession,
+ $this->appManager
+ );
+
+ $this->talkConfig->expects($this->once())
+ ->method('getSignalingTokenPublicKey')
+ ->willReturn('this-is-the-key');
+
+ $data = $capabilities->getCapabilities();
+ $this->assertEquals('this-is-the-key', $data['spreed']['config']['signaling']['hello-v2-token-key']);
+ }
}
diff --git a/tests/php/ConfigTest.php b/tests/php/ConfigTest.php
index 80fed760e..569d89177 100644
--- a/tests/php/ConfigTest.php
+++ b/tests/php/ConfigTest.php
@@ -20,6 +20,9 @@
*/
namespace OCA\Talk\Tests\php;
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
use OCA\Talk\Config;
use OCA\Talk\Events\GetTurnServersEvent;
use OCA\Talk\Tests\php\Mocks\GetTurnServerListener;
@@ -27,25 +30,38 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IGroupManager;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserManager;
use OCP\Security\ISecureRandom;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class ConfigTest extends TestCase {
- public function testGetStunServers() {
- $servers = [
- 'stun1.example.com:443',
- 'stun2.example.com:129',
- ];
-
+ private function createConfig(IConfig $config) {
/** @var MockObject|ITimeFactory $timeFactory */
$timeFactory = $this->createMock(ITimeFactory::class);
/** @var MockObject|ISecureRandom $secureRandom */
$secureRandom = $this->createMock(ISecureRandom::class);
- /** @var MockObject|IGroupManager $secureRandom */
+ /** @var MockObject|IGroupManager $groupManager */
$groupManager = $this->createMock(IGroupManager::class);
+ /** @var MockObject|IUserManager $userManager */
+ $userManager = $this->createMock(IUserManager::class);
+ /** @var MockObject|IURLGenerator $urlGenerator */
+ $urlGenerator = $this->createMock(IURLGenerator::class);
/** @var MockObject|IEventDispatcher $dispatcher */
$dispatcher = $this->createMock(IEventDispatcher::class);
+
+ $helper = new Config($config, $secureRandom, $groupManager, $userManager, $urlGenerator, $timeFactory, $dispatcher);
+ return $helper;
+ }
+
+ public function testGetStunServers() {
+ $servers = [
+ 'stun1.example.com:443',
+ 'stun2.example.com:129',
+ ];
+
/** @var MockObject|IConfig $config */
$config = $this->createMock(IConfig::class);
$config
@@ -59,19 +75,11 @@ class ConfigTest extends TestCase {
->with('has_internet_connection', true)
->willReturn(true);
- $helper = new Config($config, $secureRandom, $groupManager, $timeFactory, $dispatcher);
+ $helper = $this->createConfig($config);
$this->assertSame($helper->getStunServers(), $servers);
}
public function testGetDefaultStunServer() {
- /** @var MockObject|ITimeFactory $timeFactory */
- $timeFactory = $this->createMock(ITimeFactory::class);
- /** @var MockObject|ISecureRandom $secureRandom */
- $secureRandom = $this->createMock(ISecureRandom::class);
- /** @var MockObject|IGroupManager $secureRandom */
- $groupManager = $this->createMock(IGroupManager::class);
- /** @var MockObject|IEventDispatcher $dispatcher */
- $dispatcher = $this->createMock(IEventDispatcher::class);
/** @var MockObject|IConfig $config */
$config = $this->createMock(IConfig::class);
$config
@@ -85,19 +93,11 @@ class ConfigTest extends TestCase {
->with('has_internet_connection', true)
->willReturn(true);
- $helper = new Config($config, $secureRandom, $groupManager, $timeFactory, $dispatcher);
+ $helper = $this->createConfig($config);
$this->assertSame(['stun.nextcloud.com:443'], $helper->getStunServers());
}
public function testGetDefaultStunServerNoInternet() {
- /** @var MockObject|ITimeFactory $timeFactory */
- $timeFactory = $this->createMock(ITimeFactory::class);
- /** @var MockObject|ISecureRandom $secureRandom */
- $secureRandom = $this->createMock(ISecureRandom::class);
- /** @var MockObject|IGroupManager $secureRandom */
- $groupManager = $this->createMock(IGroupManager::class);
- /** @var MockObject|IEventDispatcher $dispatcher */
- $dispatcher = $this->createMock(IEventDispatcher::class);
/** @var MockObject|IConfig $config */
$config = $this->createMock(IConfig::class);
$config
@@ -111,7 +111,7 @@ class ConfigTest extends TestCase {
->with('has_internet_connection', true)
->willReturn(false);
- $helper = new Config($config, $secureRandom, $groupManager, $timeFactory, $dispatcher);
+ $helper = $this->createConfig($config);
$this->assertSame([], $helper->getStunServers());
}
@@ -151,8 +151,12 @@ class ConfigTest extends TestCase {
->method('getTime')
->willReturn(1479743025);
- /** @var MockObject|IGroupManager $secureRandom */
+ /** @var MockObject|IGroupManager $groupManager */
$groupManager = $this->createMock(IGroupManager::class);
+ /** @var MockObject|IUserManager $userManager */
+ $userManager = $this->createMock(IUserMana