Archived
1
0

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.
This commit is contained in:
cathugger 2018-12-12 18:38:58 +02:00
parent 2ac773cc64
commit 40e4ae1fc4
No known key found for this signature in database
GPG Key ID: 9BADDA2DAF6F01A8
3 changed files with 129 additions and 27 deletions

View File

@ -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

View File

@ -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")

View File

@ -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 {