feat(releases): torrent file downloads improve error handling (#950)

* improve content type check
checks if torrent file is a valid torrent file when content-type is text/html

* optimize content type check and file handling

* attempt to write tests

* small changes to error messages

* fix: download file content type checks

---------

Co-authored-by: Kyle Sanderson <kyle.leet@gmail.com>
Co-authored-by: ze0s <43699394+zze0s@users.noreply.github.com>
This commit is contained in:
soup 2023-06-14 19:55:34 +02:00 committed by GitHub
parent 3d9839d234
commit 28f0b878e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 109 additions and 34 deletions

View file

@ -4,6 +4,7 @@
package domain
import (
"bytes"
"context"
"crypto/tls"
"fmt"
@ -19,6 +20,7 @@ import (
"github.com/autobrr/autobrr/pkg/errors"
"github.com/anacrolix/torrent/bencode"
"github.com/anacrolix/torrent/metainfo"
"github.com/avast/retry-go"
"github.com/dustin/go-humanize"
@ -363,10 +365,10 @@ func (r *Release) DownloadTorrentFile() error {
}
func (r *Release) downloadTorrentFile(ctx context.Context) error {
if r.Protocol != ReleaseProtocolTorrent {
return errors.New("download_file: protocol is not %s: %s", ReleaseProtocolTorrent, r.Protocol)
} else if r.HasMagnetUri() {
return fmt.Errorf("error trying to download magnet link: %s", r.MagnetURI)
if r.HasMagnetUri() {
return errors.New("downloading magnet links are not supported: %s", r.MagnetURI)
} else if r.Protocol != ReleaseProtocolTorrent {
return errors.New("could not download file: protocol %s is not supported", r.Protocol)
}
if r.TorrentURL == "" {
@ -423,10 +425,10 @@ func (r *Release) downloadTorrentFile(ctx context.Context) error {
// Continue processing the response
case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
// Handle redirect
return retry.Unrecoverable(errors.New("redirect encountered for torrent (%v) file (%v) from '%v' - status code: %d. Check indexer keys.", r.TorrentName, r.TorrentURL, r.Indexer, resp.StatusCode))
return retry.Unrecoverable(errors.New("redirect encountered for torrent (%v) file (%v) - status code: %d - check indexer keys for %s", r.TorrentName, r.TorrentURL, resp.StatusCode, r.Indexer))
case http.StatusUnauthorized, http.StatusForbidden:
return retry.Unrecoverable(errors.New("unrecoverable error downloading torrent (%v) file (%v) from '%v' - status code: %d. Check indexer keys", r.TorrentName, r.TorrentURL, r.Indexer, resp.StatusCode))
return retry.Unrecoverable(errors.New("unrecoverable error downloading torrent (%v) file (%v) - status code: %d - check indexer keys for %s", r.TorrentName, r.TorrentURL, resp.StatusCode, r.Indexer))
case http.StatusMethodNotAllowed:
return retry.Unrecoverable(errors.New("unrecoverable error downloading torrent (%v) file (%v) from '%v' - status code: %d. Check if the request method is correct", r.TorrentName, r.TorrentURL, r.Indexer, resp.StatusCode))
@ -434,47 +436,61 @@ func (r *Release) downloadTorrentFile(ctx context.Context) error {
case http.StatusNotFound:
return errors.New("torrent %s not found on %s (%d) - retrying", r.TorrentName, r.Indexer, resp.StatusCode)
case http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
case http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
return errors.New("server error (%d) encountered while downloading torrent (%v) file (%v) from '%v' - retrying", resp.StatusCode, r.TorrentName, r.TorrentURL, r.Indexer)
case http.StatusInternalServerError:
return errors.New("server error (%d) encountered while downloading torrent (%v) file (%v) - check indexer keys for %s", resp.StatusCode, r.TorrentName, r.TorrentURL, r.Indexer)
default:
return retry.Unrecoverable(errors.New("unexpected status code %d: check indexer keys for %s", resp.StatusCode, r.Indexer))
}
// Check if the Content-Type header is correct
contentType := resp.Header.Get("Content-Type")
if strings.Contains(contentType, "text/html") {
return retry.Unrecoverable(errors.New("unexpected content type '%s': check indexer keys for %s", contentType, r.Indexer))
}
resetTmpFile := func() {
tmpFile.Seek(0, io.SeekStart)
tmpFile.Truncate(0)
}
// Write the body to file
if _, err := io.Copy(tmpFile, resp.Body); err != nil {
resetTmpFile()
return errors.Wrap(err, "error writing downloaded file: %v", tmpFile.Name())
// Read the body into bytes
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return errors.Wrap(err, "error reading response body")
}
meta, err := metainfo.LoadFromFile(tmpFile.Name())
// Create a new reader for bodyBytes
bodyReader := bytes.NewReader(bodyBytes)
// Try to decode as torrent file
meta, err := metainfo.Load(bodyReader)
if err != nil {
resetTmpFile()
return errors.Wrap(err, "metainfo could not load file contents: %v", tmpFile.Name())
// explicitly check for unexpected content type that match html
var bse *bencode.SyntaxError
if errors.As(err, &bse) {
// regular error so we can retry if we receive html first run
return errors.Wrap(err, "metainfo unexpected content type, got HTML expected a bencoded torrent. check indexer keys for %s - %s", r.Indexer, r.TorrentName)
}
return retry.Unrecoverable(errors.Wrap(err, "metainfo unexpected content type. check indexer keys for %s - %s", r.Indexer, r.TorrentName))
}
// Write the body to file
if _, err := tmpFile.Write(bodyBytes); err != nil {
resetTmpFile()
return errors.Wrap(err, "error writing downloaded file: %s", tmpFile.Name())
}
torrentMetaInfo, err := meta.UnmarshalInfo()
if err != nil {
resetTmpFile()
return errors.Wrap(err, "metainfo could not unmarshal info from torrent: %v", tmpFile.Name())
return retry.Unrecoverable(errors.Wrap(err, "metainfo could not unmarshal info from torrent: %s", tmpFile.Name()))
}
hashInfoBytes := meta.HashInfoBytes().Bytes()
if len(hashInfoBytes) < 1 {
resetTmpFile()
return errors.New("could not read infohash")
return retry.Unrecoverable(errors.New("could not read infohash"))
}
r.TorrentTmpFile = tmpFile.Name()