more work, gutting.

This commit is contained in:
William Floyd 2025-02-05 01:16:21 +05:30
parent 19fdaf1710
commit 95c057a9a0
Signed by untrusted user who does not match committer: william
GPG key ID: B3EEEDD81893CAF9
11 changed files with 557 additions and 709 deletions

View file

@ -1,21 +1,18 @@
package main
import (
"github.com/W-Floyd/qbittorrent-docker-multiplexer/docker"
"github.com/W-Floyd/qbittorrent-docker-multiplexer/multiplexer"
"github.com/W-Floyd/qbittorrent-docker-multiplexer/qbittorrent"
)
type Config struct {
Multiplexer multiplexer.Config
QBittorrent qbittorrent.Config
Docker docker.Config
QBittorrent qbittorrent.Configs
}
func (c Config) Validate() (errs []error) {
errs = append(errs, c.Multiplexer.Validate()...)
errs = append(errs, c.QBittorrent.Validate()...)
errs = append(errs, c.Docker.Validate()...)
return errs
}

View file

@ -2,25 +2,25 @@ name: ${COMPOSE_PROJECT_NAME}
include:
- vpn-docker-compose.yaml
- qbittorrent-docker-compose.yaml
services:
qbittorrent-docker-multiplexer:
container_name: qbittorrent-docker-multiplexer
build: .
network_mode: "container:${VPN_SERVICE_NAME}"
environment:
COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
QBITTORRENT_SECRETSEED: ${QBITTORRENT_SECRETSEED}
MULTIPLEXER_SECRET: ${MULTIPLEXER_SECRET}
MULTIPLEXER_PORT: ${MULTIPLEXER_PORT}
volumes:
# This allows spawning qBittorrent containers on demand
- /var/run/docker.sock:/var/run/docker.sock:rw
# Template files
- ./qbittorrent.conf.tmpl:/config/qbittorrent.conf.tmpl
- ./docker-compose.yaml.tmpl:/config/docker-compose.yaml.tmpl
# State files (so we re-spawn containers as needed)
- ./data/:/data/
develop:
watch:
- action: rebuild
path: ./
# services:
# qbittorrent-docker-multiplexer:
# container_name: qbittorrent-docker-multiplexer
# build: .
# network_mode: "container:${VPN_SERVICE_NAME}"
# environment:
# COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
# QBITTORRENT_SECRETSEED: ${QBITTORRENT_SECRETSEED}
# MULTIPLEXER_SECRET: ${MULTIPLEXER_SECRET}
# MULTIPLEXER_PORT: ${MULTIPLEXER_PORT}
# volumes:
# # This allows spawning qBittorrent containers on demand
# - /var/run/docker.sock:/var/run/docker.sock:rw
# # Template files
# - ./qbittorrent.conf.tmpl:/config/qbittorrent.conf.tmpl
# - ./docker-compose.yaml.tmpl:/config/docker-compose.yaml.tmpl
# # State files (so we re-spawn containers as needed)
# - ./data/:/data/
# develop:
# watch:
# - action: rebuild
# path: ./

View file

@ -1 +0,0 @@

View file

@ -1,32 +0,0 @@
package docker
import (
"errors"
"os"
"text/template"
)
type Config struct {
ProjectName string `default:"qbittorrent-docker-multiplexer" usage:"Docker project name"`
DockerCompose struct {
Qbittorrent string `default:"/config/docker-compose.yaml.tmpl" usage:"Docker Compose entry GoTemplate file for qBittorrent"`
}
}
func (c Config) Validate() (errs []error) {
if c.ProjectName == "" {
errs = append(errs, errors.New("(Docker) Empty Project Name key"))
}
if c.DockerCompose.Qbittorrent == "" {
errs = append(errs, errors.New("(Docker) Empty Docker Compose entry Template File key"))
} else if _, err := os.Stat(c.DockerCompose.Qbittorrent); errors.Is(err, os.ErrNotExist) {
errs = append(errs, errors.New("(Docker) Docker Compose entry Template File ("+c.DockerCompose.Qbittorrent+") does not exist"))
} else if _, err := template.ParseFiles(c.DockerCompose.Qbittorrent); err != nil {
errs = append(errs, errors.New("(Docker) Docker Compose entry Template File ("+c.DockerCompose.Qbittorrent+") count not be parsed: "), err)
}
return errs
}

6
go.mod
View file

