/** * Monaco Editor YAML completion provider for proxy configurations */ export class ProxyYamlCompletionProvider { constructor(monaco) { this.monaco = monaco; this.completionProvider = null; this.hoverProvider = null; this.setupLanguageFeatures(); } setupLanguageFeatures() { try { if (!this.completionProvider) { this.completionProvider = this.monaco.languages.registerCompletionItemProvider('yaml', { provideCompletionItems: (model, position) => { try { return this.provideCompletionItems(model, position); } catch (error) { console.warn('Completion error:', error); return { suggestions: [] }; } } }); } if (!this.hoverProvider) { this.hoverProvider = this.monaco.languages.registerHoverProvider('yaml', { provideHover: (model, position) => { try { return this.provideHover(model, position); } catch (error) { console.warn('Hover error:', error); return null; } } }); } } catch (error) { console.warn('Failed to setup language features:', error); } } dispose() { if (this.completionProvider) { this.completionProvider.dispose(); this.completionProvider = null; } if (this.hoverProvider) { this.hoverProvider.dispose(); this.hoverProvider = null; } } provideCompletionItems(model, position) { const word = model.getWordUntilPosition(position); const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn }; const lineContent = model.getLineContent(position.lineNumber); const linePrefix = lineContent.substring(0, position.column - 1); const fullContent = model.getValue(); const linesAbove = model.getLinesContent().slice(0, position.lineNumber - 1); return { suggestions: this.getSuggestions(linePrefix, range, linesAbove, fullContent) }; } getSuggestions(linePrefix, range, linesAbove, fullContent) { const suggestions = []; const currentIndent = this.getIndent(linePrefix); // Handle specific field value completions if (linePrefix.match(/mode:\s*$/)) { return this.getModeSuggestions(range); } if (linePrefix.match(/from:\s*$/)) { return this.getFromSuggestions(range); } if (linePrefix.match(/method:\s*$/)) { return this.getMethodSuggestions(range); } if (linePrefix.match(/(with_session|without_session):\s*$/)) { return this.getActionSuggestions(range); } // Handle array items if (linePrefix.match(/^\s*-\s*$/)) { const context = this.findParentSection(linesAbove, currentIndent); if (context === 'paths') { return this.getPathPatternSuggestions(range); } if (context === 'capture') { return this.getNewCaptureSuggestions(range); } if (context === 'rewrite') { return this.getNewRewriteSuggestions(range); } if (context === 'response') { return this.getNewResponseSuggestions(range); } } // Handle field completions based on context const context = this.findParentSection(linesAbove, currentIndent); if (currentIndent === 0) { return this.getTopLevelSuggestions(range); } switch (context) { case 'global': return this.getGlobalSuggestions(range); case 'domain': return this.getDomainSuggestions(range); case 'access': return this.getAccessSuggestions(range); case 'on_deny': return this.getOnDenySuggestions(range); case 'capture': return this.getCaptureSuggestions(range); case 'rewrite': return this.getRewriteSuggestions(range); case 'response': return this.getResponseSuggestions(range); default: return []; } } getIndent(line) { const match = line.match(/^\s*/); return match ? match[0].length : 0; } findParentSection(linesAbove, currentIndent) { // Look backwards to find the parent section for (let i = linesAbove.length - 1; i >= 0; i--) { const line = linesAbove[i]; const lineIndent = this.getIndent(line); const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; // If we find a line with less indentation that has a colon, it's a parent if (lineIndent < currentIndent && trimmed.includes(':')) { const key = trimmed.split(':')[0].trim(); // Top level sections if (lineIndent === 0) { if (key === 'global') return 'global'; if (key.match(/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)) return 'domain'; } // Nested sections if (key === 'access') return 'access'; if (key === 'capture') return 'capture'; if (key === 'rewrite') return 'rewrite'; if (key === 'response') return 'response'; if (key === 'on_deny') return 'on_deny'; if (key === 'paths') return 'paths'; } } return null; } getTopLevelSuggestions(range) { return [ { label: 'version', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'version: "0.0"', documentation: 'Configuration version', range }, { label: 'proxy', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'proxy: "proxy-name"', documentation: 'Optional proxy name', range }, { label: 'global', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'global:', documentation: 'Global rules for all domains', range } ]; } getGlobalSuggestions(range) { return [ { label: 'access', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'access:', documentation: 'Global access control', range }, { label: 'capture', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'capture:', documentation: 'Global capture rules', range }, { label: 'rewrite', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'rewrite:', documentation: 'Global rewrite rules', range }, { label: 'response', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'response:', documentation: 'Global response rules', range } ]; } getDomainSuggestions(range) { return [ { label: 'to', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'to: "phishing-domain.com"', documentation: 'Target phishing domain (required)', range }, { label: 'access', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'access:', documentation: 'Domain access control', range }, { label: 'capture', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'capture:', documentation: 'Domain capture rules', range }, { label: 'rewrite', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'rewrite:', documentation: 'Domain rewrite rules', range }, { label: 'response', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'response:', documentation: 'Domain response rules', range } ]; } getAccessSuggestions(range) { return [ { label: 'mode', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'mode: "allow"', documentation: 'Access control mode: allow or deny', range }, { label: 'paths', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'paths:', documentation: 'Array of path patterns', range }, { label: 'on_deny', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'on_deny:', documentation: 'Response when access denied', range } ]; } getOnDenySuggestions(range) { return [ { label: 'with_session', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'with_session: 403', documentation: 'Response for users with sessions', range }, { label: 'without_session', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'without_session: 404', documentation: 'Response for users without sessions', range } ]; } getResponseSuggestions(range) { return [ { label: '- Response Rule', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: [ '- path: "^/path/pattern$"', ' status: 200', ' headers:', ' Content-Type: "application/json"', ' body: \'{"message": "Hello"}\'', ' forward: false' ].join('\n '), documentation: 'Complete response rule template', range }, { label: 'path', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'path: "^/api/health$"', documentation: 'Regex pattern for request path', range }, { label: 'status', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'status: 200', documentation: 'HTTP status code (default: 200)', range }, { label: 'headers', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'headers:', documentation: 'Response headers', range }, { label: 'body', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'body: "Response content"', documentation: 'Response body content (plain text/HTML/JSON/etc.)', range }, { label: 'forward', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'forward: false', documentation: 'Whether to also forward request to target (default: false)', range } ]; } getCaptureSuggestions(range) { return [ { label: 'name', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'name: "capture_name"', documentation: 'Unique capture rule name (required)', range }, { label: 'method', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'method: "POST"', documentation: 'HTTP method to match', range }, { label: 'path', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'path: "/login"', documentation: 'URL path pattern to match (required)', range }, { label: 'find', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'find: "pattern"', documentation: 'Regex pattern to capture', range }, { label: 'from', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'from: "request_body"', documentation: 'Where to search for pattern', range }, { label: 'required', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'required: true', documentation: 'Whether capture is required', range } ]; } getRewriteSuggestions(range) { return [ { label: 'name', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'name: "rewrite_name"', documentation: 'Optional rewrite rule name', range }, { label: 'find', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'find: "pattern"', documentation: 'Regex pattern to find (required)', range }, { label: 'replace', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'replace: "replacement"', documentation: 'Replacement text (required)', range }, { label: 'from', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'from: "response_body"', documentation: 'Where to apply replacement', range } ]; } getNewCaptureSuggestions(range) { return [ { label: 'capture rule', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "capture_name"\n method: "POST"\n path: "/path"\n find: "pattern"\n from: "request_body"', documentation: 'New capture rule template', range } ]; } getNewRewriteSuggestions(range) { return [ { label: 'rewrite rule', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "rewrite_name"\n find: "pattern"\n replace: "replacement"\n from: "response_body"', documentation: 'New rewrite rule template', range } ]; } getNewResponseSuggestions(range) { return [ { label: 'response rule', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'path: "^/api/health$"\n status: 200\n headers:\n Content-Type: "application/json"\n body: \'{"status": "ok"}\'\n forward: false', documentation: 'New response rule template', range } ]; } getModeSuggestions(range) { return [ { label: '"allow"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"allow"', documentation: 'Allowlist mode - only specified paths allowed', range }, { label: '"deny"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"deny"', documentation: 'Denylist mode - specified paths blocked', range } ]; } getFromSuggestions(range) { return [ { label: '"request_body"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"request_body"', documentation: 'Search in request body', range }, { label: '"request_header"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"request_header"', documentation: 'Search in request headers', range }, { label: '"response_body"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"response_body"', documentation: 'Search in response body', range }, { label: '"response_header"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"response_header"', documentation: 'Search in response headers', range }, { label: '"cookie"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"cookie"', documentation: 'Capture cookie data', range }, { label: '"any"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"any"', documentation: 'Search anywhere', range } ]; } getMethodSuggestions(range) { return [ { label: '"GET"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"GET"', documentation: 'HTTP GET method', range }, { label: '"POST"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"POST"', documentation: 'HTTP POST method', range }, { label: '"PUT"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"PUT"', documentation: 'HTTP PUT method', range }, { label: '"DELETE"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"DELETE"', documentation: 'HTTP DELETE method', range } ]; } getActionSuggestions(range) { return [ { label: '"allow"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"allow"', documentation: 'Allow access (override deny)', range }, { label: '"redirect:https://example.com"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"redirect:https://example.com"', documentation: 'Redirect to URL', range }, { label: '404', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '404', documentation: 'Return 404 Not Found', range }, { label: '403', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '403', documentation: 'Return 403 Forbidden', range }, { label: '503', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '503', documentation: 'Return 503 Service Unavailable', range } ]; } getPathPatternSuggestions(range) { return [ { label: '"^/admin/"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"^/admin/"', documentation: 'Admin panel paths', range }, { label: '"^/login"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"^/login"', documentation: 'Login page', range }, { label: '"^/api/"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"^/api/"', documentation: 'API endpoints', range }, { label: '"^/assets/"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"^/assets/"', documentation: 'Static assets', range }, { label: '"^/\\.git/"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"^/\\.git/"', documentation: 'Git repository', range } ]; } provideHover(model, position) { const word = model.getWordAtPosition(position); if (!word) return null; const hoverInfo = this.getHoverInfo(word.word); if (!hoverInfo) return null; return { range: new this.monaco.Range( position.lineNumber, word.startColumn, position.lineNumber, word.endColumn ), contents: [{ value: `**${word.word}**` }, { value: hoverInfo }] }; } getHoverInfo(word) { const hoverData = { version: 'Configuration version. Currently supports "0.0"', global: 'Rules that apply to all domain mappings', access: 'Access control configuration - restricts which paths are accessible', mode: 'Access control mode: "allow" (allowlist) or "deny" (denylist)', paths: 'Array of regex patterns for path matching', on_deny: 'Response configuration when access is denied', with_session: 'Response for users with active proxy sessions (request with mitm cookie)', without_session: 'Response for requests without sessions', capture: 'Rules for capturing data from requests/responses', name: 'Unique identifier for the rule', method: 'HTTP method to match (GET, POST, PUT, DELETE, etc.)', path: 'URL path pattern to match (regex)', find: 'Regex pattern to capture data, or cookie name if from=cookie', from: 'Location to search: request_body, request_header, response_body, response_header, cookie, any', required: 'Whether this capture is required for page and capture completion', rewrite: 'Rules for modifying request/response content', replace: 'Replacement text for the find pattern', to: 'Target phishing domain for this original domain', response: 'Rules for custom responses to specific paths', status: 'HTTP status code for response (default: 200)', headers: 'HTTP headers to include in response', body: 'Response body content (plain text/HTML/JSON/etc.)', forward: 'Whether to also forward request to target server (default: false)' }; return hoverData[word] || null; } } // Global provider instance to prevent duplicates let globalProvider = null; // Initialize the completion provider export function setupProxyYamlCompletion(monaco) { // Dispose existing provider if it exists if (globalProvider) { globalProvider.dispose(); } // Create new provider globalProvider = new ProxyYamlCompletionProvider(monaco); return globalProvider; }