From 40e4ae1fc4b5ff868ad4c3e92db77bec1af55dbd Mon Sep 17 00:00:00 2001 From: cathugger Date: Wed, 12 Dec 2018 18:38:58 +0200 Subject: [PATCH] srnd: custom email address formatter, some tweaks This adds custom email address formatter, which, unlike stdlib one, doesn't needlessly quote names. Quoted names can be a bit of issue with older nodes which parse addresses in simpler way, and end up not removing quote characters. This also ensures that newlines cannot be inserted in in From and Subject headers, which effectively allowed insertion of new headers in message being posted, and generating invalid messages. --- .../backends/srndv2/src/srnd/frontend_http.go | 34 ++--- contrib/backends/srndv2/src/srnd/message.go | 5 +- contrib/backends/srndv2/src/srnd/util.go | 117 ++++++++++++++++++ 3 files changed, 129 insertions(+), 27 deletions(-) diff --git a/contrib/backends/srndv2/src/srnd/frontend_http.go b/contrib/backends/srndv2/src/srnd/frontend_http.go index c87798d..9f2e661 100644 --- a/contrib/backends/srndv2/src/srnd/frontend_http.go +++ b/contrib/backends/srndv2/src/srnd/frontend_http.go @@ -19,7 +19,6 @@ import ( "log" "mime" "net/http" - "net/mail" "strings" "time" ) @@ -785,10 +784,10 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er return } - subject := pr.Subject + subject := strings.TrimSpace(pr.Subject) // set subject - if len(subject) == 0 { + if subject == "" { subject = "None" } else if len(subject) > 256 { // subject too big @@ -796,28 +795,20 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er return } - nntp.headers.Set("Subject", subject) + nntp.headers.Set("Subject", safeHeader(subject)) if isSage(subject) { nntp.headers.Set("X-Sage", "1") } - name := pr.Name - + name := strings.TrimSpace(pr.Name) var tripcode_privkey []byte - - // set name - if len(name) == 0 { + // tripcode + if idx := strings.IndexByte(name, '#'); idx >= 0 { + tripcode_privkey = parseTripcodeSecret(name[idx+1:]) + name = strings.TrimSpace(name[:idx]) + } + if name == "" { name = "Anonymous" - } else { - idx := strings.Index(name, "#") - // tripcode - if idx >= 0 { - tripcode_privkey = parseTripcodeSecret(name[idx+1:]) - name = strings.Trim(name[:idx], "\t ") - if name == "" { - name = "Anonymous" - } - } } if len(name) > 128 { // name too long @@ -830,10 +821,7 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er msgid = genMessageID(pr.Frontend) } - nntp.headers.Set("From", (&mail.Address{ - Name: name, - Address: "poster@" + pr.Frontend, - }).String()) + nntp.headers.Set("From", formatAddress(safeHeader(name), "poster@" + pr.Frontend)) nntp.headers.Set("Message-ID", msgid) // set message diff --git a/contrib/backends/srndv2/src/srnd/message.go b/contrib/backends/srndv2/src/srnd/message.go index a0a8922..cc4706b 100644 --- a/contrib/backends/srndv2/src/srnd/message.go +++ b/contrib/backends/srndv2/src/srnd/message.go @@ -156,10 +156,7 @@ func newPlaintextArticle(message, email, subject, name, instance, message_id, ne nntp := &nntpArticle{ headers: make(ArticleHeaders), } - nntp.headers.Set("From", (&mail.Address{ - Name: name, - Address: email, - }).String()) + nntp.headers.Set("From", formatAddress(name, email)) nntp.headers.Set("Subject", subject) if isSage(subject) { nntp.headers.Set("X-Sage", "1") diff --git a/contrib/backends/srndv2/src/srnd/util.go b/contrib/backends/srndv2/src/srnd/util.go index e3c6706..a7efb67 100644 --- a/contrib/backends/srndv2/src/srnd/util.go +++ b/contrib/backends/srndv2/src/srnd/util.go @@ -15,6 +15,7 @@ import ( "golang.org/x/crypto/ed25519" "io" "log" + "mime" "net" "net/http" "net/mail" @@ -27,6 +28,7 @@ import ( "strings" "time" "unicode" + "unicode/utf8" ) func DelFile(fname string) { @@ -174,6 +176,121 @@ func safeHeader(s string) string { return strings.TrimSpace(safeHeaderReplacer.Replace(s)) } +func isVchar(r rune) bool { + // RFC 5234 B.1: VCHAR = %x21-7E ; visible (printing) characters + // RFC 6532 3.2: VCHAR =/ UTF8-non-ascii + return (r >= 0x21 && r <= 0x7E) || r >= 0x80 +} + +func isAtext(r rune) bool { + // RFC 5322: Printable US-ASCII characters not including specials. Used for atoms. + switch r { + case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"': + return false + } + return isVchar(r) +} + +func isWSP(r rune) bool { return r == ' ' || r == '\t' } + +func isQtext(r rune) bool { + if r == '\\' || r == '"' { + return false + } + return isVchar(r) +} + +func writeQuoted(b *strings.Builder, s string) { + last := 0 + b.WriteByte('"') + for i, r := range s { + if !isQtext(r) && !isWSP(r) { + if i > last { + b.WriteString(s[last:i]) + } + b.WriteByte('\\') + b.WriteRune(r) + last = i + utf8.RuneLen(r) + } + } + if last < len(s) { + b.WriteString(s[last:]) + } + b.WriteByte('"') +} + +func formatAddress(name, email string) string { + // somewhat based on stdlib' mail.Address.String() + + b := &strings.Builder{} + + if name != "" { + needsEncoding := false + needsQuoting := false + for _, r := range name { + if r >= 0x80 || (!isWSP(r) && !isVchar(r)) { + needsEncoding = true + break + } + if !isAtext(r) { + needsQuoting = true + } + } + + if needsEncoding { + // Text in an encoded-word in a display-name must not contain certain + // characters like quotes or parentheses (see RFC 2047 section 5.3). + // When this is the case encode the name using base64 encoding. + if strings.ContainsAny(name, "\"#$%&'(),.:;<>@[]^`{|}~") { + b.WriteString(mime.BEncoding.Encode("utf-8", name)) + } else { + b.WriteString(mime.QEncoding.Encode("utf-8", name)) + } + } else if needsQuoting { + writeQuoted(b, name) + } else { + b.WriteString(name) + } + + b.WriteByte(' ') + } + + at := strings.LastIndex(email, "@") + var local, domain string + if at >= 0 { + local, domain = email[:at], email[at+1:] + } else { + local = email + } + + quoteLocal := false + for i, r := range local { + if isAtext(r) { + // if atom then okay + continue + } + if r == '.' && r > 0 && local[i-1] != '.' && i < len(local)-1 { + // dots are okay but only if surrounded by non-dots + continue + } + quoteLocal = true + break + } + + b.WriteByte('<') + if !quoteLocal { + b.WriteString(local) + } else { + writeQuoted(b, local) + } + b.WriteByte('@') + b.WriteString(domain) + b.WriteByte('>') + + return b.String() +} + + type int64Sorter []int64 func (self int64Sorter) Len() int {