@ -3,21 +3,21 @@ module github.com/W-Floyd/qbittorrent-docker-multiplexer
go 1.23.4
require (
github.com/Jeffail/gabs/v2 v2.7.0
github.com/gorilla/mux v1.8.1
github.com/leosunmo/zapchi v0.2.0
github.com/motemen/go-loghttp v0.0.0-20231107055348-29ae44b293f4
github.com/motemen/go-nuts v0.0.0-20220604134737-2658d0104f31
github.com/omeid/uconfig v0.7.0
go.uber.org/zap v1.27.0
golang.org/x/sync v0.10.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/text v0.22.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

21
go.sum
View file

@ -33,8 +33,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Jeffail/gabs/v2 v2.7.0 h1:Y2edYaTcE8ZpRsR2AtmPu5xQdFDIthFG0jYhu5PY8kg=
github.com/Jeffail/gabs/v2 v2.7.0/go.mod h1:dp5ocw1FvBBQYssgHsG7I1WYsiLRtkUaB1FEtSwvNUw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@ -49,6 +47,9 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -119,6 +120,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leosunmo/zapchi v0.2.0 h1:BSX9FIcPbgVBgMgVBAfN0CrLWv012tjFcqSTfDDUYyY=
github.com/leosunmo/zapchi v0.2.0/go.mod h1:SWyRMhaEvYcIG1Y/6rJ3nGW7Vlz71HnNtCQlRxm5ZcA=
github.com/motemen/go-loghttp v0.0.0-20231107055348-29ae44b293f4 h1:WLWwzjax2/L5NAQul9bdk1EAP0+YGnAzJBJ/LzL8Dgs=
github.com/motemen/go-loghttp v0.0.0-20231107055348-29ae44b293f4/go.mod h1:ykaRC7b5xKciHTUFZ60bbsOojQAkCmmehBNbBWeIz1Y=
github.com/motemen/go-nuts v0.0.0-20220604134737-2658d0104f31 h1:lQ+0Zt2gm+w5+9iaBWKdJXC/gMrWjHhNbw9ts/9rSZ4=
@ -126,6 +129,7 @@ github.com/motemen/go-nuts v0.0.0-20220604134737-2658d0104f31/go.mod h1:vkBO+XDN
github.com/omeid/uconfig v0.7.0 h1:aoNKjAANazU0LbV5EqvzUlqVW8n4ML7W0qe2UUhwtYQ=
github.com/omeid/uconfig v0.7.0/go.mod h1:aZqGvjN8OcabCs19rqF1CnILm9+jWohUNF/MABA29JM=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -135,6 +139,7 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
@ -147,10 +152,14 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -230,8 +239,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -266,8 +273,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -286,6 +293,8 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

View file

@ -1,418 +1,359 @@
package main
import (
"bytes"
"context"
"errors"
"io"
"log"
"net/http"
"net/url"
"slices"
"strings"
"github.com/W-Floyd/qbittorrent-docker-multiplexer/qbittorrent"
"github.com/W-Floyd/qbittorrent-docker-multiplexer/state"
"golang.org/x/sync/errgroup"
"github.com/Jeffail/gabs/v2"
)
func (c *Config) HandleAll(w http.ResponseWriter, r *http.Request) {
// if r.URL.Path == "/api/v2/torrents/info" {
// c.HandlerFetchMergeArray(w, r)
// } else if slices.Contains([]string{
// "/api/v2/sync/maindata",
// "/api/v2/torrents/categories",
// }, r.URL.Path) {
// c.HandlerFetchMerge(w, r)
// } else if r.Form.Has("hash") || r.Form.Has("hashes") {
// c.HandlerHashFinder(w, r)
// } else {
if false {
} else {
c.HandlerPassthrough(w, r)
}
}
func (c *Config) HandlerPassthrough(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.RequestURI(), "/api/") {
log.Println("Warning - Passthrough on API call: " + r.URL.RequestURI())
}
i := state.AppState.BalancerCount
resp, err := c.MakeRequest(r, &i)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
c.MakeResponse(resp, w)
state.AppState.BalancerCount += 1
state.AppState.BalancerCount %= state.AppState.NumberOfClients
i := qbittorrent.NextRoundRobin()
resp, err := i.GetResponse(r)
c.MakeResponse(err, resp, w)
}
func (c *Config) MakeRequest(r *http.Request, i *uint) (*http.Response, error) {
// func (c *Config) MakeRequest(r *http.Request, i *uint) (*http.Response, error) {
body, err := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(body))
// body, err := io.ReadAll(r.Body)
// r.Body = io.NopCloser(bytes.NewBuffer(body))
proxy, err := qbittorrent.GetProxy(&c.QBittorrent, i)
// proxy, err := qbittorrent.GetProxy(&c.QBittorrent, i)
// if err != nil {
// return nil, err
// }
// req, err := http.NewRequest(
// r.Method,
// qbittorrent.URL(&c.QBittorrent, i, r.URL).String(),
// bytes.NewBuffer(body),
// )
// if err != nil {
// return nil, err
// }
// req.Header = r.Header.Clone()
// err = PrepareRequest(req, c, i)
// if err != nil {
// return nil, err
// }
// oldDirector := proxy.Director
// proxy.Director = func(r *http.Request) {
// rewriteRequestURL(req, qbittorrent.URL(&c.QBittorrent, i, r.URL))
// r.Header.Del("Referer")
// }
// // if strings.HasPrefix(r.URL.RequestURI(), "/api/v2/torrents/add") {
// // log.Println(req)
// // }
// resp, err := proxy.Transport.RoundTrip(req)
// proxy.Director = oldDirector
// return resp, err
// }
func (c Config) MakeResponse(err error, resp *http.Response, w http.ResponseWriter) {
if err != nil {
return nil, err
}
req, err := http.NewRequest(
r.Method,
qbittorrent.URL(&c.QBittorrent, i, r.URL).String(),
bytes.NewBuffer(body),
)
if err != nil {
return nil, err
}
req.Header = r.Header.Clone()
err = PrepareRequest(req, c, i)
if err != nil {
return nil, err
}
oldDirector := proxy.Director
proxy.Director = func(r *http.Request) {
rewriteRequestURL(req, qbittorrent.URL(&c.QBittorrent, i, r.URL))
r.Header.Del("Referer")
}
// if strings.HasPrefix(r.URL.RequestURI(), "/api/v2/torrents/add") {
// log.Println(req)
// }
resp, err := proxy.Transport.RoundTrip(req)
proxy.Director = oldDirector
return resp, err
}
func (c Config) MakeResponse(resp *http.Response, w http.ResponseWriter) {
for header := range resp.Header {
w.Header().Add(header, resp.Header.Get(header))
}
io.Copy(w, resp.Body)
}
func (c *Config) HandlerHashFinder(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(body))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
hashes := []string{}
key := "hash"
if r.Form.Has("hash") {
hashes = append(hashes, r.Form.Get("hash"))
}
if r.Form.Has("hashes") {
key = "hashes"
hashes = append(hashes, strings.Split(r.Form.Get("hashes"), "|")...)
}
state.AppState.Locks.Torrents.Lock()
defer state.AppState.Locks.Torrents.Unlock()
resps := []*http.Response{}
for _, hash := range hashes {
form, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
form.Del("hash")
form.Del("hashes")
form.Set(key, hash)
r.URL.RawQuery = form.Encode()
instance, ok := state.AppState.Torrents[hash]
if ok {
resp, err := c.MakeRequest(r, &instance)
if err != nil || resp.StatusCode != http.StatusOK {
ok = false
} else {
resps = append(resps, resp)
}
r.Form.Del("")
} else if !ok {
log.Println("hash not known, looking up: " + hash)
for i := uint(0); i < state.AppState.NumberOfClients; i++ {
resp, err := c.MakeRequest(r, &i)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
} else if resp.StatusCode == http.StatusOK {
resps = append(resps, resp)
state.AppState.Torrents[hash] = i
}
}
}
}
if len(resps) == 1 {
c.MakeResponse(resps[0], w)
} else if len(resps) > 0 {
baseResp := resps[0]
baseBody := ""
for _, resp := range resps {
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
baseBody += string(body)
}
baseResp.Body = io.NopCloser(bytes.NewBuffer([]byte(baseBody)))
c.MakeResponse(baseResp, w)
} else {
http.Error(w, "no responses", http.StatusInternalServerError)
}
}
func (c *Config) Parallel(r *http.Request) (output []*struct {
response *gabs.Container
instance uint
}, err error) {
g, ctx := errgroup.WithContext(context.Background())
responses := make(chan struct {
response *http.Response
instance uint
})
body, err := io.ReadAll(r.Body)
if err != nil {
log.Println(err)
return
}
for i := uint(0); i < state.AppState.NumberOfClients; i++ {
g.Go(func() (err error) {
req, err := http.NewRequest(
r.Method,
qbittorrent.URL(&c.QBittorrent, &i, r.URL).String(),
bytes.NewBuffer(body),
)
if err != nil {
return err
}
resp, err := c.MakeRequest(req, &i)
if err != nil {
return err
} else if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return errors.New("Error" + string(body))
}
select {
case responses <- struct {
response *http.Response
instance uint
}{
response: resp,
instance: i,
}:
return nil
case <-ctx.Done():
return ctx.Err()
}
})
}
go func() {
g.Wait()
close(responses)
}()
for resp := range responses {
cont, err := gabs.ParseJSONBuffer(resp.response.Body)
if err != nil {
return nil, err
for header := range resp.Header {
w.Header().Add(header, resp.Header.Get(header))
}
output = append(output, &struct {
response *gabs.Container
instance uint
}{
response: cont,
instance: resp.instance,
})
}
err = g.Wait()
if err != nil {
return
}
return output, nil
}
func (c *Config) HandlerFetchMergeArray(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v2/torrents/info" {
state.AppState.Locks.Torrents.Lock()
defer state.AppState.Locks.Torrents.Unlock()
state.AppState.Torrents = map[string]uint{}
}
resps, err := c.Parallel(r)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
output := []*gabs.Container{}
for _, resp := range resps {
for _, child := range resp.response.Children() {
output = append(output, child)
if r.URL.Path == "/api/v2/torrents/info" {
hash := child.Path("hash").Data().(string)
state.AppState.Torrents[hash] = resp.instance
log.Println("Stored hash", hash)
}
}
}
if r.URL.Path == "/api/v2/torrents/info" {
slices.SortStableFunc(output, func(a *gabs.Container, b *gabs.Container) int {
return strings.Compare(a.Path("added_on").String(), b.Path("added_on").String())
})
}
w.Write(gabs.Wrap(output).BytesIndent("", " "))
}
func (c *Config) HandlerFetchMerge(w http.ResponseWriter, r *http.Request) {
resps, err := c.Parallel(r)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
output := &gabs.Container{}
for _, resp := range resps {
output.MergeFn(resp.response, func(dest, source interface{}) interface{} {
destArr, destIsArray := dest.([]interface{})
sourceArr, sourceIsArray := source.([]interface{})
if destIsArray {
if sourceIsArray {
return append(destArr, sourceArr...)
}
return append(destArr, source)
}
if sourceIsArray {
return append(append([]interface{}{}, dest), sourceArr...)
}
return source
})
}
w.Write(gabs.Wrap(output).BytesIndent("", " "))
}
// Prepepares and rewrites headers required to auth with qBittorrent
func PrepareRequest(r *http.Request, c *Config, i *uint) (err error) {
if r == nil {
return errors.New("empty request given")
}
if c == nil {
return errors.New("empty config given")
}
if i == nil {
return errors.New("empty instance given")
}
url := qbittorrent.URL(&c.QBittorrent, i, r.URL)
r.Host = url.String()
r.Header.Del("Referer")
r.Header.Del("Origin")
r.Header.Del("Accept-Encoding")
r.Header.Set("Cookie", state.AppState.Cookies[*i].Raw)
return
}
func rewriteRequestURL(req *http.Request, target *url.URL) {
targetQuery := target.RawQuery
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path, req.URL.RawPath = joinURLPath(target, req.URL)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
io.Copy(w, resp.Body)
}
}
func joinURLPath(a, b *url.URL) (path, rawpath string) {
if a.RawPath == "" && b.RawPath == "" {
return singleJoiningSlash(a.Path, b.Path), ""
}
// Same as singleJoiningSlash, but uses EscapedPath to determine
// whether a slash should be added
apath := a.EscapedPath()
bpath := b.EscapedPath()
// func (c *Config) HandlerHashFinder(w http.ResponseWriter, r *http.Request) {
aslash := strings.HasSuffix(apath, "/")
bslash := strings.HasPrefix(bpath, "/")
// body, err := io.ReadAll(r.Body)
// r.Body = io.NopCloser(bytes.NewBuffer(body))
// if helperError(err, w) {
// return
// }
switch {
case aslash && bslash:
return a.Path + b.Path[1:], apath + bpath[1:]
case !aslash && !bslash:
return a.Path + "/" + b.Path, apath + "/" + bpath
}
return a.Path + b.Path, apath + bpath
}
// hashes := []string{}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
// key := "hash"
// if r.Form.Has("hash") {
// hashes = append(hashes, r.Form.Get("hash"))
// }
// if r.Form.Has("hashes") {
// key = "hashes"
// hashes = append(hashes, strings.Split(r.Form.Get("hashes"), "|")...)
// }
// state.AppState.Locks.Torrents.Lock()
// defer state.AppState.Locks.Torrents.Unlock()
// resps := []*http.Response{}
// for _, hash := range hashes {
// form, err := url.ParseQuery(r.URL.RawQuery)
// if helperError(err, w) {
// return
// }
// form.Del("hash")
// form.Del("hashes")
// form.Set(key, hash)
// r.URL.RawQuery = form.Encode()
// instance, ok := state.AppState.Torrents[hash]
// if ok {
// resp, err := c.MakeRequest(r, &instance)
// if err != nil || resp.StatusCode != http.StatusOK {
// ok = false
// } else {
// resps = append(resps, resp)
// }
// r.Form.Del("")
// } else if !ok {
// log.Println("hash not known, looking up: " + hash)
// for i := uint(0); i < state.AppState.NumberOfClients; i++ {
// resp, err := c.MakeRequest(r, &i)
// if helperError(err, w) {
// return
// } else if resp.StatusCode == http.StatusOK {
// resps = append(resps, resp)
// state.AppState.Torrents[hash] = i
// }
// }
// }
// }
// if len(resps) == 1 {
// c.MakeResponse(resps[0], w)
// } else if len(resps) > 0 {
// baseResp := resps[0]
// baseBody := ""
// for _, resp := range resps {
// body, err := io.ReadAll(resp.Body)
// if helperError(err, w) {
// return
// }
// baseBody += string(body)
// }
// baseResp.Body = io.NopCloser(bytes.NewBuffer([]byte(baseBody)))
// c.MakeResponse(baseResp, w)
// } else {
// http.Error(w, "no responses", http.StatusInternalServerError)
// }
// }
// func (c *Config) Parallel(r *http.Request) (output []*struct {
// response *gabs.Container
// instance uint
// }, err error) {
// g, ctx := errgroup.WithContext(context.Background())
// responses := make(chan struct {
// response *http.Response
// instance uint
// })
// body, err := io.ReadAll(r.Body)
// if err != nil {
// return
// }
// for i := uint(0); i < state.AppState.NumberOfClients; i++ {
// g.Go(func() (err error) {
// req, err := http.NewRequest(
// r.Method,
// qbittorrent.URL(&c.QBittorrent, &i, r.URL).String(),
// bytes.NewBuffer(body),
// )
// if err != nil {
// return err
// }
// resp, err := c.MakeRequest(req, &i)
// if err != nil {
// return err
// } else if resp.StatusCode != http.StatusOK {
// body, _ := io.ReadAll(resp.Body)
// return errors.New("Error" + string(body))
// }
// select {
// case responses <- struct {
// response *http.Response
// instance uint
// }{
// response: resp,
// instance: i,
// }:
// return nil
// case <-ctx.Done():
// return ctx.Err()
// }
// })
// }
// go func() {
// g.Wait()
// close(responses)
// }()
// for resp := range responses {
// cont, err := gabs.ParseJSONBuffer(resp.response.Body)
// if err != nil {
// return nil, err
// }
// output = append(output, &struct {
// response *gabs.Container
// instance uint
// }{
// response: cont,
// instance: resp.instance,
// })
// }
// err = g.Wait()
// if err != nil {
// return
// }
// return output, nil
// }
// func (c *Config) HandlerFetchMergeArray(w http.ResponseWriter, r *http.Request) {
// if r.URL.Path == "/api/v2/torrents/info" {
// state.AppState.Locks.Torrents.Lock()
// defer state.AppState.Locks.Torrents.Unlock()
// state.AppState.Torrents = map[string]uint{}
// }
// resps, err := c.Parallel(r)
// if helperError(err, w) {
// return
// }
// output := []*gabs.Container{}
// for _, resp := range resps {
// for _, child := range resp.response.Children() {
// output = append(output, child)
// if r.URL.Path == "/api/v2/torrents/info" {
// hash := child.Path("hash").Data().(string)
// state.AppState.Torrents[hash] = resp.instance
// log.Println("Stored hash", hash)
// }
// }
// }
// if r.URL.Path == "/api/v2/torrents/info" {
// slices.SortStableFunc(output, func(a *gabs.Container, b *gabs.Container) int {
// return strings.Compare(a.Path("added_on").String(), b.Path("added_on").String())
// })
// }
// w.Write(gabs.Wrap(output).BytesIndent("", " "))
// }
// func (c *Config) HandlerFetchMerge(w http.ResponseWriter, r *http.Request) {
// resps, err := c.Parallel(r)
// if helperError(err, w) {
// return
// }
// output := &gabs.Container{}
// for _, resp := range resps {
// output.MergeFn(resp.response, func(dest, source interface{}) interface{} {
// destArr, destIsArray := dest.([]interface{})
// sourceArr, sourceIsArray := source.([]interface{})
// if destIsArray {
// if sourceIsArray {
// return append(destArr, sourceArr...)
// }
// return append(destArr, source)
// }
// if sourceIsArray {
// return append(append([]interface{}{}, dest), sourceArr...)
// }
// return source
// })
// }
// w.Write(gabs.Wrap(output).BytesIndent("", " "))
// }
// // Prepepares and rewrites headers required to auth with qBittorrent
// func PrepareRequest(r *http.Request, c *Config, i *uint) (err error) {
// if r == nil {
// return errors.New("empty request given")
// }
// if c == nil {
// return errors.New("empty config given")
// }
// if i == nil {
// return errors.New("empty instance given")
// }
// url := qbittorrent.URL(&c.QBittorrent, i, r.URL)
// r.Host = url.String()
// r.Header.Del("Referer")
// r.Header.Del("Origin")
// r.Header.Del("Accept-Encoding")
// r.Header.Set("Cookie", state.AppState.Cookies[*i].Raw)
// return
// }

