autobrr/pkg/qbittorrent/client.go
Ludvig Lundgren 0e88117702
feat(logging); improve messages and errors (#336)
* feat(logger): add module context

* feat(logger): change errors package

* feat(logger): update tests
2022-07-05 13:31:44 +02:00

400 lines
8.2 KiB
Go

package qbittorrent
import (
"bytes"
"crypto/tls"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"net/http/cookiejar"
"net/url"
"os"
"path"
"strings"
"time"
"github.com/autobrr/autobrr/pkg/errors"
publicsuffix "golang.org/x/net/publicsuffix"
)
var (
backoffSchedule = []time.Duration{
5 * time.Second,
10 * time.Second,
20 * time.Second,
}
timeout = 60 * time.Second
)
type Client struct {
Name string
settings Settings
http *http.Client
Log *log.Logger
}
type Settings struct {
Hostname string
Port uint
Username string
Password string
TLS bool
TLSSkipVerify bool
protocol string
BasicAuth bool
Basic Basic
Log *log.Logger
}
type Basic struct {
Username string
Password string
}
func NewClient(s Settings) *Client {
c := &Client{
settings: s,
}
if s.Log == nil {
c.Log = log.New(io.Discard, "qbittorrent", log.LstdFlags)
}
//store cookies in jar
jarOptions := &cookiejar.Options{PublicSuffixList: publicsuffix.List}
jar, err := cookiejar.New(jarOptions)
if err != nil {
c.Log.Println("new client cookie error")
}
c.http = &http.Client{
Timeout: timeout,
Jar: jar,
}
c.settings.protocol = "http"
if c.settings.TLS {
c.settings.protocol = "https"
}
if c.settings.TLSSkipVerify {
//skip TLS verification
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
c.http.Transport = tr
}
return c
}
func (c *Client) get(endpoint string, opts map[string]string) (*http.Response, error) {
var err error
var resp *http.Response
reqUrl := buildUrlOpts(c.settings, endpoint, opts)
req, err := http.NewRequest("GET", reqUrl, nil)
if err != nil {
return nil, errors.Wrap(err, "could not build request")
}
if c.settings.BasicAuth {
req.SetBasicAuth(c.settings.Basic.Username, c.settings.Basic.Password)
}
// try request and if fail run 3 retries
for i, backoff := range backoffSchedule {
resp, err = c.http.Do(req)
// request ok, lets break out of the loop
if err == nil {
break
}
c.Log.Printf("qbit GET failed: retrying attempt %d - %v\n", i, reqUrl)
time.Sleep(backoff)
}
if err != nil {
return nil, errors.Wrap(err, "error making get request: %v", reqUrl)
}
return resp, nil
}
func (c *Client) post(endpoint string, opts map[string]string) (*http.Response, error) {
// add optional parameters that the user wants
form := url.Values{}
if opts != nil {
for k, v := range opts {
form.Add(k, v)
}
}
var err error
var resp *http.Response
reqUrl := buildUrl(c.settings, endpoint)
req, err := http.NewRequest("POST", reqUrl, strings.NewReader(form.Encode()))
if err != nil {
return nil, errors.Wrap(err, "could not build request")
}
if c.settings.BasicAuth {
req.SetBasicAuth(c.settings.Basic.Username, c.settings.Basic.Password)
}
// add the content-type so qbittorrent knows what to expect
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
// try request and if fail run 3 retries
for i, backoff := range backoffSchedule {
resp, err = c.http.Do(req)
// request ok, lets break out of the loop
if err == nil {
break
}
c.Log.Printf("qbit POST failed: retrying attempt %d - %v\n", i, reqUrl)
time.Sleep(backoff)
}
if err != nil {
return nil, errors.Wrap(err, "error making post request: %v", reqUrl)
}
return resp, nil
}
func (c *Client) postBasic(endpoint string, opts map[string]string) (*http.Response, error) {
// add optional parameters that the user wants
form := url.Values{}
if opts != nil {
for k, v := range opts {
form.Add(k, v)
}
}
var err error
var resp *http.Response
reqUrl := buildUrl(c.settings, endpoint)
req, err := http.NewRequest("POST", reqUrl, strings.NewReader(form.Encode()))
if err != nil {
return nil, errors.Wrap(err, "could not build request")
}
if c.settings.BasicAuth {
req.SetBasicAuth(c.settings.Basic.Username, c.settings.Basic.Password)
}
// add the content-type so qbittorrent knows what to expect
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err = c.http.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error making post request: %v", reqUrl)
}
return resp, nil
}
func (c *Client) postFile(endpoint string, fileName string, opts map[string]string) (*http.Response, error) {
var err error
var resp *http.Response
file, err := os.Open(fileName)
if err != nil {
return nil, errors.Wrap(err, "error opening file %v", fileName)
}
// Close the file later
defer file.Close()
// Buffer to store our request body as bytes
var requestBody bytes.Buffer
// Store a multipart writer
multiPartWriter := multipart.NewWriter(&requestBody)
// Initialize file field
fileWriter, err := multiPartWriter.CreateFormFile("torrents", fileName)
if err != nil {
return nil, errors.Wrap(err, "error initializing file field %v", fileName)
}
// Copy the actual file content to the fields writer
_, err = io.Copy(fileWriter, file)
if err != nil {
return nil, errors.Wrap(err, "error copy file contents to writer %v", fileName)
}
// Populate other fields
if opts != nil {
for key, val := range opts {
fieldWriter, err := multiPartWriter.CreateFormField(key)
if err != nil {
return nil, errors.Wrap(err, "error creating form field %v with value %v", key, val)
}
_, err = fieldWriter.Write([]byte(val))
if err != nil {
return nil, errors.Wrap(err, "error writing field %v with value %v", key, val)
}
}
}
// Close multipart writer
multiPartWriter.Close()
reqUrl := buildUrl(c.settings, endpoint)
req, err := http.NewRequest("POST", reqUrl, &requestBody)
if err != nil {
return nil, errors.Wrap(err, "error creating request %v", fileName)
}
if c.settings.BasicAuth {
req.SetBasicAuth(c.settings.Basic.Username, c.settings.Basic.Password)
}
// Set correct content type
req.Header.Set("Content-Type", multiPartWriter.FormDataContentType())
// try request and if fail run 3 retries
for i, backoff := range backoffSchedule {
resp, err = c.http.Do(req)
// request ok, lets break out of the loop
if err == nil {
break
}
c.Log.Printf("qbit POST file failed: retrying attempt %d - %v\n", i, reqUrl)
time.Sleep(backoff)
}
if err != nil {
return nil, errors.Wrap(err, "error making post file request %v", fileName)
}
return resp, nil
}
func (c *Client) setCookies(cookies []*http.Cookie) {
cookieURL, _ := url.Parse(buildUrl(c.settings, ""))
c.http.Jar.SetCookies(cookieURL, cookies)
}
func buildUrl(settings Settings, endpoint string) string {
// parse url
u, _ := url.Parse(settings.Hostname)
// reset Opaque
u.Opaque = ""
// set scheme
scheme := "http"
if u.Scheme == "http" || u.Scheme == "https" {
if settings.TLS {
scheme = "https"
}
u.Scheme = scheme
} else {
if settings.TLS {
scheme = "https"
}
u.Scheme = scheme
}
// if host is empty lets use one from settings
if u.Host == "" {
u.Host = settings.Hostname
}
// reset Path
if u.Host == u.Path {
u.Path = ""
}
// handle ports
if settings.Port > 0 {
if settings.Port == 80 || settings.Port == 443 {
// skip for regular http and https
} else {
u.Host = fmt.Sprintf("%v:%v", u.Host, settings.Port)
}
}
// join path
u.Path = path.Join(u.Path, "/api/v2/", endpoint)
// make into new string and return
return u.String()
}
func buildUrlOpts(settings Settings, endpoint string, opts map[string]string) string {
// parse url
u, _ := url.Parse(settings.Hostname)
// reset Opaque
u.Opaque = ""
// set scheme
scheme := "http"
if u.Scheme == "http" || u.Scheme == "https" {
if settings.TLS {
scheme = "https"
}
u.Scheme = scheme
} else {
if settings.TLS {
scheme = "https"
}
u.Scheme = scheme
}
// if host is empty lets use one from settings
if u.Host == "" {
u.Host = settings.Hostname
}
// reset Path
if u.Host == u.Path {
u.Path = ""
}
// handle ports
if settings.Port > 0 {
if settings.Port == 80 || settings.Port == 443 {
// skip for regular http and https
} else {
u.Host = fmt.Sprintf("%v:%v", u.Host, settings.Port)
}
}
// add query params
q := u.Query()
for k, v := range opts {
q.Set(k, v)
}
u.RawQuery = q.Encode()
// join path
u.Path = path.Join(u.Path, "/api/v2/", endpoint)
// make into new string and return
return u.String()
}