0
0
mirror of https://github.com/rls-moe/nyx synced 2025-10-13 12:04:15 +00:00

Improved stability, improved spam algorithm, fixed some bugs

This commit is contained in:
Tim Schuster
2017-03-15 09:13:15 +01:00
parent afd9ae71cf
commit 19d0e9282d
37 changed files with 2099 additions and 255 deletions

View File

@@ -1,21 +1,139 @@
package admin
import (
"encoding/json"
"fmt"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"net/http"
"strings"
"time"
)
func handleCleanup(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
if sess.CAttr("mode") != "admin" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
fmt.Println("Beginning cleanup...")
db := middle.GetDB(r)
err := db.Update(func(tx *buntdb.Tx) error {
var delKeys = []string{}
err := db.View(func(tx *buntdb.Tx) error {
var err error
tx.AscendKeys("*", func(key, value string) bool {
keyType := detectType(key)
if keyType == "thread" {
var host string
host, err = resources.GetHostnameFromKey(key)
if err != nil {
fmt.Printf("Error: %s", err)
return false
}
var thread = &resources.Thread{}
err = json.Unmarshal([]byte(value), thread)
if err != nil {
fmt.Printf("Error: %s", err)
return false
}
threadTime := resources.DateFromId(thread.ID)
dur := threadTime.Sub(time.Now())
if dur > time.Hour*24*7 {
fmt.Printf("Sched %s for deletion: expired\n", key)
delKeys = append(delKeys, key)
return true
}
err = resources.FillReplies(tx, host, thread)
if err != nil {
fmt.Printf("Error: %s", err)
return false
}
if len(thread.GetReplies()) == 0 {
fmt.Printf("Sched %s for deletion: empty\n", key)
delKeys = append(delKeys, key)
return true
}
if _, err := resources.GetReply(tx, host, thread.Board, thread.ID, thread.StartReply); err == buntdb.ErrNotFound {
fmt.Printf("Sched %s for delection: main reply dead\n", key)
delKeys = append(delKeys, key)
return true
}
} else if keyType == "reply" {
var host string
host, err = resources.GetHostnameFromKey(key)
if err != nil {
fmt.Printf("Error: %s", err)
return false
}
var reply = &resources.Reply{}
err = json.Unmarshal([]byte(value), reply)
if err != nil {
fmt.Printf("Error: %s", err)
return false
}
replyTime := resources.DateFromId(reply.ID)
dur := replyTime.Sub(time.Now())
if dur > time.Hour*24*7 {
fmt.Printf("Sched %s for deletion: expired\n", key)
delKeys = append(delKeys, key)
return true
}
if val, ok := reply.Metadata["deleted"]; ok && val == "yes" {
fmt.Printf("Sched %s for deletion: deleted\n", key)
delKeys = append(delKeys, key)
return true
}
if err := resources.TestThread(tx, host, reply.Board, reply.Thread); err == buntdb.ErrNotFound {
fmt.Printf("Sched %s for deletion: missing parent %d: %s\n", key, reply.Thread, err)
delKeys = append(delKeys, key)
return true
}
}
return true
})
/* Insert cleanup codes here */
return err
})
fmt.Println("Removing sched' entries")
db.Update(func(tx *buntdb.Tx) error {
for _, v := range delKeys {
fmt.Printf("Deleting %s\n", v)
tx.Delete(v)
}
return nil
})
fmt.Println("Shrinking DB")
err = db.Shrink()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
fmt.Println("Finished Cleanup")
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}
func detectType(key string) string {
if strings.Contains(key, "/jack/") {
if strings.HasSuffix(key, "/board-data") {
return "board"
}
if strings.HasSuffix(key, "/thread") {
return "thread"
}
if strings.HasSuffix(key, "/reply-data") {
return "reply"
}
return "system"
}
return "none"
}

74
http/admin/delpost.go Normal file
View File

@@ -0,0 +1,74 @@
package admin
import (
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"strconv"
"go.rls.moe/nyx/resources"
"fmt"
"net/http"
"github.com/tidwall/buntdb"
)
func handleDelPost(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
if sess.CAttr("mode") != "admin" && sess.CAttr("mode") != "mod" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
rid, err := strconv.Atoi(r.FormValue("reply_id"))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
trid, err := strconv.Atoi(r.FormValue("thread_id"))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
board := r.FormValue("board")
if sess.CAttr("mode") == "mod" && sess.CAttr("board") != board {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Not on this board"))
return
}
db := middle.GetDB(r)
err = db.Update(func(tx *buntdb.Tx) error {
reply, err := resources.GetReply(tx, r.Host, board, trid, rid)
if err != nil {
return err
}
reply.Text = "[deleted]"
reply.Metadata["deleted"] = "yes"
reply.Image = nil
reply.Thumbnail = nil
err = resources.UpdateReply(tx, r.Host, board, reply)
if err != nil {
return err
}
return nil
})
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.Redirect(w, r, fmt.Sprintf("/%s/%d/thread.html", board, trid), http.StatusSeeOther)
}

View File

@@ -2,17 +2,12 @@ package admin
import (
"bytes"
"fmt"
"github.com/GeertJohan/go.rice"
"github.com/icza/session"
"github.com/pressly/chi"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"html/template"
"net/http"
"strconv"
"time"
)
@@ -27,8 +22,9 @@ var riceConf = rice.Config{
var box = riceConf.MustFindBox("http/admin/res/")
var (
panelTmpl = template.New("admin/panel")
loginTmpl = template.New("admin/login")
panelTmpl = template.New("admin/panel")
loginTmpl = template.New("admin/login")
statusTmpl = template.New("admin/status")
)
func init() {
@@ -41,6 +37,10 @@ func init() {
if err != nil {
panic(err)
}
statusTmpl, err = statusTmpl.Parse(box.MustString("status.html"))
if err != nil {
panic(err)
}
}
// Router sets up the Administration Panel
@@ -50,8 +50,12 @@ func AdminRouter(r chi.Router) {
r.Get("/index.html", serveLogin)
r.Get("/panel.html", servePanel)
r.Post("/new_board.sh", handleNewBoard)
r.Post("/cleanup.sh", handleCleanup)
r.Post("/login.sh", handleLogin)
r.Post("/logout.sh", handleLogout)
r.Post("/new_admin.sh", handleNewAdmin)
r.Post("/del_admin.sh", handleDelAdmin)
r.Get("/status.sh", serveStatus)
}
// Router sets up moderation functions
@@ -60,69 +64,6 @@ func ModRouter(r chi.Router) {
r.Post("/del_reply.sh", handleDelPost)
}
func handleDelPost(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
if sess.CAttr("mode") != "admin" && sess.CAttr("mode") != "mod" {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
rid, err := strconv.Atoi(r.FormValue("reply_id"))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
trid, err := strconv.Atoi(r.FormValue("thread_id"))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
board := r.FormValue("board")
if sess.CAttr("mode") == "mod" && sess.CAttr("board") != board {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Not on this board"))
return
}
db := middle.GetDB(r)
err = db.Update(func(tx *buntdb.Tx) error {
reply, err := resources.GetReply(tx, r.Host, board, trid, rid)
if err != nil {
return err
}
reply.Text = "[deleted]"
reply.Metadata["deleted"] = "yes"
reply.Image = nil
reply.Thumbnail = nil
err = resources.UpdateReply(tx, r.Host, board, reply)
if err != nil {
return err
}
return nil
})
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.Redirect(w, r, fmt.Sprintf("/%s/%d/thread.html", board, trid), http.StatusSeeOther)
}
func serveLogin(w http.ResponseWriter, r *http.Request) {
dat := bytes.NewBuffer([]byte{})
err := loginTmpl.Execute(dat, middle.GetBaseCtx(r))
@@ -150,41 +91,3 @@ func servePanel(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "panel.html", time.Now(),
bytes.NewReader(dat.Bytes()))
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
}
session.Remove(sess, w)
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
}
db := middle.GetDB(r)
var admin = &resources.AdminPass{}
err = db.View(func(tx *buntdb.Tx) error {
var err error
admin, err = resources.GetAdmin(tx, r.FormValue("id"))
return err
})
if err != nil {
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
errw.ErrorWriter(err, w, r)
}
err = admin.VerifyLogin(r.FormValue("pass"))
if err != nil {
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
errw.ErrorWriter(err, w, r)
}
sess := session.NewSessionOptions(&session.SessOptions{
CAttrs: map[string]interface{}{"mode": "admin"},
})
session.Add(sess, w)
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}

50
http/admin/login.go Normal file
View File

@@ -0,0 +1,50 @@
package admin
import (
"github.com/icza/session"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"net/http"
)
func handleLogout(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
}
session.Remove(sess, w)
http.Redirect(w, r, "/admin/index.html", http.StatusSeeOther)
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
}
db := middle.GetDB(r)
var admin = &resources.AdminPass{}
err = db.View(func(tx *buntdb.Tx) error {
var err error
admin, err = resources.GetAdmin(tx, r.FormValue("id"))
return err
})
if err != nil {
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
errw.ErrorWriter(err, w, r)
return
}
err = admin.VerifyLogin(r.FormValue("pass"))
if err != nil {
err = errw.MakeErrorWithTitle("Access Denied", "User or Password Invalid")
errw.ErrorWriter(err, w, r)
return
}
sess := session.NewSessionOptions(&session.SessOptions{
CAttrs: map[string]interface{}{"mode": "admin"},
})
session.Add(sess, w)
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}

92
http/admin/newadmin.go Normal file
View File

@@ -0,0 +1,92 @@
package admin
import (
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"net/http"
)
func handleDelAdmin(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if !middle.IsAdminSession(sess) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
db := middle.GetDB(r)
adminID := r.FormValue("adminid")
if len(adminID) > 255 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too long", "The ID of the administrator is too long"), w, r)
return
}
if len(adminID) < 4 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too short", "The ID of the administrator is too short"), w, r)
return
}
if err = db.Update(func(tx *buntdb.Tx) error {
return resources.DelAdmin(tx, adminID)
}); err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}
func handleNewAdmin(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if !middle.IsAdminSession(sess) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
err := r.ParseForm()
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
db := middle.GetDB(r)
var admin = &resources.AdminPass{}
admin.ID = r.FormValue("adminid")
if len(admin.ID) > 255 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too long", "The ID of the administrator is too long"), w, r)
return
}
if len(admin.ID) < 4 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too short", "The ID of the administrator is too short"), w, r)
return
}
if len(r.FormValue("adminpass")) > 255 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too long", "The Password of the administrator is too long"), w, r)
return
}
if len(r.FormValue("adminpass")) < 12 {
errw.ErrorWriter(errw.MakeErrorWithTitle("Too short", "The Password of the administrator is too short"), w, r)
return
}
admin.HashLogin(r.FormValue("adminpass"))
if err = db.Update(func(tx *buntdb.Tx) error {
return resources.NewAdmin(tx, admin)
}); err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.Redirect(w, r, "/admin/panel.html", http.StatusSeeOther)
}

View File

@@ -11,12 +11,7 @@ import (
func handleNewBoard(w http.ResponseWriter, r *http.Request) {
sess := middle.GetSession(r)
if sess == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
if sess.CAttr("mode") != "admin" {
if !middle.IsAdminSession(sess) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return

View File

@@ -19,20 +19,53 @@
<input type="submit" value="Logout" />
</form>
</div>
<div class="panel boardmgr">
<form method="POST" action="/admin/new_board.sh">
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
<input type="text" placeholder="shortname" name="shortname"/>
<input type="text" placeholder="longname" name="longname"/>
<input type="submit" value="Create Board" />
<input type="reset" value="Reset" />
<br clear="left" /><hr />
<div class="postarea">
<form id="postform1" action="/admin/new_board.sh" method="POST">
<table>
<tbody>
<tr>
<td class="postblock">
Action
</td>
<td>
New Board
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
</td>
</tr>
<tr>
<td class="postblock">
Short Name
</td>
<td>
<input type="text" placeholder="shortname" name="shortname"/>
</td>
</tr>
<tr>
<td class="postblock">
Long Name
</td>
<td>
<input type="text" placeholder="longname" name="longname"/>
</td>
</tr>
<tr>
<td class="postblock"></td>
<td>
<input type="submit" value="Create Board" />
<input type="reset" value="Reset" />
</td>
</tr>
</tbody>
</table>
</form>
</div>
<br clear="left" /><hr />
<div class="postarea">
<form id="postform1" action="/admin/new_admin.sh" method="POST">
<form id="postform2" action="/admin/new_admin.sh" method="POST">
<table>
<tbody>
<tr>
@@ -53,8 +86,10 @@
</td>
<td>
<input type="text"
minlength="8"
name="id" />
minlength="4"
maxlength="255"
name="adminid"
required />
</td>
</tr>
<tr>
@@ -65,7 +100,83 @@
<input type="password"
minlength="12"
maxlength="255"
name="id" />
name="adminpass"
required />
</td>
</tr>
<tr>
<td class="postblock"></td>
<td>
<input type="submit" value="Create Admin ID" />
<input type="reset" value="Reset" />
</td>
</tr>
</tbody>
</table>
</form>
</div>
<br clear="left" /><hr />
<div class="postarea">
<form id="postform3" action="/admin/del_admin.sh" method="POST">
<table>
<tbody>
<tr>
<td class="postblock">
Action
</td>
<td>
Delete Administrator
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
</td>
</tr>
<tr>
<td class="postblock">
ID
</td>
<td>
<input type="text"
minlength="4"
maxlength="255"
name="adminid"
required />
</td>
</tr>
<tr>
<td class="postblock"></td>
<td>
<input type="submit" value="Delete Admin ID" />
<input type="reset" value="Reset" />
</td>
</tr>
</tbody>
</table>
</form>
</div>
<br clear="left" /><hr />
<div class="postarea">
<form id="postform4" action="/admin/cleanup.sh" method="POST">
<table>
<tbody>
<tr>
<td class="postblock">
Action
</td>
<td>
Cleanup Database
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
</td>
</tr>
<tr>
<td class="postblock"></td>
<td>
<input type="submit" value="Start Cleanup" />
<input type="reset" value="Reset" />
</td>
</tr>
</tbody>

125
http/admin/res/status.html Normal file
View File

@@ -0,0 +1,125 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Config.Site.Title}} Status</title>
<link rel="stylesheet" href="/@/style.css">
<link rel="stylesheet" href="/@/custom.css">
<link rel="stylesheet" href="/@/admin.css">
</head>
<body>
<div class="banner logo">
<h2>Runtime Statistics</h2>
</div>
<br/>
<br/>
<div class="postarea">
<form id="postform1" action="/admin/new_board.sh" method="POST">
<table>
<tbody>
<tr><td class="postblock">
Start Time
</td><td>
{{.Uptime.human}}
</td></tr>
<tr><td class="postblock">
Uptime (Precise)
</td><td>
{{ .Uptime.hours | printf "%.4f"}} hours<br/>
{{ .Uptime.seconds | printf "%.4f"}} seconds
</td></tr>
<tr><td class="postblock">
Num. CPU
</td><td>
{{.GC.numcpu}}
</td></tr>
<tr><td class="postblock">
Num. GoRoutines
</td><td>
{{.GC.numgor}}
</td></tr>
<tr><td class="postblock">
Go Version<br/>
Arch/OS/Compiler
</td><td>
{{.GC.version}}<br/>
{{.GC.arch}} / {{.GC.os}} / {{.GC.compiler}}
</td></tr>
<tr><td class="postblock">
Current Allocs
</td><td>
{{.GC.memory.alloc}}
</td></tr>
<tr><td class="postblock">
Cumulative Allocs
</td><td>
{{.GC.memory.calloc}}
</td></tr>
<tr><td class="postblock">
Used Sys Memory
</td><td>
{{.GC.memory.sysmem}}
</td></tr>
<tr><td class="postblock">
Pointer Lookups
</td><td>
{{.GC.memory.lookups}}
</td></tr>
<tr><td class="postblock">
MAllocs
</td><td>
{{.GC.memory.mallocs}}
</td></tr>
<tr><td class="postblock">
MFrees
</td><td>
{{.GC.memory.frees}}
</td></tr>
<tr><td class="postblock">
Live Objects
</td><td>
{{.GC.memory.liveobj}}
</td></tr>
<tr><td class="postblock">
Heap Allocated
</td><td>
{{.GC.memory.heapalloc}}
</td></tr>
<tr><td class="postblock">
Heap Released
</td><td>
{{.GC.memory.heaprelease}}
</td></tr>
<tr><td class="postblock">
GC Metadata
</td><td>
{{.GC.memory.gcmeta}}
</td></tr>
<tr><td class="postblock">
GC Pause
</td><td>
{{.GC.memory.pause}}
</td></tr>
<tr><td class="postblock">
GC Invokations
</td><td>
{{.GC.memory.gctimes}}
</td></tr>
<tr><td class="postblock">
GC Forced
</td><td>
{{.GC.memory.fgctimes}}
</td></tr>
<tr><td class="postblock">
GC CPU Usage
</td><td>
{{.GC.memory.cpufrac}}
</td></tr>
</tbody>
</table>
</form>
</div>
<br clear="left" /><hr />
</body>
</html>

87
http/admin/status.go Normal file
View File

@@ -0,0 +1,87 @@
package admin
import (
"bytes"
"fmt"
"github.com/dustin/go-humanize"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"net/http"
"runtime"
"sync"
"time"
)
var memStat = map[string]interface{}{}
var memStatLock = new(sync.RWMutex)
var startTime = time.Now().UTC()
func init() {
go func() {
update()
ticker := time.Tick(time.Second * 10)
for _ = range ticker {
update()
}
}()
}
func update() {
memStatLock.Lock()
defer memStatLock.Unlock()
memStat["Uptime"] = uptime()
memStat["GC"] = gcStat()
}
func uptime() map[string]interface{} {
return map[string]interface{}{
"human": humanize.Time(startTime),
"hours": time.Now().Sub(startTime).Hours(),
"seconds": time.Now().Sub(startTime).Seconds(),
}
}
func gcStat() map[string]interface{} {
m := &runtime.MemStats{}
runtime.ReadMemStats(m)
mem := map[string]interface{}{}
mem["alloc"] = fmt.Sprintf("%.5f GiB", float64(m.Alloc)/1024/1024/1024)
mem["calloc"] = fmt.Sprintf("%.5f GiB", float64(m.TotalAlloc)/1024/1024/1024)
mem["sysmem"] = fmt.Sprintf("%.5f MiB", float64(m.Sys)/1024/1024)
mem["lookups"] = fmt.Sprintf("× %d", m.Lookups)
mem["mallocs"] = fmt.Sprintf("× %d", m.Mallocs)
mem["frees"] = fmt.Sprintf("× %d", m.Frees)
mem["liveobj"] = fmt.Sprintf("× %d", m.Mallocs-m.Frees)
mem["heapalloc"] = fmt.Sprintf("%.5f MiB", float64(m.HeapSys)/1024/1024)
mem["heaprelease"] = fmt.Sprintf("%.5f MiB", float64(m.HeapReleased)/1024/1024)
mem["gcmeta"] = fmt.Sprintf("%.5f MiB", float64(m.GCSys)/1024/1024)
mem["pause"] = fmt.Sprintf("%.5f min", float64(m.PauseTotalNs)/1000/1000/1000/60)
mem["gctimes"] = fmt.Sprintf("× %d", m.NumGC)
mem["fgctimes"] = fmt.Sprintf("× %d", m.NumForcedGC)
mem["cpufrac"] = fmt.Sprintf("%.5f %%", m.GCCPUFraction*100)
return map[string]interface{}{
"numcpu": runtime.NumCPU(),
"numgor": runtime.NumGoroutine(),
"version": runtime.Version(),
"arch": runtime.GOARCH,
"os": runtime.GOOS,
"compiler": runtime.Compiler,
"memory": mem,
}
}
func serveStatus(w http.ResponseWriter, r *http.Request) {
memStatLock.RLock()
defer memStatLock.RUnlock()
ctx := middle.GetBaseCtx(r)
ctx["Uptime"] = memStat["Uptime"]
ctx["GC"] = memStat["GC"]
dat := bytes.NewBuffer([]byte{})
err := statusTmpl.Execute(dat, ctx)
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.ServeContent(w, r, "panel.html", time.Now(),
bytes.NewReader(dat.Bytes()))
}

View File

@@ -18,18 +18,21 @@ func serveBoard(w http.ResponseWriter, r *http.Request) {
ctx := middle.GetBaseCtx(r)
err := db.View(func(tx *buntdb.Tx) error {
bName := chi.URLParam(r, "board")
log.Println("Getting board")
b, err := resources.GetBoard(tx, r.Host, bName)
if err != nil {
return err
}
ctx["Board"] = b
log.Println("Listing Threads...")
threads, err := resources.ListThreads(tx, r.Host, bName)
if err != nil {
return err
}
log.Println("Number of Thread on board: ", len(threads))
log.Println("Filling threads")
for k := range threads {
err := resources.FillReplies(tx, r.Host, threads[k])
if err != nil {

View File

@@ -27,9 +27,6 @@ var box = riceConf.MustFindBox("http/board/res/")
var (
tmpls = template.New("base")
//dirTmpl = template.New("board/dir")
//boardTmpl = template.New("board/board")
//threadTmpl = template.New("board/thread")
hdlFMap = template.FuncMap{
"renderText": resources.OperateReplyText,
@@ -53,6 +50,10 @@ var (
"formatDate": func(date time.Time) string {
return date.Format("02 Jan 06 15:04:05")
},
"isAdminSession": middle.IsAdminSession,
"isModSession": middle.IsModSession,
"captchaProb": resources.CaptchaProb,
"percentFloat": func(in float64) float64 { return in * 100 },
}
)
@@ -93,6 +94,7 @@ func Router(r chi.Router) {
func serveThumb(w http.ResponseWriter, r *http.Request) {
dat := bytes.NewBuffer([]byte{})
var date time.Time
db := middle.GetDB(r)
err := db.View(func(tx *buntdb.Tx) error {
bName := chi.URLParam(r, "board")
@@ -113,17 +115,19 @@ func serveThumb(w http.ResponseWriter, r *http.Request) {
if err != nil {
return err
}
date = resources.DateFromId(reply.ID)
return nil
})
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.ServeContent(w, r, "thumb.png", time.Now(), bytes.NewReader(dat.Bytes()))
http.ServeContent(w, r, "thumb.png", date, bytes.NewReader(dat.Bytes()))
}
func serveFullImage(w http.ResponseWriter, r *http.Request) {
dat := bytes.NewBuffer([]byte{})
var date time.Time
db := middle.GetDB(r)
err := db.View(func(tx *buntdb.Tx) error {
bName := chi.URLParam(r, "board")
@@ -144,13 +148,14 @@ func serveFullImage(w http.ResponseWriter, r *http.Request) {
if err != nil {
return err
}
date = resources.DateFromId(reply.ID)
return nil
})
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
http.ServeContent(w, r, "image.png", time.Now(), bytes.NewReader(dat.Bytes()))
http.ServeContent(w, r, "image.png", date, bytes.NewReader(dat.Bytes()))
}
func serveDir(w http.ResponseWriter, r *http.Request) {

61
http/board/imageparser.go Normal file
View File

@@ -0,0 +1,61 @@
package board
import (
"bytes"
"github.com/nfnt/resize"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/resources"
"image"
"image/png"
"io"
"log"
"mime/multipart"
"net/http"
)
func parseImage(reply *resources.Reply, file multipart.File, hdr *multipart.FileHeader, err error) error {
if err != nil && err != http.ErrMissingFile {
return err
}
if err == http.ErrMissingFile {
return nil
}
cfg, _, err := image.DecodeConfig(file)
if err != nil {
return err
}
if cfg.Height > 8000 || cfg.Width > 8000 {
log.Println("Somebody tried to detonate the memory!")
return errw.MakeErrorWithTitle("Too large", "Your upload was too large")
}
_, err = file.Seek(0, io.SeekStart)
if err != nil {
return err
}
img, _, err := image.Decode(file)
if err != nil {
return err
}
if img.Bounds().Dx() > 8000 || img.Bounds().Dy() > 8000 {
log.Println("Somebody tried to detonate the memory!")
return errw.MakeErrorWithTitle("Too large", "Your upload was too large")
}
thumb := resize.Thumbnail(128, 128, img, resize.Lanczos3)
imgBuf := bytes.NewBuffer([]byte{})
err = png.Encode(imgBuf, thumb)
if err != nil {
return err
}
log.Printf("Thumb has size %d KiB", imgBuf.Len()/1024)
reply.Thumbnail = make([]byte, imgBuf.Len())
copy(reply.Thumbnail, imgBuf.Bytes())
imgBuf.Reset()
err = png.Encode(imgBuf, img)
if err != nil {
return err
}
log.Printf("Image has size %d KiB", imgBuf.Len()/1024)
reply.Image = make([]byte, imgBuf.Len())
copy(reply.Image, imgBuf.Bytes())
return nil
}

View File

@@ -1,20 +1,15 @@
package board
import (
"bytes"
"fmt"
"github.com/nfnt/resize"
"github.com/pressly/chi"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"image"
_ "image/gif"
_ "image/jpeg"
"image/png"
"net/http"
"strconv"
)
func handleNewReply(w http.ResponseWriter, r *http.Request) {
@@ -23,7 +18,7 @@ func handleNewReply(w http.ResponseWriter, r *http.Request) {
errw.ErrorWriter(err, w, r)
return
}
err = r.ParseMultipartForm(10 * 1024 * 1024)
err = r.ParseMultipartForm(4 * 1024 * 1024)
if err != nil {
errw.ErrorWriter(err, w, r)
return
@@ -39,66 +34,16 @@ func handleNewReply(w http.ResponseWriter, r *http.Request) {
var reply = &resources.Reply{}
reply.Board = chi.URLParam(r, "board")
tid, err := strconv.Atoi(chi.URLParam(r, "thread"))
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
reply.Thread = tid
reply.Text = r.FormValue("text")
if len(reply.Text) > 10000 {
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are too many characters"), w, r)
return
}
if len(reply.Text) < 5 {
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r)
return
}
if score, err := resources.SpamScore(reply.Text); err != nil || !resources.CaptchaPass(score) {
err = parseReply(r, reply)
if err == trollThrottle {
http.Redirect(w, r,
fmt.Sprintf("/%s/%s/thread.html?err=trollthrottle",
chi.URLParam(r, "board"), chi.URLParam(r, "thread")),
http.StatusSeeOther)
return
}
{
file, _, err := r.FormFile("image")
if err != nil && err != http.ErrMissingFile {
errw.ErrorWriter(err, w, r)
return
} else if err != http.ErrMissingFile {
img, _, err := image.Decode(file)
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
thumb := resize.Thumbnail(128, 128, img, resize.Lanczos3)
imgBuf := bytes.NewBuffer([]byte{})
err = png.Encode(imgBuf, img)
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
fmt.Println("Image has size ", len(imgBuf.Bytes()))
reply.Image = imgBuf.Bytes()
imgBuf = bytes.NewBuffer([]byte{})
err = png.Encode(imgBuf, thumb)
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
reply.Thumbnail = imgBuf.Bytes()
}
}
reply.Metadata = map[string]string{}
if r.FormValue("tripcode") != "" {
reply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode"))
} else {
reply.Metadata["trip"] = "Anonymous"
} else if err != nil {
errw.ErrorWriter(err, w, r)
return
}
db := middle.GetDB(r)

View File

@@ -1,16 +1,12 @@
package board
import (
"bytes"
"fmt"
"github.com/nfnt/resize"
"github.com/pressly/chi"
"github.com/tidwall/buntdb"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"image"
"image/png"
"net/http"
)
@@ -37,59 +33,18 @@ func handleNewThread(w http.ResponseWriter, r *http.Request) {
var thread = &resources.Thread{}
var mainReply = &resources.Reply{}
mainReply.Board = chi.URLParam(r, "board")
thread.Board = chi.URLParam(r, "board")
mainReply.Text = r.FormValue("text")
if len(mainReply.Text) > 10000 {
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are too many characters"), w, r)
return
}
if len(mainReply.Text) < 5 {
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r)
return
}
if score, err := resources.SpamScore(mainReply.Text); err != nil || !resources.CaptchaPass(score) {
err = parseReply(r, mainReply)
if err == trollThrottle {
http.Redirect(w, r,
fmt.Sprintf("/%s/board.html?err=trollthrottle",
chi.URLParam(r, "board")),
http.StatusSeeOther)
return
}
{
file, _, err := r.FormFile("image")
if err != nil && err != http.ErrMissingFile {
errw.ErrorWriter(err, w, r)
return
} else if err != http.ErrMissingFile {
img, _, err := image.Decode(file)
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
thumb := resize.Thumbnail(128, 128, img, resize.Lanczos3)
imgBuf := bytes.NewBuffer([]byte{})
err = png.Encode(imgBuf, img)
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
fmt.Println("Image has size ", len(imgBuf.Bytes()))
mainReply.Image = imgBuf.Bytes()
imgBuf = bytes.NewBuffer([]byte{})
err = png.Encode(imgBuf, thumb)
if err != nil {
errw.ErrorWriter(err, w, r)
return
}
mainReply.Thumbnail = imgBuf.Bytes()
}
}
mainReply.Metadata = map[string]string{}
if r.FormValue("tripcode") != "" {
mainReply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode"))
} else if err != nil {
errw.ErrorWriter(err, w, r)
return
}
db := middle.GetDB(r)

72
http/board/replyparser.go Normal file
View File

@@ -0,0 +1,72 @@
package board
import (
"errors"
"fmt"
"github.com/pressly/chi"
"go.rls.moe/nyx/http/errw"
"go.rls.moe/nyx/http/middle"
"go.rls.moe/nyx/resources"
"net/http"
"strconv"
)
var trollThrottle = errors.New("Troll throttle")
func parseReply(r *http.Request, reply *resources.Reply) error {
reply.Board = chi.URLParam(r, "board")
reply.Text = r.FormValue("text")
if tidStr := chi.URLParam(r, "thread"); tidStr != "" {
var err error
reply.Thread, err = strconv.Atoi(tidStr)
if err != nil {
return err
}
}
if len(reply.Text) > 10000 {
return errw.MakeErrorWithTitle(
"I'm sorry but I can't do that",
"There are too many characters")
}
if len(reply.Text) < 5 {
return errw.MakeErrorWithTitle(
"I'm sorry but I can't do that",
"There are not enough characters")
}
reply.Metadata = map[string]string{}
spamScore, err := resources.SpamScore(reply.Text)
if err != nil {
return err
}
reply.Metadata["spamscore"] = fmt.Sprintf("%.6f", spamScore)
reply.Metadata["captchaprob"] = fmt.Sprintf("%.2f", resources.CaptchaProb(spamScore)*100)
if !resources.CaptchaPass(spamScore) {
return trollThrottle
}
file, hdr, err := r.FormFile("image")
err = parseImage(reply, file, hdr, err)
if err != nil {
return err
}
if r.FormValue("tripcode") != "" {
reply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode"))
}
if middle.IsModSession(middle.GetSession(r)) {
if r.FormValue("modpost") != "" {
reply.Metadata["modpost"] = "yes"
}
if middle.IsAdminSession(middle.GetSession(r)) {
if r.FormValue("adminpost") != "" {
reply.Metadata["adminpost"] = "yes"
}
}
}
return nil
}

View File

@@ -78,6 +78,23 @@
</td>
</tr>
{{ end }}
{{ if (isModSession .Session) }}
<tr>
<td class="postblock">
Mod Post
</td>
<td>
<label>
<input type="checkbox" name="modpost"/>Mark as Mod Post
</label>
{{ if (isAdminSession .Session) }}
<label>
<input type="checkbox" name="adminpost"/>Mark as Admin Post
</label>
{{ end }}
</td>
</tr>
{{ end }}
<tr>
<td class="postblock">
@@ -100,6 +117,12 @@
{{ else }}
Anonymous
{{ end }}
{{ if .Reply.Metadata.modpost }}
(Mod)
{{ end }}
{{ if .Reply.Metadata.adminpost }}
[Admin]
{{ end }}
</span></label>
<span class="date">{{dateFromID .Reply.ID | formatDate}}</span>
{{ if .Session }}
@@ -126,7 +149,15 @@
{{ end }}
{{ end }}
<span>
{{printf "[SpamScore: %f]" (rateSpam .Reply.Text) }}
{{ if not .Reply.Metadata.spamscore }}
{{ $score := (rateSpam .Reply.Text) }}
{{printf "[SpamScore: %f]" $score }}
{{printf "[Captcha: %.3f%%]" (percentFloat (captchaProb $score)) }}
{{printf "[OLD]"}}
{{ else }}
{{ printf "[SpamScore: %s]" .Reply.Metadata.spamscore }}
{{ printf "[Captcha: %s %%]" .Reply.Metadata.captchaprob }}
{{ end }}
</span>
<span class="reflink">
<a href="/{{.Boardlink}}/{{.ThreadID}}/thread.html">No.{{.Reply.ID}}</a>

15
http/middle/limitsize.go Normal file
View File

@@ -0,0 +1,15 @@
package middle
import (
"go.rls.moe/nyx/config"
"net/http"
)
func LimitSize(c *config.Config) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 10*1024*1024)
next.ServeHTTP(w, r)
})
}
}

View File

@@ -13,3 +13,26 @@ func init() {
func GetSession(r *http.Request) session.Session {
return session.Get(r)
}
func IsAdminSession(sess session.Session) bool {
if sess == nil {
return false
}
if sess.CAttr("mode") == "admin" {
return true
}
return false
}
func IsModSession(sess session.Session) bool {
if sess == nil {
return false
}
if IsAdminSession(sess) {
return true
}
if sess.CAttr("mode") == "mod" {
return true
}
return false
}

View File

@@ -10,6 +10,7 @@ import (
"go.rls.moe/nyx/http/board"
"go.rls.moe/nyx/http/middle"
"net/http"
"time"
)
var riceConf = rice.Config{
@@ -27,6 +28,7 @@ func Start(config *config.Config) {
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.CloseNotify)
r.Use(middle.LimitSize(config))
r.Use(middleware.DefaultCompress)
r.Use(middle.ConfigCtx(config))
@@ -50,5 +52,13 @@ func Start(config *config.Config) {
r.Group(board.Router)
fmt.Println("Setup Complete, Starting Web Server...")
http.ListenAndServe(config.ListenOn, r)
srv := &http.Server{
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
Handler: r,
Addr: config.ListenOn,
MaxHeaderBytes: 1 * 1024 * 1024,
}
srv.ListenAndServe()
}