123
main.go
View file

@ -8,20 +8,14 @@ import (
"net/http"
"os"
"os/signal"
"slices"
"strconv"
"strings"
"time"
"github.com/W-Floyd/qbittorrent-docker-multiplexer/qbittorrent"
"github.com/W-Floyd/qbittorrent-docker-multiplexer/state"
"github.com/W-Floyd/qbittorrent-docker-multiplexer/util"
"github.com/gorilla/mux"
"github.com/leosunmo/zapchi"
"go.uber.org/zap"
"github.com/motemen/go-loghttp"
_ "github.com/motemen/go-loghttp/global"
"github.com/motemen/go-nuts/roundtime"
"github.com/omeid/uconfig"
"gopkg.in/yaml.v3"
)
@ -29,47 +23,47 @@ import (
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
loghttp.DefaultLogRequest = func(req *http.Request) {
if false ||
strings.HasPrefix(req.URL.RequestURI(), "/api/v2/torrents/delete") ||
strings.HasPrefix(req.URL.RequestURI(), "/api/v2/torrents/add") {
log.Printf("--> %s %s", req.Method, req.URL)
for name, values := range req.Header {
// Loop over all values for the name.
for _, value := range values {
log.Println(name, value)
}
}
fmt.Println("")
for k, v := range req.Form {
log.Println(k, v)
}
fmt.Println("")
}
}
// loghttp.DefaultLogRequest = func(req *http.Request) {
// if false ||
// strings.HasPrefix(req.URL.RequestURI(), "/api/v2/torrents/delete") ||
// strings.HasPrefix(req.URL.RequestURI(), "/api/v2/torrents/add") {
// log.Printf("--> %s %s", req.Method, req.URL)
// for name, values := range req.Header {
// // Loop over all values for the name.
// for _, value := range values {
// log.Println(name, value)
// }
// }
// fmt.Println("")
// for k, v := range req.Form {
// log.Println(k, v)
// }
// fmt.Println("")
// }
// }
loghttp.DefaultLogResponse = func(resp *http.Response) {
loc := resp.Request.URL
if loc != nil {
if false ||
strings.HasPrefix(loc.RequestURI(), "/api/v2/torrents/delete") ||
strings.HasPrefix(loc.RequestURI(), "/api/v2/torrents/add") {
ctx := resp.Request.Context()
if start, ok := ctx.Value(loghttp.ContextKeyRequestStart).(time.Time); ok {
log.Printf("<-- %d %s (%s)", resp.StatusCode, resp.Request.URL, roundtime.Duration(time.Now().Sub(start), 2))
} else {
log.Printf("<-- %d %s", resp.StatusCode, resp.Request.URL)
}
for name, values := range resp.Header {
// Loop over all values for the name.
for _, value := range values {
log.Println(name, value)
}
}
fmt.Println("")
}
}
}
// loghttp.DefaultLogResponse = func(resp *http.Response) {
// loc := resp.Request.URL
// if loc != nil {
// if false ||
// strings.HasPrefix(loc.RequestURI(), "/api/v2/torrents/delete") ||
// strings.HasPrefix(loc.RequestURI(), "/api/v2/torrents/add") {
// ctx := resp.Request.Context()
// if start, ok := ctx.Value(loghttp.ContextKeyRequestStart).(time.Time); ok {
// log.Printf("<-- %d %s (%s)", resp.StatusCode, resp.Request.URL, roundtime.Duration(time.Now().Sub(start), 2))
// } else {
// log.Printf("<-- %d %s", resp.StatusCode, resp.Request.URL)
// }
// for name, values := range resp.Header {
// // Loop over all values for the name.
// for _, value := range values {
// log.Println(name, value)
// }
// }
// fmt.Println("")
// }
// }
// }
}
func main() {
@ -110,44 +104,13 @@ func main() {
os.Exit(1)
}
// Logging
for i := uint(0); i < state.AppState.NumberOfClients; i++ {
fmt.Println("Instance at 127.0.0.1:" + util.UintToString(qbittorrent.Port(&conf.QBittorrent, &i)))
fmt.Println("Username: " + qbittorrent.Username(&conf.QBittorrent, &i))
fmt.Println("Password: " + qbittorrent.Password(&conf.QBittorrent, &i))
fmt.Println("")
}
// Docker Compose
// client.
// Router
r := mux.NewRouter()
r.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.PathPrefix("/").HandlerFunc(conf.HandleAll)
r.ParseForm()
r.ParseMultipartForm(131072)
if r.URL.Path == "/api/v2/torrents/info" {
conf.HandlerFetchMergeArray(w, r)
} else if slices.Contains([]string{
"/api/v2/sync/maindata",
"/api/v2/torrents/categories",
}, r.URL.Path) {
conf.HandlerFetchMerge(w, r)
} else if r.Form.Has("hash") || r.Form.Has("hashes") {
conf.HandlerHashFinder(w, r)
} else {
conf.HandlerPassthrough(w, r)
}
})
// r.Use(zapchi.Logger(logger, "router"))
r.Use(zapchi.Logger(logger, "router"))
srv := &http.Server{
Addr: conf.Multiplexer.Address + ":" + strconv.FormatUint(uint64(conf.Multiplexer.Port), 10),

View file

View file

@ -2,191 +2,196 @@ package qbittorrent
import (
"errors"
"io"
"log"
"net/http"
"net/http/httputil"
"net/http/cookiejar"
"net/url"
"os"
"strconv"
"strings"
"text/template"
"github.com/W-Floyd/qbittorrent-docker-multiplexer/state"
util "github.com/W-Floyd/qbittorrent-docker-multiplexer/util"
"sync"
"time"
)
type Config struct {
PortRangeStart uint `default:"11000" usage:"qBittorrent port range start"`
SecretSeed string `default:"" usage:"qBittorrent app secret seed"`
MaximumTorrents uint `default:"1000" usage:"qBittorrent maximum torrents per instance"`
ConfigTemplateFile string `default:"/config/qbittorrent.conf.tmpl" usage:"qBittorrent config GoTemplate file"`
URL string `usage:"URL of qBittorrent instance (http://hostname:port)"`
Authenticate bool `default:"true" usage:"Whether to authenticate"`
Username string `usage:"Username for auth"`
Password string `usage:"Password for auth"`
}
func (c *Config) Validate() (errs []error) {
if c.PortRangeStart < 1024 {
errs = append(errs, errors.New("(qBittorrent) Port Range Start is in privileged range: "+strconv.FormatUint(uint64(c.PortRangeStart), 10)))
}
if c.SecretSeed == "" {
errs = append(errs, errors.New("(qBittorrent) Empty Secret Seed key"))
}
if !(c.MaximumTorrents > 0) {
errs = append(errs, errors.New("(qBittorrent) Maximum Torrents not greater than 0"+util.UintToString(c.PortRangeStart)))
}
if c.ConfigTemplateFile == "" {
errs = append(errs, errors.New("(qBittorrent) Empty Config Template File key"))
} else if _, err := os.Stat(c.ConfigTemplateFile); errors.Is(err, os.ErrNotExist) {
errs = append(errs, errors.New("(Docker) Config Template File ("+c.ConfigTemplateFile+") does not exist"))
} else if _, err := template.ParseFiles(c.ConfigTemplateFile); err != nil {
errs = append(errs, errors.New("(Docker) Config Template File ("+c.ConfigTemplateFile+") count not be parsed: "), err)
}
return errs
}
func Port(c *Config, i *uint) uint {
return c.PortRangeStart + *i
}
func Hostname(c *Config, i *uint) string {
return "127.0.0.1:" + util.UintToString(Port(c, i))
}
func URL(c *Config, i *uint, baseUrl *url.URL) *url.URL {
var output url.URL
if baseUrl == nil {
output = url.URL{}
} else {
output = *baseUrl
}
output.Scheme = "http"
output.Host = Hostname(c, i)
return &output
}
func Password(c *Config, i *uint) string {
return util.StringToRand("password" + strconv.FormatUint(uint64(*i), 10) + c.SecretSeed)
}
func Username(c *Config, i *uint) string {
return util.StringToRand("user" + strconv.FormatUint(uint64(*i), 10) + c.SecretSeed)
}
func GetProxy(c *Config, i *uint) (*httputil.ReverseProxy, error) {
state.AppState.Locks.Proxies.Lock()
defer state.AppState.Locks.Proxies.Unlock()
if state.AppState.Proxies == nil {
state.AppState.Proxies = map[uint]*httputil.ReverseProxy{}
}
proxy, ok := state.AppState.Proxies[*i]
var err error
if !ok {
url := URL(c, i, nil)
proxy = httputil.NewSingleHostReverseProxy(url)
proxy.Transport = http.DefaultTransport
state.AppState.Proxies[*i] = proxy
err = AuthProxy(c, i, proxy)
if err != nil {
return nil, err
type Instance struct {
URL *url.URL
Client *http.Client
Auth struct {
Enabled *bool
Lock sync.Mutex
Credentials struct {
Username *string
Password *string
}
}
}
if proxy == nil {
err = errors.New("coultn't get client from AppState")
type Configs []*Config
type Hash string
var (
Instances []*Instance
Torrents map[Hash]*Instance
RoundRobinCounter int
Locks struct {
Instances sync.Mutex
Torrents sync.Mutex
RoundRobinCounter sync.Mutex
}
)
func (c Configs) Validate() (errs []error) {
for _, config := range c {
instance, instanceErrors := config.New()
errs = append(errs, instanceErrors...)
Instances = append(Instances, instance)
}
log.Println("Config validated")
return
}
func (c *Config) New() (i *Instance, errs []error) {
i = &Instance{}
// URL
u, err := url.Parse(c.URL)
if err != nil {
return nil, err
errs = append(errs, err)
} else {
i.URL = u
}
return proxy, nil
i.Auth.Enabled = &c.Authenticate
// Authentication
if *i.Auth.Enabled {
// Credentials
if c.Username == "" {
errs = append(errs, errors.New("empty username"))
} else {
i.Auth.Credentials.Username = &c.Username
}
if c.Password == "" {
errs = append(errs, errors.New("empty password"))
} else {
i.Auth.Credentials.Password = &c.Password
}
}
if len(errs) == 0 {
i.Client = &http.Client{
Transport: http.DefaultTransport,
Jar: &cookiejar.Jar{},
}
}
// Authentication
if *i.Auth.Enabled {
err := i.Login()
if err != nil {
errs = append(errs, errors.New("login failed"), err)
}
}
return
}
func AuthProxy(c *Config, i *uint, proxy *httputil.ReverseProxy) error {
func (i *Instance) Login() error {
if proxy == nil {
return errors.New("empty proxy")
needToUpdate := false
for _, cookie := range i.Client.Jar.Cookies(&url.URL{Path: "/api/v2/"}) {
if !cookie.Expires.After(time.Now()) {
needToUpdate = true
break
}
}
if i == nil {
return errors.New("empty instance")
}
if c == nil {
return errors.New("empty config")
if !needToUpdate {
return nil
}
form := url.Values{}
form.Add("username", Username(c, i))
form.Add("password", Password(c, i))
req, err := http.NewRequest(http.MethodPost, URL(c, i, &url.URL{Path: "/api/v2/auth/login"}).String(), strings.NewReader(form.Encode()))
if err != nil {
return err
}
form.Add("username", *i.Auth.Credentials.Username)
form.Add("password", *i.Auth.Credentials.Password)
req := i.MakeRequest("/api/v2/auth/login")
req.URL.RawQuery = form.Encode()
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := proxy.Transport.RoundTrip(req)
resp, err := i.GetResponse(req)
if err != nil {
return err
} else if resp.StatusCode != http.StatusOK {
return errors.New("failed to authenticate, status code" + strconv.Itoa(resp.StatusCode))
body, _ := io.ReadAll(resp.Body)
return errors.New("status code " + strconv.Itoa(resp.StatusCode) + ", body:\n" + string(body))
}
cookiesString := resp.Header.Get("Set-Cookie")
cookie, err := http.ParseSetCookie(cookiesString)
if err != nil {
return err
}
state.AppState.Locks.Cookies.Lock()
defer state.AppState.Locks.Cookies.Unlock()
state.AppState.Cookies[*i] = *cookie
return nil
}
func LeastBusy() uint {
counts := map[uint]uint{}
func (i *Instance) MakeRequest(method string, pathElements ...string) *http.Request {
return &http.Request{
Method: method,
URL: i.URL.JoinPath(pathElements...),
}
}
state.AppState.Locks.Torrents.Lock()
defer state.AppState.Locks.Torrents.Unlock()
func (i *Instance) GetResponse(r *http.Request) (resp *http.Response, err error) {
return i.Client.Transport.RoundTrip(r)
}
for _, instance := range state.AppState.Torrents {
func LeastBusy() *Instance {
Locks.Torrents.Lock()
defer Locks.Torrents.Unlock()
Locks.Instances.Lock()
defer Locks.Instances.Unlock()
counts := map[*Instance]uint{}
for _, instance := range Torrents {
counts[instance] += 1
}
var minimum *uint
var minimumInstance *uint
var minimumInstance *Instance
for i, c := range counts {
if minimum == nil {
minimum = &c
minimumInstance = &i
minimumInstance = i
} else {
if c < *minimum {
minimum = &c
minimumInstance = &i
minimumInstance = i
}
}
}
return *minimumInstance
return minimumInstance
}
func NextRoundRobin() *Instance {
Locks.Instances.Lock()
defer Locks.Instances.Unlock()
Locks.RoundRobinCounter.Lock()
defer Locks.RoundRobinCounter.Unlock()
RoundRobinCounter += 1
if RoundRobinCounter >= len(Instances) {
RoundRobinCounter = 0
}
return Instances[RoundRobinCounter]
}

View file

@ -1,34 +0,0 @@
package state
import (
"net/http"
"net/http/httputil"
"sync"
)
var (
AppState = State{
NumberOfClients: 2,
BalancerCount: 0,
Proxies: map[uint]*httputil.ReverseProxy{},
Torrents: map[string]uint{},
Cookies: map[uint]http.Cookie{},
}
)
/// TODO: Create cookie store so we can augment proxy requests
type State struct {
NumberOfClients uint
BalancerCount uint `json:"-"`
Proxies map[uint]*httputil.ReverseProxy `json:"-"`
Torrents map[string]uint `json:"-"`
Cookies map[uint]http.Cookie `json:"-"`
Locks struct {
Cookies sync.Mutex `json:"-"`
Proxies sync.Mutex `json:"-"`
Torrents sync.Mutex `json:"-"`
} `json:"-"`
}