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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/W-Floyd/qbittorrent-docker-multiplexer/docker"
|
|
||||||
"github.com/W-Floyd/qbittorrent-docker-multiplexer/multiplexer"
|
"github.com/W-Floyd/qbittorrent-docker-multiplexer/multiplexer"
|
||||||
"github.com/W-Floyd/qbittorrent-docker-multiplexer/qbittorrent"
|
"github.com/W-Floyd/qbittorrent-docker-multiplexer/qbittorrent"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Multiplexer multiplexer.Config
|
Multiplexer multiplexer.Config
|
||||||
QBittorrent qbittorrent.Config
|
QBittorrent qbittorrent.Configs
|
||||||
Docker docker.Config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) Validate() (errs []error) {
|
func (c Config) Validate() (errs []error) {
|
||||||
errs = append(errs, c.Multiplexer.Validate()...)
|
errs = append(errs, c.Multiplexer.Validate()...)
|
||||||
errs = append(errs, c.QBittorrent.Validate()...)
|
errs = append(errs, c.QBittorrent.Validate()...)
|
||||||
errs = append(errs, c.Docker.Validate()...)
|
|
||||||
|
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,25 @@ name: ${COMPOSE_PROJECT_NAME}
|
||||||
include:
|
include:
|
||||||
- vpn-docker-compose.yaml
|
- vpn-docker-compose.yaml
|
||||||
- qbittorrent-docker-compose.yaml
|
- qbittorrent-docker-compose.yaml
|
||||||
services:
|
# services:
|
||||||
qbittorrent-docker-multiplexer:
|
# qbittorrent-docker-multiplexer:
|
||||||
container_name: qbittorrent-docker-multiplexer
|
# container_name: qbittorrent-docker-multiplexer
|
||||||
build: .
|
# build: .
|
||||||
network_mode: "container:${VPN_SERVICE_NAME}"
|
# network_mode: "container:${VPN_SERVICE_NAME}"
|
||||||
environment:
|
# environment:
|
||||||
COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
|
# COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME}
|
||||||
QBITTORRENT_SECRETSEED: ${QBITTORRENT_SECRETSEED}
|
# QBITTORRENT_SECRETSEED: ${QBITTORRENT_SECRETSEED}
|
||||||
MULTIPLEXER_SECRET: ${MULTIPLEXER_SECRET}
|
# MULTIPLEXER_SECRET: ${MULTIPLEXER_SECRET}
|
||||||
MULTIPLEXER_PORT: ${MULTIPLEXER_PORT}
|
# MULTIPLEXER_PORT: ${MULTIPLEXER_PORT}
|
||||||
volumes:
|
# volumes:
|
||||||
# This allows spawning qBittorrent containers on demand
|
# # This allows spawning qBittorrent containers on demand
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:rw
|
# - /var/run/docker.sock:/var/run/docker.sock:rw
|
||||||
# Template files
|
# # Template files
|
||||||
- ./qbittorrent.conf.tmpl:/config/qbittorrent.conf.tmpl
|
# - ./qbittorrent.conf.tmpl:/config/qbittorrent.conf.tmpl
|
||||||
- ./docker-compose.yaml.tmpl:/config/docker-compose.yaml.tmpl
|
# - ./docker-compose.yaml.tmpl:/config/docker-compose.yaml.tmpl
|
||||||
# State files (so we re-spawn containers as needed)
|
# # State files (so we re-spawn containers as needed)
|
||||||
- ./data/:/data/
|
# - ./data/:/data/
|
||||||
develop:
|
# develop:
|
||||||
watch:
|
# watch:
|
||||||
- action: rebuild
|
# - action: rebuild
|
||||||
path: ./
|
# 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
|
go 1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Jeffail/gabs/v2 v2.7.0
|
|
||||||
github.com/gorilla/mux v1.8.1
|
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-loghttp v0.0.0-20231107055348-29ae44b293f4
|
||||||
github.com/motemen/go-nuts v0.0.0-20220604134737-2658d0104f31
|
github.com/motemen/go-nuts v0.0.0-20220604134737-2658d0104f31
|
||||||
github.com/omeid/uconfig v0.7.0
|
github.com/omeid/uconfig v0.7.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/sync v0.10.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // 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
|
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=
|
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/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/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/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/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
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.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/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/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 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-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:WLWwzjax2/L5NAQul9bdk1EAP0+YGnAzJBJ/LzL8Dgs=
|
||||||
github.com/motemen/go-loghttp v0.0.0-20231107055348-29ae44b293f4/go.mod h1:ykaRC7b5xKciHTUFZ60bbsOojQAkCmmehBNbBWeIz1Y=
|
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=
|
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 h1:aoNKjAANazU0LbV5EqvzUlqVW8n4ML7W0qe2UUhwtYQ=
|
||||||
github.com/omeid/uconfig v0.7.0/go.mod h1:aZqGvjN8OcabCs19rqF1CnILm9+jWohUNF/MABA29JM=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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=
|
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/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/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/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.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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
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.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.3/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.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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
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 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
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 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
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=
|
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-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-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.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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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.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.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.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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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-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-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-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-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-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/W-Floyd/qbittorrent-docker-multiplexer/qbittorrent"
|
"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) {
|
func (c *Config) HandlerPassthrough(w http.ResponseWriter, r *http.Request) {
|
||||||
|
i := qbittorrent.NextRoundRobin()
|
||||||
if strings.HasPrefix(r.URL.RequestURI(), "/api/") {
|
resp, err := i.GetResponse(r)
|
||||||
log.Println("Warning - Passthrough on API call: " + r.URL.RequestURI())
|
c.MakeResponse(err, resp, w)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
// body, err := io.ReadAll(r.Body)
|
||||||
r.Body = io.NopCloser(bytes.NewBuffer(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 {
|
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)
|
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 {
|
} else {
|
||||||
http.Error(w, "no responses", http.StatusInternalServerError)
|
for header := range resp.Header {
|
||||||
}
|
w.Header().Add(header, resp.Header.Get(header))
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
io.Copy(w, resp.Body)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinURLPath(a, b *url.URL) (path, rawpath string) {
|
// func (c *Config) HandlerHashFinder(w http.ResponseWriter, r *http.Request) {
|
||||||
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()
|
|
||||||
|
|
||||||
aslash := strings.HasSuffix(apath, "/")
|
// body, err := io.ReadAll(r.Body)
|
||||||
bslash := strings.HasPrefix(bpath, "/")
|
// r.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||||
|
// if helperError(err, w) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
switch {
|
// hashes := []string{}
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func singleJoiningSlash(a, b string) string {
|
// key := "hash"
|
||||||
aslash := strings.HasSuffix(a, "/")
|
|
||||||
bslash := strings.HasPrefix(b, "/")
|
// if r.Form.Has("hash") {
|
||||||
switch {
|
// hashes = append(hashes, r.Form.Get("hash"))
|
||||||
case aslash && bslash:
|
// }
|
||||||
return a + b[1:]
|
|
||||||
case !aslash && !bslash:
|
// if r.Form.Has("hashes") {
|
||||||
return a + "/" + b
|
// key = "hashes"
|
||||||
}
|
// hashes = append(hashes, strings.Split(r.Form.Get("hashes"), "|")...)
|
||||||
return a + b
|
// }
|
||||||
}
|
|
||||||
|
// 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"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"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/gorilla/mux"
|
||||||
|
"github.com/leosunmo/zapchi"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/motemen/go-loghttp"
|
|
||||||
_ "github.com/motemen/go-loghttp/global"
|
_ "github.com/motemen/go-loghttp/global"
|
||||||
"github.com/motemen/go-nuts/roundtime"
|
|
||||||
"github.com/omeid/uconfig"
|
"github.com/omeid/uconfig"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
@ -29,47 +23,47 @@ import (
|
||||||
func init() {
|
func init() {
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
|
||||||
loghttp.DefaultLogRequest = func(req *http.Request) {
|
// loghttp.DefaultLogRequest = func(req *http.Request) {
|
||||||
if false ||
|
// if false ||
|
||||||
strings.HasPrefix(req.URL.RequestURI(), "/api/v2/torrents/delete") ||
|
// strings.HasPrefix(req.URL.RequestURI(), "/api/v2/torrents/delete") ||
|
||||||
strings.HasPrefix(req.URL.RequestURI(), "/api/v2/torrents/add") {
|
// strings.HasPrefix(req.URL.RequestURI(), "/api/v2/torrents/add") {
|
||||||
log.Printf("--> %s %s", req.Method, req.URL)
|
// log.Printf("--> %s %s", req.Method, req.URL)
|
||||||
for name, values := range req.Header {
|
// for name, values := range req.Header {
|
||||||
// Loop over all values for the name.
|
// // Loop over all values for the name.
|
||||||
for _, value := range values {
|
// for _, value := range values {
|
||||||
log.Println(name, value)
|
// log.Println(name, value)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
fmt.Println("")
|
// fmt.Println("")
|
||||||
for k, v := range req.Form {
|
// for k, v := range req.Form {
|
||||||
log.Println(k, v)
|
// log.Println(k, v)
|
||||||
}
|
// }
|
||||||
fmt.Println("")
|
// fmt.Println("")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
loghttp.DefaultLogResponse = func(resp *http.Response) {
|
// loghttp.DefaultLogResponse = func(resp *http.Response) {
|
||||||
loc := resp.Request.URL
|
// loc := resp.Request.URL
|
||||||
if loc != nil {
|
// if loc != nil {
|
||||||
if false ||
|
// if false ||
|
||||||
strings.HasPrefix(loc.RequestURI(), "/api/v2/torrents/delete") ||
|
// strings.HasPrefix(loc.RequestURI(), "/api/v2/torrents/delete") ||
|
||||||
strings.HasPrefix(loc.RequestURI(), "/api/v2/torrents/add") {
|
// strings.HasPrefix(loc.RequestURI(), "/api/v2/torrents/add") {
|
||||||
ctx := resp.Request.Context()
|
// ctx := resp.Request.Context()
|
||||||
if start, ok := ctx.Value(loghttp.ContextKeyRequestStart).(time.Time); ok {
|
// 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))
|
// log.Printf("<-- %d %s (%s)", resp.StatusCode, resp.Request.URL, roundtime.Duration(time.Now().Sub(start), 2))
|
||||||
} else {
|
// } else {
|
||||||
log.Printf("<-- %d %s", resp.StatusCode, resp.Request.URL)
|
// log.Printf("<-- %d %s", resp.StatusCode, resp.Request.URL)
|
||||||
}
|
// }
|
||||||
for name, values := range resp.Header {
|
// for name, values := range resp.Header {
|
||||||
// Loop over all values for the name.
|
// // Loop over all values for the name.
|
||||||
for _, value := range values {
|
// for _, value := range values {
|
||||||
log.Println(name, value)
|
// log.Println(name, value)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
fmt.Println("")
|
// fmt.Println("")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
@ -110,44 +104,13 @@ func main() {
|
||||||
os.Exit(1)
|
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
|
// Router
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
r.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
r.PathPrefix("/").HandlerFunc(conf.HandleAll)
|
||||||
|
|
||||||
r.ParseForm()
|
r.Use(zapchi.Logger(logger, "router"))
|
||||||
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"))
|
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: conf.Multiplexer.Address + ":" + strconv.FormatUint(uint64(conf.Multiplexer.Port), 10),
|
Addr: conf.Multiplexer.Address + ":" + strconv.FormatUint(uint64(conf.Multiplexer.Port), 10),
|
||||||
|
|
|
||||||
|
|
@ -2,191 +2,196 @@ package qbittorrent
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"sync"
|
||||||
"text/template"
|
"time"
|
||||||
|
|
||||||
"github.com/W-Floyd/qbittorrent-docker-multiplexer/state"
|
|
||||||
util "github.com/W-Floyd/qbittorrent-docker-multiplexer/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
PortRangeStart uint `default:"11000" usage:"qBittorrent port range start"`
|
URL string `usage:"URL of qBittorrent instance (http://hostname:port)"`
|
||||||
SecretSeed string `default:"" usage:"qBittorrent app secret seed"`
|
Authenticate bool `default:"true" usage:"Whether to authenticate"`
|
||||||
MaximumTorrents uint `default:"1000" usage:"qBittorrent maximum torrents per instance"`
|
Username string `usage:"Username for auth"`
|
||||||
ConfigTemplateFile string `default:"/config/qbittorrent.conf.tmpl" usage:"qBittorrent config GoTemplate file"`
|
Password string `usage:"Password for auth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Validate() (errs []error) {
|
type Instance struct {
|
||||||
|
URL *url.URL
|
||||||
if c.PortRangeStart < 1024 {
|
Client *http.Client
|
||||||
errs = append(errs, errors.New("(qBittorrent) Port Range Start is in privileged range: "+strconv.FormatUint(uint64(c.PortRangeStart), 10)))
|
Auth struct {
|
||||||
}
|
Enabled *bool
|
||||||
|
Lock sync.Mutex
|
||||||
if c.SecretSeed == "" {
|
Credentials struct {
|
||||||
errs = append(errs, errors.New("(qBittorrent) Empty Secret Seed key"))
|
Username *string
|
||||||
}
|
Password *string
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if proxy == nil {
|
type Configs []*Config
|
||||||
err = errors.New("coultn't get client from AppState")
|
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 {
|
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 {
|
needToUpdate := false
|
||||||
return errors.New("empty proxy")
|
|
||||||
|
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 !needToUpdate {
|
||||||
}
|
return nil
|
||||||
if c == nil {
|
|
||||||
return errors.New("empty config")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
form := url.Values{}
|
form := url.Values{}
|
||||||
form.Add("username", Username(c, i))
|
form.Add("username", *i.Auth.Credentials.Username)
|
||||||
form.Add("password", Password(c, i))
|
form.Add("password", *i.Auth.Credentials.Password)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
req := i.MakeRequest("/api/v2/auth/login")
|
||||||
|
req.URL.RawQuery = form.Encode()
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
resp, err := proxy.Transport.RoundTrip(req)
|
resp, err := i.GetResponse(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if resp.StatusCode != http.StatusOK {
|
} 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
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LeastBusy() uint {
|
func (i *Instance) MakeRequest(method string, pathElements ...string) *http.Request {
|
||||||
counts := map[uint]uint{}
|
return &http.Request{
|
||||||
|
Method: method,
|
||||||
|
URL: i.URL.JoinPath(pathElements...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.AppState.Locks.Torrents.Lock()
|
func (i *Instance) GetResponse(r *http.Request) (resp *http.Response, err error) {
|
||||||
defer state.AppState.Locks.Torrents.Unlock()
|
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
|
counts[instance] += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var minimum *uint
|
var minimum *uint
|
||||||
var minimumInstance *uint
|
var minimumInstance *Instance
|
||||||
|
|
||||||
for i, c := range counts {
|
for i, c := range counts {
|
||||||
if minimum == nil {
|
if minimum == nil {
|
||||||
minimum = &c
|
minimum = &c
|
||||||
minimumInstance = &i
|
minimumInstance = i
|
||||||
} else {
|
} else {
|
||||||
if c < *minimum {
|
if c < *minimum {
|
||||||
minimum = &c
|
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