mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-22 07:46:17 -04:00
implement complete WSPeer as fallback if WebRTC is deactivated. Only ever use on self-hosted instances as clients need to trust the server!
This commit is contained in:
parent
b8c78bccfa
commit
616f6a6799
38 changed files with 5162 additions and 14 deletions
2
public_included_ws_fallback/scripts/NoSleep.min.js
vendored
Normal file
2
public_included_ws_fallback/scripts/NoSleep.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
893
public_included_ws_fallback/scripts/network.js
Normal file
893
public_included_ws_fallback/scripts/network.js
Normal file
|
@ -0,0 +1,893 @@
|
|||
window.URL = window.URL || window.webkitURL;
|
||||
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
|
||||
|
||||
class ServerConnection {
|
||||
|
||||
constructor() {
|
||||
this._connect();
|
||||
Events.on('pagehide', _ => this._disconnect());
|
||||
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('resend-peers', _ => this.send({ type: 'resend-peers'}));
|
||||
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
|
||||
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
|
||||
Events.on('pair-device-cancel', _ => this.send({ type: 'pair-device-cancel' }));
|
||||
Events.on('offline', _ => clearTimeout(this._reconnectTimer));
|
||||
Events.on('online', _ => this._connect());
|
||||
}
|
||||
|
||||
async _connect() {
|
||||
clearTimeout(this._reconnectTimer);
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
const ws = new WebSocket(await this._endpoint());
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = _ => this._onOpen();
|
||||
ws.onmessage = e => this._onMessage(e.data);
|
||||
ws.onclose = _ => this._onDisconnect();
|
||||
ws.onerror = e => this._onError(e);
|
||||
this._socket = ws;
|
||||
}
|
||||
|
||||
_onOpen() {
|
||||
console.log('WS: server connected');
|
||||
Events.fire('ws-connected');
|
||||
}
|
||||
|
||||
_sendRoomSecrets(roomSecrets) {
|
||||
this.send({ type: 'room-secrets', roomSecrets: roomSecrets });
|
||||
}
|
||||
|
||||
_onPairDeviceInitiate() {
|
||||
if (!this._isConnected()) {
|
||||
Events.fire('notify-user', 'You need to be online to pair devices.');
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-initiate' })
|
||||
}
|
||||
|
||||
_onPairDeviceJoin(roomKey) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||
}
|
||||
|
||||
_onMessage(msg) {
|
||||
msg = JSON.parse(msg);
|
||||
if (msg.type !== 'ping') console.log('WS:', msg);
|
||||
switch (msg.type) {
|
||||
case 'peers':
|
||||
Events.fire('peers', msg);
|
||||
break;
|
||||
case 'peer-joined':
|
||||
Events.fire('peer-joined', msg);
|
||||
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;
|
||||
case 'display-name':
|
||||
this._onDisplayName(msg);
|
||||
break;
|
||||
case 'pair-device-initiated':
|
||||
Events.fire('pair-device-initiated', msg);
|
||||
break;
|
||||
case 'pair-device-joined':
|
||||
Events.fire('pair-device-joined', msg.roomSecret);
|
||||
break;
|
||||
case 'pair-device-join-key-invalid':
|
||||
Events.fire('pair-device-join-key-invalid');
|
||||
break;
|
||||
case 'pair-device-canceled':
|
||||
Events.fire('pair-device-canceled', msg.roomKey);
|
||||
break;
|
||||
case 'pair-device-join-key-rate-limit':
|
||||
Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.');
|
||||
break;
|
||||
case 'secret-room-deleted':
|
||||
Events.fire('secret-room-deleted', msg.roomSecret);
|
||||
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 'ws-chunk':
|
||||
Events.fire('ws-relay', JSON.stringify(msg));
|
||||
break;
|
||||
default:
|
||||
console.error('WS: unknown message type', msg);
|
||||
}
|
||||
}
|
||||
|
||||
send(msg) {
|
||||
if (!this._isConnected()) return;
|
||||
this._socket.send(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
_onDisplayName(msg) {
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
PersistentStorage.get('peerId').then(peerId => {
|
||||
if (!peerId) {
|
||||
// save peerId to indexedDB to retrieve after PWA is installed
|
||||
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
|
||||
console.log(`peerId saved to indexedDB: ${peerId}`);
|
||||
});
|
||||
}
|
||||
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
|
||||
Events.fire('display-name', msg);
|
||||
}
|
||||
|
||||
async _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);
|
||||
const peerId = await this._peerId();
|
||||
if (peerId) ws_url.searchParams.append('peer_id', peerId)
|
||||
return ws_url.toString();
|
||||
}
|
||||
|
||||
async _peerId() {
|
||||
// make peerId persistent when pwa is installed
|
||||
return window.matchMedia('(display-mode: minimal-ui)').matches
|
||||
? await PersistentStorage.get('peerId')
|
||||
: sessionStorage.getItem("peerId");
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
if (this._socket) {
|
||||
this._socket.onclose = null;
|
||||
this._socket.close();
|
||||
this._socket = null;
|
||||
Events.fire('ws-disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
_onDisconnect() {
|
||||
console.log('WS: server disconnected');
|
||||
Events.fire('notify-user', 'No server connection. Retry in 5s...');
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
||||
Events.fire('ws-disconnected');
|
||||
}
|
||||
|
||||
_onVisibilityChange() {
|
||||
if (document.hidden) return;
|
||||
this._connect();
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
_reconnect() {
|
||||
this._disconnect();
|
||||
this._connect();
|
||||
}
|
||||
}
|
||||
|
||||
class Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
this._server = serverConnection;
|
||||
this._peerId = peerId;
|
||||
this._roomType = roomType;
|
||||
this._roomSecret = roomSecret;
|
||||
this._filesQueue = [];
|
||||
this._busy = false;
|
||||
}
|
||||
|
||||
sendJSON(message) {
|
||||
this._send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
async createHeader(file) {
|
||||
return {
|
||||
name: file.name,
|
||||
mime: file.type,
|
||||
size: file.size,
|
||||
};
|
||||
}
|
||||
|
||||
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let image = new Image();
|
||||
image.src = URL.createObjectURL(file);
|
||||
image.onload = _ => {
|
||||
let imageWidth = image.width;
|
||||
let imageHeight = image.height;
|
||||
let canvas = document.createElement('canvas');
|
||||
|
||||
// resize the canvas and draw the image data into it
|
||||
if (width && height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
} else if (width) {
|
||||
canvas.width = width;
|
||||
canvas.height = Math.floor(imageHeight * width / imageWidth)
|
||||
} else if (height) {
|
||||
canvas.width = Math.floor(imageWidth * height / imageHeight);
|
||||
canvas.height = height;
|
||||
} else {
|
||||
canvas.width = imageWidth;
|
||||
canvas.height = imageHeight
|
||||
}
|
||||
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
let dataUrl = canvas.toDataURL("image/jpeg", quality);
|
||||
resolve(dataUrl);
|
||||
}
|
||||
image.onerror = _ => reject(`Could not create an image thumbnail from type ${file.type}`);
|
||||
}).then(dataUrl => {
|
||||
return dataUrl;
|
||||
}).catch(e => console.error(e));
|
||||
}
|
||||
|
||||
async requestFileTransfer(files) {
|
||||
let header = [];
|
||||
let totalSize = 0;
|
||||
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]));
|
||||
totalSize += files[i].size;
|
||||
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
|
||||
}
|
||||
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8, status: 'prepare'})
|
||||
|
||||
let dataUrl = '';
|
||||
if (files[0].type.split('/')[0] === 'image') {
|
||||
dataUrl = await this.getResizedImageDataUrl(files[0], 400, null, 0.9);
|
||||
}
|
||||
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'prepare'})
|
||||
|
||||
this._filesRequested = files;
|
||||
|
||||
this.sendJSON({type: 'request',
|
||||
header: header,
|
||||
totalSize: totalSize,
|
||||
imagesOnly: imagesOnly,
|
||||
thumbnailDataUrl: dataUrl
|
||||
});
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'wait'})
|
||||
}
|
||||
|
||||
async sendFiles() {
|
||||
for (let i=0; i<this._filesRequested.length; i++) {
|
||||
this._filesQueue.push(this._filesRequested[i]);
|
||||
}
|
||||
this._filesRequested = null
|
||||
if (this._busy) return;
|
||||
this._dequeueFile();
|
||||
}
|
||||
|
||||
_dequeueFile() {
|
||||
this._busy = true;
|
||||
const file = this._filesQueue.shift();
|
||||
this._sendFile(file);
|
||||
}
|
||||
|
||||
async _sendFile(file) {
|
||||
this.sendJSON({
|
||||
type: 'header',
|
||||
size: file.size,
|
||||
name: file.name,
|
||||
mime: file.type
|
||||
});
|
||||
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) {
|
||||
this.sendJSON({ type: 'partition-received', offset: offset });
|
||||
}
|
||||
|
||||
_sendNextPartition() {
|
||||
if (!this._chunker || this._chunker.isFileEnd()) return;
|
||||
this._chunker.nextPartition();
|
||||
}
|
||||
|
||||
_sendProgress(progress) {
|
||||
this.sendJSON({ type: 'progress', progress: progress });
|
||||
}
|
||||
|
||||
_onMessage(message, logMessage = true) {
|
||||
if (typeof message !== 'string') {
|
||||
this._onChunkReceived(message);
|
||||
return;
|
||||
}
|
||||
message = JSON.parse(message);
|
||||
if (logMessage) console.log('RTC:', message);
|
||||
switch (message.type) {
|
||||
case 'request':
|
||||
this._onFilesTransferRequest(message);
|
||||
break;
|
||||
case 'header':
|
||||
this._onFilesHeader(message);
|
||||
break;
|
||||
case 'partition':
|
||||
this._onReceivedPartitionEnd(message);
|
||||
break;
|
||||
case 'partition-received':
|
||||
this._sendNextPartition();
|
||||
break;
|
||||
case 'progress':
|
||||
this._onDownloadProgress(message.progress);
|
||||
break;
|
||||
case 'files-transfer-response':
|
||||
this._onFileTransferRequestResponded(message);
|
||||
break;
|
||||
case 'file-transfer-complete':
|
||||
this._onFileTransferCompleted();
|
||||
break;
|
||||
case 'message-transfer-complete':
|
||||
this._onMessageTransferCompleted();
|
||||
break;
|
||||
case 'text':
|
||||
this._onTextReceived(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onFilesTransferRequest(request) {
|
||||
if (this._requestPending) {
|
||||
// Only accept one request at a time
|
||||
this.sendJSON({type: 'files-transfer-response', accepted: false});
|
||||
return;
|
||||
}
|
||||
if (window.iOS && request.totalSize >= 200*1024*1024) {
|
||||
// iOS Safari can only put 400MB at once to memory.
|
||||
// Request to send them in chunks of 200MB instead:
|
||||
this.sendJSON({type: 'files-transfer-response', accepted: false, reason: 'ios-memory-limit'});
|
||||
return;
|
||||
}
|
||||
|
||||
this._requestPending = request;
|
||||
Events.fire('files-transfer-request', {
|
||||
request: request,
|
||||
peerId: this._peerId
|
||||
});
|
||||
}
|
||||
|
||||
_respondToFileTransferRequest(accepted) {
|
||||
this.sendJSON({type: 'files-transfer-response', accepted: accepted});
|
||||
if (accepted) {
|
||||
this._requestAccepted = this._requestPending;
|
||||
this._totalBytesReceived = 0;
|
||||
this._busy = true;
|
||||
this._filesReceived = [];
|
||||
}
|
||||
this._requestPending = null;
|
||||
}
|
||||
|
||||
_onFilesHeader(header) {
|
||||
if (this._requestAccepted?.header.length) {
|
||||
this._lastProgress = 0;
|
||||
this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime},
|
||||
this._requestAccepted.totalSize,
|
||||
this._totalBytesReceived,
|
||||
fileBlob => this._onFileReceived(fileBlob)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_abortTransfer() {
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||
Events.fire('notify-user', 'Files are incorrect.');
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
this._digester = null;
|
||||
throw new Error("Received files differ from requested files. Abort!");
|
||||
}
|
||||
|
||||
_onChunkReceived(chunk) {
|
||||
if(!this._digester || !(chunk.byteLength || chunk.size)) return;
|
||||
|
||||
this._digester.unchunk(chunk);
|
||||
const progress = this._digester.progress;
|
||||
|
||||
if (progress > 1) {
|
||||
this._abortTransfer();
|
||||
}
|
||||
|
||||
this._onDownloadProgress(progress);
|
||||
|
||||
// occasionally notify sender about our progress
|
||||
if (progress - this._lastProgress < 0.005 && progress !== 1) return;
|
||||
this._lastProgress = progress;
|
||||
this._sendProgress(progress);
|
||||
}
|
||||
|
||||
_onDownloadProgress(progress) {
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: progress, status: 'transfer'});
|
||||
}
|
||||
|
||||
async _onFileReceived(fileBlob) {
|
||||
const acceptedHeader = this._requestAccepted.header.shift();
|
||||
this._totalBytesReceived += fileBlob.size;
|
||||
|
||||
this.sendJSON({type: 'file-transfer-complete'});
|
||||
|
||||
const sameSize = fileBlob.size === acceptedHeader.size;
|
||||
const sameName = fileBlob.name === acceptedHeader.name
|
||||
if (!sameSize || !sameName) {
|
||||
this._abortTransfer();
|
||||
}
|
||||
|
||||
this._filesReceived.push(fileBlob);
|
||||
if (!this._requestAccepted.header.length) {
|
||||
this._busy = false;
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
|
||||
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
|
||||
this._filesReceived = [];
|
||||
this._requestAccepted = null;
|
||||
}
|
||||
}
|
||||
|
||||
_onFileTransferCompleted() {
|
||||
this._chunker = null;
|
||||
if (!this._filesQueue.length) {
|
||||
this._busy = false;
|
||||
Events.fire('notify-user', 'File transfer completed.');
|
||||
} else {
|
||||
this._dequeueFile();
|
||||
}
|
||||
}
|
||||
|
||||
_onFileTransferRequestResponded(message) {
|
||||
if (!message.accepted) {
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
|
||||
this._filesRequested = null;
|
||||
if (message.reason === 'ios-memory-limit') {
|
||||
Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once");
|
||||
}
|
||||
return;
|
||||
}
|
||||
Events.fire('file-transfer-accepted');
|
||||
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'transfer'});
|
||||
this.sendFiles();
|
||||
}
|
||||
|
||||
_onMessageTransferCompleted() {
|
||||
Events.fire('notify-user', 'Message transfer completed.');
|
||||
}
|
||||
|
||||
sendText(text) {
|
||||
const unescaped = btoa(unescape(encodeURIComponent(text)));
|
||||
this.sendJSON({ type: 'text', text: unescaped });
|
||||
}
|
||||
|
||||
_onTextReceived(message) {
|
||||
if (!message.text) return;
|
||||
const escaped = decodeURIComponent(escape(atob(message.text)));
|
||||
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
||||
this.sendJSON({ type: 'message-transfer-complete' });
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
if (!peerId) 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);
|
||||
|
||||
if (isCaller) {
|
||||
this._openChannel();
|
||||
} else {
|
||||
this._conn.ondatachannel = e => this._onChannelOpened(e);
|
||||
}
|
||||
}
|
||||
|
||||
_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 = _ => this._onConnectionStateChange();
|
||||
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
|
||||
}
|
||||
|
||||
_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
|
||||
});
|
||||
channel.onopen = e => this._onChannelOpened(e);
|
||||
this._conn.createOffer().then(d => this._onDescription(d)).catch(e => this._onError(e));
|
||||
}
|
||||
|
||||
_onDescription(description) {
|
||||
// description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400');
|
||||
this._conn.setLocalDescription(description)
|
||||
.then(_ => this._sendSignal({ sdp: description }))
|
||||
.catch(e => this._onError(e));
|
||||
}
|
||||
|
||||
_onIceCandidate(event) {
|
||||
if (!event.candidate) return;
|
||||
this._sendSignal({ ice: event.candidate });
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
if (!this._conn) this._connect(message.sender.id, false);
|
||||
|
||||
if (message.sdp) {
|
||||
this._conn.setRemoteDescription(message.sdp)
|
||||
.then( _ => {
|
||||
return this._conn.createAnswer()
|
||||
.then(d => this._onDescription(d));
|
||||
})
|
||||
.catch(e => this._onError(e));
|
||||
} else if (message.ice) {
|
||||
this._conn.addIceCandidate(new RTCIceCandidate(message.ice));
|
||||
}
|
||||
}
|
||||
|
||||
_onChannelOpened(event) {
|
||||
console.log('RTC: channel opened with', this._peerId);
|
||||
Events.fire('peer-connected', this._peerId);
|
||||
const channel = event.channel || event.target;
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
channel.onclose = _ => this._onChannelClosed();
|
||||
Events.on('pagehide', _ => this._conn.close());
|
||||
this._channel = channel;
|
||||
}
|
||||
|
||||
_onChannelClosed() {
|
||||
console.log('RTC: channel closed', this._peerId);
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
if (this._channel) this._channel.onclose = null;
|
||||
this._conn.close();
|
||||
if (!this._isCaller) return;
|
||||
this._connect(this._peerId, true); // reopen the channel
|
||||
}
|
||||
|
||||
_onConnectionStateChange() {
|
||||
console.log('RTC: state changed:', this._conn.connectionState);
|
||||
switch (this._conn.connectionState) {
|
||||
case 'disconnected':
|
||||
this._onError('rtc connection disconnected');
|
||||
break;
|
||||
case 'failed':
|
||||
this._onError('rtc connection failed');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_onIceConnectionStateChange() {
|
||||
switch (this._conn.iceConnectionState) {
|
||||
case 'failed':
|
||||
this._onError('ICE Gathering failed');
|
||||
break;
|
||||
default:
|
||||
console.log('ICE Gathering', this._conn.iceConnectionState);
|
||||
}
|
||||
}
|
||||
|
||||
_onError(error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
_send(message) {
|
||||
if (!this._channel) this.refresh();
|
||||
this._channel.send(message);
|
||||
}
|
||||
|
||||
_sendSignal(signal) {
|
||||
signal.type = 'signal';
|
||||
signal.to = this._peerId;
|
||||
signal.roomType = this._roomType;
|
||||
signal.roomSecret = this._roomSecret;
|
||||
this._server.send(signal);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
// 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';
|
||||
}
|
||||
}
|
||||
|
||||
class WSPeer extends Peer {
|
||||
|
||||
constructor(serverConnection, peerId, roomType, roomSecret) {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
if (!peerId) 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._roomType;
|
||||
message.roomSecret = this._roomSecret;
|
||||
this._server.send(message);
|
||||
}
|
||||
|
||||
_sendSignal() {
|
||||
this.sendJSON({type: 'signal'});
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
Events.fire('peer-connected', message.sender.id)
|
||||
if (this._peerId) return;
|
||||
this._peerId = message.sender.id;
|
||||
this._sendSignal();
|
||||
}
|
||||
}
|
||||
|
||||
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('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-disconnected', e => this._onPeerDisconnected(e.detail));
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
||||
}
|
||||
|
||||
_onBeforeUnload(e) {
|
||||
for (const peerId in this.peers) {
|
||||
if (this.peers[peerId]._busy) {
|
||||
e.preventDefault();
|
||||
return "There are unfinished transfers. Are you sure you want to close?";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_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]) {
|
||||
if (window.isRtcSupported && message.sender.rtcSupported) {
|
||||
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
|
||||
} else {
|
||||
this.peers[message.sender.id] = new WSPeer(this._server, undefined, message.roomType, message.roomSecret);
|
||||
}
|
||||
}
|
||||
this.peers[message.sender.id].onServerMessage(message);
|
||||
}
|
||||
|
||||
_onWsRelay(message) {
|
||||
const messageJSON = JSON.parse(message)
|
||||
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
|
||||
this.peers[messageJSON.sender.id]._onMessage(message, false)
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
msg.peers.forEach(peer => {
|
||||
if (this.peers[peer.id]) {
|
||||
// if different roomType -> abort
|
||||
if (this.peers[peer.id].roomType !== msg.roomType) return;
|
||||
this.peers[peer.id].refresh();
|
||||
return;
|
||||
}
|
||||
if (window.isRtcSupported && peer.rtcSupported) {
|
||||
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
|
||||
} else {
|
||||
this.peers[peer.id] = new WSPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
sendTo(peerId, message) {
|
||||
this.peers[peerId].send(message);
|
||||
}
|
||||
|
||||
_onRespondToFileTransferRequest(detail) {
|
||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||
}
|
||||
|
||||
_onFilesSelected(message) {
|
||||
let inputFiles = Array.from(message.files);
|
||||
delete message.files;
|
||||
let files = [];
|
||||
const l = inputFiles.length;
|
||||
for (let i=0; i<l; i++) {
|
||||
// when filetype is empty guess via suffix
|
||||
const inputFile = inputFiles.shift();
|
||||
const file = inputFile.type
|
||||
? inputFile
|
||||
: new File([inputFile], inputFile.name, {type: mime.getMimeByFilename(inputFile.name)});
|
||||
files.push(file)
|
||||
}
|
||||
this.peers[message.to].requestFileTransfer(files);
|
||||
}
|
||||
|
||||
_onSendText(message) {
|
||||
this.peers[message.to].sendText(message.text);
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
delete this.peers[peerId];
|
||||
}
|
||||
|
||||
_onPeerLeft(peerId) {
|
||||
if (!this.peers[peerId]?.rtcSupported) {
|
||||
console.log('WSPeer left:', peerId)
|
||||
Events.fire('peer-disconnected', peerId)
|
||||
}
|
||||
}
|
||||
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
if (peer._roomSecret === roomSecret) {
|
||||
this._onPeerLeft(peerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FileChunker {
|
||||
|
||||
constructor(file, onChunk, onPartitionEnd) {
|
||||
this._chunkSize = 64000; // 64 KB
|
||||
this._maxPartitionSize = 1e6; // 1 MB
|
||||
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()) {
|
||||
this._onPartitionEnd(this._offset);
|
||||
return;
|
||||
}
|
||||
this._readChunk();
|
||||
}
|
||||
|
||||
repeatPartition() {
|
||||
this._offset -= this._partitionSize;
|
||||
this.nextPartition();
|
||||
}
|
||||
|
||||
_isPartitionEnd() {
|
||||
return this._partitionSize >= this._maxPartitionSize;
|
||||
}
|
||||
|
||||
isFileEnd() {
|
||||
return this._offset >= this._file.size;
|
||||
}
|
||||
}
|
||||
|
||||
class FileDigester {
|
||||
|
||||
constructor(meta, totalSize, totalBytesReceived, callback) {
|
||||
this._buffer = [];
|
||||
this._bytesReceived = 0;
|
||||
this._size = meta.size;
|
||||
this._name = meta.name;
|
||||
this._mime = meta.mime;
|
||||
this._totalSize = totalSize;
|
||||
this._totalBytesReceived = totalBytesReceived;
|
||||
this._callback = callback;
|
||||
}
|
||||
|
||||
unchunk(chunk) {
|
||||
this._buffer.push(chunk);
|
||||
this._bytesReceived += chunk.byteLength || chunk.size;
|
||||
this.progress = (this._totalBytesReceived + this._bytesReceived) / this._totalSize;
|
||||
if (isNaN(this.progress)) this.progress = 1
|
||||
|
||||
if (this._bytesReceived < this._size) return;
|
||||
// we are done
|
||||
const blob = new Blob(this._buffer)
|
||||
this._buffer = null;
|
||||
this._callback(new File([blob], this._name, {
|
||||
type: this._mime,
|
||||
lastModified: new Date().getTime()
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Events {
|
||||
static fire(type, detail) {
|
||||
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
|
||||
}
|
||||
|
||||
static on(type, callback) {
|
||||
return window.addEventListener(type, callback, false);
|
||||
}
|
||||
|
||||
static off(type, callback) {
|
||||
return window.removeEventListener(type, callback, false);
|
||||
}
|
||||
}
|
||||
|
||||
RTCPeer.config = {
|
||||
'sdpSemantics': 'unified-plan',
|
||||
'iceServers': [
|
||||
{
|
||||
urls: 'stun:stun.l.google.com:19302'
|
||||
},
|
||||
{
|
||||
urls: 'stun:openrelay.metered.ca:80'
|
||||
},
|
||||
{
|
||||
urls: 'turn:openrelay.metered.ca:443',
|
||||
username: 'openrelayproject',
|
||||
credential: 'openrelayproject',
|
||||
},
|
||||
]
|
||||
}
|
2
public_included_ws_fallback/scripts/qrcode.js
Normal file
2
public_included_ws_fallback/scripts/qrcode.js
Normal file
File diff suppressed because one or more lines are too long
39
public_included_ws_fallback/scripts/theme.js
Normal file
39
public_included_ws_fallback/scripts/theme.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
(function(){
|
||||
|
||||
// Select the button
|
||||
const btnTheme = document.getElementById('theme');
|
||||
// Check for dark mode preference at the OS level
|
||||
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
// Get the user's theme preference from local storage, if it's available
|
||||
const currentTheme = localStorage.getItem('theme');
|
||||
// If the user's preference in localStorage is dark...
|
||||
if (currentTheme === 'dark') {
|
||||
// ...let's toggle the .dark-theme class on the body
|
||||
document.body.classList.toggle('dark-theme');
|
||||
// Otherwise, if the user's preference in localStorage is light...
|
||||
} else if (currentTheme === 'light') {
|
||||
// ...let's toggle the .light-theme class on the body
|
||||
document.body.classList.toggle('light-theme');
|
||||
}
|
||||
|
||||
// Listen for a click on the button
|
||||
btnTheme.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// If the user's OS setting is dark and matches our .dark-theme class...
|
||||
let theme;
|
||||
if (prefersDarkScheme.matches) {
|
||||
// ...then toggle the light mode class
|
||||
document.body.classList.toggle('light-theme');
|
||||
// ...but use .dark-theme if the .light-theme class is already on the body,
|
||||
theme = document.body.classList.contains('light-theme') ? 'light' : 'dark';
|
||||
} else {
|
||||
// Otherwise, let's do the same thing, but for .dark-theme
|
||||
document.body.classList.toggle('dark-theme');
|
||||
theme = document.body.classList.contains('dark-theme') ? 'dark' : 'light';
|
||||
}
|
||||
// Finally, let's save the current preference to localStorage to keep using it
|
||||
localStorage.setItem('theme', theme);
|
||||
});
|
||||
|
||||
})();
|
1717
public_included_ws_fallback/scripts/ui.js
Normal file
1717
public_included_ws_fallback/scripts/ui.js
Normal file
File diff suppressed because it is too large
Load diff
402
public_included_ws_fallback/scripts/util.js
Normal file
402
public_included_ws_fallback/scripts/util.js
Normal file
|
@ -0,0 +1,402 @@
|
|||
// Polyfill for Navigator.clipboard.writeText
|
||||
if (!navigator.clipboard) {
|
||||
navigator.clipboard = {
|
||||
writeText: text => {
|
||||
|
||||
// A <span> contains the text to copy
|
||||
const span = document.createElement('span');
|
||||
span.textContent = text;
|
||||
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
|
||||
|
||||
// Paint the span outside the viewport
|
||||
span.style.position = 'absolute';
|
||||
span.style.left = '-9999px';
|
||||
span.style.top = '-9999px';
|
||||
|
||||
const win = window;
|
||||
const selection = win.getSelection();
|
||||
win.document.body.appendChild(span);
|
||||
|
||||
const range = win.document.createRange();
|
||||
selection.removeAllRanges();
|
||||
range.selectNode(span);
|
||||
selection.addRange(range);
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
success = win.document.execCommand('copy');
|
||||
} catch (err) {
|
||||
return Promise.error();
|
||||
}
|
||||
|
||||
selection.removeAllRanges();
|
||||
span.remove();
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const zipper = (() => {
|
||||
|
||||
let zipWriter;
|
||||
return {
|
||||
createNewZipWriter() {
|
||||
zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), { bufferedWrite: true, level: 0 });
|
||||
},
|
||||
addFile(file, options) {
|
||||
return zipWriter.add(file.name, new zip.BlobReader(file), options);
|
||||
},
|
||||
async getBlobURL() {
|
||||
if (zipWriter) {
|
||||
const blobURL = URL.createObjectURL(await zipWriter.close());
|
||||
zipWriter = null;
|
||||
return blobURL;
|
||||
} else {
|
||||
throw new Error("Zip file closed");
|
||||
}
|
||||
},
|
||||
async getZipFile(filename = "archive.zip") {
|
||||
if (zipWriter) {
|
||||
const file = new File([await zipWriter.close()], filename, {type: "application/zip"});
|
||||
zipWriter = null;
|
||||
return file;
|
||||
} else {
|
||||
throw new Error("Zip file closed");
|
||||
}
|
||||
},
|
||||
async getEntries(file, options) {
|
||||
return await (new zip.ZipReader(new zip.BlobReader(file))).getEntries(options);
|
||||
},
|
||||
async getData(entry, options) {
|
||||
return await entry.getData(new zip.BlobWriter(), options);
|
||||
},
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
const mime = (() => {
|
||||
|
||||
return {
|
||||
getMimeByFilename(filename) {
|
||||
try {
|
||||
const arr = filename.split('.');
|
||||
const suffix = arr[arr.length - 1].toLowerCase();
|
||||
return {
|
||||
"cpl": "application/cpl+xml",
|
||||
"gpx": "application/gpx+xml",
|
||||
"gz": "application/gzip",
|
||||
"jar": "application/java-archive",
|
||||
"war": "application/java-archive",
|
||||
"ear": "application/java-archive",
|
||||
"class": "application/java-vm",
|
||||
"js": "application/javascript",
|
||||
"mjs": "application/javascript",
|
||||
"json": "application/json",
|
||||
"map": "application/json",
|
||||
"webmanifest": "application/manifest+json",
|
||||
"doc": "application/msword",
|
||||
"dot": "application/msword",
|
||||
"wiz": "application/msword",
|
||||
"bin": "application/octet-stream",
|
||||
"dms": "application/octet-stream",
|
||||
"lrf": "application/octet-stream",
|
||||
"mar": "application/octet-stream",
|
||||
"so": "application/octet-stream",
|
||||
"dist": "application/octet-stream",
|
||||
"distz": "application/octet-stream",
|
||||
"pkg": "application/octet-stream",
|
||||
"bpk": "application/octet-stream",
|
||||
"dump": "application/octet-stream",
|
||||
"elc": "application/octet-stream",
|
||||
"deploy": "application/octet-stream",
|
||||
"img": "application/octet-stream",
|
||||
"msp": "application/octet-stream",
|
||||
"msm": "application/octet-stream",
|
||||
"buffer": "application/octet-stream",
|
||||
"oda": "application/oda",
|
||||
"oxps": "application/oxps",
|
||||
"pdf": "application/pdf",
|
||||
"asc": "application/pgp-signature",
|
||||
"sig": "application/pgp-signature",
|
||||
"prf": "application/pics-rules",
|
||||
"p7c": "application/pkcs7-mime",
|
||||
"cer": "application/pkix-cert",
|
||||
"ai": "application/postscript",
|
||||
"eps": "application/postscript",
|
||||
"ps": "application/postscript",
|
||||
"apk": "application/vnd.android.package-archive",
|
||||
"m3u8": "application/vnd.apple.mpegurl",
|
||||
"pkpass": "application/vnd.apple.pkpass",
|
||||
"kml": "application/vnd.google-earth.kml+xml",
|
||||
"kmz": "application/vnd.google-earth.kmz",
|
||||
"cab": "application/vnd.ms-cab-compressed",
|
||||
"xls": "application/vnd.ms-excel",
|
||||
"xlm": "application/vnd.ms-excel",
|
||||
"xla": "application/vnd.ms-excel",
|
||||
"xlc": "application/vnd.ms-excel",
|
||||
"xlt": "application/vnd.ms-excel",
|
||||
"xlw": "application/vnd.ms-excel",
|
||||
"msg": "application/vnd.ms-outlook",
|
||||
"ppt": "application/vnd.ms-powerpoint",
|
||||
"pot": "application/vnd.ms-powerpoint",
|
||||
"ppa": "application/vnd.ms-powerpoint",
|
||||
"pps": "application/vnd.ms-powerpoint",
|
||||
"pwz": "application/vnd.ms-powerpoint",
|
||||
"mpp": "application/vnd.ms-project",
|
||||
"mpt": "application/vnd.ms-project",
|
||||
"xps": "application/vnd.ms-xpsdocument",
|
||||
"odb": "application/vnd.oasis.opendocument.database",
|
||||
"ods": "application/vnd.oasis.opendocument.spreadsheet",
|
||||
"odt": "application/vnd.oasis.opendocument.text",
|
||||
"osm": "application/vnd.openstreetmap.data+xml",
|
||||
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"pcap": "application/vnd.tcpdump.pcap",
|
||||
"cap": "application/vnd.tcpdump.pcap",
|
||||
"dmp": "application/vnd.tcpdump.pcap",
|
||||
"wpd": "application/vnd.wordperfect",
|
||||
"wasm": "application/wasm",
|
||||
"7z": "application/x-7z-compressed",
|
||||
"dmg": "application/x-apple-diskimage",
|
||||
"bcpio": "application/x-bcpio",
|
||||
"torrent": "application/x-bittorrent",
|
||||
"cbr": "application/x-cbr",
|
||||
"cba": "application/x-cbr",
|
||||
"cbt": "application/x-cbr",
|
||||
"cbz": "application/x-cbr",
|
||||
"cb7": "application/x-cbr",
|
||||
"vcd": "application/x-cdlink",
|
||||
"crx": "application/x-chrome-extension",
|
||||
"cpio": "application/x-cpio",
|
||||
"csh": "application/x-csh",
|
||||
"deb": "application/x-debian-package",
|
||||
"udeb": "application/x-debian-package",
|
||||
"dvi": "application/x-dvi",
|
||||
"arc": "application/x-freearc",
|
||||
"gtar": "application/x-gtar",
|
||||
"hdf": "application/x-hdf",
|
||||
"h5": "application/x-hdf5",
|
||||
"php": "application/x-httpd-php",
|
||||
"iso": "application/x-iso9660-image",
|
||||
"key": "application/x-iwork-keynote-sffkey",
|
||||
"numbers": "application/x-iwork-numbers-sffnumbers",
|
||||
"pages": "application/x-iwork-pages-sffpages",
|
||||
"latex": "application/x-latex",
|
||||
"run": "application/x-makeself",
|
||||
"mif": "application/x-mif",
|
||||
"lnk": "application/x-ms-shortcut",
|
||||
"mdb": "application/x-msaccess",
|
||||
"exe": "application/x-msdownload",
|
||||
"dll": "application/x-msdownload",
|
||||
"com": "application/x-msdownload",
|
||||
"bat": "application/x-msdownload",
|
||||
"msi": "application/x-msdownload",
|
||||
"pub": "application/x-mspublisher",
|
||||
"cdf": "application/x-netcdf",
|
||||
"nc": "application/x-netcdf",
|
||||
"pl": "application/x-perl",
|
||||
"pm": "application/x-perl",
|
||||
"prc": "application/x-pilot",
|
||||
"pdb": "application/x-pilot",
|
||||
"p12": "application/x-pkcs12",
|
||||
"pfx": "application/x-pkcs12",
|
||||
"ram": "application/x-pn-realaudio",
|
||||
"pyc": "application/x-python-code",
|
||||
"pyo": "application/x-python-code",
|
||||
"rar": "application/x-rar-compressed",
|
||||
"rpm": "application/x-redhat-package-manager",
|
||||
"sh": "application/x-sh",
|
||||
"shar": "application/x-shar",
|
||||
"swf": "application/x-shockwave-flash",
|
||||
"sql": "application/x-sql",
|
||||
"srt": "application/x-subrip",
|
||||
"sv4cpio": "application/x-sv4cpio",
|
||||
"sv4crc": "application/x-sv4crc",
|
||||
"gam": "application/x-tads",
|
||||
"tar": "application/x-tar",
|
||||
"tcl": "application/x-tcl",
|
||||
"tex": "application/x-tex",
|
||||
"roff": "application/x-troff",
|
||||
"t": "application/x-troff",
|
||||
"tr": "application/x-troff",
|
||||
"man": "application/x-troff-man",
|
||||
"me": "application/x-troff-me",
|
||||
"ms": "application/x-troff-ms",
|
||||
"ustar": "application/x-ustar",
|
||||
"src": "application/x-wais-source",
|
||||
"xpi": "application/x-xpinstall",
|
||||
"xhtml": "application/xhtml+xml",
|
||||
"xht": "application/xhtml+xml",
|
||||
"xsl": "application/xml",
|
||||
"rdf": "application/xml",
|
||||
"wsdl": "application/xml",
|
||||
"xpdl": "application/xml",
|
||||
"zip": "application/zip",
|
||||
"3gp": "audio/3gp",
|
||||
"3gpp": "audio/3gpp",
|
||||
"3g2": "audio/3gpp2",
|
||||
"3gpp2": "audio/3gpp2",
|
||||
"aac": "audio/aac",
|
||||
"adts": "audio/aac",
|
||||
"loas": "audio/aac",
|
||||
"ass": "audio/aac",
|
||||
"au": "audio/basic",
|
||||
"snd": "audio/basic",
|
||||
"mid": "audio/midi",
|
||||
"midi": "audio/midi",
|
||||
"kar": "audio/midi",
|
||||
"rmi": "audio/midi",
|
||||
"mpga": "audio/mpeg",
|
||||
"mp2": "audio/mpeg",
|
||||
"mp2a": "audio/mpeg",
|
||||
"mp3": "audio/mpeg",
|
||||
"m2a": "audio/mpeg",
|
||||
"m3a": "audio/mpeg",
|
||||
"oga": "audio/ogg",
|
||||
"ogg": "audio/ogg",
|
||||
"spx": "audio/ogg",
|
||||
"opus": "audio/opus",
|
||||
"aif": "audio/x-aiff",
|
||||
"aifc": "audio/x-aiff",
|
||||
"aiff": "audio/x-aiff",
|
||||
"flac": "audio/x-flac",
|
||||
"m4a": "audio/x-m4a",
|
||||
"m3u": "audio/x-mpegurl",
|
||||
"wma": "audio/x-ms-wma",
|
||||
"ra": "audio/x-pn-realaudio",
|
||||
"wav": "audio/x-wav",
|
||||
"otf": "font/otf",
|
||||
"ttf": "font/ttf",
|
||||
"woff": "font/woff",
|
||||
"woff2": "font/woff2",
|
||||
"emf": "image/emf",
|
||||
"gif": "image/gif",
|
||||
"heic": "image/heic",
|
||||
"heif": "image/heif",
|
||||
"ief": "image/ief",
|
||||
"jpeg": "image/jpeg",
|
||||
"jpg": "image/jpeg",
|
||||
"pict": "image/pict",
|
||||
"pct": "image/pict",
|
||||
"pic": "image/pict",
|
||||
"png": "image/png",
|
||||
"svg": "image/svg+xml",
|
||||
"svgz": "image/svg+xml",
|
||||
"tif": "image/tiff",
|
||||
"tiff": "image/tiff",
|
||||
"psd": "image/vnd.adobe.photoshop",
|
||||
"djvu": "image/vnd.djvu",
|
||||
"djv": "image/vnd.djvu",
|
||||
"dwg": "image/vnd.dwg",
|
||||
"dxf": "image/vnd.dxf",
|
||||
"dds": "image/vnd.ms-dds",
|
||||
"webp": "image/webp",
|
||||
"3ds": "image/x-3ds",
|
||||
"ras": "image/x-cmu-raster",
|
||||
"ico": "image/x-icon",
|
||||
"bmp": "image/x-ms-bmp",
|
||||
"pnm": "image/x-portable-anymap",
|
||||
"pbm": "image/x-portable-bitmap",
|
||||
"pgm": "image/x-portable-graymap",
|
||||
"ppm": "image/x-portable-pixmap",
|
||||
"rgb": "image/x-rgb",
|
||||
"tga": "image/x-tga",
|
||||
"xbm": "image/x-xbitmap",
|
||||
"xpm": "image/x-xpixmap",
|
||||
"xwd": "image/x-xwindowdump",
|
||||
"eml": "message/rfc822",
|
||||
"mht": "message/rfc822",
|
||||
"mhtml": "message/rfc822",
|
||||
"nws": "message/rfc822",
|
||||
"obj": "model/obj",
|
||||
"stl": "model/stl",
|
||||
"dae": "model/vnd.collada+xml",
|
||||
"ics": "text/calendar",
|
||||
"ifb": "text/calendar",
|
||||
"css": "text/css",
|
||||
"csv": "text/csv",
|
||||
"html": "text/html",
|
||||
"htm": "text/html",
|
||||
"shtml": "text/html",
|
||||
"markdown": "text/markdown",
|
||||
"md": "text/markdown",
|
||||
"txt": "text/plain",
|
||||
"text": "text/plain",
|
||||
"conf": "text/plain",
|
||||
"def": "text/plain",
|
||||
"list": "text/plain",
|
||||
"log": "text/plain",
|
||||
"in": "text/plain",
|
||||
"ini": "text/plain",
|
||||
"rtx": "text/richtext",
|
||||
"rtf": "text/rtf",
|
||||
"tsv": "text/tab-separated-values",
|
||||
"c": "text/x-c",
|
||||
"cc": "text/x-c",
|
||||
"cxx": "text/x-c",
|
||||
"cpp": "text/x-c",
|
||||
"h": "text/x-c",
|
||||
"hh": "text/x-c",
|
||||
"dic": "text/x-c",
|
||||
"java": "text/x-java-source",
|
||||
"lua": "text/x-lua",
|
||||
"py": "text/x-python",
|
||||
"etx": "text/x-setext",
|
||||
"sgm": "text/x-sgml",
|
||||
"sgml": "text/x-sgml",
|
||||
"vcf": "text/x-vcard",
|
||||
"xml": "text/xml",
|
||||
"xul": "text/xul",
|
||||
"yaml": "text/yaml",
|
||||
"yml": "text/yaml",
|
||||
"ts": "video/mp2t",
|
||||
"mp4": "video/mp4",
|
||||
"mp4v": "video/mp4",
|
||||
"mpg4": "video/mp4",
|
||||
"mpeg": "video/mpeg",
|
||||
"m1v": "video/mpeg",
|
||||
"mpa": "video/mpeg",
|
||||
"mpe": "video/mpeg",
|
||||
"mpg": "video/mpeg",
|
||||
"mov": "video/quicktime",
|
||||
"qt": "video/quicktime",
|
||||
"webm": "video/webm",
|
||||
"flv": "video/x-flv",
|
||||
"m4v": "video/x-m4v",
|
||||
"asf": "video/x-ms-asf",
|
||||
"asx": "video/x-ms-asf",
|
||||
"vob": "video/x-ms-vob",
|
||||
"wmv": "video/x-ms-wmv",
|
||||
"avi": "video/x-msvideo",
|
||||
"*": "video/x-sgi-movie",
|
||||
}[suffix] || '';
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
var binary = '';
|
||||
var bytes = new Uint8Array(buffer);
|
||||
var len = bytes.byteLength;
|
||||
for (var i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa( binary );
|
||||
}
|
||||
|
||||
function base64ToArrayBuffer(base64) {
|
||||
var binary_string = window.atob(base64);
|
||||
var len = binary_string.length;
|
||||
var bytes = new Uint8Array(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
1
public_included_ws_fallback/scripts/zip.min.js
vendored
Normal file
1
public_included_ws_fallback/scripts/zip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue