const dns = require('dns'); const ipaddr = require('ipaddr.js'); const { isIPv6, isIPv4 } = require('net'); const cfg = require('../../../lib/config'); const jwt = require('../../../lib/jwt'); const user = require('../../../lib/db/user'); const ServerUser = require('../../../lib/db/user/User'); const { ERROR, WARNING, INFO, DEBUG } = require('DOUGAL_ROOT/debug')(__filename); function parseIP(ip) { if (!ip || typeof ip !== 'string') { WARNING('Invalid IP input:', ip); return null; } // Handle comma-separated X-Forwarded-For (e.g., "87.90.254.127,") const cleanIp = ip.split(',')[0].trim(); if (!cleanIp) { WARNING('Empty IP after parsing:', ip); return null; } // Convert IPv6-mapped IPv4 (e.g., ::ffff:127.0.0.1 -> 127.0.0.1) if (cleanIp.startsWith('::ffff:') && isIPv4(cleanIp.split('::ffff:')[1])) { return cleanIp.split('::ffff:')[1]; } return cleanIp; } function normalizeCIDR(range) { if (!range || typeof range !== 'string') { WARNING('Invalid CIDR range:', range); return null; } // If no /prefix, assume /32 for IPv4 or /128 for IPv6 if (!range.includes('/')) { try { const parsed = ipaddr.parse(range); const prefix = parsed.kind() === 'ipv4' ? 32 : 128; return `${range}/${prefix}`; } catch (err) { WARNING(`Failed to parse bare IP ${range}:`, err.message); return null; } } return range; } async function authorisedIP(req, res) { const ip = parseIP(req.ip || req.headers['x-forwarded-for'] || req.headers['x-real-ip']); DEBUG('authorisedIP:', { ip, headers: req.headers }); // Debug if (!ip) { WARNING('No valid IP provided:', { ip, headers: req.headers }); return false; } let addr; try { addr = ipaddr.parse(ip); } catch (err) { WARNING('Invalid IP:', ip, err.message); return false; } const validIPs = await user.ip({ active: true }); // Get active IP logins // Attach parsed CIDR to each IP entry validIPs.forEach(i => { const normalized = normalizeCIDR(i.ip); if (!normalized) { i.$range = null; return; } try { const [rangeAddr, prefix] = ipaddr.parseCIDR(normalized); i.$range = { addr: rangeAddr, prefix }; } catch (err) { WARNING(`Invalid CIDR range ${i.ip}:`, err.message); i.$range = null; // Skip invalid ranges } }); // Filter out invalid ranges and sort by specificity (descending prefix length) const validRanges = validIPs.filter(i => i.$range).sort((a, b) => b.$range.prefix - a.$range.prefix); for (const ipEntry of validRanges) { const { addr: rangeAddr, prefix } = ipEntry.$range; try { if (addr.match(rangeAddr, prefix)) { const payload = { ...ipEntry, ip, autologin: true }; delete payload.$range; delete payload.hash; delete payload.active; jwt.issue(payload, req, res); return true; } } catch (err) { WARNING(`Error checking range ${ipEntry.ip}:`, err.message); continue; } } return false; } async function authorisedHost(req, res) { const ip = parseIP(req.ip || req.headers['x-forwarded-for'] || req.headers['x-real-ip']); DEBUG('authorisedHost:', { ip, headers: req.headers }); // Debug if (!ip) { WARNING('No valid IP for host check:', { ip, headers: req.headers }); return false; } const validHosts = await user.host({ active: true }); for (const key in validHosts) { try { const resolvedIPs = await dns.promises.resolve(key); if (resolvedIPs.includes(ip)) { const payload = { ...validHosts[key], ip, autologin: true }; delete payload.$block; delete payload.hash; delete payload.active; jwt.issue(payload, req, res); return true; } } catch (err) { if (err.code !== 'ENODATA') { ERROR(`DNS error for host ${key}:`, err); } } } return false; } async function auth(req, res, next) { if (res.headersSent) { return; // Handled by another middleware } // Check for valid JWT if (req.user) { if (!req.user.autologin && req.user.exp) { const ttl = req.user.exp - Date.now() / 1000; if (ttl < cfg.jwt.options.expiresIn / 2) { const credentials = await ServerUser.fromSQL(null, req.user.id); if (credentials) { const payload = Object.assign({}, credentials.toJSON()); jwt.issue(payload, req, res); } } } next(); return; } // Check IP and host if (await authorisedIP(req, res)) { next(); return; } if (await authorisedHost(req, res)) { next(); return; } next({ status: 401, message: 'Not authorised' }); } module.exports = auth;