From 8214f22a3102e2abd36d240a195b4948ad6e8e90 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Mon, 20 Nov 2017 14:00:24 +0100 Subject: [PATCH 1/8] Notify backend when "in-call" status of sessions changes. Signed-off-by: Joachim Bauch --- lib/AppInfo/Application.php | 16 ++++++++++++++++ lib/Signaling/BackendNotifier.php | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index cefddbf31f7..37ea601846d 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -133,6 +133,22 @@ protected function registerSignalingBackendHooks(EventDispatcherInterface $dispa $user = $event->getArgument('user'); $notifier->roomsDisinvited($room, [$user->getUID()]); }); + $dispatcher->addListener(Room::class . '::postSessionJoinCall', function(GenericEvent $event) { + /** @var BackendNotifier $notifier */ + $notifier = $this->getContainer()->query(BackendNotifier::class); + + $room = $event->getSubject(); + $sessionId = $event->getArgument('sessionId'); + $notifier->roomInCallChanged($room, true, [$sessionId]); + }); + $dispatcher->addListener(Room::class . '::postSessionLeaveCall', function(GenericEvent $event) { + /** @var BackendNotifier $notifier */ + $notifier = $this->getContainer()->query(BackendNotifier::class); + + $room = $event->getSubject(); + $sessionId = $event->getArgument('sessionId'); + $notifier->roomInCallChanged($room, false, [$sessionId]); + }); } protected function registerCallActivityHooks(EventDispatcherInterface $dispatcher) { diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index 37fc0cb5cb3..5c6f631e47e 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -198,4 +198,23 @@ public function roomDeleted($room, $participants) { ]); } + /** + * The "in-call" status of the given session ids has changed.. + * + * @param Room $room + * @param bool $inCall + * @param array $sessionids + * @throws \Exception + */ + public function roomInCallChanged($room, $inCall, $sessionIds) { + $this->logger->info('Room in-call status changed: ' . $room->getToken() . ' ' . $inCall . ' ' . print_r($sessionIds, true)); + $this->backendRequest('/api/v1/room/' . $room->getToken(), [ + 'type' => 'incall', + 'incall' => [ + 'incall' => $inCall, + 'sessionids' => $sessionIds, + ], + ]); + } + } From b3c601a8cb795d5536d25afd25ec42e04513d651 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Mon, 20 Nov 2017 14:10:55 +0100 Subject: [PATCH 2/8] Split "room" from "call" when using the standalone signaling server. Clients use the regular joinRoom/-Call API and get a Nextcloud session id. No special handling for sessions from the standalone signaling server are required. The signaling server regularly "pings" active sessions to prevent them from timing out (in case of guest users). Signed-off-by: Joachim Bauch --- js/signaling.js | 298 ++++++++++++++++--------- js/webrtc.js | 93 +++++--- lib/Controller/SignalingController.php | 77 ++++++- lib/Signaling/BackendNotifier.php | 24 +- 4 files changed, 338 insertions(+), 154 deletions(-) diff --git a/js/signaling.js b/js/signaling.js index 8788d81adcf..1a6ce373d69 100644 --- a/js/signaling.js +++ b/js/signaling.js @@ -137,6 +137,122 @@ return defer; }; + SignalingBase.prototype.joinRoom = function(token, password) { + $.ajax({ + url: OC.linkToOCS('apps/spreed/api/v1/room', 2) + token + '/participants/active', + type: 'POST', + beforeSend: function (request) { + request.setRequestHeader('Accept', 'application/json'); + }, + data: { + password: password + }, + success: function (result) { + console.log("Joined", result); + this.currentRoomToken = token; + this._joinRoomSuccess(token, result.ocs.data.sessionId); + }.bind(this), + error: function (result) { + if (result.status === 404 || result.status === 503) { + // Room not found or maintenance mode + OC.redirect(OC.generateUrl('apps/spreed')); + } + + if (result.status === 403) { + // This should not happen anymore since we ask for the password before + // even trying to join the call, but let's keep it for now. + OC.dialogs.prompt( + t('spreed', 'Please enter the password for this call'), + t('spreed','Password required'), + function (result, password) { + if (result && password !== '') { + this.joinRoom(token, password); + } + }.bind(this), + true, + t('spreed','Password'), + true + ).then(function() { + var $dialog = $('.oc-dialog:visible'); + $dialog.find('.ui-icon').remove(); + + var $buttons = $dialog.find('button'); + $buttons.eq(0).text(t('core', 'Cancel')); + $buttons.eq(1).text(t('core', 'Submit')); + }); + } + }.bind(this) + }); + }; + + SignalingBase.prototype._leaveRoomSuccess = function(/* token */) { + // Override in subclasses if necessary. + }; + + SignalingBase.prototype.leaveRoom = function(token) { + this.leaveCurrentCall(); + this._doLeaveRoom(token); + + $.ajax({ + url: OC.linkToOCS('apps/spreed/api/v1/room', 2) + token + '/participants/active', + method: 'DELETE', + async: false, + success: function () { + this._leaveRoomSuccess(token); + // We left the current room. + if (token === this.currentRoomToken) { + this.currentRoomToken = null; + } + }.bind(this) + }); + }; + + SignalingBase.prototype._joinCallSuccess = function(/* token */) { + // Override in subclasses if necessary. + }; + + SignalingBase.prototype.joinCall = function(token, callback) { + $.ajax({ + url: OC.linkToOCS('apps/spreed/api/v1/call', 2) + token, + type: 'POST', + beforeSend: function (request) { + request.setRequestHeader('Accept', 'application/json'); + }, + success: function () { + this.currentCallToken = token; + this._joinCallSuccess(token); + // We send an empty call description to simplewebrtc since + // usersChanged (webrtc.js) will create/remove peer connections + // with call participants + var callDescription = {'clients': {}}; + callback('', callDescription); + }.bind(this), + error: function () { + // Room not found or maintenance mode + OC.redirect(OC.generateUrl('apps/spreed')); + }.bind(this) + }); + }; + + SignalingBase.prototype._leaveCallSuccess = function(/* token */) { + // Override in subclasses if necessary. + }; + + SignalingBase.prototype.leaveCall = function(token) { + $.ajax({ + url: OC.linkToOCS('apps/spreed/api/v1/call', 2) + token, + method: 'DELETE', + async: false, + success: function () { + this._leaveCallSuccess(token); + // We left the current call. + if (token === this.currentCallToken) { + this.currentCallToken = null; + } + }.bind(this) + }); + }; + // Connection to the internal signaling server provided by the app. function InternalSignaling(/*settings*/) { SignalingBase.prototype.constructor.apply(this, arguments); @@ -221,101 +337,17 @@ return defer; }; - InternalSignaling.prototype.joinRoom = function(token, password) { - $.ajax({ - url: OC.linkToOCS('apps/spreed/api/v1/room', 2) + token + '/participants/active', - type: 'POST', - beforeSend: function (request) { - request.setRequestHeader('Accept', 'application/json'); - }, - data: { - password: password - }, - success: function (result) { - console.log("Joined", result); - this.sessionId = result.ocs.data.sessionId; - this.currentRoomToken = token; - this._startPingCall(); - this._startPullingMessages(); - }.bind(this), - error: function (result) { - if (result.status === 404 || result.status === 503) { - // Room not found or maintenance mode - OC.redirect(OC.generateUrl('apps/spreed')); - } - - if (result.status === 403) { - // This should not happen anymore since we ask for the password before - // even trying to join the call, but let's keep it for now. - OC.dialogs.prompt( - t('spreed', 'Please enter the password for this call'), - t('spreed','Password required'), - function (result, password) { - if (result && password !== '') { - this.joinRoom(token, password); - } - }.bind(this), - true, - t('spreed','Password'), - true - ).then(function() { - var $dialog = $('.oc-dialog:visible'); - $dialog.find('.ui-icon').remove(); - - var $buttons = $dialog.find('button'); - $buttons.eq(0).text(t('core', 'Cancel')); - $buttons.eq(1).text(t('core', 'Submit')); - }); - } - }.bind(this) - }); + InternalSignaling.prototype._joinRoomSuccess = function(token, sessionId) { + this.sessionId = sessionId; + this._startPingCall(); + this._startPullingMessages(); }; - InternalSignaling.prototype.leaveRoom = function(token) { - if (this.currentCallToken) { - this.leaveCall(); - } - + InternalSignaling.prototype._doLeaveRoom = function(token) { if (token === this.currentRoomToken) { this._stopPingCall(); this._closeEventSource(); } - - $.ajax({ - url: OC.linkToOCS('apps/spreed/api/v1/room', 2) + token + '/participants/active', - method: 'DELETE', - async: false - }); - }; - - InternalSignaling.prototype.joinCall = function(token, callback) { - $.ajax({ - url: OC.linkToOCS('apps/spreed/api/v1/call', 2) + token, - type: 'POST', - beforeSend: function (request) { - request.setRequestHeader('Accept', 'application/json'); - }, - success: function () { - this.currentCallToken = token; - // We send an empty call description to simplewebrtc since - // usersChanged (webrtc.js) will create/remove peer connections - // with call participants - var callDescription = {'clients': {}}; - callback('', callDescription); - }.bind(this), - error: function () { - // Room not found or maintenance mode - OC.redirect(OC.generateUrl('apps/spreed')); - }.bind(this) - }); - }; - - InternalSignaling.prototype.leaveCall = function(token) { - $.ajax({ - url: OC.linkToOCS('apps/spreed/api/v1/call', 2) + token, - method: 'DELETE', - async: false - }); }; InternalSignaling.prototype.sendCallMessage = function(data) { @@ -517,6 +549,7 @@ this.maxReconnectIntervalMs = 16000; this.reconnectIntervalMs = this.initialReconnectIntervalMs; this.joinedUsers = {}; + this.rooms = []; this.connect(); } @@ -588,10 +621,10 @@ } break; case "room": - if (this.currentCallToken && data.room.roomid !== this.currentCallToken) { - this._trigger('roomChanged', [this.currentCallToken, data.room.roomid]); + if (this.currentRoomToken && data.room.roomid !== this.currentRoomToken) { + this._trigger('roomChanged', [this.currentRoomToken, data.room.roomid]); this.joinedUsers = {}; - this.currentCallToken = null; + this.currentRoomToken = null; } break; case "event": @@ -726,26 +759,44 @@ // so perform resync once. this.internalSyncRooms(); } - if (!resumedSession && this.currentCallToken) { - this.joinCall(this.currentCallToken); + if (!resumedSession && this.currentRoomToken) { + this.joinRoom(this.currentRoomToken); } }; - StandaloneSignaling.prototype.joinCall = function(token, callback) { - console.log("Join call", token); + StandaloneSignaling.prototype.setRoom = function(/* room */) { + SignalingBase.prototype.setRoom.apply(this, arguments); + return this.internalSyncRooms(); + }; + + StandaloneSignaling.prototype._joinRoomSuccess = function(token, nextcloudSessionId) { + console.log("Join room", token); this.doSend({ "type": "room", "room": { - "roomid": token + "roomid": token, + // Pass the Nextcloud session id to the signaling server. The + // session id will be passed through to Nextcloud to check if + // the (Nextcloud) user is allowed to join the room. + "sessionid": nextcloudSessionId, } }, function(data) { - this.joinResponseReceived(data, token, callback); + this.joinResponseReceived(data, token); }.bind(this)); }; - StandaloneSignaling.prototype.joinResponseReceived = function(data, token, callback) { + StandaloneSignaling.prototype._joinCallSuccess = function(/* token */) { + // Update room list to fetch modified properties. + this.internalSyncRooms(); + }; + + StandaloneSignaling.prototype._leaveCallSuccess = function(/* token */) { + // Update room list to fetch modified properties. + this.internalSyncRooms(); + }; + + StandaloneSignaling.prototype.joinResponseReceived = function(data, token) { console.log("Joined", data, token); - this.currentCallToken = token; if (this.roomCollection) { // The list of rooms is not fetched from the server. Update ping // of joined room so it gets sorted to the top. @@ -756,16 +807,10 @@ }); this.roomCollection.sort(); } - if (callback) { - var roomDescription = { - "clients": {} - }; - callback('', roomDescription); - } }; - StandaloneSignaling.prototype.leaveCall = function(token) { - console.log("Leave call", token); + StandaloneSignaling.prototype._doLeaveRoom = function(token) { + console.log("Leave room", token); this.doSend({ "type": "room", "room": { @@ -779,7 +824,6 @@ this._trigger("usersLeft", [leftUsers]); } this.joinedUsers = {}; - this.currentCallToken = null; }.bind(this)); }; @@ -791,6 +835,9 @@ case "roomlist": this.processRoomListEvent(data); break; + case "participants": + this.processRoomParticipantsEvent(data); + break; default: console.log("Unsupported event target", data); break; @@ -845,15 +892,32 @@ }; StandaloneSignaling.prototype.syncRooms = function() { + if (this.pending_sync) { + // A sync request is already in progress, don't start another one. + return this.pending_sync; + } + // Never manually sync rooms, will be done based on notifications // from the signaling server. var defer = $.Deferred(); - defer.resolve([]); + defer.resolve(this.rooms); return defer; }; StandaloneSignaling.prototype.internalSyncRooms = function() { - return SignalingBase.prototype.syncRooms.apply(this, arguments); + if (this.pending_sync) { + // A sync request is already in progress, don't start another one. + return this.pending_sync; + } + + var defer = $.Deferred(); + this.pending_sync = SignalingBase.prototype.syncRooms.apply(this, arguments); + this.pending_sync.then(function(rooms) { + this.pending_sync = null; + this.rooms = rooms; + defer.resolve(rooms); + }.bind(this)); + return defer; }; StandaloneSignaling.prototype.processRoomListEvent = function(data) { @@ -861,6 +925,18 @@ this.internalSyncRooms(); }; + StandaloneSignaling.prototype.processRoomParticipantsEvent = function(data) { + switch (data.event.type) { + case "update": + this._trigger("usersChanged", [data.event.update.users]); + this.internalSyncRooms(); + break; + default: + console.log("Unknown room participant event", data); + break; + } + }; + OCA.SpreedMe.createSignalingConnection = function() { var settings = $("#app #signaling-settings").text(); if (settings) { diff --git a/js/webrtc.js b/js/webrtc.js index 870f657a22f..55ba34f604d 100644 --- a/js/webrtc.js +++ b/js/webrtc.js @@ -12,6 +12,7 @@ var spreedPeerConnectionTable = []; OCA.SpreedMe = OCA.SpreedMe || {}; var previousUsersInRoom = []; + var usersInCallMapping = {}; function updateParticipantsUI(currentUsersNo) { 'use strict'; @@ -110,6 +111,49 @@ var spreedPeerConnectionTable = []; updateParticipantsUI(previousUsersInRoom.length + 1); } + function usersInCallChanged(users) { + // The passed list are the users that are currently in the room, + // i.e. that are in the call and should call each other. + var currentSessionId = webrtc.connection.getSessionid(); + var currentUsersInRoom = []; + var userMapping = {}; + var selfInCall = false; + var sessionId; + for (sessionId in users) { + if (!users.hasOwnProperty(sessionId)) { + continue; + } + var user = users[sessionId]; + if (!user.inCall) { + continue; + } + + if (sessionId === currentSessionId) { + selfInCall = true; + continue; + } + + currentUsersInRoom.push(sessionId); + userMapping[sessionId] = user; + } + + if (!selfInCall) { + // Own session is no longer in the call, disconnect from all others. + usersChanged([], previousUsersInRoom); + return; + } + + var newSessionIds = currentUsersInRoom.diff(previousUsersInRoom); + var disconnectedSessionIds = previousUsersInRoom.diff(currentUsersInRoom); + var newUsers = []; + newSessionIds.forEach(function(sessionId) { + newUsers.push(userMapping[sessionId]); + }); + if (newUsers.length || disconnectedSessionIds.length) { + usersChanged(newUsers, disconnectedSessionIds); + } + } + function initWebRTC() { 'use strict'; Array.prototype.diff = function(a) { @@ -119,47 +163,26 @@ var spreedPeerConnectionTable = []; }; var signaling = OCA.SpreedMe.createSignalingConnection(); - signaling.on('usersJoined', function(users) { - usersChanged(users, []); - }); signaling.on('usersLeft', function(users) { + users.forEach(function(user) { + delete usersInCallMapping[user]; + }); usersChanged([], users); }); - signaling.on('usersInRoom', function(users) { - // The passed list are the users that are currently in the room, - // i.e. that are in the call and should call each other. - var currentSessionId = webrtc.connection.getSessionid(); - var currentUsersInRoom = []; - var userMapping = {}; - var selfInCall = false; + signaling.on('usersChanged', function(users) { users.forEach(function(user) { - if (!user['inCall']) { - return; - } - - var sessionId = user['sessionId'] || user.sessionid; - if (sessionId === currentSessionId) { - selfInCall = true; - return; - } - - currentUsersInRoom.push(sessionId); - userMapping[sessionId] = user; + var sessionId = user.sessionId || user.sessionid; + usersInCallMapping[sessionId] = user; }); - - if (!selfInCall) { - return; - } - - var newSessionIds = currentUsersInRoom.diff(previousUsersInRoom); - var disconnectedSessionIds = previousUsersInRoom.diff(currentUsersInRoom); - var newUsers = []; - newSessionIds.forEach(function(sessionId) { - newUsers.push(userMapping[sessionId]); + usersInCallChanged(usersInCallMapping); + }); + signaling.on('usersInRoom', function(users) { + usersInCallMapping = {}; + users.forEach(function(user) { + var sessionId = user.sessionId || user.sessionid; + usersInCallMapping[sessionId] = user; }); - if (newUsers.length || disconnectedSessionIds.length) { - usersChanged(newUsers, disconnectedSessionIds); - } + usersInCallChanged(usersInCallMapping); }); var nick = OC.getCurrentUser()['displayName']; diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 8815a2b8a01..4fcc2eaeb42 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -25,7 +25,9 @@ use OCA\Spreed\Config; use OCA\Spreed\Exceptions\RoomNotFoundException; +use OCA\Spreed\Exceptions\ParticipantNotFoundException; use OCA\Spreed\Manager; +use OCA\Spreed\Participant; use OCA\Spreed\Room; use OCA\Spreed\Signaling\Messages; use OCP\AppFramework\Http; @@ -285,6 +287,9 @@ public function backend() { case 'room': // Query information about a room. return $this->backendRoom($message['room']); + case 'ping': + // Ping sessions connected to a room. + return $this->backendPing($message['ping']); default: return new JSONResponse([ 'type' => 'error', @@ -337,11 +342,13 @@ private function backendAuth($auth) { return new JSONResponse($response); } - private function backendRoom($room) { - $roomId = $room['roomid']; - $userId = $room['userid']; + private function backendRoom($roomRequest) { + $roomId = $roomRequest['roomid']; + $userId = $roomRequest['userid']; + $sessionId = $roomRequest['sessionid']; + try { - $room = $this->manager->getRoomForParticipantByToken($roomId, $userId); + $room = $this->manager->getRoomByToken($roomId); } catch (RoomNotFoundException $e) { return new JSONResponse([ 'type' => 'error', @@ -352,12 +359,36 @@ private function backendRoom($room) { ]); } + $participant = null; if (!empty($userId)) { - // Rooms get sorted by last ping time for users, so make sure to - // update when a user joins a room. - $room->ping($userId, '0', time()); + // User trying to join room. + try { + $participant = $room->getParticipant($userId); + } catch (ParticipantNotFoundException $e) { + // Ignore, will check for public rooms below. + } + } + + if (empty($participant)) { + // User was not invited to the room, check for access to public room. + try { + $participant = $room->getParticipantBySession($sessionId); + } catch (ParticipantNotFoundException $e) { + // Return generic error to avoid leaking which rooms exist. + return new JSONResponse([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'The user is not invited to this room.', + ], + ]); + } } + // Rooms get sorted by last ping time for users, so make sure to + // update when a user joins a room. + $room->ping($userId, $sessionId, time()); + $response = [ 'type' => 'room', 'room' => [ @@ -372,4 +403,36 @@ private function backendRoom($room) { return new JSONResponse($response); } + private function backendPing($request) { + try { + $room = $this->manager->getRoomByToken($request['roomid']); + } catch (RoomNotFoundException $e) { + return new JSONResponse([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'No such room.', + ], + ]); + } + + $now = time(); + foreach ($request['entries'] as $entry) { + if (array_key_exists('userid', $entry)) { + $room->ping($entry['userid'], $entry['sessionid'], $now); + } else { + $room->ping('', $entry['sessionid'], $now); + } + } + + $response = [ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $room->getToken(), + ], + ]; + return new JSONResponse($response); + } + } diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index 5c6f631e47e..8e64ec77125 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -208,11 +208,33 @@ public function roomDeleted($room, $participants) { */ public function roomInCallChanged($room, $inCall, $sessionIds) { $this->logger->info('Room in-call status changed: ' . $room->getToken() . ' ' . $inCall . ' ' . print_r($sessionIds, true)); + $changed = []; + $users = []; + $participants = $room->getParticipants(); + foreach ($participants['users'] as $userId => $participant) { + if ($participant['inCall']) { + $users[] = $participant; + } + if (in_array($participant['sessionId'], $sessionIds)) { + $participant['userId'] = $userId; + $changed[] = $participant; + } + } + foreach ($participants['guests'] as $participant) { + if ($participant['inCall']) { + $users[] = $participant; + } + if (in_array($participant['sessionId'], $sessionIds)) { + $changed[] = $participant; + } + } + $this->backendRequest('/api/v1/room/' . $room->getToken(), [ 'type' => 'incall', 'incall' => [ 'incall' => $inCall, - 'sessionids' => $sessionIds, + 'changed' => $changed, + 'users' => $users ], ]); } From 3f4f0b1ee106ec39f98697c4e2a6a0d2268587ee Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Mon, 29 Jan 2018 16:54:12 +0100 Subject: [PATCH 3/8] Update room list on "room" events. Signed-off-by: Joachim Bauch --- js/signaling.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/signaling.js b/js/signaling.js index 1a6ce373d69..a986767395a 100644 --- a/js/signaling.js +++ b/js/signaling.js @@ -625,6 +625,9 @@ this._trigger('roomChanged', [this.currentRoomToken, data.room.roomid]); this.joinedUsers = {}; this.currentRoomToken = null; + } else { + // TODO(fancycode): Only fetch properties of room that was modified. + this.internalSyncRooms(); } break; case "event": From 882519342909d516954c601ff112a4c73c08cdca Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Tue, 30 Jan 2018 10:16:33 +0100 Subject: [PATCH 4/8] Add tests for the backend endpoint of "SignalingController". Signed-off-by: Joachim Bauch --- lib/Controller/SignalingController.php | 20 +- .../Controller/SignalingControllerTest.php | 619 ++++++++++++++++++ 2 files changed, 636 insertions(+), 3 deletions(-) create mode 100644 tests/php/Controller/SignalingControllerTest.php diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 4fcc2eaeb42..86eed59c6ed 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -243,6 +243,10 @@ protected function getUsersInRoom(Room $room) { * @return bool */ private function validateBackendRequest($data) { + if (!isset($_SERVER['HTTP_SPREED_SIGNALING_RANDOM']) || + !isset($_SERVER['HTTP_SPREED_SIGNALING_CHECKSUM'])) { + return false; + } $random = $_SERVER['HTTP_SPREED_SIGNALING_RANDOM']; if (empty($random) || strlen($random) < 32) { return false; @@ -255,6 +259,16 @@ private function validateBackendRequest($data) { return hash_equals($hash, strtolower($checksum)); } + /** + * Return the body of the backend request. This can be overridden in + * tests. + * + * @return string + */ + protected function getInputStream() { + return file_get_contents('php://input'); + } + /** * Backend API to query information required for standalone signaling * servers. @@ -268,7 +282,7 @@ private function validateBackendRequest($data) { * @return JSONResponse */ public function backend() { - $json = file_get_contents('php://input'); + $json = $this->getInputStream(); if (!$this->validateBackendRequest($json)) { return new JSONResponse([ 'type' => 'error', @@ -280,7 +294,7 @@ public function backend() { } $message = json_decode($json, true); - switch ($message['type']) { + switch (isset($message['type']) ? $message['type'] : "") { case 'auth': // Query authentication information about a user. return $this->backendAuth($message['auth']); @@ -295,7 +309,7 @@ public function backend() { 'type' => 'error', 'error' => [ 'code' => 'unknown_type', - 'message' => 'The given type ' . print_r($message, true) . ' is not supported.', + 'message' => 'The given type ' . json_encode($message) . ' is not supported.', ], ]); } diff --git a/tests/php/Controller/SignalingControllerTest.php b/tests/php/Controller/SignalingControllerTest.php new file mode 100644 index 00000000000..d3bffa9da84 --- /dev/null +++ b/tests/php/Controller/SignalingControllerTest.php @@ -0,0 +1,619 @@ +. + * + */ + +namespace OCA\Spreed\Tests\php\Controller; + +use OCA\Spreed\Config; +use OCA\Spreed\Controller\SignalingController; +use OCA\Spreed\Exceptions\ParticipantNotFoundException; +use OCA\Spreed\Exceptions\RoomNotFoundException; +use OCA\Spreed\Manager; +use OCA\Spreed\Participant; +use OCA\Spreed\Room; +use OCA\Spreed\Signaling\Messages; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Security\ISecureRandom; + +class CustomInputSignalingController extends SignalingController { + + private $inputStream; + + public function setInputStream($data) { + $this->inputStream = $data; + } + + protected function getInputStream() { + return $this->inputStream; + } + +} + +/** + * @group DB + */ +class SignalingControllerTest extends \Test\TestCase { + + /** @var OCA\Spreed\Config */ + private $config; + + /** @var \OCP\ISession|\PHPUnit_Framework_MockObject_MockObject */ + private $session; + + /** @var \OCA\Spreed\Manager|\PHPUnit_Framework_MockObject_MockObject */ + protected $manager; + + /** @var \OCP\IDBConnection|\PHPUnit_Framework_MockObject_MockObject */ + protected $dbConnection; + + /** @var \OCA\Spreed\Signaling\Messages|\PHPUnit_Framework_MockObject_MockObject */ + protected $messages; + + /** @var \OCP\IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $userManager; + + /** @var string */ + private $userId; + + /** @var CustomInputSignalingController */ + private $controller; + + public function setUp() { + parent::setUp(); + + $this->userId = 'testUser'; + $secureRandom = \OC::$server->getSecureRandom(); + $timeFactory = $this->createMock(ITimeFactory::class); + $config = \OC::$server->getConfig(); + $config->setAppValue('spreed', 'signaling_servers', json_encode([ + 'secret' => 'MySecretValue', + ])); + $config->setAppValue('spreed', 'signaling_ticket_secret', 'the-app-ticket-secret'); + $config->setUserValue($this->userId, 'spreed', 'signaling_ticket_secret', 'the-user-ticket-secret'); + $this->config = new Config($config, $secureRandom, $timeFactory); + $this->session = $this->createMock(ISession::class); + $this->dbConnection = \OC::$server->getDatabaseConnection(); + $this->manager = $this->createMock(Manager::class); + $this->messages = $this->createMock(Messages::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->recreateSignalingController(); + } + + private function recreateSignalingController() { + $this->controller = new CustomInputSignalingController( + 'spreed', + $this->createMock(\OCP\IRequest::class), + $this->config, + $this->session, + $this->manager, + $this->dbConnection, + $this->messages, + $this->userManager, + $this->userId + ); + } + + private function validateBackendRandom($data, $random, $checksum) { + if (empty($random) || strlen($random) < 32) { + return false; + } + if (empty($checksum)) { + return false; + } + $hash = hash_hmac('sha256', $random . $data, $this->config->getSignalingSecret()); + return hash_equals($hash, strtolower($checksum)); + } + + private function calculateBackendChecksum($data, $random) { + if (empty($random) || strlen($random) < 32) { + return false; + } + $hash = hash_hmac('sha256', $random . $data, $this->config->getSignalingSecret()); + return $hash; + } + + public function testBackendChecksums() { + // Test checksum generation / validation with the example from the API documentation. + $data = '{"type":"auth","auth":{"version":"1.0","params":{"hello":"world"}}}'; + $random = 'afb6b872ab03e3376b31bf0af601067222ff7990335ca02d327071b73c0119c6'; + $checksum = '3c4a69ff328299803ac2879614b707c807b4758cf19450755c60656cac46e3bc'; + $this->assertEquals($checksum, $this->calculateBackendChecksum($data, $random)); + $this->assertTrue($this->validateBackendRandom($data, $random, $checksum)); + } + + private function performBackendRequest($data) { + if (!is_string($data)) { + $data = json_encode($data); + } + $random = 'afb6b872ab03e3376b31bf0af601067222ff7990335ca02d327071b73c0119c6'; + $checksum = $this->calculateBackendChecksum($data, $random); + $_SERVER['HTTP_SPREED_SIGNALING_RANDOM'] = $random; + $_SERVER['HTTP_SPREED_SIGNALING_CHECKSUM'] = $checksum; + $this->controller->setInputStream($data); + return $this->controller->backend(); + } + + public function testBackendChecksumValidation() { + $data = '{}'; + + // Random and checksum missing. + $this->controller->setInputStream($data); + $result = $this->controller->backend(); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'invalid_request', + 'message' => 'The request could not be authenticated.', + ], + ], $result->getData()); + + // Invalid checksum. + $this->controller->setInputStream($data); + $random = 'afb6b872ab03e3376b31bf0af601067222ff7990335ca02d327071b73c0119c6'; + $checksum = $this->calculateBackendChecksum('{"foo": "bar"}', $random); + $_SERVER['HTTP_SPREED_SIGNALING_RANDOM'] = $random; + $_SERVER['HTTP_SPREED_SIGNALING_CHECKSUM'] = $checksum; + $result = $this->controller->backend(); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'invalid_request', + 'message' => 'The request could not be authenticated.', + ], + ], $result->getData()); + + // Short random + $this->controller->setInputStream($data); + $random = '12345'; + $checksum = $this->calculateBackendChecksum($data, $random); + $_SERVER['HTTP_SPREED_SIGNALING_RANDOM'] = $random; + $_SERVER['HTTP_SPREED_SIGNALING_CHECKSUM'] = $checksum; + $result = $this->controller->backend(); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'invalid_request', + 'message' => 'The request could not be authenticated.', + ], + ], $result->getData()); + } + + public function testBackendUnsupportedType() { + $result = $this->performBackendRequest([ + 'type' => 'unsupported-type', + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'unknown_type', + 'message' => 'The given type {"type":"unsupported-type"} is not supported.', + ], + ], $result->getData()); + } + + public function testBackendAuth() { + // Check validating of tickets. + $result = $this->performBackendRequest([ + 'type' => 'auth', + 'auth' => [ + 'params' => [ + 'userid' => $this->userId, + 'ticket' => 'invalid-ticket', + ], + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'invalid_ticket', + 'message' => 'The given ticket is not valid for this user.', + ], + ], $result->getData()); + + // Check validating ticket for passed user. + $result = $this->performBackendRequest([ + 'type' => 'auth', + 'auth' => [ + 'params' => [ + 'userid' => 'invalid-userid', + 'ticket' => $this->config->getSignalingTicket($this->userId), + ], + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'invalid_ticket', + 'message' => 'The given ticket is not valid for this user.', + ], + ], $result->getData()); + + // Check validating of existing users. + $result = $this->performBackendRequest([ + 'type' => 'auth', + 'auth' => [ + 'params' => [ + 'userid' => 'unknown-userid', + 'ticket' => $this->config->getSignalingTicket('unknown-userid'), + ], + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_user', + 'message' => 'The given user does not exist.', + ], + ], $result->getData()); + + // Check successfull authentication of users. + $testUser = $this->createMock(IUser::class); + $testUser->expects($this->once()) + ->method('getDisplayName') + ->willReturn('Test User'); + $testUser->expects($this->once()) + ->method('getUID') + ->willReturn($this->userId); + $this->userManager->expects($this->once()) + ->method('get') + ->with($this->userId) + ->willReturn($testUser); + $result = $this->performBackendRequest([ + 'type' => 'auth', + 'auth' => [ + 'params' => [ + 'userid' => $this->userId, + 'ticket' => $this->config->getSignalingTicket($this->userId), + ], + ], + ]); + $this->assertSame([ + 'type' => 'auth', + 'auth' => [ + 'version' => '1.0', + 'userid' => $this->userId, + 'user' => [ + 'displayname' => 'Test User', + ], + ], + ], $result->getData()); + + // Check successfull authentication of anonymous participants. + $result = $this->performBackendRequest([ + 'type' => 'auth', + 'auth' => [ + 'params' => [ + 'userid' => '', + 'ticket' => $this->config->getSignalingTicket(''), + ], + ], + ]); + $this->assertSame([ + 'type' => 'auth', + 'auth' => [ + 'version' => '1.0', + ], + ], $result->getData()); + } + + public function testBackendRoomUnknown() { + $roomToken = 'the-room'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willThrowException(new RoomNotFoundException()); + + $result = $this->performBackendRequest([ + 'type' => 'room', + 'room' => [ + 'roomid' => $roomToken, + 'userid' => $this->userId, + 'sessionid' => '', + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'The user is not invited to this room.', + ], + ], $result->getData()); + } + + public function testBackendRoomInvited() { + $roomToken = 'the-room'; + $roomName = 'the-room-name'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + $room->expects($this->once()) + ->method('getName') + ->willReturn($roomName); + $room->expects($this->once()) + ->method('getParticipant') + ->with($this->userId) + ->willReturn($participant); + $room->expects($this->once()) + ->method('getToken') + ->willReturn($roomToken); + $room->expects($this->once()) + ->method('getType') + ->willReturn(Room::ONE_TO_ONE_CALL); + + $result = $this->performBackendRequest([ + 'type' => 'room', + 'room' => [ + 'roomid' => $roomToken, + 'userid' => $this->userId, + 'sessionid' => '', + ], + ]); + $this->assertSame([ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $roomToken, + 'properties' => [ + 'name' => $roomName, + 'type' => Room::ONE_TO_ONE_CALL, + ], + ], + ], $result->getData()); + } + + public function testBackendRoomAnonymousPublic() { + $roomToken = 'the-room'; + $roomName = 'the-room-name'; + $sessionId = 'the-session'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + $room->expects($this->once()) + ->method('getName') + ->willReturn($roomName); + $room->expects($this->once()) + ->method('getParticipantBySession') + ->with($sessionId) + ->willReturn($participant); + $room->expects($this->once()) + ->method('getToken') + ->willReturn($roomToken); + $room->expects($this->once()) + ->method('getType') + ->willReturn(Room::PUBLIC_CALL); + + $result = $this->performBackendRequest([ + 'type' => 'room', + 'room' => [ + 'roomid' => $roomToken, + 'userid' => '', + 'sessionid' => $sessionId, + ], + ]); + $this->assertSame([ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $roomToken, + 'properties' => [ + 'name' => $roomName, + 'type' => Room::PUBLIC_CALL, + ], + ], + ], $result->getData()); + } + + public function testBackendRoomInvitedPublic() { + $roomToken = 'the-room'; + $roomName = 'the-room-name'; + $sessionId = 'the-session'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + $room->expects($this->once()) + ->method('getName') + ->willReturn($roomName); + $room->expects($this->once()) + ->method('getParticipant') + ->with($this->userId) + ->willThrowException(new ParticipantNotFoundException()); + $room->expects($this->once()) + ->method('getParticipantBySession') + ->with($sessionId) + ->willReturn($participant); + $room->expects($this->once()) + ->method('getToken') + ->willReturn($roomToken); + $room->expects($this->once()) + ->method('getType') + ->willReturn(Room::PUBLIC_CALL); + + $result = $this->performBackendRequest([ + 'type' => 'room', + 'room' => [ + 'roomid' => $roomToken, + 'userid' => $this->userId, + 'sessionid' => $sessionId, + ], + ]); + $this->assertSame([ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $roomToken, + 'properties' => [ + 'name' => $roomName, + 'type' => Room::PUBLIC_CALL, + ], + ], + ], $result->getData()); + } + + public function testBackendRoomAnonymousOneToOne() { + $roomToken = 'the-room'; + $sessionId = 'the-session'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + + $participant = $this->createMock(Participant::class); + $room->expects($this->once()) + ->method('getParticipantBySession') + ->willThrowException(new ParticipantNotFoundException()); + + $result = $this->performBackendRequest([ + 'type' => 'room', + 'room' => [ + 'roomid' => $roomToken, + 'userid' => '', + 'sessionid' => $sessionId, + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'The user is not invited to this room.', + ], + ], $result->getData()); + } + + public function testBackendPingUnknownRoom() { + $roomToken = 'the-room'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willThrowException(new RoomNotFoundException()); + + $result = $this->performBackendRequest([ + 'type' => 'ping', + 'ping' => [ + 'roomid' => $roomToken, + 'entries' => [ + [ + 'userid' => $this->userId, + ], + ], + ], + ]); + $this->assertSame([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'No such room.', + ], + ], $result->getData()); + } + + public function testBackendPingUser() { + $roomToken = 'the-room'; + $sessionId = 'the-session'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + $room->expects($this->once()) + ->method('getToken') + ->willReturn($roomToken); + $room->expects($this->once()) + ->method('ping') + ->with($this->userId, $sessionId); + + $result = $this->performBackendRequest([ + 'type' => 'ping', + 'ping' => [ + 'roomid' => $roomToken, + 'entries' => [ + [ + 'userid' => $this->userId, + 'sessionid' => $sessionId, + ], + ], + ], + ]); + $this->assertSame([ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $roomToken, + ], + ], $result->getData()); + } + + public function testBackendPingAnonymous() { + $roomToken = 'the-room'; + $sessionId = 'the-session'; + $room = $this->createMock(Room::class); + $this->manager->expects($this->once()) + ->method('getRoomByToken') + ->with($roomToken) + ->willReturn($room); + $room->expects($this->once()) + ->method('getToken') + ->willReturn($roomToken); + $room->expects($this->once()) + ->method('ping') + ->with('', $sessionId); + + $result = $this->performBackendRequest([ + 'type' => 'ping', + 'ping' => [ + 'roomid' => $roomToken, + 'entries' => [ + [ + 'userid' => '', + 'sessionid' => $sessionId, + ], + ], + ], + ]); + $this->assertSame([ + 'type' => 'room', + 'room' => [ + 'version' => '1.0', + 'roomid' => $roomToken, + ], + ], $result->getData()); + } + +} From 2b1bc063427b876e2fa63f546e63feeaf46fdb29 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Tue, 30 Jan 2018 12:28:43 +0100 Subject: [PATCH 5/8] Add tests for "BackendNotifier". Signed-off-by: Joachim Bauch --- lib/AppInfo/Application.php | 18 +- lib/Signaling/BackendNotifier.php | 23 +- tests/php/Signaling/BackendNotifierTest.php | 347 ++++++++++++++++++++ 3 files changed, 378 insertions(+), 10 deletions(-) create mode 100644 tests/php/Signaling/BackendNotifierTest.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 37ea601846d..f37b3e1214c 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -92,10 +92,14 @@ protected function registerInternalSignalingHooks(EventDispatcherInterface $disp $dispatcher->addListener(Room::class . '::postSessionLeaveCall', $listener); } + protected function getBackendNotifier() { + return $this->getContainer()->query(BackendNotifier::class); + } + protected function registerSignalingBackendHooks(EventDispatcherInterface $dispatcher) { $dispatcher->addListener(Room::class . '::postAddUsers', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); $participants= $event->getArgument('users'); @@ -103,14 +107,14 @@ protected function registerSignalingBackendHooks(EventDispatcherInterface $dispa }); $dispatcher->addListener(Room::class . '::postSetName', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); $notifier->roomModified($room); }); $dispatcher->addListener(Room::class . '::postSetParticipantType', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); // The type of a participant has changed, notify all participants @@ -119,7 +123,7 @@ protected function registerSignalingBackendHooks(EventDispatcherInterface $dispa }); $dispatcher->addListener(Room::class . '::postDeleteRoom', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); $participants = $event->getArgument('participants'); @@ -127,7 +131,7 @@ protected function registerSignalingBackendHooks(EventDispatcherInterface $dispa }); $dispatcher->addListener(Room::class . '::postRemoveUser', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); $user = $event->getArgument('user'); @@ -135,7 +139,7 @@ protected function registerSignalingBackendHooks(EventDispatcherInterface $dispa }); $dispatcher->addListener(Room::class . '::postSessionJoinCall', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); $sessionId = $event->getArgument('sessionId'); @@ -143,7 +147,7 @@ protected function registerSignalingBackendHooks(EventDispatcherInterface $dispa }); $dispatcher->addListener(Room::class . '::postSessionLeaveCall', function(GenericEvent $event) { /** @var BackendNotifier $notifier */ - $notifier = $this->getContainer()->query(BackendNotifier::class); + $notifier = $this->getBackendNotifier(); $room = $event->getSubject(); $sessionId = $event->getArgument('sessionId'); diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index 8e64ec77125..99489b75d67 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -24,6 +24,7 @@ namespace OCA\Spreed\Signaling; use OCA\Spreed\Config; +use OCA\Spreed\Participant; use OCA\Spreed\Room; use OCP\Http\Client\IClientService; use OCP\ILogger; @@ -55,6 +56,20 @@ public function __construct(Config $config, $this->secureRandom = $secureRandom; } + /** + * Perform actual network request to the signaling backend. + * This can be overridden in tests. + */ + protected function doRequest($url, $params) { + if (defined('PHPUNIT_RUN')) { + // Don't perform network requests when running tests. + return; + } + + $client = $this->clientService->newClient(); + $client->post($url, $params); + } + /** * Perform a request to the signaling backend. * @@ -77,7 +92,6 @@ private function backendRequest($url, $data) { } else if (strpos($url, 'ws://') === 0) { $url = 'http://' . substr($url, 5); } - $client = $this->clientService->newClient(); $body = json_encode($data); $headers = [ 'Content-Type' => 'application/json', @@ -92,10 +106,10 @@ private function backendRequest($url, $data) { 'headers' => $headers, 'body' => $body, ]; - if (!$signaling['verify']) { + if (!empty($signaling['verify'])) { $params['verify'] = false; } - $client->post($url, $params); + $this->doRequest($url, $params); } /** @@ -221,6 +235,9 @@ public function roomInCallChanged($room, $inCall, $sessionIds) { } } foreach ($participants['guests'] as $participant) { + if (!isset($participant['participantType'])) { + $participant['participantType'] = Participant::GUEST; + } if ($participant['inCall']) { $users[] = $participant; } diff --git a/tests/php/Signaling/BackendNotifierTest.php b/tests/php/Signaling/BackendNotifierTest.php new file mode 100644 index 00000000000..2c0d20e7d5a --- /dev/null +++ b/tests/php/Signaling/BackendNotifierTest.php @@ -0,0 +1,347 @@ +. + * + */ + +namespace OCA\Spreed\Tests\php\Signaling; + +use OCA\Spreed\AppInfo\Application; +use OCA\Spreed\Config; +use OCA\Spreed\Manager; +use OCA\Spreed\Participant; +use OCA\Spreed\Signaling\BackendNotifier; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Http\Client\IClientService; +use OCP\ILogger; +use OCP\IUser; +use OCP\Security\IHasher; + +class CustomBackendNotifier extends BackendNotifier { + + private $lastRequest; + + public function getLastRequest() { + return $this->lastRequest; + } + + public function clearLastRequest() { + $this->lastRequest = null; + } + + protected function doRequest($url, $params) { + $this->lastRequest = [ + 'url' => $url, + 'params' => $params, + ]; + } + +} + +class CustomApplication extends Application { + + private $notifier; + + public function setBackendNotifier($notifier) { + $this->notifier = $notifier; + } + + protected function getBackendNotifier() { + return $this->notifier; + } + +} + +/** + * @group DB + */ +class BackendNotifierTest extends \Test\TestCase { + + /** @var OCA\Spreed\Config */ + private $config; + + /** @var ISecureRandom */ + private $secureRandom; + + /** @var CustomBackendNotifier */ + private $controller; + + /** @var Manager */ + private $manager; + + /** @var string */ + private $userId; + + public function setUp() { + parent::setUp(); + // Make sure necessary database tables are set up. + \OC_App::updateApp('spreed'); + + $this->userId = 'testUser'; + $this->secureRandom = \OC::$server->getSecureRandom(); + $timeFactory = $this->createMock(ITimeFactory::class); + $config = \OC::$server->getConfig(); + $this->signalingSecret = 'the-signaling-secret'; + $this->baseUrl = 'https://localhost/signaling'; + $config->setAppValue('spreed', 'signaling_servers', json_encode([ + 'secret' => $this->signalingSecret, + 'servers' => [ + [ + 'server' => $this->baseUrl, + ], + ], + ])); + + $this->config = new Config($config, $this->secureRandom, $timeFactory); + $this->recreateBackendNotifier(); + + $app = new CustomApplication(); + $app->setBackendNotifier($this->controller); + $app->register(); + + \OC::$server->registerService(BackendNotifier::class, function() { + return $this->controller; + }); + + $dbConnection = \OC::$server->getDatabaseConnection(); + $dispatcher = \OC::$server->getEventDispatcher(); + $this->manager = new Manager($dbConnection, $config, $this->secureRandom, $dispatcher, $this->createMock(IHasher::class)); + } + + private function recreateBackendNotifier() { + $this->controller = new CustomBackendNotifier( + $this->config, + $this->createMock(ILogger::class), + $this->createMock(IClientService::class), + $this->secureRandom + ); + } + + private function calculateBackendChecksum($data, $random) { + if (empty($random) || strlen($random) < 32) { + return false; + } + $hash = hash_hmac('sha256', $random . $data, $this->signalingSecret); + return $hash; + } + + private function validateBackendRequest($expectedUrl, $request) { + $this->assertTrue(isset($request)); + $this->assertEquals($expectedUrl, $request['url']); + $headers = $request['params']['headers']; + $this->assertEquals('application/json', $headers['Content-Type']); + $random = $headers['Spreed-Signaling-Random']; + $checksum = $headers['Spreed-Signaling-Checksum']; + $body = $request['params']['body']; + $this->assertEquals($this->calculateBackendChecksum($body, $random), $checksum); + return $body; + } + + public function testRoomInvite() { + $room = $this->manager->createPublicRoom(); + $room->addUsers([ + 'userId' => $this->userId, + ]); + + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'invite', + 'invite' => [ + 'userids' => [ + $this->userId, + ], + 'alluserids' => [ + $this->userId, + ], + 'properties' => [ + 'name' => $room->getName(), + 'type' => $room->getType(), + ], + ], + ], json_decode($body, true)); + } + + public function testRoomDisinvite() { + $room = $this->manager->createPublicRoom(); + $room->addUsers([ + 'userId' => $this->userId, + ]); + $this->controller->clearLastRequest(); + $testUser = $this->createMock(IUser::class); + $testUser + ->method('getUID') + ->willReturn($this->userId); + $room->removeUser($testUser); + + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'disinvite', + 'disinvite' => [ + 'userids' => [ + $this->userId, + ], + 'alluserids' => [ + ], + 'properties' => [ + 'name' => $room->getName(), + 'type' => $room->getType(), + ], + ], + ], json_decode($body, true)); + } + + public function testRoomNameChanged() { + $room = $this->manager->createPublicRoom(); + $room->setName('Test room'); + + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'update', + 'update' => [ + 'userids' => [ + ], + 'properties' => [ + 'name' => $room->getName(), + 'type' => $room->getType(), + ], + ], + ], json_decode($body, true)); + } + + public function testRoomDelete() { + $room = $this->manager->createPublicRoom(); + $room->addUsers([ + 'userId' => $this->userId, + ]); + $room->deleteRoom(); + + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'delete', + 'delete' => [ + 'userids' => [ + $this->userId, + ], + ], + ], json_decode($body, true)); + } + + public function testRoomInCallChanged() { + $room = $this->manager->createPublicRoom(); + $userSession = 'user-session'; + $room->addUsers([ + 'userId' => $this->userId, + 'sessionId' => $userSession, + ]); + $room->changeInCall($userSession, true); + + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'incall', + 'incall' => [ + 'incall' => true, + 'changed' => [ + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $userSession, + 'participantType' => Participant::USER, + 'userId' => $this->userId, + ], + ], + 'users' => [ + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $userSession, + 'participantType' => Participant::USER, + ], + ], + ], + ], json_decode($body, true)); + + $this->controller->clearLastRequest(); + $guestSession = $room->enterRoomAsGuest(''); + $room->changeInCall($guestSession, true); + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'incall', + 'incall' => [ + 'incall' => true, + 'changed' => [ + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $guestSession, + 'participantType' => Participant::GUEST, + ], + ], + 'users' => [ + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $userSession, + 'participantType' => Participant::USER, + ], + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $guestSession, + 'participantType' => Participant::GUEST, + ], + ], + ], + ], json_decode($body, true)); + + $this->controller->clearLastRequest(); + $room->changeInCall($userSession, false); + $request = $this->controller->getLastRequest(); + $body = $this->validateBackendRequest($this->baseUrl . '/api/v1/room/' . $room->getToken(), $request); + $this->assertSame([ + 'type' => 'incall', + 'incall' => [ + 'incall' => false, + 'changed' => [ + [ + 'inCall' => false, + 'lastPing' => 0, + 'sessionId' => $userSession, + 'participantType' => Participant::USER, + 'userId' => $this->userId, + ], + ], + 'users' => [ + [ + 'inCall' => true, + 'lastPing' => 0, + 'sessionId' => $guestSession, + 'participantType' => Participant::GUEST, + ], + ], + ], + ], json_decode($body, true)); + } + +} From 9a39ed981ac8a43b6a56fb27f71ec3e803b09201 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Tue, 30 Jan 2018 14:41:49 +0100 Subject: [PATCH 6/8] Fix type in condition. Signed-off-by: Joachim Bauch --- lib/Signaling/BackendNotifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index 99489b75d67..f5111bfb833 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -106,7 +106,7 @@ private function backendRequest($url, $data) { 'headers' => $headers, 'body' => $body, ]; - if (!empty($signaling['verify'])) { + if (empty($signaling['verify'])) { $params['verify'] = false; } $this->doRequest($url, $params); From f54e9de17b56b64118291b23e491a8ed53f7ffb2 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Wed, 31 Jan 2018 16:12:02 +0100 Subject: [PATCH 7/8] Fix link to OCS signaling backend. Signed-off-by: Joachim Bauch --- js/signaling.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/signaling.js b/js/signaling.js index a986767395a..868e98df360 100644 --- a/js/signaling.js +++ b/js/signaling.js @@ -701,13 +701,13 @@ }; } else { var user = OC.getCurrentUser(); - var url = OC.generateUrl("/ocs/v2.php/apps/spreed/api/v1/signaling/backend"); + var url = OC.linkToOCS('apps/spreed/api/v1/signaling', 2) + 'backend'; msg = { "type": "hello", "hello": { "version": "1.0", "auth": { - "url": OC.getProtocol() + "://" + OC.getHost() + url, + "url": url, "params": { "userid": user.uid, "ticket": this.settings.ticket From e3ef4d23b99d3c7c4cd068c7f0e7bd094743082a Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 1 Feb 2018 11:47:44 +0100 Subject: [PATCH 8/8] Return "DataResponse" to be OCS compatible. Signed-off-by: Joachim Bauch --- lib/Controller/SignalingController.php | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 86eed59c6ed..4c76b25f980 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -32,7 +32,6 @@ use OCA\Spreed\Signaling\Messages; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\OCSController; use OCP\IDBConnection; use OCP\IRequest; @@ -279,12 +278,12 @@ protected function getInputStream() { * @NoCSRFRequired * @PublicPage * - * @return JSONResponse + * @return DataResponse */ public function backend() { $json = $this->getInputStream(); if (!$this->validateBackendRequest($json)) { - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'invalid_request', @@ -305,7 +304,7 @@ public function backend() { // Ping sessions connected to a room. return $this->backendPing($message['ping']); default: - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'unknown_type', @@ -319,7 +318,7 @@ private function backendAuth($auth) { $params = $auth['params']; $userId = $params['userid']; if (!$this->config->validateSignalingTicket($userId, $params['ticket'])) { - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'invalid_ticket', @@ -331,7 +330,7 @@ private function backendAuth($auth) { if (!empty($userId)) { $user = $this->userManager->get($userId); if (!$user instanceof IUser) { - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'no_such_user', @@ -353,7 +352,7 @@ private function backendAuth($auth) { 'displayname' => $user->getDisplayName(), ]; } - return new JSONResponse($response); + return new DataResponse($response); } private function backendRoom($roomRequest) { @@ -364,7 +363,7 @@ private function backendRoom($roomRequest) { try { $room = $this->manager->getRoomByToken($roomId); } catch (RoomNotFoundException $e) { - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'no_such_room', @@ -389,7 +388,7 @@ private function backendRoom($roomRequest) { $participant = $room->getParticipantBySession($sessionId); } catch (ParticipantNotFoundException $e) { // Return generic error to avoid leaking which rooms exist. - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'no_such_room', @@ -414,14 +413,14 @@ private function backendRoom($roomRequest) { ], ], ]; - return new JSONResponse($response); + return new DataResponse($response); } private function backendPing($request) { try { $room = $this->manager->getRoomByToken($request['roomid']); } catch (RoomNotFoundException $e) { - return new JSONResponse([ + return new DataResponse([ 'type' => 'error', 'error' => [ 'code' => 'no_such_room', @@ -446,7 +445,7 @@ private function backendPing($request) { 'roomid' => $room->getToken(), ], ]; - return new JSONResponse($response); + return new DataResponse($response); } }