diff --git a/config.go b/config.go index ec1914a..90280de 100644 --- a/config.go +++ b/config.go @@ -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 } diff --git a/docker-compose.yaml b/docker-compose.yaml index 5d0a58a..4a45b36 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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: ./ \ No newline at end of file +# 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: ./ \ No newline at end of file diff --git a/docker-compose.yaml.tmpl b/docker-compose.yaml.tmpl deleted file mode 100644 index 8b13789..0000000 --- a/docker-compose.yaml.tmpl +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docker/docker.go b/docker/docker.go deleted file mode 100644 index c19f2ff..0000000 --- a/docker/docker.go +++ /dev/null @@ -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 - -} diff --git a/go.mod b/go.mod index 6fdcfba..6f810eb 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index e358c40..109528b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/handlers.go b/handlers.go index 91452f5..b6725a1 100644 --- a/handlers.go +++ b/handlers.go @@ -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 +// } diff --git a/main.go b/main.go index e430ff2..4e91311 100644 --- a/main.go +++ b/main.go @@ -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), diff --git a/qbittorrent.conf.tmpl b/qbittorrent.conf.tmpl deleted file mode 100644 index e69de29..0000000 diff --git a/qbittorrent/qbittorrent.go b/qbittorrent/qbittorrent.go index 5eb5f79..19338a3 100644 --- a/qbittorrent/qbittorrent.go +++ b/qbittorrent/qbittorrent.go @@ -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] +} diff --git a/state/state.go b/state/state.go deleted file mode 100644 index abc66af..0000000 --- a/state/state.go +++ /dev/null @@ -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:"-"` -}