diff --git a/common/config/conf_domain.go b/common/config/conf_domain.go index 5443efce..0a80e1c3 100644 --- a/common/config/conf_domain.go +++ b/common/config/conf_domain.go @@ -50,6 +50,7 @@ func NewDefaultDomainConfig() DomainRepoConfig { DefaultLanguage: "en-US,en", UserAgent: "matrix-media-repo", OEmbed: false, + ProxyURL: "", }, Thumbnails: ThumbnailsConfig{ MaxSourceBytes: 10485760, // 10mb diff --git a/common/config/conf_main.go b/common/config/conf_main.go index 8b7e3ac6..fb5e037a 100644 --- a/common/config/conf_main.go +++ b/common/config/conf_main.go @@ -80,6 +80,7 @@ func NewDefaultMainConfig() MainRepoConfig { DefaultLanguage: "en-US,en", UserAgent: "matrix-media-repo", OEmbed: false, + ProxyURL: "", }, NumWorkers: 10, ExpireDays: 0, diff --git a/common/config/models_domain.go b/common/config/models_domain.go index 2cd87755..dd01f7ac 100644 --- a/common/config/models_domain.go +++ b/common/config/models_domain.go @@ -71,6 +71,7 @@ type UrlPreviewsConfig struct { DefaultLanguage string `yaml:"defaultLanguage"` UserAgent string `yaml:"userAgent"` OEmbed bool `yaml:"oEmbed"` + ProxyURL string `yaml:"proxyUrl"` } type IdenticonsConfig struct { diff --git a/config.sample.yaml b/config.sample.yaml index 5e037828..12b1aa26 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -407,6 +407,13 @@ urlPreviews: # Defaults to disabled. oEmbed: false + # When provided, preview webpage requests will be routed through the specified proxy. + # This can be useful to separate preview traffic from Matrix traffic in your network + # for more granular control. Currently only SOCKS5 proxies are supported. The URL + # should be in the format "socks5://127.0.0.1:1080" + # Defaults to a blank string. + proxyUrl: "" + # The thumbnail configuration for the media repository. thumbnails: # The maximum number of bytes an image can be before the thumbnailer refuses. diff --git a/url_previewing/u/http.go b/url_previewing/u/http.go index 04e757c4..9f5d577a 100644 --- a/url_previewing/u/http.go +++ b/url_previewing/u/http.go @@ -4,10 +4,12 @@ import ( "context" "crypto/tls" "errors" + "fmt" "io" "mime" "net" "net/http" + "net/url" "strconv" "time" @@ -17,8 +19,17 @@ import ( "github.com/t2bot/matrix-media-repo/url_previewing/m" "github.com/t2bot/matrix-media-repo/util" "github.com/t2bot/matrix-media-repo/util/readers" + "golang.org/x/net/proxy" ) +func getProxy(dialer *net.Dialer, ctx rcontext.RequestContext) (proxy.Dialer, error) { + url, err := url.Parse(ctx.Config.UrlPreviews.ProxyURL) + if err != nil { + return nil, fmt.Errorf("error parsing proxy url: %w", err) + } + return proxy.FromURL(url, dialer) +} + func doHttpGet(urlPayload *m.UrlPayload, languageHeader string, ctx rcontext.RequestContext) (*http.Response, error) { var client *http.Client @@ -37,6 +48,18 @@ func doHttpGet(urlPayload *m.UrlPayload, languageHeader string, ctx rcontext.Req return nil, err } + if ctx.Config.UrlPreviews.ProxyURL != "" { + proxyDialer, err := getProxy(dialer, ctx) + if err != nil { + return nil, fmt.Errorf("error creating proxy: %w", err) + } + if contextDialer, ok := proxyDialer.(proxy.ContextDialer); ok { + return contextDialer.DialContext(ctx2, network, net.JoinHostPort(safeIp.String(), safePort)) + } else { + return nil, errors.New("failed proxy type assertion to ContextDialer") + } + } + return dialer.DialContext(ctx2, network, net.JoinHostPort(safeIp.String(), safePort)) } @@ -48,9 +71,19 @@ func doHttpGet(urlPayload *m.UrlPayload, languageHeader string, ctx rcontext.Req TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // Based on https://github.com/matrix-org/gomatrixserverlib/blob/51152a681e69a832efcd934b60080b92bc98b286/client.go#L74-L90 DialTLSContext: func(ctx2 context.Context, network, addr string) (net.Conn, error) { - rawconn, err := net.Dial(network, addr) - if err != nil { - return nil, err + var rawconn net.Conn + var connErr error + if ctx.Config.UrlPreviews.ProxyURL != "" { + proxyDialer, err := getProxy(dialer, ctx) + if err != nil { + return nil, fmt.Errorf("error creating proxy: %w", err) + } + rawconn, connErr = proxyDialer.Dial(network, addr) + } else { + rawconn, connErr = net.Dial(network, addr) + } + if connErr != nil { + return nil, connErr } // Wrap a raw connection ourselves since tls.Dial defaults the SNI conn := tls.Client(rawconn, &tls.Config{