diff --git a/proxy/image_proxy.go b/proxy/image_proxy.go new file mode 100644 index 00000000..56109599 --- /dev/null +++ b/proxy/image_proxy.go @@ -0,0 +1,80 @@ +// Copyright 2020 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package proxy // import "miniflux.app/proxy" + +import ( + "strings" + + "miniflux.app/config" + "miniflux.app/url" + + "github.com/PuerkitoBio/goquery" + "github.com/gorilla/mux" +) + +// ImageProxyRewriter replaces image URLs with internal proxy URLs. +func ImageProxyRewriter(router *mux.Router, data string) string { + proxyImages := config.Opts.ProxyImages() + if proxyImages == "none" { + return data + } + + doc, err := goquery.NewDocumentFromReader(strings.NewReader(data)) + if err != nil { + return data + } + + doc.Find("img").Each(func(i int, img *goquery.Selection) { + if srcAttr, ok := img.Attr("src"); ok { + if proxyImages == "all" || !url.IsHTTPS(srcAttr) { + img.SetAttr("src", ProxifyURL(router, srcAttr)) + } + } + + if srcsetAttr, ok := img.Attr("srcset"); ok { + if proxyImages == "all" || !url.IsHTTPS(srcsetAttr) { + proxifySourceSet(img, router, srcsetAttr) + } + } + }) + + doc.Find("picture source").Each(func(i int, sourceElement *goquery.Selection) { + if srcsetAttr, ok := sourceElement.Attr("srcset"); ok { + if proxyImages == "all" || !url.IsHTTPS(srcsetAttr) { + proxifySourceSet(sourceElement, router, srcsetAttr) + } + } + }) + + output, err := doc.Find("body").First().Html() + if err != nil { + return data + } + + return output +} + +func proxifySourceSet(element *goquery.Selection, router *mux.Router, attributeValue string) { + var proxifiedSources []string + + for _, source := range strings.Split(attributeValue, ",") { + parts := strings.Split(strings.TrimSpace(source), " ") + nbParts := len(parts) + + if nbParts > 0 { + source = ProxifyURL(router, parts[0]) + + if nbParts > 1 { + source += " " + parts[1] + } + + proxifiedSources = append(proxifiedSources, source) + } + } + + if len(proxifiedSources) > 0 { + element.SetAttr("srcset", strings.Join(proxifiedSources, ", ")) + } +} diff --git a/proxy/image_proxy_test.go b/proxy/image_proxy_test.go new file mode 100644 index 00000000..d336a7e5 --- /dev/null +++ b/proxy/image_proxy_test.go @@ -0,0 +1,244 @@ +// Copyright 2020 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package proxy // import "miniflux.app/proxy" + +import ( + "net/http" + "os" + "testing" + + "github.com/gorilla/mux" + "miniflux.app/config" +) + +func TestProxyFilterWithHttpDefault(t *testing.T) { + os.Clearenv() + os.Setenv("PROXY_IMAGES", "http-only") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + r := mux.NewRouter() + r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") + + input := `

Test

` + output := ImageProxyRewriter(r, input) + expected := `

Test

` + + if expected != output { + t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) + } +} + +func TestProxyFilterWithHttpsDefault(t *testing.T) { + os.Clearenv() + os.Setenv("PROXY_IMAGES", "http-only") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + r := mux.NewRouter() + r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") + + input := `

Test

` + output := ImageProxyRewriter(r, input) + expected := `

Test

` + + if expected != output { + t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) + } +} + +func TestProxyFilterWithHttpNever(t *testing.T) { + os.Clearenv() + os.Setenv("PROXY_IMAGES", "none") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + r := mux.NewRouter() + r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") + + input := `

Test

` + output := ImageProxyRewriter(r, input) + expected := input + + if expected != output { + t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) + } +} + +func TestProxyFilterWithHttpsNever(t *testing.T) { + os.Clearenv() + os.Setenv("PROXY_IMAGES", "none") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + r := mux.NewRouter() + r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") + + input := `

Test

` + output := ImageProxyRewriter(r, input) + expected := input + + if expected != output { + t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) + } +} + +func TestProxyFilterWithHttpAlways(t *testing.T) { + os.Clearenv() + os.Setenv("PROXY_IMAGES", "all") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + r := mux.NewRouter() + r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") + + input := `

Test

` + output := ImageProxyRewriter(r, input) + expected := `

Test

` + + if expected != output { + t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) + } +} + +func TestProxyFilterWithHttpsAlways(t *testing.T) { + os.Clearenv() + os.Setenv("PROXY_IMAGES", "all") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + r := mux.NewRouter() + r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") + + input := `

Test

` + output := ImageProxyRewriter(r, input) + expected := `

Test

` + + if expected != output { + t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) + } +} + +func TestProxyFilterWithHttpInvalid(t *testing.T) { + os.Clearenv() + os.Setenv("PROXY_IMAGES", "invalid") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + r := mux.NewRouter() + r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") + + input := `

Test

` + output := ImageProxyRewriter(r, input) + expected := `

Test

