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 {