Files
phishingclub/frontend/src/lib/utils/proxyYamlCompletion.js
T
RonniSkansing 185e07c42d fix comments sweep
Signed-off-by: RonniSkansing <rskansing@gmail.com>
2026-10-02 12:14:35 +02:00

1313 lines
39 KiB
JavaScript

/**
* 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;
}