Archived
1
0
This repository has been archived on 2023-08-12. You can view files and clone it, but cannot push or open issues or pull requests.
nntpchan/contrib/backends/srndv2/src/srnd/mod.go

365 lines
9.5 KiB
Go
Raw Normal View History

//
// mod.go
// post moderation
//
package srnd
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
)
// regenerate pages function
type RegenFunc func(newsgroup, msgid, root string, page int)
// does an action for the administrator
// takes in json
type AdminFunc func(param map[string]interface{}) (interface{}, error)
// interface for moderation ui
type ModUI interface {
// channel for daemon to poll for nntp articles from the mod ui
MessageChan() chan NNTPMessage
// check if this key is allowed to access
// return true if it can otherwise false
CheckKey(privkey, scope string) (bool, error)
// serve the base page
ServeModPage(wr http.ResponseWriter, r *http.Request)
// handle a login POST request
HandleLogin(wr http.ResponseWriter, r *http.Request)
// handle a delete article request
HandleDeletePost(wr http.ResponseWriter, r *http.Request)
// handle a ban address request
HandleBanAddress(wr http.ResponseWriter, r *http.Request)
// handle an unban address request
HandleUnbanAddress(wr http.ResponseWriter, r *http.Request)
// handle add a pubkey
HandleAddPubkey(wr http.ResponseWriter, r *http.Request)
// handle removing a pubkey
HandleDelPubkey(wr http.ResponseWriter, r *http.Request)
// handle key generation
HandleKeyGen(wr http.ResponseWriter, r *http.Request)
// handle admin command
HandleAdminCommand(wr http.ResponseWriter, r *http.Request)
}
type ModEvent interface {
// turn it into a string for putting into an article
String() string
// what type of mod event
Action() string
// what reason for the event
Reason() string
// what is the event acting on
Target() string
// scope of the event, regex of newsgroup
Scope() string
// when this mod event expires, unix nano
Expires() int64
}
type simpleModEvent string
func (self simpleModEvent) String() string {
return string(self)
}
func (self simpleModEvent) Action() string {
return strings.Split(string(self), " ")[0]
}
func (self simpleModEvent) Reason() string {
return ""
}
func (self simpleModEvent) Target() string {
return strings.Split(string(self), " ")[1]
}
func (self simpleModEvent) Scope() string {
// TODO: hard coded
return "overchan.*"
}
func (self simpleModEvent) Expires() int64 {
// no expiration
return -1
}
// create an overchan-delete mod event
func overchanDelete(msgid string) ModEvent {
return simpleModEvent(fmt.Sprintf("delete %s", msgid))
}
// create an overchan-inet-ban mod event
func overchanInetBan(encAddr, key string, expire int64) ModEvent {
return simpleModEvent(fmt.Sprintf("overchan-inet-ban %s:%s:%d", encAddr, key, expire))
}
// moderation message
// wraps multiple mod events
// is turned into an NNTPMessage later
type ModMessage []ModEvent
// write this mod message's body
func (self ModMessage) WriteTo(wr io.Writer, delim []byte) (err error) {
// write body
for _, ev := range self {
_, err = io.WriteString(wr, ev.String())
_, err = wr.Write(delim)
}
return
}
func ParseModEvent(line string) ModEvent {
return simpleModEvent(line)
}
// wrap mod message in an nntp message
// does not sign
func wrapModMessage(mm ModMessage) NNTPMessage {
pathname := "nntpchan.censor"
nntp := &nntpArticle{
headers: make(ArticleHeaders),
}
nntp.headers.Set("Newsgroups", "ctl")
nntp.headers.Set("Content-Type", "text/plain; charset=UTF-8")
nntp.headers.Set("Message-ID", genMessageID(pathname))
nntp.headers.Set("Date", timeNowStr())
nntp.headers.Set("Path", pathname)
// todo: set these maybe?
nntp.headers.Set("From", "anon <a@n.on>")
nntp.headers.Set("Subject", "censor")
var buff bytes.Buffer
// crlf delimited
_ = mm.WriteTo(&buff, []byte{10})
// create plaintext attachment, cut off last 2 bytes
nntp.message = buff.String()
buff.Reset()
return nntp
}
type ModEngine interface {
// chan to send the mod engine posts given message_id
MessageChan() chan string
// delete post of a poster
DeletePost(msgid string, regen RegenFunc) error
// ban a cidr
BanAddress(cidr string) error
// do we allow this public key to delete this message-id ?
AllowDelete(pubkey, msgid string) bool
// do we allow this public key to do inet-ban?
AllowBan(pubkey string) bool
// load a mod message
LoadMessage(msgid string) NNTPMessage
}
type modEngine struct {
database Database
store ArticleStore
chnl chan string
}
func (self modEngine) LoadMessage(msgid string) NNTPMessage {
return self.store.GetMessage(msgid)
}
func (self modEngine) MessageChan() chan string {
return self.chnl
}
func (self modEngine) BanAddress(cidr string) (err error) {
return self.database.BanAddr(cidr)
}
func (self modEngine) DeletePost(msgid string, regen RegenFunc) (err error) {
hdr, err := self.database.GetHeadersForMessage(msgid)
var delposts []string
var page int64
var ref, group string
rootmsgid := ""
if hdr == nil {
log.Println("failed to get headers for article", msgid, err)
} else {
ref = hdr.Get("References", "")
group = hdr.Get("Newsgroups", "")
if ref == "" || ref == msgid {
// is root post
// delete replies too
repls := self.database.GetThreadReplies(msgid, 0, 0)
if repls == nil {
log.Println("cannot get thread replies for", msgid)
} else {
delposts = append(delposts, repls...)
}
_, page, err = self.database.GetPageForRootMessage(msgid)
ref = msgid
rootmsgid = msgid
} else {
_, page, err = self.database.GetPageForRootMessage(ref)
}
}
delposts = append(delposts, msgid)
// get list of files to delete
var delfiles []string
for _, delmsg := range delposts {
article := self.store.GetFilename(delmsg)
delfiles = append(delfiles, article)
// get attachments for post
atts := self.database.GetPostAttachments(delmsg)
if atts != nil {
for _, att := range atts {
img := self.store.AttachmentFilepath(att)
thm := self.store.ThumbnailFilepath(att)
delfiles = append(delfiles, img, thm)
}
}
}
// delete all files
for _, f := range delfiles {
log.Printf("delete file: %s", f)
os.Remove(f)
}
if rootmsgid != "" {
self.database.DeleteThread(rootmsgid)
}
for _, delmsg := range delposts {
// delete article from post database
err = self.database.DeleteArticle(delmsg)
if err != nil {
log.Println(err)
}
// ban article
self.database.BanArticle(delmsg, "deleted by moderator")
}
regen(group, msgid, ref, int(page))
return nil
}
func (self modEngine) AllowBan(pubkey string) bool {
is_admin, _ := self.database.CheckAdminPubkey(pubkey)
if is_admin {
// admins can do whatever
return true
}
return self.database.CheckModPubkeyGlobal(pubkey)
}
func (self modEngine) AllowDelete(pubkey, msgid string) (allow bool) {
is_admin, _ := self.database.CheckAdminPubkey(pubkey)
if is_admin {
// admins can do whatever
return true
}
if self.database.CheckModPubkeyGlobal(pubkey) {
// globals can delete as they wish
return true
}
// check for scoped permissions
_, group, _, err := self.database.GetInfoForMessage(msgid)
if err == nil && newsgroupValidFormat(group) {
allow = self.database.CheckModPubkeyCanModGroup(pubkey, group)
} else if err != nil {
log.Println("db error in mod engine while checking permissions", err)
}
return
}
// run a mod engine logic mainloop
func RunModEngine(mod ModEngine, regen RegenFunc) {
chnl := mod.MessageChan()
for {
msgid := <-chnl
nntp := mod.LoadMessage(msgid)
if nntp == nil {
log.Println("failed to load mod message", msgid)
continue
}
// sanity check
if nntp.Newsgroup() == "ctl" {
pubkey := nntp.Pubkey()
for _, line := range strings.Split(nntp.Message(), "\n") {
line = strings.Trim(line, "\r\t\n ")
ev := ParseModEvent(line)
action := ev.Action()
if action == "delete" {
msgid := ev.Target()
if !ValidMessageID(msgid) {
// invalid message-id
log.Println("invalid message-id for mod delete", msgid, "from", pubkey)
continue
}
// this is a delete action
if mod.AllowDelete(pubkey, msgid) {
err := mod.DeletePost(msgid, regen)
if err != nil {
log.Println(msgid, err)
}
} else {
log.Printf("pubkey=%s will not delete %s not trusted", pubkey, msgid)
}
} else if action == "overchan-inet-ban" {
// ban action
target := ev.Target()
if target[0] == '[' {
// probably a literal ipv6 rangeban
if mod.AllowBan(pubkey) {
err := mod.BanAddress(target)
if err != nil {
log.Println("failed to do literal ipv6 range ban on", target, err)
}
} else {
log.Println("ignoring literal ipv6 rangeban from", pubkey, "as they are not allowed to ban")
}
continue
}
parts := strings.Split(target, ":")
if len(parts) == 3 {
// encrypted ip
encaddr, key := parts[0], parts[1]
cidr := decAddr(encaddr, key)
if cidr == "" {
log.Println("failed to decrypt inet ban")
} else if mod.AllowBan(pubkey) {
err := mod.BanAddress(cidr)
if err != nil {
log.Println("failed to do range ban on", cidr, err)
}
} else {
log.Println("ingoring encrypted-ip inet ban from", pubkey, "as they are not allowed to ban")
}
} else if len(parts) == 1 {
// literal cidr
cidr := parts[0]
if mod.AllowBan(pubkey) {
err := mod.BanAddress(cidr)
if err != nil {
log.Println("failed to do literal range ban on", cidr, err)
}
} else {
log.Println("ingoring literal cidr range ban from", pubkey, "as they are not allowed to ban")
}
} else {
log.Printf("invalid overchan-inet-ban: target=%s", target)
}
} else {
log.Println("invalid mod action", action, "from", pubkey)
}
}
}
}
}