unarr/internal/engine/stream_server_tls_test.go

152 lines
4.4 KiB
Go
Raw Normal View History

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")
}
}