more work, gutting.
This commit is contained in:
parent
19fdaf1710
commit
95c057a9a0
11 changed files with 557 additions and 709 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: ./
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -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
6
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
|
||||
)
|
||||
|
|
|
|||
21
go.sum
21
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=
|
||||
|
|
|
|||
729
handlers.go
729
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
|
||||
// }
|
||||
|
|
|
|||
123
main.go
123
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),
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:"-"`
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue