mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
Brings the five remaining gocyclo > 20 warnings to zero by extracting cohesive sub-functions; same observable behaviour, smaller surface to read at each call site. Bonus: the new helpers are individually testable. - pkg/models/clockdisplay.go: split ClockDisplay.UnmarshalXML attr handling into applyClockDisplayOuterAttrs (legacy flat shape) and applyClockConfigAttrs (current nested shape). - pkg/service/setup/ssh_probe_apply.go: split applyProbeToSummary into applyProbeCurrentConfig / applyProbeResolvConf / applyProbeRemoteServices / applyProbeCACert — one helper per MigrationSummary axis the probe populates. - pkg/service/setup/init_plan.go: split ExecuteInitPlan into applyInitPlanDefaults, runURLRewrite, resolveAccountID, and verifyPairing. Cleans up several shadowed err variables in the process. - cmd/soundtouch-cli/cmd_setup.go: split renderInspectReport into renderInspectIdentityAndPairing / renderInspectNetwork / renderInspectSources / renderInspectPresets / renderInspectRuntimeURLs, and buildPlanSteps into resetSteps + migrationSteps helpers. golangci-lint run ./pkg/service/setup/... ./pkg/models/... ./cmd/soundtouch-cli/... now reports zero findings. Tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
222 lines
6.6 KiB
Go
222 lines
6.6 KiB
Go
package setup
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// applyProbeToSummary populates the SSH-derived fields of a
|
|
// MigrationSummary directly from a batched speakerProbe. Mirrors what
|
|
// the per-helper path (checkCurrentConfig + checkRemoteServices +
|
|
// checkCACertTrusted + the inline resolv read) used to do across
|
|
// multiple SSH dials.
|
|
//
|
|
// One subtle difference from the legacy path: the original
|
|
// checkCurrentConfig has a fallback that reads the file via base64 when
|
|
// `cat` returns empty but the file has size > 0. The batched script
|
|
// already does base64 for every file, so that fallback is implicit —
|
|
// any readable file appears in probe.Files.
|
|
func (m *Manager) applyProbeToSummary(
|
|
summary *MigrationSummary,
|
|
probe *speakerProbe,
|
|
plannedCfg *PrivateCfg,
|
|
proxyURL, targetURL string,
|
|
options map[string]string,
|
|
) {
|
|
summary.SSHSuccess = probe.SSHOK
|
|
|
|
if !probe.SSHOK && probe.Err != nil {
|
|
summary.CurrentConfig = fmt.Sprintf("SSH connection failed: %v", probe.Err)
|
|
}
|
|
|
|
m.applyProbeCurrentConfig(summary, probe, plannedCfg, proxyURL, targetURL, options)
|
|
applyProbeResolvConf(summary, probe)
|
|
applyProbeRemoteServices(summary, probe)
|
|
m.applyProbeCACert(summary, probe)
|
|
}
|
|
|
|
// applyProbeCurrentConfig populates CurrentConfig / OriginalConfig and
|
|
// parses the on-disk SoundTouchSdkPrivateCfg.xml into ParsedCurrentConfig.
|
|
// Also applies proxy options to the planned config when the caller asks
|
|
// for it — that path needs the parsed current config.
|
|
func (m *Manager) applyProbeCurrentConfig(
|
|
summary *MigrationSummary,
|
|
probe *speakerProbe,
|
|
plannedCfg *PrivateCfg,
|
|
proxyURL, targetURL string,
|
|
options map[string]string,
|
|
) {
|
|
if cfg, ok := probe.Files[SoundTouchSdkPrivateCfgPath]; ok && cfg != "" {
|
|
summary.CurrentConfig = cfg
|
|
fmt.Printf("Current config from %s (length: %d):\n%q\n", probeDeviceTagFor(summary), len(cfg), cfg)
|
|
|
|
var currentCfg PrivateCfg
|
|
if xml.Unmarshal([]byte(cfg), ¤tCfg) == nil {
|
|
summary.ParsedCurrentConfig = ¤tCfg
|
|
|
|
if proxyURL == "" {
|
|
proxyURL = targetURL
|
|
}
|
|
|
|
if options != nil {
|
|
m.applyProxyOptions(plannedCfg, proxyURL, options, ¤tCfg)
|
|
}
|
|
}
|
|
}
|
|
|
|
if orig, ok := probe.Files[SoundTouchSdkPrivateCfgPath+".original"]; ok && orig != "" {
|
|
summary.OriginalConfig = orig
|
|
}
|
|
}
|
|
|
|
// applyProbeResolvConf caches the /etc/resolv.conf contents on the
|
|
// summary so checkIsMigratedFromProbe doesn't have to make another dial.
|
|
func applyProbeResolvConf(summary *MigrationSummary, probe *speakerProbe) {
|
|
if resolv, ok := probe.Files["/etc/resolv.conf"]; ok {
|
|
summary.CurrentResolvConf = resolv
|
|
}
|
|
}
|
|
|
|
// applyProbeRemoteServices records which remote_services marker files
|
|
// exist on the device — these toggle SSH enablement state.
|
|
func applyProbeRemoteServices(summary *MigrationSummary, probe *speakerProbe) {
|
|
for _, loc := range []string{"/etc/remote_services", "/mnt/nv/remote_services", "/tmp/remote_services"} {
|
|
if !probe.Exists[loc] {
|
|
continue
|
|
}
|
|
|
|
summary.RemoteServicesFound = append(summary.RemoteServicesFound, loc)
|
|
summary.RemoteServicesEnabled = true
|
|
|
|
if loc != "/tmp/remote_services" {
|
|
summary.RemoteServicesPersistent = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// applyProbeCACert checks the device's CA bundle for our injection. The
|
|
// fast path is the CALabel grep (works without Manager.Crypto and is the
|
|
// only path the CLI ever uses). The fallback that compares the actual
|
|
// cert payload runs only when Manager.Crypto is configured — i.e., in
|
|
// the web-UI / in-process flow.
|
|
func (m *Manager) applyProbeCACert(summary *MigrationSummary, probe *speakerProbe) {
|
|
bundle, ok := probe.Files["/etc/pki/tls/certs/ca-bundle.crt"]
|
|
if !ok || bundle == "" {
|
|
return
|
|
}
|
|
|
|
if strings.Contains(bundle, CALabel) {
|
|
summary.CACertTrusted = true
|
|
return
|
|
}
|
|
|
|
if m.Crypto == nil {
|
|
return
|
|
}
|
|
|
|
caCertPEM, err := os.ReadFile(m.Crypto.GetCACertPath())
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for _, line := range strings.Split(string(caCertPEM), "\n") {
|
|
if line == "" || strings.Contains(line, "BEGIN CERTIFICATE") || strings.Contains(line, "END CERTIFICATE") {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(bundle, line) {
|
|
summary.CACertTrusted = true
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// probeDeviceTagFor returns a short identifier for the device used in the
|
|
// "Current config from …" log line. We keep the legacy log shape so any
|
|
// downstream log scraping continues to work.
|
|
func probeDeviceTagFor(summary *MigrationSummary) string {
|
|
if summary.DeviceID != "" {
|
|
return summary.DeviceID
|
|
}
|
|
|
|
return "unknown"
|
|
}
|
|
|
|
// checkIsMigratedFromProbe is the probe-driven equivalent of
|
|
// checkIsMigrated. Unlike the legacy path it makes no fresh SSH dials —
|
|
// every file it inspects came from the single batched probe.
|
|
//
|
|
// Behavioural note: the legacy isResolvConfMigrated has a third fallback
|
|
// that DNS-resolves the target host via SSH (`getent` on the device) so
|
|
// it can match the *resolved* IP against resolv.conf. The probe path
|
|
// skips that — it would force a second SSH dial just for the corner
|
|
// case where the device's resolv.conf has the resolved IP but neither
|
|
// the marker comment nor the target hostname. In practice the hook file
|
|
// or marker comment is always present, so this is acceptable.
|
|
func (m *Manager) checkIsMigratedFromProbe(summary *MigrationSummary, probe *speakerProbe) {
|
|
summary.TelnetMigrated = m.isTelnetMigrated(summary)
|
|
|
|
if probe.SSHOK {
|
|
summary.XMLMigrated = m.isXMLMigrated(summary)
|
|
summary.HostsMigrated = isHostsMigratedFromProbe(probe, summary)
|
|
summary.ResolvMigrated = m.isResolvConfMigratedFromProbe(probe, summary)
|
|
}
|
|
|
|
summary.IsMigrated = summary.TelnetMigrated ||
|
|
summary.XMLMigrated ||
|
|
summary.HostsMigrated ||
|
|
summary.ResolvMigrated
|
|
}
|
|
|
|
func isHostsMigratedFromProbe(probe *speakerProbe, summary *MigrationSummary) bool {
|
|
hostsContent, ok := probe.Files["/etc/hosts"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
boseDomains := []string{
|
|
"streaming.bose.com",
|
|
"updates.bose.com",
|
|
"stats.bose.com",
|
|
"bmx.bose.com",
|
|
}
|
|
|
|
for _, domain := range boseDomains {
|
|
if strings.Contains(hostsContent, domain) && summary.CACertTrusted {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (m *Manager) isResolvConfMigratedFromProbe(probe *speakerProbe, summary *MigrationSummary) bool {
|
|
if probe.Exists["/mnt/nv/aftertouch.resolv.conf"] {
|
|
return summary.CACertTrusted
|
|
}
|
|
|
|
if summary.CurrentResolvConf == "" {
|
|
return false
|
|
}
|
|
|
|
if strings.Contains(summary.CurrentResolvConf, "# Priority nameserver for Bose service redirection") && summary.CACertTrusted {
|
|
return true
|
|
}
|
|
|
|
parsedTarget, err := url.Parse(m.ServerURL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
targetHost := parsedTarget.Hostname()
|
|
if strings.Contains(summary.CurrentResolvConf, targetHost) && summary.CACertTrusted {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|