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
text/typescript
/**
* 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();