Skip to content
This repository was archived by the owner on Oct 23, 2024. It is now read-only.

Commit 8b5c13a

Browse files
committed
Authenticate WebTransport connections with tokens.
Tokens are issued by portal after joining. QUIC agent asks portal for authentication.
1 parent 6a84d9f commit 8b5c13a

File tree

6 files changed

+145
-27
lines changed

6 files changed

+145
-27
lines changed

source/agent/conference/conference.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1843,6 +1843,15 @@ var Conference = function (rpcClient, selfRpcId) {
18431843
callback('callback', result);
18441844
};
18451845

1846+
that.getPortal = function(participantId, callback) {
1847+
log.debug('Get participant ' + participantId);
1848+
if (!participants[participantId]) {
1849+
callback('callback', 'error', 'Invalid participant ID.');
1850+
return;
1851+
}
1852+
callback('callback', participants[participantId].getPortal());
1853+
};
1854+
18461855
//FIXME: Should handle updates other than authorities as well.
18471856
that.controlParticipant = function(participantId, authorities, callback) {
18481857
log.debug('controlParticipant', participantId, 'authorities:', authorities);
@@ -2674,7 +2683,10 @@ module.exports = function (rpcClient, selfRpcId, parentRpcId, clusterWorkerIP) {
26742683
controlSipCall: conference.controlSipCall,
26752684
endSipCall: conference.endSipCall,
26762685
drawText: conference.drawText,
2677-
destroy: conference.destroy
2686+
destroy: conference.destroy,
2687+
2688+
// RPC from QUIC nodes.
2689+
getPortal: conference.getPortal
26782690
};
26792691

26802692
that.onFaultDetected = conference.onFaultDetected;

source/agent/quic/dist.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
"../../common/clusterWorker.js",
1919
"../../common/loadCollector.js",
2020
"../../common/logger.js",
21-
"../../common/makeRPC.js",
22-
"../../common/rpcChannel.js",
2321
"../../../scripts/release/initcert.js",
2422
"../../../scripts/release/initauth.js",
2523
"../../../scripts/detectOS.sh"
@@ -28,7 +26,9 @@
2826
"quic": [
2927
"index.js",
3028
"../connections.js",
31-
"../InternalConnectionFactory.js"
29+
"../InternalConnectionFactory.js",
30+
"../../common/makeRPC.js",
31+
"../../common/rpcChannel.js"
3232
],
3333
"quic/webtransport": [
3434
"webtransport/quicTransportServer.js",

source/agent/quic/index.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const path = require('path');
2929
log.info('QUIC transport node.')
3030

3131
module.exports = function (rpcClient, selfRpcId, parentRpcId, clusterWorkerIP) {
32+
const rpcChannel = require('./rpcChannel')(rpcClient);
3233
const that = {
3334
agentID: parentRpcId,
3435
clusterIP: clusterWorkerIP
@@ -40,23 +41,60 @@ module.exports = function (rpcClient, selfRpcId, parentRpcId, clusterWorkerIP) {
4041
const outgoingStreamPipelines =
4142
new Map(); // Key is subscription ID, value is stream pipeline.
4243
let quicTransportServer;
44+
const clusterName = global && global.config && global.config.cluster ?
45+
global.config.cluster.name :
46+
undefined;
47+
48+
const getRpcPortal = async (roomId, participantId) => {
49+
const controllerAgent =
50+
await rpcChannel.makeRPC(clusterName, 'schedule', [
51+
'conference', roomId, 'preference' /*TODO: specify preference*/,
52+
30 * 1000
53+
]);
54+
let controller = null;
55+
let portal = null;
56+
const retry = 5;
57+
const sleep = require('util').promisify(setTimeout);
58+
for (let i = 0; i < retry; i++) {
59+
try {
60+
controller = await rpcChannel.makeRPC(
61+
controllerAgent.id, 'getNode', [{room: roomId, task: roomId}]);
62+
if (controller) {
63+
break;
64+
}
65+
} catch (error) {
66+
sleep(1000);
67+
}
68+
}
69+
portal =
70+
await rpcChannel.makeRPC(controller, 'getPortal', [participantId])
71+
return portal;
72+
};
4373

4474
const notifyStatus = (controller, sessionId, direction, status) => {
45-
rpcClient.remoteCast(controller, 'onSessionProgress', [sessionId, direction, status]);
75+
rpcClient.remoteCast(
76+
controller, 'onSessionProgress', [sessionId, direction, status]);
77+
};
78+
79+
const validateToken = async (token) => {
80+
if (!token || !token.roomId) {
81+
throw new TypeError('Invalid token.');
82+
}
83+
const portal = await getRpcPortal(token.roomId, token.participantId);
84+
return rpcChannel.makeRPC(
85+
portal, 'validateAndDeleteWebTransportToken', [token])
4686
};
4787

4888
const keystore = path.resolve(path.dirname(global.config.quic.keystorePath), cipher.kstore);
49-
log.info('before unlock');
5089
cipher.unlock(cipher.k, keystore, (error, password) => {
51-
log.info('unlocked.');
5290
if (error) {
5391
log.error('Failed to read certificate and key.');
5492
return;
5593
}
5694
log.info('path is '+path.resolve(global.config.quic.keystorePath));
5795
quicTransportServer = new QuicTransportServer(
5896
addon, global.config.quic.port, path.resolve(global.config.quic.keystorePath),
59-
password);
97+
password, validateToken);
6098
quicTransportServer.start();
6199
quicTransportServer.on('streamadded', (stream) => {
62100
const conn = connections.getConnection(stream.contentSessionId);

source/agent/quic/webtransport/quicTransportServer.js

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ const zeroUuid = '00000000000000000000000000000000';
2323
const authenticationTimeout = 10;
2424

2525
module.exports = class QuicTransportServer extends EventEmitter {
26-
constructor(addon, port, pfxPath, password) {
26+
constructor(addon, port, pfxPath, password, validateTokenCallback) {
2727
super();
2828
this._server = new addon.QuicTransportServer(port, pfxPath, password);
2929
this._connections = new Map(); // Key is transport ID.
3030
this._streams = new Map(); // Key is content session ID.
3131
this._unAuthenticatedConnections = []; // When it's authenticated, it will be moved to this.connections.
3232
this._unAssociatedStreams = []; // No content session ID assgined to them.
3333
this._assignedTransportIds = []; // Transport channels assigned to this server.
34+
this._validateTokenCallback = validateTokenCallback;
3435
this._server.onconnection = (connection) => {
3536
this._unAuthenticatedConnections.push(connection);
3637
setTimeout(() => {
@@ -61,16 +62,68 @@ module.exports = class QuicTransportServer extends EventEmitter {
6162
};
6263
stream.ondata = (data) => {
6364
if (stream.contentSessionId === zeroUuid) {
64-
const transportId = data.toString('utf8');
65-
log.info('Received new transport ID: '+transportId);
66-
if (!this._assignedTransportIds.includes(transportId)) {
67-
log.info(JSON.stringify(connection));
68-
connection.close();
65+
// Please refer
66+
// https://github.com/open-webrtc-toolkit/owt-server/blob/20e8aad216cc446095f63c409339c34c7ee770ee/doc/design/quic-transport-payload-format.md#signaling-session
67+
// for the format of data received on this stream.
68+
69+
// Size of data received, but haven't been read.
70+
let unreadSize = data.length;
71+
let nextReadSize = stream.frameSize ? stream.frameSize : 0;
72+
while (unreadSize != 0) {
73+
if (nextReadSize == 0) { // Starts a new frame.
74+
// 32 bit indicates the length of next frame.
75+
const lengthSize = 4;
76+
if (data.length >= lengthSize) {
77+
for (let i = 0; i < lengthSize; i++) {
78+
nextReadSize += (data[i] << 8 * i);
79+
}
80+
unreadSize -= lengthSize;
81+
} else {
82+
log.error('Not implemented.');
83+
return;
84+
}
85+
} else {
86+
nextReadSize = stream.frameSize - stream.unreadData.length;
87+
}
88+
const startIndex = data.length - unreadSize;
89+
if (unreadSize >= nextReadSize) {
90+
let frame = data.slice(startIndex, startIndex + nextReadSize);
91+
if (stream.unreadData) {
92+
frame = stream.unreadData + frame;
93+
stream.unreadData = null;
94+
stream.frameSize = 0;
95+
}
96+
const message = frame.toString('utf8');
97+
unreadSize -= nextReadSize;
98+
// Currently, Signaling over WebTransport is not supported, so
99+
// only one message (WebTransport token) will be sent on this
100+
// stream. We process the message and discard all other
101+
// messages.
102+
let token;
103+
try {
104+
token = JSON.parse(Buffer.from(message, 'base64').toString());
105+
} catch (error) {
106+
log.error('Invalid token.');
107+
connection.close();
108+
return;
109+
}
110+
this._validateTokenCallback(token).then(result => {
111+
if (result !== 'ok') {
112+
log.error('Authentication failed.');
113+
connection.close();
114+
return;
115+
}
116+
connection.transportId = token.transportId;
117+
stream.transportId = token.transportId;
118+
this._connections.set(connection.transportId, connection);
119+
});
120+
return;
121+
} else { // Store the data.
122+
stream.unreadData = data.slice(startIndex);
123+
stream.frameSize = nextReadSize;
124+
}
69125
}
70-
connection.transportId = transportId;
71-
stream.transportId = transportId;
72-
this._connections.set(connection.transportId, connection);
73-
log.info('New connection for transport ID: ' + transportId);
126+
return;
74127
}
75128
}
76129
}
@@ -132,4 +185,9 @@ module.exports = class QuicTransportServer extends EventEmitter {
132185
}
133186
return uuidArray;
134187
}
188+
189+
_validateToken(tokenString) {
190+
const token = JSON.parse(Buffer.from(tokenString, 'base64').toString());
191+
return this._validateTokenCallback(token);
192+
}
135193
};

source/portal/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,11 @@ var rpcPublic = {
233233
socketio_server && socketio_server.notify(participantId, event, data).catch(notifyFail);
234234
callback('callback', 'ok');
235235
},
236-
validateWebTransportToken: (token, callback) => {
237-
if(portal.validateWebTransportToken(token)) {
236+
validateAndDeleteWebTransportToken: (token, callback) => {
237+
if(portal.validateAndDeleteWebTransportToken(token)) {
238238
callback('callback','ok');
239239
} else {
240-
callback('callback', 'error');
240+
callback('callback', 'error', 'Invalid token for WebTransport.');
241241
}
242242
}
243243
};

source/portal/portal.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,36 +26,46 @@ var Portal = function(spec, rpcReq) {
2626
*/
2727
var participants = {};
2828

29-
// Key is token, value is participant ID. An ID is only valid when the participant is online.
29+
// Key is participantId, value is token ID.
3030
const webTransportIds = new Map();
3131
const calculateSignatureForWebTransportToken = (token) => {
32-
const toSign = vsprintf('%s,%s,%s,%s', [
32+
const toSign = vsprintf('%s,%s,%s,%s,%s', [
3333
token.tokenId,
3434
token.transportId,
3535
token.participantId,
36+
token.roomId,
3637
token.issueTime
3738
]);
3839
const signed = crypto.createHmac('sha256', token_key).update(toSign).digest('hex');
3940
return (Buffer.from(signed)).toString('base64');
4041
};
41-
const generateWebTransportToken = (participantId) => {
42+
const generateWebTransportToken = (participantId, roomId) => {
4243
const now = Date.now();
4344
const token = {
4445
tokenId : uuid().replace(/-/g, ''),
4546
transportId: uuid().replace(/-/g, ''),
4647
participantId : participantId,
48+
roomId: roomId,
4749
issueTime : now,
4850
};
4951
token.signature = calculateSignatureForWebTransportToken(token);
52+
webTransportIds.set(participantId, token.tokenId);
5053
return token;
5154
};
5255

53-
that.validateWebTransportToken = (token) => {
56+
that.validateAndDeleteWebTransportToken = (token) => {
5457
// |participants| is better to be a map.
5558
if (!participants.hasOwnProperty(token.participantId)) {
5659
return false;
5760
}
58-
return calculateSignatureForWebTransportToken(token) == token.signature;
61+
if (!webTransportIds.has(token.participantId) || webTransportIds.get(token.participantId) !== token.tokenId) {
62+
return false;
63+
}
64+
if (calculateSignatureForWebTransportToken(token) !== token.signature) {
65+
return false;
66+
}
67+
webTransportIds.delete(token.participantId);
68+
return true;
5969
};
6070

6171
that.updateTokenKey = function(tokenKey) {
@@ -114,7 +124,7 @@ var Portal = function(spec, rpcReq) {
114124

115125
let webTransportToken = undefined;
116126
if (token.webTransportUrl) {
117-
webTransportToken = (Buffer.from(JSON.stringify(generateWebTransportToken(participantId)))).toString('base64');
127+
webTransportToken = (Buffer.from(JSON.stringify(generateWebTransportToken(participantId, room_id)))).toString('base64');
118128
}
119129

120130
return {

0 commit comments

Comments
 (0)