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: ://: */ 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 }