- Route image-validation, Matrix image-download, and wayback/archive.today fetches through safehttp so feed-controlled URLs can't reach loopback/ RFC1918/cloud-metadata IPs (incl. via redirects). - Cap feed body size with safehttp.LimitedBody (16 MiB) to prevent OOM. - Fail closed if matrix.pickle_key is unset/<16 chars; drop the hardcoded "pete_pickle_key" default that silently weakened E2EE-at-rest. - Gate !post behind a matrix.admins allowlist; empty = disabled (channel rooms are public, so empty must not mean anyone). - Reject /\host open-redirect bypass in post-login safeNext. - Allowlist http(s) schemes for the Matrix link href; escape JSON embedded in inline <script> (prefs/sources blobs).
80 lines
2.3 KiB
Go
80 lines
2.3 KiB
Go
package matrix
|
|
|
|
import (
|
|
"fmt"
|
|
"html"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// safeHref returns raw if it is an http(s) URL, else "". Keeps feed-supplied
|
|
// links from injecting javascript:/data: schemes into the formatted_body href
|
|
// (html.EscapeString blocks attribute breakout but not the scheme itself).
|
|
func safeHref(raw string) string {
|
|
u, err := url.Parse(strings.TrimSpace(raw))
|
|
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
|
|
return ""
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// PostableStory contains the data needed to format and send a story.
|
|
type PostableStory struct {
|
|
ImageURL string
|
|
Headline string
|
|
ArticleURL string
|
|
Lede string
|
|
Source string
|
|
Channel string
|
|
Platforms []string
|
|
}
|
|
|
|
// formatPost returns plain text and HTML bodies for a Matrix message.
|
|
func formatPost(s *PostableStory) (plain, htmlBody string) {
|
|
// Plain text body
|
|
var plainParts []string
|
|
plainParts = append(plainParts, fmt.Sprintf("**%s** \u2192 %s", s.Headline, s.ArticleURL))
|
|
if s.Lede != "" {
|
|
plainParts = append(plainParts, s.Lede)
|
|
}
|
|
plainParts = append(plainParts, formatSourceTag(s.Source, s.Platforms, false))
|
|
plain = strings.Join(plainParts, "\n")
|
|
|
|
// HTML body
|
|
var htmlParts []string
|
|
if href := safeHref(s.ArticleURL); href != "" {
|
|
htmlParts = append(htmlParts, fmt.Sprintf(
|
|
`<strong><a href="%s">%s</a></strong>`,
|
|
html.EscapeString(href),
|
|
html.EscapeString(s.Headline),
|
|
))
|
|
} else {
|
|
// Non-http(s) link: render the headline without an anchor.
|
|
htmlParts = append(htmlParts, fmt.Sprintf(`<strong>%s</strong>`, html.EscapeString(s.Headline)))
|
|
}
|
|
if s.Lede != "" {
|
|
htmlParts = append(htmlParts, html.EscapeString(s.Lede))
|
|
}
|
|
htmlParts = append(htmlParts, formatSourceTag(s.Source, s.Platforms, true))
|
|
htmlBody = strings.Join(htmlParts, "<br/>")
|
|
|
|
return plain, htmlBody
|
|
}
|
|
|
|
// formatSourceTag builds the source + platform tags line.
|
|
func formatSourceTag(source string, platforms []string, isHTML bool) string {
|
|
if isHTML {
|
|
parts := []string{fmt.Sprintf("<code>%s</code>", html.EscapeString(strings.ToLower(source)))}
|
|
for _, p := range platforms {
|
|
parts = append(parts, fmt.Sprintf("<code>%s</code>", html.EscapeString(p)))
|
|
}
|
|
return strings.Join(parts, " \u00b7 ")
|
|
}
|
|
|
|
parts := []string{fmt.Sprintf("`%s`", strings.ToLower(source))}
|
|
for _, p := range platforms {
|
|
parts = append(parts, fmt.Sprintf("`%s`", p))
|
|
}
|
|
return strings.Join(parts, " \u00b7 ")
|
|
}
|