Merge branch 'master' of ssh://github.com/majestrate/nntpchan
This commit is contained in:
commit
966c999d68
@ -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,28 +795,20 @@ 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
|
||||||
|
// tripcode
|
||||||
// set name
|
if idx := strings.IndexByte(name, '#'); idx >= 0 {
|
||||||
if len(name) == 0 {
|
tripcode_privkey = parseTripcodeSecret(name[idx+1:])
|
||||||
|
name = strings.TrimSpace(name[:idx])
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
name = "Anonymous"
|
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 {
|
if len(name) > 128 {
|
||||||
// name too long
|
// 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
|
||||||
|
@ -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]"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
name = a.Name
|
||||||
}
|
}
|
||||||
name := strings.TrimSpace(a.Name)
|
|
||||||
|
name = safeHeader(name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return defname
|
return defname
|
||||||
}
|
}
|
||||||
|
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
Reference in New Issue
Block a user