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_http.go

734 lines
21 KiB
Go
Raw Normal View History

//
// mod_http.go
//
// http mod panel
//
package srnd
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
"github.com/majestrate/nacl"
"io"
"log"
"net"
"net/http"
"strings"
)
type httpModUI struct {
regenAll func()
regen func(ArticleEntry)
regenGroup func(string)
delete func(string)
deleteBoardPages func(string)
modMessageChan chan NNTPMessage
daemon *NNTPDaemon
articles ArticleStore
store *sessions.CookieStore
prefix string
mod_prefix string
}
func createHttpModUI(frontend *httpFrontend) httpModUI {
return httpModUI{frontend.regenAll, frontend.Regen, frontend.regenerateBoard, frontend.deleteThreadMarkup, frontend.deleteBoardMarkup, make(chan NNTPMessage), frontend.daemon, frontend.daemon.store, frontend.store, frontend.prefix, frontend.prefix + "mod/"}
}
func extractGroup(param map[string]interface{}) string {
return extractParam(param, "newsgroup")
}
func (self httpModUI) getAdminFunc(funcname string) AdminFunc {
if funcname == "template.reload" {
return func(param map[string]interface{}) (interface{}, error) {
tname, ok := param["template"]
if ok {
t := ""
switch tname.(type) {
case string:
t = tname.(string)
default:
return "failed to reload templates", errors.New("invalid parameters")
}
template.reloadTemplate(t)
return "reloaded " + t, nil
}
template.reloadAllTemplates()
return "reloaded all templates", nil
}
} else if funcname == "frontend.regen" {
return func(param map[string]interface{}) (interface{}, error) {
newsgroup := extractGroup(param)
if len(newsgroup) > 0 {
if self.daemon.database.HasNewsgroup(newsgroup) {
go self.regenGroup(newsgroup)
} else {
return "failed to regen group", errors.New("no such board")
}
} else {
go self.regenAll()
}
return "started regeneration", nil
}
} else if funcname == "thumbnail.regen" {
return func(param map[string]interface{}) (interface{}, error) {
threads, ok := param["threads"]
t := 1
if ok {
switch threads.(type) {
case int64:
t = int(threads.(int64))
if t <= 0 {
return "failed to regen thumbnails", errors.New("invalid number of threads")
}
default:
return "failed to regen thumbnails", errors.New("invalid parameters")
}
}
log.Println("regenerating all thumbnails with", t, "threads")
msgid := extractParam(param, "message-id")
if ValidMessageID(msgid) {
a := self.daemon.database.GetPostAttachments(msgid)
go func(atts []string) {
for _, att := range atts {
self.articles.GenerateThumbnail(att)
}
}(a)
return fmt.Sprintf("regenerating %d thumbnails for %s", len(a), msgid), nil
}
go reThumbnail(t, self.articles, true)
return fmt.Sprintf("started rethumbnailing with %d threads", t), nil
}
} else if funcname == "frontend.add" {
return func(param map[string]interface{}) (interface{}, error) {
newsgroup := extractGroup(param)
if len(newsgroup) > 0 && newsgroupValidFormat(newsgroup) && strings.HasPrefix(newsgroup, "overchan.") && newsgroup != "overchan." {
if self.daemon.database.HasNewsgroup(newsgroup) {
// we already have this newsgroup
return "already have that newsgroup", nil
} else {
// we dont got this newsgroup
log.Println("adding newsgroup", newsgroup)
self.daemon.database.RegisterNewsgroup(newsgroup)
return "added " + newsgroup, nil
}
}
return "bad newsgroup", errors.New("invalid newsgroup name: " + newsgroup)
}
} else if funcname == "frontend.ban" {
return func(param map[string]interface{}) (interface{}, error) {
newsgroup := extractGroup(param)
if len(newsgroup) > 0 {
log.Println("banning", newsgroup)
// check ban
banned, err := self.daemon.database.NewsgroupBanned(newsgroup)
if banned {
// already banned
return "cannot ban newsgroup", errors.New("already banned " + newsgroup)
} else if err == nil {
// do the ban here
err = self.daemon.database.BanNewsgroup(newsgroup)
// check for error
if err == nil {
// all gud
return "banned " + newsgroup, nil
} else {
// error while banning
return "error banning newsgroup", err
}
} else {
// error checking ban
return "cannot check ban", err
}
} else {
// bad parameters
return "cannot ban newsgroup", errors.New("invalid parameters")
}
}
} else if funcname == "frontend.unban" {
return func(param map[string]interface{}) (interface{}, error) {
newsgroup := extractGroup(param)
if len(newsgroup) > 0 {
log.Println("unbanning", newsgroup)
err := self.daemon.database.UnbanNewsgroup(newsgroup)
if err == nil {
return "unbanned " + newsgroup, nil
} else {
return "couldn't unban " + newsgroup, err
}
} else {
return "cannot unban", errors.New("invalid paramters")
}
}
} else if funcname == "frontend.nuke" {
return func(param map[string]interface{}) (interface{}, error) {
newsgroup := extractGroup(param)
if len(newsgroup) > 0 {
log.Println("nuking", newsgroup)
// get every thread we have in this group
for _, entry := range self.daemon.database.GetLastBumpedThreads(newsgroup, 10000) {
// delete their thread page
self.delete(entry.MessageID())
}
// delete every board page
self.deleteBoardPages(newsgroup)
go self.daemon.database.NukeNewsgroup(newsgroup, self.articles)
return "nuke started", nil
} else {
return "cannot nuke", errors.New("invalid parameters")
}
}
} else if funcname == "pubkey.add" {
return func(param map[string]interface{}) (interface{}, error) {
pubkey := extractParam(param, "pubkey")
group := extractGroup(param)
if group == "" {
log.Println("pubkey.add global mod", pubkey)
if self.daemon.database.CheckModPubkeyGlobal(pubkey) {
return "already added", nil
} else {
err := self.daemon.database.MarkModPubkeyGlobal(pubkey)
if err == nil {
return "added", nil
} else {
return "error", err
}
}
} else if newsgroupValidFormat(group) {
log.Println("pubkey.add", group, "mod", pubkey)
if self.daemon.database.CheckModPubkeyCanModGroup(pubkey, group) {
return "already added", nil
}
err := self.daemon.database.MarkModPubkeyCanModGroup(pubkey, group)
if err == nil {
return "added", nil
} else {
return "error", err
}
} else {
return "bad newsgroup: " + group, nil
}
}
} else if funcname == "pubkey.del" {
return func(param map[string]interface{}) (interface{}, error) {
pubkey := extractParam(param, "pubkey")
log.Println("pubkey.del", pubkey)
if self.daemon.database.CheckModPubkeyGlobal(pubkey) {
err := self.daemon.database.UnMarkModPubkeyGlobal(pubkey)
if err == nil {
return "removed", nil
} else {
return "error", err
}
} else {
return "key not already trusted", nil
}
}
} else if funcname == "nntp.login.del" {
return func(param map[string]interface{}) (interface{}, error) {
username := extractParam(param, "username")
if len(username) > 0 {
exists, err := self.daemon.database.CheckNNTPUserExists(username)
if exists {
err = self.daemon.database.RemoveNNTPLogin(username)
if err == nil {
return "removed user", nil
}
return "", nil
} else if err == nil {
return "no such user", nil
} else {
return "", err
}
} else {
return "no such user", nil
}
}
} else if funcname == "nntp.login.add" {
return func(param map[string]interface{}) (interface{}, error) {
username := extractParam(param, "username")
passwd := extractParam(param, "passwd")
if len(username) > 0 && len(passwd) > 0 {
log.Println("nntp.login.add", username)
// check if users is there
exists, err := self.daemon.database.CheckNNTPUserExists(username)
if exists {
// user is already there
return "nntp user already exists", nil
} else if err == nil {
// now add the user
err = self.daemon.database.AddNNTPLogin(username, passwd)
// success adding?
if err == nil {
// yeh
return "added nntp user", nil
}
// nah
return "", err
} else {
// error happened
return "", err
}
} else {
return "invalid username or password format", nil
}
}
} else if funcname == "feed.add" {
return func(param map[string]interface{}) (interface{}, error) {
host := extractParam(param, "host")
port := extractParam(param, "port")
name := extractParam(param, "name")
if len(host) == 0 || len(port) == 0 || len(name) == 0 {
// bad parameter
return "", errors.New("please specific host, port and name")
}
// make new config
conf := FeedConfig{
policy: FeedPolicy{
// default rules for default policy
rules: map[string]string{"overchan.*": "0", "ctl": "1"},
},
Addr: host + ":" + port,
Name: name,
quarks: make(map[string]string),
}
err := self.daemon.addFeed(conf)
if err == nil {
return "feed added", err
} else {
return "", err
}
}
} else if funcname == "feed.list" {
return func(_ map[string]interface{}) (interface{}, error) {
feeds := self.daemon.activeFeeds()
return feeds, nil
}
} else if funcname == "feed.sync" {
return func(_ map[string]interface{}) (interface{}, error) {
go self.daemon.syncAllMessages()
return "sync started", nil
}
} else if funcname == "feed.del" {
return func(param map[string]interface{}) (interface{}, error) {
name := extractParam(param, "name")
self.daemon.removeFeed(name)
return "okay", nil
}
} else if funcname == "store.expire" {
return func(_ map[string]interface{}) (interface{}, error) {
if self.daemon.expire == nil {
// TODO: expire orphans?
return "archive mode enabled, will not expire orphans", nil
} else {
go self.daemon.expire.ExpireOrphans()
return "expiration started", nil
}
}
} else if funcname == "frontend.posts" {
// get all posts given parameters
return func(param map[string]interface{}) (interface{}, error) {
// by cidr
cidr := extractParam(param, "cidr")
// by encrypted ip
encip := extractParam(param, "encip")
var err error
var post_msgids []string
if len(cidr) > 0 {
var cnet *net.IPNet
_, cnet, err = net.ParseCIDR(cidr)
if err == nil {
post_msgids, err = self.daemon.database.GetMessageIDByCIDR(cnet)
}
} else if len(encip) > 0 {
post_msgids, err = self.daemon.database.GetMessageIDByEncryptedIP(encip)
}
return post_msgids, err
}
}
return nil
}
// handle an admin action
func (self httpModUI) HandleAdminCommand(wr http.ResponseWriter, r *http.Request) {
self.asAuthed("admin", func(url string) {
action := strings.Split(url, "/admin/")[1]
f := self.getAdminFunc(action)
if f == nil {
wr.WriteHeader(404)
} else {
var result interface{}
var err error
req := make(map[string]interface{})
if r.Method == "POST" {
dec := json.NewDecoder(r.Body)
err = dec.Decode(&req)
r.Body.Close()
}
if err == nil {
result, err = f(req)
}
resp := make(map[string]interface{})
if err == nil {
resp["error"] = nil
} else {
resp["error"] = err.Error()
}
resp["result"] = result
enc := json.NewEncoder(wr)
enc.Encode(resp)
}
}, wr, r)
}
func (self httpModUI) CheckPubkey(pubkey, scope string) (bool, error) {
is_admin, err := self.daemon.database.CheckAdminPubkey(pubkey)
if is_admin {
// admin can do what they want
return true, nil
}
if self.daemon.database.CheckModPubkeyGlobal(pubkey) {
// this user is a global mod, can't do admin
return scope != "admin", nil
}
// check for board specific mods
if strings.Index(scope, "mod-") == 0 {
group := scope[4:]
if self.daemon.database.CheckModPubkeyCanModGroup(pubkey, group) {
return true, nil
}
} else if scope == "login" {
// check if a user can log in
return self.daemon.database.CheckModPubkey(pubkey), nil
}
return false, err
}
func (self httpModUI) CheckKey(privkey, scope string) (bool, error) {
privkey_bytes, err := hex.DecodeString(privkey)
if err == nil {
kp := nacl.LoadSignKey(privkey_bytes)
if kp != nil {
defer kp.Free()
pubkey := hex.EncodeToString(kp.Public())
return self.CheckPubkey(pubkey, scope)
}
}
log.Println("invalid key format for key", privkey)
return false, err
}
func (self httpModUI) MessageChan() chan NNTPMessage {
return self.modMessageChan
}
func (self httpModUI) getSession(r *http.Request) *sessions.Session {
s, _ := self.store.Get(r, "nntpchan-mod")
return s
}
// get the session's private key as bytes or nil if we don't have it
func (self httpModUI) getSessionPrivkeyBytes(r *http.Request) []byte {
s := self.getSession(r)
k, ok := s.Values["privkey"]
if ok {
privkey_bytes, err := hex.DecodeString(k.(string))
if err == nil {
return privkey_bytes
}
log.Println("failed to decode private key bytes from session", err)
} else {
log.Println("failed to get private key from session, no private key in session?")
}
return nil
}
// returns true if the session is okay for a scope
// otherwise redirect to login page
func (self httpModUI) checkSession(r *http.Request, scope string) bool {
s := self.getSession(r)
k, ok := s.Values["privkey"]
if ok {
ok, err := self.CheckKey(k.(string), scope)
if err != nil {
return false
}
return ok
}
return false
}
func (self httpModUI) writeTemplate(wr http.ResponseWriter, r *http.Request, name string) {
self.writeTemplateParam(wr, r, name, nil)
}
func (self httpModUI) writeTemplateParam(wr http.ResponseWriter, r *http.Request, name string, param map[string]interface{}) {
if param == nil {
param = make(map[string]interface{})
}
param[csrf.TemplateTag] = csrf.TemplateField(r)
param["prefix"] = self.prefix
param["mod_prefix"] = self.mod_prefix
io.WriteString(wr, template.renderTemplate(name, param))
}
// do a function as authenticated
// pass in the request path to the handler
func (self httpModUI) asAuthed(scope string, handler func(string), wr http.ResponseWriter, r *http.Request) {
if self.checkSession(r, scope) {
handler(r.URL.Path)
} else {
wr.WriteHeader(403)
}
}
// do stuff to a certain message if with have it and are authed
func (self httpModUI) asAuthedWithMessage(scope string, handler func(ArticleEntry, *http.Request) map[string]interface{}, wr http.ResponseWriter, req *http.Request) {
self.asAuthed(scope, func(path string) {
// get the long hash
if strings.Count(path, "/") > 2 {
// TOOD: prefix detection
longhash := strings.Split(path, "/")[3]
// get the message id
msg, err := self.daemon.database.GetMessageIDByHash(longhash)
resp := make(map[string]interface{})
if err == nil {
group := msg.Newsgroup()
if err == nil {
if self.checkSession(req, "mod-"+group) {
// we can moderate this group
resp = handler(msg, req)
} else {
// no permission to moderate this group
resp["error"] = fmt.Sprint("you don't have permission to moderate '%s'", group)
}
} else {
resp["error"] = err.Error()
}
} else {
resp["error"] = fmt.Sprint("don't have post %s, %s", longhash, err.Error())
}
enc := json.NewEncoder(wr)
enc.Encode(resp)
} else {
wr.WriteHeader(404)
}
}, wr, req)
}
func (self httpModUI) HandleAddPubkey(wr http.ResponseWriter, r *http.Request) {
}
func (self httpModUI) HandleDelPubkey(wr http.ResponseWriter, r *http.Request) {
}
func (self httpModUI) HandleUnbanAddress(wr http.ResponseWriter, r *http.Request) {
self.asAuthed("ban", func(path string) {
// extract the ip address
// TODO: ip ranges and prefix detection
if strings.Count(path, "/") > 2 {
addr := strings.Split(path, "/")[3]
resp := make(map[string]interface{})
banned, err := self.daemon.database.CheckIPBanned(addr)
if err != nil {
resp["error"] = fmt.Sprintf("cannot tell if %s is banned: %s", addr, err.Error())
} else if banned {
// TODO: rangebans
err = self.daemon.database.UnbanAddr(addr)
if err == nil {
resp["result"] = fmt.Sprintf("%s was unbanned", addr)
} else {
resp["error"] = err.Error()
}
} else {
resp["error"] = fmt.Sprintf("%s was not banned", addr)
}
enc := json.NewEncoder(wr)
enc.Encode(resp)
} else {
wr.WriteHeader(404)
}
}, wr, r)
}
// handle ban logic
func (self httpModUI) handleBanAddress(msg ArticleEntry, r *http.Request) map[string]interface{} {
// get the article headers
resp := make(map[string]interface{})
msgid := msg.MessageID()
hdr, err := self.daemon.database.GetHeadersForMessage(msgid)
if hdr == nil {
// we don't got it?!
resp["error"] = fmt.Sprintf("could not load headers for %s: %s", msgid, err.Error())
} else {
// get the associated encrypted ip
encip := hdr.Get("x-encrypted-ip", "")
encip = strings.Trim(encip, "\t ")
if len(encip) == 0 {
// no ip header detected
resp["error"] = fmt.Sprintf("%s has no IP, ban Tor instead", msgid)
} else {
// get the ip address if we have it
ip, err := self.daemon.database.GetIPAddress(encip)
if len(ip) > 0 {
// we have it
// ban the address
err = self.daemon.database.BanAddr(ip)
// then we tell everyone about it
var key string
// TODO: we SHOULD have the key, but what if we do not?
key, err = self.daemon.database.GetEncKey(encip)
// create mod message
// TODO: hardcoded ban period
mm := ModMessage{overchanInetBan(encip, key, -1)}
privkey_bytes := self.getSessionPrivkeyBytes(r)
if privkey_bytes == nil {
// this should not happen
log.Println("failed to get privkey bytes from session")
resp["error"] = "failed to get private key from session. wtf?"
} else {
// wrap and sign
nntp := wrapModMessage(mm)
nntp, err = signArticle(nntp, privkey_bytes)
if err == nil {
// federate
self.modMessageChan <- nntp
}
}
} else {
// we don't have it
// ban the encrypted version
err = self.daemon.database.BanEncAddr(encip)
}
if err == nil {
result_msg := fmt.Sprintf("We banned %s", encip)
if len(ip) > 0 {
result_msg += fmt.Sprintf(" (%s)", ip)
}
resp["banned"] = result_msg
} else {
resp["error"] = err.Error()
}
}
}
return resp
}
func (self httpModUI) handleDeletePost(msg ArticleEntry, r *http.Request) map[string]interface{} {
var mm ModMessage
resp := make(map[string]interface{})
msgid := msg.MessageID()
mm = append(mm, overchanDelete(msgid))
delmsgs := []string{}
// get headers
hdr, _ := self.daemon.database.GetHeadersForMessage(msgid)
if hdr != nil {
ref := hdr.Get("References", hdr.Get("Reference", ""))
ref = strings.Trim(ref, "\t ")
// is it a root post?
if ref == "" {
// load replies
replies := self.daemon.database.GetThreadReplies(msgid, 0, 0)
if replies != nil {
for _, repl := range replies {
// append mod line to mod message for reply
mm = append(mm, overchanDelete(repl))
// add to delete queue
delmsgs = append(delmsgs, repl)
}
}
}
}
delmsgs = append(delmsgs, msgid)
// append mod line to mod message
resp["deleted"] = delmsgs
// only regen threads when we delete a non root port
privkey_bytes := self.getSessionPrivkeyBytes(r)
if privkey_bytes == nil {
// crap this should never happen
log.Println("failed to get private keys from session, not federating")
} else {
// wrap and sign mod message
nntp, err := signArticle(wrapModMessage(mm), privkey_bytes)
if err == nil {
// send it off to federate
self.modMessageChan <- nntp
} else {
resp["error"] = fmt.Sprintf("signing error: %s", err.Error())
}
}
return resp
}
// ban the address of a poster
func (self httpModUI) HandleBanAddress(wr http.ResponseWriter, r *http.Request) {
self.asAuthedWithMessage("ban", self.handleBanAddress, wr, r)
}
// delete a post
func (self httpModUI) HandleDeletePost(wr http.ResponseWriter, r *http.Request) {
self.asAuthedWithMessage("login", self.handleDeletePost, wr, r)
}
func (self httpModUI) HandleLogin(wr http.ResponseWriter, r *http.Request) {
privkey := r.FormValue("privkey")
msg := "failed login: "
if len(privkey) == 0 {
msg += "no key"
} else {
ok, err := self.CheckKey(privkey, "login")
if err != nil {
msg += fmt.Sprintf("%s", err)
} else if ok {
msg = "login okay"
sess := self.getSession(r)
sess.Values["privkey"] = privkey
sess.Save(r, wr)
} else {
msg += "invalid key"
}
}
self.writeTemplateParam(wr, r, "modlogin_result.mustache", map[string]interface{}{"message": msg, csrf.TemplateTag: csrf.TemplateField(r)})
}
func (self httpModUI) HandleKeyGen(wr http.ResponseWriter, r *http.Request) {
pk, sk := newSignKeypair()
tripcode := makeTripcode(pk)
self.writeTemplateParam(wr, r, "keygen.mustache", map[string]interface{}{"public": pk, "secret": sk, "tripcode": tripcode})
}
func (self httpModUI) ServeModPage(wr http.ResponseWriter, r *http.Request) {
if self.checkSession(r, "login") {
wr.Header().Set("X-CSRF-Token", csrf.Token(r))
// we are logged in
url := r.URL.String()
if strings.HasSuffix(url, "/mod/feeds") {
// serve feeds page
self.writeTemplate(wr, r, "modfeed.mustache")
} else {
// serve mod page
self.writeTemplate(wr, r, "modpage.mustache")
}
} else {
// we are not logged in
// serve login page
self.writeTemplate(wr, r, "modlogin.mustache")
}
if r.Body != nil {
r.Body.Close()
}
}