@factoidal/core 0.31.1 → 0.33.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/CHANGELOG.md +24 -0
- package/bin/factoidal.mjs +173 -23
- package/bin/foafnaut-bleedingedge.mjs +4 -0
- package/bin/foafnaut-latest.mjs +4 -0
- package/bin/foafnaut.mjs +151 -0
- package/browser.js +5 -2
- package/index.d.ts +20 -7
- package/index.mjs +7 -4
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +6 -6
- package/lib/api.mjs +5 -3
- package/lib/sbm11-lws.mjs +25 -35
- package/lib/version.mjs +1 -1
- package/lws/server.mjs +82 -14
- package/package.json +5 -2
- package/webdav/server.mjs +12 -0
- package/xmpp/README.md +30 -3
- package/xmpp/client.mjs +18 -3
- package/xmpp/inpage.mjs +7 -1
- package/xmpp/server.mjs +174 -23
package/xmpp/server.mjs
CHANGED
|
@@ -37,10 +37,13 @@
|
|
|
37
37
|
// server-to-server federation, and any limit on how many bytes
|
|
38
38
|
// a client may send without completing a unit beyond `maxBuffered`,
|
|
39
39
|
// which is a transport limit this host applies by closing the socket.
|
|
40
|
+
// The other transport limits are `maxConnections`, `loginTimeoutMs` and
|
|
41
|
+
// `maxQueuedBytes`. There is no connection limit per address: behind a
|
|
42
|
+
// proxy, every client has the same one.
|
|
40
43
|
|
|
41
|
-
import { randomBytes } from 'node:crypto'
|
|
44
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
42
45
|
import { StringDecoder } from 'node:string_decoder'
|
|
43
|
-
import { mkdir, readFile, writeFile, readdir, unlink, rename } from 'node:fs/promises'
|
|
46
|
+
import { mkdir, readFile, writeFile, readdir, unlink, rename, stat } from 'node:fs/promises'
|
|
44
47
|
import { join } from 'node:path'
|
|
45
48
|
import { loadEngine } from '../bin/engine.mjs'
|
|
46
49
|
import { createXmppRouter } from './router.mjs'
|
|
@@ -49,7 +52,7 @@ import { createXmppRouter } from './router.mjs'
|
|
|
49
52
|
export const XMPP_SERVER_OPS = [
|
|
50
53
|
'xmppOpen', 'xmppClose',
|
|
51
54
|
'xmppSessionOpen', 'xmppSessionFeed', 'xmppSessionDeliver', 'xmppSessionClose',
|
|
52
|
-
'xmppSessionTlsUp', 'xmppRosterGet', 'xmppRosterSet', 'xmppWsOpen', 'xmppStateLoad'
|
|
55
|
+
'xmppSessionTlsUp', 'xmppRosterGet', 'xmppRosterSet', 'xmppWsOpen', 'xmppStateLoad', 'xmppClock'
|
|
53
56
|
]
|
|
54
57
|
|
|
55
58
|
/** An error raised by this host, as opposed to one the engine reported.
|
|
@@ -116,10 +119,19 @@ function randomToken (bytes = 18) {
|
|
|
116
119
|
* @param {object} options
|
|
117
120
|
* @param {string} [options.domain] the domain the server is authoritative for (default `localhost`)
|
|
118
121
|
* @param {Array<{username: string, password: string}>} [options.accounts]
|
|
119
|
-
* @param {string} [options.accountsText]
|
|
120
|
-
*
|
|
122
|
+
* @param {string} [options.accountsText] the accounts file's text, one account per line:
|
|
123
|
+
* `name:password`, or the keys `factoidal xmpp-serve --verifier NAME` writes. A line that is
|
|
124
|
+
* neither stops the start (`Server.accountsOfText`)
|
|
125
|
+
* @param {string} [options.saltB64] one SCRAM salt for every name, base64; absent, each name has its own
|
|
126
|
+
* @param {string} [options.saltSeed] the seed of the salts; absent, a random one for this process
|
|
127
|
+
* @param {string} [options.occupantSeed] the key of the rooms' occupant ids; absent, a random one
|
|
121
128
|
* @param {number} [options.iterationCount] the SCRAM iteration count
|
|
122
129
|
* @param {boolean} [options.tlsCarrier] whether the carrier terminates TLS
|
|
130
|
+
* @param {boolean} [options.disableRateLimits] no failed SASL login limit and no sender rate on
|
|
131
|
+
* held stanzas (`--dangerously-disable-ratelimits`)
|
|
132
|
+
* @param {object} [options.heldLimits] caps of the stanzas held for offline accounts, each a
|
|
133
|
+
* number: `perAccount`, `perAccountBytes`, `total`, `totalBytes`, `senderPerWindow`,
|
|
134
|
+
* `senderWindow` (seconds); what is absent keeps the engine's default
|
|
123
135
|
* @param {object} [options.engine] an engine handle to reuse
|
|
124
136
|
* @returns {Promise<{engine: object, handle: string, domain: string,
|
|
125
137
|
* openSession: Function, roster: Function, setRoster: Function, close: Function}>}
|
|
@@ -130,11 +142,20 @@ export async function openXmppServer (options = {}) {
|
|
|
130
142
|
if (typeof options.domain === 'string') config.domain = options.domain
|
|
131
143
|
if (Array.isArray(options.accounts)) config.accounts = options.accounts
|
|
132
144
|
if (typeof options.accountsText === 'string') config.accountsText = options.accountsText
|
|
145
|
+
// One salt per name, from a seed the engine cannot make; a fixed `saltB64`
|
|
146
|
+
// (a transcript that must replay) is one salt for all.
|
|
133
147
|
if (typeof options.saltB64 === 'string') config.saltB64 = options.saltB64
|
|
148
|
+
else config.saltSeed = typeof options.saltSeed === 'string' ? options.saltSeed : randomToken(32)
|
|
149
|
+
// The key of the XEP-0421 occupant ids of the rooms.
|
|
150
|
+
config.occupantSeed = typeof options.occupantSeed === 'string' ? options.occupantSeed : randomToken(32)
|
|
134
151
|
if (typeof options.iterationCount === 'number') config.iterationCount = options.iterationCount
|
|
135
152
|
if (typeof options.tlsCarrier === 'boolean') config.tlsCarrier = options.tlsCarrier
|
|
136
153
|
// XEP-0045 room service (L4Factoidal/XMPP/Muc.lean); off unless named.
|
|
137
154
|
if (typeof options.mucDomain === 'string') config.mucDomain = options.mucDomain
|
|
155
|
+
// Members-only rooms: [{room, members: [bare JID]}]; no list, an open room.
|
|
156
|
+
if (Array.isArray(options.mucMembers)) config.mucMembers = options.mucMembers
|
|
157
|
+
// Nicks reserved for a JID: [{nick, jid}]. Each account's name is reserved for it anyway.
|
|
158
|
+
if (Array.isArray(options.mucReservedNicks)) config.mucReservedNicks = options.mucReservedNicks
|
|
138
159
|
// XEP-0369 channels (L4Factoidal/XMPP/Mix.lean), joined through MIX-PAM; off unless named.
|
|
139
160
|
if (typeof options.mixDomain === 'string') config.mixDomain = options.mixDomain
|
|
140
161
|
// GC3 prototype groups (L4Factoidal/XMPP/Gc3.lean; no published spec); off unless named.
|
|
@@ -142,6 +163,10 @@ export async function openXmppServer (options = {}) {
|
|
|
142
163
|
// The host keeps the state across restarts (a state key and a DIR): only
|
|
143
164
|
// then does the engine produce `store`/`unstore`.
|
|
144
165
|
if (options.persist === true) config.persist = true
|
|
166
|
+
// The failed SASL login limit is on unless the operator turns it off
|
|
167
|
+
// (https://github.com/danbri/factoidal/issues/1128); the engine decides.
|
|
168
|
+
if (options.disableRateLimits === true) config.disableRateLimits = true
|
|
169
|
+
if (options.heldLimits && typeof options.heldLimits === 'object') config.heldLimits = options.heldLimits
|
|
145
170
|
let opened
|
|
146
171
|
try {
|
|
147
172
|
opened = engine.call('xmppOpen', [JSON.stringify(config)])
|
|
@@ -157,20 +182,45 @@ export async function openXmppServer (options = {}) {
|
|
|
157
182
|
throw new XmppHostError('xmppOpen answered with no handle')
|
|
158
183
|
}
|
|
159
184
|
const handle = opened.handle
|
|
185
|
+
// The rate limits read the host's clock: tell the engine when the second changes.
|
|
186
|
+
let lastSecond = 0
|
|
187
|
+
const tick = () => {
|
|
188
|
+
const now = Math.floor(Date.now() / 1000)
|
|
189
|
+
if (now === lastSecond) return
|
|
190
|
+
lastSecond = now
|
|
191
|
+
try {
|
|
192
|
+
engine.call('xmppClock', [handle, String(now)])
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// An older engine has no such op, and no limit that reads it.
|
|
195
|
+
if (!isUnknownOp(error)) throw error
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// An older engine ignores the list, and the rooms would be open: do not start.
|
|
199
|
+
if (Array.isArray(options.mucMembers) && opened.mucMembers !== options.mucMembers.length) {
|
|
200
|
+
engine.call('xmppClose', [handle])
|
|
201
|
+
throw new XmppHostError('this engine does not carry members-only rooms; rebuild the engine')
|
|
202
|
+
}
|
|
160
203
|
return {
|
|
161
204
|
engine,
|
|
162
205
|
handle,
|
|
163
206
|
domain: typeof opened.domain === 'string' ? opened.domain : (config.domain || 'localhost'),
|
|
164
207
|
accounts: typeof opened.accounts === 'number' ? opened.accounts : undefined,
|
|
208
|
+
/** The engine's start warnings (--dangerously-disable-ratelimits). */
|
|
209
|
+
warnings: Array.isArray(opened.warnings) ? opened.warnings : [],
|
|
210
|
+
tick,
|
|
165
211
|
/**
|
|
166
212
|
* One client connection.
|
|
167
|
-
*
|
|
213
|
+
* `remoteAddress` (the socket's) and the clock go to the engine for the
|
|
214
|
+
* failed SASL login limit (https://github.com/danbri/factoidal/issues/1128).
|
|
215
|
+
* @param {{streamId?: string, nonce?: string, tlsCarrier?: boolean, startTls?: boolean, maxUnits?: number, remoteAddress?: string}} [sessionOptions]
|
|
168
216
|
*/
|
|
169
217
|
openSession (sessionOptions = {}) {
|
|
170
218
|
const record = {
|
|
171
219
|
streamId: typeof sessionOptions.streamId === 'string' ? sessionOptions.streamId : randomToken(),
|
|
172
|
-
nonce: typeof sessionOptions.nonce === 'string' ? sessionOptions.nonce : randomToken()
|
|
220
|
+
nonce: typeof sessionOptions.nonce === 'string' ? sessionOptions.nonce : randomToken(),
|
|
221
|
+
now: Math.floor(Date.now() / 1000)
|
|
173
222
|
}
|
|
223
|
+
if (typeof sessionOptions.remoteAddress === 'string') record.remoteAddress = sessionOptions.remoteAddress
|
|
174
224
|
if (typeof sessionOptions.tlsCarrier === 'boolean') record.tlsCarrier = sessionOptions.tlsCarrier
|
|
175
225
|
if (typeof sessionOptions.startTls === 'boolean') record.startTls = sessionOptions.startTls
|
|
176
226
|
if (typeof sessionOptions.maxUnits === 'number') record.maxUnits = sessionOptions.maxUnits
|
|
@@ -182,6 +232,7 @@ export async function openXmppServer (options = {}) {
|
|
|
182
232
|
handle: session,
|
|
183
233
|
/** Feed received text; returns the engine's answer document. */
|
|
184
234
|
feed (text) {
|
|
235
|
+
tick()
|
|
185
236
|
return engine.call('xmppSessionFeed', [session, text])
|
|
186
237
|
},
|
|
187
238
|
/** The TLS handshake a feed asked for (`startTls: true`) finished. */
|
|
@@ -228,6 +279,14 @@ async function readAccountsFile (path) {
|
|
|
228
279
|
}
|
|
229
280
|
}
|
|
230
281
|
|
|
282
|
+
/** A warning when the accounts file can be read by anyone but its owner. */
|
|
283
|
+
async function accountsFileWarnings (path) {
|
|
284
|
+
if (process.platform === 'win32') return []
|
|
285
|
+
const { mode } = await stat(path)
|
|
286
|
+
if ((mode & 0o077) === 0) return []
|
|
287
|
+
return [`${path} can be read by other users of this machine: chmod 600 it`]
|
|
288
|
+
}
|
|
289
|
+
|
|
231
290
|
/**
|
|
232
291
|
* A `net.Server` (or `tls.Server`, when `cert` and `key` are given —
|
|
233
292
|
* XEP-0368 direct TLS) that runs one engine session per connection.
|
|
@@ -235,6 +294,7 @@ async function readAccountsFile (path) {
|
|
|
235
294
|
* The server is returned unlistened: the caller chooses the port.
|
|
236
295
|
*
|
|
237
296
|
* @param {object} options as `openXmppServer`, plus:
|
|
297
|
+
* @param {string} [options.accountsFile] the path `accountsText` came from, to check its mode
|
|
238
298
|
* @param {string} [options.dir] state directory: `accounts` is read from it when
|
|
239
299
|
* no accounts are given; rosters are persisted under `roster/`; `spool/` is
|
|
240
300
|
* polled for stanzas written by other processes
|
|
@@ -243,10 +303,20 @@ async function readAccountsFile (path) {
|
|
|
243
303
|
* @param {boolean} [options.startTls] listen in clear and use `cert` and
|
|
244
304
|
* `key` for STARTTLS (RFC 6120 section 5), which the engine then requires
|
|
245
305
|
* before anything else; without it, `cert` and `key` mean direct TLS
|
|
306
|
+
* @param {number} [options.directTlsPort] with `startTls`, `listen` also serves
|
|
307
|
+
* XEP-0368 direct TLS on this port, in the same server: one set of rooms
|
|
246
308
|
* @param {number} [options.pollMs] spool poll interval (default 250; 0 disables)
|
|
247
309
|
* @param {number} [options.maxBuffered] residual UTF-8 bytes held by the engine
|
|
248
310
|
* after a feed before the socket is closed (default 1 MiB); the decoder
|
|
249
311
|
* may separately hold up to three bytes of an incomplete UTF-8 sequence
|
|
312
|
+
* @param {number} [options.maxConnections] open connections, of all kinds, before
|
|
313
|
+
* a new one is closed at once (default 1024)
|
|
314
|
+
* @param {number} [options.loginTimeoutMs] how long a connection may stay
|
|
315
|
+
* without a bound resource before it is closed (default 30000; 0 disables)
|
|
316
|
+
* @param {number} [options.maxQueuedBytes] bytes waiting to be written to a
|
|
317
|
+
* client that is not reading, before the socket is closed (default 8 MiB)
|
|
318
|
+
* @param {boolean} [options.addressLimit] false: count failed logins per account name only, for a
|
|
319
|
+
* listener behind a proxy, where every client has one address (default true)
|
|
250
320
|
* @param {Function} [options.onError]
|
|
251
321
|
* @param {Function} [options.onLog]
|
|
252
322
|
* @returns {Promise<{server: object, xmpp: object, close: Function, sessions: Function}>}
|
|
@@ -259,6 +329,14 @@ export async function createXmppServer (options = {}) {
|
|
|
259
329
|
const dir = typeof options.dir === 'string' ? options.dir : null
|
|
260
330
|
const pollMs = typeof options.pollMs === 'number' ? options.pollMs : 250
|
|
261
331
|
const maxBuffered = typeof options.maxBuffered === 'number' ? options.maxBuffered : 1024 * 1024
|
|
332
|
+
const limit = (name, fallback) => {
|
|
333
|
+
const value = typeof options[name] === 'number' ? options[name] : fallback
|
|
334
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new XmppHostError(`${name} must be a nonnegative safe integer`)
|
|
335
|
+
return value
|
|
336
|
+
}
|
|
337
|
+
const maxConnections = limit('maxConnections', 1024)
|
|
338
|
+
const loginTimeoutMs = limit('loginTimeoutMs', 30000)
|
|
339
|
+
const maxQueuedBytes = limit('maxQueuedBytes', 8 * 1024 * 1024)
|
|
262
340
|
if (!Number.isSafeInteger(maxBuffered) || maxBuffered < 0) {
|
|
263
341
|
throw new XmppHostError('maxBuffered must be a nonnegative safe integer')
|
|
264
342
|
}
|
|
@@ -267,9 +345,19 @@ export async function createXmppServer (options = {}) {
|
|
|
267
345
|
}
|
|
268
346
|
|
|
269
347
|
const serverOptions = { ...options }
|
|
348
|
+
let accountsFile = typeof options.accountsFile === 'string' ? options.accountsFile : null
|
|
270
349
|
if (dir !== null && !Array.isArray(options.accounts) && typeof options.accountsText !== 'string') {
|
|
271
350
|
const text = await readAccountsFile(join(dir, 'accounts'))
|
|
272
|
-
if (text !== null) serverOptions.accountsText = text
|
|
351
|
+
if (text !== null) { serverOptions.accountsText = text; accountsFile = join(dir, 'accounts') }
|
|
352
|
+
}
|
|
353
|
+
// Salts that survive a restart when the state does: a client that keeps a
|
|
354
|
+
// salted password goes on working, and a name's salt does not change.
|
|
355
|
+
if (typeof options.saltSeed !== 'string' && typeof options.stateKey === 'string') {
|
|
356
|
+
serverOptions.saltSeed = createHash('sha256').update('xmpp salt seed\0' + options.stateKey).digest('base64url')
|
|
357
|
+
}
|
|
358
|
+
// Occupant ids that survive a restart, for the same reason.
|
|
359
|
+
if (typeof options.occupantSeed !== 'string' && typeof options.stateKey === 'string') {
|
|
360
|
+
serverOptions.occupantSeed = createHash('sha256').update('xmpp occupant seed\0' + options.stateKey).digest('base64url')
|
|
273
361
|
}
|
|
274
362
|
const haveKey = options.cert !== undefined && options.key !== undefined
|
|
275
363
|
if (options.startTls === true && !haveKey) {
|
|
@@ -280,6 +368,7 @@ export async function createXmppServer (options = {}) {
|
|
|
280
368
|
if (typeof serverOptions.tlsCarrier !== 'boolean') serverOptions.tlsCarrier = useTls
|
|
281
369
|
if (dir !== null && options.stateKey !== undefined) serverOptions.persist = true
|
|
282
370
|
const xmpp = await openXmppServer(serverOptions)
|
|
371
|
+
if (accountsFile !== null) xmpp.warnings.push(...await accountsFileWarnings(accountsFile))
|
|
283
372
|
|
|
284
373
|
// Rosters the previous run persisted.
|
|
285
374
|
try {
|
|
@@ -337,6 +426,10 @@ export async function createXmppServer (options = {}) {
|
|
|
337
426
|
if (got.status !== 200) throw new XmppHostError(`state: ${path} answered ${got.status}`)
|
|
338
427
|
const loaded = xmpp.engine.call('xmppStateLoad', [xmpp.handle, path, new TextDecoder().decode(got.body)])
|
|
339
428
|
if (loaded === null || typeof loaded !== 'object') throw new XmppHostError(`state: ${path} was refused`)
|
|
429
|
+
if (loaded.stale === true) {
|
|
430
|
+
const gone = await state.step({ method: 'DELETE', target: path, headers: [], body: new Uint8Array(0) })
|
|
431
|
+
if (gone.status >= 300 && gone.status !== 404) throw new XmppHostError(`state: DELETE ${path} answered ${gone.status}`)
|
|
432
|
+
}
|
|
340
433
|
}
|
|
341
434
|
}
|
|
342
435
|
} catch (error) {
|
|
@@ -385,8 +478,44 @@ export async function createXmppServer (options = {}) {
|
|
|
385
478
|
// connection callback and therefore have no engine session yet.
|
|
386
479
|
const transports = new Set()
|
|
387
480
|
|
|
481
|
+
// The client's address for the login limit. Behind a proxy every client has
|
|
482
|
+
// the proxy's address, and a limit on it would stop them all: `addressLimit: false`.
|
|
483
|
+
const addressOf = (socket) =>
|
|
484
|
+
options.addressLimit !== false && typeof socket.remoteAddress === 'string' ? socket.remoteAddress : ''
|
|
485
|
+
|
|
486
|
+
// Transport limits, like `maxBuffered`: bytes and time, not protocol.
|
|
487
|
+
function closeIfBacklogged (c) {
|
|
488
|
+
if (c.socket.destroyed || !(c.socket.writableLength > maxQueuedBytes)) return
|
|
489
|
+
onError(new XmppHostError(`connection exceeded ${maxQueuedBytes} queued bytes`))
|
|
490
|
+
c.socket.destroy()
|
|
491
|
+
}
|
|
492
|
+
|
|
388
493
|
function writeAll (c, lines) {
|
|
389
494
|
for (const line of lines) if (!c.socket.destroyed) c.socket.write(line)
|
|
495
|
+
closeIfBacklogged(c)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** A connection gets `loginTimeoutMs` to bind a resource. */
|
|
499
|
+
function startLoginTimer (c) {
|
|
500
|
+
if (loginTimeoutMs === 0) return
|
|
501
|
+
c.loginTimer = setTimeout(() => {
|
|
502
|
+
if (c.full === null && connections.has(c)) {
|
|
503
|
+
onLog('closing a connection that did not log in in time')
|
|
504
|
+
c.socket.destroy()
|
|
505
|
+
}
|
|
506
|
+
}, loginTimeoutMs)
|
|
507
|
+
c.loginTimer.unref()
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Count a new socket, or close it at once if the server is full. */
|
|
511
|
+
function admit (socket) {
|
|
512
|
+
if (transports.size >= maxConnections) {
|
|
513
|
+
onError(new XmppHostError(`refused a connection: ${maxConnections} are open`))
|
|
514
|
+
socket.destroy()
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
transports.add(socket)
|
|
518
|
+
socket.once('close', () => transports.delete(socket))
|
|
390
519
|
}
|
|
391
520
|
|
|
392
521
|
let rosterWrites = Promise.resolve()
|
|
@@ -471,6 +600,7 @@ export async function createXmppServer (options = {}) {
|
|
|
471
600
|
function endSession (c) {
|
|
472
601
|
if (!connections.has(c)) return
|
|
473
602
|
connections.delete(c)
|
|
603
|
+
clearTimeout(c.loginTimer)
|
|
474
604
|
let answer
|
|
475
605
|
try { answer = c.session.close() } catch (error) { onError(error); answer = { send: [] } }
|
|
476
606
|
// The session left its MUC rooms (Muc.leaveAll): tell the other occupants.
|
|
@@ -482,10 +612,10 @@ export async function createXmppServer (options = {}) {
|
|
|
482
612
|
}
|
|
483
613
|
}
|
|
484
614
|
|
|
485
|
-
function onConnection (socket) {
|
|
615
|
+
function onConnection (socket, direct = false) {
|
|
486
616
|
let session
|
|
487
617
|
try {
|
|
488
|
-
session = xmpp.openSession({ tlsCarrier: useTls, startTls })
|
|
618
|
+
session = xmpp.openSession({ tlsCarrier: useTls || direct, startTls: startTls && !direct, remoteAddress: addressOf(socket) })
|
|
489
619
|
} catch (error) {
|
|
490
620
|
onError(error)
|
|
491
621
|
socket.destroy()
|
|
@@ -505,6 +635,7 @@ export async function createXmppServer (options = {}) {
|
|
|
505
635
|
}
|
|
506
636
|
}
|
|
507
637
|
connections.add(c)
|
|
638
|
+
startLoginTimer(c)
|
|
508
639
|
onLog(`connection from ${socket.remoteAddress}:${socket.remotePort}`)
|
|
509
640
|
c.onData = (chunk) => {
|
|
510
641
|
if (!connections.has(c) || c.closing || c.upgrading) return
|
|
@@ -552,7 +683,7 @@ export async function createXmppServer (options = {}) {
|
|
|
552
683
|
let handle
|
|
553
684
|
try {
|
|
554
685
|
const opened = xmpp.engine.call('xmppWsOpen', [xmpp.handle, JSON.stringify({
|
|
555
|
-
streamId: randomToken(), nonce: randomToken(),
|
|
686
|
+
streamId: randomToken(), nonce: randomToken(), remoteAddress: addressOf(socket),
|
|
556
687
|
path: typeof ws.path === 'string' ? ws.path : '/xmpp-websocket',
|
|
557
688
|
allowedOrigins: Array.isArray(ws.allowedOrigins) ? ws.allowedOrigins : [],
|
|
558
689
|
allowNoOrigin: ws.allowNoOrigin === true,
|
|
@@ -579,15 +710,17 @@ export async function createXmppServer (options = {}) {
|
|
|
579
710
|
},
|
|
580
711
|
deliver (text) {
|
|
581
712
|
const got = wsCall('xmppWsDeliver', [handle, text], new Uint8Array(0))
|
|
582
|
-
if (got.envelope.delivered === true) { if (!socket.destroyed) socket.write(got.bytes); return true }
|
|
713
|
+
if (got.envelope.delivered === true) { if (!socket.destroyed) socket.write(got.bytes); closeIfBacklogged(c); return true }
|
|
583
714
|
return false
|
|
584
715
|
}
|
|
585
716
|
}
|
|
586
717
|
connections.add(c)
|
|
718
|
+
startLoginTimer(c)
|
|
587
719
|
socket.on('data', (chunk) => {
|
|
588
720
|
if (!connections.has(c) || c.closing) return
|
|
589
721
|
let got
|
|
590
722
|
try {
|
|
723
|
+
xmpp.tick()
|
|
591
724
|
got = wsCall('xmppWsFeed', [handle], new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength))
|
|
592
725
|
} catch (error) {
|
|
593
726
|
onError(error)
|
|
@@ -623,29 +756,32 @@ export async function createXmppServer (options = {}) {
|
|
|
623
756
|
const net = await import('node:net')
|
|
624
757
|
wsServer = net.createServer(onWsConnection)
|
|
625
758
|
}
|
|
626
|
-
wsServer.on('connection',
|
|
627
|
-
transports.add(socket)
|
|
628
|
-
socket.once('close', () => transports.delete(socket))
|
|
629
|
-
})
|
|
759
|
+
wsServer.on('connection', admit)
|
|
630
760
|
}
|
|
631
761
|
|
|
632
762
|
let server
|
|
633
763
|
try {
|
|
634
764
|
if (useTls) {
|
|
635
765
|
const tls = await import('node:tls')
|
|
636
|
-
server = tls.createServer({ cert: options.cert, key: options.key }, onConnection)
|
|
766
|
+
server = tls.createServer({ cert: options.cert, key: options.key }, (socket) => onConnection(socket))
|
|
637
767
|
} else {
|
|
638
768
|
const net = await import('node:net')
|
|
639
|
-
server = net.createServer(onConnection)
|
|
769
|
+
server = net.createServer((socket) => onConnection(socket))
|
|
640
770
|
}
|
|
641
771
|
} catch (error) {
|
|
642
772
|
xmpp.close()
|
|
643
773
|
throw error
|
|
644
774
|
}
|
|
645
|
-
server.on('connection',
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
775
|
+
server.on('connection', admit)
|
|
776
|
+
|
|
777
|
+
// XEP-0368 direct TLS beside a STARTTLS listener: the engine takes its
|
|
778
|
+
// answer per session, so this differs only in what `onConnection` passes.
|
|
779
|
+
let directServer = null
|
|
780
|
+
if (startTls && options.directTlsPort !== undefined) {
|
|
781
|
+
const tls = await import('node:tls')
|
|
782
|
+
directServer = tls.createServer({ cert: options.cert, key: options.key }, (socket) => onConnection(socket, true))
|
|
783
|
+
directServer.on('connection', admit)
|
|
784
|
+
}
|
|
649
785
|
|
|
650
786
|
// The harness's mailbox: `<dir>/spool/<bare jid>/<name>` holds one
|
|
651
787
|
// stanza's bytes for that account. Polled for every bound session,
|
|
@@ -693,6 +829,7 @@ export async function createXmppServer (options = {}) {
|
|
|
693
829
|
return {
|
|
694
830
|
server,
|
|
695
831
|
wsServer,
|
|
832
|
+
directServer,
|
|
696
833
|
xmpp,
|
|
697
834
|
/** The live connections' `{bare, full}` pairs, for a host's own view. */
|
|
698
835
|
sessions () {
|
|
@@ -714,6 +851,7 @@ export async function createXmppServer (options = {}) {
|
|
|
714
851
|
try {
|
|
715
852
|
await new Promise(resolve => server.close(resolve))
|
|
716
853
|
if (wsServer !== null) await new Promise(resolve => wsServer.close(resolve))
|
|
854
|
+
if (directServer !== null) await new Promise(resolve => directServer.close(resolve))
|
|
717
855
|
await pollTask
|
|
718
856
|
await rosterWrites
|
|
719
857
|
await stateWrites
|
|
@@ -765,7 +903,20 @@ export async function listen (options = {}) {
|
|
|
765
903
|
const path = typeof options.websocket.path === 'string' ? options.websocket.path : '/xmpp-websocket'
|
|
766
904
|
wsOrigin = `${wsScheme}://${host}:${created.wsServer.address().port}${path}`
|
|
767
905
|
}
|
|
906
|
+
let directOrigin = null
|
|
907
|
+
if (created.directServer !== null) {
|
|
908
|
+
try {
|
|
909
|
+
await new Promise((resolve, reject) => {
|
|
910
|
+
created.directServer.once('error', reject)
|
|
911
|
+
created.directServer.listen(options.directTlsPort, host, resolve)
|
|
912
|
+
})
|
|
913
|
+
} catch (error) {
|
|
914
|
+
await created.close()
|
|
915
|
+
throw error
|
|
916
|
+
}
|
|
917
|
+
directOrigin = `xmpps://${host}:${created.directServer.address().port}`
|
|
918
|
+
}
|
|
768
919
|
const bound = created.server.address()
|
|
769
920
|
const scheme = options.cert !== undefined && options.key !== undefined ? 'xmpps' : 'xmpp'
|
|
770
|
-
return { ...created, port: bound.port, host, origin: `${scheme}://${host}:${bound.port}`, wsOrigin }
|
|
921
|
+
return { ...created, port: bound.port, host, origin: `${scheme}://${host}:${bound.port}`, wsOrigin, directOrigin }
|
|
771
922
|
}
|