Compare commits

..

2 Commits

6 changed files with 151 additions and 134 deletions

View File

@@ -737,6 +737,10 @@
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M256 0c4.6 0 9.2 1 13.4 2.9L457.7 82.8c22 9.3 38.4 31 38.3 57.2c-.5 99.2-41.3 280.7-213.6 363.2c-16.7 8-36.1 8-52.8 0C57.3 420.7 16.5 239.2 16 140c-.1-26.2 16.3-47.9 38.3-57.2L242.7 2.9C246.8 1 251.4 0 256 0zm0 66.8V444.8C394 378 431.1 230.1 432 141.4L256 66.8l0 0z"></path>
</symbol>
<symbol id="turn-indicator" viewBox="0 0 576 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
<path d="M0 80C0 53.5 21.5 32 48 32h96c26.5 0 48 21.5 48 48V96H384V80c0-26.5 21.5-48 48-48h96c26.5 0 48 21.5 48 48v96c0 26.5-21.5 48-48 48H432c-26.5 0-48-21.5-48-48V160H192v16c0 1.7-.1 3.4-.3 5L272 288h96c26.5 0 48 21.5 48 48v96c0 26.5-21.5 48-48 48H272c-26.5 0-48-21.5-48-48V336c0-1.7 .1-3.4 .3-5L144 224H48c-26.5 0-48-21.5-48-48V80z"></path>
</symbol>
</svg>
<!-- Scripts -->
<script src="scripts/localization.js" defer></script>

View File

@@ -178,6 +178,7 @@
"click-to-send-share-mode": "Click to send {{descriptor}}",
"click-to-send": "Click to send files or right click to send a message",
"connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices",
"turn-indicator": "Transfer over the Internet",
"connecting": "Connecting…",
"preparing": "Preparing…",
"waiting": "Waiting…",

View File

