pairdrop/server/peer.js

200 lines
6.1 KiB
JavaScript

import crypto from "crypto";
import parser from "ua-parser-js";
import {animals, colors, uniqueNamesGenerator} from "unique-names-generator";
import {cyrb53, hasher} from "./helper.js";
export default class Peer {
constructor(socket, request, conf) {
this.conf = conf
// set socket
this.socket = socket;
// set remote ip
this._setIP(request);
// set peer id
this._setPeerId(request);
// is WebRTC supported ?
this.rtcSupported = request.url.indexOf('webrtc') > -1;
// set name
this._setName(request);
this.requestRate = 0;
this.roomSecrets = [];
this.pairKey = null;
this.publicRoomId = null;
}
rateLimitReached() {
// rate limit implementation: max 10 attempts every 10s
if (this.requestRate >= 10) {
return true;
}
this.requestRate += 1;
setTimeout(_ => this.requestRate -= 1, 10000);
return false;
}
_setIP(request) {
if (request.headers['cf-connecting-ip']) {
this.ip = request.headers['cf-connecting-ip'].split(/\s*,\s*/)[0];
} else if (request.headers['x-forwarded-for']) {
this.ip = request.headers['x-forwarded-for'].split(/\s*,\s*/)[0];
} else {
this.ip = request.connection.remoteAddress;
}
// remove the prefix used for IPv4-translated addresses
if (this.ip.substring(0,7) === "::ffff:")
this.ip = this.ip.substring(7);
let ipv6_was_localized = false;
if (this.conf.ipv6Localize && this.ip.includes(':')) {
this.ip = this.ip.split(':',this.conf.ipv6Localize).join(':');
ipv6_was_localized = true;
}
if (this.conf.debugMode) {
console.debug("\n");
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", this.conf.ipv6Localize, this.conf.ipv6Localize > 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)) {
this.ip = '127.0.0.1';
}
}
ipIsPrivate(ip) {
// if ip is IPv4
if (!ip.includes(":")) {
// 10.0.0.0 - 10.255.255.255 || 172.16.0.0 - 172.31.255.255 || 192.168.0.0 - 192.168.255.255
return /^(10)\.(.*)\.(.*)\.(.*)$/.test(ip) || /^(172)\.(1[6-9]|2[0-9]|3[0-1])\.(.*)\.(.*)$/.test(ip) || /^(192)\.(168)\.(.*)\.(.*)$/.test(ip)
}
// else: ip is IPv6
const firstWord = ip.split(":").find(el => !!el); //get first not empty word
if (/^fe[c-f][0-f]$/.test(firstWord)) {
// The original IPv6 Site Local addresses (fec0::/10) are deprecated. Range: fec0 - feff
return true;
}
// These days Unique Local Addresses (ULA) are used in place of Site Local.
// Range: fc00 - fcff
else if (/^fc[0-f]{2}$/.test(firstWord)) {
return true;
}
// Range: fd00 - fcff
else if (/^fd[0-f]{2}$/.test(firstWord)) {
return true;
}
// Link local addresses (prefixed with fe80) are not routable
else if (firstWord === "fe80") {
return true;
}
// Discard Prefix
else if (firstWord === "100") {
return true;
}
// Any other IP address is not Unique Local Address (ULA)
return false;
}
_setPeerId(request) {
const searchParams = new URL(request.url, "http://server").searchParams;
let peerId = searchParams.get("peer_id");
let peerIdHash = searchParams.get("peer_id_hash");
if (peerId && Peer.isValidUuid(peerId) && this.isPeerIdHashValid(peerId, peerIdHash)) {
this.id = peerId;
} else {
this.id = crypto.randomUUID();
}
}
_setName(req) {
let ua = parser(req.headers['user-agent']);
let deviceName = '';
if (ua.os && ua.os.name) {
deviceName = ua.os.name.replace('Mac OS', 'Mac') + ' ';
}
if (ua.device.model) {
deviceName += ua.device.model;
} else {
deviceName += ua.browser.name;
}
if (!deviceName) {
deviceName = 'Unknown Device';
}
const displayName = uniqueNamesGenerator({
length: 2,
separator: ' ',
dictionaries: [colors, animals],
style: 'capital',
seed: cyrb53(this.id)
})
this.name = {
model: ua.device.model,
os: ua.os.name,
browser: ua.browser.name,
type: ua.device.type,
deviceName,
displayName
};
}
getInfo() {
return {
id: this.id,
name: this.name,
rtcSupported: this.rtcSupported
}
}
static isValidUuid(uuid) {
return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid);
}
isPeerIdHashValid(peerId, peerIdHash) {
return peerIdHash === hasher.hashCodeSalted(peerId);
}
addRoomSecret(roomSecret) {
if (!(roomSecret in this.roomSecrets)) {
this.roomSecrets.push(roomSecret);
}
}
removeRoomSecret(roomSecret) {
if (roomSecret in this.roomSecrets) {
delete this.roomSecrets[roomSecret];
}
}
}