ep_webrtc 0.1.99 → 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/.github/workflows/backend-tests.yml +2 -2
- package/.github/workflows/frontend-tests.yml +2 -2
- package/.github/workflows/npmpublish.yml +3 -3
- package/README.md +49 -1
- package/ep.json +1 -0
- package/index.js +107 -28
- package/package.json +9 -5
- package/static/js/index.js +1 -1
|
@@ -22,12 +22,12 @@ jobs:
|
|
|
22
22
|
sudo apt install -y --no-install-recommends libreoffice libreoffice-pdfimport
|
|
23
23
|
-
|
|
24
24
|
name: Install etherpad core
|
|
25
|
-
uses: actions/checkout@
|
|
25
|
+
uses: actions/checkout@v3
|
|
26
26
|
with:
|
|
27
27
|
repository: ether/etherpad-lite
|
|
28
28
|
-
|
|
29
29
|
name: Checkout plugin repository
|
|
30
|
-
uses: actions/checkout@
|
|
30
|
+
uses: actions/checkout@v3
|
|
31
31
|
with:
|
|
32
32
|
path: ./node_modules/__tmp
|
|
33
33
|
-
|
|
@@ -28,7 +28,7 @@ jobs:
|
|
|
28
28
|
printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}'
|
|
29
29
|
-
|
|
30
30
|
name: Check out Etherpad core
|
|
31
|
-
uses: actions/checkout@
|
|
31
|
+
uses: actions/checkout@v3
|
|
32
32
|
with:
|
|
33
33
|
repository: ether/etherpad-lite
|
|
34
34
|
-
|
|
@@ -41,7 +41,7 @@ jobs:
|
|
|
41
41
|
src/bin/doc/package-lock.json
|
|
42
42
|
-
|
|
43
43
|
name: Check out the plugin
|
|
44
|
-
uses: actions/checkout@
|
|
44
|
+
uses: actions/checkout@v3
|
|
45
45
|
with:
|
|
46
46
|
path: ./node_modules/__tmp
|
|
47
47
|
-
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
# conflicts with this plugin's clone, etherpad-lite must be cloned and
|
|
23
23
|
# moved out before this plugin's repo is cloned to $GITHUB_WORKSPACE.
|
|
24
24
|
-
|
|
25
|
-
uses: actions/checkout@
|
|
25
|
+
uses: actions/checkout@v3
|
|
26
26
|
with:
|
|
27
27
|
repository: ether/etherpad-lite
|
|
28
28
|
path: etherpad-lite
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
# etherpad-lite has been moved outside of $GITHUB_WORKSPACE, so it is now
|
|
32
32
|
# safe to clone this plugin's repo to $GITHUB_WORKSPACE.
|
|
33
33
|
-
|
|
34
|
-
uses: actions/checkout@
|
|
34
|
+
uses: actions/checkout@v3
|
|
35
35
|
# This is necessary for actions/setup-node because '..' can't be used in
|
|
36
36
|
# cache-dependency-path.
|
|
37
37
|
-
|
|
@@ -77,7 +77,7 @@ jobs:
|
|
|
77
77
|
runs-on: ubuntu-latest
|
|
78
78
|
steps:
|
|
79
79
|
-
|
|
80
|
-
uses: actions/checkout@
|
|
80
|
+
uses: actions/checkout@v3
|
|
81
81
|
with:
|
|
82
82
|
fetch-depth: 0
|
|
83
83
|
-
|
package/README.md
CHANGED
|
@@ -74,6 +74,20 @@ Supported values for `"disabled"`:
|
|
|
74
74
|
* `"soft"`: Initially disabled by default.
|
|
75
75
|
* `"hard"`: Unavailable (it cannot be enabled).
|
|
76
76
|
|
|
77
|
+
The camera's record resolution can be configured by setting `videoConstraints`
|
|
78
|
+
to any [video
|
|
79
|
+
constraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#parameters)
|
|
80
|
+
value acceptable to client browsers. It has the following default value:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
"ep_webrtc": {
|
|
84
|
+
"videoConstraints": {
|
|
85
|
+
"width": {"ideal": 160},
|
|
86
|
+
"height": {"ideal": 120}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
77
91
|
### Custom Activate Button
|
|
78
92
|
|
|
79
93
|
The misnamed `listenClass` setting allows you to specify a CSS selector for an
|
|
@@ -120,6 +134,8 @@ example:
|
|
|
120
134
|
}
|
|
121
135
|
```
|
|
122
136
|
|
|
137
|
+
#### Ephemeral credentials
|
|
138
|
+
|
|
123
139
|
To limit abuse, the [coturn](https://github.com/coturn/coturn) TURN server
|
|
124
140
|
supports [ephemeral (temporary) usernames and
|
|
125
141
|
passwords](https://github.com/coturn/coturn/blob/60e7a199fe748cb7080594a458d22c2f7bb15a8c/README.turnserver#L664-L729).
|
|
@@ -151,7 +167,36 @@ Example:
|
|
|
151
167
|
"lifetime": 3600
|
|
152
168
|
}
|
|
153
169
|
]
|
|
154
|
-
}
|
|
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
|
+
},
|
|
155
200
|
```
|
|
156
201
|
|
|
157
202
|
### Video Sizes
|
|
@@ -170,6 +215,9 @@ one or both of the following in your `settings.json`:
|
|
|
170
215
|
}
|
|
171
216
|
```
|
|
172
217
|
|
|
218
|
+
This only controls the size of the video display widget. To set the camera's
|
|
219
|
+
record resolution, see the `videoConstraints` setting.
|
|
220
|
+
|
|
173
221
|
## Metrics
|
|
174
222
|
|
|
175
223
|
You can see metrics for various errors that users have when attempting to
|
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: {
|
|
@@ -32,12 +36,26 @@ const settings = {
|
|
|
32
36
|
disabled: 'none',
|
|
33
37
|
sizes: {large: 260, small: 160},
|
|
34
38
|
},
|
|
39
|
+
videoConstraints: {
|
|
40
|
+
width: {ideal: 160},
|
|
41
|
+
height: {ideal: 120},
|
|
42
|
+
},
|
|
35
43
|
iceServers: [{urls: ['stun:stun.l.google.com:19302']}],
|
|
36
44
|
listenClass: null,
|
|
37
45
|
moreInfoUrl: {},
|
|
38
46
|
};
|
|
47
|
+
let settings = null;
|
|
39
48
|
let socketio;
|
|
40
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
|
+
|
|
41
59
|
// Copied from:
|
|
42
60
|
// https://github.com/ether/etherpad-lite/blob/f95b09e0b6752a0d226d58d8b246831164dc9533/src/node/handler/PadMessageHandler.js#L1411-L1420
|
|
43
61
|
const _getRoomSockets = (padId) => {
|
|
@@ -98,21 +116,70 @@ const handleErrorStatMessage = (statName) => {
|
|
|
98
116
|
if (statErrorNames.includes(statName)) {
|
|
99
117
|
stats.meter(`ep_webrtc_err_${statName}`).mark();
|
|
100
118
|
} else {
|
|
101
|
-
|
|
119
|
+
logger.warn(`Invalid ep_webrtc error stat: ${statName}`);
|
|
102
120
|
}
|
|
103
121
|
};
|
|
104
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);
|
|
131
|
+
}
|
|
132
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
133
|
+
return await res.json();
|
|
134
|
+
};
|
|
135
|
+
|
|
105
136
|
exports.clientVars = async (hookName, {clientVars: {userId: authorId}}) => ({ep_webrtc: {
|
|
106
137
|
...settings,
|
|
107
|
-
iceServers: settings.iceServers.map((server) => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
})),
|
|
116
183
|
}});
|
|
117
184
|
|
|
118
185
|
exports.handleMessage = async (hookName, {message, socket}) => {
|
|
@@ -126,6 +193,22 @@ exports.handleMessage = async (hookName, {message, socket}) => {
|
|
|
126
193
|
}
|
|
127
194
|
};
|
|
128
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
|
+
|
|
129
212
|
exports.setSocketIO = (hookName, {io}) => { socketio = io; };
|
|
130
213
|
|
|
131
214
|
exports.eejsBlock_mySettings = (hookName, context) => {
|
|
@@ -139,27 +222,23 @@ exports.eejsBlock_styles = (hookName, context) => {
|
|
|
139
222
|
context.content += eejs.require('./templates/styles.html', {}, module);
|
|
140
223
|
};
|
|
141
224
|
|
|
142
|
-
exports.loadSettings = async (hookName, {settings: {ep_webrtc = {}}}) => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// The own-property check on the target prevents prototype pollution. (Prototype pollution
|
|
148
|
-
// shouldn't be exploitable here because the source comes from admin-controlled settings.json,
|
|
149
|
-
// but the check is added anyway to hopefully pacify static analysis tools.)
|
|
150
|
-
if (Object.prototype.hasOwnProperty.call(target, k) && isObj(tv) && isObj(sv)) merge(tv, sv);
|
|
151
|
-
else target[k] = sv;
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
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
|
+
});
|
|
155
230
|
settings.configError = (() => {
|
|
156
231
|
for (const k of ['audio', 'video']) {
|
|
157
232
|
const {[k]: {disabled} = {}} = settings;
|
|
158
233
|
if (disabled != null && !['none', 'hard', 'soft'].includes(disabled)) {
|
|
159
|
-
|
|
234
|
+
logger.error(`Invalid value in settings.json for ep_webrtc.${k}.disabled`);
|
|
160
235
|
return true;
|
|
161
236
|
}
|
|
162
237
|
}
|
|
163
238
|
return false;
|
|
164
239
|
})();
|
|
240
|
+
logger.info('configured:', {
|
|
241
|
+
...settings,
|
|
242
|
+
iceServers: settings.iceServers.map((s) => s.credential ? {...s, credential: '*****'} : s),
|
|
243
|
+
});
|
|
165
244
|
};
|
package/package.json
CHANGED
|
@@ -5,22 +5,26 @@
|
|
|
5
5
|
"url": "git@github.com:ether/ep_webrtc.git",
|
|
6
6
|
"type": "git"
|
|
7
7
|
},
|
|
8
|
-
"version": "
|
|
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
|
-
"eslint": "^8.
|
|
22
|
-
"eslint-config-etherpad": "^3.0.
|
|
23
|
-
"typescript": "^4.
|
|
25
|
+
"eslint": "^8.10.0",
|
|
26
|
+
"eslint-config-etherpad": "^3.0.5",
|
|
27
|
+
"typescript": "^4.6.2"
|
|
24
28
|
},
|
|
25
29
|
"scripts": {
|
|
26
30
|
"lint": "eslint .",
|
package/static/js/index.js
CHANGED
|
@@ -669,7 +669,7 @@ exports.rtc = new class {
|
|
|
669
669
|
debug(`requesting permission to access ${devices.join(' and ')}`);
|
|
670
670
|
const stream = await window.navigator.mediaDevices.getUserMedia({
|
|
671
671
|
audio: addAudioTrack,
|
|
672
|
-
video: addVideoTrack &&
|
|
672
|
+
video: addVideoTrack && this._settings.videoConstraints,
|
|
673
673
|
});
|
|
674
674
|
debug('successfully accessed device(s)');
|
|
675
675
|
return stream;
|