diff --git a/README.md b/README.md
index 0602ff0..fc8ce2f 100644
--- a/README.md
+++ b/README.md
@@ -20,8 +20,12 @@
## Features
[PairDrop](https://pairdrop.net) is a sublime alternative to AirDrop that works on all platforms.
-Send images, documents or text via peer to peer connection to devices in the same local network/Wi-Fi or to paired devices.
-As it is web based, it runs on all devices.
+- File Sharing on your local network
+ - Send images, documents or text via peer to peer connection to devices on the same local network.
+- Internet Transfers
+ - Join temporary public rooms to transfer files easily over the internet!
+- Web-Application
+ - As it is web based, it runs on all devices.
You want to quickly send a file from your phone to your laptop?
You want to share photos in original quality with friends that use a mixture of Android and iOS?
@@ -32,14 +36,29 @@ You want to quickly send a file from your phone to your laptop?
Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
## Differences to Snapdrop
+Click to expand
-### Device Pairing / Internet Transfer
-* Pair devices via 6-digit code or QR-Code
-* Pair devices outside your local network or in complex network environment (public Wi-Fi, company network, Apple Private Relay, VPN etc.).
+### Paired Devices and Public Rooms - Internet Transfer
+* Transfer files over the internet between paired devices or by entering temporary public rooms.
+* Connect to devices in complex network environments (public Wi-Fi, company network, Apple Private Relay, VPN etc.).
* Connect to devices on your mobile hotspot.
-* Paired devices will always find each other via shared secrets even after reopening the browser or the Progressive Web App
-* You will always discover devices on your local network. Paired devices are shown additionally.
-* Paired devices outside your local network that are behind a NAT are connected automatically via the PairDrop TURN server.
+* Devices outside your local network that are behind a NAT are connected automatically via the PairDrop TURN server.
+* Connect to devices on your mobile hotspot.
+* You will always discover devices on your local network. Paired devices and devices in the same public room are shown additionally.
+
+#### Persistent Device Pairing
+* Pair your devices via a 6-digit code or a QR-Code.
+* Paired devices will always find each other via shared secrets independently of their local network.
+* Paired devices are persistent. You find your devices even after reopening PairDrop.
+* You can edit and unpair devices easily
+* Ideal to always connect easily to your own devices
+
+#### Temporary Public Rooms
+* Enter a public room via a 5-letter code or a QR-Code.
+* Enter a public room to temporarily connect to devices outside your local network.
+* All devices in the same public room see each other mutually.
+* Public rooms are temporary. Public rooms are left as soon as PairDrop is closed.
+* Ideal to connect easily to others in complex network situations or over the internet.
### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
* Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
@@ -66,13 +85,12 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
* When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers)
+* Built-in translations
+
+
## PairDrop is built with the following awesome technologies:
* Vanilla HTML5 / ES6 / CSS3 frontend
@@ -82,23 +100,29 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
* [zip.js](https://gildas-lormeau.github.io/zip.js/)
* [cyrb53](https://github.com/bryc) super fast hash function
+* [Weblate](https://weblate.org/) Web based localization tool
Have any questions? Read our [FAQ](/docs/faq.md).
You can [host your own instance with Docker](/docs/host-your-own.md).
-## Support the Community
-PairDrop is free and always will be. Still, we have to pay for the domain and the server.
-
-To contribute and support:
+PairDrop is free and always will be.
+Still, we have to pay for the domain and the server.
+
+To contribute and support, please use BuyMeACoffee via the button above.
+
Thanks a lot for supporting free and open software!
-To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop).
+## Translate PairDrop
+
+
+
## How to contribute
diff --git a/index.js b/index.js
index 029a955..1d08d7d 100644
--- a/index.js
+++ b/index.js
@@ -130,8 +130,10 @@ class PairDropServer {
this._wss = new WebSocket.Server({ server });
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
- this._rooms = {};
- this._roomSecrets = {};
+ this._rooms = {}; // { roomId: peers[] }
+ this._roomSecrets = {}; // { pairKey: roomSecret }
+
+ this._keepAliveTimers = {};
console.log('PairDrop is running on port', port);
}
@@ -139,7 +141,9 @@ class PairDropServer {
_onConnection(peer) {
peer.socket.on('message', message => this._onMessage(peer, message));
peer.socket.onerror = e => console.error(e);
+
this._keepAlive(peer);
+
this._send(peer, {
type: 'rtc-config',
config: rtcConfig
@@ -170,10 +174,10 @@ class PairDropServer {
this._onDisconnect(sender);
break;
case 'pong':
- sender.lastBeat = Date.now();
+ this._keepAliveTimers[sender.id].lastBeat = Date.now();
break;
case 'join-ip-room':
- this._joinRoom(sender);
+ this._joinIpRoom(sender);
break;
case 'room-secrets':
this._onRoomSecrets(sender, message);
@@ -192,9 +196,15 @@ class PairDropServer {
break;
case 'regenerate-room-secret':
this._onRegenerateRoomSecret(sender, message);
- break
- case 'resend-peers':
- this._notifyPeers(sender);
+ break;
+ case 'create-public-room':
+ this._onCreatePublicRoom(sender);
+ break;
+ case 'join-public-room':
+ this._onJoinPublicRoom(sender, message);
+ break;
+ case 'leave-public-room':
+ this._onLeavePublicRoom(sender);
break;
case 'signal':
default:
@@ -203,7 +213,9 @@ class PairDropServer {
}
_signalAndRelay(sender, message) {
- const room = message.roomType === 'ip' ? sender.ip : message.roomSecret;
+ const room = message.roomType === 'ip'
+ ? sender.ip
+ : message.roomId;
// relay message to recipient
if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) {
@@ -223,10 +235,16 @@ class PairDropServer {
}
_disconnect(sender) {
- this._leaveRoom(sender, 'ip', '', true);
+ this._removePairKey(sender.pairKey);
+ sender.pairKey = null;
+
+ this._cancelKeepAlive(sender);
+ delete this._keepAliveTimers[sender.id];
+
+ this._leaveIpRoom(sender, true);
this._leaveAllSecretRooms(sender, true);
- this._removeRoomKey(sender.roomKey);
- sender.roomKey = null;
+ this._leavePublicRoom(sender, true);
+
sender.socket.terminate();
}
@@ -255,7 +273,7 @@ class PairDropServer {
for (const peerId in room) {
const peer = room[peerId];
- this._leaveRoom(peer, 'secret', roomSecret);
+ this._leaveSecretRoom(peer, roomSecret, true);
this._send(peer, {
type: 'secret-room-deleted',
@@ -266,34 +284,35 @@ class PairDropServer {
_onPairDeviceInitiate(sender) {
let roomSecret = randomizer.getRandomString(256);
- let roomKey = this._createRoomKey(sender, roomSecret);
- if (sender.roomKey) this._removeRoomKey(sender.roomKey);
- sender.roomKey = roomKey;
+ let pairKey = this._createPairKey(sender, roomSecret);
+
+ if (sender.pairKey) {
+ this._removePairKey(sender.pairKey);
+ }
+ sender.pairKey = pairKey;
+
this._send(sender, {
type: 'pair-device-initiated',
roomSecret: roomSecret,
- roomKey: roomKey
+ pairKey: pairKey
});
- this._joinRoom(sender, 'secret', roomSecret);
+ this._joinSecretRoom(sender, roomSecret);
}
_onPairDeviceJoin(sender, message) {
- // rate limit implementation: max 10 attempts every 10s
- if (sender.roomKeyRate >= 10) {
- this._send(sender, { type: 'pair-device-join-key-rate-limit' });
+ if (sender.rateLimitReached()) {
+ this._send(sender, { type: 'join-key-rate-limit' });
return;
}
- sender.roomKeyRate += 1;
- setTimeout(_ => sender.roomKeyRate -= 1, 10000);
- if (!this._roomSecrets[message.roomKey] || sender.id === this._roomSecrets[message.roomKey].creator.id) {
+ if (!this._roomSecrets[message.pairKey] || sender.id === this._roomSecrets[message.pairKey].creator.id) {
this._send(sender, { type: 'pair-device-join-key-invalid' });
return;
}
- const roomSecret = this._roomSecrets[message.roomKey].roomSecret;
- const creator = this._roomSecrets[message.roomKey].creator;
- this._removeRoomKey(message.roomKey);
+ const roomSecret = this._roomSecrets[message.pairKey].roomSecret;
+ const creator = this._roomSecrets[message.pairKey].creator;
+ this._removePairKey(message.pairKey);
this._send(sender, {
type: 'pair-device-joined',
roomSecret: roomSecret,
@@ -304,22 +323,53 @@ class PairDropServer {
roomSecret: roomSecret,
peerId: sender.id
});
- this._joinRoom(sender, 'secret', roomSecret);
- this._removeRoomKey(sender.roomKey);
+ this._joinSecretRoom(sender, roomSecret);
+ this._removePairKey(sender.pairKey);
}
_onPairDeviceCancel(sender) {
- const roomKey = sender.roomKey
+ const pairKey = sender.pairKey
- if (!roomKey) return;
+ if (!pairKey) return;
- this._removeRoomKey(roomKey);
+ this._removePairKey(pairKey);
this._send(sender, {
type: 'pair-device-canceled',
- roomKey: roomKey,
+ pairKey: pairKey,
});
}
+ _onCreatePublicRoom(sender) {
+ let publicRoomId = randomizer.getRandomString(5, true).toLowerCase();
+
+ this._send(sender, {
+ type: 'public-room-created',
+ roomId: publicRoomId
+ });
+
+ this._joinPublicRoom(sender, publicRoomId);
+ }
+
+ _onJoinPublicRoom(sender, message) {
+ if (sender.rateLimitReached()) {
+ this._send(sender, { type: 'join-key-rate-limit' });
+ return;
+ }
+
+ if (!this._rooms[message.publicRoomId] && !message.createIfInvalid) {
+ this._send(sender, { type: 'public-room-id-invalid', publicRoomId: message.publicRoomId });
+ return;
+ }
+
+ this._leavePublicRoom(sender);
+ this._joinPublicRoom(sender, message.publicRoomId);
+ }
+
+ _onLeavePublicRoom(sender) {
+ this._leavePublicRoom(sender, true);
+ this._send(sender, { type: 'public-room-left' });
+ }
+
_onRegenerateRoomSecret(sender, message) {
const oldRoomSecret = message.roomSecret;
const newRoomSecret = randomizer.getRandomString(256);
@@ -337,122 +387,158 @@ class PairDropServer {
delete this._rooms[oldRoomSecret];
}
- _createRoomKey(creator, roomSecret) {
- let roomKey;
+ _createPairKey(creator, roomSecret) {
+ let pairKey;
do {
// get randomInt until keyRoom not occupied
- roomKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s
- } while (roomKey in this._roomSecrets)
+ pairKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s
+ } while (pairKey in this._roomSecrets)
- this._roomSecrets[roomKey] = {
+ this._roomSecrets[pairKey] = {
roomSecret: roomSecret,
creator: creator
}
- return roomKey;
+ return pairKey;
}
- _removeRoomKey(roomKey) {
+ _removePairKey(roomKey) {
if (roomKey in this._roomSecrets) {
this._roomSecrets[roomKey].creator.roomKey = null
delete this._roomSecrets[roomKey];
}
}
- _joinRoom(peer, roomType = 'ip', roomSecret = '') {
- const room = roomType === 'ip' ? peer.ip : roomSecret;
+ _joinIpRoom(peer) {
+ this._joinRoom(peer, 'ip', peer.ip);
+ }
- if (this._rooms[room] && this._rooms[room][peer.id]) {
+ _joinSecretRoom(peer, roomSecret) {
+ this._joinRoom(peer, 'secret', roomSecret);
+
+ // add secret to peer
+ peer.addRoomSecret(roomSecret);
+ }
+
+ _joinPublicRoom(peer, publicRoomId) {
+ // prevent joining of 2 public rooms simultaneously
+ this._leavePublicRoom(peer);
+
+ this._joinRoom(peer, 'public-id', publicRoomId);
+
+ peer.publicRoomId = publicRoomId;
+ }
+
+ _joinRoom(peer, roomType, roomId) {
+ // roomType: 'ip', 'secret' or 'public-id'
+ if (this._rooms[roomId] && this._rooms[roomId][peer.id]) {
// ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect.
- this._leaveRoom(peer, roomType, roomSecret);
+ this._leaveRoom(peer, roomType, roomId);
}
// if room doesn't exist, create it
- if (!this._rooms[room]) {
- this._rooms[room] = {};
+ if (!this._rooms[roomId]) {
+ this._rooms[roomId] = {};
}
- this._notifyPeers(peer, roomType, roomSecret);
+ this._notifyPeers(peer, roomType, roomId);
// add peer to room
- this._rooms[room][peer.id] = peer;
- // add secret to peer
- if (roomType === 'secret') {
- peer.addRoomSecret(roomSecret);
- }
+ this._rooms[roomId][peer.id] = peer;
}
- _leaveRoom(peer, roomType = 'ip', roomSecret = '', disconnect = false) {
- const room = roomType === 'ip' ? peer.ip : roomSecret;
- if (!this._rooms[room] || !this._rooms[room][peer.id]) return;
- this._cancelKeepAlive(this._rooms[room][peer.id]);
+ _leaveIpRoom(peer, disconnect = false) {
+ this._leaveRoom(peer, 'ip', peer.ip, disconnect);
+ }
- // delete the peer
- delete this._rooms[room][peer.id];
+ _leaveSecretRoom(peer, roomSecret, disconnect = false) {
+ this._leaveRoom(peer, 'secret', roomSecret, disconnect)
- //if room is empty, delete the room
- if (!Object.keys(this._rooms[room]).length) {
- delete this._rooms[room];
- } else {
- // notify all other peers
- for (const otherPeerId in this._rooms[room]) {
- const otherPeer = this._rooms[room][otherPeerId];
- this._send(otherPeer, {
- type: 'peer-left',
- peerId: peer.id,
- roomType: roomType,
- roomSecret: roomSecret,
- disconnect: disconnect
- });
- }
- }
//remove secret from peer
- if (roomType === 'secret') {
- peer.removeRoomSecret(roomSecret);
+ peer.removeRoomSecret(roomSecret);
+ }
+
+ _leavePublicRoom(peer, disconnect = false) {
+ if (!peer.publicRoomId) return;
+
+ this._leaveRoom(peer, 'public-id', peer.publicRoomId, disconnect);
+
+ peer.publicRoomId = null;
+ }
+
+ _leaveRoom(peer, roomType, roomId, disconnect = false) {
+ if (!this._rooms[roomId] || !this._rooms[roomId][peer.id]) return;
+
+ // remove peer from room
+ delete this._rooms[roomId][peer.id];
+
+ // delete room if empty and abort
+ if (!Object.keys(this._rooms[roomId]).length) {
+ delete this._rooms[roomId];
+ return;
+ }
+
+ // notify all other peers that remain in room that peer left
+ for (const otherPeerId in this._rooms[roomId]) {
+ const otherPeer = this._rooms[roomId][otherPeerId];
+
+ let msg = {
+ type: 'peer-left',
+ peerId: peer.id,
+ roomType: roomType,
+ roomId: roomId,
+ disconnect: disconnect
+ };
+
+ this._send(otherPeer, msg);
}
}
- _notifyPeers(peer, roomType = 'ip', roomSecret = '') {
- const room = roomType === 'ip' ? peer.ip : roomSecret;
- if (!this._rooms[room]) return;
+ _notifyPeers(peer, roomType, roomId) {
+ if (!this._rooms[roomId]) return;
- // notify all other peers
- for (const otherPeerId in this._rooms[room]) {
+ // notify all other peers that peer joined
+ for (const otherPeerId in this._rooms[roomId]) {
if (otherPeerId === peer.id) continue;
- const otherPeer = this._rooms[room][otherPeerId];
- this._send(otherPeer, {
+ const otherPeer = this._rooms[roomId][otherPeerId];
+
+ let msg = {
type: 'peer-joined',
peer: peer.getInfo(),
roomType: roomType,
- roomSecret: roomSecret
- });
+ roomId: roomId
+ };
+
+ this._send(otherPeer, msg);
}
- // notify peer about the other peers
+ // notify peer about peers already in the room
const otherPeers = [];
- for (const otherPeerId in this._rooms[room]) {
+ for (const otherPeerId in this._rooms[roomId]) {
if (otherPeerId === peer.id) continue;
- otherPeers.push(this._rooms[room][otherPeerId].getInfo());
+ otherPeers.push(this._rooms[roomId][otherPeerId].getInfo());
}
- this._send(peer, {
+ let msg = {
type: 'peers',
peers: otherPeers,
roomType: roomType,
- roomSecret: roomSecret
- });
+ roomId: roomId
+ };
+
+ this._send(peer, msg);
}
_joinSecretRooms(peer, roomSecrets) {
for (let i=0; i