snapshot of https://github.com/nats-io/nats-server.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s

This commit is contained in:
upstream-mirror
2026-09-15 20:16:01 +05:00
commit 709385cbaa
455 changed files with 155508 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
// Copyright 2022-2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file is used iff the `enable_antithesis_sdk` build tag is not present
//go:build !enable_antithesis_sdk
package antithesis
import (
"testing"
)
// AssertUnreachable this implementation is a NOOP
func AssertUnreachable(_ testing.TB, _ string, _ map[string]any) {}
// Assert this implementation is a NOOP
func Assert(_ testing.TB, _ bool, _ string, _ map[string]any) {}

View File

@@ -0,0 +1,111 @@
// Copyright 2022-2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This file is used iff the `enable_antithesis_sdk` build tag is present
//go:build enable_antithesis_sdk
package antithesis
import (
"encoding/json"
"fmt"
"runtime/debug"
"testing"
"github.com/antithesishq/antithesis-sdk-go/assert"
)
// This file provides assertions utility functions suitable for use in tests.
// It is a thin wrapper around the Antithesis SDK.
//
// Notice that unlike other assertions libraries, a violation does not halt execution or fail the test.
// The effects of violating an assertion, are:
// 1. Print the violation message, prefixed by the test name in which it happened
// 2. Print the stack for the calling goroutine so each failed test has a clear trace of the failure
// 3. Invoke the underlying Antithesis assertion
//
// N.B. Enabling this module outside for tests running outside of Antithesis will enable 1 and 2 above, but not 3.
// therefore it can be useful to output additional test failure details when running tests locally or in CI.
// AssertUnreachable is used to flag code branches that should not get invoked ever.
// Example:
//
// pubAck, err := js.Publish(...)
//
// if err != nil {
// antithesis.AssertUnreachable(t, "Publish failed", map[string]any{"error": err.Error()})
// t.Fatalf("Publish failed with error: %s", err)
// }
func AssertUnreachable(t testing.TB, message string, details map[string]any) {
// Always print a message
fmt.Printf("{*} [%s] Assert Unreachable violation: %s\n", t.Name(), message)
if details != nil && len(details) > 0 {
fmt.Printf("{*} Details:\n")
jsonDetails, err := json.MarshalIndent(details, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(jsonDetails))
}
// Always print the stack trace
fmt.Printf("{*} Stack trace:\n")
debug.PrintStack()
// N.B. as of today, message (as-is) is the unique identifier of the event
// Therefore this will de-duplicate the same assertion failing in 2 different tests
// But not the same assertion failing at 2 different lines of the same test
messageWithTestName := fmt.Sprintf("[%s] %s", t.Name(), message)
// Fire assertion violation event (if Antithesis is enabled)
assert.Unreachable(messageWithTestName, details)
}
// Assert is used to check that some given condition is always true,
// Example:
//
// antithesis.Assert(t, sequence > lastSequence, "Non-monotonic stream sequence number", map[string]any{
// "stream": streamName,
// "connection_id": nc.Id(),
// "sequence": sequence,
// "lastSequence": lastSequence,
// })
func Assert(t testing.TB, condition bool, message string, details map[string]any) {
// Condition is true, nothing to do
if condition {
return
}
// Always print a message
fmt.Printf("{*} [%s] Assert violation: %s\n", t.Name(), message)
if details != nil && len(details) > 0 {
fmt.Printf("{*} Details:\n")
jsonDetails, err := json.MarshalIndent(details, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(jsonDetails))
}
// Always print the stack trace
fmt.Printf("{*} Stack trace:\n")
debug.PrintStack()
// N.B. as of today, message (as-is) is the unique identifier of the event
// Therefore this will de-duplicate the same assertion failing in 2 different tests
// But not the same assertion failing at 2 different lines of the same test
messageWithTestName := fmt.Sprintf("[%s] %s", t.Name(), message)
// Fire assertion violation event (if Antithesis is enabled)
assert.AlwaysOrUnreachable(false, messageWithTestName, details)
}

305
internal/ldap/dn.go Normal file
View File

