From c7e055eb514d4a36c34b4abdc89ff93cc725e3a6 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Thu, 12 Feb 2026 23:39:08 +0100 Subject: [PATCH] fix: golangci-lint issues --- cmd/soundtouch-service/main.go | 190 ++++++++++++++----------- pkg/service/crypto/crypto.go | 9 +- pkg/service/handlers/handlers_setup.go | 28 ++-- pkg/service/setup/setup.go | 120 +++++++++------- 4 files changed, 204 insertions(+), 143 deletions(-) diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index 06c203f..914c762 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -4,6 +4,7 @@ package main import ( "context" + "crypto/tls" "log" "net/http" "net/http/httputil" @@ -23,13 +24,54 @@ import ( ) func main() { + config := loadConfig() + ds := initDataStore(config.dataDir) + cm := initCertificateManager(config.dataDir) + sm := setup.NewManager(config.serverURL, ds, cm) + server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody) + + tlsConfig, err := cm.GetServerTLSConfig(config.domains) + if err != nil { + log.Printf("Warning: Failed to setup TLS: %v", err) + } + + pyProxy := setupPythonProxy(config.targetURL, config.redact, config.logBody) + + startDeviceDiscovery(server) + + r := setupRouter(server, pyProxy) + + log.Printf("Go service starting on %s, proxying to %s", config.serverURL, config.targetURL) + + if tlsConfig != nil { + startHTTPSServer(config.httpsAddr, r, tlsConfig, config.httpsServerURL) + } + + log.Fatal(http.ListenAndServe(config.addr, r)) +} + +type serviceConfig struct { + port string + bindAddr string + addr string + targetURL string + dataDir string + serverURL string + httpsServerURL string + httpsAddr string + redact bool + logBody bool + domains []string +} + +func loadConfig() serviceConfig { port := os.Getenv("PORT") if port == "" { port = "8000" } bindAddr := os.Getenv("BIND_ADDR") - // If BIND_ADDR is explicitly set, use it. Otherwise, bind to all interfaces (IPv4 and IPv6). + addr := bindAddr + ":" + port if bindAddr == "" { addr = ":" + port @@ -40,24 +82,13 @@ func main() { targetURL = "http://localhost:8001" } - target, err := url.Parse(targetURL) - if err != nil { - log.Fatalf("Failed to parse target URL: %v", err) - } - dataDir := os.Getenv("DATA_DIR") if dataDir == "" { dataDir = "data" } - ds := datastore.NewDataStore(dataDir) - if err := ds.Initialize(); err != nil { - log.Printf("Warning: Failed to initialize datastore: %v", err) - } - serverURL := os.Getenv("SERVER_URL") if serverURL == "" { - // Try to guess the server URL hostname, _ := os.Hostname() if hostname == "" { hostname = "localhost" @@ -66,41 +97,8 @@ func main() { serverURL = "http://" + strings.ToLower(hostname) + ":" + port } - httpsServerURL := os.Getenv("HTTPS_SERVER_URL") - if httpsServerURL == "" { - // Guess HTTPS server URL - hostname, _ := os.Hostname() - if hostname == "" { - hostname = "localhost" - } - - // Re-fetch httpsPort as it is defined later in the code, but let's move it up or just use the logic - guessHTTPSPort := os.Getenv("HTTPS_PORT") - if guessHTTPSPort == "" { - guessHTTPSPort = "8443" - } - - httpsServerURL = "https://" + strings.ToLower(hostname) + ":" + guessHTTPSPort - } - - cm := crypto.NewCertificateManager(filepath.Join(dataDir, "certs")) - if err := cm.EnsureCA(); err != nil { - log.Printf("Warning: Failed to ensure CA: %v", err) - } - - sm := setup.NewManager(serverURL, ds, cm) - - redact := os.Getenv("REDACT_PROXY_LOGS") != "false" - logBody := os.Getenv("LOG_PROXY_BODY") == "true" - - server := handlers.NewServer(ds, sm, serverURL, redact, logBody) - - // Phase 11: Setup HTTPS if CA and certificates are available httpsPort := os.Getenv("HTTPS_PORT") if httpsPort == "" { - // We don't default to 443 because it usually requires root, - // and we want the service to start out-of-the-box for developers. - // However, 443 is needed for the device to connect via /etc/hosts without a port. httpsPort = "8443" } @@ -109,6 +107,16 @@ func main() { httpsAddr = ":" + httpsPort } + httpsServerURL := os.Getenv("HTTPS_SERVER_URL") + if httpsServerURL == "" { + hostname, _ := os.Hostname() + if hostname == "" { + hostname = "localhost" + } + + httpsServerURL = "https://" + strings.ToLower(hostname) + ":" + httpsPort + } + hostname, _ := os.Hostname() if hostname == "" { hostname = "localhost" @@ -128,22 +136,51 @@ func main() { "127.0.0.1", } - tlsConfig, err := cm.GetServerTLSConfig(domains) + return serviceConfig{ + port: port, + bindAddr: bindAddr, + addr: addr, + targetURL: targetURL, + dataDir: dataDir, + serverURL: serverURL, + httpsServerURL: httpsServerURL, + httpsAddr: httpsAddr, + redact: os.Getenv("REDACT_PROXY_LOGS") != "false", + logBody: os.Getenv("LOG_PROXY_BODY") == "true", + domains: domains, + } +} + +func initDataStore(dataDir string) *datastore.DataStore { + ds := datastore.NewDataStore(dataDir) + if err := ds.Initialize(); err != nil { + log.Printf("Warning: Failed to initialize datastore: %v", err) + } + + return ds +} + +func initCertificateManager(dataDir string) *crypto.CertificateManager { + cm := crypto.NewCertificateManager(filepath.Join(dataDir, "certs")) + if err := cm.EnsureCA(); err != nil { + log.Printf("Warning: Failed to ensure CA: %v", err) + } + + return cm +} + +func setupPythonProxy(targetURL string, redact, logBody bool) *httputil.ReverseProxy { + target, err := url.Parse(targetURL) if err != nil { - log.Printf("Warning: Failed to setup TLS: %v", err) + log.Fatalf("Failed to parse target URL: %v", err) } pyProxy := httputil.NewSingleHostReverseProxy(target) pyProxy.ModifyResponse = func(res *http.Response) error { - // Generic Header Preservation: - // Go's net/http canonicalizes headers (e.g., ETag becomes Etag). - // We ensure ETag specifically uses uppercase 'T' as some Bose devices are case-sensitive. if etags, ok := res.Header["Etag"]; ok { delete(res.Header, "Etag") res.Header["ETag"] = etags } - // Also restore other potentially sensitive headers if needed, but for now we focus on ETag - // as it's the most common culprit. currentLp := proxy.NewLoggingProxy(target.String(), redact) currentLp.LogBody = logBody @@ -151,6 +188,7 @@ func main() { return nil } + originalPyDirector := pyProxy.Director pyProxy.Director = func(req *http.Request) { originalPyDirector(req) @@ -160,22 +198,23 @@ func main() { currentLp.LogRequest(req) } - // Phase 5: Device Discovery + return pyProxy +} + +func startDeviceDiscovery(server *handlers.Server) { go func() { for { server.DiscoverDevices(context.Background()) time.Sleep(5 * time.Minute) } }() +} +func setupRouter(server *handlers.Server, pyProxy *httputil.ReverseProxy) *chi.Mux { r := chi.NewRouter() - - // Update HTTPS server handler if it was initialized - // (Deferred logic in Phase 11 will use this 'r') r.Use(middleware.Logger) r.Use(middleware.Recoverer) - // Phase 2: Root endpoint implemented in Go r.Get("/", server.HandleRoot) r.Get("/health", server.HandleHealth) r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { @@ -183,11 +222,9 @@ func main() { server.HandleMedia()(w, r) }) - // Phase 2: Static file serving for /media and /web r.Get("/media/*", server.HandleMedia()) r.Get("/web/*", server.HandleWeb()) - // Phase 3: BMX endpoints r.Route("/bmx", func(r chi.Router) { r.Get("/registry/v1/services", server.HandleBMXRegistry) r.Get("/tunein/v1/playback/station/{stationID}", server.HandleTuneInPlayback) @@ -196,7 +233,6 @@ func main() { r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback) }) - // Phase 4: Marge endpoints r.Route("/marge", func(r chi.Router) { r.Get("/streaming/sourceproviders", server.HandleMargeSourceProviders) r.Get("/accounts/{account}/full", server.HandleMargeAccountFull) @@ -212,16 +248,13 @@ func main() { r.Post("/streaming/support/customersupport", server.HandleMargeCustomerSupport) }) - // Phase 10: Stats endpoints r.Route("/streaming/stats", func(r chi.Router) { r.Post("/usage", server.HandleUsageStats) r.Post("/error", server.HandleErrorStats) }) - // Proxy route integrated into main router r.Get("/proxy/*", server.HandleProxyRequest) - // Phase 7: Setup and Discovery endpoints r.Route("/setup", func(r chi.Router) { r.Get("/devices", server.HandleListDiscoveredDevices) r.Post("/discover", server.HandleTriggerDiscovery) @@ -241,28 +274,25 @@ func main() { r.Get("/devices/{deviceId}/events", server.HandleGetDeviceEvents) }) - // Delegation Logic: Proxy everything else to Python r.NotFound(func(w http.ResponseWriter, r *http.Request) { pyProxy.ServeHTTP(w, r) }) - log.Printf("Go service starting on %s, proxying to %s", serverURL, targetURL) + return r +} - if tlsConfig != nil { - httpsServer := &http.Server{ - Addr: httpsAddr, - Handler: r, - TLSConfig: tlsConfig, - } - - log.Printf("Go service starting HTTPS on %s", httpsServerURL) - - go func() { - if err := httpsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { - log.Printf("HTTPS server error: %v", err) - } - }() +func startHTTPSServer(httpsAddr string, r http.Handler, tlsConfig *tls.Config, httpsServerURL string) { + httpsServer := &http.Server{ + Addr: httpsAddr, + Handler: r, + TLSConfig: tlsConfig, } - log.Fatal(http.ListenAndServe(addr, r)) + log.Printf("Go service starting HTTPS on %s", httpsServerURL) + + go func() { + if err := httpsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { + log.Printf("HTTPS server error: %v", err) + } + }() } diff --git a/pkg/service/crypto/crypto.go b/pkg/service/crypto/crypto.go index 7898a4e..b8092f4 100644 --- a/pkg/service/crypto/crypto.go +++ b/pkg/service/crypto/crypto.go @@ -1,3 +1,4 @@ +// Package crypto provides tools for managing Root CAs and generating SSL certificates. package crypto import ( @@ -164,8 +165,8 @@ func (cm *CertificateManager) GenerateCA() error { } certPath := cm.GetCACertPath() - if err := os.MkdirAll(cm.CertsDir, 0755); err != nil { - return err + if mkdirErr := os.MkdirAll(cm.CertsDir, 0755); mkdirErr != nil { + return mkdirErr } certOut, err := os.Create(certPath) @@ -173,8 +174,8 @@ func (cm *CertificateManager) GenerateCA() error { return err } - if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { - return err + if encodeErr := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); encodeErr != nil { + return encodeErr } certOut.Close() diff --git a/pkg/service/handlers/handlers_setup.go b/pkg/service/handlers/handlers_setup.go index 3e0b64a..52d2529 100644 --- a/pkg/service/handlers/handlers_setup.go +++ b/pkg/service/handlers/handlers_setup.go @@ -330,21 +330,27 @@ func (s *Server) HandleTestHostsRedirection(w http.ResponseWriter, r *http.Reque if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // Return 200 but ok: false so UI can show the output - _ = json.NewEncoder(w).Encode(map[string]interface{}{ + + if encodeErr := json.NewEncoder(w).Encode(map[string]interface{}{ "ok": false, "message": err.Error(), "output": output, - }) + }); encodeErr != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } return } w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]interface{}{ + + if encodeErr := json.NewEncoder(w).Encode(map[string]interface{}{ "ok": true, "message": "Hosts redirection test successful", "output": output, - }) + }); encodeErr != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } } // HandleTestConnection performs a connection check from the device to the server. @@ -367,19 +373,25 @@ func (s *Server) HandleTestConnection(w http.ResponseWriter, r *http.Request) { if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // Return 200 but ok: false so UI can show the output - _ = json.NewEncoder(w).Encode(map[string]interface{}{ + + if encodeErr := json.NewEncoder(w).Encode(map[string]interface{}{ "ok": false, "message": err.Error(), "output": output, - }) + }); encodeErr != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } return } w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]interface{}{ + + if encodeErr := json.NewEncoder(w).Encode(map[string]interface{}{ "ok": true, "message": "Connection test successful", "output": output, - }) + }); encodeErr != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } } diff --git a/pkg/service/setup/setup.go b/pkg/service/setup/setup.go index e0af1a3..722d35b 100644 --- a/pkg/service/setup/setup.go +++ b/pkg/service/setup/setup.go @@ -706,32 +706,56 @@ func (m *Manager) TestHostsRedirection(deviceIP, targetURL string) (string, erro client := m.NewSSH(deviceIP) rwCmd := "(rw || mount -o remount,rw /)" - // 1. Parse targetURL to get IP for /etc/hosts - parsedURL, err := url.Parse(targetURL) + hostIP, parsedURL, err := m.parseTargetURLAndResolveIP(targetURL, client) if err != nil { - return "", fmt.Errorf("failed to parse target URL: %w", err) + return "", err } - hostName := parsedURL.Hostname() - if hostName == "" || hostName == "localhost" { - return "", fmt.Errorf("target URL must contain a valid IP or hostname (got %s)", hostName) - } - - hostIP := m.resolveIP(hostName, client) - testDomain := TestDomain testEntry := fmt.Sprintf("%s\t%s", hostIP, testDomain) - // 2. Add temporary entry to /etc/hosts + if addErr := m.addTemporaryHostEntry(client, deviceIP, testDomain, testEntry, rwCmd); addErr != nil { + return "", addErr + } + + defer m.cleanupTemporaryHostEntry(client, testDomain, rwCmd) + + output, err := m.runHTTPRedirectionTest(client, parsedURL, testDomain) + if err != nil { + return output, err + } + + httpsOutput, httpsErr := m.runHTTPSRedirectionTest(client, testDomain) + + combinedOutput := output + "\n---\n" + httpsOutput + if httpsErr != nil { + return combinedOutput, fmt.Errorf("hosts redirection HTTPS test failed: %w", httpsErr) + } + + return combinedOutput, nil +} + +func (m *Manager) parseTargetURLAndResolveIP(targetURL string, client SSHClient) (string, *url.URL, error) { + parsedURL, err := url.Parse(targetURL) + if err != nil { + return "", nil, fmt.Errorf("failed to parse target URL: %w", err) + } + + hostName := parsedURL.Hostname() + if hostName == "" || hostName == "localhost" { + return "", nil, fmt.Errorf("target URL must contain a valid IP or hostname (got %s)", hostName) + } + + return m.resolveIP(hostName, client), parsedURL, nil +} + +func (m *Manager) addTemporaryHostEntry(client SSHClient, deviceIP, testDomain, testEntry, rwCmd string) error { hostsContent, err := client.Run("cat /etc/hosts") if err != nil { - return "", fmt.Errorf("failed to read /etc/hosts: %w", err) + return fmt.Errorf("failed to read /etc/hosts: %w", err) } if strings.Contains(hostsContent, testDomain) { - // Even if it's there, let's make sure it's correct (pointing to the current hostIP) - // but for now, if it's there, we just assume it's okay or from a previous failed cleanup. - // Let's remove it and re-add to be sure. lines := strings.Split(hostsContent, "\n") var newLines []string @@ -749,47 +773,45 @@ func (m *Manager) TestHostsRedirection(deviceIP, targetURL string) (string, erro } _, _ = client.Run(rwCmd) - // Ensure hostsContent ends with a newline if not empty + if hostsContent != "" && !strings.HasSuffix(hostsContent, "\n") { hostsContent += "\n" } newHostsContent := hostsContent + testEntry + "\n" - if err := client.UploadContent([]byte(newHostsContent), "/etc/hosts"); err != nil { - return "", fmt.Errorf("failed to add test entry to /etc/hosts: %w", err) + if uploadErr := client.UploadContent([]byte(newHostsContent), "/etc/hosts"); uploadErr != nil { + return fmt.Errorf("failed to add test entry to /etc/hosts: %w", uploadErr) } fmt.Printf("Updated /etc/hosts on %s with test entry:\n%s\n", deviceIP, newHostsContent) - defer func() { - // Clean up test entry - currentContent, _ := client.Run("cat /etc/hosts") - lines := strings.Split(currentContent, "\n") + return nil +} - var newLines []string +func (m *Manager) cleanupTemporaryHostEntry(client SSHClient, testDomain, rwCmd string) { + currentContent, _ := client.Run("cat /etc/hosts") + lines := strings.Split(currentContent, "\n") - for _, line := range lines { - if line != "" && !strings.Contains(line, testDomain) { - newLines = append(newLines, line) - } + var newLines []string + + for _, line := range lines { + if line != "" && !strings.Contains(line, testDomain) { + newLines = append(newLines, line) } + } - finalContent := strings.Join(newLines, "\n") - if len(newLines) > 0 { - finalContent += "\n" - } + finalContent := strings.Join(newLines, "\n") + if len(newLines) > 0 { + finalContent += "\n" + } - _, _ = client.Run(rwCmd) - _ = client.UploadContent([]byte(finalContent), "/etc/hosts") - }() + _, _ = client.Run(rwCmd) + _ = client.UploadContent([]byte(finalContent), "/etc/hosts") +} - // 3. Test connection to the fake domain - // 3a. HTTP (for simplicity of redirection test) - // We use the health check endpoint on the same port but with the fake domain +func (m *Manager) runHTTPRedirectionTest(client SSHClient, parsedURL *url.URL, testDomain string) (string, error) { httpTestURL := fmt.Sprintf("http://%s:%s/health", testDomain, parsedURL.Port()) - if parsedURL.Port() == "" { - httpTestURL = fmt.Sprintf("http://%s/health", testDomain) - } else if parsedURL.Port() == "80" { + if parsedURL.Port() == "" || parsedURL.Port() == "80" { httpTestURL = fmt.Sprintf("http://%s/health", testDomain) } @@ -800,7 +822,10 @@ func (m *Manager) TestHostsRedirection(deviceIP, targetURL string) (string, erro return output, fmt.Errorf("hosts redirection HTTP test failed: %w", err) } - // 3b. HTTPS (to verify TLS reachability) + return output, nil +} + +func (m *Manager) runHTTPSRedirectionTest(client SSHClient, testDomain string) (string, error) { httpsPort := os.Getenv("HTTPS_PORT") if httpsPort == "" { httpsPort = "8443" @@ -811,16 +836,14 @@ func (m *Manager) TestHostsRedirection(deviceIP, targetURL string) (string, erro httpsTestURL = fmt.Sprintf("https://%s/health", testDomain) } - // We now include the testDomain in our SSL certificate. - // We use the local CA certificate to verify the connection. caPEM, err := os.ReadFile(m.Crypto.GetCACertPath()) if err != nil { - return output, fmt.Errorf("failed to read CA cert for HTTPS test: %w", err) + return "", fmt.Errorf("failed to read CA cert for HTTPS test: %w", err) } caPath := "/tmp/soundtouch-test-ca.crt" if err := client.UploadContent(caPEM, caPath); err != nil { - return output, fmt.Errorf("failed to upload temporary CA for HTTPS test: %w", err) + return "", fmt.Errorf("failed to upload temporary CA for HTTPS test: %w", err) } defer func() { @@ -829,12 +852,7 @@ func (m *Manager) TestHostsRedirection(deviceIP, targetURL string) (string, erro httpsCmd := fmt.Sprintf("curl -v -s -L --cacert %s %s", caPath, httpsTestURL) - httpsOutput, httpsErr := client.Run(httpsCmd) - if httpsErr != nil { - return output + "\n---\n" + httpsOutput, fmt.Errorf("hosts redirection HTTPS test failed: %w", httpsErr) - } - - return output + "\n---\n" + httpsOutput, nil + return client.Run(httpsCmd) } // TestConnection performs a connection check from the device to the server.