diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go
index b7e12a1..7536726 100644
--- a/cmd/soundtouch-service/main.go
+++ b/cmd/soundtouch-service/main.go
@@ -872,17 +872,6 @@ func setupRouter(server *handlers.Server) *chi.Mux {
r.Get("/", server.HandleRoot)
r.Get("/health", server.HandleHealth)
- // DEPRECATED: /probe/{token}[/*] backs the deprecated round-trip
- // probe (RunTelnetRoundTripProbe). Removed in a follow-up commit
- // alongside the rest of the active-probe code path; see
- // docs/analysis/TELNET-MIGRATION-METHOD.md §9.8.
- //
- // Telnet round-trip probe inbound. The orchestrator temporarily
- // sets the speaker's swUpdateUrl to /probe/{token}; the speaker
- // then fans out a request that we observe here. Catch-all suffix
- // because firmware may append path components (e.g. /index.xml).
- r.Get("/probe/{token}", server.HandleProbeInbound)
- r.Get("/probe/{token}/*", server.HandleProbeInbound)
// Passive peer-reachability probe. Registers a device IP with the
// in-process observer, nudges :8090/swUpdateCheck, and waits for
// any inbound from that IP. Used post-migration where the daemon
@@ -1108,8 +1097,6 @@ func setupRouter(server *handlers.Server) *chi.Mux {
r.Post("/test-connection/{deviceId}", server.HandleTestConnection)
r.Post("/test-hosts/{deviceId}", server.HandleTestHostsRedirection)
r.Post("/test-dns/{deviceId}", server.HandleTestDNSRedirection)
- // DEPRECATED: see §9.8; removed in the follow-up commit.
- r.Post("/telnet-probe/{deviceId}", server.HandleTelnetProbe)
r.Get("/ca.crt", server.HandleGetCACert)
r.Get("/proxy-settings", server.HandleGetProxySettings)
r.Post("/proxy-settings", server.HandleUpdateProxySettings)
diff --git a/pkg/service/handlers/handlers_telnet_probe.go b/pkg/service/handlers/handlers_telnet_probe.go
deleted file mode 100644
index 8c20d25..0000000
--- a/pkg/service/handlers/handlers_telnet_probe.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package handlers
-
-import (
- "encoding/json"
- "net/http"
- "time"
-
- "github.com/go-chi/chi/v5"
-)
-
-// DEPRECATED: handlers_telnet_probe.go (HandleTelnetProbe,
-// HandleProbeInbound, telnetProbeTimeout, telnetProbeResponse) is
-// scheduled for removal. The swUpdate daemon caches its target URL
-// at boot; the active flip in RunTelnetRoundTripProbe never reaches
-// the running daemon. See docs/analysis/TELNET-MIGRATION-METHOD.md
-// §9.8. Replaced by HandlePeerProbe (handlers_peer_probe.go).
-
-// telnetProbeTimeout caps how long the orchestrator waits for the
-// device's outbound swUpdateCheck fan-out to land on /probe/{token}.
-// 6s lines up with the existing telnet preflight budgets and is well
-// above the median observed round-trip (<1s on FW 27.0.6).
-const telnetProbeTimeout = 6 * time.Second
-
-// DEPRECATED: see the file-level note above.
-//
-// HandleProbeInbound is the catch-all for /probe/{token}/* — the path
-// the round-trip orchestrator sets as the speaker's swUpdateUrl. Any
-// hit signals the registered channel; the response body is a minimal
-// XML stub so the speaker's swUpdateCheck doesn't error out on a
-// missing structure.
-func (s *Server) HandleProbeInbound(w http.ResponseWriter, r *http.Request) {
- token := chi.URLParam(r, "token")
- if token != "" {
- s.probes.Signal(token)
- }
-
- w.Header().Set("Content-Type", "application/xml")
- _, _ = w.Write([]byte(``))
-}
-
-// telnetProbeResponse is the body of POST /setup/telnet-probe/{deviceId}.
-type telnetProbeResponse struct {
- OK bool `json:"ok"`
- Result any `json:"result,omitempty"`
- Error string `json:"error,omitempty"`
-}
-
-// DEPRECATED: see the file-level note above.
-//
-// HandleTelnetProbe runs the SSH-less round-trip reachability check.
-// Generates a token, temporarily points the speaker's swUpdateUrl at
-// /probe/{token} via telnet, triggers :8090/swUpdateCheck, and reports
-// whether the device's outbound landed on our service within
-// telnetProbeTimeout.
-//
-// Query params:
-// - target_url (optional) — defaults to the configured server URL.
-func (s *Server) HandleTelnetProbe(w http.ResponseWriter, r *http.Request) {
- deviceID := chi.URLParam(r, "deviceId")
- if deviceID == "" {
- writeJSONError(w, http.StatusBadRequest, "Device ID is required")
- return
- }
-
- deviceIP, err := s.resolveDeviceIDToIP(deviceID)
- if err != nil {
- writeJSONError(w, http.StatusNotFound, err.Error())
- return
- }
-
- targetURL := r.URL.Query().Get("target_url")
- if targetURL == "" {
- targetURL = s.sm.ServerURL
- }
-
- result, err := s.sm.RunTelnetRoundTripProbe(deviceIP, targetURL, s.probes, telnetProbeTimeout)
-
- w.Header().Set("Content-Type", "application/json")
-
- body := telnetProbeResponse{
- OK: err == nil && result != nil && result.Reached,
- Result: result,
- }
- if err != nil {
- body.Error = err.Error()
- }
-
- if err := json.NewEncoder(w).Encode(body); err != nil {
- http.Error(w, "Failed to encode response", http.StatusInternalServerError)
- }
-}
diff --git a/pkg/service/handlers/probe_registry.go b/pkg/service/handlers/probe_registry.go
deleted file mode 100644
index 697faff..0000000
--- a/pkg/service/handlers/probe_registry.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package handlers
-
-import "sync"
-
-// DEPRECATED: probeRegistry is scheduled for removal. The active
-// round-trip probe it backs cannot work without a reboot (the swUpdate
-// daemon caches its URL at boot). See
-// docs/analysis/TELNET-MIGRATION-METHOD.md §9.8. Replaced by
-// peerObserver (peer_observer.go) which keys on device IP and supports
-// passive post-migration reachability checks.
-
-// probeRegistry is the rendezvous between the telnet round-trip probe
-// orchestrator (which registers a one-shot token and waits for an
-// inbound) and the /probe/{token}/* HTTP handler (which closes the
-// matching channel when the speaker's swUpdateCheck fan-out lands).
-type probeRegistry struct {
- mu sync.Mutex
- pending map[string]chan struct{}
-}
-
-func newProbeRegistry() *probeRegistry {
- return &probeRegistry{pending: make(map[string]chan struct{})}
-}
-
-// Register creates a one-shot channel keyed by token. The caller waits
-// on the returned channel for the matching inbound; the channel is
-// closed by Signal. Must be paired with Forget to release the entry.
-func (r *probeRegistry) Register(token string) <-chan struct{} {
- r.mu.Lock()
- defer r.mu.Unlock()
-
- ch := make(chan struct{})
- r.pending[token] = ch
-
- return ch
-}
-
-// Signal closes the channel for token (idempotent — repeated hits on
-// the same probe path are tolerated, the device sometimes retries).
-// Returns true when a matching registration existed.
-func (r *probeRegistry) Signal(token string) bool {
- r.mu.Lock()
- defer r.mu.Unlock()
-
- ch, ok := r.pending[token]
- if !ok {
- return false
- }
-
- select {
- case <-ch:
- // already closed; nothing to do
- default:
- close(ch)
- }
-
- return true
-}
-
-// Forget removes the entry. Safe to call after Register's channel has
-// been closed (or never signalled); does not affect already-returned
-// channels.
-func (r *probeRegistry) Forget(token string) {
- r.mu.Lock()
- defer r.mu.Unlock()
-
- delete(r.pending, token)
-}
diff --git a/pkg/service/handlers/probe_registry_test.go b/pkg/service/handlers/probe_registry_test.go
deleted file mode 100644
index 07f21fe..0000000
--- a/pkg/service/handlers/probe_registry_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package handlers
-
-import (
- "testing"
- "time"
-)
-
-func TestProbeRegistry_RegisterSignalForget(t *testing.T) {
- r := newProbeRegistry()
-
- ch := r.Register("abc123")
- if ch == nil {
- t.Fatal("Register returned nil channel")
- }
-
- if !r.Signal("abc123") {
- t.Error("Signal returned false for registered token")
- }
-
- select {
- case <-ch:
- // channel closed as expected
- case <-time.After(100 * time.Millisecond):
- t.Error("Signal did not close the channel")
- }
-
- // Signal again on the same token must be idempotent (no panic on
- // double close).
- if !r.Signal("abc123") {
- t.Error("second Signal returned false")
- }
-
- r.Forget("abc123")
-
- // After Forget, Signal returns false.
- if r.Signal("abc123") {
- t.Error("Signal returned true after Forget")
- }
-}
-
-func TestProbeRegistry_UnknownToken(t *testing.T) {
- r := newProbeRegistry()
- if r.Signal("never-registered") {
- t.Error("Signal returned true for unregistered token")
- }
-}
diff --git a/pkg/service/handlers/server.go b/pkg/service/handlers/server.go
index 5b9b02b..2652e01 100644
--- a/pkg/service/handlers/server.go
+++ b/pkg/service/handlers/server.go
@@ -61,12 +61,7 @@ type Server struct {
amazonClientSecret string
amazonRedirectURI string
amazonService *amazon.Service
- // DEPRECATED: probes (probeRegistry) is removed in a follow-up
- // commit alongside the round-trip probe handlers; see
- // docs/analysis/TELNET-MIGRATION-METHOD.md §9.8. peerObserver is
- // the replacement substrate.
- probes *probeRegistry
- peerObserver *peerObserver
+ peerObserver *peerObserver
}
// RequestSnapshot represents an immutable snapshot of an HTTP request.
@@ -101,7 +96,6 @@ func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, pro
recordEnabled: recordEnabled,
discoveryInterval: 5 * time.Minute,
discoveryEnabled: true,
- probes: newProbeRegistry(),
peerObserver: newPeerObserver(),
}
diff --git a/pkg/service/handlers/web/js/script.js b/pkg/service/handlers/web/js/script.js
index 7e30170..86394fb 100644
--- a/pkg/service/handlers/web/js/script.js
+++ b/pkg/service/handlers/web/js/script.js
@@ -3047,39 +3047,6 @@ async function checkPeerReachability(deviceId) {
}
}
-// checkTelnetRoundTrip is the SSH-less alternative to the curl-from-
-// device HTTPS test: temporarily flips the speaker's swUpdateUrl via
-// telnet, triggers :8090/swUpdateCheck, and reports whether the
-// device's outbound landed on our /probe/{token} catch-all. See
-// pkg/service/setup/telnet_probe.go for the orchestration details.
-//
-// DEPRECATED: scheduled for removal. The swUpdate daemon caches its
-// URL at startup and ignores live `sys configuration` writes, so the
-// flip never reaches the running daemon. On a migrated speaker, the
-// passive observer (checkPeerReachability) is the honest test; on an
-// unmigrated speaker, no round-trip via swUpdateUrl is possible
-// without a reboot — Apply + reboot is the only path. The orchestrator
-// no longer calls this function; it is retained only until the
-// deletion commit lands.
-async function checkTelnetRoundTrip(deviceId, targetUrl) {
- try {
- const q = `?target_url=${encodeURIComponent(targetUrl)}`;
- const resp = await fetch(`/setup/telnet-probe/${encodeURIComponent(deviceId)}${q}`, {method: "POST"});
- const result = await resp.json();
- if (result.ok) {
- const ms = result.result && result.result.elapsed_ms;
- return {status: "ok", message: ms ? `reached in ${ms}ms` : undefined};
- }
- if (result.error) return {status: "fail", message: result.error.split("\n")[0]};
- if (result.result && result.result.reached === false) {
- return {status: "fail", message: "probe inbound not observed before timeout"};
- }
- return {status: "fail", message: "probe failed"};
- } catch (e) {
- return {status: "fail", message: String(e)};
- }
-}
-
// --- Pre-flight panel: orchestrator ---------------------------------
// runApplyPreflight runs the checks visible in the pre-flight panel.
diff --git a/pkg/service/setup/telnet_probe.go b/pkg/service/setup/telnet_probe.go
deleted file mode 100644
index b54be18..0000000
--- a/pkg/service/setup/telnet_probe.go
+++ /dev/null
@@ -1,205 +0,0 @@
-package setup
-
-import (
- "crypto/rand"
- "encoding/hex"
- "errors"
- "fmt"
- "net/url"
- "strings"
- "time"
-)
-
-// DEPRECATED: ProbeRegistrar, TelnetProbeResult, generateProbeToken,
-// and RunTelnetRoundTripProbe are scheduled for removal. The swUpdate
-// daemon caches its target URL at boot and ignores live `sys
-// configuration` writes, so the temporary flip never reaches the
-// running daemon. See docs/analysis/TELNET-MIGRATION-METHOD.md §9.8.
-// Replaced by Manager.RunPeerReachabilityProbe in peer_probe.go for
-// the migrated case; pre-migration validation relies on the other
-// pre-flight rows plus the Apply + reboot cycle.
-
-// ProbeRegistrar is the rendezvous between the round-trip probe
-// orchestrator (which registers a token and waits) and an HTTP layer
-// (which signals the channel when the device's outbound lands on the
-// matching /probe/{token}/* path). The handlers package wires its
-// probeRegistry into this interface.
-type ProbeRegistrar interface {
- Register(token string) <-chan struct{}
- Forget(token string)
-}
-
-// TelnetProbeResult records what RunTelnetRoundTripProbe observed.
-// Reached reports whether the device's outbound landed on our service
-// within the configured timeout; Restored reports whether the
-// temporary swUpdateUrl override was reverted to the captured
-// original. The orchestrator always attempts the restore even on the
-// failure path, so a Reached=false + Restored=true is the common
-// "couldn't reach us, device is back to its old configuration" state.
-type TelnetProbeResult struct {
- Reached bool `json:"reached"`
- Restored bool `json:"restored"`
- OriginalURL string `json:"original_url,omitempty"`
- ProbeURL string `json:"probe_url,omitempty"`
- ElapsedMs int64 `json:"elapsed_ms"`
- Logs string `json:"logs,omitempty"`
-}
-
-// generateProbeToken returns a random hex token suitable for use in a
-// URL path. 12 bytes → 24 hex chars; collision probability is
-// negligible for the dozens-of-probes-per-session scope.
-func generateProbeToken() (string, error) {
- b := make([]byte, 12)
- if _, err := rand.Read(b); err != nil {
- return "", err
- }
-
- return hex.EncodeToString(b), nil
-}
-
-// DEPRECATED: see the package-level note above.
-//
-// RunTelnetRoundTripProbe is the SSH-less reachability check that
-// fills the gap the curl-from-device HTTPS test leaves on USB-
-// unlock-refusing speakers. The sequence:
-//
-// 1. Telnet `getpdo CurrentSystemConfiguration` to capture the
-// speaker's current swUpdateUrl.
-// 2. Generate a token, register a one-shot signal channel under it.
-// 3. Telnet `sys configuration swUpdateUrl /probe/`
-// to point the runtime layer at our service. Deliberately NOT
-// `envswitch boseurls set …` — the persistence layer keeps the
-// original, so a reboot heals the device naturally if our
-// restore step fails.
-// 4. HTTP GET `:8090/swUpdateCheck` to make the speaker
-// fan out a request to the new swUpdateUrl.
-// 5. Wait on the registered channel up to timeout.
-// 6. Telnet `sys configuration swUpdateUrl ` to revert.
-//
-// Returns Reached=true only if the inbound landed before the timeout
-// fired. Restore runs in a deferred call so it executes even when
-// earlier steps fail.
-func (m *Manager) RunTelnetRoundTripProbe(deviceIP, targetURL string, registrar ProbeRegistrar, timeout time.Duration) (*TelnetProbeResult, error) {
- if m.NewTelnet == nil {
- return nil, errors.New("telnet probe not configured: Manager.NewTelnet is nil")
- }
-
- if registrar == nil {
- return nil, errors.New("telnet probe not configured: registrar is nil")
- }
-
- parsedTarget, err := url.Parse(strings.TrimSpace(targetURL))
- if err != nil || parsedTarget.Host == "" {
- return nil, fmt.Errorf("invalid target URL %q: hostname required", targetURL)
- }
-
- result := &TelnetProbeResult{}
-
- var logs strings.Builder
-
- t := m.NewTelnet(deviceIP)
- if dialErr := t.Dial(); dialErr != nil {
- return nil, fmt.Errorf("telnet dial %s:17000 failed: %w", deviceIP, dialErr)
- }
-
- defer func() { _ = t.Close() }()
-
- // 1. Capture the current swUpdateUrl from getpdo. If the device
- // refuses getpdo we cannot safely flip the URL — abort.
- verify, err := t.SendCommand("getpdo CurrentSystemConfiguration")
- if err != nil {
- return nil, fmt.Errorf("getpdo CurrentSystemConfiguration failed: %w", err)
- }
-
- if isCommandNotFound(verify) {
- return nil, errors.New("device rejected getpdo CurrentSystemConfiguration — cannot capture original URL")
- }
-
- parsed := parseGetpdoConfig(verify)
-
- originalURL := parsed["swUpdateUrl"]
- if originalURL == "" {
- return nil, errors.New("could not parse original swUpdateUrl from getpdo response")
- }
-
- result.OriginalURL = originalURL
- fmt.Fprintf(&logs, "Original swUpdateUrl: %s\n", originalURL)
-
- // 2. Token + registration.
- token, err := generateProbeToken()
- if err != nil {
- return nil, fmt.Errorf("generate probe token: %w", err)
- }
-
- probeCh := registrar.Register(token)
- defer registrar.Forget(token)
-
- probeURL := fmt.Sprintf("%s://%s/probe/%s", parsedTarget.Scheme, parsedTarget.Host, token)
- result.ProbeURL = probeURL
-
- fmt.Fprintf(&logs, "Probe URL: %s\n", probeURL)
-
- // 3. Set swUpdateUrl to the probe URL via telnet. Deferred restore
- // runs regardless of subsequent failures.
- setCmd := "sys configuration swUpdateUrl " + probeURL
-
- resp, err := t.SendCommand(setCmd)
- if err != nil {
- return result, fmt.Errorf("telnet set swUpdateUrl failed: %w", err)
- }
-
- if isCommandNotFound(resp) {
- return result, fmt.Errorf("device rejected %q (firmware does not expose this command)", setCmd)
- }
-
- fmt.Fprintf(&logs, "→ %s\n%s\n", setCmd, strings.TrimRight(resp, "\r\n"))
-
- defer func() {
- restoreCmd := "sys configuration swUpdateUrl " + originalURL
- if rresp, rerr := t.SendCommand(restoreCmd); rerr == nil && !isCommandNotFound(rresp) {
- result.Restored = true
-
- fmt.Fprintf(&logs, "→ %s (restored)\n%s\n", restoreCmd, strings.TrimRight(rresp, "\r\n"))
- } else if rerr != nil {
- fmt.Fprintf(&logs, "Restore failed: %v (envswitch persistence will heal on next reboot)\n", rerr)
- }
-
- result.Logs = logs.String()
- }()
-
- // 4. Trigger the device's outbound via :8090/swUpdateCheck. The
- // HTTP call is fire-and-forget — we don't need its response, only
- // that the device fans out to the probe URL we just set.
- swCheckURL := fmt.Sprintf("http://%s:8090/swUpdateCheck", deviceIP)
-
- go func() {
- if m.HTTPGet == nil {
- return
- }
-
- resp, err := m.HTTPGet(swCheckURL)
- if err != nil {
- return
- }
-
- _ = resp.Body.Close()
- }()
-
- // 5. Wait for the inbound.
- start := time.Now()
-
- select {
- case <-probeCh:
- result.Reached = true
-
- fmt.Fprintf(&logs, "Probe inbound observed after %v\n", time.Since(start))
- case <-time.After(timeout):
- result.Reached = false
-
- fmt.Fprintf(&logs, "Probe timed out after %v\n", timeout)
- }
-
- result.ElapsedMs = time.Since(start).Milliseconds()
-
- return result, nil
-}
diff --git a/pkg/service/setup/telnet_probe_test.go b/pkg/service/setup/telnet_probe_test.go
deleted file mode 100644
index a57a04b..0000000
--- a/pkg/service/setup/telnet_probe_test.go
+++ /dev/null
@@ -1,305 +0,0 @@
-package setup
-
-import (
- "errors"
- "net/http"
- "net/http/httptest"
- "strings"
- "sync"
- "testing"
- "time"
-)
-
-// fakeRegistrar is a deterministic ProbeRegistrar for unit tests. It
-// exposes the channel it returned from Register so the test can
-// signal it manually to simulate the device's outbound landing on our
-// service.
-type fakeRegistrar struct {
- mu sync.Mutex
- channels map[string]chan struct{}
- registered []string
- forgotten []string
-}
-
-func newFakeRegistrar() *fakeRegistrar {
- return &fakeRegistrar{channels: map[string]chan struct{}{}}
-}
-
-func (r *fakeRegistrar) Register(token string) <-chan struct{} {
- r.mu.Lock()
- defer r.mu.Unlock()
- ch := make(chan struct{})
- r.channels[token] = ch
- r.registered = append(r.registered, token)
- return ch
-}
-
-func (r *fakeRegistrar) Forget(token string) {
- r.mu.Lock()
- defer r.mu.Unlock()
- delete(r.channels, token)
- r.forgotten = append(r.forgotten, token)
-}
-
-// fire closes the channel for the most-recently-registered token so
-// the orchestrator's select wakes.
-func (r *fakeRegistrar) fire() {
- r.mu.Lock()
- defer r.mu.Unlock()
- if len(r.registered) == 0 {
- return
- }
- last := r.registered[len(r.registered)-1]
- ch, ok := r.channels[last]
- if !ok {
- return
- }
- select {
- case <-ch:
- default:
- close(ch)
- }
-}
-
-// telnetProbeManager builds a Manager pre-wired for probe tests:
-// fakeTelnet supplies getpdo and sys configuration responses, and
-// HTTPGet is overridden so the :8090/swUpdateCheck trigger doesn't
-// reach out to anything real. The httptest server simulates the
-// device's swUpdateCheck so we observe the request landing.
-func telnetProbeManager(ft *fakeTelnet, onTrigger func()) *Manager {
- m := &Manager{
- ServerURL: "http://example:8000",
- NewTelnet: func(string) TelnetClient { return ft },
- HTTPGet: func(url string) (*http.Response, error) {
- if onTrigger != nil {
- onTrigger()
- }
- rr := httptest.NewRecorder()
- rr.WriteHeader(200)
- return rr.Result(), nil
- },
- }
- return m
-}
-
-func TestRunTelnetRoundTripProbe_HappyPath(t *testing.T) {
- target := "http://example:8000"
- ft := &fakeTelnet{
- responses: map[string]string{
- "getpdo CurrentSystemConfiguration": `swUpdateUrl {
- text: "https://worldwide.bose.com/updates/soundtouch"
-}
-`,
- },
- }
- registrar := newFakeRegistrar()
-
- // The :8090 trigger should cause the device to fan out to the
- // probe URL. In the test we simulate by closing the channel from
- // the trigger goroutine.
- m := telnetProbeManager(ft, func() { registrar.fire() })
-
- // fakeTelnet returns "Command not found\n" for unmapped commands.
- // We need `sys configuration swUpdateUrl …` (any value) to look
- // like a success. Pre-populate the map with the canonical happy
- // response — the test will fill in the actual command after
- // generateProbeToken runs, but we can pattern-match instead.
- // Trick: keep the responses map empty for the set command and
- // override the fakeTelnet behaviour.
- ft.responses = map[string]string{
- "getpdo CurrentSystemConfiguration": `swUpdateUrl {
- text: "https://worldwide.bose.com/updates/soundtouch"
-}
-`,
- }
- // The set/restore commands aren't in the responses map; the
- // fakeTelnet defaults to "Command not found\n" which would fail
- // the run. Override by injecting an OK response for any command
- // starting with "sys configuration swUpdateUrl ".
- origSendCommand := ft.SendCommand
- _ = origSendCommand // unused — fakeTelnet uses a method, not a field.
-
- // Use a custom telnet client that returns OK for sys configuration.
- customTelnet := &probeFakeTelnet{
- responses: ft.responses,
- }
- m.NewTelnet = func(string) TelnetClient { return customTelnet }
-
- result, err := m.RunTelnetRoundTripProbe("192.0.2.1", target, registrar, 2*time.Second)
- if err != nil {
- t.Fatalf("RunTelnetRoundTripProbe: %v", err)
- }
-
- if !result.Reached {
- t.Errorf("Reached = false, want true")
- }
-
- if !result.Restored {
- t.Errorf("Restored = false, want true (restore command should have succeeded)")
- }
-
- if result.OriginalURL != "https://worldwide.bose.com/updates/soundtouch" {
- t.Errorf("OriginalURL = %q, want the captured value", result.OriginalURL)
- }
-
- if !strings.Contains(result.ProbeURL, "/probe/") {
- t.Errorf("ProbeURL = %q, want a /probe/ path", result.ProbeURL)
- }
-
- if len(registrar.forgotten) != 1 {
- t.Errorf("Forget calls = %d, want 1", len(registrar.forgotten))
- }
-}
-
-// probeFakeTelnet returns OK for any "sys configuration swUpdateUrl …"
-// command and falls back to the responses map for everything else.
-type probeFakeTelnet struct {
- responses map[string]string
- commands []string
-}
-
-func (f *probeFakeTelnet) Dial() error { return nil }
-func (f *probeFakeTelnet) Close() error { return nil }
-func (f *probeFakeTelnet) Probe() (string, error) { return "", nil }
-func (f *probeFakeTelnet) SendCommand(cmd string) (string, error) {
- f.commands = append(f.commands, cmd)
- if resp, ok := f.responses[cmd]; ok {
- return resp, nil
- }
- if strings.HasPrefix(cmd, "sys configuration swUpdateUrl ") {
- return "OK\n", nil
- }
- return "Command not found\n", nil
-}
-
-func TestRunTelnetRoundTripProbe_TimeoutWhenInboundNeverArrives(t *testing.T) {
- target := "http://example:8000"
- registrar := newFakeRegistrar()
-
- // Do NOT fire the registrar — simulate the device not making the
- // outbound (e.g. firewall, hung firmware).
- m := telnetProbeManager(nil, nil)
- m.NewTelnet = func(string) TelnetClient {
- return &probeFakeTelnet{
- responses: map[string]string{
- "getpdo CurrentSystemConfiguration": `swUpdateUrl {
- text: "https://worldwide.bose.com/updates/soundtouch"
-}
-`,
- },
- }
- }
-
- result, err := m.RunTelnetRoundTripProbe("192.0.2.1", target, registrar, 100*time.Millisecond)
- if err != nil {
- t.Fatalf("expected nil error on timeout, got %v", err)
- }
-
- if result.Reached {
- t.Errorf("Reached = true, want false (no inbound was fired)")
- }
-
- if !result.Restored {
- t.Errorf("Restored = false, want true even on the timeout path")
- }
-}
-
-func TestRunTelnetRoundTripProbe_AbortsWhenGetpdoMissesSwUpdateURL(t *testing.T) {
- target := "http://example:8000"
- registrar := newFakeRegistrar()
- m := telnetProbeManager(nil, nil)
- m.NewTelnet = func(string) TelnetClient {
- return &probeFakeTelnet{
- responses: map[string]string{
- // No swUpdateUrl key — older firmware variant. We refuse
- // to flip anything because we wouldn't know what to
- // restore to.
- "getpdo CurrentSystemConfiguration": `margeServerUrl {
- text: "https://streaming.bose.com"
-}
-`,
- },
- }
- }
-
- _, err := m.RunTelnetRoundTripProbe("192.0.2.1", target, registrar, 100*time.Millisecond)
- if err == nil {
- t.Fatal("expected error when getpdo response has no swUpdateUrl, got nil")
- }
-
- if !strings.Contains(err.Error(), "swUpdateUrl") {
- t.Errorf("err = %v, want it to mention the missing field", err)
- }
-}
-
-func TestRunTelnetRoundTripProbe_AbortsWhenDeviceRejectsSysConfiguration(t *testing.T) {
- target := "http://example:8000"
- registrar := newFakeRegistrar()
- m := telnetProbeManager(nil, nil)
- m.NewTelnet = func(string) TelnetClient {
- return &probeFakeTelnetReject{
- responses: map[string]string{
- "getpdo CurrentSystemConfiguration": `swUpdateUrl {
- text: "https://worldwide.bose.com/updates/soundtouch"
-}
-`,
- },
- }
- }
-
- _, err := m.RunTelnetRoundTripProbe("192.0.2.1", target, registrar, 100*time.Millisecond)
- if err == nil {
- t.Fatal("expected error when device rejects sys configuration, got nil")
- }
-
- if !strings.Contains(err.Error(), "firmware does not expose") {
- t.Errorf("err = %v, want a firmware-rejection message", err)
- }
-}
-
-type probeFakeTelnetReject struct {
- responses map[string]string
-}
-
-func (f *probeFakeTelnetReject) Dial() error { return nil }
-func (f *probeFakeTelnetReject) Close() error { return nil }
-func (f *probeFakeTelnetReject) Probe() (string, error) { return "", nil }
-func (f *probeFakeTelnetReject) SendCommand(cmd string) (string, error) {
- if resp, ok := f.responses[cmd]; ok {
- return resp, nil
- }
- // Any other command, including sys configuration, is rejected.
- return "Command not found\n", nil
-}
-
-func TestRunTelnetRoundTripProbe_DialFailure(t *testing.T) {
- registrar := newFakeRegistrar()
- m := &Manager{
- ServerURL: "http://example:8000",
- NewTelnet: func(string) TelnetClient {
- return &fakeTelnet{dialErr: errors.New("connection refused")}
- },
- }
-
- _, err := m.RunTelnetRoundTripProbe("192.0.2.1", "http://example:8000", registrar, 100*time.Millisecond)
- if err == nil {
- t.Fatal("expected dial error, got nil")
- }
-
- if !strings.Contains(err.Error(), "connection refused") {
- t.Errorf("err = %v, want to wrap connection refused", err)
- }
-}
-
-func TestRunTelnetRoundTripProbe_InvalidTargetURL(t *testing.T) {
- registrar := newFakeRegistrar()
- m := &Manager{
- ServerURL: "http://example:8000",
- NewTelnet: func(string) TelnetClient { return &fakeTelnet{} },
- }
-
- _, err := m.RunTelnetRoundTripProbe("192.0.2.1", "not-a-url", registrar, 100*time.Millisecond)
- if err == nil {
- t.Fatal("expected error on invalid target URL, got nil")
- }
-}