From cdb3cfdc810d07ba1aa44088811b9734d70c3238 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20N=C3=A9grier?= <d.negrier@thecodingmachine.com>
Date: Tue, 16 Feb 2021 09:58:08 +0100
Subject: [PATCH] [Feature] Connect to a Coturn server using REST API

This allows connecting to a TURN server with temporary passwords.
The passwords are expiring after 4 hours.
---
 .env.template                         |  4 +++
 README.md                             |  2 --
 back/src/Enum/EnvironmentVariable.ts  |  1 +
 back/src/Services/SocketManager.ts    | 39 ++++++++++++++++++++++++++-
 deeployer.libsonnet                   |  2 ++
 docker-compose.yaml                   |  7 +++--
 front/src/Connexion/RoomConnection.ts |  6 +++--
 front/src/WebRtc/ScreenSharingPeer.ts |  6 ++---
 front/src/WebRtc/SimplePeer.ts        |  6 +++--
 front/src/WebRtc/VideoPeer.ts         |  8 +++---
 messages/protos/messages.proto        |  2 ++
 11 files changed, 67 insertions(+), 16 deletions(-)

diff --git a/.env.template b/.env.template
index 330f3865..a83bd171 100644
--- a/.env.template
+++ b/.env.template
@@ -6,3 +6,7 @@ JITSI_ISS=
 SECRET_JITSI_KEY=
 ADMIN_API_TOKEN=123
 START_ROOM_URL=/_/global/maps.workadventure.localhost/Floor0/floor0.json
