package qbitt import ( "encoding/json" "errors" "fmt" "io" "net/http" "net/http/cookiejar" "net/url" "strings" "git.aetherial.dev/aeth/gluetun-qbitt-sidecar/pkg/shared" ) /* Need to make a request to the qbittorrent API POST /api/v2/app/setPreferences with structure: { "listen_port": 58925 } */ const ( LOGIN_PATH = "/api/v2/auth/login" LOGOUT_PATH = "/api/v2/auth/logout" PREFERENCES_PATH = "/api/v2/app/setPreferences" CONFIG_PATH = "/api/v2/app/preferences" ) type ListenPortRequest struct { ListenPort int `json:"listen_port"` } type Configuration struct { ListenPort int `json:"listen_port"` RandomPort bool `json:"random_port"` Upnp bool `json:"upnp"` Pex bool `json:"pex"` Dht bool `json:"dht"` } func makeListenPortRequest(port int) (string, error) { b, err := json.Marshal(ListenPortRequest{ListenPort: port}) if err != nil { return "", err } return string(b), nil } type QbittorrentClient struct { server shared.Server client *http.Client // plumbing } /* create a new qbit client :param username: username for your qbittorrent Web UI :param password: password for your qbittorrent Web UI :param host: the host/url of the server. should be in format: ://: */ func NewQbittorrentClient(username, password, host string) (QbittorrentClient, error) { ckJar, err := cookiejar.New(nil) client := http.Client{Jar: ckJar} srv, err := shared.ParseServer(host) if err != nil { return QbittorrentClient{}, err } formattedUrl := srv.FormatWith(LOGIN_PATH) data := url.Values{} data.Set("username", username) data.Set("password", password) body := strings.NewReader(data.Encode()) req, err := http.NewRequest(http.MethodPost, formattedUrl, body) if err != nil { return QbittorrentClient{}, err } req.Header.Add("Referer", srv.Format()) req.Header.Add("Origin", srv.Format()) // req.Header.Add("Host", srv.Host) // req.Header.Add("Content-Type", "text/plain") req.Header.Add("Content-Type", "application/x-www-form-urlencoded") resp, err := client.Do(req) if err != nil { return QbittorrentClient{}, err } if resp.StatusCode != 200 { return QbittorrentClient{}, errors.New(fmt.Sprintf("Authentication failed.\n Host: %s\n Status Code: %s\n", srv.Format(), resp.Status)) } client.Jar.SetCookies(resp.Request.URL, resp.Cookies()) return QbittorrentClient{server: srv, client: &client}, nil } // updates the listening port for incoming bittorrent requests to 'port' on your server // :param port: the port to update for incoming connections func (q QbittorrentClient) UpdateIncomingPort(port int) error { body, err := makeListenPortRequest(port) if err != nil { return err } data := url.Values{} data.Set("json", body) req, err := http.NewRequest(http.MethodPost, q.server.FormatWith(PREFERENCES_PATH), strings.NewReader(data.Encode())) req.Header.Add("Origin", q.server.Format()) req.Header.Add("Referer", q.server.Format()) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") resp, err := q.client.Do(req) if err != nil { return err } if resp.StatusCode != 200 { return errors.New(fmt.Sprintf("Update request failed.\n Host: %s\n Status Code: %s\n", q.server.Format(), resp.Status)) } return nil } // gets the running configuration func (q QbittorrentClient) GetRunningConfiguration() (Configuration, error) { var cfg Configuration req, err := http.NewRequest(http.MethodGet, q.server.FormatWith(CONFIG_PATH), nil) if err != nil { return cfg, err } req.Header.Add("Origin", q.server.Format()) req.Header.Add("Referer", q.server.Format()) resp, err := q.client.Do(req) if err != nil { return cfg, err } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return cfg, err } err = json.Unmarshal(b, &cfg) if err != nil { return cfg, err } return cfg, nil } // verify that the listening port was modified func (q QbittorrentClient) VerifyPortChange(port int) (bool, error) { cfg, err := q.GetRunningConfiguration() if err != nil { return false, err } if cfg.ListenPort != port { return false, errors.New(fmt.Sprintf("Listening port on server: '%v' did not match what was supplied: '%v'\n", cfg.ListenPort, port)) } return true, nil }