Files
OpsLog/internal/relaydev/httpstls_test.go
T
rouggy 0bab7f05b9 feat(relays): accept a self-signed certificate on a generic HTTP board
HTTPS to a relay board could not work. Nearly every board that offers it signs
its own certificate — there is no authority anywhere that could have signed it —
so the request failed verification before it left.

A checkbox, per board, off by default. Not a blanket switch, because the other
HTTPS case is real and opposite: a board reached from outside through a proxy
with a genuine certificate, where verification is the only thing standing
between an antenna switch and the internet. Same setting, two boards, different
answers.

Off by default is only safe if the failure explains itself, so a certificate
error now names the box to tick. Go's own "x509: certificate signed by unknown
authority" is accurate and tells an operator nothing about what to do next.

Shown only once an https:// URL is actually in the board's configuration. A
board on plain HTTP has no certificate to argue about, and an option that cannot
matter yet is one more thing to wonder about.

The flag joins the driver cache key: ticking it has to rebuild the driver, or
the cached one would go on refusing the certificate with the verifying client it
already holds.

The boards that take a bare host — WebSwitch, KMTronic — keep verification. An
https:// typed there is the proxy case by construction, since they default to
plain HTTP on the LAN.

Three tests against a real self-signed TLS server: accepted with the box,
refused with a message naming it without the box, and one board's setting not
leaking into another's.
2026-08-17 10:57:29 +02:00

81 lines
2.9 KiB
Go

package relaydev
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
)
// A relay board on the LAN signs its own certificate — there is no authority
// anywhere that could have signed it. httptest.NewTLSServer presents exactly
// that: a certificate from an unknown issuer, which is what the hardware does.
func selfSignedRelay(t *testing.T) (*httptest.Server, func() []string) {
t.Helper()
var mu sync.Mutex
var got []string
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
got = append(got, r.URL.Path)
mu.Unlock()
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(srv.Close)
return srv, func() []string {
mu.Lock()
defer mu.Unlock()
return append([]string(nil), got...)
}
}
// With the box ticked, the board answers.
func TestHTTPSRelayWithASelfSignedCertificate(t *testing.T) {
srv, seen := selfSignedRelay(t)
d := NewHTTPGeneric(nil, nil, srv.URL+"/on/{relay}", srv.URL+"/off/{relay}", "", "", 2, nil, true)
if err := d.Set(context.Background(), 1, true); err != nil {
t.Fatalf("Set over HTTPS: %v", err)
}
if paths := seen(); len(paths) != 1 || paths[0] != "/on/1" {
t.Errorf("the board was asked for %v, want /on/1", paths)
}
}
// Without it, the request is refused — and the refusal has to name the box.
//
// Go's own message, "x509: certificate signed by unknown authority", is
// accurate and tells an operator nothing about what to do next. This is the
// difference between a dead end and an instruction, and it is the whole reason
// the default can safely stay OFF.
func TestARefusedCertificateNamesTheSetting(t *testing.T) {
srv, seen := selfSignedRelay(t)
d := NewHTTPGeneric(nil, nil, srv.URL+"/on/{relay}", srv.URL+"/off/{relay}", "", "", 2, nil, false)
err := d.Set(context.Background(), 1, true)
if err == nil {
t.Fatal("an unverifiable certificate was accepted with the box unticked")
}
if !strings.Contains(err.Error(), "self-signed") {
t.Errorf("the refusal reads %q — it does not say which setting to change", err)
}
if len(seen()) != 0 {
t.Error("the request reached the board despite the certificate being refused")
}
}
// The box belongs to ONE board. An operator with a self-signed switch on the
// LAN and a second board reached through a proper HTTPS proxy must keep real
// verification on the second — that link crosses the internet, and it commands
// an antenna.
func TestAcceptingOneBoardsCertificateDoesNotAffectAnother(t *testing.T) {
srv, _ := selfSignedRelay(t)
lan := NewHTTPGeneric(nil, nil, srv.URL+"/on/{relay}", "", "", "", 1, nil, true)
if err := lan.Set(context.Background(), 1, true); err != nil {
t.Fatalf("the LAN board: %v", err)
}
strict := NewHTTPGeneric(nil, nil, srv.URL+"/on/{relay}", "", "", "", 1, nil, false)
if err := strict.Set(context.Background(), 1, true); err == nil {
t.Error("the second board accepted the certificate too — the setting is not per board")
}
}