UNPKG

twenty-mcp-server

Version:

Easy-to-install Model Context Protocol server for Twenty CRM. Try instantly with 'npx twenty-mcp-server setup' or install globally for permanent use.

194 lines 6.82 kB
import { isIP } from 'node:net'; export class IPMiddleware { config; constructor() { this.config = this.parseConfig(); } parseConfig() { return { enabled: process.env.IP_PROTECTION_ENABLED === 'true', allowlist: this.parseIPList(process.env.IP_ALLOWLIST || ''), blockUnknown: process.env.IP_BLOCK_UNKNOWN !== 'false', trustedProxies: this.parseIPList(process.env.TRUSTED_PROXIES || ''), }; } parseIPList(ipList) { return ipList .split(',') .map(ip => ip.trim()) .filter(ip => ip.length > 0); } getClientIP(req) { const headers = req.headers; // Check forwarded headers if we have trusted proxies if (this.config.trustedProxies.length > 0) { const forwardedFor = headers['x-forwarded-for']; if (forwardedFor) { const ips = forwardedFor.split(',').map(ip => ip.trim()); return ips[0] || null; } const realIP = headers['x-real-ip']; if (realIP) { return realIP.trim(); } } // Fallback to socket remote address return req.socket.remoteAddress || null; } isIPInCIDR(ip, cidr) { // Handle single IP addresses if (!cidr.includes('/')) { return ip === cidr; } const [network, prefixLength] = cidr.split('/'); const prefix = parseInt(prefixLength, 10); if (isNaN(prefix)) { return false; } // Determine IP version const ipVersion = isIP(ip); const networkVersion = isIP(network); if (ipVersion === 0 || networkVersion === 0 || ipVersion !== networkVersion) { return false; } if (ipVersion === 4) { return this.isIPv4InCIDR(ip, network, prefix); } else { return this.isIPv6InCIDR(ip, network, prefix); } } isIPv4InCIDR(ip, network, prefix) { if (prefix < 0 || prefix > 32) { return false; } const ipParts = ip.split('.').map(Number); const networkParts = network.split('.').map(Number); if (ipParts.length !== 4 || networkParts.length !== 4) { return false; } const ipNum = (ipParts[0] << 24) + (ipParts[1] << 16) + (ipParts[2] << 8) + ipParts[3]; const networkNum = (networkParts[0] << 24) + (networkParts[1] << 16) + (networkParts[2] << 8) + networkParts[3]; const mask = prefix === 0 ? 0 : -1 << (32 - prefix); return (ipNum & mask) === (networkNum & mask); } isIPv6InCIDR(ip, network, prefix) { if (prefix < 0 || prefix > 128) { return false; } // Expand IPv6 addresses to full form const expandedIP = this.expandIPv6(ip); const expandedNetwork = this.expandIPv6(network); if (!expandedIP || !expandedNetwork) { return false; } const bitsToCheck = Math.floor(prefix / 16); const remainingBits = prefix % 16; const ipParts = expandedIP.split(':'); const networkParts = expandedNetwork.split(':'); // Check full 16-bit segments for (let i = 0; i < bitsToCheck; i++) { if (ipParts[i] !== networkParts[i]) { return false; } } // Check remaining bits in the next segment if (remainingBits > 0 && bitsToCheck < 8) { const ipSegment = parseInt(ipParts[bitsToCheck], 16); const networkSegment = parseInt(networkParts[bitsToCheck], 16); const mask = 0xFFFF << (16 - remainingBits); if ((ipSegment & mask) !== (networkSegment & mask)) { return false; } } return true; } expandIPv6(ip) { try { // Handle :: expansion if (ip.includes('::')) { const parts = ip.split('::'); if (parts.length !== 2) { return null; } const leftParts = parts[0] ? parts[0].split(':') : []; const rightParts = parts[1] ? parts[1].split(':') : []; const missingParts = 8 - leftParts.length - rightParts.length; const expanded = [ ...leftParts, ...Array(missingParts).fill('0000'), ...rightParts ]; return expanded.map(part => part.padStart(4, '0')).join(':'); } else { // Already expanded or single segments const parts = ip.split(':'); if (parts.length !== 8) { return null; } return parts.map(part => part.padStart(4, '0')).join(':'); } } catch { return null; } } isIPAllowed(ip) { // Always allow localhost/loopback if (ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1') { return true; } // Check against allowlist for (const allowedIP of this.config.allowlist) { if (this.isIPInCIDR(ip, allowedIP)) { return true; } } return false; } async checkAccess(req, res) { // Skip if IP protection is disabled if (!this.config.enabled) { return true; } const clientIP = this.getClientIP(req); if (!clientIP) { if (this.config.blockUnknown) { this.sendForbidden(res, 'Unable to determine client IP address'); return false; } return true; } // Validate IP format if (isIP(clientIP) === 0) { this.sendForbidden(res, 'Invalid IP address format'); return false; } // Check if IP is allowed if (!this.isIPAllowed(clientIP)) { this.sendForbidden(res, `Access denied for IP: ${clientIP}`); return false; } return true; } sendForbidden(res, message) { res.writeHead(403, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Authorization, Content-Type', }); res.end(JSON.stringify({ error: 'forbidden', error_description: message, })); } getConfig() { return { ...this.config }; } updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } } //# sourceMappingURL=ip-middleware.js.map