diff --git a/.travis.yml b/.travis.yml index a188d2fe..e5342f64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ install: - go get github.com/gorilla/securecookie - go get golang.org/x/crypto/bcrypt - go get github.com/nicksnyder/go-i18n/i18n +- go get github.com/dchest/captcha - go build deploy: provider: releases diff --git a/public/css/style.css b/public/css/style.css index 65dd20d0..1eafc9f7 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -103,6 +103,12 @@ a { padding: 4px; } +.captcha-container { + display: grid; + grid-template-rows: auto; + grid-template-columns: 240px; +} + tr.torrent-info td.date { white-space: nowrap; } diff --git a/router/router.go b/router/router.go index b184ac25..472e7ab4 100644 --- a/router/router.go +++ b/router/router.go @@ -1,9 +1,10 @@ package router import ( - "github.com/gorilla/mux" - "net/http" + + "github.com/ewhal/nyaa/service/captcha" + "github.com/gorilla/mux" ) var Router *mux.Router @@ -30,4 +31,5 @@ func init() { Router.HandleFunc("/view/{id}", ViewHandler).Name("view_torrent") Router.HandleFunc("/upload", UploadHandler).Name("upload") Router.HandleFunc("/user/register", UserRegisterFormHandler).Name("user_register") + Router.PathPrefix("/captcha").Methods("GET").HandlerFunc(captcha.ServeFiles) } diff --git a/router/upload.go b/router/upload.go index 44a2f077..dfe341f7 100644 --- a/router/upload.go +++ b/router/upload.go @@ -2,18 +2,16 @@ package router import ( "errors" + "net/http" + "github.com/ewhal/nyaa/util" "github.com/ewhal/nyaa/util/metainfo" "github.com/zeebo/bencode" - "net/http" ) // UploadForm serializing HTTP form for torrent upload type UploadForm struct { - Name string - Magnet string - Category string - Description string + Name, Magnet, Category, Description, CaptchaID string } // TODO: these should be in another package (?) diff --git a/router/uploadHandler.go b/router/uploadHandler.go index 76b9d91c..10fcf326 100644 --- a/router/uploadHandler.go +++ b/router/uploadHandler.go @@ -4,6 +4,7 @@ import ( "html/template" "net/http" + "github.com/ewhal/nyaa/service/captcha" "github.com/gorilla/mux" ) @@ -15,18 +16,27 @@ func init() { func UploadHandler(w http.ResponseWriter, r *http.Request) { var err error - var uploadForm UploadForm - if r.Method == "POST" { + switch r.Method { + case "POST": + var form UploadForm defer r.Body.Close() - err = uploadForm.ExtractInfo(r) + err = form.ExtractInfo(r) if err == nil { - //validate name + hash - //add to db and redirect depending on result + // validate name + hash + // authenticate captcha + // add to db and redirect depending on result + } + case "GET": + htv := UploadTemplateVariables{ + Upload: UploadForm{ + CaptchaID: captcha.GetID(r.RemoteAddr), + }, + Search: NewSearchForm(), + URL: r.URL, + Route: mux.CurrentRoute(r), } - } else if r.Method == "GET" { - htv := UploadTemplateVariables{uploadForm, NewSearchForm(), Navigation{}, r.URL, mux.CurrentRoute(r)} err = uploadTemplate.ExecuteTemplate(w, "index.html", htv) - } else { + default: w.WriteHeader(http.StatusMethodNotAllowed) return } diff --git a/router/userHandler.go b/router/userHandler.go index 78668e1f..49d4c11b 100644 --- a/router/userHandler.go +++ b/router/userHandler.go @@ -4,6 +4,7 @@ import ( "html/template" "net/http" + "github.com/ewhal/nyaa/service/captcha" "github.com/ewhal/nyaa/service/user/form" "github.com/ewhal/nyaa/util/modelHelper" "github.com/gorilla/mux" @@ -21,10 +22,16 @@ func init() { // Getting View User Registration func UserRegisterFormHandler(w http.ResponseWriter, r *http.Request) { - - b := form.RegistrationForm{} + b := form.RegistrationForm{ + CaptchaID: captcha.GetID(r.RemoteAddr), + } modelHelper.BindValueForm(b, r) - htv := UserRegisterTemplateVariables{b, NewSearchForm(), Navigation{}, r.URL, mux.CurrentRoute(r)} + htv := UserRegisterTemplateVariables{ + RegistrationForm: b, + Search: NewSearchForm(), + URL: r.URL, + Route: mux.CurrentRoute(r), + } err := viewTemplate.ExecuteTemplate(w, "index.html", htv) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/service/captcha/capthca.go b/service/captcha/capthca.go new file mode 100644 index 00000000..5a7114e8 --- /dev/null +++ b/service/captcha/capthca.go @@ -0,0 +1,97 @@ +package captcha + +import ( + "net/http" + "sync" + "time" + + "github.com/dchest/captcha" +) + +const lifetime = time.Minute * 20 + +var ( + server = captcha.Server(captcha.StdWidth, captcha.StdHeight) + captchas = captchaMap{ + m: make(map[string]store, 64), + } +) + +func init() { + captcha.SetCustomStore(captcha.NewMemoryStore(1<<10, lifetime)) + + go func() { + t := time.Tick(time.Minute) + for { + <-t + captchas.cleanUp() + } + }() +} + +// Captcha is to be embedded into any form struct requiring a captcha +type Captcha struct { + CaptchaID, Solution string +} + +// Captchas are IP-specific and need eventual cleanup +type captchaMap struct { + sync.Mutex + m map[string]store +} + +type store struct { + id string + created time.Time +} + +// Returns a captcha id by IP. If a captcha for this IP already exists, it is +// reloaded and returned. Otherwise, a new captcha is created. +func (n *captchaMap) get(ip string) string { + n.Lock() + defer n.Unlock() + + old, ok := n.m[ip] + + // No existing captcha, it expired or this IP already used the captcha + if !ok || !captcha.Reload(old.id) { + id := captcha.New() + n.m[ip] = store{ + id: id, + created: time.Now(), + } + return id + } + + old.created = time.Now() + n.m[ip] = old + return old.id +} + +// Remove expired ip -> captchaID mappings +func (n *captchaMap) cleanUp() { + n.Lock() + defer n.Unlock() + + till := time.Now().Add(-lifetime) + for ip, c := range n.m { + if c.created.Before(till) { + delete(n.m, ip) + } + } +} + +// GetID returns a new or previous captcha id by IP +func GetID(ip string) string { + return captchas.get(ip) +} + +// ServeFiles serves captcha images and audio +func ServeFiles(w http.ResponseWriter, r *http.Request) { + server.ServeHTTP(w, r) +} + +// Authenticate check's if a captcha solution is valid +func Authenticate(req Captcha) bool { + return captcha.VerifyString(req.CaptchaID, req.Solution) +} diff --git a/service/user/form/formValidator.go b/service/user/form/formValidator.go index 2711d88a..f9a6650c 100644 --- a/service/user/form/formValidator.go +++ b/service/user/form/formValidator.go @@ -2,6 +2,7 @@ package form import ( "regexp" + "github.com/ewhal/nyaa/util/log" ) @@ -19,9 +20,10 @@ func EmailValidation(email string) bool { // RegistrationForm is used when creating a user. type RegistrationForm struct { - Username string `form:"registrationUsername" binding:"required"` - Email string `form:"registrationEmail" binding:"required"` - Password string `form:"registrationPassword" binding:"required"` + Username string `form:"registrationUsername" binding:"required"` + Email string `form:"registrationEmail" binding:"required"` + Password string `form:"registrationPassword" binding:"required"` + CaptchaID string `form:"captchaID" binding:"required"` } // RegistrationForm is used when creating a user authentication. diff --git a/templates/_capthca.html b/templates/_capthca.html new file mode 100644 index 00000000..22185d13 --- /dev/null +++ b/templates/_capthca.html @@ -0,0 +1,7 @@ +{{define "captcha"}} +