mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-24 14:47:23 +00:00
Implement dynamic Bose proxy with detailed origin logging and Soundcork fallback
This commit is contained in:
@@ -9,7 +9,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -161,6 +160,7 @@ func main() {
|
||||
cm := initCertificateManager(config.dataDir)
|
||||
sm := setup.NewManager(config.serverURL, ds, cm)
|
||||
server := handlers.NewServer(ds, sm, config.serverURL, config.redact, config.logBody, config.record, config.enableSoundcorkProxy)
|
||||
server.SetSoundcorkURL(config.soundcorkURL)
|
||||
server.SetHTTPServerURL(config.httpsServerURL)
|
||||
server.SetVersionInfo(version, commit, date)
|
||||
server.SetDiscoverySettings(config.discoveryInterval, persisted.DiscoveryEnabled)
|
||||
@@ -203,11 +203,9 @@ func main() {
|
||||
log.Printf("Warning: Failed to setup TLS: %v", err)
|
||||
}
|
||||
|
||||
scProxy := setupSoundcorkProxy(config.soundcorkURL, config.redact, config.logBody, recorder, server)
|
||||
|
||||
startDeviceDiscovery(server)
|
||||
|
||||
r := setupRouter(server, scProxy, config.enableSoundcorkProxy)
|
||||
r := setupRouter(server)
|
||||
|
||||
log.Printf("Go service starting on %s, proxying to %s", config.serverURL, config.soundcorkURL)
|
||||
|
||||
@@ -429,64 +427,6 @@ func initCertificateManager(dataDir string) *certmanager.CertificateManager {
|
||||
return cm
|
||||
}
|
||||
|
||||
func setupSoundcorkProxy(soundcorkURL string, redact, logBody bool, recorder *proxy.Recorder, server *handlers.Server) *httputil.ReverseProxy {
|
||||
target, err := url.Parse(soundcorkURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse Soundcork URL: %v", err)
|
||||
}
|
||||
|
||||
scProxy := httputil.NewSingleHostReverseProxy(target)
|
||||
scProxy.ModifyResponse = func(res *http.Response) error {
|
||||
if etags, ok := res.Header["Etag"]; ok {
|
||||
delete(res.Header, "Etag")
|
||||
res.Header["ETag"] = etags
|
||||
}
|
||||
|
||||
currentLp := proxy.NewLoggingProxy(target.String(), redact)
|
||||
currentLp.LogBody = logBody
|
||||
currentLp.RecordEnabled = server.GetRecordEnabled()
|
||||
currentLp.SetRecorder(recorder)
|
||||
currentLp.LogResponse(res)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
originalScDirector := scProxy.Director
|
||||
scProxy.Director = func(req *http.Request) {
|
||||
originalScDirector(req)
|
||||
|
||||
// Fix X-Forwarded-For bloat by deduplicating
|
||||
if xff := req.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
parts := strings.Split(xff, ",")
|
||||
seen := make(map[string]bool)
|
||||
unique := make([]string, 0, len(parts))
|
||||
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" && !seen[p] {
|
||||
seen[p] = true
|
||||
unique = append(unique, p)
|
||||
}
|
||||
}
|
||||
|
||||
// Limit the number of entries to prevent header overflow
|
||||
if len(unique) > 10 {
|
||||
unique = unique[len(unique)-10:]
|
||||
}
|
||||
|
||||
req.Header.Set("X-Forwarded-For", strings.Join(unique, ", "))
|
||||
}
|
||||
|
||||
currentLp := proxy.NewLoggingProxy(target.String(), redact)
|
||||
currentLp.LogBody = logBody
|
||||
currentLp.RecordEnabled = server.GetRecordEnabled()
|
||||
currentLp.SetRecorder(recorder)
|
||||
currentLp.LogRequest(req)
|
||||
}
|
||||
|
||||
return scProxy
|
||||
}
|
||||
|
||||
func startDeviceDiscovery(server *handlers.Server) {
|
||||
go func() {
|
||||
for {
|
||||
@@ -500,9 +440,9 @@ func startDeviceDiscovery(server *handlers.Server) {
|
||||
}()
|
||||
}
|
||||
|
||||
func setupRouter(server *handlers.Server, scProxy *httputil.ReverseProxy, enableSoundcorkProxy bool) *chi.Mux {
|
||||
func setupRouter(server *handlers.Server) *chi.Mux {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(server.OriginMiddleware)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(server.ShortcutMiddleware)
|
||||
r.Use(server.RecordMiddleware)
|
||||
@@ -526,6 +466,13 @@ func setupRouter(server *handlers.Server, scProxy *httputil.ReverseProxy, enable
|
||||
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
|
||||
})
|
||||
|
||||
// Legacy or direct domain calls without /bmx prefix
|
||||
r.Get("/registry/v1/services", server.HandleBMXRegistry)
|
||||
r.Get("/tunein/v1/playback/station/{stationID}", server.HandleTuneInPlayback)
|
||||
r.Get("/tunein/v1/playback/episodes/{podcastID}", server.HandleTuneInPodcastInfo)
|
||||
r.Get("/tunein/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast)
|
||||
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
|
||||
|
||||
r.Route("/marge", func(r chi.Router) {
|
||||
r.Get("/streaming/sourceproviders", server.HandleMargeSourceProviders)
|
||||
r.Get("/accounts/{account}/full", server.HandleMargeAccountFull)
|
||||
@@ -544,6 +491,23 @@ func setupRouter(server *handlers.Server, scProxy *httputil.ReverseProxy, enable
|
||||
r.Get("/streaming/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
|
||||
})
|
||||
|
||||
// Legacy or direct domain calls without /marge prefix
|
||||
r.Get("/streaming/sourceproviders", server.HandleMargeSourceProviders)
|
||||
r.Get("/accounts/{account}/full", server.HandleMargeAccountFull)
|
||||
r.Post("/streaming/support/power_on", server.HandleMargePowerOn)
|
||||
r.Get("/updates/soundtouch", server.HandleMargeSoftwareUpdate)
|
||||
r.Get("/accounts/{account}/devices/{device}/presets", server.HandleMargePresets)
|
||||
r.Post("/accounts/{account}/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
|
||||
r.Post("/accounts/{account}/devices/{device}/recents", server.HandleMargeAddRecent)
|
||||
r.Post("/accounts/{account}/devices", server.HandleMargeAddDevice)
|
||||
r.Delete("/accounts/{account}/devices/{device}", server.HandleMargeRemoveDevice)
|
||||
r.Get("/streaming/account/{account}/provider_settings", server.HandleMargeProviderSettings)
|
||||
r.Get("/streaming/device/{device}/streaming_token", server.HandleMargeStreamingToken)
|
||||
r.Post("/streaming/support/customersupport", server.HandleMargeCustomerSupport)
|
||||
r.Get("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeGetDeviceSettings)
|
||||
r.Post("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings)
|
||||
r.Get("/streaming/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
|
||||
|
||||
r.Route("/customer", func(r chi.Router) {
|
||||
r.Get("/account/{account}", server.HandleMargeAccountProfile)
|
||||
r.Post("/account/{account}", server.HandleMargeUpdateAccountProfile)
|
||||
@@ -595,11 +559,7 @@ func setupRouter(server *handlers.Server, scProxy *httputil.ReverseProxy, enable
|
||||
r.Get("/devices/{deviceId}/events", server.HandleGetDeviceEvents)
|
||||
})
|
||||
|
||||
if enableSoundcorkProxy {
|
||||
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||
scProxy.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
r.NotFound(server.HandleNotFound)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -33,33 +37,155 @@ func (s *Server) HandleProxyRequest(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
lp := proxy.NewLoggingProxy(target.String(), s.proxyRedact)
|
||||
lp.LogBody = s.proxyLogBody
|
||||
lp.RecordEnabled = s.recordEnabled
|
||||
lp.SetRecorder(s.recorder)
|
||||
s.ServeProxy(target)(w, r)
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
// Update director to set the correct host and path
|
||||
originalDirector := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
originalDirector(req)
|
||||
req.Host = target.Host
|
||||
req.URL.Path = target.Path
|
||||
req.URL.RawQuery = r.URL.RawQuery
|
||||
lp.LogRequest(req)
|
||||
}
|
||||
// ServeProxy returns a handler that proxies to the given target.
|
||||
func (s *Server) ServeProxy(target *url.URL) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
lp := proxy.NewLoggingProxy(target.String(), s.proxyRedact)
|
||||
lp.LogBody = s.proxyLogBody
|
||||
lp.RecordEnabled = s.recordEnabled
|
||||
lp.SetRecorder(s.recorder)
|
||||
|
||||
proxy.ModifyResponse = func(res *http.Response) error {
|
||||
// Generic Header Preservation
|
||||
if etags, ok := res.Header["Etag"]; ok {
|
||||
delete(res.Header, "Etag")
|
||||
res.Header["ETag"] = etags
|
||||
rp := httputil.NewSingleHostReverseProxy(target)
|
||||
rp.Transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
lp.LogResponse(res)
|
||||
// Update director to set the correct host and path
|
||||
originalDirector := rp.Director
|
||||
rp.Director = func(req *http.Request) {
|
||||
originalDirector(req)
|
||||
req.Host = target.Host
|
||||
// If target has a path, we should probably append or replace.
|
||||
// For Bose upstream, it's usually just the domain.
|
||||
if target.Path != "" && target.Path != "/" {
|
||||
req.URL.Path = target.Path
|
||||
}
|
||||
|
||||
return nil
|
||||
lp.LogRequest(req)
|
||||
}
|
||||
|
||||
rp.ModifyResponse = func(res *http.Response) error {
|
||||
res.Header.Set("X-Proxy-Origin", "upstream")
|
||||
// Generic Header Preservation
|
||||
if etags, ok := res.Header["Etag"]; ok {
|
||||
delete(res.Header, "Etag")
|
||||
res.Header["ETag"] = etags
|
||||
}
|
||||
|
||||
lp.LogResponse(res)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
rp.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleNotFound handles requests that don't match any route.
|
||||
func (s *Server) HandleNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
if s.enableSoundcorkProxy {
|
||||
s.HandleSoundcorkWithFallback(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(w, r)
|
||||
s.HandleBoseProxy(w, r)
|
||||
}
|
||||
|
||||
// HandleSoundcorkWithFallback tries Soundcork first, then Bose if Soundcork returns 404 or fails.
|
||||
func (s *Server) HandleSoundcorkWithFallback(w http.ResponseWriter, r *http.Request) {
|
||||
target, _ := url.Parse(s.soundcorkURL)
|
||||
|
||||
// Buffer request body if any, to allow multiple proxy attempts
|
||||
var bodyBytes []byte
|
||||
if r.Body != nil {
|
||||
bodyBytes, _ = io.ReadAll(r.Body)
|
||||
_ = r.Body.Close()
|
||||
}
|
||||
|
||||
// We use a custom response writer to catch 404s
|
||||
rw := &fallbackResponseWriter{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
buffer: &bytes.Buffer{},
|
||||
}
|
||||
|
||||
// Create a shallow copy of the request to avoid side effects between attempts
|
||||
r2 := r.Clone(r.Context())
|
||||
if bodyBytes != nil {
|
||||
r2.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
} else {
|
||||
r2.Body = nil
|
||||
}
|
||||
|
||||
// Remove RequestURI as it's not allowed in client requests
|
||||
r2.RequestURI = ""
|
||||
|
||||
s.ServeProxy(target)(rw, r2)
|
||||
|
||||
if rw.statusCode == http.StatusNotFound || rw.statusCode == http.StatusBadGateway || rw.statusCode == http.StatusServiceUnavailable {
|
||||
log.Printf("[PROXY] Soundcork returned %d for %s, falling back to Bose", rw.statusCode, r.URL.Path)
|
||||
|
||||
if !rw.wroteHeader {
|
||||
// Restore original body if any
|
||||
if bodyBytes != nil {
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
}
|
||||
|
||||
s.HandleBoseProxy(w, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fallbackResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
wroteHeader bool
|
||||
buffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func (rw *fallbackResponseWriter) WriteHeader(code int) {
|
||||
rw.statusCode = code
|
||||
if code != http.StatusNotFound && code != http.StatusBadGateway && code != http.StatusServiceUnavailable {
|
||||
rw.wroteHeader = true
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *fallbackResponseWriter) Write(b []byte) (int, error) {
|
||||
if rw.statusCode == http.StatusNotFound || rw.statusCode == http.StatusBadGateway || rw.statusCode == http.StatusServiceUnavailable {
|
||||
return len(b), nil // Drop the body
|
||||
}
|
||||
|
||||
rw.wroteHeader = true
|
||||
|
||||
return rw.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// HandleBoseProxy proxies the request to the Bose upstream.
|
||||
func (s *Server) HandleBoseProxy(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Host
|
||||
if host == "" {
|
||||
host = "streaming.bose.com"
|
||||
}
|
||||
|
||||
// Default to HTTPS for Bose services
|
||||
scheme := "https"
|
||||
if strings.HasPrefix(host, "localhost") || strings.HasPrefix(host, "127.0.0.1") || strings.HasPrefix(host, "::1") {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
targetURL := scheme + "://" + host
|
||||
|
||||
target, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
log.Printf("[PROXY_ERR] Failed to parse target URL %s: %v", targetURL, err)
|
||||
http.Error(w, "Invalid upstream host", http.StatusBadGateway)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
s.ServeProxy(target)(w, r)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server) {
|
||||
target, _ := url.Parse(targetURL)
|
||||
proxy := &reverseProxy{target: target}
|
||||
server := &Server{ds: ds}
|
||||
server := NewServer(ds, nil, "http://localhost:8000", false, false, false, false)
|
||||
server.SetSoundcorkURL(targetURL)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(server.OriginMiddleware)
|
||||
r.Use(server.ShortcutMiddleware)
|
||||
r.Use(server.RecordMiddleware)
|
||||
|
||||
r.Get("/", server.HandleRoot)
|
||||
|
||||
// Setup media and web directories for tests
|
||||
@@ -29,6 +29,13 @@ func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server)
|
||||
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
|
||||
})
|
||||
|
||||
// Legacy or direct domain calls without /bmx prefix
|
||||
r.Get("/registry/v1/services", server.HandleBMXRegistry)
|
||||
r.Get("/tunein/v1/playback/station/{stationID}", server.HandleTuneInPlayback)
|
||||
r.Get("/tunein/v1/playback/episodes/{podcastID}", server.HandleTuneInPodcastInfo)
|
||||
r.Get("/tunein/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast)
|
||||
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
|
||||
|
||||
// Setup Marge for tests
|
||||
r.Route("/marge", func(r chi.Router) {
|
||||
r.Get("/streaming/sourceproviders", server.HandleMargeSourceProviders)
|
||||
@@ -48,6 +55,23 @@ func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server)
|
||||
r.Get("/streaming/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
|
||||
})
|
||||
|
||||
// Legacy or direct domain calls without /marge prefix
|
||||
r.Get("/streaming/sourceproviders", server.HandleMargeSourceProviders)
|
||||
r.Get("/accounts/{account}/full", server.HandleMargeAccountFull)
|
||||
r.Post("/streaming/support/power_on", server.HandleMargePowerOn)
|
||||
r.Get("/updates/soundtouch", server.HandleMargeSoftwareUpdate)
|
||||
r.Get("/accounts/{account}/devices/{device}/presets", server.HandleMargePresets)
|
||||
r.Post("/accounts/{account}/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
|
||||
r.Post("/accounts/{account}/devices/{device}/recents", server.HandleMargeAddRecent)
|
||||
r.Post("/accounts/{account}/devices", server.HandleMargeAddDevice)
|
||||
r.Delete("/accounts/{account}/devices/{device}", server.HandleMargeRemoveDevice)
|
||||
r.Get("/streaming/account/{account}/provider_settings", server.HandleMargeProviderSettings)
|
||||
r.Get("/streaming/device/{device}/streaming_token", server.HandleMargeStreamingToken)
|
||||
r.Post("/streaming/support/customersupport", server.HandleMargeCustomerSupport)
|
||||
r.Get("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeGetDeviceSettings)
|
||||
r.Post("/streaming/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings)
|
||||
r.Get("/streaming/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
|
||||
|
||||
// Setup Customer for tests
|
||||
r.Route("/customer", func(r chi.Router) {
|
||||
r.Get("/account/{account}", server.HandleMargeAccountProfile)
|
||||
@@ -74,19 +98,12 @@ func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server)
|
||||
r.Get("/ca.crt", server.HandleGetCACert)
|
||||
})
|
||||
|
||||
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.ServeHTTP(w, r)
|
||||
})
|
||||
r.NotFound(server.HandleNotFound)
|
||||
|
||||
return r, server
|
||||
}
|
||||
|
||||
type reverseProxy struct {
|
||||
target *url.URL
|
||||
}
|
||||
|
||||
func (p *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Simplified proxy for testing
|
||||
w.WriteHeader(http.StatusAccepted) // Custom status to identify proxy hit in tests
|
||||
_, _ = w.Write([]byte("Proxied to " + p.target.String()))
|
||||
func init() {
|
||||
// Silence logger for tests
|
||||
// log.SetOutput(io.Discard)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
// OriginMiddleware returns a middleware that logs whether the request was handled "self" or "upstream".
|
||||
func (s *Server) OriginMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||
|
||||
next.ServeHTTP(ww, r)
|
||||
|
||||
origin := "self"
|
||||
if ww.Header().Get("X-Proxy-Origin") != "" {
|
||||
origin = "upstream"
|
||||
}
|
||||
|
||||
log.Printf("[LOG] %s %s | %d | %s | %v", r.Method, r.URL.Path, ww.Status(), origin, time.Since(start))
|
||||
})
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -30,6 +31,7 @@ type Server struct {
|
||||
enableSoundcorkProxy bool
|
||||
shortcuts map[string]int
|
||||
recorder *proxy.Recorder
|
||||
UpstreamProxy http.Handler
|
||||
Version string
|
||||
Commit string
|
||||
Date string
|
||||
@@ -41,7 +43,7 @@ func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, pro
|
||||
ds: ds,
|
||||
sm: sm,
|
||||
serverURL: serverURL,
|
||||
soundcorkURL: serverURL,
|
||||
soundcorkURL: "http://localhost:8001",
|
||||
proxyRedact: proxyRedact,
|
||||
proxyLogBody: proxyLogBody,
|
||||
recordEnabled: recordEnabled,
|
||||
@@ -101,6 +103,14 @@ func (s *Server) SetHTTPServerURL(url string) {
|
||||
s.httpsServerURL = url
|
||||
}
|
||||
|
||||
// SetSoundcorkURL sets the URL for the Soundcork backend.
|
||||
func (s *Server) SetSoundcorkURL(url string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.soundcorkURL = url
|
||||
}
|
||||
|
||||
// SetRecorder sets the recorder for the server.
|
||||
func (s *Server) SetRecorder(r *proxy.Recorder) {
|
||||
s.recorder = r
|
||||
|
||||
@@ -218,6 +218,7 @@ func (m *Manager) GetMigrationSummary(deviceIP, targetURL, proxyURL string, opti
|
||||
"events.api.bosecm.com",
|
||||
"bose-prod.apigee.net",
|
||||
"worldwide.bose.com",
|
||||
"music.api.bose.com",
|
||||
}
|
||||
|
||||
var hostsLines []string
|
||||
|
||||
Reference in New Issue
Block a user