fix skip campaign template domain when mitm domain is first page

fix align extract IP on phishing server

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-10-11 15:42:58 +02:00
parent c02dbf8e94
commit c64f5a7dec
6 changed files with 500 additions and 65 deletions
+4 -4
View File
@@ -675,7 +675,7 @@ func (s *Server) checkAndServePhishingPage(
return false, fmt.Errorf("failed to get campaign template: %s", err)
}
// check that the requesters IP is allow listed
ip := c.ClientIP()
ip := utils.ExtractClientIP(c.Request)
servedByIPFilter, err := s.checkIPFilter(c, ip, campaign, domain, &campaignID)
if err != nil {
return false, err
@@ -868,7 +868,7 @@ func (s *Server) checkAndServePhishingPage(
}
newEventID := uuid.New()
campaignID := campaign.ID.MustGet()
clientIP := vo.NewOptionalString64Must(c.ClientIP())
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
submittedData := vo.NewEmptyOptionalString1MB()
if campaign.SaveSubmittedData.MustGet() {
@@ -1040,7 +1040,7 @@ func (s *Server) checkAndServePhishingPage(
eventName = data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED
}
eventID := cache.EventIDByName[eventName]
clientIP := vo.NewOptionalString64Must(c.ClientIP())
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
var visitEvent *model.CampaignEvent
if !campaign.IsAnonymous.MustGet() {
@@ -1262,7 +1262,7 @@ func (s *Server) checkAndServePhishingPage(
eventName = data.EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED
}
eventID := cache.EventIDByName[eventName]
clientIP := vo.NewOptionalString64Must(c.ClientIP())
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
var visitEvent *model.CampaignEvent
if !campaign.IsAnonymous.MustGet() {
+143 -10
View File
@@ -33,6 +33,7 @@ import (
"github.com/phishingclub/phishingclub/vo"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
"gorm.io/gorm"
)
/*
@@ -316,6 +317,9 @@ func (m *ProxyHandler) resolveSessionContext(req *http.Request, reqCtx *RequestC
reqCtx.SessionID = newSession.ID
reqCtx.SessionCreated = true
reqCtx.Session = newSession
// register page visit event for MITM landing
m.registerPageVisitEvent(req, newSession)
} else {
// load existing session
sessionVal, exists := m.sessions.Load(reqCtx.SessionID)
@@ -1920,15 +1924,7 @@ func (m *ProxyHandler) createCampaignSubmitEvent(session *ProxySession, captured
submitDataEventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA]
eventID := uuid.New()
// get real client ip
clientIP := strings.SplitN(req.RemoteAddr, ":", 2)[0]
proxyHeaders := []string{"X-Forwarded-For", "X-Real-IP", "X-Client-IP", "Connecting-IP", "True-Client-IP", "Client-IP"}
for _, header := range proxyHeaders {
if headerValue := req.Header.Get(header); headerValue != "" {
clientIP = strings.SplitN(headerValue, ":", 2)[0]
break
}
}
clientIP := utils.ExtractClientIP(req)
event := &model.CampaignEvent{
ID: &eventID,
@@ -2216,6 +2212,7 @@ func (m *ProxyHandler) createDenyResponse(req *http.Request, reqCtx *RequestCont
}
return m.createStatusResponse(404) // fallback
}
// createRedirectResponse creates a redirect response
@@ -2232,7 +2229,143 @@ func (m *ProxyHandler) createRedirectResponse(url string) *http.Response {
func (m *ProxyHandler) createStatusResponse(statusCode int) *http.Response {
return &http.Response{
StatusCode: statusCode,
Header: make(map[string][]string),
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader("")),
}
}
// registerPageVisitEvent registers a page visit event when a new MITM session is created
func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *ProxySession) {
if session.CampaignRecipientID == nil || session.CampaignID == nil || session.RecipientID == nil {
return
}
ctx := req.Context()
// get campaign template to determine page type
templateID, err := session.Campaign.TemplateID.Get()
if err != nil {
m.logger.Errorw("failed to get template ID for page visit event", "error", err)
return
}
cTemplate, err := m.CampaignTemplateRepository.GetByID(ctx, &templateID, &repository.CampaignTemplateOption{})
if err != nil {
m.logger.Errorw("failed to get campaign template for page visit event", "error", err, "templateID", templateID)
return
}
// determine which page type this is
currentPageType := m.getCurrentPageType(req, cTemplate, session)
// determine event name based on page type
var eventName string
switch currentPageType {
case data.PAGE_TYPE_BEFORE:
eventName = data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED
case data.PAGE_TYPE_LANDING:
eventName = data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED
case data.PAGE_TYPE_AFTER:
eventName = data.EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED
default:
eventName = data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED
}
// get event ID
eventID, exists := cache.EventIDByName[eventName]
if !exists {
m.logger.Errorw("unknown event name", "eventName", eventName)
return
}
// create visit event
visitEventID := uuid.New()
clientIP := utils.ExtractClientIP(req)
clientIPVO := vo.NewOptionalString64Must(clientIP)
userAgent := vo.NewOptionalString255Must(utils.Substring(req.UserAgent(), 0, 255))
var visitEvent *model.CampaignEvent
if !session.Campaign.IsAnonymous.MustGet() {
visitEvent = &model.CampaignEvent{
ID: &visitEventID,
CampaignID: session.CampaignID,
RecipientID: session.RecipientID,
IP: clientIPVO,
UserAgent: userAgent,
EventID: eventID,
Data: vo.NewEmptyOptionalString1MB(),
}
} else {
visitEvent = &model.CampaignEvent{
ID: &visitEventID,
CampaignID: session.CampaignID,
RecipientID: nil,
IP: vo.NewEmptyOptionalString64(),
UserAgent: vo.NewEmptyOptionalString255(),
EventID: eventID,
Data: vo.NewEmptyOptionalString1MB(),
}
}
// save the visit event
err = m.CampaignRepository.SaveEvent(ctx, visitEvent)
if err != nil {
m.logger.Errorw("failed to save MITM page visit event",
"error", err,
"campaignRecipientID", session.CampaignRecipientID.String(),
"pageType", currentPageType,
)
return
}
// update most notable event for recipient if needed
campaignRecipient, err := m.CampaignRecipientRepository.GetByID(ctx, session.CampaignRecipientID, &repository.CampaignRecipientOption{})
if err != nil {
m.logger.Errorw("failed to get campaign recipient for notable event update", "error", err)
return
}
currentNotableEventID, _ := campaignRecipient.NotableEventID.Get()
if cache.IsMoreNotableCampaignRecipientEventID(&currentNotableEventID, eventID) {
campaignRecipient.NotableEventID.Set(*eventID)
err := m.CampaignRecipientRepository.UpdateByID(ctx, session.CampaignRecipientID, campaignRecipient)
if err != nil {
m.logger.Errorw("failed to update notable event for MITM visit",
"campaignRecipientID", session.CampaignRecipientID.String(),
"eventID", eventID.String(),
"error", err,
)
}
}
// handle webhook for MITM page visit
webhookID, err := m.CampaignRepository.GetWebhookIDByCampaignID(ctx, session.CampaignID)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
m.logger.Errorw("failed to get webhook id by campaign id for MITM proxy",
"campaignID", session.CampaignID.String(),
"error", err,
)
}
if webhookID != nil && currentPageType != data.PAGE_TYPE_DONE {
err = m.CampaignService.HandleWebhook(
ctx,
webhookID,
session.CampaignID,
session.RecipientID,
eventName,
)
if err != nil {
m.logger.Errorw("failed to handle webhook for MITM page visit",
"error", err,
"campaignRecipientID", session.CampaignRecipientID.String(),
)
}
}
m.logger.Debugw("registered MITM page visit event",
"campaignRecipientID", session.CampaignRecipientID.String(),
"pageType", currentPageType,
"eventName", eventName,
)
}
+38 -1
View File
@@ -528,6 +528,20 @@ func (a *APISender) Send(
domain *model.Domain,
mailTmpl *template.Template,
email *model.Email,
) error {
return a.SendWithCustomURL(ctx, session, cTemplate, campaignRecipient, domain, mailTmpl, email, "")
}
// SendWithCustomURL sends an API request with optional custom campaign URL
func (a *APISender) SendWithCustomURL(
ctx context.Context,
session *model.Session,
cTemplate *model.CampaignTemplate,
campaignRecipient *model.CampaignRecipient,
domain *model.Domain,
mailTmpl *template.Template,
email *model.Email,
customCampaignURL string,
) error {
// get sender details
apiSenderID, err := cTemplate.APISenderID.Get()
@@ -550,19 +564,21 @@ func (a *APISender) Send(
if apiSender == nil {
return errors.New("api sender did not load")
}
domainName := domain.Name.MustGet()
urlIdentifier := cTemplate.URLIdentifier
if urlIdentifier == nil {
return errors.New("url identifier MUST be loaded in campaign template")
}
urlPath := cTemplate.URLPath.MustGet().String()
url, headers, body, err := a.buildRequest(
url, headers, body, err := a.buildRequestWithCustomURL(
apiSender,
domainName.String(),
urlIdentifier.Name.MustGet(),
urlPath,
campaignRecipient,
email,
customCampaignURL,
)
resp, respBodyClose, err := a.sendRequest(
context.Background(),
@@ -717,6 +733,19 @@ func (a *APISender) buildRequest(
urlPath string,
campaignRecipient *model.CampaignRecipient,
email *model.Email, // todo is this superfluous? it should be in the campaign recipient?
) (*apiRequestURL, []*model.HTTPHeader, *apiRequestBody, error) {
return a.buildRequestWithCustomURL(apiSender, domainName, urlKey, urlPath, campaignRecipient, email, "")
}
// buildRequestWithCustomURL builds an API request with optional custom campaign URL
func (a *APISender) buildRequestWithCustomURL(
apiSender *model.APISender,
domainName string,
urlKey string,
urlPath string,
campaignRecipient *model.CampaignRecipient,
email *model.Email,
customCampaignURL string,
) (*apiRequestURL, []*model.HTTPHeader, *apiRequestBody, error) {
// setup headers
apiReqHeaders, err := a.buildHeader(apiSender)
@@ -739,6 +768,14 @@ func (a *APISender) buildRequest(
email,
apiSender,
)
// override campaign URL if custom one is provided
if customCampaignURL != "" {
templateURL := fmt.Sprintf("https://%s%s?%s=%s", domainName, urlPath, urlKey, campaignRecipient.ID.MustGet().String())
if customCampaignURL != templateURL {
(*t)["URL"] = customCampaignURL
}
}
var apiURL bytes.Buffer
if err := urlTemplate.Execute(&apiURL, t); err != nil {
return nil, nil, nil, fmt.Errorf("failed to execute url template: %s", err)
+248 -50
View File
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"math/rand"
"net/url"
"os"
"path/filepath"
"slices"
@@ -16,6 +17,7 @@ import (
"time"
go_errors "github.com/go-errors/errors"
"gopkg.in/yaml.v3"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -28,6 +30,7 @@ import (
"github.com/phishingclub/phishingclub/log"
"github.com/phishingclub/phishingclub/model"
"github.com/phishingclub/phishingclub/repository"
"github.com/phishingclub/phishingclub/utils"
"github.com/phishingclub/phishingclub/validate"
"github.com/phishingclub/phishingclub/vo"
"github.com/wneessen/go-mail"
@@ -1105,7 +1108,7 @@ func (c *Campaign) SaveTrackingPixelLoaded(
Data: vo.NewOptionalString1MBMust(""),
}
} else {
ip := vo.NewOptionalString64Must(ctx.ClientIP())
ip := vo.NewOptionalString64Must(utils.ExtractClientIP(ctx.Request))
ua := ctx.Request.UserAgent()
if len(ua) > 255 {
ua = strings.TrimSpace(ua[:255])
@@ -1601,9 +1604,11 @@ func (c *Campaign) sendCampaignMessages(
session,
&templateID,
&repository.CampaignTemplateOption{
WithDomain: true,
WithSMTPConfiguration: true,
WithIdentifier: true,
WithDomain: true,
WithSMTPConfiguration: true,
WithIdentifier: true,
WithBeforeLandingProxy: true,
WithLandingProxy: true,
},
)
if err != nil {
@@ -1741,7 +1746,16 @@ func (c *Campaign) sendCampaignMessages(
fmt.Errorf("failed to update last attempted at: %s \nThis is critical for sending, aborting...", err),
)
}
err = c.APISenderService.Send(
// generate custom campaign URL if first page is MITM
recipientID := campaignRecipient.ID.MustGet()
customCampaignURL, urlErr := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
if urlErr != nil {
c.Logger.Errorw("failed to get campaign url for API sender", "error", urlErr)
customCampaignURL = ""
}
// send via API with custom URL (domain and template stay the same for assets)
err = c.APISenderService.SendWithCustomURL(
ctx,
session,
cTemplate,
@@ -1749,6 +1763,7 @@ func (c *Campaign) sendCampaignMessages(
domain,
mailTmpl,
email,
customCampaignURL,
)
if err != nil {
c.Logger.Errorw("failed to send message via. API", "error", err)
@@ -1945,17 +1960,28 @@ func (c *Campaign) sendCampaignMessages(
}
}
m.Subject(email.MailHeaderSubject.MustGet().String())
domainName, err := domain.Name.Get()
if err != nil {
c.Logger.Errorw("failed to get domain name", "error", err)
return errs.Wrap(err)
}
urlIdentifier := cTemplate.URLIdentifier
if urlIdentifier == nil {
c.Logger.Error("url identifier is MUST be loaded for the campaign template")
return fmt.Errorf("url identifier is MUST be loaded for the campaign template")
}
// get template domain for assets and tracking pixel
domainName, err := domain.Name.Get()
if err != nil {
c.Logger.Errorw("failed to get domain name", "error", err)
return errs.Wrap(err)
}
urlPath := cTemplate.URLPath.MustGet().String()
// generate custom campaign URL if first page is MITM
recipientID := campaignRecipient.ID.MustGet()
customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
if err != nil {
c.Logger.Errorw("failed to get campaign url", "error", err)
return errs.Wrap(err)
}
t := c.TemplateService.CreateMail(
domainName.String(),
urlIdentifier.Name.MustGet(),
@@ -1964,6 +1990,12 @@ func (c *Campaign) sendCampaignMessages(
email,
nil,
)
// override campaign URL if it's different from template domain URL
templateURL := fmt.Sprintf("https://%s%s?%s=%s", domainName.String(), urlPath, urlIdentifier.Name.MustGet(), recipientID.String())
if customCampaignURL != templateURL {
(*t)["URL"] = customCampaignURL
}
var bodyBuffer bytes.Buffer
err = mailTmpl.Execute(&bodyBuffer, t)
if err != nil {
@@ -2004,13 +2036,22 @@ func (c *Campaign) sendCampaignMessages(
attachmentAsEmail.Content = nullable.NewNullableWithValue(
*vo.NewUnsafeOptionalString1MB(string(attachmentContent)),
)
attachmentStr, err := c.TemplateService.CreateMailBody(
// generate custom campaign URL for attachment
recipientID := campaignRecipient.ID.MustGet()
customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
if err != nil {
c.Logger.Errorw("failed to get campaign url for attachment", "error", err)
return errs.Wrap(err)
}
attachmentStr, err := c.TemplateService.CreateMailBodyWithCustomURL(
urlIdentifier.Name.MustGet(),
urlPath,
domain,
campaignRecipient,
&attachmentAsEmail,
nil,
customCampaignURL,
)
if err != nil {
return errs.Wrap(fmt.Errorf("failed to setup attachment with embedded content: %s", err))
@@ -2589,7 +2630,9 @@ func (c *Campaign) GetCampaignEmailBody(
session,
&templateID,
&repository.CampaignTemplateOption{
WithIdentifier: true,
WithIdentifier: true,
WithBeforeLandingProxy: true,
WithLandingProxy: true,
},
)
emailID, err := cTemplate.EmailID.Get()
@@ -2617,9 +2660,15 @@ func (c *Campaign) GetCampaignEmailBody(
c.Logger.Errorw("failed to get message by id", "error", err)
return "", errs.Wrap(err)
}
urlIdentifier := cTemplate.URLIdentifier
if urlIdentifier == nil {
return "", errors.New("url identifier is nil")
}
// get template domain for assets and tracking pixel
domainID, err := cTemplate.DomainID.Get()
if err != nil {
c.Logger.Infow("failed domain from template - template ID: %s", templateID)
c.Logger.Infow("failed domain from template - template ID", "templateID", templateID)
return "", errs.NewValidationError(
errors.New("Campaign template has no domain"),
)
@@ -2634,24 +2683,30 @@ func (c *Campaign) GetCampaignEmailBody(
c.Logger.Errorw("failed to get domain by id", "error", err)
return "", errs.Wrap(err)
}
urlIdentifier := cTemplate.URLIdentifier
if urlIdentifier == nil {
return "", errors.New("url identifier is nil")
}
urlPath := cTemplate.URLPath.MustGet().String()
// generate custom campaign URL if first page is MITM
customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, campaignRecipientID)
if err != nil {
c.Logger.Errorw("failed to get campaign url", "error", err)
return "", errs.Wrap(err)
}
// no audit on read
return c.TemplateService.CreateMailBody(
return c.TemplateService.CreateMailBodyWithCustomURL(
urlIdentifier.Name.MustGet(),
urlPath,
domain,
campaignRecipient,
email,
nil,
customCampaignURL,
)
}
// GetLandingPageURLByCampaignRecipientID returns the URL for a campaign recipient
// GetLandingPageURLByCampaignRecipientID generates the lure URL for a campaign recipient.
// if the first page in the campaign flow is a mitm proxy, the url goes directly to the
// mitm domain instead of redirecting through the template domain.
func (c *Campaign) GetLandingPageURLByCampaignRecipientID(
ctx context.Context,
session *model.Session,
@@ -2701,31 +2756,139 @@ func (c *Campaign) GetLandingPageURLByCampaignRecipientID(
WithIdentifier: true,
},
)
domainID, err := cTemplate.DomainID.Get()
if err != nil {
c.Logger.Infow("failed email from template - template ID", "templateID", templateID)
return "", errs.NewValidationError(
errors.New("Campaign template has no email"),
)
}
domain, err := c.DomainService.GetByID(
ctx,
session,
&domainID,
&repository.DomainOption{},
)
if err != nil {
c.Logger.Errorw("failed to get domain by id", err)
return "", errs.Wrap(err)
}
urlPath := cTemplate.URLPath.MustGet().String()
baseURL := "https://" + domain.Name.MustGet().String()
// determine if we should use mitm domain for first page
var baseURL string
var urlPath string
idIdentifier := cTemplate.URLIdentifier.Name.MustGet()
url := fmt.Sprintf("%s%s?%s=%s", baseURL, urlPath, idIdentifier, campaignRecipientID.String())
// check if first page is a mitm proxy
firstPageProxy := c.getFirstPageProxy(cTemplate)
if firstPageProxy != nil {
// get the phishing domain for this proxy
phishingDomain, err := c.getPhishingDomainForProxy(ctx, firstPageProxy)
if err != nil {
c.Logger.Errorw("failed to get phishing domain for first page proxy", "error", err)
// fallback to template domain
firstPageProxy = nil
} else {
// use phishing domain directly
startURL, err := firstPageProxy.StartURL.Get()
if err != nil {
c.Logger.Errorw("failed to get start url from first page proxy", "error", err)
return "", errs.Wrap(err)
}
parsedStartURL, err := url.Parse(startURL.String())
if err != nil {
c.Logger.Errorw("failed to parse start url from first page proxy", "error", err)
return "", errs.Wrap(err)
}
baseURL = "https://" + phishingDomain
urlPath = parsedStartURL.Path
}
}
if firstPageProxy == nil {
// use template domain (current behavior)
domainID, err := cTemplate.DomainID.Get()
if err != nil {
c.Logger.Infow("failed email from template - template ID", "templateID", templateID)
return "", errs.NewValidationError(
errors.New("Campaign template has no email"),
)
}
domain, err := c.DomainService.GetByID(
ctx,
session,
&domainID,
&repository.DomainOption{},
)
if err != nil {
c.Logger.Errorw("failed to get domain by id", err)
return "", errs.Wrap(err)
}
urlPath = cTemplate.URLPath.MustGet().String()
baseURL = "https://" + domain.Name.MustGet().String()
}
// build final url
separator := "?"
if strings.Contains(baseURL, "?") {
separator = "&"
}
url := fmt.Sprintf("%s%s%s%s=%s", baseURL, urlPath, separator, idIdentifier, campaignRecipientID.String())
// no audit on read
return url, nil
}
// getFirstPageProxy returns the proxy for the first page in the campaign flow
// returns nil if the first page is not a proxy
func (c *Campaign) getFirstPageProxy(template *model.CampaignTemplate) *model.Proxy {
if template.BeforeLandingPageID != nil {
return nil
}
if template.BeforeLandingProxyID != nil {
return template.BeforeLandingProxy
}
if template.LandingPageID != nil {
return nil
}
if template.LandingProxyID != nil {
return template.LandingProxy
}
if template.AfterLandingPageID != nil {
return nil
}
if template.AfterLandingProxyID != nil {
return template.AfterLandingProxy
}
return nil
}
// getPhishingDomainForProxy finds the phishing domain that maps to the proxy's start url
func (c *Campaign) getPhishingDomainForProxy(ctx context.Context, proxy *model.Proxy) (string, error) {
startURL, err := proxy.StartURL.Get()
if err != nil {
return "", fmt.Errorf("failed to get start url from proxy: %w", err)
}
proxyConfig, err := proxy.ProxyConfig.Get()
if err != nil {
return "", fmt.Errorf("failed to get proxy config: %w", err)
}
// parse the proxy configuration to find domain mappings
var rawConfig map[string]interface{}
err = yaml.Unmarshal([]byte(proxyConfig.String()), &rawConfig)
if err != nil {
return "", fmt.Errorf("failed to parse proxy config yaml: %w", err)
}
// parse the start URL to get the target domain
parsedStartURL, err := url.Parse(startURL.String())
if err != nil {
return "", fmt.Errorf("failed to parse start url: %w", err)
}
startDomain := parsedStartURL.Host
// find the phishing domain mapping for the start URL domain
for originalHost, domainData := range rawConfig {
if originalHost == "proxy" || originalHost == "global" {
continue
}
if originalHost == startDomain {
if domainMap, ok := domainData.(map[string]interface{}); ok {
if to, exists := domainMap["to"]; exists {
if toStr, ok := to.(string); ok {
return toStr, nil
}
}
}
}
}
return "", fmt.Errorf("no phishing domain mapping found for start url domain: %s", startDomain)
}
// SetSentAtByCampaignRecipientID sets the sent at time for a recipient
func (c *Campaign) SetSentAtByCampaignRecipientID(
ctx context.Context,
@@ -3086,9 +3249,11 @@ func (c *Campaign) sendSingleCampaignMessage(
session,
&templateID,
&repository.CampaignTemplateOption{
WithDomain: true,
WithSMTPConfiguration: true,
WithIdentifier: true,
WithDomain: true,
WithSMTPConfiguration: true,
WithIdentifier: true,
WithBeforeLandingProxy: true,
WithLandingProxy: true,
},
)
if err != nil {
@@ -3152,8 +3317,16 @@ func (c *Campaign) sendSingleCampaignMessage(
}
if isAPISenderCampaign {
// send via API
err = c.APISenderService.Send(
// generate custom campaign URL if first page is MITM
recipientID := campaignRecipient.ID.MustGet()
customCampaignURL, urlErr := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
if urlErr != nil {
c.Logger.Errorw("failed to get campaign url for API sender", "error", urlErr)
customCampaignURL = ""
}
// send via API with custom URL (domain and template stay the same for assets)
err = c.APISenderService.SendWithCustomURL(
ctx,
session,
cTemplate,
@@ -3161,6 +3334,7 @@ func (c *Campaign) sendSingleCampaignMessage(
domain,
mailTmpl,
email,
customCampaignURL,
)
} else {
// send via SMTP
@@ -3291,17 +3465,26 @@ func (c *Campaign) sendSingleEmailSMTP(
m.Subject(email.MailHeaderSubject.MustGet().String())
// setup template variables
domainName, err := domain.Name.Get()
if err != nil {
return errs.Wrap(err)
}
urlIdentifier := cTemplate.URLIdentifier
if urlIdentifier == nil {
return errors.New("url identifier must be loaded for the campaign template")
}
// get template domain for assets and tracking pixel
domainName, err := domain.Name.Get()
if err != nil {
return errs.Wrap(err)
}
urlPath := cTemplate.URLPath.MustGet().String()
// generate custom campaign URL if first page is MITM
recipientID := campaignRecipient.ID.MustGet()
customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
if err != nil {
c.Logger.Errorw("failed to get campaign url", "error", err)
return errs.Wrap(err)
}
t := c.TemplateService.CreateMail(
domainName.String(),
urlIdentifier.Name.MustGet(),
@@ -3311,6 +3494,12 @@ func (c *Campaign) sendSingleEmailSMTP(
nil,
)
// override campaign URL if it's different from template domain URL
templateURL := fmt.Sprintf("https://%s%s?%s=%s", domainName.String(), urlPath, urlIdentifier.Name.MustGet(), recipientID.String())
if customCampaignURL != templateURL {
(*t)["URL"] = customCampaignURL
}
var bodyBuffer bytes.Buffer
err = mailTmpl.Execute(&bodyBuffer, t)
if err != nil {
@@ -3351,13 +3540,22 @@ func (c *Campaign) sendSingleEmailSMTP(
attachmentAsEmail.Content = nullable.NewNullableWithValue(
*vo.NewUnsafeOptionalString1MB(string(attachmentContent)),
)
attachmentStr, err := c.TemplateService.CreateMailBody(
// generate custom campaign URL for attachment
recipientID := campaignRecipient.ID.MustGet()
customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
if err != nil {
c.Logger.Errorw("failed to get campaign url for attachment", "error", err)
return errs.Wrap(err)
}
attachmentStr, err := c.TemplateService.CreateMailBodyWithCustomURL(
urlIdentifier.Name.MustGet(),
urlPath,
domain,
campaignRecipient,
&attachmentAsEmail,
nil,
customCampaignURL,
)
if err != nil {
return errs.Wrap(fmt.Errorf("failed to setup attachment with embedded content: %s", err))
+26
View File
@@ -208,6 +208,27 @@ func (t *Template) CreateMailBody(
campaignRecipient *model.CampaignRecipient,
email *model.Email,
apiSender *model.APISender, // can be nil
) (string, error) {
return t.CreateMailBodyWithCustomURL(
urlIdentifier,
urlPath,
domain,
campaignRecipient,
email,
apiSender,
"", // empty customURL means use template domain
)
}
// CreateMailBodyWithCustomURL returns a rendered mail body to string with optional custom campaign URL
func (t *Template) CreateMailBodyWithCustomURL(
urlIdentifier string,
urlPath string,
domain *model.Domain,
campaignRecipient *model.CampaignRecipient,
email *model.Email,
apiSender *model.APISender, // can be nil
customCampaignURL string, // if provided, overrides the default campaign URL
) (string, error) {
mailData := t.CreateMail(
domain.Name.MustGet().String(),
@@ -217,6 +238,11 @@ func (t *Template) CreateMailBody(
email,
apiSender,
)
// override campaign URL if custom one is provided
if customCampaignURL != "" {
(*mailData)["URL"] = customCampaignURL
}
mailContentTemplate := template.New("mailContent")
mailContentTemplate = mailContentTemplate.Funcs(TemplateFuncs())
content, err := email.Content.Get()
+41
View File
@@ -0,0 +1,41 @@
package utils
import (
"net/http"
"strings"
)
// ExtractClientIP extracts the real client IP from an HTTP request,
// checking common proxy headers in order of preference.
// This provides consistent IP extraction across the application.
func ExtractClientIP(req *http.Request) string {
// start with direct connection IP
clientIP := req.RemoteAddr
// check common proxy headers in order of preference
proxyHeaders := []string{
"CF-Connecting-IP", // cloudflare - most reliable for cloudflare setups
"True-Client-IP", // cloudflare enterprise
"X-Forwarded-For", // most common proxy header
"X-Real-IP", // nginx standard
"X-Client-IP", // some proxies
"Connecting-IP", // some cdns
"Client-IP", // some load balancers
}
for _, header := range proxyHeaders {
if headerValue := req.Header.Get(header); headerValue != "" {
// take first IP if comma-separated list
ip := strings.SplitN(headerValue, ",", 2)[0]
ip = strings.TrimSpace(ip)
// use first non-empty value found
if ip != "" {
clientIP = ip
break
}
}
}
return clientIP
}