UNPKG

capacitor-cors-bypass-enhanced

Version:

Enhanced Capacitor plugin for CORS bypass with HTTP/2, HTTP/3, gRPC, GraphQL, file operations, and advanced networking features. Modular TypeScript definitions for better maintainability.

569 lines (488 loc) 18.7 kB
import { WebPlugin } from '@capacitor/core'; // MCP SDK imports (ESM) import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import type { CorsBypassPlugin, HttpRequestOptions, HttpResponse, StreamRequestOptions, SSEOptions, SSEConnectionOptions, SSEConnection, WebSocketConnectionOptions, WebSocketConnection, MCPClientOptions, MCPClient, MCPResourceList, MCPResource, MCPToolList, MCPToolResult, MCPPromptList, MCPPrompt, MCPSamplingRequest, MCPSamplingResponse, Interceptor, InterceptorOptions, InterceptorHandle, GlobalProxyConfig, ProxyConfig, ProxyTestResult, ProxyStatus, } from './definitions'; // Import modular managers import { UtilsManager } from './web/utils'; import { HttpManager } from './web/http'; import { StreamManager } from './web/stream'; import { SSEManager } from './web/sse'; import { WebSocketManager } from './web/websocket'; import { InterceptorManager } from './web/interceptor'; export class CorsBypassWeb extends WebPlugin implements CorsBypassPlugin { private proxyServerUrl: string | null = null; private globalProxyConfig: GlobalProxyConfig | null = null; private proxyRequestCount: number = 0; private proxyLastSuccessTime: number | null = null; private proxyLastError: string | null = null; // Modular managers private utilsManager: UtilsManager; private httpManager: HttpManager; private streamManager: StreamManager; private sseManager: SSEManager; private wsManager: WebSocketManager; private interceptorManager: InterceptorManager; // MCP specific private mcpClients = new Map<string, any>(); private mcpTransports = new Map<string, any>(); private connectionCounter = 0; constructor() { super(); // Initialize managers this.utilsManager = new UtilsManager(); this.httpManager = new HttpManager(this.proxyServerUrl); this.streamManager = new StreamManager(this.proxyServerUrl, this.notifyListeners.bind(this)); this.sseManager = new SSEManager(this.proxyServerUrl, this.notifyListeners.bind(this)); this.wsManager = new WebSocketManager(this.notifyListeners.bind(this)); this.interceptorManager = new InterceptorManager(); // Try to detect if a proxy server is available this.detectProxyServer(); } private async detectProxyServer() { const possibleUrls = [ 'http://localhost:3002', 'http://127.0.0.1:3002', 'http://localhost:3001', 'http://127.0.0.1:3001', 'http://localhost:8080', 'http://127.0.0.1:8080' ]; for (const url of possibleUrls) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1000); const response = await fetch(`${url}/health`, { method: 'GET', signal: controller.signal }); clearTimeout(timeoutId); if (response.ok) { this.proxyServerUrl = url; this.httpManager.setProxyServer(url); this.streamManager = new StreamManager(this.proxyServerUrl, this.notifyListeners.bind(this)); this.sseManager = new SSEManager(this.proxyServerUrl, this.notifyListeners.bind(this)); console.log(`🔧 CORS Proxy server detected at: ${url}`); break; } } catch (error) { // Ignore errors, continue checking } } if (!this.proxyServerUrl) { console.warn('⚠️ No CORS proxy server detected. Some requests may fail due to CORS.'); console.log('💡 To enable full functionality, run: node web-proxy-server.js'); } } /** * Set custom proxy server URL */ setProxyServer(url: string) { this.proxyServerUrl = url; this.httpManager.setProxyServer(url); this.streamManager = new StreamManager(this.proxyServerUrl, this.notifyListeners.bind(this)); this.sseManager = new SSEManager(this.proxyServerUrl, this.notifyListeners.bind(this)); console.log(`🔧 Proxy server set to: ${url}`); } async request(options: HttpRequestOptions): Promise<HttpResponse> { const interceptors = this.interceptorManager.getInterceptorsInternal(); return this.httpManager.request(options, interceptors); } async get(options: HttpRequestOptions): Promise<HttpResponse> { const interceptors = this.interceptorManager.getInterceptorsInternal(); return this.httpManager.get(options, interceptors); } async post(options: HttpRequestOptions): Promise<HttpResponse> { const interceptors = this.interceptorManager.getInterceptorsInternal(); return this.httpManager.post(options, interceptors); } async put(options: HttpRequestOptions): Promise<HttpResponse> { const interceptors = this.interceptorManager.getInterceptorsInternal(); return this.httpManager.put(options, interceptors); } async patch(options: HttpRequestOptions): Promise<HttpResponse> { const interceptors = this.interceptorManager.getInterceptorsInternal(); return this.httpManager.patch(options, interceptors); } async delete(options: HttpRequestOptions): Promise<HttpResponse> { const interceptors = this.interceptorManager.getInterceptorsInternal(); return this.httpManager.delete(options, interceptors); } /** * Streaming HTTP request - supports AI model streaming output */ async streamRequest(options: StreamRequestOptions): Promise<{ streamId: string }> { return this.streamManager.streamRequest(options); } /** * Cancel streaming request */ async cancelStream(options: { streamId: string }): Promise<void> { return this.streamManager.cancelStream(options); } async startSSE(options: SSEOptions): Promise<{ connectionId: string }> { return this.sseManager.startSSE(options); } async stopSSE(options: { connectionId: string }): Promise<void> { return this.sseManager.stopSSE(options); } async createSSEConnection(options: SSEConnectionOptions): Promise<SSEConnection> { return this.sseManager.createSSEConnection(options); } async closeSSEConnection(options: { connectionId: string }): Promise<void> { return this.sseManager.closeSSEConnection(options); } async createWebSocketConnection(options: WebSocketConnectionOptions): Promise<WebSocketConnection> { return this.wsManager.createWebSocketConnection(options); } async closeWebSocketConnection(options: { connectionId: string }): Promise<void> { return this.wsManager.closeWebSocketConnection(options); } async sendWebSocketMessage(options: { connectionId: string; message: string }): Promise<void> { return this.wsManager.sendWebSocketMessage(options); } // ===== MCP Protocol Methods ===== async createMCPClient(options: MCPClientOptions): Promise<MCPClient> { const connectionId = `mcp_${++this.connectionCounter}`; try { // Determine transport type and URL const transport = options.transport || 'streamablehttp'; // Get URL (support both new and legacy config) let url = options.url; if (!url && options.sseUrl) { // Backward compatibility: use sseUrl if url is not provided url = options.sseUrl; } if (!url) { throw new Error('URL is required for MCP client (provide either "url" or "sseUrl")'); } // Create transport layer let mcpTransport: any; if (transport === 'streamablehttp') { // Use new StreamableHTTP transport (recommended) throw new Error('StreamableHTTP transport should use mcpClientManager. Use @capacitor/cors-bypass-enhanced web managers directly.'); } else if (transport === 'sse' || options.sseUrl) { // Legacy SSE transport if (this.proxyServerUrl && this.utilsManager.isCrossOrigin(url)) { // Use proxy server const proxyUrl = `${this.proxyServerUrl}/sse-proxy/${encodeURIComponent(url)}`; mcpTransport = new SSEClientTransport(new URL(proxyUrl)); } else { // Direct connection mcpTransport = new SSEClientTransport(new URL(url)); } } else { throw new Error(`Unsupported transport type: ${transport}`); } // Create MCP client const client = new Client( { name: options.clientInfo.name, version: options.clientInfo.version, }, { capabilities: { roots: options.capabilities?.roots ? { listChanged: true } : undefined, sampling: options.capabilities?.sampling ? {} : undefined, } } ); // Connect to server await client.connect(mcpTransport); // Store client and transport this.mcpClients.set(connectionId, client); this.mcpTransports.set(connectionId, mcpTransport); console.log(`✅ MCP client connected: ${connectionId}`); return { connectionId, status: 'connected', serverCapabilities: client.getServerCapabilities(), protocolVersion: '2025-03-26' }; } catch (error) { console.error(`❌ MCP client connection failed:`, error); throw new Error(`Failed to create MCP client: ${error}`); } } async listMCPResources(options: { connectionId: string; cursor?: string }): Promise<MCPResourceList> { const client = this.mcpClients.get(options.connectionId); if (!client) { throw new Error('MCP client not found'); } try { const result = await client.listResources(options.cursor ? { cursor: options.cursor } : {}); return { resources: result.resources || [], nextCursor: result.nextCursor }; } catch (error) { throw new Error(`Failed to list MCP resources: ${error}`); } } async readMCPResource(options: { connectionId: string; uri: string }): Promise<MCPResource> { const client = this.mcpClients.get(options.connectionId); if (!client) { throw new Error('MCP client not found'); } try { const result = await client.readResource({ uri: options.uri }); return { uri: options.uri, mimeType: result.contents?.[0]?.mimeType || 'text/plain', text: (result.contents?.[0] as any)?.text || '', blob: (result.contents?.[0] as any)?.data }; } catch (error) { throw new Error(`Failed to read MCP resource: ${error}`); } } async listMCPTools(options: { connectionId: string; cursor?: string }): Promise<MCPToolList> { const client = this.mcpClients.get(options.connectionId); if (!client) { throw new Error('MCP client not found'); } try { const result = await client.listTools(options.cursor ? { cursor: options.cursor } : {}); return { tools: result.tools || [], nextCursor: result.nextCursor }; } catch (error) { throw new Error(`Failed to list MCP tools: ${error}`); } } async callMCPTool(options: { connectionId: string; name: string; arguments?: any }): Promise<MCPToolResult> { const client = this.mcpClients.get(options.connectionId); if (!client) { throw new Error('MCP client not found'); } try { const result = await client.callTool({ name: options.name, arguments: options.arguments || {} }); return { content: (result.content as any) || [], isError: (result as any).isError || false }; } catch (error) { throw new Error(`Failed to call MCP tool: ${error}`); } } async listMCPPrompts(options: { connectionId: string; cursor?: string }): Promise<MCPPromptList> { const client = this.mcpClients.get(options.connectionId); if (!client) { throw new Error('MCP client not found'); } try { const result = await client.listPrompts(options.cursor ? { cursor: options.cursor } : {}); return { prompts: result.prompts || [], nextCursor: result.nextCursor }; } catch (error) { throw new Error(`Failed to list MCP prompts: ${error}`); } } async getMCPPrompt(options: { connectionId: string; name: string; arguments?: any }): Promise<MCPPrompt> { const client = this.mcpClients.get(options.connectionId); if (!client) { throw new Error('MCP client not found'); } try { const result = await client.getPrompt({ name: options.name, arguments: options.arguments || {} }); return { description: result.description, messages: result.messages || [] }; } catch (error) { throw new Error(`Failed to get MCP prompt: ${error}`); } } async sendMCPSampling(options: { connectionId: string; request: MCPSamplingRequest }): Promise<MCPSamplingResponse> { const client = this.mcpClients.get(options.connectionId); if (!client) { throw new Error('MCP client not found'); } try { const result = await client.request({ method: options.request.method, params: options.request.params }); return result; } catch (error) { throw new Error(`Failed to send MCP sampling request: ${error}`); } } // ==================== Interceptor Management ==================== async addInterceptor(interceptor: Interceptor, options?: InterceptorOptions): Promise<InterceptorHandle> { return this.interceptorManager.addInterceptor(interceptor, options); } async removeInterceptor(handle: InterceptorHandle | string): Promise<void> { return this.interceptorManager.removeInterceptor(handle); } async removeAllInterceptors(): Promise<void> { return this.interceptorManager.removeAllInterceptors(); } async getInterceptors(): Promise<InterceptorHandle[]> { return this.interceptorManager.getInterceptors(); } // ==================== Proxy Management ==================== /** * Set global proxy configuration * Note: On Web platform, proxy is handled through the CORS proxy server * The proxy config is stored and can be passed to the server for server-side proxying */ async setGlobalProxy(config: GlobalProxyConfig): Promise<void> { this.globalProxyConfig = config; // If using a proxy server, we can configure it to use the specified proxy if (this.proxyServerUrl && config.enabled) { console.log(`🔧 [Web] Global proxy configured: ${config.type || 'http'}://${config.host}:${config.port}`); console.log('💡 Note: Web platform proxying requires server-side support.'); } } /** * Get current global proxy configuration */ async getGlobalProxy(): Promise<GlobalProxyConfig | null> { return this.globalProxyConfig; } /** * Clear global proxy configuration */ async clearGlobalProxy(): Promise<void> { this.globalProxyConfig = null; this.proxyLastError = null; console.log('🔧 [Web] Global proxy configuration cleared'); } /** * Test proxy connection * On Web platform, this tests connectivity through the CORS proxy server */ async testProxy(config: ProxyConfig, testUrl?: string): Promise<ProxyTestResult> { const startTime = Date.now(); const url = testUrl || 'https://www.google.com'; if (!config.enabled || !config.host) { return { success: false, error: 'Proxy configuration is invalid or disabled', responseTime: 0 }; } try { // On Web, we can only test through our proxy server if (this.proxyServerUrl) { const response = await fetch(`${this.proxyServerUrl}/proxy`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url, method: 'HEAD', proxy: { enabled: true, type: config.type || 'http', host: config.host, port: config.port, username: config.username, password: config.password } }) }); const responseTime = Date.now() - startTime; this.proxyRequestCount++; if (response.ok) { this.proxyLastSuccessTime = Date.now(); this.proxyLastError = null; return { success: true, responseTime, statusCode: response.status }; } else { const error = `HTTP ${response.status}`; this.proxyLastError = error; return { success: false, responseTime, statusCode: response.status, error }; } } else { // No proxy server available, test direct connection const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); try { const response = await fetch(url, { method: 'HEAD', mode: 'no-cors', signal: controller.signal }); clearTimeout(timeoutId); const responseTime = Date.now() - startTime; return { success: true, responseTime, statusCode: response.status || 0 }; } catch (fetchError: any) { clearTimeout(timeoutId); return { success: false, responseTime: Date.now() - startTime, error: fetchError.message || 'Connection failed' }; } } } catch (error: any) { this.proxyLastError = error.message; return { success: false, responseTime: Date.now() - startTime, error: error.message || 'Proxy test failed' }; } } /** * Get current proxy status */ async getProxyStatus(): Promise<ProxyStatus> { return { active: this.globalProxyConfig?.enabled ?? false, config: this.globalProxyConfig ?? undefined, requestCount: this.proxyRequestCount, lastError: this.proxyLastError ?? undefined, lastSuccessTime: this.proxyLastSuccessTime ?? undefined }; } }