2017-05-09 17:09:45 +02:00
package apiService
2017-05-09 18:37:40 +02:00
import (
2017-05-11 23:59:00 +02:00
"encoding/base32"
2017-05-11 05:04:11 +02:00
"encoding/hex"
2017-06-05 15:19:25 +02:00
"encoding/json"
2017-05-11 05:04:11 +02:00
"errors"
2017-05-11 06:12:54 +02:00
"fmt"
2017-05-11 05:04:11 +02:00
"io"
2017-06-05 15:19:25 +02:00
"io/ioutil"
"mime/multipart"
2017-05-09 19:37:39 +02:00
"net/http"
"net/url"
2017-06-05 15:19:25 +02:00
"os"
2017-05-09 18:37:40 +02:00
"reflect"
2017-05-09 19:37:39 +02:00
"regexp"
2017-06-05 15:19:25 +02:00
"strconv"
2017-05-09 19:37:39 +02:00
"strings"
2017-05-09 18:37:40 +02:00
2017-06-05 15:19:25 +02:00
"github.com/NyaaPantsu/nyaa/cache"
"github.com/NyaaPantsu/nyaa/config"
2017-05-17 07:58:40 +02:00
"github.com/NyaaPantsu/nyaa/model"
"github.com/NyaaPantsu/nyaa/service"
"github.com/NyaaPantsu/nyaa/service/upload"
2017-06-05 15:19:25 +02:00
"github.com/NyaaPantsu/nyaa/util"
"github.com/NyaaPantsu/nyaa/util/categories"
2017-05-17 07:58:40 +02:00
"github.com/NyaaPantsu/nyaa/util/metainfo"
2017-06-12 01:14:26 +02:00
"github.com/NyaaPantsu/nyaa/util/torrentLanguages"
2017-05-11 05:04:11 +02:00
"github.com/zeebo/bencode"
2017-05-09 18:37:40 +02:00
)
2017-06-05 15:19:25 +02:00
// 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"
2017-06-12 01:14:26 +02:00
const uploadFormLanguage = "language"
2017-06-05 15:19:25 +02:00
// 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" )
2017-06-12 01:14:26 +02:00
// 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." )
2017-05-09 18:37:40 +02:00
type torrentsQuery struct {
Category int ` json:"category" `
SubCategory int ` json:"sub_category" `
Status int ` json:"status" `
Uploader int ` json:"uploader" `
Downloads int ` json:"downloads" `
}
2017-05-26 12:12:52 +02:00
// TorrentsRequest struct
2017-05-09 18:37:40 +02:00
type TorrentsRequest struct {
Query torrentsQuery ` json:"search" `
Page int ` json:"page" `
MaxPerPage int ` json:"limit" `
}
2017-06-05 15:19:25 +02:00
// 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" `
}
2017-05-26 12:12:52 +02:00
// TorrentRequest struct
2017-06-05 15:19:25 +02:00
// Same json name as the constant!
2017-05-09 20:54:50 +02:00
type TorrentRequest struct {
2017-06-05 15:19:25 +02:00
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" `
2017-06-12 01:14:26 +02:00
Language string ` json:"language,omitempty" `
2017-06-05 15:19:25 +02:00
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" `
2017-05-09 19:37:39 +02:00
}
2017-05-26 12:12:52 +02:00
// UpdateRequest struct
2017-05-09 20:54:50 +02:00
type UpdateRequest struct {
Consistency, formatting, error checking, cleanup, and a couple bug fixes (#245)
* Checkpoint: it builds
The config, db, model, network, os, and public packages have had some
fixes to glaringly obvious flaws, dead code removed, and stylistic
changes.
* Style changes and old code removal in router
Router needs a lot of work done to its (lack of) error handling.
* Dead code removal and style changes
Now up to util/email/email.go. After I'm finished with the initial sweep
I'll go back and fix error handling and security issues. Then I'll fix
the broken API. Then I'll go through to add documentation and fix code
visibility.
* Finish dead code removal and style changes
Vendored libraries not touched. Everything still needs security fixes
and documentation. There's also one case of broken functionality.
* Fix accidental find-and-replace
* Style, error checking, saftey, bug fix changes
* Redo error checking erased during merge
* Re-add merge-erased fix. Make Safe safe.
2017-05-10 04:34:40 +02:00
ID int ` json:"id" `
2017-05-09 20:54:50 +02:00
Update TorrentRequest ` json:"update" `
}
2017-05-26 12:12:52 +02:00
// ToParams : Convert a torrentsrequest to searchparams
2017-05-10 20:42:20 +02:00
func ( r * TorrentsRequest ) ToParams ( ) serviceBase . WhereParams {
res := serviceBase . WhereParams { }
2017-05-09 18:37:40 +02:00
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
}
2017-05-09 19:37:39 +02:00
2017-06-05 15:19:25 +02:00
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 ) > 500 {
return errInvalidTorrentDescription
}
return nil
2017-05-09 22:24:32 +02:00
}
2017-06-14 12:10:03 +02:00
func ( r * TorrentRequest ) validateWebsiteLink ( ) error {
2017-05-28 06:22:39 +02:00
if r . WebsiteLink != "" {
// WebsiteLink
urlRegexp , _ := regexp . Compile ( ` ^(https?:\/\/|ircs?:\/\/)?([\da-z\.-]+)\.([a-z\.] { 2,6})([\/\w \.-]*)*\/?$ ` )
if ! urlRegexp . MatchString ( r . WebsiteLink ) {
2017-06-14 12:10:03 +02:00
return errInvalidWebsiteLink
2017-05-28 06:22:39 +02:00
}
}
2017-06-14 12:10:03 +02:00
return nil
2017-05-28 06:22:39 +02:00
}
2017-06-05 15:19:25 +02:00
func ( r * TorrentRequest ) validateMagnet ( ) error {
2017-05-26 12:12:52 +02:00
magnetURL , err := url . Parse ( string ( r . Magnet ) ) //?
2017-05-09 22:24:32 +02:00
if err != nil {
2017-06-05 15:19:25 +02:00
return err
2017-05-09 22:24:32 +02:00
}
2017-05-26 12:12:52 +02:00
xt := magnetURL . Query ( ) . Get ( "xt" )
2017-05-11 23:59:00 +02:00
if ! strings . HasPrefix ( xt , "urn:btih:" ) {
2017-06-05 15:19:25 +02:00
return ErrMagnet
2017-05-09 19:37:39 +02:00
}
2017-05-11 23:59:00 +02:00
xt = strings . SplitAfter ( xt , ":" ) [ 2 ]
2017-06-05 15:19:25 +02:00
r . Infohash = strings . ToUpper ( strings . Split ( xt , "&" ) [ 0 ] )
return nil
2017-05-09 22:24:32 +02:00
}
2017-05-09 19:37:39 +02:00
2017-06-05 15:19:25 +02:00
func ( r * TorrentRequest ) validateHash ( ) error {
isBase32 , err := regexp . MatchString ( "^[2-7A-Z]{32}$" , r . Infohash )
2017-05-09 19:37:39 +02:00
if err != nil {
2017-06-05 15:19:25 +02:00
return err
2017-05-09 19:37:39 +02:00
}
2017-05-11 23:59:00 +02:00
if ! isBase32 {
2017-06-05 15:19:25 +02:00
isBase16 , err := regexp . MatchString ( "^[0-9A-F]{40}$" , r . Infohash )
2017-05-11 23:59:00 +02:00
if err != nil {
2017-06-05 15:19:25 +02:00
return err
2017-05-11 23:59:00 +02:00
}
if ! isBase16 {
2017-06-05 15:19:25 +02:00
return ErrHash
2017-05-11 23:59:00 +02:00
}
} else {
//convert to base16
2017-06-05 15:19:25 +02:00
data , err := base32 . StdEncoding . DecodeString ( r . Infohash )
2017-05-11 23:59:00 +02:00
if err != nil {
2017-06-05 15:19:25 +02:00
return err
2017-05-11 23:59:00 +02:00
}
hash16 := make ( [ ] byte , hex . EncodedLen ( len ( data ) ) )
hex . Encode ( hash16 , data )
2017-06-05 15:19:25 +02:00
r . Infohash = strings . ToUpper ( string ( hash16 ) )
2017-05-09 19:37:39 +02:00
}
2017-06-05 15:19:25 +02:00
return nil
2017-05-09 19:37:39 +02:00
}
2017-05-09 20:54:50 +02:00
2017-06-05 15:19:25 +02:00
// 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
}
2017-06-10 20:09:25 +02:00
err = r . validateName ( )
if err != nil {
return err
}
2017-06-05 15:19:25 +02:00
defer req . Body . Close ( )
err = r . ExtractCategory ( req )
if err != nil {
return err
2017-05-09 20:54:50 +02:00
}
2017-06-12 01:14:26 +02:00
err = r . ExtractLanguage ( req )
2017-06-14 09:20:42 +02:00
return err
2017-06-05 15:19:25 +02:00
}
// 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
2017-06-14 12:10:03 +02:00
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
}
2017-06-05 15:19:25 +02:00
2017-06-14 12:10:03 +02:00
if ! categories . CategoryExists ( r . Category ) {
2017-06-05 15:19:25 +02:00
return errInvalidTorrentCategory
2017-05-09 22:24:32 +02:00
}
2017-06-14 12:10:03 +02:00
r . CategoryID = CatID
r . SubCategoryID = SubCatID
2017-06-05 15:19:25 +02:00
return nil
}
2017-06-12 01:14:26 +02:00
// 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
}
2017-06-05 15:19:25 +02:00
// 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 )
2017-06-12 01:14:26 +02:00
r . Language = req . FormValue ( uploadFormLanguage )
2017-06-05 15:19:25 +02:00
} 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
2017-06-10 20:09:25 +02:00
err := r . validateDescription ( )
2017-06-05 15:19:25 +02:00
if err != nil {
return err
}
2017-06-14 12:10:03 +02:00
err = r . validateWebsiteLink ( )
return err
2017-06-05 15:19:25 +02:00
}
// 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
}
2017-06-12 01:14:26 +02:00
err = r . ExtractLanguage ( req )
if err != nil {
return err
}
2017-06-05 15:19:25 +02:00
tfile , err := r . ValidateMultipartUpload ( req )
if err != nil {
return err
}
2017-06-10 20:09:25 +02:00
// We check name only here, reason: we can try to retrieve them from the torrent file
err = r . validateName ( )
if err != nil {
return err
}
2017-06-05 15:19:25 +02:00
// 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
2017-05-09 22:24:32 +02:00
}
2017-05-26 12:12:52 +02:00
// ValidateMultipartUpload : Check if multipart upload is valid
2017-06-05 15:19:25 +02:00
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 )
2017-05-11 05:04:11 +02:00
if err == nil {
var torrent metainfo . TorrentFile
// decode torrent
2017-06-05 15:19:25 +02:00
_ , seekErr := tfile . Seek ( 0 , io . SeekStart )
if seekErr != nil {
return tfile , seekErr
2017-05-11 05:04:11 +02:00
}
2017-06-05 15:19:25 +02:00
err = bencode . NewDecoder ( tfile ) . Decode ( & torrent )
if err != nil {
return tfile , metainfo . ErrInvalidTorrentFile
2017-05-11 05:04:11 +02:00
}
2017-06-05 15:19:25 +02:00
2017-05-11 05:04:11 +02:00
// check a few things
if torrent . IsPrivate ( ) {
2017-06-05 15:19:25 +02:00
return tfile , errPrivateTorrent
2017-05-11 05:04:11 +02:00
}
trackers := torrent . GetAllAnnounceURLS ( )
2017-06-05 15:19:25 +02:00
r . Trackers = uploadService . CheckTrackers ( trackers )
if len ( r . Trackers ) == 0 {
return tfile , errTrackerProblem
2017-05-11 05:04:11 +02:00
}
2017-06-05 15:19:25 +02:00
// Name
if len ( r . Name ) == 0 {
2017-05-11 05:04:11 +02:00
r . Name = torrent . TorrentName ( )
}
2017-06-05 15:19:25 +02:00
// 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
2017-05-11 05:04:11 +02:00
}
2017-05-28 23:47:47 +02:00
infohash , err := metainfo . DecodeInfohash ( tfile )
if err != nil {
2017-06-05 15:19:25 +02:00
return tfile , metainfo . ErrInvalidTorrentFile
2017-05-28 23:47:47 +02:00
}
2017-06-05 15:19:25 +02:00
r . Infohash = infohash
r . Magnet = util . InfoHashToMagnet ( infohash , r . Name , trackers ... )
2017-05-11 05:04:11 +02:00
// extract filesize
2017-06-05 15:19:25 +02:00
r . Filesize = int64 ( torrent . TotalSize ( ) )
2017-05-11 05:04:11 +02:00
2017-06-05 15:19:25 +02:00
// extract filelist
fileInfos := torrent . Info . GetFiles ( )
for _ , fileInfo := range fileInfos {
r . FileList = append ( r . FileList , uploadedFile {
Path : fileInfo . Path ,
Filesize : int64 ( fileInfo . Length ) ,
} )
2017-05-09 20:54:50 +02:00
}
2017-06-05 15:19:25 +02:00
} else {
err = r . validateMagnet ( )
2017-05-09 22:24:32 +02:00
if err != nil {
2017-06-05 15:19:25 +02:00
return tfile , err
2017-05-09 20:54:50 +02:00
}
2017-06-05 15:19:25 +02:00
err = r . validateHash ( )
if err != nil {
return tfile , err
}
// TODO: Get Trackers from magnet URL
r . Filesize = 0
r . Filepath = ""
2017-05-09 20:54:50 +02:00
2017-06-05 15:19:25 +02:00
return tfile , nil
}
return tfile , err
2017-05-09 20:54:50 +02:00
}
2017-05-26 12:12:52 +02:00
// UpdateTorrent : Update torrent model
2017-05-09 20:54:50 +02:00
//rewrite with reflect ?
2017-06-05 15:19:25 +02:00
func ( r * UpdateRequest ) UpdateTorrent ( t * model . Torrent , currentUser * model . User ) {
2017-05-09 20:54:50 +02:00
if r . Update . Name != "" {
t . Name = r . Update . Name
}
2017-06-05 15:19:25 +02:00
if r . Update . Infohash != "" {
t . Hash = r . Update . Infohash
2017-05-09 20:54:50 +02:00
}
2017-06-05 15:19:25 +02:00
if r . Update . CategoryID != 0 {
t . Category = r . Update . CategoryID
2017-05-09 20:54:50 +02:00
}
2017-06-05 15:19:25 +02:00
if r . Update . SubCategoryID != 0 {
t . SubCategory = r . Update . SubCategoryID
2017-05-09 20:54:50 +02:00
}
if r . Update . Description != "" {
t . Description = r . Update . Description
}
2017-05-28 06:22:39 +02:00
if r . Update . WebsiteLink != "" {
t . WebsiteLink = r . Update . WebsiteLink
}
2017-06-05 15:19:25 +02:00
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
2017-05-09 20:54:50 +02:00
}