` + + if expected != output { + t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) + } +} + +func TestProxyFilterWithHttpsInvalid(t *testing.T) { + os.Clearenv() + os.Setenv("PROXY_IMAGES", "invalid") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + r := mux.NewRouter() + r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") + + input := `

Test

` + output := ImageProxyRewriter(r, input) + expected := `

Test

` + + if expected != output { + t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) + } +} + +func TestProxyFilterWithSrcset(t *testing.T) { + os.Clearenv() + os.Setenv("PROXY_IMAGES", "all") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + r := mux.NewRouter() + r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") + + input := `

test

` + expected := `

test

` + output := ImageProxyRewriter(r, input) + + if expected != output { + t.Errorf(`Not expected output: got %s`, output) + } +} + +func TestProxyFilterWithPictureSource(t *testing.T) { + os.Clearenv() + os.Setenv("PROXY_IMAGES", "all") + + var err error + parser := config.NewParser() + config.Opts, err = parser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf(`Parsing failure: %v`, err) + } + + r := mux.NewRouter() + r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") + + input := `` + expected := `` + output := ImageProxyRewriter(r, input) + + if expected != output { + t.Errorf(`Not expected output: got %s`, output) + } +} diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 00000000..a6705638 --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,18 @@ +// Copyright 2020 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package proxy // import "miniflux.app/proxy" + +import ( + "encoding/base64" + + "miniflux.app/http/route" + + "github.com/gorilla/mux" +) + +// ProxifyURL generates an URL for a proxified resource. +func ProxifyURL(router *mux.Router, link string) string { + return route.Path(router, "proxy", "encodedURL", base64.URLEncoding.EncodeToString([]byte(link))) +} diff --git a/template/functions.go b/template/functions.go index 6914d096..fc03e122 100644 --- a/template/functions.go +++ b/template/functions.go @@ -5,7 +5,6 @@ package template // import "miniflux.app/template" import ( - "encoding/base64" "fmt" "html/template" "math" @@ -18,11 +17,11 @@ import ( "miniflux.app/http/route" "miniflux.app/locale" "miniflux.app/model" + "miniflux.app/proxy" "miniflux.app/reader/sanitizer" "miniflux.app/timezone" "miniflux.app/url" - "github.com/PuerkitoBio/goquery" "github.com/gorilla/mux" "github.com/rylans/getlang" ) @@ -58,13 +57,13 @@ func (f *funcMap) Map() template.FuncMap { return template.HTML(str) }, "proxyFilter": func(data string) string { - return imageProxyFilter(f.router, data) + return proxy.ImageProxyRewriter(f.router, data) }, "proxyURL": func(link string) string { proxyImages := config.Opts.ProxyImages() if proxyImages == "all" || (proxyImages != "none" && !url.IsHTTPS(link)) { - return proxify(f.router, link) + return proxy.ProxifyURL(f.router, link) } return link @@ -183,71 +182,6 @@ func elapsedTime(printer *locale.Printer, tz string, t time.Time) string { } } -func imageProxyFilter(router *mux.Router, data string) string { - proxyImages := config.Opts.ProxyImages() - if proxyImages == "none" { - return data - } - - doc, err := goquery.NewDocumentFromReader(strings.NewReader(data)) - if err != nil { - return data - } - - doc.Find("img").Each(func(i int, img *goquery.Selection) { - if srcAttr, ok := img.Attr("src"); ok { - if proxyImages == "all" || !url.IsHTTPS(srcAttr) { - img.SetAttr("src", proxify(router, srcAttr)) - } - } - - if srcsetAttr, ok := img.Attr("srcset"); ok { - if proxyImages == "all" || !url.IsHTTPS(srcsetAttr) { - proxifySourceSet(img, router, srcsetAttr) - } - } - }) - - doc.Find("picture source").Each(func(i int, sourceElement *goquery.Selection) { - if srcsetAttr, ok := sourceElement.Attr("srcset"); ok { - if proxyImages == "all" || !url.IsHTTPS(srcsetAttr) { - proxifySourceSet(sourceElement, router, srcsetAttr) - } - } - }) - - output, _ := doc.Find("body").First().Html() - return output -} - -func proxifySourceSet(element *goquery.Selection, router *mux.Router, attributeValue string) { - var proxifiedSources []string - - for _, source := range strings.Split(attributeValue, ",") { - parts := strings.Split(strings.TrimSpace(source), " ") - nbParts := len(parts) - - if nbParts > 0 { - source = proxify(router, parts[0]) - - if nbParts > 1 { - source += " " + parts[1] - } - - proxifiedSources = append(proxifiedSources, source) - } - } - - if len(proxifiedSources) > 0 { - element.SetAttr("srcset", strings.Join(proxifiedSources, ", ")) - } -} - -func proxify(router *mux.Router, link string) string { - // We use base64 url encoding to avoid slash in the URL. - return route.Path(router, "proxy", "encodedURL", base64.URLEncoding.EncodeToString([]byte(link))) -} - func formatFileSize(b int64) string { const unit = 1024 if b < unit { diff --git a/template/functions_test.go b/template/functions_test.go index 1b327bea..3df35bfb 100644 --- a/template/functions_test.go +++ b/template/functions_test.go @@ -5,15 +5,10 @@ package template // import "miniflux.app/template" import ( - "net/http" - "os" "testing" "time" - "miniflux.app/config" "miniflux.app/locale" - - "github.com/gorilla/mux" ) func TestDict(t *testing.T) { @@ -131,236 +126,6 @@ func TestElapsedTime(t *testing.T) { } } -func TestProxyFilterWithHttpDefault(t *testing.T) { - os.Clearenv() - os.Setenv("PROXY_IMAGES", "http-only") - - var err error - parser := config.NewParser() - config.Opts, err = parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - r := mux.NewRouter() - r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `

