fix: golangci-lint issues

This commit is contained in:
Tobias Gesellchen
2026-02-12 23:41:54 +01:00
parent 00d5bfcb69
commit c7e055eb51
4 changed files with 204 additions and 143 deletions
+110 -80
View File
@@ -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)
}
}()
}
+5 -4
View File
@@ -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()
+20 -8
View File
@@ -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)
}
}
+69 -51
View File
@@ -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.