Ctrl K
ComunidadGuías…

push-sidecar/unifiedpush.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
const dns = require('dns');
const https = require('https');
const net = require('net');
const os = require('os');
const webpush = require('web-push');

// The relay forwards whatever endpoint a client registered, so without these
// limits the sidecar would POST to any URL a client names, including hosts
// inside this machine's network.
const MAX_TOKEN_LEN = 2048;
const MAX_ENDPOINT_LEN = 1024;
const ALLOW_PRIVATE = process.env.UNIFIEDPUSH_ALLOW_PRIVATE === '1';
// Matches the relay's offline buffer: a wake older than that finds nothing.
const TTL_SECS = 24 * 60 * 60;
const TIMEOUT_MS = 5000;

const vapidDetails =
  process.env.VAPID_PUBLIC_KEY && process.env.VAPID_PRIVATE_KEY && process.env.VAPID_SUBJECT
    ? {
        subject: process.env.VAPID_SUBJECT,
        publicKey: process.env.VAPID_PUBLIC_KEY,
        privateKey: process.env.VAPID_PRIVATE_KEY,
      }
    : null;

const blocked = new net.BlockList();
for (const [addr, prefix] of [
  ['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8],
  ['169.254.0.0', 16], ['172.16.0.0', 12], ['192.0.0.0', 24], ['192.0.2.0', 24],
  ['192.168.0.0', 16], ['198.18.0.0', 15], ['198.51.100.0', 24], ['203.0.113.0', 24],
  ['224.0.0.0', 4], ['240.0.0.0', 4],
]) blocked.addSubnet(addr, prefix, 'ipv4');
for (const [addr, prefix] of [
  ['::', 128], ['::1', 128], ['64:ff9b::', 96], ['100::', 64], ['2001:db8::', 32],
  ['fc00::', 7], ['fe80::', 10], ['ff00::', 8],
]) blocked.addSubnet(addr, prefix, 'ipv6');

// This machine's own public addresses reach its listeners from inside the firewall.
// Read once at start; without the list the ranges above still hold.
function blockOwnAddresses(interfaces) {
  try {
    for (const list of Object.values(interfaces || os.networkInterfaces())) {
      for (const a of list || []) {
        blocked.addAddress(a.address, a.family === 6 || a.family === 'IPv6' ? 'ipv6' : 'ipv4');
      }
    }
  } catch (err) {
    console.error(`[push-sidecar] own addresses not read: ${err.code || err.message}`);
  }
}
blockOwnAddresses();

function isPublicAddress(address, family) {
  if (family === 6 || family === 'IPv6') {
    // An IPv4-mapped address is judged as the IPv4 address it carries.
    const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(address);
    if (mapped) return !blocked.check(mapped[1], 'ipv4');
    return !blocked.check(address, 'ipv6');
  }
  return !blocked.check(address, 'ipv4');
}

// Filters inside the socket's own resolution, so the address that is checked
// is the address that is connected to.
function guardedLookup(hostname, options, callback) {
  dns.lookup(hostname, { ...options, all: true }, (err, addresses) => {
    if (err) return callback(err);
    const allowed = ALLOW_PRIVATE
      ? addresses
      : addresses.filter(a => isPublicAddress(a.address, a.family));
    if (allowed.length === 0) {
      const e = new Error('endpoint resolves to a non-public address');
      e.code = 'ENDPOINT_NOT_PUBLIC';
      return callback(e);
    }
    if (options.all) return callback(null, allowed);
    callback(null, allowed[0].address, allowed[0].family);
  });
}

const agent = new https.Agent({ lookup: guardedLookup, keepAlive: true, maxSockets: 16 });

const B64URL = /^[A-Za-z0-9_-]+={0,2}$/;

// Parses the app's registration, or null when anything is missing or off.
function parseToken(token) {
  if (typeof token !== 'string' || token.length > MAX_TOKEN_LEN) return null;
  let t;
  try {
    t = JSON.parse(token);
  } catch (_) {
    return null;
  }
  if (!t || t.v !== 1) return null;
  const { endpoint, p256dh, auth } = t;
  if (typeof endpoint !== 'string' || endpoint.length > MAX_ENDPOINT_LEN) return null;
  if (typeof p256dh !== 'string' || !B64URL.test(p256dh) || p256dh.length > 128) return null;
  if (typeof auth !== 'string' || !B64URL.test(auth) || auth.length > 64) return null;
  let url;
  try {
    url = new URL(endpoint);
  } catch (_) {
    return null;
  }
  if (url.protocol !== 'https:' || url.username || url.password) return null;
  // Sockets skip the lookup for an IP literal, so the guard runs here too.
  const literal = url.hostname.replace(/^\[|\]$/g, '');
  const family = net.isIP(literal);
  if (family && !ALLOW_PRIVATE && !isPublicAddress(literal, family)) return null;
  return { url, subscription: { endpoint, keys: { p256dh, auth } } };
}

// Encrypts [data] to the device's key set and posts it to its endpoint.
// Resolves to the HTTP status and body code the relay gets back.
async function sendUnifiedPush(token, data) {
  const parsed = parseToken(token);
  if (!parsed) return { status: 400, code: 'bad_unifiedpush_token' };
  // Nothing from the endpoint is logged: its path is the device's private address
  // on the push server, and a self-hosted server's host names its owner.
  try {
    await webpush.sendNotification(parsed.subscription, JSON.stringify(data), {
      TTL: TTL_SECS,
      urgency: 'high',
      contentEncoding: 'aes128gcm',
      vapidDetails,
      agent,
      timeout: TIMEOUT_MS,
    });
    return { status: 200, code: 'ok' };
  } catch (err) {
    if (err.statusCode === 404 || err.statusCode === 410) {
      console.error(`[push-sidecar] endpoint gone platform=unifiedpush status=${err.statusCode}`);
      return { status: 410, code: 'token_expired' };
    }
    if (err.code === 'ENDPOINT_NOT_PUBLIC') {
      console.error('[push-sidecar] refused platform=unifiedpush (non-public address)');
      return { status: 400, code: 'endpoint_not_public' };
    }
    console.error(
      `[push-sidecar] UnifiedPush error status=${err.statusCode || '-'} code=${err.code || '-'}`
    );
    return { status: 502, code: 'error' };
  }
}

module.exports = { sendUnifiedPush, parseToken, isPublicAddress, blockOwnAddresses };