+# If your Turn server is configured to use the Turn REST API, you should put the shared auth secret here.
+# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file.
+# Keep empty if you are sharing hard coded / clear text credentials.
+TURN_STATIC_AUTH_SECRET=
diff --git a/README.md b/README.md
index 5945ac48..a8c186b6 100644
--- a/README.md
+++ b/README.md
@@ -6,8 +6,6 @@ Demo here : [https://workadventu.re/](https://workadventu.re/).
 
 # Work Adventure
 
-## Work in progress
-
 Work Adventure is a web-based collaborative workspace for small to medium teams (2-100 people) presented in the form of a
 16-bit video game.
 
diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts
index 95a705fa..b12f0542 100644
--- a/back/src/Enum/EnvironmentVariable.ts
+++ b/back/src/Enum/EnvironmentVariable.ts
@@ -11,6 +11,7 @@ const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || '';
 const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080;
 const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051;
 export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
+export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || '';
 
 export {
     MINIMUM_DISTANCE,
diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts
index c90b51cf..194080ec 100644
--- a/back/src/Services/SocketManager.ts
+++ b/back/src/Services/SocketManager.ts
@@ -28,7 +28,13 @@ import {User, UserSocket} from "../Model/User";
 import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
 import {Group} from "../Model/Group";
 import {cpuTracker} from "./CpuTracker";
-import {GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable";
+import {
+    GROUP_RADIUS,
+    JITSI_ISS,
+    MINIMUM_DISTANCE,
+    SECRET_JITSI_KEY,
+    TURN_STATIC_AUTH_SECRET
+} from "../Enum/EnvironmentVariable";
 import {Movable} from "../Model/Movable";
 import {PositionInterface} from "../Model/PositionInterface";
 import {adminApi, CharacterTexture} from "./AdminApi";
@@ -40,6 +46,8 @@ import {ZoneSocket} from "../RoomManager";
 import {Zone} from "_Model/Zone";
 import Debug from "debug";
 import {Admin} from "_Model/Admin";
+import crypto from "crypto";
+
 
 const debug = Debug('sockermanager');
 
@@ -487,6 +495,11 @@ export class SocketManager {
             webrtcStartMessage1.setUserid(otherUser.id);
             webrtcStartMessage1.setName(otherUser.name);
             webrtcStartMessage1.setInitiator(true);
+            if (TURN_STATIC_AUTH_SECRET !== '') {
+                const {username, password} = this.getTURNCredentials(''+otherUser.id, TURN_STATIC_AUTH_SECRET);
+                webrtcStartMessage1.setWebrtcusername(username);
+                webrtcStartMessage1.setWebrtcpassword(password);
+            }
 
             const serverToClientMessage1 = new ServerToClientMessage();
             serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
@@ -500,6 +513,11 @@ export class SocketManager {
             webrtcStartMessage2.setUserid(user.id);
             webrtcStartMessage2.setName(user.name);
             webrtcStartMessage2.setInitiator(false);
+            if (TURN_STATIC_AUTH_SECRET !== '') {
+                const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
+                webrtcStartMessage2.setWebrtcusername(username);
+                webrtcStartMessage2.setWebrtcpassword(password);
+            }
 
             const serverToClientMessage2 = new ServerToClientMessage();
             serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
@@ -512,6 +530,25 @@ export class SocketManager {
         }
     }
 
+    /**
+     * Computes a unique user/password for the TURN server, using a shared secret between the WorkAdventure API server
+     * and the Coturn server.
+     * The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey`
+     */
+    private getTURNCredentials(name: string, secret: string): {username: string, password: string} {
+        const unixTimeStamp = Math.floor(Date.now()/1000) + 4*3600;   // this credential would be valid for the next 4 hours
+        const username = [unixTimeStamp, name].join(':');
+        const hmac = crypto.createHmac('sha1', secret);
+        hmac.setEncoding('base64');
+        hmac.write(username);
+        hmac.end();
+        const password = hmac.read();
+        return {
+            username: username,
+            password: password
+        };
+    }
+
     //disconnect user
     private disConnectedUser(user: User, group: Group) {
         // Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection
diff --git a/deeployer.libsonnet b/deeployer.libsonnet
index 9d201081..5093c86a 100644
--- a/deeployer.libsonnet
+++ b/deeployer.libsonnet
@@ -22,6 +22,7 @@
          "JITSI_ISS": env.JITSI_ISS,
          "JITSI_URL": env.JITSI_URL,
          "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
+         "TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
        } + if adminUrl != null then {
          "ADMIN_API_URL": adminUrl,
        } else {}
@@ -40,6 +41,7 @@
               "JITSI_ISS": env.JITSI_ISS,
               "JITSI_URL": env.JITSI_URL,
               "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
+              "TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
             } + if adminUrl != null then {
               "ADMIN_API_URL": adminUrl,
             } else {}
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 286c12ba..9e9e0842 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -32,8 +32,10 @@ services:
       STARTUP_COMMAND_1: ./templater.sh
       STARTUP_COMMAND_2: yarn install
       TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443"
-      TURN_USER: workadventure
-      TURN_PASSWORD: WorkAdventure123
+      # Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
+      # Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
+      TURN_USER:
+      TURN_PASSWORD:
       START_ROOM_URL: "$START_ROOM_URL"
     command: yarn run start
     volumes:
@@ -108,6 +110,7 @@ services:
       ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
       JITSI_URL: $JITSI_URL
       JITSI_ISS: $JITSI_ISS
+      TURN_STATIC_AUTH_SECRET:
     volumes:
       - ./back:/usr/src/app
     labels:
diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts
index 8eb7462f..65d4b4dc 100644
--- a/front/src/Connexion/RoomConnection.ts
+++ b/front/src/Connexion/RoomConnection.ts
@@ -427,7 +427,9 @@ export class RoomConnection implements RoomConnection {
             callback({
                 userId: message.getUserid(),
                 name: message.getName(),
-                initiator: message.getInitiator()
+                initiator: message.getInitiator(),
+                webRtcUser: message.getWebrtcpassword() ?? undefined,
+                webRtcPassword: message.getWebrtcpassword() ?? undefined,
             });
         });
     }
@@ -584,7 +586,7 @@ export class RoomConnection implements RoomConnection {
     public hasTag(tag: string): boolean {
         return this.tags.includes(tag);
     }
-    
+
     public isAdmin(): boolean {
         return this.hasTag('admin');
     }
diff --git a/front/src/WebRtc/ScreenSharingPeer.ts b/front/src/WebRtc/ScreenSharingPeer.ts
index 1b8680cf..2491d1e6 100644
--- a/front/src/WebRtc/ScreenSharingPeer.ts
+++ b/front/src/WebRtc/ScreenSharingPeer.ts
@@ -17,7 +17,7 @@ export class ScreenSharingPeer extends Peer {
     public toClose: boolean = false;
     public _connected: boolean = false;
 
-    constructor(private userId: number, initiator: boolean, private connection: RoomConnection) {
+    constructor(private userId: number, initiator: boolean, private connection: RoomConnection, webRtcUser: string | undefined, webRtcPassword: string | undefined) {
         super({
             initiator: initiator ? initiator : false,
             reconnectTimer: 10000,
@@ -28,8 +28,8 @@ export class ScreenSharingPeer extends Peer {
                     },
                     {
                         urls: TURN_SERVER.split(','),
-                        username: TURN_USER,
-                        credential: TURN_PASSWORD
+                        username: webRtcUser || TURN_USER,
+                        credential: webRtcPassword || TURN_PASSWORD
                     },
                 ]
             }
diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts
index 98f83b0c..889d3748 100644
--- a/front/src/WebRtc/SimplePeer.ts
+++ b/front/src/WebRtc/SimplePeer.ts
@@ -19,6 +19,8 @@ export interface UserSimplePeerInterface{
     userId: number;
     name?: string;
     initiator?: boolean;
+    webRtcUser?: string|undefined;
+    webRtcPassword?: string|undefined;
 }
 
 export interface PeerConnectionListener {
@@ -99,7 +101,7 @@ export class SimplePeer {
         // Note: the clients array contain the list of all clients (even the ones we are already connected to in case a user joints a group)
         // So we can receive a request we already had before. (which will abort at the first line of createPeerConnection)
         // This would be symmetrical to the way we handle disconnection.
-        
+
         //start connection
         console.log('receiveWebrtcStart. Initiator: ', user.initiator)
         if(!user.initiator){
@@ -189,7 +191,7 @@ export class SimplePeer {
             mediaManager.addScreenSharingActiveVideo("" + user.userId);
         }
 
-        const peer = new ScreenSharingPeer(user.userId, user.initiator ? user.initiator : false, this.Connection);
+        const peer = new ScreenSharingPeer(user.userId, user.initiator ? user.initiator : false, this.Connection, user.webRtcUser, user.webRtcPassword);
         this.PeerScreenSharingConnectionArray.set(user.userId, peer);
 
         for (const peerConnectionListener of this.peerConnectionListeners) {
diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts
index b2df80c2..350b046f 100644
--- a/front/src/WebRtc/VideoPeer.ts
+++ b/front/src/WebRtc/VideoPeer.ts
@@ -36,8 +36,8 @@ export class VideoPeer extends Peer {
                     },
                     {
                         urls: TURN_SERVER.split(','),
-                        username: TURN_USER,
-                        credential: TURN_PASSWORD
+                        username: user.webRtcUser || TURN_USER,
+                        credential: user.webRtcPassword || TURN_PASSWORD
                     },
                 ]
             }
@@ -89,7 +89,7 @@ export class VideoPeer extends Peer {
                     mediaManager.addNewMessage(message.name, message.message);
                 }
             } else if(message.type === MESSAGE_TYPE_BLOCKED) {
-                //FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream. 
+                //FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
                 // Find a way to block A's output stream in A's js client
                 //However, the output stream stream B is correctly blocked in A client
                 this.blocked = true;
@@ -117,7 +117,7 @@ export class VideoPeer extends Peer {
                 this.sendBlockMessage(false);
             }
         });
-        
+
         if (blackListManager.isBlackListed(this.userId)) {
             this.sendBlockMessage(true)
         }
diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto
index 39f575be..a1e7688e 100644
--- a/messages/protos/messages.proto
+++ b/messages/protos/messages.proto
@@ -168,6 +168,8 @@ message WebRtcStartMessage {
   int32 userId = 1;
   string name = 2;
   bool initiator = 3;
+  string webrtcUserName = 4;
+  string webrtcPassword = 5;
 }
 
 message WebRtcDisconnectMessage {
-- 
GitLab