Decrease redundancy by changing the way the websocket fallback is included; Adding new env var SIGNALING_SERVER to host client files but use another server for signaling.

This commit is contained in:
schlagmichdoch 2023-11-08 20:31:57 +01:00
parent cb72edef20
commit 3439e7f6d4
62 changed files with 439 additions and 10101 deletions

View file

@ -1,24 +1,3 @@
window.URL = window.URL || window.webkitURL;
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
if (!window.isRtcSupported) alert("WebRTC must be enabled for PairDrop to work");
window.hiddenProperty = 'hidden' in document
? 'hidden'
: 'webkitHidden' in document
? 'webkitHidden'
: 'mozHidden' in document
? 'mozHidden'
: null;
window.visibilityChangeEvent = 'visibilitychange' in document
? 'visibilitychange'
: 'webkitvisibilitychange' in document
? 'webkitvisibilitychange'
: 'mozvisibilitychange' in document
? 'mozvisibilitychange'
: null;
class ServerConnection {
constructor() {
@ -44,7 +23,33 @@ class ServerConnection {
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
Events.on('online', _ => this._connect());
this._connect();
this._getConfig().then(() => this._connect());
}
_getConfig() {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', 'config', true);
xhr.addEventListener("load", () => {
if (xhr.status === 200) {
// Config received
let config = JSON.parse(xhr.responseText);
this._config = config;
Events.fire('config', config);
resolve()
} else if (xhr.status !== 200) {
// Handle errors
console.error('Error:', xhr.status, xhr.statusText);
reject();
}
});
xhr.send();
})
}
_setWsConfig(wsConfig) {
this._wsConfig = wsConfig;
Events.fire('ws-config', wsConfig);
}
_connect() {
@ -111,16 +116,12 @@ class ServerConnection {
this.send({ type: 'leave-public-room' });
}
_setRtcConfig(config) {
window.rtcConfig = config;
}
_onMessage(msg) {
msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
case 'ws-config':
this._setWsConfig(msg.wsConfig);
break;
case 'peers':
this._onPeers(msg);
@ -170,6 +171,25 @@ class ServerConnection {
case 'public-room-left':
Events.fire('public-room-left');
break;
case 'request':
case 'header':
case 'partition':
case 'partition-received':
case 'progress':
case 'files-transfer-response':
case 'file-transfer-complete':
case 'message-transfer-complete':
case 'text':
case 'display-name-changed':
case 'ws-chunk':
// ws-fallback
if (this._wsConfig.wsFallback) {
Events.fire('ws-relay', JSON.stringify(msg));
}
else {
console.log("WS receive: message type is for websocket fallback only but websocket fallback is not activated on this instance.")
}
break;
default:
console.error('WS receive: unknown message type', msg);
}
@ -209,17 +229,24 @@ class ServerConnection {
}
_endpoint() {
// hack to detect if deployment or development environment
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
// Check whether the instance specifies another signaling server otherwise use the current instance for signaling
let wsServerDomain = this._config.signalingServer
? this._config.signalingServer
: location.host + location.pathname;
let wsUrl = new URL(protocol + '://' + wsServerDomain + 'server');
wsUrl.searchParams.append('webrtc_supported', window.isRtcSupported ? 'true' : 'false');
const peerId = sessionStorage.getItem('peer_id');
const peerIdHash = sessionStorage.getItem('peer_id_hash');
if (peerId && peerIdHash) {
ws_url.searchParams.append('peer_id', peerId);
ws_url.searchParams.append('peer_id_hash', peerIdHash);
wsUrl.searchParams.append('peer_id', peerId);
wsUrl.searchParams.append('peer_id_hash', peerIdHash);
}
return ws_url.toString();
return wsUrl.toString();
}
_disconnect() {
@ -298,6 +325,9 @@ class Peer {
this._send(JSON.stringify(message));
}
// Is overwritten in expanding classes
_send(message) {}
sendDisplayName(displayName) {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
@ -314,14 +344,24 @@ class Peer {
return this._roomIds['secret'];
}
_regenerationOfPairSecretNeeded() {
return this._getPairSecret() && this._getPairSecret().length !== 256
}
_getRoomTypes() {
return Object.keys(this._roomIds);
}
_updateRoomIds(roomType, roomId) {
const roomTypeIsSecret = roomType === "secret";
const roomIdIsNotPairSecret = this._getPairSecret() !== roomId;
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> do not delete duplicates and do not regenerate room secrets
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret() !== roomId) {
if (!this._isSameBrowser()
&& roomTypeIsSecret
&& this._isPaired()
&& roomIdIsNotPairSecret) {
// multiple roomSecrets with same peer -> delete old roomSecret
PersistentStorage
.deleteRoomSecret(this._getPairSecret())
@ -332,8 +372,13 @@ class Peer {
this._roomIds[roomType] = roomId;
if (!this._isSameBrowser() && roomType === "secret" && this._isPaired() && this._getPairSecret().length !== 256 && this._isCaller) {
// increase security by initiating the increase of the roomSecret length from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
if (!this._isSameBrowser()
&& roomTypeIsSecret
&& this._isPaired()
&& this._regenerationOfPairSecretNeeded()
&& this._isCaller) {
// increase security by initiating the increase of the roomSecret length
// from 64 chars (<v1.7.0) to 256 chars (v1.7.0+)
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._getPairSecret());
}
@ -698,9 +743,12 @@ class Peer {
class RTCPeer extends Peer {
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
constructor(serverConnection, isCaller, peerId, roomType, roomId, rtcConfig) {
super(serverConnection, isCaller, peerId, roomType, roomId);
this.rtcSupported = true;
this.rtcConfig = rtcConfig
if (!this._isCaller) return; // we will listen for a caller
this._connect();
}
@ -717,7 +765,7 @@ class RTCPeer extends Peer {
}
_openConnection() {
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn = new RTCPeerConnection(this.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
@ -910,6 +958,48 @@ class RTCPeer extends Peer {
}
}
class WSPeer extends Peer {
constructor(serverConnection, isCaller, peerId, roomType, roomId) {
super(serverConnection, isCaller, peerId, roomType, roomId);
this.rtcSupported = false;
if (!this._isCaller) return; // we will listen for a caller
this._sendSignal();
}
_send(chunk) {
this.sendJSON({
type: 'ws-chunk',
chunk: arrayBufferToBase64(chunk)
});
}
sendJSON(message) {
message.to = this._peerId;
message.roomType = this._getRoomTypes()[0];
message.roomId = this._roomIds[this._getRoomTypes()[0]];
this._server.send(message);
}
_sendSignal(connected = false) {
this.sendJSON({type: 'signal', connected: connected});
}
onServerMessage(message) {
this._peerId = message.sender.id;
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (message.connected) return;
this._sendSignal(true);
}
getConnectionHash() {
// Todo: implement SubtleCrypto asymmetric encryption and create connectionHash from public keys
return "";
}
}
class PeersManager {
constructor(serverConnection) {
@ -937,6 +1027,13 @@ class PeersManager {
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
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));
Events.on('ws-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail));
Events.on('ws-config', e => this._onWsConfig(e.detail));
}
_onWsConfig(wsConfig) {
this._wsConfig = wsConfig;
}
_onMessage(message) {
@ -944,9 +1041,10 @@ class PeersManager {
this.peers[peerId].onServerMessage(message);
}
_refreshPeer(peer, roomType, roomId) {
if (!peer) return false;
_refreshPeer(peerId, roomType, roomId) {
if (!this._peerExists(peerId)) return false;
const peer = this.peers[peerId];
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
const roomIdsDiffer = peer._roomIds[roomType] !== roomId;
@ -964,26 +1062,42 @@ class PeersManager {
return true;
}
_createOrRefreshPeer(isCaller, peerId, roomType, roomId) {
const peer = this.peers[peerId];
if (peer) {
this._refreshPeer(peer, roomType, roomId);
_createOrRefreshPeer(isCaller, peerId, roomType, roomId, rtcSupported) {
if (this._peerExists(peerId)) {
this._refreshPeer(peerId, roomType, roomId);
return;
}
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId);
if (window.isRtcSupported && rtcSupported) {
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId, this._wsConfig.rtcConfig);
}
else if (this._wsConfig.wsFallback) {
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomId);
}
else {
console.warn("Websocket fallback is not activated on this instance.\n" +
"Activate WebRTC in this browser or ask the admin of this instance to activate the websocket fallback.")
}
}
_onPeerJoined(message) {
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId);
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomId, message.peer.rtcSupported);
}
_onPeers(message) {
message.peers.forEach(peer => {
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId);
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomId, peer.rtcSupported);
})
}
_onWsRelay(message) {
if (!this._wsConfig.wsFallback) return;
const messageJSON = JSON.parse(message);
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
this.peers[messageJSON.sender.id]._onMessage(message);
}
_onRespondToFileTransferRequest(detail) {
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
}
@ -1009,6 +1123,9 @@ class PeersManager {
}
_onPeerLeft(message) {
if (this._peerExists(message.peerId) && this._webRtcSupported(message.peerId)) {
console.log('WSPeer left:', message.peerId);
}
if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);
@ -1029,6 +1146,24 @@ class PeersManager {
this._notifyPeerDisplayNameChanged(peerId);
}
_peerExists(peerId) {
return !!this.peers[peerId];
}
_webRtcSupported(peerId) {
return this.peers[peerId].rtcSupported
}
_onWsDisconnected() {
if (!this._wsConfig || !this._wsConfig.wsFallback) return;
for (const peerId in this.peers) {
if (!this._webRtcSupported(peerId)) {
Events.fire('peer-disconnected', peerId);
}
}
}
_onPeerDisconnected(peerId) {
const peer = this.peers[peerId];
delete this.peers[peerId];

View file

@ -1,9 +1,3 @@
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent);
window.isMobile = window.iOS || window.android;
window.pasteMode = {};
window.pasteMode.activated = false;
class PeersUI {
constructor() {
@ -16,11 +10,16 @@ class PeersUI {
this.$discoveryWrapper = $$('footer .discovery-wrapper');
this.$displayName = $('display-name');
this.$header = $$('header.opacity-0');
this.$wsFallbackWarning = $('websocket-fallback');
this.evaluateHeader = ["notification", "edit-paired-devices"];
this.fadedIn = false;
this.peers = {};
this.pasteMode = {};
this.pasteMode.activated = false;
this.pasteMode.descriptor = "";
Events.on('display-name', e => this._onDisplayName(e.detail.displayName));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-added', _ => this._evaluateOverflowing());
@ -61,6 +60,20 @@ class PeersUI {
// Load saved display name on page load
Events.on('ws-connected', _ => this._loadSavedDisplayName());
Events.on('ws-config', e => this._evaluateRtcSupport(e.detail))
}
_evaluateRtcSupport(wsConfig) {
if (wsConfig.wsFallback) {
this.$wsFallbackWarning.hidden = false;
}
else {
this.$wsFallbackWarning.hidden = true;
if (!window.isRtcSupported) {
alert(Localization.getTranslation("instructions.webrtc-requirement"));
}
}
}
_loadSavedDisplayName() {
@ -198,7 +211,7 @@ class PeersUI {
}
_onKeyDown(e) {
if (this._noDialogShown() && window.pasteMode.activated && e.code === "Escape") {
if (this._noDialogShown() && this.pasteMode.activated && e.code === "Escape") {
Events.fire('deactivate-paste-mode');
}
@ -318,7 +331,7 @@ class PeersUI {
}
_activatePasteMode(files, text) {
if (!window.pasteMode.activated && (files.length > 0 || text.length > 0)) {
if (!this.pasteMode.activated && (files.length > 0 || text.length > 0)) {
const openPairDrop = Localization.getTranslation("instructions.activate-paste-mode-base");
const andOtherFiles = Localization.getTranslation("instructions.activate-paste-mode-and-other-files", null, {count: files.length-1});
const sharedText = Localization.getTranslation("instructions.activate-paste-mode-shared-text");
@ -344,17 +357,20 @@ class PeersUI {
this.$xNoPeers.querySelector('h2').innerHTML = `${openPairDrop}<br>${descriptor}`;
const _callback = (e) => this._sendClipboardData(e, files, text);
this.pasteMode.descriptor = descriptor;
const _callback = e => this._sendClipboardData(e, files, text);
Events.on('paste-pointerdown', _callback);
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true });
this.$cancelPasteModeBtn.removeAttribute('hidden');
window.pasteMode.descriptor = descriptor;
window.pasteMode.activated = true;
console.log('Paste mode activated.');
Events.fire('paste-mode-changed');
Events.fire('paste-mode-changed', {
active: true,
descriptor: descriptor
});
}
}
@ -363,9 +379,9 @@ class PeersUI {
}
_deactivatePasteMode(_callback) {
if (window.pasteMode.activated) {
window.pasteMode.descriptor = undefined;
window.pasteMode.activated = false;
if (this.pasteMode.activated) {
this.pasteMode.activated = false;
this.pasteMode.descriptor = "";
Events.off('paste-pointerdown', _callback);
this.$xInstructions.querySelector('p').innerText = '';
@ -379,7 +395,10 @@ class PeersUI {
this.$cancelPasteModeBtn.setAttribute('hidden', "");
console.log('Paste mode deactivated.')
Events.fire('paste-mode-changed');
Events.fire('paste-mode-changed', {
active: false,
descriptor: null
});
}
}
@ -405,22 +424,29 @@ class PeersUI {
class PeerUI {
constructor(peer, connectionHash) {
this.$xInstructions = $$('x-instructions');
this.$xPeers = $$('x-peers');
this._peer = peer;
this._connectionHash =
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;
this.pasteMode = {};
this.pasteMode.activated = false;
this.pasteMode.descriptor = "";
this._initDom();
this._bindListeners();
$$('x-peers').appendChild(this.$el)
this.$xPeers.appendChild(this.$el);
Events.fire('peer-added');
this.$xInstructions = $$('x-instructions');
}
html() {
let title;
let input = '';
if (window.pasteMode.activated) {
title = Localization.getTranslation("peer-ui.click-to-send-paste-mode", null, {descriptor: window.pasteMode.descriptor});
if (this.pasteMode.activated) {
title = Localization.getTranslation("peer-ui.click-to-send-paste-mode", null, {descriptor: this.pasteMode.descriptor});
}
else {
title = Localization.getTranslation("peer-ui.click-to-send");
@ -463,6 +489,8 @@ class PeerUI {
}
Object.keys(this._peer._roomIds).forEach(roomType => this.$el.classList.add(`type-${roomType}`));
if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer');
}
_initDom() {
@ -488,16 +516,18 @@ class PeerUI {
this._callbackPointerDown = e => this._onPointerDown(e)
// PasteMode
Events.on('paste-mode-changed', _ => this._onPasteModeChanged());
Events.on('paste-mode-changed', e => this._onPasteModeChanged(e.detail.active, e.detail.descriptor));
}
_onPasteModeChanged() {
_onPasteModeChanged(active, descriptor) {
this.pasteMode.active = active
this.pasteMode.descriptor = descriptor
this.html();
this._bindListeners();
}
_bindListeners() {
if(!window.pasteMode.activated) {
if(!this.pasteMode.activated) {
// Remove Events Paste Mode
this.$el.removeEventListener('pointerdown', this._callbackPointerDown);
@ -1827,7 +1857,7 @@ class SendTextDialog extends Dialog {
this.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]');
this.$form.addEventListener('submit', e => this._onSubmit(e));
this.$text.addEventListener('input', e => this._onChange(e));
this.$text.addEventListener('input', _ => this._onChange());
Events.on('keydown', e => this._onKeyDown(e));
}
@ -1847,7 +1877,7 @@ class SendTextDialog extends Dialog {
return !this.$text.innerText || this.$text.innerText === "\n";
}
_onChange(e) {
_onChange() {
if (this._textInputEmpty()) {
this.$submit.setAttribute('disabled', '');
}

View file

@ -37,9 +37,34 @@ if (!navigator.clipboard) {
}
}
// Polyfills
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
window.hiddenProperty = 'hidden' in document
? 'hidden'
: 'webkitHidden' in document
? 'webkitHidden'
: 'mozHidden' in document
? 'mozHidden'
: null;
window.visibilityChangeEvent = 'visibilitychange' in document
? 'visibilitychange'
: 'webkitvisibilitychange' in document
? 'webkitvisibilitychange'
: 'mozvisibilitychange' in document
? 'mozvisibilitychange'
: null;
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent);
window.isMobile = window.iOS || window.android;
// Selector shortcuts
const $ = query => document.getElementById(query);
const $$ = query => document.querySelector(query);
// Helper functions
const zipper = (() => {
let zipWriter;
@ -416,3 +441,23 @@ function changeFavicon(src) {
document.querySelector('[rel="icon"]').href = src;
document.querySelector('[rel="shortcut icon"]').href = src;
}
function arrayBufferToBase64(buffer) {
let binary = '';
let bytes = new Uint8Array(buffer);
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa( binary );
}
function base64ToArrayBuffer(base64) {
let binary_string = window.atob(base64);
let len = binary_string.length;
let bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}