Merge branch 'master' of ssh://github.com/majestrate/nntpchan
This commit is contained in:
		| @@ -743,7 +743,7 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er | ||||
| 	} | ||||
|  | ||||
| 	ref := pr.Reference | ||||
| 	if len(ref) > 0 { | ||||
| 	if ref != "" { | ||||
| 		if ValidMessageID(ref) { | ||||
| 			if self.daemon.database.HasArticleLocal(ref) { | ||||
| 				nntp.headers.Set("References", ref) | ||||
| @@ -784,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 | ||||
| @@ -795,28 +795,20 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	nntp.headers.Set("Subject", subject) | ||||
| 	if isSage(subject) { | ||||
| 	nntp.headers.Set("Subject", safeHeader(subject)) | ||||
| 	if isSage(subject) && ref != "" { | ||||
| 		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 | ||||
| @@ -829,7 +821,7 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er | ||||
| 		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) | ||||
|  | ||||
| 	// set message | ||||
| @@ -842,7 +834,21 @@ func (self *httpFrontend) handle_postRequest(pr *postRequest, b bannedFunc, e er | ||||
| 	} | ||||
|  | ||||
| 	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 | ||||
|   | ||||
| @@ -135,7 +135,7 @@ func (self *nntpArticle) Reset() { | ||||
| 	self.boundary = "" | ||||
| 	self.message = "" | ||||
| 	if self.attachments != nil { | ||||
| 		for idx, _ := range self.attachments { | ||||
| 		for idx := range self.attachments { | ||||
| 			self.attachments[idx].Reset() | ||||
| 			self.attachments[idx] = nil | ||||
| 		} | ||||
| @@ -156,7 +156,7 @@ func newPlaintextArticle(message, email, subject, name, instance, message_id, ne | ||||
| 	nntp := &nntpArticle{ | ||||
| 		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) | ||||
| 	if isSage(subject) { | ||||
| 		nntp.headers.Set("X-Sage", "1") | ||||
| @@ -296,18 +296,30 @@ func (self *nntpArticle) Newsgroup() string { | ||||
|  | ||||
| func (self *nntpArticle) Name() string { | ||||
| 	const defname = "Anonymous" | ||||
|  | ||||
| 	from := strings.TrimSpace(self.headers.Get("From", "")) | ||||
| 	if from == "" { | ||||
| 		return defname | ||||
| 	} | ||||
|  | ||||
| 	a, e := mail.ParseAddress(from) | ||||
| 	var name string | ||||
| 	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 == "" { | ||||
| 		return defname | ||||
| 	} | ||||
|  | ||||
| 	return name | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -914,7 +914,25 @@ func (self *nntpConnection) handleLine(daemon *NNTPDaemon, code int, line string | ||||
| 						for _, model := range models { | ||||
| 							if model != 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 { | ||||
| 				log.Println("failed to read entire message", e) | ||||
| 			} | ||||
| 			pw_in.Close() | ||||
| 			pr_in.Close() | ||||
| 			pw_in.CloseWithError(e) | ||||
| 			pr_in.CloseWithError(e) | ||||
| 		}() | ||||
| 		r := bufio.NewReader(pr_out) | ||||
| 		m, e := readMIMEHeader(r) | ||||
| @@ -579,7 +579,7 @@ func (self *articleStore) ProcessMessage(wr io.Writer, msg io.Reader, spamfilter | ||||
| 			return | ||||
| 		} | ||||
| 		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 | ||||
| } | ||||
| @@ -632,7 +632,7 @@ func read_message_body(body io.Reader, hdr map[string][]string, store ArticleSto | ||||
| 		body = io.TeeReader(body, wr) | ||||
| 	} | ||||
| 	boundary, ok := params["boundary"] | ||||
| 	if ok || content_type == "multipart/mixed" { | ||||
| 	if strings.HasPrefix(media_type, "multipart/") && ok { | ||||
| 		partReader := multipart.NewReader(body, boundary) | ||||
| 		for { | ||||
| 			part, err := partReader.NextPart() | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import ( | ||||
| 	"golang.org/x/crypto/ed25519" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"mime" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/mail" | ||||
| @@ -26,6 +27,7 @@ import ( | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 	"unicode" | ||||
| ) | ||||
|  | ||||
| func DelFile(fname string) { | ||||
| @@ -90,6 +92,10 @@ func ValidMessageID(id string) bool { | ||||
| 		strings.IndexAny(id[1:len(id)-1], "/\\") < 0 | ||||
| } | ||||
|  | ||||
| func ReservedMessageID(id string) bool { | ||||
| 	return id == "<0>" || id == "<keepalive@dummy.tld>" | ||||
| } | ||||
|  | ||||
| // message id hash | ||||
| func HashMessageID(msgid string) string { | ||||
| 	return fmt.Sprintf("%x", sha1.Sum([]byte(msgid))) | ||||
| @@ -161,6 +167,133 @@ func nntpSanitize(data string) (ret string) { | ||||
| 	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 | ||||
|  | ||||
| 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") | ||||
| 		_, err = io.Copy(Discard, body) | ||||
| 		return | ||||
| 	} else if ValidMessageID(msgid) { | ||||
| 	} else if ValidMessageID(msgid) && !ReservedMessageID(msgid) { | ||||
| 		f = daemon.store.CreateFile(msgid) | ||||
| 	} else { | ||||
| 		// invalid message-id | ||||
| @@ -785,9 +918,9 @@ func storeMessage(daemon *NNTPDaemon, hdr textproto.MIMEHeader, body io.Reader) | ||||
| 	} | ||||
|  | ||||
| 	// ask for replies | ||||
| 	replyTos := strings.Split(hdr.Get("Reply-To"), " ") | ||||
| 	replyTos := strings.Split(hdr.Get("In-Reply-To"), " ") | ||||
| 	for _, reply := range replyTos { | ||||
| 		if ValidMessageID(reply) { | ||||
| 		if ValidMessageID(reply) && !ReservedMessageID(reply) { | ||||
| 			if !daemon.store.HasArticle(reply) { | ||||
| 				go daemon.askForArticle(reply) | ||||
| 			} | ||||
| @@ -801,8 +934,8 @@ func storeMessage(daemon *NNTPDaemon, hdr textproto.MIMEHeader, body io.Reader) | ||||
| 	go func() { | ||||
| 		var buff [65536]byte | ||||
| 		writeMIMEHeader(pw, hdr) | ||||
| 		io.CopyBuffer(pw, body, buff[:]) | ||||
| 		pw.Close() | ||||
| 		_, e := io.CopyBuffer(pw, body, buff[:]) | ||||
| 		pw.CloseWithError(e) | ||||
| 	}() | ||||
| 	err = daemon.store.ProcessMessage(f, pr, daemon.CheckText, hdr.Get("Newsgroups")) | ||||
| 	pr.Close() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user