| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- package qbitt
- import (
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "log"
- "net/http"
- "net/http/cookiejar"
- "net/url"
- "strconv"
- "strings"
- )
- /*
- 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 server struct {
- proto string // http or https
- host string // just the domain name, like 'qbit.local.lan' or whatever
- port int // port that the web ui listens on
- }
- func (s server) format() string {
- return fmt.Sprintf("%s://%s:%v", s.proto, s.host, s.port)
- }
- func (s server) formatWith(path string) string {
- return fmt.Sprintf("%s%s", s.format(), path)
- }
- type QbittorrentClient struct {
- server server
- client *http.Client // plumbing
- }
- // takes string s and returns the host and port
- func splitServerPort(s string) (string, int) {
- splitPort := strings.Split(s, ":")
- if len(splitPort) < 1 {
- // add log here
- log.Fatal("unhandled for now")
- }
- host := splitPort[0]
- port, err := strconv.Atoi(splitPort[1])
- if err != nil {
- log.Fatal("couldnt parse port. unhandled for now.")
- }
- return host, port
- }
- // strips everything but the port number and hostname from a string
- func parseServer(s string) (server, error) {
- var proto, host string
- var port int
- if strings.Contains(s, "https") {
- proto = "https"
- } else {
- proto = "http"
- }
- protoStripped := strings.ReplaceAll(
- strings.ReplaceAll(
- strings.ReplaceAll(
- s, "https", "",
- ), "http", "",
- ), "://", "",
- )
- splitHost := strings.Split(protoStripped, "/")
- host, port = splitServerPort(splitHost[0])
- return server{proto: proto, host: host, port: port}, nil
- }
- /*
- 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: <protocol>://<host>:<port>
- */
- func NewQbittorrentClient(username, password, host string) (QbittorrentClient, error) {
- ckJar, err := cookiejar.New(nil)
- client := http.Client{Jar: ckJar}
- srv, err := 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
- }
|