292 lines
5.7 KiB
Go
292 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"container/ring"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/textproto"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.maride.cc/maride/go-nntp-plusplus"
|
|
"git.maride.cc/maride/go-nntp-plusplus/server"
|
|
)
|
|
|
|
const maxArticles = 100
|
|
|
|
type articleRef struct {
|
|
msgid string
|
|
num int64
|
|
}
|
|
|
|
type groupStorage struct {
|
|
group *nntp.Group
|
|
// article refs
|
|
articles *ring.Ring
|
|
}
|
|
|
|
type articleStorage struct {
|
|
headers textproto.MIMEHeader
|
|
body string
|
|
refcount int
|
|
}
|
|
|
|
type testBackendType struct {
|
|
// group name -> group storage
|
|
groups map[string]*groupStorage
|
|
// message ID -> article
|
|
articles map[string]*articleStorage
|
|
}
|
|
|
|
var testBackend = testBackendType{
|
|
groups: map[string]*groupStorage{},
|
|
articles: map[string]*articleStorage{},
|
|
}
|
|
|
|
func init() {
|
|
|
|
testBackend.groups["alt.test"] = &groupStorage{
|
|
group: &nntp.Group{
|
|
Name: "alt.test",
|
|
Description: "A test.",
|
|
Posting: nntp.PostingNotPermitted},
|
|
articles: ring.New(maxArticles),
|
|
}
|
|
|
|
testBackend.groups["misc.test"] = &groupStorage{
|
|
group: &nntp.Group{
|
|
Name: "misc.test",
|
|
Description: "More testing.",
|
|
Posting: nntp.PostingPermitted},
|
|
articles: ring.New(maxArticles),
|
|
}
|
|
|
|
}
|
|
|
|
func (tb *testBackendType) ListGroups(max int) ([]*nntp.Group, error) {
|
|
rv := []*nntp.Group{}
|
|
for _, g := range tb.groups {
|
|
rv = append(rv, g.group)
|
|
}
|
|
return rv, nil
|
|
}
|
|
|
|
func (tb *testBackendType) GetGroup(name string) (*nntp.Group, error) {
|
|
var group *nntp.Group
|
|
|
|
for _, g := range tb.groups {
|
|
if g.group.Name == name {
|
|
group = g.group
|
|
break
|
|
}
|
|
}
|
|
|
|
if group == nil {
|
|
return nil, nntpserver.ErrNoSuchGroup
|
|
}
|
|
|
|
return group, nil
|
|
}
|
|
|
|
func mkArticle(a *articleStorage) *nntp.Article {
|
|
return &nntp.Article{
|
|
Header: a.headers,
|
|
Body: strings.NewReader(a.body),
|
|
Bytes: len(a.body),
|
|
Lines: strings.Count(a.body, "\n"),
|
|
}
|
|
}
|
|
|
|
func findInRing(in *ring.Ring, f func(r interface{}) bool) *ring.Ring {
|
|
if f(in.Value) {
|
|
return in
|
|
}
|
|
for p := in.Next(); p != in; p = p.Next() {
|
|
if f(p.Value) {
|
|
return p
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (tb *testBackendType) GetArticle(group *nntp.Group, id string) (*nntp.Article, error) {
|
|
|
|
msgID := id
|
|
var a *articleStorage
|
|
|
|
if intid, err := strconv.ParseInt(id, 10, 64); err == nil {
|
|
msgID = ""
|
|
// by int ID. Gotta go find it.
|
|
if groupStorage, ok := tb.groups[group.Name]; ok {
|
|
r := findInRing(groupStorage.articles, func(v interface{}) bool {
|
|
if v != nil {
|
|
log.Printf("Looking at %v", v)
|
|
}
|
|
if aref, ok := v.(articleRef); ok && aref.num == intid {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
if aref, ok := r.Value.(articleRef); ok {
|
|
msgID = aref.msgid
|
|
}
|
|
}
|
|
}
|
|
|
|
a = tb.articles[msgID]
|
|
if a == nil {
|
|
return nil, nntpserver.ErrInvalidMessageID
|
|
}
|
|
|
|
return mkArticle(a), nil
|
|
}
|
|
|
|
// Because I suck at ring, I'm going to just post-sort these.
|
|
type nalist []nntpserver.NumberedArticle
|
|
|
|
func (n nalist) Len() int {
|
|
return len(n)
|
|
}
|
|
|
|
func (n nalist) Less(i, j int) bool {
|
|
return n[i].Num < n[j].Num
|
|
}
|
|
|
|
func (n nalist) Swap(i, j int) {
|
|
n[i], n[j] = n[j], n[i]
|
|
}
|
|
|
|
func (tb *testBackendType) GetArticles(group *nntp.Group,
|
|
from, to int64) ([]nntpserver.NumberedArticle, error) {
|
|
|
|
gs, ok := tb.groups[group.Name]
|
|
if !ok {
|
|
return nil, nntpserver.ErrNoSuchGroup
|
|
}
|
|
|
|
log.Printf("Getting articles from %d to %d", from, to)
|
|
|
|
rv := []nntpserver.NumberedArticle{}
|
|
gs.articles.Do(func(v interface{}) {
|
|
if v != nil {
|
|
if aref, ok := v.(articleRef); ok {
|
|
if aref.num >= from && aref.num <= to {
|
|
a, ok := tb.articles[aref.msgid]
|
|
if ok {
|
|
article := mkArticle(a)
|
|
rv = append(rv,
|
|
nntpserver.NumberedArticle{
|
|
Num: aref.num,
|
|
Article: article})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
sort.Sort(nalist(rv))
|
|
|
|
return rv, nil
|
|
}
|
|
|
|
func (tb *testBackendType) AllowPost() bool {
|
|
return true
|
|
}
|
|
|
|
func (tb *testBackendType) decr(msgid string) {
|
|
if a, ok := tb.articles[msgid]; ok {
|
|
a.refcount--
|
|
if a.refcount == 0 {
|
|
log.Printf("Getting rid of %v", msgid)
|
|
delete(tb.articles, msgid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tb *testBackendType) Post(article *nntp.Article) error {
|
|
log.Printf("Got headers: %#v", article.Header)
|
|
b := []byte{}
|
|
buf := bytes.NewBuffer(b)
|
|
n, err := io.Copy(buf, article.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Printf("Read %d bytes of body", n)
|
|
|
|
a := articleStorage{
|
|
headers: article.Header,
|
|
body: buf.String(),
|
|
refcount: 0,
|
|
}
|
|
|
|
msgID := a.headers.Get("Message-Id")
|
|
|
|
if _, ok := tb.articles[msgID]; ok {
|
|
return nntpserver.ErrPostingFailed
|
|
}
|
|
|
|
for _, g := range article.Header["Newsgroups"] {
|
|
if g, ok := tb.groups[g]; ok {
|
|
g.articles = g.articles.Next()
|
|
if g.articles.Value != nil {
|
|
aref := g.articles.Value.(articleRef)
|
|
tb.decr(aref.msgid)
|
|
}
|
|
if g.articles.Value != nil || g.group.Low == 0 {
|
|
g.group.Low++
|
|
}
|
|
g.group.High++
|
|
g.articles.Value = articleRef{
|
|
msgID,
|
|
g.group.High,
|
|
}
|
|
log.Printf("Placed %v", g.articles.Value)
|
|
a.refcount++
|
|
g.group.Count = int64(g.articles.Len())
|
|
|
|
log.Printf("Stored %v in %v", msgID, g.group.Name)
|
|
}
|
|
}
|
|
|
|
if a.refcount > 0 {
|
|
tb.articles[msgID] = &a
|
|
} else {
|
|
return nntpserver.ErrPostingFailed
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (tb *testBackendType) Authorized() bool {
|
|
return true
|
|
}
|
|
|
|
func (tb *testBackendType) Authenticate(user, pass string) (nntpserver.Backend, error) {
|
|
return nil, nntpserver.ErrAuthRejected
|
|
}
|
|
|
|
func maybefatal(err error, f string, a ...interface{}) {
|
|
if err != nil {
|
|
log.Fatalf(f, a...)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
a, err := net.ResolveTCPAddr("tcp", ":1119")
|
|
maybefatal(err, "Error resolving listener: %v", err)
|
|
l, err := net.ListenTCP("tcp", a)
|
|
maybefatal(err, "Error setting up listener: %v", err)
|
|
defer l.Close()
|
|
|
|
s := nntpserver.NewServer(&testBackend)
|
|
|
|
for {
|
|
c, err := l.AcceptTCP()
|
|
maybefatal(err, "Error accepting connection: %v", err)
|
|
go s.Process(c)
|
|
}
|
|
}
|