Added support for recipient variables in proxies

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2026-01-30 10:29:46 +01:00
parent 3767adfad2
commit 28d448c15f
4 changed files with 502 additions and 11 deletions
+186 -10
View File
@@ -74,6 +74,13 @@ var (
MATCH_URL_REGEXP_WITHOUT_SCHEME = regexp.MustCompile(`\b(([A-Za-z0-9-]{1,63}\.)?[A-Za-z0-9]+(-[a-z0-9]+)*\.)+(arpa|root|aero|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|bot|inc|game|xyz|cloud|live|today|online|shop|tech|art|site|wiki|ink|vip|lol|club|click|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|dev|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|test|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)|([0-9]{1,3}\.{3}[0-9]{1,3})\b`)
)
// VariablesContext holds recipient and campaign data for template variable interpolation
type VariablesContext struct {
Data map[string]string // the variable data map
Config *service.ProxyServiceVariablesConfig // the variables configuration
Enabled bool // whether variables are enabled
}
// RequestContext holds all the context data for a proxy request
type RequestContext struct {
SessionID string
@@ -709,7 +716,7 @@ func (m *ProxyHandler) processRequestWithSessionContext(req *http.Request, reqCt
m.normalizeRequestHeaders(req, reqCtx.Session)
// apply replace and capture rules
m.onRequestBody(req, reqCtx.Session)
m.onRequestBody(req, reqCtx.Session, reqCtx.ProxyConfig)
m.onRequestHeader(req, reqCtx.Session)
// patch query parameters
@@ -953,11 +960,16 @@ func (m *ProxyHandler) rewriteResponseHeadersWithContext(resp *http.Response, re
// apply custom replacement rules for response headers (after all hardcoded changes)
if reqCtx.Session != nil {
m.applyCustomResponseHeaderReplacements(resp, reqCtx.Session)
varCtx := m.buildVariablesContext(resp.Request.Context(), reqCtx.Session, reqCtx.ProxyConfig)
m.applyCustomResponseHeaderReplacementsWithVariables(resp, reqCtx.Session, varCtx)
}
}
func (m *ProxyHandler) applyCustomResponseHeaderReplacements(resp *http.Response, session *service.ProxySession) {
m.applyCustomResponseHeaderReplacementsWithVariables(resp, session, nil)
}
func (m *ProxyHandler) applyCustomResponseHeaderReplacementsWithVariables(resp *http.Response, session *service.ProxySession, varCtx *VariablesContext) {
// get all headers as a string
var buf bytes.Buffer
resp.Header.Write(&buf)
@@ -969,7 +981,7 @@ func (m *ProxyHandler) applyCustomResponseHeaderReplacements(resp *http.Response
if hCfg.Rewrite != nil {
for _, replacement := range hCfg.Rewrite {
if replacement.From == "response_header" || replacement.From == "any" {
headers = m.applyReplacement(headers, replacement, session.ID)
headers = m.applyReplacementWithVariables(headers, replacement, session.ID, varCtx)
}
}
}
@@ -1019,7 +1031,10 @@ func (m *ProxyHandler) rewriteResponseBodyWithContext(resp *http.Response, reqCt
body = m.patchUrls(reqCtx.ConfigMap, body, CONVERT_TO_PHISHING_URLS)
body = m.applyURLPathRewrites(body, reqCtx)
body = m.applyCustomReplacements(body, reqCtx.Session)
// build variables context for template interpolation
varCtx := m.buildVariablesContext(resp.Request.Context(), reqCtx.Session, reqCtx.ProxyConfig)
body = m.applyCustomReplacementsWithVariables(body, reqCtx.Session, varCtx)
// apply obfuscation if enabled
if reqCtx.Campaign != nil && strings.Contains(contentType, "text/html") {
@@ -1458,7 +1473,7 @@ func (m *ProxyHandler) initializeRequiredCaptures(session *service.ProxySession)
}
}
func (m *ProxyHandler) onRequestBody(req *http.Request, session *service.ProxySession) {
func (m *ProxyHandler) onRequestBody(req *http.Request, session *service.ProxySession, proxyConfig *service.ProxyServiceConfigYAML) {
if req.Body == nil {
return
}
@@ -1477,7 +1492,9 @@ func (m *ProxyHandler) onRequestBody(req *http.Request, session *service.ProxySe
}
}
m.applyRequestBodyReplacements(req, session)
// build variables context using the proxy config passed from caller
varCtx := m.buildVariablesContext(req.Context(), session, proxyConfig)
m.applyRequestBodyReplacementsWithVariables(req, session, varCtx)
}
func (m *ProxyHandler) onRequestHeader(req *http.Request, session *service.ProxySession) {
@@ -2326,6 +2343,10 @@ func (m *ProxyHandler) createCookieBundle(cookieCaptures map[string]map[string]s
}
func (m *ProxyHandler) applyRequestBodyReplacements(req *http.Request, session *service.ProxySession) {
m.applyRequestBodyReplacementsWithVariables(req, session, nil)
}
func (m *ProxyHandler) applyRequestBodyReplacementsWithVariables(req *http.Request, session *service.ProxySession, varCtx *VariablesContext) {
if req.Body == nil {
return
}
@@ -2338,7 +2359,7 @@ func (m *ProxyHandler) applyRequestBodyReplacements(req *http.Request, session *
if hCfg.Rewrite != nil {
for _, replacement := range hCfg.Rewrite {
if replacement.From == "" || replacement.From == "request_body" || replacement.From == "any" {
body = m.applyReplacement(body, replacement, session.ID)
body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx)
}
}
}
@@ -2348,13 +2369,17 @@ func (m *ProxyHandler) applyRequestBodyReplacements(req *http.Request, session *
}
func (m *ProxyHandler) applyCustomReplacements(body []byte, session *service.ProxySession) []byte {
return m.applyCustomReplacementsWithVariables(body, session, nil)
}
func (m *ProxyHandler) applyCustomReplacementsWithVariables(body []byte, session *service.ProxySession, varCtx *VariablesContext) []byte {
// only apply rewrite rules for the current host
if hostConfig, ok := session.Config.Load(session.TargetDomain); ok {
hCfg := hostConfig.(service.ProxyServiceDomainConfig)
if hCfg.Rewrite != nil {
for _, replacement := range hCfg.Rewrite {
if replacement.From == "" || replacement.From == "response_body" || replacement.From == "any" {
body = m.applyReplacement(body, replacement, session.ID)
body = m.applyReplacementWithVariables(body, replacement, session.ID, varCtx)
}
}
}
@@ -2379,6 +2404,21 @@ func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config
}
func (m *ProxyHandler) applyReplacement(body []byte, replacement service.ProxyServiceReplaceRule, sessionID string) []byte {
return m.applyReplacementWithVariables(body, replacement, sessionID, nil)
}
// applyReplacementWithVariables applies replacement with optional template variable interpolation
func (m *ProxyHandler) applyReplacementWithVariables(body []byte, replacement service.ProxyServiceReplaceRule, sessionID string, varCtx *VariablesContext) []byte {
// interpolate variables in the replacement value if enabled
replaceValue := replacement.Replace
if varCtx != nil && varCtx.Enabled && varCtx.Data != nil {
replaceValue = m.interpolateVariables(replaceValue, varCtx)
}
// create a copy of the replacement with the interpolated value
interpolatedReplacement := replacement
interpolatedReplacement.Replace = replaceValue
// default to regex engine if not specified
engine := replacement.Engine
if engine == "" {
@@ -2387,15 +2427,151 @@ func (m *ProxyHandler) applyReplacement(body []byte, replacement service.ProxySe
switch engine {
case "regex":
return m.applyRegexReplacement(body, replacement, sessionID)
return m.applyRegexReplacement(body, interpolatedReplacement, sessionID)
case "dom":
return m.applyDomReplacement(body, replacement, sessionID)
return m.applyDomReplacement(body, interpolatedReplacement, sessionID)
default:
m.logger.Errorw("unsupported replacement engine", "engine", engine, "sessionID", sessionID)
return body
}
}
// interpolateVariables replaces only explicitly allowed template variables in a string
// this uses simple string replacement to avoid destroying legitimate {{.Something}} content
// in the proxied site that we don't control
func (m *ProxyHandler) interpolateVariables(input string, varCtx *VariablesContext) string {
if varCtx == nil || !varCtx.Enabled || varCtx.Data == nil {
return input
}
// check if input contains any template syntax
if !strings.Contains(input, "{{") {
return input
}
result := input
// determine which variables to replace
var allowedVars []string
if varCtx.Config != nil && len(varCtx.Config.Allowed) > 0 {
// only replace explicitly allowed variables
allowedVars = varCtx.Config.Allowed
} else {
// all variables are allowed - get keys from data map
allowedVars = make([]string, 0, len(varCtx.Data))
for varName := range varCtx.Data {
allowedVars = append(allowedVars, varName)
}
}
// replace only the allowed variables with simple string replacement
// this preserves any other {{.Something}} patterns in the content
for _, varName := range allowedVars {
if val, ok := varCtx.Data[varName]; ok {
placeholder := "{{." + varName + "}}"
result = strings.ReplaceAll(result, placeholder, val)
}
}
return result
}
// buildVariablesContext creates a VariablesContext from session and recipient data
func (m *ProxyHandler) buildVariablesContext(ctx context.Context, session *service.ProxySession, proxyConfig *service.ProxyServiceConfigYAML) *VariablesContext {
// check if variables are enabled in the config
if proxyConfig == nil || proxyConfig.Global == nil || proxyConfig.Global.Variables == nil || !proxyConfig.Global.Variables.Enabled {
return &VariablesContext{Enabled: false}
}
varCtx := &VariablesContext{
Config: proxyConfig.Global.Variables,
Enabled: true,
Data: make(map[string]string),
}
// if no session or recipient ID, return empty context
if session == nil || session.RecipientID == nil {
return varCtx
}
// fetch recipient data
recipientRepo := repository.Recipient{DB: m.CampaignRecipientRepository.DB}
recipient, err := recipientRepo.GetByID(ctx, session.RecipientID, &repository.RecipientOption{})
if err != nil {
m.logger.Debugw("failed to get recipient for variables context", "error", err, "recipientID", session.RecipientID)
return varCtx
}
// populate recipient fields
varCtx.Data["rID"] = session.ID
if v, err := recipient.FirstName.Get(); err == nil {
varCtx.Data["FirstName"] = v.String()
}
if v, err := recipient.LastName.Get(); err == nil {
varCtx.Data["LastName"] = v.String()
}
if v, err := recipient.Email.Get(); err == nil {
varCtx.Data["Email"] = v.String()
varCtx.Data["To"] = v.String() // alias
}
if v, err := recipient.Phone.Get(); err == nil {
varCtx.Data["Phone"] = v.String()
}
if v, err := recipient.ExtraIdentifier.Get(); err == nil {
varCtx.Data["ExtraIdentifier"] = v.String()
}
if v, err := recipient.Position.Get(); err == nil {
varCtx.Data["Position"] = v.String()
}
if v, err := recipient.Department.Get(); err == nil {
varCtx.Data["Department"] = v.String()
}
if v, err := recipient.City.Get(); err == nil {
varCtx.Data["City"] = v.String()
}
if v, err := recipient.Country.Get(); err == nil {
varCtx.Data["Country"] = v.String()
}
if v, err := recipient.Misc.Get(); err == nil {
varCtx.Data["Misc"] = v.String()
}
// note: sender fields (From, FromName, FromEmail, Subject) and custom fields
// are not available in proxy context as they come from email templates
// they are initialized as empty strings for safety
varCtx.Data["From"] = ""
varCtx.Data["FromName"] = ""
varCtx.Data["FromEmail"] = ""
varCtx.Data["Subject"] = ""
varCtx.Data["BaseURL"] = ""
varCtx.Data["URL"] = ""
varCtx.Data["CustomField1"] = ""
varCtx.Data["CustomField2"] = ""
varCtx.Data["CustomField3"] = ""
varCtx.Data["CustomField4"] = ""
return varCtx
}
// getProxyConfig fetches and parses the proxy configuration for a given proxy ID
func (m *ProxyHandler) getProxyConfig(ctx context.Context, proxyID *uuid.UUID) (*service.ProxyServiceConfigYAML, error) {
if proxyID == nil {
return nil, fmt.Errorf("proxy ID is nil")
}
proxyEntry, err := m.ProxyRepository.GetByID(ctx, proxyID, &repository.ProxyOption{})
if err != nil {
return nil, fmt.Errorf("failed to fetch proxy config: %w", err)
}
proxyConfig, err := m.parseProxyConfig(proxyEntry.ProxyConfig.MustGet().String())
if err != nil {
return nil, fmt.Errorf("failed to parse proxy config: %w", err)
}
return proxyConfig, nil
}
// applyRegexReplacement applies regex-based replacement
func (m *ProxyHandler) applyRegexReplacement(body []byte, replacement service.ProxyServiceReplaceRule, sessionID string) []byte {
re, err := regexp.Compile(replacement.Find)
+87
View File
@@ -57,6 +57,7 @@ type ProxyServiceRules struct {
TLS *ProxyServiceTLSConfig `yaml:"tls,omitempty"`
Access *ProxyServiceAccessControl `yaml:"access,omitempty"`
Impersonate *ProxyServiceImpersonateConfig `yaml:"impersonate,omitempty"`
Variables *ProxyServiceVariablesConfig `yaml:"variables,omitempty"`
Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"`
Rewrite []ProxyServiceReplaceRule `yaml:"rewrite,omitempty"`
Response []ProxyServiceResponseRule `yaml:"response,omitempty"`
@@ -93,6 +94,68 @@ type ProxyServiceAccessControl struct {
OnDeny string `yaml:"on_deny,omitempty"` // "404" | "redirect:URL" | status code (only used for private mode)
}
// ProxyServiceVariablesConfig represents template variable interpolation configuration
// when enabled, template variables like {{.Email}} in rewrite rules will be replaced with recipient data
//
// available variables:
// - recipient fields: rID, FirstName, LastName, Email, To, Phone, ExtraIdentifier, Position, Department, City, Country, Misc
// - sender fields: From, FromName, FromEmail, Subject
// - general fields: BaseURL, URL
// - custom fields: CustomField1, CustomField2, CustomField3, CustomField4
//
// example usage:
//
// global:
// variables:
// enabled: true
// allowed: ["Email", "FirstName", "LastName"] # optional: restrict to specific variables
// rewrite:
// - find: "PLACEHOLDER_EMAIL"
// replace: "{{.Email}}"
type ProxyServiceVariablesConfig struct {
Enabled bool `yaml:"enabled"` // enable template variable interpolation in rewrite rules
Allowed []string `yaml:"allowed,omitempty"` // if empty and enabled, all variables are allowed; if set, only these variables can be used
}
// ValidProxyVariables defines all valid template variable names that can be used in proxy rewrite rules
var ValidProxyVariables = map[string]bool{
// recipient fields
"rID": true,
"FirstName": true,
"LastName": true,
"Email": true,
"To": true,
"Phone": true,
"ExtraIdentifier": true,
"Position": true,
"Department": true,
"City": true,
"Country": true,
"Misc": true,
// sender fields
"From": true,
"FromName": true,
"FromEmail": true,
"Subject": true,
// general fields
"BaseURL": true,
"URL": true,
// custom fields
"CustomField1": true,
"CustomField2": true,
"CustomField3": true,
"CustomField4": true,
}
// GetValidProxyVariableNames returns a slice of all valid proxy variable names
func GetValidProxyVariableNames() []string {
names := make([]string, 0, len(ValidProxyVariables))
for name := range ValidProxyVariables {
names = append(names, name)
}
return names
}
// Access control modes:
// - "public": Allow all traffic (traditional proxy mode) - on_deny is ignored
// - "private": Strict IP-based mode like evilginx2 - whitelist IP after lure access, deny all others (DEFAULT)
@@ -1392,6 +1455,27 @@ func (m *Proxy) validateTLSConfig(tlsConfig *ProxyServiceTLSConfig) error {
return nil
}
// validateVariablesConfig validates the variables configuration
func (m *Proxy) validateVariablesConfig(variablesConfig *ProxyServiceVariablesConfig) error {
if variablesConfig == nil {
return nil
}
// validate allowed variable names if specified
if len(variablesConfig.Allowed) > 0 {
for _, varName := range variablesConfig.Allowed {
if !ValidProxyVariables[varName] {
return validate.WrapErrorWithField(
fmt.Errorf("invalid variable name '%s' in allowed list. valid variables: %v", varName, GetValidProxyVariableNames()),
"proxyConfig",
)
}
}
}
return nil
}
func (m *Proxy) validateAccessControl(accessControl *ProxyServiceAccessControl) error {
if accessControl == nil {
return nil // access control will be set to defaults
@@ -1678,6 +1762,9 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err
if err := m.validateAccessControl(config.Global.Access); err != nil {
return err
}
if err := m.validateVariablesConfig(config.Global.Variables); err != nil {
return err
}
if err := m.validateCaptureRules(config.Global.Capture); err != nil {
return err
}
@@ -26,6 +26,7 @@
tls: { mode: 'managed' },
access: { mode: 'private', on_deny: '' },
impersonate: { enabled: false, retain_ua: false },
variables: { enabled: false, allowed: [] },
capture: [],
rewrite: [],
response: [],
@@ -34,6 +35,36 @@
hosts: []
};
// valid proxy template variables that can be used in rewrite rules
const validProxyVariables = [
// recipient fields
'rID',
'FirstName',
'LastName',
'Email',
'To',
'Phone',
'ExtraIdentifier',
'Position',
'Department',
'City',
'Country',
'Misc',
// sender fields
'From',
'FromName',
'FromEmail',
'Subject',
// general fields
'BaseURL',
'URL',
// custom fields
'CustomField1',
'CustomField2',
'CustomField3',
'CustomField4'
];
// active tab for main sections
let activeTab = 'basic';
@@ -158,6 +189,10 @@
enabled: false,
retain_ua: false
};
configData.global.variables = parsed.global.variables || {
enabled: false,
allowed: []
};
configData.global.capture = (parsed.global.capture || []).map((r) => ({
...r,
_id: getRuleId()
@@ -224,6 +259,7 @@
tls: { mode: 'managed' },
access: { mode: 'private', on_deny: '' },
impersonate: { enabled: false, retain_ua: false },
variables: { enabled: false, allowed: [] },
capture: [],
rewrite: [],
response: [],
@@ -310,6 +346,14 @@
global.impersonate.retain_ua = configData.global.impersonate.retain_ua;
}
}
if (configData.global.variables?.enabled) {
global.variables = {
enabled: configData.global.variables.enabled
};
if (configData.global.variables.allowed?.length > 0) {
global.variables.allowed = configData.global.variables.allowed;
}
}
// filter and add global rules (only include touched/valid rules)
const globalCapture = (configData.global.capture || []).filter(isCaptureRuleTouched);
if (globalCapture.length > 0) {
@@ -913,6 +957,14 @@
global.impersonate.retain_ua = configData.global.impersonate.retain_ua;
}
}
if (configData.global.variables?.enabled) {
global.variables = {
enabled: configData.global.variables.enabled
};
if (configData.global.variables.allowed?.length > 0) {
global.variables.allowed = configData.global.variables.allowed;
}
}
// filter and add global rules (only include touched/valid rules)
const globalCapture = (configData.global.capture || []).filter(isCaptureRuleTouched);
if (globalCapture.length > 0) {
@@ -1056,6 +1108,10 @@
enabled: false,
retain_ua: false
};
configData.global.variables = parsed.global.variables || {
enabled: false,
allowed: []
};
configData.global.capture = (parsed.global.capture || []).map((r) => ({
...r,
_id: getRuleId()
@@ -1078,6 +1134,7 @@
tls: { mode: 'managed' },
access: { mode: 'private', on_deny: '' },
impersonate: { enabled: false, retain_ua: false },
variables: { enabled: false, allowed: [] },
capture: [],
rewrite: [],
response: [],
@@ -2280,6 +2337,89 @@
{/if}
</div>
</div>
<!-- Template Variables -->
<div class="global-section">
<h3 class="section-title">
<svg
class="section-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"
/>
</svg>
Template Variables
</h3>
<div class="section-content">
<label class="checkbox-label">
<input
type="checkbox"
checked={configData.global.variables.enabled}
on:change={(e) => {
configData.global.variables.enabled = e.currentTarget.checked;
if (!e.currentTarget.checked) {
configData.global.variables.allowed = [];
}
configData = configData;
}}
class="checkbox-input"
/>
<span class="checkbox-text">Enable Variables</span>
</label>
<span class="form-hint"
>Allow template variables like <code>{'{{.Email}}'}</code> in rewrite rules to be replaced
with recipient data</span
>
{#if configData.global.variables.enabled}
<div class="field-wrapper" style="margin-top: 0.75rem;">
<label class="flex flex-col">
<span class="text-sm font-medium text-pc-darkblue dark:text-white mb-1.5"
>Allowed Variables (optional)</span
>
<div class="variables-selector">
{#each validProxyVariables as varName}
<label class="variable-chip">
<input
type="checkbox"
checked={configData.global.variables.allowed?.includes(varName)}
on:change={(e) => {
if (e.currentTarget.checked) {
configData.global.variables.allowed = [
...(configData.global.variables.allowed || []),
varName
];
} else {
configData.global.variables.allowed =
configData.global.variables.allowed?.filter(
(v) => v !== varName
) || [];
}
configData = configData;
}}
class="hidden"
/>
<span
class="chip-text"
class:selected={configData.global.variables.allowed?.includes(
varName
)}>{varName}</span
>
</label>
{/each}
</div>
</label>
<span class="form-hint"
>Leave empty to allow all variables, or select specific ones to restrict which
can be used</span
>
</div>
{/if}
</div>
</div>
</div>
<!-- Global Rules Tabs -->
@@ -3640,4 +3780,83 @@
height: 1.25rem;
padding: 0.125rem;
}
/* Variables selector styles */
.variables-selector {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
padding: 0.5rem;
background: #f8fafc;
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
}
:global(.dark) .variables-selector {
background: rgba(17, 24, 39, 0.4);
border-color: rgba(55, 65, 81, 0.6);
}
.variable-chip {
cursor: pointer;
}
.chip-text {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
border-radius: 0.25rem;
background: #e2e8f0;
color: #64748b;
transition: all 0.15s;
user-select: none;
}
:global(.dark) .chip-text {
background: rgba(55, 65, 81, 0.6);
color: #9ca3af;
}
.chip-text:hover {
background: #cbd5e1;
color: #475569;
}
:global(.dark) .chip-text:hover {
background: rgba(75, 85, 99, 0.8);
color: #d1d5db;
}
.chip-text.selected {
background: #3b82f6;
color: white;
}
:global(.dark) .chip-text.selected {
background: #2563eb;
color: white;
}
.chip-text.selected:hover {
background: #2563eb;
}
:global(.dark) .chip-text.selected:hover {
background: #1d4ed8;
}
.section-content code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.8125rem;
padding: 0.125rem 0.375rem;
background: #e2e8f0;
border-radius: 0.25rem;
color: #475569;
}
:global(.dark) .section-content code {
background: rgba(55, 65, 81, 0.6);
color: #d1d5db;
}
</style>
+10 -1
View File
@@ -220,10 +220,14 @@
# proxy: "socks5://user:pass@192.168.1.100:1080" # socks5 with auth
# proxy: "http://user:pass@192.168.1.100:8080" # http with auth
# global TLS configuration (applies to all hosts unless overridden)
# global configuration (applies to all hosts unless overridden)
global:
tls:
mode: "managed" # "managed" (Let's Encrypt) or "self-signed"
# template variables allow recipient data in rewrite rules
# variables:
# enabled: true
# allowed: ["Email", "FirstName", "LastName"] # optional: restrict to specific variables
portal.example.com:
to: "evil.example.com"
@@ -252,6 +256,11 @@ portal.example.com:
find: "logo\\.png"
replace: "evil-logo.png"
from: "response_body"
# when variables are enabled, you can use recipient data:
# - name: "personalize_greeting"
# find: "Welcome, User"
# replace: "Welcome, {{.FirstName}}"
# from: "response_body"
# dom-based manipulations
- name: "change_title"
engine: "dom"