/** * 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', { triggerCharacters: ['-'], 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); // don't show autocomplete if user just typed a value after colon and space // (likely wants to continue to next line) const trimmedPrefix = linePrefix.trim(); if (trimmedPrefix.includes(':')) { const afterColon = trimmedPrefix.split(':').pop().trim(); // if there's already a value after the colon, don't autocomplete if (afterColon.length > 0 && !afterColon.endsWith('-')) { return { suggestions: [] }; } } 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(/\s*engine:\s*$/)) { return this.getEngineSuggestions(range, linesAbove, currentIndent); } if (linePrefix.match(/\s*event:\s*$/)) { return this.getEventSuggestions(range); } if (linePrefix.match(/\s*action:\s*$/)) { return this.getDomActionSuggestions(range); } if (linePrefix.match(/\s*target:\s*$/)) { return this.getTargetSuggestions(range); } if (linePrefix.match(/\s*scheme:\s*$/)) { return this.getSchemeSuggestions(range); } if (linePrefix.match(/\s*mode:\s*$/)) { // determine context for mode - could be access or tls const context = this.findParentSection(linesAbove, currentIndent); if (context === 'tls') { return this.getTLSModeSuggestions(range); } return this.getAccessModeSuggestions(range); } if (linePrefix.match(/\bfrom:\s*["']?/)) { return this.getFromSuggestions(range); } if (linePrefix.match(/\s*method:\s*$/)) { return this.getMethodSuggestions(range); } if (linePrefix.match(/\s*on_deny:\s*$/)) { return this.getOnDenySuggestions(range); } // Handle array items if (linePrefix.match(/^\s*-\s*$/)) { const context = this.findParentSection(linesAbove, currentIndent); if (context === 'capture') { return this.getNewCaptureSuggestions(range); } if (context === 'rewrite') { return this.getNewRewriteSuggestions(range); } if (context === 'response') { return this.getNewResponseSuggestions(range); } if (context === 'rewrite_urls') { return this.getNewRewriteUrlsSuggestions(range); } if ( context === 'query' && this.findParentSection(linesAbove, currentIndent - 2) === 'rewrite_urls' ) { return this.getNewQueryMappingSuggestions(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 'tls': return this.getTLSSuggestions(range); case 'access': return this.getAccessSuggestions(range); case 'impersonate': return this.getImpersonateSuggestions(range); case 'capture': return this.getCaptureSuggestions(range); case 'rewrite': return this.getRewriteSuggestions(range); case 'response': return this.getResponseSuggestions(range); case 'rewrite_urls': return this.getRewriteUrlsSuggestions(range); case 'query': // Check if we're in a rewrite_urls context if (this.findParentSection(linesAbove, currentIndent - 2) === 'rewrite_urls') { return this.getQueryMappingSuggestions(range); } return []; 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 === 'tls') return 'tls'; if (key === 'access') return 'access'; if (key === 'impersonate') return 'impersonate'; if (key === 'capture') return 'capture'; if (key === 'rewrite') return 'rewrite'; if (key === 'rewrite_urls') return 'rewrite_urls'; if (key === 'response') return 'response'; if (key === 'on_deny') return 'on_deny'; if (key === 'paths') return 'paths'; if (key === 'query') return 'query'; } } 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: 'script', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'script: "script-name"', documentation: 'Optional session script. Names a saved Script run once when a session starts. It receives the incoming connection (ip, country, asns, ja4, headers) and may return an object overriding session options such as the upstream proxy', range }, { label: 'global', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'global:', documentation: 'Global rules for all domains', range } ]; } getGlobalSuggestions(range) { return [ { label: 'tls', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'tls:', documentation: 'Global TLS configuration (applies to all hosts unless overridden)', range }, { 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 }, { label: 'rewrite_urls', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'rewrite_urls:', documentation: 'Rules for rewriting URLs in responses', range }, { label: 'impersonate', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'impersonate:', documentation: 'Client browser impersonation configuration using surf library', range } ]; } getDomainSuggestions(range) { return [ { label: 'to', kind: this.monaco.languages.CompletionItemKind.Field, insertText: 'to: ', documentation: 'Phishing domain (where victims will visit)', range }, { label: 'scheme', kind: this.monaco.languages.CompletionItemKind.Field, insertText: 'scheme: ', documentation: 'Scheme for proxying to target (http or https, defaults to https)', range }, { label: 'tls', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'tls:', documentation: 'TLS configuration for this domain (overrides global setting)', range }, { label: 'access', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'access:', documentation: 'Access control configuration', range }, { label: 'capture', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'capture:', documentation: 'Capture rules for this domain', range }, { label: 'rewrite', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'rewrite:', documentation: 'Rewrite rules for this domain', range }, { label: 'response', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'response:', documentation: 'Response rules for this domain', range }, { label: 'rewrite_urls', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'rewrite_urls:', documentation: 'URL rewrite rules for anti-detection', range } ]; } getTLSSuggestions(range) { return [ { label: 'mode', kind: this.monaco.languages.CompletionItemKind.Field, insertText: 'mode: ', documentation: 'TLS mode: "managed" (Let\'s Encrypt) or "self-signed"', range } ]; } getTLSModeSuggestions(range) { return [ { label: '"managed"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"managed"', documentation: "Managed TLS via Let's Encrypt (DEFAULT)", range }, { label: '"self-signed"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"self-signed"', documentation: 'Automatically generated self-signed certificates', range } ]; } getSchemeSuggestions(range) { return [ { label: 'http', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'http', documentation: 'Use HTTP when connecting to target', range }, { label: 'https', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'https', documentation: 'Use HTTPS when connecting to target (default)', range } ]; } getImpersonateSuggestions(range) { return [ { label: 'enabled', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'enabled: true', documentation: 'Enable surf browser impersonation based on JA4 fingerprint. Replicates client TLS fingerprint, HTTP/2 settings, header ordering, and platform', range }, { label: 'retain_ua', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'retain_ua: false', documentation: "Retain client's original User-Agent header instead of using surf's impersonated one. Useful when you want fingerprint matching but original UA", range } ]; } getAccessSuggestions(range) { return [ { label: 'mode', kind: this.monaco.languages.CompletionItemKind.Field, insertText: 'mode: ', documentation: 'Access mode: "public" (allow all) or "private" (IP whitelist after lure access)', range }, { label: 'on_deny', kind: this.monaco.languages.CompletionItemKind.Field, insertText: 'on_deny: ', documentation: 'Action when access denied in private mode: "404", status code, or "redirect:URL"', range } ]; } getOnDenySuggestions(range) { return [ { label: '"404"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"404"', documentation: 'Return 404 Not Found status', range }, { label: '"403"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"403"', documentation: 'Return 403 Forbidden status', range }, { label: '"https://example.com"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"https://example.com"', documentation: 'Redirect to specified URL (auto-detected)', 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: 'engine', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'engine: "regex"', documentation: 'Capture engine type: regex, header, cookie, json, form, urlencoded, formdata, multipart, status', range }, { label: 'find', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'find: "pattern"', documentation: 'Pattern/field to capture (can be string or array of strings)', range }, { label: 'from', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'from: "request_body"', documentation: 'Where to search for pattern (deprecated, use engine instead)', range }, { label: 'required', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'required: true', documentation: 'Whether capture is required', range }, { label: 'event', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'event: "submit"', documentation: 'Event type to save: "submit" (default) or "info"', range } ]; } getRewriteSuggestions(range, linesAbove, current) { return [ { label: 'name', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'name: "rewrite_name"', documentation: 'Optional rewrite rule name', range }, { label: 'engine', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'engine: "regex"', documentation: 'Rewrite engine: "regex" (default), "dom", or "header"', range }, { label: 'find', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'find: "pattern"', documentation: 'Pattern/selector/header to find (regex pattern, CSS selector, or exact header name)', range }, { label: 'replace', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'replace: "replacement"', documentation: 'Replacement value (replacement text for regex, value for dom/header actions)', range }, { label: 'action', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'action: "set"', documentation: 'DOM action: setText, setHtml, setAttr, removeAttr, addClass, removeClass, remove. Header action: set, add, remove', range }, { label: 'target', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'target: "all"', documentation: 'Target matching (dom engine only): "first", "last", "all" (default), "1,3,5", "2-4"', range }, { label: 'from', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'from: "response_body"', documentation: 'Where to apply replacement: response_body (default), request_body, response_header, request_header, any', range }, { label: 'path', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'path: "^/login"', documentation: 'Optional regex to restrict rule to matching request paths', range }, { label: 'method', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'method: "POST"', documentation: 'Optional HTTP method to restrict this rule to (GET, POST, etc.)', range } ]; } getNewCaptureSuggestions(range) { return [ { label: 'capture rule (regex)', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "capture_name"\n method: "POST"\n path: "/path"\n engine: "regex"\n find: "pattern"\n event: "submit"', documentation: 'New regex capture rule template', range }, { label: 'capture header', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "capture_header"\n method: "POST"\n path: "/path"\n engine: "header"\n find: "x-auth-token"\n event: "submit"', documentation: 'Capture HTTP header value by name', range }, { label: 'capture cookie', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "capture_cookie"\n method: "POST"\n path: "/path"\n engine: "cookie"\n find: "session_id"\n event: "submit"', documentation: 'Capture cookie value by name', range }, { label: 'capture JSON field', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "capture_json"\n method: "POST"\n path: "/api/login"\n engine: "json"\n find: "user.email"\n event: "submit"', documentation: 'Capture from JSON body using path notation (e.g., user.name)', range }, { label: 'capture JSON array', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "capture_json_array"\n method: "POST"\n path: "/api/data"\n engine: "json"\n find: "[0].user.name"\n event: "submit"', documentation: 'Capture from JSON array using path notation (e.g., [1].user.name)', range }, { label: 'capture form field', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "capture_form"\n method: "POST"\n path: "/login"\n engine: "urlencoded"\n find: ["username", "password"]\n event: "submit"', documentation: 'Capture from URL encoded form data', range }, { label: 'capture multipart', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "capture_multipart"\n method: "POST"\n path: "/upload"\n engine: "multipart"\n find: ["file", "description"]\n event: "submit"', documentation: 'Capture from multipart/form-data', range }, { label: 'capture info (no submit)', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "capture_info"\n method: "GET"\n path: "/path"\n engine: "header"\n find: "x-request-id"\n event: "info"', documentation: 'Capture and save as info event (does not count as submitted data)', range }, { label: 'capture status (diagnostic)', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "checkpoint"\n path: "/path"\n engine: "status"', documentation: 'Diagnostic rule: report which required captures have not fired yet when the visitor reaches this path', range } ]; } getNewRewriteSuggestions(range) { return [ { label: 'regex rewrite rule', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "regex_rule"\n find: "pattern"\n replace: "replacement"\n from: "response_body"', documentation: 'New regex-based rewrite rule template', range }, { label: 'dom rewrite rule', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "dom_rule"\n engine: "dom"\n find: "css-selector"\n action: "setText"\n replace: "new-value"\n target: "all"', documentation: 'New DOM-based rewrite rule template', range }, { label: 'dom change title', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "change_title"\n engine: "dom"\n find: "title"\n action: "setText"\n replace: "Secure Login Portal"\n target: "first"', documentation: 'Change page title using DOM', range }, { label: 'dom modify form action', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "modify_form"\n engine: "dom"\n find: "form[action]"\n action: "setAttr"\n replace: "action:/evil/submit"\n target: "all"', documentation: 'Modify form action attribute using DOM', range }, { label: 'dom add CSS class', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "add_class"\n engine: "dom"\n find: ".login-form"\n action: "addClass"\n replace: "enhanced-security"\n target: "all"', documentation: 'Add CSS class to elements using DOM', range }, { label: 'dom remove elements', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "remove_warnings"\n engine: "dom"\n find: ".security-warning"\n action: "remove"\n target: "all"', documentation: 'Remove security warnings using DOM', range }, { label: 'dom remove attribute', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'name: "remove_csrf"\n engine: "dom"\n find: "input[name=\'_token\']"\n action: "removeAttr"\n replace: "name"\n target: "all"', documentation: 'Remove attributes using DOM', 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 } ]; } getRewriteUrlsSuggestions(range) { return [ { label: 'find', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'find: "^/path/to/match$"', documentation: 'Regex pattern to match URL path', range }, { label: 'replace', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'replace: "/new/path"', documentation: 'Replacement path for matched URLs', range }, { label: 'query', kind: this.monaco.languages.CompletionItemKind.Module, insertText: 'query:', documentation: 'Query parameter mappings', range }, { label: 'filter', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'filter: ["client_id", "state"]', documentation: 'Query parameters to keep (if empty, keep all)', range } ]; } getNewRewriteUrlsSuggestions(range) { return [ { label: 'url rewrite rule', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'find: "/common/oauth2/v2\\.0/authorize"\n replace: "/signin"\n query:\n - find: "response_type"\n replace: "type"\n filter: ["client_id", "state"]', documentation: 'New URL rewrite rule for anti-detection', range }, { label: 'simple path rewrite', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'find: "/original/path"\n replace: "/new/path"', documentation: 'Simple path rewrite without query changes', range } ]; } getQueryMappingSuggestions(range) { return [ { label: 'find', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'find: "client_id"', documentation: 'Original query parameter name to find', range }, { label: 'replace', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'replace: "app_id"', documentation: 'New query parameter name to replace with', range } ]; } getNewQueryMappingSuggestions(range) { return [ { label: 'query mapping', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'find: "client_id"\n replace: "app_id"', documentation: 'Map query parameter names for anti-detection', range }, { label: 'oauth parameter mapping', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: 'find: "response_type"\n replace: "type"', documentation: 'Common OAuth parameter mapping', range } ]; } getAccessModeSuggestions(range) { return [ { label: '"private"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"private"', documentation: 'Private mode - IP-based whitelist after lure access (DEFAULT, secure)', range }, { label: '"public"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"public"', documentation: 'Public mode - allow all traffic (traditional proxy behavior)', 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 } ]; } getEngineSuggestions(range, linesAbove, currentIndent) { // Determine context, rewrite rules have 'dom' and 'header', capture rules don't have 'dom' const context = this.findParentSection(linesAbove || [], currentIndent); const isRewrite = context === 'rewrite'; const captureEngines = [ { label: 'regex', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'regex', documentation: 'Regular expression pattern matching', range }, { label: 'header', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'header', documentation: isRewrite ? 'Directly set, add, or remove a header by name (use with action: set/add/remove)' : 'Capture from HTTP headers by key', range }, { label: 'cookie', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'cookie', documentation: 'Capture from cookies by name', range }, { label: 'json', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'json', documentation: 'Capture from JSON body using path notation (e.g., user.name or [0].user.name)', range }, { label: 'form', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'form', documentation: 'Capture from URL encoded form data', range }, { label: 'urlencoded', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'urlencoded', documentation: 'Capture from application/x-www-form-urlencoded body', range }, { label: 'formdata', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'formdata', documentation: 'Capture from multipart/form-data body', range }, { label: 'multipart', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'multipart', documentation: 'Capture from multipart form data', range } ]; const rewriteOnlyEngines = [ { label: 'dom', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'dom', documentation: 'DOM manipulation: modify HTML elements via CSS selectors', range } ]; const captureOnlyEngines = [ { label: 'status', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'status', documentation: 'Diagnostic only: on visiting the rule path, report which required captures have not fired yet. Captures no data and does not affect the flow', range } ]; return isRewrite ? [...captureEngines, ...rewriteOnlyEngines] : [...captureEngines, ...captureOnlyEngines]; } getEventSuggestions(range) { return [ { label: 'submit', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'submit', documentation: 'Save as a submitted-data event (default)', range }, { label: 'info', kind: this.monaco.languages.CompletionItemKind.Value, insertText: 'info', documentation: 'Save as a low-priority info event', range } ]; } getTargetSuggestions(range) { return [ { label: '"all"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"all"', documentation: 'Target all matching elements (default)', range }, { label: '"first"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"first"', documentation: 'Target only the first matching element', range }, { label: '"last"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"last"', documentation: 'Target only the last matching element', range }, { label: '"1,3,5"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"1,3,5"', documentation: 'Target specific elements by index (comma-separated)', range }, { label: '"2-4"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"2-4"', documentation: 'Target a range of elements (start-end)', range } ]; } getDomActionSuggestions(range) { return [ { label: '"setText"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"setText"', documentation: 'Set the text content of selected elements (preserves HTML structure)', range }, { label: '"setHtml"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"setHtml"', documentation: 'Set the HTML content of selected elements (replaces inner HTML)', range }, { label: '"setAttr"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"setAttr"', documentation: 'Set attribute value (use value format: "attribute:value")', range }, { label: '"removeAttr"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"removeAttr"', documentation: 'Remove attribute from selected elements', range }, { label: '"addClass"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"addClass"', documentation: 'Add CSS class to selected elements', range }, { label: '"removeClass"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"removeClass"', documentation: 'Remove CSS class from selected elements', range }, { label: '"remove"', kind: this.monaco.languages.CompletionItemKind.Value, insertText: '"remove"', documentation: 'Remove selected elements from DOM', 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"', script: 'Session script name. Names a saved Script run once when a proxy session starts, before the upstream client is built. It receives the incoming connection (ip, country, asns, ja4, userAgent, acceptLanguage, headers, targetDomain) and may return an object with a "proxy" field to set the upstream forwarding proxy for that session. On any error or when omitted, the session uses the proxy from this config', global: 'Rules that apply to all domain mappings', impersonate: 'Client browser impersonation configuration. When enabled, uses surf library to replicate the exact TLS fingerprint, HTTP/2 settings, header ordering, and platform of the original client browser', enabled: 'Enable surf browser impersonation based on client JA4 fingerprint. Detects browser (Chrome, Firefox, Safari, Edge) and platform (Windows, macOS, Linux, Android, iOS). Default: false', retain_ua: "Retain client's original User-Agent header instead of using surf's impersonated one. Useful when you want TLS/HTTP fingerprint matching but need to preserve the exact original User-Agent. Default: false", tls: 'TLS certificate configuration for proxy domains', access: 'Access control configuration (optional - defaults to private mode for security)', mode: 'Access control mode: "public" (allow all traffic) or "private" (IP whitelist after lure access, DEFAULT), OR TLS mode: "managed" (Let\'s Encrypt) or "self-signed"', on_deny: 'Response when access is denied in private mode (e.g., "404", "https://example.com")', scheme: 'Protocol scheme for proxying to target: "http" or "https" (defaults to https). Specifies whether to use HTTP or HTTPS when connecting to the target domain', 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: 'Pattern to find (can be string or array of strings). Meaning depends on engine: regex pattern (regex), header name (header), cookie name (cookie), JSON path (json), form field name (form/urlencoded/form-data/multipart), CSS selector (dom)', from: 'Location to search: request_body, request_header, response_body, response_header, any. For capture: cookie also valid (deprecated, use engine instead)', required: 'Whether this capture is required for page and capture completion', event: 'Event type to save when data is captured: "submit" (default, saved as submitted-data event) or "info" (saved as low-priority info event)', 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)', rewrite: 'Rules for modifying request/response content using regex, dom, or header engines', replace: 'Replacement value: replacement text (regex engine), value for dom actions, or new header value (header engine)', engine: 'Engine type. For capture: regex (default), header, cookie, json, form/urlencoded/formdata/multipart. For rewrite: regex (default), dom (HTML manipulation), header (set/add/remove a header directly)', action: 'For dom engine: setText, setHtml, setAttr, removeAttr, addClass, removeClass, remove. For header engine: set (overwrite), add (append), remove (delete)', target: 'Target matching (dom engine only): "first", "last", "all" (default), "1,3,5" (specific), "2-4" (range)', to: 'Target phishing domain for this original domain', rewrite_urls: 'URL rewrite rules for anti-detection - changes paths and query parameters', query: 'Query parameter mappings for URL rewriting', filter: 'Query parameters to keep (if empty, keep all)' }; 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; }