go-nntp-plusplus/examples/couchserver/couchserver.go

345 lines
7.6 KiB
Go
Raw Normal View History

2012-02-23 09:50:22 +00:00
package main
import (
"bytes"
2012-02-24 00:19:48 +00:00
"encoding/base64"
"encoding/json"
2012-02-23 09:50:22 +00:00
"flag"
2012-02-24 00:19:48 +00:00
"fmt"
2012-02-23 09:50:22 +00:00
"io"
"log"
2012-10-25 03:25:43 +00:00
"log/syslog"
2012-02-23 09:50:22 +00:00
"net"
"net/textproto"
"net/url"
2012-02-23 09:50:22 +00:00
"strconv"
"strings"
2012-02-25 21:27:35 +00:00
"sync"
"sync/atomic"
"time"
2012-02-23 09:50:22 +00:00
2023-11-12 12:48:11 +00:00
"git.maride.cc/maride/go-nntp-plusplus"
"git.maride.cc/maride/go-nntp-plusplus/server"
2012-02-23 09:50:22 +00:00
2013-12-05 09:21:56 +00:00
"github.com/dustin/go-couch"
2012-02-23 09:50:22 +00:00
)
var groupCacheTimeout = flag.Int("groupTimeout", 300,
2012-02-25 21:27:35 +00:00
"Time (in seconds), group cache is valid")
var optimisticPost = flag.Bool("optimistic", false,
"Optimistically return success on store before storing")
2012-10-25 03:25:43 +00:00
var useSyslog = flag.Bool("syslog", false,
"Log to syslog")
2012-02-25 21:27:35 +00:00
2014-02-28 06:14:03 +00:00
type groupRow struct {
2012-02-23 09:50:22 +00:00
Group string `json:"key"`
Value []interface{} `json:"value"`
}
2014-02-28 06:14:03 +00:00
type groupResults struct {
Rows []groupRow
2012-02-23 09:50:22 +00:00
}
2014-02-28 06:14:03 +00:00
type attachment struct {
2012-02-24 00:19:48 +00:00
Type string `json:"content-type"`
Data []byte `json:"data"`
}
func removeSpace(r rune) rune {
if r == ' ' || r == '\n' || r == '\r' {
return -1
}
return r
}
2014-02-28 06:14:03 +00:00
func (a *attachment) MarshalJSON() ([]byte, error) {
2012-02-24 00:19:48 +00:00
m := map[string]string{
"content_type": a.Type,
"data": strings.Map(removeSpace, base64.StdEncoding.EncodeToString(a.Data)),
}
return json.Marshal(m)
}
2014-02-28 06:14:03 +00:00
type article struct {
MsgID string `json:"_id"`
2012-02-24 00:19:48 +00:00
DocType string `json:"type"`
Headers map[string][]string `json:"headers"`
Bytes int `json:"bytes"`
Lines int `json:"lines"`
Nums map[string]int64 `json:"nums"`
2014-02-28 06:14:03 +00:00
Attachments map[string]*attachment `json:"_attachments"`
2012-02-25 22:31:12 +00:00
Added time.Time `json:"added"`
2012-02-23 09:50:22 +00:00
}
2014-02-28 06:14:03 +00:00
type articleResults struct {
2012-02-23 09:50:22 +00:00
Rows []struct {
Key []interface{} `json:"key"`
2014-02-28 06:14:03 +00:00
Article article `json:"doc"`
2012-02-23 09:50:22 +00:00
}
}
type couchBackend struct {
2012-02-25 21:27:35 +00:00
db *couch.Database
groups map[string]*nntp.Group
grouplock sync.Mutex
2012-02-23 09:50:22 +00:00
}
2012-02-25 21:27:35 +00:00
func (cb *couchBackend) clearGroups() {
cb.grouplock.Lock()
defer cb.grouplock.Unlock()
log.Printf("Dumping group cache")
cb.groups = nil
}
func (cb *couchBackend) fetchGroups() error {
cb.grouplock.Lock()
defer cb.grouplock.Unlock()
if cb.groups != nil {
return nil
}
log.Printf("Filling group cache")
2014-02-28 06:14:03 +00:00
results := groupResults{}
err := cb.db.Query("_design/groups/_view/active", map[string]interface{}{
2012-02-23 09:50:22 +00:00
"group": true,
}, &results)
2012-02-25 21:27:35 +00:00
if err != nil {
return err
}
cb.groups = make(map[string]*nntp.Group)
2012-02-23 09:50:22 +00:00
for _, gr := range results.Rows {
if gr.Value[0].(string) != "" {
group := nntp.Group{
Name: gr.Group,
Description: gr.Value[0].(string),
Count: int64(gr.Value[1].(float64)),
Low: int64(gr.Value[2].(float64)),
High: int64(gr.Value[3].(float64)),
2012-02-25 22:31:12 +00:00
Posting: nntp.PostingPermitted,
}
2012-02-25 21:27:35 +00:00
cb.groups[group.Name] = &group
2012-02-23 09:50:22 +00:00
}
}
2012-02-25 21:27:35 +00:00
go func() {
time.Sleep(time.Duration(*groupCacheTimeout) * time.Second)
cb.clearGroups()
}()
2012-02-23 09:50:22 +00:00
2012-02-25 21:27:35 +00:00
return nil
}
func (cb *couchBackend) ListGroups(max int) ([]*nntp.Group, error) {
if cb.groups == nil {
if err := cb.fetchGroups(); err != nil {
return nil, err
}
}
rv := make([]*nntp.Group, 0, len(cb.groups))
for _, g := range cb.groups {
rv = append(rv, g)
2012-02-23 09:50:22 +00:00
}
2012-02-25 21:27:35 +00:00
return rv, nil
}
2012-02-23 09:50:22 +00:00
2012-02-25 21:27:35 +00:00
func (cb *couchBackend) GetGroup(name string) (*nntp.Group, error) {
if cb.groups == nil {
if err := cb.fetchGroups(); err != nil {
return nil, err
}
2012-02-23 09:50:22 +00:00
}
2012-02-25 21:27:35 +00:00
g, exists := cb.groups[name]
if !exists {
2014-02-26 06:55:09 +00:00
return nil, nntpserver.ErrNoSuchGroup
}
2012-02-25 21:27:35 +00:00
return g, nil
2012-02-23 09:50:22 +00:00
}
2014-02-28 06:14:03 +00:00
func (cb *couchBackend) mkArticle(ar article) *nntp.Article {
url := fmt.Sprintf("%s/%s/article", cb.db.DBURL(), cleanupID(ar.MsgID, true))
2012-02-23 09:50:22 +00:00
return &nntp.Article{
Header: textproto.MIMEHeader(ar.Headers),
2012-02-24 01:32:59 +00:00
Body: &lazyOpener{url, nil, nil},
2012-02-24 00:19:48 +00:00
Bytes: ar.Bytes,
Lines: ar.Lines,
2012-02-23 09:50:22 +00:00
}
}
func (cb *couchBackend) GetArticle(group *nntp.Group, id string) (*nntp.Article, error) {
2014-02-28 06:14:03 +00:00
var ar article
2012-02-23 09:50:22 +00:00
if intid, err := strconv.ParseInt(id, 10, 64); err == nil {
2014-02-28 06:14:03 +00:00
results := articleResults{}
2012-02-23 09:50:22 +00:00
cb.db.Query("_design/articles/_view/list", map[string]interface{}{
"include_docs": true,
2012-02-25 22:31:12 +00:00
"reduce": false,
2012-02-23 09:50:22 +00:00
"key": []interface{}{group.Name, intid},
}, &results)
if len(results.Rows) != 1 {
2014-02-26 06:55:09 +00:00
return nil, nntpserver.ErrInvalidArticleNumber
2012-02-23 09:50:22 +00:00
}
ar = results.Rows[0].Article
} else {
2014-02-28 06:14:03 +00:00
err := cb.db.Retrieve(cleanupID(id, false), &ar)
2012-02-23 09:50:22 +00:00
if err != nil {
2014-02-26 06:55:09 +00:00
return nil, nntpserver.ErrInvalidMessageID
2012-02-23 09:50:22 +00:00
}
}
2012-02-24 00:19:48 +00:00
return cb.mkArticle(ar), nil
2012-02-23 09:50:22 +00:00
}
func (cb *couchBackend) GetArticles(group *nntp.Group,
from, to int64) ([]nntpserver.NumberedArticle, error) {
rv := make([]nntpserver.NumberedArticle, 0, 100)
2014-02-28 06:14:03 +00:00
results := articleResults{}
2012-02-23 09:50:22 +00:00
cb.db.Query("_design/articles/_view/list", map[string]interface{}{
"include_docs": true,
2012-02-25 22:31:12 +00:00
"reduce": false,
2012-02-23 09:50:22 +00:00
"start_key": []interface{}{group.Name, from},
"end_key": []interface{}{group.Name, to},
}, &results)
for _, r := range results.Rows {
rv = append(rv, nntpserver.NumberedArticle{
Num: int64(r.Key[1].(float64)),
2012-02-24 00:19:48 +00:00
Article: cb.mkArticle(r.Article),
2012-02-23 09:50:22 +00:00
})
}
return rv, nil
}
2014-02-28 06:14:03 +00:00
func (cb *couchBackend) AllowPost() bool {
2012-02-23 09:50:22 +00:00
return true
}
2014-02-28 06:14:03 +00:00
func cleanupID(msgid string, escapedAt bool) string {
s := strings.TrimFunc(msgid, func(r rune) bool {
2012-02-23 09:50:22 +00:00
return r == ' ' || r == '<' || r == '>'
})
2012-06-26 08:59:42 +00:00
qe := url.QueryEscape(s)
if escapedAt {
return qe
}
return strings.Replace(qe, "%40", "@", -1)
2012-02-23 09:50:22 +00:00
}
2014-02-28 06:14:03 +00:00
func (cb *couchBackend) Post(art *nntp.Article) error {
a := article{
2012-02-24 00:19:48 +00:00
DocType: "article",
2014-02-28 06:14:03 +00:00
Headers: map[string][]string(art.Header),
2012-02-24 00:19:48 +00:00
Nums: make(map[string]int64),
2014-02-28 06:14:03 +00:00
MsgID: cleanupID(art.Header.Get("Message-Id"), false),
Attachments: make(map[string]*attachment),
2012-02-25 22:31:12 +00:00
Added: time.Now(),
2012-02-23 09:50:22 +00:00
}
b := []byte{}
buf := bytes.NewBuffer(b)
2014-02-28 06:14:03 +00:00
n, err := io.Copy(buf, art.Body)
2012-02-23 09:50:22 +00:00
if err != nil {
return err
}
log.Printf("Read %d bytes of body", n)
2012-02-24 00:19:48 +00:00
b = buf.Bytes()
a.Bytes = len(b)
a.Lines = bytes.Count(b, []byte{'\n'})
2014-02-28 06:14:03 +00:00
a.Attachments["article"] = &attachment{"text/plain", b}
2012-02-23 09:50:22 +00:00
2014-02-28 06:14:03 +00:00
for _, g := range strings.Split(art.Header.Get("Newsgroups"), ",") {
2012-02-23 22:20:54 +00:00
g = strings.TrimSpace(g)
2012-02-23 09:50:22 +00:00
group, err := cb.GetGroup(g)
if err == nil {
2012-02-25 21:27:35 +00:00
a.Nums[g] = atomic.AddInt64(&group.High, 1)
2012-02-25 22:31:12 +00:00
atomic.AddInt64(&group.Count, 1)
2012-02-23 22:08:42 +00:00
} else {
log.Printf("Error getting group %q: %v", g, err)
2012-02-23 09:50:22 +00:00
}
}
if len(a.Nums) == 0 {
2012-02-23 22:08:42 +00:00
log.Printf("Found no matching groups in %v",
2014-02-28 06:14:03 +00:00
art.Header["Newsgroups"])
2014-02-26 06:55:09 +00:00
return nntpserver.ErrPostingFailed
2012-02-23 09:50:22 +00:00
}
if *optimisticPost {
go func() {
_, _, err = cb.db.Insert(&a)
if err != nil {
log.Printf("error optimistically posting article: %v", err)
}
}()
} else {
_, _, err = cb.db.Insert(&a)
if err != nil {
log.Printf("error posting article: %v", err)
2014-02-26 06:55:09 +00:00
return nntpserver.ErrPostingFailed
}
2012-02-23 09:50:22 +00:00
}
return nil
}
2014-02-28 06:14:03 +00:00
func (cb *couchBackend) Authorized() bool {
2012-02-23 09:50:22 +00:00
return true
}
2014-02-28 06:14:03 +00:00
func (cb *couchBackend) Authenticate(user, pass string) (nntpserver.Backend, error) {
2014-02-26 06:55:09 +00:00
return nil, nntpserver.ErrAuthRejected
2012-02-23 09:50:22 +00:00
}
func maybefatal(err error, f string, a ...interface{}) {
if err != nil {
log.Fatalf(f, a...)
}
}
func main() {
2014-02-28 06:14:03 +00:00
couchURL := flag.String("couch", "http://localhost:5984/news",
2012-02-23 09:50:22 +00:00
"Couch DB.")
flag.Parse()
2012-10-25 03:25:43 +00:00
if *useSyslog {
sl, err := syslog.New(syslog.LOG_INFO, "nntpd")
if err != nil {
log.Fatalf("Error initializing syslog: %v", err)
}
log.SetOutput(sl)
log.SetFlags(0)
}
2012-02-23 09:50:22 +00:00
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()
2014-02-28 06:14:03 +00:00
db, err := couch.Connect(*couchURL)
2012-02-23 09:50:22 +00:00
maybefatal(err, "Can't connect to the couch: %v", err)
err = ensureViews(&db)
maybefatal(err, "Error setting up views: %v", err)
2012-02-23 09:50:22 +00:00
backend := couchBackend{
db: &db,
}
s := nntpserver.NewServer(&backend)
for {
c, err := l.AcceptTCP()
maybefatal(err, "Error accepting connection: %v", err)
go s.Process(c)
}
}