UNPKG

@dollhousemcp/mcp-server

Version:

DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.

776 lines 108 kB
/** * SecureDownloader - Reusable utility for safe content downloads * * Implements the validate-before-write pattern with comprehensive security features: * - Content validation hooks (customizable validators) * - Atomic file operations with temp files * - Guaranteed cleanup on failure * - Memory-efficient streaming for large files * - Size limits to prevent DoS attacks * - Path validation to prevent traversal * - Timeout handling for network operations * - Content type validation * * Usage Examples: * * // Basic download with validation * const downloader = new SecureDownloader(); * await downloader.downloadToFile( * 'https://example.com/file.md', * './downloads/file.md', * { * validator: async (content) => ({ * isValid: !content.includes('malicious'), * errorMessage: content.includes('malicious') ? 'Malicious content detected' : undefined * }), * maxSize: 1024 * 1024, // 1MB limit * timeout: 30000 // 30 second timeout * } * ); * * // Download to memory with validation * const content = await downloader.downloadToMemory( * 'https://example.com/data.json', * { * validator: async (content) => { * try { * JSON.parse(content); * return { isValid: true }; * } catch { * return { isValid: false, errorMessage: 'Invalid JSON format' }; * } * } * } * ); * * // Streaming download for large files * await downloader.downloadStream( * 'https://example.com/large-file.zip', * './downloads/large-file.zip', * { * streamValidator: (chunk) => !chunk.includes(Buffer.from('VIRUS')), * maxSize: 100 * 1024 * 1024, // 100MB limit * timeout: 300000 // 5 minute timeout * } * ); */ import * as path from 'path'; import { randomBytes, createHash } from 'crypto'; import { Readable } from 'stream'; import { pipeline } from 'stream/promises'; import { createWriteStream } from 'fs'; import { SecurityError } from '../errors/SecurityError.js'; import { SECURITY_LIMITS } from '../security/constants.js'; import { ContentValidator as SecurityContentValidator } from '../security/contentValidator.js'; import { PathValidator } from '../security/pathValidator.js'; import { SecurityMonitor } from '../security/securityMonitor.js'; import { UnicodeValidator } from '../security/validators/unicodeValidator.js'; import { RateLimiter } from './RateLimiter.js'; import { logger } from './logger.js'; /** * Custom error types for different failure scenarios */ export class DownloadError extends Error { code; originalError; constructor(message, code, originalError) { super(message); this.code = code; this.originalError = originalError; this.name = 'DownloadError'; } static networkError(message, originalError) { return new DownloadError(message, 'NETWORK_ERROR', originalError); } static validationError(message) { return new DownloadError(message, 'VALIDATION_ERROR'); } static securityError(message) { return new DownloadError(message, 'SECURITY_ERROR'); } static timeoutError(message) { return new DownloadError(message, 'TIMEOUT_ERROR'); } static filesystemError(message, originalError) { return new DownloadError(message, 'FILESYSTEM_ERROR', originalError); } } /** * SecureDownloader - Implements validate-before-write pattern for safe downloads * * Key Security Features: * 1. VALIDATE-BEFORE-WRITE: All content validation occurs before any disk operations * 2. ATOMIC OPERATIONS: Uses temporary files with atomic rename to prevent corruption * 3. GUARANTEED CLEANUP: Automatic cleanup of temporary files on any failure * 4. SIZE LIMITS: Prevents DoS attacks through large file downloads * 5. PATH VALIDATION: Prevents directory traversal attacks * 6. TIMEOUT PROTECTION: Prevents hanging network operations * 7. CONTENT VALIDATION: Extensible validation system for different content types */ export class SecureDownloader { defaultTimeout; defaultMaxSize; tempDir; globalRateLimiter; urlRateLimiters; fileLockManager; fileOperations; constructor(options) { this.defaultTimeout = options?.defaultTimeout || 30000; // 30 seconds this.defaultMaxSize = options?.defaultMaxSize || SECURITY_LIMITS.MAX_FILE_SIZE; this.tempDir = options?.tempDir || '.tmp'; this.fileLockManager = options?.fileLockManager ?? undefined; this.fileOperations = options?.fileOperations ?? undefined; // Initialize rate limiters const rateLimitConfig = options?.rateLimitOptions || {}; this.globalRateLimiter = new RateLimiter({ maxRequests: rateLimitConfig.maxGlobalRequests || 100, // 100 downloads per hour globally windowMs: rateLimitConfig.windowMs || 60 * 60 * 1000, // 1 hour minDelayMs: 1000 // Minimum 1 second between requests }); this.urlRateLimiters = new Map(); } /** * Download content to a file with validation * * SECURITY: Implements validate-before-write pattern: * 1. Download content to memory * 2. Validate all content * 3. Only then write to disk atomically * * @param url - URL to download from * @param destinationPath - Local file path to save to * @param options - Download and validation options */ async downloadToFile(url, destinationPath, options = {}) { const startTime = Date.now(); logger.debug(`Starting secure download from ${url} to ${destinationPath}`); try { // SECURITY: Validate URL and destination path first this.validateUrl(url); const validatedPath = await this.validateDestinationPath(destinationPath); // SECURITY: Check if file already exists (prevent accidental overwrites) const fileExists = await this.fileOperations.exists(validatedPath); if (fileExists) { throw DownloadError.filesystemError(`File already exists: ${destinationPath}`); } // STEP 1: Check rate limits before download await this.checkRateLimit(url); // STEP 2: Download content to memory (no disk operations yet) const content = await this.downloadToMemory(url, options); // STEP 3: Validate checksum if provided if (options.expectedChecksum) { await this.validateChecksum(content, options.expectedChecksum); } // STEP 4: All validation is complete, now write atomically const useAtomic = options.atomic !== false; // Default to true if (useAtomic) { await this.atomicWriteFile(validatedPath, content); } else { await this.directWriteFile(validatedPath, content); } const duration = Date.now() - startTime; logger.info(`Secure download completed: ${destinationPath} (${content.length} bytes, ${duration}ms)`); // Log successful download for security monitoring SecurityMonitor.logSecurityEvent({ type: 'FILE_COPIED', severity: 'LOW', source: 'SecureDownloader', details: `Downloaded ${content.length} bytes from ${url} to ${destinationPath}`, metadata: { url, destinationPath, contentLength: content.length, duration } }); } catch (error) { const duration = Date.now() - startTime; logger.error(`Secure download failed: ${error instanceof Error ? error.message : String(error)}`); // Log failed download for security monitoring SecurityMonitor.logSecurityEvent({ type: 'PATH_TRAVERSAL_ATTEMPT', severity: 'MEDIUM', source: 'SecureDownloader', details: `Download failed: ${error instanceof Error ? error.message : String(error)}`, metadata: { url, destinationPath, duration, errorType: error instanceof DownloadError ? error.code : 'UNKNOWN' } }); throw error; } } /** * Download content to memory with validation * * @param url - URL to download from * @param options - Download and validation options * @returns Validated content as string */ async downloadToMemory(url, options = {}) { const timeout = options.timeout || this.defaultTimeout; const maxSize = options.maxSize || this.defaultMaxSize; logger.debug(`Downloading content from ${url} (max: ${maxSize} bytes, timeout: ${timeout}ms)`); try { // SECURITY: Validate URL format this.validateUrl(url); // STEP 1: Check rate limits before download await this.checkRateLimit(url); // STEP 2: Fetch content with size and timeout protection const content = await this.fetchWithLimits(url, maxSize, timeout, options.headers); // STEP 3: Validate content type if specified if (options.expectedContentType) { await this.validateContentType(content, options.expectedContentType); } // STEP 4: Validate checksum if provided if (options.expectedChecksum) { await this.validateChecksum(content, options.expectedChecksum); } // STEP 5: Run built-in security validation const securityResult = SecurityContentValidator.validateAndSanitize(content); if (!securityResult.isValid && securityResult.severity === 'critical') { throw DownloadError.securityError(`Critical security threat detected: ${securityResult.detectedPatterns?.join(', ')}`); } // STEP 6: Run custom validator if provided if (options.validator) { logger.debug('Running custom content validation'); const validationResult = await options.validator(content); if (!validationResult.isValid) { throw DownloadError.validationError(validationResult.errorMessage || 'Content validation failed'); } } logger.debug(`Content validation passed (${content.length} bytes)`); return securityResult.sanitizedContent || content; } catch (error) { if (error instanceof DownloadError) { throw error; } throw DownloadError.networkError(`Failed to download content from ${url}: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined); } } /** * Download large files using streaming with chunk-level validation * * @param url - URL to download from * @param destinationPath - Local file path to save to * @param options - Streaming download options */ async downloadStream(url, destinationPath, options = {}) { const startTime = Date.now(); const maxSize = options.maxSize || this.defaultMaxSize; const timeout = options.timeout || this.defaultTimeout; logger.debug(`Starting streaming download from ${url} to ${destinationPath}`); try { // SECURITY: Check rate limits before download await this.checkRateLimit(url); // SECURITY: Validate URL and destination path this.validateUrl(url); const validatedPath = await this.validateDestinationPath(destinationPath); // Generate temporary file path for atomic operation const tempPath = await this.getTempFilePath(validatedPath); let downloadedSize = 0; let timeoutHandle; // Track write stream for cleanup (declared outside try for catch block access) let writeStream; let writeStreamClosed = false; // Create abort controller for timeout handling const abortController = new AbortController(); timeoutHandle = setTimeout(() => { abortController.abort(); }, timeout); try { // SECURITY: Fetch with abort signal for timeout const response = await fetch(url, { signal: abortController.signal, headers: options.headers }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } if (!response.body) { throw new Error('Response body is null'); } // Ensure temp directory exists await this.fileOperations.createDirectory(path.dirname(tempPath)); // Create write stream to temporary file writeStream = createWriteStream(tempPath); // Track whether write stream has been closed for cleanup writeStream.on('close', () => { writeStreamClosed = true; }); // Create a transform stream for validation and size checking const validationStream = new Readable({ async read() { // This stream will be fed by the pipeline } }); // Set up chunk validation and size checking const reader = response.body.getReader(); const pump = async () => { try { while (true) { const { done, value } = await reader.read(); if (done) break; // SECURITY: Check size limit downloadedSize += value.length; if (downloadedSize > maxSize) { throw DownloadError.securityError(`File size exceeds limit: ${downloadedSize} > ${maxSize} bytes`); } // SECURITY: Run chunk validator if provided if (options.streamValidator && !options.streamValidator(value)) { throw DownloadError.validationError('Chunk validation failed'); } validationStream.push(value); } validationStream.push(null); // End stream } catch (error) { validationStream.destroy(error instanceof Error ? error : new Error(String(error))); } }; // Start the pump and pipeline concurrently await Promise.all([ pump(), pipeline(validationStream, writeStream) ]); // Clear timeout if (timeoutHandle) { clearTimeout(timeoutHandle); timeoutHandle = undefined; } // SECURITY: Atomic rename to final destination await this.fileOperations.renameFile(tempPath, validatedPath); const duration = Date.now() - startTime; logger.info(`Streaming download completed: ${destinationPath} (${downloadedSize} bytes, ${duration}ms)`); // Log successful streaming download SecurityMonitor.logSecurityEvent({ type: 'FILE_COPIED', severity: 'LOW', source: 'SecureDownloader', details: `Streamed ${downloadedSize} bytes from ${url} to ${destinationPath}`, metadata: { url, destinationPath, contentLength: downloadedSize, duration } }); } catch (error) { // SECURITY: Ensure write stream is closed before cleanup to release file handle // This is critical on macOS where unlink can fail or leave stale directory entries // if the file handle is still open if (writeStream && !writeStreamClosed) { await new Promise((resolve) => { writeStream.destroy(); writeStream.once('close', () => resolve()); // Safety timeout in case close event never fires setTimeout(() => resolve(), 100); }); } // SECURITY: Guaranteed cleanup of temporary file try { await this.fileOperations.deleteFile(tempPath, undefined, { source: 'SecureDownloader.downloadStream' }); logger.debug(`Cleaned up temp file: ${tempPath}`); } catch (cleanupError) { logger.warn(`Failed to clean up temp file ${tempPath}: ${cleanupError}`); } throw error; } finally { if (timeoutHandle) { clearTimeout(timeoutHandle); } } } catch (error) { const duration = Date.now() - startTime; logger.error(`Streaming download failed: ${error instanceof Error ? error.message : String(error)}`); // Log failed streaming download SecurityMonitor.logSecurityEvent({ type: 'PATH_TRAVERSAL_ATTEMPT', severity: 'MEDIUM', source: 'SecureDownloader', details: `Streaming download failed: ${error instanceof Error ? error.message : String(error)}`, metadata: { url, destinationPath, duration, errorType: error instanceof DownloadError ? error.code : 'UNKNOWN' } }); if (error instanceof Error && error.name === 'AbortError') { throw DownloadError.timeoutError(`Download timed out after ${timeout}ms`); } if (error instanceof DownloadError) { throw error; } throw DownloadError.networkError(`Streaming download failed: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error : undefined); } } /** * Validate URL format and security with Unicode normalization */ validateUrl(url) { if (!url || typeof url !== 'string') { throw DownloadError.validationError('URL must be a non-empty string'); } // SECURITY FIX: DMCP-SEC-004 - Unicode normalization on user input const unicodeValidation = UnicodeValidator.normalize(url); const normalizedUrl = unicodeValidation.normalizedContent; if (!unicodeValidation.isValid) { SecurityMonitor.logSecurityEvent({ type: 'UNICODE_VALIDATION_ERROR', severity: 'MEDIUM', source: 'SecureDownloader', details: `URL contains suspicious Unicode patterns: ${unicodeValidation.detectedIssues?.join(', ')}`, metadata: { originalUrl: url, normalizedUrl } }); } // Use normalized URL for further validation url = normalizedUrl; let parsedUrl; try { parsedUrl = new URL(url); } catch { throw DownloadError.validationError(`Invalid URL format: ${url}`); } // SECURITY: Only allow HTTPS and HTTP protocols if (!['https:', 'http:'].includes(parsedUrl.protocol)) { throw DownloadError.securityError(`Unsupported protocol: ${parsedUrl.protocol}. Only HTTP/HTTPS allowed.`); } // SECURITY: Prevent requests to localhost/private networks const hostname = parsedUrl.hostname.toLowerCase(); if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') { throw DownloadError.securityError('Downloads from localhost are not allowed'); } // SECURITY: Check for private IP ranges (basic protection) if (hostname.startsWith('192.168.') || hostname.startsWith('10.') || hostname.startsWith('172.')) { throw DownloadError.securityError('Downloads from private IP ranges are not allowed'); } } /** * Validate destination path for security */ async validateDestinationPath(filePath) { try { // Use existing PathValidator for comprehensive path validation return await PathValidator.validatePersonaPath(filePath); } catch (error) { throw DownloadError.securityError(`Invalid destination path: ${error instanceof Error ? error.message : String(error)}`); } } /** * Fetch content with size and timeout limits */ async fetchWithLimits(url, maxSize, timeout, headers) { const abortController = new AbortController(); const timeoutHandle = setTimeout(() => abortController.abort(), timeout); try { const response = await fetch(url, { signal: abortController.signal, headers: { 'User-Agent': 'DollhouseMCP-SecureDownloader/1.0', ...headers } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // SECURITY: Check Content-Length header if available const contentLength = response.headers.get('content-length'); if (contentLength && Number.parseInt(contentLength, 10) > maxSize) { throw DownloadError.securityError(`Content size ${contentLength} exceeds limit of ${maxSize} bytes`); } // Read content with size checking const chunks = []; let totalSize = 0; if (!response.body) { throw new Error('Response body is null'); } const reader = response.body.getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; totalSize += value.length; if (totalSize > maxSize) { throw DownloadError.securityError(`Content size ${totalSize} exceeds limit of ${maxSize} bytes`); } chunks.push(value); } } finally { reader.releaseLock(); } // Combine chunks and decode const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0); const combined = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { combined.set(chunk, offset); offset += chunk.length; } return new TextDecoder('utf-8').decode(combined); } catch (error) { if (error instanceof Error && error.name === 'AbortError') { throw DownloadError.timeoutError(`Request timed out after ${timeout}ms`); } throw error; } finally { clearTimeout(timeoutHandle); } } /** * Validate content type if specified */ async validateContentType(content, expectedType) { // Basic content type validation based on content analysis switch (expectedType.toLowerCase()) { case 'json': try { JSON.parse(content); } catch { throw DownloadError.validationError('Content is not valid JSON'); } break; case 'yaml': case 'yml': // Use existing YAML validation if (!SecurityContentValidator.validateYamlContent(content)) { throw DownloadError.validationError('Content is not valid YAML'); } break; case 'markdown': case 'md': // Basic markdown validation (check for frontmatter format) if (content.startsWith('---')) { const frontmatterEnd = content.indexOf('\n---\n', 3); if (frontmatterEnd === -1) { throw DownloadError.validationError('Invalid markdown frontmatter format'); } } break; default: logger.debug(`No specific validation for content type: ${expectedType}`); } } /** * Atomic file write using FileOperationsService */ async atomicWriteFile(filePath, content) { // Ensure directory exists await this.fileOperations.createDirectory(path.dirname(filePath)); // Use FileOperationsService's atomic write await this.fileOperations.writeFile(filePath, content, { source: 'SecureDownloader.atomicWriteFile', atomic: true }); } /** * Direct file write (non-atomic, for when atomic is disabled) */ async directWriteFile(filePath, content) { // Ensure directory exists await this.fileOperations.createDirectory(path.dirname(filePath)); // Direct write await this.fileOperations.writeFile(filePath, content, { source: 'SecureDownloader.directWriteFile', atomic: false }); } /** * Generate temporary file path for atomic operations */ async getTempFilePath(originalPath) { const dir = path.dirname(originalPath); const basename = path.basename(originalPath); const random = randomBytes(8).toString('hex'); const tempDir = path.join(dir, this.tempDir); // Ensure temp directory exists await this.fileOperations.createDirectory(tempDir); return path.join(tempDir, `${basename}.${random}.tmp`); } /** * Check rate limits for downloads */ async checkRateLimit(url) { // Check global rate limit const globalStatus = this.globalRateLimiter.checkLimit(); if (!globalStatus.allowed) { SecurityMonitor.logSecurityEvent({ type: 'RATE_LIMIT_EXCEEDED', severity: 'MEDIUM', source: 'SecureDownloader', details: `Global download rate limit exceeded. Retry after ${globalStatus.retryAfterMs}ms`, metadata: { url, retryAfterMs: globalStatus.retryAfterMs } }); throw DownloadError.securityError(`Download rate limit exceeded. Please retry after ${Math.ceil(globalStatus.retryAfterMs / 1000)} seconds`); } // Check per-URL rate limit const parsedUrl = new URL(url); const urlKey = `${parsedUrl.hostname}:${parsedUrl.port || (parsedUrl.protocol === 'https:' ? '443' : '80')}`; if (!this.urlRateLimiters.has(urlKey)) { this.urlRateLimiters.set(urlKey, new RateLimiter({ maxRequests: 10, // 10 requests per hour per URL windowMs: 60 * 60 * 1000, minDelayMs: 5000 // 5 second minimum delay between requests to same URL })); } const urlLimiter = this.urlRateLimiters.get(urlKey); const urlStatus = urlLimiter.checkLimit(); if (!urlStatus.allowed) { SecurityMonitor.logSecurityEvent({ type: 'RATE_LIMIT_EXCEEDED', severity: 'MEDIUM', source: 'SecureDownloader', details: `Per-URL download rate limit exceeded for ${urlKey}. Retry after ${urlStatus.retryAfterMs}ms`, metadata: { url, urlKey, retryAfterMs: urlStatus.retryAfterMs } }); throw DownloadError.securityError(`Too many requests to ${urlKey}. Please retry after ${Math.ceil(urlStatus.retryAfterMs / 1000)} seconds`); } // Consume rate limit tokens this.globalRateLimiter.consumeToken(); urlLimiter.consumeToken(); } /** * Validate content checksum for integrity verification */ async validateChecksum(content, expectedChecksum) { const normalizedExpected = expectedChecksum.toLowerCase().trim(); // Validate checksum format (SHA-256 should be 64 hex characters) if (!/^[a-f0-9]{64}$/.test(normalizedExpected)) { throw DownloadError.validationError('Invalid checksum format. Expected SHA-256 (64 hex characters)'); } const contentBuffer = Buffer.from(content, 'utf-8'); const actualChecksum = createHash('sha256').update(contentBuffer).digest('hex'); if (actualChecksum !== normalizedExpected) { SecurityMonitor.logSecurityEvent({ type: 'CONTENT_INJECTION_ATTEMPT', severity: 'HIGH', source: 'SecureDownloader', details: `Checksum mismatch detected - possible content tampering`, metadata: { expectedChecksum: normalizedExpected, actualChecksum, contentLength: content.length } }); throw DownloadError.securityError(`Content checksum verification failed. Expected: ${normalizedExpected}, Got: ${actualChecksum}`); } logger.debug(`Checksum validation passed: ${actualChecksum}`); } /** * Create a content validator that combines multiple validators */ static combineValidators(...validators) { return async (content) => { for (const validator of validators) { const result = await validator(content); if (!result.isValid) { return result; } } return { isValid: true }; }; } /** * Create a content validator for JSON content */ static jsonValidator() { return async (content) => { try { JSON.parse(content); return { isValid: true }; } catch (error) { return { isValid: false, errorMessage: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}`, severity: 'medium' }; } }; } /** * Create a content validator for YAML content */ static yamlValidator() { return async (content) => { const isValid = SecurityContentValidator.validateYamlContent(content); return { isValid, errorMessage: isValid ? undefined : 'Invalid or malicious YAML content', severity: isValid ? 'low' : 'high' }; }; } /** * Create a content validator for markdown content */ static markdownValidator() { return async (content) => { try { // Use existing persona content sanitization for markdown SecurityContentValidator.sanitizePersonaContent(content); return { isValid: true }; } catch (error) { return { isValid: false, errorMessage: `Invalid markdown: ${error instanceof Error ? error.message : String(error)}`, severity: error instanceof SecurityError ? 'critical' : 'medium' }; } }; } /** * Create a content validator with size limits */ static sizeValidator(maxSize) { return async (content) => { const size = Buffer.byteLength(content, 'utf-8'); if (size > maxSize) { return { isValid: false, errorMessage: `Content size ${size} exceeds limit of ${maxSize} bytes`, severity: 'high' }; } return { isValid: true }; }; } /** * Create a content validator that checks for forbidden patterns */ static patternValidator(forbiddenPatterns, errorMessage = 'Forbidden pattern detected') { return async (content) => { for (const pattern of forbiddenPatterns) { if (pattern.test(content)) { return { isValid: false, errorMessage, severity: 'high', metadata: { pattern: pattern.source } }; } } return { isValid: true }; }; } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiU2VjdXJlRG93bmxvYWRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9TZWN1cmVEb3dubG9hZGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBdURHO0FBRUgsT0FBTyxLQUFLLElBQUksTUFBTSxNQUFNLENBQUM7QUFDN0IsT0FBTyxFQUFFLFdBQVcsRUFBRSxVQUFVLEVBQUUsTUFBTSxRQUFRLENBQUM7QUFDakQsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLFFBQVEsQ0FBQztBQUNsQyxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFDM0MsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sSUFBSSxDQUFDO0FBRXZDLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUMzRCxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDM0QsT0FBTyxFQUFFLGdCQUFnQixJQUFJLHdCQUF3QixFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDL0YsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLDhCQUE4QixDQUFDO0FBRTdELE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUNqRSxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSw0Q0FBNEMsQ0FBQztBQUM5RSxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDL0MsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGFBQWEsQ0FBQztBQTZEckM7O0dBRUc7QUFDSCxNQUFNLE9BQU8sYUFBYyxTQUFRLEtBQUs7SUFHcEI7SUFDQTtJQUhsQixZQUNFLE9BQWUsRUFDQyxJQUFZLEVBQ1osYUFBcUI7UUFFckMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBSEMsU0FBSSxH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