client.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. package qbitt
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "log"
  8. "net/http"
  9. "net/http/cookiejar"
  10. "net/url"
  11. "strconv"
  12. "strings"
  13. )
  14. /*
  15. Need to make a request to the qbittorrent API
  16. POST /api/v2/app/setPreferences
  17. with structure:
  18. {
  19. "listen_port": 58925
  20. }
  21. */
  22. const (
  23. LOGIN_PATH = "/api/v2/auth/login"
  24. LOGOUT_PATH = "/api/v2/auth/logout"
  25. PREFERENCES_PATH = "/api/v2/app/setPreferences"
  26. CONFIG_PATH = "/api/v2/app/preferences"
  27. )
  28. type ListenPortRequest struct {
  29. ListenPort int `json:"listen_port"`
  30. }
  31. type Configuration struct {
  32. ListenPort int `json:"listen_port"`
  33. RandomPort bool `json:"random_port"`
  34. Upnp bool `json:"upnp"`
  35. Pex bool `json:"pex"`
  36. Dht bool `json:"dht"`
  37. }
  38. func makeListenPortRequest(port int) (string, error) {
  39. b, err := json.Marshal(ListenPortRequest{ListenPort: port})
  40. if err != nil {
  41. return "", err
  42. }
  43. return string(b), nil
  44. }
  45. type server struct {
  46. proto string // http or https
  47. host string // just the domain name, like 'qbit.local.lan' or whatever
  48. port int // port that the web ui listens on
  49. }
  50. func (s server) format() string {
  51. return fmt.Sprintf("%s://%s:%v", s.proto, s.host, s.port)
  52. }
  53. func (s server) formatWith(path string) string {
  54. return fmt.Sprintf("%s%s", s.format(), path)
  55. }
  56. type QbittorrentClient struct {
  57. server server
  58. client *http.Client // plumbing
  59. }
  60. // takes string s and returns the host and port
  61. func splitServerPort(s string) (string, int) {
  62. splitPort := strings.Split(s, ":")
  63. if len(splitPort) < 1 {
  64. // add log here
  65. log.Fatal("unhandled for now")
  66. }
  67. host := splitPort[0]
  68. port, err := strconv.Atoi(splitPort[1])
  69. if err != nil {
  70. log.Fatal("couldnt parse port. unhandled for now.")
  71. }
  72. return host, port
  73. }
  74. // strips everything but the port number and hostname from a string
  75. func parseServer(s string) (server, error) {
  76. var proto, host string
  77. var port int
  78. if strings.Contains(s, "https") {
  79. proto = "https"
  80. } else {
  81. proto = "http"
  82. }
  83. protoStripped := strings.ReplaceAll(
  84. strings.ReplaceAll(
  85. strings.ReplaceAll(
  86. s, "https", "",
  87. ), "http", "",
  88. ), "://", "",
  89. )
  90. splitHost := strings.Split(protoStripped, "/")
  91. host, port = splitServerPort(splitHost[0])
  92. return server{proto: proto, host: host, port: port}, nil
  93. }
  94. /*
  95. create a new qbit client
  96. :param username: username for your qbittorrent Web UI
  97. :param password: password for your qbittorrent Web UI
  98. :param host: the host/url of the server. should be in format: <protocol>://<host>:<port>
  99. */
  100. func NewQbittorrentClient(username, password, host string) (QbittorrentClient, error) {
  101. ckJar, err := cookiejar.New(nil)
  102. client := http.Client{Jar: ckJar}
  103. srv, err := parseServer(host)
  104. if err != nil {
  105. return QbittorrentClient{}, err
  106. }
  107. formattedUrl := srv.formatWith(LOGIN_PATH)
  108. data := url.Values{}
  109. data.Set("username", username)
  110. data.Set("password", password)
  111. body := strings.NewReader(data.Encode())
  112. req, err := http.NewRequest(http.MethodPost, formattedUrl, body)
  113. if err != nil {
  114. return QbittorrentClient{}, err
  115. }
  116. req.Header.Add("Referer", srv.format())
  117. req.Header.Add("Origin", srv.format())
  118. req.Header.Add("Host", srv.host)
  119. // req.Header.Add("Content-Type", "text/plain")
  120. req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
  121. resp, err := client.Do(req)
  122. if err != nil {
  123. return QbittorrentClient{}, err
  124. }
  125. if resp.StatusCode != 200 {
  126. return QbittorrentClient{}, errors.New(fmt.Sprintf("Authentication failed.\n Host: %s\n Status Code: %s\n", srv.format(), resp.Status))
  127. }
  128. client.Jar.SetCookies(resp.Request.URL, resp.Cookies())
  129. return QbittorrentClient{server: srv, client: &client}, nil
  130. }
  131. // updates the listening port for incoming bittorrent requests to 'port' on your server
  132. // :param port: the port to update for incoming connections
  133. func (q QbittorrentClient) UpdateIncomingPort(port int) error {
  134. body, err := makeListenPortRequest(port)
  135. if err != nil {
  136. return err
  137. }
  138. data := url.Values{}
  139. data.Set("json", body)
  140. req, err := http.NewRequest(http.MethodPost, q.server.formatWith(PREFERENCES_PATH), strings.NewReader(data.Encode()))
  141. req.Header.Add("Origin", q.server.format())
  142. req.Header.Add("Referer", q.server.format())
  143. req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
  144. resp, err := q.client.Do(req)
  145. if err != nil {
  146. return err
  147. }
  148. if resp.StatusCode != 200 {
  149. return errors.New(fmt.Sprintf("Update request failed.\n Host: %s\n Status Code: %s\n", q.server.format(), resp.Status))
  150. }
  151. return nil
  152. }
  153. // gets the running configuration
  154. func (q QbittorrentClient) GetRunningConfiguration() (Configuration, error) {
  155. var cfg Configuration
  156. req, err := http.NewRequest(http.MethodGet, q.server.formatWith(CONFIG_PATH), nil)
  157. if err != nil {
  158. return cfg, err
  159. }
  160. req.Header.Add("Origin", q.server.format())
  161. req.Header.Add("Referer", q.server.format())
  162. resp, err := q.client.Do(req)
  163. if err != nil {
  164. return cfg, err
  165. }
  166. defer resp.Body.Close()
  167. b, err := io.ReadAll(resp.Body)
  168. if err != nil {
  169. return cfg, err
  170. }
  171. err = json.Unmarshal(b, &cfg)
  172. if err != nil {
  173. return cfg, err
  174. }
  175. return cfg, nil
  176. }
  177. // verify that the listening port was modified
  178. func (q QbittorrentClient) VerifyPortChange(port int) (bool, error) {
  179. cfg, err := q.GetRunningConfiguration()
  180. if err != nil {
  181. return false, err
  182. }
  183. if cfg.ListenPort != port {
  184. return false, errors.New(fmt.Sprintf("Listening port on server: '%v' did not match what was supplied: '%v'\n", cfg.ListenPort, port))
  185. }
  186. return true, nil
  187. }