@@ -0,0 +1,305 @@
// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)
// Portions copyright (c) 2015-2016 go-ldap Authors
package ldap
import (
"bytes"
"crypto/x509/pkix"
"encoding/asn1"
enchex "encoding/hex"
"errors"
"fmt"
"strings"
)
var attributeTypeNames = map[string]string{
"2.5.4.3": "CN",
"2.5.4.5": "SERIALNUMBER",
"2.5.4.6": "C",
"2.5.4.7": "L",
"2.5.4.8": "ST",
"2.5.4.9": "STREET",
"2.5.4.10": "O",
"2.5.4.11": "OU",
"2.5.4.17": "POSTALCODE",
// FIXME: Add others.
"0.9.2342.19200300.100.1.25": "DC",
}
// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514
type AttributeTypeAndValue struct {
// Type is the attribute type
Type string
// Value is the attribute value
Value string
}
// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514
type RelativeDN struct {
Attributes []*AttributeTypeAndValue
}
// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514
type DN struct {
RDNs []*RelativeDN
}
// FromCertSubject takes a pkix.Name from a cert and returns a DN
// that uses the same set. Does not support multi value RDNs.
func FromCertSubject(subject pkix.Name) (*DN, error) {
dn := &DN{
RDNs: make([]*RelativeDN, 0),
}
for i := len(subject.Names) - 1; i >= 0; i-- {
name := subject.Names[i]
oidString := name.Type.String()
typeName, ok := attributeTypeNames[oidString]
if !ok {
return nil, fmt.Errorf("invalid type name: %+v", name)
}
v, ok := name.Value.(string)
if !ok {
return nil, fmt.Errorf("invalid type value: %+v", v)
}
rdn := &RelativeDN{
Attributes: []*AttributeTypeAndValue{
{
Type: typeName,
Value: v,
},
},
}
dn.RDNs = append(dn.RDNs, rdn)
}
return dn, nil
}
// FromRawCertSubject takes a raw subject from a certificate
// and uses asn1.Unmarshal to get the individual RDNs in the
// original order, including multi-value RDNs.
func FromRawCertSubject(rawSubject []byte) (*DN, error) {
dn := &DN{
RDNs: make([]*RelativeDN, 0),
}
var rdns pkix.RDNSequence
_, err := asn1.Unmarshal(rawSubject, &rdns)
if err != nil {
return nil, err
}
for i := len(rdns) - 1; i >= 0; i-- {
rdn := rdns[i]
if len(rdn) == 0 {
continue
}
r := &RelativeDN{}
attrs := make([]*AttributeTypeAndValue, 0)
for j := len(rdn) - 1; j >= 0; j-- {
atv := rdn[j]
typeName := ""
name := atv.Type.String()
typeName, ok := attributeTypeNames[name]
if !ok {
return nil, fmt.Errorf("invalid type name: %+v", name)
}
value, ok := atv.Value.(string)
if !ok {
return nil, fmt.Errorf("invalid type value: %+v", atv.Value)
}
attr := &AttributeTypeAndValue{
Type: typeName,
Value: value,
}
attrs = append(attrs, attr)
}
r.Attributes = attrs
dn.RDNs = append(dn.RDNs, r)
}
return dn, nil
}
// ParseDN returns a distinguishedName or an error.
// The function respects https://tools.ietf.org/html/rfc4514
func ParseDN(str string) (*DN, error) {
dn := new(DN)
dn.RDNs = make([]*RelativeDN, 0)
rdn := new(RelativeDN)
rdn.Attributes = make([]*AttributeTypeAndValue, 0)
buffer := bytes.Buffer{}
attribute := new(AttributeTypeAndValue)
escaping := false
unescapedTrailingSpaces := 0
stringFromBuffer := func() string {
s := buffer.String()
s = s[0 : len(s)-unescapedTrailingSpaces]
buffer.Reset()
unescapedTrailingSpaces = 0
return s
}
for i := 0; i < len(str); i++ {
char := str[i]
switch {
case escaping:
unescapedTrailingSpaces = 0
escaping = false
switch char {
case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\':
buffer.WriteByte(char)
continue
}
// Not a special character, assume hex encoded octet
if len(str) == i+1 {
return nil, errors.New("got corrupted escaped character")
}
dst := []byte{0}
n, err := enchex.Decode([]byte(dst), []byte(str[i:i+2]))
if err != nil {
return nil, fmt.Errorf("failed to decode escaped character: %s", err)
} else if n != 1 {
return nil, fmt.Errorf("expected 1 byte when un-escaping, got %d", n)
}
buffer.WriteByte(dst[0])
i++
case char == '\\':
unescapedTrailingSpaces = 0
escaping = true
case char == '=':
attribute.Type = stringFromBuffer()
// Special case: If the first character in the value is # the following data
// is BER encoded. Throw an error since not supported right now.
if len(str) > i+1 && str[i+1] == '#' {
return nil, errors.New("unsupported BER encoding")
}
case char == ',' || char == '+':
// We're done with this RDN or value, push it
if len(attribute.Type) == 0 {
return nil, errors.New("incomplete type, value pair")
}
attribute.Value = stringFromBuffer()
rdn.Attributes = append(rdn.Attributes, attribute)
attribute = new(AttributeTypeAndValue)
if char == ',' {
dn.RDNs = append(dn.RDNs, rdn)
rdn = new(RelativeDN)
rdn.Attributes = make([]*AttributeTypeAndValue, 0)
}
case char == ' ' && buffer.Len() == 0:
// ignore unescaped leading spaces
continue
default:
if char == ' ' {
// Track unescaped spaces in case they are trailing and we need to remove them
unescapedTrailingSpaces++
} else {
// Reset if we see a non-space char
unescapedTrailingSpaces = 0
}
buffer.WriteByte(char)
}
}
if buffer.Len() > 0 {
if len(attribute.Type) == 0 {
return nil, errors.New("DN ended with incomplete type, value pair")
}
attribute.Value = stringFromBuffer()
rdn.Attributes = append(rdn.Attributes, attribute)
dn.RDNs = append(dn.RDNs, rdn)
}
return dn, nil
}
// Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
// Returns true if they have the same number of relative distinguished names
// and corresponding relative distinguished names (by position) are the same.
func (d *DN) Equal(other *DN) bool {
if len(d.RDNs) != len(other.RDNs) {
return false
}
for i := range d.RDNs {
if !d.RDNs[i].Equal(other.RDNs[i]) {
return false
}
}
return true
}
// RDNsMatch returns true if the individual RDNs of the DNs
// are the same regardless of ordering.
func (d *DN) RDNsMatch(other *DN) bool {
if len(d.RDNs) != len(other.RDNs) {
return false
}
matched := make([]bool, len(other.RDNs))
for _, irdn := range d.RDNs {
found := false
for j, ordn := range other.RDNs {
if !matched[j] && irdn.Equal(ordn) {
matched[j] = true
found = true
break
}
}
if !found {
return false
}
}
return true
}
// AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN.
// "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com"
// "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com"
// "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com"
func (d *DN) AncestorOf(other *DN) bool {
if len(d.RDNs) >= len(other.RDNs) {
return false
}
// Take the last `len(d.RDNs)` RDNs from the other DN to compare against
otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):]
for i := range d.RDNs {
if !d.RDNs[i].Equal(otherRDNs[i]) {
return false
}
}
return true
}
// Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
// Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues
// and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type.
// The order of attributes is not significant.
// Case of attribute types is not significant.
func (r *RelativeDN) Equal(other *RelativeDN) bool {
if len(r.Attributes) != len(other.Attributes) {
return false
}
return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes)
}
func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool {
for _, attr := range attrs {
found := false
for _, myattr := range r.Attributes {
if myattr.Equal(attr) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue
// Case of the attribute type is not significant
func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool {
return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value
}

225
internal/ldap/dn_test.go Normal file
View File

@@ -0,0 +1,225 @@
// Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com)
// Portions copyright (c) 2015-2016 go-ldap Authors
// Static-Check Fixes Copyright 2024 The NATS Authors
package ldap
import (
"reflect"
"testing"
)
func TestSuccessfulDNParsing(t *testing.T) {
testcases := map[string]DN{
"": {[]*RelativeDN{}},
"cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": {[]*RelativeDN{
{[]*AttributeTypeAndValue{{"cn", "Jim, \"Hasse Hö\" Hansson!"}}},
{[]*AttributeTypeAndValue{{"dc", "dummy"}}},
{[]*AttributeTypeAndValue{{"dc", "com"}}}}},
"UID=jsmith,DC=example,DC=net": {[]*RelativeDN{
{[]*AttributeTypeAndValue{{"UID", "jsmith"}}},
{[]*AttributeTypeAndValue{{"DC", "example"}}},
{[]*AttributeTypeAndValue{{"DC", "net"}}}}},
"OU=Sales+CN=J. Smith,DC=example,DC=net": {[]*RelativeDN{
{[]*AttributeTypeAndValue{
{"OU", "Sales"},
{"CN", "J. Smith"}}},
{[]*AttributeTypeAndValue{{"DC", "example"}}},
{[]*AttributeTypeAndValue{{"DC", "net"}}}}},
//
// "1.3.6.1.4.1.1466.0=#04024869": {[]*RelativeDN{
// {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}}}},
// "1.3.6.1.4.1.1466.0=#04024869,DC=net": {[]*RelativeDN{
// {[]*AttributeTypeAndValue{{"1.3.6.1.4.1.1466.0", "Hi"}}},
// {[]*AttributeTypeAndValue{{"DC", "net"}}}}},
"CN=Lu\\C4\\8Di\\C4\\87": {[]*RelativeDN{
{[]*AttributeTypeAndValue{{"CN", "Lučić"}}}}},
" CN = Lu\\C4\\8Di\\C4\\87 ": {[]*RelativeDN{
{[]*AttributeTypeAndValue{{"CN", "Lučić"}}}}},
` A = 1 , B = 2 `: {[]*RelativeDN{
{[]*AttributeTypeAndValue{{"A", "1"}}},
{[]*AttributeTypeAndValue{{"B", "2"}}}}},
` A = 1 + B = 2 `: {[]*RelativeDN{
{[]*AttributeTypeAndValue{
{"A", "1"},
{"B", "2"}}}}},
` \ \ A\ \ = \ \ 1\ \ , \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{
{[]*AttributeTypeAndValue{{" A ", " 1 "}}},
{[]*AttributeTypeAndValue{{" B ", " 2 "}}}}},
` \ \ A\ \ = \ \ 1\ \ + \ \ B\ \ = \ \ 2\ \ `: {[]*RelativeDN{
{[]*AttributeTypeAndValue{
{" A ", " 1 "},
{" B ", " 2 "}}}}},
}
for test, answer := range testcases {
dn, err := ParseDN(test)
if err != nil {
t.Error(err.Error())
continue
}
if !reflect.DeepEqual(dn, &answer) {
t.Errorf("Parsed DN %s is not equal to the expected structure", test)
t.Logf("Expected:")
for _, rdn := range answer.RDNs {
for _, attribs := range rdn.Attributes {
t.Logf("#%v\n", attribs)
}
}
t.Logf("Actual:")
for _, rdn := range dn.RDNs {
for _, attribs := range rdn.Attributes {
t.Logf("#%v\n", attribs)
}
}
}
}
}
func TestErrorDNParsing(t *testing.T) {
testcases := map[string]string{
"*": "DN ended with incomplete type, value pair",
"cn=Jim\\0Test": "failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'",
"cn=Jim\\0": "got corrupted escaped character",
"DC=example,=net": "DN ended with incomplete type, value pair",
// "1=#0402486": "failed to decode BER encoding: encoding/hex: odd length hex string",
"test,DC=example,DC=com": "incomplete type, value pair",
"=test,DC=example,DC=com": "incomplete type, value pair",
}
for test, answer := range testcases {
_, err := ParseDN(test)
if err == nil {
t.Errorf("Expected %s to fail parsing but succeeded\n", test)
} else if err.Error() != answer {
t.Errorf("Unexpected error on %s:\n%s\nvs.\n%s\n", test, answer, err.Error())
}
}
}
func TestDNEqual(t *testing.T) {
testcases := []struct {
A string
B string
Equal bool
}{
// Exact match
{"", "", true},
{"o=A", "o=A", true},
{"o=A", "o=B", false},
{"o=A,o=B", "o=A,o=B", true},
{"o=A,o=B", "o=A,o=C", false},
{"o=A+o=B", "o=A+o=B", true},
{"o=A+o=B", "o=A+o=C", false},
// Case mismatch in type is ignored
{"o=A", "O=A", true},
{"o=A,o=B", "o=A,O=B", true},
{"o=A+o=B", "o=A+O=B", true},
// Case mismatch in value is significant
{"o=a", "O=A", false},
{"o=a,o=B", "o=A,O=B", false},
{"o=a+o=B", "o=A+O=B", false},
// Multi-valued RDN order mismatch is ignored
{"o=A+o=B", "O=B+o=A", true},
// Number of RDN attributes is significant
{"o=A+o=B", "O=B+o=A+O=B", false},
// Missing values are significant
{"o=A+o=B", "O=B+o=A+O=C", false}, // missing values matter
{"o=A+o=B+o=C", "O=B+o=A", false}, // missing values matter
// Whitespace tests
// Matching
{
"cn=John Doe, ou=People, dc=sun.com",
"cn=John Doe, ou=People, dc=sun.com",
true,
},
// Difference in leading/trailing chars is ignored
{
"cn=John Doe, ou=People, dc=sun.com",
"cn=John Doe,ou=People,dc=sun.com",
true,
},
// Difference in values is significant
{
"cn=John Doe, ou=People, dc=sun.com",
"cn=John Doe, ou=People, dc=sun.com",
false,
},
}
for i, tc := range testcases {
a, err := ParseDN(tc.A)
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
b, err := ParseDN(tc.B)
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
if expected, actual := tc.Equal, a.Equal(b); expected != actual {
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
continue
}
if expected, actual := tc.Equal, b.Equal(a); expected != actual {
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
continue
}
}
}
func TestDNAncestor(t *testing.T) {
testcases := []struct {
A string
B string
Ancestor bool
}{
// Exact match returns false
{"", "", false},
{"o=A", "o=A", false},
{"o=A,o=B", "o=A,o=B", false},
{"o=A+o=B", "o=A+o=B", false},
// Mismatch
{"ou=C,ou=B,o=A", "ou=E,ou=D,ou=B,o=A", false},
// Descendant
{"ou=C,ou=B,o=A", "ou=E,ou=C,ou=B,o=A", true},
}
for i, tc := range testcases {
a, err := ParseDN(tc.A)
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
b, err := ParseDN(tc.B)
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
if expected, actual := tc.Ancestor, a.AncestorOf(b); expected != actual {
t.Errorf("%d: when comparing '%s' and '%s' expected %v, got %v", i, tc.A, tc.B, expected, actual)
continue
}
}
}
func TestDNReverseMatchDuplicates(t *testing.T) {
// A has two CN=Alice RDNs and one O=NATS.
// B has one CN=Alice, one O=NATS, and one OU=Users.
// They have the same length (3).
dnA, _ := ParseDN("CN=Alice,CN=Alice,O=NATS")
dnB, _ := ParseDN("CN=Alice,O=NATS,OU=Users")
if dnA.RDNsMatch(dnB) {
t.Fatal("Expected RDNsMatch to return false for different RDN sets, but it returned true")
}
}

276
internal/ocsp/ocsp.go Normal file
View File

@@ -0,0 +1,276 @@
// Copyright 2019-2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testhelper
import (
"crypto"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"sync"
"testing"
"time"
"golang.org/x/crypto/ocsp"
)
const (
defaultResponseTTL = 4 * time.Second
defaultAddress = "127.0.0.1:8888"
)
func NewOCSPResponderCustomAddress(t *testing.T, issuerCertPEM, issuerKeyPEM string, addr string) *http.Server {
t.Helper()
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, addr, defaultResponseTTL, "")
}
func NewOCSPResponder(t *testing.T, issuerCertPEM, issuerKeyPEM string) *http.Server {
t.Helper()
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, defaultResponseTTL, "")
}
func NewOCSPResponderDesignatedCustomAddress(t *testing.T, issuerCertPEM, respCertPEM, respKeyPEM string, addr string) *http.Server {
t.Helper()
return NewOCSPResponderBase(t, issuerCertPEM, respCertPEM, respKeyPEM, true, addr, defaultResponseTTL, "")
}
func NewOCSPResponderPreferringHTTPMethod(t *testing.T, issuerCertPEM, issuerKeyPEM, method string) *http.Server {
t.Helper()
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, defaultResponseTTL, method)
}
func NewOCSPResponderCustomTimeout(t *testing.T, issuerCertPEM, issuerKeyPEM string, responseTTL time.Duration) *http.Server {
t.Helper()
return NewOCSPResponderBase(t, issuerCertPEM, issuerCertPEM, issuerKeyPEM, false, defaultAddress, responseTTL, "")
}
func NewOCSPResponderBase(t *testing.T, issuerCertPEM, respCertPEM, respKeyPEM string, embed bool, addr string, responseTTL time.Duration, method string) *http.Server {
t.Helper()
var mu sync.Mutex
status := make(map[string]int)
issuerCert := parseCertPEM(t, issuerCertPEM)
respCert := parseCertPEM(t, respCertPEM)
respKey := parseKeyPEM(t, respKeyPEM)
mux := http.NewServeMux()
// The "/statuses/" endpoint is for directly setting a key-value pair in
// the CA's status database.
mux.HandleFunc("/statuses/", func(rw http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
key := r.URL.Path[len("/statuses/"):]
switch r.Method {
case "GET":
mu.Lock()
n, ok := status[key]
if !ok {
n = ocsp.Unknown
}
mu.Unlock()
fmt.Fprintf(rw, "%s %d", key, n)
case "POST":
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
n, err := strconv.Atoi(string(data))
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
mu.Lock()
status[key] = n
mu.Unlock()
fmt.Fprintf(rw, "%s %d", key, n)
default:
http.Error(rw, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
})
// The "/" endpoint is for normal OCSP requests. This actually parses an
// OCSP status request and signs a response with a CA. Lightly based off:
// https://www.ietf.org/rfc/rfc2560.txt
mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
var reqData []byte
var err error
switch {
case r.Method == "GET":
if method != "" && r.Method != method {
http.Error(rw, "", http.StatusBadRequest)
return
}
reqData, err = base64.StdEncoding.DecodeString(r.URL.Path[1:])
case r.Method == "POST":
if method != "" && r.Method != method {
http.Error(rw, "", http.StatusBadRequest)
return
}
reqData, err = io.ReadAll(r.Body)
r.Body.Close()
default:
http.Error(rw, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
ocspReq, err := ocsp.ParseRequest(reqData)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
mu.Lock()
n, ok := status[ocspReq.SerialNumber.String()]
if !ok {
n = ocsp.Unknown
}
mu.Unlock()
tmpl := ocsp.Response{
Status: n,
SerialNumber: ocspReq.SerialNumber,
ThisUpdate: time.Now(),
}
if responseTTL != 0 {
tmpl.NextUpdate = tmpl.ThisUpdate.Add(responseTTL)
}
if embed {
tmpl.Certificate = respCert
}
respData, err := ocsp.CreateResponse(issuerCert, respCert, tmpl, respKey)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/ocsp-response")
rw.Header().Set("Content-Length", fmt.Sprint(len(respData)))
fmt.Fprint(rw, string(respData))
})
srv := &http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: time.Second * 5,
}
go srv.ListenAndServe()
time.Sleep(1 * time.Second)
return srv
}
func parseCertPEM(t *testing.T, certPEM string) *x509.Certificate {
t.Helper()
block := parsePEM(t, certPEM)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to parse cert '%s': %s", certPEM, err)
}
return cert
}
func parseKeyPEM(t *testing.T, keyPEM string) crypto.Signer {
t.Helper()
block := parsePEM(t, keyPEM)
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
t.Fatalf("failed to parse ikey %s: %s", keyPEM, err)
}
}
keyc := key.(crypto.Signer)
return keyc
}
func parsePEM(t *testing.T, pemPath string) *pem.Block {
t.Helper()
data, err := os.ReadFile(pemPath)
if err != nil {
t.Fatal(err)
}
block, _ := pem.Decode(data)
if block == nil {
t.Fatalf("failed to decode PEM %s", pemPath)
}
return block
}
func GetOCSPStatus(s tls.ConnectionState) (*ocsp.Response, error) {
if len(s.VerifiedChains) == 0 {
return nil, fmt.Errorf("missing TLS verified chains")
}
chain := s.VerifiedChains[0]
if got, want := len(chain), 2; got < want {
return nil, fmt.Errorf("incomplete cert chain, got %d, want at least %d", got, want)
}
leaf, issuer := chain[0], chain[1]
resp, err := ocsp.ParseResponseForCert(s.OCSPResponse, leaf, issuer)
if err != nil {
return nil, fmt.Errorf("failed to parse OCSP response: %w", err)
}
if err := resp.CheckSignatureFrom(issuer); err != nil {
return resp, err
}
return resp, nil
}
func SetOCSPStatus(t *testing.T, ocspURL, certPEM string, status int) {
t.Helper()
cert := parseCertPEM(t, certPEM)
hc := &http.Client{Timeout: 10 * time.Second}
resp, err := hc.Post(
fmt.Sprintf("%s/statuses/%s", ocspURL, cert.SerialNumber),
"",
strings.NewReader(fmt.Sprint(status)),
)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("failed to read OCSP HTTP response body: %s", err)
}
if got, want := resp.Status, "200 OK"; got != want {
t.Error(strings.TrimSpace(string(data)))
t.Fatalf("unexpected OCSP HTTP set status, got %q, want %q", got, want)
}
}

View File

@@ -0,0 +1,127 @@
// Copyright 2019-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testhelper
// These routines need to be accessible in both the server and test
// directories, and tests importing a package don't get exported symbols from
// _test.go files in the imported package, so we put them here where they can
// be used freely.
import (
"fmt"
"strings"
"sync"
"testing"
)
type DummyLogger struct {
sync.Mutex
Msg string
AllMsgs []string
}
func (l *DummyLogger) CheckContent(t *testing.T, expectedStr string) {
t.Helper()
l.Lock()
defer l.Unlock()
if l.Msg != expectedStr {
t.Fatalf("Expected log to be: %v, got %v", expectedStr, l.Msg)
}
}
func (l *DummyLogger) aggregate() {
if l.AllMsgs != nil {
l.AllMsgs = append(l.AllMsgs, l.Msg)
}
}
func (l *DummyLogger) Noticef(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
func (l *DummyLogger) Errorf(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
func (l *DummyLogger) Warnf(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
func (l *DummyLogger) Fatalf(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
func (l *DummyLogger) Debugf(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
func (l *DummyLogger) Tracef(format string, v ...any) {
l.Lock()
defer l.Unlock()
l.Msg = fmt.Sprintf(format, v...)
l.aggregate()
}
// NewDummyLogger creates a dummy logger and allows to ask for logs to be
// retained instead of just keeping the most recent. Use retain to provide an
// initial size estimate on messages (not to provide a max capacity).
func NewDummyLogger(retain uint) *DummyLogger {
l := &DummyLogger{}
if retain > 0 {
l.AllMsgs = make([]string, 0, retain)
}
return l
}
func (l *DummyLogger) Drain() {
l.Lock()
defer l.Unlock()
if l.AllMsgs == nil {
return
}
l.AllMsgs = make([]string, 0, len(l.AllMsgs))
}
func (l *DummyLogger) CheckForProhibited(t *testing.T, reason, needle string) {
t.Helper()
l.Lock()
defer l.Unlock()
if l.AllMsgs == nil {
t.Fatal("DummyLogger.CheckForProhibited called without AllMsgs being collected")
}
// Collect _all_ matches, rather than have to re-test repeatedly.
// This will particularly help with less deterministic tests with multiple matches.
shouldFail := false
for i := range l.AllMsgs {
if strings.Contains(l.AllMsgs[i], needle) {
t.Errorf("log contains %s: %v", reason, l.AllMsgs[i])
shouldFail = true
}
}
if shouldFail {
t.FailNow()
}
}