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
JavaScript
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 },
};
}
}