mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-06-01 12:21:39 +02:00
Added support for recipient variables in proxies
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
+186
-10
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user