UNPKG

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.

592 lines (518 loc) 15.3 kB
/** * DNSweeper API統合サービス * * Cloudflare・Route53・DNSプロバイダーAPI統合レイヤー */ import { EventEmitter } from 'events'; import type { Account } from '../types/auth'; export interface DNSProvider { id: string; name: string; type: 'cloudflare' | 'route53' | 'generic'; enabled: boolean; config: DNSProviderConfig; limits: { requestsPerSecond: number; requestsPerHour: number; maxConcurrent: number; }; } export interface DNSProviderConfig { cloudflare?: { apiToken: string; accountId: string; baseUrl?: string; }; route53?: { accessKeyId: string; secretAccessKey: string; region: string; sessionToken?: string; }; generic?: { baseUrl: string; apiKey: string; authMethod: 'header' | 'query' | 'bearer'; headers?: Record<string, string>; }; } export interface DNSZone { id: string; name: string; status: 'active' | 'pending' | 'error'; provider: string; recordCount: number; lastModified: Date; nameservers: string[]; } export interface DNSRecord { id: string; zoneId: string; zoneName: string; name: string; type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA' | 'PTR' | 'SRV' | 'CAA'; content: string; ttl: number; priority?: number; weight?: number; port?: number; proxied?: boolean; comment?: string; tags?: string[]; lastModified: Date; provider: string; } export interface APIRequestMetrics { provider: string; endpoint: string; method: string; statusCode: number; responseTime: number; requestSize: number; responseSize: number; timestamp: Date; error?: string; } export interface SyncJob { id: string; accountId: string; providerId: string; type: 'full_sync' | 'incremental_sync' | 'zone_sync' | 'record_sync'; status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'; progress: { total: number; completed: number; errors: number; }; startedAt?: Date; completedAt?: Date; error?: string; results: { zonesProcessed: number; recordsProcessed: number; recordsCreated: number; recordsUpdated: number; recordsDeleted: number; errors: string[]; }; } export class APIIntegrationService extends EventEmitter { private providers: Map<string, DNSProvider> = new Map(); private requestMetrics: APIRequestMetrics[] = []; private syncJobs: Map<string, SyncJob> = new Map(); private rateLimiters: Map<string, RateLimiter> = new Map(); constructor() { super(); this.initializeDefaultProviders(); } /** * デフォルトプロバイダーの初期化 */ private initializeDefaultProviders() { // Cloudflare プロバイダー const cloudflareProvider: DNSProvider = { id: 'cloudflare_default', name: 'Cloudflare DNS', type: 'cloudflare', enabled: false, config: { cloudflare: { apiToken: '', // 環境変数から取得 accountId: '', // 環境変数から取得 baseUrl: 'https://api.cloudflare.com/client/v4' } }, limits: { requestsPerSecond: 4, requestsPerHour: 1200, maxConcurrent: 10 } }; // Route53 プロバイダー const route53Provider: DNSProvider = { id: 'route53_default', name: 'AWS Route53', type: 'route53', enabled: false, config: { route53: { accessKeyId: '', // 環境変数から取得 secretAccessKey: '', // 環境変数から取得 region: 'us-east-1' } }, limits: { requestsPerSecond: 5, requestsPerHour: 1000, maxConcurrent: 5 } }; this.providers.set(cloudflareProvider.id, cloudflareProvider); this.providers.set(route53Provider.id, route53Provider); // レート制限器の初期化 this.rateLimiters.set(cloudflareProvider.id, new RateLimiter( cloudflareProvider.limits.requestsPerSecond, cloudflareProvider.limits.requestsPerHour )); this.rateLimiters.set(route53Provider.id, new RateLimiter( route53Provider.limits.requestsPerSecond, route53Provider.limits.requestsPerHour )); } /** * プロバイダー設定の更新 */ async updateProviderConfig(providerId: string, config: DNSProviderConfig, account: Account): Promise<void> { const provider = this.providers.get(providerId); if (!provider) { throw new Error(`Provider ${providerId} not found`); } // 認証情報の暗号化保存(実装時) provider.config = config; provider.enabled = true; this.providers.set(providerId, provider); // 設定テストを実行 await this.testProviderConnection(providerId); this.emit('providerConfigUpdated', { providerId, account }); } /** * プロバイダー接続テスト */ async testProviderConnection(providerId: string): Promise<boolean> { const provider = this.providers.get(providerId); if (!provider || !provider.enabled) { throw new Error(`Provider ${providerId} not found or disabled`); } try { switch (provider.type) { case 'cloudflare': return await this.testCloudflareConnection(provider); case 'route53': return await this.testRoute53Connection(provider); case 'generic': return await this.testGenericConnection(provider); default: throw new Error(`Unsupported provider type: ${provider.type}`); } } catch (error: any) { this.emit('providerTestFailed', { providerId, error: error.message }); throw error; } } /** * Cloudflare接続テスト */ private async testCloudflareConnection(provider: DNSProvider): Promise<boolean> { const config = provider.config.cloudflare!; const startTime = Date.now(); try { const response = await fetch(`${config.baseUrl}/user/tokens/verify`, { method: 'GET', headers: { 'Authorization': `Bearer ${config.apiToken}`, 'Content-Type': 'application/json' } }); const responseTime = Date.now() - startTime; this.recordAPIMetrics({ provider: provider.id, endpoint: '/user/tokens/verify', method: 'GET', statusCode: response.status, responseTime, requestSize: 0, responseSize: 0, timestamp: new Date() }); if (!response.ok) { throw new Error(`Cloudflare API test failed: ${response.status} ${response.statusText}`); } const data = await response.json(); return data.success === true; } catch (error: any) { this.recordAPIMetrics({ provider: provider.id, endpoint: '/user/tokens/verify', method: 'GET', statusCode: 0, responseTime: Date.now() - startTime, requestSize: 0, responseSize: 0, timestamp: new Date(), error: error.message }); throw error; } } /** * Route53接続テスト */ private async testRoute53Connection(provider: DNSProvider): Promise<boolean> { // TODO: AWS SDK v3を使用したRoute53接続テスト実装 // 一時的にモック実装 await new Promise(resolve => setTimeout(resolve, 100)); return true; } /** * 汎用DNS API接続テスト */ private async testGenericConnection(provider: DNSProvider): Promise<boolean> { const config = provider.config.generic!; const startTime = Date.now(); try { const headers: Record<string, string> = { 'Content-Type': 'application/json', ...config.headers }; if (config.authMethod === 'header') { headers['X-API-Key'] = config.apiKey; } else if (config.authMethod === 'bearer') { headers['Authorization'] = `Bearer ${config.apiKey}`; } let url = `${config.baseUrl}/health`; if (config.authMethod === 'query') { url += `?api_key=${config.apiKey}`; } const response = await fetch(url, { method: 'GET', headers }); const responseTime = Date.now() - startTime; this.recordAPIMetrics({ provider: provider.id, endpoint: '/health', method: 'GET', statusCode: response.status, responseTime, requestSize: 0, responseSize: 0, timestamp: new Date() }); return response.ok; } catch (error: any) { this.recordAPIMetrics({ provider: provider.id, endpoint: '/health', method: 'GET', statusCode: 0, responseTime: Date.now() - startTime, requestSize: 0, responseSize: 0, timestamp: new Date(), error: error.message }); throw error; } } /** * ゾーン一覧の取得 */ async getZones(providerId: string): Promise<DNSZone[]> { const provider = this.providers.get(providerId); if (!provider || !provider.enabled) { throw new Error(`Provider ${providerId} not found or disabled`); } const rateLimiter = this.rateLimiters.get(providerId); if (rateLimiter && !await rateLimiter.canMakeRequest()) { throw new Error(`Rate limit exceeded for provider ${providerId}`); } try { switch (provider.type) { case 'cloudflare': return await this.getCloudflareZones(provider); case 'route53': return await this.getRoute53Zones(provider); case 'generic': return await this.getGenericZones(provider); default: throw new Error(`Unsupported provider type: ${provider.type}`); } } catch (error) { this.emit('apiError', { providerId, operation: 'getZones', error }); throw error; } } /** * Cloudflareゾーン取得 */ private async getCloudflareZones(provider: DNSProvider): Promise<DNSZone[]> { const config = provider.config.cloudflare!; // TODO: 実装 return []; } /** * Route53ゾーン取得 */ private async getRoute53Zones(provider: DNSProvider): Promise<DNSZone[]> { // TODO: AWS SDK v3実装 return []; } /** * 汎用APIゾーン取得 */ private async getGenericZones(provider: DNSProvider): Promise<DNSZone[]> { // TODO: 実装 return []; } /** * 同期ジョブの作成・実行 */ async createSyncJob( accountId: string, providerId: string, type: SyncJob['type'], options?: { zoneIds?: string[] } ): Promise<string> { const jobId = `sync_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const job: SyncJob = { id: jobId, accountId, providerId, type, status: 'pending', progress: { total: 0, completed: 0, errors: 0 }, results: { zonesProcessed: 0, recordsProcessed: 0, recordsCreated: 0, recordsUpdated: 0, recordsDeleted: 0, errors: [] } }; this.syncJobs.set(jobId, job); // 非同期で同期処理を開始 this.executeSyncJob(jobId, options).catch(error => { console.error(`Sync job ${jobId} failed:`, error); job.status = 'failed'; job.error = error.message; job.completedAt = new Date(); this.emit('syncJobFailed', job); }); return jobId; } /** * 同期ジョブの実行 */ private async executeSyncJob(jobId: string, options?: { zoneIds?: string[] }): Promise<void> { const job = this.syncJobs.get(jobId); if (!job) { throw new Error(`Sync job ${jobId} not found`); } job.status = 'running'; job.startedAt = new Date(); this.emit('syncJobStarted', job); try { // 実際の同期処理 const zones = await this.getZones(job.providerId); job.progress.total = zones.length; for (const zone of zones) { if (options?.zoneIds && !options.zoneIds.includes(zone.id)) { continue; } try { // ゾーンレコードの同期処理 job.progress.completed++; job.results.zonesProcessed++; this.emit('syncJobProgress', job); } catch (error: any) { job.progress.errors++; job.results.errors.push(`Zone ${zone.name}: ${error.message}`); } } job.status = 'completed'; job.completedAt = new Date(); this.emit('syncJobCompleted', job); } catch (error: any) { job.status = 'failed'; job.error = error.message; job.completedAt = new Date(); throw error; } } /** * API リクエストメトリクスの記録 */ private recordAPIMetrics(metrics: APIRequestMetrics): void { this.requestMetrics.push(metrics); // 古いメトリクスの削除(メモリ使用量制限) if (this.requestMetrics.length > 10000) { this.requestMetrics = this.requestMetrics.slice(-5000); } this.emit('apiMetrics', metrics); } /** * プロバイダー一覧の取得 */ getProviders(): DNSProvider[] { return Array.from(this.providers.values()); } /** * 同期ジョブの取得 */ getSyncJob(jobId: string): SyncJob | undefined { return this.syncJobs.get(jobId); } /** * アカウント別同期ジョブ一覧の取得 */ getSyncJobs(accountId: string): SyncJob[] { return Array.from(this.syncJobs.values()) .filter(job => job.accountId === accountId) .sort((a, b) => (b.startedAt?.getTime() || 0) - (a.startedAt?.getTime() || 0)); } /** * APIメトリクスの取得 */ getAPIMetrics(providerId?: string, timeRange?: { from: Date; to: Date }): APIRequestMetrics[] { let metrics = this.requestMetrics; if (providerId) { metrics = metrics.filter(m => m.provider === providerId); } if (timeRange) { metrics = metrics.filter(m => m.timestamp >= timeRange.from && m.timestamp <= timeRange.to ); } return metrics.slice(-1000); // 最新1000件を返す } } /** * レート制限器クラス */ class RateLimiter { private requestsPerSecond: number; private requestsPerHour: number; private requestTimestamps: number[] = []; constructor(requestsPerSecond: number, requestsPerHour: number) { this.requestsPerSecond = requestsPerSecond; this.requestsPerHour = requestsPerHour; } async canMakeRequest(): Promise<boolean> { const now = Date.now(); // 1時間以上古いタイムスタンプを削除 this.requestTimestamps = this.requestTimestamps.filter( timestamp => now - timestamp < 60 * 60 * 1000 ); // 1秒以内のリクエスト数をチェック const recentRequests = this.requestTimestamps.filter( timestamp => now - timestamp < 1000 ); if (recentRequests.length >= this.requestsPerSecond) { return false; } // 1時間以内のリクエスト数をチェック if (this.requestTimestamps.length >= this.requestsPerHour) { return false; } // リクエスト実行を記録 this.requestTimestamps.push(now); return true; } } // グローバルインスタンス export const apiIntegrationService = new APIIntegrationService();