mirror of
https://github.com/idanoo/autobrr
synced 2025-07-22 16:29:12 +00:00
feat(qbit): refactor url parse and add basic auth support (#245)
* feat(qbit): add basic auth and refactor url parse * build: update dockerfile go base * feat: only show port for legacy reasons
This commit is contained in:
parent
cf326a6c10
commit
62ada6de37
7 changed files with 301 additions and 8 deletions
|
@ -7,7 +7,7 @@ COPY web .
|
|||
RUN yarn build
|
||||
|
||||
# build app
|
||||
FROM golang:1.17.6-alpine AS app-builder
|
||||
FROM golang:1.17.9-alpine AS app-builder
|
||||
|
||||
ARG VERSION=dev
|
||||
ARG REVISION=dev
|
||||
|
|
|
@ -111,6 +111,13 @@ func (s *service) qbittorrentCheckRulesCanDownload(action domain.Action) (bool,
|
|||
TLSSkipVerify: client.TLSSkipVerify,
|
||||
}
|
||||
|
||||
// only set basic auth if enabled
|
||||
if client.Settings.Basic.Auth {
|
||||
qbtSettings.BasicAuth = client.Settings.Basic.Auth
|
||||
qbtSettings.Basic.Username = client.Settings.Basic.Username
|
||||
qbtSettings.Basic.Password = client.Settings.Basic.Password
|
||||
}
|
||||
|
||||
qbt := qbittorrent.NewClient(qbtSettings)
|
||||
qbt.Name = client.Name
|
||||
// save cookies?
|
||||
|
|
|
@ -50,6 +50,13 @@ func (s *service) testQbittorrentConnection(client domain.DownloadClient) error
|
|||
TLSSkipVerify: client.TLSSkipVerify,
|
||||
}
|
||||
|
||||
// only set basic auth if enabled
|
||||
if client.Settings.Basic.Auth {
|
||||
qbtSettings.BasicAuth = client.Settings.Basic.Auth
|
||||
qbtSettings.Basic.Username = client.Settings.Basic.Username
|
||||
qbtSettings.Basic.Password = client.Settings.Basic.Password
|
||||
}
|
||||
|
||||
qbt := qbittorrent.NewClient(qbtSettings)
|
||||
err := qbt.Login()
|
||||
if err != nil {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -40,6 +41,13 @@ type Settings struct {
|
|||
TLS bool
|
||||
TLSSkipVerify bool
|
||||
protocol string
|
||||
BasicAuth bool
|
||||
Basic Basic
|
||||
}
|
||||
|
||||
type Basic struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func NewClient(s Settings) *Client {
|
||||
|
@ -77,17 +85,21 @@ func NewClient(s Settings) *Client {
|
|||
}
|
||||
|
||||
func (c *Client) get(endpoint string, opts map[string]string) (*http.Response, error) {
|
||||
reqUrl := fmt.Sprintf("%v://%v:%v/api/v2/%v", c.settings.protocol, c.settings.Hostname, c.settings.Port, endpoint)
|
||||
|
||||
var err error
|
||||
var resp *http.Response
|
||||
|
||||
reqUrl := buildUrl(c.settings, endpoint)
|
||||
|
||||
req, err := http.NewRequest("GET", reqUrl, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("GET: error %v", reqUrl)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -122,13 +134,18 @@ func (c *Client) post(endpoint string, opts map[string]string) (*http.Response,
|
|||
var err error
|
||||
var resp *http.Response
|
||||
|
||||
reqUrl := fmt.Sprintf("%v://%v:%v/api/v2/%v", c.settings.protocol, c.settings.Hostname, c.settings.Port, endpoint)
|
||||
reqUrl := buildUrl(c.settings, endpoint)
|
||||
|
||||
req, err := http.NewRequest("POST", reqUrl, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("POST: req %v", reqUrl)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
|
@ -154,6 +171,42 @@ func (c *Client) post(endpoint string, opts map[string]string) (*http.Response,
|
|||
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 {
|
||||
log.Error().Err(err).Msgf("POST: req %v", reqUrl)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Error().Err(err).Msgf("POST: do %v", reqUrl)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -206,13 +259,17 @@ func (c *Client) postFile(endpoint string, fileName string, opts map[string]stri
|
|||
// Close multipart writer
|
||||
multiPartWriter.Close()
|
||||
|
||||
reqUrl := fmt.Sprintf("%v://%v:%v/api/v2/%v", c.settings.protocol, c.settings.Hostname, c.settings.Port, endpoint)
|
||||
reqUrl := buildUrl(c.settings, endpoint)
|
||||
req, err := http.NewRequest("POST", reqUrl, &requestBody)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("POST file: could not create request object %v", fileName)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
|
@ -242,3 +299,50 @@ func (c *Client) setCookies(cookies []*http.Cookie) {
|
|||
cookieURL, _ := url.Parse(fmt.Sprintf("%v://%v:%v", c.settings.protocol, c.settings.Hostname, c.settings.Port))
|
||||
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()
|
||||
}
|
||||
|
|
135
pkg/qbittorrent/client_test.go
Normal file
135
pkg/qbittorrent/client_test.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package qbittorrent
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_buildUrl(t *testing.T) {
|
||||
type args struct {
|
||||
settings Settings
|
||||
endpoint string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "build_url_1",
|
||||
args: args{
|
||||
settings: Settings{
|
||||
Hostname: "https://qbit.domain.ltd",
|
||||
Port: 0,
|
||||
Username: "",
|
||||
Password: "",
|
||||
TLS: true,
|
||||
TLSSkipVerify: false,
|
||||
protocol: "",
|
||||
},
|
||||
endpoint: "auth/login",
|
||||
},
|
||||
want: "https://qbit.domain.ltd/api/v2/auth/login",
|
||||
},
|
||||
{
|
||||
name: "build_url_2",
|
||||
args: args{
|
||||
settings: Settings{
|
||||
Hostname: "http://qbit.domain.ltd",
|
||||
Port: 0,
|
||||
Username: "",
|
||||
Password: "",
|
||||
TLS: false,
|
||||
TLSSkipVerify: false,
|
||||
protocol: "",
|
||||
},
|
||||
endpoint: "/auth/login",
|
||||
},
|
||||
want: "http://qbit.domain.ltd/api/v2/auth/login",
|
||||
},
|
||||
{
|
||||
name: "build_url_3",
|
||||
args: args{
|
||||
settings: Settings{
|
||||
Hostname: "https://qbit.domain.ltd:8080",
|
||||
Port: 0,
|
||||
Username: "",
|
||||
Password: "",
|
||||
TLS: true,
|
||||
TLSSkipVerify: false,
|
||||
protocol: "",
|
||||
},
|
||||
endpoint: "/auth/login",
|
||||
},
|
||||
want: "https://qbit.domain.ltd:8080/api/v2/auth/login",
|
||||
},
|
||||
{
|
||||
name: "build_url_4",
|
||||
args: args{
|
||||
settings: Settings{
|
||||
Hostname: "qbit.domain.ltd:8080",
|
||||
Port: 0,
|
||||
Username: "",
|
||||
Password: "",
|
||||
TLS: false,
|
||||
TLSSkipVerify: false,
|
||||
protocol: "",
|
||||
},
|
||||
endpoint: "/auth/login",
|
||||
},
|
||||
want: "http://qbit.domain.ltd:8080/api/v2/auth/login",
|
||||
},
|
||||
{
|
||||
name: "build_url_5",
|
||||
args: args{
|
||||
settings: Settings{
|
||||
Hostname: "qbit.domain.ltd",
|
||||
Port: 8080,
|
||||
Username: "",
|
||||
Password: "",
|
||||
TLS: false,
|
||||
TLSSkipVerify: false,
|
||||
protocol: "",
|
||||
},
|
||||
endpoint: "/auth/login",
|
||||
},
|
||||
want: "http://qbit.domain.ltd:8080/api/v2/auth/login",
|
||||
},
|
||||
{
|
||||
name: "build_url_6",
|
||||
args: args{
|
||||
settings: Settings{
|
||||
Hostname: "qbit.domain.ltd",
|
||||
Port: 443,
|
||||
Username: "",
|
||||
Password: "",
|
||||
TLS: true,
|
||||
TLSSkipVerify: false,
|
||||
protocol: "",
|
||||
},
|
||||
endpoint: "/auth/login",
|
||||
},
|
||||
want: "https://qbit.domain.ltd/api/v2/auth/login",
|
||||
},
|
||||
{
|
||||
name: "build_url_6",
|
||||
args: args{
|
||||
settings: Settings{
|
||||
Hostname: "qbit.domain.ltd",
|
||||
Port: 10200,
|
||||
Username: "",
|
||||
Password: "",
|
||||
TLS: false,
|
||||
TLSSkipVerify: false,
|
||||
protocol: "",
|
||||
},
|
||||
endpoint: "/auth/login",
|
||||
},
|
||||
want: "http://qbit.domain.ltd:10200/api/v2/auth/login",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := buildUrl(tt.args.settings, tt.args.endpoint); got != tt.want {
|
||||
t.Errorf("buildUrl() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ func (c *Client) Login() error {
|
|||
credentials["username"] = c.settings.Username
|
||||
credentials["password"] = c.settings.Password
|
||||
|
||||
resp, err := c.post("auth/login", credentials)
|
||||
resp, err := c.postBasic("auth/login", credentials)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("login error")
|
||||
return err
|
||||
|
|
|
@ -97,6 +97,46 @@ function FormFieldsArr() {
|
|||
);
|
||||
}
|
||||
|
||||
function FormFieldsQbit() {
|
||||
const {
|
||||
values: { port, tls, settings }
|
||||
} = useFormikContext<InitialValues>();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TextFieldWide name="host" label="Host" help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd:port" />
|
||||
|
||||
{port > 0 && (
|
||||
<NumberFieldWide name="port" label="Port" help="WebUI port for qBittorrent" />
|
||||
)}
|
||||
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
|
||||
<SwitchGroupWide name="tls" label="TLS" />
|
||||
|
||||
{tls && (
|
||||
<Fragment>
|
||||
<SwitchGroupWide name="tls_skip_verify" label="Skip TLS verification (insecure)" />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TextFieldWide name="username" label="Username" />
|
||||
<PasswordFieldWide name="password" label="Password" />
|
||||
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||
<SwitchGroupWide name="settings.basic.auth" label="Basic auth" />
|
||||
</div>
|
||||
|
||||
{settings.basic?.auth === true && (
|
||||
<Fragment>
|
||||
<TextFieldWide name="settings.basic.username" label="Username" />
|
||||
<PasswordFieldWide name="settings.basic.password" label="Password" />
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export interface componentMapType {
|
||||
[key: string]: React.ReactElement;
|
||||
}
|
||||
|
@ -104,7 +144,7 @@ export interface componentMapType {
|
|||
export const componentMap: componentMapType = {
|
||||
DELUGE_V1: <FormFieldsDefault/>,
|
||||
DELUGE_V2: <FormFieldsDefault/>,
|
||||
QBITTORRENT: <FormFieldsDefault/>,
|
||||
QBITTORRENT: <FormFieldsQbit/>,
|
||||
RADARR: <FormFieldsArr/>,
|
||||
SONARR: <FormFieldsArr/>,
|
||||
LIDARR: <FormFieldsArr/>,
|
||||
|
@ -356,7 +396,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
|
|||
type: "QBITTORRENT",
|
||||
enabled: true,
|
||||
host: "",
|
||||
port: 10000,
|
||||
port: 0,
|
||||
tls: false,
|
||||
tls_skip_verify: false,
|
||||
username: "",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue