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:
parent
69b0d20825
commit
4177901714
@ -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))
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"))
|
||||||
|
@ -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"))
|
||||||
|
@ -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">>></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>
|
||||||
|
@ -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">>></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>
|
169
http/board/res/thread.tmpl.html
Normal file
169
http/board/res/thread.tmpl.html
Normal 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">>></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" . }}
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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()))
|
||||||
|
@ -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
16
resources/captcha.go
Normal 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)
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
19
vendor/github.com/dchest/captcha/LICENSE
generated
vendored
Normal 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
275
vendor/github.com/dchest/captcha/README.md
generated
vendored
Normal 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
232
vendor/github.com/dchest/captcha/audio.go
generated
vendored
Normal 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
165
vendor/github.com/dchest/captcha/captcha.go
generated
vendored
Normal 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
214
vendor/github.com/dchest/captcha/font.go
generated
vendored
Normal 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
271
vendor/github.com/dchest/captcha/image.go
generated
vendored
Normal 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
108
vendor/github.com/dchest/captcha/random.go
generated
vendored
Normal 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
87
vendor/github.com/dchest/captcha/server.go
generated
vendored
Normal 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
278
vendor/github.com/dchest/captcha/siprng.go
generated
vendored
Normal 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
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
117
vendor/github.com/dchest/captcha/store.go
generated
vendored
Normal 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
6
vendor/vendor.json
vendored
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user