Test

` - output := imageProxyFilter(r, input) - expected := `

Test

` - - if expected != output { - t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) - } -} - -func TestProxyFilterWithHttpsDefault(t *testing.T) { - os.Clearenv() - os.Setenv("PROXY_IMAGES", "http-only") - - var err error - parser := config.NewParser() - config.Opts, err = parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - r := mux.NewRouter() - r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `

Test

` - output := imageProxyFilter(r, input) - expected := `

Test

` - - if expected != output { - t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) - } -} - -func TestProxyFilterWithHttpNever(t *testing.T) { - os.Clearenv() - os.Setenv("PROXY_IMAGES", "none") - - var err error - parser := config.NewParser() - config.Opts, err = parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - r := mux.NewRouter() - r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `

Test

` - output := imageProxyFilter(r, input) - expected := input - - if expected != output { - t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) - } -} - -func TestProxyFilterWithHttpsNever(t *testing.T) { - os.Clearenv() - os.Setenv("PROXY_IMAGES", "none") - - var err error - parser := config.NewParser() - config.Opts, err = parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - r := mux.NewRouter() - r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `

Test

` - output := imageProxyFilter(r, input) - expected := input - - if expected != output { - t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) - } -} - -func TestProxyFilterWithHttpAlways(t *testing.T) { - os.Clearenv() - os.Setenv("PROXY_IMAGES", "all") - - var err error - parser := config.NewParser() - config.Opts, err = parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - r := mux.NewRouter() - r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `

Test

` - output := imageProxyFilter(r, input) - expected := `

Test

` - - if expected != output { - t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) - } -} - -func TestProxyFilterWithHttpsAlways(t *testing.T) { - os.Clearenv() - os.Setenv("PROXY_IMAGES", "all") - - var err error - parser := config.NewParser() - config.Opts, err = parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - r := mux.NewRouter() - r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `

Test

` - output := imageProxyFilter(r, input) - expected := `

Test

` - - if expected != output { - t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) - } -} - -func TestProxyFilterWithHttpInvalid(t *testing.T) { - os.Clearenv() - os.Setenv("PROXY_IMAGES", "invalid") - - var err error - parser := config.NewParser() - config.Opts, err = parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - r := mux.NewRouter() - r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `

Test

` - output := imageProxyFilter(r, input) - expected := `

Test

` - - if expected != output { - t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) - } -} - -func TestProxyFilterWithHttpsInvalid(t *testing.T) { - os.Clearenv() - os.Setenv("PROXY_IMAGES", "invalid") - - var err error - parser := config.NewParser() - config.Opts, err = parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - r := mux.NewRouter() - r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `

Test

` - output := imageProxyFilter(r, input) - expected := `

Test

` - - if expected != output { - t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected) - } -} - -func TestProxyFilterWithSrcset(t *testing.T) { - os.Clearenv() - os.Setenv("PROXY_IMAGES", "all") - - var err error - parser := config.NewParser() - config.Opts, err = parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - r := mux.NewRouter() - r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `

test

` - expected := `

test

` - output := imageProxyFilter(r, input) - - if expected != output { - t.Errorf(`Not expected output: got %s`, output) - } -} - -func TestProxyFilterWithPictureSource(t *testing.T) { - os.Clearenv() - os.Setenv("PROXY_IMAGES", "all") - - var err error - parser := config.NewParser() - config.Opts, err = parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - r := mux.NewRouter() - r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `` - expected := `` - output := imageProxyFilter(r, input) - - if expected != output { - t.Errorf(`Not expected output: got %s`, output) - } -} - func TestFormatFileSize(t *testing.T) { scenarios := []struct { input int64 diff --git a/ui/entry_scraper.go b/ui/entry_scraper.go index 0e7904a2..4ece7e82 100644 --- a/ui/entry_scraper.go +++ b/ui/entry_scraper.go @@ -10,6 +10,7 @@ import ( "miniflux.app/http/request" "miniflux.app/http/response/json" "miniflux.app/model" + "miniflux.app/proxy" "miniflux.app/reader/processor" ) @@ -37,5 +38,5 @@ func (h *handler) fetchContent(w http.ResponseWriter, r *http.Request) { h.store.UpdateEntryContent(entry) - json.OK(w, r, map[string]string{"content": entry.Content}) + json.OK(w, r, map[string]string{"content": proxy.ImageProxyRewriter(h.router, entry.Content)}) }