dnsweeper
Version:
Advanced CLI tool for DNS record risk analysis and cleanup. Features CSV import for Cloudflare/Route53, automated risk assessment, and parallel DNS validation.
336 lines (282 loc) • 7.67 kB
text/typescript
/**
* DNS解決結果のキャッシュ機能
*/
import type { IDNSQuery } from '../types/index.js';
import type { IDNSResponse } from './dns-resolver.js';
/**
* キャッシュエントリ
*/
interface CacheEntry {
response: IDNSResponse;
timestamp: number;
ttl: number;
accessCount: number;
lastAccess: number;
}
/**
* キャッシュ設定
*/
export interface DnsCacheOptions {
maxSize?: number;
defaultTtl?: number;
maxTtl?: number;
minTtl?: number;
cleanupInterval?: number;
}
/**
* キャッシュ統計情報
*/
export interface CacheStats {
totalEntries: number;
hitRate: number;
totalHits: number;
totalMisses: number;
totalEvictions: number;
averageTtl: number;
oldestEntry: number;
newestEntry: number;
memoryUsage: number;
}
/**
* DNS解決結果のキャッシュクラス
*/
export class DnsCache {
private cache = new Map<string, CacheEntry>();
private stats = {
hits: 0,
misses: 0,
evictions: 0,
};
private cleanupTimer?: NodeJS.Timeout;
constructor(private options: DnsCacheOptions = {}) {
this.options = {
maxSize: 1000,
defaultTtl: 300, // 5分
maxTtl: 86400, // 24時間
minTtl: 60, // 1分
cleanupInterval: 60000, // 1分
...options,
};
// 定期的なクリーンアップを開始
this.startCleanup();
}
/**
* キャッシュキーを生成
*/
private createKey(query: IDNSQuery): string {
return `${query.domain}:${query.type}:${query.server || 'default'}`.toLowerCase();
}
/**
* TTLを計算(レコードのTTLまたはデフォルトTTLを使用)
*/
private calculateTtl(response: IDNSResponse): number {
// レコードからTTLを取得
const recordTtls = response.records
.map((record) => record.ttl)
.filter((ttl) => ttl !== undefined);
let ttl = this.options.defaultTtl!;
if (recordTtls.length > 0) {
// 最小のTTLを使用
ttl = Math.min(...recordTtls);
}
// TTL制限を適用
ttl = Math.max(this.options.minTtl!, Math.min(this.options.maxTtl!, ttl));
return ttl;
}
/**
* キャッシュから値を取得
*/
get(query: IDNSQuery): IDNSResponse | null {
const key = this.createKey(query);
const entry = this.cache.get(key);
if (!entry) {
this.stats.misses++;
return null;
}
const now = Date.now();
const age = (now - entry.timestamp) / 1000;
// TTL期限切れチェック
if (age > entry.ttl) {
this.cache.delete(key);
this.stats.misses++;
return null;
}
// アクセス統計を更新
entry.accessCount++;
entry.lastAccess = now;
this.stats.hits++;
return entry.response;
}
/**
* キャッシュに値を設定
*/
set(query: IDNSQuery, response: IDNSResponse): void {
// エラーレスポンスはキャッシュしない
if (response.status === 'error') {
return;
}
const key = this.createKey(query);
const now = Date.now();
const ttl = this.calculateTtl(response);
// キャッシュサイズ制限
if (this.cache.size >= this.options.maxSize! && !this.cache.has(key)) {
this.evictOldest();
}
const entry: CacheEntry = {
response,
timestamp: now,
ttl,
accessCount: 1,
lastAccess: now,
};
this.cache.set(key, entry);
}
/**
* 最も古いエントリを削除
*/
private evictOldest(): void {
let oldestKey: string | null = null;
let oldestTime = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (entry.lastAccess < oldestTime) {
oldestTime = entry.lastAccess;
oldestKey = key;
}
}
if (oldestKey) {
this.cache.delete(oldestKey);
this.stats.evictions++;
}
}
/**
* 期限切れエントリをクリーンアップ
*/
cleanup(): number {
const now = Date.now();
let cleaned = 0;
for (const [key, entry] of this.cache.entries()) {
const age = (now - entry.timestamp) / 1000;
if (age > entry.ttl) {
this.cache.delete(key);
cleaned++;
}
}
return cleaned;
}
/**
* 定期的なクリーンアップを開始
*/
private startCleanup(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
}
this.cleanupTimer = setInterval(() => {
this.cleanup();
}, this.options.cleanupInterval);
}
/**
* キャッシュをクリア
*/
clear(): void {
this.cache.clear();
this.stats = { hits: 0, misses: 0, evictions: 0 };
}
/**
* 特定のドメインのキャッシュを削除
*/
invalidate(domain?: string, type?: string): number {
let deleted = 0;
for (const [key, _] of this.cache.entries()) {
const [cachedDomain, cachedType] = key.split(':');
let shouldDelete = false;
if (domain && type) {
shouldDelete = cachedDomain === domain.toLowerCase() && cachedType === type.toUpperCase();
} else if (domain) {
shouldDelete = cachedDomain === domain.toLowerCase();
} else if (type) {
shouldDelete = cachedType === type.toUpperCase();
}
if (shouldDelete) {
this.cache.delete(key);
deleted++;
}
}
return deleted;
}
/**
* キャッシュ統計を取得
*/
getStats(): CacheStats {
const totalRequests = this.stats.hits + this.stats.misses;
const hitRate = totalRequests > 0 ? (this.stats.hits / totalRequests) * 100 : 0;
const now = Date.now();
let totalTtl = 0;
let oldestEntry = now;
let newestEntry = 0;
for (const entry of this.cache.values()) {
totalTtl += entry.ttl;
oldestEntry = Math.min(oldestEntry, entry.timestamp);
newestEntry = Math.max(newestEntry, entry.timestamp);
}
const averageTtl = this.cache.size > 0 ? totalTtl / this.cache.size : 0;
const memoryUsage = this.estimateMemoryUsage();
return {
totalEntries: this.cache.size,
hitRate: Math.round(hitRate * 100) / 100,
totalHits: this.stats.hits,
totalMisses: this.stats.misses,
totalEvictions: this.stats.evictions,
averageTtl: Math.round(averageTtl),
oldestEntry: this.cache.size > 0 ? now - oldestEntry : 0,
newestEntry: this.cache.size > 0 ? now - newestEntry : 0,
memoryUsage,
};
}
/**
* メモリ使用量を推定
*/
private estimateMemoryUsage(): number {
let size = 0;
for (const [key, entry] of this.cache.entries()) {
// キーのサイズ
size += key.length * 2; // UTF-16
// エントリのサイズ(簡易計算)
size += JSON.stringify(entry.response).length * 2;
size += 64; // エントリのメタデータ
}
return size;
}
/**
* キャッシュを破棄
*/
destroy(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = undefined;
}
this.clear();
}
/**
* キャッシュの内容をエクスポート
*/
export(): Array<{ key: string; entry: CacheEntry }> {
return Array.from(this.cache.entries()).map(([key, entry]) => ({
key,
entry: { ...entry },
}));
}
/**
* キャッシュの内容をインポート
*/
import(data: Array<{ key: string; entry: CacheEntry }>): void {
this.clear();
const now = Date.now();
for (const { key, entry } of data) {
// 期限切れでないエントリのみインポート
const age = (now - entry.timestamp) / 1000;
if (age < entry.ttl) {
this.cache.set(key, entry);
}
}
}
}