Compare commits

...

48 Commits

Author SHA1 Message Date
schlagmichdoch
f195c686e7 increase version to v1.7.5 2023-06-01 01:32:06 +02:00
schlagmichdoch
3505f161c6 strip 'NO-BREAK SPACE' (U+00A0) of received text as some browsers seem to add them when pasting text 2023-06-01 01:29:00 +02:00
schlagmichdoch
3e2368c0c9 stabilize connection on reconnect by terminating websocket only on timeout and not always when peer leaves its ip room 2023-06-01 01:26:53 +02:00
schlagmichdoch
d36cd3524c Fix clearBrowserHistory: url should not always be replaced by "/" as PairDrop might not always be hosted at domain root 2023-05-30 02:34:50 +02:00
schlagmichdoch
a3a8228327 increase version to v1.7.4 2023-05-26 20:37:38 +02:00
schlagmichdoch
520b772bc8 fix #112 and differentiate between textContent and innerText 2023-05-26 20:36:12 +02:00
schlagmichdoch
27bf0fa74f fix #113 2023-05-26 20:36:12 +02:00
schlagmichdoch
e9f3c39f38 Merge pull request #114 from fm-sys/patch-1
Add 'files-sent' event used by 'Snapdrop & PairDrop for Android' app to support vibrations
2023-05-26 20:35:54 +02:00
fm-sys
58b7f6bb7c Add 'files-sent' event 2023-05-26 09:52:17 +02:00
schlagmichdoch
b5987cf017 increase version to v1.7.3 2023-05-23 02:45:29 +02:00
schlagmichdoch
4433e1c58f add version number to about page 2023-05-23 02:44:25 +02:00
schlagmichdoch
b106d90b64 Fix ReferenceError: ipv6_lcl is not defined 2023-05-23 02:43:17 +02:00
schlagmichdoch
c9e1c2504a Merge pull request #110 from luckman212/luckman212-patch-1
Add new env var `IPV6_LOCALIZE` to enable auto discovery for IPv6 addresses
2023-05-23 01:53:23 +02:00
luckman212
32e909b8c2 fixes for https://github.com/schlagmichdoch/PairDrop/issues/69
(squashed, docs updated, IPV6_LOCALIZE input validation)
2023-05-19 12:18:20 -04:00
schlagmichdoch
a444be226f Fix canvas selector 2023-05-16 19:15:47 +02:00
schlagmichdoch
df778ba42c Speed up canvas by removing fade-in animation 2023-05-16 19:09:59 +02:00
schlagmichdoch
8a17b82fa4 Fix _textInputEmpty() for Chromium based browsers
Co-authored-by: luckman212 <1992842+luckman212@users.noreply.github.com>
2023-05-16 02:53:56 +02:00
schlagmichdoch
56eb29c91b increase version to v1.7.2 2023-05-16 02:35:03 +02:00
schlagmichdoch
6e4bda0adf Fix message sending via submit button.
Co-authored-by: luckman212 <1992842+luckman212@users.noreply.github.com>
2023-05-16 02:25:50 +02:00
Lopolin-LP
0baced640a Fix About Background not filling up full viewport under certain circumstances (#109)
* Fix About Background Not filling up full viewport under certain circumstances

It is now based on vw/vh instead of px. It can also easily be adjusted, mostly. There is no way it will not fill up the viewport.

* add fix for about bg size to websocket fallback too and tidy up

---------

Co-authored-by: schlagmichdoch <schlagmichdoch@users.noreply.github.com>
2023-05-16 01:50:12 +02:00
schlagmichdoch
3c2e73fc0c fix position of about background circle 2023-05-12 04:59:44 +02:00
schlagmichdoch
c629d7cd88 increase version to v1.7.1 2023-05-12 01:41:10 +02:00
schlagmichdoch
ba20c72026 fix error on empty roomSecrets 2023-05-12 01:16:37 +02:00
schlagmichdoch
347f9b87c0 fix check whether peer is same browser 2023-05-12 01:16:37 +02:00
schlagmichdoch
ae9909f596 fix notification "Key null invalidated" on cancel device pairing 2023-05-11 19:56:47 +02:00
schlagmichdoch
26c1878bb9 increase version to v1.7.0 2023-05-11 19:23:39 +02:00
schlagmichdoch
de0afce4ea Merge pull request #107 from schlagmichdoch/add_auto_accept
Add auto accept functionality via Edit Paired Devices Dialog + implement pair secret regeneration functionality
2023-05-11 19:21:26 +02:00
schlagmichdoch
2a837eb195 add 'visbilitychange' event support for older browsers 2023-05-10 21:59:45 +02:00
schlagmichdoch
fdf20cfdd9 save roomSecret and notify user that the pairing is successful only after the corresponding pairPeer has joined. 2023-05-10 21:59:45 +02:00
schlagmichdoch
7606fb398b Fix: notify user that "Selected peer left." only if dialog is shown. 2023-05-10 21:59:45 +02:00
schlagmichdoch
8d640be3a2 increase roomSecret length to 264 chars and implement roomSecret regeneration functionality 2023-05-10 21:59:45 +02:00
schlagmichdoch
241ea4f988 implement auto_accept (#91) and manual unpairing via new Edit Paired Devices Dialog and a BrowserTabsConnector 2023-05-10 21:59:43 +02:00
schlagmichdoch
0ac3c5a11f remove debugging logs 2023-05-04 17:39:40 +02:00
schlagmichdoch
f39bfedf98 use sha3-512 hash instead of cyrb53 to authenticate peerIds on reconnect 2023-05-04 17:34:33 +02:00
schlagmichdoch
fafdbcc829 increase version to v1.6.3 2023-04-27 19:17:41 +02:00
schlagmichdoch
b9806d4327 Merge pull request #70 from schlagmichdoch/add_ip_debugging_flag
Add debug mode to enable debugging auto discovery
2023-04-27 18:17:27 +02:00
schlagmichdoch
fb08bdaf36 add environment variable DEBUG_MODE to docs 2023-04-27 18:14:45 +02:00
schlagmichdoch
5a363e90dd add debug mode to enable debugging auto discovery 2023-04-24 17:00:03 +02:00
schlagmichdoch
8f4ce63a0c increase version to v1.6.2 2023-04-20 22:04:57 +02:00
schlagmichdoch
b42c8a0b1a remove background animation in favor of speed and efficiency 2023-04-20 22:02:00 +02:00
schlagmichdoch
4c7bdd3a0f move robots.txt into correct folder 2023-04-20 21:57:31 +02:00
schlagmichdoch
3f72fa1160 remove fade-in from description (LCP) on page load 2023-04-20 21:57:24 +02:00
schlagmichdoch
5c3f5ece7d increase seo by adding an aria-label and removing 'user-scalable=no' 2023-04-20 21:57:01 +02:00
schlagmichdoch
8de899f124 increase version to v1.6.1 2023-04-19 21:16:43 +02:00
schlagmichdoch
87097e9cd4 fix header btn shadow styling 2023-04-19 21:15:03 +02:00
schlagmichdoch
b2fc6415da include example files to run an own TURN server via coturn or via docker-compose 2023-04-19 17:38:14 +02:00
schlagmichdoch
2d8bbd5a79 Change docs to include the usage of our own TURN server instead of the TURN server of the Open Relay Project 2023-04-19 16:50:22 +02:00
schlagmichdoch
cae3bb7c7b Add server costs to README.md 2023-04-18 13:06:21 +02:00
22 changed files with 2155 additions and 832 deletions

View File

@@ -33,13 +33,13 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
## Differences to Snapdrop
### Device Pairing
### 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.).
* 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 [Open Relay: Free WebRTC TURN Server](https://www.metered.ca/tools/openrelay/)
* Paired devices outside your local network that are behind a NAT are connected automatically via the PairDrop TURN server.
### [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.
@@ -89,9 +89,9 @@ 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.
PairDrop is free and always will be. Still, we have to pay for the domain and the server.
To contribute and support me:<br>
To contribute and support:<br>
<a href="https://www.buymeacoffee.com/pairdrop" target="_blank">
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" >
</a>

19
docker-compose-coturn.yml Normal file
View File

@@ -0,0 +1,19 @@
version: "3"
services:
node:
image: "node:lts-alpine"
user: "node"
working_dir: /home/node/app
volumes:
- ./:/home/node/app
command: ash -c "npm i && npm run start:prod"
restart: unless-stopped
ports:
- "3000:3000"
coturn_server:
image: "coturn/coturn"
restart: always
network_mode: "host"
volumes:
- ./turnserver.conf:/etc/coturn/turnserver.conf
#you need to copy turnserver_example.conf to turnserver.conf and specify domain, IP address, user and password

View File

@@ -104,10 +104,13 @@ Here's a list of some third-party apps compatible with PairDrop:
What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server?
</summary>
It uses a P2P connection if WebRTC is supported by the browser. WebRTC needs a Signaling Server, but it is only used to establish a connection and is not involved in the file transfer.
It uses a WebRTC peer to peer connection. WebRTC needs a Signaling Server that is only used to establish a connection. The server is not involved in the file transfer.
If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages.
If devices are on the same network, none of your files are ever sent to any server.
If your devices are paired and behind a NAT, the PairDrop TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC.
If you host your own instance and want to support devices that do not support WebRTC, you can [start the PairDrop instance with an activated Websocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn).
<br>
@@ -118,11 +121,12 @@ If your devices are paired and behind a NAT, the public TURN Server from [Open R
What about privacy? Will files be saved on third-party-servers?
</summary>
None of your files are ever sent to any server. Files are sent only between peers. PairDrop doesn't even use a database. If you are curious have a look [at the Server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js).
Files are sent directly between peers. PairDrop doesn't even use a database. If you are curious, have a look [at the Server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js).
WebRTC encrypts the files on transit.
If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages.
If devices are on the same network, none of your files are ever sent to any server.
If your devices are paired and behind a NAT, the PairDrop TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC.
<br>
@@ -147,9 +151,7 @@ Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure
Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases.
As the public TURN server used is not super fast, you can easily [specify to use your own TURN server](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#specify-stunturn-servers) if you host your own instance.
Alternatively, you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster as no TURN server is needed.
You can open a hotspot on one of your devices to bridge the connection which omits the need of the TURN server.
- [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11)
- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)
@@ -171,7 +173,7 @@ Then, all data should be sent directly between devices and your data plan should
Snapdrop and PairDrop are a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer.
We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity.
If you want to learn more about simplicity you can read [Insanely Simple: The Obsession that Drives Apple's Success](https://www.amazon.com/Insanely-Simple-Ken-Segall-audiobook/dp/B007Z9686O) or [Thinking, Fast and Slow](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555).
If you want to learn more about simplicity you can read *Insanely Simple: The Obsession that Drives Apple's Success* or *Thinking, Fast and Slow*.
<br>
@@ -183,7 +185,7 @@ If you want to learn more about simplicity you can read [Insanely Simple: The Ob
Snapdrop and PairDrop are awesome! How can I support them?
</summary>
* [Buy me a coffee to support open source software](https://www.buymeacoffee.com/pairdrop)
* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support open source software
* [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues)
* Share PairDrop on social media.
* Fix bugs and make a pull request.

View File

@@ -1,6 +1,12 @@
# Deployment Notes
The easiest way to get PairDrop up and running is by using Docker.
> <b>TURN server for Internet Transfer</b>
>
> Beware that you have to host your own TURN server in order to enable transfers between different networks.
>
> You can follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) or deploy it via docker-compose (Step 5).
## Deployment with Docker
### Docker Image from Docker Hub
@@ -24,11 +30,19 @@ Set options by using the following flags in the `docker run` command:
> - 3000 -> `-p 127.0.0.1:3000:3000`
> - 8080 -> `-p 127.0.0.1:8080:3000`
##### Rate limiting requests
```
```bash
-e RATE_LIMIT=true
```
> Limits clients to 1000 requests per 5 min
##### IPv6 Localization
```bash
-e IPV6_LOCALIZE=4
```
> To enable Peer Discovery among IPv6 peers, you can specify a reduced number of segments of the client IPv6 address to be evaluated as the peer's IP. This can be especially useful when using Cloudflare as a proxy.
>
> The flag must be set to an **integer** between `1` and `7`. The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) to match the client IP against. The most common value would be `4`, which will group peers within the same `/64` subnet.
##### Websocket Fallback (for VPN)
```bash
-e WS_FALLBACK=true
@@ -50,6 +64,8 @@ Set options by using the following flags in the `docker run` command:
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
> You can use `pairdrop/rtc_config_example.json` as a starting point.
>
> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/
>
> Default configuration:
> ```json
> {
@@ -57,19 +73,36 @@ Set options by using the following flags in the `docker run` command:
> "iceServers": [
> {
> "urls": "stun:stun.l.google.com:19302"
> },
> {
> "urls": "stun:openrelay.metered.ca:80"
> },
> {
> "urls": "turn:openrelay.metered.ca:443",
> "username": "openrelayproject",
> "credential": "openrelayproject"
> }
> ]
> }
> ```
##### Debug Mode
```bash
-e DEBUG_MODE="true"
```
> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server)
> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status.
>
> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this:
> ```
> ----DEBUGGING-PEER-IP-START----
> remoteAddress: ::ffff:172.17.0.1
> x-forwarded-for: 19.117.63.126
> cf-connecting-ip: undefined
> PairDrop uses: 19.117.63.126
> IP is private: false
> if IP is private, '127.0.0.1' is used instead
> ----DEBUGGING-PEER-IP-END----
> ```
> If the IP PairDrop uses is the public IP of your device everything is correctly setup.
>To find out your devices public IP visit https://www.whatismyip.com/.
>
> To preserve your clients' privacy, **never use this flag in production!**
<br>
### Docker Image from GHCR
@@ -82,7 +115,7 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 gh
>
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](./docker-swarm-usage.md#docker-swarm-usage)
> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage)
### Docker Image self-built
#### Build the image
@@ -103,7 +136,7 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -i
>
> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1)
> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](./docker-swarm-usage.md#docker-swarm-usage)
> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage)
<br>
@@ -175,6 +208,12 @@ $env:PORT=3010; npm start
```
> Specify the port PairDrop is running on. (Default: 3000)
#### IPv6 Localization
```bash
IPV6_LOCALIZE=4
```
> Truncate a portion of the client IPv6 address to make peers more discoverable. See [Options/Flags](#options--flags) above.
#### Specify STUN/TURN Server
On Unix based systems
```bash
@@ -186,6 +225,8 @@ $env:RTC_CONFIG="rtc_config.json"; npm start
```
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
> You can use `pairdrop/rtc_config_example.json` as a starting point.
>
> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/
>
> Default configuration:
> ```json
@@ -194,19 +235,41 @@ $env:RTC_CONFIG="rtc_config.json"; npm start
> "iceServers": [
> {
> "urls": "stun:stun.l.google.com:19302"
> },
> {
> "urls": "stun:openrelay.metered.ca:80"
> },
> {
> "urls": "turn:openrelay.metered.ca:443",
> "username": "openrelayproject",
> "credential": "openrelayproject"
> }
> ]
> }
> ```
#### Debug Mode
On Unix based systems
```bash
DEBUG_MODE="true" npm start
```
On Windows
```bash
$env:DEBUG_MODE="true"; npm start
```
> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server)
> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status.
>
> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this:
> ```
> ----DEBUGGING-PEER-IP-START----
> remoteAddress: ::ffff:172.17.0.1
> x-forwarded-for: 19.117.63.126
> cf-connecting-ip: undefined
> PairDrop uses: 19.117.63.126
> IP is private: false
> if IP is private, '127.0.0.1' is used instead
> ----DEBUGGING-PEER-IP-END----
> ```
> If the IP PairDrop uses is the public IP of your device everything is correctly setup.
>To find out your devices public IP visit https://www.whatismyip.com/.
>
> To preserve your clients' privacy, **never use this flag in production!**
### Options / Flags
#### Local Run
```bash
@@ -263,6 +326,8 @@ npm run start:prod -- --localhost-only --include-ws-fallback
## HTTP-Server
When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. Otherwise, all clients will be mutually visible.
To check if your setup is configured correctly [use the environment variable `DEBUG_MODE="true"`](#debug-mode).
### Using nginx
#### Allow http and https requests
```

218
index.js
View File

@@ -90,6 +90,23 @@ if (process.argv.includes('--include-ws-fallback')) {
app.use(express.static('public'));
}
const debugMode = process.env.DEBUG_MODE === "true";
if (debugMode) {
console.log("DEBUG_MODE is active. To protect privacy, do not use in production.")
}
let ipv6_lcl;
if (process.env.IPV6_LOCALIZE) {
ipv6_lcl = parseInt(process.env.IPV6_LOCALIZE);
if (!ipv6_lcl || !(0 < ipv6_lcl && ipv6_lcl < 8)) {
console.error("IPV6_LOCALIZE must be an integer between 1 and 7");
return;
}
console.log("IPv6 client IPs will be localized to", ipv6_lcl, ipv6_lcl === 1 ? "segment" : "segments");
}
app.use(function(req, res) {
res.redirect('/');
});
@@ -127,7 +144,6 @@ class PairDropServer {
type: 'rtc-config',
config: rtcConfig
});
this._joinRoom(peer);
// send displayName
this._send(peer, {
@@ -136,7 +152,7 @@ class PairDropServer {
displayName: peer.name.displayName,
deviceName: peer.name.deviceName,
peerId: peer.id,
peerIdHash: peer.id.hashCode128BitSalted()
peerIdHash: hasher.hashCodeSalted(peer.id)
}
});
}
@@ -156,14 +172,14 @@ class PairDropServer {
case 'pong':
sender.lastBeat = Date.now();
break;
case 'join-ip-room':
this._joinRoom(sender);
break;
case 'room-secrets':
this._onRoomSecrets(sender, message);
break;
case 'room-secret-deleted':
this._onRoomSecretDeleted(sender, message);
break;
case 'room-secrets-cleared':
this._onRoomSecretsCleared(sender, message);
case 'room-secrets-deleted':
this._onRoomSecretsDeleted(sender, message);
break;
case 'pair-device-initiate':
this._onPairDeviceInitiate(sender);
@@ -174,6 +190,9 @@ class PairDropServer {
case 'pair-device-cancel':
this._onPairDeviceCancel(sender);
break;
case 'regenerate-room-secret':
this._onRegenerateRoomSecret(sender, message);
break
case 'resend-peers':
this._notifyPeers(sender);
break;
@@ -200,64 +219,53 @@ class PairDropServer {
}
_onDisconnect(sender) {
this._disconnect(sender);
}
_disconnect(sender) {
this._leaveRoom(sender, 'ip', '', true);
this._leaveAllSecretRooms(sender, true);
this._removeRoomKey(sender.roomKey);
sender.roomKey = null;
sender.socket.terminate();
}
_onRoomSecrets(sender, message) {
if (!message.roomSecrets) return;
const roomSecrets = message.roomSecrets.filter(roomSecret => {
return /^[\x00-\x7F]{64}$/.test(roomSecret);
return /^[\x00-\x7F]{64,256}$/.test(roomSecret);
})
if (!roomSecrets) return;
this._joinSecretRooms(sender, roomSecrets);
}
_onRoomSecretDeleted(sender, message) {
this._deleteSecretRoom(sender, message.roomSecret)
}
_onRoomSecretsCleared(sender, message) {
_onRoomSecretsDeleted(sender, message) {
for (let i = 0; i<message.roomSecrets.length; i++) {
this._deleteSecretRoom(sender, message.roomSecrets[i]);
this._deleteSecretRoom(message.roomSecrets[i]);
}
}
_deleteSecretRoom(sender, roomSecret) {
_deleteSecretRoom(roomSecret) {
const room = this._rooms[roomSecret];
if (room) {
for (const peerId in room) {
const peer = room[peerId];
this._leaveRoom(peer, 'secret', roomSecret);
this._send(peer, {
type: 'secret-room-deleted',
roomSecret: roomSecret,
});
}
}
this._notifyPeers(sender);
}
if (!room) return;
getRandomString(length) {
let string = "";
while (string.length < length) {
let arr = new Uint16Array(length);
crypto.webcrypto.getRandomValues(arr);
arr = Array.apply([], arr); /* turn into non-typed array */
arr = arr.map(function (r) {
return r % 128
})
arr = arr.filter(function (r) {
/* strip non-printables: if we transform into desirable range we have a propability bias, so I suppose we better skip this character */
return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122;
for (const peerId in room) {
const peer = room[peerId];
this._leaveRoom(peer, 'secret', roomSecret);
this._send(peer, {
type: 'secret-room-deleted',
roomSecret: roomSecret,
});
string += String.fromCharCode.apply(String, arr);
}
return string.substring(0, length)
}
_onPairDeviceInitiate(sender) {
let roomSecret = this.getRandomString(64);
let roomSecret = randomizer.getRandomString(256);
let roomKey = this._createRoomKey(sender, roomSecret);
if (sender.roomKey) this._removeRoomKey(sender.roomKey);
sender.roomKey = roomKey;
@@ -270,16 +278,19 @@ class PairDropServer {
}
_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' });
return;
}
sender.roomKeyRate += 1;
setTimeout(_ => sender.roomKeyRate -= 1, 10000);
if (!this._roomSecrets[message.roomKey] || sender.id === this._roomSecrets[message.roomKey].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);
@@ -298,13 +309,32 @@ class PairDropServer {
}
_onPairDeviceCancel(sender) {
if (sender.roomKey) {
this._send(sender, {
type: 'pair-device-canceled',
roomKey: sender.roomKey,
const roomKey = sender.roomKey
if (!roomKey) return;
this._removeRoomKey(roomKey);
this._send(sender, {
type: 'pair-device-canceled',
roomKey: roomKey,
});
}
_onRegenerateRoomSecret(sender, message) {
const oldRoomSecret = message.roomSecret;
const newRoomSecret = randomizer.getRandomString(256);
// notify all other peers
for (const peerId in this._rooms[oldRoomSecret]) {
const peer = this._rooms[oldRoomSecret][peerId];
this._send(peer, {
type: 'room-secret-regenerated',
oldRoomSecret: oldRoomSecret,
newRoomSecret: newRoomSecret,
});
this._removeRoomKey(sender.roomKey);
peer.removeRoomSecret(oldRoomSecret);
}
delete this._rooms[oldRoomSecret];
}
_createRoomKey(creator, roomSecret) {
@@ -332,10 +362,6 @@ class PairDropServer {
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
const room = roomType === 'ip' ? peer.ip : roomSecret;
if (this._rooms[room] && this._rooms[room][peer.id]) {
this._leaveRoom(peer, roomType, roomSecret);
}
// if room doesn't exist, create it
if (!this._rooms[room]) {
this._rooms[room] = {};
@@ -360,10 +386,6 @@ class PairDropServer {
// delete the peer
delete this._rooms[room][peer.id];
if (roomType === 'ip') {
peer.socket.terminate();
}
//if room is empty, delete the room
if (!Object.keys(this._rooms[room]).length) {
delete this._rooms[room];
@@ -443,8 +465,7 @@ class PairDropServer {
peer.lastBeat = Date.now();
}
if (Date.now() - peer.lastBeat > 2 * timeout) {
this._leaveRoom(peer);
this._leaveAllSecretRooms(peer);
this._disconnect(peer);
return;
}
@@ -472,7 +493,7 @@ class Peer {
this._setIP(request);
// set peer id
this._setPeerId(request)
this._setPeerId(request);
// is WebRTC supported ?
this.rtcSupported = request.url.indexOf('webrtc') > -1;
@@ -502,6 +523,25 @@ class Peer {
if (this.ip.substring(0,7) === "::ffff:")
this.ip = this.ip.substring(7);
let ipv6_was_localized = false;
if (ipv6_lcl && this.ip.includes(':')) {
this.ip = this.ip.split(':',ipv6_lcl).join(':');
ipv6_was_localized = true;
}
if (debugMode) {
console.debug("----DEBUGGING-PEER-IP-START----");
console.debug("remoteAddress:", request.connection.remoteAddress);
console.debug("x-forwarded-for:", request.headers['x-forwarded-for']);
console.debug("cf-connecting-ip:", request.headers['cf-connecting-ip']);
if (ipv6_was_localized)
console.debug("IPv6 client IP was localized to", ipv6_lcl, ipv6_lcl > 1 ? "segments" : "segment");
console.debug("PairDrop uses:", this.ip);
console.debug("IP is private:", this.ipIsPrivate(this.ip));
console.debug("if IP is private, '127.0.0.1' is used instead");
console.debug("----DEBUGGING-PEER-IP-END----");
}
// IPv4 and IPv6 use different values to refer to localhost
// put all peers on the same network as the server into the same room as well
if (this.ip === '::1' || this.ipIsPrivate(this.ip)) {
@@ -583,7 +623,7 @@ class Peer {
separator: ' ',
dictionaries: [colors, animals],
style: 'capital',
seed: this.id.hashCode()
seed: cyrb53(this.id)
})
this.name = {
@@ -609,7 +649,7 @@ class Peer {
}
isPeerIdHashValid(peerId, peerIdHash) {
return peerIdHash === peerId.hashCode128BitSalted();
return peerIdHash === hasher.hashCodeSalted(peerId);
}
addRoomSecret(roomSecret) {
@@ -625,39 +665,43 @@ class Peer {
}
}
Object.defineProperty(String.prototype, 'hashCode', {
value: function() {
return cyrb53(this);
}
});
Object.defineProperty(String.prototype, 'hashCode128BitSalted', {
value: function() {
return hasher.hashCode128BitSalted(this);
}
});
const hasher = (() => {
let seeds;
let password;
return {
hashCode128BitSalted(str) {
if (!seeds) {
// seeds are created on first call to salt hash.
seeds = [4];
for (let i=0; i<4; i++) {
const randomBuffer = new Uint32Array(1);
crypto.webcrypto.getRandomValues(randomBuffer);
seeds[i] = randomBuffer[0];
}
hashCodeSalted(salt) {
if (!password) {
// password is created on first call.
password = randomizer.getRandomString(128);
}
let hashCode = "";
for (let i=0; i<4; i++) {
hashCode += cyrb53(str, seeds[i]);
}
return hashCode;
return crypto.createHash("sha3-512")
.update(password)
.update(crypto.createHash("sha3-512").update(salt, "utf8").digest("hex"))
.digest("hex");
}
}
})()
const randomizer = (() => {
return {
getRandomString(length) {
let string = "";
while (string.length < length) {
let arr = new Uint16Array(length);
crypto.webcrypto.getRandomValues(arr);
arr = Array.apply([], arr); /* turn into non-typed array */
arr = arr.map(function (r) {
return r % 128
})
arr = arr.filter(function (r) {
/* strip non-printables: if we transform into desirable range we have a probability bias, so I suppose we better skip this character */
return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122;
});
string += String.fromCharCode.apply(String, arr);
}
return string.substring(0, length)
}
}
})()
/*

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "pairdrop",
"version": "1.6.0",
"version": "1.7.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pairdrop",
"version": "1.6.0",
"version": "1.7.5",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",

View File

@@ -1,6 +1,6 @@
{
"name": "pairdrop",
"version": "1.6.0",
"version": "1.7.5",
"description": "",
"main": "index.js",
"scripts": {

View File

@@ -6,7 +6,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- Web App Config -->
<title>PairDrop</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#3367d6">
<meta name="color-scheme" content="dark light">
<meta name="apple-mobile-web-app-capable" content="no">
@@ -39,24 +39,24 @@
<body translate="no">
<header class="row-reverse">
<a href="#about" class="icon-button" title="About PairDrop">
<a href="#about" class="icon-button" title="About PairDrop" aria-label="Open About PairDrop">
<svg class="icon">
<use xlink:href="#info-outline" />
</svg>
</a>
<div id="theme-wrapper">
<div id="theme-auto" class="icon-button selected" title="Adapt to System" >
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" >
<svg class="icon">
<use xlink:href="#icon-theme-auto" />
</svg>
</div>
<div>
<div id="theme-light" class="icon-button" title="Always Light" >
<div id="theme-light" class="icon-button" title="Always Use Light-Theme" >
<svg class="icon">
<use xlink:href="#icon-theme-light" />
</svg>
</div>
<div id="theme-dark" class="icon-button" title="Always Dark" >
<div id="theme-dark" class="icon-button" title="Always Use Dark-Theme" >
<svg class="icon">
<use xlink:href="#icon-theme-dark" />
</svg>
@@ -78,9 +78,9 @@
<use xlink:href="#pair-device-icon" />
</svg>
</div>
<div id="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
<svg class="icon">
<use xlink:href="#clear-pair-devices-icon" />
<use xlink:href="#edit-pair-devices-icon" />
</svg>
</div>
<div id="cancel-paste-mode" class="button" hidden>Done</div>
@@ -142,16 +142,18 @@
</x-background>
</form>
</x-dialog>
<!-- Clear Devices Dialog -->
<x-dialog id="clear-devices-dialog">
<!-- Edit Paired Devices Dialog -->
<x-dialog id="edit-paired-devices-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<h2 class="center">Edit Paired Devices</h2>
<div class="paired-devices-wrapper"></div>
<div class="font-subheading center">
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
</div>
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<button class="button" type="button" close>Cancel</button>
<button class="button" type="button" close>Close</button>
</div>
</x-paper>
</x-background>
@@ -222,7 +224,7 @@
<div class="row-separator"></div>
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
@@ -264,7 +266,7 @@
<!-- About Page -->
<x-about id="about" class="full center column">
<header class="row-reverse fade-in">
<a href="#" class="close icon-button">
<a href="#" class="close icon-button" aria-label="Close About PairDrop">
<svg class="icon">
<use xlink:href="#close-icon" />
</svg>
@@ -274,7 +276,10 @@
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
</svg>
<h1>PairDrop</h1>
<div class="title-wrapper">
<h1>PairDrop</h1>
<div class="font-subheading">v1.7.5</div>
</div>
<div class="font-subheading">The easiest way to transfer files across devices</div>
<div class="row">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
@@ -301,6 +306,7 @@
</section>
<x-background></x-background>
</x-about>
<canvas class="circles"></canvas>
<!-- SVG Icon Library -->
<svg style="display: none;">
<symbol id=wifi-tethering viewBox="0 0 24 24">
@@ -355,9 +361,11 @@
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
</symbol>
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
<symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<!--! edited by @schlagmichdoch -->
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.4-10.3,17.7-30.3,7.4-44.6s-30.3-17.7-44.6-7.4l-1.6,1.1c-32.1,22.9-76,19.3-103.8-8.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-14.4,10.3-17.7,30.3-7.4,44.6s30.3,17.7,44.6,7.4l1.6-1.1c32.1-22.9,76-19.3,103.8,8.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"/>
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"/>
</symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->

View File

@@ -3,16 +3,26 @@ window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnecti
if (!window.isRtcSupported) alert("WebRTC must be enabled for PairDrop to work");
window.hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
null;
class ServerConnection {
constructor() {
this._connect();
Events.on('pagehide', _ => this._disconnect());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
document.addEventListener(window.visibilityChangeEvent, _ => 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('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: 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));
@@ -39,10 +49,6 @@ class ServerConnection {
if (this._isReconnect) Events.fire('notify-user', '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.');
@@ -65,13 +71,13 @@ class ServerConnection {
_onMessage(msg) {
msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg);
if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers':
Events.fire('peers', msg);
this._onPeers(msg);
break;
case 'peer-joined':
Events.fire('peer-joined', msg);
@@ -106,19 +112,41 @@ class ServerConnection {
case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret);
break;
case 'room-secret-regenerated':
Events.fire('room-secret-regenerated', msg);
break;
default:
console.error('WS: unknown message type', msg);
console.error('WS receive: unknown message type', msg);
}
}
send(msg) {
if (!this._isConnected()) return;
if (msg.type !== 'pong') console.log("WS send:", msg)
this._socket.send(JSON.stringify(msg));
}
_onPeers(msg) {
Events.fire('peers', msg);
}
_onDisplayName(msg) {
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
sessionStorage.setItem("peerId", msg.message.peerId);
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (!peerId) return;
console.log("successfully added peerId to localStorage");
// Only now join rooms
Events.fire('join-ip-room');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
});
});
Events.fire('display-name', msg);
}
@@ -138,13 +166,19 @@ class ServerConnection {
_disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) {
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
const peerId = sessionStorage.getItem("peerId");
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
console.log("successfully removed peerId from localStorage");
});
if (!this._socket) return;
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
_onDisconnect() {
@@ -157,7 +191,7 @@ class ServerConnection {
}
_onVisibilityChange() {
if (document.hidden) return;
if (window.hiddenProperty) return;
this._connect();
}
@@ -181,13 +215,18 @@ class ServerConnection {
class Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
this._server = serverConnection;
this._isCaller = isCaller;
this._peerId = peerId;
this._roomType = roomType;
this._roomSecret = roomSecret;
this._updateRoomSecret(roomSecret);
this._filesQueue = [];
this._busy = false;
// evaluate auto accept
this._evaluateAutoAccept();
}
sendJSON(message) {
@@ -198,12 +237,47 @@ class Peer {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) {
return {
name: file.name,
mime: file.type,
size: file.size,
};
_isSameBrowser() {
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
}
_updateRoomSecret(roomSecret) {
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> do not delete duplicates and do not regenerate room secrets
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) {
// remove old roomSecrets to prevent multiple pairings with same peer
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
})
}
this._roomSecret = roomSecret;
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
// increase security by increasing roomSecret length
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._roomSecret);
}
}
_evaluateAutoAccept() {
if (!this._roomSecret) {
this._setAutoAccept(false);
return;
}
PersistentStorage.getRoomSecretEntry(this._roomSecret)
.then(roomSecretEntry => {
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
this._setAutoAccept(autoAccept);
})
.catch(_ => {
this._setAutoAccept(false);
});
}
_setAutoAccept(autoAccept) {
this._autoAccept = autoAccept;
}
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
@@ -248,7 +322,11 @@ class Peer {
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]));
header.push({
name: files[i].name,
mime: files[i].type,
size: files[i].size
});
totalSize += files[i].size;
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
}
@@ -360,7 +438,7 @@ class Peer {
_onFilesTransferRequest(request) {
if (this._requestPending) {
// Only accept one request at a time
// Only accept one request at a time per peer
this.sendJSON({type: 'files-transfer-response', accepted: false});
return;
}
@@ -372,6 +450,14 @@ class Peer {
}
this._requestPending = request;
if (this._autoAccept) {
// auto accept if set via Edit Paired Devices Dialog
this._respondToFileTransferRequest(true);
return;
}
// default behavior: show user transfer request
Events.fire('files-transfer-request', {
request: request,
peerId: this._peerId
@@ -443,7 +529,7 @@ class Peer {
this._abortTransfer();
}
// include for compatibility with Snapdrop for Android app
// include for compatibility with 'Snapdrop & PairDrop for Android' app
Events.fire('file-received', fileBlob);
this._filesReceived.push(fileBlob);
@@ -461,6 +547,7 @@ class Peer {
if (!this._filesQueue.length) {
this._busy = false;
Events.fire('notify-user', 'File transfer completed.');
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
} else {
this._dequeueFile();
}
@@ -497,34 +584,39 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
const displayNameHasChanged = this._displayName !== message.displayName
if (message.displayName && displayNameHasChanged) {
this._displayName = message.displayName;
}
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
if (!displayNameHasChanged) return;
Events.fire('notify-peer-display-name-changed', this._peerId);
}
}
class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
super(serverConnection, isCaller, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller
this._connect(peerId, true);
if (!this._isCaller) return; // we will listen for a caller
this._connect();
}
_connect(peerId, isCaller) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller);
_connect() {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection();
if (isCaller) {
if (this._isCaller) {
this._openChannel();
} else {
this._conn.ondatachannel = e => this._onChannelOpened(e);
}
}
_openConnection(peerId, isCaller) {
this._isCaller = isCaller;
this._peerId = peerId;
_openConnection() {
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
@@ -556,7 +648,7 @@ class RTCPeer extends Peer {
}
onServerMessage(message) {
if (!this._conn) this._connect(message.sender.id, false);
if (!this._conn) this._connect();
if (message.sdp) {
this._conn.setRemoteDescription(message.sdp)
@@ -641,7 +733,7 @@ class RTCPeer extends Peer {
console.log('RTC: channel closed', this._peerId);
Events.fire('peer-disconnected', this._peerId);
if (!this._isCaller) return;
this._connect(this._peerId, true); // reopen the channel
this._connect(); // reopen the channel
}
_onConnectionStateChange() {
@@ -688,7 +780,11 @@ class RTCPeer extends Peer {
refresh() {
// check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return;
this._connect(this._peerId, this._isCaller);
// only reconnect if peer is caller
if (!this._isCaller) return;
this._connect();
}
_isConnected() {
@@ -716,32 +812,63 @@ class PeersManager {
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-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));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
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));
}
_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]) {
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
}
this.peers[message.sender.id].onServerMessage(message);
const peerId = message.sender.id;
this.peers[peerId].onServerMessage(message);
}
_onPeers(msg) {
msg.peers.forEach(peer => {
if (this.peers[peer.id]) {
// if different roomType -> abort
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) return;
this.peers[peer.id].refresh();
return;
}
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
_refreshPeer(peer, roomType, roomSecret) {
if (!peer) return false;
const roomTypeIsSecret = roomType === "secret";
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
if (roomTypeIsSecret && roomSecretsDiffer) {
peer._updateRoomSecret(roomSecret);
peer._evaluateAutoAccept();
return true;
}
const roomTypesDiffer = peer._roomType !== roomType;
// if roomTypes differ peer is already connected -> abort
if (roomTypesDiffer) return true;
peer.refresh();
return true;
}
_createOrRefreshPeer(isCaller, peerId, roomType, roomSecret) {
const peer = this.peers[peerId];
if (peer) {
this._refreshPeer(peer, roomType, roomSecret);
return;
}
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomSecret);
}
_onPeerJoined(message) {
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret);
}
_onPeers(message) {
message.peers.forEach(peer => {
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret);
})
}
@@ -769,10 +896,19 @@ class PeersManager {
this.peers[message.to].sendText(message.text);
}
_onPeerLeft(msg) {
if (msg.disconnect === true) {
// if user actively disconnected from PairDrop disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId);
_onPeerLeft(message) {
if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', message.peerId);
// 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;
console.log("successfully removed other peerIds from localStorage");
});
}
}
}
@@ -792,12 +928,19 @@ class PeersManager {
_onSecretRoomDeleted(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) {
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
this._onPeerDisconnected(peerId);
}
}
}
_onRoomSecretRegenerated(message) {
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
console.log("successfully regenerated room secret");
Events.fire("room-secrets", [message.newRoomSecret]);
})
}
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
@@ -813,6 +956,25 @@ class PeersManager {
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
// if the displayName has not been changed (yet) set the displayName to the original displayName
if (!this._displayName) this._displayName = displayName;
}
_onAutoAcceptUpdated(roomSecret, autoAccept) {
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
if (!peerId) return;
this.peers[peerId]._setAutoAccept(autoAccept);
}
_getPeerIdFromRoomSecret(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
// peer must have same roomSecret and not be on the same browser.
if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) {
return peer._peerId;
}
}
return false;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ if (!navigator.clipboard) {
// A <span> contains the text to copy
const span = document.createElement('span');
span.textContent = text;
span.innerText = text;
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
// Paint the span outside the viewport
@@ -398,3 +398,11 @@ const cyrb53 = function(str, seed = 0) {
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
function onlyUnique (value, index, array) {
return array.indexOf(value) === index;
}
function getUrlWithoutArguments() {
return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
}

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.6.0';
const cacheVersion = 'v1.7.5';
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [
'index.html',
@@ -72,8 +72,7 @@ self.addEventListener('fetch', function(event) {
if (event.request.method === "POST") {
// Requests related to Web Share Target.
event.respondWith((async () => {
let share_url = await evaluateRequestData(event.request);
share_url = event.request.url + share_url;
const share_url = await evaluateRequestData(event.request);
return Response.redirect(encodeURI(share_url), 302);
})());
} else {
@@ -101,15 +100,16 @@ self.addEventListener('activate', evt =>
)
);
const evaluateRequestData = async function (request) {
const formData = await request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.getAll("allfiles");
const evaluateRequestData = function (request) {
return new Promise(async (resolve) => {
const formData = await request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.getAll("allfiles");
const pairDropUrl = request.url;
if (files && files.length > 0) {
let fileObjects = [];
for (let i=0; i<files.length; i++) {
@@ -128,21 +128,21 @@ const evaluateRequestData = async function (request) {
const objectStoreRequest = objectStore.add(fileObjects[i]);
objectStoreRequest.onsuccess = _ => {
if (i === fileObjects.length - 1) resolve('?share-target=files');
if (i === fileObjects.length - 1) resolve(pairDropUrl + '?share-target=files');
}
}
}
DBOpenRequest.onerror = _ => {
resolve('');
resolve(pairDropUrl);
}
} else {
let share_url = '?share-target=text';
let urlArgument = '?share-target=text';
if (title) share_url += `&title=${title}`;
if (text) share_url += `&text=${text}`;
if (url) share_url += `&url=${url}`;
if (title) urlArgument += `&title=${title}`;
if (text) urlArgument += `&text=${text}`;
if (url) urlArgument += `&url=${url}`;
resolve(share_url);
resolve(pairDropUrl + urlArgument);
}
});
}

View File

@@ -19,6 +19,10 @@ body {
overflow-x: hidden;
overscroll-behavior: none;
overflow-y: hidden;
/* Only allow selection on message and pair key */
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
body {
@@ -112,7 +116,7 @@ header > div:not(:hover) .icon-button:not(.selected) {
opacity: 0;
}
header > div:hover::before {
#theme-wrapper:hover::before {
border-radius: 20px;
background: currentColor;
opacity: 0.1;
@@ -204,7 +208,8 @@ body {
line-height: 18px;
}
a {
a,
.icon-button {
text-decoration: none;
color: currentColor;
cursor: pointer;
@@ -214,6 +219,14 @@ hr {
color: white;
}
input {
cursor: pointer;
}
input[type="checkbox"] {
min-width: 13px;
}
x-noscript {
background: var(--primary-color);
color: white;
@@ -272,7 +285,7 @@ x-noscript {
}
@media screen and (max-width: 425px) {
header:has(#clear-pair-devices:not([hidden]))~#center {
header:has(#edit-pair-devices:not([hidden]))~#center {
--footer-height: 150px;
}
}
@@ -404,10 +417,10 @@ x-no-peers {
flex-direction: column;
padding: 8px;
text-align: center;
/* prevent flickering on load */
animation: fade-in 300ms;
animation-delay: 500ms;
animation-fill-mode: backwards;
/* prevent flickering on load */
animation-iteration-count: 0;
}
x-no-peers h2,
@@ -441,8 +454,6 @@ x-no-peers[drop-bg] * {
/* Peer */
x-peer {
-webkit-user-select: none;
user-select: none;
padding: 8px;
align-content: start;
flex-wrap: wrap;
@@ -529,7 +540,6 @@ x-peer[status] x-icon {
.status,
.device-name,
.connection-hash {
height: 18px;
opacity: 0.7;
}
@@ -690,6 +700,12 @@ x-dialog x-paper {
height: 625px;
}
#pair-device-dialog ::-moz-selection,
#pair-device-dialog ::selection {
color: black;
background: var(--paired-device-color);
}
x-dialog:not([show]) {
pointer-events: none;
}
@@ -711,7 +727,7 @@ x-dialog .font-subheading {
margin-bottom: 5px;
}
/* PairDevicesDialog */
/* Pair Devices Dialog */
#key-input-container {
width: 100%;
@@ -719,7 +735,7 @@ x-dialog .font-subheading {
justify-content: center;
}
#key-input-container>input {
#key-input-container > input {
width: 45px;
height: 45px;
font-size: 30px;
@@ -735,15 +751,18 @@ x-dialog .font-subheading {
justify-content: center;
}
#key-input-container>input + * {
#key-input-container > input + * {
margin-left: 6px;
}
#key-input-container>input:nth-of-type(4) {
#key-input-container > input:nth-of-type(4) {
margin-left: 5%;
}
#room-key {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
display: inline-block;
@@ -755,14 +774,106 @@ x-dialog .font-subheading {
margin: 16px;
}
#pair-device-dialog hr {
margin: 40px -24px;
x-dialog hr {
margin: 40px -24px 30px -24px;
border: solid 1.25px var(--border-color);
}
#pair-device-dialog x-background {
padding: 16px!important;
}
/* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before {
content: "No paired devices.";
}
.paired-devices-wrapper:empty {
padding: 10px;
}
.paired-devices-wrapper {
border-top: solid 4px var(--paired-device-color);
border-bottom: solid 4px var(--paired-device-color);
max-height: 65vh;
overflow: scroll;
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .3), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
.paired-device {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
}
.paired-device:not(:last-child) {
border-bottom: solid 4px var(--paired-device-color);
}
.paired-device > .display-name,
.paired-device > .device-name {
width: 100%;
height: 36px;
display: flex;
align-items: center;
text-align: center;
align-self: center;
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
opacity: 1;
}
.paired-device span {
width: 100%;
}
.paired-device > .button-wrapper {
display: flex;
height: 36px;
justify-content: space-between;
flex-direction: row;
align-items: center;
width: 100%;
}
.paired-device > .button-wrapper > label,
.paired-device > .button-wrapper > button {
display: flex;
align-items: center;
text-align: center;
white-space: nowrap;
justify-content: center;
width: 50%;
padding-left: 6px;
padding-right: 6px;
height: 36px;
}
.paired-device > .button-wrapper > :not(:last-child) {
border-right: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device > .button-wrapper > :not(:first-child) {
border-left: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device * {
overflow: hidden;
text-overflow: ellipsis;
}
.paired-device > .auto-accept {
cursor: pointer;
}
/* Receive Dialog */
x-dialog .row {
@@ -811,7 +922,7 @@ x-paper > div:last-child > .button:not(:last-child) {
}
/* Send Text Dialog */
/* Todo: add pair underline to send / receive dialogs displayName */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
@@ -829,9 +940,9 @@ x-dialog .dialog-subheader {
max-height: calc(100vh - 393px);
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: all;
-moz-user-select: all;
user-select: all;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
white-space: pre-wrap;
padding: 15px 0;
}
@@ -1013,11 +1124,14 @@ button::-moz-focus-inner {
text-align: center;
}
#about header {
z-index: 1;
}
#about .fade-in {
transition: opacity 300ms;
will-change: opacity;
transition-delay: 300ms;
z-index: 11;
pointer-events: all;
}
@@ -1031,12 +1145,23 @@ button::-moz-focus-inner {
--icon-size: 96px;
}
#about .title-wrapper {
display: flex;
align-items: baseline;
}
#about .title-wrapper > div {
margin-left: 0.5em;
}
#about x-background {
position: absolute;
top: calc(28px - 250px);
right: calc(36px - 250px);
width: 500px;
height: 500px;
--size: max(max(230vw, 230vh), calc(150vh + 150vw));
--size-half: calc(var(--size)/2);
top: calc(28px - var(--size-half));
right: calc(36px - var(--size-half));
width: var(--size);
height: var(--size);
border-radius: 50%;
background: var(--primary-color);
transform: scale(0);
@@ -1050,7 +1175,7 @@ button::-moz-focus-inner {
}
#about:target x-background {
transform: scale(10);
transform: scale(1);
}
#about .row a {
@@ -1065,6 +1190,14 @@ button::-moz-focus-inner {
align-self: end;
}
canvas.circles {
width: 100vw;
position: absolute;
z-index: -10;
top: 0;
left: 0;
}
/* Loading Indicator */
.progress {
@@ -1173,27 +1306,10 @@ x-peers:empty~x-instructions {
@media (hover: none) and (pointer: coarse) {
x-peer {
transform: scale(0.95);
padding: 4px 0;
padding: 4px;
}
}
#websocket-fallback {
margin-left: 5px;
margin-right: 5px;
padding: 5px;
text-align: center;
opacity: 0.5;
transition: opacity 300ms;
}
#websocket-fallback>span {
margin: 2px;
}
#websocket-fallback > span > span {
border-bottom: solid 4px var(--ws-peer-color);
}
/* Responsive Styles */
@media screen and (max-width: 360px) {
x-dialog x-paper {
@@ -1336,3 +1452,9 @@ x-dialog x-paper {
background: #bfbfbf;
border-radius: 4px;
}
::-moz-selection,
::selection {
color: black;
background: var(--primary-color);
}

View File

@@ -6,7 +6,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!-- Web App Config -->
<title>PairDrop</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#3367d6">
<meta name="color-scheme" content="dark light">
<meta name="apple-mobile-web-app-capable" content="no">
@@ -39,24 +39,24 @@
<body translate="no">
<header class="row-reverse">
<a href="#about" class="icon-button" title="About PairDrop">
<a href="#about" class="icon-button" title="About PairDrop" aria-label="Open About PairDrop">
<svg class="icon">
<use xlink:href="#info-outline" />
</svg>
</a>
<div id="theme-wrapper">
<div id="theme-auto" class="icon-button selected" title="Adapt to System" >
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" >
<svg class="icon">
<use xlink:href="#icon-theme-auto" />
</svg>
</div>
<div>
<div id="theme-light" class="icon-button" title="Always Light" >
<div id="theme-light" class="icon-button" title="Always Use Light-Theme" >
<svg class="icon">
<use xlink:href="#icon-theme-light" />
</svg>
</div>
<div id="theme-dark" class="icon-button" title="Always Dark" >
<div id="theme-dark" class="icon-button" title="Always Use Dark-Theme" >
<svg class="icon">
<use xlink:href="#icon-theme-dark" />
</svg>
@@ -78,9 +78,9 @@
<use xlink:href="#pair-device-icon" />
</svg>
</div>
<div id="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
<svg class="icon">
<use xlink:href="#clear-pair-devices-icon" />
<use xlink:href="#edit-pair-devices-icon" />
</svg>
</div>
<div id="cancel-paste-mode" class="button" hidden>Done</div>
@@ -145,16 +145,18 @@
</x-background>
</form>
</x-dialog>
<!-- Clear Devices Dialog -->
<x-dialog id="clear-devices-dialog">
<!-- Edit Paired Devices Dialog -->
<x-dialog id="edit-paired-devices-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<h2 class="center">Edit Paired Devices</h2>
<div class="paired-devices-wrapper"></div>
<div class="font-subheading center">
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
</div>
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<button class="button" type="button" close>Cancel</button>
<button class="button" type="button" close>Close</button>
</div>
</x-paper>
</x-background>
@@ -225,7 +227,7 @@
<div class="row-separator"></div>
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
@@ -267,7 +269,7 @@
<!-- About Page -->
<x-about id="about" class="full center column">
<header class="row-reverse fade-in">
<a href="#" class="close icon-button">
<a href="#" class="close icon-button" aria-label="Close About PairDrop">
<svg class="icon">
<use xlink:href="#close-icon" />
</svg>
@@ -277,7 +279,10 @@
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
</svg>
<h1>PairDrop</h1>
<div class="title-wrapper">
<h1>PairDrop</h1>
<div class="font-subheading">v1.7.5</div>
</div>
<div class="font-subheading">The easiest way to transfer files across devices</div>
<div class="row">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
@@ -304,6 +309,7 @@
</section>
<x-background></x-background>
</x-about>
<canvas class="circles"></canvas>
<!-- SVG Icon Library -->
<svg style="display: none;">
<symbol id=wifi-tethering viewBox="0 0 24 24">
@@ -358,9 +364,11 @@
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
</symbol>
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
<symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<!--! edited by @schlagmichdoch -->
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.4-10.3,17.7-30.3,7.4-44.6s-30.3-17.7-44.6-7.4l-1.6,1.1c-32.1,22.9-76,19.3-103.8-8.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-14.4,10.3-17.7,30.3-7.4,44.6s30.3,17.7,44.6,7.4l1.6-1.1c32.1-22.9,76-19.3,103.8,8.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"/>
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"/>
</symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->

View File

@@ -1,16 +1,26 @@
window.URL = window.URL || window.webkitURL;
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
window.hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
null;
class ServerConnection {
constructor() {
this._connect();
Events.on('pagehide', _ => this._disconnect());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
document.addEventListener(window.visibilityChangeEvent, _ => 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('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: 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));
@@ -37,10 +47,6 @@ class ServerConnection {
if (this._isReconnect) Events.fire('notify-user', '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.');
@@ -63,13 +69,13 @@ class ServerConnection {
_onMessage(msg) {
msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg);
if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers':
Events.fire('peers', msg);
this._onPeers(msg);
break;
case 'peer-joined':
Events.fire('peer-joined', msg);
@@ -104,6 +110,9 @@ class ServerConnection {
case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret);
break;
case 'room-secret-regenerated':
Events.fire('room-secret-regenerated', msg);
break;
case 'request':
case 'header':
case 'partition':
@@ -118,18 +127,43 @@ class ServerConnection {
Events.fire('ws-relay', JSON.stringify(msg));
break;
default:
console.error('WS: unknown message type', msg);
console.error('WS receive: unknown message type', msg);
}
}
send(msg) {
if (!this._isConnected()) return;
if (msg.type !== 'pong') console.log("WS send:", msg)
this._socket.send(JSON.stringify(msg));
}
_onPeers(msg) {
Events.fire('peers', msg);
if (msg.roomType === "ip" && msg.peers.length === 0) {
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerId => {
if (!peerId) return;
console.log("successfully removed other peerIds from localStorage");
});
}
}
_onDisplayName(msg) {
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
sessionStorage.setItem("peerId", msg.message.peerId);
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (!peerId) return;
console.log("successfully added peerId to localStorage");
// Only now join rooms
Events.fire('join-ip-room');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
});
});
Events.fire('display-name', msg);
}
@@ -149,13 +183,19 @@ class ServerConnection {
_disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) {
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
const peerId = sessionStorage.getItem("peerId");
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
console.log("successfully removed peerId from localStorage");
});
if (!this._socket) return;
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
_onDisconnect() {
@@ -168,7 +208,7 @@ class ServerConnection {
}
_onVisibilityChange() {
if (document.hidden) return;
if (window.hiddenProperty) return;
this._connect();
}
@@ -192,13 +232,18 @@ class ServerConnection {
class Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
this._server = serverConnection;
this._isCaller = isCaller;
this._peerId = peerId;
this._roomType = roomType;
this._roomSecret = roomSecret;
this._updateRoomSecret(roomSecret);
this._filesQueue = [];
this._busy = false;
// evaluate auto accept
this._evaluateAutoAccept();
}
sendJSON(message) {
@@ -209,12 +254,47 @@ class Peer {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) {
return {
name: file.name,
mime: file.type,
size: file.size,
};
_isSameBrowser() {
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
}
_updateRoomSecret(roomSecret) {
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> do not delete duplicates and do not regenerate room secrets
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) {
// remove old roomSecrets to prevent multiple pairings with same peer
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
})
}
this._roomSecret = roomSecret;
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
// increase security by increasing roomSecret length
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._roomSecret);
}
}
_evaluateAutoAccept() {
if (!this._roomSecret) {
this._setAutoAccept(false);
return;
}
PersistentStorage.getRoomSecretEntry(this._roomSecret)
.then(roomSecretEntry => {
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
this._setAutoAccept(autoAccept);
})
.catch(_ => {
this._setAutoAccept(false);
});
}
_setAutoAccept(autoAccept) {
this._autoAccept = autoAccept;
}
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
@@ -259,7 +339,11 @@ class Peer {
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]));
header.push({
name: files[i].name,
mime: files[i].type,
size: files[i].size
});
totalSize += files[i].size;
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
}
@@ -371,7 +455,7 @@ class Peer {
_onFilesTransferRequest(request) {
if (this._requestPending) {
// Only accept one request at a time
// Only accept one request at a time per peer
this.sendJSON({type: 'files-transfer-response', accepted: false});
return;
}
@@ -383,6 +467,14 @@ class Peer {
}
this._requestPending = request;
if (this._autoAccept) {
// auto accept if set via Edit Paired Devices Dialog
this._respondToFileTransferRequest(true);
return;
}
// default behavior: show user transfer request
Events.fire('files-transfer-request', {
request: request,
peerId: this._peerId
@@ -508,34 +600,39 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
const displayNameHasChanged = this._displayName !== message.displayName
if (message.displayName && displayNameHasChanged) {
this._displayName = message.displayName;
}
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
if (!displayNameHasChanged) return;
Events.fire('notify-peer-display-name-changed', this._peerId);
}
}
class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
super(serverConnection, isCaller, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller
this._connect(peerId, true);
if (!this._isCaller) return; // we will listen for a caller
this._connect();
}
_connect(peerId, isCaller) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller);
_connect() {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection();
if (isCaller) {
if (this._isCaller) {
this._openChannel();
} else {
this._conn.ondatachannel = e => this._onChannelOpened(e);
}
}
_openConnection(peerId, isCaller) {
this._isCaller = isCaller;
this._peerId = peerId;
_openConnection() {
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
@@ -567,7 +664,7 @@ class RTCPeer extends Peer {
}
onServerMessage(message) {
if (!this._conn) this._connect(message.sender.id, false);
if (!this._conn) this._connect();
if (message.sdp) {
this._conn.setRemoteDescription(message.sdp)
@@ -652,7 +749,7 @@ class RTCPeer extends Peer {
console.log('RTC: channel closed', this._peerId);
Events.fire('peer-disconnected', this._peerId);
if (!this._isCaller) return;
this._connect(this._peerId, true); // reopen the channel
this._connect(); // reopen the channel
}
_onConnectionStateChange() {
@@ -699,7 +796,11 @@ class RTCPeer extends Peer {
refresh() {
// check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return;
this._connect(this._peerId, this._isCaller);
// only reconnect if peer is caller
if (!this._isCaller) return;
this._connect();
}
_isConnected() {
@@ -718,11 +819,10 @@ class RTCPeer extends Peer {
class WSPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
super(serverConnection, isCaller, peerId, roomType, roomSecret);
this.rtcSupported = false;
if (!peerId) return; // we will listen for a caller
this._isCaller = true;
if (!this._isCaller) return; // we will listen for a caller
this._sendSignal();
}
@@ -734,7 +834,6 @@ class WSPeer extends Peer {
}
sendJSON(message) {
console.debug(message)
message.to = this._peerId;
message.roomType = this._roomType;
message.roomSecret = this._roomSecret;
@@ -769,49 +868,76 @@ class PeersManager {
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-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));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
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));
}
_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);
}
const peerId = message.sender.id;
this.peers[peerId].onServerMessage(message);
}
_refreshPeer(peer, roomType, roomSecret) {
if (!peer) return false;
const roomTypeIsSecret = roomType === "secret";
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
if (roomTypeIsSecret && roomSecretsDiffer) {
peer._updateRoomSecret(roomSecret);
peer._evaluateAutoAccept();
return true;
}
this.peers[message.sender.id].onServerMessage(message);
const roomTypesDiffer = peer._roomType !== roomType;
// if roomTypes differ peer is already connected -> abort
if (roomTypesDiffer) return true;
peer.refresh();
return true;
}
_createOrRefreshPeer(isCaller, peerId, roomType, roomSecret, rtcSupported) {
const peer = this.peers[peerId];
if (peer) {
this._refreshPeer(peer, roomType, roomSecret);
return;
}
if (window.isRtcSupported && rtcSupported) {
this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomSecret);
} else {
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomSecret);
}
}
_onPeerJoined(message) {
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret, message.peer.rtcSupported);
}
_onPeers(message) {
message.peers.forEach(peer => {
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret, peer.rtcSupported);
})
}
_onWsRelay(message) {
const messageJSON = JSON.parse(message)
const messageJSON = JSON.parse(message);
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
this.peers[messageJSON.sender.id]._onMessage(message)
}
_onPeers(msg) {
msg.peers.forEach(peer => {
if (this.peers[peer.id]) {
// if different roomType -> abort
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) 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);
}
})
this.peers[messageJSON.sender.id]._onMessage(message);
}
_onRespondToFileTransferRequest(detail) {
@@ -838,13 +964,22 @@ class PeersManager {
this.peers[message.to].sendText(message.text);
}
_onPeerLeft(msg) {
if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
console.log('WSPeer left:', msg.peerId);
Events.fire('peer-disconnected', msg.peerId);
} else if (msg.disconnect === true) {
_onPeerLeft(message) {
if (this.peers[message.peerId] && (!this.peers[message.peerId].rtcSupported || !window.isRtcSupported)) {
console.log('WSPeer left:', message.peerId);
}
if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId);
Events.fire('peer-disconnected', message.peerId);
// 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;
console.log("successfully removed other peerIds from localStorage");
});
}
}
}
@@ -854,7 +989,6 @@ class PeersManager {
_onWsDisconnected() {
for (const peerId in this.peers) {
console.debug(this.peers[peerId].rtcSupported);
if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) {
Events.fire('peer-disconnected', peerId);
}
@@ -873,12 +1007,19 @@ class PeersManager {
_onSecretRoomDeleted(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) {
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
this._onPeerDisconnected(peerId);
}
}
}
_onRoomSecretRegenerated(message) {
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
console.log("successfully regenerated room secret");
Events.fire("room-secrets", [message.newRoomSecret]);
})
}
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
@@ -894,6 +1035,25 @@ class PeersManager {
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
// if the displayName has not been changed (yet) set the displayName to the original displayName
if (!this._displayName) this._displayName = displayName;
}
_onAutoAcceptUpdated(roomSecret, autoAccept) {
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
if (!peerId) return;
this.peers[peerId]._setAutoAccept(autoAccept);
}
_getPeerIdFromRoomSecret(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
// peer must have same roomSecret and not be on the same browser.
if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) {
return peer._peerId;
}
}
return false;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ if (!navigator.clipboard) {
// A <span> contains the text to copy
const span = document.createElement('span');
span.textContent = text;
span.innerText = text;
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
// Paint the span outside the viewport
@@ -399,6 +399,14 @@ const cyrb53 = function(str, seed = 0) {
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
function onlyUnique (value, index, array) {
return array.indexOf(value) === index;
}
function getUrlWithoutArguments() {
return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
}
function arrayBufferToBase64(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
@@ -410,10 +418,10 @@ function arrayBufferToBase64(buffer) {
}
function base64ToArrayBuffer(base64) {
var binary_string = window.atob(base64);
var binary_string = window.atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.6.0';
const cacheVersion = 'v1.7.5';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [
'index.html',
@@ -72,8 +72,7 @@ self.addEventListener('fetch', function(event) {
if (event.request.method === "POST") {
// Requests related to Web Share Target.
event.respondWith((async () => {
let share_url = await evaluateRequestData(event.request);
share_url = event.request.url + share_url;
const share_url = await evaluateRequestData(event.request);
return Response.redirect(encodeURI(share_url), 302);
})());
} else {
@@ -101,15 +100,16 @@ self.addEventListener('activate', evt =>
)
);
const evaluateRequestData = async function (request) {
const formData = await request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.getAll("allfiles");
const evaluateRequestData = function (request) {
return new Promise(async (resolve) => {
const formData = await request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.getAll("allfiles");
const pairDropUrl = request.url;
if (files && files.length > 0) {
let fileObjects = [];
for (let i=0; i<files.length; i++) {
@@ -128,21 +128,21 @@ const evaluateRequestData = async function (request) {
const objectStoreRequest = objectStore.add(fileObjects[i]);
objectStoreRequest.onsuccess = _ => {
if (i === fileObjects.length - 1) resolve('?share-target=files');
if (i === fileObjects.length - 1) resolve(pairDropUrl + '?share-target=files');
}
}
}
DBOpenRequest.onerror = _ => {
resolve('');
resolve(pairDropUrl);
}
} else {
let share_url = '?share-target=text';
let urlArgument = '?share-target=text';
if (title) share_url += `&title=${title}`;
if (text) share_url += `&text=${text}`;
if (url) share_url += `&url=${url}`;
if (title) urlArgument += `&title=${title}`;
if (text) urlArgument += `&text=${text}`;
if (url) urlArgument += `&url=${url}`;
resolve(share_url);
resolve(pairDropUrl + urlArgument);
}
});
}

View File

@@ -20,6 +20,10 @@ body {
overflow-x: hidden;
overscroll-behavior: none;
overflow-y: hidden;
/* Only allow selection on message and pair key */
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
body {
@@ -113,7 +117,7 @@ header > div:not(:hover) .icon-button:not(.selected) {
opacity: 0;
}
header > div:hover::before {
#theme-wrapper:hover::before {
border-radius: 20px;
background: currentColor;
opacity: 0.1;
@@ -205,7 +209,8 @@ body {
line-height: 18px;
}
a {
a,
.icon-button {
text-decoration: none;
color: currentColor;
cursor: pointer;
@@ -215,6 +220,14 @@ hr {
color: white;
}
input {
cursor: pointer;
}
input[type="checkbox"] {
min-width: 13px;
}
x-noscript {
background: var(--primary-color);
color: white;
@@ -273,7 +286,7 @@ x-noscript {
}
@media screen and (min-width: 402px) and (max-width: 425px) {
header:has(#clear-pair-devices:not([hidden]))~#center {
header:has(#edit-pair-devices:not([hidden]))~#center {
--footer-height: 164px;
}
}
@@ -414,10 +427,10 @@ x-no-peers {
flex-direction: column;
padding: 8px;
text-align: center;
/* prevent flickering on load */
animation: fade-in 300ms;
animation-delay: 500ms;
animation-fill-mode: backwards;
/* prevent flickering on load */
animation-iteration-count: 0;
}
x-no-peers h2,
@@ -451,8 +464,6 @@ x-no-peers[drop-bg] * {
/* Peer */
x-peer {
-webkit-user-select: none;
user-select: none;
padding: 8px;
align-content: start;
flex-wrap: wrap;
@@ -556,7 +567,6 @@ x-peer.ws-peer .highlight-wrapper {
.status,
.device-name,
.connection-hash {
height: 18px;
opacity: 0.7;
}
@@ -716,6 +726,12 @@ x-dialog x-paper {
height: 625px;
}
#pair-device-dialog ::-moz-selection,
#pair-device-dialog ::selection {
color: black;
background: var(--paired-device-color);
}
x-dialog:not([show]) {
pointer-events: none;
}
@@ -737,7 +753,7 @@ x-dialog .font-subheading {
margin-bottom: 5px;
}
/* PairDevicesDialog */
/* Pair Devices Dialog */
#key-input-container {
width: 100%;
@@ -745,7 +761,7 @@ x-dialog .font-subheading {
justify-content: center;
}
#key-input-container>input {
#key-input-container > input {
width: 45px;
height: 45px;
font-size: 30px;
@@ -761,15 +777,18 @@ x-dialog .font-subheading {
justify-content: center;
}
#key-input-container>input + * {
#key-input-container > input + * {
margin-left: 6px;
}
#key-input-container>input:nth-of-type(4) {
#key-input-container > input:nth-of-type(4) {
margin-left: 5%;
}
#room-key {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
display: inline-block;
@@ -781,14 +800,106 @@ x-dialog .font-subheading {
margin: 16px;
}
#pair-device-dialog hr {
margin: 40px -24px;
x-dialog hr {
margin: 40px -24px 30px -24px;
border: solid 1.25px var(--border-color);
}
#pair-device-dialog x-background {
padding: 16px!important;
}
/* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before {
content: "No paired devices.";
}
.paired-devices-wrapper:empty {
padding: 10px;
}
.paired-devices-wrapper {
border-top: solid 4px var(--paired-device-color);
border-bottom: solid 4px var(--paired-device-color);
max-height: 65vh;
overflow: scroll;
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .3), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
.paired-device {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
}
.paired-device:not(:last-child) {
border-bottom: solid 4px var(--paired-device-color);
}
.paired-device > .display-name,
.paired-device > .device-name {
width: 100%;
height: 36px;
display: flex;
align-items: center;
text-align: center;
align-self: center;
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
opacity: 1;
}
.paired-device span {
width: 100%;
}
.paired-device > .button-wrapper {
display: flex;
height: 36px;
justify-content: space-between;
flex-direction: row;
align-items: center;
width: 100%;
}
.paired-device > .button-wrapper > label,
.paired-device > .button-wrapper > button {
display: flex;
align-items: center;
text-align: center;
white-space: nowrap;
justify-content: center;
width: 50%;
padding-left: 6px;
padding-right: 6px;
height: 36px;
}
.paired-device > .button-wrapper > :not(:last-child) {
border-right: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device > .button-wrapper > :not(:first-child) {
border-left: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device * {
overflow: hidden;
text-overflow: ellipsis;
}
.paired-device > .auto-accept {
cursor: pointer;
}
/* Receive Dialog */
x-dialog .row {
@@ -837,7 +948,7 @@ x-paper > div:last-child > .button:not(:last-child) {
}
/* Send Text Dialog */
/* Todo: add pair underline to send / receive dialogs displayName */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
@@ -855,9 +966,9 @@ x-dialog .dialog-subheader {
max-height: calc(100vh - 393px);
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: all;
-moz-user-select: all;
user-select: all;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
white-space: pre-wrap;
padding: 15px 0;
}
@@ -1039,11 +1150,14 @@ button::-moz-focus-inner {
text-align: center;
}
#about header {
z-index: 1;
}
#about .fade-in {
transition: opacity 300ms;
will-change: opacity;
transition-delay: 300ms;
z-index: 11;
pointer-events: all;
}
@@ -1057,12 +1171,23 @@ button::-moz-focus-inner {
--icon-size: 96px;
}
#about .title-wrapper {
display: flex;
align-items: baseline;
}
#about .title-wrapper > div {
margin-left: 0.5em;
}
#about x-background {
position: absolute;
top: calc(32px - 250px);
right: calc(32px - 250px);
width: 500px;
height: 500px;
--size: max(max(230vw, 230vh), calc(150vh + 150vw));
--size-half: calc(var(--size)/2);
top: calc(28px - var(--size-half));
right: calc(36px - var(--size-half));
width: var(--size);
height: var(--size);
border-radius: 50%;
background: var(--primary-color);
transform: scale(0);
@@ -1076,7 +1201,7 @@ button::-moz-focus-inner {
}
#about:target x-background {
transform: scale(10);
transform: scale(1);
}
#about .row a {
@@ -1091,6 +1216,14 @@ button::-moz-focus-inner {
align-self: end;
}
canvas.circles {
width: 100vw;
position: absolute;
z-index: -10;
top: 0;
left: 0;
}
/* Loading Indicator */
.progress {
@@ -1199,7 +1332,7 @@ x-peers:empty~x-instructions {
@media (hover: none) and (pointer: coarse) {
x-peer {
transform: scale(0.95);
padding: 4px 0;
padding: 4px;
}
}
@@ -1212,7 +1345,7 @@ x-peers:empty~x-instructions {
transition: opacity 300ms;
}
#websocket-fallback>span {
#websocket-fallback > span {
margin: 2px;
}
@@ -1232,7 +1365,7 @@ x-peers:empty~x-instructions {
@media screen and (min-height: 800px) {
#websocket-fallback {
padding-bottom: 15px;
padding-bottom: 16px;
}
}
@@ -1362,3 +1495,9 @@ x-dialog x-paper {
background: #bfbfbf;
border-radius: 4px;
}
::-moz-selection,
::selection {
color: black;
background: var(--primary-color);
}

View File

@@ -3,6 +3,11 @@
"iceServers": [
{
"urls": "stun:stun.l.google.com:19302"
},
{
"urls": "turn:example.com:3478",
"username": "username",
"credential": "password"
}
]
}

38
turnserver_example.conf Normal file
View File

@@ -0,0 +1,38 @@
# TURN server name and realm
realm=<DOMAIN>
server-name=pairdrop
# IPs the TURN server listens to
listening-ip=0.0.0.0
# External IP-Address of the TURN server
external-ip=<IP_ADDRESS>
# Main listening port
listening-port=3478
# Further ports that are open for communication
min-port=10000
max-port=20000
# Use fingerprint in TURN message
fingerprint
# Log file path
log-file=/var/log/turnserver.log
# Enable verbose logging
verbose
# Specify the user for the TURN authentification
user=user:password
# Enable long-term credential mechanism
lt-cred-mech
# SSL certificates
cert=/etc/letsencrypt/live/<DOMAIN>/cert.pem
pkey=/etc/letsencrypt/live/<DOMAIN>/privkey.pem
# 443 for TURN over TLS, which can bypass firewalls
tls-listening-port=443