Sfoglia il codice sorgente

restructuring and adding automatic detection for generically named gluetun and qbittorrent containers when running as a sidecar

aeth 2 giorni fa
parent
commit
79fead61fa
5 ha cambiato i file con 159 aggiunte e 91 eliminazioni
  1. 1 1
      Makefile
  2. 6 2
      main.go
  3. 68 20
      pkg/gluetun/client.go
  4. 16 68
      pkg/qbitt/client.go
  5. 68 0
      pkg/shared/util.go

+ 1 - 1
Makefile

@@ -2,7 +2,7 @@
 
 BIN = gluetun-qbitt-sidecar
 build:
-	mkdir ./build && go build -o ./build/$(BIN) ./main.go
+	mkdir -p ./build && go build -o ./build/$(BIN) ./main.go
 
 install:
 	sudo cp ./build/$(BIN) /usr/local/bin/

+ 6 - 2
main.go

@@ -14,7 +14,7 @@ import (
 func main() {
 	godotenv.Load()
 	gluetunAddress := os.Getenv("GLUETUN_ADDRESS")
-	gluetunApikey := os.Getenv("GLUETUN_API_KEY")
+	//	gluetunApikey := os.Getenv("GLUETUN_API_KEY")
 	qbitAddress := os.Getenv("QBITTORRENT_ADDRESS")
 	qbitUsername := os.Getenv("QBITTORRENT_USERNAME")
 	qbitPassword := os.Getenv("QBITTORRENT_PASSWORD")
@@ -24,7 +24,11 @@ func main() {
 	}
 
 	for {
-		port, err := gluetun.GetForwardedPort(gluetunAddress, gluetunApikey)
+		auth, err := gluetun.GetAuthentication("./config.toml")
+		if err != nil {
+			log.Fatal(err)
+		}
+		port, err := gluetun.GetForwardedPort(gluetunAddress, auth.Apikey)
 		if err != nil {
 			log.Fatal(err)
 		}

+ 68 - 20
pkg/gluetun/client.go

@@ -2,11 +2,15 @@ package gluetun
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
 	"os"
 	"strings"
+
+	"git.aetherial.dev/aeth/gluetun-qbitt-sidecar/pkg/shared"
+	"github.com/pelletier/go-toml/v2"
 )
 
 /*
@@ -38,18 +42,33 @@ will need to just retrieve the exposed port via the route:
 
 const (
 	PORT_FORWARD_PATH = "v1/portforward"
+	STATUS_PATH       = "v1/vpn/status"
 	API_KEY_HEADER    = "X-API-Key"
+	GLUETUN_ADDRESS   = "GLUETUN_ADDRESS"
 )
 
+type GluetunClient struct {
+	client *http.Client
+	server shared.Server
+}
+
 type Port struct {
 	Port int `json:"port"`
 }
 
-type Authentication struct {
-	ApiKey   string `toml:"apikey"`
-	Username string `toml:"username"`
-	Password string `toml:"password"`
+type Configuration struct {
+	Roles []Role `toml:"roles"`
 }
+type Role struct {
+	Name     string   `toml:"name"`
+	Routes   []string `toml:"routes"`
+	Auth     string   `toml:"auth"`
+	Username string   `toml:"username,omitempty"`
+	Password string   `toml:"password,omitempty"`
+	Apikey   string   `toml:"apikey,omitempty"`
+}
+
+func NewGluetunClient() {}
 
 /*
 Read in the config.toml file that gluetun uses to set the api key / username and password
@@ -57,27 +76,56 @@ Read in the config.toml file that gluetun uses to set the api key / username and
 	:param path: the path to the config file. Use the path for where this is running, since this
 	can be either ran as a container next to your deployment, or on the host itself
 */
-func GetAuthentication(path string) (Authentication, error) {
-	var auth Authentication
-	_, err := os.ReadFile(path)
+func GetAuthentication(path string) (Role, error) {
+	var config Configuration
+	var role Role
+	fb, err := os.ReadFile(path)
 	if err != nil {
-		return auth, err
+		return role, err
 	}
-	return auth, nil
+	fmt.Printf("%s\n", string(fb))
+	err = toml.Unmarshal(fb, &config)
+	if err != nil {
+		return role, err
+	}
+	for i := range config.Roles {
+		for x := range config.Roles[i].Routes {
+			if strings.Contains(config.Roles[i].Routes[x], PORT_FORWARD_PATH) {
+				return config.Roles[i], nil
+			}
+		}
+	}
+	return role, errors.New(fmt.Sprintf("No routes found that contained the path: '%s'", PORT_FORWARD_PATH))
 
 }
 
-// strips everything but the port number and hostname from a string
-func sanitizeUrl(s string) string {
-	protoStripped := strings.ReplaceAll(
-		strings.ReplaceAll(
-			strings.ReplaceAll(
-				s, "https", "",
-			), "http", "",
-		), "://", "",
-	)
-	splitHost := strings.Split(protoStripped, "/")
-	return splitHost[0]
+// get the gluetun address
+func GetGluetunAddress() (shared.Server, error) {
+	var srv shared.Server
+	client := http.Client{}
+	srv, err := shared.ParseServer("http://gluetun:8000")
+	if err != nil {
+		return srv, err
+	}
+
+	req, err := http.NewRequest(http.MethodGet, srv.FormatWith(STATUS_PATH), nil)
+	if err != nil {
+		return srv, err
+	}
+	resp, err := client.Do(req)
+	if err != nil {
+		return srv, err
+	}
+	if resp.StatusCode == http.StatusUnauthorized { // can assume that unauthorized means that the server is alive on that endpoint
+		return srv, nil
+	}
+	// try to parse out the environment variable that supposed to carry the value (if it is different)
+	addressFromEnv := os.Getenv(GLUETUN_ADDRESS)
+	srv, err = shared.ParseServer(addressFromEnv)
+	if err != nil {
+		return srv, err
+	}
+	return srv, nil
 
 }
 

+ 16 - 68
pkg/qbitt/client.go

@@ -5,12 +5,12 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"log"
 	"net/http"
 	"net/http/cookiejar"
 	"net/url"
-	"strconv"
 	"strings"
+
+	"git.aetherial.dev/aeth/gluetun-qbitt-sidecar/pkg/shared"
 )
 
 /*
@@ -52,63 +52,11 @@ func makeListenPortRequest(port int) (string, error) {
 
 }
 
-type server struct {
-	proto string // http or https
-	host  string // just the domain name, like 'qbit.local.lan' or whatever
-	port  int    // port that the web ui listens on
-}
-
-func (s server) format() string {
-	return fmt.Sprintf("%s://%s:%v", s.proto, s.host, s.port)
-}
-
-func (s server) formatWith(path string) string {
-	return fmt.Sprintf("%s%s", s.format(), path)
-}
-
 type QbittorrentClient struct {
-	server server
+	server shared.Server
 	client *http.Client // plumbing
 }
 
-// takes string s and returns the host and port
-func splitServerPort(s string) (string, int) {
-	splitPort := strings.Split(s, ":")
-	if len(splitPort) < 1 {
-		// add log here
-		log.Fatal("unhandled for now")
-	}
-	host := splitPort[0]
-	port, err := strconv.Atoi(splitPort[1])
-	if err != nil {
-		log.Fatal("couldnt parse port. unhandled for now.")
-	}
-	return host, port
-
-}
-
-// strips everything but the port number and hostname from a string
-func parseServer(s string) (server, error) {
-	var proto, host string
-	var port int
-	if strings.Contains(s, "https") {
-		proto = "https"
-	} else {
-		proto = "http"
-	}
-	protoStripped := strings.ReplaceAll(
-		strings.ReplaceAll(
-			strings.ReplaceAll(
-				s, "https", "",
-			), "http", "",
-		), "://", "",
-	)
-	splitHost := strings.Split(protoStripped, "/")
-	host, port = splitServerPort(splitHost[0])
-	return server{proto: proto, host: host, port: port}, nil
-
-}
-
 /*
 	 create a new qbit client
 		:param username: username for your qbittorrent Web UI
@@ -118,11 +66,11 @@ func parseServer(s string) (server, error) {
 func NewQbittorrentClient(username, password, host string) (QbittorrentClient, error) {
 	ckJar, err := cookiejar.New(nil)
 	client := http.Client{Jar: ckJar}
-	srv, err := parseServer(host)
+	srv, err := shared.ParseServer(host)
 	if err != nil {
 		return QbittorrentClient{}, err
 	}
-	formattedUrl := srv.formatWith(LOGIN_PATH)
+	formattedUrl := srv.FormatWith(LOGIN_PATH)
 	data := url.Values{}
 	data.Set("username", username)
 	data.Set("password", password)
@@ -131,9 +79,9 @@ func NewQbittorrentClient(username, password, host string) (QbittorrentClient, e
 	if err != nil {
 		return QbittorrentClient{}, err
 	}
-	req.Header.Add("Referer", srv.format())
-	req.Header.Add("Origin", srv.format())
-	req.Header.Add("Host", srv.host)
+	req.Header.Add("Referer", srv.Format())
+	req.Header.Add("Origin", srv.Format())
+	// req.Header.Add("Host", srv.Host)
 	// req.Header.Add("Content-Type", "text/plain")
 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 	resp, err := client.Do(req)
@@ -141,7 +89,7 @@ func NewQbittorrentClient(username, password, host string) (QbittorrentClient, e
 		return QbittorrentClient{}, err
 	}
 	if resp.StatusCode != 200 {
-		return QbittorrentClient{}, errors.New(fmt.Sprintf("Authentication failed.\n  Host: %s\n  Status Code: %s\n", srv.format(), resp.Status))
+		return QbittorrentClient{}, errors.New(fmt.Sprintf("Authentication failed.\n  Host: %s\n  Status Code: %s\n", srv.Format(), resp.Status))
 	}
 
 	client.Jar.SetCookies(resp.Request.URL, resp.Cookies())
@@ -158,16 +106,16 @@ func (q QbittorrentClient) UpdateIncomingPort(port int) error {
 	}
 	data := url.Values{}
 	data.Set("json", body)
-	req, err := http.NewRequest(http.MethodPost, q.server.formatWith(PREFERENCES_PATH), strings.NewReader(data.Encode()))
-	req.Header.Add("Origin", q.server.format())
-	req.Header.Add("Referer", q.server.format())
+	req, err := http.NewRequest(http.MethodPost, q.server.FormatWith(PREFERENCES_PATH), strings.NewReader(data.Encode()))
+	req.Header.Add("Origin", q.server.Format())
+	req.Header.Add("Referer", q.server.Format())
 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
 	resp, err := q.client.Do(req)
 	if err != nil {
 		return err
 	}
 	if resp.StatusCode != 200 {
-		return errors.New(fmt.Sprintf("Update request failed.\n  Host: %s\n  Status Code: %s\n", q.server.format(), resp.Status))
+		return errors.New(fmt.Sprintf("Update request failed.\n  Host: %s\n  Status Code: %s\n", q.server.Format(), resp.Status))
 	}
 	return nil
 }
@@ -175,12 +123,12 @@ func (q QbittorrentClient) UpdateIncomingPort(port int) error {
 // gets the running configuration
 func (q QbittorrentClient) GetRunningConfiguration() (Configuration, error) {
 	var cfg Configuration
-	req, err := http.NewRequest(http.MethodGet, q.server.formatWith(CONFIG_PATH), nil)
+	req, err := http.NewRequest(http.MethodGet, q.server.FormatWith(CONFIG_PATH), nil)
 	if err != nil {
 		return cfg, err
 	}
-	req.Header.Add("Origin", q.server.format())
-	req.Header.Add("Referer", q.server.format())
+	req.Header.Add("Origin", q.server.Format())
+	req.Header.Add("Referer", q.server.Format())
 	resp, err := q.client.Do(req)
 	if err != nil {
 		return cfg, err

+ 68 - 0
pkg/shared/util.go

@@ -0,0 +1,68 @@
+package shared
+
+import (
+	"fmt"
+	"log"
+	"strconv"
+	"strings"
+)
+
+type Server struct {
+	proto string // http or https
+	host  string // just the domain name, like 'qbit.local.lan' or whatever
+	port  int    // port that the web ui listens on
+}
+
+func (s Server) Format() string {
+	return fmt.Sprintf("%s://%s:%v", s.proto, s.host, s.port)
+}
+
+func (s Server) FormatWith(path string) string {
+	return fmt.Sprintf("%s%s", s.Format(), path)
+}
+
+// strips everything but the port number and hostname from a string
+func sanitizeUrl(s string) string {
+	protoStripped := strings.ReplaceAll(
+		strings.ReplaceAll(
+			strings.ReplaceAll(
+				s, "https", "",
+			), "http", "",
+		), "://", "",
+	)
+	splitHost := strings.Split(protoStripped, "/")
+	return splitHost[0]
+
+}
+
+// takes string s and returns the host and port
+func splitServerPort(s string) (string, int) {
+	splitPort := strings.Split(s, ":")
+	if len(splitPort) < 1 {
+		// add log here
+		log.Fatal("unhandled for now")
+	}
+	host := splitPort[0]
+	port, err := strconv.Atoi(splitPort[1])
+	if err != nil {
+		log.Fatal("couldnt parse port. unhandled for now.")
+	}
+	return host, port
+
+}
+
+// strips everything but the port number and hostname from a string
+func ParseServer(s string) (Server, error) {
+	var proto, host string
+	var port int
+	if strings.Contains(s, "https") {
+		proto = "https"
+	} else {
+		proto = "http"
+	}
+	protoStripped := sanitizeUrl(s)
+	splitHost := strings.Split(protoStripped, "/")
+	host, port = splitServerPort(splitHost[0])
+	return Server{proto: proto, host: host, port: port}, nil
+
+}