@@ -137,7 +137,6 @@ class PairDrop {
let stylesheet = document.createElement('link');
stylesheet.rel = 'preload';
stylesheet.as = 'style';
stylesheet.defer = true;
stylesheet.href = url;
stylesheet.onload = _ => {
stylesheet.onload = null;

View File

@@ -63,8 +63,8 @@ class ServerConnection {
}
_setWsConfig(wsConfig) {
window._wsConfig = wsConfig;
Events.fire('ws-config-loaded');
this._wsConfig = wsConfig;
Events.fire('ws-config', wsConfig);
}
_connect() {
@@ -137,7 +137,7 @@ class ServerConnection {
_onMessage(message) {
const messageJSON = JSON.parse(message);
if (messageJSON.type !== 'ping' && messageJSON.type !== 'ws-relay') {
Logger.debug('WS Receive:', messageJSON);
Logger.debug('WS receive:', messageJSON);
}
switch (messageJSON.type) {
case 'ws-config':
@@ -193,15 +193,15 @@ class ServerConnection {
break;
case 'ws-relay':
// ws-fallback
if (window._wsConfig.wsFallback) {
if (this._wsConfig.wsFallback) {
Events.fire('ws-relay', {peerId: messageJSON.sender.id, message: message});
}
else {
Logger.warn("WS Receive: message type is for websocket fallback only but websocket fallback is not activated on this instance.")
Logger.warn("WS receive: message type is for websocket fallback only but websocket fallback is not activated on this instance.")
}
break;
default:
Logger.error('WS Receive: unknown message type', messageJSON);
Logger.error('WS receive: unknown message type', messageJSON);
}
}
@@ -1005,17 +1005,15 @@ 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 = window._wsConfig.rtcConfig;
this.rtcConfig = rtcConfig;
this.pendingInboundServerSignalMessages = [];
this.pendingOutboundMessages = [];
this.errorCount = 0;
this._connect();
}
@@ -1031,6 +1029,33 @@ class RTCPeer extends Peer {
);
}
async _connectionType() {
if (!this._conn) return "";
const stats = await this._conn.getStats(null);
let id;
stats.forEach((report) => {
if (report.type === "candidate-pair" && report.state === "succeeded") {
id = report.localCandidateId;
}
});
if (!id) return "";
let connectionType;
stats.forEach((report) => {
if (report.id === id) {
connectionType = report.candidateType;
}
});
return connectionType;
}
async _isTurn() {
return await this._connectionType() === "relay";
}
_messageChannelOpen() {
return this._messageChannel && this._messageChannel.readyState === 'open';
}
@@ -1116,14 +1141,12 @@ class RTCPeer extends Peer {
_onConnectionStateChange() {
Logger.debug('RTC: Connection state changed:', this._conn.connectionState);
switch (this._conn.connectionState) {
case 'connected':
this.errorCount = 0;
break;
case 'disconnected':
this._refresh();
break;
case 'failed':
Logger.warn('RTC connection failed');
// Todo: if error is "TURN server needed" -> fallback to WS if activated
this._refresh();
}
}
@@ -1132,28 +1155,8 @@ class RTCPeer extends Peer {
this._handleLocalCandidate(event.candidate);
}
_onIceCandidateError(event) {
this.errorCount++
// Todo: remove this
// Todo: test which errorCode is thrown on "TURN server needed" and what other codes are relevant
console.debug(this.errorCount, event.errorCode, event)
Logger.error(event);
if (event.errorCode === 701) {
this._retryOrFallback();
}
}
_retryOrFallback() {
// If fallback is activated, fallback to WS Peer if third retry fails
if (this.errorCount > 3 && window._wsConfig.wsFallback) {
Events.fire('fallback-to-ws', { peerId: this._peerId });
this._sendFallbackToWs();
}
else {
this._refresh();
}
_onIceCandidateError(error) {
Logger.error(error);
}
_openMessageChannel() {
@@ -1182,13 +1185,13 @@ class RTCPeer extends Peer {
return channel;
}
_onChannelOpened(e) {
async _onChannelOpened(e) {
Logger.debug(`RTC: Channel ${e.target.label} opened with`, this._peerId);
// wait until all channels are open
if (!this._stable()) return;
Events.fire('peer-connected', { peerId: this._peerId, connectionHash: this.getConnectionHash(), rtcSupported: this.rtcSupported });
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash(), connectionType: await this._connectionType()});
super._onPeerConnected();
this._sendPendingOutboundMessaged();
@@ -1349,20 +1352,6 @@ class RTCPeer extends Peer {
_sendSignal(message) {
message.type = 'signal';
this._sendMessageViaServer(message);
}
_sendFallbackToWs() {
const message = {
type: 'ws-relay',
message: {
type: 'fallback-to-ws'
}
};
this._sendMessageViaServer(message);
}
_sendMessageViaServer(message) {
message.to = this._peerId;
message.roomType = this._getRoomTypes()[0];
message.roomId = this._roomIds[this._getRoomTypes()[0]];
@@ -1382,7 +1371,7 @@ class RTCPeer extends Peer {
}
async _onMessage(message) {
Logger.debug('RTCPeer Receive:', JSON.parse(message));
Logger.debug('RTC Receive:', JSON.parse(message));
try {
message = JSON.parse(message);
} catch (e) {
@@ -1481,7 +1470,7 @@ class WSPeer extends Peer {
}
async _onMessage(message) {
Logger.debug('WSPeer Receive:', message);
Logger.debug('WS Receive:', message);
await super._onMessage(message);
}
@@ -1527,44 +1516,39 @@ class PeersManager {
constructor(serverConnection) {
this.peers = {};
this._server = serverConnection;
// Initiation
Events.on('signal', e => this._onSignal(e.detail));
Events.on('peers', e => this._onPeers(e.detail));
// File / Message transfer
Events.on('files-selected', e => this._onFilesSelected(e.detail));
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail));
// Peer
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));
// WS-Peer specific
Events.on('ws-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail.peerId, e.detail.message));
Events.on('fallback-to-ws', e => this._onFallbackToWs(e.detail.peerId));
// Rooms and secrets: this device closes connection
// this device closes connection
Events.on('room-secrets-deleted', e => this._onRoomSecretsDeleted(e.detail));
Events.on('leave-public-room', e => this._onLeavePublicRoom(e.detail));
// Rooms and secrets: other peer closes connection
// peer closes connection
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
// Room secret, displayname, auto-accept
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.displayName));
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.peerId, e.detail.message));
Events.on('ws-config', e => this._onWsConfig(e.detail));
// NoSleep evaluation
Events.on('evaluate-no-sleep', _ => this._onEvaluateNoSleep());
}
_onWsConfig(wsConfig) {
this._wsConfig = wsConfig;
}
_onSignal(message) {
const peerId = message.sender.id;
this.peers[peerId]._onServerSignalMessage(message);
@@ -1610,9 +1594,9 @@ class PeersManager {
_createPeer(isCaller, peerId, roomType, roomId, rtcSupported) {
if (window.isRtcSupported && rtcSupported) {
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId);
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId, this._wsConfig.rtcConfig);
}
else if (window._wsConfig.wsFallback) {
else if (this._wsConfig.wsFallback) {
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomId);
}
else {
@@ -1632,20 +1616,11 @@ class PeersManager {
}
_onWsRelay(peerId, message) {
if (!window._wsConfig.wsFallback) return;
if (!this._wsConfig.wsFallback) return;
const peer = this.peers[peerId];
if (!peer) return;
// Check if RTCPeer wants to fall back to WS fallback
if (peer.rtcSupported && JSON.parse(message).message.type === 'fallback-to-ws') {
this._onFallbackToWs(peerId);
return;
}
// Relay messages to WSPeers only
if (peer.rtcSupported) return;
if (!peer || peer.rtcSupported) return;
peer._onWsRelay(message);
}
@@ -1667,23 +1642,20 @@ class PeersManager {
if (this._peerExists(message.peerId) && !this._webRtcSupported(message.peerId)) {
Logger.debug('WSPeer left:', message.peerId);
}
else if (message.disconnect !== true) {
// if RTCPeer and disconnect is false -> abort and wait for reconnect
return;
}
if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);
// if user actively disconnected from PairDrop server or is WSPeer, disconnect all peer to peer connections immediately
this._disconnectOrRemoveRoomTypeByPeerId(message.peerId, message.roomType);
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
// Tidy up peerIds in localStorage
if (Object.keys(this.peers).length === 0) {
BrowserTabsConnector
.removeOtherPeerIdsFromLocalStorage()
.then(peerIds => {
if (!peerIds) return;
Logger.debug("successfully removed other peerIds from localStorage");
});
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
// Tidy up peerIds in localStorage
if (Object.keys(this.peers).length === 0) {
BrowserTabsConnector
.removeOtherPeerIdsFromLocalStorage()
.then(peerIds => {
if (!peerIds) return;
Logger.debug("successfully removed other peerIds from localStorage");
});
}
}
}
@@ -1700,7 +1672,7 @@ class PeersManager {
}
_onWsDisconnected() {
if (!window._wsConfig || !window._wsConfig.wsFallback) return;
if (!this._wsConfig || !this._wsConfig.wsFallback) return;
for (const peerId in this.peers) {
if (!this._webRtcSupported(peerId)) {
@@ -1709,28 +1681,6 @@ class PeersManager {
}
}
_onFallbackToWs(peerId) {
const peer = this.peers[peerId];
if (!peer || !window._wsConfig.wsFallback) return;
peer._onDisconnected();
const isCaller = peer._isCaller;
const roomType = peer._getRoomTypes()[0];
const roomId = peer._roomIds[roomType];
// create WSPeer with same arguments
this._createPeer(isCaller, peerId, roomType, roomId, false);
// add missing room ids
for (let i = 1; i < peer._roomIds.length; i++) {
let roomType = peer._getRoomTypes()[i];
let roomId = peer._roomIds[roomType];
this.peers[peerId]._updateRoomIds(roomType, roomId);
}
}
_onPeerDisconnected(peerId) {
const peer = this.peers[peerId];
delete this.peers[peerId];

View File

@@ -25,7 +25,7 @@ class PeersUI {
this.shareMode.text = "";
Events.on('peer-joined', e => this._onPeerJoined(e.detail.peer, e.detail.roomType, e.detail.roomId));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash, e.detail.rtcSupported));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash, e.detail.connectionType));
Events.on('peer-connecting', e => this._onPeerConnecting(e.detail));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('peers', e => this._onPeers(e.detail));
@@ -110,12 +110,12 @@ class PeersUI {
this.peerUIs[peer.id] = peerUI;
}
_onPeerConnected(peerId, connectionHash, rtcSupported) {
_onPeerConnected(peerId, connectionHash, connectionType) {
const peerUI = this.peerUIs[peerId];
if (!peerUI) return;
peerUI._peerConnected(true, connectionHash, rtcSupported);
peerUI._peerConnected(true, connectionHash, connectionType);
this._addPeerUIIfMissing(peerUI);
}
@@ -459,7 +459,9 @@ class PeerUI {
this.$displayName.textContent = this._displayName();
this.$deviceName.textContent = this._deviceName();
this.updateTypesClassList();
this._updateRoomTypeClasses();
this._updateSameBrowserClass();
this._updateWsPeerClass();
this.setStatus("connect");
@@ -472,14 +474,18 @@ class PeerUI {
}
html() {
let title= Localization.getTranslation("peer-ui.click-to-send");
const titleLabelTranslation= Localization.getTranslation("peer-ui.click-to-send");
const titleTurnTranslation= Localization.getTranslation("peer-ui.turn-indicator");
this.$el.innerHTML = `
<label class="column center pointer" title="${title}">
<label class="column center pointer" title="${titleLabelTranslation}">
<input type="file" multiple/>
<x-icon>
<div class="icon-wrapper" shadow="1">
<svg class="icon"><use xlink:href="#"/></svg>
<div class="turn-indicator-wrapper center" title="${titleTurnTranslation}">
<svg class="turn-indicator"><use xlink:href="#turn-indicator"/></svg>
</div>
</div>
<div class="highlight-wrapper center">
<div class="highlight highlight-room-ip" shadow="1"></div>
@@ -499,30 +505,49 @@ class PeerUI {
</label>`;
}
updateTypesClassList() {
_updateRoomTypeClasses() {
// Remove all classes
this.$el.classList.remove('type-ip', 'type-secret', 'type-public-id', 'type-same-browser', 'ws-peer');
this.$el.classList.remove('type-ip', 'type-secret', 'type-public-id');
// Add classes accordingly
Object.keys(this._roomIds).forEach(roomType => this.$el.classList.add(`type-${roomType}`));
}
_updateSameBrowserClass() {
if (BrowserTabsConnector.peerIsSameBrowser(this._peer.id)) {
this.$el.classList.add(`type-same-browser`);
this.$el.classList.add('same-browser');
}
else {
this.$el.classList.remove('same-browser');
}
}
_updateWsPeerClass() {
if (!this._peer.rtcSupported || !window.isRtcSupported) {
this.$el.classList.add('ws-peer');
}
else {
this.$el.classList.remove('ws-peer');
}
}
_updateTurnClass() {
if (this._connectionType === "relay") {
this.$el.classList.add('turn');
}
else {
this.$el.classList.remove('turn');
}
}
_addRoomId(roomType, roomId) {
this._roomIds[roomType] = roomId;
this.updateTypesClassList();
this._updateRoomTypeClasses();
}
_removeRoomId(roomType) {
delete this._roomIds[roomType];
this.updateTypesClassList();
this._updateRoomTypeClasses();
}
_onShareModeChanged(active = false, descriptor = "") {
@@ -598,7 +623,7 @@ class PeerUI {
});
}
_peerConnected(connected = true, connectionHash = "", rtcSupported = false) {
_peerConnected(connected, connectionHash = "", connectionType = "") {
if (connected) {
this._connected = true;
@@ -606,9 +631,12 @@ class PeerUI {
this.setStatus(this._oldStatus);
this._oldStatus = null;
this._peer.rtcSupported = rtcSupported;
this._connectionHash = connectionHash;
this.updateTypesClassList();
this._connectionType = connectionType;
this._updateSameBrowserClass();
this._updateWsPeerClass();
this._updateTurnClass();
}
else {
this._connected = false;
@@ -620,6 +648,7 @@ class PeerUI {
this.setStatus("connect");
this._connectionHash = "";
this._connectionType = "";
}
}

View File

@@ -279,6 +279,40 @@ x-peer[drop] x-icon {
transform: scale(1.1);
}
x-peer:not(.turn) .turn-indicator-wrapper {
display: none;
}
x-peer .turn-indicator-wrapper {
z-index: 0;
display: flex;
position: absolute;
top: 43px;
right: 23px;
width: 22px;
height: 22px;
}
x-peer .turn-indicator-wrapper:before {
z-index: -1;
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: white;
border-radius: 50%;
border: solid 2px var(--primary-color);
}
x-peer .turn-indicator {
margin: auto;
width: calc(var(--icon-size) / 3);
height: calc(var(--icon-size) / 3);
fill: var(--primary-color);
}
/* Checkboxes as slider */
.switch {