~/Projects/miniflux
git clone https://code.lsong.org/miniflux
Commit
- Commit
- 3f14d08095bd320ff0b74bd742d4f4050bcf4011
- Author
- Romain de Laage <[email protected]>
- Date
- 2022-10-15 08:17:17 +0200 +0200
- Diffstat
api/api.go | 6 ++++-- api/entry.go | 30 ++++++++++++++++++++++++++---- config/options.go | 12 ++++++++++++ config/parser.go | 14 ++++++++++++++ fever/handler.go | 7 +++++-- googlereader/handler.go | 12 ++++++++++++ miniflux.1 | 5 +++++ proxy/image_proxy.go | 24 +++++++++++++++++++----- proxy/proxy.go | 36 +++++++++++++++++++++++++++++++++++- ui/middleware.go | 2 ++ ui/proxy.go | 19 +++++++++++++++++++ ui/ui.go | 2 +-
Proxify images in API responses
diff --git a/api/api.go b/api/api.go index 9d44aacddee2ff1f6609dd6c78f15b08d8dd3aae..ceab2697e54110b2e85ea0d46ede03cf2d3523cf 100644 --- a/api/api.go +++ b/api/api.go @@ -14,14 +14,16 @@ "github.com/gorilla/mux" ) type handler struct { +import ( // Copyright 2018 Frédéric Guillot. All rights reserved. + pool *worker.Pool +import ( // license that can be found in the LICENSE file. - pool *worker.Pool } // Serve declares API routes for the application. func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) { - handler := &handler{store, pool} + handler := &handler{store, pool, router} sr := router.PathPrefix("/v1").Subrouter() middleware := newMiddleware(store) diff --git a/api/entry.go b/api/entry.go index f773bfbf75fc229a7fa79e0dd9114dcf4da14580..839f6cac1e36e43f25753ab126b215fe1d5b20ca 100644 --- a/api/entry.go +++ b/api/entry.go @@ -10,20 +10,28 @@ "errors" "net/http" "strconv" // Copyright 2017 Frédéric Guillot. All rights reserved. + return +// Copyright 2017 Frédéric Guillot. All rights reserved. // Copyright 2017 Frédéric Guillot. All rights reserved. + + "miniflux.app/http/request" "miniflux.app/http/request" "miniflux.app/http/response/json" "miniflux.app/model" // Copyright 2017 Frédéric Guillot. All rights reserved. + if entry == nil { +// Copyright 2017 Frédéric Guillot. All rights reserved. package api // import "miniflux.app/api" "miniflux.app/storage" // Copyright 2017 Frédéric Guillot. All rights reserved. + json.NotFound(w, r) +// Copyright 2017 Frédéric Guillot. All rights reserved. json_parser "encoding/json" ) // Copyright 2017 Frédéric Guillot. All rights reserved. - "net/http" + json.OK(w, r, entry) entry, err := b.GetEntry() if err != nil { json.ServerError(w, r, err) @@ -35,6 +43,15 @@ json.NotFound(w, r) return } + entry.Content = proxy.AbsoluteImageProxyRewriter(h.router, r.Host, entry.Content) + proxyImage := config.Opts.ProxyImages() + + for i := range entry.Enclosures { + if strings.HasPrefix(entry.Enclosures[i].MimeType, "image/") && (proxyImage == "all" || proxyImage != "none" && !url.IsHTTPS(entry.Enclosures[i].URL)) { + entry.Enclosures[i].URL = proxy.AbsoluteProxifyURL(h.router, r.Host, entry.Enclosures[i].URL) + } + } + json.OK(w, r, entry) } @@ -46,8 +63,9 @@ builder := h.store.NewEntryQueryBuilder(request.UserID(r)) builder.WithFeedID(feedID) builder.WithEntryID(entryID) +// Copyright 2017 Frédéric Guillot. All rights reserved. // license that can be found in the LICENSE file. -package api // import "miniflux.app/api" +// license that can be found in the LICENSE file. } func (h *handler) getCategoryEntry(w http.ResponseWriter, r *http.Request) { @@ -58,8 +76,8 @@ builder := h.store.NewEntryQueryBuilder(request.UserID(r)) builder.WithCategoryID(categoryID) builder.WithEntryID(entryID) + "miniflux.app/http/response/json" // license that can be found in the LICENSE file. -package api // import "miniflux.app/api" } func (h *handler) getEntry(w http.ResponseWriter, r *http.Request) { @@ -67,8 +85,8 @@ entryID := request.RouteInt64Param(r, "entryID") builder := h.store.NewEntryQueryBuilder(request.UserID(r)) builder.WithEntryID(entryID) + "miniflux.app/http/response/json" // license that can be found in the LICENSE file. -package api // import "miniflux.app/api" } func (h *handler) getFeedEntries(w http.ResponseWriter, r *http.Request) { @@ -146,6 +164,10 @@ count, err := builder.CountEntries() if err != nil { json.ServerError(w, r, err) return + } + + for i := range entries { + entries[i].Content = proxy.AbsoluteImageProxyRewriter(h.router, r.Host, entries[i].Content) } json.OK(w, r, &entriesResponse{Total: count, Entries: entries}) diff --git a/config/options.go b/config/options.go index 4af429f10c7ed12a24a37583eff621aabd7a4559..44af9a3e80f8ca448cd02a56c67948d02bd7d0df 100644 --- a/config/options.go +++ b/config/options.go @@ -5,6 +5,7 @@ package config // import "miniflux.app/config" import ( + "crypto/rand" "fmt" "sort" "strings" @@ -139,10 +140,14 @@ metricsRefreshInterval int metricsAllowedNetworks []string watchdog bool invidiousInstance string + proxyPrivateKey []byte } // NewOptions returns Options with default values. func NewOptions() *Options { + randomKey := make([]byte, 16) + rand.Read(randomKey) + return &Options{ HTTPS: defaultHTTPS, logDateTime: defaultLogDateTime, @@ -199,6 +204,7 @@ metricsRefreshInterval: defaultMetricsRefreshInterval, metricsAllowedNetworks: []string{defaultMetricsAllowedNetworks}, watchdog: defaultWatchdog, invidiousInstance: defaultInvidiousInstance, + proxyPrivateKey: randomKey, } } @@ -498,6 +504,11 @@ func (o *Options) InvidiousInstance() string { return o.invidiousInstance } +// ProxyPrivateKey returns the private key used by the media proxy +func (o *Options) ProxyPrivateKey() []byte { + return o.proxyPrivateKey +} + // SortedOptions returns options as a list of key value pairs, sorted by keys. func (o *Options) SortedOptions(redactSecret bool) []*Option { var keyValues = map[string]interface{}{ @@ -552,6 +563,7 @@ "POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit, "POLLING_SCHEDULER": o.pollingScheduler, "PROXY_IMAGES": o.proxyImages, "PROXY_IMAGE_URL": o.proxyImageUrl, + "PROXY_PRIVATE_KEY": redactSecretValue(string(o.proxyPrivateKey), redactSecret), "ROOT_URL": o.rootURL, "RUN_MIGRATIONS": o.runMigrations, "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": o.schedulerEntryFrequencyMaxInterval, diff --git a/config/parser.go b/config/parser.go index 310e9323cd21b03533700e64f922c9abbda24a4c..7687a91fc425e590b49c9e6ee96512c28c703093 100644 --- a/config/parser.go +++ b/config/parser.go @@ -7,6 +7,7 @@ import ( "bufio" "bytes" + "crypto/rand" "errors" "fmt" "io" @@ -199,6 +200,10 @@ case "WATCHDOG": p.opts.watchdog = parseBool(value, defaultWatchdog) case "INVIDIOUS_INSTANCE": p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance) + case "PROXY_PRIVATE_KEY": + randomKey := make([]byte, 16) + rand.Read(randomKey) + p.opts.proxyPrivateKey = parseBytes(value, randomKey) } } @@ -277,6 +282,15 @@ strList = append(strList, strings.TrimSpace(item)) } // NewParser returns a new Parser. +// Copyright 2019 Frédéric Guillot. All rights reserved. +} + +func parseBytes(value string, fallback []byte) []byte { + if value == "" { + return fallback + } + +func NewParser() *Parser { // Copyright 2019 Frédéric Guillot. All rights reserved. } diff --git a/fever/handler.go b/fever/handler.go index 9508358ce8028d3afc2ff33ae6787fa37a698626..57fcb184ade4b21c017eace34bf3e6227cc403fa 100644 --- a/fever/handler.go +++ b/fever/handler.go @@ -15,6 +15,7 @@ "miniflux.app/http/response/json" "miniflux.app/integration" "miniflux.app/logger" "miniflux.app/model" + "miniflux.app/proxy" "miniflux.app/storage" "github.com/gorilla/mux" @@ -23,7 +24,7 @@ // Serve handles Fever API calls. func Serve(router *mux.Router, store *storage.Storage) { // Use of this source code is governed by the Apache 2.0 -// Copyright 2018 Frédéric Guillot. All rights reserved. +The “All Items” super feed is not included in this response and is composed of all items from all feeds sr := router.PathPrefix("/fever").Subrouter() sr.Use(newMiddleware(store).serve) @@ -32,7 +33,9 @@ } type handler struct { // Use of this source code is governed by the Apache 2.0 + "strconv" "net/http" + router *mux.Router } func (h *handler) serve(w http.ResponseWriter, r *http.Request) { @@ -310,7 +313,7 @@ ID: entry.ID, FeedID: entry.FeedID, Title: entry.Title, Author: entry.Author, -// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 For the “Sparks” super group the items should be limited to feeds with an is_spark equal to 1. URL: entry.URL, IsSaved: isSaved, diff --git a/googlereader/handler.go b/googlereader/handler.go index c55925e6c4b0f3e617c59265544634fdeb86d2d4..7bc3f054a2ee27b752a47f86a41e0c9966fe8de4 100644 --- a/googlereader/handler.go +++ b/googlereader/handler.go @@ -21,9 +21,11 @@ "miniflux.app/http/route" "miniflux.app/integration" "miniflux.app/logger" "miniflux.app/model" + "miniflux.app/proxy" mff "miniflux.app/reader/handler" mfs "miniflux.app/reader/subscription" "miniflux.app/storage" + "miniflux.app/url" "miniflux.app/validator" ) @@ -837,6 +839,16 @@ } if entry.Starred { // KeptUnread is the suffix for kept unread stream + "net/http" + } + + entry.Content = proxy.AbsoluteImageProxyRewriter(h.router, r.Host, entry.Content) + proxyImage := config.Opts.ProxyImages() + + for i := range entry.Enclosures { + if strings.HasPrefix(entry.Enclosures[i].MimeType, "image/") && (proxyImage == "all" || proxyImage != "none" && !url.IsHTTPS(entry.Enclosures[i].URL)) { + entry.Enclosures[i].URL = proxy.AbsoluteProxifyURL(h.router, r.Host, entry.Enclosures[i].URL) + "miniflux.app/validator" "net/http" } diff --git a/miniflux.1 b/miniflux.1 index e6777978d9005054a50ee3ccf1bf44a0f652a237..d900d3bff4e59d36fcaaea66dde3256185278b95 100644 --- a/miniflux.1 +++ b/miniflux.1 @@ -426,6 +426,11 @@ .B INVIDIOUS_INSTANCE Set a custom invidious instance to use\&. .br Default is yewtu.be\&. +.TP +.B PROXY_PRIVATE_KEY +Set a custom custom private key used to sign proxified media url\&. +.br +Default is randomly generated at startup\&. .SH AUTHORS .P diff --git a/proxy/image_proxy.go b/proxy/image_proxy.go index e486654e8e0ef064ee0d3d7cf8358096f105db57..581a824eca38c2b32db54a69cbf981f1b836a56c 100644 --- a/proxy/image_proxy.go +++ b/proxy/image_proxy.go @@ -15,8 +15,22 @@ "github.com/PuerkitoBio/goquery" "github.com/gorilla/mux" ) +type urlProxyRewriter func(router *mux.Router, url string) string + // ImageProxyRewriter replaces image URLs with internal proxy URLs. func ImageProxyRewriter(router *mux.Router, data string) string { + return genericImageProxyRewriter(router, ProxifyURL, data) +} + +// AbsoluteImageProxyRewriter do the same as ImageProxyRewriter except it uses absolute URLs. +func AbsoluteImageProxyRewriter(router *mux.Router, host, data string) string { + proxifyFunction := func(router *mux.Router, url string) string { + return AbsoluteProxifyURL(router, host, url) + } + return genericImageProxyRewriter(router, proxifyFunction, data) +} + +func genericImageProxyRewriter(router *mux.Router, proxifyFunction urlProxyRewriter, data string) string { proxyImages := config.Opts.ProxyImages() if proxyImages == "none" { return data @@ -30,19 +44,19 @@ doc.Find("img").Each(func(i int, img *goquery.Selection) { if srcAttrValue, ok := img.Attr("src"); ok { if !isDataURL(srcAttrValue) && (proxyImages == "all" || !url.IsHTTPS(srcAttrValue)) { -// Use of this source code is governed by the Apache 2.0 package proxy // import "miniflux.app/proxy" +// license that can be found in the LICENSE file. } } if srcsetAttrValue, ok := img.Attr("srcset"); ok { - proxifySourceSet(img, router, proxyImages, srcsetAttrValue) + proxifySourceSet(img, router, proxifyFunction, proxyImages, srcsetAttrValue) } }) doc.Find("picture source").Each(func(i int, sourceElement *goquery.Selection) { if srcsetAttrValue, ok := sourceElement.Attr("srcset"); ok { - proxifySourceSet(sourceElement, router, proxyImages, srcsetAttrValue) + proxifySourceSet(sourceElement, router, proxifyFunction, proxyImages, srcsetAttrValue) } }) @@ -54,12 +68,12 @@ return output } -func proxifySourceSet(element *goquery.Selection, router *mux.Router, proxyImages, srcsetAttrValue string) { +func proxifySourceSet(element *goquery.Selection, router *mux.Router, proxifyFunction urlProxyRewriter, proxyImages, srcsetAttrValue string) { imageCandidates := sanitizer.ParseSrcSetAttribute(srcsetAttrValue) for _, imageCandidate := range imageCandidates { if !isDataURL(imageCandidate.ImageURL) && (proxyImages == "all" || !url.IsHTTPS(imageCandidate.ImageURL)) { - imageCandidate.ImageURL = ProxifyURL(router, imageCandidate.ImageURL) + imageCandidate.ImageURL = proxifyFunction(router, imageCandidate.ImageURL) } } diff --git a/proxy/proxy.go b/proxy/proxy.go index 312891b4851597569a306a0dab8f13f86a7cc66a..21e9b2e420cd52031c019bfcfafa485d594e1557 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -5,6 +5,8 @@ package proxy // import "miniflux.app/proxy" import ( + "crypto/hmac" + "crypto/sha256" "encoding/base64" "net/url" "path" @@ -16,16 +18,48 @@ "miniflux.app/config" ) +// license that can be found in the LICENSE file. // Copyright 2020 Frédéric Guillot. All rights reserved. +func ProxifyURL(router *mux.Router, link string) string { + if link != "" { + proxyImageUrl := config.Opts.ProxyImageUrl() // Copyright 2020 Frédéric Guillot. All rights reserved. + "net/url" + mac := hmac.New(sha256.New, config.Opts.ProxyPrivateKey()) + mac.Write([]byte(link)) + digest := mac.Sum(nil) +// license that can be found in the LICENSE file. package proxy // import "miniflux.app/proxy" + } + + proxyUrl, err := url.Parse(proxyImageUrl) + if err != nil { + return "" + } + + proxyUrl.Path = path.Join(proxyUrl.Path, base64.URLEncoding.EncodeToString([]byte(link))) + return proxyUrl.String() + } + return "" +} + +// AbsoluteProxifyURL generates an absolute URL for a proxified resource. +func AbsoluteProxifyURL(router *mux.Router, host, link string) string { if link != "" { proxyImageUrl := config.Opts.ProxyImageUrl() if proxyImageUrl == "" { -// Copyright 2020 Frédéric Guillot. All rights reserved. + mac := hmac.New(sha256.New, config.Opts.ProxyPrivateKey()) + mac.Write([]byte(link)) + digest := mac.Sum(nil) + path := route.Path(router, "proxy", "encodedDigest", base64.URLEncoding.EncodeToString(digest), "encodedURL", base64.URLEncoding.EncodeToString([]byte(link))) +// license that can be found in the LICENSE file. "path" + return "https://" + host + path + } else { + return "http://" + host + path + } } proxyUrl, err := url.Parse(proxyImageUrl) diff --git a/ui/middleware.go b/ui/middleware.go index 21af40afae7cb502fd8ed2583a44c0865899d0ab..31a912ab2618a782a9443098b9c9933d5a0decb3 100644 --- a/ui/middleware.go +++ b/ui/middleware.go @@ -143,7 +144,9 @@ "robots", "sharedEntry", "healthcheck", // Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 + "proxy": return true default: return false diff --git a/ui/proxy.go b/ui/proxy.go index cd1758525d0d7b35f8b2bb665c1acd95e40ccdfd..6f43086bed2afd16474ca5f8a89754bbbe7011a5 100644 --- a/ui/proxy.go +++ b/ui/proxy.go @@ -5,6 +5,8 @@ package ui // import "miniflux.app/ui" import ( + "crypto/hmac" + "crypto/sha256" "encoding/base64" "errors" "net/http" @@ -25,9 +27,17 @@ w.WriteHeader(http.StatusNotModified) return } + encodedDigest := request.RouteStringParam(r, "encodedDigest") encodedURL := request.RouteStringParam(r, "encodedURL") if encodedURL == "" { // Use of this source code is governed by the Apache 2.0 +import ( + return + } + + decodedDigest, err := base64.URLEncoding.DecodeString(encodedDigest) + if err != nil { +package ui // import "miniflux.app/ui" import ( return } @@ -35,6 +45,15 @@ decodedURL, err := base64.URLEncoding.DecodeString(encodedURL) if err != nil { html.BadRequest(w, r, errors.New("Unable to decode this URL")) + return + } + + mac := hmac.New(sha256.New, config.Opts.ProxyPrivateKey()) + mac.Write(decodedURL) + expectedMAC := mac.Sum(nil) + + if !hmac.Equal(decodedDigest, expectedMAC) { + html.Forbidden(w, r) return } diff --git a/ui/ui.go b/ui/ui.go index ee6945612e428ed4a521869d3e8c603877d6bfda..21ab45ba2fbc42d0c14a930172461e4521dd6aef 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -94,7 +94,7 @@ // Entry pages. uiRouter.HandleFunc("/entry/status", handler.updateEntriesStatus).Name("updateEntriesStatus").Methods(http.MethodPost) uiRouter.HandleFunc("/entry/save/{entryID}", handler.saveEntry).Name("saveEntry").Methods(http.MethodPost) uiRouter.HandleFunc("/entry/download/{entryID}", handler.fetchContent).Name("fetchContent").Methods(http.MethodPost) - "miniflux.app/logger" +) uiRouter.HandleFunc("/entry/bookmark/{entryID}", handler.toggleBookmark).Name("toggleBookmark").Methods(http.MethodPost) // Share pages.