diff --git a/README.md b/README.md
index 4db77e20..e26d1bd5 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,8 @@ that anyone will be able to deploy locally or remotely.
* Golang
# Installation
-* Install [Golang](https://golang.org/doc/install)
+Ubuntu 17.04 fails to build, use a different OS or docker
+* Install [Golang](https://golang.org/doc/install) (version >=1.8)
* `go get github.com/ewhal/nyaa`
* `go build`
* Download DB and place it in your root folder named as "nyaa.db"
@@ -64,11 +65,13 @@ Access the website by going to [localhost:9999](http://localhost:9999).
> nyaa_psql.backup.
## TODO
+ * Remove gorm
+ * Use elastic search or sphinix search
* sukebei
* get sukebei_torrents table working
* add config option for sukebei or maybe make things all in one
* sukebei categories and category images
-
+
* Get code up to standard of go lint recommendations
* Write tests
diff --git a/cache/cache.go b/cache/cache.go
index 98eab0de..c16a16d9 100644
--- a/cache/cache.go
+++ b/cache/cache.go
@@ -7,8 +7,6 @@ import (
"github.com/ewhal/nyaa/common"
"github.com/ewhal/nyaa/config"
"github.com/ewhal/nyaa/model"
-
- "errors"
)
// Cache defines interface for caching search results
@@ -17,8 +15,6 @@ type Cache interface {
ClearAll()
}
-var ErrInvalidCacheDialect = errors.New("invalid cache dialect")
-
// Impl cache implementation instance
var Impl Cache
diff --git a/cache/native/native.go b/cache/native/native.go
index c9b3ba36..56c1094c 100644
--- a/cache/native/native.go
+++ b/cache/native/native.go
@@ -111,7 +111,9 @@ func (n *NativeCache) updateUsedSize(delta int) {
}
s := n.ll.Remove(e).(*store)
delete(n.cache, s.key)
+ s.Lock()
n.totalUsed -= s.size
+ s.Unlock()
}
}
@@ -136,8 +138,6 @@ func (s *store) update(data []model.Torrent, count int) {
s.size = newSize
s.lastFetched = time.Now()
- // Technically it is possible to update the size even when the store is
- // already evicted, but that should never happen, unless you have a very
- // small cache, very large stored datasets and a lot of traffic.
- s.n.updateUsedSize(delta)
+ // In a separate goroutine, to ensure there is never any lock intersection
+ go s.n.updateUsedSize(delta)
}
diff --git a/cache/native/native_test.go b/cache/native/native_test.go
new file mode 100644
index 00000000..6c977ed4
--- /dev/null
+++ b/cache/native/native_test.go
@@ -0,0 +1,37 @@
+package native
+
+import (
+ "sync"
+ "testing"
+
+ "github.com/ewhal/nyaa/common"
+ "github.com/ewhal/nyaa/model"
+)
+
+// Basic test for deadlocks and race conditions
+func TestConcurrency(t *testing.T) {
+ c := New(0.000001)
+
+ fn := func() ([]model.Torrent, int, error) {
+ return []model.Torrent{{}, {}, {}}, 10, nil
+ }
+
+ var wg sync.WaitGroup
+ wg.Add(300)
+ for i := 0; i < 3; i++ {
+ go func() {
+ for j := 0; j < 100; j++ {
+ go func(j int) {
+ defer wg.Done()
+ k := common.SearchParam{
+ Page: j,
+ }
+ if _, _, err := c.Get(k, fn); err != nil {
+ t.Fatal(err)
+ }
+ }(j)
+ }
+ }()
+ }
+ wg.Wait()
+}
diff --git a/config/email.go b/config/email.go
index 4bdc3566..f64ba5ca 100644
--- a/config/email.go
+++ b/config/email.go
@@ -16,4 +16,4 @@ const (
EmailTimeout = 10 * time.Second
)
-var EmailTokenHashKey = []byte("CHANGE_THIS_BEFORE_DEPLOYING_YOU_RETARD")
+var EmailTokenHashKey = []byte("CHANGE_THIS_BEFORE_DEPLOYING_YOU_GIT")
diff --git a/config/trackers.go b/config/trackers.go
index 8fdbb9b7..4f38177f 100644
--- a/config/trackers.go
+++ b/config/trackers.go
@@ -10,6 +10,5 @@ var Trackers = []string{
"udp://explodie.org:6969",
"udp://tracker.opentrackr.org:1337",
"udp://tracker.internetwarriors.net:1337/announce",
- "udp://eddie4.nl:6969/announce",
"http://mgtracker.org:6969/announce",
"http://tracker.baka-sub.cf/announce"}
diff --git a/main.go b/main.go
index 48368b9e..5b56615a 100644
--- a/main.go
+++ b/main.go
@@ -38,8 +38,8 @@ func RunServer(conf *config.Config) {
// Set up server,
srv := &http.Server{
- WriteTimeout: 15 * time.Second,
- ReadTimeout: 15 * time.Second,
+ WriteTimeout: 24 * time.Second,
+ ReadTimeout: 8 * time.Second,
}
l, err := network.CreateHTTPListener(conf)
log.CheckError(err)
diff --git a/model/report.go b/model/report.go
index 68129ee4..8e246ff9 100644
--- a/model/report.go
+++ b/model/report.go
@@ -29,8 +29,30 @@ type TorrentReportJson struct {
/* Model Conversion to Json */
+func getReportDescription(d string) string {
+ if d == "illegal" {
+ return "Illegal content"
+ } else if d == "spam" {
+ return "Spam / Garbage"
+ } else if d == "wrongcat" {
+ return "Wrong category"
+ } else if d == "dup" {
+ return "Duplicate / Deprecated"
+ }
+ return "???"
+}
+
func (report *TorrentReport) ToJson() TorrentReportJson {
- json := TorrentReportJson{report.ID, report.Description, report.Torrent.ToJSON(), report.User.ToJSON()}
+ // FIXME: report.Torrent and report.User should never be nil
+ var t TorrentJSON = TorrentJSON{}
+ if report.Torrent != nil {
+ t = report.Torrent.ToJSON()
+ }
+ var u UserJSON = UserJSON{}
+ if report.User != nil {
+ u = report.User.ToJSON()
+ }
+ json := TorrentReportJson{report.ID, getReportDescription(report.Description), t, u}
return json
}
diff --git a/model/user.go b/model/user.go
index 1e1e3ca7..44609ab7 100644
--- a/model/user.go
+++ b/model/user.go
@@ -12,8 +12,8 @@ type User struct {
Status int `gorm:"column:status"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
- Token string `gorm:"column:api_token"`
- TokenExpiration time.Time `gorm:"column:api_token_expiry"`
+ ApiToken string `gorm:"column:api_token"`
+ ApiTokenExpiry time.Time `gorm:"column:api_token_expiry"`
Language string `gorm:"column:language"`
// TODO: move this to PublicUser
@@ -42,7 +42,7 @@ func (u User) Size() (s int) {
4*3 + //time.Time
3*2 + // arrays
// string arrays
- len(u.Username) + len(u.Password) + len(u.Email) + len(u.Token) + len(u.MD5) + len(u.Language)
+ len(u.Username) + len(u.Password) + len(u.Email) + len(u.ApiToken) + len(u.MD5) + len(u.Language)
s *= 8
// Ignoring foreign key users. Fuck them.
diff --git a/public/css/style.css b/public/css/style.css
index 448a2b7f..e6c98d07 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -345,6 +345,12 @@ footer {
text-shadow: -1px -1px #999999;
}
+select#bottom_language_selector {
+ width: 20%;
+ margin: auto;
+ margin-bottom: 2rem;
+}
+
/* Force images on description to fit width */
#description img {
display: block;
diff --git a/public/js/main.js b/public/js/main.js
index 5d985acf..fbf5b3b9 100644
--- a/public/js/main.js
+++ b/public/js/main.js
@@ -47,3 +47,36 @@ for(var i in list) {
var date = new Date(e.innerText);
e.innerText = date.toDateString() + " " + date.toLocaleTimeString();
}
+
+function loadLanguages() {
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4 && xhr.status == 200) {
+ var selector = document.getElementById("bottom_language_selector");
+ selector.hidden = false
+ /* Response format is
+ * { "current": "(user current language)",
+ * "languages": {
+ * "(language_code)": "(language_name"),
+ * }} */
+ var response = JSON.parse(xhr.responseText);
+ for (var language in response.languages) {
+ if (!response.languages.hasOwnProperty(language)) continue;
+
+ var opt = document.createElement("option")
+ opt.value = language
+ opt.innerHTML = response.languages[language]
+ if (language == response.current) {
+ opt.selected = true
+ }
+
+ selector.appendChild(opt)
+ }
+ }
+ }
+ xhr.open("GET", "/language?format=json", true)
+ xhr.send()
+}
+
+loadLanguages();
+
diff --git a/router/changeLanguageHandler.go b/router/changeLanguageHandler.go
new file mode 100644
index 00000000..75f88314
--- /dev/null
+++ b/router/changeLanguageHandler.go
@@ -0,0 +1,62 @@
+package router;
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/ewhal/nyaa/util/languages"
+ "github.com/ewhal/nyaa/service/user"
+ "github.com/gorilla/mux"
+)
+
+type LanguagesJSONResponse struct {
+ Current string `json:"current"`
+ Languages map[string]string `json:"languages"`
+}
+
+func SeeLanguagesHandler(w http.ResponseWriter, r *http.Request) {
+ _, Tlang := languages.GetTfuncAndLanguageFromRequest(r, "en-us")
+ availableLanguages := languages.GetAvailableLanguages()
+
+ format := r.URL.Query().Get("format")
+ if format == "json" {
+ w.Header().Set("Content-Type", "application/json")
+ err := json.NewEncoder(w).Encode(LanguagesJSONResponse{Tlang.Tag, availableLanguages})
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ } else {
+ clv := ChangeLanguageVariables{NewSearchForm(), Navigation{}, Tlang.Tag, availableLanguages, GetUser(r), r.URL, mux.CurrentRoute(r)}
+ languages.SetTranslationFromRequest(changeLanguageTemplate, r, "en-us")
+ err := changeLanguageTemplate.ExecuteTemplate(w, "index.html", clv)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }
+}
+
+func ChangeLanguageHandler(w http.ResponseWriter, r *http.Request) {
+ lang := r.FormValue("language")
+
+ availableLanguages := languages.GetAvailableLanguages()
+ if _, exists := availableLanguages[lang]; !exists {
+ http.Error(w, "Language not available", http.StatusInternalServerError)
+ return
+ }
+
+ // If logged in, update user language; if not, set cookie.
+ user, err := userService.CurrentUser(r)
+ if err == nil {
+ user.Language = lang
+ // I don't know if I should use this...
+ userService.UpdateUserCore(&user)
+ } else {
+ http.SetCookie(w, &http.Cookie{Name: "lang", Value: lang})
+ }
+
+ url, _ := Router.Get("home").URL()
+ http.Redirect(w, r, url.String(), http.StatusSeeOther)
+}
+
diff --git a/router/modpanel.go b/router/modpanel.go
index 834c28d1..5446e00b 100644
--- a/router/modpanel.go
+++ b/router/modpanel.go
@@ -32,8 +32,9 @@ func IndexModPanel(w http.ResponseWriter, r *http.Request) {
torrentReports, _, _ := reportService.GetAllTorrentReports(offset, 0)
languages.SetTranslationFromRequest(panelIndex, r, "en-us")
- htv := PanelIndexVbs{torrents, torrentReports, users, comments, NewSearchForm(), currentUser, r.URL}
- _ = panelIndex.ExecuteTemplate(w, "admin_index.html", htv)
+ htv := PanelIndexVbs{torrents, model.TorrentReportsToJSON(torrentReports), users, comments, NewSearchForm(), currentUser, r.URL}
+ err := panelIndex.ExecuteTemplate(w, "admin_index.html", htv)
+ log.CheckError(err)
} else {
http.Error(w, "admins only", http.StatusForbidden)
}
@@ -217,7 +218,8 @@ func TorrentPostEditModPanel(w http.ResponseWriter, r *http.Request) {
}
languages.SetTranslationFromRequest(panelTorrentEd, r, "en-us")
htv := PanelTorrentEdVbs{uploadForm, NewSearchForm(), currentUser, err, infos, r.URL}
- _ = panelTorrentEd.ExecuteTemplate(w, "admin_index.html", htv)
+ err_ := panelTorrentEd.ExecuteTemplate(w, "admin_index.html", htv)
+ log.CheckError(err_)
}
func CommentDeleteModPanel(w http.ResponseWriter, r *http.Request) {
diff --git a/router/router.go b/router/router.go
index 8f93b335..5964bac3 100755
--- a/router/router.go
+++ b/router/router.go
@@ -112,5 +112,8 @@ func init() {
//Router.HandleFunc("/moderation/report/delete", gzipTorrentReportDeleteHandler).Name("torrent_report_delete").Methods("POST")
//Router.HandleFunc("/moderation/torrent/delete", gzipTorrentDeleteHandler).Name("torrent_delete").Methods("POST")
+ Router.HandleFunc("/language", SeeLanguagesHandler).Methods("GET").Name("see_languages")
+ Router.HandleFunc("/language", ChangeLanguageHandler).Methods("POST").Name("change_language")
+
Router.NotFoundHandler = http.HandlerFunc(NotFoundHandler)
}
diff --git a/router/rssHandler.go b/router/rssHandler.go
index d5ba112f..ff82f887 100644
--- a/router/rssHandler.go
+++ b/router/rssHandler.go
@@ -6,7 +6,6 @@ import (
"github.com/ewhal/nyaa/util/search"
"github.com/gorilla/feeds"
"net/http"
- "strconv"
"time"
)
@@ -31,7 +30,7 @@ func RSSHandler(w http.ResponseWriter, r *http.Request) {
for i, torrent := range torrents {
torrentJSON := torrent.ToJSON()
feed.Items[i] = &feeds.Item{
- Id: "https://" + config.WebAddress + "/view/" + strconv.FormatUint(uint64(torrents[i].ID), 10),
+ Id: "https://" + config.WebAddress + "/view/" + torrentJSON.ID,
Title: torrent.Name,
Link: &feeds.Link{Href: string(torrentJSON.Magnet)},
Description: string(torrentJSON.Description),
@@ -43,11 +42,11 @@ func RSSHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
rss, rssErr := feed.ToRss()
if rssErr != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ http.Error(w, rssErr.Error(), http.StatusInternalServerError)
}
_, writeErr := w.Write([]byte(rss))
if writeErr != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
+ http.Error(w, writeErr.Error(), http.StatusInternalServerError)
}
}
diff --git a/router/template.go b/router/template.go
index 26314ea4..dab38d86 100644
--- a/router/template.go
+++ b/router/template.go
@@ -7,7 +7,7 @@ import (
var TemplateDir = "templates"
-var homeTemplate, searchTemplate, faqTemplate, uploadTemplate, viewTemplate, viewRegisterTemplate, viewLoginTemplate, viewRegisterSuccessTemplate, viewVerifySuccessTemplate, viewProfileTemplate, viewProfileEditTemplate, viewUserDeleteTemplate, notFoundTemplate *template.Template
+var homeTemplate, searchTemplate, faqTemplate, uploadTemplate, viewTemplate, viewRegisterTemplate, viewLoginTemplate, viewRegisterSuccessTemplate, viewVerifySuccessTemplate, viewProfileTemplate, viewProfileEditTemplate, viewUserDeleteTemplate, notFoundTemplate, changeLanguageTemplate *template.Template
var panelIndex, panelTorrentList, panelUserList, panelCommentList, panelTorrentEd, panelTorrentReportList *template.Template
@@ -86,6 +86,11 @@ func ReloadTemplates() {
name: "404",
file: "404.html",
},
+ templateLoader{
+ templ: &changeLanguageTemplate,
+ name: "change_language",
+ file: "change_language.html",
+ },
}
for idx := range pubTempls {
pubTempls[idx].indexFile = filepath.Join(TemplateDir, "index.html")
diff --git a/router/templateFunctions.go b/router/templateFunctions.go
index 2750248b..d1b3fe54 100644
--- a/router/templateFunctions.go
+++ b/router/templateFunctions.go
@@ -19,10 +19,20 @@ var FuncMap = template.FuncMap{
}
return "error"
},
- "genRouteWithQuery": func(name string, currentUrl *url.URL, params ...string) template.HTML {
+ "genRouteWithQuery": func(name string, currentUrl *url.URL, params ...string) template.URL {
url, err := Router.Get(name).URL(params...)
if err == nil {
- return template.HTML(template.HTMLEscapeString(url.String() + "?" + currentUrl.RawQuery)) // TODO: Review application of character escaping
+ return template.URL(url.String() + "?" + currentUrl.RawQuery)
+ }
+ return "error"
+ },
+ "genViewTorrentRoute": func(torrent_id uint) string {
+ // Helper for when you have an uint while genRoute("view_torrent", ...) takes a string
+ // FIXME better solution?
+ s := strconv.FormatUint(uint64(torrent_id), 10)
+ url, err := Router.Get("view_torrent").URL("id", s)
+ if err == nil {
+ return url.String()
}
return "error"
},
@@ -69,6 +79,7 @@ var FuncMap = template.FuncMap{
"CurrentOrAdmin": userPermission.CurrentOrAdmin,
"CurrentUserIdentical": userPermission.CurrentUserIdentical,
"HasAdmin": userPermission.HasAdmin,
+ "NeedsCaptcha": userPermission.NeedsCaptcha,
"GetRole": userPermission.GetRole,
"IsFollower": userPermission.IsFollower,
"NoEncode": func(str string) template.HTML {
diff --git a/router/templateVariables.go b/router/templateVariables.go
index d9b3d4df..5e424088 100644
--- a/router/templateVariables.go
+++ b/router/templateVariables.go
@@ -6,7 +6,6 @@ import (
"github.com/ewhal/nyaa/common"
"github.com/ewhal/nyaa/model"
- "github.com/ewhal/nyaa/service/captcha"
"github.com/ewhal/nyaa/service/user"
userForms "github.com/ewhal/nyaa/service/user/form"
"github.com/gorilla/mux"
@@ -36,7 +35,7 @@ type NotFoundTemplateVariables struct {
type ViewTemplateVariables struct {
Torrent model.TorrentJSON
- Captcha captcha.Captcha
+ CaptchaID string
Search SearchForm
Navigation Navigation
User *model.User
@@ -114,11 +113,22 @@ type UploadTemplateVariables struct {
Route *mux.Route
}
+type ChangeLanguageVariables struct {
+ Search SearchForm
+ Navigation Navigation
+ Language string
+ Languages map[string]string
+ User *model.User
+ URL *url.URL
+ Route *mux.Route
+}
+
+
/* MODERATION Variables */
type PanelIndexVbs struct {
Torrents []model.Torrent
- TorrentReports []model.TorrentReport
+ TorrentReports []model.TorrentReportJson
Users []model.User
Comments []model.Comment
Search SearchForm
diff --git a/router/upload.go b/router/upload.go
index 48437e40..2b143da9 100644
--- a/router/upload.go
+++ b/router/upload.go
@@ -17,7 +17,6 @@ import (
"github.com/ewhal/nyaa/cache"
"github.com/ewhal/nyaa/config"
- "github.com/ewhal/nyaa/service/captcha"
"github.com/ewhal/nyaa/service/upload"
"github.com/ewhal/nyaa/util"
"github.com/ewhal/nyaa/util/metainfo"
@@ -33,7 +32,7 @@ type UploadForm struct {
Remake bool
Description string
Status int
- captcha.Captcha
+ CaptchaID string
Infohash string
CategoryID int
@@ -84,12 +83,6 @@ func (f *UploadForm) ExtractInfo(r *http.Request) error {
f.Status, _ = strconv.Atoi(r.FormValue(UploadFormStatus))
f.Magnet = r.FormValue(UploadFormMagnet)
f.Remake = r.FormValue(UploadFormRemake) == "on"
- f.Captcha = captcha.Extract(r)
-
- if !captcha.Authenticate(f.Captcha) {
- // TODO: Prettier passing of mistyped Captcha errors
- return errors.New(captcha.ErrInvalidCaptcha.Error())
- }
// trim whitespace
f.Name = util.TrimWhitespaces(f.Name)
@@ -189,7 +182,7 @@ func (f *UploadForm) ExtractInfo(r *http.Request) error {
}
hash16 := make([]byte, hex.EncodedLen(len(data)))
hex.Encode(hash16, data)
- f.Infohash = string(hash16)
+ f.Infohash = strings.ToUpper(string(hash16))
}
f.Filesize = 0
diff --git a/router/uploadHandler.go b/router/uploadHandler.go
index 8706ebe5..50bb47da 100644
--- a/router/uploadHandler.go
+++ b/router/uploadHandler.go
@@ -10,7 +10,7 @@ import (
"github.com/ewhal/nyaa/db"
"github.com/ewhal/nyaa/model"
"github.com/ewhal/nyaa/service/captcha"
- "github.com/ewhal/nyaa/service/user"
+ "github.com/ewhal/nyaa/service/user/permission"
"github.com/ewhal/nyaa/util/languages"
"github.com/gorilla/mux"
)
@@ -23,26 +23,32 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
var uploadForm UploadForm
if r.Method == "POST" {
defer r.Body.Close()
+ user := GetUser(r)
+ if userPermission.NeedsCaptcha(user) {
+ userCaptcha := captcha.Extract(r)
+ if !captcha.Authenticate(userCaptcha) {
+ http.Error(w, captcha.ErrInvalidCaptcha.Error(), http.StatusInternalServerError)
+ return
+ }
+ }
+
// validation is done in ExtractInfo()
err := uploadForm.ExtractInfo(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
- user, _, err := userService.RetrieveCurrentUser(r)
- if err != nil {
- fmt.Printf("error %+v\n", err)
- }
status := 1 // normal
if uploadForm.Remake { // overrides trusted
status = 2
} else if user.Status == 1 {
status = 3 // mark as trusted if user is trusted
}
- var sameTorrents int
- db.ORM.Model(&model.Torrent{}).Where("torrent_hash = ?", uploadForm.Infohash).Count(&sameTorrents)
- if (sameTorrents == 0) {
- //add to db and redirect depending on result
+
+ var sameTorrents int
+ db.ORM.Model(&model.Torrent{}).Where("torrent_hash = ?", uploadForm.Infohash).Count(&sameTorrents)
+ if (sameTorrents == 0) {
+ // add to db and redirect
torrent := model.Torrent{
Name: uploadForm.Name,
Category: uploadForm.CategoryID,
@@ -54,7 +60,6 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
Description: uploadForm.Description,
UploaderID: user.ID}
db.ORM.Create(&torrent)
- fmt.Printf("%+v\n", torrent)
url, err := Router.Get("view_torrent").URL("id", strconv.FormatUint(uint64(torrent.ID), 10))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -66,7 +71,14 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
return
}
} else if r.Method == "GET" {
- uploadForm.CaptchaID = captcha.GetID()
+ user := GetUser(r)
+ if userPermission.NeedsCaptcha(user) {
+ uploadForm.CaptchaID = captcha.GetID()
+ } else {
+ uploadForm.CaptchaID = ""
+ }
+
+
htv := UploadTemplateVariables{uploadForm, NewSearchForm(), Navigation{}, GetUser(r), r.URL, mux.CurrentRoute(r)}
languages.SetTranslationFromRequest(uploadTemplate, r, "en-us")
err := uploadTemplate.ExecuteTemplate(w, "index.html", htv)
diff --git a/router/viewTorrentHandler.go b/router/viewTorrentHandler.go
index 7e467a50..627d99ba 100644
--- a/router/viewTorrentHandler.go
+++ b/router/viewTorrentHandler.go
@@ -10,6 +10,7 @@ import (
"github.com/ewhal/nyaa/model"
"github.com/ewhal/nyaa/service/captcha"
"github.com/ewhal/nyaa/service/torrent"
+ "github.com/ewhal/nyaa/service/user/permission"
"github.com/ewhal/nyaa/util"
"github.com/ewhal/nyaa/util/languages"
"github.com/ewhal/nyaa/util/log"
@@ -26,7 +27,12 @@ func ViewHandler(w http.ResponseWriter, r *http.Request) {
return
}
b := torrent.ToJSON()
- htv := ViewTemplateVariables{b, captcha.Captcha{CaptchaID: captcha.GetID()}, NewSearchForm(), Navigation{}, GetUser(r), r.URL, mux.CurrentRoute(r)}
+ captchaID := ""
+ user := GetUser(r)
+ if userPermission.NeedsCaptcha(user) {
+ captchaID = captcha.GetID()
+ }
+ htv := ViewTemplateVariables{b, captchaID, NewSearchForm(), Navigation{}, user, r.URL, mux.CurrentRoute(r)}
languages.SetTranslationFromRequest(viewTemplate, r, "en-us")
err = viewTemplate.ExecuteTemplate(w, "index.html", htv)
@@ -39,12 +45,14 @@ func PostCommentHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
- userCaptcha := captcha.Extract(r)
- if !captcha.Authenticate(userCaptcha) {
- http.Error(w, "bad captcha", 403)
- return
- }
currentUser := GetUser(r)
+ if userPermission.NeedsCaptcha(currentUser) {
+ userCaptcha := captcha.Extract(r)
+ if !captcha.Authenticate(userCaptcha) {
+ http.Error(w, "bad captcha", 403)
+ return
+ }
+ }
content := p.Sanitize(r.FormValue("comment"))
if strings.TrimSpace(content) == "" {
@@ -75,12 +83,14 @@ func ReportTorrentHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
- userCaptcha := captcha.Extract(r)
- if !captcha.Authenticate(userCaptcha) {
- http.Error(w, "bad captcha", 403)
- return
- }
currentUser := GetUser(r)
+ if userPermission.NeedsCaptcha(currentUser) {
+ userCaptcha := captcha.Extract(r)
+ if !captcha.Authenticate(userCaptcha) {
+ http.Error(w, "bad captcha", 403)
+ return
+ }
+ }
idNum, err := strconv.Atoi(id)
userID := currentUser.ID
diff --git a/router/wrapHandler.go b/router/wrapHandler.go
index d0170f44..1f98325d 100644
--- a/router/wrapHandler.go
+++ b/router/wrapHandler.go
@@ -46,4 +46,4 @@ func (wh *wrappedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func wrapHandler(handler http.Handler) http.Handler {
return &wrappedHandler{handler}
-}
\ No newline at end of file
+}
diff --git a/service/api/api.go b/service/api/api.go
index 417f3242..268ee80c 100644
--- a/service/api/api.go
+++ b/service/api/api.go
@@ -125,7 +125,7 @@ func validateHash(r *TorrentRequest) (error, int) {
}
hash16 := make([]byte, hex.EncodedLen(len(data)))
hex.Encode(hash16, data)
- r.Hash = string(hash16)
+ r.Hash = strings.ToUpper(string(hash16))
}
return nil, http.StatusOK
}
diff --git a/service/report/report.go b/service/report/report.go
index 9b640625..7b32eba1 100644
--- a/service/report/report.go
+++ b/service/report/report.go
@@ -48,7 +48,6 @@ func getTorrentReportsOrderBy(parameters *serviceBase.WhereParams, orderBy strin
return
}
}
- // TODO: Vulnerable to injections. Use query builder. (is it?)
// build custom db query for performance reasons
dbQuery := "SELECT * FROM torrent_reports"
@@ -63,7 +62,7 @@ func getTorrentReportsOrderBy(parameters *serviceBase.WhereParams, orderBy strin
if limit != 0 || offset != 0 { // if limits provided
dbQuery = dbQuery + " LIMIT " + strconv.Itoa(limit) + " OFFSET " + strconv.Itoa(offset)
}
- err = db.ORM.Preload("Torrent").Preload("User").Raw(dbQuery, params...).Find(&torrentReports).Error //fixed !!!!
+ err = db.ORM.Preload("Torrent").Preload("User").Raw(dbQuery, params...).Find(&torrentReports).Error
return
}
diff --git a/service/torrent/torrent.go b/service/torrent/torrent.go
index caa776f7..7c5eade4 100644
--- a/service/torrent/torrent.go
+++ b/service/torrent/torrent.go
@@ -129,9 +129,10 @@ func getTorrentsOrderBy(parameters *serviceBase.WhereParams, orderBy string, lim
if conditions != "" {
dbQuery = dbQuery + " WHERE " + conditions
}
- if strings.Contains(conditions, "torrent_name") {
+ /* This makes all queries take roughly the same amount of time (lots)...
+ if strings.Contains(conditions, "torrent_name") && offset > 0 {
dbQuery = "WITH t AS (SELECT * FROM torrents WHERE " + conditions + ") SELECT * FROM t"
- }
+ }*/
if orderBy == "" { // default OrderBy
orderBy = "torrent_id DESC"
diff --git a/service/user/cookieHelper.go b/service/user/cookieHelper.go
index b28e5a98..940d5b3d 100644
--- a/service/user/cookieHelper.go
+++ b/service/user/cookieHelper.go
@@ -5,96 +5,98 @@ import (
"github.com/ewhal/nyaa/db"
"github.com/ewhal/nyaa/model"
formStruct "github.com/ewhal/nyaa/service/user/form"
- "github.com/ewhal/nyaa/util/log"
"github.com/ewhal/nyaa/util/modelHelper"
+ "github.com/ewhal/nyaa/util/timeHelper"
"github.com/gorilla/securecookie"
"golang.org/x/crypto/bcrypt"
"net/http"
+ "strconv"
+ "time"
)
+const CookieName = "session"
+
+// If you want to keep login cookies between restarts you need to make these permanent
var cookieHandler = securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32))
-func Token(r *http.Request) (string, error) {
- var token string
- cookie, err := r.Cookie("session")
+// Encoding & Decoding of the cookie value
+func DecodeCookie(cookie_value string) (uint, error) {
+ value := make(map[string]string)
+ err := cookieHandler.Decode(CookieName, cookie_value, &value)
if err != nil {
- return token, err
+ return 0, err
}
- cookieValue := make(map[string]string)
- err = cookieHandler.Decode("session", cookie.Value, &cookieValue)
- if err != nil {
- return token, err
+ time_int, _ := strconv.ParseInt(value["t"], 10, 0)
+ if timeHelper.IsExpired(time.Unix(time_int, 0)) {
+ return 0, errors.New("Cookie is expired")
}
- token = cookieValue["token"]
- if len(token) == 0 {
- return token, errors.New("token is empty")
- }
- return token, nil
+ ret, err := strconv.ParseUint(value["u"], 10, 0)
+ return uint(ret), err
}
-// SetCookie sets a cookie.
-func SetCookie(w http.ResponseWriter, token string) (int, error) {
+func EncodeCookie(user_id uint) (string, error) {
+ validUntil := timeHelper.FewDaysLater(7) // 1 week
value := map[string]string{
- "token": token,
+ "u": strconv.FormatUint(uint64(user_id), 10),
+ "t": strconv.FormatInt(validUntil.Unix(), 10),
}
- encoded, err := cookieHandler.Encode("session", value)
- if err != nil {
- return http.StatusInternalServerError, err
- }
- cookie := &http.Cookie{
- Name: "session",
- Value: encoded,
- Path: "/",
- }
- http.SetCookie(w, cookie)
- return http.StatusOK, nil
+ return cookieHandler.Encode(CookieName, value)
}
-// ClearCookie clears a cookie.
func ClearCookie(w http.ResponseWriter) (int, error) {
cookie := &http.Cookie{
- Name: "session",
+ Name: CookieName,
Value: "",
Path: "/",
+ HttpOnly: true,
MaxAge: -1,
}
http.SetCookie(w, cookie)
return http.StatusOK, nil
}
-// SetCookieHandler sets a cookie with email and password.
+// SetCookieHandler sets the authentication cookie
func SetCookieHandler(w http.ResponseWriter, email string, pass string) (int, error) {
- if email != "" && pass != "" {
- var user model.User
- isValidEmail, _ := formStruct.EmailValidation(email, formStruct.NewErrors())
- if isValidEmail {
- log.Debug("User entered valid email.")
- if db.ORM.Where("email = ?", email).First(&user).RecordNotFound() {
- return http.StatusNotFound, errors.New("User not found")
- }
- } else {
- log.Debug("User entered username.")
- if db.ORM.Where("username = ?", email).First(&user).RecordNotFound() {
- return http.StatusNotFound, errors.New("User not found")
- }
- }
- err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(pass))
- if err != nil {
- return http.StatusUnauthorized, errors.New("Password incorrect")
- }
- if user.Status == -1 {
- return http.StatusUnauthorized, errors.New("Account banned")
- }
- status, err := SetCookie(w, user.Token)
- if err != nil {
- return status, err
- }
- w.Header().Set("X-Auth-Token", user.Token)
- return http.StatusOK, nil
+ if email == "" || pass == "" {
+ return http.StatusNotFound, errors.New("No username/password entered")
}
- return http.StatusNotFound, errors.New("user not found")
+
+ var user model.User
+ // search by email or username
+ isValidEmail, _ := formStruct.EmailValidation(email, formStruct.NewErrors())
+ if isValidEmail {
+ if db.ORM.Where("email = ?", email).First(&user).RecordNotFound() {
+ return http.StatusNotFound, errors.New("User not found")
+ }
+ } else {
+ if db.ORM.Where("username = ?", email).First(&user).RecordNotFound() {
+ return http.StatusNotFound, errors.New("User not found")
+ }
+ }
+ err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(pass))
+ if err != nil {
+ return http.StatusUnauthorized, errors.New("Password incorrect")
+ }
+ if user.Status == -1 {
+ return http.StatusUnauthorized, errors.New("Account banned")
+ }
+
+ encoded, err := EncodeCookie(user.ID)
+ if err != nil {
+ return http.StatusInternalServerError, err
+ }
+ cookie := &http.Cookie{
+ Name: CookieName,
+ Value: encoded,
+ Path: "/",
+ HttpOnly: true,
+ }
+ http.SetCookie(w, cookie)
+ // also set response header for convenience
+ w.Header().Set("X-Auth-Token", encoded)
+ return http.StatusOK, nil
}
// RegisterHanderFromForm sets cookie from a RegistrationForm.
@@ -111,24 +113,31 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) (int, error) {
return RegisterHanderFromForm(w, registrationForm)
}
-// CurrentUser get a current user.
+// CurrentUser determines the current user from the request
func CurrentUser(r *http.Request) (model.User, error) {
var user model.User
- var token string
- var err error
- token = r.Header.Get("X-Auth-Token")
- if len(token) > 0 {
- log.Debug("header token exists")
- } else {
- token, err = Token(r)
- log.Debug("header token does not exist")
+ var encoded string
+
+ encoded = r.Header.Get("X-Auth-Token")
+ if len(encoded) == 0 {
+ // check cookie instead
+ cookie, err := r.Cookie(CookieName)
if err != nil {
return user, err
}
+ encoded = cookie.Value
}
- if db.ORM.Where("api_token = ?", token).First(&user).RecordNotFound() {
- return user, errors.New("user not found")
+ user_id, err := DecodeCookie(encoded)
+ if err != nil {
+ return user, err
}
- err = db.ORM.Model(&user).Error
- return user, err
+ if db.ORM.Where("user_id = ?", user_id).First(&user).RecordNotFound() {
+ return user, errors.New("User not found")
+ }
+
+ if user.Status == -1 {
+ // recheck as user might've been banned in the meantime
+ return user, errors.New("Account banned")
+ }
+ return user, nil
}
diff --git a/service/user/permission/permission.go b/service/user/permission/permission.go
index 320388c8..5bdca117 100644
--- a/service/user/permission/permission.go
+++ b/service/user/permission/permission.go
@@ -6,6 +6,7 @@ import (
"github.com/ewhal/nyaa/util/log"
)
+
// HasAdmin checks that user has an admin permission.
func HasAdmin(user *model.User) bool {
return user.Status == 2
@@ -18,11 +19,16 @@ func CurrentOrAdmin(user *model.User, userID uint) bool {
}
// CurrentUserIdentical check that userID is same as current user's ID.
-// TODO: Inline this
+// TODO: Inline this (won't go do this for us?)
func CurrentUserIdentical(user *model.User, userID uint) bool {
return user.ID == userID
}
+func NeedsCaptcha(user *model.User) bool {
+ // Trusted members & Moderators don't
+ return !(user.Status == 1 || user.Status == 2)
+}
+
func GetRole(user *model.User) string {
switch user.Status {
case -1:
diff --git a/service/user/user.go b/service/user/user.go
index 88b2ada6..ece49467 100644
--- a/service/user/user.go
+++ b/service/user/user.go
@@ -7,7 +7,6 @@ import (
"strconv"
"time"
- "github.com/ewhal/nyaa/config"
"github.com/ewhal/nyaa/db"
"github.com/ewhal/nyaa/model"
formStruct "github.com/ewhal/nyaa/service/user/form"
@@ -15,7 +14,6 @@ import (
"github.com/ewhal/nyaa/util/crypto"
"github.com/ewhal/nyaa/util/log"
"github.com/ewhal/nyaa/util/modelHelper"
- "github.com/ewhal/nyaa/util/timeHelper"
"golang.org/x/crypto/bcrypt"
)
@@ -69,20 +67,16 @@ func CreateUserFromForm(registrationForm formStruct.RegistrationForm) (model.Use
return user, err
}
}
- token, err := crypto.GenerateRandomToken32()
- if err != nil {
- return user, errors.New("token not generated")
- }
user.Email = "" // unset email because it will be verified later
+ user.CreatedAt = time.Now()
+ // currently unused but needs to be set:
+ user.ApiToken = ""
+ user.ApiTokenExpiry = time.Unix(0, 0)
- user.Token = token
- user.TokenExpiration = timeHelper.FewDaysLater(config.AuthTokenExpirationDay)
- log.Debugf("user %+v\n", user)
if db.ORM.Create(&user).Error != nil {
return user, errors.New("user not created")
}
- user.CreatedAt = time.Now()
return user, nil
}
@@ -157,17 +151,12 @@ func UpdateUserCore(user *model.User) (int, error) {
}
}
- token, err := crypto.GenerateRandomToken32()
+ user.UpdatedAt = time.Now()
+ err := db.ORM.Save(user).Error
if err != nil {
return http.StatusInternalServerError, err
}
- user.Token = token
- user.TokenExpiration = timeHelper.FewDaysLater(config.AuthTokenExpirationDay)
- if db.ORM.Save(user).Error != nil {
- return http.StatusInternalServerError, err
- }
- user.UpdatedAt = time.Now()
return http.StatusOK, nil
}
@@ -197,18 +186,13 @@ func UpdateUser(w http.ResponseWriter, form *formStruct.UserForm, currentUser *m
form.Username = user.Username
}
if (form.Email != user.Email) {
+ // send verification to new email and keep old
SendVerificationToUser(user, form.Email)
form.Email = user.Email
}
log.Debugf("form %+v\n", form)
modelHelper.AssignValue(&user, form)
status, err := UpdateUserCore(&user)
- if err != nil {
- return user, status, err
- }
- if userPermission.CurrentUserIdentical(currentUser, user.ID) {
- status, err = SetCookie(w, user.Token)
- }
return user, status, err
}
diff --git a/templates/FAQ.html b/templates/FAQ.html
index b0a58191..a29d19c4 100644
--- a/templates/FAQ.html
+++ b/templates/FAQ.html
@@ -45,7 +45,6 @@ udp://tracker.leechers-paradise.org:6969
udp://explodie.org:6969
udp://tracker.opentrackr.org:1337
udp://tracker.internetwarriors.net:1337/announce
-udp://eddie4.nl:6969/announce
http://mgtracker.org:6969/announce
http://tracker.baka-sub.cf/announce
@@ -64,8 +63,5 @@ http://tracker.baka-sub.cf/announce
-
-
-