mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-05-27 18:12:24 +02:00
93708cef17
Vim suggestions for proxy yaml Signed-off-by: Ronni Skansing <rskansing@gmail.com>
1928 lines
57 KiB
Go
1928 lines
57 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/oapi-codegen/nullable"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/phishingclub/phishingclub/data"
|
|
"github.com/phishingclub/phishingclub/errs"
|
|
"github.com/phishingclub/phishingclub/model"
|
|
"github.com/phishingclub/phishingclub/repository"
|
|
"github.com/phishingclub/phishingclub/validate"
|
|
"github.com/phishingclub/phishingclub/vo"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Proxy is a Proxy service
|
|
type Proxy struct {
|
|
Common
|
|
ProxyRepository *repository.Proxy
|
|
DomainRepository *repository.Domain
|
|
CampaignRepository *repository.Campaign
|
|
CampaignTemplateService *CampaignTemplate
|
|
DomainService *Domain
|
|
}
|
|
|
|
// ProxyServiceConfig represents the YAML configuration for proxy
|
|
type ProxyServiceConfig struct {
|
|
Proxy string `yaml:"proxy,omitempty"`
|
|
Global *ProxyServiceRules `yaml:"global,omitempty"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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
|
|
func CompilePathPatterns(config *ProxyServiceConfigYAML) error {
|
|
// Compile global capture rule patterns
|
|
if config.Global != nil {
|
|
for i := range config.Global.Capture {
|
|
if err := compileCapturePath(&config.Global.Capture[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compile host-specific capture rule patterns
|
|
for _, hostConfig := range config.Hosts {
|
|
if hostConfig != nil {
|
|
for i := range hostConfig.Capture {
|
|
if err := compileCapturePath(&hostConfig.Capture[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// compileCapturePath compiles the path pattern for a capture rule
|
|
func compileCapturePath(rule *ProxyServiceCaptureRule) error {
|
|
if rule.Path != "" {
|
|
pathRe, err := regexp.Compile(rule.Path)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid regex pattern for path '%s': %w", rule.Path, err)
|
|
}
|
|
rule.PathRe = pathRe
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ProxyServiceCaptureRule represents a capture rule
|
|
type ProxyServiceCaptureRule struct {
|
|
Name string `yaml:"name"`
|
|
Method string `yaml:"method,omitempty"`
|
|
Path string `yaml:"path,omitempty"`
|
|
Find string `yaml:"find,omitempty"`
|
|
From string `yaml:"from,omitempty"`
|
|
Required *bool `yaml:"required,omitempty"`
|
|
PathRe *regexp.Regexp `yaml:"-"` // compiled regex for path matching
|
|
}
|
|
|
|
// ProxyServiceReplaceRule represents a replacement rule
|
|
type ProxyServiceReplaceRule struct {
|
|
Name string `yaml:"name,omitempty"`
|
|
Find string `yaml:"find"`
|
|
Replace string `yaml:"replace"`
|
|
From string `yaml:"from,omitempty"`
|
|
}
|
|
|
|
// 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"`
|
|
Global *ProxyServiceRules `yaml:"global,omitempty"`
|
|
Hosts map[string]*ProxyServiceDomainConfig `yaml:",inline"` // inline allows domain names as top-level keys
|
|
}
|
|
|
|
// ValidateVersion validates that the version is supported
|
|
func ValidateVersion(config *ProxyServiceConfigYAML) error {
|
|
if config.Version != "0.0" {
|
|
return errors.New("only version 0.0 is supported")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Create creates a new Proxy
|
|
func (m *Proxy) Create(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
proxy *model.Proxy,
|
|
) (*uuid.UUID, error) {
|
|
ae := NewAuditEvent("Proxy.Create", session)
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
m.LogAuthError(err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
m.AuditLogNotAuthorized(ae)
|
|
return nil, errs.ErrAuthorizationFailed
|
|
}
|
|
var companyID *uuid.UUID
|
|
if cid, err := proxy.CompanyID.Get(); err == nil {
|
|
companyID = &cid
|
|
}
|
|
|
|
// validate data
|
|
if err := proxy.Validate(); err != nil {
|
|
m.Logger.Errorw("failed to validate proxy", "error", err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
|
|
// validate Proxy configuration
|
|
if err := m.validateProxyConfig(ctx, proxy); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// check uniqueness
|
|
name := proxy.Name.MustGet()
|
|
isOK, err := repository.CheckNameIsUnique(
|
|
ctx,
|
|
m.ProxyRepository.DB,
|
|
"proxies",
|
|
name.String(),
|
|
companyID,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to check proxy uniqueness", "error", err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if !isOK {
|
|
m.Logger.Debugw("proxy name is already taken", "name", name.String())
|
|
return nil, validate.WrapErrorWithField(errors.New("is not unique"), "name")
|
|
}
|
|
|
|
// create proxy
|
|
id, err := m.ProxyRepository.Insert(
|
|
ctx,
|
|
proxy,
|
|
)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to create proxy", "error", err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
|
|
// create associated domains
|
|
err = m.createProxyDomains(ctx, session, id, proxy)
|
|
if err != nil {
|
|
// rollback proxy creation
|
|
m.ProxyRepository.DeleteByID(ctx, id)
|
|
m.Logger.Errorw("failed to create proxy domains", "error", err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
|
|
ae.Details["id"] = id.String()
|
|
m.AuditLogAuthorized(ae)
|
|
|
|
return id, nil
|
|
}
|
|
|
|
// GetAll gets proxies
|
|
func (m *Proxy) GetAll(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
companyID *uuid.UUID,
|
|
options *repository.ProxyOption,
|
|
) (*model.Result[model.Proxy], error) {
|
|
result := model.NewEmptyResult[model.Proxy]()
|
|
ae := NewAuditEvent("Proxy.GetAll", session)
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
m.LogAuthError(err)
|
|
return result, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
m.AuditLogNotAuthorized(ae)
|
|
return result, errs.ErrAuthorizationFailed
|
|
}
|
|
result, err = m.ProxyRepository.GetAll(
|
|
ctx,
|
|
companyID,
|
|
options,
|
|
)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to get proxies", "error", err)
|
|
return result, errs.Wrap(err)
|
|
}
|
|
// no audit log on read
|
|
return result, nil
|
|
}
|
|
|
|
// GetAllOverview gets proxies with limited data
|
|
func (m *Proxy) GetAllOverview(
|
|
companyID *uuid.UUID, // can be null
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
queryArgs *vo.QueryArgs,
|
|
) (*model.Result[model.ProxyOverview], error) {
|
|
result := model.NewEmptyResult[model.ProxyOverview]()
|
|
ae := NewAuditEvent("Proxy.GetAllOverview", session)
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
m.LogAuthError(err)
|
|
return result, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
m.AuditLogNotAuthorized(ae)
|
|
return result, errs.ErrAuthorizationFailed
|
|
}
|
|
// get proxies
|
|
result, err = m.ProxyRepository.GetAllSubset(
|
|
ctx,
|
|
companyID,
|
|
&repository.ProxyOption{
|
|
QueryArgs: queryArgs,
|
|
},
|
|
)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to get proxies subset", "error", err)
|
|
return result, errs.Wrap(err)
|
|
}
|
|
// no audit log on read
|
|
return result, nil
|
|
}
|
|
|
|
// GetByID gets a Proxy by ID
|
|
func (m *Proxy) GetByID(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
id *uuid.UUID,
|
|
options *repository.ProxyOption,
|
|
) (*model.Proxy, error) {
|
|
ae := NewAuditEvent("Proxy.GetByID", session)
|
|
ae.Details["id"] = id.String()
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
m.LogAuthError(err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if !isAuthorized {
|
|
m.AuditLogNotAuthorized(ae)
|
|
return nil, errs.ErrAuthorizationFailed
|
|
}
|
|
// get proxy
|
|
proxy, err := m.ProxyRepository.GetByID(
|
|
ctx,
|
|
id,
|
|
options,
|
|
)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
// return early this is not a an error
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to get proxy by ID", "error", err)
|
|
return nil, errs.Wrap(err)
|
|
}
|
|
|
|
// apply defaults to Proxy configuration for display
|
|
if err := m.applyConfigurationDefaults(proxy); err != nil {
|
|
m.Logger.Errorw("failed to apply configuration defaults", "error", err)
|
|
// don't fail the request, just log the error
|
|
}
|
|
|
|
// no audit log on read
|
|
return proxy, nil
|
|
}
|
|
|
|
// UpdateByID updates a Proxy by ID
|
|
func (m *Proxy) UpdateByID(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
id *uuid.UUID,
|
|
proxy *model.Proxy,
|
|
) error {
|
|
ae := NewAuditEvent("Proxy.UpdateByID", session)
|
|
ae.Details["id"] = id.String()
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
m.LogAuthError(err)
|
|
return err
|
|
}
|
|
if !isAuthorized {
|
|
m.AuditLogNotAuthorized(ae)
|
|
return errs.ErrAuthorizationFailed
|
|
}
|
|
// get current
|
|
current, err := m.ProxyRepository.GetByID(
|
|
ctx,
|
|
id,
|
|
&repository.ProxyOption{},
|
|
)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
m.Logger.Debugw("failed to update proxy by ID", "error", err)
|
|
return err
|
|
}
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to update proxy by ID", "error", err)
|
|
return err
|
|
}
|
|
// update proxy - if a field is present and not null, update it
|
|
if v, err := proxy.Name.Get(); err == nil {
|
|
// check uniqueness
|
|
var companyID *uuid.UUID
|
|
if cid, err := current.CompanyID.Get(); err == nil {
|
|
companyID = &cid
|
|
}
|
|
name := proxy.Name.MustGet()
|
|
isOK, err := repository.CheckNameIsUnique(
|
|
ctx,
|
|
m.ProxyRepository.DB,
|
|
"proxies",
|
|
name.String(),
|
|
companyID,
|
|
id,
|
|
)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to check proxy uniqueness", "error", err)
|
|
return err
|
|
}
|
|
if !isOK {
|
|
m.Logger.Debugw("proxy name is already taken", "name", name.String())
|
|
return validate.WrapErrorWithField(errors.New("is not unique"), "name")
|
|
}
|
|
current.Name.Set(v)
|
|
}
|
|
if v, err := proxy.Description.Get(); err == nil {
|
|
current.Description.Set(v)
|
|
}
|
|
if v, err := proxy.StartURL.Get(); err == nil {
|
|
current.StartURL.Set(v)
|
|
}
|
|
if v, err := proxy.ProxyConfig.Get(); err == nil {
|
|
current.ProxyConfig.Set(v)
|
|
}
|
|
|
|
// validate updated Proxy configuration
|
|
if err := m.validateProxyConfigForUpdate(ctx, current, id); err != nil {
|
|
return err
|
|
}
|
|
|
|
// update proxy
|
|
err = m.ProxyRepository.UpdateByID(
|
|
ctx,
|
|
id,
|
|
current,
|
|
)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to update proxy by ID", "error", err)
|
|
return err
|
|
}
|
|
|
|
// update associated domains
|
|
err = m.syncProxyDomains(ctx, session, id, current)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to sync proxy domains", "error", err)
|
|
return err
|
|
}
|
|
|
|
ae.Details["id"] = id.String()
|
|
m.AuditLogAuthorized(ae)
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateProxyConfigForUpdate validates Proxy configuration during update, allowing same domains for same proxy
|
|
func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.Proxy, proxyID *uuid.UUID) error {
|
|
// validate Proxy configuration YAML
|
|
proxyConfig, err := proxy.ProxyConfig.Get()
|
|
if err != nil {
|
|
return validate.WrapErrorWithField(errors.New("Proxy configuration is required"), "proxyConfig")
|
|
}
|
|
|
|
// parse complete YAML structure
|
|
var config ProxyServiceConfigYAML
|
|
if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
|
|
return validate.WrapErrorWithField(errors.New("invalid YAML format: "+err.Error()), "proxyConfig")
|
|
}
|
|
|
|
// set default values
|
|
m.setProxyConfigDefaults(&config)
|
|
|
|
// validate version (after defaults are applied)
|
|
if err := ValidateVersion(&config); err != nil {
|
|
return validate.WrapErrorWithField(err, "proxyConfig")
|
|
}
|
|
|
|
// validate that at least one domain mapping exists
|
|
if len(config.Hosts) == 0 {
|
|
return validate.WrapErrorWithField(errors.New("at least one domain mapping must be specified"), "proxyConfig")
|
|
}
|
|
|
|
// validate global uniqueness of capture names across all domains and global rules
|
|
if err := m.validateGlobalCaptureNameUniqueness(&config); err != nil {
|
|
return err
|
|
}
|
|
|
|
// ensure that the start URL domain is mentioned in the domain mappings
|
|
startURL, err := proxy.StartURL.Get()
|
|
if err == nil {
|
|
startURLStr := startURL.String()
|
|
var startDomain string
|
|
|
|
// extract domain from start URL
|
|
if strings.Contains(startURLStr, "://") {
|
|
// full URL like https://auth.example.com/login
|
|
parts := strings.Split(startURLStr, "://")
|
|
if len(parts) > 1 {
|
|
domainParts := strings.Split(parts[1], "/")
|
|
startDomain = domainParts[0]
|
|
}
|
|
} else if strings.Contains(startURLStr, "/") {
|
|
// domain/path format like auth.example.com/login
|
|
parts := strings.Split(startURLStr, "/")
|
|
startDomain = parts[0]
|
|
} else {
|
|
// just domain like auth.example.com
|
|
startDomain = startURLStr
|
|
}
|
|
|
|
// check if start domain is in the domain mappings
|
|
if startDomain != "" {
|
|
found := false
|
|
for originalDomain := range config.Hosts {
|
|
if originalDomain == startDomain {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("start URL domain '%s' must be included in domain mappings", startDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// validate each domain mapping
|
|
for originalDomain, domainConfig := range config.Hosts {
|
|
if domainConfig == nil {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("domain config for '%s' is nil", originalDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
// validate that 'to' is specified
|
|
if domainConfig.To == "" {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("'to' field is required for domain '%s'", originalDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// validate domain-specific rewrite rules
|
|
if err := m.validateReplaceRules(domainConfig.Rewrite); err != nil {
|
|
return err
|
|
}
|
|
|
|
// note: domain uniqueness validation is skipped during updates
|
|
// the syncProxyDomains method will handle domain management properly
|
|
}
|
|
|
|
// 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
|
|
}
|
|
if err := m.validateReplaceRules(config.Global.Rewrite); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateCaptureRules validates a slice of capture rules
|
|
func (m *Proxy) validateCaptureRules(captureRules []ProxyServiceCaptureRule) error {
|
|
// track capture names to prevent duplicates
|
|
captureNames := make(map[string]bool)
|
|
|
|
for _, capture := range captureRules {
|
|
if capture.Name == "" {
|
|
return validate.WrapErrorWithField(errors.New("capture rule name is required"), "proxyConfig")
|
|
}
|
|
|
|
// check for duplicate capture names
|
|
if captureNames[capture.Name] {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("duplicate capture rule name '%s' found - each capture rule must have a unique name", capture.Name)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
captureNames[capture.Name] = true
|
|
|
|
if capture.Path == "" {
|
|
return validate.WrapErrorWithField(errors.New("capture rule path is required"), "proxyConfig")
|
|
}
|
|
|
|
// allow empty find pattern for any method path-based navigation tracking
|
|
isNavigationTracking := capture.Path != "" && capture.Find == ""
|
|
|
|
if capture.Find == "" && !isNavigationTracking {
|
|
return validate.WrapErrorWithField(
|
|
errors.New("capture rule must have a find pattern, except for path-based navigation tracking"),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
if capture.Find != "" {
|
|
// for cookie captures, find field contains cookie name (literal string)
|
|
// for other captures, find field contains regex pattern
|
|
if capture.From != "cookie" {
|
|
if _, err := regexp.Compile(capture.Find); err != nil {
|
|
return validate.WrapErrorWithField(
|
|
errors.New("invalid regex pattern in capture rule: "+err.Error()),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 'from' field defaults to 'any' if not specified (handled in setProxyConfigDefaults)
|
|
// validate 'from' field if specified
|
|
if capture.From != "" {
|
|
validFromValues := []string{"request_body", "request_header", "response_body", "response_header", "cookie", "any"}
|
|
valid := false
|
|
for _, validFrom := range validFromValues {
|
|
if capture.From == validFrom {
|
|
valid = true
|
|
break
|
|
}
|
|
}
|
|
if !valid {
|
|
return validate.WrapErrorWithField(
|
|
errors.New("invalid 'from' value in capture rule, must be one of: "+strings.Join(validFromValues, ", ")),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
}
|
|
|
|
// validate cookie-specific rules
|
|
if capture.From == "cookie" {
|
|
if capture.Find == "" {
|
|
return validate.WrapErrorWithField(
|
|
errors.New("capture rule with from='cookie' must specify cookie name in 'find' field"),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
// validate cookie name format (basic validation)
|
|
cookieName := capture.Find
|
|
if len(cookieName) == 0 {
|
|
return validate.WrapErrorWithField(
|
|
errors.New("cookie name cannot be empty"),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
// cookie names cannot contain certain characters
|
|
invalidChars := []string{" ", "\t", "\n", "\r", "=", ";", ","}
|
|
for _, char := range invalidChars {
|
|
if strings.Contains(cookieName, char) {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("cookie name '%s' contains invalid character '%s'", cookieName, char)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
}
|
|
|
|
// method should be specified for cookie captures
|
|
if capture.Method == "" {
|
|
return validate.WrapErrorWithField(
|
|
errors.New("capture rule with from='cookie' should specify HTTP method"),
|
|
"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
|
|
if config.Version == "" {
|
|
config.Version = "0.0"
|
|
}
|
|
|
|
for domain, domainConfig := range config.Hosts {
|
|
for i := range domainConfig.Capture {
|
|
// set default required to true if not specified
|
|
if domainConfig.Capture[i].Required == nil {
|
|
trueValue := true
|
|
domainConfig.Capture[i].Required = &trueValue
|
|
}
|
|
// set default 'from' to 'any' if not specified
|
|
if domainConfig.Capture[i].From == "" {
|
|
domainConfig.Capture[i].From = "any"
|
|
}
|
|
}
|
|
config.Hosts[domain] = domainConfig
|
|
}
|
|
|
|
// set defaults for global capture rules
|
|
if config.Global != nil {
|
|
for i := range config.Global.Capture {
|
|
// set default required to true if not specified
|
|
if config.Global.Capture[i].Required == nil {
|
|
trueValue := true
|
|
config.Global.Capture[i].Required = &trueValue
|
|
}
|
|
// set default 'from' to 'any' if not specified
|
|
if config.Global.Capture[i].From == "" {
|
|
config.Global.Capture[i].From = "any"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// validateReplaceRules validates a slice of replace rules
|
|
func (m *Proxy) validateReplaceRules(replaceRules []ProxyServiceReplaceRule) error {
|
|
for _, replace := range replaceRules {
|
|
if replace.Find == "" {
|
|
return validate.WrapErrorWithField(errors.New("replace rule 'find' is required"), "proxyConfig")
|
|
}
|
|
if _, err := regexp.Compile(replace.Find); err != nil {
|
|
return validate.WrapErrorWithField(
|
|
errors.New("invalid regex pattern in replace rule 'find': "+err.Error()),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
if replace.From != "" {
|
|
validFromValues := []string{"request_body", "request_header", "response_body", "response_header", "any"}
|
|
valid := false
|
|
for _, validFrom := range validFromValues {
|
|
if replace.From == validFrom {
|
|
valid = true
|
|
break
|
|
}
|
|
}
|
|
if !valid {
|
|
return validate.WrapErrorWithField(
|
|
errors.New("invalid 'from' value in replace rule, must be one of: "+strings.Join(validFromValues, ", ")),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
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
|
|
proxyConfig, err := proxy.ProxyConfig.Get()
|
|
if err != nil {
|
|
return validate.WrapErrorWithField(errors.New("Proxy configuration is required"), "proxyConfig")
|
|
}
|
|
|
|
// parse complete YAML structure
|
|
var config ProxyServiceConfigYAML
|
|
if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
|
|
return validate.WrapErrorWithField(errors.New("invalid YAML format: "+err.Error()), "proxyConfig")
|
|
}
|
|
|
|
// set default values
|
|
m.setProxyConfigDefaults(&config)
|
|
|
|
// validate version (after defaults are applied)
|
|
if err := ValidateVersion(&config); err != nil {
|
|
return validate.WrapErrorWithField(err, "proxyConfig")
|
|
}
|
|
|
|
// validate that at least one domain mapping exists
|
|
if len(config.Hosts) == 0 {
|
|
return validate.WrapErrorWithField(errors.New("at least one domain mapping must be specified"), "proxyConfig")
|
|
}
|
|
|
|
// validate global uniqueness of capture names across all domains and global rules
|
|
if err := m.validateGlobalCaptureNameUniqueness(&config); err != nil {
|
|
return err
|
|
}
|
|
|
|
// ensure that the start URL domain is mentioned in the domain mappings
|
|
startURL, err := proxy.StartURL.Get()
|
|
if err == nil {
|
|
startURLStr := startURL.String()
|
|
var startDomain string
|
|
|
|
// extract domain from start URL
|
|
if strings.Contains(startURLStr, "://") {
|
|
// full URL like https://auth.example.com/login
|
|
parts := strings.Split(startURLStr, "://")
|
|
if len(parts) > 1 {
|
|
domainParts := strings.Split(parts[1], "/")
|
|
startDomain = domainParts[0]
|
|
}
|
|
} else if strings.Contains(startURLStr, "/") {
|
|
// domain/path format like auth.example.com/login
|
|
parts := strings.Split(startURLStr, "/")
|
|
startDomain = parts[0]
|
|
} else {
|
|
// just domain like auth.example.com
|
|
startDomain = startURLStr
|
|
}
|
|
|
|
// check if start domain is in the domain mappings
|
|
if startDomain != "" {
|
|
found := false
|
|
for originalDomain := range config.Hosts {
|
|
if originalDomain == startDomain {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("start URL domain '%s' must be included in domain mappings", startDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// validate each domain mapping
|
|
for originalDomain, domainConfig := range config.Hosts {
|
|
if domainConfig == nil {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("domain config for '%s' is nil", originalDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
// validate that 'to' is specified
|
|
if domainConfig.To == "" {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("'to' field is required for domain mapping '%s'", originalDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
// validate that phishing domain doesn't already exist (unless it's managed by this proxy)
|
|
phishingDomainVO, err := vo.NewString255(domainConfig.To)
|
|
if err != nil {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("invalid phishing domain format: %s", domainConfig.To)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
if existingDomain != nil {
|
|
// check if this domain is managed by a different proxy or is a regular domain
|
|
if existingDomain.Type.MustGet().String() != "proxy" {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("domain '%s' already exists as a regular domain", domainConfig.To)),
|
|
"proxyConfig",
|
|
)
|
|
} else {
|
|
// it's a proxy domain, check if it belongs to a different proxy
|
|
existingTarget, err := existingDomain.ProxyTargetDomain.Get()
|
|
if err == nil {
|
|
startURL, err := proxy.StartURL.Get()
|
|
if err == nil {
|
|
// extract domain from start URL for comparison
|
|
startURLParsed, err := url.Parse(startURL.String())
|
|
if err == nil && existingTarget.String() != startURLParsed.Host {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("phishing domain '%s' is already used by another Proxy configuration", domainConfig.To)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// validate domain-specific rewrite rules
|
|
if err := m.validateReplaceRules(domainConfig.Rewrite); 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
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
if err := m.validateReplaceRules(config.Global.Rewrite); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateGlobalCaptureNameUniqueness ensures all capture rule names are unique across the entire Proxy configuration
|
|
func (m *Proxy) validateGlobalCaptureNameUniqueness(config *ProxyServiceConfigYAML) error {
|
|
allCaptureNames := make(map[string]string) // name -> location
|
|
|
|
// collect all capture names from domain-specific rules
|
|
for domain, domainConfig := range config.Hosts {
|
|
for _, capture := range domainConfig.Capture {
|
|
if capture.Name == "" {
|
|
continue // this will be caught by other validation
|
|
}
|
|
|
|
if existingLocation, exists := allCaptureNames[capture.Name]; exists {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("duplicate capture rule name '%s' found in domain '%s' - already used in %s", capture.Name, domain, existingLocation)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
allCaptureNames[capture.Name] = fmt.Sprintf("domain '%s'", domain)
|
|
}
|
|
}
|
|
|
|
// collect all capture names from global rules
|
|
if config.Global != nil {
|
|
for _, capture := range config.Global.Capture {
|
|
if capture.Name == "" {
|
|
continue // this will be caught by other validation
|
|
}
|
|
|
|
if existingLocation, exists := allCaptureNames[capture.Name]; exists {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("duplicate capture rule name '%s' found in global rules - already used in %s", capture.Name, existingLocation)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
allCaptureNames[capture.Name] = "global rules"
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyConfigurationDefaults applies default values to Proxy configuration for display
|
|
func (m *Proxy) applyConfigurationDefaults(proxy *model.Proxy) error {
|
|
proxyConfig, err := proxy.ProxyConfig.Get()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// parse complete YAML structure
|
|
var config ProxyServiceConfigYAML
|
|
if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
|
|
return err
|
|
}
|
|
|
|
// apply defaults
|
|
m.setProxyConfigDefaults(&config)
|
|
|
|
// marshal back to YAML
|
|
updatedConfigBytes, err := yaml.Marshal(&config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// update the Proxy configuration with defaults applied
|
|
updatedConfigVO := vo.NewString1MBMust(string(updatedConfigBytes))
|
|
proxy.ProxyConfig = nullable.NewNullableWithValue(*updatedConfigVO)
|
|
return nil
|
|
}
|
|
|
|
// deleteProxyDomains deletes all domains associated with a proxy
|
|
func (m *Proxy) deleteProxyDomains(ctx context.Context, session *model.Session, proxyID *uuid.UUID, proxy *model.Proxy) error {
|
|
proxyConfig, err := proxy.ProxyConfig.Get()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// parse complete YAML structure
|
|
var config ProxyServiceConfigYAML
|
|
if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
|
|
return err
|
|
}
|
|
|
|
// set default values
|
|
m.setProxyConfigDefaults(&config)
|
|
|
|
// delete domains for each mapping
|
|
for _, domainConfig := range config.Hosts {
|
|
if domainConfig == nil {
|
|
continue
|
|
}
|
|
|
|
// get domain by name and delete if it's a proxy domain
|
|
phishingDomainVO, err := vo.NewString255(domainConfig.To)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
|
|
if err == nil && existingDomain != nil {
|
|
// delete old domains that have proxy type
|
|
if existingDomain.Type.MustGet().String() == "proxy" {
|
|
domainID, err := existingDomain.ID.Get()
|
|
if err == nil {
|
|
err = m.DomainService.DeleteProxyDomain(ctx, session, &domainID)
|
|
if err != nil {
|
|
m.Logger.Warnw("failed to delete proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
"error", err,
|
|
)
|
|
} else {
|
|
m.Logger.Debugw("deleted proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validatePhishingDomainUniqueness checks if a phishing domain is already used by another proxy
|
|
func (m *Proxy) validatePhishingDomainUniqueness(ctx context.Context, phishingDomain string, excludeProxyID *uuid.UUID) error {
|
|
phishingDomainVO, err := vo.NewString255(phishingDomain)
|
|
if err != nil {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("invalid phishing domain format: %s", phishingDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
if existingDomain != nil {
|
|
// check if this domain is managed by a different proxy
|
|
if existingDomain.Type.MustGet().String() == "proxy" {
|
|
// check if it's managed by the same proxy we're updating (allowed)
|
|
if excludeProxyID != nil {
|
|
// this is a bit of a workaround - we'd need to track which proxy owns which domain
|
|
// for now, we'll allow updates to existing proxy domains
|
|
// TODO: add a proxy_id field to domains table for proper tracking
|
|
return nil
|
|
}
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("phishing domain '%s' is already used by another proxy", phishingDomain)),
|
|
"proxyConfig",
|
|
)
|
|
} else {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("domain '%s' already exists as a regular domain", phishingDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validatePhishingDomainUniquenessByStartURL checks if a phishing domain is already used by another proxy using start URL comparison
|
|
func (m *Proxy) validatePhishingDomainUniquenessByStartURL(ctx context.Context, phishingDomain string, currentStartURL string) error {
|
|
phishingDomainVO, err := vo.NewString255(phishingDomain)
|
|
if err != nil {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("invalid phishing domain format: %s", phishingDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
if existingDomain != nil {
|
|
if existingDomain.Type.MustGet().String() == "proxy" {
|
|
// check if it belongs to a different proxy by comparing target domains
|
|
existingTarget, err := existingDomain.ProxyTargetDomain.Get()
|
|
if err == nil {
|
|
// extract domain from current start URL for comparison
|
|
currentStartURLParsed, err := url.Parse(currentStartURL)
|
|
if err != nil {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("invalid start URL format: %s", currentStartURL)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
// normalize and extract domain for comparison
|
|
existingTargetStr := strings.ToLower(strings.TrimSpace(existingTarget.String()))
|
|
currentHostNormalized := strings.ToLower(strings.TrimSpace(currentStartURLParsed.Host))
|
|
|
|
// if existing target is a full URL, extract just the host part
|
|
var existingTargetNormalized string
|
|
if strings.Contains(existingTargetStr, "://") {
|
|
// it's a full URL, parse it to get the host
|
|
existingTargetParsed, err := url.Parse(existingTargetStr)
|
|
if err != nil {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("invalid existing target URL format: %s", existingTargetStr)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
existingTargetNormalized = strings.ToLower(strings.TrimSpace(existingTargetParsed.Host))
|
|
} else {
|
|
// it's already just a domain
|
|
existingTargetNormalized = existingTargetStr
|
|
}
|
|
|
|
if existingTargetNormalized != currentHostNormalized {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("phishing domain '%s' is already used by another Proxy configuration", phishingDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("domain '%s' already exists as a regular domain", phishingDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validatePhishingDomainUniquenessForUpdate validates phishing domain uniqueness during proxy updates
|
|
func (m *Proxy) validatePhishingDomainUniquenessForUpdate(ctx context.Context, phishingDomain string, currentStartURL string, currentProxyID *uuid.UUID) error {
|
|
m.Logger.Debugw("validating phishing domain uniqueness for update",
|
|
"phishingDomain", phishingDomain,
|
|
"currentStartURL", currentStartURL,
|
|
"currentProxyID", currentProxyID.String(),
|
|
)
|
|
|
|
phishingDomainVO, err := vo.NewString255(phishingDomain)
|
|
if err != nil {
|
|
m.Logger.Errorw("invalid phishing domain format",
|
|
"phishingDomain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("invalid phishing domain format: %s", phishingDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
m.Logger.Errorw("error getting existing domain",
|
|
"phishingDomain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
return err
|
|
}
|
|
|
|
if existingDomain == nil {
|
|
m.Logger.Debugw("no existing domain found, validation passed",
|
|
"phishingDomain", phishingDomain,
|
|
)
|
|
return nil
|
|
}
|
|
|
|
m.Logger.Debugw("existing domain found",
|
|
"phishingDomain", phishingDomain,
|
|
"existingDomainType", existingDomain.Type.MustGet().String(),
|
|
)
|
|
|
|
if existingDomain.Type.MustGet().String() == "proxy" {
|
|
// check if it belongs to a different proxy by comparing target domains
|
|
existingTarget, err := existingDomain.ProxyTargetDomain.Get()
|
|
if err != nil {
|
|
m.Logger.Errorw("error getting existing domain proxy target",
|
|
"phishingDomain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
return err
|
|
}
|
|
|
|
m.Logger.Debugw("existing domain proxy target found",
|
|
"phishingDomain", phishingDomain,
|
|
"existingTarget", existingTarget.String(),
|
|
)
|
|
|
|
// extract domain from current start URL for comparison
|
|
currentStartURLParsed, err := url.Parse(currentStartURL)
|
|
if err != nil {
|
|
m.Logger.Errorw("error parsing current start URL",
|
|
"currentStartURL", currentStartURL,
|
|
"error", err,
|
|
)
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("invalid start URL format: %s", currentStartURL)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
// normalize and extract domain for comparison
|
|
existingTargetStr := strings.ToLower(strings.TrimSpace(existingTarget.String()))
|
|
currentHostNormalized := strings.ToLower(strings.TrimSpace(currentStartURLParsed.Host))
|
|
|
|
// if existing target is a full URL, extract just the host part
|
|
var existingTargetNormalized string
|
|
if strings.Contains(existingTargetStr, "://") {
|
|
// it's a full URL, parse it to get the host
|
|
existingTargetParsed, err := url.Parse(existingTargetStr)
|
|
if err != nil {
|
|
m.Logger.Errorw("error parsing existing target URL",
|
|
"existingTarget", existingTargetStr,
|
|
"error", err,
|
|
)
|
|
return err
|
|
}
|
|
existingTargetNormalized = strings.ToLower(strings.TrimSpace(existingTargetParsed.Host))
|
|
} else {
|
|
// it's already just a domain
|
|
existingTargetNormalized = existingTargetStr
|
|
}
|
|
|
|
m.Logger.Debugw("comparing normalized domains",
|
|
"phishingDomain", phishingDomain,
|
|
"existingTargetStr", existingTargetStr,
|
|
"existingTargetNormalized", existingTargetNormalized,
|
|
"currentHostNormalized", currentHostNormalized,
|
|
)
|
|
|
|
// if target domains don't match, it belongs to a different proxy
|
|
if existingTargetNormalized != currentHostNormalized {
|
|
m.Logger.Warnw("phishing domain belongs to different proxy",
|
|
"phishingDomain", phishingDomain,
|
|
"existingTargetNormalized", existingTargetNormalized,
|
|
"currentHostNormalized", currentHostNormalized,
|
|
"currentProxyID", currentProxyID.String(),
|
|
)
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("phishing domain '%s' is already used by another Proxy configuration (existing target: %s, current target: %s)", phishingDomain, existingTargetNormalized, currentHostNormalized)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
|
|
// if target domains match, this domain belongs to the current proxy being updated, so it's allowed
|
|
m.Logger.Debugw("phishing domain belongs to current proxy, allowing reuse",
|
|
"domain", phishingDomain,
|
|
"proxyID", currentProxyID.String(),
|
|
"existingTarget", existingTargetNormalized,
|
|
"currentHost", currentHostNormalized,
|
|
)
|
|
} else {
|
|
m.Logger.Warnw("domain exists as regular domain, not proxy",
|
|
"phishingDomain", phishingDomain,
|
|
"existingDomainType", existingDomain.Type.MustGet().String(),
|
|
)
|
|
return validate.WrapErrorWithField(
|
|
errors.New(fmt.Sprintf("domain '%s' already exists as a regular domain", phishingDomain)),
|
|
"proxyConfig",
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// createProxyDomains creates domains for the proxy based on the configuration
|
|
func (m *Proxy) createProxyDomains(ctx context.Context, session *model.Session, proxyID *uuid.UUID, proxy *model.Proxy) error {
|
|
proxyConfig, err := proxy.ProxyConfig.Get()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get proxy config: %w", err)
|
|
}
|
|
|
|
// parse complete YAML structure
|
|
var config ProxyServiceConfigYAML
|
|
if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
|
|
return fmt.Errorf("failed to parse proxy config YAML: %w", err)
|
|
}
|
|
|
|
// set default values
|
|
m.setProxyConfigDefaults(&config)
|
|
|
|
var companyID *uuid.UUID
|
|
if cid, err := proxy.CompanyID.Get(); err == nil {
|
|
companyID = &cid
|
|
}
|
|
|
|
startURL := proxy.StartURL.MustGet()
|
|
createdDomains := make([]string, 0)
|
|
|
|
// create domains for each mapping
|
|
for originalDomain, domainConfig := range config.Hosts {
|
|
if domainConfig == nil {
|
|
continue
|
|
}
|
|
|
|
if domainConfig.To == "" {
|
|
m.Logger.Warnw("empty 'to' field in domain config",
|
|
"proxyID", proxyID.String(),
|
|
"originalDomain", originalDomain,
|
|
)
|
|
// rollback created domains on error
|
|
m.rollbackCreatedDomains(ctx, session, createdDomains)
|
|
return fmt.Errorf("'to' field is required for domain mapping '%s'", originalDomain)
|
|
}
|
|
|
|
// check if domain already exists (might be from previous failed attempt)
|
|
phishingDomainVO, err := vo.NewString255(domainConfig.To)
|
|
if err != nil {
|
|
m.Logger.Warnw("invalid phishing domain format",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
"error", err,
|
|
)
|
|
// rollback created domains on error
|
|
m.rollbackCreatedDomains(ctx, session, createdDomains)
|
|
return fmt.Errorf("invalid phishing domain format %s: %w", domainConfig.To, err)
|
|
}
|
|
|
|
existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
|
|
if err == nil && existingDomain != nil {
|
|
// domain already exists, check if it's compatible
|
|
if existingDomain.Type.MustGet().String() == "proxy" {
|
|
existingTarget, err := existingDomain.ProxyTargetDomain.Get()
|
|
if err == nil && existingTarget.String() == startURL.String() {
|
|
// compatible existing domain, skip creation
|
|
m.Logger.Debugw("proxy domain already exists, skipping creation",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
)
|
|
createdDomains = append(createdDomains, domainConfig.To)
|
|
continue
|
|
}
|
|
}
|
|
// incompatible domain exists
|
|
m.Logger.Warnw("incompatible domain already exists",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
"existingType", existingDomain.Type.MustGet().String(),
|
|
)
|
|
// rollback created domains on error
|
|
m.rollbackCreatedDomains(ctx, session, createdDomains)
|
|
return fmt.Errorf("domain %s already exists and is incompatible", domainConfig.To)
|
|
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
// database error
|
|
m.Logger.Errorw("failed to check existing domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
"error", err,
|
|
)
|
|
// rollback created domains on error
|
|
m.rollbackCreatedDomains(ctx, session, createdDomains)
|
|
return fmt.Errorf("failed to check existing domain %s: %w", domainConfig.To, err)
|
|
}
|
|
|
|
// create new domain
|
|
domain := &model.Domain{}
|
|
domain.Name.Set(*vo.NewString255Must(domainConfig.To))
|
|
domain.Type.Set(*vo.NewString32Must("proxy"))
|
|
domain.ProxyID.Set(*proxyID)
|
|
|
|
// set the target domain to the original domain from the YAML config
|
|
proxyTargetDomain, err := vo.NewOptionalString255(originalDomain)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to create proxy target domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
"startURL", startURL.String(),
|
|
"error", err,
|
|
)
|
|
// rollback created domains on error
|
|
m.rollbackCreatedDomains(ctx, session, createdDomains)
|
|
return fmt.Errorf("failed to create proxy target domain for %s: %w", domainConfig.To, err)
|
|
}
|
|
domain.ProxyTargetDomain.Set(*proxyTargetDomain)
|
|
|
|
domain.HostWebsite.Set(false)
|
|
domain.ManagedTLS.Set(true)
|
|
domain.OwnManagedTLS.Set(false)
|
|
|
|
pageContent, err := vo.NewOptionalString1MB("")
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to create page content",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
"error", err,
|
|
)
|
|
// rollback created domains on error
|
|
m.rollbackCreatedDomains(ctx, session, createdDomains)
|
|
return fmt.Errorf("failed to create page content for %s: %w", domainConfig.To, err)
|
|
}
|
|
domain.PageContent.Set(*pageContent)
|
|
|
|
pageNotFoundContent, err := vo.NewOptionalString1MB("")
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to create page not found content",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
"error", err,
|
|
)
|
|
// rollback created domains on error
|
|
m.rollbackCreatedDomains(ctx, session, createdDomains)
|
|
return fmt.Errorf("failed to create page not found content for %s: %w", domainConfig.To, err)
|
|
}
|
|
domain.PageNotFoundContent.Set(*pageNotFoundContent)
|
|
|
|
redirectURL, err := vo.NewOptionalString1024("")
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to create redirect URL",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
"error", err,
|
|
)
|
|
// rollback created domains on error
|
|
m.rollbackCreatedDomains(ctx, session, createdDomains)
|
|
return fmt.Errorf("failed to create redirect URL for %s: %w", domainConfig.To, err)
|
|
}
|
|
domain.RedirectURL.Set(*redirectURL)
|
|
|
|
if companyID != nil {
|
|
domain.CompanyID.Set(*companyID)
|
|
}
|
|
|
|
_, err = m.DomainService.CreateProxyDomain(ctx, session, domain)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to create proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
"error", err,
|
|
)
|
|
// rollback created domains on error
|
|
m.rollbackCreatedDomains(ctx, session, createdDomains)
|
|
return fmt.Errorf("failed to create domain %s: %w", domainConfig.To, err)
|
|
}
|
|
|
|
createdDomains = append(createdDomains, domainConfig.To)
|
|
m.Logger.Debugw("created proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", domainConfig.To,
|
|
)
|
|
}
|
|
|
|
m.Logger.Infow("successfully created all proxy domains",
|
|
"proxyID", proxyID.String(),
|
|
"domainsCreated", len(createdDomains),
|
|
"domains", createdDomains,
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// rollbackCreatedDomains attempts to delete domains that were created during a failed proxy creation
|
|
func (m *Proxy) rollbackCreatedDomains(ctx context.Context, session *model.Session, createdDomains []string) {
|
|
for _, domainName := range createdDomains {
|
|
phishingDomainVO, err := vo.NewString255(domainName)
|
|
if err != nil {
|
|
m.Logger.Warnw("failed to create domain VO for rollback",
|
|
"domain", domainName,
|
|
"error", err,
|
|
)
|
|
continue
|
|
}
|
|
|
|
existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomainVO, &repository.DomainOption{})
|
|
if err != nil {
|
|
m.Logger.Warnw("failed to get domain for rollback",
|
|
"domain", domainName,
|
|
"error", err,
|
|
)
|
|
continue
|
|
}
|
|
|
|
if existingDomain != nil && existingDomain.Type.MustGet().String() == "proxy" {
|
|
domainID, err := existingDomain.ID.Get()
|
|
if err == nil {
|
|
err = m.DomainService.DeleteProxyDomain(ctx, session, &domainID)
|
|
if err != nil {
|
|
m.Logger.Warnw("failed to rollback proxy domain",
|
|
"domain", domainName,
|
|
"error", err,
|
|
)
|
|
} else {
|
|
m.Logger.Debugw("rolled back proxy domain",
|
|
"domain", domainName,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// syncProxyDomains synchronizes domains for the proxy based on the configuration
|
|
func (m *Proxy) syncProxyDomains(ctx context.Context, session *model.Session, proxyID *uuid.UUID, proxy *model.Proxy) error {
|
|
proxyConfig, err := proxy.ProxyConfig.Get()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get proxy config for sync: %w", err)
|
|
}
|
|
|
|
// get current proxy domains by proxy ID
|
|
currentDomainsResult, err := m.DomainService.GetByProxyID(ctx, session, proxyID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get current proxy domains: %w", err)
|
|
}
|
|
|
|
// collect specific errors for better reporting
|
|
var syncErrors []string
|
|
|
|
currentDomains := make(map[string]*model.Domain)
|
|
for _, domain := range currentDomainsResult.Rows {
|
|
currentDomains[domain.Name.MustGet().String()] = domain
|
|
}
|
|
|
|
m.Logger.Debugw("found existing proxy domains for sync",
|
|
"proxyID", proxyID.String(),
|
|
"currentDomainCount", len(currentDomains),
|
|
"currentDomains", func() []string {
|
|
domains := make([]string, 0, len(currentDomains))
|
|
for name := range currentDomains {
|
|
domains = append(domains, name)
|
|
}
|
|
return domains
|
|
}(),
|
|
)
|
|
|
|
// parse complete YAML structure
|
|
var config ProxyServiceConfigYAML
|
|
if err := yaml.Unmarshal([]byte(proxyConfig.String()), &config); err != nil {
|
|
return fmt.Errorf("failed to parse proxy config YAML for sync: %w", err)
|
|
}
|
|
|
|
// set default values
|
|
m.setProxyConfigDefaults(&config)
|
|
|
|
// get desired domains from config
|
|
desiredDomains := make(map[string]string) // phishing domain -> original domain
|
|
for originalDomain, domainConfig := range config.Hosts {
|
|
if domainConfig == nil {
|
|
continue
|
|
}
|
|
|
|
if domainConfig.To != "" {
|
|
desiredDomains[domainConfig.To] = originalDomain
|
|
}
|
|
}
|
|
|
|
m.Logger.Debugw("parsed desired domains from config",
|
|
"proxyID", proxyID.String(),
|
|
"desiredDomainCount", len(desiredDomains),
|
|
"desiredDomains", func() []string {
|
|
domains := make([]string, 0, len(desiredDomains))
|
|
for name := range desiredDomains {
|
|
domains = append(domains, name)
|
|
}
|
|
return domains
|
|
}(),
|
|
)
|
|
|
|
// delete domains that are no longer needed
|
|
deletedCount := 0
|
|
for phishingDomain, domain := range currentDomains {
|
|
if _, exists := desiredDomains[phishingDomain]; !exists {
|
|
m.Logger.Debugw("domain marked for deletion",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
)
|
|
domainID, err := domain.ID.Get()
|
|
if err == nil {
|
|
err = m.DomainService.DeleteProxyDomain(ctx, session, &domainID)
|
|
if err != nil {
|
|
m.Logger.Warnw("failed to delete removed proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
} else {
|
|
m.Logger.Infow("deleted removed proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
)
|
|
deletedCount++
|
|
}
|
|
} else {
|
|
m.Logger.Warnw("failed to get domain ID for deletion",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
}
|
|
} else {
|
|
m.Logger.Debugw("domain still needed, keeping",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
)
|
|
}
|
|
}
|
|
|
|
// create or update domains that are needed
|
|
createdCount := 0
|
|
updatedCount := 0
|
|
errorCount := 0
|
|
|
|
for phishingDomain, originalDomain := range desiredDomains {
|
|
if existingDomain, exists := currentDomains[phishingDomain]; exists {
|
|
// domain already exists, check if target domain needs updating
|
|
needsUpdate := false
|
|
currentTarget, err := existingDomain.ProxyTargetDomain.Get()
|
|
|
|
if err != nil || currentTarget.String() != originalDomain {
|
|
// update the target domain
|
|
proxyTargetDomain, err := vo.NewOptionalString255(originalDomain)
|
|
if err == nil {
|
|
existingDomain.ProxyTargetDomain.Set(*proxyTargetDomain)
|
|
needsUpdate = true
|
|
} else {
|
|
m.Logger.Warnw("failed to create proxy target domain for update",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
errorCount++
|
|
continue
|
|
}
|
|
}
|
|
|
|
if needsUpdate {
|
|
domainID, err := existingDomain.ID.Get()
|
|
if err == nil {
|
|
err = m.DomainService.UpdateProxyDomain(ctx, session, &domainID, existingDomain)
|
|
if err != nil {
|
|
m.Logger.Warnw("failed to update existing proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
errorCount++
|
|
} else {
|
|
m.Logger.Debugw("updated existing proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
)
|
|
updatedCount++
|
|
}
|
|
} else {
|
|
m.Logger.Warnw("failed to get domain ID for update",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
errorCount++
|
|
}
|
|
}
|
|
} else {
|
|
// create new domain
|
|
domain := &model.Domain{}
|
|
domain.Name.Set(*vo.NewString255Must(phishingDomain))
|
|
domain.Type.Set(*vo.NewString32Must("proxy"))
|
|
domain.ProxyID.Set(*proxyID)
|
|
|
|
proxyTargetDomain, err := vo.NewOptionalString255(originalDomain)
|
|
if err != nil {
|
|
m.Logger.Warnw("failed to create proxy target domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
"originalDomain", originalDomain,
|
|
"error", err,
|
|
)
|
|
errorCount++
|
|
continue
|
|
}
|
|
domain.ProxyTargetDomain.Set(*proxyTargetDomain)
|
|
|
|
domain.HostWebsite.Set(false)
|
|
domain.ManagedTLS.Set(true)
|
|
domain.OwnManagedTLS.Set(false)
|
|
|
|
pageContent, err := vo.NewOptionalString1MB("")
|
|
if err != nil {
|
|
m.Logger.Warnw("failed to create page content for proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
errorCount++
|
|
continue
|
|
}
|
|
domain.PageContent.Set(*pageContent)
|
|
|
|
pageNotFoundContent, err := vo.NewOptionalString1MB("")
|
|
if err != nil {
|
|
m.Logger.Warnw("failed to create page not found content for proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
errorCount++
|
|
continue
|
|
}
|
|
domain.PageNotFoundContent.Set(*pageNotFoundContent)
|
|
|
|
redirectURL, err := vo.NewOptionalString1024("")
|
|
if err != nil {
|
|
errMsg := fmt.Sprintf("failed to create redirect URL for domain '%s': %v", phishingDomain, err)
|
|
m.Logger.Warnw("failed to create redirect URL for proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
syncErrors = append(syncErrors, errMsg)
|
|
errorCount++
|
|
continue
|
|
}
|
|
domain.RedirectURL.Set(*redirectURL)
|
|
|
|
var companyID *uuid.UUID
|
|
if cid, err := proxy.CompanyID.Get(); err == nil {
|
|
companyID = &cid
|
|
domain.CompanyID.Set(*companyID)
|
|
}
|
|
|
|
_, err = m.DomainService.CreateProxyDomain(ctx, session, domain)
|
|
if err != nil {
|
|
errMsg := fmt.Sprintf("failed to create domain '%s': %v", phishingDomain, err)
|
|
m.Logger.Warnw("failed to create new proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
"error", err,
|
|
)
|
|
syncErrors = append(syncErrors, errMsg)
|
|
errorCount++
|
|
} else {
|
|
m.Logger.Debugw("created new proxy domain",
|
|
"proxyID", proxyID.String(),
|
|
"domain", phishingDomain,
|
|
)
|
|
createdCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
m.Logger.Infow("completed proxy domain synchronization",
|
|
"proxyID", proxyID.String(),
|
|
"domainsDeleted", deletedCount,
|
|
"domainsCreated", createdCount,
|
|
"domainsUpdated", updatedCount,
|
|
"errors", errorCount,
|
|
)
|
|
|
|
if errorCount > 0 {
|
|
errorDetails := strings.Join(syncErrors, "; ")
|
|
return fmt.Errorf("proxy domain sync failed with %d errors: %s", errorCount, errorDetails)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteByID deletes a proxy by ID
|
|
func (m *Proxy) DeleteByID(
|
|
ctx context.Context,
|
|
session *model.Session,
|
|
id *uuid.UUID,
|
|
) error {
|
|
ae := NewAuditEvent("Proxy.DeleteByID", session)
|
|
ae.Details["id"] = id.String()
|
|
// check permissions
|
|
isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
|
|
if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
|
|
m.LogAuthError(err)
|
|
return err
|
|
}
|
|
if !isAuthorized {
|
|
m.AuditLogNotAuthorized(ae)
|
|
return errs.ErrAuthorizationFailed
|
|
}
|
|
|
|
// get current proxy before deletion to access its domains
|
|
current, err := m.ProxyRepository.GetByID(ctx, id, &repository.ProxyOption{})
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
m.Logger.Errorw("failed to get proxy for domain cleanup", "error", err)
|
|
return err
|
|
}
|
|
|
|
// delete associated proxy domains
|
|
if current != nil {
|
|
err = m.deleteProxyDomains(ctx, session, id, current)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to delete proxy domains", "error", err)
|
|
// continue with proxy deletion even if domain cleanup fails
|
|
}
|
|
}
|
|
|
|
// remove the relation from campaign templates
|
|
err = m.CampaignTemplateService.RemoveProxiesByProxyID(
|
|
ctx,
|
|
session,
|
|
id,
|
|
)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to remove proxy ID relations from campaign templates", "error", err)
|
|
return err
|
|
}
|
|
|
|
// delete proxy
|
|
err = m.ProxyRepository.DeleteByID(
|
|
ctx,
|
|
id,
|
|
)
|
|
if err != nil {
|
|
m.Logger.Errorw("failed to delete proxy by ID", "error", err)
|
|
return err
|
|
}
|
|
m.AuditLogAuthorized(ae)
|
|
|
|
return nil
|
|
}
|