208 lines
6.3 KiB
JavaScript
208 lines
6.3 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._setRtcSupported(request);
|
|
|
|
// 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.socket.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();
|
|
}
|
|
}
|
|
|
|
_setRtcSupported(request) {
|
|
const searchParams = new URL(request.url, "http://server").searchParams;
|
|
this.rtcSupported = searchParams.get('webrtc_supported') === "true";
|
|
}
|
|
|
|
_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];
|
|
}
|
|
}
|
|
} |