From d94b1bc067546fd8a63e7efd5508c44ba3beae36 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 31 May 2026 23:32:56 +0200 Subject: [PATCH] fix(web): trust service CA and send a known target for TTS soundtouch-web's "Speak" feature proxies to the AfterTouch service's /setup/tts/speak endpoint. Two issues blocked it end to end. 1. TLS: the proxy used http.DefaultClient, which trusts only system roots, so the HTTPS call to a service using its own self-signed CA failed with "x509: certificate signed by unknown authority". Add a --service-ca flag (SERVICE_CA env) that loads the CA PEM, appends it to the system pool, and uses a custom client for the TTS call. 2. Target: soundtouch-web sent device.Client.Host() (a full base URL like http://ip:8090), but the service's SSRF guard exact-matches the target against bare datastore IPs, returning "host ... is not a known device". Prefer the device ID (the canonical key) and send a bare-IP host fallback. Also normalize the incoming host in resolveTTSHost so a URL/host:port form still resolves; it still only ever returns a datastore IP, so the SSRF guarantee is unchanged. Adds unit tests for the CA client builder, hostOnly, and resolveTTSHost (including the preserved unknown-host/device rejections). Documents --service-ca in the soundtouch-web README and TROUBLESHOOTING guide. Wires SERVICE_URL and SERVICE_CA (empty defaults) into the Raspberry Pi install-web.sh env file and documents them in the Pi guide. Co-Authored-By: Claude Opus 4.8 (1M context) --- cmd/soundtouch-web/README.md | 33 +++++- cmd/soundtouch-web/main.go | 16 +++ docs/content/docs/guides/RASPBERRY-PI.md | 18 ++++ docs/content/docs/guides/TROUBLESHOOTING.md | 46 ++++++++ pkg/service/handlers/handlers_tts.go | 28 ++++- pkg/service/handlers/handlers_tts_test.go | 59 +++++++++++ pkg/service/soundtouchweb/handler.go | 17 +++ pkg/service/soundtouchweb/handlers_tts.go | 40 ++++++- pkg/service/soundtouchweb/service_client.go | 49 +++++++++ .../soundtouchweb/service_client_test.go | 100 ++++++++++++++++++ scripts/raspberry-pi/install-web.sh | 16 +++ 11 files changed, 416 insertions(+), 6 deletions(-) create mode 100644 pkg/service/soundtouchweb/service_client.go create mode 100644 pkg/service/soundtouchweb/service_client_test.go diff --git a/cmd/soundtouch-web/README.md b/cmd/soundtouch-web/README.md index 8b9da49..a93279b 100644 --- a/cmd/soundtouch-web/README.md +++ b/cmd/soundtouch-web/README.md @@ -93,11 +93,38 @@ go build -o soundtouch-web ### Command Line Options ``` --port string Web server port (default "8080") --host string Specific SoundTouch device host (optional, enables single-device mode) --help Show help information +--port, -p string HTTP port to listen on (default "8080", env PORT) +--bind string Address for the HTTP listener: host, IP, or interface name (env BIND_ADDR) +--interface string Network interface name for mDNS/UPnP discovery (env DISCOVERY_INTERFACE) +--devices strings SoundTouch device IP(s) to add manually, repeatable (env SOUNDTOUCH_DEVICES) +--service-url string AfterTouch service base URL, e.g. https://soundtouch.local (env SERVICE_URL) +--service-ca string Path to the AfterTouch service CA certificate (PEM) to trust (env SERVICE_CA) +--help, -h Show help information ``` +### Text-to-Speech (TTS) + +TTS synthesis and the Bose `app_key` live in the AfterTouch service, not in +soundtouch-web, so the "Speak" feature proxies to the service's +`/setup/tts/speak` endpoint. To use it, point soundtouch-web at the service +with `--service-url`. + +When the service is served over HTTPS with its own self-signed certificate +(the default), soundtouch-web also needs to trust the service's CA, or the +proxied call fails with `x509: certificate signed by unknown authority`. Pass +the CA with `--service-ca`; it is the service's `/certs/ca.crt`: + +```bash +soundtouch-web \ + --service-url https://soundtouch.fritz.box \ + --service-ca /path/to/certs/ca.crt +``` + +The CA is appended to the system trust store, so a service URL that uses a +publicly trusted certificate keeps working without the flag. The target +speaker must be known to the service (it resolves the speaker against its own +device datastore). + ## Usage ### Accessing the Interface diff --git a/cmd/soundtouch-web/main.go b/cmd/soundtouch-web/main.go index f887cad..e647865 100644 --- a/cmd/soundtouch-web/main.go +++ b/cmd/soundtouch-web/main.go @@ -81,6 +81,11 @@ func main() { Usage: "AfterTouch service base URL (e.g. https://soundtouch.local). Required for custom stream URLs to work as presets via LOCAL_INTERNET_RADIO", EnvVars: []string{"SERVICE_URL"}, }, + &cli.StringFlag{ + Name: "service-ca", + Usage: "Path to the AfterTouch service CA certificate (PEM) to trust for server-side calls such as TTS. Typically the service's /certs/ca.crt. Appended to the system trust store", + EnvVars: []string{"SERVICE_CA"}, + }, }, Action: func(c *cli.Context) error { port := c.String("port") @@ -116,6 +121,17 @@ func main() { webApp.RepoURL = repoURL webApp.ServiceURL = strings.TrimRight(c.String("service-url"), "/") + if caPath := c.String("service-ca"); caPath != "" { + client, err := soundtouchweb.NewServiceHTTPClient(caPath) + if err != nil { + log.Fatalf("--service-ca: %v", err) + } + + webApp.ServiceClient = client + + log.Printf("Trusting AfterTouch service CA from %s", sanitizeLog(caPath)) + } + discoveryService := soundtouchweb.NewDiscoveryService(ifaceName) // Discover devices on startup diff --git a/docs/content/docs/guides/RASPBERRY-PI.md b/docs/content/docs/guides/RASPBERRY-PI.md index cc27765..65c1790 100644 --- a/docs/content/docs/guides/RASPBERRY-PI.md +++ b/docs/content/docs/guides/RASPBERRY-PI.md @@ -161,6 +161,8 @@ PORT=8080 BIND_ADDR= DISCOVERY_INTERFACE= SOUNDTOUCH_DEVICES= +SERVICE_URL= +SERVICE_CA= ``` `SOUNDTOUCH_DEVICES` accepts a comma-separated list of IP addresses for manual @@ -171,6 +173,22 @@ network: SOUNDTOUCH_DEVICES=192.0.2.1,192.0.2.2 ``` +`SERVICE_URL` links `soundtouch-web` to your `soundtouch-service` instance, +which is required for Text-to-Speech ("Speak"). When the service is served +over HTTPS with its own self-signed certificate (the default), also set +`SERVICE_CA` to that CA certificate, or the proxied TTS call fails with +`x509: certificate signed by unknown authority`. The CA is the service's +`/certs/ca.crt` (also downloadable from `GET /setup/ca.crt`). For +example: + +```bash +SERVICE_URL=https://soundtouch.local +SERVICE_CA=/var/lib/soundtouch-service/certs/ca.crt +``` + +With a plain `http://` `SERVICE_URL`, `SERVICE_CA` is unused (no TLS) and can +be left empty. + After editing the env file: ```bash diff --git a/docs/content/docs/guides/TROUBLESHOOTING.md b/docs/content/docs/guides/TROUBLESHOOTING.md index ab8c0f3..967e2ff 100644 --- a/docs/content/docs/guides/TROUBLESHOOTING.md +++ b/docs/content/docs/guides/TROUBLESHOOTING.md @@ -623,6 +623,52 @@ fmt.Printf("Current source: %s, status: %s\n", nowPlaying.Source, nowPlaying.PlayStatus) ``` +### ❌ soundtouch-web TTS fails with `certificate signed by unknown authority` + +**Symptoms:** +``` +TTS service request failed: Post "https://soundtouch.fritz.box/setup/tts/speak": +tls: failed to verify certificate: x509: certificate signed by unknown authority +``` + +**Cause:** TTS synthesis and the Bose app key live in `soundtouch-service`, +so `soundtouch-web` proxies the "Speak" action to the service. When the +service is served over HTTPS with its own self-signed certificate (the +default — see `GET /setup/ca.crt`), `soundtouch-web` doesn't trust that CA out +of the box, so the proxied call fails verification. + +**Solution:** start `soundtouch-web` with `--service-ca` pointing at the +service's CA certificate (its `/certs/ca.crt`, or the file served at +`/setup/ca.crt`): + +```bash +soundtouch-web \ + --service-url https://soundtouch.fritz.box \ + --service-ca /path/to/certs/ca.crt +``` + +`SERVICE_CA` is the equivalent environment variable. The CA is appended to the +system trust store, so a service URL that uses a publicly trusted certificate +needs no flag. + +### ❌ soundtouch-web TTS returns `host ... is not a known device` + +**Symptoms:** +``` +TTS service returned 400: {"error":"host http://192.0.2.10:8090 is not a known device"} +``` + +**Cause:** the service only plays TTS on speakers it knows (an SSRF guard: +the target is matched against the service's device datastore, never taken +verbatim from the request). + +**Solution:** make sure the target speaker is known to `soundtouch-service` +(discovered or manually added, and migrated to AfterTouch), not only to +`soundtouch-web`'s own discovery. Check with `GET /setup/devices` on the +service. (Recent `soundtouch-web` versions identify the speaker by its device +ID and a bare IP, so this error otherwise indicates the speaker simply isn't +registered with the service.) + --- ## 📡 **WebSocket Issues** diff --git a/pkg/service/handlers/handlers_tts.go b/pkg/service/handlers/handlers_tts.go index c25a1a5..8aab845 100644 --- a/pkg/service/handlers/handlers_tts.go +++ b/pkg/service/handlers/handlers_tts.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "net" "net/http" "net/url" "strconv" @@ -152,9 +153,15 @@ func (s *Server) HandleSpeakerAuth(w http.ResponseWriter, _ *http.Request) { // baseURL -> the outbound request). Match by DeviceID, or by Host equal to a // known device's IP; either way the returned string is the datastore's // IPAddress, not the caller-supplied value. +// +// The incoming Host is normalized to a bare host (scheme and port stripped) +// before the match so callers that pass a base URL (e.g. "http://ip:8090", +// which is what a client's Host() returns) still resolve. This only makes the +// needle comparable to the datastore's bare IPs; the result is still always a +// datastore IP, so the SSRF guarantee is unchanged. func (s *Server) resolveTTSHost(req ttsSpeakRequest) (string, error) { deviceID := strings.TrimSpace(req.DeviceID) - host := strings.TrimSpace(req.Host) + host := ttsHostOnly(strings.TrimSpace(req.Host)) if deviceID == "" && host == "" { return "", fmt.Errorf("either deviceId or host is required") @@ -183,6 +190,25 @@ func (s *Server) resolveTTSHost(req ttsSpeakRequest) (string, error) { return "", fmt.Errorf("host %s is not a known device", host) } +// ttsHostOnly reduces a base URL or host:port to a bare host so it can be +// compared against the datastore's bare IPs. Inputs that are already bare are +// returned unchanged. The empty string maps to the empty string. +func ttsHostOnly(raw string) string { + if raw == "" { + return "" + } + + if u, err := url.Parse(raw); err == nil && u.Host != "" { + return u.Hostname() + } + + if host, _, err := net.SplitHostPort(raw); err == nil { + return host + } + + return raw +} + // HandleTTSMedia serves a synthesized clip by id for the speaker to fetch. func (s *Server) HandleTTSMedia(w http.ResponseWriter, r *http.Request) { svc := s.ttsSvc() diff --git a/pkg/service/handlers/handlers_tts_test.go b/pkg/service/handlers/handlers_tts_test.go index 527ec32..42f50fe 100644 --- a/pkg/service/handlers/handlers_tts_test.go +++ b/pkg/service/handlers/handlers_tts_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/gesellix/bose-soundtouch/pkg/models" "github.com/gesellix/bose-soundtouch/pkg/service/datastore" "github.com/gesellix/bose-soundtouch/pkg/service/tts" "github.com/go-chi/chi/v5" @@ -181,3 +182,61 @@ func TestHandleTTSSpeakValidation(t *testing.T) { }) } } + +// TestResolveTTSHost covers the SSRF guard plus the host-normalization that +// lets a base URL (what a client's Host() returns, e.g. soundtouch-web sends +// "http://ip:8090") resolve to a known device. The result is always the +// datastore's bare IP, never the caller's value. +func TestResolveTTSHost(t *testing.T) { + ds := datastore.NewDataStore(t.TempDir()) + if err := ds.Initialize(); err != nil { + t.Fatalf("Initialize: %v", err) + } + + if err := ds.SaveDeviceInfo("1000001", "DEVICEID01", &models.ServiceDeviceInfo{ + DeviceID: "DEVICEID01", + AccountID: "1000001", + IPAddress: "192.0.2.10", + }); err != nil { + t.Fatalf("SaveDeviceInfo: %v", err) + } + + server := NewServer(ds, nil, "http://localhost:8001", false, false, false) + + cases := []struct { + name string + req ttsSpeakRequest + want string + wantErr bool + }{ + {"bare ip", ttsSpeakRequest{Host: "192.0.2.10"}, "192.0.2.10", false}, + {"base url", ttsSpeakRequest{Host: "http://192.0.2.10:8090"}, "192.0.2.10", false}, + {"host:port", ttsSpeakRequest{Host: "192.0.2.10:8090"}, "192.0.2.10", false}, + {"by device id", ttsSpeakRequest{DeviceID: "DEVICEID01"}, "192.0.2.10", false}, + // SSRF guard intact: unknown targets are rejected even in URL form. + {"unknown host url", ttsSpeakRequest{Host: "http://203.0.113.99:8090"}, "", true}, + {"unknown device", ttsSpeakRequest{DeviceID: "NOPE"}, "", true}, + {"no target", ttsSpeakRequest{}, "", true}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := server.resolveTTSHost(tc.req) + if tc.wantErr { + if err == nil { + t.Fatalf("expected error, got %q", got) + } + + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if got != tc.want { + t.Fatalf("got %q, want %q", got, tc.want) + } + }) + } +} diff --git a/pkg/service/soundtouchweb/handler.go b/pkg/service/soundtouchweb/handler.go index 5ff6b60..2bb7059 100644 --- a/pkg/service/soundtouchweb/handler.go +++ b/pkg/service/soundtouchweb/handler.go @@ -42,9 +42,26 @@ type WebApp struct { RepoURL string ServiceURL string + // ServiceClient is used for server-side calls to the AfterTouch service + // (currently the TTS proxy). When nil, serviceHTTPClient falls back to + // http.DefaultClient. Set it via NewServiceHTTPClient to trust the + // service's self-signed CA. + ServiceClient *http.Client + discoveryStatus atomic.Value // stores *webtypes.DiscoveryStatus } +// serviceHTTPClient returns the client used for outbound calls to the +// AfterTouch service, falling back to http.DefaultClient when no CA-trusting +// client was configured. +func (app *WebApp) serviceHTTPClient() *http.Client { + if app.ServiceClient != nil { + return app.ServiceClient + } + + return http.DefaultClient +} + // DeviceEntry pairs a device id with its connection. Used by // DeviceSnapshot so callers can iterate without holding the lock. type DeviceEntry struct { diff --git a/pkg/service/soundtouchweb/handlers_tts.go b/pkg/service/soundtouchweb/handlers_tts.go index ea3deb8..81c4fa8 100644 --- a/pkg/service/soundtouchweb/handlers_tts.go +++ b/pkg/service/soundtouchweb/handlers_tts.go @@ -5,12 +5,36 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" + "net/url" "strings" "github.com/go-chi/chi/v5" ) +// hostOnly reduces a base URL or host:port to a bare host (IP or hostname). +// device.Client.Host() returns a full base URL like "http://192.168.0.2:8090", +// but the AfterTouch service matches the TTS target against bare datastore IPs, +// so we strip the scheme and port before sending it. Inputs that are already +// bare ("192.168.0.2") are returned unchanged. +func hostOnly(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "" + } + + if u, err := url.Parse(raw); err == nil && u.Host != "" { + return u.Hostname() + } + + if host, _, err := net.SplitHostPort(raw); err == nil { + return host + } + + return raw +} + // HandleAPISpeakText synthesizes and plays text on a device. The Web UI talks // to speakers directly for most controls, but TTS synthesis (Google Cloud) and // the Bose app_key live in the AfterTouch service, so this proxies to the @@ -60,10 +84,22 @@ func (app *WebApp) HandleAPISpeakText(w http.ResponseWriter, r *http.Request) { return } + // Identify the target speaker for the service. Prefer the DeviceID (the + // canonical, unambiguous key the service matches in its datastore). Also + // send a bare-IP host as a fallback: device.Client.Host() is a full base + // URL (http://ip:8090), which the service's exact-match SSRF guard + // (resolveTTSHost) would reject, so strip it down to host-only. payload := map[string]interface{}{ - "host": device.Client.Host(), "text": req.Text, } + if device.DeviceInfo != nil && device.DeviceInfo.DeviceID != "" { + payload["deviceId"] = device.DeviceInfo.DeviceID + } + + if h := hostOnly(device.Client.Host()); h != "" { + payload["host"] = h + } + if req.Language != "" { payload["language"] = req.Language } @@ -90,7 +126,7 @@ func (app *WebApp) HandleAPISpeakText(w http.ResponseWriter, r *http.Request) { upstream.Header.Set("Content-Type", "application/json") - resp, err := http.DefaultClient.Do(upstream) + resp, err := app.serviceHTTPClient().Do(upstream) if err != nil { app.sendError(w, fmt.Sprintf("TTS service request failed: %v", err), http.StatusBadGateway) return diff --git a/pkg/service/soundtouchweb/service_client.go b/pkg/service/soundtouchweb/service_client.go new file mode 100644 index 0000000..f73724a --- /dev/null +++ b/pkg/service/soundtouchweb/service_client.go @@ -0,0 +1,49 @@ +package soundtouchweb + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "os" + "time" +) + +// NewServiceHTTPClient builds an *http.Client that trusts the AfterTouch +// service's CA certificate (PEM at caPath) in addition to the system trust +// store. soundtouch-web uses it for the only server-side call it makes to the +// service (the TTS proxy in handlers_tts.go): the service serves a self-signed +// certificate signed by its own "AfterTouch Local Root CA", which isn't in any +// system trust store, so http.DefaultClient would reject it with +// "x509: certificate signed by unknown authority". +// +// The CA is appended to a copy of the system pool (not a fresh empty one) so a +// deployment whose service URL happens to use a publicly trusted certificate +// keeps working. +func NewServiceHTTPClient(caPath string) (*http.Client, error) { + pem, err := os.ReadFile(caPath) + if err != nil { + return nil, fmt.Errorf("read CA: %w", err) + } + + pool, err := x509.SystemCertPool() + if err != nil || pool == nil { + pool = x509.NewCertPool() + } + + if !pool.AppendCertsFromPEM(pem) { + return nil, fmt.Errorf("no valid certificate found in %s", caPath) + } + + return &http.Client{ + // TTS round-trips through Google Cloud synthesis and speaker playback, + // so allow more than the bare connect time. + Timeout: 30 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: pool, + MinVersion: tls.VersionTLS12, + }, + }, + }, nil +} diff --git a/pkg/service/soundtouchweb/service_client_test.go b/pkg/service/soundtouchweb/service_client_test.go new file mode 100644 index 0000000..6296016 --- /dev/null +++ b/pkg/service/soundtouchweb/service_client_test.go @@ -0,0 +1,100 @@ +package soundtouchweb + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net/http" + "os" + "path/filepath" + "testing" + "time" +) + +// writeTestCA generates a throwaway self-signed CA and returns its PEM path. +func writeTestCA(t *testing.T) string { + t.Helper() + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("generate key: %v", err) + } + + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "Test CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + } + + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + t.Fatalf("create cert: %v", err) + } + + path := filepath.Join(t.TempDir(), "ca.crt") + if err := os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600); err != nil { + t.Fatalf("write ca: %v", err) + } + + return path +} + +func TestNewServiceHTTPClientValidCA(t *testing.T) { + client, err := NewServiceHTTPClient(writeTestCA(t)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + tr, ok := client.Transport.(*http.Transport) + if !ok { + t.Fatalf("transport type = %T, want *http.Transport", client.Transport) + } + + if tr.TLSClientConfig == nil || tr.TLSClientConfig.RootCAs == nil { + t.Fatal("expected a non-nil RootCAs pool") + } + + if client.Timeout == 0 { + t.Fatal("expected a non-zero timeout") + } +} + +func TestNewServiceHTTPClientMissingFile(t *testing.T) { + if _, err := NewServiceHTTPClient(filepath.Join(t.TempDir(), "absent.crt")); err == nil { + t.Fatal("expected an error for a missing file") + } +} + +func TestNewServiceHTTPClientNoCertInFile(t *testing.T) { + path := filepath.Join(t.TempDir(), "junk.crt") + if err := os.WriteFile(path, []byte("not a pem certificate"), 0o600); err != nil { + t.Fatalf("write junk: %v", err) + } + + if _, err := NewServiceHTTPClient(path); err == nil { + t.Fatal("expected an error for a file with no certificate") + } +} + +func TestHostOnly(t *testing.T) { + cases := map[string]string{ + "http://192.168.178.35:8090": "192.168.178.35", + "https://soundtouch.local": "soundtouch.local", + "192.168.178.35:8090": "192.168.178.35", + "192.168.178.35": "192.168.178.35", + "": "", + } + + for in, want := range cases { + if got := hostOnly(in); got != want { + t.Errorf("hostOnly(%q) = %q, want %q", in, got, want) + } + } +} diff --git a/scripts/raspberry-pi/install-web.sh b/scripts/raspberry-pi/install-web.sh index 29834db..8456160 100644 --- a/scripts/raspberry-pi/install-web.sh +++ b/scripts/raspberry-pi/install-web.sh @@ -14,6 +14,12 @@ set -euo pipefail # HTTP_PORT=8081 \ # bash install-web.sh # +# # With an AfterTouch service link for TTS (HTTPS + self-signed CA): +# sudo \ +# SERVICE_URL=https://soundtouch.local \ +# SERVICE_CA=/var/lib/soundtouch-service/certs/ca.crt \ +# bash install-web.sh +# # Or with a version argument to perform an update: # sudo bash install-web.sh v0.104.0 # @@ -48,6 +54,13 @@ BIND_ADDR="${BIND_ADDR:-}" DISCOVERY_INTERFACE="${DISCOVERY_INTERFACE:-}" SOUNDTOUCH_DEVICES="${SOUNDTOUCH_DEVICES:-}" +# Optional AfterTouch service link (needed for TTS / "Speak"). +# SERVICE_URL: base URL of soundtouch-service, e.g. https://soundtouch.local +# SERVICE_CA: path to the service CA cert when it serves HTTPS with its own +# self-signed certificate, e.g. /var/lib/soundtouch-service/certs/ca.crt +SERVICE_URL="${SERVICE_URL:-}" +SERVICE_CA="${SERVICE_CA:-}" + # Override if you want to force a specific asset suffix: # ARCH_ASSET=linux-armv7|linux-arm64|linux-amd64 ARCH_ASSET="${ARCH_ASSET:-}" @@ -179,6 +192,7 @@ self_update() { export IS_SELF_UPDATE="true" export VERSION HTTP_PORT BIND_ADDR DISCOVERY_INTERFACE SOUNDTOUCH_DEVICES + export SERVICE_URL SERVICE_CA export BIN_PATH CONFIG_DIR ENV_FILE SERVICE_USER SERVICE_GROUP exec "${SCRIPT_PATH}" "$@" @@ -192,6 +206,8 @@ write_env_file() { "BIND_ADDR=${BIND_ADDR}" "DISCOVERY_INTERFACE=${DISCOVERY_INTERFACE}" "SOUNDTOUCH_DEVICES=${SOUNDTOUCH_DEVICES}" + "SERVICE_URL=${SERVICE_URL}" + "SERVICE_CA=${SERVICE_CA}" ) if [[ ! -f "${ENV_FILE}" ]]; then