[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.
This commit is contained in:
parent
e07efbdf28
commit
cdb3cfdc81
@ -6,3 +6,7 @@ JITSI_ISS=
|
|||||||
SECRET_JITSI_KEY=
|
SECRET_JITSI_KEY=
|
||||||
ADMIN_API_TOKEN=123
|
ADMIN_API_TOKEN=123
|
||||||
START_ROOM_URL=/_/global/maps.workadventure.localhost/Floor0/floor0.json
|
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=
|
||||||
|
@ -6,8 +6,6 @@ Demo here : [https://workadventu.re/](https://workadventu.re/).
|
|||||||
|
|
||||||
# Work Adventure
|
# 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
|
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.
|
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 HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080;
|
||||||
const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051;
|
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 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 {
|
export {
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
|
@ -28,7 +28,13 @@ import {User, UserSocket} from "../Model/User";
|
|||||||
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
|
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
|
||||||
import {Group} from "../Model/Group";
|
import {Group} from "../Model/Group";
|
||||||
import {cpuTracker} from "./CpuTracker";
|
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 {Movable} from "../Model/Movable";
|
||||||
import {PositionInterface} from "../Model/PositionInterface";
|
import {PositionInterface} from "../Model/PositionInterface";
|
||||||
import {adminApi, CharacterTexture} from "./AdminApi";
|
import {adminApi, CharacterTexture} from "./AdminApi";
|
||||||
@ -40,6 +46,8 @@ import {ZoneSocket} from "../RoomManager";
|
|||||||
import {Zone} from "_Model/Zone";
|
import {Zone} from "_Model/Zone";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
import {Admin} from "_Model/Admin";
|
import {Admin} from "_Model/Admin";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
|
||||||
const debug = Debug('sockermanager');
|
const debug = Debug('sockermanager');
|
||||||
|
|
||||||
@ -487,6 +495,11 @@ export class SocketManager {
|
|||||||
webrtcStartMessage1.setUserid(otherUser.id);
|
webrtcStartMessage1.setUserid(otherUser.id);
|
||||||
webrtcStartMessage1.setName(otherUser.name);
|
webrtcStartMessage1.setName(otherUser.name);
|
||||||
webrtcStartMessage1.setInitiator(true);
|
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();
|
const serverToClientMessage1 = new ServerToClientMessage();
|
||||||
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
||||||
@ -500,6 +513,11 @@ export class SocketManager {
|
|||||||
webrtcStartMessage2.setUserid(user.id);
|
webrtcStartMessage2.setUserid(user.id);
|
||||||
webrtcStartMessage2.setName(user.name);
|
webrtcStartMessage2.setName(user.name);
|
||||||
webrtcStartMessage2.setInitiator(false);
|
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();
|
const serverToClientMessage2 = new ServerToClientMessage();
|
||||||
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
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
|
//disconnect user
|
||||||
private disConnectedUser(user: User, group: Group) {
|
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
|
// 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_ISS": env.JITSI_ISS,
|
||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
|
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||||
} + if adminUrl != null then {
|
} + if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {}
|
} else {}
|
||||||
@ -40,6 +41,7 @@
|
|||||||
"JITSI_ISS": env.JITSI_ISS,
|
"JITSI_ISS": env.JITSI_ISS,
|
||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
|
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||||
} + if adminUrl != null then {
|
} + if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {}
|
} else {}
|
||||||
|
@ -32,8 +32,10 @@ services:
|
|||||||
STARTUP_COMMAND_1: ./templater.sh
|
STARTUP_COMMAND_1: ./templater.sh
|
||||||
STARTUP_COMMAND_2: yarn install
|
STARTUP_COMMAND_2: yarn install
|
||||||
TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443"
|
TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443"
|
||||||
TURN_USER: workadventure
|
# Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
|
||||||
TURN_PASSWORD: WorkAdventure123
|
# 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"
|
START_ROOM_URL: "$START_ROOM_URL"
|
||||||
command: yarn run start
|
command: yarn run start
|
||||||
volumes:
|
volumes:
|
||||||
@ -108,6 +110,7 @@ services:
|
|||||||
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
|
TURN_STATIC_AUTH_SECRET:
|
||||||
volumes:
|
volumes:
|
||||||
- ./back:/usr/src/app
|
- ./back:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
|
@ -427,7 +427,9 @@ export class RoomConnection implements RoomConnection {
|
|||||||
callback({
|
callback({
|
||||||
userId: message.getUserid(),
|
userId: message.getUserid(),
|
||||||
name: message.getName(),
|
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 {
|
public hasTag(tag: string): boolean {
|
||||||
return this.tags.includes(tag);
|
return this.tags.includes(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAdmin(): boolean {
|
public isAdmin(): boolean {
|
||||||
return this.hasTag('admin');
|
return this.hasTag('admin');
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
public toClose: boolean = false;
|
public toClose: boolean = false;
|
||||||
public _connected: 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({
|
super({
|
||||||
initiator: initiator ? initiator : false,
|
initiator: initiator ? initiator : false,
|
||||||
reconnectTimer: 10000,
|
reconnectTimer: 10000,
|
||||||
@ -28,8 +28,8 @@ export class ScreenSharingPeer extends Peer {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
urls: TURN_SERVER.split(','),
|
urls: TURN_SERVER.split(','),
|
||||||
username: TURN_USER,
|
username: webRtcUser || TURN_USER,
|
||||||
credential: TURN_PASSWORD
|
credential: webRtcPassword || TURN_PASSWORD
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,8 @@ export interface UserSimplePeerInterface{
|
|||||||
userId: number;
|
userId: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
initiator?: boolean;
|
initiator?: boolean;
|
||||||
|
webRtcUser?: string|undefined;
|
||||||
|
webRtcPassword?: string|undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PeerConnectionListener {
|
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)
|
// 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)
|
// 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.
|
// This would be symmetrical to the way we handle disconnection.
|
||||||
|
|
||||||
//start connection
|
//start connection
|
||||||
console.log('receiveWebrtcStart. Initiator: ', user.initiator)
|
console.log('receiveWebrtcStart. Initiator: ', user.initiator)
|
||||||
if(!user.initiator){
|
if(!user.initiator){
|
||||||
@ -189,7 +191,7 @@ export class SimplePeer {
|
|||||||
mediaManager.addScreenSharingActiveVideo("" + user.userId);
|
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);
|
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
|
||||||
|
|
||||||
for (const peerConnectionListener of this.peerConnectionListeners) {
|
for (const peerConnectionListener of this.peerConnectionListeners) {
|
||||||
|
@ -36,8 +36,8 @@ export class VideoPeer extends Peer {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
urls: TURN_SERVER.split(','),
|
urls: TURN_SERVER.split(','),
|
||||||
username: TURN_USER,
|
username: user.webRtcUser || TURN_USER,
|
||||||
credential: TURN_PASSWORD
|
credential: user.webRtcPassword || TURN_PASSWORD
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -89,7 +89,7 @@ export class VideoPeer extends Peer {
|
|||||||
mediaManager.addNewMessage(message.name, message.message);
|
mediaManager.addNewMessage(message.name, message.message);
|
||||||
}
|
}
|
||||||
} else if(message.type === MESSAGE_TYPE_BLOCKED) {
|
} 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
|
// 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
|
//However, the output stream stream B is correctly blocked in A client
|
||||||
this.blocked = true;
|
this.blocked = true;
|
||||||
@ -117,7 +117,7 @@ export class VideoPeer extends Peer {
|
|||||||
this.sendBlockMessage(false);
|
this.sendBlockMessage(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blackListManager.isBlackListed(this.userId)) {
|
if (blackListManager.isBlackListed(this.userId)) {
|
||||||
this.sendBlockMessage(true)
|
this.sendBlockMessage(true)
|
||||||
}
|
}
|
||||||
|
@ -168,6 +168,8 @@ message WebRtcStartMessage {
|
|||||||
int32 userId = 1;
|
int32 userId = 1;
|
||||||
string name = 2;
|
string name = 2;
|
||||||
bool initiator = 3;
|
bool initiator = 3;
|
||||||
|
string webrtcUserName = 4;
|
||||||
|
string webrtcPassword = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WebRtcDisconnectMessage {
|
message WebRtcDisconnectMessage {
|
||||||
|
Loading…
Reference in New Issue
Block a user