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
funny meme - -
-

{{T "nyaa_pantsu_dont_host_files"}}

{{end}} diff --git a/templates/_captcha.html b/templates/_captcha.html index f5963db7..c8e62353 100644 --- a/templates/_captcha.html +++ b/templates/_captcha.html @@ -1,8 +1,11 @@ {{define "captcha"}} + {{/* unset if user doesn't need captcha */}} + {{if ne .CaptchaID ""}}
+ {{end}} {{end}} diff --git a/templates/admin/panelindex.html b/templates/admin/panelindex.html index 9eec9b70..4385dba3 100644 --- a/templates/admin/panelindex.html +++ b/templates/admin/panelindex.html @@ -7,9 +7,12 @@ Uploader Action -{{ range .Torrents}} -{{ .Name }}{{ .UploaderID }} -{{ T "delete" }} +{{range .Torrents}} + + {{ .Name }} (Edit) + {{ .UploaderID }} + {{ T "delete" }} + {{end}}