Merge pull request #746 from thecodingmachine/turncredentials

[Feature] Connect to a Coturn server using REST API
This commit is contained in:
David Négrier 2021-02-16 19:32:21 +01:00 committed by GitHub
commit 9affa36608
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 133 additions and 30 deletions

View File

@ -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=

View File

@ -150,6 +150,7 @@ jobs:
JITSI_ISS: ${{ secrets.JITSI_ISS }}
JITSI_URL: ${{ secrets.JITSI_URL }}
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }}
with:
namespace: workadventure-${{ env.GITHUB_REF_SLUG }}

View File

@ -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.

View File

@ -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,

View File

@ -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');
@ -275,6 +283,12 @@ export class SocketManager {
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
webrtcSignalToClient.setUserid(user.id);
webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== '') {
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password);
}
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
@ -295,6 +309,12 @@ export class SocketManager {
const webrtcSignalToClient = new WebRtcSignalToClientMessage();
webrtcSignalToClient.setUserid(user.id);
webrtcSignalToClient.setSignal(data.getSignal());
// TODO: only compute credentials if data.signal.type === "offer"
if (TURN_STATIC_AUTH_SECRET !== '') {
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
webrtcSignalToClient.setWebrtcusername(username);
webrtcSignalToClient.setWebrtcpassword(password);
}
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
@ -487,6 +507,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 +525,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 +542,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

View File

@ -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 {}

View File

@ -31,9 +31,12 @@ services:
ADMIN_URL: workadventure.localhost
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
STUN_SERVER: "stun:stun.l.google.com:19302"
TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349"
# 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 +111,7 @@ services:
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
JITSI_URL: $JITSI_URL
JITSI_ISS: $JITSI_ISS
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
volumes:
- ./back:/usr/src/app
labels:
@ -149,3 +153,28 @@ services:
- ./back:/usr/src/back
- ./front:/usr/src/front
- ./pusher:/usr/src/pusher
# coturn:
# image: coturn/coturn:4.5.2
# command:
# - turnserver
# #- -c=/etc/coturn/turnserver.conf
# - --log-file=stdout
# - --external-ip=$$(detect-external-ip)
# - --listening-port=3478
# - --min-port=10000
# - --max-port=10010
# - --tls-listening-port=5349
# - --listening-ip=0.0.0.0
# - --realm=coturn.workadventure.localhost
# - --server-name=coturn.workadventure.localhost
# - --lt-cred-mech
# # Enable Coturn "REST API" to validate temporary passwords.
# #- --use-auth-secret
# #- --static-auth-secret=SomeStaticAuthSecret
# #- --userdb=/var/lib/turn/turndb
# - --user=workadventure:WorkAdventure123
# # use real-valid certificate/privatekey files
# #- --cert=/root/letsencrypt/fullchain.pem
# #- --pkey=/root/letsencrypt/privkey.pem
# network_mode: host

View File

@ -96,7 +96,9 @@ export interface WebRtcSignalSentMessageInterface {
export interface WebRtcSignalReceivedMessageInterface {
userId: number,
signal: SignalData
signal: SignalData,
webRtcUser: string | undefined,
webRtcPassword: string | undefined
}
export interface StartMapInterface {

View File

@ -427,7 +427,9 @@ export class RoomConnection implements RoomConnection {
callback({
userId: message.getUserid(),
name: message.getName(),
initiator: message.getInitiator()
initiator: message.getInitiator(),
webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined,
});
});
}
@ -436,7 +438,9 @@ export class RoomConnection implements RoomConnection {
this.onMessage(EventMessage.WEBRTC_SIGNAL, (message: WebRtcSignalToClientMessage) => {
callback({
userId: message.getUserid(),
signal: JSON.parse(message.getSignal())
signal: JSON.parse(message.getSignal()),
webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined,
});
});
}
@ -445,7 +449,9 @@ export class RoomConnection implements RoomConnection {
this.onMessage(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, (message: WebRtcSignalToClientMessage) => {
callback({
userId: message.getUserid(),
signal: JSON.parse(message.getSignal())
signal: JSON.parse(message.getSignal()),
webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined,
});
});
}
@ -584,7 +590,7 @@ export class RoomConnection implements RoomConnection {
public hasTag(tag: string): boolean {
return this.tags.includes(tag);
}
public isAdmin(): boolean {
return this.hasTag('admin');
}

View File

@ -4,9 +4,9 @@ const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? w
const UPLOADER_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.UPLOADER_URL || 'uploader.workadventure.localhost');
const ADMIN_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.ADMIN_URL || "workadventure.localhost");
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca";
const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com';
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$';
const TURN_SERVER: string = process.env.TURN_SERVER || "";
const TURN_USER: string = process.env.TURN_USER || '';
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || '';
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true";
const RESOLUTION = 2;

View File

@ -3,6 +3,7 @@ import {mediaManager} from "./MediaManager";
import {STUN_SERVER, TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "../Connexion/RoomConnection";
import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer";
import {UserSimplePeerInterface} from "./SimplePeer";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
@ -16,8 +17,9 @@ export class ScreenSharingPeer extends Peer {
private isReceivingStream:boolean = false;
public toClose: boolean = false;
public _connected: boolean = false;
private userId: number;
constructor(private userId: number, initiator: boolean, private connection: RoomConnection) {
constructor(user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection) {
super({
initiator: initiator ? initiator : false,
reconnectTimer: 10000,
@ -26,15 +28,17 @@ export class ScreenSharingPeer extends Peer {
{
urls: STUN_SERVER.split(',')
},
{
TURN_SERVER !== '' ? {
urls: TURN_SERVER.split(','),
username: TURN_USER,
credential: TURN_PASSWORD
},
]
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD
} : undefined,
].filter((value) => value !== undefined)
}
});
this.userId = user.userId;
//start listen signal for the peer connection
this.on('signal', (data: unknown) => {
this.sendWebrtcScreenSharingSignal(data);

View File

@ -19,6 +19,8 @@ export interface UserSimplePeerInterface{
userId: number;
name?: string;
initiator?: boolean;
webRtcUser?: string|undefined;
webRtcPassword?: string|undefined;
}
export interface PeerConnectionListener {
@ -99,9 +101,9 @@ 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)
//console.log('receiveWebrtcStart. Initiator: ', user.initiator)
if(!user.initiator){
return;
}
@ -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, user.initiator ? user.initiator : false, this.Connection);
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) {

View File

@ -34,14 +34,15 @@ export class VideoPeer extends Peer {
{
urls: STUN_SERVER.split(',')
},
{
TURN_SERVER !== '' ? {
urls: TURN_SERVER.split(','),
username: TURN_USER,
credential: TURN_PASSWORD
},
]
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD
} : undefined,
].filter((value) => value !== undefined)
}
});
this.userId = user.userId;
this.userName = user.name || '';
@ -89,7 +90,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 +118,7 @@ export class VideoPeer extends Peer {
this.sendBlockMessage(false);
}
});
if (blackListManager.isBlackListed(this.userId)) {
this.sendBlockMessage(true)
}

View File

@ -168,6 +168,8 @@ message WebRtcStartMessage {
int32 userId = 1;
string name = 2;
bool initiator = 3;
string webrtcUserName = 4;
string webrtcPassword = 5;
}
message WebRtcDisconnectMessage {
@ -177,6 +179,8 @@ message WebRtcDisconnectMessage {
message WebRtcSignalToClientMessage {
int32 userId = 1;
string signal = 2;
string webrtcUserName = 4;
string webrtcPassword = 5;
}
message TeleportMessageMessage{