fix(websocket): fix multiple websocket connections persisting on forms

This commit is contained in:
yflory 2026-03-11 16:03:48 +01:00
parent 2a3ab9056f
commit 1a541ba706
7 changed files with 153 additions and 97 deletions

View File

@ -5,7 +5,7 @@
const factory = (Sortify, UserObject, ProxyManager,
Migrate, Hash, Util, Constants, Feedback,
Realtime, Messaging, Pinpad, Rpc, Cryptget, Cache,
SF, AccountTS, DriveTS, PadTS, Cursor,
SF, AccountTS, DriveTS, PadTS, Form, Cursor,
Support, Integration, OnlyOffice,
Mailbox, Profile, Team, Messenger, History,
Calendar, BadgeTS, Block, NetConfig,
@ -70,6 +70,9 @@ const factory = (Sortify, UserObject, ProxyManager,
Store.drive = Drive.initAPI({
Store, store, postMessage, broadcast
});
Store.form = Form.init({
Store, store, postMessage, broadcast
});
// Drive clients
var driveEventClients = [];
@ -3057,6 +3060,7 @@ module.exports = factory(
require('./components/account'), // .ts
require('./components/drive'), // .ts
require('./components/pad'), // .ts
require('./components/form'),
require('./modules/cursor'),
require('./modules/support'),
require('./modules/integration'),

View File

@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: 2026 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later
const factory = (Util, Nacl, CPNetflux, Crypto) => {
const Form = {};
const u8_slice = (A, start, end) => {
return new Uint8Array(Array.prototype.slice.call(A, start, end));
};
const checkAnonProof = (proofObj, channel, curvePrivate) => {
const pub = proofObj.key;
const proofTxt = proofObj.proof;
try {
let u8_bundle = Util.decodeBase64(proofTxt);
let u8_nonce = u8_slice(u8_bundle, 0, Nacl.box.nonceLength);
let u8_cipher = u8_slice(u8_bundle, Nacl.box.nonceLength);
let u8_plain = Nacl.box.open(
u8_cipher,
u8_nonce,
Util.decodeBase64(pub),
Util.decodeBase64(curvePrivate)
);
return channel === Util.encodeUTF8(u8_plain);
} catch (e) {
console.error(e);
return false;
}
};
const getResponses = (ctx, data, cb) => {
const { store, Store } = ctx;
const {
cryptoKeys, // encryptor keys
channel, // form responses channel id
edPublic, // my edPublic
validateKey, // form responses secondaryValidateKey
deleteLines,
cantEdit
} = data;
const curvePrivate = cryptoKeys.curvePrivate;
const crypto = Crypto.Mailbox.createEncryptor(cryptoKeys);
let config = {
network: store.network,
channel,
noChainPad: true,
validateKey,
owners: [edPublic],
crypto: crypto,
metadata: {
deleteLines: true
}
};
let cpNf;
let results = {};
config.onError = (info) => {
cb({ error: info.type });
cpNf?.stop();
};
config.onRejected = Store.onRejected;
config.onReady = function () {
cb({ results });
cpNf?.stop();
};
config.onMessage = (msg, peer, vKey, isCp, hash, senderCurve, cfg) => {
let parsed = Util.tryParse(msg);
if (!parsed) { return; }
let uid = parsed._uid || '000';
// If we have a "non-anonymous" answer, it may be the edition of a
// previous anonymous answer. Check if a previous anonymous answer exists
// with the same uid and delete it.
if (parsed._proof) {
const check = checkAnonProof(parsed._proof, channel, curvePrivate);
const theirAnonKey = parsed._proof.key;
if (check && results[theirAnonKey] && results[theirAnonKey][uid]) {
delete results[theirAnonKey][uid];
}
}
parsed._time = cfg && cfg.time;
if (deleteLines) { parsed._hash = hash; }
if (cantEdit && results[senderCurve]
&& results[senderCurve][uid]) { return; }
results[senderCurve] = results[senderCurve] || {};
results[senderCurve][uid] = {
msg: parsed,
hash: hash,
time: cfg && cfg.time
};
};
cpNf = CPNetflux.start(config);
};
Form.init = (config) => {
const { store, Store } = config;
const ctx = { store, Store };
const form = {}
form.getResponses = (cId, data, cb) => {
return getResponses(ctx, data, cb);
};
return form;
};
return Form;
};
module.exports = factory(
require("../../common/common-util"),
require('tweetnacl/nacl-fast'),
require("chainpad-netflux"),
require("chainpad-crypto")
);

View File

@ -85,6 +85,8 @@ const factory = AStore => {
CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin,
GET_SNAPSHOT: Store.getSnapshot,
DELETE_MAILBOX_MESSAGE: Store.deleteMailboxMessage,
// Form
FORM_GET_RESPONSES: Store.form?.getResponses,
// Drive
DRIVE_USEROBJECT: Store.userObjectCommand,
GET_DRIVE: Store.drive.get,

View File

@ -289,6 +289,10 @@ define([
});
});
};
common.getFormResponses = (data, cb) => {
postMessage("FORM_GET_RESPONSES", data, cb);
};
common.muteChannel = function (channel, state, cb) {
var mutedChannels = [];
nThen(function (waitFor) {

File diff suppressed because one or more lines are too long

View File

@ -135,28 +135,6 @@ define([
});
});
});
var u8_slice = function (A, start, end) {
return new Uint8Array(Array.prototype.slice.call(A, start, end));
};
var checkAnonProof = function (proofObj, channel, curvePrivate) {
var pub = proofObj.key;
var proofTxt = proofObj.proof;
try {
var u8_bundle = Util.decodeBase64(proofTxt);
var u8_nonce = u8_slice(u8_bundle, 0, Nacl.box.nonceLength);
var u8_cipher = u8_slice(u8_bundle, Nacl.box.nonceLength);
var u8_plain = Nacl.box.open(
u8_cipher,
u8_nonce,
Util.decodeBase64(pub),
Util.decodeBase64(curvePrivate)
);
return channel === Util.encodeUTF8(u8_plain);
} catch (e) {
console.error(e);
return false;
}
};
sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) {
var formHref = data.href;
var cb = Utils.Util.once(_cb);
@ -178,7 +156,6 @@ define([
Cryptpad.getAccessKeys(w(function (_keys) {
if (!Array.isArray(_keys)) { return; }
accessKeys = _keys;
_keys.some(function (_k) {
if ((personalDrive && !_k.id) || Cryptpad.initialTeam === Number(_k.id)) {
myKeys = _k;
@ -194,14 +171,10 @@ define([
}
myFormKeys = keys;
}));
Cryptpad.makeNetwork(w(function (err, nw) {
network = nw;
}));
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
if (md && md.deleteLines) { deleteLines = true; }
}));
}).nThen(function () {
if (!network) { return void cb({error: "E_CONNECT"}); }
if (myFormKeys.formSeed) {
myFormKeys = Cryptpad.getAnonymousKeys(myFormKeys.formSeed, data.channel, Utils);
}
@ -220,86 +193,33 @@ define([
publicKey = formData?.form_public;
var curvePrivate = privateKey || data.privateKey;
if (!curvePrivate) { return void cb({error: 'EFORBIDDEN'}); }
var crypto = Utils.Crypto.Mailbox.createEncryptor({
var cryptoKeys = {
curvePrivate: curvePrivate,
curvePublic: publicKey || data.publicKey,
validateKey: data.validateKey
});
};
var config = {
network: network,
Cryptpad.getFormResponses({
cryptoKeys,
channel: data.channel,
noChainPad: true,
edPublic: myKeys.edPublic,
validateKey: keys.secondaryValidateKey,
owners: [myKeys.edPublic],
crypto: crypto,
metadata: {
deleteLines: true
}
//Cache: Utils.Cache // TODO enable cache for form responses when the cache stops evicting old answers
};
var results = {};
config.onError = function (info) {
cb({ error: info.type });
};
config.onRejected = function (data, cb) {
if (!Array.isArray(data) || !data.length || data[0].length !== 16) {
return void cb(true);
}
if (!Array.isArray(accessKeys)) { return void cb(true); }
network.historyKeeper = data[0];
nThen(function (waitFor) {
accessKeys.forEach(function (obj) {
Pinpad.create(network, obj, waitFor(function (e) {
if (e) { console.error(e); }
}));
});
}).nThen(function () {
cb();
});
};
config.onReady = function () {
var myKey;
cantEdit: data.cantEdit,
deleteLines
}, (obj) => {
if (obj?.error) { return void cb(obj); }
const results = obj.results;
// If we have submitted an anonymous answer, retrieve it
let myKey;
if (myFormKeys.curvePublic && results[myFormKeys.curvePublic]) {
myKey = myFormKeys.curvePublic;
}
cb({
noDriveAnswered: noDriveAnswered,
myKey: myKey,
results: results
noDriveAnswered, myKey, results
});
network.disconnect();
};
config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) {
var parsed = Utils.Util.tryParse(msg);
if (!parsed) { return; }
var uid = parsed._uid || '000';
// If we have a "non-anonymous" answer, it may be the edition of a
// previous anonymous answer. Check if a previous anonymous answer exists
// with the same uid and delete it.
if (parsed._proof) {
var check = checkAnonProof(parsed._proof, data.channel, curvePrivate);
var theirAnonKey = parsed._proof.key;
if (check && results[theirAnonKey] && results[theirAnonKey][uid]) {
delete results[theirAnonKey][uid];
}
}
parsed._time = cfg && cfg.time;
if (deleteLines) { parsed._hash = hash; }
if (data.cantEdit && results[senderCurve]
&& results[senderCurve][uid]) { return; }
results[senderCurve] = results[senderCurve] || {};
results[senderCurve][uid] = {
msg: parsed,
hash: hash,
time: cfg && cfg.time
};
};
CPNetflux.start(config);
});
});
});
var noDriveSeed = Utils.Hash.createChannelId();

View File

@ -25,6 +25,7 @@ define([
'/common/diffMarked.js',
'/common/sframe-common-codemirror.js',
'/common/text-cursor.js',
'/common/visible.js',
'cm/lib/codemirror',
'/components/chainpad/chainpad.dist.js',
'tui-date-picker',
@ -73,6 +74,7 @@ define([
DiffMd,
SFCodeMirror,
TextCursor,
Visible,
CMeditor,
ChainPad,
DatePicker,
@ -3280,6 +3282,8 @@ define([
h('span.cp-button-name', Messages._getKey('form_results', [l])),
]));
var it = setInterval(function () {
let visible = Visible.currently();
if (!visible) { return; }
sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, obj) {
var answers = obj && obj.results;
var l = getAnswersLength(answers);