diff --git a/.env.template b/.env.template index 330f386526e642c7e7e40c466b826b92ad43aeb9..a83bd1713efcec0e85badd5b4c97c5b443d1ab2d 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 5945ac489a9a446f0912e74ced13261fa67c6dd2..a8c186b6857fcf2794b84c6ff2ea4f7b3792b769 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 95a705fa087612a853a20ad7d53d208596116807..b12f0542b7e7b7bfbd4a35b9d5a1526d0cfe7f2a 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 c90b51cf9953b2ec77dbd686bfa8c5758c243cee..194080ecee5a0227624adccff2edf39cbc9fe30e 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 9d201081c3455161dcbf38ba1df60f87eec6bbbb..5093c86a0c141f624ea280320ac03df595bc50e3 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 286c12baf1e66c9bb2c3d45a278c4f555def24b6..9e9e08429d380b7255ad560374cca2b62ad3a3e4 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 8eb7462fcbfc30434985347c3cfe1a6e294ffc18..65d4b4dce184772db2d22716d405b7b54c7832b6 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 1b8680cff9c94f08298b6c974a4b574d26cdda07..2491d1e61e36500938875711a26f3e1f44cd9c36 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 98f83b0c657677cb8ac73d727c33a3f34185aa73..889d374888175ee7b3a23a701702c34536c1282a 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 b2df80c2081911707791392d9933742bc6affa7d..350b046f2f4a942895cac70c1e4351cc7e1ea791 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 39f575be53cfd19b1e2ec54ab1090f4a981b8a99..a1e7688ec27747b3eca4da26dbeda3169b9e9a57 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 {