fix(setup): don't offer a telnet revert on speakers that were never migrated

telnetRevertAvailable compared the live URLs against canonicalBoseTelnetURLs
and reported "revertable" on any difference. That set is one original variant,
not the only one: this repo's own model of a factory speaker
(pkg/service/testing/fakespeaker) uses stats.bose.com and bmxservice.bose.com
where the canonical set has events.api.bosecm.com and content.api.bose.io.

Feeding that fixture to the gate returned true, so the web UI offered
"Restore Bose URLs via Telnet" on a pristine speaker. Pressing it rewrites the
device's genuine factory URLs and commits them through envswitch, the layer
that wins on the next reboot. The speaker keeps working, since both host sets
point at the shut-down Bose cloud, but the record of what that device's URLs
actually were is gone, and telnet migration takes no backup to recover it from.

The question the gate should answer is "has this been changed away from a
factory configuration", not "does it differ from our canonical set". It now
compares each field against the values observed on unmigrated speakers,
normalising case and a trailing slash so firmware echoing does not decide it.

Only observed values are listed. Other Bose hostnames appear in the DNS
interception lists and in DNS recordings, but those capture hosts a speaker
resolves at runtime rather than the configured value of these four fields, and
a wrong entry would hide the revert from someone who needs it.

The existing test could not catch this: it asserts against the same canonical
constants the code was comparing with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-09-05 22:32:50 +02:00
co-authored by Claude Opus 5
parent 67d64c74ff
commit 1a56b184dc
2 changed files with 130 additions and 1 deletions
+68 -1
View File
@@ -152,11 +152,78 @@ func validateTelnetURL(field, value string) error {
return nil
}
// knownOriginalTelnetURLs lists, per field, the values actually observed on
// speakers that were never migrated. Firmware variants differ: the stats and
// BMX registry endpoints have each been seen with two different hosts, so a
// single canonical set is not enough to recognise an untouched device.
//
// Only values actually observed in these four fields belong here. Other Bose
// hostnames appear elsewhere in this repo (updates.bose.com and bmx.bose.com
// in the DNS interception and /etc/hosts lists) and in DNS recordings, but
// those record hosts a speaker RESOLVES at runtime, including ones reached
// through redirects and unrelated APIs. That is a different thing from the
// configured value of margeServerUrl and friends.
//
// The asymmetry matters: a wrong entry here makes a changed device look
// original, so the revert quietly disappears for someone who needs it. Being
// incomplete only offers a revert that turns out to be unnecessary.
//
// Compared against the full URL rather than just the host. Any Bose-looking
// host would also accept a speaker pointed at some other Bose endpoint, which
// is not the same thing as being unmigrated.
func knownOriginalTelnetURLs() map[string][]string {
return map[string][]string{
"margeServerUrl": {"https://streaming.bose.com"},
"statsServerUrl": {
"https://stats.bose.com",
"https://events.api.bosecm.com",
},
"swUpdateUrl": {"https://worldwide.bose.com/updates/soundtouch"},
"bmxRegistryUrl": {
"https://content.api.bose.io/bmx/registry/v1/services",
"https://bmxservice.bose.com/bmx/registry/v1/services",
},
}
}
// normalizeTelnetURLForComparison makes URL equality tolerant of the
// differences firmware introduces when echoing a value back, without
// loosening which endpoints count as original.
func normalizeTelnetURLForComparison(value string) string {
return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(value)), "/")
}
// telnetRevertAvailable reports whether the device carries a URL set worth
// offering to restore.
//
// It answers "has this been changed away from a factory configuration", not
// "does it differ from our canonical set". Those are not the same question:
// the canonical set is one of several original variants, so comparing against
// it alone offers a destructive, persisted rewrite on speakers that were never
// migrated, destroying the record of what their URLs actually were. Telnet
// migration takes no backup, so that record is not recoverable.
func telnetRevertAvailable(response string) bool {
current := parseGetpdoConfig(response)
known := knownOriginalTelnetURLs()
for _, field := range canonicalBoseTelnetURLs().fields() {
if value, ok := current[field.configName]; ok && value != "" && value != field.value {
value, ok := current[field.configName]
if !ok || value == "" {
continue
}
if !matchesKnownOriginalTelnetURL(known[field.configName], value) {
return true
}
}
return false
}
func matchesKnownOriginalTelnetURL(originals []string, value string) bool {
normalized := normalizeTelnetURLForComparison(value)
for _, original := range originals {
if normalizeTelnetURLForComparison(original) == normalized {
return true
}
}
@@ -437,3 +437,65 @@ func TestMigrateViaTelnet_MissingNewTelnetIsClearError(t *testing.T) {
t.Errorf("err = %v, want a configuration error mentioning NewTelnet", err)
}
}
// TestTelnetRevertNotOfferedOnUnmigratedSpeakers: offering the revert here
// would rewrite a pristine speaker's genuine factory URLs and commit them
// through envswitch. Telnet migration takes no backup, so the record of what
// those URLs were is then gone.
func TestTelnetRevertNotOfferedOnUnmigratedSpeakers(t *testing.T) {
getpdo := func(marge, stats, swUpdate, bmx string) string {
return "margeServerUrl {\n text: \"" + marge + "\"\n}\n" +
"statsServerUrl {\n text: \"" + stats + "\"\n}\n" +
"swUpdateUrl {\n text: \"" + swUpdate + "\"\n}\n" +
"bmxRegistryUrl {\n text: \"" + bmx + "\"\n}\n->OK\n"
}
for _, test := range []struct {
name string
response string
want bool
}{
{
// Observed on real hardware, and what canonicalBoseTelnetURLs holds.
name: "canonical original variant",
response: getpdo("https://streaming.bose.com", "https://events.api.bosecm.com",
"https://worldwide.bose.com/updates/soundtouch",
"https://content.api.bose.io/bmx/registry/v1/services"),
},
{
// The variant pkg/service/testing/fakespeaker models. Comparing
// against the canonical set alone offered a revert here.
name: "older original variant",
response: getpdo("https://streaming.bose.com", "https://stats.bose.com",
"https://worldwide.bose.com/updates/soundtouch",
"https://bmxservice.bose.com/bmx/registry/v1/services"),
},
{
name: "original with firmware-normalised casing and trailing slash",
response: getpdo("https://STREAMING.BOSE.COM/", "https://events.api.bosecm.com",
"https://worldwide.bose.com/updates/soundtouch",
"https://content.api.bose.io/bmx/registry/v1/services"),
},
{
name: "migrated to AfterTouch",
response: getpdo("http://aftertouch.example:8000", "http://aftertouch.example:8000",
"http://aftertouch.example:8000/updates/soundtouch",
"http://aftertouch.example:8000/bmx/registry/v1/services"),
want: true,
},
{
// A single changed field is still a changed device.
name: "partially migrated",
response: getpdo("http://aftertouch.example:8000", "https://events.api.bosecm.com",
"https://worldwide.bose.com/updates/soundtouch",
"https://content.api.bose.io/bmx/registry/v1/services"),
want: true,
},
} {
t.Run(test.name, func(t *testing.T) {
if got := telnetRevertAvailable(test.response); got != test.want {
t.Errorf("telnetRevertAvailable() = %v, want %v", got, test.want)
}
})
}
}