feat(stream): optional per-agent HTTPS listener with hot-reloadable cert
Foundation for direct, valid-cert browser playback (agent-TLS feature) — the
cert broker + DNS are a later phase; this is inert until a certificate exists.
- StreamServer runs a second TLS listener on https_stream_port (default 11819)
serving the SAME mux as HTTP (11818): same token + CORS gates, no new exposure.
- Certificate is read per-handshake from an atomic holder via tls.Config
GetCertificate, so a cert issued/renewed asynchronously applies without a
restart. SetTLSCertificate / LoadTLSCertificateFromFiles / HasTLSCertificate.
- Daemon arms HTTPS only when a cert pair exists at certs/agent.{crt,key} under
the state dir; without it, no HTTPS port is opened and HTTP + funnel are
unaffected. Shutdown drains the HTTPS server too.
- config: downloads.https_stream_port (default 11819, 0 = disabled).
Tests: real TLS handshake + hot-install (no-cert handshake fails, install →
200), disabled path, missing-cert load error.
This commit is contained in:
parent
132c88b3f0
commit
27bee8cdf4
4 changed files with 294 additions and 8 deletions
|
|
@ -362,6 +362,21 @@ func runDaemonStart() error {
|
||||||
corsExtras := append([]string(nil), cfg.Download.CORSExtraOrigins...)
|
corsExtras := append([]string(nil), cfg.Download.CORSExtraOrigins...)
|
||||||
corsExtras = append(corsExtras, mirrorCORSOrigins(ctx, cfg, userAgent)...)
|
corsExtras = append(corsExtras, mirrorCORSOrigins(ctx, cfg, userAgent)...)
|
||||||
streamSrv.SetCORSAllowedOrigins(corsExtras)
|
streamSrv.SetCORSAllowedOrigins(corsExtras)
|
||||||
|
|
||||||
|
// HTTPS stream listener (agent-TLS feature): only armed when a certificate is
|
||||||
|
// present on disk — without a valid cert there is nothing to serve over TLS,
|
||||||
|
// and the HTTP listener + funnel keep working. The future ACME broker writes
|
||||||
|
// the cert pair to certs/agent.{crt,key} under the agent state dir.
|
||||||
|
if cfg.Download.HTTPSStreamPort > 0 {
|
||||||
|
certPath := filepath.Join(config.DataDir(), "certs", "agent.crt")
|
||||||
|
keyPath := filepath.Join(config.DataDir(), "certs", "agent.key")
|
||||||
|
if err := streamSrv.LoadTLSCertificateFromFiles(certPath, keyPath); err != nil {
|
||||||
|
log.Printf("[stream] HTTPS disabled — no usable certificate at %s (%v)", certPath, err)
|
||||||
|
} else {
|
||||||
|
streamSrv.EnableTLS(cfg.Download.HTTPSStreamPort)
|
||||||
|
log.Printf("[stream] HTTPS armed on port %d with certificate %s", cfg.Download.HTTPSStreamPort, certPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Reap HLS tmpdirs left over from a previous daemon run before we start
|
// Reap HLS tmpdirs left over from a previous daemon run before we start
|
||||||
// accepting new sessions. The in-memory registry doesn't survive a
|
// accepting new sessions. The in-memory registry doesn't survive a
|
||||||
// restart, so without this disk usage grows unbounded across restarts.
|
// restart, so without this disk usage grows unbounded across restarts.
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,15 @@ type DownloadConfig struct {
|
||||||
// Seeding lifecycle (BitTorrent only). Off by default — the daemon leeches
|
// Seeding lifecycle (BitTorrent only). Off by default — the daemon leeches
|
||||||
// then drops the torrent. Enable to keep uploading after a download finishes;
|
// then drops the torrent. Enable to keep uploading after a download finishes;
|
||||||
// seeding stops at whichever target is hit first, or never if both are unset.
|
// seeding stops at whichever target is hit first, or never if both are unset.
|
||||||
SeedEnabled bool `toml:"seed_enabled"` // keep uploading after completion (default: false)
|
SeedEnabled bool `toml:"seed_enabled"` // keep uploading after completion (default: false)
|
||||||
SeedRatio float64 `toml:"seed_ratio"` // stop once uploaded/size reaches this ratio (0 = no ratio target)
|
SeedRatio float64 `toml:"seed_ratio"` // stop once uploaded/size reaches this ratio (0 = no ratio target)
|
||||||
SeedTime string `toml:"seed_time"` // stop after this long since completion, e.g. "24h" (0/"" = no time target)
|
SeedTime string `toml:"seed_time"` // stop after this long since completion, e.g. "24h" (0/"" = no time target)
|
||||||
MetadataTimeout string `toml:"metadata_timeout"` // e.g. "1h", "30m", "0" = unlimited (default: "0")
|
MetadataTimeout string `toml:"metadata_timeout"` // e.g. "1h", "30m", "0" = unlimited (default: "0")
|
||||||
StallTimeout string `toml:"stall_timeout"` // e.g. "30m", "1h", "0" = unlimited (default: "30m")
|
StallTimeout string `toml:"stall_timeout"` // e.g. "30m", "1h", "0" = unlimited (default: "30m")
|
||||||
ListenPort int `toml:"listen_port"` // fixed port for incoming peer connections (default: 42069, 0 = random)
|
ListenPort int `toml:"listen_port"` // fixed port for incoming peer connections (default: 42069, 0 = random)
|
||||||
StreamPort int `toml:"stream_port"` // fixed port for streaming HTTP server (default: 11818)
|
StreamPort int `toml:"stream_port"` // fixed port for streaming HTTP server (default: 11818)
|
||||||
EnableUPnP bool `toml:"enable_upnp"` // map StreamPort to the WAN via UPnP/NAT-PMP (default: false; opt-in)
|
HTTPSStreamPort int `toml:"https_stream_port"` // HTTPS stream listener for direct valid-cert playback (default: 11819, 0 = disabled). Only serves once a certificate is present (agent-TLS feature).
|
||||||
|
EnableUPnP bool `toml:"enable_upnp"` // map StreamPort to the WAN via UPnP/NAT-PMP (default: false; opt-in)
|
||||||
// RequireStreamToken gates remote (non-loopback) /stream + /hls requests on a
|
// RequireStreamToken gates remote (non-loopback) /stream + /hls requests on a
|
||||||
// signed, short-lived token embedded in the URLs the agent reports. Default
|
// signed, short-lived token embedded in the URLs the agent reports. Default
|
||||||
// true (secure by default); loopback callers (local mpv/vlc) are always exempt.
|
// true (secure by default); loopback callers (local mpv/vlc) are always exempt.
|
||||||
|
|
@ -204,6 +205,7 @@ func Default() Config {
|
||||||
MaxConcurrent: 3,
|
MaxConcurrent: 3,
|
||||||
MinFreeDiskMB: 2048, // 2 GiB reserve
|
MinFreeDiskMB: 2048, // 2 GiB reserve
|
||||||
StreamPort: 11818,
|
StreamPort: 11818,
|
||||||
|
HTTPSStreamPort: 11819,
|
||||||
RequireStreamToken: true, // secure by default; loopback exempt
|
RequireStreamToken: true, // secure by default; loopback exempt
|
||||||
Transcode: TranscodeConfig{
|
Transcode: TranscodeConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
|
|
@ -307,6 +309,9 @@ func applyDefaults(cfg *Config, meta toml.MetaData) {
|
||||||
if !meta.IsDefined("downloads", "stream_port") {
|
if !meta.IsDefined("downloads", "stream_port") {
|
||||||
cfg.Download.StreamPort = 11818
|
cfg.Download.StreamPort = 11818
|
||||||
}
|
}
|
||||||
|
if !meta.IsDefined("downloads", "https_stream_port") {
|
||||||
|
cfg.Download.HTTPSStreamPort = 11819
|
||||||
|
}
|
||||||
if !meta.IsDefined("general", "country") {
|
if !meta.IsDefined("general", "country") {
|
||||||
cfg.General.Country = "US"
|
cfg.General.Country = "US"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package engine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -69,6 +70,15 @@ type StreamServer struct {
|
||||||
url string // best single URL (backward compat)
|
url string // best single URL (backward compat)
|
||||||
urls StreamURLs // all available URLs by network type
|
urls StreamURLs // all available URLs by network type
|
||||||
upnpMapping *UPnPMapping
|
upnpMapping *UPnPMapping
|
||||||
|
|
||||||
|
// TLS — optional HTTPS listener for direct, valid-cert browser playback
|
||||||
|
// (agent-TLS feature). httpsPort 0 = disabled. tlsCert holds the current
|
||||||
|
// server certificate, swapped atomically on renewal; the TLS config reads it
|
||||||
|
// via GetCertificate so a renewed cert applies without dropping the listener.
|
||||||
|
// HTTP (port) keeps serving regardless — loopback players + the funnel use it.
|
||||||
|
httpsPort int
|
||||||
|
httpsServer *http.Server
|
||||||
|
tlsCert atomic.Pointer[tls.Certificate]
|
||||||
// enableUPnP gates whether Listen() asks the gateway to publish the
|
// enableUPnP gates whether Listen() asks the gateway to publish the
|
||||||
// stream port to the WAN. UPnP is opt-in (false by default) because
|
// stream port to the WAN. UPnP is opt-in (false by default) because
|
||||||
// /stream and /hls have no auth — exposing them on the public internet
|
// /stream and /hls have no auth — exposing them on the public internet
|
||||||
|
|
@ -152,6 +162,41 @@ func (ss *StreamServer) SetUPnPEnabled(enabled bool) {
|
||||||
ss.enableUPnP = enabled
|
ss.enableUPnP = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnableTLS arms the HTTPS listener on httpsPort. Call before Listen(). The
|
||||||
|
// listener starts even without a certificate installed yet — handshakes fail
|
||||||
|
// until one is set via SetTLSCertificate, so a cert issued asynchronously (the
|
||||||
|
// future ACME broker) applies live without a restart. httpsPort <= 0 is a no-op.
|
||||||
|
func (ss *StreamServer) EnableTLS(httpsPort int) {
|
||||||
|
if httpsPort > 0 {
|
||||||
|
ss.httpsPort = httpsPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTLSCertificate atomically installs or replaces the server certificate used
|
||||||
|
// by the HTTPS listener. Safe to call at any time (startup or on renewal); the
|
||||||
|
// new cert applies to the next TLS handshake without dropping the listener.
|
||||||
|
func (ss *StreamServer) SetTLSCertificate(cert *tls.Certificate) {
|
||||||
|
ss.tlsCert.Store(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadTLSCertificateFromFiles reads a PEM cert+key pair from disk and installs
|
||||||
|
// it. Returns an error if the pair is missing or invalid — the caller decides
|
||||||
|
// whether that's fatal (the daemon treats it as "TLS off, HTTP keeps serving").
|
||||||
|
func (ss *StreamServer) LoadTLSCertificateFromFiles(certPath, keyPath string) error {
|
||||||
|
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load TLS keypair: %w", err)
|
||||||
|
}
|
||||||
|
ss.SetTLSCertificate(&cert)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasTLSCertificate reports whether a server certificate is currently installed.
|
||||||
|
func (ss *StreamServer) HasTLSCertificate() bool { return ss.tlsCert.Load() != nil }
|
||||||
|
|
||||||
|
// HTTPSPort returns the active HTTPS port, or 0 when TLS is disabled.
|
||||||
|
func (ss *StreamServer) HTTPSPort() int { return ss.httpsPort }
|
||||||
|
|
||||||
// SetFFmpegPath sets the ffmpeg binary used by /thumbnail to extract single
|
// SetFFmpegPath sets the ffmpeg binary used by /thumbnail to extract single
|
||||||
// frames on demand. Call before Listen(); empty leaves thumbnails disabled
|
// frames on demand. Call before Listen(); empty leaves thumbnails disabled
|
||||||
// (the handler returns 503). Read-only after Listen() — no locking in the handler.
|
// (the handler returns 503). Read-only after Listen() — no locking in the handler.
|
||||||
|
|
@ -295,6 +340,71 @@ func (ss *StreamServer) Listen(ctx context.Context) error {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Printf("[stream] server listening on port %d", ss.port)
|
log.Printf("[stream] server listening on port %d", ss.port)
|
||||||
|
|
||||||
|
// Optional HTTPS listener (agent-TLS feature). Non-fatal: if it can't bind,
|
||||||
|
// HTTP keeps serving so the funnel + LAN HTTP path are unaffected.
|
||||||
|
if ss.httpsPort > 0 {
|
||||||
|
if err := ss.listenTLS(ctx, mux); err != nil {
|
||||||
|
log.Printf("[stream] HTTPS listener disabled: %v", err)
|
||||||
|
ss.httpsPort = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// listenTLS starts the HTTPS listener on ss.httpsPort serving the same mux as
|
||||||
|
// the HTTP server. The certificate is read per-handshake from the atomic holder
|
||||||
|
// (tlsCert) so a renewed cert applies without restarting the listener; until a
|
||||||
|
// cert is installed, handshakes fail cleanly (the HTTP path is unaffected).
|
||||||
|
func (ss *StreamServer) listenTLS(ctx context.Context, mux http.Handler) error {
|
||||||
|
lc := net.ListenConfig{
|
||||||
|
Control: func(network, address string, c syscall.RawConn) error {
|
||||||
|
return c.Control(func(fd uintptr) { _ = setReuseAddr(fd) })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var listener net.Listener
|
||||||
|
var err error
|
||||||
|
basePort := ss.httpsPort
|
||||||
|
for attempt := 0; attempt < 10; attempt++ {
|
||||||
|
listener, err = lc.Listen(ctx, "tcp", fmt.Sprintf("0.0.0.0:%d", ss.httpsPort))
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "address already in use") {
|
||||||
|
return fmt.Errorf("https listen on %d: %w", ss.httpsPort, err)
|
||||||
|
}
|
||||||
|
ss.httpsPort++
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("https: all ports busy (%d-%d): %w", basePort, ss.httpsPort, err)
|
||||||
|
}
|
||||||
|
ss.httpsPort = listener.Addr().(*net.TCPAddr).Port
|
||||||
|
|
||||||
|
tlsCfg := &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
|
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
if cert := ss.tlsCert.Load(); cert != nil {
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no TLS certificate installed")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ss.httpsServer = &http.Server{
|
||||||
|
Handler: mux,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
TLSConfig: tlsCfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Empty cert/key paths → ServeTLS uses TLSConfig.GetCertificate.
|
||||||
|
if err := ss.httpsServer.ServeTLS(listener, "", ""); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Printf("[stream] HTTPS server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("[stream] HTTPS listening on port %d (certificate installed: %v)", ss.httpsPort, ss.HasTLSCertificate())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -447,6 +557,11 @@ func (ss *StreamServer) Shutdown(ctx context.Context) error {
|
||||||
if ss.hls != nil {
|
if ss.hls != nil {
|
||||||
ss.hls.CloseAll()
|
ss.hls.CloseAll()
|
||||||
}
|
}
|
||||||
|
if ss.httpsServer != nil {
|
||||||
|
if err := ss.httpsServer.Shutdown(ctx); err != nil {
|
||||||
|
log.Printf("[stream] HTTPS shutdown: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
if ss.server != nil {
|
if ss.server != nil {
|
||||||
return ss.server.Shutdown(ctx)
|
return ss.server.Shutdown(ctx)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
151
internal/engine/stream_server_tls_test.go
Normal file
151
internal/engine/stream_server_tls_test.go
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// genSelfSignedCert builds an in-memory self-signed cert valid for 127.0.0.1,
|
||||||
|
// used to exercise the agent's HTTPS listener without any CA/ACME plumbing.
|
||||||
|
func genSelfSignedCert(t *testing.T) (tls.Certificate, *x509.Certificate) {
|
||||||
|
t.Helper()
|
||||||
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("genkey: %v", err)
|
||||||
|
}
|
||||||
|
tmpl := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(1),
|
||||||
|
Subject: pkix.Name{CommonName: "unarr-test"},
|
||||||
|
NotBefore: time.Now().Add(-time.Hour),
|
||||||
|
NotAfter: time.Now().Add(time.Hour),
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||||
|
DNSNames: []string{"localhost"},
|
||||||
|
}
|
||||||
|
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create cert: %v", err)
|
||||||
|
}
|
||||||
|
leaf, err := x509.ParseCertificate(der)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse cert: %v", err)
|
||||||
|
}
|
||||||
|
return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key, Leaf: leaf}, leaf
|
||||||
|
}
|
||||||
|
|
||||||
|
// freePort grabs an ephemeral TCP port and releases it, so the caller can hand
|
||||||
|
// a concrete port number to EnableTLS (which treats 0 as "disabled").
|
||||||
|
func freePort(t *testing.T) int {
|
||||||
|
t.Helper()
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("freePort: %v", err)
|
||||||
|
}
|
||||||
|
p := l.Addr().(*net.TCPAddr).Port
|
||||||
|
l.Close()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStreamServerTLS_HotInstall verifies the HTTPS listener: it starts even
|
||||||
|
// with no certificate (handshake fails), and a certificate installed *after*
|
||||||
|
// Listen applies live via the GetCertificate path — no restart, which is what
|
||||||
|
// the future ACME broker relies on.
|
||||||
|
func TestStreamServerTLS_HotInstall(t *testing.T) {
|
||||||
|
cert, leaf := genSelfSignedCert(t)
|
||||||
|
|
||||||
|
ss := NewStreamServer(0) // HTTP on a random free port
|
||||||
|
ss.EnableTLS(freePort(t))
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
if err := ss.Listen(ctx); err != nil {
|
||||||
|
t.Fatalf("Listen: %v", err)
|
||||||
|
}
|
||||||
|
defer ss.Shutdown(context.Background())
|
||||||
|
|
||||||
|
if ss.HTTPSPort() == 0 {
|
||||||
|
t.Fatal("HTTPSPort() = 0, want the armed HTTPS port")
|
||||||
|
}
|
||||||
|
if ss.HasTLSCertificate() {
|
||||||
|
t.Fatal("no certificate should be installed yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
pool.AddCert(leaf)
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
Transport: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: pool}},
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("https://127.0.0.1:%d/health", ss.HTTPSPort())
|
||||||
|
|
||||||
|
// Before a cert is installed, the handshake must fail.
|
||||||
|
if resp, err := client.Get(url); err == nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
t.Fatal("GET succeeded before a certificate was installed; want handshake failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install the cert — the listener stays up and the next handshake succeeds.
|
||||||
|
ss.SetTLSCertificate(&cert)
|
||||||
|
if !ss.HasTLSCertificate() {
|
||||||
|
t.Fatal("HasTLSCertificate() = false after install")
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt < 20; attempt++ {
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("GET %s = %d, want 200", url, resp.StatusCode)
|
||||||
|
}
|
||||||
|
return // success
|
||||||
|
}
|
||||||
|
t.Fatalf("GET %s never succeeded after cert install: %v", url, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStreamServerTLS_Disabled verifies that with TLS not armed, no HTTPS port
|
||||||
|
// is opened and the HTTP listener is unaffected.
|
||||||
|
func TestStreamServerTLS_Disabled(t *testing.T) {
|
||||||
|
ss := NewStreamServer(0)
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
if err := ss.Listen(ctx); err != nil {
|
||||||
|
t.Fatalf("Listen: %v", err)
|
||||||
|
}
|
||||||
|
defer ss.Shutdown(context.Background())
|
||||||
|
|
||||||
|
if ss.HTTPSPort() != 0 {
|
||||||
|
t.Errorf("HTTPSPort() = %d, want 0 (TLS disabled)", ss.HTTPSPort())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLoadTLSCertificateFromFiles_Missing verifies the loader reports an error
|
||||||
|
// (not a panic) when the cert pair is absent — the daemon treats this as
|
||||||
|
// "TLS off, HTTP keeps serving".
|
||||||
|
func TestLoadTLSCertificateFromFiles_Missing(t *testing.T) {
|
||||||
|
ss := NewStreamServer(0)
|
||||||
|
err := ss.LoadTLSCertificateFromFiles(
|
||||||
|
t.TempDir()+"/nope.crt", t.TempDir()+"/nope.key")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error loading a missing cert pair")
|
||||||
|
}
|
||||||
|
if ss.HasTLSCertificate() {
|
||||||
|
t.Error("no certificate should be installed after a failed load")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue