UNPKG

lotus-sdk

Version:

Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem

275 lines (274 loc) 10.6 kB
import { EventEmitter } from 'events'; import { CORE_P2P_SECURITY_LIMITS, } from './types.js'; export class DHTAnnouncementRateLimiter { lastAnnouncement = new Map(); announcementCount = new Map(); canAnnounce(peerId, minInterval = 30_000) { const now = Date.now(); const lastTime = this.lastAnnouncement.get(peerId); if (!lastTime) { this.lastAnnouncement.set(peerId, now); this.incrementCount(peerId); return true; } const elapsed = now - lastTime; if (elapsed < minInterval) { return false; } this.lastAnnouncement.set(peerId, now); this.incrementCount(peerId); return true; } incrementCount(peerId) { const count = (this.announcementCount.get(peerId) || 0) + 1; this.announcementCount.set(peerId, count); } getCount(peerId) { return this.announcementCount.get(peerId) || 0; } cleanup() { const now = Date.now(); const maxAge = 24 * 60 * 60 * 1000; for (const [peerId, timestamp] of this.lastAnnouncement) { if (now - timestamp > maxAge) { this.lastAnnouncement.delete(peerId); this.announcementCount.delete(peerId); } } } } export class DHTResourceTracker { peerResources = new Map(); peerResourcesByType = new Map(); canAnnounceResource(peerId, resourceType, resourceId) { const resourceKey = `${resourceType}:${resourceId}`; const peerResourceSet = this.peerResources.get(peerId) || new Set(); if (peerResourceSet.size >= CORE_P2P_SECURITY_LIMITS.MAX_DHT_RESOURCES_PER_PEER && !peerResourceSet.has(resourceKey)) { console.warn(`[P2P Security] Peer ${peerId} exceeded global DHT resource limit (${CORE_P2P_SECURITY_LIMITS.MAX_DHT_RESOURCES_PER_PEER})`); return false; } let typeMap = this.peerResourcesByType.get(peerId); if (!typeMap) { typeMap = new Map(); this.peerResourcesByType.set(peerId, typeMap); } const typeResourceSet = typeMap.get(resourceType) || new Set(); if (typeResourceSet.size >= CORE_P2P_SECURITY_LIMITS.MAX_DHT_RESOURCES_PER_TYPE_PER_PEER && !typeResourceSet.has(resourceId)) { console.warn(`[P2P Security] Peer ${peerId} exceeded per-type DHT resource limit for ${resourceType} (${CORE_P2P_SECURITY_LIMITS.MAX_DHT_RESOURCES_PER_TYPE_PER_PEER})`); return false; } peerResourceSet.add(resourceKey); this.peerResources.set(peerId, peerResourceSet); typeResourceSet.add(resourceId); typeMap.set(resourceType, typeResourceSet); return true; } removeResource(peerId, resourceType, resourceId) { const resourceKey = `${resourceType}:${resourceId}`; const peerResourceSet = this.peerResources.get(peerId); if (peerResourceSet) { peerResourceSet.delete(resourceKey); if (peerResourceSet.size === 0) { this.peerResources.delete(peerId); } } const typeMap = this.peerResourcesByType.get(peerId); if (typeMap) { const typeResourceSet = typeMap.get(resourceType); if (typeResourceSet) { typeResourceSet.delete(resourceId); if (typeResourceSet.size === 0) { typeMap.delete(resourceType); } } if (typeMap.size === 0) { this.peerResourcesByType.delete(peerId); } } } getResourceCount(peerId) { return this.peerResources.get(peerId)?.size || 0; } getResourceCountByType(peerId, resourceType) { const typeMap = this.peerResourcesByType.get(peerId); return typeMap?.get(resourceType)?.size || 0; } } export class CorePeerBanManager extends EventEmitter { blacklist = new Set(); tempBans = new Map(); warningCount = new Map(); banPeer(peerId, reason) { this.blacklist.add(peerId); console.warn(`[P2P Security] ⛔ BANNED peer: ${peerId} (${reason})`); this.emit('peer:banned', peerId, reason); } tempBanPeer(peerId, durationMs, reason) { const until = Date.now() + durationMs; this.tempBans.set(peerId, until); console.warn(`[P2P Security] ⚠️ TEMP BAN: ${peerId} for ${Math.round(durationMs / 1000)}s (${reason})`); this.emit('peer:temp-banned', peerId, durationMs, reason); } warnPeer(peerId, reason) { const count = (this.warningCount.get(peerId) || 0) + 1; this.warningCount.set(peerId, count); console.warn(`[P2P Security] ⚠️ WARNING ${count}: ${peerId} (${reason})`); this.emit('peer:warned', peerId, count, reason); if (count >= 5) { this.tempBanPeer(peerId, 60 * 60 * 1000, 'repeated-warnings'); } if (count >= 10) { this.banPeer(peerId, 'excessive-warnings'); } } isAllowed(peerId) { if (this.blacklist.has(peerId)) { return false; } const tempBanUntil = this.tempBans.get(peerId); if (tempBanUntil && Date.now() < tempBanUntil) { return false; } if (tempBanUntil && Date.now() >= tempBanUntil) { this.tempBans.delete(peerId); console.log(`[P2P Security] Temp ban expired: ${peerId}`); } return true; } unbanPeer(peerId) { this.blacklist.delete(peerId); this.tempBans.delete(peerId); this.warningCount.delete(peerId); console.log(`[P2P Security] Unbanned peer: ${peerId}`); this.emit('peer:unbanned', peerId); } getBannedCount() { return this.blacklist.size; } getWarningCount() { return this.warningCount.size; } getBannedPeers() { return Array.from(this.blacklist); } } export class CoreSecurityManager extends EventEmitter { dhtRateLimiter; resourceTracker; peerBanManager; protocolValidators = new Map(); metrics; disableRateLimiting; customLimits; constructor(config) { super(); this.disableRateLimiting = config?.disableRateLimiting ?? false; this.customLimits = config?.customLimits ?? {}; this.dhtRateLimiter = new DHTAnnouncementRateLimiter(); this.resourceTracker = new DHTResourceTracker(); this.peerBanManager = new CorePeerBanManager(); this.metrics = { dhtAnnouncements: { total: 0, rejected: 0, rateLimited: 0 }, messages: { total: 0, rejected: 0, oversized: 0 }, peers: { banned: 0, warnings: 0 }, }; this.peerBanManager.on('peer:banned', (peerId, reason) => { this.metrics.peers.banned++; this.emit('peer:banned', peerId, reason); }); this.peerBanManager.on('peer:warned', (peerId, count, reason) => { this.metrics.peers.warnings++; this.emit('peer:warned', peerId, count, reason); }); if (this.disableRateLimiting) { console.warn('[P2P Security] ⚠️ RATE LIMITING DISABLED (testing mode)'); } } registerProtocolValidator(protocolName, validator) { this.protocolValidators.set(protocolName, validator); } async canAnnounceToDHT(peerId, resourceType, resourceId, data) { this.metrics.dhtAnnouncements.total++; if (this.disableRateLimiting) { return true; } if (!this.peerBanManager.isAllowed(peerId)) { this.metrics.dhtAnnouncements.rejected++; console.warn(`[P2P Security] DHT announcement rejected from banned peer: ${peerId}`); return false; } const minInterval = this.customLimits.MIN_DHT_ANNOUNCEMENT_INTERVAL ?? CORE_P2P_SECURITY_LIMITS.MIN_DHT_ANNOUNCEMENT_INTERVAL; if (!this.dhtRateLimiter.canAnnounce(peerId, minInterval)) { this.metrics.dhtAnnouncements.rateLimited++; this.peerBanManager.warnPeer(peerId, 'dht-rate-limit-violation'); console.warn(`[P2P Security] DHT announcement rate limited: ${peerId}`); return false; } if (!this.resourceTracker.canAnnounceResource(peerId, resourceType, resourceId)) { this.metrics.dhtAnnouncements.rejected++; this.peerBanManager.warnPeer(peerId, 'dht-resource-limit-exceeded'); return false; } const validator = this._findValidatorForResourceType(resourceType); if (validator?.canAnnounceResource) { const allowed = await validator.canAnnounceResource(resourceType, peerId); if (!allowed) { this.metrics.dhtAnnouncements.rejected++; return false; } } if (validator?.validateResourceAnnouncement) { const valid = await validator.validateResourceAnnouncement(resourceType, resourceId, data, peerId); if (!valid) { this.metrics.dhtAnnouncements.rejected++; this.peerBanManager.warnPeer(peerId, 'invalid-dht-announcement'); return false; } } return true; } recordMessage(valid, oversized = false) { this.metrics.messages.total++; if (!valid) { this.metrics.messages.rejected++; } if (oversized) { this.metrics.messages.oversized++; } } _findValidatorForResourceType(resourceType) { if (this.protocolValidators.has(resourceType)) { return this.protocolValidators.get(resourceType); } for (const [protocolName, validator] of this.protocolValidators) { if (resourceType.startsWith(protocolName)) { return validator; } } return undefined; } cleanup() { this.dhtRateLimiter.cleanup(); } getMetrics() { return { ...this.metrics, peers: { banned: this.peerBanManager.getBannedCount(), warnings: this.peerBanManager.getWarningCount(), }, }; } resetMetrics() { this.metrics = { dhtAnnouncements: { total: 0, rejected: 0, rateLimited: 0 }, messages: { total: 0, rejected: 0, oversized: 0 }, peers: { banned: 0, warnings: 0 }, }; } }