Albirew/nyaa-pantsu
Archivé
1
0
Bifurcation 0
Ce dépôt a été archivé le 2022-05-07. Vous pouvez voir ses fichiers ou le cloner, mais pas ouvrir de ticket ou de demandes d'ajout, ni soumettre de changements.
nyaa-pantsu/service/api/api.go

521 lignes
14 Kio
Go

package apiService
import (
"encoding/base32"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/NyaaPantsu/nyaa/cache"
"github.com/NyaaPantsu/nyaa/config"
"github.com/NyaaPantsu/nyaa/model"
"github.com/NyaaPantsu/nyaa/service"
"github.com/NyaaPantsu/nyaa/service/upload"
"github.com/NyaaPantsu/nyaa/util"
"github.com/NyaaPantsu/nyaa/util/categories"
"github.com/NyaaPantsu/nyaa/util/metainfo"
"github.com/NyaaPantsu/nyaa/util/torrentLanguages"
"github.com/zeebo/bencode"
)
// form names
const uploadFormName = "name"
const uploadFormTorrent = "torrent"
const uploadFormMagnet = "magnet"
const uploadFormCategory = "c"
const uploadFormRemake = "remake"
const uploadFormDescription = "desc"
const uploadFormWebsiteLink = "website_link"
const uploadFormStatus = "status"
const uploadFormHidden = "hidden"
const uploadFormLanguage = "language"
// error indicating that you can't send both a magnet link and torrent
var errTorrentPlusMagnet = errors.New("Upload either a torrent file or magnet link, not both")
// error indicating a torrent is private
var errPrivateTorrent = errors.New("Torrent is private")
// error indicating a problem with its trackers
var errTrackerProblem = errors.New("Torrent does not have any (working) trackers: " + config.Conf.WebAddress.Nyaa + "/faq#trackers")
// error indicating a torrent's name is invalid
var errInvalidTorrentName = errors.New("Torrent name is invalid")
// error indicating a torrent's description is invalid
var errInvalidTorrentDescription = errors.New("Torrent description is invalid")
// error indicating a torrent's website link is invalid
var errInvalidWebsiteLink = errors.New("Website url or IRC link is invalid")
// error indicating a torrent's category is invalid
var errInvalidTorrentCategory = errors.New("Torrent category is invalid")
// error indicating a torrent's language is invalid
var errInvalidTorrentLanguage = errors.New("Torrent language is invalid")
// error indicating that a non-english torrent was uploaded to a english category
var errNonEnglishLanguageInEnglishCategory = errors.New("Torrent's category is for English translations, but torrent language isn't English.")
// error indicating that a english torrent was uploaded to a non-english category
var errEnglishLanguageInNonEnglishCategory = errors.New("Torrent's category is for non-English translations, but torrent language is English.")
type torrentsQuery struct {
Category int `json:"category"`
SubCategory int `json:"sub_category"`
Status int `json:"status"`
Uploader int `json:"uploader"`
Downloads int `json:"downloads"`
}
// TorrentsRequest struct
type TorrentsRequest struct {
Query torrentsQuery `json:"search"`
Page int `json:"page"`
MaxPerPage int `json:"limit"`
}
// Use this, because we seem to avoid using models, and we would need
// the torrent ID to create the File in the DB
type uploadedFile struct {
Path []string `json:"path"`
Filesize int64 `json:"filesize"`
}
// TorrentRequest struct
// Same json name as the constant!
type TorrentRequest struct {
Name string `json:"name,omitempty"`
Magnet string `json:"magnet,omitempty"`
Category string `json:"c"`
Remake bool `json:"remake,omitempty"`
Description string `json:"desc,omitempty"`
Status int `json:"status,omitempty"`
Hidden bool `json:"hidden,omitempty"`
CaptchaID string `json:"-"`
WebsiteLink string `json:"website_link,omitempty"`
SubCategory int `json:"sub_category,omitempty"`
Language string `json:"language,omitempty"`
Infohash string `json:"hash,omitempty"`
CategoryID int `json:"-"`
SubCategoryID int `json:"-"`
Filesize int64 `json:"filesize,omitempty"`
Filepath string `json:"-"`
FileList []uploadedFile `json:"filelist,omitempty"`
Trackers []string `json:"trackers,omitempty"`
}
// UpdateRequest struct
type UpdateRequest struct {
ID int `json:"id"`
Update TorrentRequest `json:"update"`
}
// ToParams : Convert a torrentsrequest to searchparams
func (r *TorrentsRequest) ToParams() serviceBase.WhereParams {
res := serviceBase.WhereParams{}
conditions := ""
v := reflect.ValueOf(r.Query)
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Interface() != reflect.Zero(field.Type()).Interface() {
if i != 0 {
conditions += " AND "
}
conditions += v.Type().Field(i).Tag.Get("json") + " = ?"
res.Params = append(res.Params, field.Interface())
}
}
res.Conditions = conditions
return res
}
func (r *TorrentRequest) validateName() error {
// then actually check that we have everything we need
if len(r.Name) == 0 {
return errInvalidTorrentName
}
return nil
}
func (r *TorrentRequest) validateDescription() error {
if len(r.Description) > config.Conf.DescriptionLength {
return errInvalidTorrentDescription
}
return nil
}
func (r *TorrentRequest) validateWebsiteLink() error {
if r.WebsiteLink != "" {
// WebsiteLink
urlRegexp, _ := regexp.Compile(`^(https?:\/\/|ircs?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$`)
if !urlRegexp.MatchString(r.WebsiteLink) {
return errInvalidWebsiteLink
}
}
return nil
}
func (r *TorrentRequest) validateMagnet() error {
magnetURL, err := url.Parse(string(r.Magnet)) //?
if err != nil {
return err
}
xt := magnetURL.Query().Get("xt")
if !strings.HasPrefix(xt, "urn:btih:") {
return ErrMagnet
}
xt = strings.SplitAfter(xt, ":")[2]
r.Infohash = strings.TrimSpace(strings.ToUpper(strings.Split(xt, "&")[0]))
return nil
}
func (r *TorrentRequest) validateHash() error {
isBase32, err := regexp.MatchString("^[2-7A-Z]{32}$", r.Infohash)
if err != nil {
return err
}
if !isBase32 {
isBase16, err := regexp.MatchString("^[0-9A-F]{40}$", r.Infohash)
if err != nil {
return err
}
if !isBase16 {
return ErrHash
}
} else {
//convert to base16
data, err := base32.StdEncoding.DecodeString(r.Infohash)
if err != nil {
return err
}
hash16 := make([]byte, hex.EncodedLen(len(data)))
hex.Encode(hash16, data)
r.Infohash = strings.ToUpper(string(hash16))
}
return nil
}
// ExtractEditInfo : takes an http request and computes all fields for this form
func (r *TorrentRequest) ExtractEditInfo(req *http.Request) error {
err := r.ExtractBasicValue(req)
if err != nil {
return err
}
err = r.validateName()
if err != nil {
return err
}
defer req.Body.Close()
err = r.ExtractCategory(req)
if err != nil {
return err
}
err = r.ExtractLanguage(req)
return err
}
// ExtractCategory : takes an http request and computes category field for this form
func (r *TorrentRequest) ExtractCategory(req *http.Request) error {
catsSplit := strings.Split(r.Category, "_")
// need this to prevent out of index panics
if len(catsSplit) != 2 {
return errInvalidTorrentCategory
}
CatID, err := strconv.Atoi(catsSplit[0])
if err != nil {
return errInvalidTorrentCategory
}
SubCatID, err := strconv.Atoi(catsSplit[1])
if err != nil {
return errInvalidTorrentCategory
}
if !categories.CategoryExists(r.Category) {
return errInvalidTorrentCategory
}
r.CategoryID = CatID
r.SubCategoryID = SubCatID
return nil
}
// ExtractLanguage : takes a http request, computes the torrent language from the form.
func (r *TorrentRequest) ExtractLanguage(req *http.Request) error {
isEnglishCategory := false
for _, cat := range config.Conf.Torrents.EnglishOnlyCategories {
if cat == r.Category {
isEnglishCategory = true
break
}
}
if r.Language == "other" || r.Language == "multiple" {
// In this case, only check if it's on a English-only category.
if isEnglishCategory {
return errNonEnglishLanguageInEnglishCategory
}
return nil
}
if r.Language == "" && isEnglishCategory { // If no language, but in an English category, set to en-us.
// FIXME Maybe this shouldn't be hard-coded?
r.Language = "en-us"
}
if !torrentLanguages.LanguageExists(r.Language) {
return errInvalidTorrentLanguage
}
if !strings.HasPrefix(r.Language, "en") {
if isEnglishCategory {
return errNonEnglishLanguageInEnglishCategory
}
} else {
for _, cat := range config.Conf.Torrents.NonEnglishOnlyCategories {
if cat == r.Category {
return errEnglishLanguageInNonEnglishCategory
}
}
}
return nil
}
// ExtractBasicValue : takes an http request and computes all basic fields for this form
func (r *TorrentRequest) ExtractBasicValue(req *http.Request) error {
if strings.HasPrefix(req.Header.Get("Content-type"), "multipart/form-data") || req.Header.Get("Content-Type") == "application/x-www-form-urlencoded" { // Multipart
if strings.HasPrefix(req.Header.Get("Content-type"), "multipart/form-data") { // We parse the multipart form
err := req.ParseMultipartForm(15485760)
if err != nil {
return err
}
}
r.Name = req.FormValue(uploadFormName)
r.Category = req.FormValue(uploadFormCategory)
r.WebsiteLink = req.FormValue(uploadFormWebsiteLink)
r.Description = req.FormValue(uploadFormDescription)
r.Hidden = req.FormValue(uploadFormHidden) == "on"
r.Status, _ = strconv.Atoi(req.FormValue(uploadFormStatus))
r.Remake = req.FormValue(uploadFormRemake) == "on"
r.Magnet = req.FormValue(uploadFormMagnet)
r.Language = req.FormValue(uploadFormLanguage)
} else { // JSON (no file upload then)
decoder := json.NewDecoder(req.Body)
err := decoder.Decode(&r)
if err != nil {
return err
}
}
// trim whitespace
r.Name = strings.TrimSpace(r.Name)
r.Description = util.Sanitize(strings.TrimSpace(r.Description), "default")
r.WebsiteLink = strings.TrimSpace(r.WebsiteLink)
r.Magnet = strings.TrimSpace(r.Magnet)
// then actually check that we have everything we need
err := r.validateDescription()
if err != nil {
return err
}
err = r.validateWebsiteLink()
return err
}
// ExtractInfo : takes an http request and computes all fields for this form
func (r *TorrentRequest) ExtractInfo(req *http.Request) error {
err := r.ExtractBasicValue(req)
if err != nil {
return err
}
cache.Impl.ClearAll()
defer req.Body.Close()
err = r.ExtractCategory(req)
if err != nil {
return err
}
err = r.ExtractLanguage(req)
if err != nil {
return err
}
tfile, err := r.ValidateMultipartUpload(req)
if err != nil {
return err
}
// We check name only here, reason: we can try to retrieve them from the torrent file
err = r.validateName()
if err != nil {
return err
}
// after data has been checked & extracted, write it to disk
if len(config.Conf.Torrents.FileStorage) > 0 {
err := writeTorrentToDisk(tfile, r.Infohash+".torrent", &r.Filepath)
if err != nil {
return err
}
} else {
r.Filepath = ""
}
return nil
}
// ValidateMultipartUpload : Check if multipart upload is valid
func (r *TorrentRequest) ValidateMultipartUpload(req *http.Request) (multipart.File, error) {
// first: parse torrent file (if any) to fill missing information
tfile, _, err := req.FormFile(uploadFormTorrent)
if err == nil {
var torrent metainfo.TorrentFile
// decode torrent
_, seekErr := tfile.Seek(0, io.SeekStart)
if seekErr != nil {
return tfile, seekErr
}
err = bencode.NewDecoder(tfile).Decode(&torrent)
if err != nil {
return tfile, metainfo.ErrInvalidTorrentFile
}
// check a few things
if torrent.IsPrivate() {
return tfile, errPrivateTorrent
}
trackers := torrent.GetAllAnnounceURLS()
r.Trackers = uploadService.CheckTrackers(trackers)
if len(r.Trackers) == 0 {
return tfile, errTrackerProblem
}
// Name
if len(r.Name) == 0 {
r.Name = torrent.TorrentName()
}
// Magnet link: if a file is provided it should be empty
if len(r.Magnet) != 0 {
return tfile, errTorrentPlusMagnet
}
_, seekErr = tfile.Seek(0, io.SeekStart)
if seekErr != nil {
return tfile, seekErr
}
infohash, err := metainfo.DecodeInfohash(tfile)
if err != nil {
return tfile, metainfo.ErrInvalidTorrentFile
}
r.Infohash = infohash
r.Magnet = util.InfoHashToMagnet(infohash, r.Name, trackers...)
// extract filesize
r.Filesize = int64(torrent.TotalSize())
// extract filelist
fileInfos := torrent.Info.GetFiles()
for _, fileInfo := range fileInfos {
r.FileList = append(r.FileList, uploadedFile{
Path: fileInfo.Path,
Filesize: int64(fileInfo.Length),
})
}
} else {
err = r.validateMagnet()
if err != nil {
return tfile, err
}
err = r.validateHash()
if err != nil {
return tfile, err
}
// TODO: Get Trackers from magnet URL
r.Filesize = 0
r.Filepath = ""
return tfile, nil
}
return tfile, err
}
// UpdateTorrent : Update torrent model
//rewrite with reflect ?
func (r *UpdateRequest) UpdateTorrent(t *model.Torrent, currentUser *model.User) {
if r.Update.Name != "" {
t.Name = r.Update.Name
}
if r.Update.Infohash != "" {
t.Hash = r.Update.Infohash
}
if r.Update.CategoryID != 0 {
t.Category = r.Update.CategoryID
}
if r.Update.SubCategoryID != 0 {
t.SubCategory = r.Update.SubCategoryID
}
if r.Update.Description != "" {
t.Description = r.Update.Description
}
if r.Update.WebsiteLink != "" {
t.WebsiteLink = r.Update.WebsiteLink
}
status := model.TorrentStatusNormal
if r.Update.Remake { // overrides trusted
status = model.TorrentStatusRemake
} else if currentUser.IsTrusted() {
status = model.TorrentStatusTrusted
}
t.Status = status
}
func writeTorrentToDisk(file multipart.File, name string, fullpath *string) error {
_, seekErr := file.Seek(0, io.SeekStart)
if seekErr != nil {
return seekErr
}
b, err := ioutil.ReadAll(file)
if err != nil {
return err
}
*fullpath = fmt.Sprintf("%s%c%s", config.Conf.Torrents.FileStorage, os.PathSeparator, name)
return ioutil.WriteFile(*fullpath, b, 0644)
}
// NewTorrentRequest : creates a new torrent request struc with some default value
func NewTorrentRequest(params ...string) (torrentRequest TorrentRequest) {
if len(params) > 1 {
torrentRequest.Category = params[0]
} else {
torrentRequest.Category = "3_12"
}
if len(params) > 2 {
torrentRequest.Description = params[1]
} else {
torrentRequest.Description = "Description"
}
return
}