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-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"
// 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-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" `
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-05-19 04:55:59 +02:00
// TODO Check category is within accepted range
2017-05-09 22:24:32 +02:00
func validateCategory ( r * TorrentRequest ) ( error , int ) {
2017-06-05 15:19:25 +02:00
if r . CategoryID == 0 {
2017-05-09 19:37:39 +02:00
return ErrCategory , http . StatusNotAcceptable
}
2017-05-09 22:24:32 +02:00
return nil , http . StatusOK
}
2017-05-19 04:55:59 +02:00
// TODO Check subCategory is within accepted range
2017-05-09 22:24:32 +02:00
func validateSubCategory ( r * TorrentRequest ) ( error , int ) {
2017-05-09 19:37:39 +02:00
if r . SubCategory == 0 {
return ErrSubCategory , http . StatusNotAcceptable
}
2017-05-09 22:24:32 +02:00
return nil , http . StatusOK
}
2017-05-09 19:37:39 +02:00
2017-05-28 06:22:39 +02:00
func validateWebsiteLink ( r * TorrentRequest ) ( error , int ) {
if r . WebsiteLink != "" {
// WebsiteLink
urlRegexp , _ := regexp . Compile ( ` ^(https?:\/\/|ircs?:\/\/)?([\da-z\.-]+)\.([a-z\.] { 2,6})([\/\w \.-]*)*\/?$ ` )
if ! urlRegexp . MatchString ( r . WebsiteLink ) {
return ErrWebsiteLink , http . StatusNotAcceptable
}
}
return nil , http . StatusOK
}
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
}
defer req . Body . Close ( )
err = r . ExtractCategory ( req )
if err != nil {
return err
2017-05-09 20:54:50 +02:00
}
2017-06-05 15:19:25 +02:00
return nil
}
// 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 {
CatID , err := strconv . Atoi ( catsSplit [ 0 ] )
if err != nil {
return errInvalidTorrentCategory
2017-05-09 20:54:50 +02:00
}
2017-06-05 15:19:25 +02:00
SubCatID , err := strconv . Atoi ( catsSplit [ 1 ] )
2017-05-09 20:54:50 +02:00
if err != nil {
2017-06-05 15:19:25 +02:00
return errInvalidTorrentCategory
2017-05-09 22:24:32 +02:00
}
2017-06-05 15:19:25 +02:00
if ! categories . CategoryExists ( r . Category ) {
return errInvalidTorrentCategory
}
r . CategoryID = CatID
r . SubCategoryID = SubCatID
} else {
return errInvalidTorrentCategory
2017-05-09 22:24:32 +02:00
}
2017-06-05 15:19:25 +02:00
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 )
} 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 . validateName ( )
if err != nil {
return err
}
err = r . validateDescription ( )
if err != nil {
return err
}
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
}
// 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
}
tfile , err := r . ValidateMultipartUpload ( req )
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
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
}