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 {