diff --git a/plugins/viewhtml/plugin.go b/plugins/viewhtml/plugin.go
index f22364e..b34372f 100644
--- a/plugins/viewhtml/plugin.go
+++ b/plugins/viewhtml/plugin.go
@@ -1,10 +1,59 @@
package koushinviewhtml
import (
+ "io"
+ "mime"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+
"git.sr.ht/~emersion/koushin"
+ "github.com/labstack/echo/v4"
+)
+
+var (
+ proxyEnabled = true
+ proxyMaxSize = 5 * 1024 * 1024 // 5 MiB
)
func init() {
p := koushin.GoPlugin{Name: "viewhtml"}
+
+ p.GET("/proxy", func(ctx *koushin.Context) error {
+ if !proxyEnabled {
+ return echo.NewHTTPError(http.StatusForbidden, "proxy disabled")
+ }
+
+ u, err := url.Parse(ctx.QueryParam("src"))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "invalid URL")
+ }
+
+ if u.Scheme != "https" {
+ return echo.NewHTTPError(http.StatusBadRequest, "invalid scheme")
+ }
+
+ resp, err := http.Get(u.String())
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
+ if err != nil || !strings.HasPrefix(mediaType, "image/") {
+ return echo.NewHTTPError(http.StatusBadRequest, "invalid resource type")
+ }
+
+ size, err := strconv.Atoi(resp.Header.Get("Content-Length"))
+ if err != nil || size > proxyMaxSize {
+ return echo.NewHTTPError(http.StatusBadRequest, "invalid resource length")
+ }
+
+ ctx.Response().Header().Set("Content-Length", strconv.Itoa(size))
+ lr := io.LimitedReader{resp.Body, int64(proxyMaxSize)}
+ return ctx.Stream(http.StatusOK, mediaType, &lr)
+ })
+
koushin.RegisterPluginLoader(p.Loader())
}
diff --git a/plugins/viewhtml/sanitize.go b/plugins/viewhtml/sanitize.go
index c7de703..a931fe1 100644
--- a/plugins/viewhtml/sanitize.go
+++ b/plugins/viewhtml/sanitize.go
@@ -80,17 +80,32 @@ func (san *sanitizer) sanitizeImageURL(src string) string {
return "about:blank"
}
+ switch strings.ToLower(u.Scheme) {
// TODO: mid support?
- if !strings.EqualFold(u.Scheme, "cid") || san.msg == nil {
+ case "cid":
+ if san.msg == nil {
+ return "about:blank"
+ }
+
+ part := san.msg.PartByID(u.Opaque)
+ if part == nil || !strings.HasPrefix(part.MIMEType, "image/") {
+ return "about:blank"
+ }
+
+ return part.URL(true).String()
+ case "https":
+ if !proxyEnabled {
+ return "about:blank"
+ }
+
+ proxyURL := url.URL{Path: "/proxy"}
+ proxyQuery := make(url.Values)
+ proxyQuery.Set("src", u.String())
+ proxyURL.RawQuery = proxyQuery.Encode()
+ return proxyURL.String()
+ default:
return "about:blank"
}
-
- part := san.msg.PartByID(u.Opaque)
- if part == nil || !strings.HasPrefix(part.MIMEType, "image/") {
- return "about:blank"
- }
-
- return part.URL(true).String()
}
func (san *sanitizer) sanitizeCSSDecls(decls []*css.Declaration) []*css.Declaration {