PairDrop/public/scripts/network.js

610 lines
18 KiB
JavaScript
Raw Normal View History

2018-10-09 15:45:07 +02:00
window.URL = window.URL || window.webkitURL;
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
2018-09-21 16:05:03 +02:00
class ServerConnection {
constructor() {
this._connect();
2022-12-30 23:31:58 +01:00
Events.on('beforeunload', _ => this._disconnect());
Events.on('pagehide', _ => this._disconnect());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
2022-12-31 18:03:37 +01:00
Events.on('reconnect', _ => this._reconnect());
2018-09-21 16:05:03 +02:00
}
_connect() {
2018-09-21 21:12:11 +02:00
clearTimeout(this._reconnectTimer);
if (this._isConnected() || this._isConnecting()) return;
2018-09-21 16:05:03 +02:00
const ws = new WebSocket(this._endpoint());
ws.binaryType = 'arraybuffer';
2023-01-07 01:45:52 +01:00
ws.onopen = _ => this._onOpen();
2018-09-21 16:05:03 +02:00
ws.onmessage = e => this._onMessage(e.data);
ws.onclose = _ => this._onDisconnect();
ws.onerror = e => this._onError(e);
2018-09-21 16:05:03 +02:00
this._socket = ws;
}
2023-01-07 01:45:52 +01:00
_onOpen() {
console.log('WS: server connected');
if (!this.firstConnect) {
this.firstConnect = true;
return;
}
Events.fire('ws-connected');
}
2018-09-21 16:05:03 +02:00
_onMessage(msg) {
msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg);
2018-09-21 16:05:03 +02:00
switch (msg.type) {
case 'peers':
Events.fire('peers', msg.peers);
break;
case 'peer-joined':
Events.fire('peer-joined', msg.peer);
break;
case 'peer-left':
Events.fire('peer-left', msg.peerId);
break;
case 'signal':
Events.fire('signal', msg);
break;
case 'ping':
this.send({ type: 'pong' });
break;
2020-12-16 04:16:53 +01:00
case 'display-name':
sessionStorage.setItem("peerId", msg.message.peerId);
2020-12-16 04:16:53 +01:00
Events.fire('display-name', msg);
2019-08-28 17:14:51 +02:00
break;
2018-09-21 16:05:03 +02:00
default:
console.error('WS: unknown message type', msg);
2018-09-21 16:05:03 +02:00
}
}
send(message) {
2018-10-09 15:45:07 +02:00
if (!this._isConnected()) return;
2018-09-21 16:05:03 +02:00
this._socket.send(JSON.stringify(message));
}
_endpoint() {
// hack to detect if deployment or development environment
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
let url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
if (sessionStorage.getItem('peerId')) {
url.searchParams.append('peer_id', sessionStorage.getItem('peerId'))
}
return url.toString();
2018-09-21 16:05:03 +02:00
}
_disconnect() {
this.send({ type: 'disconnect' });
2018-09-22 08:47:40 +02:00
this._socket.onclose = null;
2018-09-21 16:05:03 +02:00
this._socket.close();
2022-12-31 18:03:37 +01:00
this._socket = null;
2023-01-07 01:45:52 +01:00
Events.fire('ws-disconnect');
2018-09-21 16:05:03 +02:00
}
_onDisconnect() {
console.log('WS: server disconnected');
Events.fire('notify-user', 'Connection lost. Retry in 5 seconds...');
clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(this._connect, 5000);
2023-01-07 01:45:52 +01:00
Events.fire('ws-disconnect');
}
2018-09-21 20:51:56 +02:00
_onVisibilityChange() {
2022-12-31 18:03:37 +01:00
if (document.hidden) return;
2018-09-21 20:51:56 +02:00
this._connect();
}
2018-09-22 04:44:17 +02:00
_isConnected() {
return this._socket && this._socket.readyState === this._socket.OPEN;
}
_isConnecting() {
return this._socket && this._socket.readyState === this._socket.CONNECTING;
}
_onError(e) {
console.error(e);
}
2022-12-31 18:03:37 +01:00
_reconnect() {
this._disconnect();
this._connect();
}
2018-09-21 16:05:03 +02:00
}
class Peer {
constructor(serverConnection, peerId) {
this._server = serverConnection;
this._peerId = peerId;
this._filesQueue = [];
this._busy = false;
}
sendJSON(message) {
this._send(JSON.stringify(message));
}
sendFiles(files) {
for (let i = 0; i < files.length; i++) {
this._filesQueue.push(files[i]);
}
if (this._busy) return;
this._dequeueFile();
}
_dequeueFile() {
if (!this._filesQueue.length) return;
this._busy = true;
const file = this._filesQueue.shift();
this._sendFile(file);
}
_sendFile(file) {
this.sendJSON({
type: 'header',
name: file.name,
mime: file.type,
2018-10-09 15:45:07 +02:00
size: file.size
2018-09-21 16:05:03 +02:00
});
this._chunker = new FileChunker(file,
chunk => this._send(chunk),
offset => this._onPartitionEnd(offset));
this._chunker.nextPartition();
}
_onPartitionEnd(offset) {
this.sendJSON({ type: 'partition', offset: offset });
}
_onReceivedPartitionEnd(offset) {
2018-09-22 08:47:40 +02:00
this.sendJSON({ type: 'partition-received', offset: offset });
2018-09-21 16:05:03 +02:00
}
_sendNextPartition() {
if (!this._chunker || this._chunker.isFileEnd()) return;
this._chunker.nextPartition();
}
_sendProgress(progress) {
this.sendJSON({ type: 'progress', progress: progress });
}
_onMessage(message) {
if (typeof message !== 'string') {
this._onChunkReceived(message);
return;
}
message = JSON.parse(message);
console.log('RTC:', message);
switch (message.type) {
case 'header':
this._onFileHeader(message);
break;
case 'partition':
this._onReceivedPartitionEnd(message);
break;
2018-09-22 08:47:40 +02:00
case 'partition-received':
2018-09-21 16:05:03 +02:00
this._sendNextPartition();
break;
case 'progress':
this._onDownloadProgress(message.progress);
break;
case 'file-transfer-complete':
this._onFileTransferCompleted();
break;
case 'message-transfer-complete':
this._onMessageTransferCompleted();
2018-09-21 16:05:03 +02:00
break;
case 'text':
this._onTextReceived(message);
break;
}
}
_onFileHeader(header) {
this._lastProgress = 0;
this._digester = new FileDigester({
name: header.name,
mime: header.mime,
size: header.size
}, file => this._onFileReceived(file));
}
_onChunkReceived(chunk) {
if(!(chunk.byteLength || chunk.size)) return;
2018-09-21 16:05:03 +02:00
this._digester.unchunk(chunk);
const progress = this._digester.progress;
this._onDownloadProgress(progress);
// occasionally notify sender about our progress
2018-09-21 16:05:03 +02:00
if (progress - this._lastProgress < 0.01) return;
this._lastProgress = progress;
this._sendProgress(progress);
}
_onDownloadProgress(progress) {
2018-09-22 04:44:17 +02:00
Events.fire('file-progress', { sender: this._peerId, progress: progress });
2018-09-21 16:05:03 +02:00
}
_onFileReceived(proxyFile) {
Events.fire('file-received', proxyFile);
this.sendJSON({ type: 'file-transfer-complete' });
2018-09-21 16:05:03 +02:00
}
_onFileTransferCompleted() {
2018-09-21 16:05:03 +02:00
this._onDownloadProgress(1);
this._reader = null;
this._busy = false;
this._dequeueFile();
Events.fire('notify-user', 'File transfer completed.');
}
_onMessageTransferCompleted() {
Events.fire('notify-user', 'Message transfer completed.');
}
2018-09-21 16:05:03 +02:00
sendText(text) {
2018-09-22 04:44:17 +02:00
const unescaped = btoa(unescape(encodeURIComponent(text)));
this.sendJSON({ type: 'text', text: unescaped });
2018-09-21 16:05:03 +02:00
}
_onTextReceived(message) {
2018-09-22 04:44:17 +02:00
const escaped = decodeURIComponent(escape(atob(message.text)));
Events.fire('text-received', { text: escaped, sender: this._peerId });
this.sendJSON({ type: 'message-transfer-complete' });
2018-09-21 16:05:03 +02:00
}
}
class RTCPeer extends Peer {
constructor(serverConnection, peerId) {
super(serverConnection, peerId);
if (!peerId) return; // we will listen for a caller
2018-09-22 04:44:17 +02:00
this._connect(peerId, true);
2018-09-21 16:05:03 +02:00
}
2018-09-22 04:44:17 +02:00
_connect(peerId, isCaller) {
if (!this._conn) this._openConnection(peerId, isCaller);
2018-09-21 16:05:03 +02:00
if (isCaller) {
2018-09-22 04:44:17 +02:00
this._openChannel();
2018-09-21 16:05:03 +02:00
} else {
2018-09-22 04:44:17 +02:00
this._conn.ondatachannel = e => this._onChannelOpened(e);
2018-09-21 16:05:03 +02:00
}
}
2018-09-22 04:44:17 +02:00
_openConnection(peerId, isCaller) {
this._isCaller = isCaller;
this._peerId = peerId;
this._conn = new RTCPeerConnection(RTCPeer.config);
this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onconnectionstatechange = e => this._onConnectionStateChange(e);
2018-09-25 18:55:47 +02:00
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
2018-09-22 04:44:17 +02:00
}
_openChannel() {
const channel = this._conn.createDataChannel('data-channel', {
ordered: true,
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
});
2018-09-21 19:14:25 +02:00
channel.onopen = e => this._onChannelOpened(e);
2018-10-09 23:00:18 +02:00
this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e));
2018-09-21 16:05:03 +02:00
}
_onDescription(description) {
// description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400');
2018-10-09 23:00:18 +02:00
this._conn.setLocalDescription(description)
.then(_ => this._sendSignal({ sdp: description }))
.catch(e => this._onError(e));
2018-09-21 16:05:03 +02:00
}
_onIceCandidate(event) {
if (!event.candidate) return;
this._sendSignal({ ice: event.candidate });
}
onServerMessage(message) {
2018-09-22 04:44:17 +02:00
if (!this._conn) this._connect(message.sender, false);
2018-09-21 16:05:03 +02:00
if (message.sdp) {
2018-10-09 23:00:18 +02:00
this._conn.setRemoteDescription(new RTCSessionDescription(message.sdp))
.then( _ => {
2019-02-18 22:19:07 +01:00
if (message.sdp.type === 'offer') {
return this._conn.createAnswer()
.then(d => this._onDescription(d));
}
})
2018-10-09 23:00:18 +02:00
.catch(e => this._onError(e));
2018-09-21 16:05:03 +02:00
} else if (message.ice) {
2018-09-22 04:44:17 +02:00
this._conn.addIceCandidate(new RTCIceCandidate(message.ice));
2018-09-21 16:05:03 +02:00
}
}
_onChannelOpened(event) {
console.log('RTC: channel opened with', this._peerId);
Events.fire('peer-connected', this._peerId);
2018-09-21 16:05:03 +02:00
const channel = event.channel || event.target;
2022-10-23 06:17:39 +02:00
channel.binaryType = 'arraybuffer';
2018-09-21 16:05:03 +02:00
channel.onmessage = e => this._onMessage(e.data);
2023-01-06 16:05:17 +01:00
channel.onclose = _ => this._onChannelClosed();
2018-09-21 16:05:03 +02:00
this._channel = channel;
}
_onChannelClosed() {
2018-09-21 19:24:01 +02:00
console.log('RTC: channel closed', this._peerId);
Events.fire('peer-disconnected', this._peerId);
2023-01-06 16:05:17 +01:00
if (!this._isCaller) return;
2018-09-22 04:44:17 +02:00
this._connect(this._peerId, true); // reopen the channel
2018-09-21 16:05:03 +02:00
}
2018-09-21 19:24:01 +02:00
_onConnectionStateChange(e) {
2018-09-22 04:44:17 +02:00
console.log('RTC: state changed:', this._conn.connectionState);
switch (this._conn.connectionState) {
2018-09-21 20:51:56 +02:00
case 'disconnected':
this._onChannelClosed();
break;
case 'failed':
2018-09-22 04:44:17 +02:00
this._conn = null;
2018-09-21 20:51:56 +02:00
this._onChannelClosed();
break;
2018-09-21 19:24:01 +02:00
}
}
2018-09-25 18:58:52 +02:00
_onIceConnectionStateChange() {
switch (this._conn.iceConnectionState) {
case 'failed':
console.error('ICE Gathering failed');
Events.fire('reconnect');
2018-09-25 18:58:52 +02:00
break;
default:
console.log('ICE Gathering', this._conn.iceConnectionState);
}
2018-09-25 18:55:47 +02:00
}
2018-09-22 04:44:17 +02:00
_onError(error) {
console.error(error);
2022-12-31 18:03:37 +01:00
Events.fire('reconnect');
2018-09-22 04:44:17 +02:00
}
2018-09-21 16:05:03 +02:00
_send(message) {
2018-10-09 23:00:18 +02:00
if (!this._channel) return this.refresh();
2018-09-21 16:05:03 +02:00
this._channel.send(message);
}
2018-09-22 04:44:17 +02:00
_sendSignal(signal) {
signal.type = 'signal';
signal.to = this._peerId;
this._server.send(signal);
2018-09-21 16:05:03 +02:00
}
refresh() {
2018-09-22 04:44:17 +02:00
// check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return;
this._connect(this._peerId, this._isCaller);
}
_isConnected() {
return this._channel && this._channel.readyState === 'open';
}
_isConnecting() {
return this._channel && this._channel.readyState === 'connecting';
2018-09-21 16:05:03 +02:00
}
}
class PeersManager {
constructor(serverConnection) {
this.peers = {};
this._server = serverConnection;
Events.on('signal', e => this._onMessage(e.detail));
Events.on('peers', e => this._onPeers(e.detail));
Events.on('files-selected', e => this._onFilesSelected(e.detail));
Events.on('send-text', e => this._onSendText(e.detail));
2022-12-31 18:03:37 +01:00
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
2018-09-21 16:05:03 +02:00
Events.on('peer-left', e => this._onPeerLeft(e.detail));
2023-01-07 01:45:52 +01:00
Events.on('ws-disconnect', _ => this._clearPeers());
2018-09-21 16:05:03 +02:00
}
_onMessage(message) {
if (!this.peers[message.sender]) {
this.peers[message.sender] = new RTCPeer(this._server);
}
this.peers[message.sender].onServerMessage(message);
}
_onPeers(peers) {
peers.forEach(peer => {
if (this.peers[peer.id]) {
this.peers[peer.id].refresh();
return;
}
if (window.isRtcSupported && peer.rtcSupported) {
this.peers[peer.id] = new RTCPeer(this._server, peer.id);
} else {
this.peers[peer.id] = new WSPeer(this._server, peer.id);
}
})
}
sendTo(peerId, message) {
this.peers[peerId].send(message);
}
_onFilesSelected(message) {
this.peers[message.to].sendFiles(message.files);
}
_onSendText(message) {
this.peers[message.to].sendText(message.text);
}
2022-12-31 18:03:37 +01:00
_onPeerJoined(peer) {
this._onMessage(peer.id);
}
2018-09-21 16:05:03 +02:00
_onPeerLeft(peerId) {
const peer = this.peers[peerId];
delete this.peers[peerId];
2022-12-31 18:03:37 +01:00
if (!peer || !peer._conn) return;
2023-01-06 19:50:09 +01:00
if (peer._channel) peer._channel.onclose = null;
2022-12-31 18:03:37 +01:00
peer._conn.close();
2018-09-21 16:05:03 +02:00
}
_clearPeers() {
if (this.peers) {
Object.keys(this.peers).forEach(peerId => this._onPeerLeft(peerId));
}
}
2018-09-21 16:05:03 +02:00
}
class WSPeer extends Peer {
2018-09-21 16:05:03 +02:00
_send(message) {
message.to = this._peerId;
this._server.send(message);
}
}
class FileChunker {
constructor(file, onChunk, onPartitionEnd) {
2018-10-09 23:00:18 +02:00
this._chunkSize = 64000; // 64 KB
2018-10-09 15:45:07 +02:00
this._maxPartitionSize = 1e6; // 1 MB
2018-09-21 16:05:03 +02:00
this._offset = 0;
this._partitionSize = 0;
this._file = file;
this._onChunk = onChunk;
this._onPartitionEnd = onPartitionEnd;
this._reader = new FileReader();
this._reader.addEventListener('load', e => this._onChunkRead(e.target.result));
}
nextPartition() {
this._partitionSize = 0;
this._readChunk();
}
_readChunk() {
const chunk = this._file.slice(this._offset, this._offset + this._chunkSize);
this._reader.readAsArrayBuffer(chunk);
}
_onChunkRead(chunk) {
this._offset += chunk.byteLength;
this._partitionSize += chunk.byteLength;
this._onChunk(chunk);
if (this.isFileEnd()) return;
if (this._isPartitionEnd()) {
2018-09-22 04:44:17 +02:00
this._onPartitionEnd(this._offset);
2018-09-21 16:05:03 +02:00
return;
}
this._readChunk();
}
repeatPartition() {
this._offset -= this._partitionSize;
2023-01-06 16:28:00 +01:00
this.nextPartition();
2018-09-21 16:05:03 +02:00
}
_isPartitionEnd() {
return this._partitionSize >= this._maxPartitionSize;
}
isFileEnd() {
return this._offset >= this._file.size;
2018-09-21 16:05:03 +02:00
}
get progress() {
return this._offset / this._file.size;
}
}
class FileDigester {
2018-09-22 08:47:40 +02:00
2018-09-21 16:05:03 +02:00
constructor(meta, callback) {
this._buffer = [];
this._bytesReceived = 0;
this._size = meta.size;
this._mime = meta.mime || 'application/octet-stream';
this._name = meta.name;
this._callback = callback;
}
unchunk(chunk) {
this._buffer.push(chunk);
this._bytesReceived += chunk.byteLength || chunk.size;
const totalChunks = this._buffer.length;
this.progress = this._bytesReceived / this._size;
2021-08-12 15:38:02 +02:00
if (isNaN(this.progress)) this.progress = 1
2018-09-21 16:05:03 +02:00
if (this._bytesReceived < this._size) return;
2018-09-22 08:47:40 +02:00
// we are done
let blob = new Blob(this._buffer, { type: this._mime });
2018-09-21 16:05:03 +02:00
this._callback({
name: this._name,
mime: this._mime,
size: this._size,
blob: blob
2018-09-21 16:05:03 +02:00
});
}
2018-09-22 04:44:17 +02:00
2018-09-21 16:05:03 +02:00
}
class Events {
static fire(type, detail) {
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
}
static on(type, callback) {
return window.addEventListener(type, callback, false);
}
2022-09-13 14:33:03 +02:00
static off(type, callback) {
return window.removeEventListener(type, callback, false);
}
2018-09-21 16:05:03 +02:00
}
RTCPeer.config = {
'sdpSemantics': 'unified-plan',
// iceServers: [
// {
// urls: 'stun:127.0.0.1:3478',
// },
// {
// urls: 'turn:127.0.0.1:3478',
// username: 'snapdrop',
// credential: 'ifupvrwelijmoyjxmefcsvfxxmcphvxo'
// }
// ]
iceServers: [
{
urls: "stun:relay.metered.ca:80",
},
{
urls: "turn:relay.metered.ca:80",
username: "411061cd290de7ca6cc1a753",
credential: "CuCIGdVfA9Gias1E",
},
{
urls: "turn:relay.metered.ca:443",
username: "411061cd290de7ca6cc1a753",
credential: "CuCIGdVfA9Gias1E",
},
],
// iceServers: [
// {
// urls: 'stun:stun.l.google.com:19302'
// },
// {
// urls: 'turn:om.wulingate.com',
// username: 'hmzJ0OHZivkod703',
// credential: 'KDF04PBYD9xHAp0s'
// },
// ]
2019-02-18 22:19:07 +01:00
}