A webmail client. Forked from https://git.sr.ht/~migadu/alps
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

238 lines
5.1 KiB

  1. package alpsviewhtml
  2. import (
  3. "bytes"
  4. "fmt"
  5. "net/url"
  6. "regexp"
  7. "strings"
  8. alpsbase "git.sr.ht/~emersion/alps/plugins/base"
  9. "github.com/aymerick/douceur/css"
  10. cssparser "github.com/chris-ramon/douceur/parser"
  11. "github.com/microcosm-cc/bluemonday"
  12. "golang.org/x/net/html"
  13. )
  14. // TODO: this doesn't accomodate for quoting
  15. var (
  16. cssURLRegexp = regexp.MustCompile(`url\([^)]*\)`)
  17. cssExprRegexp = regexp.MustCompile(`expression\([^)]*\)`)
  18. )
  19. var allowedStyles = map[string]bool{
  20. "direction": true,
  21. "font": true,
  22. "font-family": true,
  23. "font-style": true,
  24. "font-variant": true,
  25. "font-size": true,
  26. "font-weight": true,
  27. "letter-spacing": true,
  28. "line-height": true,
  29. "text-align": true,
  30. "text-decoration": true,
  31. "text-indent": true,
  32. "text-overflow": true,
  33. "text-shadow": true,
  34. "text-transform": true,
  35. "white-space": true,
  36. "word-spacing": true,
  37. "word-wrap": true,
  38. "vertical-align": true,
  39. "color": true,
  40. "background": true,
  41. "background-color": true,
  42. "background-image": true,
  43. "background-repeat": true,
  44. "border": true,
  45. "border-color": true,
  46. "border-radius": true,
  47. "height": true,
  48. "margin": true,
  49. "padding": true,
  50. "width": true,
  51. "max-width": true,
  52. "min-width": true,
  53. "clear": true,
  54. "float": true,
  55. "border-collapse": true,
  56. "border-spacing": true,
  57. "caption-side": true,
  58. "empty-cells": true,
  59. "table-layout": true,
  60. "list-style-type": true,
  61. "list-style-position": true,
  62. }
  63. type sanitizer struct {
  64. msg *alpsbase.IMAPMessage
  65. allowRemoteResources bool
  66. hasRemoteResources bool
  67. }
  68. func (san *sanitizer) sanitizeImageURL(src string) string {
  69. u, err := url.Parse(src)
  70. if err != nil {
  71. return "about:blank"
  72. }
  73. switch strings.ToLower(u.Scheme) {
  74. // TODO: mid support?
  75. case "cid":
  76. if san.msg == nil {
  77. return "about:blank"
  78. }
  79. part := san.msg.PartByID(u.Opaque)
  80. if part == nil || !strings.HasPrefix(part.MIMEType, "image/") {
  81. return "about:blank"
  82. }
  83. return part.URL(true).String()
  84. case "https":
  85. san.hasRemoteResources = true
  86. if !proxyEnabled || !san.allowRemoteResources {
  87. return "about:blank"
  88. }
  89. proxyURL := url.URL{Path: "/proxy"}
  90. proxyQuery := make(url.Values)
  91. proxyQuery.Set("src", u.String())
  92. proxyURL.RawQuery = proxyQuery.Encode()
  93. return proxyURL.String()
  94. default:
  95. return "about:blank"
  96. }
  97. }
  98. func (san *sanitizer) sanitizeCSSDecls(decls []*css.Declaration) []*css.Declaration {
  99. sanitized := make([]*css.Declaration, 0, len(decls))
  100. for _, decl := range decls {
  101. if !allowedStyles[decl.Property] {
  102. continue
  103. }
  104. if cssExprRegexp.FindStringIndex(decl.Value) != nil {
  105. continue
  106. }
  107. // TODO: more robust CSS declaration parsing
  108. decl.Value = cssURLRegexp.ReplaceAllString(decl.Value, "url(about:blank)")
  109. sanitized = append(sanitized, decl)
  110. }
  111. return sanitized
  112. }
  113. func (san *sanitizer) sanitizeCSSRule(rule *css.Rule) {
  114. // Disallow @import
  115. if rule.Kind == css.AtRule && strings.EqualFold(rule.Name, "@import") {
  116. rule.Prelude = "url(about:blank)"
  117. }
  118. rule.Declarations = san.sanitizeCSSDecls(rule.Declarations)
  119. for _, child := range rule.Rules {
  120. san.sanitizeCSSRule(child)
  121. }
  122. }
  123. func (san *sanitizer) sanitizeNode(n *html.Node) {
  124. if n.Type == html.ElementNode {
  125. if strings.EqualFold(n.Data, "img") {
  126. for i := range n.Attr {
  127. attr := &n.Attr[i]
  128. if strings.EqualFold(attr.Key, "src") {
  129. attr.Val = san.sanitizeImageURL(attr.Val)
  130. }
  131. }
  132. } else if strings.EqualFold(n.Data, "style") {
  133. var s string
  134. c := n.FirstChild
  135. for c != nil {
  136. if c.Type == html.TextNode {
  137. s += c.Data
  138. }
  139. next := c.NextSibling
  140. n.RemoveChild(c)
  141. c = next
  142. }
  143. stylesheet, err := cssparser.Parse(s)
  144. if err != nil {
  145. s = ""
  146. } else {
  147. for _, rule := range stylesheet.Rules {
  148. san.sanitizeCSSRule(rule)
  149. }
  150. s = stylesheet.String()
  151. }
  152. n.AppendChild(&html.Node{
  153. Type: html.TextNode,
  154. Data: s,
  155. })
  156. }
  157. for i := range n.Attr {
  158. // Don't use `i, attr := range n.Attr` since `attr` would be a copy
  159. attr := &n.Attr[i]
  160. if strings.EqualFold(attr.Key, "style") {
  161. decls, err := cssparser.ParseDeclarations(attr.Val)
  162. if err != nil {
  163. attr.Val = ""
  164. continue
  165. }
  166. decls = san.sanitizeCSSDecls(decls)
  167. attr.Val = ""
  168. for _, d := range decls {
  169. attr.Val += d.String()
  170. }
  171. }
  172. }
  173. }
  174. for c := n.FirstChild; c != nil; c = c.NextSibling {
  175. san.sanitizeNode(c)
  176. }
  177. }
  178. func (san *sanitizer) sanitizeHTML(b []byte) ([]byte, error) {
  179. doc, err := html.Parse(bytes.NewReader(b))
  180. if err != nil {
  181. return nil, fmt.Errorf("failed to parse HTML: %v", err)
  182. }
  183. san.sanitizeNode(doc)
  184. var buf bytes.Buffer
  185. if err := html.Render(&buf, doc); err != nil {
  186. return nil, fmt.Errorf("failed to render HTML: %v", err)
  187. }
  188. b = buf.Bytes()
  189. // bluemonday must always be run last
  190. p := bluemonday.UGCPolicy()
  191. // TODO: use bluemonday's AllowStyles once it's released and
  192. // supports <style>
  193. p.AllowElements("style")
  194. p.AllowAttrs("style").Globally()
  195. p.AddTargetBlankToFullyQualifiedLinks(true)
  196. p.RequireNoFollowOnLinks(true)
  197. return p.SanitizeBytes(b), nil
  198. }