Make the membership, its subject, stream, bare address and the pin public, so the route proxy leaves the controller's module (issue 1633)
mesh/repo-check head c61212eba77c merged into trunk c6e669f71e8e — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/merge-gate head c61212eba77c merged into trunk c6e669f71e8e — pass: group feat/r4d composed: every machine composes with the change as it did without …
mesh/delivery delivered
mesh/delivery-group group feat/r4d delivered: every member is delivered
mesh/check failed: unit: ..test: node:internal/modules/esm/resolve:241 | c61212eba77c on 5652312f09d2 | unit 1629 run, 0 kept, 3 skipped | integration…
mesh/repo-check head c61212eba77c merged into trunk c6e669f71e8e — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/merge-gate head c61212eba77c merged into trunk c6e669f71e8e — pass: group feat/r4d composed: every machine composes with the change as it did without …
mesh/delivery delivered
mesh/delivery-group group feat/r4d delivered: every member is delivered
mesh/check failed: unit: ..test: node:internal/modules/esm/resolve:241 | c61212eba77c on 5652312f09d2 | unit 1629 run, 0 kept, 3 skipped | integration…
This commit is contained in:
@@ -0,0 +1,165 @@
|
|||||||
|
// Package membership is what the mesh issues an assignment to serve and to reach (novox/hq ADR 0160), as a
|
||||||
|
// process reads it from the bus: the Membership shape, the one subject it is published on, the stream that
|
||||||
|
// holds it, and the bus address helpers a reader needs. The controller composes it and the route proxy reads
|
||||||
|
// it; both use this one definition, so there is no second copy to drift (novox/hq issue 1633).
|
||||||
|
package membership
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AssignmentsStream holds every assignment's membership, the newest per subject.
|
||||||
|
const AssignmentsStream = "ASSIGNMENTS"
|
||||||
|
|
||||||
|
// Subject is the one address a runtime derives for itself: where its own membership is published, from
|
||||||
|
// the two names its credential carries. Everything else is in the membership.
|
||||||
|
func Subject(node, module string) string {
|
||||||
|
return "mesh.assignment." + node + "." + module
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirectGetSubject is the subject-addressed direct get of a membership from its stream: the one request an
|
||||||
|
// account may make of the stream.
|
||||||
|
func DirectGetSubject(node, module string) string {
|
||||||
|
return "$JS.API.DIRECT.GET." + AssignmentsStream + "." + Subject(node, module)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Membership is one assignment's subjects: what this instance of a module on this machine serves,
|
||||||
|
// and what it may reach.
|
||||||
|
type Membership struct {
|
||||||
|
Node string `json:"node"`
|
||||||
|
Module string `json:"module"`
|
||||||
|
// Serves is every address a tool of this instance answers on. `{tool}` stands for the tool's
|
||||||
|
// own name. An address with a queue is shared with the module's other instances.
|
||||||
|
Serves []Served `json:"serves"`
|
||||||
|
// Seats is every verb of a seat this instance holds, at the subject the seat's callers use.
|
||||||
|
Seats []SeatServed `json:"seats,omitempty"`
|
||||||
|
// Emits is where an event of this module lands; `{event}` stands for the event's name.
|
||||||
|
Emits string `json:"emits"`
|
||||||
|
// Reaches is each tool this module may call, `<module>.<tool>`, to the subjects that reach it.
|
||||||
|
Reaches map[string][]string `json:"reaches,omitempty"`
|
||||||
|
// Tools is where this instance answers what it serves.
|
||||||
|
Tools string `json:"tools"`
|
||||||
|
// Receives is what this assignment is given for each requirement it receives, by requirement (novox/hq
|
||||||
|
// ADR 0167): kept as JSON because the catalogue owns the shape of a contribution.
|
||||||
|
Receives map[string]json.RawMessage `json:"receives,omitempty"`
|
||||||
|
// Mesh is every machine's address on the private network (novox/hq ADR 0167).
|
||||||
|
Mesh []string `json:"mesh,omitempty"`
|
||||||
|
// State is every bucket this module's code may reach, and whether it may write it (novox/hq ADR 0201).
|
||||||
|
State []StateIssued `json:"state,omitempty"`
|
||||||
|
// Logs is every log this module's code may reach (novox/hq ADR 0297).
|
||||||
|
Logs []LogIssued `json:"logs,omitempty"`
|
||||||
|
// SeatTraffic is what this module's code may submit, say, hear, take, ask, answer and read on seats
|
||||||
|
// that name their caller or their kind (novox/hq ADR 0259 §3).
|
||||||
|
SeatTraffic *SeatTraffic `json:"seat-traffic,omitempty"`
|
||||||
|
// Streams are this module's own streams, Hears the module streams it hears (novox/hq ADR 0363).
|
||||||
|
Streams []StreamIssued `json:"streams,omitempty"`
|
||||||
|
Hears []HeardIssued `json:"hears,omitempty"`
|
||||||
|
// Waits are the event families a session may wait on through the mesh MCP server (novox/hq ADR 0367 §2).
|
||||||
|
Waits []WaitIssued `json:"waits,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Served is one address a tool is answered on.
|
||||||
|
type Served struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Queue string `json:"queue,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeatServed is one verb of a held seat, where its callers ask.
|
||||||
|
type SeatServed struct {
|
||||||
|
Seat string `json:"seat"`
|
||||||
|
Verb string `json:"verb"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateIssued is one bucket an assignment may reach, by the name its module uses for it (novox/hq ADR 0201).
|
||||||
|
// Key, when set, is the one key it may reach (novox/hq ADR 0260): this machine's.
|
||||||
|
type StateIssued struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Bucket string `json:"bucket"`
|
||||||
|
Writes bool `json:"writes,omitempty"`
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogIssued is one log an assignment may reach, by the name its module uses for it (novox/hq ADR 0297).
|
||||||
|
type LogIssued struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Stream string `json:"stream"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Writes bool `json:"writes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker is one durable consumer a holder pulls a seat's work from.
|
||||||
|
type Worker struct {
|
||||||
|
Stream string
|
||||||
|
Consumer string
|
||||||
|
Filter string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeatTraffic is one module's seat traffic beyond tools. Publish and Subscribe are subject patterns, in the
|
||||||
|
// server's wildcards.
|
||||||
|
type SeatTraffic struct {
|
||||||
|
Publish []string `json:"publish,omitempty"`
|
||||||
|
Subscribe []string `json:"subscribe,omitempty"`
|
||||||
|
Answers []string `json:"answers,omitempty"`
|
||||||
|
Workers []Worker `json:"workers,omitempty"`
|
||||||
|
Records []string `json:"records,omitempty"`
|
||||||
|
Kinds []KindHeld `json:"kinds,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KindHeld is one kind of a kinded bench and who holds it.
|
||||||
|
type KindHeld struct {
|
||||||
|
Seat string `json:"seat"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Module string `json:"module"`
|
||||||
|
Node string `json:"node"`
|
||||||
|
Capabilities []string `json:"capabilities,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamIssued is one of a module's own streams as its membership names it (ADR 0363 §2).
|
||||||
|
type StreamIssued struct {
|
||||||
|
Stream string `json:"stream"`
|
||||||
|
Events []string `json:"events"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeardIssued is one module stream an assignment hears (ADR 0363 §8).
|
||||||
|
type HeardIssued struct {
|
||||||
|
Stream string `json:"stream"`
|
||||||
|
Consumer string `json:"consumer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitIssued is one family as a membership issues it: the name a session asks for, and its subject.
|
||||||
|
type WaitIssued struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialIn reads the credential out of a bus address, `[scheme://]user:password@host:port`, splitting on
|
||||||
|
// the last at-sign so a password containing one stays whole, and returns the address without it.
|
||||||
|
func CredentialIn(address string) (user, password, bare string) {
|
||||||
|
at := strings.LastIndex(address, "@")
|
||||||
|
if at < 0 {
|
||||||
|
return "", "", address
|
||||||
|
}
|
||||||
|
scheme := ""
|
||||||
|
rest := address[:at]
|
||||||
|
if i := strings.Index(rest, "://"); i >= 0 {
|
||||||
|
scheme, rest = rest[:i+3], rest[i+3:]
|
||||||
|
}
|
||||||
|
user, password, _ = strings.Cut(rest, ":")
|
||||||
|
return user, password, scheme + address[at+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// BareAddress is a bus address with any credential stripped, for something that only needs to know
|
||||||
|
// whether a server is answering there.
|
||||||
|
func BareAddress(address string) string {
|
||||||
|
_, _, bare := CredentialIn(address)
|
||||||
|
if bare == "" {
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
if strings.Contains(bare, "://") {
|
||||||
|
return bare
|
||||||
|
}
|
||||||
|
return "nats://" + bare
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package membership
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestACredentialIsReadOutOfABusAddress(t *testing.T) {
|
||||||
|
for _, c := range []struct{ in, user, password, bare string }{
|
||||||
|
{"nats://controller:secret@127.0.0.1:4222", "controller", "secret", "nats://127.0.0.1:4222"},
|
||||||
|
{"controller:secret@127.0.0.1:4222", "controller", "secret", "127.0.0.1:4222"},
|
||||||
|
{"nats://127.0.0.1:4222", "", "", "nats://127.0.0.1:4222"},
|
||||||
|
{"127.0.0.1:4222", "", "", "127.0.0.1:4222"},
|
||||||
|
// A password containing an at-sign: split on the last one.
|
||||||
|
{"nats://controller:a@b@127.0.0.1:4222", "controller", "a@b", "nats://127.0.0.1:4222"},
|
||||||
|
} {
|
||||||
|
user, password, bare := CredentialIn(c.in)
|
||||||
|
if user != c.user || password != c.password || bare != c.bare {
|
||||||
|
t.Errorf("%q read as %q/%q at %q; wanted %q/%q at %q", c.in, user, password, bare, c.user, c.password, c.bare)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestABareAddressCarriesNoCredentialAndAScheme(t *testing.T) {
|
||||||
|
for in, want := range map[string]string{
|
||||||
|
"nats://controller:secret@10.0.0.1:4222": "nats://10.0.0.1:4222",
|
||||||
|
"controller:secret@10.0.0.1:4222": "nats://10.0.0.1:4222",
|
||||||
|
"nats://10.0.0.1:4222": "nats://10.0.0.1:4222",
|
||||||
|
"10.0.0.1:4222": "nats://10.0.0.1:4222",
|
||||||
|
} {
|
||||||
|
if got := BareAddress(in); got != want {
|
||||||
|
t.Errorf("BareAddress(%q) = %q, want %q", in, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAMembershipIsPublishedAndReadAtTheSubjectDerivedFromTheTwoNames(t *testing.T) {
|
||||||
|
if got := Subject("ace", "route-proxy"); got != "mesh.assignment.ace.route-proxy" {
|
||||||
|
t.Errorf("subject: %q", got)
|
||||||
|
}
|
||||||
|
if got := DirectGetSubject("ace", "route-proxy"); got != "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.ace.route-proxy" {
|
||||||
|
t.Errorf("direct get: %q", got)
|
||||||
|
}
|
||||||
|
if AssignmentsStream != "ASSIGNMENTS" {
|
||||||
|
t.Errorf("stream: %q", AssignmentsStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shape on the wire is the controller's: a membership as issued reads back whole, with the routes kept raw.
|
||||||
|
func TestAMembershipRoundTripsAsIssued(t *testing.T) {
|
||||||
|
in := Membership{Node: "ace", Module: "route-proxy", Emits: "e", Tools: "t",
|
||||||
|
Serves: []Served{{Subject: "s", Queue: "q"}},
|
||||||
|
Receives: map[string]json.RawMessage{"route": json.RawMessage(`[{"name":"a"}]`)},
|
||||||
|
Mesh: []string{"10.10.0.7"},
|
||||||
|
State: []StateIssued{{Name: "n", Bucket: "b", Writes: true, Key: "ace"}},
|
||||||
|
SeatTraffic: &SeatTraffic{Publish: []string{"p"}, Kinds: []KindHeld{{Seat: "channel", Kind: "tg"}}}}
|
||||||
|
raw, err := json.Marshal(in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var out Membership
|
||||||
|
if err := json.Unmarshal(raw, &out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(in, out) {
|
||||||
|
t.Fatalf("round trip changed it:\n%+v\n%+v", in, out)
|
||||||
|
}
|
||||||
|
var wire map[string]json.RawMessage
|
||||||
|
_ = json.Unmarshal(raw, &wire)
|
||||||
|
for _, k := range []string{"receives", "mesh", "seat-traffic"} {
|
||||||
|
if _, ok := wire[k]; !ok {
|
||||||
|
t.Errorf("field %q is not on the wire under that name", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-4
@@ -40,7 +40,7 @@ var refusedPublish = regexp.MustCompile(`(?i)Publish to "(\S+)"`)
|
|||||||
// other certificate), authenticated as its user and password, on the inbox prefix _INBOX.<user> and no
|
// other certificate), authenticated as its user and password, on the inbox prefix _INBOX.<user> and no
|
||||||
// other, reconnecting without limit as the tool runner does (novox/hq issue 1192: a process outlives a
|
// other, reconnecting without limit as the tool runner does (novox/hq issue 1192: a process outlives a
|
||||||
// bus restart, never giving up the way the client library's own default of 60 attempts would). Ported
|
// bus restart, never giving up the way the client library's own default of 60 attempts would). Ported
|
||||||
// from mesh-controller internal/broker/jetstream.go:106-132 (DialPinned, PinnedToFingerprint) since this
|
// from mesh-controller internal/broker/jetstream.go (DialPinned, PinnedToFingerprint), which now uses this one since this
|
||||||
// package may not import the controller.
|
// package may not import the controller.
|
||||||
//
|
//
|
||||||
// An ErrorHandler among opts is chained behind this package's own, which watches every async error for a
|
// An ErrorHandler among opts is chained behind this package's own, which watches every async error for a
|
||||||
@@ -78,7 +78,7 @@ func (c *Conn) options(opts []nats.Option) []nats.Option {
|
|||||||
nats.MaxReconnects(-1),
|
nats.MaxReconnects(-1),
|
||||||
}
|
}
|
||||||
if c.account.Fingerprint != "" {
|
if c.account.Fingerprint != "" {
|
||||||
all = append(all, nats.Secure(pinnedToFingerprint(c.account.Fingerprint)))
|
all = append(all, nats.Secure(PinnedToFingerprint(c.account.Fingerprint)))
|
||||||
}
|
}
|
||||||
all = append(all, opts...)
|
all = append(all, opts...)
|
||||||
return append(all, nats.ErrorHandler(func(nc *nats.Conn, sub *nats.Subscription, err error) {
|
return append(all, nats.ErrorHandler(func(nc *nats.Conn, sub *nats.Subscription, err error) {
|
||||||
@@ -140,9 +140,11 @@ func (c *Conn) publishRefused(err error) bool {
|
|||||||
return len(cancels) > 0
|
return len(cancels) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// pinnedToFingerprint accepts exactly the certificate with this SHA-256 and no other — the leaf alone, its
|
// PinnedToFingerprint accepts exactly the certificate with this SHA-256 and no other — the leaf alone, its
|
||||||
// name and chain deliberately not consulted, as mesh-controller's own pin does (jetstream.go:116-132).
|
// name and chain deliberately not consulted, as mesh-controller's own pin does (jetstream.go:116-132).
|
||||||
func pinnedToFingerprint(want string) *tls.Config {
|
// PinnedToFingerprint is public so that a process dialling the bus on its own terms, and the controller,
|
||||||
|
// share this one pin (novox/hq issue 1633).
|
||||||
|
func PinnedToFingerprint(want string) *tls.Config {
|
||||||
return &tls.Config{
|
return &tls.Config{
|
||||||
InsecureSkipVerify: true, //nolint:gosec // replaced by the pin below, which is stricter
|
InsecureSkipVerify: true, //nolint:gosec // replaced by the pin below, which is stricter
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/membership"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FollowMembership reads this account's own membership and follows it (novox/hq ADR 0160): apply is called
|
||||||
|
// with every membership the mesh issues this account's node and module, the one already issued first and
|
||||||
|
// each later one as it is published, so a change reaches a running process without a restart. A body that is
|
||||||
|
// not a membership is handed to onBad (nil to ignore it) and skipped.
|
||||||
|
//
|
||||||
|
// The subject is followed before it is read, so an issue landing between the two is applied, not missed. The
|
||||||
|
// read is the subject-addressed direct get, the one request this account may make of the stream; no
|
||||||
|
// membership issued yet, or the read failing, is not an error: the process follows the subject and is told
|
||||||
|
// when one arrives. The returned error is a refusal to follow at all.
|
||||||
|
func (c *Conn) FollowMembership(apply func(membership.Membership), onBad func(error)) error {
|
||||||
|
if c.account.Node == "" || c.account.Module == "" {
|
||||||
|
return fmt.Errorf("the account names no node or module, so it has no membership")
|
||||||
|
}
|
||||||
|
bad := func(err error) {
|
||||||
|
if onBad != nil {
|
||||||
|
onBad(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
take := func(body []byte) {
|
||||||
|
var issued membership.Membership
|
||||||
|
if err := json.Unmarshal(body, &issued); err != nil {
|
||||||
|
bad(fmt.Errorf("a membership arrived that is not one: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
apply(issued)
|
||||||
|
}
|
||||||
|
subject := membership.Subject(c.account.Node, c.account.Module)
|
||||||
|
if _, err := c.nc.Subscribe(subject, func(m *nats.Msg) { take(m.Data) }); err != nil {
|
||||||
|
return fmt.Errorf("cannot follow %s: %w", subject, err)
|
||||||
|
}
|
||||||
|
got, err := c.nc.Request(membership.DirectGetSubject(c.account.Node, c.account.Module), nil, 5*time.Second)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
bad(fmt.Errorf("cannot read the membership issued on %s yet (%v); following it", subject, err))
|
||||||
|
case got.Header.Get("Status") != "" || len(got.Data) == 0:
|
||||||
|
// None issued yet: the follow above delivers it.
|
||||||
|
default:
|
||||||
|
take(got.Data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/membership"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A process connected pinned to a test bus hears the membership published for its node and module, and
|
||||||
|
// ignores what is not one (novox/hq issue 1633, AC2).
|
||||||
|
func TestFollowMembershipHearsWhatIsIssuedToTheAccount(t *testing.T) {
|
||||||
|
url, fingerprint := selfSignedServer(t)
|
||||||
|
account := mailSenderAccount(t, url, fingerprint)
|
||||||
|
conn, err := Connect(account)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
got := make(chan membership.Membership, 1)
|
||||||
|
bad := make(chan error, 4)
|
||||||
|
if err := conn.FollowMembership(func(m membership.Membership) { got <- m }, func(e error) { bad <- e }); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := conn.nc.Publish(membership.Subject("novox", "mesh-mail"), []byte("not json")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := conn.nc.Publish(membership.Subject("novox", "mesh-mail"), []byte(`{"node":"novox","module":"mesh-mail","mesh":["10.0.0.1"]}`)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case m := <-got:
|
||||||
|
if m.Module != "mesh-mail" || len(m.Mesh) != 1 {
|
||||||
|
t.Fatalf("membership: %+v", m)
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("no membership heard")
|
||||||
|
}
|
||||||
|
if len(bad) == 0 {
|
||||||
|
t.Error("the body that is not a membership was not reported")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The pin accepts exactly the leaf certificate with the named SHA-256, and no other (novox/hq issue 1633).
|
||||||
|
func TestThePinAcceptsOnlyTheNamedCertificate(t *testing.T) {
|
||||||
|
leaf := []byte("a certificate")
|
||||||
|
sum := sha256.Sum256(leaf)
|
||||||
|
cfg := PinnedToFingerprint("sha256:" + hex.EncodeToString(sum[:]))
|
||||||
|
if err := cfg.VerifyPeerCertificate([][]byte{leaf}, nil); err != nil {
|
||||||
|
t.Errorf("the pinned certificate was refused: %v", err)
|
||||||
|
}
|
||||||
|
if err := cfg.VerifyPeerCertificate([][]byte{[]byte("another")}, nil); err == nil {
|
||||||
|
t.Error("another certificate was accepted")
|
||||||
|
}
|
||||||
|
if err := cfg.VerifyPeerCertificate(nil, nil); err == nil {
|
||||||
|
t.Error("no certificate was accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user