ep_webrtc 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/ep.json +1 -0
- package/index.js +103 -28
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -134,6 +134,8 @@ example:
|
|
|
134
134
|
}
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
#### Ephemeral credentials
|
|
138
|
+
|
|
137
139
|
To limit abuse, the [coturn](https://github.com/coturn/coturn) TURN server
|
|
138
140
|
supports [ephemeral (temporary) usernames and
|
|
139
141
|
passwords](https://github.com/coturn/coturn/blob/60e7a199fe748cb7080594a458d22c2f7bb15a8c/README.turnserver#L664-L729).
|
|
@@ -165,7 +167,36 @@ Example:
|
|
|
165
167
|
"lifetime": 3600
|
|
166
168
|
}
|
|
167
169
|
]
|
|
168
|
-
}
|
|
170
|
+
},
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
There is also support for ephemeral credentials from the
|
|
174
|
+
[Xirsys](https://xirsys.com/) [API](https://docs.xirsys.com/?pg=api-turn):
|
|
175
|
+
|
|
176
|
+
* `credentialType` (required): Must be set to the exact string `"xirsys
|
|
177
|
+
ephemeral credentials"`.
|
|
178
|
+
* `url` (required): The desired Xirsys TURN API endpoint.
|
|
179
|
+
* `username` (required): Your Xirsys username.
|
|
180
|
+
* `credential` (required): Your Xirsys API secret.
|
|
181
|
+
* `lifetime` (optional; defaults to 43200 = 12 hours): How long (in seconds)
|
|
182
|
+
the ephemeral credentials will remain valid after the user visits a pad.
|
|
183
|
+
After this amount of time, new TURN connections will fail until the user
|
|
184
|
+
reloads the page (which will generate a new password).
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
"ep_webrtc": {
|
|
190
|
+
"iceServers": [
|
|
191
|
+
{
|
|
192
|
+
"credentialType": "xirsys ephemeral credentials",
|
|
193
|
+
"url": "https://global.xirsys.net/_turn/myChannel",
|
|
194
|
+
"username": "myUsername",
|
|
195
|
+
"credential": "myPassword",
|
|
196
|
+
"lifetime": 3600
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
},
|
|
169
200
|
```
|
|
170
201
|
|
|
171
202
|
### Video Sizes
|
package/ep.json
CHANGED
package/index.js
CHANGED
|
@@ -14,15 +14,19 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
+
const _ = require('lodash');
|
|
18
|
+
const {Buffer} = require('buffer');
|
|
17
19
|
const crypto = require('crypto');
|
|
18
|
-
const log4js = require('ep_etherpad-lite/node_modules/log4js');
|
|
19
|
-
const statsLogger = log4js.getLogger('stats');
|
|
20
|
-
const configLogger = log4js.getLogger('configuration');
|
|
21
20
|
const eejs = require('ep_etherpad-lite/node/eejs/');
|
|
22
21
|
const sessioninfos = require('ep_etherpad-lite/node/handler/PadMessageHandler').sessioninfos;
|
|
23
22
|
const stats = require('ep_etherpad-lite/node/stats');
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
let logger = {};
|
|
25
|
+
for (const level of ['debug', 'info', 'warn', 'error']) {
|
|
26
|
+
logger[level] = console[level].bind(console, 'ep_webrtc:');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const defaultSettings = {
|
|
26
30
|
// The defaults here are overridden by the values in the `ep_webrtc` object from `settings.json`.
|
|
27
31
|
enabled: true,
|
|
28
32
|
audio: {
|
|
@@ -40,8 +44,18 @@ const settings = {
|
|
|
40
44
|
listenClass: null,
|
|
41
45
|
moreInfoUrl: {},
|
|
42
46
|
};
|
|
47
|
+
let settings = null;
|
|
43
48
|
let socketio;
|
|
44
49
|
|
|
50
|
+
const addContextToError = (err, pfx) => {
|
|
51
|
+
const newErr = new Error(`${pfx}${err.message}`, {cause: err});
|
|
52
|
+
if (Error.captureStackTrace) Error.captureStackTrace(newErr, addContextToError);
|
|
53
|
+
// Check for https://github.com/tc39/proposal-error-cause support, available in Node.js >= v16.10.
|
|
54
|
+
if (newErr.cause === err) return newErr;
|
|
55
|
+
err.message = `${pfx}${err.message}`;
|
|
56
|
+
return err;
|
|
57
|
+
};
|
|
58
|
+
|
|
45
59
|
// Copied from:
|
|
46
60
|
// https://github.com/ether/etherpad-lite/blob/f95b09e0b6752a0d226d58d8b246831164dc9533/src/node/handler/PadMessageHandler.js#L1411-L1420
|
|
47
61
|
const _getRoomSockets = (padId) => {
|
|
@@ -102,21 +116,70 @@ const handleErrorStatMessage = (statName) => {
|
|
|
102
116
|
if (statErrorNames.includes(statName)) {
|
|
103
117
|
stats.meter(`ep_webrtc_err_${statName}`).mark();
|
|
104
118
|
} else {
|
|
105
|
-
|
|
119
|
+
logger.warn(`Invalid ep_webrtc error stat: ${statName}`);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const fetchJson = async (url, opts = {}) => {
|
|
124
|
+
const c = new globalThis.AbortController();
|
|
125
|
+
const t = setTimeout(() => c.abort(), 5000);
|
|
126
|
+
let res;
|
|
127
|
+
try {
|
|
128
|
+
res = await globalThis.fetch(url, {signal: c.signal, ...opts});
|
|
129
|
+
} finally {
|
|
130
|
+
clearTimeout(t);
|
|
106
131
|
}
|
|
132
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
133
|
+
return await res.json();
|
|
107
134
|
};
|
|
108
135
|
|
|
109
136
|
exports.clientVars = async (hookName, {clientVars: {userId: authorId}}) => ({ep_webrtc: {
|
|
110
137
|
...settings,
|
|
111
|
-
iceServers: settings.iceServers.map((server) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
iceServers: await Promise.all(settings.iceServers.map(async (server) => {
|
|
139
|
+
switch (server.credentialType) {
|
|
140
|
+
case 'coturn ephemeral password': {
|
|
141
|
+
const {lifetime = 60 * 60 * 12 /* seconds */} = server;
|
|
142
|
+
const username = `${Math.floor(Date.now() / 1000) + lifetime}:${authorId}`;
|
|
143
|
+
const hmac = crypto.createHmac('sha1', server.credential);
|
|
144
|
+
hmac.update(username);
|
|
145
|
+
const credential = hmac.digest('base64');
|
|
146
|
+
return {urls: server.urls, username, credential};
|
|
147
|
+
}
|
|
148
|
+
case 'xirsys ephemeral credentials': {
|
|
149
|
+
const {
|
|
150
|
+
url,
|
|
151
|
+
username,
|
|
152
|
+
credential,
|
|
153
|
+
lifetime: expire = 12 * 60 * 60, // seconds
|
|
154
|
+
method = 'PUT',
|
|
155
|
+
headers: h = {},
|
|
156
|
+
jsonBody: b = {},
|
|
157
|
+
} = server;
|
|
158
|
+
// Can't set default values for the Content-Type and Authorization headers by using an
|
|
159
|
+
// object literal with spread (e.g., `{'content-type': 'foo', ...h}`) because the Headers
|
|
160
|
+
// constructor uses `.append()` internally instead of `.set()`. This matters if a header is
|
|
161
|
+
// repeated multiple times by using different mixes of upper- and lower-case letters.
|
|
162
|
+
const headers = new globalThis.Headers(h);
|
|
163
|
+
if (!headers.has('content-type')) headers.set('content-type', 'application/json');
|
|
164
|
+
if (username && !headers.has('authorization')) {
|
|
165
|
+
headers.set('authorization',
|
|
166
|
+
`Basic ${Buffer.from(`${username}:${credential}`).toString('base64')}`);
|
|
167
|
+
}
|
|
168
|
+
const body =
|
|
169
|
+
JSON.stringify(b && typeof b === 'object' ? {format: 'urls', expire, ...b} : b);
|
|
170
|
+
try {
|
|
171
|
+
const {v, s} = await fetchJson(url, {method, headers, body});
|
|
172
|
+
if (s !== 'ok') throw new Error(`API error: ${v}`);
|
|
173
|
+
return v.iceServers;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
const newErr = addContextToError(err, 'failed to get TURN credentials: ');
|
|
176
|
+
logger.error(newErr.stack || newErr.toString());
|
|
177
|
+
throw newErr;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
default: return server;
|
|
181
|
+
}
|
|
182
|
+
})),
|
|
120
183
|
}});
|
|
121
184
|
|
|
122
185
|
exports.handleMessage = async (hookName, {message, socket}) => {
|
|
@@ -130,6 +193,22 @@ exports.handleMessage = async (hookName, {message, socket}) => {
|
|
|
130
193
|
}
|
|
131
194
|
};
|
|
132
195
|
|
|
196
|
+
exports.init_ep_webrtc = async (hookName, {logger: l}) => {
|
|
197
|
+
if (l != null) logger = l;
|
|
198
|
+
// TODO: Remove this once all supported Node.js versions have the fetch API (added in Node.js
|
|
199
|
+
// v17.5.0 behind the --experimental-fetch flag).
|
|
200
|
+
if (!globalThis.fetch) {
|
|
201
|
+
// eslint-disable-next-line node/no-unsupported-features/es-syntax -- https://github.com/mysticatea/eslint-plugin-node/issues/250
|
|
202
|
+
const {default: fetch, Headers, Request, Response} = await import('node-fetch');
|
|
203
|
+
Object.assign(globalThis, {fetch, Headers, Request, Response});
|
|
204
|
+
}
|
|
205
|
+
// TODO: Remove this once all supported Node.js versions have AbortController (>= v15.4.0).
|
|
206
|
+
if (!globalThis.AbortController) {
|
|
207
|
+
// eslint-disable-next-line node/no-unsupported-features/es-syntax -- https://github.com/mysticatea/eslint-plugin-node/issues/250
|
|
208
|
+
globalThis.AbortController = (await import('abort-controller')).default;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
133
212
|
exports.setSocketIO = (hookName, {io}) => { socketio = io; };
|
|
134
213
|
|
|
135
214
|
exports.eejsBlock_mySettings = (hookName, context) => {
|
|
@@ -143,27 +222,23 @@ exports.eejsBlock_styles = (hookName, context) => {
|
|
|
143
222
|
context.content += eejs.require('./templates/styles.html', {}, module);
|
|
144
223
|
};
|
|
145
224
|
|
|
146
|
-
exports.loadSettings = async (hookName, {settings: {ep_webrtc = {}}}) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// The own-property check on the target prevents prototype pollution. (Prototype pollution
|
|
152
|
-
// shouldn't be exploitable here because the source comes from admin-controlled settings.json,
|
|
153
|
-
// but the check is added anyway to hopefully pacify static analysis tools.)
|
|
154
|
-
if (Object.prototype.hasOwnProperty.call(target, k) && isObj(tv) && isObj(sv)) merge(tv, sv);
|
|
155
|
-
else target[k] = sv;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
merge(settings, ep_webrtc);
|
|
225
|
+
exports.loadSettings = async (hookName, {settings: {ep_webrtc: s = {}}}) => {
|
|
226
|
+
settings = _.mergeWith({}, defaultSettings, s, (objV, srcV, key, obj, src) => {
|
|
227
|
+
if (Array.isArray(srcV)) return _.cloneDeep(srcV); // Don't merge arrays, replace them.
|
|
228
|
+
if (src === s && key === 'videoConstraints') return _.cloneDeep(srcV);
|
|
229
|
+
});
|
|
159
230
|
settings.configError = (() => {
|
|
160
231
|
for (const k of ['audio', 'video']) {
|
|
161
232
|
const {[k]: {disabled} = {}} = settings;
|
|
162
233
|
if (disabled != null && !['none', 'hard', 'soft'].includes(disabled)) {
|
|
163
|
-
|
|
234
|
+
logger.error(`Invalid value in settings.json for ep_webrtc.${k}.disabled`);
|
|
164
235
|
return true;
|
|
165
236
|
}
|
|
166
237
|
}
|
|
167
238
|
return false;
|
|
168
239
|
})();
|
|
240
|
+
logger.info('configured:', {
|
|
241
|
+
...settings,
|
|
242
|
+
iceServers: settings.iceServers.map((s) => s.credential ? {...s, credential: '*****'} : s),
|
|
243
|
+
});
|
|
169
244
|
};
|
package/package.json
CHANGED
|
@@ -5,21 +5,25 @@
|
|
|
5
5
|
"url": "git@github.com:ether/ep_webrtc.git",
|
|
6
6
|
"type": "git"
|
|
7
7
|
},
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.1.0",
|
|
9
9
|
"description": "WebRTC based audio/video chat to Etherpad",
|
|
10
10
|
"author": "John McLear <john@mclear.co.uk>",
|
|
11
11
|
"contributors": [],
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=12.17.0"
|
|
14
14
|
},
|
|
15
|
-
"dependencies": {
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"abort-controller": "^3.0.0",
|
|
17
|
+
"lodash": "^4.17.21",
|
|
18
|
+
"node-fetch": "^3.2.1"
|
|
19
|
+
},
|
|
16
20
|
"funding": {
|
|
17
21
|
"type": "individual",
|
|
18
22
|
"url": "https://etherpad.org/"
|
|
19
23
|
},
|
|
20
24
|
"devDependencies": {
|
|
21
25
|
"eslint": "^8.10.0",
|
|
22
|
-
"eslint-config-etherpad": "^3.0.
|
|
26
|
+
"eslint-config-etherpad": "^3.0.5",
|
|
23
27
|
"typescript": "^4.6.2"
|
|
24
28
|
},
|
|
25
29
|
"scripts": {
|