2017-05-06 13:43:24 +02:00
package router
import (
2017-05-07 02:48:24 +02:00
"encoding/hex"
2017-05-06 13:43:24 +02:00
"errors"
2017-05-07 15:55:34 +02:00
"fmt"
"io"
"io/ioutil"
"mime/multipart"
2017-05-07 10:25:09 +02:00
"net/http"
2017-05-07 02:48:24 +02:00
"net/url"
2017-05-07 15:55:34 +02:00
"os"
2017-05-07 19:47:43 +02:00
"regexp"
2017-05-07 06:26:09 +02:00
"strconv"
2017-05-07 02:48:24 +02:00
"strings"
2017-05-07 10:25:09 +02:00
2017-05-07 15:19:36 +02:00
"github.com/ewhal/nyaa/config"
2017-05-07 11:08:44 +02:00
"github.com/ewhal/nyaa/service/captcha"
2017-05-06 13:43:24 +02:00
"github.com/ewhal/nyaa/util"
"github.com/ewhal/nyaa/util/metainfo"
2017-05-07 11:08:44 +02:00
"github.com/microcosm-cc/bluemonday"
2017-05-06 13:43:24 +02:00
"github.com/zeebo/bencode"
)
// UploadForm serializing HTTP form for torrent upload
type UploadForm struct {
2017-05-07 19:47:43 +02:00
Name string
Magnet string
Category string
Description string
2017-05-07 11:08:44 +02:00
captcha . Captcha
2017-05-07 13:48:28 +02:00
2017-05-07 15:55:34 +02:00
Infohash string
2017-05-07 13:48:28 +02:00
CategoryId int
SubCategoryId int
Filesize int64
2017-05-07 15:55:34 +02:00
Filepath string
2017-05-06 13:43:24 +02:00
}
// TODO: these should be in another package (?)
/ / form value for torrent name
const UploadFormName = "name"
// form value for torrent file
const UploadFormTorrent = "torrent"
// form value for magnet uri (?)
const UploadFormMagnet = "magnet"
// form value for category
const UploadFormCategory = "c"
// form value for description
const UploadFormDescription = "desc"
2017-05-07 13:38:46 +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" )
2017-05-06 13:43:24 +02:00
// error indicating a torrent is private
var ErrPrivateTorrent = errors . New ( "torrent is private" )
2017-05-07 12:20:08 +02:00
// error indicating a problem with its trackers
2017-05-07 15:19:36 +02:00
var ErrTrackerProblem = errors . New ( "torrent does not have any (working) trackers: https://" + config . WebAddress + "/faq#trackers" )
2017-05-07 12:20:08 +02:00
2017-05-06 13:43:24 +02:00
// 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" )
2017-05-07 06:26:09 +02:00
// error indicating a torrent's category is invalid
var ErrInvalidTorrentCategory = errors . New ( "torrent category is invalid" )
2017-05-07 07:18:41 +02:00
var p = bluemonday . UGCPolicy ( )
2017-05-06 13:43:24 +02:00
/ * *
UploadForm . ExtractInfo takes an http request and computes all fields for this form
* /
func ( f * UploadForm ) ExtractInfo ( r * http . Request ) error {
f . Name = r . FormValue ( UploadFormName )
f . Category = r . FormValue ( UploadFormCategory )
f . Description = r . FormValue ( UploadFormDescription )
f . Magnet = r . FormValue ( UploadFormMagnet )
2017-05-07 11:08:44 +02:00
f . Captcha = captcha . Extract ( r )
2017-05-06 13:43:24 +02:00
2017-05-07 19:47:43 +02:00
if ! captcha . Authenticate ( f . Captcha ) {
// TODO: Prettier passing of mistyoed captcha errors
return errors . New ( captcha . ErrInvalidCaptcha . Error ( ) )
}
2017-05-07 19:41:07 +02:00
2017-05-06 13:43:24 +02:00
// trim whitespaces
f . Name = util . TrimWhitespaces ( f . Name )
2017-05-07 07:18:41 +02:00
f . Description = p . Sanitize ( util . TrimWhitespaces ( f . Description ) )
2017-05-06 13:43:24 +02:00
f . Magnet = util . TrimWhitespaces ( f . Magnet )
2017-05-07 06:26:09 +02:00
catsSplit := strings . Split ( f . Category , "_" )
// need this to prevent out of index panics
if len ( catsSplit ) == 2 {
CatId , err := strconv . Atoi ( catsSplit [ 0 ] )
if err != nil {
return ErrInvalidTorrentCategory
}
SubCatId , err := strconv . Atoi ( catsSplit [ 1 ] )
if err != nil {
return ErrInvalidTorrentCategory
}
f . CategoryId = CatId
f . SubCategoryId = SubCatId
} else {
return ErrInvalidTorrentCategory
2017-05-06 13:43:24 +02:00
}
2017-05-07 13:38:46 +02:00
// first: parse torrent file (if any) to fill missing information
tfile , _ , err := r . FormFile ( UploadFormTorrent )
if err == nil {
2017-05-06 13:43:24 +02:00
var torrent metainfo . TorrentFile
2017-05-07 15:55:34 +02:00
2017-05-06 13:43:24 +02:00
// decode torrent
2017-05-07 15:55:34 +02:00
tfile . Seek ( 0 , io . SeekStart )
2017-05-06 13:43:24 +02:00
err = bencode . NewDecoder ( tfile ) . Decode ( & torrent )
if err != nil {
return metainfo . ErrInvalidTorrentFile
}
2017-05-07 13:38:46 +02:00
// check a few things
2017-05-06 13:43:24 +02:00
if torrent . IsPrivate ( ) {
return ErrPrivateTorrent
}
2017-05-07 12:20:08 +02:00
trackers := torrent . GetAllAnnounceURLS ( )
if ! CheckTrackers ( trackers ) {
return ErrTrackerProblem
}
2017-05-07 13:38:46 +02:00
// Name
if len ( f . Name ) == 0 {
f . Name = torrent . TorrentName ( )
}
// Magnet link: if a file is provided it should be empty
if len ( f . Magnet ) != 0 {
return ErrTorrentPlusMagnet
}
2017-05-07 03:32:26 +02:00
binInfohash := torrent . Infohash ( )
2017-05-07 13:38:46 +02:00
f . Infohash = strings . ToUpper ( hex . EncodeToString ( binInfohash [ : ] ) )
2017-05-07 19:47:43 +02:00
f . Magnet = util . InfoHashToMagnet ( f . Infohash , f . Name , trackers ... )
2017-05-07 13:48:28 +02:00
// extract filesize
f . Filesize = int64 ( torrent . TotalSize ( ) )
2017-05-07 02:48:24 +02:00
} else {
2017-05-07 13:38:46 +02:00
// No torrent file provided
2017-05-07 02:48:24 +02:00
magnetUrl , parseErr := url . Parse ( f . Magnet )
if parseErr != nil {
return metainfo . ErrInvalidTorrentFile
}
exactTopic := magnetUrl . Query ( ) . Get ( "xt" )
if ! strings . HasPrefix ( exactTopic , "urn:btih:" ) {
return metainfo . ErrInvalidTorrentFile
}
2017-05-07 13:38:46 +02:00
f . Infohash = strings . ToUpper ( strings . TrimPrefix ( exactTopic , "urn:btih:" ) )
matched , err := regexp . MatchString ( "^[0-9A-F]{40}$" , f . Infohash )
if err != nil || ! matched {
return metainfo . ErrInvalidTorrentFile
}
2017-05-07 13:48:28 +02:00
f . Filesize = 0
2017-05-07 15:55:34 +02:00
f . Filepath = ""
2017-05-07 13:38:46 +02:00
}
// then actually check that we have everything we need
if len ( f . Name ) == 0 {
return ErrInvalidTorrentName
2017-05-06 13:43:24 +02:00
}
2017-05-07 02:48:24 +02:00
2017-05-07 13:38:46 +02:00
//if len(f.Description) == 0 {
// return ErrInvalidTorrentDescription
//}
2017-05-07 15:55:34 +02:00
// after data has been checked & extracted, write it to disk
if len ( config . TorrentFileStorage ) > 0 {
2017-05-07 19:47:43 +02:00
err := WriteTorrentToDisk ( tfile , f . Infohash + ".torrent" , & f . Filepath )
2017-05-07 15:55:34 +02:00
if err != nil {
return err
}
} else {
f . Filepath = ""
}
2017-05-06 13:43:24 +02:00
return nil
}
2017-05-07 15:55:34 +02:00
func WriteTorrentToDisk ( file multipart . File , name string , fullpath * string ) error {
file . Seek ( 0 , io . SeekStart )
b , err := ioutil . ReadAll ( file )
if err != nil {
return err
}
* fullpath = fmt . Sprintf ( "%s%c%s" , config . TorrentFileStorage , os . PathSeparator , name )
return ioutil . WriteFile ( * fullpath , b , 0644 )
}
2017-05-07 12:20:08 +02:00
var dead_trackers = [ ] string { // substring matches!
"://open.nyaatorrents.info:6544" ,
"://tracker.openbittorrent.com:80" ,
"://tracker.publicbt.com:80" ,
"://stats.anisource.net:2710" ,
"://exodus.desync.com" ,
"://open.demonii.com:1337" ,
"://tracker.istole.it:80" ,
"://tracker.ccc.de:80" ,
"://bt2.careland.com.cn:6969" ,
"://announce.torrentsmd.com:8080" }
func CheckTrackers ( trackers [ ] string ) bool {
var numGood int
for _ , t := range trackers {
var good bool = true
for _ , check := range dead_trackers {
if strings . Contains ( t , check ) {
good = false
}
}
if good {
numGood += 1
}
}
return numGood > 0
}
2017-05-06 13:43:24 +02:00
// NewUploadForm creates a new upload form given parameters as list
func NewUploadForm ( params ... string ) ( uploadForm UploadForm ) {
if len ( params ) > 1 {
uploadForm . Category = params [ 0 ]
} else {
uploadForm . Category = "3_12"
}
if len ( params ) > 2 {
uploadForm . Description = params [ 1 ]
} else {
uploadForm . Description = "Description"
}
return
}