Merge pull request #746 from thecodingmachine/turncredentials
[Feature] Connect to a Coturn server using REST API
This commit is contained in:
commit
9affa36608
@ -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=
|
||||
|
1
.github/workflows/build-and-deploy.yml
vendored
1
.github/workflows/build-and-deploy.yml
vendored
@ -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 }}
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 {}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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{
|
||||
|
Loading…
Reference in New Issue
Block a user