implement auto_accept (#91) and manual unpairing via new Edit Paired Devices Dialog and a BrowserTabsConnector

This commit is contained in:
schlagmichdoch 2023-05-04 17:38:51 +02:00
parent 0ac3c5a11f
commit 241ea4f988
11 changed files with 1442 additions and 441 deletions

View file

@ -11,8 +11,7 @@ class ServerConnection {
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
Events.on('room-secret-deleted', e => this.send({ type: 'room-secret-deleted', roomSecret: e.detail}));
Events.on('room-secrets-cleared', e => this.send({ type: 'room-secrets-cleared', roomSecrets: e.detail}));
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
@ -65,13 +64,13 @@ class ServerConnection {
_onMessage(msg) {
msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg);
if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers':
Events.fire('peers', msg);
this._onPeers(msg);
break;
case 'peer-joined':
Events.fire('peer-joined', msg);
@ -107,18 +106,38 @@ class ServerConnection {
Events.fire('secret-room-deleted', msg.roomSecret);
break;
default:
console.error('WS: unknown message type', msg);
console.error('WS receive: unknown message type', msg);
}
}
send(msg) {
if (!this._isConnected()) return;
if (msg.type !== 'pong') console.log("WS send:", msg)
this._socket.send(JSON.stringify(msg));
}
_onPeers(msg) {
Events.fire('peers', msg);
if (msg.roomType === "ip" && msg.peers.length === 0) {
BrowserTabsConnector.removePeerIdsFromLocalStorage();
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (peerId) return;
console.log("successfully added peerId from localStorage");
});
}
}
_onDisplayName(msg) {
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
sessionStorage.setItem("peerId", msg.message.peerId);
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
// Add peerId to localStorage to mark it on other PairDrop tabs on the same browser
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (peerId) return;
console.log("successfully added peerId from localStorage");
});
Events.fire('display-name', msg);
}
@ -138,13 +157,19 @@ class ServerConnection {
_disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) {
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
const peerId = sessionStorage.getItem("peerId");
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
console.log("successfully removed peerId from localStorage");
});
if (!this._socket) return;
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
_onDisconnect() {
@ -183,11 +208,16 @@ class Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
this._server = serverConnection;
this._isCaller = !!peerId;
this._peerId = peerId;
this._roomType = roomType;
this._roomSecret = roomSecret;
this._updateRoomSecret(roomSecret);
this._filesQueue = [];
this._busy = false;
// evaluate auto accept
this._evaluateAutoAccept();
}
sendJSON(message) {
@ -198,12 +228,48 @@ class Peer {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) {
return {
name: file.name,
mime: file.type,
size: file.size,
};
_updateRoomSecret(roomSecret) {
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> abort
if (BrowserTabsConnector.peerIsSameBrowser(this._peerId)) {
this._roomSecret = "";
return;
}
if (this._roomSecret && this._roomSecret !== roomSecret) {
// remove old roomSecrets to prevent multiple pairings with same peer
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
})
}
this._roomSecret = roomSecret;
if (this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
// increase security by increasing roomSecret length
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._roomSecret);
}
}
_evaluateAutoAccept() {
if (!this._roomSecret) {
this._setAutoAccept(false);
return;
}
PersistentStorage.getRoomSecretEntry(this._roomSecret)
.then(roomSecretEntry => {
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
this._setAutoAccept(autoAccept);
})
.catch(_ => {
this._setAutoAccept(false);
});
}
_setAutoAccept(autoAccept) {
this._autoAccept = autoAccept;
}
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
@ -248,7 +314,11 @@ class Peer {
let imagesOnly = true
for (let i=0; i<files.length; i++) {
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})
header.push(await this.createHeader(files[i]));
header.push({
name: files[i].name,
mime: files[i].type,
size: files[i].size
});
totalSize += files[i].size;
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
}
@ -360,7 +430,7 @@ class Peer {
_onFilesTransferRequest(request) {
if (this._requestPending) {
// Only accept one request at a time
// Only accept one request at a time per peer
this.sendJSON({type: 'files-transfer-response', accepted: false});
return;
}
@ -372,6 +442,14 @@ class Peer {
}
this._requestPending = request;
if (this._autoAccept) {
// auto accept if set via Edit Paired Devices Dialog
this._respondToFileTransferRequest(true);
return;
}
// default behavior: show user transfer request
Events.fire('files-transfer-request', {
request: request,
peerId: this._peerId
@ -497,9 +575,16 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
const displayNameHasChanged = this._displayName !== message.displayName
if (message.displayName && displayNameHasChanged) {
this._displayName = message.displayName;
}
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
if (!displayNameHasChanged) return;
Events.fire('notify-peer-display-name-changed', this._peerId);
}
}
@ -508,22 +593,21 @@ class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller
if (!this._isCaller) return; // we will listen for a caller
this._connect(peerId, true);
}
_connect(peerId, isCaller) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller);
_connect(peerId) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId);
if (isCaller) {
if (this._isCaller) {
this._openChannel();
} else {
this._conn.ondatachannel = e => this._onChannelOpened(e);
}
}
_openConnection(peerId, isCaller) {
this._isCaller = isCaller;
_openConnection(peerId) {
this._peerId = peerId;
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
@ -688,7 +772,11 @@ class RTCPeer extends Peer {
refresh() {
// check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return;
this._connect(this._peerId, this._isCaller);
// only reconnect if peer is caller
if (!this._isCaller) return;
this._connect(this._peerId);
}
_isConnected() {
@ -716,32 +804,59 @@ class PeersManager {
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
}
_onMessage(message) {
// if different roomType -> abort
if (this.peers[message.sender.id] && this.peers[message.sender.id]._roomType !== message.roomType) return;
if (!this.peers[message.sender.id]) {
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
}
this.peers[message.sender.id].onServerMessage(message);
const peerId = message.sender.id;
this.peers[peerId].onServerMessage(message);
}
_onPeers(msg) {
msg.peers.forEach(peer => {
if (this.peers[peer.id]) {
// if different roomType -> abort
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) return;
this.peers[peer.id].refresh();
return;
_refreshExistingPeer(peerId, roomType, roomSecret) {
const peer = this.peers[peerId];
if (peer) {
const roomTypeIsSecret = roomType === "secret";
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
if (roomTypeIsSecret && roomSecretsDiffer) {
peer._updateRoomSecret(roomSecret);
peer._evaluateAutoAccept();
return true;
}
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
const roomTypesDiffer = peer._roomType !== roomType;
// if roomTypes differ peer is already connected -> abort
if (roomTypesDiffer) return true;
peer.refresh();
return true;
}
// peer does not yet exist: return false
return false;
}
_onPeerJoined(message) {
if (this._refreshExistingPeer(message.peer.id, message.roomType, message.roomSecret)) return;
this.peers[message.peer.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
}
_onPeers(message) {
message.peers.forEach(messagePeer => {
if (this._refreshExistingPeer(messagePeer.id, message.roomType, message.roomSecret)) return;
this.peers[messagePeer.id] = new RTCPeer(this._server, messagePeer.id, message.roomType, message.roomSecret);
})
}
@ -769,10 +884,10 @@ class PeersManager {
this.peers[message.to].sendText(message.text);
}
_onPeerLeft(msg) {
if (msg.disconnect === true) {
// if user actively disconnected from PairDrop disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId);
_onPeerLeft(message) {
if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', message.peerId);
}
}
@ -792,7 +907,7 @@ class PeersManager {
_onSecretRoomDeleted(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) {
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
this._onPeerDisconnected(peerId);
}
}
@ -813,6 +928,24 @@ class PeersManager {
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
// if the displayName has not been changed (yet) set the displayName to the original displayName
if (!this._displayName) this._displayName = displayName;
}
_onAutoAcceptUpdated(roomSecret, autoAccept) {
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
if (!peerId) return;
this.peers[peerId]._setAutoAccept(autoAccept);
}
_getPeerIdFromRoomSecret(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) {
return peer._peerId;
}
}
return false;
}
}

View file

@ -50,7 +50,7 @@ class PeersUI {
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
// Load saved display name on page load
this._getSavedDisplayName().then(displayName => {
@ -87,26 +87,31 @@ class PeersUI {
if (newDisplayName === savedDisplayName) return;
if (newDisplayName) {
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
Events.fire('notify-user', 'Device name is changed permanently.');
}).catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.");
localStorage.setItem('editedDisplayName', newDisplayName);
Events.fire('notify-user', 'Device name is changed only for this session.');
}).finally(_ => {
Events.fire('self-display-name-changed', newDisplayName);
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
});
PersistentStorage.set('editedDisplayName', newDisplayName)
.then(_ => {
Events.fire('notify-user', 'Device name is changed permanently.');
})
.catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.");
localStorage.setItem('editedDisplayName', newDisplayName);
Events.fire('notify-user', 'Device name is changed only for this session.');
})
.finally(_ => {
Events.fire('self-display-name-changed', newDisplayName);
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
});
} else {
PersistentStorage.delete('editedDisplayName').catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.")
localStorage.removeItem('editedDisplayName');
Events.fire('notify-user', 'Random Display name is used again.');
}).finally(_ => {
Events.fire('notify-user', 'Device name is randomly generated again.');
Events.fire('self-display-name-changed', '');
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
});
PersistentStorage.delete('editedDisplayName')
.catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.")
localStorage.removeItem('editedDisplayName');
Events.fire('notify-user', 'Random Display name is used again.');
})
.finally(_ => {
Events.fire('notify-user', 'Device name is randomly generated again.');
Events.fire('self-display-name-changed', '');
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
});
}
}
@ -129,6 +134,12 @@ class PeersUI {
this.peers[peerId].name.displayName = displayName;
const peerIdNode = $(peerId);
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
this._redrawPeerRoomTypes(peerId);
}
_onPeerDisplayNameChanged(e) {
if (!e.detail.displayName) return;
this._changePeerDisplayName(e.detail.peerId, e.detail.displayName);
}
_onKeyDown(e) {
@ -142,26 +153,43 @@ class PeersUI {
}
_joinPeer(peer, roomType, roomSecret) {
const existingPeer = this.peers[peer.id];
if (existingPeer) {
// peer already exists. Abort but add roomType to GUI and update roomSecret
// skip if peer is a tab on the same browser
if (!existingPeer.sameBrowser()) {
// add roomType to PeerUI
if (!existingPeer.roomTypes.includes(roomType)) {
existingPeer.roomTypes.push(roomType);
}
this._redrawPeerRoomTypes(peer.id);
if (roomType === "secret") existingPeer.roomSecret = roomSecret;
}
return;
}
peer.sameBrowser = _ => BrowserTabsConnector.peerIsSameBrowser(peer.id);
peer.roomTypes = [roomType];
peer.roomSecret = roomSecret;
if (this.peers[peer.id]) {
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
this._redrawPeer(this.peers[peer.id]);
return; // peer already exists
}
this.peers[peer.id] = peer;
}
_onPeerConnected(peerId, connectionHash) {
if(this.peers[peerId] && !$(peerId))
new PeerUI(this.peers[peerId], connectionHash);
if (!this.peers[peerId] || $(peerId)) return;
const peer = this.peers[peerId];
new PeerUI(peer, connectionHash);
}
_redrawPeer(peer) {
const peerNode = $(peer.id);
_redrawPeerRoomTypes(peerId) {
const peer = this.peers[peerId]
const peerNode = $(peerId);
if (!peerNode) return;
peerNode.classList.remove('type-ip', 'type-secret');
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
if (!peer.sameBrowser()) {
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
}
}
evaluateOverflowing() {
@ -187,7 +215,17 @@ class PeersUI {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer.roomSecret === roomSecret) {
let index = peer.roomTypes.indexOf('secret');
peer.roomTypes.splice(index, 1);
peer.roomSecret = "";
if (peer.roomTypes.length) {
this._redrawPeerRoomTypes(peerId)
return;
}
this._onPeerDisconnected(peerId);
return;
}
}
}
@ -364,8 +402,8 @@ class PeerUI {
this.$el = document.createElement('x-peer');
this.$el.id = this._peer.id;
this.$el.ui = this;
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
this.$el.classList.add('center');
if (!this._peer.sameBrowser()) this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
this.html();
this._callbackInput = e => this._onFilesSelected(e)
@ -862,8 +900,12 @@ class ReceiveRequestDialog extends ReceiveDialog {
}
hide() {
this.$previewBox.innerHTML = '';
// clear previewBox after dialog is closed
setTimeout(_ => this.$previewBox.innerHTML = '', 300);
super.hide();
// show next request
setTimeout(_ => this._dequeueRequests(), 500);
}
}
@ -876,7 +918,7 @@ class PairDeviceDialog extends Dialog {
this.$roomKey = this.$el.querySelector('#room-key');
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
this.$pairDeviceBtn = $('pair-device');
this.$clearSecretsBtn = $('clear-pair-devices');
this.$editPairedDevicesBtn = $('edit-paired-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
this.$createJoinForm = this.$el.querySelector('form');
@ -896,7 +938,7 @@ class PairDeviceDialog extends Dialog {
Events.on('pair-device-joined', e => this._pairDeviceJoined(e.detail.peerId, e.detail.roomSecret));
Events.on('pair-device-join-key-invalid', _ => this._pairDeviceJoinKeyInvalid());
Events.on('pair-device-canceled', e => this._pairDeviceCanceled(e.detail));
Events.on('clear-room-secrets', e => this._onClearRoomSecrets(e.detail))
Events.on('evaluate-number-room-secrets', _ => this._evaluateNumberRoomSecrets())
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
this.$el.addEventListener('paste', e => this._onPaste(e));
@ -978,7 +1020,7 @@ class PairDeviceDialog extends Dialog {
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
this._evaluateNumberRoomSecrets();
}).catch(_ => PersistentStorage.logBrowserNotCapable());
});
}
_pairDeviceInitiate() {
@ -1027,19 +1069,34 @@ class PairDeviceDialog extends Dialog {
_pairDeviceJoined(peerId, roomSecret) {
this.hide();
PersistentStorage.addRoomSecret(roomSecret).then(_ => {
Events.fire('notify-user', 'Devices paired successfully.');
const oldRoomSecret = $(peerId).ui.roomSecret;
if (oldRoomSecret) PersistentStorage.deleteRoomSecret(oldRoomSecret);
$(peerId).ui.roomSecret = roomSecret;
this._evaluateNumberRoomSecrets();
}).finally(_ => {
// skip adding to IndexedDB if peer is another tab on the same browser
if (BrowserTabsConnector.peerIsSameBrowser(peerId)) {
this._cleanUp();
})
.catch(_ => {
Events.fire('notify-user', 'Paired devices are not persistent.');
PersistentStorage.logBrowserNotCapable();
});
Events.fire('notify-user', 'Pairing of two browser tabs is not possible.');
return;
}
// if devices are paired that are already connected we must save the names at this point
const $peer = $(peerId);
let displayName, deviceName;
if ($peer) {
displayName = $peer.ui._peer.name.displayName;
deviceName = $peer.ui._peer.name.deviceName;
}
PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName)
.then(_ => {
Events.fire('notify-user', 'Devices paired successfully.');
this._evaluateNumberRoomSecrets();
})
.finally(_ => {
this._cleanUp();
})
.catch(_ => {
Events.fire('notify-user', 'Paired devices are not persistent.');
PersistentStorage.logBrowserNotCapable();
});
}
_pairDeviceJoinKeyInvalid() {
@ -1064,56 +1121,120 @@ class PairDeviceDialog extends Dialog {
this.$inputRoomKeyChars.forEach(el => el.setAttribute("disabled", ""));
}
_onClearRoomSecrets() {
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets-cleared', roomSecrets);
PersistentStorage.clearRoomSecrets().finally(_ => {
Events.fire('notify-user', 'All Devices unpaired.')
this._evaluateNumberRoomSecrets();
})
}).catch(_ => PersistentStorage.logBrowserNotCapable());
}
_onSecretRoomDeleted(roomSecret) {
PersistentStorage.deleteRoomSecret(roomSecret).then(_ => {
this._evaluateNumberRoomSecrets();
}).catch(e => console.error(e));
});
}
_evaluateNumberRoomSecrets() {
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
if (roomSecrets.length > 0) {
this.$clearSecretsBtn.removeAttribute('hidden');
this.$editPairedDevicesBtn.removeAttribute('hidden');
this.$footerInstructionsPairedDevices.removeAttribute('hidden');
} else {
this.$clearSecretsBtn.setAttribute('hidden', '');
this.$editPairedDevicesBtn.setAttribute('hidden', '');
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
}
Events.fire('bg-resize');
}).catch(_ => PersistentStorage.logBrowserNotCapable());
});
}
}
class ClearDevicesDialog extends Dialog {
class EditPairedDevicesDialog extends Dialog {
constructor() {
super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
super('edit-paired-devices-dialog');
this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper');
$('edit-paired-devices').addEventListener('click', _ => this._onEditPairedDevices());
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
Events.on('keydown', e => this._onKeyDown(e));
}
_onClearPairDevices() {
this.show();
_onKeyDown(e) {
if (this.$el.attributes["show"] && e.code === "Escape") {
this.hide();
}
}
_onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
async _initDOM() {
const roomSecretsEntries = await PersistentStorage.getAllRoomSecretEntries();
roomSecretsEntries.forEach(roomSecretsEntry => {
let $pairedDevice = document.createElement('div');
$pairedDevice.classList = ["paired-device"];
$pairedDevice.innerHTML = `
<div class="display-name">
<span>${roomSecretsEntry.display_name}</span>
</div>
<div class="device-name">
<span>${roomSecretsEntry.device_name}</span>
</div>
<div class="button-wrapper">
<label class="auto-accept">auto-accept
<input type="checkbox" ${roomSecretsEntry.auto_accept ? "checked" : ""}>
</label>
<button class="button" type="button">unpair</button>
</div>`
$pairedDevice.querySelector('input[type="checkbox"]').addEventListener('click', e => {
PersistentStorage.updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked).then(roomSecretsEntry => {
Events.fire('auto-accept-updated', {
'roomSecret': roomSecretsEntry.entry.secret,
'autoAccept': e.target.checked
});
});
});
$pairedDevice.querySelector('button').addEventListener('click', e => {
PersistentStorage.deleteRoomSecret(roomSecretsEntry.secret).then(roomSecret => {
Events.fire('room-secrets-deleted', [roomSecret]);
Events.fire('evaluate-number-room-secrets');
e.target.parentNode.parentNode.remove();
});
})
this.$pairedDevicesWrapper.appendChild($pairedDevice)
})
}
hide() {
super.hide();
setTimeout(_ => {
this.$pairedDevicesWrapper.innerHTML = ""
}, 300);
}
_onEditPairedDevices() {
this._initDOM().then(_ => this.show());
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets');
this.hide();
PersistentStorage.getAllRoomSecrets()
.then(roomSecrets => {
PersistentStorage.clearRoomSecrets().finally(_ => {
Events.fire('room-secrets-deleted', roomSecrets);
Events.fire('evaluate-number-room-secrets');
Events.fire('notify-user', 'All Devices unpaired.');
this.hide();
})
});
}
_onPeerDisplayNameChanged(e) {
const peerId = e.detail.peerId;
const peerNode = $(peerId);
if (!peerNode) return;
const peer = peerNode.ui._peer;
if (!peer.roomSecret) return;
PersistentStorage.updateRoomSecretNames(peer.roomSecret, peer.name.displayName, peer.name.deviceName).then(roomSecretEntry => {
console.log(`Successfully updated DisplayName and DeviceName for roomSecretEntry ${roomSecretEntry.key}`);
})
}
}
@ -1682,7 +1803,7 @@ class PersistentStorage {
PersistentStorage.logBrowserNotCapable();
return;
}
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3);
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 4);
DBOpenRequest.onerror = (e) => {
PersistentStorage.logBrowserNotCapable();
console.log('Error initializing database: ');
@ -1693,27 +1814,32 @@ class PersistentStorage {
};
DBOpenRequest.onupgradeneeded = (e) => {
const db = e.target.result;
const txn = e.target.transaction;
db.onerror = e => console.log('Error loading database: ' + e);
try {
console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
if (e.oldVersion === 0) {
// initiate v1
db.createObjectStore('keyval');
} catch (error) {
console.log("Object store named 'keyval' already exists")
let roomSecretsObjectStore1 = db.createObjectStore('room_secrets', {autoIncrement: true});
roomSecretsObjectStore1.createIndex('secret', 'secret', { unique: true });
}
try {
const roomSecretsObjectStore = db.createObjectStore('room_secrets', {autoIncrement: true});
roomSecretsObjectStore.createIndex('secret', 'secret', { unique: true });
} catch (error) {
console.log("Object store named 'room_secrets' already exists")
if (e.oldVersion <= 1) {
// migrate to v2
db.createObjectStore('share_target_files');
}
try {
if (db.objectStoreNames.contains('share_target_files')) {
db.deleteObjectStore('share_target_files');
}
if (e.oldVersion <= 2) {
// migrate to v3
db.deleteObjectStore('share_target_files');
db.createObjectStore('share_target_files', {autoIncrement: true});
} catch (error) {
console.log("Object store named 'share_target_files' already exists")
}
if (e.oldVersion <= 3) {
// migrate to v4
let roomSecretsObjectStore4 = txn.objectStore('room_secrets');
roomSecretsObjectStore4.createIndex('display_name', 'display_name');
roomSecretsObjectStore4.createIndex('auto_accept', 'auto_accept');
}
}
}
@ -1746,7 +1872,7 @@ class PersistentStorage {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('keyval', 'readwrite');
const transaction = db.transaction('keyval', 'readonly');
const objectStore = transaction.objectStore('keyval');
const objectStoreRequest = objectStore.get(key);
objectStoreRequest.onsuccess = _ => {
@ -1779,16 +1905,21 @@ class PersistentStorage {
})
}
static addRoomSecret(roomSecret) {
static addRoomSecret(roomSecret, displayName, deviceName) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.add({'secret': roomSecret});
objectStoreRequest.onsuccess = _ => {
console.log(`Request successful. RoomSecret added: ${roomSecret}`);
const objectStoreRequest = objectStore.add({
'secret': roomSecret,
'display_name': displayName,
'device_name': deviceName,
'auto_accept': false
});
objectStoreRequest.onsuccess = e => {
console.log(`Request successful. RoomSecret added: ${e.target.result}`);
resolve();
}
}
@ -1798,21 +1929,26 @@ class PersistentStorage {
})
}
static getAllRoomSecrets() {
static async getAllRoomSecrets() {
const roomSecrets = await this.getAllRoomSecretEntries();
let secrets = [];
for (let i=0; i<roomSecrets.length; i++) {
secrets.push(roomSecrets[i].secret);
}
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
return(secrets);
}
static getAllRoomSecretEntries() {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const transaction = db.transaction('room_secrets', 'readonly');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.getAll();
objectStoreRequest.onsuccess = e => {
let secrets = [];
for (let i=0; i<e.target.result.length; i++) {
secrets.push(e.target.result[i].secret);
}
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
resolve(secrets);
resolve(e.target.result);
}
}
DBOpenRequest.onerror = (e) => {
@ -1821,24 +1957,59 @@ class PersistentStorage {
});
}
static deleteRoomSecret(room_secret) {
static getRoomSecretEntry(roomSecret) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readonly');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
objectStoreRequestKey.onsuccess = e => {
const key = e.target.result;
if (!key) {
console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const objectStoreRequestRetrieval = objectStore.get(key);
objectStoreRequestRetrieval.onsuccess = e => {
console.log(`Request successful. Retrieved entry for room_secret: ${key}`);
resolve({
"entry": e.target.result,
"key": key
});
}
objectStoreRequestRetrieval.onerror = (e) => {
reject(e);
}
};
}
DBOpenRequest.onerror = (e) => {
reject(e);
}
});
}
static deleteRoomSecret(roomSecret) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequestKey = objectStore.index("secret").getKey(room_secret);
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
objectStoreRequestKey.onsuccess = e => {
if (!e.target.result) {
console.log(`Nothing to delete. room_secret not existing: ${room_secret}`);
console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const objectStoreRequestDeletion = objectStore.delete(e.target.result);
const key = e.target.result;
const objectStoreRequestDeletion = objectStore.delete(key);
objectStoreRequestDeletion.onsuccess = _ => {
console.log(`Request successful. Deleted room_secret: ${room_secret}`);
resolve();
console.log(`Request successful. Deleted room_secret: ${key}`);
resolve(roomSecret);
}
objectStoreRequestDeletion.onerror = (e) => {
reject(e);
@ -1869,22 +2040,108 @@ class PersistentStorage {
}
})
}
static updateRoomSecretNames(roomSecret, displayName, deviceName) {
return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName);
}
static updateRoomSecretAutoAccept(roomSecret, autoAccept) {
return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept);
}
static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
this.getRoomSecretEntry(roomSecret)
.then(roomSecretEntry => {
if (!roomSecretEntry) {
resolve(false);
return;
}
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
// Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers
const updatedRoomSecretEntry = {
'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret,
'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name,
'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name,
'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
};
const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key);
objectStoreRequestUpdate.onsuccess = e => {
console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
resolve({
"entry": updatedRoomSecretEntry,
"key": roomSecretEntry.key
});
}
objectStoreRequestUpdate.onerror = (e) => {
reject(e);
}
})
.catch(e => reject(e));
};
DBOpenRequest.onerror = e => reject(e);
})
}
}
class Broadcast {
class BrowserTabsConnector {
constructor() {
this.bc = new BroadcastChannel('pairdrop');
this.bc.addEventListener('message', e => this._onMessage(e));
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
Events.on('broadcast-send', e => this._broadcastSend(e.detail));
}
_broadcastMessage(message) {
_broadcastSend(message) {
this.bc.postMessage(message);
}
_onMessage(e) {
console.log('Broadcast message received:', e.data)
Events.fire(e.data.type, e.data.detail);
console.log('Broadcast:', e.data)
switch (e.data.type) {
case 'self-display-name-changed':
Events.fire('self-display-name-changed', e.data.detail);
break;
}
}
static peerIsSameBrowser(peerId) {
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
return peerIdsBrowser
? peerIdsBrowser.indexOf(peerId) !== -1
: false;
}
static async addPeerIdToLocalStorage() {
const peerId = sessionStorage.getItem("peerId");
if (!peerId) return false;
let peerIdsBrowser = [];
let peerIdsBrowserOld = JSON.parse(localStorage.getItem("peerIdsBrowser"));
if (peerIdsBrowserOld) peerIdsBrowser.push(...peerIdsBrowserOld);
peerIdsBrowser.push(peerId);
peerIdsBrowser = peerIdsBrowser.filter(onlyUnique);
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
return peerId;
}
static async removePeerIdFromLocalStorage(peerId) {
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
const index = peerIdsBrowser.indexOf(peerId);
peerIdsBrowser.splice(index, 1);
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
return peerId;
}
static removePeerIdsFromLocalStorage() {
localStorage.removeItem("peerIdsBrowser");
}
}
@ -1899,7 +2156,7 @@ class PairDrop {
const sendTextDialog = new SendTextDialog();
const receiveTextDialog = new ReceiveTextDialog();
const pairDeviceDialog = new PairDeviceDialog();
const clearDevicesDialog = new ClearDevicesDialog();
const clearDevicesDialog = new EditPairedDevicesDialog();
const base64ZipDialog = new Base64ZipDialog();
const toast = new Toast();
const notifications = new Notifications();
@ -1907,7 +2164,7 @@ class PairDrop {
const webShareTargetUI = new WebShareTargetUI();
const webFileHandlersUI = new WebFileHandlersUI();
const noSleepUI = new NoSleepUI();
const broadCast = new Broadcast();
const broadCast = new BrowserTabsConnector();
});
}
}

View file

@ -398,3 +398,7 @@ const cyrb53 = function(str, seed = 0) {
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
function onlyUnique (value, index, array) {
return array.indexOf(value) === index;
}