diff --git a/backend/proxy/surf_impersonate.go b/backend/proxy/surf_impersonate.go index 773b649..06e938f 100644 --- a/backend/proxy/surf_impersonate.go +++ b/backend/proxy/surf_impersonate.go @@ -9,6 +9,7 @@ import ( "github.com/enetx/surf" "github.com/phishingclub/phishingclub/service" + "golang.org/x/net/proxy" ) // browserProfile represents detected browser and platform information @@ -133,9 +134,13 @@ func (m *ProxyHandler) createSurfClient(userAgent string, proxyConfig *service.P // configure proxy if specified if proxyConfig.Proxy != "" { - builder = builder.Proxy("http://" + proxyConfig.Proxy) + proxyURL, err := m.parseProxyURL(proxyConfig.Proxy) + if err != nil { + return nil, err + } + builder = builder.Proxy(proxyURL.String()) m.logger.Debugw("configured surf client with proxy", - "proxy", proxyConfig.Proxy, + "proxy", proxyURL.String(), ) } @@ -179,6 +184,29 @@ func (m *ProxyHandler) createHTTPClientWithImpersonation(req *http.Request, reqC return client, nil } +// parseProxyURL parses and normalizes the proxy URL string +// if the proxy string is just an IP:port, it prepends "http://" +// otherwise it uses the full string to support socks4/socks5 and authentication +func (m *ProxyHandler) parseProxyURL(proxyStr string) (*url.URL, error) { + // check if the string already contains a scheme (http://, https://, socks4://, socks5://) + hasScheme := strings.Contains(proxyStr, "://") + + // check if it contains authentication credentials + hasAuth := strings.Contains(proxyStr, "@") + + // if it has a scheme or auth, use it as-is + if hasScheme || hasAuth { + // if it has auth but no scheme, default to http:// + if hasAuth && !hasScheme { + proxyStr = "http://" + proxyStr + } + return url.Parse(proxyStr) + } + + // otherwise, it's just an IP:port, so prepend http:// + return url.Parse("http://" + proxyStr) +} + // createStandardHTTPClient creates a standard http client without impersonation func (m *ProxyHandler) createStandardHTTPClient(proxyConfig *service.ProxyServiceConfigYAML) (*http.Client, error) { client := &http.Client{ @@ -187,15 +215,42 @@ func (m *ProxyHandler) createStandardHTTPClient(proxyConfig *service.ProxyServic } if proxyConfig.Proxy != "" { - proxyURL, err := url.Parse("http://" + proxyConfig.Proxy) + proxyURL, err := m.parseProxyURL(proxyConfig.Proxy) if err != nil { return nil, err } - client.Transport = &http.Transport{ - Proxy: http.ProxyURL(proxyURL), - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, + + // handle socks5 proxies + if proxyURL.Scheme == "socks5" { + var auth *proxy.Auth + if proxyURL.User != nil { + password, _ := proxyURL.User.Password() + auth = &proxy.Auth{ + User: proxyURL.User.Username(), + Password: password, + } + } + + // create socks5 dialer + dialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct) + if err != nil { + return nil, err + } + + client.Transport = &http.Transport{ + Dial: dialer.Dial, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + } else { + // handle http/https proxies + client.Transport = &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } } } return client, nil diff --git a/backend/service/proxy.go b/backend/service/proxy.go index 46b8757..39263da 100644 --- a/backend/service/proxy.go +++ b/backend/service/proxy.go @@ -1408,6 +1408,11 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err return validate.WrapErrorWithField(err, "proxyConfig") } + // validate forward proxy configuration + if err := m.validateForwardProxy(&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") @@ -1583,6 +1588,34 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err return nil } +// validateForwardProxy validates the forward proxy configuration +func (m *Proxy) validateForwardProxy(config *ProxyServiceConfigYAML) error { + if config.Proxy == "" { + return nil // proxy is optional + } + + // check if it contains socks4 scheme + if strings.HasPrefix(config.Proxy, "socks4://") { + return errors.New("socks4 proxies are not supported. please use socks5:// instead") + } + + // validate that it can be parsed as a URL if it has a scheme + if strings.Contains(config.Proxy, "://") { + parsedURL, err := url.Parse(config.Proxy) + if err != nil { + return errors.New("invalid proxy URL format: " + err.Error()) + } + + // validate supported schemes + scheme := parsedURL.Scheme + if scheme != "http" && scheme != "https" && scheme != "socks5" { + return errors.New(fmt.Sprintf("unsupported proxy scheme '%s'. supported schemes: http, https, socks5", scheme)) + } + } + + 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 diff --git a/frontend/src/routes/proxy/+page.svelte b/frontend/src/routes/proxy/+page.svelte index 3304136..73cdd9d 100644 --- a/frontend/src/routes/proxy/+page.svelte +++ b/frontend/src/routes/proxy/+page.svelte @@ -67,7 +67,15 @@ let isLoadingIPAllowList = false; const currentExample = `version: "0.0" -proxy: "My Proxy Campaign" + +# optional: forward proxy for outbound requests +# if just ip:port is provided, http:// is automatically prepended +# supported formats: +# proxy: "192.168.1.100:8080" # http proxy (ip:port) +# proxy: "http://192.168.1.100:8080" # http proxy with scheme +# proxy: "socks5://192.168.1.100:1080" # socks5 proxy +# proxy: "socks5://user:pass@192.168.1.100:1080" # socks5 with auth +# proxy: "http://user:pass@192.168.1.100:8080" # http with auth # global TLS configuration (applies to all hosts unless overridden) global: