0
0
mirror of https://github.com/rls-moe/nyx synced 2024-11-22 22:12:24 +00:00

Added Moderation Tools, Captcha & Trollthrottle

This commit is contained in:
Tim Schuster 2017-03-13 16:04:00 +01:00
parent 69b0d20825
commit 4177901714
No known key found for this signature in database
GPG Key ID: F9E27097EFB77F61
33 changed files with 9696 additions and 261 deletions

View File

@ -2,6 +2,7 @@ package admin
import ( import (
"bytes" "bytes"
"fmt"
"github.com/GeertJohan/go.rice" "github.com/GeertJohan/go.rice"
"github.com/icza/session" "github.com/icza/session"
"github.com/pressly/chi" "github.com/pressly/chi"
@ -11,6 +12,7 @@ import (
"go.rls.moe/nyx/resources" "go.rls.moe/nyx/resources"
"html/template" "html/template"
"net/http" "net/http"
"strconv"
"time" "time"
) )
@ -43,7 +45,7 @@ func init() {
// Router sets up the Administration Panel // Router sets up the Administration Panel
// It **must** be setup on the /admin/ basepath // It **must** be setup on the /admin/ basepath
func Router(r chi.Router) { func AdminRouter(r chi.Router) {
r.Get("/", serveLogin) r.Get("/", serveLogin)
r.Get("/index.html", serveLogin) r.Get("/index.html", serveLogin)
r.Get("/panel.html", servePanel) r.Get("/panel.html", servePanel)
@ -52,6 +54,73 @@ func Router(r chi.Router) {
r.Post("/logout.sh", handleLogout) r.Post("/logout.sh", handleLogout)
} }
// Router sets up moderation functions
// It **must** be setup on the /mod/ basepath
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"
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) { func serveLogin(w http.ResponseWriter, r *http.Request) {
dat := bytes.NewBuffer([]byte{}) dat := bytes.NewBuffer([]byte{})
err := loginTmpl.Execute(dat, middle.GetBaseCtx(r)) err := loginTmpl.Execute(dat, middle.GetBaseCtx(r))

View File

@ -25,6 +25,7 @@ func handleNewBoard(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
errw.ErrorWriter(err, w, r) errw.ErrorWriter(err, w, r)
return
} }
db := middle.GetDB(r) db := middle.GetDB(r)
@ -38,7 +39,7 @@ func handleNewBoard(w http.ResponseWriter, r *http.Request) {
return return
} }
if board.ShortName == "admin" && board.ShortName == "@" { if board.ShortName == "admin" || board.ShortName == "@" || board.ShortName == "mod"{
errw.ErrorWriter(errors.New("No"), w, r) errw.ErrorWriter(errors.New("No"), w, r)
} }

View File

@ -31,27 +31,47 @@
<input type="reset" value="Reset" /> <input type="reset" value="Reset" />
</form> </form>
</div> </div>
<div class="panel remover post"> <div class="postarea">
<form method="POST" action="/admin/rem_post.sh"> <form id="postform1" action="/admin/new_admin.sh" method="POST">
<input <table>
type="hidden" <tbody>
name="csrf_token" <tr>
value="{{ .CSRFToken }}" /> <td class="postblock">
<input type="text" placeholder="post id" name="post id"/> Action
<input type="submit" value="Remove Post" /> </td>
<input type="reset" value="Reset" /> <td>
</form> New Administrator
</div> <input
<div class="panel remover thread"> type="hidden"
<form method="POST" action="/admin/rem_thread.sh"> name="csrf_token"
<input value="{{ .CSRFToken }}" />
type="hidden" </td>
name="csrf_token" </tr>
value="{{ .CSRFToken }}" /> <tr>
<input type="text" placeholder="thread id" name="thread id"/> <td class="postblock">
<input type="submit" value="Remove thread" /> ID
<input type="reset" value="Reset" /> </td>
<td>
<input type="text"
minlength="8"
name="id" />
</td>
</tr>
<tr>
<td class="postblock">
Password
</td>
<td>
<input type="password"
minlength="12"
maxlength="255"
name="id" />
</td>
</tr>
</tbody>
</table>
</form> </form>
</div> </div>
<br clear="left" /><hr />
</body> </body>
</html> </html>

View File

@ -43,7 +43,7 @@ func serveBoard(w http.ResponseWriter, r *http.Request) {
errw.ErrorWriter(err, w, r) errw.ErrorWriter(err, w, r)
return return
} }
err = boardTmpl.Execute(dat, ctx) err = tmpls.ExecuteTemplate(dat, "board/board", ctx)
if err != nil { if err != nil {
errw.ErrorWriter(err, w, r) errw.ErrorWriter(err, w, r)
return return

View File

@ -2,6 +2,7 @@ package board
import ( import (
"bytes" "bytes"
"errors"
"github.com/GeertJohan/go.rice" "github.com/GeertJohan/go.rice"
"github.com/pressly/chi" "github.com/pressly/chi"
"github.com/tidwall/buntdb" "github.com/tidwall/buntdb"
@ -24,26 +25,48 @@ var riceConf = rice.Config{
var box = riceConf.MustFindBox("http/board/res/") var box = riceConf.MustFindBox("http/board/res/")
var ( var (
dirTmpl = template.New("board/dir") tmpls = template.New("base")
boardTmpl = template.New("board/board") //dirTmpl = template.New("board/dir")
threadTmpl = template.New("board/thread") //boardTmpl = template.New("board/board")
//threadTmpl = template.New("board/thread")
hdlFMap = template.FuncMap{ hdlFMap = template.FuncMap{
"renderText": resources.OperateReplyText, "renderText": resources.OperateReplyText,
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
},
"rateSpam": resources.SpamScore,
"makeCaptcha": resources.MakeCaptcha,
} }
) )
func init() { func init() {
var err error var err error
dirTmpl, err = dirTmpl.Parse(box.MustString("dir.html")) tmpls = tmpls.Funcs(hdlFMap)
tmpls, err = tmpls.New("thread/postlists").Parse(box.MustString("thread.tmpl.html"))
if err != nil { if err != nil {
panic(err) panic(err)
} }
boardTmpl, err = boardTmpl.Funcs(hdlFMap).Parse(box.MustString("board.html")) _, err = tmpls.New("board/dir").Parse(box.MustString("dir.html"))
if err != nil { if err != nil {
panic(err) panic(err)
} }
threadTmpl, err = threadTmpl.Funcs(hdlFMap).Parse(box.MustString("thread.html")) _, err = tmpls.New("board/board").Parse(box.MustString("board.html"))
if err != nil {
panic(err)
}
_, err = tmpls.New("board/thread").Parse(box.MustString("thread.html"))
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -57,6 +80,9 @@ func Router(r chi.Router) {
r.Get("/:board/:thread/thread.html", serveThread) r.Get("/:board/:thread/thread.html", serveThread)
r.Get("/:board/:thread/:post/post.html", servePost) r.Get("/:board/:thread/:post/post.html", servePost)
r.Post("/:board/:thread/reply.sh", handleNewReply) r.Post("/:board/:thread/reply.sh", handleNewReply)
r.Handle("/captcha/:captchaId.png", resources.ServeCaptcha)
r.Handle("/captcha/:captchaId.wav", resources.ServeCaptcha)
r.Handle("/captcha/download/:captchaId.wav", resources.ServeCaptcha)
} }
func servePost(w http.ResponseWriter, r *http.Request) { func servePost(w http.ResponseWriter, r *http.Request) {
@ -79,7 +105,7 @@ func serveDir(w http.ResponseWriter, r *http.Request) {
errw.ErrorWriter(err, w, r) errw.ErrorWriter(err, w, r)
return return
} }
err = dirTmpl.Execute(dat, ctx) err = tmpls.ExecuteTemplate(dat, "board/dir", ctx)
if err != nil { if err != nil {
errw.ErrorWriter(err, w, r) errw.ErrorWriter(err, w, r)
return return

View File

@ -18,6 +18,14 @@ func handleNewReply(w http.ResponseWriter, r *http.Request) {
return return
} }
if !resources.VerifyCaptcha(r) {
http.Redirect(w, r,
fmt.Sprintf("/%s/%s/thread.html?err=wrong_captcha",
chi.URLParam(r, "board"), chi.URLParam(r, "thread")),
http.StatusSeeOther)
return
}
var reply = &resources.Reply{} var reply = &resources.Reply{}
reply.Board = chi.URLParam(r, "board") reply.Board = chi.URLParam(r, "board")
@ -26,9 +34,9 @@ func handleNewReply(w http.ResponseWriter, r *http.Request) {
errw.ErrorWriter(err, w, r) errw.ErrorWriter(err, w, r)
return return
} }
reply.Thread = int64(tid) reply.Thread = tid
reply.Text = r.FormValue("text") reply.Text = r.FormValue("text")
if len(reply.Text) > 1000 { 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) errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are too many characters"), w, r)
return return
} }
@ -36,6 +44,15 @@ func handleNewReply(w http.ResponseWriter, r *http.Request) {
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r) errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r)
return return
} }
if score, err := resources.SpamScore(reply.Text); err != nil || !resources.CaptchaPass(score) {
http.Redirect(w, r,
fmt.Sprintf("/%s/%s/thread.html?err=trollthrottle",
chi.URLParam(r, "board"), chi.URLParam(r, "thread")),
http.StatusSeeOther)
return
}
reply.Metadata = map[string]string{} reply.Metadata = map[string]string{}
if r.FormValue("tripcode") != "" { if r.FormValue("tripcode") != "" {
reply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode")) reply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode"))

View File

@ -17,13 +17,21 @@ func handleNewThread(w http.ResponseWriter, r *http.Request) {
return return
} }
if !resources.VerifyCaptcha(r) {
http.Redirect(w, r,
fmt.Sprintf("/%s/board.html?err=wrong_captcha",
chi.URLParam(r, "board")),
http.StatusSeeOther)
return
}
var thread = &resources.Thread{} var thread = &resources.Thread{}
var mainReply = &resources.Reply{} var mainReply = &resources.Reply{}
mainReply.Board = chi.URLParam(r, "board") mainReply.Board = chi.URLParam(r, "board")
thread.Board = chi.URLParam(r, "board") thread.Board = chi.URLParam(r, "board")
mainReply.Text = r.FormValue("text") mainReply.Text = r.FormValue("text")
if len(mainReply.Text) > 1000 { 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) errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are too many characters"), w, r)
return return
} }
@ -31,6 +39,15 @@ func handleNewThread(w http.ResponseWriter, r *http.Request) {
errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r) errw.ErrorWriter(errw.MakeErrorWithTitle("I'm sorry but I can't do that", "These are not enough characters"), w, r)
return return
} }
if score, err := resources.SpamScore(mainReply.Text); err != nil || !resources.CaptchaPass(score) {
http.Redirect(w, r,
fmt.Sprintf("/%s/board.html?err=trollthrottle",
chi.URLParam(r, "board")),
http.StatusSeeOther)
return
}
mainReply.Metadata = map[string]string{} mainReply.Metadata = map[string]string{}
if r.FormValue("tripcode") != "" { if r.FormValue("tripcode") != "" {
mainReply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode")) mainReply.Metadata["trip"] = resources.CalcTripCode(r.FormValue("tripcode"))

View File

@ -13,106 +13,22 @@
<div class="site description"><h2>{{.Board.LongName}}</h2></div> <div class="site description"><h2>{{.Board.LongName}}</h2></div>
</div> </div>
{{ $boardlink := .Board.ShortName }} {{ $boardlink := .Board.ShortName }}
<div class="postarea"> {{ if .Session }}
<form id="postform" action="/{{$boardlink}}/new_thread.sh" method="POST"> {{ if eq (.Session.CAttr "mode") "admin" }}
<table> Logged in as Admin
<tbody> {{ end }}
<tr> {{ if eq (.Session.CAttr "mode") "mod" }}
<td class="postblock"> Logged in as Mod for {{ .Session.CAttr "board" }}
TripCode {{ end }}
</td> {{ end }}
<td>
<input type="text" name="tripcode" size=48 placeholder="Anonymous"/>
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
</td>
</tr>
<tr>
<td class="postblock">
Comment
</td>
<td>
<textarea
name="text"
placeholder="your comment"
rows="4"
cols="48"
minlength="10"
required
></textarea>
</td>
</tr>
<tr>
<td class="postblock">
Image File
</td>
<td>
<input type="file" name="image" />
</td>
</tr>
{{ if ne .Config.Captcha.Mode "disabled" }}
<tr>
<td class="postblock">
Captcha
</td>
<td>
<input type="text" name="captcha" size=48 />
<input type="hidden"
value="{{.CaptchaToken}}"/>
<img alt=""
src="{{.CaptchaImage}}" />
</td>
</tr>
{{ end }}
<tr>
<td class="postblock">
</td>
<td>
<input type="submit" value="Post" />
</td>
</tr>
</tbody>
</table>
</form>
</div>
<hr /> <hr />
{{ template "thread/post" . }}
<div class="postlists"> <div class="postlists">
{{ $board := .Board }}
{{ $csrf := .CSRFToken }}
{{ $session := .Session }}
{{range .Threads}} {{range .Threads}}
{{ $threadrid := .GetReply.ID }} {{ template "thread/postlists" dict "Thread" . "Board" $board "CSRFToken" $csrf "Session" $session }}
<label><span class="postertrip">
{{ if .GetReply.Metadata.trip }}
{{.GetReply.Metadata.trip}}
{{ else }}
Anonymous
{{ end }}
</span></label>
<span class="reflink"><a href="/{{$boardlink}}/{{.ID}}/thread.html">No.{{.ID}}</a></span>
<blockquote><blockquote>
{{ renderText .GetReply.Text}}
</blockquote></blockquote>
{{range .GetReplies}}
{{ if ne .ID $threadrid }}
<table><tbody><tr><td class="doubledash">&gt;&gt;</td>
<td class="reply" id="reply{{.ID}}">
<label><span class="postertrip">
{{ if .Metadata.trip }}
{{.Metadata.trip}}
{{ else }}
Anonymous
{{ end }}
</span></label>
<span class="reflink"><a href="/{{$boardlink}}/{{.Thread}}/thread.html">No.{{.ID}}</a></span>
<blockquote><blockquote>
{{ renderText .Text}}
</blockquote></blockquote>
</td>
</tr></tbody></table>
{{end}}
{{end}}
<br clear="left" /><hr />
{{end}} {{end}}
</div> </div>
</body> </body>

View File

@ -11,119 +11,11 @@
<div class="banner logo"> <div class="banner logo">
<div class="site title"><h1><span class="reflink"><a href="/{{.Board.ShortName}}/board.html">/{{.Board.ShortName}}/</a></span></h1></div> <div class="site title"><h1><span class="reflink"><a href="/{{.Board.ShortName}}/board.html">/{{.Board.ShortName}}/</a></span></h1></div>
<div class="site description"><h2>{{.Board.LongName}}</h2></div> <div class="site description"><h2>{{.Board.LongName}}</h2></div>
<div class="site thread"><h3>{{.Thread.ID}}</h3></div>
</div> </div>
{{ $boardlink := .Board.ShortName }} {{ $boardlink := .Board.ShortName }}
<hr /> <hr />
<div class="postarea"> {{ template "thread/post" . }}
<form id="postform" action="/{{.Board.ShortName}}/{{.Thread.ID}}/reply.sh" method="POST"> {{ template "thread/postlists" . }}
<table>
<tbody>
<tr>
<td class="postblock">
TripCode
</td>
<td>
<input type="text" name="tripcode" size=48 placeholder="Anonymous"/>
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
</td>
</tr>
<tr>
<td class="postblock">
Comment
</td>
<td>
<textarea
name="text"
placeholder="your comment"
rows="4"
cols="48"
minlength="10"
required
></textarea>
</td>
</tr>
<tr>
<td class="postblock">
Image File
</td>
<td>
<input type="file" name="image" />
</td>
</tr>
{{ if ne .Config.Captcha.Mode "disabled" }}
<tr>
<td class="postblock">
Captcha
</td>
<td>
<input type="text" name="captcha" size=48 />
<input type="hidden"
value="{{.CaptchaToken}}"/>
<img alt=""
src="{{.CaptchaImage}}" />
</td>
</tr>
{{ end }}
<tr>
<td class="postblock">
</td>
<td>
<input type="submit" value="Post" />
</td>
</tr>
{{ if .Board.Metadata.rules }}
<tr>
<td class="postblock">
Rules
</td>
<td>
{{ .Board.Metadata.rules }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</form>
</div>
<div class="postlists">
{{with .Thread }}
{{ $threadrid := .GetReply.ID }}
<label><span class="postertrip">
{{ if .GetReply.Metadata.trip }}
{{.GetReply.Metadata.trip}}
{{ else }}
Anonymous
{{ end }}
</span></label>
<span class="reflink"><a href="/{{$boardlink}}/{{.ID}}/thread.html">No.{{.ID}}</a></span>
<blockquote><blockquote>
{{ renderText .GetReply.Text}}
</blockquote></blockquote>
{{range .GetReplies}}
{{ if ne .ID $threadrid }}
<table><tbody><tr><td class="doubledash">&gt;&gt;</td>
<td class="reply" id="reply{{.ID}}">
<label><span class="postertrip">
{{ if .Metadata.trip }}
{{.Metadata.trip}}
{{ else }}
Anonymous
{{ end }}
</span></label>
<span class="reflink"><a href="/{{$boardlink}}/{{.Thread}}/thread.html">No.{{.ID}}</a></span>
<blockquote><blockquote>
{{ renderText .Text}}
</blockquote></blockquote>
</td>
</tr></tbody></table>
{{end}}
{{end}}
<br clear="left" /><hr />
{{end}}
</div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,169 @@
{{ define "thread/post" }}
<div class="postarea">
{{ if .Thread }}
<form id="postform"
action="/{{.Board.ShortName}}/{{.Thread.ID}}/reply.sh" method="POST">
{{ else }}
<form id="postform"
action="/{{.Board.ShortName}}/new_thread.sh" method="POST">
{{ end }}
<table>
<tbody>
{{ if .PreviousError }}
<tr>
<td class="postblock">
Error
</td>
<td>
{{.PreviousError}}
</td>
</tr>
{{ end }}
<tr>
<td class="postblock">
TripCode
</td>
<td>
<input type="text" name="tripcode" size=48 placeholder="Anonymous"/>
<input
type="hidden"
name="csrf_token"
value="{{ .CSRFToken }}" />
</td>
</tr>
<tr>
<td class="postblock">
Comment
</td>
<td>
<textarea
name="text"
placeholder="your comment"
rows="4"
cols="48"
minlength="10"
required
></textarea>
</td>
</tr>
<tr>
<td class="postblock">
Image File
</td>
<td>
<input type="file" name="image" />
</td>
</tr>
{{ if ne .Config.Captcha.Mode "disabled" }}
<tr>
<td class="postblock">
Captcha
</td>
<td>
{{ $captchaId := makeCaptcha }}
<img id="image" src="/captcha/{{$captchaId}}.png" alt="Captcha Image"/>
<audio id=audio controls style="display:none" src="/captcha/{{$captchaId}}.wav" preload=none>
You browser doesn't support audio.
<a href="/captcha/download/{{$captchaId}}.wav">Download file</a> to play it in the external player.
</audio>
<br>
<input type="text" name="captchaSolution" size=48 />
<input type="hidden"
name="captchaId"
value="{{$captchaId}}"/>
</td>
</tr>
{{ end }}
<tr>
<td class="postblock">
</td>
<td>
<input type="submit" value="Post" />
</td>
</tr>
</tbody>
</table>
</form>
</div>
<hr />
{{ end }}
{{ define "thread/reply" }}
<label><span class="postertrip">
{{ if .Reply.Metadata.trip }}
{{ .Reply.Metadata.trip}}
{{ else }}
Anonymous
{{ end }}
</span></label>
<span class="reflink">
<a href="/{{.Boardlink}}/{{.ThreadID}}/thread.html">No.{{.Reply.ID}}</a>
</span>
{{ if .Session }}
{{ if eq (.Session.CAttr "mode") "admin" }}
<form class="delform" action="/mod/del_reply.sh" method="POST">
<input
type="hidden"
name="csrf_token"
value="{{ .CSRF }}" />
<input
type="hidden"
name="reply_id"
value="{{ .Reply.ID }}" />
<input
type="hidden"
name="thread_id"
value="{{ .ThreadID }}" />
<input
type="hidden"
name="board"
value="{{ .Boardlink }}" />
<input type="submit" value="delete" />
</form>
{{ end }}
{{ end }}
<span>
{{printf "[SpamScore: %f]" (rateSpam .Reply.Text) }}
</span>
{{ if .Reply.Metadata.deleted }}
<blockquote><blockquote class="deleted">
{{ renderText .Reply.Text }}
</blockquote></blockquote>
{{ else }}
<blockquote><blockquote>
{{ renderText .Reply.Text}}
</blockquote></blockquote>
{{ end }}
{{ end }}
{{ define "thread/main" }}
<div class="postlists">
{{ $boardlink := .Board.ShortName }}
{{ $threadrid := .Thread.GetReply.ID }}
{{ $threadid := .Thread.ID }}
{{ $csrf := .CSRFToken }}
{{ $session := .Session }}
{{ with .Thread }}
{{ with .GetReply }}
{{ with dict "Reply" . "Boardlink" $boardlink "CSRF" $csrf "ThreadID" $threadid "Session" $session }}
{{ template "thread/reply" . }}
{{ end }}
{{ end }}
{{range .GetReplies}}
{{ if ne .ID $threadrid }}
<table><tbody><tr><td class="doubledash">&gt;&gt;</td>
<td class="reply" id="reply{{.ID}}">
{{ with dict "Reply" . "Boardlink" $boardlink "CSRF" $csrf "ThreadID" $threadid "Session" $session }}
{{ template "thread/reply" . }}
{{ end }}
</td>
</tr></tbody></table>
{{end}}
{{end}}
{{end}}
<br clear="left" /><hr />
</div>
{{ end }}
{{ template "thread/main" . }}

View File

@ -28,7 +28,7 @@ func serveThread(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
return err return err
} }
thread, err := resources.GetThread(tx, r.Host, bName, int64(id)) thread, err := resources.GetThread(tx, r.Host, bName, id)
if err != nil { if err != nil {
return err return err
} }
@ -48,7 +48,7 @@ func serveThread(w http.ResponseWriter, r *http.Request) {
errw.ErrorWriter(err, w, r) errw.ErrorWriter(err, w, r)
return return
} }
err = threadTmpl.Execute(dat, ctx) err = tmpls.ExecuteTemplate(dat, "board/thread", ctx)
if err != nil { if err != nil {
errw.ErrorWriter(err, w, r) errw.ErrorWriter(err, w, r)
return return

View File

@ -13,6 +13,15 @@ func GetBaseCtx(r *http.Request) map[string]interface{} {
"CSRFToken": nosurf.Token(r), "CSRFToken": nosurf.Token(r),
} }
if sess := GetSession(r); sess != nil {
val["Session"] = sess
}
err := r.URL.Query().Get("err")
if err != "" {
val["PreviousError"] = err
}
return val return val
} }

View File

@ -9,6 +9,10 @@ import (
func ConfigCtx(config *config.Config) func(http.Handler) http.Handler { func ConfigCtx(config *config.Config) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !config.IsHostNameValid(r.Host) {
return
}
r = r.WithContext(context.WithValue(r.Context(), configKey, config)) r = r.WithContext(context.WithValue(r.Context(), configKey, config))
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })

View File

@ -18,4 +18,17 @@ div {
blockquote blockquote { max-width: 80%; word-wrap: break-word; white-space: normal; } blockquote blockquote { max-width: 80%; word-wrap: break-word; white-space: normal; }
.reply blockquote, blockquote :last-child { max-width: 80%; word-wrap: break-word; white-space: normal; } .reply blockquote, blockquote :last-child { max-width: 80%; word-wrap: break-word; white-space: normal; }
.delform {
display: inline;
margin: 0;
padding: 0;
}
.delform input {
display: inline;
}
.deleted {
color: #707070;
}

View File

@ -40,7 +40,8 @@ func Start(config *config.Config) {
r.Use(mw) r.Use(mw)
} }
r.Route("/admin/", admin.Router) r.Route("/admin/", admin.AdminRouter)
r.Route("/mod/", admin.ModRouter)
{ {
box := riceConf.MustFindBox("http/res") box := riceConf.MustFindBox("http/res")
atFileServer := http.StripPrefix("/@/", http.FileServer(box.HTTPBox())) atFileServer := http.StripPrefix("/@/", http.FileServer(box.HTTPBox()))

View File

@ -38,6 +38,28 @@ func TestBoard(tx *buntdb.Tx, hostname, shortname string) (error) {
return err return err
} }
func UpdateBoard(tx *buntdb.Tx, hostname string, b *Board) error {
if err := TestBoard(tx, hostname, b.ShortName); err != nil {
return err
}
dat, err := json.Marshal(b)
if err != nil {
return err
}
_, replaced, err := tx.Set(
fmt.Sprintf(boardPath, escapeString(hostname), escapeString(b.ShortName)),
string(dat),
nil)
if err != nil {
return err
}
if !replaced {
return errors.New("Board " + escapeString(b.ShortName) + " does not exist")
}
return nil
}
func GetBoard(tx *buntdb.Tx, hostname, shortname string) (*Board, error) { func GetBoard(tx *buntdb.Tx, hostname, shortname string) (*Board, error) {
var ret = &Board{} var ret = &Board{}
dat, err := tx.Get( dat, err := tx.Get(

16
resources/captcha.go Normal file
View File

@ -0,0 +1,16 @@
package resources
import (
"github.com/dchest/captcha"
"net/http"
)
func MakeCaptcha() string {
return captcha.NewLen(5)
}
func VerifyCaptcha(r *http.Request) bool {
return captcha.VerifyString(r.FormValue("captchaId"), r.FormValue("captchaSolution"))
}
var ServeCaptcha = captcha.Server(captcha.StdWidth, captcha.StdHeight)

View File

@ -12,6 +12,7 @@ var fountain = snowflakes.Generator{
0, time.UTC).Unix(), 0, time.UTC).Unix(),
} }
func getID() (int64, error) { func getID() (int, error) {
return fountain.NewID() id, err := fountain.NewID()
return int(id), err
} }

View File

@ -10,10 +10,10 @@ import (
) )
type Reply struct { type Reply struct {
ID int64 `json:"id"` ID int `json:"id"`
Text string `json:"text"` Text string `json:"text"`
Image []byte `json:"image"` Image []byte `json:"image"`
Thread int64 `json:"thread"` Thread int `json:"thread"`
Board string `json:"board"` Board string `json:"board"`
Metadata Metadata `json:"meta"` Metadata Metadata `json:"meta"`
} }
@ -52,7 +52,33 @@ func NewReply(tx *buntdb.Tx, host, board string, thread *Thread, in *Reply, noId
return nil return nil
} }
func GetReply(tx *buntdb.Tx, host, board string, thread, id int64) (*Reply, error) { func UpdateReply(tx *buntdb.Tx, host, board string, r *Reply) error {
if err := TestBoard(tx, host, board); err != nil {
return err
}
if err := TestThread(tx, host, board, r.Thread); err != nil {
return err
}
dat, err := json.Marshal(r)
if err != nil {
return err
}
_, replaced, err := tx.Set(
fmt.Sprintf(replyPath, escapeString(host), escapeString(board), r.Thread, r.ID),
string(dat),
nil)
if err != nil {
return err
}
if !replaced {
return fmt.Errorf("Reply %d/%d does not exist", r.Thread, r.ID)
}
return nil
}
func GetReply(tx *buntdb.Tx, host, board string, thread, id int) (*Reply, error) {
var ret = &Reply{} var ret = &Reply{}
dat, err := tx.Get( dat, err := tx.Get(
fmt.Sprintf(replyPath, escapeString(host), escapeString(board), thread, id), fmt.Sprintf(replyPath, escapeString(host), escapeString(board), thread, id),
@ -66,7 +92,7 @@ func GetReply(tx *buntdb.Tx, host, board string, thread, id int64) (*Reply, erro
return ret, nil return ret, nil
} }
func DelReply(tx *buntdb.Tx, host, board string, thread, id int64) error { func DelReply(tx *buntdb.Tx, host, board string, thread, id int) error {
if _, err := tx.Delete( if _, err := tx.Delete(
fmt.Sprintf(replyPath, escapeString(host), escapeString(board), thread, id), fmt.Sprintf(replyPath, escapeString(host), escapeString(board), thread, id),
); err != nil { ); err != nil {
@ -75,7 +101,7 @@ func DelReply(tx *buntdb.Tx, host, board string, thread, id int64) error {
return nil return nil
} }
func ListReplies(tx *buntdb.Tx, host, board string, thread int64) ([]*Reply, error) { func ListReplies(tx *buntdb.Tx, host, board string, thread int) ([]*Reply, error) {
var replyList = []*Reply{} var replyList = []*Reply{}
var err error var err error
@ -84,7 +110,7 @@ func ListReplies(tx *buntdb.Tx, host, board string, thread int64) ([]*Reply, err
return nil, err return nil, err
} }
tx.DescendKeys( tx.AscendKeys(
fmt.Sprintf( fmt.Sprintf(
replySPath, replySPath,
escapeString(host), escapeString(host),
@ -98,9 +124,6 @@ func ListReplies(tx *buntdb.Tx, host, board string, thread int64) ([]*Reply, err
return false return false
} }
replyList = append(replyList, reply) replyList = append(replyList, reply)
if len(replyList) >= 100 {
return false
}
return true return true
}) })

View File

@ -1,7 +1,12 @@
package resources package resources
import ( import (
"compress/flate"
"fmt"
"html/template" "html/template"
"io"
"math"
"math/rand"
"strings" "strings"
) )
@ -10,3 +15,55 @@ func OperateReplyText(unsafe string) template.HTML {
unsafe = strings.Replace(unsafe, "\n", "<br />", -1) unsafe = strings.Replace(unsafe, "\n", "<br />", -1)
return template.HTML(unsafe) return template.HTML(unsafe)
} }
var (
blacklist = []string{
"spam",
"pizza",
"buy",
"free",
"subscription",
"penis",
"nazi",
}
)
func SpamScore(spam string) (float64, error) {
spam = strings.ToLower(spam)
counter := &byteCounter{1}
compressor, err := flate.NewWriter(counter, flate.BestSpeed)
if err != nil {
return 0.0, err
}
_, err = io.WriteString(compressor, spam)
if err != nil {
return 0.0, err
}
compressor.Flush()
compressor.Close()
blScore := 1.0
for _, v := range blacklist {
blScore += float64(strings.Count(spam, v))
}
score := float64(len(spam)) / float64(counter.p)
return (score * blScore) / 100, nil
}
type byteCounter struct {
p int
}
func (b *byteCounter) Write(p []byte) (n int, err error) {
b.p += len(p)
return len(p), nil
}
func CaptchaPass(spamScore float64) bool {
chance := math.Max(0, math.Min(0.65*math.Atan(7.1*spamScore), 0.99))
take := rand.Float64()
fmt.Printf("Chance: %f, Take %f", chance, take)
return take > chance
}

View File

@ -8,8 +8,8 @@ import (
) )
type Thread struct { type Thread struct {
ID int64 `json:"id"` ID int `json:"id"`
StartReply int64 `json:"start"` StartReply int `json:"start"`
Board string `json:"board"` Board string `json:"board"`
Metadata Metadata `json:"-"` Metadata Metadata `json:"-"`
@ -66,7 +66,7 @@ func NewThread(tx *buntdb.Tx, host, board string, in *Thread, in2 *Reply) error
return NewReply(tx, host, board, in, in2, true) return NewReply(tx, host, board, in, in2, true)
} }
func TestThread(tx *buntdb.Tx, host, board string, id int64) error { func TestThread(tx *buntdb.Tx, host, board string, id int) error {
err := TestBoard(tx, host, board) err := TestBoard(tx, host, board)
if err != nil { if err != nil {
return err return err
@ -78,7 +78,7 @@ func TestThread(tx *buntdb.Tx, host, board string, id int64) error {
return err return err
} }
func GetThread(tx *buntdb.Tx, host, board string, id int64) (*Thread, error) { func GetThread(tx *buntdb.Tx, host, board string, id int) (*Thread, error) {
var ret = &Thread{} var ret = &Thread{}
dat, err := tx.Get( dat, err := tx.Get(
fmt.Sprintf(threadPath, escapeString(host), escapeString(board), id), fmt.Sprintf(threadPath, escapeString(host), escapeString(board), id),
@ -94,7 +94,7 @@ func GetThread(tx *buntdb.Tx, host, board string, id int64) (*Thread, error) {
return ret, nil return ret, nil
} }
func DelThread(tx *buntdb.Tx, host, board string, id int64) error { func DelThread(tx *buntdb.Tx, host, board string, id int) error {
if _, err := tx.Delete( if _, err := tx.Delete(
fmt.Sprintf(threadPath, escapeString(host), escapeString(board), id), fmt.Sprintf(threadPath, escapeString(host), escapeString(board), id),
); err != nil { ); err != nil {

19
vendor/github.com/dchest/captcha/LICENSE generated vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2011-2014 Dmitry Chestnykh <dmitry@codingrobots.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

275
vendor/github.com/dchest/captcha/README.md generated vendored Normal file
View File

@ -0,0 +1,275 @@
Package captcha
=====================
import "github.com/dchest/captcha"
Package captcha implements generation and verification of image and audio
CAPTCHAs.
A captcha solution is the sequence of digits 0-9 with the defined length.
There are two captcha representations: image and audio.
An image representation is a PNG-encoded image with the solution printed on
it in such a way that makes it hard for computers to solve it using OCR.
An audio representation is a WAVE-encoded (8 kHz unsigned 8-bit) sound with the
spoken solution (currently in English, Russian, and Chinese). To make it hard
for computers to solve audio captcha, the voice that pronounces numbers has
random speed and pitch, and there is a randomly generated background noise
mixed into the sound.
This package doesn't require external files or libraries to generate captcha
representations; it is self-contained.
To make captchas one-time, the package includes a memory storage that stores
captcha ids, their solutions, and expiration time. Used captchas are removed
from the store immediately after calling Verify or VerifyString, while
unused captchas (user loaded a page with captcha, but didn't submit the
form) are collected automatically after the predefined expiration time.
Developers can also provide custom store (for example, which saves captcha
ids and solutions in database) by implementing Store interface and
registering the object with SetCustomStore.
Captchas are created by calling New, which returns the captcha id. Their
representations, though, are created on-the-fly by calling WriteImage or
WriteAudio functions. Created representations are not stored anywhere, but
subsequent calls to these functions with the same id will write the same
captcha solution. Reload function will create a new different solution for the
provided captcha, allowing users to "reload" captcha if they can't solve the
displayed one without reloading the whole page. Verify and VerifyString are
used to verify that the given solution is the right one for the given captcha
id.
Server provides an http.Handler which can serve image and audio
representations of captchas automatically from the URL. It can also be used
to reload captchas. Refer to Server function documentation for details, or
take a look at the example in "capexample" subdirectory.
Examples
--------
![Image](https://github.com/dchest/captcha/raw/master/capgen/example.png)
[Audio](https://github.com/dchest/captcha/raw/master/capgen/example.wav)
Constants
---------
``` go
const (
// Default number of digits in captcha solution.
DefaultLen = 6
// The number of captchas created that triggers garbage collection used
// by default store.
CollectNum = 100
// Expiration time of captchas used by default store.
Expiration = 10 * time.Minute
)
```
``` go
const (
// Standard width and height of a captcha image.
StdWidth = 240
StdHeight = 80
)
```
Variables
---------
``` go
var (
ErrNotFound = errors.New("captcha: id not found")
)
```
Functions
---------
### func New
func New() string
New creates a new captcha with the standard length, saves it in the internal
storage and returns its id.
### func NewLen
func NewLen(length int) (id string)
NewLen is just like New, but accepts length of a captcha solution as the
argument.
### func RandomDigits
func RandomDigits(length int) (b []byte)
RandomDigits returns a byte slice of the given length containing
pseudorandom numbers in range 0-9. The slice can be used as a captcha
solution.
### func Reload
func Reload(id string) bool
Reload generates and remembers new digits for the given captcha id. This
function returns false if there is no captcha with the given id.
After calling this function, the image or audio presented to a user must be
refreshed to show the new captcha representation (WriteImage and WriteAudio
will write the new one).
### func Server
func Server(imgWidth, imgHeight int) http.Handler
Server returns a handler that serves HTTP requests with image or
audio representations of captchas. Image dimensions are accepted as
arguments. The server decides which captcha to serve based on the last URL
path component: file name part must contain a captcha id, file extension —
its format (PNG or WAV).
For example, for file name "LBm5vMjHDtdUfaWYXiQX.png" it serves an image captcha
with id "LBm5vMjHDtdUfaWYXiQX", and for "LBm5vMjHDtdUfaWYXiQX.wav" it serves the
same captcha in audio format.
To serve a captcha as a downloadable file, the URL must be constructed in
such a way as if the file to serve is in the "download" subdirectory:
"/download/LBm5vMjHDtdUfaWYXiQX.wav".
To reload captcha (get a different solution for the same captcha id), append
"?reload=x" to URL, where x may be anything (for example, current time or a
random number to make browsers refetch an image instead of loading it from
cache).
By default, the Server serves audio in English language. To serve audio
captcha in one of the other supported languages, append "lang" value, for
example, "?lang=ru".
### func SetCustomStore
func SetCustomStore(s Store)
SetCustomStore sets custom storage for captchas, replacing the default
memory store. This function must be called before generating any captchas.
### func Verify
func Verify(id string, digits []byte) bool
Verify returns true if the given digits are the ones that were used to
create the given captcha id.
The function deletes the captcha with the given id from the internal
storage, so that the same captcha can't be verified anymore.
### func VerifyString
func VerifyString(id string, digits string) bool
VerifyString is like Verify, but accepts a string of digits. It removes
spaces and commas from the string, but any other characters, apart from
digits and listed above, will cause the function to return false.
### func WriteAudio
func WriteAudio(w io.Writer, id string, lang string) error
WriteAudio writes WAV-encoded audio representation of the captcha with the
given id and the given language. If there are no sounds for the given
language, English is used.
### func WriteImage
func WriteImage(w io.Writer, id string, width, height int) error
WriteImage writes PNG-encoded image representation of the captcha with the
given id. The image will have the given width and height.
Types
-----
``` go
type Audio struct {
// contains unexported fields
}
```
### func NewAudio
func NewAudio(id string, digits []byte, lang string) *Audio
NewAudio returns a new audio captcha with the given digits, where each digit
must be in range 0-9. Digits are pronounced in the given language. If there
are no sounds for the given language, English is used.
Possible values for lang are "en", "ru", "zh".
### func (*Audio) EncodedLen
func (a *Audio) EncodedLen() int
EncodedLen returns the length of WAV-encoded audio captcha.
### func (*Audio) WriteTo
func (a *Audio) WriteTo(w io.Writer) (n int64, err error)
WriteTo writes captcha audio in WAVE format into the given io.Writer, and
returns the number of bytes written and an error if any.
``` go
type Image struct {
*image.Paletted
// contains unexported fields
}
```
### func NewImage
func NewImage(id string, digits []byte, width, height int) *Image
NewImage returns a new captcha image of the given width and height with the
given digits, where each digit must be in range 0-9.
### func (*Image) WriteTo
func (m *Image) WriteTo(w io.Writer) (int64, error)
WriteTo writes captcha image in PNG format into the given writer.
``` go
type Store interface {
// Set sets the digits for the captcha id.
Set(id string, digits []byte)
// Get returns stored digits for the captcha id. Clear indicates
// whether the captcha must be deleted from the store.
Get(id string, clear bool) (digits []byte)
}
```
An object implementing Store interface can be registered with SetCustomStore
function to handle storage and retrieval of captcha ids and solutions for
them, replacing the default memory store.
It is the responsibility of an object to delete expired and used captchas
when necessary (for example, the default memory store collects them in Set
method after the certain amount of captchas has been stored.)
### func NewMemoryStore
func NewMemoryStore(collectNum int, expiration time.Duration) Store
NewMemoryStore returns a new standard memory store for captchas with the
given collection threshold and expiration time in seconds. The returned
store must be registered with SetCustomStore to replace the default one.

232
vendor/github.com/dchest/captcha/audio.go generated vendored Normal file
View File

@ -0,0 +1,232 @@
// Copyright 2011-2014 Dmitry Chestnykh. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package captcha
import (
"bytes"
"encoding/binary"
"io"
"math"
)
const sampleRate = 8000 // Hz
var endingBeepSound []byte
func init() {
endingBeepSound = changeSpeed(beepSound, 1.4)
}
type Audio struct {
body *bytes.Buffer
digitSounds [][]byte
rng siprng
}
// NewAudio returns a new audio captcha with the given digits, where each digit
// must be in range 0-9. Digits are pronounced in the given language. If there
// are no sounds for the given language, English is used.
//
// Possible values for lang are "en", "ru", "zh".
func NewAudio(id string, digits []byte, lang string) *Audio {
a := new(Audio)
// Initialize PRNG.
a.rng.Seed(deriveSeed(audioSeedPurpose, id, digits))
if sounds, ok := digitSounds[lang]; ok {
a.digitSounds = sounds
} else {
a.digitSounds = digitSounds["en"]
}
numsnd := make([][]byte, len(digits))
nsdur := 0
for i, n := range digits {
snd := a.randomizedDigitSound(n)
nsdur += len(snd)
numsnd[i] = snd
}
// Random intervals between digits (including beginning).
intervals := make([]int, len(digits)+1)
intdur := 0
for i := range intervals {
dur := a.rng.Int(sampleRate, sampleRate*3) // 1 to 3 seconds
intdur += dur
intervals[i] = dur
}
// Generate background sound.
bg := a.makeBackgroundSound(a.longestDigitSndLen()*len(digits) + intdur)
// Create buffer and write audio to it.
sil := makeSilence(sampleRate / 5)
bufcap := 3*len(beepSound) + 2*len(sil) + len(bg) + len(endingBeepSound)
a.body = bytes.NewBuffer(make([]byte, 0, bufcap))
// Write prelude, three beeps.
a.body.Write(beepSound)
a.body.Write(sil)
a.body.Write(beepSound)
a.body.Write(sil)
a.body.Write(beepSound)
// Write digits.
pos := intervals[0]
for i, v := range numsnd {
mixSound(bg[pos:], v)
pos += len(v) + intervals[i+1]
}
a.body.Write(bg)
// Write ending (one beep).
a.body.Write(endingBeepSound)
return a
}
// WriteTo writes captcha audio in WAVE format into the given io.Writer, and
// returns the number of bytes written and an error if any.
func (a *Audio) WriteTo(w io.Writer) (n int64, err error) {
// Calculate padded length of PCM chunk data.
bodyLen := uint32(a.body.Len())
paddedBodyLen := bodyLen
if bodyLen%2 != 0 {
paddedBodyLen++
}
totalLen := uint32(len(waveHeader)) - 4 + paddedBodyLen
// Header.
header := make([]byte, len(waveHeader)+4) // includes 4 bytes for chunk size
copy(header, waveHeader)
// Put the length of whole RIFF chunk.
binary.LittleEndian.PutUint32(header[4:], totalLen)
// Put the length of WAVE chunk.
binary.LittleEndian.PutUint32(header[len(waveHeader):], bodyLen)
// Write header.
nn, err := w.Write(header)
n = int64(nn)
if err != nil {
return
}
// Write data.
n, err = a.body.WriteTo(w)
n += int64(nn)
if err != nil {
return
}
// Pad byte if chunk length is odd.
// (As header has even length, we can check if n is odd, not chunk).
if bodyLen != paddedBodyLen {
w.Write([]byte{0})
n++
}
return
}
// EncodedLen returns the length of WAV-encoded audio captcha.
func (a *Audio) EncodedLen() int {
return len(waveHeader) + 4 + a.body.Len()
}
func (a *Audio) makeBackgroundSound(length int) []byte {
b := a.makeWhiteNoise(length, 4)
for i := 0; i < length/(sampleRate/10); i++ {
snd := reversedSound(a.digitSounds[a.rng.Intn(10)])
snd = changeSpeed(snd, a.rng.Float(0.8, 1.4))
place := a.rng.Intn(len(b) - len(snd))
setSoundLevel(snd, a.rng.Float(0.2, 0.5))
mixSound(b[place:], snd)
}
return b
}
func (a *Audio) randomizedDigitSound(n byte) []byte {
s := a.randomSpeed(a.digitSounds[n])
setSoundLevel(s, a.rng.Float(0.75, 1.2))
return s
}
func (a *Audio) longestDigitSndLen() int {
n := 0
for _, v := range a.digitSounds {
if n < len(v) {
n = len(v)
}
}
return n
}
func (a *Audio) randomSpeed(b []byte) []byte {
pitch := a.rng.Float(0.9, 1.2)
return changeSpeed(b, pitch)
}
func (a *Audio) makeWhiteNoise(length int, level uint8) []byte {
noise := a.rng.Bytes(length)
adj := 128 - level/2
for i, v := range noise {
v %= level
v += adj
noise[i] = v
}
return noise
}
// mixSound mixes src into dst. Dst must have length equal to or greater than
// src length.
func mixSound(dst, src []byte) {
for i, v := range src {
av := int(v)
bv := int(dst[i])
if av < 128 && bv < 128 {
dst[i] = byte(av * bv / 128)
} else {
dst[i] = byte(2*(av+bv) - av*bv/128 - 256)
}
}
}
func setSoundLevel(a []byte, level float64) {
for i, v := range a {
av := float64(v)
switch {
case av > 128:
if av = (av-128)*level + 128; av < 128 {
av = 128
}
case av < 128:
if av = 128 - (128-av)*level; av > 128 {
av = 128
}
default:
continue
}
a[i] = byte(av)
}
}
// changeSpeed returns new PCM bytes from the bytes with the speed and pitch
// changed to the given value that must be in range [0, x].
func changeSpeed(a []byte, speed float64) []byte {
b := make([]byte, int(math.Floor(float64(len(a))*speed)))
var p float64
for _, v := range a {
for i := int(p); i < int(p+speed); i++ {
b[i] = v
}
p += speed
}
return b
}
func makeSilence(length int) []byte {
b := make([]byte, length)
for i := range b {
b[i] = 128
}
return b
}
func reversedSound(a []byte) []byte {
n := len(a)
b := make([]byte, n)
for i, v := range a {
b[n-1-i] = v
}
return b
}

165
vendor/github.com/dchest/captcha/captcha.go generated vendored Normal file
View File

@ -0,0 +1,165 @@
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// Package captcha implements generation and verification of image and audio
// CAPTCHAs.
//
// A captcha solution is the sequence of digits 0-9 with the defined length.
// There are two captcha representations: image and audio.
//
// An image representation is a PNG-encoded image with the solution printed on
// it in such a way that makes it hard for computers to solve it using OCR.
//
// An audio representation is a WAVE-encoded (8 kHz unsigned 8-bit) sound with
// the spoken solution (currently in English, Russian, and Chinese). To make it
// hard for computers to solve audio captcha, the voice that pronounces numbers
// has random speed and pitch, and there is a randomly generated background
// noise mixed into the sound.
//
// This package doesn't require external files or libraries to generate captcha
// representations; it is self-contained.
//
// To make captchas one-time, the package includes a memory storage that stores
// captcha ids, their solutions, and expiration time. Used captchas are removed
// from the store immediately after calling Verify or VerifyString, while
// unused captchas (user loaded a page with captcha, but didn't submit the
// form) are collected automatically after the predefined expiration time.
// Developers can also provide custom store (for example, which saves captcha
// ids and solutions in database) by implementing Store interface and
// registering the object with SetCustomStore.
//
// Captchas are created by calling New, which returns the captcha id. Their
// representations, though, are created on-the-fly by calling WriteImage or
// WriteAudio functions. Created representations are not stored anywhere, but
// subsequent calls to these functions with the same id will write the same
// captcha solution. Reload function will create a new different solution for
// the provided captcha, allowing users to "reload" captcha if they can't solve
// the displayed one without reloading the whole page. Verify and VerifyString
// are used to verify that the given solution is the right one for the given
// captcha id.
//
// Server provides an http.Handler which can serve image and audio
// representations of captchas automatically from the URL. It can also be used
// to reload captchas. Refer to Server function documentation for details, or
// take a look at the example in "capexample" subdirectory.
package captcha
import (
"bytes"
"errors"
"io"
"time"
)
const (
// Default number of digits in captcha solution.
DefaultLen = 6
// The number of captchas created that triggers garbage collection used
// by default store.
CollectNum = 100
// Expiration time of captchas used by default store.
Expiration = 10 * time.Minute
)
var (
ErrNotFound = errors.New("captcha: id not found")
// globalStore is a shared storage for captchas, generated by New function.
globalStore = NewMemoryStore(CollectNum, Expiration)
)
// SetCustomStore sets custom storage for captchas, replacing the default
// memory store. This function must be called before generating any captchas.
func SetCustomStore(s Store) {
globalStore = s
}
// New creates a new captcha with the standard length, saves it in the internal
// storage and returns its id.
func New() string {
return NewLen(DefaultLen)
}
// NewLen is just like New, but accepts length of a captcha solution as the
// argument.
func NewLen(length int) (id string) {
id = randomId()
globalStore.Set(id, RandomDigits(length))
return
}
// Reload generates and remembers new digits for the given captcha id. This
// function returns false if there is no captcha with the given id.
//
// After calling this function, the image or audio presented to a user must be
// refreshed to show the new captcha representation (WriteImage and WriteAudio
// will write the new one).
func Reload(id string) bool {
old := globalStore.Get(id, false)
if old == nil {
return false
}
globalStore.Set(id, RandomDigits(len(old)))
return true
}
// WriteImage writes PNG-encoded image representation of the captcha with the
// given id. The image will have the given width and height.
func WriteImage(w io.Writer, id string, width, height int) error {
d := globalStore.Get(id, false)
if d == nil {
return ErrNotFound
}
_, err := NewImage(id, d, width, height).WriteTo(w)
return err
}
// WriteAudio writes WAV-encoded audio representation of the captcha with the
// given id and the given language. If there are no sounds for the given
// language, English is used.
func WriteAudio(w io.Writer, id string, lang string) error {
d := globalStore.Get(id, false)
if d == nil {
return ErrNotFound
}
_, err := NewAudio(id, d, lang).WriteTo(w)
return err
}
// Verify returns true if the given digits are the ones that were used to
// create the given captcha id.
//
// The function deletes the captcha with the given id from the internal
// storage, so that the same captcha can't be verified anymore.
func Verify(id string, digits []byte) bool {
if digits == nil || len(digits) == 0 {
return false
}
reald := globalStore.Get(id, true)
if reald == nil {
return false
}
return bytes.Equal(digits, reald)
}
// VerifyString is like Verify, but accepts a string of digits. It removes
// spaces and commas from the string, but any other characters, apart from
// digits and listed above, will cause the function to return false.
func VerifyString(id string, digits string) bool {
if digits == "" {
return false
}
ns := make([]byte, len(digits))
for i := range ns {
d := digits[i]
switch {
case '0' <= d && d <= '9':
ns[i] = d - '0'
case d == ' ' || d == ',':
// ignore
default:
return false
}
}
return Verify(id, ns)
}

214
vendor/github.com/dchest/captcha/font.go generated vendored Normal file
View File

@ -0,0 +1,214 @@
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package captcha
const (
fontWidth = 11
fontHeight = 18
blackChar = 1
)
var font = [][]byte{
{ // 0
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 1
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0,
0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
},
{ // 2
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,
0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
},
{ // 3
0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 4
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0,
0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0,
0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0,
0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0,
0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0,
0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0,
1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0,
1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
},
{ // 5
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 6
0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0,
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0,
1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0,
1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 7
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0,
0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0,
0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0,
},
{ // 8
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0,
0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
},
{ // 9
0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1,
1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1,
0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,
0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0,
0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
},
}

271
vendor/github.com/dchest/captcha/image.go generated vendored Normal file
View File

@ -0,0 +1,271 @@
// Copyright 2011-2014 Dmitry Chestnykh. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package captcha
import (
"bytes"
"image"
"image/color"
"image/png"
"io"
"math"
)
const (
// Standard width and height of a captcha image.
StdWidth = 240
StdHeight = 80
// Maximum absolute skew factor of a single digit.
maxSkew = 0.7
// Number of background circles.
circleCount = 20
)
type Image struct {
*image.Paletted
numWidth int
numHeight int
dotSize int
rng siprng
}
// NewImage returns a new captcha image of the given width and height with the
// given digits, where each digit must be in range 0-9.
func NewImage(id string, digits []byte, width, height int) *Image {
m := new(Image)
// Initialize PRNG.
m.rng.Seed(deriveSeed(imageSeedPurpose, id, digits))
m.Paletted = image.NewPaletted(image.Rect(0, 0, width, height), m.getRandomPalette())
m.calculateSizes(width, height, len(digits))
// Randomly position captcha inside the image.
maxx := width - (m.numWidth+m.dotSize)*len(digits) - m.dotSize
maxy := height - m.numHeight - m.dotSize*2
var border int
if width > height {
border = height / 5
} else {
border = width / 5
}
x := m.rng.Int(border, maxx-border)
y := m.rng.Int(border, maxy-border)
// Draw digits.
for _, n := range digits {
m.drawDigit(font[n], x, y)
x += m.numWidth + m.dotSize
}
// Draw strike-through line.
m.strikeThrough()
// Apply wave distortion.
m.distort(m.rng.Float(5, 10), m.rng.Float(100, 200))
// Fill image with random circles.
m.fillWithCircles(circleCount, m.dotSize)
return m
}
func (m *Image) getRandomPalette() color.Palette {
p := make([]color.Color, circleCount+1)
// Transparent color.
p[0] = color.RGBA{0xFF, 0xFF, 0xFF, 0x00}
// Primary color.
prim := color.RGBA{
uint8(m.rng.Intn(129)),
uint8(m.rng.Intn(129)),
uint8(m.rng.Intn(129)),
0xFF,
}
p[1] = prim
// Circle colors.
for i := 2; i <= circleCount; i++ {
p[i] = m.randomBrightness(prim, 255)
}
return p
}
// encodedPNG encodes an image to PNG and returns
// the result as a byte slice.
func (m *Image) encodedPNG() []byte {
var buf bytes.Buffer
if err := png.Encode(&buf, m.Paletted); err != nil {
panic(err.Error())
}
return buf.Bytes()
}
// WriteTo writes captcha image in PNG format into the given writer.
func (m *Image) WriteTo(w io.Writer) (int64, error) {
n, err := w.Write(m.encodedPNG())
return int64(n), err
}
func (m *Image) calculateSizes(width, height, ncount int) {
// Goal: fit all digits inside the image.
var border int
if width > height {
border = height / 4
} else {
border = width / 4
}
// Convert everything to floats for calculations.
w := float64(width - border*2)
h := float64(height - border*2)
// fw takes into account 1-dot spacing between digits.
fw := float64(fontWidth + 1)
fh := float64(fontHeight)
nc := float64(ncount)
// Calculate the width of a single digit taking into account only the
// width of the image.
nw := w / nc
// Calculate the height of a digit from this width.
nh := nw * fh / fw
// Digit too high?
if nh > h {
// Fit digits based on height.
nh = h
nw = fw / fh * nh
}
// Calculate dot size.
m.dotSize = int(nh / fh)
if m.dotSize < 1 {
m.dotSize = 1
}
// Save everything, making the actual width smaller by 1 dot to account
// for spacing between digits.
m.numWidth = int(nw) - m.dotSize
m.numHeight = int(nh)
}
func (m *Image) drawHorizLine(fromX, toX, y int, colorIdx uint8) {
for x := fromX; x <= toX; x++ {
m.SetColorIndex(x, y, colorIdx)
}
}
func (m *Image) drawCircle(x, y, radius int, colorIdx uint8) {
f := 1 - radius
dfx := 1
dfy := -2 * radius
xo := 0
yo := radius
m.SetColorIndex(x, y+radius, colorIdx)
m.SetColorIndex(x, y-radius, colorIdx)
m.drawHorizLine(x-radius, x+radius, y, colorIdx)
for xo < yo {
if f >= 0 {
yo--
dfy += 2
f += dfy
}
xo++
dfx += 2
f += dfx
m.drawHorizLine(x-xo, x+xo, y+yo, colorIdx)
m.drawHorizLine(x-xo, x+xo, y-yo, colorIdx)
m.drawHorizLine(x-yo, x+yo, y+xo, colorIdx)
m.drawHorizLine(x-yo, x+yo, y-xo, colorIdx)
}
}
func (m *Image) fillWithCircles(n, maxradius int) {
maxx := m.Bounds().Max.X
maxy := m.Bounds().Max.Y
for i := 0; i < n; i++ {
colorIdx := uint8(m.rng.Int(1, circleCount-1))
r := m.rng.Int(1, maxradius)
m.drawCircle(m.rng.Int(r, maxx-r), m.rng.Int(r, maxy-r), r, colorIdx)
}
}
func (m *Image) strikeThrough() {
maxx := m.Bounds().Max.X
maxy := m.Bounds().Max.Y
y := m.rng.Int(maxy/3, maxy-maxy/3)
amplitude := m.rng.Float(5, 20)
period := m.rng.Float(80, 180)
dx := 2.0 * math.Pi / period
for x := 0; x < maxx; x++ {
xo := amplitude * math.Cos(float64(y)*dx)
yo := amplitude * math.Sin(float64(x)*dx)
for yn := 0; yn < m.dotSize; yn++ {
r := m.rng.Int(0, m.dotSize)
m.drawCircle(x+int(xo), y+int(yo)+(yn*m.dotSize), r/2, 1)
}
}
}
func (m *Image) drawDigit(digit []byte, x, y int) {
skf := m.rng.Float(-maxSkew, maxSkew)
xs := float64(x)
r := m.dotSize / 2
y += m.rng.Int(-r, r)
for yo := 0; yo < fontHeight; yo++ {
for xo := 0; xo < fontWidth; xo++ {
if digit[yo*fontWidth+xo] != blackChar {
continue
}
m.drawCircle(x+xo*m.dotSize, y+yo*m.dotSize, r, 1)
}
xs += skf
x = int(xs)
}
}
func (m *Image) distort(amplude float64, period float64) {
w := m.Bounds().Max.X
h := m.Bounds().Max.Y
oldm := m.Paletted
newm := image.NewPaletted(image.Rect(0, 0, w, h), oldm.Palette)
dx := 2.0 * math.Pi / period
for x := 0; x < w; x++ {
for y := 0; y < h; y++ {
xo := amplude * math.Sin(float64(y)*dx)
yo := amplude * math.Cos(float64(x)*dx)
newm.SetColorIndex(x, y, oldm.ColorIndexAt(x+int(xo), y+int(yo)))
}
}
m.Paletted = newm
}
func (m *Image) randomBrightness(c color.RGBA, max uint8) color.RGBA {
minc := min3(c.R, c.G, c.B)
maxc := max3(c.R, c.G, c.B)
if maxc > max {
return c
}
n := m.rng.Intn(int(max-maxc)) - int(minc)
return color.RGBA{
uint8(int(c.R) + n),
uint8(int(c.G) + n),
uint8(int(c.B) + n),
uint8(c.A),
}
}
func min3(x, y, z uint8) (m uint8) {
m = x
if y < m {
m = y
}
if z < m {
m = z
}
return
}
func max3(x, y, z uint8) (m uint8) {
m = x
if y > m {
m = y
}
if z > m {
m = z
}
return
}

108
vendor/github.com/dchest/captcha/random.go generated vendored Normal file
View File

@ -0,0 +1,108 @@
// Copyright 2011-2014 Dmitry Chestnykh. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package captcha
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"io"
)
// idLen is a length of captcha id string.
// (20 bytes of 62-letter alphabet give ~119 bits.)
const idLen = 20
// idChars are characters allowed in captcha id.
var idChars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
// rngKey is a secret key used to deterministically derive seeds for
// PRNGs used in image and audio. Generated once during initialization.
var rngKey [32]byte
func init() {
if _, err := io.ReadFull(rand.Reader, rngKey[:]); err != nil {
panic("captcha: error reading random source: " + err.Error())
}
}
// Purposes for seed derivation. The goal is to make deterministic PRNG produce
// different outputs for images and audio by using different derived seeds.
const (
imageSeedPurpose = 0x01
audioSeedPurpose = 0x02
)
// deriveSeed returns a 16-byte PRNG seed from rngKey, purpose, id and digits.
// Same purpose, id and digits will result in the same derived seed for this
// instance of running application.
//
// out = HMAC(rngKey, purpose || id || 0x00 || digits) (cut to 16 bytes)
//
func deriveSeed(purpose byte, id string, digits []byte) (out [16]byte) {
var buf [sha256.Size]byte
h := hmac.New(sha256.New, rngKey[:])
h.Write([]byte{purpose})
io.WriteString(h, id)
h.Write([]byte{0})
h.Write(digits)
sum := h.Sum(buf[:0])
copy(out[:], sum)
return
}
// RandomDigits returns a byte slice of the given length containing
// pseudorandom numbers in range 0-9. The slice can be used as a captcha
// solution.
func RandomDigits(length int) []byte {
return randomBytesMod(length, 10)
}
// randomBytes returns a byte slice of the given length read from CSPRNG.
func randomBytes(length int) (b []byte) {
b = make([]byte, length)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
panic("captcha: error reading random source: " + err.Error())
}
return
}
// randomBytesMod returns a byte slice of the given length, where each byte is
// a random number modulo mod.
func randomBytesMod(length int, mod byte) (b []byte) {
if length == 0 {
return nil
}
if mod == 0 {
panic("captcha: bad mod argument for randomBytesMod")
}
maxrb := 255 - byte(256%int(mod))
b = make([]byte, length)
i := 0
for {
r := randomBytes(length + (length / 4))
for _, c := range r {
if c > maxrb {
// Skip this number to avoid modulo bias.
continue
}
b[i] = c % mod
i++
if i == length {
return
}
}
}
}
// randomId returns a new random id string.
func randomId() string {
b := randomBytesMod(idLen, byte(len(idChars)))
for i, c := range b {
b[i] = idChars[c]
}
return string(b)
}

87
vendor/github.com/dchest/captcha/server.go generated vendored Normal file
View File

@ -0,0 +1,87 @@
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package captcha
import (
"bytes"
"net/http"
"path"
"strings"
"time"
)
type captchaHandler struct {
imgWidth int
imgHeight int
}
// Server returns a handler that serves HTTP requests with image or
// audio representations of captchas. Image dimensions are accepted as
// arguments. The server decides which captcha to serve based on the last URL
// path component: file name part must contain a captcha id, file extension —
// its format (PNG or WAV).
//
// For example, for file name "LBm5vMjHDtdUfaWYXiQX.png" it serves an image captcha
// with id "LBm5vMjHDtdUfaWYXiQX", and for "LBm5vMjHDtdUfaWYXiQX.wav" it serves the
// same captcha in audio format.
//
// To serve a captcha as a downloadable file, the URL must be constructed in
// such a way as if the file to serve is in the "download" subdirectory:
// "/download/LBm5vMjHDtdUfaWYXiQX.wav".
//
// To reload captcha (get a different solution for the same captcha id), append
// "?reload=x" to URL, where x may be anything (for example, current time or a
// random number to make browsers refetch an image instead of loading it from
// cache).
//
// By default, the Server serves audio in English language. To serve audio
// captcha in one of the other supported languages, append "lang" value, for
// example, "?lang=ru".
func Server(imgWidth, imgHeight int) http.Handler {
return &captchaHandler{imgWidth, imgHeight}
}
func (h *captchaHandler) serve(w http.ResponseWriter, r *http.Request, id, ext, lang string, download bool) error {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
var content bytes.Buffer
switch ext {
case ".png":
w.Header().Set("Content-Type", "image/png")
WriteImage(&content, id, h.imgWidth, h.imgHeight)
case ".wav":
w.Header().Set("Content-Type", "audio/x-wav")
WriteAudio(&content, id, lang)
default:
return ErrNotFound
}
if download {
w.Header().Set("Content-Type", "application/octet-stream")
}
http.ServeContent(w, r, id+ext, time.Time{}, bytes.NewReader(content.Bytes()))
return nil
}
func (h *captchaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
dir, file := path.Split(r.URL.Path)
ext := path.Ext(file)
id := file[:len(file)-len(ext)]
if ext == "" || id == "" {
http.NotFound(w, r)
return
}
if r.FormValue("reload") != "" {
Reload(id)
}
lang := strings.ToLower(r.FormValue("lang"))
download := path.Base(dir) == "download"
if h.serve(w, r, id, ext, lang, download) == ErrNotFound {
http.NotFound(w, r)
}
// Ignore other errors.
}

278
vendor/github.com/dchest/captcha/siprng.go generated vendored Normal file
View File

@ -0,0 +1,278 @@
// Copyright 2014 Dmitry Chestnykh. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package captcha
import "encoding/binary"
// siprng is PRNG based on SipHash-2-4.
// (Note: it's not safe to use a single siprng from multiple goroutines.)
type siprng struct {
k0, k1, ctr uint64
}
// siphash implements SipHash-2-4, accepting a uint64 as a message.
func siphash(k0, k1, m uint64) uint64 {
// Initialization.
v0 := k0 ^ 0x736f6d6570736575
v1 := k1 ^ 0x646f72616e646f6d
v2 := k0 ^ 0x6c7967656e657261
v3 := k1 ^ 0x7465646279746573
t := uint64(8) << 56
// Compression.
v3 ^= m
// Round 1.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 2.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
v0 ^= m
// Compress last block.
v3 ^= t
// Round 1.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 2.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
v0 ^= t
// Finalization.
v2 ^= 0xff
// Round 1.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 2.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 3.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
// Round 4.
v0 += v1
v1 = v1<<13 | v1>>(64-13)
v1 ^= v0
v0 = v0<<32 | v0>>(64-32)
v2 += v3
v3 = v3<<16 | v3>>(64-16)
v3 ^= v2
v0 += v3
v3 = v3<<21 | v3>>(64-21)
v3 ^= v0
v2 += v1
v1 = v1<<17 | v1>>(64-17)
v1 ^= v2
v2 = v2<<32 | v2>>(64-32)
return v0 ^ v1 ^ v2 ^ v3
}
// SetSeed sets a new secret seed for PRNG.
func (p *siprng) Seed(k [16]byte) {
p.k0 = binary.LittleEndian.Uint64(k[0:8])
p.k1 = binary.LittleEndian.Uint64(k[8:16])
p.ctr = 1
}
// Uint64 returns a new pseudorandom uint64.
func (p *siprng) Uint64() uint64 {
v := siphash(p.k0, p.k1, p.ctr)
p.ctr++
return v
}
func (p *siprng) Bytes(n int) []byte {
// Since we don't have a buffer for generated bytes in siprng state,
// we just generate enough 8-byte blocks and then cut the result to the
// required length. Doing it this way, we lose generated bytes, and we
// don't get the strictly sequential deterministic output from PRNG:
// calling Uint64() and then Bytes(3) produces different output than
// when calling them in the reverse order, but for our applications
// this is OK.
numBlocks := (n + 8 - 1) / 8
b := make([]byte, numBlocks*8)
for i := 0; i < len(b); i += 8 {
binary.LittleEndian.PutUint64(b[i:], p.Uint64())
}
return b[:n]
}
func (p *siprng) Int63() int64 {
return int64(p.Uint64() & 0x7fffffffffffffff)
}
func (p *siprng) Uint32() uint32 {
return uint32(p.Uint64())
}
func (p *siprng) Int31() int32 {
return int32(p.Uint32() & 0x7fffffff)
}
func (p *siprng) Intn(n int) int {
if n <= 0 {
panic("invalid argument to Intn")
}
if n <= 1<<31-1 {
return int(p.Int31n(int32(n)))
}
return int(p.Int63n(int64(n)))
}
func (p *siprng) Int63n(n int64) int64 {
if n <= 0 {
panic("invalid argument to Int63n")
}
max := int64((1 << 63) - 1 - (1<<63)%uint64(n))
v := p.Int63()
for v > max {
v = p.Int63()
}
return v % n
}
func (p *siprng) Int31n(n int32) int32 {
if n <= 0 {
panic("invalid argument to Int31n")
}
max := int32((1 << 31) - 1 - (1<<31)%uint32(n))
v := p.Int31()
for v > max {
v = p.Int31()
}
return v % n
}
func (p *siprng) Float64() float64 { return float64(p.Int63()) / (1 << 63) }
// Int returns a pseudorandom int in range [from, to].
func (p *siprng) Int(from, to int) int {
return p.Intn(to+1-from) + from
}
// Float returns a pseudorandom float64 in range [from, to].
func (p *siprng) Float(from, to float64) float64 {
return (to-from)*p.Float64() + from
}

7390
vendor/github.com/dchest/captcha/sounds.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

117
vendor/github.com/dchest/captcha/store.go generated vendored Normal file
View File

@ -0,0 +1,117 @@
// Copyright 2011 Dmitry Chestnykh. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package captcha
import (
"container/list"
"sync"
"time"
)
// An object implementing Store interface can be registered with SetCustomStore
// function to handle storage and retrieval of captcha ids and solutions for
// them, replacing the default memory store.
//
// It is the responsibility of an object to delete expired and used captchas
// when necessary (for example, the default memory store collects them in Set
// method after the certain amount of captchas has been stored.)
type Store interface {
// Set sets the digits for the captcha id.
Set(id string, digits []byte)
// Get returns stored digits for the captcha id. Clear indicates
// whether the captcha must be deleted from the store.
Get(id string, clear bool) (digits []byte)
}
// expValue stores timestamp and id of captchas. It is used in the list inside
// memoryStore for indexing generated captchas by timestamp to enable garbage
// collection of expired captchas.
type idByTimeValue struct {
timestamp time.Time
id string
}
// memoryStore is an internal store for captcha ids and their values.
type memoryStore struct {
sync.RWMutex
digitsById map[string][]byte
idByTime *list.List
// Number of items stored since last collection.
numStored int
// Number of saved items that triggers collection.
collectNum int
// Expiration time of captchas.
expiration time.Duration
}
// NewMemoryStore returns a new standard memory store for captchas with the
// given collection threshold and expiration time (duration). The returned
// store must be registered with SetCustomStore to replace the default one.
func NewMemoryStore(collectNum int, expiration time.Duration) Store {
s := new(memoryStore)
s.digitsById = make(map[string][]byte)
s.idByTime = list.New()
s.collectNum = collectNum
s.expiration = expiration
return s
}
func (s *memoryStore) Set(id string, digits []byte) {
s.Lock()
s.digitsById[id] = digits
s.idByTime.PushBack(idByTimeValue{time.Now(), id})
s.numStored++
if s.numStored <= s.collectNum {
s.Unlock()
return
}
s.Unlock()
go s.collect()
}
func (s *memoryStore) Get(id string, clear bool) (digits []byte) {
if !clear {
// When we don't need to clear captcha, acquire read lock.
s.RLock()
defer s.RUnlock()
} else {
s.Lock()
defer s.Unlock()
}
digits, ok := s.digitsById[id]
if !ok {
return
}
if clear {
delete(s.digitsById, id)
// XXX(dchest) Index (s.idByTime) will be cleaned when
// collecting expired captchas. Can't clean it here, because
// we don't store reference to expValue in the map.
// Maybe store it?
}
return
}
func (s *memoryStore) collect() {
now := time.Now()
s.Lock()
defer s.Unlock()
s.numStored = 0
for e := s.idByTime.Front(); e != nil; {
ev, ok := e.Value.(idByTimeValue)
if !ok {
return
}
if ev.timestamp.Add(s.expiration).Before(now) {
delete(s.digitsById, ev.id)
next := e.Next()
s.idByTime.Remove(e)
e = next
} else {
return
}
}
}

6
vendor/vendor.json vendored
View File

@ -32,6 +32,12 @@
"revision": "a5fe2436ffcb3236e175e5149162b41cd28bd27d", "revision": "a5fe2436ffcb3236e175e5149162b41cd28bd27d",
"revisionTime": "2015-03-29T02:31:25Z" "revisionTime": "2015-03-29T02:31:25Z"
}, },
{
"checksumSHA1": "0GvcY9P9GSq/aJQEoRB173b4Fbw=",
"path": "github.com/dchest/captcha",
"revision": "9e952142169c3cd6268c6482a3a61c121536aca2",
"revisionTime": "2015-07-28T12:50:59Z"
},
{ {
"checksumSHA1": "6defOlYtxIqheaUEG/cWWouQnIU=", "checksumSHA1": "6defOlYtxIqheaUEG/cWWouQnIU=",
"path": "github.com/hlandau/passlib", "path": "github.com/hlandau/passlib",