mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-18 08:27:14 +02:00
add proxy response directive
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -97,6 +97,7 @@ type RequestContext struct {
|
||||
ConfigMap map[string]service.ProxyServiceDomainConfig
|
||||
CampaignRecipientID *uuid.UUID
|
||||
ParamName string
|
||||
PendingResponse *http.Response
|
||||
}
|
||||
|
||||
type ProxyHandler struct {
|
||||
@@ -268,6 +269,16 @@ func (m *ProxyHandler) processRequestWithContext(req *http.Request, reqCtx *Requ
|
||||
}
|
||||
}
|
||||
|
||||
// check for response rules first (before access control)
|
||||
if resp := m.checkResponseRules(req, reqCtx); resp != nil {
|
||||
// if response rule doesn't forward, return response immediately
|
||||
if !m.shouldForwardRequest(req, reqCtx) {
|
||||
return req, resp
|
||||
}
|
||||
// if response rule forwards, we'll send the response after proxying
|
||||
reqCtx.PendingResponse = resp
|
||||
}
|
||||
|
||||
// check access control before proceeding
|
||||
hasSession := reqCtx.SessionID != ""
|
||||
|
||||
@@ -445,6 +456,12 @@ func (m *ProxyHandler) processResponseWithContext(resp *http.Response, reqCtx *R
|
||||
return nil
|
||||
}
|
||||
|
||||
// check for pending response from response rules with forward: true
|
||||
if reqCtx.PendingResponse != nil {
|
||||
// if we have a pending response, return it instead of the proxied response
|
||||
return reqCtx.PendingResponse
|
||||
}
|
||||
|
||||
// handle responses with or without session
|
||||
if reqCtx.SessionID != "" && reqCtx.Session != nil {
|
||||
// capture response data before any rewriting
|
||||
@@ -2002,8 +2019,26 @@ func (m *ProxyHandler) setProxyConfigDefaults(config *service.ProxyServiceConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
if domainConfig != nil && domainConfig.Response != nil {
|
||||
for i := range domainConfig.Response {
|
||||
// set default status to 200 if not specified
|
||||
if domainConfig.Response[i].Status == 0 {
|
||||
domainConfig.Response[i].Status = 200
|
||||
}
|
||||
}
|
||||
}
|
||||
config.Hosts[domain] = domainConfig
|
||||
}
|
||||
|
||||
// set defaults for global response rules
|
||||
if config.Global != nil && config.Global.Response != nil {
|
||||
for i := range config.Global.Response {
|
||||
// set default status to 200 if not specified
|
||||
if config.Global.Response[i].Status == 0 {
|
||||
config.Global.Response[i].Status = 200
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) GetCookieName() string {
|
||||
@@ -2014,6 +2049,141 @@ func (m *ProxyHandler) IsValidProxyCookie(cookie string) bool {
|
||||
return m.isValidSessionCookie(cookie)
|
||||
}
|
||||
|
||||
// checkResponseRules checks if any response rules match the current request
|
||||
func (m *ProxyHandler) checkResponseRules(req *http.Request, reqCtx *RequestContext) *http.Response {
|
||||
// check global response rules first
|
||||
if reqCtx.ProxyConfig.Global != nil {
|
||||
if resp := m.matchGlobalResponseRules(reqCtx.ProxyConfig.Global, req, reqCtx); resp != nil {
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
// check domain-specific response rules
|
||||
for _, hostConfig := range reqCtx.ProxyConfig.Hosts {
|
||||
if hostConfig != nil {
|
||||
if resp := m.matchDomainResponseRules(hostConfig, req, reqCtx); resp != nil {
|
||||
return resp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldForwardRequest checks if any matching response rule has forward: true
|
||||
func (m *ProxyHandler) shouldForwardRequest(req *http.Request, reqCtx *RequestContext) bool {
|
||||
// check global response rules first
|
||||
if reqCtx.ProxyConfig.Global != nil {
|
||||
if shouldForward := m.checkForwardInGlobalRules(reqCtx.ProxyConfig.Global, req); shouldForward {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// check domain-specific response rules
|
||||
for _, hostConfig := range reqCtx.ProxyConfig.Hosts {
|
||||
if hostConfig != nil {
|
||||
if shouldForward := m.checkForwardInDomainRules(hostConfig, req); shouldForward {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// checkForwardInGlobalRules checks if any matching global response rule has forward: true
|
||||
func (m *ProxyHandler) checkForwardInGlobalRules(rules *service.ProxyServiceRules, req *http.Request) bool {
|
||||
if rules == nil || rules.Response == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, rule := range rules.Response {
|
||||
if rule.PathRe != nil && rule.PathRe.MatchString(req.URL.Path) {
|
||||
return rule.Forward
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// checkForwardInDomainRules checks if any matching domain response rule has forward: true
|
||||
func (m *ProxyHandler) checkForwardInDomainRules(rules *service.ProxyServiceDomainConfig, req *http.Request) bool {
|
||||
if rules == nil || rules.Response == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, rule := range rules.Response {
|
||||
if rule.PathRe != nil && rule.PathRe.MatchString(req.URL.Path) {
|
||||
return rule.Forward
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// matchGlobalResponseRules checks global response rules
|
||||
func (m *ProxyHandler) matchGlobalResponseRules(rules *service.ProxyServiceRules, req *http.Request, reqCtx *RequestContext) *http.Response {
|
||||
if rules == nil || rules.Response == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, rule := range rules.Response {
|
||||
if rule.PathRe != nil && rule.PathRe.MatchString(req.URL.Path) {
|
||||
return m.createResponseFromRule(rule, req, reqCtx)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchDomainResponseRules checks domain-specific response rules
|
||||
func (m *ProxyHandler) matchDomainResponseRules(rules *service.ProxyServiceDomainConfig, req *http.Request, reqCtx *RequestContext) *http.Response {
|
||||
if rules == nil || rules.Response == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, rule := range rules.Response {
|
||||
if rule.PathRe != nil && rule.PathRe.MatchString(req.URL.Path) {
|
||||
return m.createResponseFromRule(rule, req, reqCtx)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createResponseFromRule creates an HTTP response based on a response rule
|
||||
func (m *ProxyHandler) createResponseFromRule(rule service.ProxyServiceResponseRule, req *http.Request, reqCtx *RequestContext) *http.Response {
|
||||
// ensure status code defaults to 200 if not set
|
||||
status := rule.Status
|
||||
if status == 0 {
|
||||
status = 200
|
||||
}
|
||||
|
||||
resp := &http.Response{
|
||||
StatusCode: status,
|
||||
Header: make(http.Header),
|
||||
Request: req,
|
||||
}
|
||||
|
||||
// set headers
|
||||
for name, value := range rule.Headers {
|
||||
resp.Header.Set(name, value)
|
||||
}
|
||||
|
||||
// process body
|
||||
body := rule.Body
|
||||
|
||||
resp.Body = io.NopCloser(strings.NewReader(body))
|
||||
resp.ContentLength = int64(len(body))
|
||||
|
||||
// set content-length header if not already set
|
||||
if resp.Header.Get("Content-Length") == "" {
|
||||
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) CleanupExpiredSessions() {
|
||||
now := time.Now()
|
||||
cleanedCount := 0
|
||||
|
||||
+247
-9
@@ -40,18 +40,20 @@ type ProxyServiceConfig struct {
|
||||
|
||||
// ProxyServiceDomainConfig represents configuration for a specific domain mapping
|
||||
type ProxyServiceDomainConfig struct {
|
||||
To string `yaml:"to"`
|
||||
Access *ProxyServiceAccessControl `yaml:"access,omitempty"`
|
||||
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"`
|
||||
Response []ProxyServiceResponseRule `yaml:"response,omitempty"`
|
||||
}
|
||||
|
||||
// ProxyServiceRules represents capture and replace rules
|
||||
// ProxyServiceRules represents global rules that apply to all hosts
|
||||
type ProxyServiceRules struct {
|
||||
Access *ProxyServiceAccessControl `yaml:"access,omitempty"`
|
||||
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"`
|
||||
Response []ProxyServiceResponseRule `yaml:"response,omitempty"`
|
||||
}
|
||||
|
||||
// ProxyServiceAccessControl represents access control configuration
|
||||
@@ -67,7 +69,7 @@ type ProxyServiceDenyResponse struct {
|
||||
WithoutSession string `yaml:"without_session"` // "allow" | "redirect:URL" | status code
|
||||
}
|
||||
|
||||
// CompilePathPatterns compiles regex patterns for all capture rules
|
||||
// CompilePathPatterns compiles regex patterns for all capture and response rules
|
||||
func CompilePathPatterns(config *ProxyServiceConfigYAML) error {
|
||||
// Compile global capture rule patterns
|
||||
if config.Global != nil && config.Global.Capture != nil {
|
||||
@@ -78,6 +80,15 @@ func CompilePathPatterns(config *ProxyServiceConfigYAML) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Compile global response rule patterns
|
||||
if config.Global != nil && config.Global.Response != nil {
|
||||
for i := range config.Global.Response {
|
||||
if err := compileResponsePath(&config.Global.Response[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compile host-specific capture rule patterns
|
||||
for _, hostConfig := range config.Hosts {
|
||||
if hostConfig != nil && hostConfig.Capture != nil {
|
||||
@@ -88,6 +99,29 @@ func CompilePathPatterns(config *ProxyServiceConfigYAML) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compile host-specific response rule patterns
|
||||
for _, hostConfig := range config.Hosts {
|
||||
if hostConfig != nil && hostConfig.Response != nil {
|
||||
for i := range hostConfig.Response {
|
||||
if err := compileResponsePath(&hostConfig.Response[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// compileResponsePath compiles the path pattern for a response rule
|
||||
func compileResponsePath(rule *ProxyServiceResponseRule) error {
|
||||
if rule.Path != "" {
|
||||
pathRe, err := regexp.Compile(rule.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid regex pattern for response path '%s': %w", rule.Path, err)
|
||||
}
|
||||
rule.PathRe = pathRe
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -122,9 +156,97 @@ type ProxyServiceReplaceRule struct {
|
||||
From string `yaml:"from,omitempty"`
|
||||
}
|
||||
|
||||
// ProxyServiceResponseRule represents a response rule that allows custom responses for specific paths
|
||||
//
|
||||
// COMPLETE PROCESSING ORDER & PRECEDENCE:
|
||||
// The proxy processes rules in this exact order:
|
||||
//
|
||||
// REQUEST PROCESSING:
|
||||
// 1. Response rules (FIRST) - can short-circuit everything
|
||||
// 2. Session creation/loading (SECOND) - creates session ID internally
|
||||
// 3. Access control (THIRD) - can block forwarding (uses session existence)
|
||||
// 4. Capture rules on request (headers, body, cookies)
|
||||
// 5. Rewrite rules on request (URL params, body patching)
|
||||
// 6. Request forwarded to target server
|
||||
//
|
||||
// RESPONSE PROCESSING:
|
||||
// 7. Session cookie setting (for new sessions) - cookie sent to client
|
||||
// 8. Capture rules on response (headers, body, cookies)
|
||||
// 9. Rewrite rules on response (headers, body, URL replacement)
|
||||
// 10. Final response returned to client
|
||||
//
|
||||
// PRECEDENCE RULES:
|
||||
// - Response rules with forward: false → skip ALL other processing
|
||||
// - Response rules with forward: true → capture/rewrite still apply
|
||||
// - Session created before access control (affects hasSession logic)
|
||||
// - Access control can block forwarding even with pending response
|
||||
// - Cookie only set in response phase (not during session creation)
|
||||
// - Capture rules always run (unless response rule short-circuits)
|
||||
// - Rewrite rules always run (unless response rule short-circuits)
|
||||
//
|
||||
// PRACTICAL EXAMPLES:
|
||||
//
|
||||
// Example 1 - Fake API endpoint (response rule wins, bypasses everything):
|
||||
//
|
||||
// response:
|
||||
// - path: "^/api/status$"
|
||||
// body: '{"status": "ok"}'
|
||||
// forward: false
|
||||
// access:
|
||||
// mode: "deny"
|
||||
// paths: ["^/api/status$"]
|
||||
// capture:
|
||||
// - name: "api_data"
|
||||
// path: "^/api/status$"
|
||||
// Result: Returns {"status": "ok"} immediately, no access control, no capture, no forwarding
|
||||
//
|
||||
// Example 2 - Monitor + fake response (response + access rules apply):
|
||||
//
|
||||
// response:
|
||||
// - path: "^/api/status$"
|
||||
// body: '{"status": "ok"}'
|
||||
// forward: true
|
||||
// access:
|
||||
// mode: "deny"
|
||||
// paths: ["^/api/status$"]
|
||||
// capture:
|
||||
// - name: "api_data"
|
||||
// path: "^/api/status$"
|
||||
// Result: Creates session → captures request data → returns {"status": "ok"} → sets cookie → NOT forwarded (access blocks it)
|
||||
//
|
||||
// Example 3 - Full pipeline (all rules apply):
|
||||
//
|
||||
// response:
|
||||
// - path: "^/api/status$"
|
||||
// body: '{"status": "ok"}'
|
||||
// forward: true
|
||||
// access:
|
||||
// mode: "allow"
|
||||
// paths: ["^/api/"]
|
||||
// capture:
|
||||
// - name: "api_data"
|
||||
// path: "^/api/status$"
|
||||
// rewrite:
|
||||
// - find: "original.com"
|
||||
// replace: "phishing.com"
|
||||
// Result: Creates session → captures data → rewrites content → forwards to target → captures response → sets cookie → ignores custom response
|
||||
//
|
||||
// RESPONSE RULE FEATURES:
|
||||
// - forward: false (default) → replace normal proxy behavior
|
||||
// - forward: true → provide response while still attempting to forward
|
||||
// - Body content is used as-is (plain text/HTML/JSON/etc.)
|
||||
type ProxyServiceResponseRule struct {
|
||||
Path string `yaml:"path"` // regex pattern for request path
|
||||
Status int `yaml:"status"` // HTTP status code (default: 200)
|
||||
Headers map[string]string `yaml:"headers"` // response headers to set
|
||||
Body string `yaml:"body"` // response body content (supports template variables)
|
||||
Forward bool `yaml:"forward"` // whether to also forward requesto target (default: false)
|
||||
PathRe *regexp.Regexp `yaml:"-"` // compiled regex for path matching
|
||||
}
|
||||
|
||||
// ProxyServiceConfigYAML represents the complete YAML configuration structure that matches the actual YAML format
|
||||
//
|
||||
// Example YAML configuration with access control:
|
||||
// Example YAML configuration with access control and response rules:
|
||||
//
|
||||
// version: "0.0"
|
||||
// global:
|
||||
@@ -141,6 +263,12 @@ type ProxyServiceReplaceRule struct {
|
||||
// capture:
|
||||
// - name: "global_navigation"
|
||||
// path: "/important"
|
||||
// response:
|
||||
// - path: "^/favicon\\.ico$"
|
||||
// headers:
|
||||
// Content-Type: "image/x-icon"
|
||||
// body: "base64:AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAA=="
|
||||
// forward: false
|
||||
//
|
||||
// example.com:
|
||||
//
|
||||
@@ -154,6 +282,19 @@ type ProxyServiceReplaceRule struct {
|
||||
// on_deny:
|
||||
// with_session: "redirect:https://phishing-example.com/"
|
||||
// without_session: 503
|
||||
// response:
|
||||
// - path: "^/robots\\.txt$"
|
||||
// headers:
|
||||
// Content-Type: "text/plain"
|
||||
// body: |
|
||||
// User-agent: *
|
||||
// Disallow: /
|
||||
// forward: false
|
||||
// - path: "^/api/health$"
|
||||
// headers:
|
||||
// Content-Type: "application/json"
|
||||
// body: '{"status": "ok", "timestamp": "{{timestamp}}"}'
|
||||
// forward: true
|
||||
// capture:
|
||||
// - name: "login_capture"
|
||||
// method: "POST"
|
||||
@@ -478,6 +619,11 @@ func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.P
|
||||
// set default values
|
||||
m.setProxyConfigDefaults(&config)
|
||||
|
||||
// compile regex patterns for capture and response rules
|
||||
if err := CompilePathPatterns(&config); err != nil {
|
||||
return validate.WrapErrorWithField(err, "proxyConfig")
|
||||
}
|
||||
|
||||
// validate version (after defaults are applied)
|
||||
if err := ValidateVersion(&config); err != nil {
|
||||
return validate.WrapErrorWithField(err, "proxyConfig")
|
||||
@@ -581,6 +727,10 @@ func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.P
|
||||
if err := m.validateReplaceRules(config.Global.Rewrite); err != nil {
|
||||
return err
|
||||
}
|
||||
// validate global response rules
|
||||
if err := m.validateResponseRules(config.Global.Response); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -692,6 +842,60 @@ func (m *Proxy) validateCaptureRules(captureRules []ProxyServiceCaptureRule) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateResponseRules validates response rules configuration
|
||||
func (m *Proxy) validateResponseRules(responseRules []ProxyServiceResponseRule) error {
|
||||
for i, rule := range responseRules {
|
||||
// validate path is not empty
|
||||
if rule.Path == "" {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New(fmt.Sprintf("response rule at index %d must have a path", i)),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
|
||||
// validate regex pattern
|
||||
if _, err := regexp.Compile(rule.Path); err != nil {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New(fmt.Sprintf("response rule at index %d has invalid regex pattern '%s': %v", i, rule.Path, err)),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
|
||||
// validate status code if specified
|
||||
if rule.Status != 0 {
|
||||
if rule.Status < 100 || rule.Status > 599 {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New(fmt.Sprintf("response rule at index %d has invalid status code %d (must be 100-599)", i, rule.Status)),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// validate headers
|
||||
for headerName, headerValue := range rule.Headers {
|
||||
if headerName == "" {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New(fmt.Sprintf("response rule at index %d has empty header name", i)),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
if strings.Contains(headerName, ":") || strings.Contains(headerName, "\n") || strings.Contains(headerName, "\r") {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New(fmt.Sprintf("response rule at index %d has invalid header name '%s'", i, headerName)),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
if strings.Contains(headerValue, "\n") || strings.Contains(headerValue, "\r") {
|
||||
return validate.WrapErrorWithField(
|
||||
errors.New(fmt.Sprintf("response rule at index %d has invalid header value for '%s'", i, headerName)),
|
||||
"proxyConfig",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setProxyConfigDefaults sets default values for Proxy configuration after YAML parsing
|
||||
func (m *Proxy) setProxyConfigDefaults(config *ProxyServiceConfigYAML) {
|
||||
// set default version to 0.0 if not specified
|
||||
@@ -713,6 +917,15 @@ func (m *Proxy) setProxyConfigDefaults(config *ProxyServiceConfigYAML) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if domainConfig != nil && domainConfig.Response != nil {
|
||||
for i := range domainConfig.Response {
|
||||
// set default status to 200 if not specified
|
||||
if domainConfig.Response[i].Status == 0 {
|
||||
domainConfig.Response[i].Status = 200
|
||||
}
|
||||
// set default forward to false if not specified (forward is already false by default for bool)
|
||||
}
|
||||
}
|
||||
config.Hosts[domain] = domainConfig
|
||||
}
|
||||
|
||||
@@ -730,6 +943,17 @@ func (m *Proxy) setProxyConfigDefaults(config *ProxyServiceConfigYAML) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set defaults for global response rules
|
||||
if config.Global != nil && config.Global.Response != nil {
|
||||
for i := range config.Global.Response {
|
||||
// set default status to 200 if not specified
|
||||
if config.Global.Response[i].Status == 0 {
|
||||
config.Global.Response[i].Status = 200
|
||||
}
|
||||
// set default forward to false if not specified (forward is already false by default for bool)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateReplaceRules validates a slice of replace rules
|
||||
@@ -869,6 +1093,11 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err
|
||||
// set default values
|
||||
m.setProxyConfigDefaults(&config)
|
||||
|
||||
// compile regex patterns for capture and response rules
|
||||
if err := CompilePathPatterns(&config); err != nil {
|
||||
return validate.WrapErrorWithField(err, "proxyConfig")
|
||||
}
|
||||
|
||||
// validate version (after defaults are applied)
|
||||
if err := ValidateVersion(&config); err != nil {
|
||||
return validate.WrapErrorWithField(err, "proxyConfig")
|
||||
@@ -996,6 +1225,11 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err
|
||||
return err
|
||||
}
|
||||
|
||||
// validate response rules
|
||||
if err := m.validateResponseRules(domainConfig.Response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// validate that phishing domain is not used by another proxy
|
||||
if err := m.validatePhishingDomainUniquenessByStartURL(ctx, domainConfig.To, proxy.StartURL.MustGet().String()); err != nil {
|
||||
return err
|
||||
@@ -1013,6 +1247,10 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err
|
||||
if err := m.validateReplaceRules(config.Global.Rewrite); err != nil {
|
||||
return err
|
||||
}
|
||||
// validate global response rules
|
||||
if err := m.validateResponseRules(config.Global.Response); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -102,6 +102,9 @@ export class ProxyYamlCompletionProvider {
|
||||
if (context === 'rewrite') {
|
||||
return this.getNewRewriteSuggestions(range);
|
||||
}
|
||||
if (context === 'response') {
|
||||
return this.getNewResponseSuggestions(range);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle field completions based on context
|
||||
@@ -124,6 +127,8 @@ export class ProxyYamlCompletionProvider {
|
||||
return this.getCaptureSuggestions(range);
|
||||
case 'rewrite':
|
||||
return this.getRewriteSuggestions(range);
|
||||
case 'response':
|
||||
return this.getResponseSuggestions(range);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -157,6 +162,7 @@ export class ProxyYamlCompletionProvider {
|
||||
if (key === 'access') return 'access';
|
||||
if (key === 'capture') return 'capture';
|
||||
if (key === 'rewrite') return 'rewrite';
|
||||
if (key === 'response') return 'response';
|
||||
if (key === 'on_deny') return 'on_deny';
|
||||
if (key === 'paths') return 'paths';
|
||||
}
|
||||
@@ -213,6 +219,13 @@ export class ProxyYamlCompletionProvider {
|
||||
insertText: 'rewrite:',
|
||||
documentation: 'Global rewrite rules',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'response',
|
||||
kind: this.monaco.languages.CompletionItemKind.Module,
|
||||
insertText: 'response:',
|
||||
documentation: 'Global response rules',
|
||||
range
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -246,6 +259,13 @@ export class ProxyYamlCompletionProvider {
|
||||
insertText: 'rewrite:',
|
||||
documentation: 'Domain rewrite rules',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'response',
|
||||
kind: this.monaco.languages.CompletionItemKind.Module,
|
||||
insertText: 'response:',
|
||||
documentation: 'Domain response rules',
|
||||
range
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -295,6 +315,60 @@ export class ProxyYamlCompletionProvider {
|
||||
];
|
||||
}
|
||||
|
||||
getResponseSuggestions(range) {
|
||||
return [
|
||||
{
|
||||
label: '- Response Rule',
|
||||
kind: this.monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'- path: "^/path/pattern$"',
|
||||
' status: 200',
|
||||
' headers:',
|
||||
' Content-Type: "application/json"',
|
||||
' body: \'{"message": "Hello"}\'',
|
||||
' forward: false'
|
||||
].join('\n '),
|
||||
documentation: 'Complete response rule template',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'path',
|
||||
kind: this.monaco.languages.CompletionItemKind.Property,
|
||||
insertText: 'path: "^/api/health$"',
|
||||
documentation: 'Regex pattern for request path',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'status',
|
||||
kind: this.monaco.languages.CompletionItemKind.Property,
|
||||
insertText: 'status: 200',
|
||||
documentation: 'HTTP status code (default: 200)',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'headers',
|
||||
kind: this.monaco.languages.CompletionItemKind.Module,
|
||||
insertText: 'headers:',
|
||||
documentation: 'Response headers',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'body',
|
||||
kind: this.monaco.languages.CompletionItemKind.Property,
|
||||
insertText: 'body: "Response content"',
|
||||
documentation: 'Response body content (plain text/HTML/JSON/etc.)',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'forward',
|
||||
kind: this.monaco.languages.CompletionItemKind.Property,
|
||||
insertText: 'forward: false',
|
||||
documentation: 'Whether to also forward request to target (default: false)',
|
||||
range
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getCaptureSuggestions(range) {
|
||||
return [
|
||||
{
|
||||
@@ -401,6 +475,19 @@ export class ProxyYamlCompletionProvider {
|
||||
];
|
||||
}
|
||||
|
||||
getNewResponseSuggestions(range) {
|
||||
return [
|
||||
{
|
||||
label: 'response rule',
|
||||
kind: this.monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText:
|
||||
'path: "^/api/health$"\n status: 200\n headers:\n Content-Type: "application/json"\n body: \'{"status": "ok"}\'\n forward: false',
|
||||
documentation: 'New response rule template',
|
||||
range
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getModeSuggestions(range) {
|
||||
return [
|
||||
{
|
||||
@@ -617,7 +704,12 @@ export class ProxyYamlCompletionProvider {
|
||||
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'
|
||||
to: 'Target phishing domain for this original domain',
|
||||
response: 'Rules for custom responses to specific paths',
|
||||
status: 'HTTP status code for response (default: 200)',
|
||||
headers: 'HTTP headers to include in response',
|
||||
body: 'Response body content (plain text/HTML/JSON/etc.)',
|
||||
forward: 'Whether to also forward request to target server (default: false)'
|
||||
};
|
||||
|
||||
return hoverData[word] || null;
|
||||
|
||||
@@ -68,6 +68,25 @@ proxy: "My Proxy Campaign"
|
||||
|
||||
portal.example.com:
|
||||
to: "evil.example.com"
|
||||
response:
|
||||
- path: "^/favicon\\.ico$"
|
||||
headers:
|
||||
Content-Type: "image/x-icon"
|
||||
Cache-Control: "public, max-age=3600"
|
||||
body: ""
|
||||
forward: false
|
||||
- path: "^/robots\\.txt$"
|
||||
headers:
|
||||
Content-Type: "text/plain"
|
||||
body: |
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
forward: false
|
||||
- path: "^/api/health$"
|
||||
headers:
|
||||
Content-Type: "application/json"
|
||||
body: '{"status": "ok"}'
|
||||
forward: true
|
||||
capture:
|
||||
- name: "credentials"
|
||||
method: "POST"
|
||||
|
||||
Reference in New Issue
Block a user