Archived
1
0

Merge branch 'master' of ssh://github.com/majestrate/nntpchan

This commit is contained in:
Jeff Becker 2018-12-21 08:39:20 -05:00
commit 966c999d68
No known key found for this signature in database
GPG Key ID: F357B3B42F6F9B05
5 changed files with 205 additions and 36 deletions

View File

@ -743,7 +743,7 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er
} }
ref := pr.Reference ref := pr.Reference
if len(ref) > 0 { if ref != "" {
if ValidMessageID(ref) { if ValidMessageID(ref) {
if self.daemon.database.HasArticleLocal(ref) { if self.daemon.database.HasArticleLocal(ref) {
nntp.headers.Set("References", ref) nntp.headers.Set("References", ref)
@ -784,10 +784,10 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er
return return
} }
subject := pr.Subject subject := strings.TrimSpace(pr.Subject)
// set subject // set subject
if len(subject) == 0 { if subject == "" {
subject = "None" subject = "None"
} else if len(subject) > 256 { } else if len(subject) > 256 {
// subject too big // subject too big
@ -795,29 +795,21 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er
return return
} }
nntp.headers.Set("Subject", subject) nntp.headers.Set("Subject", safeHeader(subject))
if isSage(subject) { if isSage(subject) && ref != "" {
nntp.headers.Set("X-Sage", "1") nntp.headers.Set("X-Sage", "1")
} }
name := pr.Name name := strings.TrimSpace(pr.Name)
var tripcode_privkey []byte var tripcode_privkey []byte
// set name
if len(name) == 0 {
name = "Anonymous"
} else {
idx := strings.Index(name, "#")
// tripcode // tripcode
if idx >= 0 { if idx := strings.IndexByte(name, '#'); idx >= 0 {
tripcode_privkey = parseTripcodeSecret(name[idx+1:]) tripcode_privkey = parseTripcodeSecret(name[idx+1:])
name = strings.Trim(name[:idx], "\t ") name = strings.TrimSpace(name[:idx])
}
if name == "" { if name == "" {
name = "Anonymous" name = "Anonymous"
} }
}
}
if len(name) > 128 { if len(name) > 128 {
// name too long // name too long
e(errors.New("name too long")) e(errors.New("name too long"))
@ -829,7 +821,7 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er
msgid = genMessageID(pr.Frontend) msgid = genMessageID(pr.Frontend)
} }
nntp.headers.Set("From", nntpSanitize(fmt.Sprintf("%s <poster@%s>", name, pr.Frontend))) nntp.headers.Set("From", formatAddress(safeHeader(name), "poster@"+pr.Frontend))
nntp.headers.Set("Message-ID", msgid) nntp.headers.Set("Message-ID", msgid)
// set message // set message
@ -842,7 +834,21 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er
} }
if len(cites) > 0 { if len(cites) > 0 {
nntp.headers.Set("Reply-To", strings.Join(cites, " ")) if ref == "" && len(cites) == 1 {
/*
this is workaround for:
{RFC 5322}
If the parent message does not contain
a "References:" field but does have an "In-Reply-To:" field
containing a single message identifier, then the "References:" field
will contain the contents of the parent's "In-Reply-To:" field
followed by the contents of the parent's "Message-ID:" field (if
any).
*/
cites = append(cites, "<0>")
}
nntp.headers.Set("In-Reply-To", strings.Join(cites, " "))
} }
// set date // set date

View File

@ -135,7 +135,7 @@ func (self *nntpArticle) Reset() {
self.boundary = "" self.boundary = ""
self.message = "" self.message = ""
if self.attachments != nil { if self.attachments != nil {
for idx, _ := range self.attachments { for idx := range self.attachments {
self.attachments[idx].Reset() self.attachments[idx].Reset()
self.attachments[idx] = nil self.attachments[idx] = nil
} }
@ -156,7 +156,7 @@ func newPlaintextArticle(message, email, subject, name, instance, message_id, ne
nntp := &nntpArticle{ nntp := &nntpArticle{
headers: make(ArticleHeaders), headers: make(ArticleHeaders),
} }
nntp.headers.Set("From", fmt.Sprintf("%s <%s>", name, email)) nntp.headers.Set("From", formatAddress(name, email))
nntp.headers.Set("Subject", subject) nntp.headers.Set("Subject", subject)
if isSage(subject) { if isSage(subject) {
nntp.headers.Set("X-Sage", "1") nntp.headers.Set("X-Sage", "1")
@ -296,18 +296,30 @@ func (self *nntpArticle) Newsgroup() string {
func (self *nntpArticle) Name() string { func (self *nntpArticle) Name() string {
const defname = "Anonymous" const defname = "Anonymous"
from := strings.TrimSpace(self.headers.Get("From", "")) from := strings.TrimSpace(self.headers.Get("From", ""))
if from == "" { if from == "" {
return defname return defname
} }
a, e := mail.ParseAddress(from) a, e := mail.ParseAddress(from)
var name string
if e != nil { if e != nil {
return fmt.Sprintf("[Invalid From header: %v]", e) // try older method - some nodes generate non-compliant stuff
if i := strings.IndexByte(from, '<'); i > 1 {
name = from[:i]
} else {
return "[Invalid From header]"
} }
name := strings.TrimSpace(a.Name) } else {
name = a.Name
}
name = safeHeader(name)
if name == "" { if name == "" {
return defname return defname
} }
return name return name
} }

View File

@ -914,7 +914,25 @@ func (self *nntpConnection) handleLine(daemon *NNTPDaemon, code int, line string
for _, model := range models { for _, model := range models {
if model != nil { if model != nil {
if err == nil { if err == nil {
io.WriteString(dw, fmt.Sprintf("%.6d\t%s\t\"%s\" <%s@%s>\t%s\t%s\t%s\r\n", model.NNTPID(), model.Subject(), model.Name(), model.Name(), model.Frontend(), model.Date(), model.MessageID(), model.Reference())) /*
The first 8 fields MUST be the following, in order:
"0" or article number (see below)
Subject header content
From header content
Date header content
Message-ID header content
References header content
:bytes metadata item
:lines metadata item
*/
fmt.Fprintf(dw,
"%.6d\t%s\t\"%s\" <%s@%s>\t%s\t%s\t%s\r\n",
model.NNTPID(),
safeHeader(model.Subject()),
safeHeader(model.Name()), safeHeader(model.Name()), safeHeader(model.Frontend()),
safeHeader(model.Date()),
safeHeader(model.MessageID()),
safeHeader(model.Reference()))
} }
} }
} }

View File

@ -547,8 +547,8 @@ func (self *articleStore) ProcessMessage(wr io.Writer, msg io.Reader, spamfilter
if e != nil { if e != nil {
log.Println("failed to read entire message", e) log.Println("failed to read entire message", e)
} }
pw_in.Close() pw_in.CloseWithError(e)
pr_in.Close() pr_in.CloseWithError(e)
}() }()
r := bufio.NewReader(pr_out) r := bufio.NewReader(pr_out)
m, e := readMIMEHeader(r) m, e := readMIMEHeader(r)
@ -579,7 +579,7 @@ func (self *articleStore) ProcessMessage(wr io.Writer, msg io.Reader, spamfilter
return return
} }
writeMIMEHeader(wr, m.Header) writeMIMEHeader(wr, m.Header)
read_message_body(m.Body, m.Header, self, wr, false, process) err = read_message_body(m.Body, m.Header, self, wr, false, process)
} }
return return
} }
@ -632,7 +632,7 @@ func read_message_body(body io.Reader, hdr map[string][]string, store ArticleSto
body = io.TeeReader(body, wr) body = io.TeeReader(body, wr)
} }
boundary, ok := params["boundary"] boundary, ok := params["boundary"]
if ok || content_type == "multipart/mixed" { if strings.HasPrefix(media_type, "multipart/") && ok {
partReader := multipart.NewReader(body, boundary) partReader := multipart.NewReader(body, boundary)
for { for {
part, err := partReader.NextPart() part, err := partReader.NextPart()

View File

@ -15,6 +15,7 @@ import (
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"io" "io"
"log" "log"
"mime"
"net" "net"
"net/http" "net/http"
"net/mail" "net/mail"
@ -26,6 +27,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode"
) )
func DelFile(fname string) { func DelFile(fname string) {
@ -90,6 +92,10 @@ func ValidMessageID(id string) bool {
strings.IndexAny(id[1:len(id)-1], "/\\") < 0 strings.IndexAny(id[1:len(id)-1], "/\\") < 0
} }
func ReservedMessageID(id string) bool {
return id == "<0>" || id == "<keepalive@dummy.tld>"
}
// message id hash // message id hash
func HashMessageID(msgid string) string { func HashMessageID(msgid string) string {
return fmt.Sprintf("%x", sha1.Sum([]byte(msgid))) return fmt.Sprintf("%x", sha1.Sum([]byte(msgid)))
@ -161,6 +167,133 @@ func nntpSanitize(data string) (ret string) {
return ret return ret
} }
var safeHeaderReplacer = strings.NewReplacer(
"\t", " ",
"\n", string(unicode.ReplacementChar),
"\r", string(unicode.ReplacementChar),
"\000", string(unicode.ReplacementChar))
// safeHeader replaces dangerous stuff from header,
// also replaces space with tab for XOVER/OVER output
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) {
b.WriteByte('"')
for _, r := range s {
if isQtext(r) || isWSP(r) {
b.WriteRune(r)
} else {
b.WriteByte('\\')
b.WriteRune(r)
}
}
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 i, r := range name {
if r >= 0x80 || (!isWSP(r) && !isVchar(r)) {
needsEncoding = true
break
}
if isAtext(r) {
continue
}
if r == ' ' && i > 0 && name[i-1] != ' ' && i < len(name)-1 {
// allow spaces but only surrounded by non-spaces
// otherwise they will be removed by receiver
continue
}
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 type int64Sorter []int64
func (self int64Sorter) Len() int { func (self int64Sorter) Len() int {
@ -769,7 +902,7 @@ func storeMessage(daemon *NNTPDaemon, hdr textproto.MIMEHeader, body io.Reader)
log.Println("dropping message with invalid mime header, no message-id") log.Println("dropping message with invalid mime header, no message-id")
_, err = io.Copy(Discard, body) _, err = io.Copy(Discard, body)
return return
} else if ValidMessageID(msgid) { } else if ValidMessageID(msgid) && !ReservedMessageID(msgid) {
f = daemon.store.CreateFile(msgid) f = daemon.store.CreateFile(msgid)
} else { } else {
// invalid message-id // invalid message-id
@ -785,9 +918,9 @@ func storeMessage(daemon *NNTPDaemon, hdr textproto.MIMEHeader, body io.Reader)
} }
// ask for replies // ask for replies
replyTos := strings.Split(hdr.Get("Reply-To"), " ") replyTos := strings.Split(hdr.Get("In-Reply-To"), " ")
for _, reply := range replyTos { for _, reply := range replyTos {
if ValidMessageID(reply) { if ValidMessageID(reply) && !ReservedMessageID(reply) {
if !daemon.store.HasArticle(reply) { if !daemon.store.HasArticle(reply) {
go daemon.askForArticle(reply) go daemon.askForArticle(reply)
} }
@ -801,8 +934,8 @@ func storeMessage(daemon *NNTPDaemon, hdr textproto.MIMEHeader, body io.Reader)
go func() { go func() {
var buff [65536]byte var buff [65536]byte
writeMIMEHeader(pw, hdr) writeMIMEHeader(pw, hdr)
io.CopyBuffer(pw, body, buff[:]) _, e := io.CopyBuffer(pw, body, buff[:])
pw.Close() pw.CloseWithError(e)
}() }()
err = daemon.store.ProcessMessage(f, pr, daemon.CheckText, hdr.Get("Newsgroups")) err = daemon.store.ProcessMessage(f, pr, daemon.CheckText, hdr.Get("Newsgroups"))
pr.Close() pr.Close()