Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+18
View File
@@ -0,0 +1,18 @@
.PHONY: test cover
build:
go build
fmt:
gofmt -w -s *.go
goimports -w *.go
go mod tidy
test:
go vet ./...
staticcheck ./...
rm -rf ./coverage.out
go test -v -coverprofile=./coverage.out ./...
cover:
go tool cover -html=coverage.out
+485
View File
@@ -0,0 +1,485 @@
/*
* Copyright 2018-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 jwt
import (
"errors"
"fmt"
"sort"
"time"
"github.com/nats-io/nkeys"
)
// NoLimit is used to indicate a limit field is unlimited in value.
const (
NoLimit = -1
AnyAccount = "*"
)
type AccountLimits struct {
Imports int64 `json:"imports,omitempty"` // Max number of imports
Exports int64 `json:"exports,omitempty"` // Max number of exports
WildcardExports bool `json:"wildcards,omitempty"` // Are wildcards allowed in exports
DisallowBearer bool `json:"disallow_bearer,omitempty"` // User JWT can't be bearer token
Conn int64 `json:"conn,omitempty"` // Max number of active connections
LeafNodeConn int64 `json:"leaf,omitempty"` // Max number of active leaf node connections
}
// IsUnlimited returns true if all limits are unlimited
func (a *AccountLimits) IsUnlimited() bool {
return *a == AccountLimits{NoLimit, NoLimit, true, false, NoLimit, NoLimit}
}
type NatsLimits struct {
Subs int64 `json:"subs,omitempty"` // Max number of subscriptions
Data int64 `json:"data,omitempty"` // Max number of bytes
Payload int64 `json:"payload,omitempty"` // Max message payload
}
// IsUnlimited returns true if all limits are unlimited
func (n *NatsLimits) IsUnlimited() bool {
return *n == NatsLimits{NoLimit, NoLimit, NoLimit}
}
type JetStreamLimits struct {
MemoryStorage int64 `json:"mem_storage,omitempty"` // Max number of bytes stored in memory across all streams. (0 means disabled)
DiskStorage int64 `json:"disk_storage,omitempty"` // Max number of bytes stored on disk across all streams. (0 means disabled)
Streams int64 `json:"streams,omitempty"` // Max number of streams
Consumer int64 `json:"consumer,omitempty"` // Max number of consumers
MaxAckPending int64 `json:"max_ack_pending,omitempty"` // Max ack pending of a Stream
MemoryMaxStreamBytes int64 `json:"mem_max_stream_bytes,omitempty"` // Max bytes a memory backed stream can have. (0 means disabled/unlimited)
DiskMaxStreamBytes int64 `json:"disk_max_stream_bytes,omitempty"` // Max bytes a disk backed stream can have. (0 means disabled/unlimited)
MaxBytesRequired bool `json:"max_bytes_required,omitempty"` // Max bytes required by all Streams
}
// IsUnlimited returns true if all limits are unlimited
func (j *JetStreamLimits) IsUnlimited() bool {
lim := *j
// workaround in case NoLimit was used instead of 0
if lim.MemoryMaxStreamBytes < 0 {
lim.MemoryMaxStreamBytes = 0
}
if lim.DiskMaxStreamBytes < 0 {
lim.DiskMaxStreamBytes = 0
}
if lim.MaxAckPending < 0 {
lim.MaxAckPending = 0
}
return lim == JetStreamLimits{NoLimit, NoLimit, NoLimit, NoLimit, 0, 0, 0, false}
}
type JetStreamTieredLimits map[string]JetStreamLimits
// OperatorLimits are used to limit access by an account
type OperatorLimits struct {
NatsLimits
AccountLimits
JetStreamLimits
JetStreamTieredLimits `json:"tiered_limits,omitempty"`
}
// IsJSEnabled returns if this account claim has JS enabled either through a tier or the non tiered limits.
func (o *OperatorLimits) IsJSEnabled() bool {
if len(o.JetStreamTieredLimits) > 0 {
for _, l := range o.JetStreamTieredLimits {
if l.MemoryStorage != 0 || l.DiskStorage != 0 {
return true
}
}
return false
}
l := o.JetStreamLimits
return l.MemoryStorage != 0 || l.DiskStorage != 0
}
// IsEmpty returns true if all limits are 0/false/empty.
func (o *OperatorLimits) IsEmpty() bool {
return o.NatsLimits == NatsLimits{} &&
o.AccountLimits == AccountLimits{} &&
o.JetStreamLimits == JetStreamLimits{} &&
len(o.JetStreamTieredLimits) == 0
}
// IsUnlimited returns true if all limits are unlimited
func (o *OperatorLimits) IsUnlimited() bool {
return o.AccountLimits.IsUnlimited() && o.NatsLimits.IsUnlimited() &&
o.JetStreamLimits.IsUnlimited() && len(o.JetStreamTieredLimits) == 0
}
// Validate checks that the operator limits contain valid values
func (o *OperatorLimits) Validate(vr *ValidationResults) {
// negative values mean unlimited, so all numbers are valid
if len(o.JetStreamTieredLimits) > 0 {
if (o.JetStreamLimits != JetStreamLimits{}) {
vr.AddError("JetStream Limits and tiered JetStream Limits are mutually exclusive")
}
if _, ok := o.JetStreamTieredLimits[""]; ok {
vr.AddError(`Tiered JetStream Limits can not contain a blank "" tier name`)
}
}
}
// WeightedMapping for publishes
type WeightedMapping struct {
Subject Subject `json:"subject"`
Weight uint8 `json:"weight,omitempty"`
Cluster string `json:"cluster,omitempty"`
}
func (m *WeightedMapping) GetWeight() uint8 {
if m.Weight == 0 {
return 100
}
return m.Weight
}
type Mapping map[Subject][]WeightedMapping
func (m *Mapping) Validate(vr *ValidationResults) {
for ubFrom, wm := range (map[Subject][]WeightedMapping)(*m) {
ubFrom.Validate(vr)
perCluster := make(map[string]uint32)
total := uint32(0)
for _, e := range wm {
e.Subject.Validate(vr)
if e.GetWeight() > 100 {
vr.AddError("Mapping %q has a weight %d that exceeds 100", ubFrom, e.GetWeight())
}
if e.Cluster != "" {
t := perCluster[e.Cluster]
t += uint32(e.GetWeight())
perCluster[e.Cluster] = t
if t > 100 {
vr.AddError("Mapping %q in cluster %q exceeds 100%% among all of it's weighted to mappings", ubFrom, e.Cluster)
}
} else {
total += uint32(e.GetWeight())
}
}
if total > 100 {
vr.AddError("Mapping %q exceeds 100%% among all of it's weighted to mappings", ubFrom)
}
}
}
func (a *Account) AddMapping(sub Subject, to ...WeightedMapping) {
a.Mappings[sub] = to
}
// ExternalAuthorization enables external authorization for account users.
// AuthUsers are those users specified to bypass the authorization callout and should be used for the authorization service itself.
// AllowedAccounts specifies which accounts, if any, that the authorization service can bind an authorized user to.
// The authorization response, a user JWT, will still need to be signed by the correct account.
// If optional XKey is specified, that is the public xkey (x25519) and the server will encrypt the request such that only the
// holder of the private key can decrypt. The auth service can also optionally encrypt the response back to the server using it's
// public xkey which will be in the authorization request.
type ExternalAuthorization struct {
AuthUsers StringList `json:"auth_users,omitempty"`
AllowedAccounts StringList `json:"allowed_accounts,omitempty"`
XKey string `json:"xkey,omitempty"`
}
func (ac *ExternalAuthorization) IsEnabled() bool {
return len(ac.AuthUsers) > 0
}
// HasExternalAuthorization helper function to determine if external authorization is enabled.
func (a *Account) HasExternalAuthorization() bool {
return a.Authorization.IsEnabled()
}
// EnableExternalAuthorization helper function to setup external authorization.
func (a *Account) EnableExternalAuthorization(users ...string) {
a.Authorization.AuthUsers.Add(users...)
}
func (ac *ExternalAuthorization) Validate(vr *ValidationResults) {
if len(ac.AllowedAccounts) > 0 && len(ac.AuthUsers) == 0 {
vr.AddError("External authorization cannot have accounts without users specified")
}
// Make sure users are all valid user nkeys.
// Make sure allowed accounts are all valid account nkeys.
for _, u := range ac.AuthUsers {
if !nkeys.IsValidPublicUserKey(u) {
vr.AddError("AuthUser %q is not a valid user public key", u)
}
}
for _, a := range ac.AllowedAccounts {
if a == AnyAccount && len(ac.AllowedAccounts) > 1 {
vr.AddError("AllowedAccounts can only be a list of accounts or %q", AnyAccount)
continue
} else if a == AnyAccount {
continue
} else if !nkeys.IsValidPublicAccountKey(a) {
vr.AddError("Account %q is not a valid account public key", a)
}
}
if ac.XKey != "" && !nkeys.IsValidPublicCurveKey(ac.XKey) {
vr.AddError("XKey %q is not a valid public xkey", ac.XKey)
}
}
const (
ClusterTrafficSystem = "system"
ClusterTrafficOwner = "owner"
)
type ClusterTraffic string
func (ct ClusterTraffic) Valid() error {
if ct == "" || ct == ClusterTrafficSystem || ct == ClusterTrafficOwner {
return nil
}
return fmt.Errorf("unknown cluster traffic option: %q", ct)
}
// Account holds account specific claims data
type Account struct {
Imports Imports `json:"imports,omitempty"`
Exports Exports `json:"exports,omitempty"`
Limits OperatorLimits `json:"limits,omitempty"`
SigningKeys SigningKeys `json:"signing_keys,omitempty"`
Revocations RevocationList `json:"revocations,omitempty"`
DefaultPermissions Permissions `json:"default_permissions,omitempty"`
Mappings Mapping `json:"mappings,omitempty"`
Authorization ExternalAuthorization `json:"authorization,omitempty"`
Trace *MsgTrace `json:"trace,omitempty"`
ClusterTraffic ClusterTraffic `json:"cluster_traffic,omitempty"`
Info
GenericFields
}
// MsgTrace holds distributed message tracing configuration
type MsgTrace struct {
// Destination is the subject the server will send message traces to
// if the inbound message contains the "traceparent" header and has
// its sampled field indicating that the trace should be triggered.
Destination Subject `json:"dest,omitempty"`
// Sampling is used to set the probability sampling, that is, the
// server will get a random number between 1 and 100 and trigger
// the trace if the number is lower than this Sampling value.
// The valid range is [1..100]. If the value is not set Validate()
// will set the value to 100.
Sampling int `json:"sampling,omitempty"`
}
// Validate checks if the account is valid, based on the wrapper
func (a *Account) Validate(acct *AccountClaims, vr *ValidationResults) {
a.Imports.Validate(acct.Subject, vr)
a.Exports.Validate(vr)
a.Limits.Validate(vr)
a.DefaultPermissions.Validate(vr)
a.Mappings.Validate(vr)
a.Authorization.Validate(vr)
if a.Trace != nil {
tvr := CreateValidationResults()
a.Trace.Destination.Validate(tvr)
if !tvr.IsEmpty() {
vr.AddError("the account Trace.Destination %s", tvr.Issues[0].Description)
}
if a.Trace.Destination.HasWildCards() {
vr.AddError("the account Trace.Destination subject %q is not a valid publish subject", a.Trace.Destination)
}
if a.Trace.Sampling < 0 || a.Trace.Sampling > 100 {
vr.AddError("the account Trace.Sampling value '%d' is not valid, should be in the range [1..100]", a.Trace.Sampling)
} else if a.Trace.Sampling == 0 {
a.Trace.Sampling = 100
}
}
if !a.Limits.IsEmpty() && a.Limits.Imports >= 0 && int64(len(a.Imports)) > a.Limits.Imports {
vr.AddError("the account contains more imports than allowed by the operator")
}
// Check Imports and Exports for limit violations.
if a.Limits.Imports != NoLimit {
if int64(len(a.Imports)) > a.Limits.Imports {
vr.AddError("the account contains more imports than allowed by the operator")
}
}
if a.Limits.Exports != NoLimit {
if int64(len(a.Exports)) > a.Limits.Exports {
vr.AddError("the account contains more exports than allowed by the operator")
}
// Check for wildcard restrictions
if !a.Limits.WildcardExports {
for _, ex := range a.Exports {
if ex.Subject.HasWildCards() {
vr.AddError("the account contains wildcard exports that are not allowed by the operator")
}
}
}
}
a.SigningKeys.Validate(vr)
a.Info.Validate(vr)
if err := a.ClusterTraffic.Valid(); err != nil {
vr.AddError("%s", err.Error())
}
}
// AccountClaims defines the body of an account JWT
type AccountClaims struct {
ClaimsData
Account `json:"nats,omitempty"`
}
// NewAccountClaims creates a new account JWT
func NewAccountClaims(subject string) *AccountClaims {
if subject == "" {
return nil
}
c := &AccountClaims{}
c.SigningKeys = make(SigningKeys)
// Set to unlimited to start. We do it this way so we get compiler
// errors if we add to the OperatorLimits.
c.Limits = OperatorLimits{
NatsLimits{NoLimit, NoLimit, NoLimit},
AccountLimits{NoLimit, NoLimit, true, false, NoLimit, NoLimit},
JetStreamLimits{0, 0, 0, 0, 0, 0, 0, false},
JetStreamTieredLimits{},
}
c.Subject = subject
c.Mappings = Mapping{}
return c
}
// Encode converts account claims into a JWT string
func (a *AccountClaims) Encode(pair nkeys.KeyPair) (string, error) {
return a.EncodeWithSigner(pair, nil)
}
func (a *AccountClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
if !nkeys.IsValidPublicAccountKey(a.Subject) {
return "", errors.New("expected subject to be account public key")
}
sort.Sort(a.Exports)
sort.Sort(a.Imports)
a.Type = AccountClaim
return a.ClaimsData.encode(pair, a, fn)
}
// DecodeAccountClaims decodes account claims from a JWT string
func DecodeAccountClaims(token string) (*AccountClaims, error) {
claims, err := Decode(token)
if err != nil {
return nil, err
}
ac, ok := claims.(*AccountClaims)
if !ok {
return nil, errors.New("not account claim")
}
return ac, nil
}
func (a *AccountClaims) String() string {
return a.ClaimsData.String(a)
}
// Payload pulls the accounts specific payload out of the claims
func (a *AccountClaims) Payload() interface{} {
return &a.Account
}
// Validate checks the accounts contents
func (a *AccountClaims) Validate(vr *ValidationResults) {
a.ClaimsData.Validate(vr)
a.Account.Validate(a, vr)
if nkeys.IsValidPublicAccountKey(a.ClaimsData.Issuer) {
if !a.Limits.IsEmpty() {
vr.AddWarning("self-signed account JWTs shouldn't contain operator limits")
}
}
}
func (a *AccountClaims) ClaimType() ClaimType {
return a.Type
}
func (a *AccountClaims) updateVersion() {
a.GenericFields.Version = libVersion
}
// ExpectedPrefixes defines the types that can encode an account jwt, account and operator
func (a *AccountClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator}
}
// Claims returns the accounts claims data
func (a *AccountClaims) Claims() *ClaimsData {
return &a.ClaimsData
}
func (a *AccountClaims) GetTags() TagList {
return a.Account.Tags
}
// DidSign checks the claims against the account's public key and its signing keys
func (a *AccountClaims) DidSign(c Claims) bool {
if c != nil {
issuer := c.Claims().Issuer
if issuer == a.Subject {
return true
}
uc, ok := c.(*UserClaims)
if ok && uc.IssuerAccount == a.Subject {
return a.SigningKeys.Contains(issuer)
}
at, ok := c.(*ActivationClaims)
if ok && at.IssuerAccount == a.Subject {
return a.SigningKeys.Contains(issuer)
}
}
return false
}
// Revoke enters a revocation by public key using time.Now().
func (a *AccountClaims) Revoke(pubKey string) {
a.RevokeAt(pubKey, time.Now())
}
// RevokeAt enters a revocation by public key and timestamp into this account
// This will revoke all jwt issued for pubKey, prior to timestamp
// If there is already a revocation for this public key that is newer, it is kept.
// The value is expected to be a public key or "*" (means all public keys)
func (a *AccountClaims) RevokeAt(pubKey string, timestamp time.Time) {
if a.Revocations == nil {
a.Revocations = RevocationList{}
}
a.Revocations.Revoke(pubKey, timestamp)
}
// ClearRevocation removes any revocation for the public key
func (a *AccountClaims) ClearRevocation(pubKey string) {
a.Revocations.ClearRevocation(pubKey)
}
// isRevoked checks if the public key is in the revoked list with a timestamp later than the one passed in.
// Generally this method is called with the subject and issue time of the jwt to be tested.
// DO NOT pass time.Now(), it will not produce a stable/expected response.
func (a *AccountClaims) isRevoked(pubKey string, claimIssuedAt time.Time) bool {
return a.Revocations.IsRevoked(pubKey, claimIssuedAt)
}
// IsClaimRevoked checks if the account revoked the claim passed in.
// Invalid claims (nil, no Subject or IssuedAt) will return true.
func (a *AccountClaims) IsClaimRevoked(claim *UserClaims) bool {
if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" {
return true
}
return a.isRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0))
}
+182
View File
@@ -0,0 +1,182 @@
/*
* Copyright 2018-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 jwt
import (
"crypto/sha256"
"encoding/base32"
"errors"
"fmt"
"strings"
"github.com/nats-io/nkeys"
)
// Activation defines the custom parts of an activation claim
type Activation struct {
ImportSubject Subject `json:"subject,omitempty"`
ImportType ExportType `json:"kind,omitempty"`
// IssuerAccount stores the public key for the account the issuer represents.
// When set, the claim was issued by a signing key.
IssuerAccount string `json:"issuer_account,omitempty"`
GenericFields
}
// IsService returns true if an Activation is for a service
func (a *Activation) IsService() bool {
return a.ImportType == Service
}
// IsStream returns true if an Activation is for a stream
func (a *Activation) IsStream() bool {
return a.ImportType == Stream
}
// Validate checks the exports and limits in an activation JWT
func (a *Activation) Validate(vr *ValidationResults) {
if !a.IsService() && !a.IsStream() {
vr.AddError("invalid import type: %q", a.ImportType)
}
a.ImportSubject.Validate(vr)
}
// ActivationClaims holds the data specific to an activation JWT
type ActivationClaims struct {
ClaimsData
Activation `json:"nats,omitempty"`
}
// NewActivationClaims creates a new activation claim with the provided sub
func NewActivationClaims(subject string) *ActivationClaims {
if subject == "" {
return nil
}
ac := &ActivationClaims{}
ac.Subject = subject
return ac
}
// Encode turns an activation claim into a JWT strimg
func (a *ActivationClaims) Encode(pair nkeys.KeyPair) (string, error) {
return a.EncodeWithSigner(pair, nil)
}
func (a *ActivationClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
if !nkeys.IsValidPublicAccountKey(a.ClaimsData.Subject) {
return "", errors.New("expected subject to be an account")
}
a.Type = ActivationClaim
return a.ClaimsData.encode(pair, a, fn)
}
// DecodeActivationClaims tries to create an activation claim from a JWT string
func DecodeActivationClaims(token string) (*ActivationClaims, error) {
claims, err := Decode(token)
if err != nil {
return nil, err
}
ac, ok := claims.(*ActivationClaims)
if !ok {
return nil, errors.New("not activation claim")
}
return ac, nil
}
// Payload returns the activation specific part of the JWT
func (a *ActivationClaims) Payload() interface{} {
return a.Activation
}
// Validate checks the claims
func (a *ActivationClaims) Validate(vr *ValidationResults) {
a.validateWithTimeChecks(vr, true)
}
// Validate checks the claims
func (a *ActivationClaims) validateWithTimeChecks(vr *ValidationResults, timeChecks bool) {
if timeChecks {
a.ClaimsData.Validate(vr)
}
a.Activation.Validate(vr)
if a.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(a.IssuerAccount) {
vr.AddError("account_id is not an account public key")
}
}
func (a *ActivationClaims) ClaimType() ClaimType {
return a.Type
}
func (a *ActivationClaims) updateVersion() {
a.GenericFields.Version = libVersion
}
// ExpectedPrefixes defines the types that can sign an activation jwt, account and oeprator
func (a *ActivationClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return []nkeys.PrefixByte{nkeys.PrefixByteAccount, nkeys.PrefixByteOperator}
}
// Claims returns the generic part of the JWT
func (a *ActivationClaims) Claims() *ClaimsData {
return &a.ClaimsData
}
func (a *ActivationClaims) String() string {
return a.ClaimsData.String(a)
}
// HashID returns a hash of the claims that can be used to identify it.
// The hash is calculated by creating a string with
// issuerPubKey.subjectPubKey.<subject> and constructing the sha-256 hash and base32 encoding that.
// <subject> is the exported subject, minus any wildcards, so foo.* becomes foo.
// the one special case is that if the export start with "*" or is ">" the <subject> "_"
func (a *ActivationClaims) HashID() (string, error) {
if a.Issuer == "" || a.Subject == "" || a.ImportSubject == "" {
return "", fmt.Errorf("not enough data in the activaion claims to create a hash")
}
subject := cleanSubject(string(a.ImportSubject))
base := fmt.Sprintf("%s.%s.%s", a.Issuer, a.Subject, subject)
h := sha256.New()
h.Write([]byte(base))
sha := h.Sum(nil)
hash := base32.StdEncoding.EncodeToString(sha)
return hash, nil
}
func cleanSubject(subject string) string {
split := strings.Split(subject, ".")
cleaned := ""
for i, tok := range split {
if tok == "*" || tok == ">" {
if i == 0 {
cleaned = "_"
break
}
cleaned = strings.Join(split[:i], ".")
break
}
}
if cleaned == "" {
cleaned = subject
}
return cleaned
}
+255
View File
@@ -0,0 +1,255 @@
/*
* 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.
*/
package jwt
import (
"errors"
"github.com/nats-io/nkeys"
)
// ServerID is basic static info for a NATS server.
type ServerID struct {
Name string `json:"name"`
Host string `json:"host"`
ID string `json:"id"`
Version string `json:"version,omitempty"`
Cluster string `json:"cluster,omitempty"`
Tags TagList `json:"tags,omitempty"`
XKey string `json:"xkey,omitempty"`
}
// ClientInformation is information about a client that is trying to authorize.
type ClientInformation struct {
Host string `json:"host,omitempty"`
ID uint64 `json:"id,omitempty"`
User string `json:"user,omitempty"`
Name string `json:"name,omitempty"`
Tags TagList `json:"tags,omitempty"`
NameTag string `json:"name_tag,omitempty"`
Kind string `json:"kind,omitempty"`
Type string `json:"type,omitempty"`
MQTT string `json:"mqtt_id,omitempty"`
Nonce string `json:"nonce,omitempty"`
}
// ConnectOptions represents options that were set in the CONNECT protocol from the client
// during authorization.
type ConnectOptions struct {
JWT string `json:"jwt,omitempty"`
Nkey string `json:"nkey,omitempty"`
SignedNonce string `json:"sig,omitempty"`
Token string `json:"auth_token,omitempty"`
Username string `json:"user,omitempty"`
Password string `json:"pass,omitempty"`
Name string `json:"name,omitempty"`
Lang string `json:"lang,omitempty"`
Version string `json:"version,omitempty"`
Protocol int `json:"protocol"`
}
// ClientTLS is information about TLS state if present, including client certs.
// If the client certs were present and verified they will be under verified chains
// with the client peer cert being VerifiedChains[0]. These are complete and pem encoded.
// If they were not verified, they will be under certs.
type ClientTLS struct {
Version string `json:"version,omitempty"`
Cipher string `json:"cipher,omitempty"`
Certs StringList `json:"certs,omitempty"`
VerifiedChains []StringList `json:"verified_chains,omitempty"`
}
// AuthorizationRequest represents all the information we know about the client that
// will be sent to an external authorization service.
type AuthorizationRequest struct {
Server ServerID `json:"server_id"`
UserNkey string `json:"user_nkey"`
ClientInformation ClientInformation `json:"client_info"`
ConnectOptions ConnectOptions `json:"connect_opts"`
TLS *ClientTLS `json:"client_tls,omitempty"`
RequestNonce string `json:"request_nonce,omitempty"`
GenericFields
}
// AuthorizationRequestClaims defines an external auth request JWT.
// These wil be signed by a NATS server.
type AuthorizationRequestClaims struct {
ClaimsData
AuthorizationRequest `json:"nats"`
}
// NewAuthorizationRequestClaims creates an auth request JWT with the specific subject/public key.
func NewAuthorizationRequestClaims(subject string) *AuthorizationRequestClaims {
if subject == "" {
return nil
}
var ac AuthorizationRequestClaims
ac.Subject = subject
return &ac
}
// Validate checks the generic and specific parts of the auth request jwt.
func (ac *AuthorizationRequestClaims) Validate(vr *ValidationResults) {
if ac.UserNkey == "" {
vr.AddError("User nkey is required")
} else if !nkeys.IsValidPublicUserKey(ac.UserNkey) {
vr.AddError("User nkey %q is not a valid user public key", ac.UserNkey)
}
ac.ClaimsData.Validate(vr)
}
// Encode tries to turn the auth request claims into a JWT string.
func (ac *AuthorizationRequestClaims) Encode(pair nkeys.KeyPair) (string, error) {
return ac.EncodeWithSigner(pair, nil)
}
func (ac *AuthorizationRequestClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
ac.Type = AuthorizationRequestClaim
return ac.ClaimsData.encode(pair, ac, fn)
}
// DecodeAuthorizationRequestClaims tries to parse an auth request claims from a JWT string
func DecodeAuthorizationRequestClaims(token string) (*AuthorizationRequestClaims, error) {
claims, err := Decode(token)
if err != nil {
return nil, err
}
ac, ok := claims.(*AuthorizationRequestClaims)
if !ok {
return nil, errors.New("not an authorization request claim")
}
return ac, nil
}
// ExpectedPrefixes defines the types that can encode an auth request jwt, servers.
func (ac *AuthorizationRequestClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return []nkeys.PrefixByte{nkeys.PrefixByteServer}
}
func (ac *AuthorizationRequestClaims) ClaimType() ClaimType {
return ac.Type
}
// Claims returns the request claims data.
func (ac *AuthorizationRequestClaims) Claims() *ClaimsData {
return &ac.ClaimsData
}
// Payload pulls the request specific payload out of the claims.
func (ac *AuthorizationRequestClaims) Payload() interface{} {
return &ac.AuthorizationRequest
}
func (ac *AuthorizationRequestClaims) String() string {
return ac.ClaimsData.String(ac)
}
func (ac *AuthorizationRequestClaims) updateVersion() {
ac.GenericFields.Version = libVersion
}
type AuthorizationResponse struct {
Jwt string `json:"jwt,omitempty"`
Error string `json:"error,omitempty"`
// IssuerAccount stores the public key for the account the issuer represents.
// When set, the claim was issued by a signing key.
IssuerAccount string `json:"issuer_account,omitempty"`
GenericFields
}
type AuthorizationResponseClaims struct {
ClaimsData
AuthorizationResponse `json:"nats"`
}
func NewAuthorizationResponseClaims(subject string) *AuthorizationResponseClaims {
if subject == "" {
return nil
}
var ac AuthorizationResponseClaims
ac.Subject = subject
return &ac
}
// DecodeAuthorizationResponseClaims tries to parse an auth request claims from a JWT string
func DecodeAuthorizationResponseClaims(token string) (*AuthorizationResponseClaims, error) {
claims, err := Decode(token)
if err != nil {
return nil, err
}
ac, ok := claims.(*AuthorizationResponseClaims)
if !ok {
return nil, errors.New("not an authorization request claim")
}
return ac, nil
}
// ExpectedPrefixes defines the types that can encode an auth request jwt, servers.
func (ar *AuthorizationResponseClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return []nkeys.PrefixByte{nkeys.PrefixByteAccount}
}
func (ar *AuthorizationResponseClaims) ClaimType() ClaimType {
return ar.Type
}
// Claims returns the request claims data.
func (ar *AuthorizationResponseClaims) Claims() *ClaimsData {
return &ar.ClaimsData
}
// Payload pulls the request specific payload out of the claims.
func (ar *AuthorizationResponseClaims) Payload() interface{} {
return &ar.AuthorizationResponse
}
func (ar *AuthorizationResponseClaims) String() string {
return ar.ClaimsData.String(ar)
}
func (ar *AuthorizationResponseClaims) updateVersion() {
ar.GenericFields.Version = libVersion
}
// Validate checks the generic and specific parts of the auth request jwt.
func (ar *AuthorizationResponseClaims) Validate(vr *ValidationResults) {
if !nkeys.IsValidPublicUserKey(ar.Subject) {
vr.AddError("Subject must be a user public key")
}
if !nkeys.IsValidPublicServerKey(ar.Audience) {
vr.AddError("Audience must be a server public key")
}
if ar.Error == "" && ar.Jwt == "" {
vr.AddError("Error or Jwt is required")
}
if ar.Error != "" && ar.Jwt != "" {
vr.AddError("Only Error or Jwt can be set")
}
if ar.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(ar.IssuerAccount) {
vr.AddError("issuer_account is not an account public key")
}
ar.ClaimsData.Validate(vr)
}
// Encode tries to turn the auth request claims into a JWT string.
func (ar *AuthorizationResponseClaims) Encode(pair nkeys.KeyPair) (string, error) {
return ar.EncodeWithSigner(pair, nil)
}
func (ar *AuthorizationResponseClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
ar.Type = AuthorizationResponseClaim
return ar.ClaimsData.encode(pair, ar, fn)
}
+299
View File
@@ -0,0 +1,299 @@
/*
* Copyright 2018-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 jwt
import (
"crypto/sha512"
"encoding/base32"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/nats-io/nkeys"
)
// ClaimType is used to indicate the type of JWT being stored in a Claim
type ClaimType string
const (
// OperatorClaim is the type of an operator JWT
OperatorClaim = "operator"
// AccountClaim is the type of an Account JWT
AccountClaim = "account"
// UserClaim is the type of an user JWT
UserClaim = "user"
// ActivationClaim is the type of an activation JWT
ActivationClaim = "activation"
// AuthorizationRequestClaim is the type of an auth request claim JWT
AuthorizationRequestClaim = "authorization_request"
// AuthorizationResponseClaim is the response for an auth request
AuthorizationResponseClaim = "authorization_response"
// GenericClaim is a type that doesn't match Operator/Account/User/ActionClaim
GenericClaim = "generic"
)
func IsGenericClaimType(s string) bool {
switch s {
case OperatorClaim:
fallthrough
case AccountClaim:
fallthrough
case UserClaim:
fallthrough
case AuthorizationRequestClaim:
fallthrough
case AuthorizationResponseClaim:
fallthrough
case ActivationClaim:
return false
case GenericClaim:
return true
default:
return true
}
}
// SignFn is used in an external sign environment. The function should be
// able to locate the private key for the specified pub key specified and sign the
// specified data returning the signature as generated.
type SignFn func(pub string, data []byte) ([]byte, error)
// Claims is a JWT claims
type Claims interface {
Claims() *ClaimsData
Encode(kp nkeys.KeyPair) (string, error)
EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error)
ExpectedPrefixes() []nkeys.PrefixByte
Payload() interface{}
String() string
Validate(vr *ValidationResults)
ClaimType() ClaimType
verify(payload string, sig []byte) bool
updateVersion()
}
type GenericFields struct {
Tags TagList `json:"tags,omitempty"`
Type ClaimType `json:"type,omitempty"`
Version int `json:"version,omitempty"`
}
// ClaimsData is the base struct for all claims
type ClaimsData struct {
Audience string `json:"aud,omitempty"`
Expires int64 `json:"exp,omitempty"`
ID string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
Name string `json:"name,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}
// Prefix holds the prefix byte for an NKey
type Prefix struct {
nkeys.PrefixByte
}
func encodeToString(d []byte) string {
return base64.RawURLEncoding.EncodeToString(d)
}
func decodeString(s string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(s)
}
func serialize(v interface{}) (string, error) {
j, err := json.Marshal(v)
if err != nil {
return "", err
}
return encodeToString(j), nil
}
func (c *ClaimsData) doEncode(header *Header, kp nkeys.KeyPair, claim Claims, fn SignFn) (string, error) {
if header == nil {
return "", errors.New("header is required")
}
if kp == nil {
return "", errors.New("keypair is required")
}
if c != claim.Claims() {
return "", errors.New("claim and claim data do not match")
}
if c.Subject == "" {
return "", errors.New("subject is not set")
}
h, err := serialize(header)
if err != nil {
return "", err
}
issuerBytes, err := kp.PublicKey()
if err != nil {
return "", err
}
prefixes := claim.ExpectedPrefixes()
if prefixes != nil {
ok := false
for _, p := range prefixes {
switch p {
case nkeys.PrefixByteAccount:
if nkeys.IsValidPublicAccountKey(issuerBytes) {
ok = true
}
case nkeys.PrefixByteOperator:
if nkeys.IsValidPublicOperatorKey(issuerBytes) {
ok = true
}
case nkeys.PrefixByteServer:
if nkeys.IsValidPublicServerKey(issuerBytes) {
ok = true
}
case nkeys.PrefixByteCluster:
if nkeys.IsValidPublicClusterKey(issuerBytes) {
ok = true
}
case nkeys.PrefixByteUser:
if nkeys.IsValidPublicUserKey(issuerBytes) {
ok = true
}
}
}
if !ok {
return "", fmt.Errorf("unable to validate expected prefixes - %v", prefixes)
}
}
c.Issuer = issuerBytes
c.IssuedAt = time.Now().UTC().Unix()
c.ID = "" // to create a repeatable hash
c.ID, err = c.hash()
if err != nil {
return "", err
}
claim.updateVersion()
payload, err := serialize(claim)
if err != nil {
return "", err
}
toSign := fmt.Sprintf("%s.%s", h, payload)
eSig := ""
if header.Algorithm == AlgorithmNkeyOld {
return "", errors.New(AlgorithmNkeyOld + " not supported to write jwtV2")
} else if header.Algorithm == AlgorithmNkey {
var sig []byte
if fn != nil {
pk, err := kp.PublicKey()
if err != nil {
return "", err
}
sig, err = fn(pk, []byte(toSign))
if err != nil {
return "", err
}
} else {
sig, err = kp.Sign([]byte(toSign))
if err != nil {
return "", err
}
}
eSig = encodeToString(sig)
} else {
return "", errors.New(header.Algorithm + " not supported to write jwtV2")
}
// hash need no padding
return fmt.Sprintf("%s.%s", toSign, eSig), nil
}
func (c *ClaimsData) hash() (string, error) {
j, err := json.Marshal(c)
if err != nil {
return "", err
}
h := sha512.New512_256()
h.Write(j)
return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(h.Sum(nil)), nil
}
// Encode encodes a claim into a JWT token. The claim is signed with the
// provided nkey's private key
func (c *ClaimsData) encode(kp nkeys.KeyPair, payload Claims, fn SignFn) (string, error) {
return c.doEncode(&Header{TokenTypeJwt, AlgorithmNkey}, kp, payload, fn)
}
// Returns a JSON representation of the claim
func (c *ClaimsData) String(claim interface{}) string {
j, err := json.MarshalIndent(claim, "", " ")
if err != nil {
return ""
}
return string(j)
}
func parseClaims(s string, target Claims) error {
h, err := decodeString(s)
if err != nil {
return err
}
return json.Unmarshal(h, &target)
}
// Verify verifies that the encoded payload was signed by the
// provided public key. Verify is called automatically with
// the claims portion of the token and the public key in the claim.
// Client code need to insure that the public key in the
// claim is trusted.
func (c *ClaimsData) verify(payload string, sig []byte) bool {
// decode the public key
kp, err := nkeys.FromPublicKey(c.Issuer)
if err != nil {
return false
}
if err := kp.Verify([]byte(payload), sig); err != nil {
return false
}
return true
}
// Validate checks a claim to make sure it is valid. Validity checks
// include expiration and not before constraints.
func (c *ClaimsData) Validate(vr *ValidationResults) {
now := time.Now().UTC().Unix()
if c.Expires > 0 && now > c.Expires {
vr.AddTimeCheck("claim is expired")
}
if c.NotBefore > 0 && c.NotBefore > now {
vr.AddTimeCheck("claim is not yet valid")
}
}
// IsSelfSigned returns true if the claims issuer is the subject
func (c *ClaimsData) IsSelfSigned() bool {
return c.Issuer == c.Subject
}
+281
View File
@@ -0,0 +1,281 @@
/*
* Copyright 2019-2020 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 jwt
import (
"bytes"
"errors"
"fmt"
"regexp"
"strings"
"time"
"github.com/nats-io/nkeys"
)
// DecorateJWT returns a decorated JWT that describes the kind of JWT
func DecorateJWT(jwtString string) ([]byte, error) {
gc, err := Decode(jwtString)
if err != nil {
return nil, err
}
return formatJwt(string(gc.ClaimType()), jwtString)
}
func formatJwt(kind string, jwtString string) ([]byte, error) {
templ := `-----BEGIN NATS %s JWT-----
%s
------END NATS %s JWT------
`
w := bytes.NewBuffer(nil)
kind = strings.ToUpper(kind)
_, err := fmt.Fprintf(w, templ, kind, jwtString, kind)
if err != nil {
return nil, err
}
return w.Bytes(), nil
}
// DecorateSeed takes a seed and returns a string that wraps
// the seed in the form:
//
// ************************* IMPORTANT *************************
// NKEY Seed printed below can be used sign and prove identity.
// NKEYs are sensitive and should be treated as secrets.
//
// -----BEGIN USER NKEY SEED-----
// SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM
// ------END USER NKEY SEED------
func DecorateSeed(seed []byte) ([]byte, error) {
w := bytes.NewBuffer(nil)
ts := bytes.TrimSpace(seed)
if len(ts) < 2 {
return nil, errors.New("seed is too short")
}
pre := string(ts[0:2])
kind := ""
switch pre {
case "SU":
kind = "USER"
case "SA":
kind = "ACCOUNT"
case "SO":
kind = "OPERATOR"
default:
return nil, errors.New("seed is not an operator, account or user seed")
}
header := `************************* IMPORTANT *************************
NKEY Seed printed below can be used to sign and prove identity.
NKEYs are sensitive and should be treated as secrets.
-----BEGIN %s NKEY SEED-----
`
_, err := fmt.Fprintf(w, header, kind)
if err != nil {
return nil, err
}
w.Write(ts)
footer := `
------END %s NKEY SEED------
*************************************************************
`
_, err = fmt.Fprintf(w, footer, kind)
if err != nil {
return nil, err
}
return w.Bytes(), nil
}
var userConfigRE = regexp.MustCompile(`\s*(?:(?:[-]{3,}.*[-]{3,}\r?\n)([\w\-.=]+)(?:\r?\n[-]{3,}.*[-]{3,}(\r?\n|\z)))`)
// An user config file looks like this:
// -----BEGIN NATS USER JWT-----
// eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5...
// ------END NATS USER JWT------
//
// ************************* IMPORTANT *************************
// NKEY Seed printed below can be used sign and prove identity.
// NKEYs are sensitive and should be treated as secrets.
//
// -----BEGIN USER NKEY SEED-----
// SUAIO3FHUX5PNV2LQIIP7TZ3N4L7TX3W53MQGEIVYFIGA635OZCKEYHFLM
// ------END USER NKEY SEED------
// FormatUserConfig returns a decorated file with a decorated JWT and decorated seed
func FormatUserConfig(jwtString string, seed []byte) ([]byte, error) {
gc, err := Decode(jwtString)
if err != nil {
return nil, err
}
if gc.ClaimType() != UserClaim {
return nil, fmt.Errorf("%q cannot be serialized as a user config", string(gc.ClaimType()))
}
w := bytes.NewBuffer(nil)
jd, err := formatJwt(string(gc.ClaimType()), jwtString)
if err != nil {
return nil, err
}
_, err = w.Write(jd)
if err != nil {
return nil, err
}
if !bytes.HasPrefix(bytes.TrimSpace(seed), []byte("SU")) {
return nil, fmt.Errorf("nkey seed is not an user seed")
}
kp, err := nkeys.FromSeed(seed)
if err != nil {
return nil, err
}
pk, err := kp.PublicKey()
if err != nil {
return nil, err
}
if pk != gc.Claims().Subject {
return nil, fmt.Errorf("nkey seed does not match the jwt subject")
}
d, err := DecorateSeed(seed)
if err != nil {
return nil, err
}
_, err = w.Write(d)
if err != nil {
return nil, err
}
return w.Bytes(), nil
}
// ParseDecoratedJWT takes a creds file and returns the JWT portion.
func ParseDecoratedJWT(contents []byte) (string, error) {
items := userConfigRE.FindAllSubmatch(contents, -1)
if len(items) == 0 {
return string(contents), nil
}
// First result should be the user JWT.
// We copy here so that if the file contained a seed file too we wipe appropriately.
raw := items[0][1]
tmp := make([]byte, len(raw))
copy(tmp, raw)
return string(tmp), nil
}
// ParseDecoratedNKey takes a creds file, finds the NKey portion and creates a
// key pair from it.
func ParseDecoratedNKey(contents []byte) (nkeys.KeyPair, error) {
var seed []byte
items := userConfigRE.FindAllSubmatch(contents, -1)
if len(items) > 1 {
seed = items[1][1]
} else {
lines := bytes.Split(contents, []byte("\n"))
for _, line := range lines {
if bytes.HasPrefix(bytes.TrimSpace(line), []byte("SO")) ||
bytes.HasPrefix(bytes.TrimSpace(line), []byte("SA")) ||
bytes.HasPrefix(bytes.TrimSpace(line), []byte("SU")) {
seed = line
break
}
}
}
if seed == nil {
return nil, errors.New("no nkey seed found")
}
if !bytes.HasPrefix(seed, []byte("SO")) &&
!bytes.HasPrefix(seed, []byte("SA")) &&
!bytes.HasPrefix(seed, []byte("SU")) {
return nil, errors.New("doesn't contain a seed nkey")
}
kp, err := nkeys.FromSeed(seed)
if err != nil {
return nil, err
}
return kp, nil
}
// ParseDecoratedUserNKey takes a creds file, finds the NKey portion and creates a
// key pair from it. Similar to ParseDecoratedNKey but fails for non-user keys.
func ParseDecoratedUserNKey(contents []byte) (nkeys.KeyPair, error) {
nk, err := ParseDecoratedNKey(contents)
if err != nil {
return nil, err
}
seed, err := nk.Seed()
if err != nil {
return nil, err
}
if !bytes.HasPrefix(seed, []byte("SU")) {
return nil, errors.New("doesn't contain an user seed nkey")
}
kp, err := nkeys.FromSeed(seed)
if err != nil {
return nil, err
}
return kp, nil
}
// IssueUserJWT takes an account scoped signing key, account id, and use public key (and optionally a user's name, an expiration duration and tags) and returns a valid signed JWT.
// The scopedSigningKey, is a mandatory account scoped signing nkey pair to sign the generated jwt (note that it _must_ be a signing key attached to the account (and a _scoped_ signing key), not the account's private (seed) key).
// The accountId, is a mandatory public account nkey. Will return error when not set or not account nkey.
// The publicUserKey, is a mandatory public user nkey. Will return error when not set or not user nkey.
// The name, is an optional human-readable name. When absent, default to publicUserKey.
// The expirationDuration, is an optional but recommended duration, when the generated jwt needs to expire. If not set, JWT will not expire.
// The tags, is an optional list of tags to be included in the JWT.
//
// Returns:
// string, resulting jwt.
// error, when issues arose.
func IssueUserJWT(scopedSigningKey nkeys.KeyPair, accountId string, publicUserKey string, name string, expirationDuration time.Duration, tags ...string) (string, error) {
if !nkeys.IsValidPublicAccountKey(accountId) {
return "", errors.New("issueUserJWT requires an account key for the accountId parameter, but got " + nkeys.Prefix(accountId).String())
}
if !nkeys.IsValidPublicUserKey(publicUserKey) {
return "", errors.New("issueUserJWT requires an account key for the publicUserKey parameter, but got " + nkeys.Prefix(publicUserKey).String())
}
claim := NewUserClaims(publicUserKey)
claim.SetScoped(true)
if expirationDuration != 0 {
claim.Expires = time.Now().Add(expirationDuration).UTC().Unix()
}
claim.IssuerAccount = accountId
if name != "" {
claim.Name = name
} else {
claim.Name = publicUserKey
}
claim.Subject = publicUserKey
claim.Tags = tags
encoded, err := claim.Encode(scopedSigningKey)
if err != nil {
return "", errors.New("err encoding claim " + err.Error())
}
return encoded, nil
}
+173
View File
@@ -0,0 +1,173 @@
/*
* Copyright 2020-2022 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 jwt
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/nats-io/nkeys"
)
const libVersion = 2
// MaxTokenSize is the maximum size of a JWT token in bytes
const MaxTokenSize = 1024 * 1024 // 1MB
// ErrTokenTooLarge is returned when a token exceeds MaxTokenSize
var ErrTokenTooLarge = errors.New("token too large")
type identifier struct {
Type ClaimType `json:"type,omitempty"`
GenericFields `json:"nats,omitempty"`
}
func (i *identifier) Kind() ClaimType {
if i.Type != "" {
return i.Type
}
return i.GenericFields.Type
}
func (i *identifier) Version() int {
if i.Type != "" {
return 1
}
return i.GenericFields.Version
}
type v1ClaimsDataDeletedFields struct {
Tags TagList `json:"tags,omitempty"`
Type ClaimType `json:"type,omitempty"`
IssuerAccount string `json:"issuer_account,omitempty"`
}
// Decode takes a JWT string decodes it and validates it
// and return the embedded Claims. If the token header
// doesn't match the expected algorithm, or the claim is
// not valid or verification fails an error is returned.
func Decode(token string) (Claims, error) {
if len(token) > MaxTokenSize {
return nil, fmt.Errorf("token size %d exceeds maximum of %d bytes: %w", len(token), MaxTokenSize, ErrTokenTooLarge)
}
// must have 3 chunks
chunks := strings.Split(token, ".")
if len(chunks) != 3 {
return nil, errors.New("expected 3 chunks")
}
// header
if _, err := parseHeaders(chunks[0]); err != nil {
return nil, err
}
// claim
data, err := decodeString(chunks[1])
if err != nil {
return nil, err
}
ver, claim, err := loadClaims(data)
if err != nil {
return nil, err
}
// sig
sig, err := decodeString(chunks[2])
if err != nil {
return nil, err
}
if ver <= 1 {
if !claim.verify(chunks[1], sig) {
return nil, errors.New("claim failed V1 signature verification")
}
} else {
if !claim.verify(token[:len(chunks[0])+len(chunks[1])+1], sig) {
return nil, errors.New("claim failed V2 signature verification")
}
}
prefixes := claim.ExpectedPrefixes()
if prefixes != nil {
ok := false
issuer := claim.Claims().Issuer
for _, p := range prefixes {
switch p {
case nkeys.PrefixByteAccount:
if nkeys.IsValidPublicAccountKey(issuer) {
ok = true
}
case nkeys.PrefixByteOperator:
if nkeys.IsValidPublicOperatorKey(issuer) {
ok = true
}
case nkeys.PrefixByteUser:
if nkeys.IsValidPublicUserKey(issuer) {
ok = true
}
case nkeys.PrefixByteServer:
if nkeys.IsValidPublicServerKey(issuer) {
ok = true
}
}
}
if !ok {
return nil, fmt.Errorf("unable to validate expected prefixes - %v", prefixes)
}
}
return claim, nil
}
func loadClaims(data []byte) (int, Claims, error) {
var id identifier
if err := json.Unmarshal(data, &id); err != nil {
return -1, nil, err
}
if id.Version() > libVersion {
return -1, nil, errors.New("JWT was generated by a newer version ")
}
var claim Claims
var err error
switch id.Kind() {
case OperatorClaim:
claim, err = loadOperator(data, id.Version())
case AccountClaim:
claim, err = loadAccount(data, id.Version())
case UserClaim:
claim, err = loadUser(data, id.Version())
case ActivationClaim:
claim, err = loadActivation(data, id.Version())
case AuthorizationRequestClaim:
claim, err = loadAuthorizationRequest(data, id.Version())
case AuthorizationResponseClaim:
claim, err = loadAuthorizationResponse(data, id.Version())
case "cluster":
return -1, nil, errors.New("ClusterClaims are not supported")
case "server":
return -1, nil, errors.New("ServerClaims are not supported")
default:
var gc GenericClaims
if err := json.Unmarshal(data, &gc); err != nil {
return -1, nil, err
}
return -1, &gc, nil
}
return id.Version(), claim, err
}
+87
View File
@@ -0,0 +1,87 @@
/*
* Copyright 2020 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 jwt
import (
"encoding/json"
"fmt"
)
type v1NatsAccount struct {
Imports Imports `json:"imports,omitempty"`
Exports Exports `json:"exports,omitempty"`
Limits struct {
NatsLimits
AccountLimits
} `json:"limits,omitempty"`
SigningKeys StringList `json:"signing_keys,omitempty"`
Revocations RevocationList `json:"revocations,omitempty"`
}
func loadAccount(data []byte, version int) (*AccountClaims, error) {
switch version {
case 1:
var v1a v1AccountClaims
if err := json.Unmarshal(data, &v1a); err != nil {
return nil, err
}
return v1a.Migrate()
case 2:
var v2a AccountClaims
v2a.SigningKeys = make(SigningKeys)
if err := json.Unmarshal(data, &v2a); err != nil {
return nil, err
}
if len(v2a.Limits.JetStreamTieredLimits) > 0 {
v2a.Limits.JetStreamLimits = JetStreamLimits{}
}
return &v2a, nil
default:
return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version)
}
}
type v1AccountClaims struct {
ClaimsData
v1ClaimsDataDeletedFields
v1NatsAccount `json:"nats,omitempty"`
}
func (oa v1AccountClaims) Migrate() (*AccountClaims, error) {
return oa.migrateV1()
}
func (oa v1AccountClaims) migrateV1() (*AccountClaims, error) {
var a AccountClaims
// copy the base claim
a.ClaimsData = oa.ClaimsData
// move the moved fields
a.Account.Type = oa.v1ClaimsDataDeletedFields.Type
a.Account.Tags = oa.v1ClaimsDataDeletedFields.Tags
// copy the account data
a.Account.Imports = oa.v1NatsAccount.Imports
a.Account.Exports = oa.v1NatsAccount.Exports
a.Account.Limits.AccountLimits = oa.v1NatsAccount.Limits.AccountLimits
a.Account.Limits.NatsLimits = oa.v1NatsAccount.Limits.NatsLimits
a.Account.Limits.JetStreamLimits = JetStreamLimits{}
a.Account.SigningKeys = make(SigningKeys)
for _, v := range oa.SigningKeys {
a.Account.SigningKeys.Add(v)
}
a.Account.Revocations = oa.v1NatsAccount.Revocations
a.Version = 1
return &a, nil
}
+77
View File
@@ -0,0 +1,77 @@
/*
* Copyright 2020 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 jwt
import (
"encoding/json"
"fmt"
)
// Migration adds GenericFields
type v1NatsActivation struct {
ImportSubject Subject `json:"subject,omitempty"`
ImportType ExportType `json:"type,omitempty"`
// Limit values deprecated inv v2
Max int64 `json:"max,omitempty"`
Payload int64 `json:"payload,omitempty"`
Src string `json:"src,omitempty"`
Times []TimeRange `json:"times,omitempty"`
}
type v1ActivationClaims struct {
ClaimsData
v1ClaimsDataDeletedFields
v1NatsActivation `json:"nats,omitempty"`
}
func loadActivation(data []byte, version int) (*ActivationClaims, error) {
switch version {
case 1:
var v1a v1ActivationClaims
v1a.Max = NoLimit
v1a.Payload = NoLimit
if err := json.Unmarshal(data, &v1a); err != nil {
return nil, err
}
return v1a.Migrate()
case 2:
var v2a ActivationClaims
if err := json.Unmarshal(data, &v2a); err != nil {
return nil, err
}
return &v2a, nil
default:
return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version)
}
}
func (oa v1ActivationClaims) Migrate() (*ActivationClaims, error) {
return oa.migrateV1()
}
func (oa v1ActivationClaims) migrateV1() (*ActivationClaims, error) {
var a ActivationClaims
// copy the base claim
a.ClaimsData = oa.ClaimsData
// move the moved fields
a.Activation.Type = oa.v1ClaimsDataDeletedFields.Type
a.Activation.Tags = oa.v1ClaimsDataDeletedFields.Tags
a.Activation.IssuerAccount = oa.v1ClaimsDataDeletedFields.IssuerAccount
// copy the activation data
a.ImportSubject = oa.ImportSubject
a.ImportType = oa.ImportType
a.Version = 1
return &a, nil
}
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright 2022 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 jwt
import (
"encoding/json"
)
func loadAuthorizationRequest(data []byte, version int) (*AuthorizationRequestClaims, error) {
var ac AuthorizationRequestClaims
if err := json.Unmarshal(data, &ac); err != nil {
return nil, err
}
return &ac, nil
}
func loadAuthorizationResponse(data []byte, version int) (*AuthorizationResponseClaims, error) {
var ac AuthorizationResponseClaims
if err := json.Unmarshal(data, &ac); err != nil {
return nil, err
}
return &ac, nil
}
+73
View File
@@ -0,0 +1,73 @@
/*
* Copyright 2020 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 jwt
import (
"encoding/json"
"fmt"
)
type v1NatsOperator struct {
SigningKeys StringList `json:"signing_keys,omitempty"`
AccountServerURL string `json:"account_server_url,omitempty"`
OperatorServiceURLs StringList `json:"operator_service_urls,omitempty"`
SystemAccount string `json:"system_account,omitempty"`
}
func loadOperator(data []byte, version int) (*OperatorClaims, error) {
switch version {
case 1:
var v1a v1OperatorClaims
if err := json.Unmarshal(data, &v1a); err != nil {
return nil, err
}
return v1a.Migrate()
case 2:
var v2a OperatorClaims
if err := json.Unmarshal(data, &v2a); err != nil {
return nil, err
}
return &v2a, nil
default:
return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version)
}
}
type v1OperatorClaims struct {
ClaimsData
v1ClaimsDataDeletedFields
v1NatsOperator `json:"nats,omitempty"`
}
func (oa v1OperatorClaims) Migrate() (*OperatorClaims, error) {
return oa.migrateV1()
}
func (oa v1OperatorClaims) migrateV1() (*OperatorClaims, error) {
var a OperatorClaims
// copy the base claim
a.ClaimsData = oa.ClaimsData
// move the moved fields
a.Operator.Type = oa.v1ClaimsDataDeletedFields.Type
a.Operator.Tags = oa.v1ClaimsDataDeletedFields.Tags
// copy the account data
a.Operator.SigningKeys = oa.v1NatsOperator.SigningKeys
a.Operator.AccountServerURL = oa.v1NatsOperator.AccountServerURL
a.Operator.OperatorServiceURLs = oa.v1NatsOperator.OperatorServiceURLs
a.Operator.SystemAccount = oa.v1NatsOperator.SystemAccount
a.Version = 1
return &a, nil
}
+81
View File
@@ -0,0 +1,81 @@
/*
* Copyright 2020 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 jwt
import (
"encoding/json"
"fmt"
)
type v1User struct {
Permissions
Limits
BearerToken bool `json:"bearer_token,omitempty"`
// Limit values deprecated inv v2
Max int64 `json:"max,omitempty"`
}
type v1UserClaimsDataDeletedFields struct {
v1ClaimsDataDeletedFields
IssuerAccount string `json:"issuer_account,omitempty"`
}
type v1UserClaims struct {
ClaimsData
v1UserClaimsDataDeletedFields
v1User `json:"nats,omitempty"`
}
func loadUser(data []byte, version int) (*UserClaims, error) {
switch version {
case 1:
var v1a v1UserClaims
v1a.Limits = Limits{NatsLimits: NatsLimits{NoLimit, NoLimit, NoLimit}}
v1a.Max = NoLimit
if err := json.Unmarshal(data, &v1a); err != nil {
return nil, err
}
return v1a.Migrate()
case 2:
var v2a UserClaims
if err := json.Unmarshal(data, &v2a); err != nil {
return nil, err
}
return &v2a, nil
default:
return nil, fmt.Errorf("library supports version %d or less - received %d", libVersion, version)
}
}
func (oa v1UserClaims) Migrate() (*UserClaims, error) {
return oa.migrateV1()
}
func (oa v1UserClaims) migrateV1() (*UserClaims, error) {
var u UserClaims
// copy the base claim
u.ClaimsData = oa.ClaimsData
// move the moved fields
u.User.Type = oa.v1ClaimsDataDeletedFields.Type
u.User.Tags = oa.v1ClaimsDataDeletedFields.Tags
u.User.IssuerAccount = oa.IssuerAccount
// copy the user data
u.User.Permissions = oa.v1User.Permissions
u.User.Limits = oa.v1User.Limits
u.User.BearerToken = oa.v1User.BearerToken
u.Version = 1
return &u, nil
}
+317
View File
@@ -0,0 +1,317 @@
/*
* Copyright 2018-2019 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 jwt
import (
"encoding/json"
"fmt"
"strings"
"time"
)
// ResponseType is used to store an export response type
type ResponseType string
const (
// ResponseTypeSingleton is used for a service that sends a single response only
ResponseTypeSingleton = "Singleton"
// ResponseTypeStream is used for a service that will send multiple responses
ResponseTypeStream = "Stream"
// ResponseTypeChunked is used for a service that sends a single response in chunks (so not quite a stream)
ResponseTypeChunked = "Chunked"
)
// ServiceLatency is used when observing and exported service for
// latency measurements.
// Sampling 1-100, represents sampling rate, defaults to 100.
// Results is the subject where the latency metrics are published.
// A metric will be defined by the nats-server's ServiceLatency. Time durations
// are in nanoseconds.
// see https://github.com/nats-io/nats-server/blob/main/server/accounts.go#L524
// e.g.
//
// {
// "app": "dlc22",
// "start": "2019-09-16T21:46:23.636869585-07:00",
// "svc": 219732,
// "nats": {
// "req": 320415,
// "resp": 228268,
// "sys": 0
// },
// "total": 768415
// }
type ServiceLatency struct {
Sampling SamplingRate `json:"sampling"`
Results Subject `json:"results"`
}
type SamplingRate int
const Headers = SamplingRate(0)
// MarshalJSON marshals the field as "headers" or percentages
func (r *SamplingRate) MarshalJSON() ([]byte, error) {
sr := *r
if sr == 0 {
return []byte(`"headers"`), nil
}
if sr >= 1 && sr <= 100 {
return []byte(fmt.Sprintf("%d", sr)), nil
}
return nil, fmt.Errorf("unknown sampling rate")
}
// UnmarshalJSON unmashals numbers as percentages or "headers"
func (t *SamplingRate) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return fmt.Errorf("empty sampling rate")
}
if strings.ToLower(string(b)) == `"headers"` {
*t = Headers
return nil
}
var j int
err := json.Unmarshal(b, &j)
if err != nil {
return err
}
*t = SamplingRate(j)
return nil
}
func (sl *ServiceLatency) Validate(vr *ValidationResults) {
if sl.Sampling != 0 {
if sl.Sampling < 1 || sl.Sampling > 100 {
vr.AddError("sampling percentage needs to be between 1-100")
}
}
sl.Results.Validate(vr)
if sl.Results.HasWildCards() {
vr.AddError("results subject can not contain wildcards")
}
}
// Export represents a single export
type Export struct {
Name string `json:"name,omitempty"`
Subject Subject `json:"subject,omitempty"`
Type ExportType `json:"type,omitempty"`
TokenReq bool `json:"token_req,omitempty"`
Revocations RevocationList `json:"revocations,omitempty"`
ResponseType ResponseType `json:"response_type,omitempty"`
ResponseThreshold time.Duration `json:"response_threshold,omitempty"`
Latency *ServiceLatency `json:"service_latency,omitempty"`
AccountTokenPosition uint `json:"account_token_position,omitempty"`
Advertise bool `json:"advertise,omitempty"`
AllowTrace bool `json:"allow_trace,omitempty"`
Info
}
// IsService returns true if an export is for a service
func (e *Export) IsService() bool {
return e.Type == Service
}
// IsStream returns true if an export is for a stream
func (e *Export) IsStream() bool {
return e.Type == Stream
}
// IsSingleResponse returns true if an export has a single response
// or no response type is set, also checks that the type is service
func (e *Export) IsSingleResponse() bool {
return e.Type == Service && (e.ResponseType == ResponseTypeSingleton || e.ResponseType == "")
}
// IsChunkedResponse returns true if an export has a chunked response
func (e *Export) IsChunkedResponse() bool {
return e.Type == Service && e.ResponseType == ResponseTypeChunked
}
// IsStreamResponse returns true if an export has a chunked response
func (e *Export) IsStreamResponse() bool {
return e.Type == Service && e.ResponseType == ResponseTypeStream
}
// Validate appends validation issues to the passed in results list
func (e *Export) Validate(vr *ValidationResults) {
if e == nil {
vr.AddError("null export is not allowed")
return
}
if !e.IsService() && !e.IsStream() {
vr.AddError("invalid export type: %q", e.Type)
}
if e.IsService() && !e.IsSingleResponse() && !e.IsChunkedResponse() && !e.IsStreamResponse() {
vr.AddError("invalid response type for service: %q", e.ResponseType)
}
if e.IsStream() {
if e.ResponseType != "" {
vr.AddError("invalid response type for stream: %q", e.ResponseType)
}
if e.AllowTrace {
vr.AddError("AllowTrace only valid for service export")
}
}
if e.Latency != nil {
if !e.IsService() {
vr.AddError("latency tracking only permitted for services")
}
e.Latency.Validate(vr)
}
if e.ResponseThreshold.Nanoseconds() < 0 {
vr.AddError("negative response threshold is invalid")
}
if e.ResponseThreshold.Nanoseconds() > 0 && !e.IsService() {
vr.AddError("response threshold only valid for services")
}
e.Subject.Validate(vr)
if e.AccountTokenPosition > 0 {
if !e.Subject.HasWildCards() {
vr.AddError("Account Token Position can only be used with wildcard subjects: %s", e.Subject)
} else {
subj := string(e.Subject)
token := strings.Split(subj, ".")
tkCnt := uint(len(token))
if e.AccountTokenPosition > tkCnt {
vr.AddError("Account Token Position %d exceeds length of subject '%s'",
e.AccountTokenPosition, e.Subject)
} else if tk := token[e.AccountTokenPosition-1]; tk != "*" {
vr.AddError("Account Token Position %d matches '%s' but must match a * in: %s",
e.AccountTokenPosition, tk, e.Subject)
}
}
}
e.Info.Validate(vr)
}
// Revoke enters a revocation by publickey using time.Now().
func (e *Export) Revoke(pubKey string) {
e.RevokeAt(pubKey, time.Now())
}
// RevokeAt enters a revocation by publickey and timestamp into this export
// If there is already a revocation for this public key that is newer, it is kept.
func (e *Export) RevokeAt(pubKey string, timestamp time.Time) {
if e.Revocations == nil {
e.Revocations = RevocationList{}
}
e.Revocations.Revoke(pubKey, timestamp)
}
// ClearRevocation removes any revocation for the public key
func (e *Export) ClearRevocation(pubKey string) {
e.Revocations.ClearRevocation(pubKey)
}
// isRevoked checks if the public key is in the revoked list with a timestamp later than the one passed in.
// Generally this method is called with the subject and issue time of the jwt to be tested.
// DO NOT pass time.Now(), it will not produce a stable/expected response.
func (e *Export) isRevoked(pubKey string, claimIssuedAt time.Time) bool {
return e.Revocations.IsRevoked(pubKey, claimIssuedAt)
}
// IsClaimRevoked checks if the activation revoked the claim passed in.
// Invalid claims (nil, no Subject or IssuedAt) will return true.
func (e *Export) IsClaimRevoked(claim *ActivationClaims) bool {
if claim == nil || claim.IssuedAt == 0 || claim.Subject == "" {
return true
}
return e.isRevoked(claim.Subject, time.Unix(claim.IssuedAt, 0))
}
// Exports is a slice of exports
type Exports []*Export
// Add appends exports to the list
func (e *Exports) Add(i ...*Export) {
*e = append(*e, i...)
}
func isContainedIn(kind ExportType, subjects []Subject, vr *ValidationResults) {
m := make(map[string]string)
for i, ns := range subjects {
for j, s := range subjects {
if i == j {
continue
}
if ns.IsContainedIn(s) {
str := string(s)
_, ok := m[str]
if !ok {
m[str] = string(ns)
}
}
}
}
if len(m) != 0 {
for k, v := range m {
var vi ValidationIssue
vi.Blocking = true
vi.Description = fmt.Sprintf("%s export subject %q already exports %q", kind, k, v)
vr.Add(&vi)
}
}
}
// Validate calls validate on all of the exports
func (e *Exports) Validate(vr *ValidationResults) {
var serviceSubjects []Subject
var streamSubjects []Subject
for _, v := range *e {
if v == nil {
vr.AddError("null export is not allowed")
continue
}
if v.IsService() {
serviceSubjects = append(serviceSubjects, v.Subject)
} else {
streamSubjects = append(streamSubjects, v.Subject)
}
v.Validate(vr)
}
isContainedIn(Service, serviceSubjects, vr)
isContainedIn(Stream, streamSubjects, vr)
}
// HasExportContainingSubject checks if the export list has an export with the provided subject
func (e *Exports) HasExportContainingSubject(subject Subject) bool {
for _, s := range *e {
if subject.IsContainedIn(s.Subject) {
return true
}
}
return false
}
func (e Exports) Len() int {
return len(e)
}
func (e Exports) Swap(i, j int) {
e[i], e[j] = e[j], e[i]
}
func (e Exports) Less(i, j int) bool {
return e[i].Subject < e[j].Subject
}
+161
View File
@@ -0,0 +1,161 @@
/*
* Copyright 2018-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 jwt
import (
"encoding/json"
"errors"
"strings"
"github.com/nats-io/nkeys"
)
// GenericClaims can be used to read a JWT as a map for any non-generic fields
type GenericClaims struct {
ClaimsData
Data map[string]interface{} `json:"nats,omitempty"`
}
// NewGenericClaims creates a map-based Claims
func NewGenericClaims(subject string) *GenericClaims {
if subject == "" {
return nil
}
c := GenericClaims{}
c.Subject = subject
c.Data = make(map[string]interface{})
return &c
}
// DecodeGeneric takes a JWT string and decodes it into a ClaimsData and map
func DecodeGeneric(token string) (*GenericClaims, error) {
// must have 3 chunks
chunks := strings.Split(token, ".")
if len(chunks) != 3 {
return nil, errors.New("expected 3 chunks")
}
// header
header, err := parseHeaders(chunks[0])
if err != nil {
return nil, err
}
// claim
data, err := decodeString(chunks[1])
if err != nil {
return nil, err
}
gc := struct {
GenericClaims
GenericFields
}{}
if err := json.Unmarshal(data, &gc); err != nil {
return nil, err
}
// sig
sig, err := decodeString(chunks[2])
if err != nil {
return nil, err
}
if header.Algorithm == AlgorithmNkeyOld {
if !gc.verify(chunks[1], sig) {
return nil, errors.New("claim failed V1 signature verification")
}
if tp := gc.GenericFields.Type; tp != "" {
// the conversion needs to be from a string because
// on custom types the type is not going to be one of
// the constants
gc.GenericClaims.Data["type"] = string(tp)
}
if tp := gc.GenericFields.Tags; len(tp) != 0 {
gc.GenericClaims.Data["tags"] = tp
}
} else {
if !gc.verify(token[:len(chunks[0])+len(chunks[1])+1], sig) {
return nil, errors.New("claim failed V2 signature verification")
}
}
return &gc.GenericClaims, nil
}
// Claims returns the standard part of the generic claim
func (gc *GenericClaims) Claims() *ClaimsData {
return &gc.ClaimsData
}
// Payload returns the custom part of the claims data
func (gc *GenericClaims) Payload() interface{} {
return &gc.Data
}
// Encode takes a generic claims and creates a JWT string
func (gc *GenericClaims) Encode(pair nkeys.KeyPair) (string, error) {
return gc.ClaimsData.encode(pair, gc, nil)
}
func (gc *GenericClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
return gc.ClaimsData.encode(pair, gc, fn)
}
// Validate checks the generic part of the claims data
func (gc *GenericClaims) Validate(vr *ValidationResults) {
gc.ClaimsData.Validate(vr)
}
func (gc *GenericClaims) String() string {
return gc.ClaimsData.String(gc)
}
// ExpectedPrefixes returns the types allowed to encode a generic JWT, which is nil for all
func (gc *GenericClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return nil
}
func (gc *GenericClaims) ClaimType() ClaimType {
v, ok := gc.Data["type"]
if !ok {
v, ok = gc.Data["nats"]
if ok {
m, ok := v.(map[string]interface{})
if ok {
v = m["type"]
}
}
}
switch ct := v.(type) {
case string:
if IsGenericClaimType(ct) {
return GenericClaim
}
return ClaimType(ct)
case ClaimType:
return ct
default:
return ""
}
}
func (gc *GenericClaims) updateVersion() {
if gc.Data != nil {
// store as float as that is what decoding with json does too
gc.Data["version"] = float64(libVersion)
}
}
+78
View File
@@ -0,0 +1,78 @@
/*
* Copyright 2018-2019 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 jwt
import (
"encoding/json"
"fmt"
"strings"
)
const (
// Version is semantic version.
Version = "2.4.0"
// TokenTypeJwt is the JWT token type supported JWT tokens
// encoded and decoded by this library
// from RFC7519 5.1 "typ":
// it is RECOMMENDED that "JWT" always be spelled using uppercase characters for compatibility
TokenTypeJwt = "JWT"
// AlgorithmNkey is the algorithm supported by JWT tokens
// encoded and decoded by this library
AlgorithmNkeyOld = "ed25519"
AlgorithmNkey = AlgorithmNkeyOld + "-nkey"
)
// Header is a JWT Jose Header
type Header struct {
Type string `json:"typ"`
Algorithm string `json:"alg"`
}
// Parses a header JWT token
func parseHeaders(s string) (*Header, error) {
h, err := decodeString(s)
if err != nil {
return nil, err
}
header := Header{}
if err := json.Unmarshal(h, &header); err != nil {
return nil, err
}
if err := header.Valid(); err != nil {
return nil, err
}
return &header, nil
}
// Valid validates the Header. It returns nil if the Header is
// a JWT header, and the algorithm used is the NKEY algorithm.
func (h *Header) Valid() error {
if TokenTypeJwt != strings.ToUpper(h.Type) {
return fmt.Errorf("not supported type %q", h.Type)
}
alg := strings.ToLower(h.Algorithm)
if !strings.HasPrefix(alg, AlgorithmNkeyOld) {
return fmt.Errorf("unexpected %q algorithm", h.Algorithm)
}
if AlgorithmNkeyOld != alg && AlgorithmNkey != alg {
return fmt.Errorf("unexpected %q algorithm", h.Algorithm)
}
return nil
}
+177
View File
@@ -0,0 +1,177 @@
/*
* Copyright 2018-2020 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 jwt
// Import describes a mapping from another account into this one
type Import struct {
Name string `json:"name,omitempty"`
// Subject field in an import is always from the perspective of the
// initial publisher - in the case of a stream it is the account owning
// the stream (the exporter), and in the case of a service it is the
// account making the request (the importer).
Subject Subject `json:"subject,omitempty"`
Account string `json:"account,omitempty"`
Token string `json:"token,omitempty"`
// Deprecated: use LocalSubject instead
// To field in an import is always from the perspective of the subscriber
// in the case of a stream it is the client of the stream (the importer),
// from the perspective of a service, it is the subscription waiting for
// requests (the exporter). If the field is empty, it will default to the
// value in the Subject field.
To Subject `json:"to,omitempty"`
// Local subject used to subscribe (for streams) and publish (for services) to.
// This value only needs setting if you want to change the value of Subject.
// If the value of Subject ends in > then LocalSubject needs to end in > as well.
// LocalSubject can contain $<number> wildcard references where number references the nth wildcard in Subject.
// The sum of wildcard reference and * tokens needs to match the number of * token in Subject.
LocalSubject RenamingSubject `json:"local_subject,omitempty"`
Type ExportType `json:"type,omitempty"`
Share bool `json:"share,omitempty"`
AllowTrace bool `json:"allow_trace,omitempty"`
}
// IsService returns true if the import is of type service
func (i *Import) IsService() bool {
return i.Type == Service
}
// IsStream returns true if the import is of type stream
func (i *Import) IsStream() bool {
return i.Type == Stream
}
// Returns the value of To without triggering the deprecation warning for a read
func (i *Import) GetTo() string {
return string(i.To)
}
// Validate checks if an import is valid for the wrapping account
func (i *Import) Validate(actPubKey string, vr *ValidationResults) {
if i == nil {
vr.AddError("null import is not allowed")
return
}
if !i.IsService() && !i.IsStream() {
vr.AddError("invalid import type: %q", i.Type)
}
if i.IsService() && i.AllowTrace {
vr.AddError("AllowTrace only valid for stream import")
}
if i.Account == "" {
vr.AddError("account to import from is not specified")
}
if i.GetTo() != "" {
vr.AddWarning("the field to has been deprecated (use LocalSubject instead)")
}
i.Subject.Validate(vr)
if i.LocalSubject != "" {
i.LocalSubject.Validate(i.Subject, vr)
if i.To != "" {
vr.AddError("Local Subject replaces To")
}
}
if i.Share && !i.IsService() {
vr.AddError("sharing information (for latency tracking) is only valid for services: %q", i.Subject)
}
var act *ActivationClaims
if i.Token != "" {
var err error
act, err = DecodeActivationClaims(i.Token)
if err != nil {
vr.AddError("import %q contains an invalid activation token", i.Subject)
}
}
if act != nil {
if !(act.Issuer == i.Account || act.IssuerAccount == i.Account) {
vr.AddError("activation token doesn't match account for import %q", i.Subject)
}
if act.ClaimsData.Subject != actPubKey {
vr.AddError("activation token doesn't match account it is being included in, %q", i.Subject)
}
if act.ImportType != i.Type {
vr.AddError("mismatch between token import type %s and type of import %s", act.ImportType, i.Type)
}
act.validateWithTimeChecks(vr, false)
subj := i.Subject
if i.IsService() && i.To != "" {
subj = i.To
}
if !subj.IsContainedIn(act.ImportSubject) {
vr.AddError("activation token import subject %q doesn't match import %q", act.ImportSubject, i.Subject)
}
}
}
// Imports is a list of import structs
type Imports []*Import
// Validate checks if an import is valid for the wrapping account
func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) {
// Group subjects by account to check for overlaps only within the same account
subsByAcct := make(map[string]map[Subject]struct{}, len(*i))
for _, v := range *i {
if v == nil {
vr.AddError("null import is not allowed")
continue
}
if v.Type == Service {
sub := v.To
if sub == "" {
sub = v.LocalSubject.ToSubject()
}
if sub == "" {
sub = v.Subject
}
// Check for overlapping subjects only within the same account
for subOther := range subsByAcct[v.Account] {
if sub.IsContainedIn(subOther) || subOther.IsContainedIn(sub) {
vr.AddError("overlapping subject namespace for %q and %q in same account %q", sub, subOther, v.Account)
}
}
if subsByAcct[v.Account] == nil {
subsByAcct[v.Account] = make(map[Subject]struct{}, len(*i))
}
if _, ok := subsByAcct[v.Account][sub]; ok {
vr.AddError("overlapping subject namespace for %q in account %q", sub, v.Account)
}
subsByAcct[v.Account][sub] = struct{}{}
}
v.Validate(acctPubKey, vr)
}
}
// Add is a simple way to add imports
func (i *Imports) Add(a ...*Import) {
*i = append(*i, a...)
}
func (i Imports) Len() int {
return len(i)
}
func (i Imports) Swap(j, k int) {
i[j], i[k] = i[k], i[j]
}
func (i Imports) Less(j, k int) bool {
return i[j].Subject < i[k].Subject
}
+257
View File
@@ -0,0 +1,257 @@
/*
* Copyright 2018-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 jwt
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/nats-io/nkeys"
)
// Operator specific claims
type Operator struct {
// Slice of other operator NKeys that can be used to sign on behalf of the main
// operator identity.
SigningKeys StringList `json:"signing_keys,omitempty"`
// AccountServerURL is a partial URL like "https://host.domain.org:<port>/jwt/v1"
// tools will use the prefix and build queries by appending /accounts/<account_id>
// or /operator to the path provided. Note this assumes that the account server
// can handle requests in a nats-account-server compatible way. See
// https://github.com/nats-io/nats-account-server.
AccountServerURL string `json:"account_server_url,omitempty"`
// A list of NATS urls (tls://host:port) where tools can connect to the server
// using proper credentials.
OperatorServiceURLs StringList `json:"operator_service_urls,omitempty"`
// Identity of the system account
SystemAccount string `json:"system_account,omitempty"`
// Min Server version
AssertServerVersion string `json:"assert_server_version,omitempty"`
// Signing of subordinate objects will require signing keys
StrictSigningKeyUsage bool `json:"strict_signing_key_usage,omitempty"`
GenericFields
}
func ParseServerVersion(version string) (int, int, int, error) {
if version == "" {
return 0, 0, 0, nil
}
split := strings.Split(version, ".")
if len(split) != 3 {
return 0, 0, 0, fmt.Errorf("asserted server version must be of the form <major>.<minor>.<update>")
} else if major, err := strconv.Atoi(split[0]); err != nil {
return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[0])
} else if minor, err := strconv.Atoi(split[1]); err != nil {
return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[1])
} else if update, err := strconv.Atoi(split[2]); err != nil {
return 0, 0, 0, fmt.Errorf("asserted server version cant parse %s to int", split[2])
} else if major < 0 || minor < 0 || update < 0 {
return 0, 0, 0, fmt.Errorf("asserted server version can'b contain negative values: %s", version)
} else {
return major, minor, update, nil
}
}
// Validate checks the validity of the operators contents
func (o *Operator) Validate(vr *ValidationResults) {
if err := o.validateAccountServerURL(); err != nil {
vr.AddError("%s", err.Error())
}
for _, v := range o.validateOperatorServiceURLs() {
if v != nil {
vr.AddError("%s", v.Error())
}
}
for _, k := range o.SigningKeys {
if !nkeys.IsValidPublicOperatorKey(k) {
vr.AddError("%s is not an operator public key", k)
}
}
if o.SystemAccount != "" {
if !nkeys.IsValidPublicAccountKey(o.SystemAccount) {
vr.AddError("%s is not an account public key", o.SystemAccount)
}
}
if _, _, _, err := ParseServerVersion(o.AssertServerVersion); err != nil {
vr.AddError("assert server version error: %s", err)
}
}
func (o *Operator) validateAccountServerURL() error {
if o.AccountServerURL != "" {
// We don't care what kind of URL it is so long as it parses
// and has a protocol. The account server may impose additional
// constraints on the type of URLs that it is able to notify to
u, err := url.Parse(o.AccountServerURL)
if err != nil {
return fmt.Errorf("error parsing account server url: %v", err)
}
if u.Scheme == "" {
return fmt.Errorf("account server url %q requires a protocol", o.AccountServerURL)
}
}
return nil
}
// ValidateOperatorServiceURL returns an error if the URL is not a valid NATS or TLS url.
func ValidateOperatorServiceURL(v string) error {
// should be possible for the service url to not be expressed
if v == "" {
return nil
}
u, err := url.Parse(v)
if err != nil {
return fmt.Errorf("error parsing operator service url %q: %v", v, err)
}
if u.User != nil {
return fmt.Errorf("operator service url %q - credentials are not supported", v)
}
if u.Path != "" {
return fmt.Errorf("operator service url %q - paths are not supported", v)
}
lcs := strings.ToLower(u.Scheme)
switch lcs {
case "nats":
return nil
case "tls":
return nil
case "ws":
return nil
case "wss":
return nil
default:
return fmt.Errorf("operator service url %q - protocol not supported (only 'nats', 'tls', 'ws', 'wss' only)", v)
}
}
func (o *Operator) validateOperatorServiceURLs() []error {
var errs []error
for _, v := range o.OperatorServiceURLs {
if v != "" {
if err := ValidateOperatorServiceURL(v); err != nil {
errs = append(errs, err)
}
}
}
return errs
}
// OperatorClaims define the data for an operator JWT
type OperatorClaims struct {
ClaimsData
Operator `json:"nats,omitempty"`
}
// NewOperatorClaims creates a new operator claim with the specified subject, which should be an operator public key
func NewOperatorClaims(subject string) *OperatorClaims {
if subject == "" {
return nil
}
c := &OperatorClaims{}
c.Subject = subject
c.Issuer = subject
return c
}
// DidSign checks the claims against the operator's public key and its signing keys
func (oc *OperatorClaims) DidSign(op Claims) bool {
if op == nil {
return false
}
issuer := op.Claims().Issuer
if issuer == oc.Subject {
if !oc.StrictSigningKeyUsage {
return true
}
return op.Claims().Subject == oc.Subject
}
return oc.SigningKeys.Contains(issuer)
}
// Encode the claims into a JWT string
func (oc *OperatorClaims) Encode(pair nkeys.KeyPair) (string, error) {
return oc.EncodeWithSigner(pair, nil)
}
func (oc *OperatorClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
if !nkeys.IsValidPublicOperatorKey(oc.Subject) {
return "", errors.New("expected subject to be an operator public key")
}
err := oc.validateAccountServerURL()
if err != nil {
return "", err
}
oc.Type = OperatorClaim
return oc.ClaimsData.encode(pair, oc, fn)
}
func (oc *OperatorClaims) ClaimType() ClaimType {
return oc.Type
}
// DecodeOperatorClaims tries to create an operator claims from a JWt string
func DecodeOperatorClaims(token string) (*OperatorClaims, error) {
claims, err := Decode(token)
if err != nil {
return nil, err
}
oc, ok := claims.(*OperatorClaims)
if !ok {
return nil, errors.New("not operator claim")
}
return oc, nil
}
func (oc *OperatorClaims) String() string {
return oc.ClaimsData.String(oc)
}
// Payload returns the operator specific data for an operator JWT
func (oc *OperatorClaims) Payload() interface{} {
return &oc.Operator
}
// Validate the contents of the claims
func (oc *OperatorClaims) Validate(vr *ValidationResults) {
oc.ClaimsData.Validate(vr)
oc.Operator.Validate(vr)
}
// ExpectedPrefixes defines the nkey types that can sign operator claims, operator
func (oc *OperatorClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return []nkeys.PrefixByte{nkeys.PrefixByteOperator}
}
// Claims returns the generic claims data
func (oc *OperatorClaims) Claims() *ClaimsData {
return &oc.ClaimsData
}
func (oc *OperatorClaims) updateVersion() {
oc.GenericFields.Version = libVersion
}
func (oc *OperatorClaims) GetTags() TagList {
return oc.Operator.Tags
}
+84
View File
@@ -0,0 +1,84 @@
/*
* Copyright 2020 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 jwt
import (
"time"
)
const All = "*"
// RevocationList is used to store a mapping of public keys to unix timestamps
type RevocationList map[string]int64
type RevocationEntry struct {
PublicKey string
TimeStamp int64
}
// Revoke enters a revocation by publickey and timestamp into this export
// If there is already a revocation for this public key that is newer, it is kept.
func (r RevocationList) Revoke(pubKey string, timestamp time.Time) {
newTS := timestamp.Unix()
// cannot move a revocation into the future - only into the past
if ts, ok := r[pubKey]; ok && ts > newTS {
return
}
r[pubKey] = newTS
}
// MaybeCompact will compact the revocation list if jwt.All is found. Any
// revocation that is covered by a jwt.All revocation will be deleted, thus
// reducing the size of the JWT. Returns a slice of entries that were removed
// during the process.
func (r RevocationList) MaybeCompact() []RevocationEntry {
var deleted []RevocationEntry
ats, ok := r[All]
if ok {
for k, ts := range r {
if k != All && ats >= ts {
deleted = append(deleted, RevocationEntry{
PublicKey: k,
TimeStamp: ts,
})
delete(r, k)
}
}
}
return deleted
}
// ClearRevocation removes any revocation for the public key
func (r RevocationList) ClearRevocation(pubKey string) {
delete(r, pubKey)
}
// IsRevoked checks if the public key is in the revoked list with a timestamp later than
// the one passed in. Generally this method is called with an issue time but other time's can
// be used for testing.
func (r RevocationList) IsRevoked(pubKey string, timestamp time.Time) bool {
if r.allRevoked(timestamp) {
return true
}
ts, ok := r[pubKey]
return ok && ts >= timestamp.Unix()
}
// allRevoked returns true if All is set and the timestamp is later or same as the
// one passed. This is called by IsRevoked.
func (r RevocationList) allRevoked(timestamp time.Time) bool {
ts, ok := r[All]
return ok && ts >= timestamp.Unix()
}
+213
View File
@@ -0,0 +1,213 @@
/*
* Copyright 2020-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 jwt
import (
"encoding/json"
"errors"
"fmt"
"sort"
"github.com/nats-io/nkeys"
)
type Scope interface {
SigningKey() string
ValidateScopedSigner(claim Claims) error
Validate(vr *ValidationResults)
}
type ScopeType int
const (
UserScopeType ScopeType = iota + 1
)
func (t ScopeType) String() string {
switch t {
case UserScopeType:
return "user_scope"
}
return "unknown"
}
func (t *ScopeType) MarshalJSON() ([]byte, error) {
switch *t {
case UserScopeType:
return []byte("\"user_scope\""), nil
}
return nil, fmt.Errorf("unknown scope type %q", t)
}
func (t *ScopeType) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
switch s {
case "user_scope":
*t = UserScopeType
return nil
}
return fmt.Errorf("unknown scope type %q", t)
}
type UserScope struct {
Kind ScopeType `json:"kind"`
Key string `json:"key"`
Role string `json:"role"`
Template UserPermissionLimits `json:"template"`
Description string `json:"description"`
}
func NewUserScope() *UserScope {
var s UserScope
s.Kind = UserScopeType
s.Template.NatsLimits = NatsLimits{NoLimit, NoLimit, NoLimit}
return &s
}
func (us UserScope) SigningKey() string {
return us.Key
}
func (us UserScope) Validate(vr *ValidationResults) {
if !nkeys.IsValidPublicAccountKey(us.Key) {
vr.AddError("%s is not an account public key", us.Key)
}
}
func (us UserScope) ValidateScopedSigner(c Claims) error {
uc, ok := c.(*UserClaims)
if !ok {
return fmt.Errorf("not an user claim - scoped signing key requires user claim")
}
if uc.Claims().Issuer != us.Key {
return errors.New("issuer not the scoped signer")
}
if !uc.HasEmptyPermissions() {
return errors.New("scoped users require no permissions or limits set")
}
return nil
}
// SigningKeys is a map keyed by a public account key
type SigningKeys map[string]Scope
func (sk SigningKeys) Validate(vr *ValidationResults) {
for k, v := range sk {
// regular signing keys won't have a scope
if v != nil {
v.Validate(vr)
} else {
if !nkeys.IsValidPublicAccountKey(k) {
vr.AddError("%q is not a valid account signing key", k)
}
}
}
}
// MarshalJSON serializes the scoped signing keys as an array
func (sk *SigningKeys) MarshalJSON() ([]byte, error) {
if sk == nil {
return nil, nil
}
keys := sk.Keys()
sort.Strings(keys)
var a []interface{}
for _, k := range keys {
if (*sk)[k] != nil {
a = append(a, (*sk)[k])
} else {
a = append(a, k)
}
}
return json.Marshal(a)
}
func (sk *SigningKeys) UnmarshalJSON(data []byte) error {
if *sk == nil {
*sk = make(SigningKeys)
}
// read an array - we can have a string or an map
var a []interface{}
if err := json.Unmarshal(data, &a); err != nil {
return err
}
for _, i := range a {
switch v := i.(type) {
case string:
(*sk)[v] = nil
case map[string]interface{}:
d, err := json.Marshal(v)
if err != nil {
return err
}
switch v["kind"] {
case UserScopeType.String():
us := NewUserScope()
if err := json.Unmarshal(d, &us); err != nil {
return err
}
(*sk)[us.Key] = us
default:
return fmt.Errorf("unknown signing key scope %q", v["type"])
}
}
}
return nil
}
func (sk SigningKeys) Keys() []string {
var keys []string
for k := range sk {
keys = append(keys, k)
}
return keys
}
// GetScope returns nil if the key is not associated
func (sk SigningKeys) GetScope(k string) (Scope, bool) {
v, ok := sk[k]
if !ok {
return nil, false
}
return v, true
}
func (sk SigningKeys) Contains(k string) bool {
_, ok := sk[k]
return ok
}
func (sk SigningKeys) Add(keys ...string) {
for _, k := range keys {
sk[k] = nil
}
}
func (sk SigningKeys) AddScopedSigner(s Scope) {
sk[s.SigningKey()] = s
}
func (sk SigningKeys) Remove(keys ...string) {
for _, k := range keys {
delete(sk, k)
}
}
+495
View File
@@ -0,0 +1,495 @@
/*
* Copyright 2018-2019 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 jwt
import (
"encoding/json"
"fmt"
"net"
"net/url"
"reflect"
"strconv"
"strings"
"time"
)
const MaxInfoLength = 8 * 1024
type Info struct {
Description string `json:"description,omitempty"`
InfoURL string `json:"info_url,omitempty"`
}
func (s Info) Validate(vr *ValidationResults) {
if len(s.Description) > MaxInfoLength {
vr.AddError("Description is too long")
}
if s.InfoURL != "" {
if len(s.InfoURL) > MaxInfoLength {
vr.AddError("Info URL is too long")
}
u, err := url.Parse(s.InfoURL)
if err == nil && (u.Hostname() == "" || u.Scheme == "") {
err = fmt.Errorf("no hostname or scheme")
}
if err != nil {
vr.AddError("error parsing info url: %v", err)
}
}
}
// ExportType defines the type of import/export.
type ExportType int
const (
// Unknown is used if we don't know the type
Unknown ExportType = iota
// Stream defines the type field value for a stream "stream"
Stream
// Service defines the type field value for a service "service"
Service
)
func (t ExportType) String() string {
switch t {
case Stream:
return "stream"
case Service:
return "service"
}
return "unknown"
}
// MarshalJSON marshals the enum as a quoted json string
func (t *ExportType) MarshalJSON() ([]byte, error) {
switch *t {
case Stream:
return []byte("\"stream\""), nil
case Service:
return []byte("\"service\""), nil
}
return nil, fmt.Errorf("unknown export type")
}
// UnmarshalJSON unmashals a quoted json string to the enum value
func (t *ExportType) UnmarshalJSON(b []byte) error {
var j string
err := json.Unmarshal(b, &j)
if err != nil {
return err
}
switch j {
case "stream":
*t = Stream
return nil
case "service":
*t = Service
return nil
}
return fmt.Errorf("unknown export type %q", j)
}
type RenamingSubject Subject
func (s RenamingSubject) Validate(from Subject, vr *ValidationResults) {
v := Subject(s)
v.Validate(vr)
if from == "" {
vr.AddError("subject cannot be empty")
}
if strings.Contains(string(s), " ") {
vr.AddError("subject %q cannot have spaces", v)
}
matchesSuffix := func(s Subject) bool {
return s == ">" || strings.HasSuffix(string(s), ".>")
}
if matchesSuffix(v) != matchesSuffix(from) {
vr.AddError("both, renaming subject and subject, need to end or not end in >")
}
fromCnt := from.countTokenWildcards()
refCnt := 0
for _, tk := range strings.Split(string(v), ".") {
if tk == "*" {
refCnt++
}
if len(tk) < 2 {
continue
}
if tk[0] == '$' {
if idx, err := strconv.Atoi(tk[1:]); err == nil {
if idx > fromCnt {
vr.AddError("Reference $%d in %q reference * in %q that do not exist", idx, s, from)
} else {
refCnt++
}
}
}
}
if refCnt != fromCnt {
vr.AddError("subject does not contain enough * or reference wildcards $[0-9]")
}
}
// Replaces reference tokens with *
func (s RenamingSubject) ToSubject() Subject {
if !strings.Contains(string(s), "$") {
return Subject(s)
}
bldr := strings.Builder{}
tokens := strings.Split(string(s), ".")
for i, tk := range tokens {
convert := false
if len(tk) > 1 && tk[0] == '$' {
if _, err := strconv.Atoi(tk[1:]); err == nil {
convert = true
}
}
if convert {
bldr.WriteString("*")
} else {
bldr.WriteString(tk)
}
if i != len(tokens)-1 {
bldr.WriteString(".")
}
}
return Subject(bldr.String())
}
// Subject is a string that represents a NATS subject
type Subject string
// Validate checks that a subject string is valid, ie not empty and without spaces
func (s Subject) Validate(vr *ValidationResults) {
v := string(s)
if v == "" {
vr.AddError("subject cannot be empty")
// No other checks after that make sense
return
}
if strings.Contains(v, " ") {
vr.AddError("subject %q cannot have spaces", v)
}
if v[0] == '.' || v[len(v)-1] == '.' {
vr.AddError("subject %q cannot start or end with a `.`", v)
}
if strings.Contains(v, "..") {
vr.AddError("subject %q cannot contain consecutive `.`", v)
}
}
func (s Subject) countTokenWildcards() int {
v := string(s)
if v == "*" {
return 1
}
cnt := 0
for _, t := range strings.Split(v, ".") {
if t == "*" {
cnt++
}
}
return cnt
}
// HasWildCards is used to check if a subject contains a > or *
func (s Subject) HasWildCards() bool {
v := string(s)
return strings.HasSuffix(v, ".>") ||
strings.Contains(v, ".*.") ||
strings.HasSuffix(v, ".*") ||
strings.HasPrefix(v, "*.") ||
v == "*" ||
v == ">"
}
// IsContainedIn does a simple test to see if the subject is contained in another subject
func (s Subject) IsContainedIn(other Subject) bool {
otherArray := strings.Split(string(other), ".")
myArray := strings.Split(string(s), ".")
if len(myArray) > len(otherArray) && otherArray[len(otherArray)-1] != ">" {
return false
}
if len(myArray) < len(otherArray) {
return false
}
for ind, tok := range otherArray {
myTok := myArray[ind]
if ind == len(otherArray)-1 && tok == ">" {
return true
}
if tok != myTok && tok != "*" {
return false
}
}
return true
}
// TimeRange is used to represent a start and end time
type TimeRange struct {
Start string `json:"start,omitempty"`
End string `json:"end,omitempty"`
}
// Validate checks the values in a time range struct
func (tr *TimeRange) Validate(vr *ValidationResults) {
format := "15:04:05"
if tr.Start == "" {
vr.AddError("time ranges start must contain a start")
} else {
_, err := time.Parse(format, tr.Start)
if err != nil {
vr.AddError("start in time range is invalid %q", tr.Start)
}
}
if tr.End == "" {
vr.AddError("time ranges end must contain an end")
} else {
_, err := time.Parse(format, tr.End)
if err != nil {
vr.AddError("end in time range is invalid %q", tr.End)
}
}
}
// Src is a comma separated list of CIDR specifications
type UserLimits struct {
Src CIDRList `json:"src,omitempty"`
Times []TimeRange `json:"times,omitempty"`
Locale string `json:"times_location,omitempty"`
}
func (u *UserLimits) Empty() bool {
return reflect.DeepEqual(*u, UserLimits{})
}
func (u *UserLimits) IsUnlimited() bool {
return len(u.Src) == 0 && len(u.Times) == 0
}
// Limits are used to control acccess for users and importing accounts
type Limits struct {
UserLimits
NatsLimits
}
func (l *Limits) IsUnlimited() bool {
return l.UserLimits.IsUnlimited() && l.NatsLimits.IsUnlimited()
}
// Validate checks the values in a limit struct
func (l *Limits) Validate(vr *ValidationResults) {
if len(l.Src) != 0 {
for _, cidr := range l.Src {
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil || ipNet == nil {
vr.AddError("invalid cidr %q in user src limits", cidr)
}
}
}
if len(l.Times) > 0 {
for _, t := range l.Times {
t.Validate(vr)
}
}
if l.Locale != "" {
if _, err := time.LoadLocation(l.Locale); err != nil {
vr.AddError("could not parse iana time zone by name: %v", err)
}
}
}
// Permission defines allow/deny subjects
type Permission struct {
Allow StringList `json:"allow,omitempty"`
Deny StringList `json:"deny,omitempty"`
}
func (p *Permission) Empty() bool {
return len(p.Allow) == 0 && len(p.Deny) == 0
}
func checkPermission(vr *ValidationResults, subj string, permitQueue bool) {
tk := strings.Split(subj, " ")
switch len(tk) {
case 1:
Subject(tk[0]).Validate(vr)
case 2:
Subject(tk[0]).Validate(vr)
Subject(tk[1]).Validate(vr)
if !permitQueue {
vr.AddError(`Permission Subject "%s" is not allowed to contain queue`, subj)
}
default:
vr.AddError(`Permission Subject "%s" contains too many spaces`, subj)
}
}
// Validate the allow, deny elements of a permission
func (p *Permission) Validate(vr *ValidationResults, permitQueue bool) {
for _, subj := range p.Allow {
checkPermission(vr, subj, permitQueue)
}
for _, subj := range p.Deny {
checkPermission(vr, subj, permitQueue)
}
}
// ResponsePermission can be used to allow responses to any reply subject
// that is received on a valid subscription.
type ResponsePermission struct {
MaxMsgs int `json:"max"`
Expires time.Duration `json:"ttl"`
}
// Validate the response permission.
func (p *ResponsePermission) Validate(_ *ValidationResults) {
// Any values can be valid for now.
}
// Permissions are used to restrict subject access, either on a user or for everyone on a server by default
type Permissions struct {
Pub Permission `json:"pub,omitempty"`
Sub Permission `json:"sub,omitempty"`
Resp *ResponsePermission `json:"resp,omitempty"`
}
// Validate the pub and sub fields in the permissions list
func (p *Permissions) Validate(vr *ValidationResults) {
if p.Resp != nil {
p.Resp.Validate(vr)
}
p.Sub.Validate(vr, true)
p.Pub.Validate(vr, false)
}
// StringList is a wrapper for an array of strings
type StringList []string
// Contains returns true if the list contains the string
func (u *StringList) Contains(p string) bool {
for _, t := range *u {
if t == p {
return true
}
}
return false
}
// Add appends 1 or more strings to a list
func (u *StringList) Add(p ...string) {
for _, v := range p {
if !u.Contains(v) && v != "" {
*u = append(*u, v)
}
}
}
// Remove removes 1 or more strings from a list
func (u *StringList) Remove(p ...string) {
for _, v := range p {
for i, t := range *u {
if t == v {
a := *u
*u = append(a[:i], a[i+1:]...)
break
}
}
}
}
// TagList is a unique array of lower case strings
// All tag list methods lower case the strings in the arguments
type TagList []string
// Contains returns true if the list contains the tags
func (u *TagList) Contains(p string) bool {
p = strings.ToLower(strings.TrimSpace(p))
for _, t := range *u {
if t == p {
return true
}
}
return false
}
// Add appends 1 or more tags to a list
func (u *TagList) Add(p ...string) {
for _, v := range p {
v = strings.ToLower(strings.TrimSpace(v))
if !u.Contains(v) && v != "" {
*u = append(*u, v)
}
}
}
// Remove removes 1 or more tags from a list
func (u *TagList) Remove(p ...string) {
for _, v := range p {
v = strings.ToLower(strings.TrimSpace(v))
for i, t := range *u {
if t == v {
a := *u
*u = append(a[:i], a[i+1:]...)
break
}
}
}
}
type CIDRList TagList
func (c *CIDRList) Contains(p string) bool {
return (*TagList)(c).Contains(p)
}
func (c *CIDRList) Add(p ...string) {
(*TagList)(c).Add(p...)
}
func (c *CIDRList) Remove(p ...string) {
(*TagList)(c).Remove(p...)
}
func (c *CIDRList) Set(values string) {
*c = CIDRList{}
c.Add(strings.Split(strings.ToLower(values), ",")...)
}
func (c *CIDRList) UnmarshalJSON(body []byte) (err error) {
// parse either as array of strings or comma separate list
var request []string
var list string
if err := json.Unmarshal(body, &request); err == nil {
*c = request
return nil
} else if err := json.Unmarshal(body, &list); err == nil {
c.Set(list)
return nil
} else {
return err
}
}
+163
View File
@@ -0,0 +1,163 @@
/*
* Copyright 2018-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 jwt
import (
"errors"
"reflect"
"github.com/nats-io/nkeys"
)
const (
ConnectionTypeStandard = "STANDARD"
ConnectionTypeWebsocket = "WEBSOCKET"
ConnectionTypeLeafnode = "LEAFNODE"
ConnectionTypeLeafnodeWS = "LEAFNODE_WS"
ConnectionTypeMqtt = "MQTT"
ConnectionTypeMqttWS = "MQTT_WS"
ConnectionTypeInProcess = "IN_PROCESS"
)
type UserPermissionLimits struct {
Permissions
Limits
BearerToken bool `json:"bearer_token,omitempty"`
ProxyRequired bool `json:"proxy_required,omitempty"`
AllowedConnectionTypes StringList `json:"allowed_connection_types,omitempty"`
}
// User defines the user specific data in a user JWT
type User struct {
UserPermissionLimits
// IssuerAccount stores the public key for the account the issuer represents.
// When set, the claim was issued by a signing key.
IssuerAccount string `json:"issuer_account,omitempty"`
GenericFields
}
// Validate checks the permissions and limits in a User jwt
func (u *User) Validate(vr *ValidationResults) {
u.Permissions.Validate(vr)
u.Limits.Validate(vr)
// When BearerToken is true server will ignore any nonce-signing verification
}
// UserClaims defines a user JWT
type UserClaims struct {
ClaimsData
User `json:"nats,omitempty"`
}
// NewUserClaims creates a user JWT with the specific subject/public key
func NewUserClaims(subject string) *UserClaims {
if subject == "" {
return nil
}
c := &UserClaims{}
c.Subject = subject
c.Limits = Limits{
UserLimits{CIDRList{}, nil, ""},
NatsLimits{NoLimit, NoLimit, NoLimit},
}
return c
}
func (u *UserClaims) SetScoped(t bool) {
if t {
u.UserPermissionLimits = UserPermissionLimits{}
} else {
u.Limits = Limits{
UserLimits{CIDRList{}, nil, ""},
NatsLimits{NoLimit, NoLimit, NoLimit},
}
}
}
func (u *UserClaims) HasEmptyPermissions() bool {
return reflect.DeepEqual(u.UserPermissionLimits, UserPermissionLimits{})
}
// Encode tries to turn the user claims into a JWT string
func (u *UserClaims) Encode(pair nkeys.KeyPair) (string, error) {
return u.EncodeWithSigner(pair, nil)
}
func (u *UserClaims) EncodeWithSigner(pair nkeys.KeyPair, fn SignFn) (string, error) {
if !nkeys.IsValidPublicUserKey(u.Subject) {
return "", errors.New("expected subject to be user public key")
}
u.Type = UserClaim
return u.ClaimsData.encode(pair, u, fn)
}
// DecodeUserClaims tries to parse a user claims from a JWT string
func DecodeUserClaims(token string) (*UserClaims, error) {
claims, err := Decode(token)
if err != nil {
return nil, err
}
ac, ok := claims.(*UserClaims)
if !ok {
return nil, errors.New("not user claim")
}
return ac, nil
}
func (u *UserClaims) ClaimType() ClaimType {
return u.Type
}
// Validate checks the generic and specific parts of the user jwt
func (u *UserClaims) Validate(vr *ValidationResults) {
u.ClaimsData.Validate(vr)
u.User.Validate(vr)
if u.IssuerAccount != "" && !nkeys.IsValidPublicAccountKey(u.IssuerAccount) {
vr.AddError("account_id is not an account public key")
}
}
// ExpectedPrefixes defines the types that can encode a user JWT, account
func (u *UserClaims) ExpectedPrefixes() []nkeys.PrefixByte {
return []nkeys.PrefixByte{nkeys.PrefixByteAccount}
}
// Claims returns the generic data from a user jwt
func (u *UserClaims) Claims() *ClaimsData {
return &u.ClaimsData
}
// Payload returns the user specific data from a user JWT
func (u *UserClaims) Payload() interface{} {
return &u.User
}
func (u *UserClaims) String() string {
return u.ClaimsData.String(u)
}
func (u *UserClaims) updateVersion() {
u.GenericFields.Version = libVersion
}
// IsBearerToken returns true if nonce-signing requirements should be skipped
func (u *UserClaims) IsBearerToken() bool {
return u.BearerToken
}
func (u *UserClaims) GetTags() TagList {
return u.User.Tags
}
+118
View File
@@ -0,0 +1,118 @@
/*
* Copyright 2018 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 jwt
import (
"errors"
"fmt"
)
// ValidationIssue represents an issue during JWT validation, it may or may not be a blocking error
type ValidationIssue struct {
Description string
Blocking bool
TimeCheck bool
}
func (ve *ValidationIssue) Error() string {
return ve.Description
}
// ValidationResults is a list of ValidationIssue pointers
type ValidationResults struct {
Issues []*ValidationIssue
}
// CreateValidationResults creates an empty list of validation issues
func CreateValidationResults() *ValidationResults {
var issues []*ValidationIssue
return &ValidationResults{
Issues: issues,
}
}
// Add appends an issue to the list
func (v *ValidationResults) Add(vi *ValidationIssue) {
v.Issues = append(v.Issues, vi)
}
// AddError creates a new validation error and adds it to the list
func (v *ValidationResults) AddError(format string, args ...interface{}) {
v.Add(&ValidationIssue{
Description: fmt.Sprintf(format, args...),
Blocking: true,
TimeCheck: false,
})
}
// AddTimeCheck creates a new validation issue related to a time check and adds it to the list
func (v *ValidationResults) AddTimeCheck(format string, args ...interface{}) {
v.Add(&ValidationIssue{
Description: fmt.Sprintf(format, args...),
Blocking: false,
TimeCheck: true,
})
}
// AddWarning creates a new validation warning and adds it to the list
func (v *ValidationResults) AddWarning(format string, args ...interface{}) {
v.Add(&ValidationIssue{
Description: fmt.Sprintf(format, args...),
Blocking: false,
TimeCheck: false,
})
}
// IsBlocking returns true if the list contains a blocking error
func (v *ValidationResults) IsBlocking(includeTimeChecks bool) bool {
for _, i := range v.Issues {
if i.Blocking {
return true
}
if includeTimeChecks && i.TimeCheck {
return true
}
}
return false
}
// IsEmpty returns true if the list is empty
func (v *ValidationResults) IsEmpty() bool {
return len(v.Issues) == 0
}
// Errors returns only blocking issues as errors
func (v *ValidationResults) Errors() []error {
var errs []error
for _, v := range v.Issues {
if v.Blocking {
errs = append(errs, errors.New(v.Description))
}
}
return errs
}
// Warnings returns only non blocking issues as strings
func (v *ValidationResults) Warnings() []string {
var errs []string
for _, v := range v.Issues {
if !v.Blocking {
errs = append(errs, v.Description)
}
}
return errs
}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2020-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.
//go:build gofuzz
package conf
func Fuzz(data []byte) int {
_, err := Parse(string(data))
if err != nil {
return 0
}
return 1
}
File diff suppressed because it is too large Load Diff
+514
View File
@@ -0,0 +1,514 @@
// Copyright 2013-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 conf supports a configuration file format used by gnatsd. It is
// a flexible format that combines the best of traditional
// configuration formats and newer styles such as JSON and YAML.
package conf
// The format supported is less restrictive than today's formats.
// Supports mixed Arrays [], nested Maps {}, multiple comment types (# and //)
// Also supports key value assignments using '=' or ':' or whiteSpace()
// e.g. foo = 2, foo : 2, foo 2
// maps can be assigned with no key separator as well
// semicolons as value terminators in key/value assignments are optional
//
// see parse_test.go for more examples.
import (
"crypto/sha256"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"unicode"
)
const _EMPTY_ = ""
type parser struct {
mapping map[string]any
lx *lexer
// The current scoped context, can be array or map
ctx any
// stack of contexts, either map or array/slice stack
ctxs []any
// Keys stack
keys []string
// Keys stack as items
ikeys []item
// The config file path, empty by default.
fp string
// pedantic reports error when configuration is not correct.
pedantic bool
// Tracks environment variable references, to avoid cycles
envVarReferences map[string]bool
}
// Parse will return a map of keys to any, although concrete types
// underly them. The values supported are string, bool, int64, float64, DateTime.
// Arrays and nested Maps are also supported.
func Parse(data string) (map[string]any, error) {
p, err := parse(data, "", false)
if err != nil {
return nil, err
}
return p.mapping, nil
}
// ParseWithChecks is equivalent to Parse but runs in pedantic mode.
func ParseWithChecks(data string) (map[string]any, error) {
p, err := parse(data, "", true)
if err != nil {
return nil, err
}
return p.mapping, nil
}
// ParseFile is a helper to open file, etc. and parse the contents.
func ParseFile(fp string) (map[string]any, error) {
data, err := os.ReadFile(fp)
if err != nil {
return nil, fmt.Errorf("error opening config file: %v", err)
}
p, err := parse(string(data), fp, false)
if err != nil {
return nil, err
}
return p.mapping, nil
}
// ParseFileWithChecks is equivalent to ParseFile but runs in pedantic mode.
func ParseFileWithChecks(fp string) (map[string]any, error) {
data, err := os.ReadFile(fp)
if err != nil {
return nil, err
}
p, err := parse(string(data), fp, true)
if err != nil {
return nil, err
}
return p.mapping, nil
}
// configDigest returns a digest for the parsed config.
func configDigest(m map[string]any) (string, error) {
digest := sha256.New()
e := json.NewEncoder(digest)
if err := e.Encode(m); err != nil {
return _EMPTY_, err
}
return fmt.Sprintf("sha256:%x", digest.Sum(nil)), nil
}
// ParseFileWithChecksDigest returns the processed config and a digest
// that represents the configuration.
func ParseFileWithChecksDigest(fp string) (map[string]any, string, error) {
m, err := ParseFileWithChecks(fp)
if err != nil {
return nil, _EMPTY_, err
}
digest, err := configDigest(m)
if err != nil {
return nil, _EMPTY_, err
}
return m, digest, nil
}
type token struct {
item item
value any
usedVariable bool
sourceFile string
}
func (t *token) MarshalJSON() ([]byte, error) {
return json.Marshal(t.value)
}
func (t *token) Value() any {
return t.value
}
func (t *token) Line() int {
return t.item.line
}
func (t *token) IsUsedVariable() bool {
return t.usedVariable
}
func (t *token) SourceFile() string {
return t.sourceFile
}
func (t *token) Position() int {
return t.item.pos
}
func newParser(data, fp string, pedantic bool) *parser {
return &parser{
mapping: make(map[string]any),
lx: lex(data),
ctxs: make([]any, 0, 4),
keys: make([]string, 0, 4),
ikeys: make([]item, 0, 4),
fp: filepath.Dir(fp),
pedantic: pedantic,
envVarReferences: make(map[string]bool),
}
}
func parse(data, fp string, pedantic bool) (*parser, error) {
p := newParser(data, fp, pedantic)
if err := p.parse(fp); err != nil {
return nil, err
}
return p, nil
}
func parseEnv(data string, parent *parser) (*parser, error) {
p := newParser(data, "", false)
p.envVarReferences = parent.envVarReferences
if err := p.parse(""); err != nil {
return nil, err
}
return p, nil
}
func (p *parser) parse(fp string) error {
p.pushContext(p.mapping)
var prevItem item
for {
it := p.next()
if it.typ == itemEOF {
// Here we allow the final character to be a bracket '}'
// in order to support JSON like configurations.
if prevItem.typ == itemKey && prevItem.val != mapEndString {
return fmt.Errorf("config is invalid (%s:%d:%d)", fp, it.line, it.pos)
}
break
}
prevItem = it
if err := p.processItem(it, fp); err != nil {
return err
}
}
return nil
}
func (p *parser) next() item {
return p.lx.nextItem()
}
func (p *parser) pushContext(ctx any) {
p.ctxs = append(p.ctxs, ctx)
p.ctx = ctx
}
func (p *parser) popContext() any {
if len(p.ctxs) == 0 {
panic("BUG in parser, context stack empty")
}
li := len(p.ctxs) - 1
last := p.ctxs[li]
p.ctxs = p.ctxs[0:li]
p.ctx = p.ctxs[len(p.ctxs)-1]
return last
}
func (p *parser) pushKey(key string) {
p.keys = append(p.keys, key)
}
func (p *parser) popKey() string {
if len(p.keys) == 0 {
panic("BUG in parser, keys stack empty")
}
li := len(p.keys) - 1
last := p.keys[li]
p.keys = p.keys[0:li]
return last
}
func (p *parser) pushItemKey(key item) {
p.ikeys = append(p.ikeys, key)
}
func (p *parser) popItemKey() item {
if len(p.ikeys) == 0 {
panic("BUG in parser, item keys stack empty")
}
li := len(p.ikeys) - 1
last := p.ikeys[li]
p.ikeys = p.ikeys[0:li]
return last
}
func (p *parser) processItem(it item, fp string) error {
setValue := func(it item, v any) {
if p.pedantic {
p.setValue(&token{it, v, false, fp})
} else {
p.setValue(v)
}
}
switch it.typ {
case itemError:
return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val)
case itemKey:
// Keep track of the keys as items and strings,
// we do this in order to be able to still support
// includes without many breaking changes.
p.pushKey(it.val)
if p.pedantic {
p.pushItemKey(it)
}
case itemMapStart:
newCtx := make(map[string]any)
p.pushContext(newCtx)
case itemMapEnd:
setValue(it, p.popContext())
case itemString:
// FIXME(dlc) sanitize string?
setValue(it, it.val)
case itemInteger:
lastDigit := 0
for _, r := range it.val {
if !unicode.IsDigit(r) && r != '-' {
break
}
lastDigit++
}
numStr := it.val[:lastDigit]
num, err := strconv.ParseInt(numStr, 10, 64)
if err != nil {
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
return fmt.Errorf("integer '%s' is out of the range", it.val)
}
return fmt.Errorf("expected integer, but got '%s'", it.val)
}
// Process a suffix
suffix := strings.ToLower(strings.TrimSpace(it.val[lastDigit:]))
switch suffix {
case "":
setValue(it, num)
case "k":
setValue(it, num*1000)
case "kb", "ki", "kib":
setValue(it, num*1024)
case "m":
setValue(it, num*1000*1000)
case "mb", "mi", "mib":
setValue(it, num*1024*1024)
case "g":
setValue(it, num*1000*1000*1000)
case "gb", "gi", "gib":
setValue(it, num*1024*1024*1024)
case "t":
setValue(it, num*1000*1000*1000*1000)
case "tb", "ti", "tib":
setValue(it, num*1024*1024*1024*1024)
case "p":
setValue(it, num*1000*1000*1000*1000*1000)
case "pb", "pi", "pib":
setValue(it, num*1024*1024*1024*1024*1024)
case "e":
setValue(it, num*1000*1000*1000*1000*1000*1000)
case "eb", "ei", "eib":
setValue(it, num*1024*1024*1024*1024*1024*1024)
}
case itemFloat:
num, err := strconv.ParseFloat(it.val, 64)
if err != nil {
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
return fmt.Errorf("float '%s' is out of the range", it.val)
}
return fmt.Errorf("expected float, but got '%s'", it.val)
}
setValue(it, num)
case itemBool:
switch strings.ToLower(it.val) {
case "true", "yes", "on":
setValue(it, true)
case "false", "no", "off":
setValue(it, false)
default:
return fmt.Errorf("expected boolean value, but got '%s'", it.val)
}
case itemDatetime:
dt, err := time.Parse("2006-01-02T15:04:05Z", it.val)
if err != nil {
return fmt.Errorf(
"expected Zulu formatted DateTime, but got '%s'", it.val)
}
setValue(it, dt)
case itemArrayStart:
var array = make([]any, 0)
p.pushContext(array)
case itemArrayEnd:
array := p.ctx
p.popContext()
setValue(it, array)
case itemVariable:
value, found, err := p.lookupVariable(it.val)
if err != nil {
return fmt.Errorf("variable reference for '%s' on line %d could not be parsed: %s",
it.val, it.line, err)
}
if !found {
return fmt.Errorf("variable reference for '%s' on line %d can not be found",
it.val, it.line)
}
if p.pedantic {
switch tk := value.(type) {
case *token:
// Mark the looked up variable as used, and make
// the variable reference become handled as a token.
tk.usedVariable = true
p.setValue(&token{it, tk.Value(), false, fp})
default:
// Special case to add position context to bcrypt references.
p.setValue(&token{it, value, false, fp})
}
} else {
p.setValue(value)
}
case itemInclude:
var (
m map[string]any
err error
)
if p.pedantic {
m, err = ParseFileWithChecks(filepath.Join(p.fp, it.val))
} else {
m, err = ParseFile(filepath.Join(p.fp, it.val))
}
if err != nil {
return fmt.Errorf("error parsing include file '%s', %v", it.val, err)
}
for k, v := range m {
p.pushKey(k)
if p.pedantic {
switch tk := v.(type) {
case *token:
p.pushItemKey(tk.item)
}
}
p.setValue(v)
}
}
return nil
}
// Used to map an environment value into a temporary map to pass to secondary Parse call.
const pkey = "pk"
// We special case raw strings here that are bcrypt'd. This allows us not to force quoting the strings
const bcryptPrefix = "2a$"
// lookupVariable will lookup a variable reference. It will use block scoping on keys
// it has seen before, with the top level scoping being the environment variables. We
// ignore array contexts and only process the map contexts..
//
// Returns true for ok if it finds something, similar to map.
func (p *parser) lookupVariable(varReference string) (any, bool, error) {
// Do special check to see if it is a raw bcrypt string.
if strings.HasPrefix(varReference, bcryptPrefix) {
return "$" + varReference, true, nil
}
// Loop through contexts currently on the stack.
for i := len(p.ctxs) - 1; i >= 0; i-- {
ctx := p.ctxs[i]
// Process if it is a map context
if m, ok := ctx.(map[string]any); ok {
if v, ok := m[varReference]; ok {
return v, ok, nil
}
}
}
// If we are here, we have exhausted our context maps and still not found anything.
// Detect reference cycles
if p.envVarReferences[varReference] {
return nil, false, fmt.Errorf("variable reference cycle for '%s'", varReference)
}
p.envVarReferences[varReference] = true
defer delete(p.envVarReferences, varReference)
// Parse from the environment
if vStr, ok := os.LookupEnv(varReference); ok {
// Everything we get here will be a string value, so we need to process as a parser would.
if subp, err := parseEnv(fmt.Sprintf("%s=%s", pkey, vStr), p); err == nil {
v, ok := subp.mapping[pkey]
return v, ok, nil
} else {
return nil, false, err
}
}
return nil, false, nil
}
func (p *parser) setValue(val any) {
// Test to see if we are on an array or a map
// Array processing
if ctx, ok := p.ctx.([]any); ok {
p.ctx = append(ctx, val)
p.ctxs[len(p.ctxs)-1] = p.ctx
}
// Map processing
if ctx, ok := p.ctx.(map[string]any); ok {
key := p.popKey()
if p.pedantic {
// Change the position to the beginning of the key
// since more useful when reporting errors.
switch v := val.(type) {
case *token:
it := p.popItemKey()
v.item.pos = it.pos
v.item.line = it.line
ctx[key] = v
}
} else {
// FIXME(dlc), make sure to error if redefining same key?
ctx[key] = val
}
}
}
+6
View File
@@ -0,0 +1,6 @@
listen: 127.0.0.1:4222
authorization {
include 'includes/users.conf' # Pull in from file
timeout: 0.5
}
@@ -0,0 +1,27 @@
Copyright (c) 2011 The LevelDB-Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,23 @@
// Copyright 2020-2023 The LevelDB-Go, Pebble and NATS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package fastrand
import _ "unsafe" // required by go:linkname
// Uint32 returns a lock free uint32 value.
//
//go:linkname Uint32 runtime.fastrand
func Uint32() uint32
// Uint32n returns a lock free uint32 value in the interval [0, n).
//
//go:linkname Uint32n runtime.fastrandn
func Uint32n(n uint32) uint32
// Uint64 returns a lock free uint64 value.
func Uint64() uint64 {
v := uint64(Uint32())
return v<<32 | uint64(Uint32())
}
+305
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
}
+405
View File
@@ -0,0 +1,405 @@
// Copyright 2012-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 logger provides logging facilities for the NATS server
package logger
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
)
// Default file permissions for log files.
const defaultLogPerms = os.FileMode(0640)
// Logger is the server logger
type Logger struct {
sync.Mutex
logger *log.Logger
debug bool
trace bool
infoLabel string
warnLabel string
errorLabel string
fatalLabel string
debugLabel string
traceLabel string
fl *fileLogger
}
type LogOption interface {
isLoggerOption()
}
// LogUTC controls whether timestamps in the log output should be UTC or local time.
type LogUTC bool
func (l LogUTC) isLoggerOption() {}
func logFlags(time bool, opts ...LogOption) int {
flags := 0
if time {
flags = log.LstdFlags | log.Lmicroseconds
}
for _, opt := range opts {
switch v := opt.(type) {
case LogUTC:
if time && bool(v) {
flags |= log.LUTC
}
}
}
return flags
}
// NewStdLogger creates a logger with output directed to Stderr
func NewStdLogger(time, debug, trace, colors, pid bool, opts ...LogOption) *Logger {
flags := logFlags(time, opts...)
pre := ""
if pid {
pre = pidPrefix()
}
l := &Logger{
logger: log.New(os.Stderr, pre, flags),
debug: debug,
trace: trace,
}
if colors {
setColoredLabelFormats(l)
} else {
setPlainLabelFormats(l)
}
return l
}
// NewFileLogger creates a logger with output directed to a file
func NewFileLogger(filename string, time, debug, trace, pid bool, opts ...LogOption) *Logger {
flags := logFlags(time, opts...)
pre := ""
if pid {
pre = pidPrefix()
}
fl, err := newFileLogger(filename, pre, time)
if err != nil {
log.Fatalf("error opening file: %v", err)
return nil
}
l := &Logger{
logger: log.New(fl, pre, flags),
debug: debug,
trace: trace,
fl: fl,
}
fl.Lock()
fl.l = l
fl.Unlock()
setPlainLabelFormats(l)
return l
}
type writerAndCloser interface {
Write(b []byte) (int, error)
Close() error
Name() string
}
type fileLogger struct {
out int64
canRotate int32
sync.Mutex
l *Logger
f writerAndCloser
limit int64
olimit int64
pid string
time bool
closed bool
maxNumFiles int
}
func newFileLogger(filename, pidPrefix string, time bool) (*fileLogger, error) {
fileflags := os.O_WRONLY | os.O_APPEND | os.O_CREATE
f, err := os.OpenFile(filename, fileflags, defaultLogPerms)
if err != nil {
return nil, err
}
stats, err := f.Stat()
if err != nil {
f.Close()
return nil, err
}
fl := &fileLogger{
canRotate: 0,
f: f,
out: stats.Size(),
pid: pidPrefix,
time: time,
}
return fl, nil
}
func (l *fileLogger) setLimit(limit int64) {
l.Lock()
l.olimit, l.limit = limit, limit
atomic.StoreInt32(&l.canRotate, 1)
rotateNow := l.out > l.limit
l.Unlock()
if rotateNow {
l.l.Noticef("Rotating logfile...")
}
}
func (l *fileLogger) setMaxNumFiles(max int) {
l.Lock()
l.maxNumFiles = max
l.Unlock()
}
func (l *fileLogger) logDirect(label, format string, v ...any) int {
var entrya = [256]byte{}
var entry = entrya[:0]
if l.pid != "" {
entry = append(entry, l.pid...)
}
if l.time {
now := time.Now()
year, month, day := now.Date()
hour, min, sec := now.Clock()
microsec := now.Nanosecond() / 1000
entry = append(entry, fmt.Sprintf("%04d/%02d/%02d %02d:%02d:%02d.%06d ",
year, month, day, hour, min, sec, microsec)...)
}
entry = append(entry, label...)
entry = append(entry, fmt.Sprintf(format, v...)...)
entry = append(entry, '\r', '\n')
l.f.Write(entry)
return len(entry)
}
func (l *fileLogger) logPurge(fname string) {
var backups []string
lDir := filepath.Dir(fname)
lBase := filepath.Base(fname)
entries, err := os.ReadDir(lDir)
if err != nil {
l.logDirect(l.l.errorLabel, "Unable to read directory %q for log purge (%v), will attempt next rotation", lDir, err)
return
}
for _, entry := range entries {
if entry.IsDir() || entry.Name() == lBase || !strings.HasPrefix(entry.Name(), lBase) {
continue
}
if stamp, found := strings.CutPrefix(entry.Name(), fmt.Sprintf("%s%s", lBase, ".")); found {
_, err := time.Parse("2006:01:02:15:04:05.999999999", strings.Replace(stamp, ".", ":", 5))
if err == nil {
backups = append(backups, entry.Name())
}
}
}
currBackups := len(backups)
maxBackups := l.maxNumFiles - 1
if currBackups > maxBackups {
// backups sorted oldest to latest based on timestamped lexical filename (ReadDir)
for i := 0; i < currBackups-maxBackups; i++ {
if err := os.Remove(filepath.Join(lDir, string(os.PathSeparator), backups[i])); err != nil {
l.logDirect(l.l.errorLabel, "Unable to remove backup log file %q (%v), will attempt next rotation", backups[i], err)
// Bail fast, we'll try again next rotation
return
}
l.logDirect(l.l.infoLabel, "Purged log file %q", backups[i])
}
}
}
func (l *fileLogger) Write(b []byte) (int, error) {
if atomic.LoadInt32(&l.canRotate) == 0 {
n, err := l.f.Write(b)
if err == nil {
atomic.AddInt64(&l.out, int64(n))
}
return n, err
}
l.Lock()
n, err := l.f.Write(b)
if err == nil {
l.out += int64(n)
if l.out > l.limit {
if err := l.f.Close(); err != nil {
l.limit *= 2
l.logDirect(l.l.errorLabel, "Unable to close logfile for rotation (%v), will attempt next rotation at size %v", err, l.limit)
l.Unlock()
return n, err
}
fname := l.f.Name()
now := time.Now()
bak := fmt.Sprintf("%s.%04d.%02d.%02d.%02d.%02d.%02d.%09d", fname,
now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(),
now.Second(), now.Nanosecond())
os.Rename(fname, bak)
fileflags := os.O_WRONLY | os.O_APPEND | os.O_CREATE
f, err := os.OpenFile(fname, fileflags, defaultLogPerms)
if err != nil {
l.Unlock()
panic(fmt.Sprintf("Unable to re-open the logfile %q after rotation: %v", fname, err))
}
l.f = f
n := l.logDirect(l.l.infoLabel, "Rotated log, backup saved as %q", bak)
l.out = int64(n)
l.limit = l.olimit
if l.maxNumFiles > 0 {
l.logPurge(fname)
}
}
}
l.Unlock()
return n, err
}
func (l *fileLogger) close() error {
l.Lock()
if l.closed {
l.Unlock()
return nil
}
l.closed = true
l.Unlock()
return l.f.Close()
}
// SetSizeLimit sets the size of a logfile after which a backup
// is created with the file name + "year.month.day.hour.min.sec.nanosec"
// and the current log is truncated.
func (l *Logger) SetSizeLimit(limit int64) error {
l.Lock()
if l.fl == nil {
l.Unlock()
return fmt.Errorf("can set log size limit only for file logger")
}
fl := l.fl
l.Unlock()
fl.setLimit(limit)
return nil
}
// SetMaxNumFiles sets the number of archived log files that will be retained
func (l *Logger) SetMaxNumFiles(max int) error {
l.Lock()
if l.fl == nil {
l.Unlock()
return fmt.Errorf("can set log max number of files only for file logger")
}
fl := l.fl
l.Unlock()
fl.setMaxNumFiles(max)
return nil
}
// NewTestLogger creates a logger with output directed to Stderr with a prefix.
// Useful for tracing in tests when multiple servers are in the same pid
func NewTestLogger(prefix string, time bool) *Logger {
flags := 0
if time {
flags = log.LstdFlags | log.Lmicroseconds
}
l := &Logger{
logger: log.New(os.Stderr, prefix, flags),
debug: true,
trace: true,
}
setColoredLabelFormats(l)
return l
}
// Close implements the io.Closer interface to clean up
// resources in the server's logger implementation.
// Caller must ensure threadsafety.
func (l *Logger) Close() error {
if l.fl != nil {
return l.fl.close()
}
return nil
}
// Generate the pid prefix string
func pidPrefix() string {
return fmt.Sprintf("[%d] ", os.Getpid())
}
func setPlainLabelFormats(l *Logger) {
l.infoLabel = "[INF] "
l.debugLabel = "[DBG] "
l.warnLabel = "[WRN] "
l.errorLabel = "[ERR] "
l.fatalLabel = "[FTL] "
l.traceLabel = "[TRC] "
}
func setColoredLabelFormats(l *Logger) {
colorFormat := "[\x1b[%sm%s\x1b[0m] "
l.infoLabel = fmt.Sprintf(colorFormat, "32", "INF")
l.debugLabel = fmt.Sprintf(colorFormat, "36", "DBG")
l.warnLabel = fmt.Sprintf(colorFormat, "0;93", "WRN")
l.errorLabel = fmt.Sprintf(colorFormat, "31", "ERR")
l.fatalLabel = fmt.Sprintf(colorFormat, "31", "FTL")
l.traceLabel = fmt.Sprintf(colorFormat, "33", "TRC")
}
// Noticef logs a notice statement
func (l *Logger) Noticef(format string, v ...any) {
l.logger.Printf(l.infoLabel+format, v...)
}
// Warnf logs a notice statement
func (l *Logger) Warnf(format string, v ...any) {
l.logger.Printf(l.warnLabel+format, v...)
}
// Errorf logs an error statement
func (l *Logger) Errorf(format string, v ...any) {
l.logger.Printf(l.errorLabel+format, v...)
}
// Fatalf logs a fatal error
func (l *Logger) Fatalf(format string, v ...any) {
l.logger.Fatalf(l.fatalLabel+format, v...)
}
// Debugf logs a debug statement
func (l *Logger) Debugf(format string, v ...any) {
if l.debug {
l.logger.Printf(l.debugLabel+format, v...)
}
}
// Tracef logs a trace statement
func (l *Logger) Tracef(format string, v ...any) {
if l.trace {
l.logger.Printf(l.traceLabel+format, v...)
}
}
+132
View File
@@ -0,0 +1,132 @@
// Copyright 2012-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.
//go:build !windows
package logger
import (
"fmt"
"log"
"log/syslog"
"net/url"
"os"
"strings"
)
// SysLogger provides a system logger facility
type SysLogger struct {
writer *syslog.Writer
debug bool
trace bool
}
// SetSyslogName sets the name to use for the syslog.
// Currently used only on Windows.
func SetSyslogName(name string) {}
// GetSysLoggerTag generates the tag name for use in syslog statements. If
// the executable is linked, the name of the link will be used as the tag,
// otherwise, the name of the executable is used. "nats-server" is the default
// for the NATS server.
func GetSysLoggerTag() string {
procName := os.Args[0]
if strings.ContainsRune(procName, os.PathSeparator) {
parts := strings.FieldsFunc(procName, func(c rune) bool {
return c == os.PathSeparator
})
procName = parts[len(parts)-1]
}
return procName
}
// NewSysLogger creates a new system logger
func NewSysLogger(debug, trace bool) *SysLogger {
w, err := syslog.New(syslog.LOG_DAEMON|syslog.LOG_NOTICE, GetSysLoggerTag())
if err != nil {
log.Fatalf("error connecting to syslog: %q", err.Error())
}
return &SysLogger{
writer: w,
debug: debug,
trace: trace,
}
}
// NewRemoteSysLogger creates a new remote system logger
func NewRemoteSysLogger(fqn string, debug, trace bool) *SysLogger {
network, addr := getNetworkAndAddr(fqn)
w, err := syslog.Dial(network, addr, syslog.LOG_DEBUG, GetSysLoggerTag())
if err != nil {
log.Fatalf("error connecting to syslog: %q", err.Error())
}
return &SysLogger{
writer: w,
debug: debug,
trace: trace,
}
}
func getNetworkAndAddr(fqn string) (network, addr string) {
u, err := url.Parse(fqn)
if err != nil {
log.Fatal(err)
}
network = u.Scheme
if network == "udp" || network == "tcp" {
addr = u.Host
} else if network == "unix" {
addr = u.Path
} else {
log.Fatalf("error invalid network type: %q", u.Scheme)
}
return
}
// Noticef logs a notice statement
func (l *SysLogger) Noticef(format string, v ...any) {
l.writer.Notice(fmt.Sprintf(format, v...))
}
// Warnf logs a warning statement
func (l *SysLogger) Warnf(format string, v ...any) {
l.writer.Warning(fmt.Sprintf(format, v...))
}
// Fatalf logs a fatal error
func (l *SysLogger) Fatalf(format string, v ...any) {
l.writer.Crit(fmt.Sprintf(format, v...))
}
// Errorf logs an error statement
func (l *SysLogger) Errorf(format string, v ...any) {
l.writer.Err(fmt.Sprintf(format, v...))
}
// Debugf logs a debug statement
func (l *SysLogger) Debugf(format string, v ...any) {
if l.debug {
l.writer.Debug(fmt.Sprintf(format, v...))
}
}
// Tracef logs a trace statement
func (l *SysLogger) Tracef(format string, v ...any) {
if l.trace {
l.writer.Notice(fmt.Sprintf(format, v...))
}
}
@@ -0,0 +1,112 @@
// Copyright 2012-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 logger logs to the windows event log
package logger
import (
"fmt"
"os"
"strings"
"golang.org/x/sys/windows/svc/eventlog"
)
var natsEventSource = "NATS-Server"
// SetSyslogName sets the name to use for the system log event source
func SetSyslogName(name string) {
natsEventSource = name
}
// SysLogger logs to the windows event logger
type SysLogger struct {
writer *eventlog.Log
debug bool
trace bool
}
// NewSysLogger creates a log using the windows event logger
func NewSysLogger(debug, trace bool) *SysLogger {
if err := eventlog.InstallAsEventCreate(natsEventSource, eventlog.Info|eventlog.Error|eventlog.Warning); err != nil {
if !strings.Contains(err.Error(), "registry key already exists") {
panic(fmt.Sprintf("could not access event log: %v", err))
}
}
w, err := eventlog.Open(natsEventSource)
if err != nil {
panic(fmt.Sprintf("could not open event log: %v", err))
}
return &SysLogger{
writer: w,
debug: debug,
trace: trace,
}
}
// NewRemoteSysLogger creates a remote event logger
func NewRemoteSysLogger(fqn string, debug, trace bool) *SysLogger {
w, err := eventlog.OpenRemote(fqn, natsEventSource)
if err != nil {
panic(fmt.Sprintf("could not open event log: %v", err))
}
return &SysLogger{
writer: w,
debug: debug,
trace: trace,
}
}
func formatMsg(tag, format string, v ...any) string {
orig := fmt.Sprintf(format, v...)
return fmt.Sprintf("pid[%d][%s]: %s", os.Getpid(), tag, orig)
}
// Noticef logs a notice statement
func (l *SysLogger) Noticef(format string, v ...any) {
l.writer.Info(1, formatMsg("NOTICE", format, v...))
}
// Warnf logs a warning statement
func (l *SysLogger) Warnf(format string, v ...any) {
l.writer.Info(1, formatMsg("WARN", format, v...))
}
// Fatalf logs a fatal error
func (l *SysLogger) Fatalf(format string, v ...any) {
msg := formatMsg("FATAL", format, v...)
l.writer.Error(5, msg)
panic(msg)
}
// Errorf logs an error statement
func (l *SysLogger) Errorf(format string, v ...any) {
l.writer.Error(2, formatMsg("ERROR", format, v...))
}
// Debugf logs a debug statement
func (l *SysLogger) Debugf(format string, v ...any) {
if l.debug {
l.writer.Info(3, formatMsg("DEBUG", format, v...))
}
}
// Tracef logs a trace statement
func (l *SysLogger) Tracef(format string, v ...any) {
if l.trace {
l.writer.Info(4, formatMsg("TRACE", format, v...))
}
}
+595
View File
@@ -0,0 +1,595 @@
**MQTT Implementation Overview**
Revision 1.1
Authors: Ivan Kozlovic, Lev Brouk
NATS Server currently supports most of MQTT 3.1.1. This document describes how
it is implemented.
It is strongly recommended to review the [MQTT v3.1.1
specifications](https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html)
and get a detailed understanding before proceeding with this document.
# Contents
1. [Concepts](#1-concepts)
- [Server, client](#server-client)
- [Connection, client ID, session](#connection-client-id-session)
- [Packets, messages, and subscriptions](#packets-messages-and-subscriptions)
- [Quality of Service (QoS), publish identifier (PI)](#quality-of-service-qos-publish-identifier-pi)
- [Retained message](#retained-message)
- [Will message](#will-message)
2. [Use of JetStream](#2-use-of-jetstream)
- [JetStream API](#jetstream-api)
- [Streams](#streams)
- [Consumers and Internal NATS Subscriptions](#consumers-and-internal-nats-subscriptions)
3. [Lifecycles](#3-lifecycles)
- [Connection, Session](#connection-session)
- [Subscription](#subscription)
- [Message](#message)
- [Retained messages](#retained-messages)
4. [Implementation Notes](#4-implementation-notes)
- [Hooking into NATS I/O](#hooking-into-nats-io)
- [Session Management](#session-management)
- [Processing QoS acks: PUBACK, PUBREC, PUBCOMP](#processing-qos-acks-puback-pubrec-pubcomp)
- [Subject Wildcards](#subject-wildcards)
5. [Known issues](#5-known-issues)
# 1. Concepts
## Server, client
In the MQTT specification there are concepts of **Client** and **Server**, used
somewhat interchangeably with those of **Sender** and **Receiver**. A **Server**
acts as a **Receiver** when it gets `PUBLISH` messages from a **Sender**
**Client**, and acts as a **Sender** when it delivers them to subscribed
**Clients**.
In the NATS server implementation there are also concepts (types) `server` and
`client`. `client` is an internal representation of a (connected) client and
runs its own read and write loops. Both of these have an `mqtt` field that if
set makes them behave as MQTT-compliant.
The code and comments may sometimes be confusing as they refer to `server` and
`client` sometimes ambiguously between MQTT and NATS.
## Connection, client ID, session
When an MQTT client connects to a server, it must send a `CONNECT` packet to
create an **MQTT Connection**. The packet must include a **Client Identifier**.
The server will then create or load a previously saved **Session** for the (hash
of) the client ID.
## Packets, messages, and subscriptions
The low level unit of transmission in MQTT is a **Packet**. Examples of packets
are: `CONNECT`, `SUBSCRIBE`, `SUBACK`, `PUBLISH`, `PUBCOMP`, etc.
An **MQTT Message** starts with a `PUBLISH` packet that a client sends to the
server. It is then matched against the current **MQTT Subscriptions** and is
delivered to them as appropriate. During the message delivery the server acts as
an MQTT client, and the receiver acts as an MQTT server.
Internally we use **NATS Messages** and **NATS Subscriptions** to facilitate
message delivery. This may be somewhat confusing as the code refers to `msg` and
`sub`. What may be even more confusing is that some MQTT packets (specifically,
`PUBREL`) are represented as NATS messages, and that the original MQTT packet
"metadata" may be encoded as NATS message headers.
## Quality of Service (QoS), publish identifier (PI)
MQTT specifies 3 levels of quality of service (**QoS**):
- `0` for at most once. A single delivery attempt.
- `1` for at least once. Will try to redeliver until acknowledged by the
receiver.
- `2` for exactly once. See the [SPEC REF] for the acknowledgement flow.
QoS 1 and 2 messages need to be identified with publish identifiers (**PI**s). A
PI is a 16-bit integer that must uniquely identify a message for the duration of
the required exchange of acknowledgment packets.
Note that the QoS applies separately to the transmission of a message from a
sender client to the server, and from the server to the receiving client. There
is no protocol-level acknowledgements between the receiver and the original
sender. The sender passes the ownership of messages to the server, and the
server then delivers them at maximum possible QoS to the receivers
(subscribers). The PIs for in-flight outgoing messages are issued and stored per
session.
## Retained message
A **Retained Message** is not part of any MQTT session and is not removed when the
session that produced it goes away. Instead, the server needs to persist a
_single_ retained message per topic. When a subscription is started, the server
needs to send the “matching” retained messages, that is, messages that would
have been delivered to the new subscription should that subscription had been
running prior to the publication of this message.
Retained messages are removed when the server receives a retained message with
an empty body. Still, this retained message that serves as a “delete” of a
retained message will be processed as a normal published message.
Retained messages can have QoS.
## Will message
The `CONNECT` packet can contain information about a **Will Message** that needs to
be sent to any client subscribing on the Will topic/subject in the event that
the client is disconnected implicitly, that is, not as a result as the client
sending the `DISCONNECT` packet.
Will messages can have the retain flag and QoS.
# 2. Use of JetStream
The MQTT implementation relies heavily on JetStream. We use it to:
- Persist (and restore) the [Session](#connection-client-id-session) state.
- Store and retrieve [Retained messages](#retained-message).
- Persist incoming [QoS 1 and
2](#quality-of-service-qos-publish-identifier-pi) messages, and
re-deliver if needed.
- Store and de-duplicate incoming [QoS
2](#quality-of-service-qos-publish-identifier-pi) messages.
- Persist and re-deliver outgoing [QoS
2](#quality-of-service-qos-publish-identifier-pi) `PUBREL` packets.
Here is the overview of how we set up and use JetStream **streams**,
**consumers**, and **internal NATS subscriptions**.
## JetStream API
All interactions with JetStream are performed via `mqttJSA` that sends NATS
requests to JetStream. Most are processed synchronously and await a response,
some (e.g. `jsa.sendAck()`) are sent asynchronously. JetStream API is usually
referred to as `jsa` in the code. No special locking is required to use `jsa`,
however the asynchronous use of JetStream may create race conditions with
delivery callbacks.
## Streams
We create the following streams unless they already exist. Failing to ensure the
streams would prevent the client from connecting.
Each stream is created with a replica value that is determined by the size of
the cluster but limited to 3. It can also be overwritten by the stream_replicas
option in the MQTT configuration block.
The streams are created the first time an Account Session Manager is initialized
and are used by all sessions in it. Note that to avoid race conditions, some
subscriptions are created first. The streams are never deleted. See
`mqttCreateAccountSessionManager()` for details.
1. `$MQTT_sess` stores persisted **Session** records. It filters on
`"$MQTT.sess.>` subject and has a “limits” policy with `MaxMsgsPer` setting
of 1.
2. `$MQTT_msgs` is used for **QoS 1 and 2 message delivery**.
It filters on `$MQTT.msgs.>` subject and has an “interest” policy.
3. `$MQTT_rmsgs` stores **Retained Messages**. They are all
stored (and filtered) on a single subject `$MQTT.rmsg`. This stream has a
limits policy.
4. `$MQTT_qos2in` stores and deduplicates **Incoming QoS 2 Messages**. It
filters on `$MQTT.qos2.in.>` and has a "limits" policy with `MaxMsgsPer` of
1.
5. `$MQTT_out` stores **Outgoing QoS 2** `PUBREL` packets. It filters on
`$MQTT.out.>` and has a "interest" retention policy.
## Consumers and Internal NATS Subscriptions
### Account Scope
- A durable consumer for [Retained Messages](#retained-message) -
`$MQTT_rmsgs_<server name hash>`
- A subscription to handle all [jsa](#jetstream-api) replies for the account.
- A subscription to replies to "session persist" requests, so that we can detect
the use of a session with the same client ID anywhere in the cluster.
- 2 subscriptions to support [retained messages](#retained-message):
`$MQTT.sub.<nuid>` for the messages themselves, and one to receive replies to
"delete retained message" JS API (on the JS reply subject var).
### Session Scope
When a new QoS 2 MQTT subscription is detected in a session, we ensure that
there is a durable consumer for [QoS
2](#quality-of-service-qos-publish-identifier-pi) `PUBREL`s out for delivery -
`$MQTT_PUBREL_<session id hash>`
### Subscription Scope
For all MQTT subscriptions, regardless of their QoS, we create internal NATS subscriptions to
- `subject` (directly encoded from `topic`). This subscription is used to
deliver QoS 0 messages, and messages originating from NATS.
- if needed, `subject fwc` complements `subject` for topics like `topic.#` to
include `topic` itself, see [top-level wildcards](#subject-wildcards)
For QoS 1 or 2 MQTT subscriptions we ensure:
- A durable consumer for messages out for delivery - `<session ID hash>_<nuid>`
- An internal subscription to `$MQTT.sub.<nuid>` to deliver the messages to the
receiving client.
### (Old) Notes
As indicated before, for a QoS1 or QoS2 subscription, the server will create a
JetStream consumer with the appropriate subject filter. If the subscription
already existed, then only the NATS subscription is created for the JetStream
consumers delivery subject.
Note that JS consumers can be created with an “Replicas” override, which from
recent discussion is problematic with “Interest” policy streams, which
“$MQTT_msgs” is.
We do handle situations where a subscription on the same subject filter is sent
with a different QoS as per MQTT specifications. If the existing was on QoS 1 or
2, and the “new” is for QoS 0, then we delete the existing JS consumer.
Subscriptions that are QoS 0 have a NATS subscription with the callback function
being `mqttDeliverMsgCbQos0()`; while QoS 1 and 2 have a NATS subscription with
callback `mqttDeliverMsgCbQos12()`. Both those functions have comments that
describe the reason for their existence and what they are doing. For instance
the `mqttDeliverMsgCbQos0()` callback will reject any producing client that is
of type JETSTREAM, so that it handles only non JetStream (QoS 1 and 2) messages.
Both these functions end-up calling mqttDeliver() which will first enqueue the
possible retained messages buffer before delivering any new message. The message
itself being delivered is serialized in MQTT format and enqueued to the clients
outbound buffer and call to addToPCD is made so that it is flushed out of the
readloop.
# 3. Lifecycles
## Connection, Session
An MQTT connection is created when a listening MQTT server receives a `CONNECT`
packet. See `mqttProcessConnect()`. A connection is associated with a session.
Steps:
1. Ensure that we have an `AccountSessionManager` so we can have an
`mqttSession`. Lazily initialize JetStream streams, and internal consumers
and subscriptions. See `getOrCreateMQTTAccountSessionManager()`.
2. Find and disconnect any previous session/client for the same ID. See
`mqttProcessConnect()`.
3. Ensure we have an `mqttSession` - create a new or load a previously persisted
one. If the clean flag is set in `CONNECT`, clean the session. see
`mqttSession.clear()`
4. Initialize session's subscriptions, if any.
5. Always send back a `CONNACK` packet. If there were errors in previous steps,
include the error.
An MQTT connection can be closed for a number of reasons, including receiving a
`DISCONNECT` from the client, explicit internal errors processing MQTT packets,
or the server receiving another `CONNECT` packet with the same client ID. See
`mqttHandleClosedClient()` and `mqttHandleWill()`. Steps:
1. Send out the Will Message if applicable (if not caused by a `DISCONNECT` packet)
2. Delete the JetStream consumers for to QoS 1 and 2 packet delivery through
JS API calls (if "clean" session flag is set)
3. Delete the session record from the “$MQTT_sess” stream, based on recorded
stream sequence. (if "clean" session flag is set)
4. Close the client connection.
On an explicit disconnect, that is, the client sends the DISCONNECT packet, the
server will NOT send the Will, as per specifications.
For sessions that had the “clean” flag, the JS consumers corresponding to QoS 1
subscriptions are deleted through JS API calls, the session record is then
deleted (based on recorded stream sequence) from the “$MQTT_sess” stream.
Finally, the client connection is closed
Sessions are persisted on disconnect, and on subscriptions changes.
## Subscription
Receiving an MQTT `SUBSCRIBE` packet creates new subscriptions, or updates
existing subscriptions in a session. Each `SUBSCRIBE` packet may contain several
specific subscriptions (`topic` + QoS in each). We always respond with a
`SUBACK`, which may indicate which subscriptions errored out.
For each subscription in the packet, we:
1. Ignore it if `topic` starts with `$MQTT.sub.`.
2. Set up QoS 0 message delivery - an internal NATS subscription on `topic`.
3. Replay any retained messages for `topic`, once as QoS 0.
4. If we already have a subscription on `topic`, update its QoS
5. If this is a QoS 2 subscription in the session, ensure we have the [PUBREL
consumer](#session-scope) for the session.
6. If this is a QoS 1 or 2 subscription, ensure we have the [Message
consumer](#subscription-scope) for this subscription (or delete one if it
exists and this is now a QoS 0 sub).
7. Add an extra subscription for the [top-level wildcard](#subject-wildcards) case.
8. Update the session, persist it if changed.
When a session is restored (no clean flag), we go through the same steps to
re-subscribe to its stored subscription, except step #8 which would have been
redundant.
When we get an `UNSUBSCRIBE` packet, it can contain multiple subscriptions to
unsubscribe. The parsing will generate a slice of mqttFilter objects that
contain the “filter” (the topic with possibly wildcard of the subscription) and
the QoS value. The server goes through the list and deletes the JS consumer (if
QoS 1 or 2) and unsubscribes the NATS subscription for the delivery subject (if
it was a QoS 1 or 2) or on the actual topic/subject. In case of the “#”
wildcard, the server will handle the “level up” subscriptions that NATS had to
create.
Again, we update the session and persist it as needed in the `$MQTT_sess`
stream.
## Message
1. Detect an incoming PUBLISH packet, parse and check the message QoS. Fill out
the session's `mqttPublish` struct that contains information about the
published message. (see `mqttParse()`, `mqttParsePub()`)
2. Process the message according to its QoS (see `mqttProcessPub()`)
- QoS 0:
- Initiate message delivery
- QoS 1:
- Initiate message delivery
- Send back a `PUBACK`
- QoS 2:
- Store the message in `$MQTT_qos2in` stream, using a PI-specific subject.
Since `MaxMsgsPer` is set to 1, we will ignore duplicates on the PI.
- Send back a `PUBREC`
- "Wait" for a `PUBREL`, then initiate message delivery
- Remove the previously stored QoS2 message
- Send back a `PUBCOMP`
3. Initiate message delivery (see `mqttInitiateMsgDelivery()`)
- Convert the MQTT `topic` into a NATS `subject` using
`mqttTopicToNATSPubSubject()` function. If there is a known subject
mapping, then we select the new subject using `selectMappedSubject()`
function and then convert back this subject into an MQTT topic using
`natsSubjectToMQTTTopic()` function.
- Re-serialize the `PUBLISH` packet received as a NATS message. Use NATS
headers for the metadata, and the deliverable MQTT `PUBLISH` packet as the
contents.
- Publish the messages as `subject` (and `subject fwc` if applicable, see
[subject wildcards](#subject-wildcards)). Use the "standard" NATS
`c.processInboundClientMsg()` to do that. `processInboundClientMsg()` will
distribute the message to any NATS subscriptions (including routes,
gateways, leafnodes) and the relevant MQTT subscriptions.
- Check for retained messages, process as needed. See
`c.processInboundClientMsg()` calling `c.mqttHandlePubRetain()` For MQTT
clients.
- If the message QoS is 1 or 2, store it in `$MQTT_msgs` stream as
`$MQTT.msgs.<subject>` for "at least once" delivery with retries.
4. Let NATS and JetStream deliver to the internal subscriptions, and to the
receiving clients. See `mqttDeliverMsgCb...()`
- The NATS message posted to `subject` (and `subject fwc`) will be delivered
to each relevant internal subscription by calling `mqttDeliverMsgCbQoS0()`.
The function has access to both the publishing and the receiving clients.
- Ignore all irrelevant invocations. Specifically, do nothing if the
message needs to be delivered with a higher QoS - that will be handled by
the other, `...QoS12` callback. Note that if the original message was
publuished with a QoS 1 or 2, but the subscription has its maximum QoS
set to 0, the message will be delivered by this callback.
- Ignore "reserved" subscriptions, as per MQTT spec.
- Decode delivery `topic` from the NATS `subject`.
- Write (enqueue) outgoing `PUBLISH` packet.
- **DONE for QoS 0**
- The NATS message posted to JetStream as `$MQTT.msgs.subject` will be
consumed by subscription-specific consumers. Note that MQTT subscriptions
with max QoS 0 do not have JetStream consumers. They are handled by the
QoS0 callback.
The consumers will deliver it to the `$MQTT.sub.<nuid>`
subject for their respective NATS subscriptions by calling
`mqttDeliverMsgCbQoS12()`. This callback too has access to both the
publishing and the receiving clients.
- Ignore "reserved" subscriptions, as per MQTT spec.
- See if this is a re-delivery from JetStream by checking `sess.cpending`
for the JS reply subject. If so, use the existing PI and treat this as a
duplicate redelivery.
- Otherwise, assign the message a new PI (see `trackPublish()` and
`bumpPI()`) and store it in `sess.cpending` and `sess.pendingPublish`,
along with the JS reply subject that can be used to remove this pending
message from the consumer once it's delivered to the receipient.
- Decode delivery `topic` from the NATS `subject`.
- Write (enqueue) outgoing `PUBLISH` packet.
5. QoS 1: "Wait" for a `PUBACK`. See `mqttProcessPubAck()`.
- When received, remove the PI from the tracking maps, send an ACK to
consumer to remove the message.
- **DONE for QoS 1**
6. QoS 2: "Wait" for a `PUBREC`. When received, we need to do all the same
things as in the QoS 1 `PUBACK` case, but we need to send out a `PUBREL`, and
continue using the same PI until the delivery flow is complete and we get
back a `PUBCOMP`. For that, we add the PI to `sess.pendingPubRel`, and to
`sess.cpending` with the PubRel consumer durable name.
We also compose and store a headers-only NATS message signifying a `PUBREL`
out for delivery, and store it in the `$MQTT_qos2out` stream, as
`$MQTT.qos2.out.<session-id>`.
7. QoS 2: Deliver `PUBREL`. The PubRel session-specific consumer will publish to
internal subscription on `$MQTT.qos2.delivery`, calling
`mqttDeliverPubRelCb()`. We store the ACK reply subject in `cpending` to
remove the JS message on `PUBCOMP`, compose and send out a `PUBREL` packet.
8. QoS 2: "Wait" for a `PUBCOMP`. See `mqttProcessPubComp()`.
- When received, remove the PI from the tracking maps, send an ACK to
consumer to remove the `PUBREL` message.
- **DONE for QoS 2**
## Retained messages
When we process an inbound `PUBLISH` and submit it to
`processInboundClientMsg()` function, for MQTT clients it will invoke
`mqttHandlePubRetain()` which checks if the published message is “retained” or
not.
If it is, then we construct a record representing the retained message and store
it in the `$MQTT_rmsg` stream, under the single `$MQTT.rmsg` subject. The stored
record (in JSON) contains information about the subject, topic, MQTT flags, user
that produced this message and the message content itself. It is stored and the
stream sequence is remembered in the memory structure that contains retained
messages.
Note that when creating an account session manager, the retained messages stream
is read from scratch to load all the messages through the use of a JS consumer.
The associated subscription will process the recovered retained messages or any
new that comes from the network.
A retained message is added to a map and a subscription is created and inserted
into a sublist that will be used to perform a ReverseMatch() when a subscription
is started and we want to find all retained messages that the subscription would
have received if it had been running prior to the message being published.
If a retained message on topic “foo” already exists, then the server has to
delete the old message at the stream sequence we saved when storing it.
This could have been done with having retained messages stored under
`$MQTT.rmsg.<subject>` as opposed to all under a single subject, and make use of
the `MaxMsgsPer` field set to 1. The `MaxMsgsPer` option was introduced well into
the availability of MQTT and changes to the sessions was made in [PR
#2501](https://github.com/nats-io/nats-server/pull/2501), with a conversion of
existing streams such as `$MQTT*sess*<sess ID>` into a single stream with unique
subjects, but the changes were not made to the retained messages stream.
There are also subscriptions for the handling of retained messages which are
messages that are asked by the publisher to be retained by the MQTT server to be
delivered to matching subscriptions when they start. There is a single message
per topic. Retained messages are deleted when the user sends a retained message
(there is a flag in the PUBLISH protocol) on a given topic with an empty body.
The difficulty with retained messages is to handle them in a cluster since all
servers need to be aware of their presence so that they can deliver them to
subscriptions that those servers may become the leader for.
- `$MQTT_rmsgs` which has a “limits” policy and holds retained messages, all
under `$MQTT.rmsg` single subject. Not sure why I did not use MaxMsgsPer for
this stream and not filter `$MQTT.rmsg.>`.
The first step when processing a new subscription is to gather the retained
messages that would be a match for this subscription. To do so, the server will
serialize into a buffer all messages for the account session managers sublists
ReverseMatch result. We use the returned subscriptions subject to find from a
map appropriate retained message (see `serializeRetainedMsgsForSub()` for
details).
# 4. Implementation Notes
## Hooking into NATS I/O
### Starting the accept loop
The MQTT accept loop is started when the server detects that an MQTT port has
been defined in the configuration file. It works similarly to all other accept
loops. Note that for MQTT over websocket, the websocket port has to be defined
and MQTT clients will connect to that port instead of the MQTT port and need to
provide `/mqtt` as part of the URL to redirect the creation of the client to an
MQTT client (with websocket support) instead of a regular NATS with websocket.
See the branching done in `startWebsocketServer()`. See `startMQTT()`.
### Starting the read/write loops
When a TCP connection is accepted, the internal go routine will invoke
`createMQTTClient()`. This function will set a `c.mqtt` object that will make it
become an MQTT client (through the `isMqtt()` helper function). The `readLoop()`
and `writeLoop()` are started similarly to other clients. However, the read loop
will branch out to `mqttParse()` instead when detecting that this is an MQTT
client.
## Session Management
### Account Session Manager
`mqttAccountSessionManager` is an object that holds the state of all sessions in
an account. It also manages the lifecycle of JetStream streams and internal
subscriptions for processing JS API replies, session updates, etc. See
`mqttCreateAccountSessionManager()`. It is lazily initialized upon the first
MQTT `CONNECT` packet received. Account session manager is referred to as `asm`
in the code.
Note that creating the account session manager (and attempting to create the
streams) is done only once per account on a given server, since once created the
account session manager for a given account would be found in the sessions map
of the mqttSessionManager object.
### Find and disconnect previous session/client
Once all that is done, we now go to the creation of the session object itself.
For that, we first need to make sure that it does not already exist, meaning
that it is registered on the server - or anywhere in the cluster. Note that MQTT
dictates that if a session with the same ID connects, the OLD session needs to
be closed, not the new one being created. NATS Server complies with this
requirement.
Once a session is detected to already exists, the old one (as described above)
is closed and the new one accepted, however, the session ID is maintained in a
flappers map so that we detect situations where sessions with the same ID are
started multiple times causing the previous one to be closed. When that
detection occurs, the newly created session is put in “jail” for a second to
avoid a very rapid succession of connect/disconnect. This has already been seen
by users since there was some issue there where we would schedule the connection
closed instead of waiting in place which was causing a panic.
We also protect from multiple clients on a given server trying to connect with
the same ID at the “same time” while the processing of a CONNECT of a session is
not yet finished. This is done with the use of a sessLocked map, keyed by the
session ID.
### Create or restore the session
If everything is good up to that point, the server will either create or restore
a session from the stream. This is done in the `createOrRestoreSession()`
function. The client/session ID is hashed and added to the sessions stream
subject along with the JS domain to prevent clients connecting from different
domains to “pollute” the session stream of a given domain.
Since each session constitutes a subject and the stream has a maximum of 1
message per subject, we attempt to load the last message on the formed subject.
If we dont find it, then the session object is created “empty”, while if we
find a record, we create the session object based on the record persisted on the
stream.
If the session was restored from the JS stream, we keep track of the stream
sequence where the record was located. When we save the session (even if it
already exists) we will use this sequence number to set the
`JSExpectedLastSubjSeq` header so that we handle possibly different servers in a
(super)cluster to detect the race of clients trying to use the same session ID,
since only one of the write should succeed. On success, the sessions new
sequence is remembered by the server that did the write.
When created or restored, the CONNACK can now be sent back to the client, and if
there were any recovered subscriptions, they are now processed.
## Processing QoS acks: PUBACK, PUBREC, PUBCOMP
When the server delivers a message with QoS 1 or 2 (also a `PUBREL` for QoS 2) to a subscribed client, the client will send back an acknowledgement. See `mqttProcessPubAck()`, `mqttProcessPubRec()`, and `mqttProcessPubComp()`
While the specific logic for each packet differs, these handlers all update the
session's PI mappings (`cpending`, `pendingPublish`, `pendingPubRel`), and if
needed send an ACK to JetStream to remove the message from its consumer and stop
the re-delivery attempts.
## Subject Wildcards
Note that MQTT subscriptions have wildcards too, the `“+”` wildcard is equivalent
to NATSs `“*”` wildcard, however, MQTTs wildcard `“#”` is similar to `“>”`, except
that it also includes the level above. That is, a subscription on `“foo/#”` would
receive messages on `“foo/bar/baz”`, but also on `“foo”`.
So, for MQTT subscriptions enging with a `'#'` we are forced to create 2
internal NATS subscriptions, one on `“foo”` and one on `“foo.>”`.
# 5. Known issues
- "active" redelivery for QoS from JetStream (compliant, just a note)
- JetStream QoS redelivery happens out of (original) order
- finish delivery of in-flight messages after UNSUB
- finish delivery of in-flight messages after a reconnect
- consider replacing `$MQTT_msgs` with `$MQTT_out`.
- consider using unique `$MQTT.rmsg.>` and `MaxMsgsPer` for retained messages.
- add a cli command to list/clean old sessions
+17
View File
@@ -0,0 +1,17 @@
# Tests
Tests that run on Travis have been split into jobs that run in their own VM in parallel. This reduces the overall running time but also is allowing recycling of a job when we get a flapper as opposed to have to recycle the whole test suite.
## JetStream Tests
For JetStream tests, we need to observe a naming convention so that no tests are omitted when running on Travis.
The script `runTestsOnTravis.sh` will run a given job based on the definition found in "`.travis.yml`".
As for the naming convention:
- All JetStream test name should start with `TestJetStream`
- Cluster tests should go into `jetstream_cluster_test.go` and start with `TestJetStreamCluster`
- Super-cluster tests should go into `jetstream_super_cluster_test.go` and start with `TestJetStreamSuperCluster`
Not following this convention means that some tests may not be executed on Travis.
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
// Copyright 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.
// ats controls the go routines for the access time service.
// This allows more efficient unixnano operations for cache access times.
// We will have one per binary (usually per server).
package ats
import (
"sync/atomic"
"time"
)
// Update every 100ms for gathering access time in unix nano.
const TickInterval = 100 * time.Millisecond
var (
// Our unix nano time.
utime atomic.Int64
// How may registered users do we have, controls lifetime of Go routine.
refs atomic.Int64
// To signal the shutdown of the Go routine.
done chan struct{}
)
func init() {
// Initialize our done chan.
done = make(chan struct{}, 1)
}
// Register usage. This will happen on filestore creation.
func Register() {
if v := refs.Add(1); v == 1 {
// This is the first to register (could also go up and down),
// so spin up Go routine and grab initial time.
utime.Store(time.Now().UnixNano())
go func() {
ticker := time.NewTicker(TickInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
utime.Store(time.Now().UnixNano())
case <-done:
return
}
}
}()
}
}
// Unregister usage. We will shutdown the go routine if no more registered users.
func Unregister() {
if v := refs.Add(-1); v == 0 {
done <- struct{}{}
} else if v < 0 {
refs.Store(0)
panic("unbalanced unregister for access time state")
}
}
// Will load the access time from an atomic.
// If no one has registered this will return 0 or stale data.
// It is the responsibility of the user to properly register and unregister.
func AccessTime() int64 {
// Return last updated time.
v := utime.Load()
if v == 0 {
// Always register a time, the worst case is a stale time.
// On startup, we can register in parallel and could previously panic.
v = time.Now().UnixNano()
utime.Store(v)
}
return v
}
File diff suppressed because it is too large Load Diff
+503
View File
@@ -0,0 +1,503 @@
// Copyright 2022-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 server
import (
"bytes"
"crypto/tls"
"encoding/pem"
"errors"
"fmt"
"time"
"unicode"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nkeys"
)
const (
AuthCalloutSubject = "$SYS.REQ.USER.AUTH"
AuthRequestSubject = "nats-authorization-request"
AuthRequestXKeyHeader = "Nats-Server-Xkey"
)
func titleCase(m string) string {
r := []rune(m)
if len(r) == 0 {
return _EMPTY_
}
return string(append([]rune{unicode.ToUpper(r[0])}, r[1:]...))
}
// Process a callout on this client's behalf.
func (s *Server) processClientOrLeafCallout(c *client, opts *Options, proxyRequired, trustedProxy bool, ujwt string) (authorized bool, errStr string) {
isOperatorMode := len(opts.TrustedKeys) > 0
// this is the account the user connected in, or the one running the callout
var acc *Account
if !isOperatorMode && opts.AuthCallout != nil && opts.AuthCallout.Account != _EMPTY_ {
aname := opts.AuthCallout.Account
var err error
acc, err = s.LookupAccount(aname)
if err != nil {
errStr = fmt.Sprintf("No valid account %q for auth callout request: %v", aname, err)
s.Warnf(errStr)
return false, errStr
}
} else {
acc = c.acc
}
if acc == nil {
// FIX for https://github.com/nats-io/nats-server/issues/7841
// hand rolled creds on leafnode became crasher here
errStr = fmt.Sprintf("%s not mapped to a callout account", c.kindString())
s.Warnf(errStr)
return false, errStr
}
// Check if we have been requested to encrypt.
var xkp nkeys.KeyPair
var xkey string
var pubAccXKey string
if !isOperatorMode && opts.AuthCallout != nil && opts.AuthCallout.XKey != _EMPTY_ {
pubAccXKey = opts.AuthCallout.XKey
} else if isOperatorMode {
pubAccXKey = acc.externalAuthXKey()
}
// If set grab server's xkey keypair and public key.
if pubAccXKey != _EMPTY_ {
// These are only set on creation, so lock not needed.
xkp, xkey = s.xkp, s.info.XKey
}
// Create a keypair for the user. We will expect this public user to be in the signed response.
// This prevents replay attacks.
ukp, _ := nkeys.CreateUser()
pub, _ := ukp.PublicKey()
reply := s.newRespInbox()
respCh := make(chan string, 1)
decodeResponse := func(rc *client, rmsg []byte, acc *Account) (*jwt.UserClaims, error) {
account := acc.Name
_, msg := rc.msgParts(rmsg)
// This signals not authorized.
// Since this is an account subscription will always have "\r\n".
if len(msg) <= LEN_CR_LF {
return nil, fmt.Errorf("auth callout violation: %q on account %q", "no reason supplied", account)
}
// Strip trailing CRLF.
msg = msg[:len(msg)-LEN_CR_LF]
encrypted := false
// If we sent an encrypted request the response could be encrypted as well.
// we are expecting the input to be `eyJ` if it is a JWT
if xkp != nil && len(msg) > 0 && !bytes.HasPrefix(msg, []byte(jwtPrefix)) {
var err error
msg, err = xkp.Open(msg, pubAccXKey)
if err != nil {
return nil, fmt.Errorf("error decrypting auth callout response on account %q: %v", account, err)
}
encrypted = true
}
cr, err := jwt.DecodeAuthorizationResponseClaims(string(msg))
if err != nil {
return nil, err
}
vr := jwt.CreateValidationResults()
cr.Validate(vr)
if len(vr.Issues) > 0 {
return nil, fmt.Errorf("authorization response had validation errors: %v", vr.Issues[0])
}
// the subject is the user id
if cr.Subject != pub {
return nil, errors.New("auth callout violation: auth callout response is not for expected user")
}
// check the audience to be the server ID
if cr.Audience != s.info.ID {
return nil, errors.New("auth callout violation: auth callout response is not for server")
}
// check if had an error message from the auth account
if cr.Error != _EMPTY_ {
return nil, fmt.Errorf("auth callout service returned an error: %v", cr.Error)
}
// if response is encrypted none of this is needed
if isOperatorMode && !encrypted {
pkStr := cr.Issuer
if cr.IssuerAccount != _EMPTY_ {
pkStr = cr.IssuerAccount
}
if pkStr != account {
if _, ok := acc.signingKeys[pkStr]; !ok {
return nil, errors.New("auth callout signing key is unknown")
}
}
}
return jwt.DecodeUserClaims(cr.Jwt)
}
// getIssuerAccount returns the issuer (as per JWT) - it also asserts that
// only in operator mode we expect to receive `issuer_account`.
getIssuerAccount := func(arc *jwt.UserClaims, account string) (string, error) {
// Make sure correct issuer.
var issuer string
if opts.AuthCallout != nil {
issuer = opts.AuthCallout.Issuer
} else {
// Operator mode is who we send the request on unless switching accounts.
issuer = acc.Name
}
// the jwt issuer can be a signing key
jwtIssuer := arc.Issuer
if arc.IssuerAccount != _EMPTY_ {
if !isOperatorMode {
// this should be invalid - effectively it would allow the auth callout
// to issue on another account which may be allowed given the configuration
// where the auth callout account can handle multiple different ones..
return _EMPTY_, fmt.Errorf("error non operator mode account %q: attempted to use issuer_account", account)
}
jwtIssuer = arc.IssuerAccount
}
if jwtIssuer != issuer {
if !isOperatorMode {
return _EMPTY_, fmt.Errorf("wrong issuer for auth callout response on account %q, expected %q got %q", account, issuer, jwtIssuer)
} else if !acc.isAllowedAcount(jwtIssuer) {
return _EMPTY_, fmt.Errorf("account %q not permitted as valid account option for auth callout for account %q",
arc.Issuer, account)
}
}
return jwtIssuer, nil
}
getExpirationAndAllowedConnections := func(arc *jwt.UserClaims, account string) (time.Duration, map[string]struct{}, error) {
allowNow, expiration := validateTimes(arc)
if !allowNow {
c.Errorf("Outside connect times")
return 0, nil, fmt.Errorf("authorized user on account %q outside of valid connect times", account)
}
allowedConnTypes, err := convertAllowedConnectionTypes(arc.User.AllowedConnectionTypes)
if err != nil {
c.Debugf("%v", err)
if len(allowedConnTypes) == 0 {
return 0, nil, fmt.Errorf("authorized user on account %q using invalid connection type", account)
}
}
return expiration, allowedConnTypes, nil
}
assignAccountAndPermissions := func(arc *jwt.UserClaims, account string) (*Account, error) {
// Apply to this client.
var err error
issuerAccount, err := getIssuerAccount(arc, account)
if err != nil {
return nil, err
}
// if we are not in operator mode, they can specify placement as a tag
var placement string
if !isOperatorMode {
// only allow placement if we are not in operator mode
placement = arc.Audience
} else {
placement = issuerAccount
}
targetAcc, err := s.LookupAccount(placement)
if err != nil {
return nil, fmt.Errorf("no valid account %q for auth callout response on account %q: %v", placement, account, err)
}
if isOperatorMode {
// this will validate the signing key that emitted the user, and if it is a signing
// key it assigns the permissions from the target account
if scope, ok := targetAcc.hasIssuer(arc.Issuer); !ok {
return nil, fmt.Errorf("user JWT issuer %q is not known", arc.Issuer)
} else if scope != nil {
// this possibly has to be different because it could just be a plain issued by a non-scoped signing key
if err := scope.ValidateScopedSigner(arc); err != nil {
return nil, fmt.Errorf("user JWT is not valid: %v", err)
} else if uSc, ok := scope.(*jwt.UserScope); !ok {
return nil, fmt.Errorf("user JWT is not a valid scoped user")
} else if arc.User.UserPermissionLimits, err = processUserPermissionsTemplate(uSc.Template, arc, targetAcc); err != nil {
return nil, fmt.Errorf("user JWT generated invalid permissions: %v", err)
}
}
}
return targetAcc, nil
}
processReply := func(_ *subscription, rc *client, racc *Account, subject, reply string, rmsg []byte) {
arc, err := decodeResponse(rc, rmsg, racc)
if err != nil {
c.authViolation()
respCh <- titleCase(err.Error())
return
}
// If the caller had established that the user should go through a proxy,
// or if the `arc` JWT requires it, and we don't have a trusted proxy,
// reject the connection.
if (proxyRequired || arc.ProxyRequired) && !trustedProxy {
err = ErrAuthProxyRequired
c.setAuthError(err)
c.authViolation()
respCh <- titleCase(err.Error())
return
}
vr := jwt.CreateValidationResults()
arc.Validate(vr)
if len(vr.Issues) > 0 {
c.authViolation()
respCh <- fmt.Sprintf("Error validating user JWT: %v", vr.Issues[0])
return
}
// Make sure that the user is what we requested.
if arc.Subject != pub {
c.authViolation()
respCh <- fmt.Sprintf("Expected authorized user of %q but got %q on account %q", pub, arc.Subject, racc.Name)
return
}
expiration, allowedConnTypes, err := getExpirationAndAllowedConnections(arc, racc.Name)
if err != nil {
c.authViolation()
respCh <- titleCase(err.Error())
return
}
targetAcc, err := assignAccountAndPermissions(arc, racc.Name)
if err != nil {
c.authViolation()
respCh <- titleCase(err.Error())
return
}
// the JWT is cleared, because if in operator mode it may hold the JWT
// for the bearer token that connected to the callout if in operator mode
// the permissions are already set on the client, this prevents a decode
// on c.RegisterNKeyUser which would have wrong values
c.mu.Lock()
c.opts.JWT = _EMPTY_
c.mu.Unlock()
// Build internal user and bind to the targeted account.
nkuser := buildInternalNkeyUser(arc, allowedConnTypes, targetAcc)
if err := c.RegisterNkeyUser(nkuser); err != nil {
c.authViolation()
respCh <- fmt.Sprintf("Could not register auth callout user: %v", err)
return
}
// See if the response wants to override the username.
if arc.Name != _EMPTY_ {
c.mu.Lock()
c.opts.Username = arc.Name
// Clear any others.
c.opts.Nkey = _EMPTY_
c.pubKey = _EMPTY_
c.opts.Token = _EMPTY_
c.mu.Unlock()
}
// Check if we need to set an auth timer if the user jwt expires.
c.setExpiration(arc.Claims(), expiration)
respCh <- _EMPTY_
}
// create a subscription to receive a response from the authcallout
sub, err := acc.subscribeInternal(reply, processReply)
if err != nil {
errStr = fmt.Sprintf("Error setting up reply subscription for auth request: %v", err)
s.Warnf(errStr)
return false, errStr
}
defer acc.unsubscribeInternal(sub)
// Build our request claims - jwt subject should be nkey
jwtSub := acc.Name
if opts.AuthCallout != nil {
jwtSub = opts.AuthCallout.Issuer
}
// The public key of the server, if set is available on Varz.Key
// This means that when a service connects, it can now peer
// authenticate if it wants to - but that also means that it needs to be
// listening to cluster changes
claim := jwt.NewAuthorizationRequestClaims(jwtSub)
claim.Audience = AuthRequestSubject
// Set expected public user nkey.
claim.UserNkey = pub
s.mu.RLock()
claim.Server = jwt.ServerID{
Name: s.info.Name,
Host: s.info.Host,
ID: s.info.ID,
Version: s.info.Version,
Cluster: s.info.Cluster,
}
s.mu.RUnlock()
// Tags
claim.Server.Tags = s.getOpts().Tags
// Check if we have been requested to encrypt.
// FIXME: possibly this public key also needs to be on the
// Varz, because then it can be peer verified?
if xkp != nil {
claim.Server.XKey = xkey
}
authTimeout := secondsToDuration(s.getOpts().AuthTimeout)
claim.Expires = time.Now().Add(time.Duration(authTimeout)).UTC().Unix()
// Grab client info for the request.
c.mu.Lock()
c.fillClientInfo(&claim.ClientInformation)
c.fillConnectOpts(&claim.ConnectOptions, ujwt)
// If we have a sig in the client opts, fill in nonce.
if claim.ConnectOptions.SignedNonce != _EMPTY_ {
claim.ClientInformation.Nonce = string(c.nonce)
}
// TLS
if c.flags.isSet(handshakeComplete) && c.nc != nil {
var ct jwt.ClientTLS
conn := c.nc.(*tls.Conn)
cs := conn.ConnectionState()
ct.Version = tlsVersion(cs.Version)
ct.Cipher = tls.CipherSuiteName(cs.CipherSuite)
// Check verified chains.
for _, vs := range cs.VerifiedChains {
var certs []string
for _, c := range vs {
blk := &pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
}
certs = append(certs, string(pem.EncodeToMemory(blk)))
}
ct.VerifiedChains = append(ct.VerifiedChains, certs)
}
// If we do not have verified chains put in peer certs.
if len(ct.VerifiedChains) == 0 {
for _, c := range cs.PeerCertificates {
blk := &pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
}
ct.Certs = append(ct.Certs, string(pem.EncodeToMemory(blk)))
}
}
claim.TLS = &ct
}
c.mu.Unlock()
b, err := claim.Encode(s.kp)
if err != nil {
errStr = fmt.Sprintf("Error encoding auth request claim on account %q: %v", acc.Name, err)
s.Warnf(errStr)
return false, errStr
}
req := []byte(b)
var hdr []byte
// Check if we have been asked to encrypt.
if xkp != nil {
req, err = xkp.Seal([]byte(req), pubAccXKey)
if err != nil {
errStr = fmt.Sprintf("Error encrypting auth request claim on account %q: %v", acc.Name, err)
s.Warnf(errStr)
return false, errStr
}
hdr = genHeader(hdr, AuthRequestXKeyHeader, xkey)
}
// Send out our request.
if err := s.sendInternalAccountMsgWithReply(acc, AuthCalloutSubject, reply, hdr, req, false); err != nil {
errStr = fmt.Sprintf("Error sending authorization request: %v", err)
s.Debugf(errStr)
return false, errStr
}
select {
case errStr = <-respCh:
if authorized = errStr == _EMPTY_; !authorized {
s.Warnf(errStr)
}
case <-time.After(authTimeout):
s.Debugf(fmt.Sprintf("Authorization callout response not received in time on account %q", acc.Name))
}
return authorized, errStr
}
// Fill in client information for the request.
// Lock should be held.
func (c *client) fillClientInfo(ci *jwt.ClientInformation) {
if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) {
return
}
// Do it this way to fail to compile if fields are added to jwt.ClientInformation.
*ci = jwt.ClientInformation{
Host: c.host,
ID: c.cid,
User: c.getRawAuthUser(),
Name: c.opts.Name,
Tags: c.tags,
NameTag: c.nameTag,
Kind: c.kindString(),
Type: c.clientTypeString(),
MQTT: c.getMQTTClientID(),
}
}
// Fill in client options.
// Lock should be held.
func (c *client) fillConnectOpts(opts *jwt.ConnectOptions, ujwt string) {
if c == nil || (c.kind != CLIENT && c.kind != LEAF && c.kind != JETSTREAM && c.kind != ACCOUNT) {
return
}
o := c.opts
if ujwt == _EMPTY_ {
// The caller may supply a reconstructed JWT that should be sent to auth
// callout without storing it in c.opts.JWT. If not, fall back to the client
// option as before.
ujwt = o.JWT
}
// Do it this way to fail to compile if fields are added to jwt.ClientInformation.
*opts = jwt.ConnectOptions{
JWT: ujwt,
Nkey: o.Nkey,
SignedNonce: o.Sig,
Token: o.Token,
Username: o.Username,
Password: o.Password,
Name: o.Name,
Lang: o.Lang,
Version: o.Version,
Protocol: o.Protocol,
}
}
+678
View File
@@ -0,0 +1,678 @@
// Copyright 2023-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 avl
import (
"cmp"
"encoding/binary"
"errors"
"math/bits"
"slices"
)
// SequenceSet is a memory and encoding optimized set for storing unsigned ints.
//
// SequenceSet is ~80-100 times more efficient memory wise than a map[uint64]struct{}.
// SequenceSet is ~1.75 times slower at inserts than the same map.
// SequenceSet is not thread safe.
//
// We use an AVL tree with nodes that hold bitmasks for set membership.
//
// Encoding will convert to a space optimized encoding using bitmasks.
type SequenceSet struct {
root *node // root node
size int // number of items
nodes int // number of nodes
// Having this here vs on the stack in Insert/Delete
// makes a difference in memory usage.
changed bool
}
// Insert will insert the sequence into the set.
// The tree will be balanced inline.
func (ss *SequenceSet) Insert(seq uint64) {
if ss.root = ss.root.insert(seq, &ss.changed, &ss.nodes); ss.changed {
ss.changed = false
ss.size++
}
}
// Exists will return true iff the sequence is a member of this set.
func (ss *SequenceSet) Exists(seq uint64) bool {
for n := ss.root; n != nil; {
if seq < n.base {
n = n.l
continue
} else if seq >= n.base+numEntries {
n = n.r
continue
}
return n.exists(seq)
}
return false
}
// SetInitialMin should be used to set the initial minimum sequence when known.
// This will more effectively utilize space versus self selecting.
// The set should be empty.
func (ss *SequenceSet) SetInitialMin(min uint64) error {
if !ss.IsEmpty() {
return ErrSetNotEmpty
}
ss.root, ss.nodes = &node{base: min, h: 1}, 1
return nil
}
// Delete will remove the sequence from the set.
// Will optionally remove nodes and rebalance.
// Returns where the sequence was set.
func (ss *SequenceSet) Delete(seq uint64) bool {
if ss == nil || ss.root == nil {
return false
}
ss.root = ss.root.delete(seq, &ss.changed, &ss.nodes)
if ss.changed {
ss.changed = false
ss.size--
if ss.size == 0 {
ss.Empty()
}
return true
}
return false
}
// Size returns the number of items in the set.
func (ss *SequenceSet) Size() int {
return ss.size
}
// Nodes returns the number of nodes in the tree.
func (ss *SequenceSet) Nodes() int {
return ss.nodes
}
// Empty will clear all items from a set.
func (ss *SequenceSet) Empty() {
ss.root = nil
ss.size = 0
ss.nodes = 0
}
// IsEmpty is a fast check of the set being empty.
func (ss *SequenceSet) IsEmpty() bool {
if ss == nil || ss.root == nil {
return true
}
return false
}
// Range will invoke the given function for each item in the set.
// They will range over the set in ascending order.
// If the callback returns false we terminate the iteration.
func (ss *SequenceSet) Range(f func(uint64) bool) {
ss.root.iter(f)
}
// Heights returns the left and right heights of the tree.
func (ss *SequenceSet) Heights() (l, r int) {
if ss.root == nil {
return 0, 0
}
if ss.root.l != nil {
l = ss.root.l.h
}
if ss.root.r != nil {
r = ss.root.r.h
}
return l, r
}
// Returns min, max and number of set items.
func (ss *SequenceSet) State() (min, max, num uint64) {
if ss == nil || ss.root == nil {
return 0, 0, 0
}
min, max = ss.MinMax()
return min, max, uint64(ss.Size())
}
// MinMax will return the minunum and maximum values in the set.
func (ss *SequenceSet) MinMax() (min, max uint64) {
if ss.root == nil {
return 0, 0
}
for l := ss.root; l != nil; l = l.l {
if l.l == nil {
min = l.min()
}
}
for r := ss.root; r != nil; r = r.r {
if r.r == nil {
max = r.max()
}
}
return min, max
}
func clone(src *node, target **node) {
if src == nil {
return
}
n := &node{base: src.base, bits: src.bits, h: src.h}
*target = n
clone(src.l, &n.l)
clone(src.r, &n.r)
}
// Clone will return a clone of the given SequenceSet.
func (ss *SequenceSet) Clone() *SequenceSet {
if ss == nil {
return nil
}
css := &SequenceSet{nodes: ss.nodes, size: ss.size}
clone(ss.root, &css.root)
return css
}
// Union will union this SequenceSet with ssa.
func (ss *SequenceSet) Union(ssa ...*SequenceSet) {
for _, sa := range ssa {
sa.root.nodeIter(func(n *node) {
for nb, b := range n.bits {
for pos := uint64(0); b != 0; pos++ {
if b&1 == 1 {
seq := n.base + (uint64(nb) * uint64(bitsPerBucket)) + pos
ss.Insert(seq)
}
b >>= 1
}
}
})
}
}
// Union will return a union of all sets.
func Union(ssa ...*SequenceSet) *SequenceSet {
if len(ssa) == 0 {
return nil
}
// Sort so we can clone largest.
slices.SortFunc(ssa, func(i, j *SequenceSet) int { return -cmp.Compare(i.Size(), j.Size()) }) // reverse order
ss := ssa[0].Clone()
// Insert the rest through range call.
for i := 1; i < len(ssa); i++ {
ssa[i].Range(func(n uint64) bool {
ss.Insert(n)
return true
})
}
return ss
}
const (
// Magic is used to identify the encode binary state..
magic = uint8(22)
// Version
version = uint8(2)
// hdrLen
hdrLen = 2
// minimum length of an encoded SequenceSet.
minLen = 2 + 8 // magic + version + num nodes + num entries.
)
// EncodeLen returns the bytes needed for encoding.
func (ss SequenceSet) EncodeLen() int {
return minLen + (ss.Nodes() * ((numBuckets+1)*8 + 2))
}
func (ss SequenceSet) Encode(buf []byte) []byte {
nn, encLen := ss.Nodes(), ss.EncodeLen()
if cap(buf) < encLen {
buf = make([]byte, encLen)
} else {
buf = buf[:encLen]
}
// TODO(dlc) - Go 1.19 introduced Append to not have to keep track.
// Once 1.20 is out we could change this over.
// Also binary.Write() is way slower, do not use.
var le = binary.LittleEndian
buf[0], buf[1] = magic, version
i := hdrLen
le.PutUint32(buf[i:], uint32(nn))
le.PutUint32(buf[i+4:], uint32(ss.size))
i += 8
ss.root.nodeIter(func(n *node) {
le.PutUint64(buf[i:], n.base)
i += 8
for _, b := range n.bits {
le.PutUint64(buf[i:], b)
i += 8
}
le.PutUint16(buf[i:], uint16(n.h))
i += 2
})
return buf[:i]
}
// ErrBadEncoding is returned when we can not decode properly.
var (
ErrBadEncoding = errors.New("ss: bad encoding")
ErrBadVersion = errors.New("ss: bad version")
ErrSetNotEmpty = errors.New("ss: set not empty")
)
// Decode returns the sequence set and number of bytes read from the buffer on success.
func Decode(buf []byte) (*SequenceSet, int, error) {
if len(buf) < minLen || buf[0] != magic {
return nil, -1, ErrBadEncoding
}
switch v := buf[1]; v {
case 1:
return decodev1(buf)
case 2:
return decodev2(buf)
default:
return nil, -1, ErrBadVersion
}
}
// Helper to decode v2.
func decodev2(buf []byte) (*SequenceSet, int, error) {
var le = binary.LittleEndian
index := 2
nn := int(le.Uint32(buf[index:]))
sz := int(le.Uint32(buf[index+4:]))
index += 8
expectedLen := minLen + (nn * ((numBuckets+1)*8 + 2))
if len(buf) < expectedLen {
return nil, -1, ErrBadEncoding
}
ss, nodes := SequenceSet{size: sz}, make([]node, nn)
for i := 0; i < nn; i++ {
n := &nodes[i]
n.base = le.Uint64(buf[index:])
index += 8
for bi := range n.bits {
n.bits[bi] = le.Uint64(buf[index:])
index += 8
}
n.h = int(le.Uint16(buf[index:]))
index += 2
ss.insertNode(n)
}
return &ss, index, nil
}
// Helper to decode v1 into v2 which has fixed buckets of 32 vs 64 originally.
func decodev1(buf []byte) (*SequenceSet, int, error) {
var le = binary.LittleEndian
index := 2
nn := int(le.Uint32(buf[index:]))
sz := int(le.Uint32(buf[index+4:]))
index += 8
const v1NumBuckets = 64
expectedLen := minLen + (nn * ((v1NumBuckets+1)*8 + 2))
if len(buf) < expectedLen {
return nil, -1, ErrBadEncoding
}
var ss SequenceSet
for i := 0; i < nn; i++ {
base := le.Uint64(buf[index:])
index += 8
for nb := uint64(0); nb < v1NumBuckets; nb++ {
n := le.Uint64(buf[index:])
// Walk all set bits and insert sequences manually for this decode from v1.
for pos := uint64(0); n != 0; pos++ {
if n&1 == 1 {
seq := base + (nb * uint64(bitsPerBucket)) + pos
ss.Insert(seq)
}
n >>= 1
}
index += 8
}
// Skip over encoded height.
index += 2
}
// Sanity check.
if ss.Size() != sz {
return nil, -1, ErrBadEncoding
}
return &ss, index, nil
}
// insertNode places a decoded node into the tree.
// These should be done in tree order as defined by Encode()
// This allows us to not have to calculate height or do rebalancing.
// So much better performance this way.
func (ss *SequenceSet) insertNode(n *node) {
ss.nodes++
if ss.root == nil {
ss.root = n
return
}
// Walk our way to the insertion point.
for p := ss.root; p != nil; {
if n.base < p.base {
if p.l == nil {
p.l = n
return
}
p = p.l
} else {
if p.r == nil {
p.r = n
return
}
p = p.r
}
}
}
const (
bitsPerBucket = 64 // bits in uint64
numBuckets = 32
numEntries = numBuckets * bitsPerBucket
)
type node struct {
//v dvalue
base uint64
bits [numBuckets]uint64
l *node
r *node
h int
}
// Set the proper bit.
// seq should have already been qualified and inserted should be non nil.
func (n *node) set(seq uint64, inserted *bool) {
seq -= n.base
i := seq / bitsPerBucket
mask := uint64(1) << (seq % bitsPerBucket)
if (n.bits[i] & mask) == 0 {
n.bits[i] |= mask
*inserted = true
}
}
func (n *node) insert(seq uint64, inserted *bool, nodes *int) *node {
if n == nil {
base := (seq / numEntries) * numEntries
n := &node{base: base, h: 1}
n.set(seq, inserted)
*nodes++
return n
}
if seq < n.base {
n.l = n.l.insert(seq, inserted, nodes)
} else if seq >= n.base+numEntries {
n.r = n.r.insert(seq, inserted, nodes)
} else {
n.set(seq, inserted)
}
n.h = maxH(n) + 1
// Don't make a function, impacts performance.
if bf := balanceF(n); bf > 1 {
// Left unbalanced.
if balanceF(n.l) < 0 {
n.l = n.l.rotateL()
}
return n.rotateR()
} else if bf < -1 {
// Right unbalanced.
if balanceF(n.r) > 0 {
n.r = n.r.rotateR()
}
return n.rotateL()
}
return n
}
func (n *node) rotateL() *node {
r := n.r
if r != nil {
n.r = r.l
r.l = n
n.h = maxH(n) + 1
r.h = maxH(r) + 1
} else {
n.r = nil
n.h = maxH(n) + 1
}
return r
}
func (n *node) rotateR() *node {
l := n.l
if l != nil {
n.l = l.r
l.r = n
n.h = maxH(n) + 1
l.h = maxH(l) + 1
} else {
n.l = nil
n.h = maxH(n) + 1
}
return l
}
func balanceF(n *node) int {
if n == nil {
return 0
}
var lh, rh int
if n.l != nil {
lh = n.l.h
}
if n.r != nil {
rh = n.r.h
}
return lh - rh
}
func maxH(n *node) int {
if n == nil {
return 0
}
var lh, rh int
if n.l != nil {
lh = n.l.h
}
if n.r != nil {
rh = n.r.h
}
if lh > rh {
return lh
}
return rh
}
// Clear the proper bit.
// seq should have already been qualified and deleted should be non nil.
// Will return true if this node is now empty.
func (n *node) clear(seq uint64, deleted *bool) bool {
seq -= n.base
i := seq / bitsPerBucket
mask := uint64(1) << (seq % bitsPerBucket)
if (n.bits[i] & mask) != 0 {
n.bits[i] &^= mask
*deleted = true
}
for _, b := range n.bits {
if b != 0 {
return false
}
}
return true
}
func (n *node) delete(seq uint64, deleted *bool, nodes *int) *node {
if n == nil {
return nil
}
if seq < n.base {
n.l = n.l.delete(seq, deleted, nodes)
} else if seq >= n.base+numEntries {
n.r = n.r.delete(seq, deleted, nodes)
} else if empty := n.clear(seq, deleted); empty {
*nodes--
if n.l == nil {
n = n.r
} else if n.r == nil {
n = n.l
} else {
// We have both children.
n.r = n.r.insertNodePrev(n.l)
n = n.r
}
}
if n != nil {
n.h = maxH(n) + 1
}
// Check balance.
if bf := balanceF(n); bf > 1 {
// Left unbalanced.
if balanceF(n.l) < 0 {
n.l = n.l.rotateL()
}
return n.rotateR()
} else if bf < -1 {
// right unbalanced.
if balanceF(n.r) > 0 {
n.r = n.r.rotateR()
}
return n.rotateL()
}
return n
}
// Will insert nn into the node assuming it is less than all other nodes in n.
// Will re-calculate height and balance.
func (n *node) insertNodePrev(nn *node) *node {
if n.l == nil {
n.l = nn
} else {
n.l = n.l.insertNodePrev(nn)
}
n.h = maxH(n) + 1
// Check balance.
if bf := balanceF(n); bf > 1 {
// Left unbalanced.
if balanceF(n.l) < 0 {
n.l = n.l.rotateL()
}
return n.rotateR()
} else if bf < -1 {
// right unbalanced.
if balanceF(n.r) > 0 {
n.r = n.r.rotateR()
}
return n.rotateL()
}
return n
}
func (n *node) exists(seq uint64) bool {
seq -= n.base
i := seq / bitsPerBucket
mask := uint64(1) << (seq % bitsPerBucket)
return n.bits[i]&mask != 0
}
// Return minimum sequence in the set.
// This node can not be empty.
func (n *node) min() uint64 {
for i, b := range n.bits {
if b != 0 {
return n.base +
uint64(i*bitsPerBucket) +
uint64(bits.TrailingZeros64(b))
}
}
return 0
}
// Return maximum sequence in the set.
// This node can not be empty.
func (n *node) max() uint64 {
for i := numBuckets - 1; i >= 0; i-- {
if b := n.bits[i]; b != 0 {
return n.base +
uint64(i*bitsPerBucket) +
uint64(bitsPerBucket-bits.LeadingZeros64(b>>1))
}
}
return 0
}
// This is done in tree order.
func (n *node) nodeIter(f func(n *node)) {
if n == nil {
return
}
f(n)
n.l.nodeIter(f)
n.r.nodeIter(f)
}
// iter will iterate through the set's items in this node.
// If the supplied function returns false we terminate the iteration.
func (n *node) iter(f func(uint64) bool) bool {
if n == nil {
return true
}
if ok := n.l.iter(f); !ok {
return false
}
for num := n.base; num < n.base+numEntries; num++ {
if n.exists(num) {
if ok := f(num); !ok {
return false
}
}
}
if ok := n.r.iter(f); !ok {
return false
}
return true
}
@@ -0,0 +1,312 @@
// Copyright 2023-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 certidp
import (
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"golang.org/x/crypto/ocsp"
)
const (
DefaultAllowedClockSkew = 30 * time.Second
DefaultOCSPResponderTimeout = 2 * time.Second
DefaultTTLUnsetNextUpdate = 1 * time.Hour
)
type StatusAssertion int
var (
StatusAssertionStrToVal = map[string]StatusAssertion{
"good": ocsp.Good,
"revoked": ocsp.Revoked,
"unknown": ocsp.Unknown,
}
StatusAssertionValToStr = map[StatusAssertion]string{
ocsp.Good: "good",
ocsp.Revoked: "revoked",
ocsp.Unknown: "unknown",
}
StatusAssertionIntToVal = map[int]StatusAssertion{
0: ocsp.Good,
1: ocsp.Revoked,
2: ocsp.Unknown,
}
)
// GetStatusAssertionStr returns the corresponding string representation of the StatusAssertion.
func GetStatusAssertionStr(sa int) string {
// If the provided status assertion value is not found in the map (StatusAssertionIntToVal),
// the function defaults to "unknown" to avoid defaulting to "good," which is the default iota value
// for the ocsp.StatusAssertion enumeration (https://pkg.go.dev/golang.org/x/crypto/ocsp#pkg-constants).
// This ensures that we don't unintentionally default to "good" when there's no map entry.
v, ok := StatusAssertionIntToVal[sa]
if !ok {
// set unknown as fallback
v = ocsp.Unknown
}
return StatusAssertionValToStr[v]
}
func (sa StatusAssertion) MarshalJSON() ([]byte, error) {
// This ensures that we don't unintentionally default to "good" when there's no map entry.
// (see more details in the GetStatusAssertionStr() comment)
str, ok := StatusAssertionValToStr[sa]
if !ok {
// set unknown as fallback
str = StatusAssertionValToStr[ocsp.Unknown]
}
return json.Marshal(str)
}
func (sa *StatusAssertion) UnmarshalJSON(in []byte) error {
// This ensures that we don't unintentionally default to "good" when there's no map entry.
// (see more details in the GetStatusAssertionStr() comment)
v, ok := StatusAssertionStrToVal[strings.ReplaceAll(string(in), "\"", "")]
if !ok {
// set unknown as fallback
v = StatusAssertionStrToVal["unknown"]
}
*sa = v
return nil
}
type ChainLink struct {
Leaf *x509.Certificate
Issuer *x509.Certificate
OCSPWebEndpoints *[]*url.URL
}
// OCSPPeerConfig holds the parsed OCSP peer configuration section of TLS configuration
type OCSPPeerConfig struct {
Verify bool
Timeout float64
ClockSkew float64
WarnOnly bool
UnknownIsGood bool
AllowWhenCAUnreachable bool
TTLUnsetNextUpdate float64
}
func NewOCSPPeerConfig() *OCSPPeerConfig {
return &OCSPPeerConfig{
Verify: false,
Timeout: DefaultOCSPResponderTimeout.Seconds(),
ClockSkew: DefaultAllowedClockSkew.Seconds(),
WarnOnly: false,
UnknownIsGood: false,
AllowWhenCAUnreachable: false,
TTLUnsetNextUpdate: DefaultTTLUnsetNextUpdate.Seconds(),
}
}
// Log is a neutral method of passing server loggers to plugins
type Log struct {
Debugf func(format string, v ...any)
Noticef func(format string, v ...any)
Warnf func(format string, v ...any)
Errorf func(format string, v ...any)
Tracef func(format string, v ...any)
}
type CertInfo struct {
Subject string `json:"subject,omitempty"`
Issuer string `json:"issuer,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Raw []byte `json:"raw,omitempty"`
}
var OCSPPeerUsage = `
For client, leaf spoke (remotes), and leaf hub connections, you may enable OCSP peer validation:
tls {
...
# mTLS must be enabled (with exception of Leaf remotes)
verify: true
...
# short form enables peer verify and takes option defaults
ocsp_peer: true
# long form includes settable options
ocsp_peer {
# Enable OCSP peer validation (default false)
verify: true
# OCSP responder timeout in seconds (may be fractional, default 2 seconds)
ca_timeout: 2
# Allowed skew between server and OCSP responder time in seconds (may be fractional, default 30 seconds)
allowed_clockskew: 30
# Warn-only and never reject connections (default false)
warn_only: false
# Treat response Unknown status as valid certificate (default false)
unknown_is_good: false
# Warn-only if no CA response can be obtained and no cached revocation exists (default false)
allow_when_ca_unreachable: false
# If response NextUpdate unset by CA, set a default cache TTL in seconds from ThisUpdate (default 1 hour)
cache_ttl_when_next_update_unset: 3600
}
...
}
Note: OCSP validation for route and gateway connections is enabled using the 'ocsp' configuration option.
`
// GenerateFingerprint returns a base64-encoded SHA256 hash of the raw certificate
func GenerateFingerprint(cert *x509.Certificate) string {
data := sha256.Sum256(cert.Raw)
return base64.StdEncoding.EncodeToString(data[:])
}
func getWebEndpoints(uris []string) []*url.URL {
var urls []*url.URL
for _, uri := range uris {
endpoint, err := url.ParseRequestURI(uri)
if err != nil {
// skip invalid URLs
continue
}
if endpoint.Scheme != "http" && endpoint.Scheme != "https" {
// skip non-web URLs
continue
}
urls = append(urls, endpoint)
}
return urls
}
// GetSubjectDNForm returns RDN sequence concatenation of the certificate's subject to be
// used in logs, events, etc. Should never be used for reliable cache matching or other crypto purposes.
func GetSubjectDNForm(cert *x509.Certificate) string {
if cert == nil {
return ""
}
return strings.TrimSuffix(fmt.Sprintf("%s+", cert.Subject.ToRDNSequence()), "+")
}
// GetIssuerDNForm returns RDN sequence concatenation of the certificate's issuer to be
// used in logs, events, etc. Should never be used for reliable cache matching or other crypto purposes.
func GetIssuerDNForm(cert *x509.Certificate) string {
if cert == nil {
return ""
}
return strings.TrimSuffix(fmt.Sprintf("%s+", cert.Issuer.ToRDNSequence()), "+")
}
// CertOCSPEligible checks if the certificate's issuer has populated AIA with OCSP responder endpoint(s)
// and is thus eligible for OCSP validation
func CertOCSPEligible(link *ChainLink) bool {
if link == nil || link.Leaf.Raw == nil || len(link.Leaf.Raw) == 0 {
return false
}
if len(link.Leaf.OCSPServer) == 0 {
return false
}
urls := getWebEndpoints(link.Leaf.OCSPServer)
if len(urls) == 0 {
return false
}
link.OCSPWebEndpoints = &urls
return true
}
// GetLeafIssuerCert returns the issuer certificate of the leaf (positional) certificate in the chain
func GetLeafIssuerCert(chain []*x509.Certificate, leafPos int) *x509.Certificate {
if len(chain) == 0 || leafPos < 0 {
return nil
}
// self-signed certificate or too-big leafPos
if leafPos >= len(chain)-1 {
return nil
}
// returns pointer to issuer cert or nil
return (chain)[leafPos+1]
}
// OCSPResponseCurrent checks if the OCSP response is current (i.e. not expired and not future effective)
func OCSPResponseCurrent(ocspr *ocsp.Response, opts *OCSPPeerConfig, log *Log) bool {
skew := time.Duration(opts.ClockSkew * float64(time.Second))
if skew < 0*time.Second {
skew = DefaultAllowedClockSkew
}
now := time.Now().UTC()
// Typical effectivity check based on CA response ThisUpdate and NextUpdate semantics
if !ocspr.NextUpdate.IsZero() && ocspr.NextUpdate.Before(now.Add(-1*skew)) {
t := ocspr.NextUpdate.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseExpired, t, nt, skew)
return false
}
// CA responder can assert NextUpdate unset, in which case use config option to set a default cache TTL
if ocspr.NextUpdate.IsZero() {
ttl := time.Duration(opts.TTLUnsetNextUpdate * float64(time.Second))
if ttl < 0*time.Second {
ttl = DefaultTTLUnsetNextUpdate
}
expiryTime := ocspr.ThisUpdate.Add(ttl)
if expiryTime.Before(now.Add(-1 * skew)) {
t := expiryTime.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseTTLExpired, t, nt, skew)
return false
}
}
if ocspr.ThisUpdate.After(now.Add(skew)) {
t := ocspr.ThisUpdate.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseFutureDated, t, nt, skew)
return false
}
return true
}
// ValidDelegationCheck checks if the CA OCSP Response was signed by a valid CA Issuer delegate as per (RFC 6960, section 4.2.2.2)
// If a valid delegate or direct-signed by CA Issuer, true returned.
func ValidDelegationCheck(iss *x509.Certificate, ocspr *ocsp.Response) bool {
// This call assumes prior successful parse and signature validation of the OCSP response
// The Go OCSP library (as of x/crypto/ocsp v0.9) will detect and perform a 1-level delegate signature check but does not
// implement the additional criteria for delegation specified in RFC 6960, section 4.2.2.2.
if iss == nil || ocspr == nil {
return false
}
// not a delegation, no-op
if ocspr.Certificate == nil {
return true
}
// delegate is self-same with CA Issuer, not a delegation although response issued in that form
if ocspr.Certificate.Equal(iss) {
return true
}
// we need to verify CA Issuer stamped id-kp-OCSPSigning on delegate
delegatedSigner := false
for _, keyUseExt := range ocspr.Certificate.ExtKeyUsage {
if keyUseExt == x509.ExtKeyUsageOCSPSigning {
delegatedSigner = true
break
}
}
return delegatedSigner
}
@@ -0,0 +1,106 @@
// Copyright 2023 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 certidp
var (
// Returned errors
ErrIllegalPeerOptsConfig = "expected map to define OCSP peer options, got [%T]"
ErrIllegalCacheOptsConfig = "expected map to define OCSP peer cache options, got [%T]"
ErrParsingPeerOptFieldGeneric = "error parsing tls peer config, unknown field [%q]"
ErrParsingPeerOptFieldTypeConversion = "error parsing tls peer config, conversion error: %s"
ErrParsingCacheOptFieldTypeConversion = "error parsing OCSP peer cache config, conversion error: %s"
ErrUnableToPlugTLSEmptyConfig = "unable to plug TLS verify connection, config is nil"
ErrMTLSRequired = "OCSP peer verification for client connections requires TLS verify (mTLS) to be enabled"
ErrUnableToPlugTLSClient = "unable to register client OCSP verification"
ErrUnableToPlugTLSServer = "unable to register server OCSP verification"
ErrCannotWriteCompressed = "error writing to compression writer: %w"
ErrCannotReadCompressed = "error reading compression reader: %w"
ErrTruncatedWrite = "short write on body (%d != %d)"
ErrCannotCloseWriter = "error closing compression writer: %w"
ErrParsingCacheOptFieldGeneric = "error parsing OCSP peer cache config, unknown field [%q]"
ErrUnknownCacheType = "error parsing OCSP peer cache config, unknown type [%s]"
ErrInvalidChainlink = "invalid chain link"
ErrBadResponderHTTPStatus = "bad OCSP responder http status: [%d]"
ErrNoAvailOCSPServers = "no available OCSP servers"
ErrFailedWithAllRequests = "exhausted OCSP responders: %w"
// Direct logged errors
ErrLoadCacheFail = "Unable to load OCSP peer cache: %s"
ErrSaveCacheFail = "Unable to save OCSP peer cache: %s"
ErrBadCacheTypeConfig = "Unimplemented OCSP peer cache type [%v]"
ErrResponseCompressFail = "Unable to compress OCSP response for key [%s]: %s"
ErrResponseDecompressFail = "Unable to decompress OCSP response for key [%s]: %s"
ErrPeerEmptyNoEvent = "Peer certificate is nil, cannot send OCSP peer reject event"
ErrPeerEmptyAutoReject = "Peer certificate is nil, rejecting OCSP peer"
// Debug information
DbgPlugTLSForKind = "Plugging TLS OCSP peer for [%s]"
DbgNumServerChains = "Peer OCSP enabled: %d TLS server chain(s) will be evaluated"
DbgNumClientChains = "Peer OCSP enabled: %d TLS client chain(s) will be evaluated"
DbgLinksInChain = "Chain [%d]: %d total link(s)"
DbgSelfSignedValid = "Chain [%d] is self-signed, thus peer is valid"
DbgValidNonOCSPChain = "Chain [%d] has no OCSP eligible links, thus peer is valid"
DbgChainIsOCSPEligible = "Chain [%d] has %d OCSP eligible link(s)"
DbgChainIsOCSPValid = "Chain [%d] is OCSP valid for all eligible links, thus peer is valid"
DbgNoOCSPValidChains = "No OCSP valid chains, thus peer is invalid"
DbgCheckingCacheForCert = "Checking OCSP peer cache for [%s], key [%s]"
DbgCurrentResponseCached = "Cached OCSP response is current, status [%s]"
DbgExpiredResponseCached = "Cached OCSP response is expired, status [%s]"
DbgOCSPValidPeerLink = "OCSP verify pass for [%s]"
DbgCachingResponse = "Caching OCSP response for [%s], key [%s]"
DbgAchievedCompression = "OCSP response compression ratio: [%f]"
DbgCacheHit = "OCSP peer cache hit for key [%s]"
DbgCacheMiss = "OCSP peer cache miss for key [%s]"
DbgPreservedRevocation = "Revoked OCSP response for key [%s] preserved by cache policy"
DbgDeletingCacheResponse = "Deleting OCSP peer cached response for key [%s]"
DbgStartingCache = "Starting OCSP peer cache"
DbgStoppingCache = "Stopping OCSP peer cache"
DbgLoadingCache = "Loading OCSP peer cache [%s]"
DbgNoCacheFound = "No OCSP peer cache found, starting with empty cache"
DbgSavingCache = "Saving OCSP peer cache [%s]"
DbgCacheSaved = "Saved OCSP peer cache successfully (%d bytes)"
DbgMakingCARequest = "Trying OCSP responder url [%s]"
DbgResponseExpired = "OCSP response NextUpdate [%s] is before now [%s] with clockskew [%s]"
DbgResponseTTLExpired = "OCSP response cache expiry [%s] is before now [%s] with clockskew [%s]"
DbgResponseFutureDated = "OCSP response ThisUpdate [%s] is before now [%s] with clockskew [%s]"
DbgCacheSaveTimerExpired = "OCSP peer cache save timer expired"
DbgCacheDirtySave = "OCSP peer cache is dirty, saving"
// Returned to peer as TLS reject reason
MsgTLSClientRejectConnection = "client not OCSP valid"
MsgTLSServerRejectConnection = "server not OCSP valid"
// Expected runtime errors (direct logged)
ErrCAResponderCalloutFail = "Attempt to obtain OCSP response from CA responder for [%s] failed: %s"
ErrNewCAResponseNotCurrent = "New OCSP CA response obtained for [%s] but not current"
ErrCAResponseParseFailed = "Could not parse OCSP CA response for [%s]: %s"
ErrOCSPInvalidPeerLink = "OCSP verify fail for [%s] with CA status [%s]"
// Policy override warnings (direct logged)
MsgAllowWhenCAUnreachableOccurred = "Failed to obtain OCSP CA response for [%s] but AllowWhenCAUnreachable set; no cached revocation so allowing"
MsgAllowWhenCAUnreachableOccurredCachedRevoke = "Failed to obtain OCSP CA response for [%s] but AllowWhenCAUnreachable set; cached revocation exists so rejecting"
MsgAllowWarnOnlyOccurred = "OCSP verify fail for [%s] but WarnOnly is true so allowing"
// Info (direct logged)
MsgCacheOnline = "OCSP peer cache online, type [%s]"
MsgCacheOffline = "OCSP peer cache offline, type [%s]"
// OCSP cert invalid reasons (debug and event reasons)
MsgFailedOCSPResponseFetch = "Failed OCSP response fetch"
MsgOCSPResponseNotEffective = "OCSP response not in effectivity window"
MsgFailedOCSPResponseParse = "Failed OCSP response parse"
MsgOCSPResponseInvalidStatus = "Invalid OCSP response status: %s"
MsgOCSPResponseDelegationInvalid = "Invalid OCSP response delegation: %s"
MsgCachedOCSPResponseInvalid = "Invalid cached OCSP response for [%s] with fingerprint [%s]"
)
@@ -0,0 +1,92 @@
// Copyright 2023-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 certidp
import (
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/crypto/ocsp"
)
func FetchOCSPResponse(link *ChainLink, opts *OCSPPeerConfig, log *Log) ([]byte, error) {
if link == nil || link.Leaf == nil || link.Issuer == nil || opts == nil || log == nil {
return nil, errors.New(ErrInvalidChainlink)
}
timeout := time.Duration(opts.Timeout * float64(time.Second))
if timeout <= 0*time.Second {
timeout = DefaultOCSPResponderTimeout
}
getRequestBytes := func(u string, hc *http.Client) ([]byte, error) {
resp, err := hc.Get(u)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(ErrBadResponderHTTPStatus, resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
// Request documentation:
// https://tools.ietf.org/html/rfc6960#appendix-A.1
reqDER, err := ocsp.CreateRequest(link.Leaf, link.Issuer, nil)
if err != nil {
return nil, err
}
reqEnc := encodeOCSPRequest(reqDER)
responders := *link.OCSPWebEndpoints
if len(responders) == 0 {
return nil, errors.New(ErrNoAvailOCSPServers)
}
var raw []byte
hc := &http.Client{
Timeout: timeout,
}
for _, u := range responders {
responderURL := u.String()
log.Debugf(DbgMakingCARequest, responderURL)
responderURL = strings.TrimSuffix(responderURL, "/")
raw, err = getRequestBytes(fmt.Sprintf("%s/%s", responderURL, reqEnc), hc)
if err == nil {
break
}
}
if err != nil {
return nil, fmt.Errorf(ErrFailedWithAllRequests, err)
}
return raw, nil
}
// encodeOCSPRequest encodes the OCSP request in base64 and URL-encodes it.
// This is needed to fulfill the OCSP responder's requirements for the request format. (X.690)
func encodeOCSPRequest(reqDER []byte) string {
reqEnc := base64.StdEncoding.EncodeToString(reqDER)
return url.QueryEscape(reqEnc)
}
@@ -0,0 +1,104 @@
// Copyright 2022-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 certstore
import (
"crypto"
"crypto/x509"
"io"
"runtime"
"strings"
)
type StoreType int
const MATCHBYEMPTY = 0
const STOREEMPTY = 0
const (
windowsCurrentUser StoreType = iota + 1
windowsLocalMachine
)
var StoreMap = map[string]StoreType{
"windowscurrentuser": windowsCurrentUser,
"windowslocalmachine": windowsLocalMachine,
}
var StoreOSMap = map[StoreType]string{
windowsCurrentUser: "windows",
windowsLocalMachine: "windows",
}
type MatchByType int
const (
matchByIssuer MatchByType = iota + 1
matchBySubject
matchByThumbprint
)
var MatchByMap = map[string]MatchByType{
"issuer": matchByIssuer,
"subject": matchBySubject,
"thumbprint": matchByThumbprint,
}
var Usage = `
In place of cert_file and key_file you may use the windows certificate store:
tls {
cert_store: "WindowsCurrentUser"
cert_match_by: "Subject"
cert_match: "MyServer123"
}
`
func ParseCertStore(certStore string) (StoreType, error) {
certStoreType, exists := StoreMap[strings.ToLower(certStore)]
if !exists {
return 0, ErrBadCertStore
}
validOS, exists := StoreOSMap[certStoreType]
if !exists || validOS != runtime.GOOS {
return 0, ErrOSNotCompatCertStore
}
return certStoreType, nil
}
func ParseCertMatchBy(certMatchBy string) (MatchByType, error) {
certMatchByType, exists := MatchByMap[strings.ToLower(certMatchBy)]
if !exists {
return 0, ErrBadMatchByType
}
return certMatchByType, nil
}
func GetLeafIssuer(leaf *x509.Certificate, vOpts x509.VerifyOptions) (issuer *x509.Certificate) {
chains, err := leaf.Verify(vOpts)
if err != nil || len(chains) == 0 {
issuer = nil
} else {
issuer = chains[0][1]
}
return
}
// credential provides access to a public key and is a crypto.Signer.
type credential interface {
// Public returns the public key corresponding to the leaf certificate.
Public() crypto.PublicKey
// Sign signs digest with the private key.
Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error)
}
@@ -0,0 +1,45 @@
// Copyright 2022-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.
//go:build !windows
package certstore
import (
"crypto"
"crypto/tls"
"io"
)
var _ = MATCHBYEMPTY
// otherKey implements crypto.Signer and crypto.Decrypter to satisfy linter on platforms that don't implement certstore
type otherKey struct{}
func TLSConfig(_ StoreType, _ MatchByType, _ string, _ []string, _ bool, _ *tls.Config) error {
return ErrOSNotCompatCertStore
}
// Public always returns nil public key since this is a stub on non-supported platform
func (k otherKey) Public() crypto.PublicKey {
return nil
}
// Sign always returns a nil signature since this is a stub on non-supported platform
func (k otherKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
_, _, _ = rand, digest, opts
return nil, nil
}
// Verify interface conformance.
var _ credential = &otherKey{}
@@ -0,0 +1,965 @@
// Copyright 2022-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.
//
// Adapted, updated, and enhanced from CertToStore, https://github.com/google/certtostore/releases/tag/v1.0.2
// Apache License, Version 2.0, Copyright 2017 Google Inc.
package certstore
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/binary"
"fmt"
"io"
"math/big"
"reflect"
"sync"
"syscall"
"unicode/utf16"
"unsafe"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
"golang.org/x/sys/windows"
)
const (
// wincrypt.h constants
winAcquireCached = windows.CRYPT_ACQUIRE_CACHE_FLAG
winAcquireSilent = windows.CRYPT_ACQUIRE_SILENT_FLAG
winAcquireOnlyNCryptKey = windows.CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG
winEncodingX509ASN = windows.X509_ASN_ENCODING
winEncodingPKCS7 = windows.PKCS_7_ASN_ENCODING
winCertStoreProvSystem = windows.CERT_STORE_PROV_SYSTEM
winCertStoreCurrentUser = windows.CERT_SYSTEM_STORE_CURRENT_USER
winCertStoreLocalMachine = windows.CERT_SYSTEM_STORE_LOCAL_MACHINE
winCertStoreReadOnly = windows.CERT_STORE_READONLY_FLAG
winInfoIssuerFlag = windows.CERT_INFO_ISSUER_FLAG
winInfoSubjectFlag = windows.CERT_INFO_SUBJECT_FLAG
winCompareNameStrW = windows.CERT_COMPARE_NAME_STR_W
winCompareShift = windows.CERT_COMPARE_SHIFT
// Reference https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
winFindIssuerStr = windows.CERT_FIND_ISSUER_STR_W
winFindSubjectStr = windows.CERT_FIND_SUBJECT_STR_W
winFindHashStr = windows.CERT_FIND_HASH_STR
winNcryptKeySpec = windows.CERT_NCRYPT_KEY_SPEC
winBCryptPadPKCS1 uintptr = 0x2
winBCryptPadPSS uintptr = 0x8 // Modern TLS 1.2+
winBCryptPadPSSSalt uint32 = 32 // default 20, 32 optimal for typical SHA256 hash
winRSA1Magic = 0x31415352 // "RSA1" BCRYPT_RSAPUBLIC_MAGIC
winECS1Magic = 0x31534345 // "ECS1" BCRYPT_ECDSA_PUBLIC_P256_MAGIC
winECS3Magic = 0x33534345 // "ECS3" BCRYPT_ECDSA_PUBLIC_P384_MAGIC
winECS5Magic = 0x35534345 // "ECS5" BCRYPT_ECDSA_PUBLIC_P521_MAGIC
winECK1Magic = 0x314B4345 // "ECK1" BCRYPT_ECDH_PUBLIC_P256_MAGIC
winECK3Magic = 0x334B4345 // "ECK3" BCRYPT_ECDH_PUBLIC_P384_MAGIC
winECK5Magic = 0x354B4345 // "ECK5" BCRYPT_ECDH_PUBLIC_P521_MAGIC
winCryptENotFound = windows.CRYPT_E_NOT_FOUND
providerMSSoftware = "Microsoft Software Key Storage Provider"
)
var (
winBCryptRSAPublicBlob = winWide("RSAPUBLICBLOB")
winBCryptECCPublicBlob = winWide("ECCPUBLICBLOB")
winNCryptAlgorithmGroupProperty = winWide("Algorithm Group") // NCRYPT_ALGORITHM_GROUP_PROPERTY
winNCryptUniqueNameProperty = winWide("Unique Name") // NCRYPT_UNIQUE_NAME_PROPERTY
winNCryptECCCurveNameProperty = winWide("ECCCurveName") // NCRYPT_ECC_CURVE_NAME_PROPERTY
winCurveIDs = map[uint32]elliptic.Curve{
winECS1Magic: elliptic.P256(), // BCRYPT_ECDSA_PUBLIC_P256_MAGIC
winECS3Magic: elliptic.P384(), // BCRYPT_ECDSA_PUBLIC_P384_MAGIC
winECS5Magic: elliptic.P521(), // BCRYPT_ECDSA_PUBLIC_P521_MAGIC
winECK1Magic: elliptic.P256(), // BCRYPT_ECDH_PUBLIC_P256_MAGIC
winECK3Magic: elliptic.P384(), // BCRYPT_ECDH_PUBLIC_P384_MAGIC
winECK5Magic: elliptic.P521(), // BCRYPT_ECDH_PUBLIC_P521_MAGIC
}
winCurveNames = map[string]elliptic.Curve{
"nistP256": elliptic.P256(), // BCRYPT_ECC_CURVE_NISTP256
"nistP384": elliptic.P384(), // BCRYPT_ECC_CURVE_NISTP384
"nistP521": elliptic.P521(), // BCRYPT_ECC_CURVE_NISTP521
}
winAlgIDs = map[crypto.Hash]*uint16{
crypto.SHA1: winWide("SHA1"), // BCRYPT_SHA1_ALGORITHM
crypto.SHA256: winWide("SHA256"), // BCRYPT_SHA256_ALGORITHM
crypto.SHA384: winWide("SHA384"), // BCRYPT_SHA384_ALGORITHM
crypto.SHA512: winWide("SHA512"), // BCRYPT_SHA512_ALGORITHM
}
// MY is well-known system store on Windows that holds personal certificates. Read
// More about the CA locations here:
// https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/wcf/certificate-of-clientcertificate-element?redirectedfrom=MSDN
// https://superuser.com/questions/217719/what-are-the-windows-system-certificate-stores
// https://docs.microsoft.com/en-us/windows/win32/seccrypto/certificate-stores
// https://learn.microsoft.com/en-us/windows/win32/seccrypto/system-store-locations
// https://stackoverflow.com/questions/63286085/which-x509-storename-refers-to-the-certificates-stored-beneath-trusted-root-cert#:~:text=4-,StoreName.,is%20%22Intermediate%20Certification%20Authorities%22.
winMyStore = winWide("MY")
winIntermediateCAStore = winWide("CA")
winRootStore = winWide("Root")
winAuthRootStore = winWide("AuthRoot")
// These DLLs must be available on all Windows hosts
winCrypt32 = windows.NewLazySystemDLL("crypt32.dll")
winNCrypt = windows.NewLazySystemDLL("ncrypt.dll")
winCertFindCertificateInStore = winCrypt32.NewProc("CertFindCertificateInStore")
winCertVerifyTimeValidity = winCrypt32.NewProc("CertVerifyTimeValidity")
winCryptAcquireCertificatePrivateKey = winCrypt32.NewProc("CryptAcquireCertificatePrivateKey")
winNCryptExportKey = winNCrypt.NewProc("NCryptExportKey")
winNCryptOpenStorageProvider = winNCrypt.NewProc("NCryptOpenStorageProvider")
winNCryptGetProperty = winNCrypt.NewProc("NCryptGetProperty")
winNCryptSignHash = winNCrypt.NewProc("NCryptSignHash")
winFnGetProperty = winGetProperty
)
func init() {
for _, d := range []*windows.LazyDLL{
winCrypt32, winNCrypt,
} {
if err := d.Load(); err != nil {
panic(err)
}
}
for _, p := range []*windows.LazyProc{
winCertFindCertificateInStore, winCryptAcquireCertificatePrivateKey,
winNCryptExportKey, winNCryptOpenStorageProvider,
winNCryptGetProperty, winNCryptSignHash,
} {
if err := p.Find(); err != nil {
panic(err)
}
}
}
type winPKCS1PaddingInfo struct {
pszAlgID *uint16
}
type winPSSPaddingInfo struct {
pszAlgID *uint16
cbSalt uint32
}
// createCACertsPool generates a CertPool from the Windows certificate store,
// adding all matching certificates from the caCertsMatch array to the pool.
// All matching certificates (vs first) are added to the pool based on a user
// request. If no certificates are found an error is returned.
func createCACertsPool(cs *winCertStore, storeType uint32, caCertsMatch []string, skipInvalid bool) (*x509.CertPool, error) {
var errs []error
caPool := x509.NewCertPool()
for _, s := range caCertsMatch {
lfs, err := cs.caCertsBySubjectMatch(s, storeType, skipInvalid)
if err != nil {
errs = append(errs, err)
} else {
for _, lf := range lfs {
caPool.AddCert(lf)
}
}
}
// If every lookup failed return the errors.
if len(errs) == len(caCertsMatch) {
return nil, fmt.Errorf("unable to match any CA certificate: %v", errs)
}
return caPool, nil
}
// TLSConfig fulfills the same function as reading cert and key pair from
// pem files but sources the Windows certificate store instead. The
// certMatchBy and certMatch fields search the "MY" certificate location
// for the first certificate that matches the certMatch field. The
// caCertsMatch field is used to search the Trusted Root, Third Party Root,
// and Intermediate Certificate Authority locations for certificates with
// Subjects matching the provided strings. If a match is found, the
// certificate is added to the pool that is used to verify the certificate
// chain.
func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, caCertsMatch []string, skipInvalid bool, config *tls.Config) error {
var (
leaf *x509.Certificate
leafCtx *windows.CertContext
pk *winKey
vOpts = x509.VerifyOptions{}
chains [][]*x509.Certificate
chain []*x509.Certificate
rawChain [][]byte
)
// By StoreType, open a store
if certStore == windowsCurrentUser || certStore == windowsLocalMachine {
var scope uint32
cs, err := winOpenCertStore(providerMSSoftware)
if err != nil || cs == nil {
return err
}
if certStore == windowsCurrentUser {
scope = winCertStoreCurrentUser
}
if certStore == windowsLocalMachine {
scope = winCertStoreLocalMachine
}
// certByIssuer or certBySubject
if certMatchBy == matchBySubject || certMatchBy == MATCHBYEMPTY {
leaf, leafCtx, err = cs.certBySubject(certMatch, scope, skipInvalid)
} else if certMatchBy == matchByIssuer {
leaf, leafCtx, err = cs.certByIssuer(certMatch, scope, skipInvalid)
} else if certMatchBy == matchByThumbprint {
leaf, leafCtx, err = cs.certByThumbprint(certMatch, scope, skipInvalid)
} else {
return ErrBadMatchByType
}
if err != nil {
// pass through error from cert search
return err
}
if leaf == nil || leafCtx == nil {
return ErrFailedCertSearch
}
pk, err = cs.certKey(leafCtx)
if err != nil {
return err
}
if pk == nil {
return ErrNoPrivateKeyStoreRef
}
// Look for CA Certificates
if len(caCertsMatch) != 0 {
caPool, err := createCACertsPool(cs, scope, caCertsMatch, skipInvalid)
if err != nil {
return err
}
config.ClientCAs = caPool
}
} else {
return ErrBadCertStore
}
// Get intermediates in the cert store for the found leaf IFF there is a full chain of trust in the store
// otherwise just use leaf as the final chain.
//
// Using std lib Verify as a reliable way to get valid chains out of the win store for the leaf; however,
// using empty options since server TLS stanza could be TLS role as server identity or client identity.
chains, err := leaf.Verify(vOpts)
if err != nil || len(chains) == 0 {
chains = append(chains, []*x509.Certificate{leaf})
}
// We have at least one verified chain so pop the first chain and remove the self-signed CA cert (if present)
// from the end of the chain
chain = chains[0]
if len(chain) > 1 {
chain = chain[:len(chain)-1]
}
// For tls.Certificate.Certificate need a [][]byte from []*x509.Certificate
// Approximate capacity for efficiency
rawChain = make([][]byte, 0, len(chain))
for _, link := range chain {
rawChain = append(rawChain, link.Raw)
}
tlsCert := tls.Certificate{
Certificate: rawChain,
PrivateKey: pk,
Leaf: leaf,
}
config.Certificates = []tls.Certificate{tlsCert}
// note: pk is a windows pointer (not freed by Go) but needs to live the life of the server for Signing.
// The cert context (leafCtx) windows pointer must not be freed underneath the pk so also life of the server.
return nil
}
// winWide returns a pointer to uint16 representing the equivalent
// to a Windows LPCWSTR.
func winWide(s string) *uint16 {
w := utf16.Encode([]rune(s))
w = append(w, 0)
return &w[0]
}
// winOpenProvider gets a provider handle for subsequent calls
func winOpenProvider(provider string) (uintptr, error) {
var hProv uintptr
pname := winWide(provider)
// Open the provider, the last parameter is not used
r, _, err := winNCryptOpenStorageProvider.Call(uintptr(unsafe.Pointer(&hProv)), uintptr(unsafe.Pointer(pname)), 0)
if r == 0 {
return hProv, nil
}
return hProv, fmt.Errorf("NCryptOpenStorageProvider returned %X: %v", r, err)
}
// winFindCert wraps the CertFindCertificateInStore library call. Note that any cert context passed
// into prev will be freed. If no certificate was found, nil will be returned.
func winFindCert(store windows.Handle, enc, findFlags, findType uint32, para *uint16, prev *windows.CertContext) (*windows.CertContext, error) {
h, _, err := winCertFindCertificateInStore.Call(
uintptr(store),
uintptr(enc),
uintptr(findFlags),
uintptr(findType),
uintptr(unsafe.Pointer(para)),
uintptr(unsafe.Pointer(prev)),
)
if h == 0 {
// Actual error, or simply not found?
if errno, ok := err.(syscall.Errno); ok && errno == syscall.Errno(winCryptENotFound) {
return nil, ErrFailedCertSearch
}
return nil, ErrFailedCertSearch
}
// nolint:govet
return (*windows.CertContext)(unsafe.Pointer(h)), nil
}
// winVerifyCertValid wraps the CertVerifyTimeValidity and simply returns true if the certificate is valid
func winVerifyCertValid(timeToVerify *windows.Filetime, certInfo *windows.CertInfo) bool {
// this function does not document returning errors / setting lasterror
r, _, _ := winCertVerifyTimeValidity.Call(
uintptr(unsafe.Pointer(timeToVerify)),
uintptr(unsafe.Pointer(certInfo)),
)
return r == 0
}
// winCertStore is a store implementation for the Windows Certificate Store
type winCertStore struct {
Prov uintptr
ProvName string
stores map[string]*winStoreHandle
mu sync.Mutex
}
// winOpenCertStore creates a winCertStore
func winOpenCertStore(provider string) (*winCertStore, error) {
cngProv, err := winOpenProvider(provider)
if err != nil {
// pass through error from winOpenProvider
return nil, err
}
wcs := &winCertStore{
Prov: cngProv,
ProvName: provider,
stores: make(map[string]*winStoreHandle),
}
return wcs, nil
}
// winCertContextToX509 creates an x509.Certificate from a Windows cert context.
func winCertContextToX509(ctx *windows.CertContext) (*x509.Certificate, error) {
var der []byte
slice := (*reflect.SliceHeader)(unsafe.Pointer(&der))
slice.Data = uintptr(unsafe.Pointer(ctx.EncodedCert))
slice.Len = int(ctx.Length)
slice.Cap = int(ctx.Length)
return x509.ParseCertificate(der)
}
// certByIssuer matches and returns the first certificate found by passed issuer.
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
// current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_ISSUER_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) certByIssuer(issuer string, storeType uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindIssuerStr, issuer, winMyStore, storeType, skipInvalid)
}
// certBySubject matches and returns the first certificate found by passed subject field.
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
// current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) certBySubject(subject string, storeType uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindSubjectStr, subject, winMyStore, storeType, skipInvalid)
}
// certByThumbprint matches and returns the first certificate found by passed SHA1 thumbprint.
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
// current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) certByThumbprint(hash string, storeType uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindHashStr, hash, winMyStore, storeType, skipInvalid)
}
// caCertsBySubjectMatch matches and returns all matching certificates of the subject field.
//
// The following locations are searched:
// 1) Root (Trusted Root Certification Authorities)
// 2) AuthRoot (Third-Party Root Certification Authorities)
// 3) CA (Intermediate Certification Authorities)
//
// Caller specifies current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) caCertsBySubjectMatch(subject string, storeType uint32, skipInvalid bool) ([]*x509.Certificate, error) {
var (
leaf *x509.Certificate
searchLocations = [3]*uint16{winRootStore, winAuthRootStore, winIntermediateCAStore}
rv []*x509.Certificate
)
// surprisingly, an empty string returns a result. We'll treat this as an error.
if subject == "" {
return nil, ErrBadCaCertMatchField
}
for _, sr := range searchLocations {
var err error
if leaf, _, err = w.certSearch(winFindSubjectStr, subject, sr, storeType, skipInvalid); err == nil {
rv = append(rv, leaf)
} else {
// Ignore the failed search from a single location. Errors we catch include
// ErrFailedX509Extract (resulting from a malformed certificate) and errors
// around invalid attributes, unsupported algorithms, etc. These are corner
// cases as certificates with these errors shouldn't have been allowed
// to be added to the store in the first place.
if err != ErrFailedCertSearch {
return nil, err
}
}
}
// Not found anywhere
if len(rv) == 0 {
return nil, ErrFailedCertSearch
}
return rv, nil
}
// certSearch is a helper function to lookup certificates based on search type and match value.
// store is used to specify which store to perform the lookup in (system or user).
func (w *winCertStore) certSearch(searchType uint32, matchValue string, searchRoot *uint16, store uint32, skipInvalid bool) (*x509.Certificate, *windows.CertContext, error) {
// store handle to "MY" store
h, err := w.storeHandle(store, searchRoot)
if err != nil {
return nil, nil, err
}
var prev *windows.CertContext
var cert *x509.Certificate
i, err := windows.UTF16PtrFromString(matchValue)
if err != nil {
return nil, nil, ErrFailedCertSearch
}
// pass 0 as the third parameter because it is not used
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa376064(v=vs.85).aspx
for {
nc, err := winFindCert(h, winEncodingX509ASN|winEncodingPKCS7, 0, searchType, i, prev)
if err != nil {
return nil, nil, err
}
if nc != nil {
// certificate found
prev = nc
var now *windows.Filetime
if skipInvalid && !winVerifyCertValid(now, nc.CertInfo) {
continue
}
// Extract the DER-encoded certificate from the cert context
xc, err := winCertContextToX509(nc)
if err == nil {
cert = xc
break
} else {
return nil, nil, ErrFailedX509Extract
}
} else {
return nil, nil, ErrFailedCertSearch
}
}
if cert == nil {
return nil, nil, ErrFailedX509Extract
}
return cert, prev, nil
}
type winStoreHandle struct {
handle *windows.Handle
}
func winNewStoreHandle(provider uint32, store *uint16) (*winStoreHandle, error) {
var s winStoreHandle
if s.handle != nil {
return &s, nil
}
st, err := windows.CertOpenStore(
winCertStoreProvSystem,
0,
0,
provider|winCertStoreReadOnly,
uintptr(unsafe.Pointer(store)))
if err != nil {
return nil, ErrBadCryptoStoreProvider
}
s.handle = &st
return &s, nil
}
// winKey implements crypto.Signer and crypto.Decrypter for key based operations.
type winKey struct {
handle uintptr
pub crypto.PublicKey
Container string
AlgorithmGroup string
}
// Public exports a public key to implement crypto.Signer
func (k winKey) Public() crypto.PublicKey {
return k.pub
}
// Sign returns the signature of a hash to implement crypto.Signer
func (k winKey) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
switch k.AlgorithmGroup {
case "ECDSA", "ECDH":
return winSignECDSA(k.handle, digest)
case "RSA":
hf := opts.HashFunc()
algID, ok := winAlgIDs[hf]
if !ok {
return nil, ErrBadRSAHashAlgorithm
}
switch opts.(type) {
case *rsa.PSSOptions:
return winSignRSAPSSPadding(k.handle, digest, algID)
default:
return winSignRSAPKCS1Padding(k.handle, digest, algID)
}
default:
return nil, ErrBadSigningAlgorithm
}
}
func winSignECDSA(kh uintptr, digest []byte) ([]byte, error) {
var size uint32
// Obtain the size of the signature
r, _, _ := winNCryptSignHash.Call(
kh,
0,
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
0,
0,
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrStoreECDSASigningError
}
// Obtain the signature data
buf := make([]byte, size)
r, _, _ = winNCryptSignHash.Call(
kh,
0,
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrStoreECDSASigningError
}
if len(buf) != int(size) {
return nil, ErrStoreECDSASigningError
}
return winPackECDSASigValue(bytes.NewReader(buf[:size]), int(size/2))
}
func winPackECDSASigValue(r io.Reader, digestLength int) ([]byte, error) {
sigR := make([]byte, digestLength)
if _, err := io.ReadFull(r, sigR); err != nil {
return nil, ErrStoreECDSASigningError
}
sigS := make([]byte, digestLength)
if _, err := io.ReadFull(r, sigS); err != nil {
return nil, ErrStoreECDSASigningError
}
var b cryptobyte.Builder
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
b.AddASN1BigInt(new(big.Int).SetBytes(sigR))
b.AddASN1BigInt(new(big.Int).SetBytes(sigS))
})
return b.Bytes()
}
func winSignRSAPKCS1Padding(kh uintptr, digest []byte, algID *uint16) ([]byte, error) {
// PKCS#1 v1.5 padding for some TLS 1.2
padInfo := winPKCS1PaddingInfo{pszAlgID: algID}
var size uint32
// Obtain the size of the signature
r, _, _ := winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
0,
0,
uintptr(unsafe.Pointer(&size)),
winBCryptPadPKCS1)
if r != 0 {
return nil, ErrStoreRSASigningError
}
// Obtain the signature data
sig := make([]byte, size)
r, _, _ = winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
uintptr(unsafe.Pointer(&sig[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
winBCryptPadPKCS1)
if r != 0 {
return nil, ErrStoreRSASigningError
}
return sig[:size], nil
}
func winSignRSAPSSPadding(kh uintptr, digest []byte, algID *uint16) ([]byte, error) {
// PSS padding for TLS 1.3 and some TLS 1.2
padInfo := winPSSPaddingInfo{pszAlgID: algID, cbSalt: winBCryptPadPSSSalt}
var size uint32
// Obtain the size of the signature
r, _, _ := winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
0,
0,
uintptr(unsafe.Pointer(&size)),
winBCryptPadPSS)
if r != 0 {
return nil, ErrStoreRSASigningError
}
// Obtain the signature data
sig := make([]byte, size)
r, _, _ = winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
uintptr(unsafe.Pointer(&sig[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
winBCryptPadPSS)
if r != 0 {
return nil, ErrStoreRSASigningError
}
return sig[:size], nil
}
// certKey wraps CryptAcquireCertificatePrivateKey. It obtains the CNG private
// key of a known certificate and returns a pointer to a winKey which implements
// both crypto.Signer. When a nil cert context is passed
// a nil key is intentionally returned, to model the expected behavior of a
// non-existent cert having no private key.
// https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptacquirecertificateprivatekey
func (w *winCertStore) certKey(cert *windows.CertContext) (*winKey, error) {
// Return early if a nil cert was passed.
if cert == nil {
return nil, nil
}
var (
kh uintptr
spec uint32
mustFree int
)
r, _, _ := winCryptAcquireCertificatePrivateKey.Call(
uintptr(unsafe.Pointer(cert)),
winAcquireCached|winAcquireSilent|winAcquireOnlyNCryptKey,
0, // Reserved, must be null.
uintptr(unsafe.Pointer(&kh)),
uintptr(unsafe.Pointer(&spec)),
uintptr(unsafe.Pointer(&mustFree)),
)
// If the function succeeds, the return value is nonzero (TRUE).
if r == 0 {
return nil, ErrNoPrivateKeyStoreRef
}
if mustFree != 0 {
return nil, ErrNoPrivateKeyStoreRef
}
if spec != winNcryptKeySpec {
return nil, ErrNoPrivateKeyStoreRef
}
return winKeyMetadata(kh)
}
func winKeyMetadata(kh uintptr) (*winKey, error) {
// uc is used to populate the unique container name attribute of the private key
uc, err := winGetPropertyStr(kh, winNCryptUniqueNameProperty)
if err != nil {
// unable to determine key unique name
return nil, ErrExtractingPrivateKeyMetadata
}
alg, err := winGetPropertyStr(kh, winNCryptAlgorithmGroupProperty)
if err != nil {
// unable to determine key algorithm
return nil, ErrExtractingPrivateKeyMetadata
}
var pub crypto.PublicKey
switch alg {
case "ECDSA", "ECDH":
buf, err := winExport(kh, winBCryptECCPublicBlob)
if err != nil {
// failed to export ECC public key
return nil, ErrExtractingECCPublicKey
}
pub, err = unmarshalECC(buf, kh)
if err != nil {
return nil, ErrExtractingECCPublicKey
}
case "RSA":
buf, err := winExport(kh, winBCryptRSAPublicBlob)
if err != nil {
return nil, ErrExtractingRSAPublicKey
}
pub, err = winUnmarshalRSA(buf)
if err != nil {
return nil, ErrExtractingRSAPublicKey
}
default:
return nil, ErrBadPublicKeyAlgorithm
}
return &winKey{handle: kh, pub: pub, Container: uc, AlgorithmGroup: alg}, nil
}
func winGetProperty(kh uintptr, property *uint16) ([]byte, error) {
var strSize uint32
r, _, _ := winNCryptGetProperty.Call(
kh,
uintptr(unsafe.Pointer(property)),
0,
0,
uintptr(unsafe.Pointer(&strSize)),
0,
0)
if r != 0 {
return nil, ErrExtractPropertyFromKey
}
buf := make([]byte, strSize)
r, _, _ = winNCryptGetProperty.Call(
kh,
uintptr(unsafe.Pointer(property)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(strSize),
uintptr(unsafe.Pointer(&strSize)),
0,
0)
if r != 0 {
return nil, ErrExtractPropertyFromKey
}
return buf, nil
}
func winGetPropertyStr(kh uintptr, property *uint16) (string, error) {
buf, err := winFnGetProperty(kh, property)
if err != nil {
return "", ErrExtractPropertyFromKey
}
uc := bytes.ReplaceAll(buf, []byte{0x00}, []byte(""))
return string(uc), nil
}
func winExport(kh uintptr, blobType *uint16) ([]byte, error) {
var size uint32
// When obtaining the size of a public key, most parameters are not required
r, _, _ := winNCryptExportKey.Call(
kh,
0,
uintptr(unsafe.Pointer(blobType)),
0,
0,
0,
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrExtractingPublicKey
}
// Place the exported key in buf now that we know the size required
buf := make([]byte, size)
r, _, _ = winNCryptExportKey.Call(
kh,
0,
uintptr(unsafe.Pointer(blobType)),
0,
uintptr(unsafe.Pointer(&buf[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrExtractingPublicKey
}
return buf, nil
}
func unmarshalECC(buf []byte, kh uintptr) (*ecdsa.PublicKey, error) {
// BCRYPT_ECCKEY_BLOB from bcrypt.h
header := struct {
Magic uint32
Key uint32
}{}
r := bytes.NewReader(buf)
if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
return nil, ErrExtractingECCPublicKey
}
curve, ok := winCurveIDs[header.Magic]
if !ok {
// Fix for b/185945636, where despite specifying the curve, nCrypt returns
// an incorrect response with BCRYPT_ECDSA_PUBLIC_GENERIC_MAGIC.
var err error
curve, err = winCurveName(kh)
if err != nil {
// unsupported header magic or cannot match the curve by name
return nil, err
}
}
keyX := make([]byte, header.Key)
if n, err := r.Read(keyX); n != int(header.Key) || err != nil {
// failed to read key X
return nil, ErrExtractingECCPublicKey
}
keyY := make([]byte, header.Key)
if n, err := r.Read(keyY); n != int(header.Key) || err != nil {
// failed to read key Y
return nil, ErrExtractingECCPublicKey
}
pub := &ecdsa.PublicKey{
Curve: curve,
X: new(big.Int).SetBytes(keyX),
Y: new(big.Int).SetBytes(keyY),
}
return pub, nil
}
// winCurveName reads the curve name property and returns the corresponding curve.
func winCurveName(kh uintptr) (elliptic.Curve, error) {
cn, err := winGetPropertyStr(kh, winNCryptECCCurveNameProperty)
if err != nil {
// unable to determine the curve property name
return nil, ErrExtractPropertyFromKey
}
curve, ok := winCurveNames[cn]
if !ok {
// unknown curve name
return nil, ErrBadECCCurveName
}
return curve, nil
}
func winUnmarshalRSA(buf []byte) (*rsa.PublicKey, error) {
// BCRYPT_RSA_BLOB from bcrypt.h
header := struct {
Magic uint32
BitLength uint32
PublicExpSize uint32
ModulusSize uint32
UnusedPrime1 uint32
UnusedPrime2 uint32
}{}
r := bytes.NewReader(buf)
if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
return nil, ErrExtractingRSAPublicKey
}
if header.Magic != winRSA1Magic {
// invalid header magic
return nil, ErrExtractingRSAPublicKey
}
if header.PublicExpSize > 8 {
// unsupported public exponent size
return nil, ErrExtractingRSAPublicKey
}
exp := make([]byte, 8)
if n, err := r.Read(exp[8-header.PublicExpSize:]); n != int(header.PublicExpSize) || err != nil {
// failed to read public exponent
return nil, ErrExtractingRSAPublicKey
}
mod := make([]byte, header.ModulusSize)
if n, err := r.Read(mod); n != int(header.ModulusSize) || err != nil {
// failed to read modulus
return nil, ErrExtractingRSAPublicKey
}
pub := &rsa.PublicKey{
N: new(big.Int).SetBytes(mod),
E: int(binary.BigEndian.Uint64(exp)),
}
return pub, nil
}
// storeHandle returns a handle to a given cert store, opening the handle as needed.
func (w *winCertStore) storeHandle(provider uint32, store *uint16) (windows.Handle, error) {
w.mu.Lock()
defer w.mu.Unlock()
key := fmt.Sprintf("%d%s", provider, windows.UTF16PtrToString(store))
var err error
if w.stores[key] == nil {
w.stores[key], err = winNewStoreHandle(provider, store)
if err != nil {
return 0, ErrBadCryptoStoreProvider
}
}
return *w.stores[key].handle, nil
}
// Verify interface conformance.
var _ credential = &winKey{}
@@ -0,0 +1,79 @@
package certstore
import (
"errors"
)
var (
// ErrBadCryptoStoreProvider represents inablity to establish link with a certificate store
ErrBadCryptoStoreProvider = errors.New("unable to open certificate store or store not available")
// ErrBadRSAHashAlgorithm represents a bad or unsupported RSA hash algorithm
ErrBadRSAHashAlgorithm = errors.New("unsupported RSA hash algorithm")
// ErrBadSigningAlgorithm represents a bad or unsupported signing algorithm
ErrBadSigningAlgorithm = errors.New("unsupported signing algorithm")
// ErrStoreRSASigningError represents an error returned from store during RSA signature
ErrStoreRSASigningError = errors.New("unable to obtain RSA signature from store")
// ErrStoreECDSASigningError represents an error returned from store during ECDSA signature
ErrStoreECDSASigningError = errors.New("unable to obtain ECDSA signature from store")
// ErrNoPrivateKeyStoreRef represents an error getting a handle to a private key in store
ErrNoPrivateKeyStoreRef = errors.New("unable to obtain private key handle from store")
// ErrExtractingPrivateKeyMetadata represents a family of errors extracting metadata about the private key in store
ErrExtractingPrivateKeyMetadata = errors.New("unable to extract private key metadata")
// ErrExtractingECCPublicKey represents an error exporting ECC-type public key from store
ErrExtractingECCPublicKey = errors.New("unable to extract ECC public key from store")
// ErrExtractingRSAPublicKey represents an error exporting RSA-type public key from store
ErrExtractingRSAPublicKey = errors.New("unable to extract RSA public key from store")
// ErrExtractingPublicKey represents a general error exporting public key from store
ErrExtractingPublicKey = errors.New("unable to extract public key from store")
// ErrBadPublicKeyAlgorithm represents a bad or unsupported public key algorithm
ErrBadPublicKeyAlgorithm = errors.New("unsupported public key algorithm")
// ErrExtractPropertyFromKey represents a general failure to extract a metadata property field
ErrExtractPropertyFromKey = errors.New("unable to extract property from key")
// ErrBadECCCurveName represents an ECC signature curve name that is bad or unsupported
ErrBadECCCurveName = errors.New("unsupported ECC curve name")
// ErrFailedCertSearch represents not able to find certificate in store
ErrFailedCertSearch = errors.New("unable to find certificate in store")
// ErrFailedX509Extract represents not being able to extract x509 certificate from found cert in store
ErrFailedX509Extract = errors.New("unable to extract x509 from certificate")
// ErrBadMatchByType represents unknown CERT_MATCH_BY passed
ErrBadMatchByType = errors.New("cert match by type not implemented")
// ErrBadCertStore represents unknown CERT_STORE passed
ErrBadCertStore = errors.New("cert store type not implemented")
// ErrConflictCertFileAndStore represents ambiguous configuration of both file and store
ErrConflictCertFileAndStore = errors.New("'cert_file' and 'cert_store' may not both be configured")
// ErrBadCertStoreField represents malformed cert_store option
ErrBadCertStoreField = errors.New("expected 'cert_store' to be a valid non-empty string")
// ErrBadCertMatchByField represents malformed cert_match_by option
ErrBadCertMatchByField = errors.New("expected 'cert_match_by' to be a valid non-empty string")
// ErrBadCertMatchField represents malformed cert_match option
ErrBadCertMatchField = errors.New("expected 'cert_match' to be a valid non-empty string")
// ErrBadCaCertMatchField represents malformed cert_match option
ErrBadCaCertMatchField = errors.New("expected 'ca_certs_match' to be a valid non-empty string array")
// ErrBadCertMatchSkipInvalidField represents malformed cert_match_skip_invalid option
ErrBadCertMatchSkipInvalidField = errors.New("expected 'cert_match_skip_invalid' to be a boolean")
// ErrOSNotCompatCertStore represents cert_store passed that exists but is not valid on current OS
ErrOSNotCompatCertStore = errors.New("cert_store not compatible with current operating system")
)
+73
View File
@@ -0,0 +1,73 @@
// Copyright 2016-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 server
import (
"crypto/fips140"
"crypto/tls"
)
func init() {
for _, cs := range tls.CipherSuites() {
cipherMap[cs.Name] = cs
cipherMapByID[cs.ID] = cs
}
for _, cs := range tls.InsecureCipherSuites() {
cipherMap[cs.Name] = cs
cipherMapByID[cs.ID] = cs
}
}
var cipherMap = map[string]*tls.CipherSuite{}
var cipherMapByID = map[uint16]*tls.CipherSuite{}
func defaultCipherSuites() []uint16 {
ciphers := tls.CipherSuites()
defaults := make([]uint16, 0, len(ciphers))
for _, cs := range ciphers {
defaults = append(defaults, cs.ID)
}
return defaults
}
// Where we maintain available curve preferences
var curvePreferenceMap = map[string]tls.CurveID{
"X25519MLKEM768": tls.X25519MLKEM768,
"X25519": tls.X25519,
"CurveP256": tls.CurveP256,
"CurveP384": tls.CurveP384,
"CurveP521": tls.CurveP521,
}
// reorder to default to the highest level of security. See:
// https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go
func defaultCurvePreferences() []tls.CurveID {
if fips140.Enabled() {
// X25519 is not FIPS-approved by itself, but it is when
// combined with MLKEM768.
return []tls.CurveID{
tls.X25519MLKEM768, // post-quantum
tls.CurveP256,
tls.CurveP384,
tls.CurveP521,
}
}
return []tls.CurveID{
tls.X25519MLKEM768, // post-quantum
tls.X25519, // faster than P256, arguably more secure
tls.CurveP256,
tls.CurveP384,
tls.CurveP521,
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,405 @@
// Copyright 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 server
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"strconv"
"strings"
"time"
)
// PROXY protocol v2 constants
const (
// Protocol signature (12 bytes)
proxyProtoV2Sig = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"
// Version and command byte format: version(4 bits) | command(4 bits)
proxyProtoV2VerMask = 0xF0
proxyProtoV2Ver = 0x20 // Version 2
// Commands
proxyProtoCmdMask = 0x0F
proxyProtoCmdLocal = 0x00 // LOCAL command (health check, use original connection)
proxyProtoCmdProxy = 0x01 // PROXY command (proxied connection)
// Address family and protocol byte format: family(4 bits) | protocol(4 bits)
proxyProtoFamilyMask = 0xF0
proxyProtoFamilyUnspec = 0x00 // Unspecified
proxyProtoFamilyInet = 0x10 // IPv4
proxyProtoFamilyInet6 = 0x20 // IPv6
proxyProtoFamilyUnix = 0x30 // Unix socket
proxyProtoProtoMask = 0x0F
proxyProtoProtoUnspec = 0x00 // Unspecified
proxyProtoProtoStream = 0x01 // TCP/STREAM
proxyProtoProtoDatagram = 0x02 // UDP/DGRAM
// Address sizes
proxyProtoAddrSizeIPv4 = 12 // 4 (src IP) + 4 (dst IP) + 2 (src port) + 2 (dst port)
proxyProtoAddrSizeIPv6 = 36 // 16 (src IP) + 16 (dst IP) + 2 (src port) + 2 (dst port)
// Header sizes
proxyProtoV2HeaderSize = 16 // Fixed header: 12 (sig) + 1 (ver/cmd) + 1 (fam/proto) + 2 (addr len)
// Timeout for reading PROXY protocol header
proxyProtoReadTimeout = 5 * time.Second
)
// PROXY protocol v1 constants
const (
proxyProtoV1Prefix = "PROXY "
proxyProtoV1MaxLineLen = 107 // Maximum line length including CRLF
proxyProtoV1TCP4 = "TCP4"
proxyProtoV1TCP6 = "TCP6"
proxyProtoV1Unknown = "UNKNOWN"
)
var (
// Errors
errProxyProtoInvalid = errors.New("invalid PROXY protocol header")
errProxyProtoUnsupported = errors.New("unsupported PROXY protocol feature")
errProxyProtoTimeout = errors.New("timeout reading PROXY protocol header")
errProxyProtoUnrecognized = errors.New("unrecognized PROXY protocol format")
)
// proxyProtoAddr contains the address information extracted from PROXY protocol header
type proxyProtoAddr struct {
srcIP net.IP
srcPort uint16
dstIP net.IP
dstPort uint16
}
// String implements net.Addr interface
func (p *proxyProtoAddr) String() string {
return net.JoinHostPort(p.srcIP.String(), fmt.Sprintf("%d", p.srcPort))
}
// Network implements net.Addr interface
func (p *proxyProtoAddr) Network() string {
if p.srcIP.To4() != nil {
return "tcp4"
}
return "tcp6"
}
// proxyConn wraps a net.Conn to override RemoteAddr() with the address
// extracted from the PROXY protocol header
type proxyConn struct {
net.Conn
remoteAddr net.Addr
}
// RemoteAddr returns the original client address extracted from PROXY protocol
func (pc *proxyConn) RemoteAddr() net.Addr {
return pc.remoteAddr
}
// detectProxyProtoVersion reads the first bytes and determines protocol version.
// Returns 1 for v1, 2 for v2, or error.
// The first 6 bytes read are returned so they can be used by the parser.
func detectProxyProtoVersion(conn net.Conn) (version int, header []byte, err error) {
// Read first 6 bytes to check for "PROXY " or v2 signature
header = make([]byte, 6)
if _, err = io.ReadFull(conn, header); err != nil {
return 0, nil, fmt.Errorf("failed to read protocol version: %w", err)
}
switch bytesToString(header) {
case proxyProtoV1Prefix:
return 1, header, nil
case proxyProtoV2Sig[:6]:
return 2, header, nil
default:
return 0, nil, errProxyProtoUnrecognized
}
}
// readProxyProtoV1Header parses PROXY protocol v1 text format.
// Expects the "PROXY " prefix (6 bytes) to have already been consumed.
// Returns any bytes that were read past the trailing CRLF so the caller can
// replay them into the next protocol layer.
func readProxyProtoV1Header(conn net.Conn) (*proxyProtoAddr, []byte, error) {
// Read rest of line (max 107 bytes total, already read 6)
maxRemaining := proxyProtoV1MaxLineLen - 6
// Read up to maxRemaining bytes at once (more efficient than byte-by-byte)
buf := make([]byte, maxRemaining)
var line []byte
var remaining []byte
for len(line) < maxRemaining {
// Read available data
n, err := conn.Read(buf[len(line):])
if err != nil {
return nil, nil, fmt.Errorf("failed to read v1 line: %w", err)
}
line = buf[:len(line)+n]
// Look for CRLF in what we've read so far
for i := 0; i < len(line)-1; i++ {
if line[i] == '\r' && line[i+1] == '\n' {
// Found CRLF - keep any over-read bytes for the client parser.
remaining = append(remaining, line[i+2:]...)
line = line[:i]
goto foundCRLF
}
}
}
// Exceeded max length without finding CRLF
return nil, nil, fmt.Errorf("%w: v1 line too long", errProxyProtoInvalid)
foundCRLF:
// Get parts from the protocol
parts := strings.Fields(string(line))
// Validate format
if len(parts) < 1 {
return nil, nil, fmt.Errorf("%w: invalid v1 format", errProxyProtoInvalid)
}
// Handle UNKNOWN (health check, like v2 LOCAL)
if parts[0] == proxyProtoV1Unknown {
return nil, remaining, nil
}
// Must have exactly 5 parts: protocol, src-ip, dst-ip, src-port, dst-port
if len(parts) != 5 {
return nil, nil, fmt.Errorf("%w: invalid v1 format", errProxyProtoInvalid)
}
protocol := parts[0]
srcIP := net.ParseIP(parts[1])
dstIP := net.ParseIP(parts[2])
if srcIP == nil || dstIP == nil {
return nil, nil, fmt.Errorf("%w: invalid address", errProxyProtoInvalid)
}
// Parse ports
srcPort, err := strconv.ParseUint(parts[3], 10, 16)
if err != nil {
return nil, nil, fmt.Errorf("invalid source port: %w", err)
}
dstPort, err := strconv.ParseUint(parts[4], 10, 16)
if err != nil {
return nil, nil, fmt.Errorf("invalid dest port: %w", err)
}
// Validate protocol matches IP version
if protocol == proxyProtoV1TCP4 && srcIP.To4() == nil {
return nil, nil, fmt.Errorf("%w: TCP4 with IPv6 address", errProxyProtoInvalid)
}
if protocol == proxyProtoV1TCP6 && srcIP.To4() != nil {
return nil, nil, fmt.Errorf("%w: TCP6 with IPv4 address", errProxyProtoInvalid)
}
if protocol != proxyProtoV1TCP4 && protocol != proxyProtoV1TCP6 {
return nil, nil, fmt.Errorf("%w: invalid protocol %s", errProxyProtoInvalid, protocol)
}
return &proxyProtoAddr{
srcIP: srcIP,
srcPort: uint16(srcPort),
dstIP: dstIP,
dstPort: uint16(dstPort),
}, remaining, nil
}
// readProxyProtoHeader reads and parses PROXY protocol (v1 or v2) from the connection.
// Automatically detects version and routes to appropriate parser.
// If the command is LOCAL/UNKNOWN (health check), it returns nil for addr and no error.
// If the command is PROXY, it returns the parsed address information.
// It also returns any bytes that were read past the v1 header terminator so the
// caller can replay them into the normal client parser.
// The connection must be fresh (no data read yet).
func readProxyProtoHeader(conn net.Conn) (*proxyProtoAddr, []byte, error) {
// Set read deadline to prevent hanging on slow/malicious clients
if err := conn.SetReadDeadline(time.Now().Add(proxyProtoReadTimeout)); err != nil {
return nil, nil, err
}
defer conn.SetReadDeadline(time.Time{})
// Detect version
version, firstBytes, err := detectProxyProtoVersion(conn)
if err != nil {
return nil, nil, err
}
switch version {
case 1:
// v1 parser expects "PROXY " prefix already consumed
return readProxyProtoV1Header(conn)
case 2:
// Read rest of v2 signature (bytes 6-11, total 6 more bytes)
remaining := make([]byte, 6)
if _, err := io.ReadFull(conn, remaining); err != nil {
return nil, nil, fmt.Errorf("failed to read v2 signature: %w", err)
}
// Verify full signature
fullSig := string(firstBytes) + string(remaining)
if fullSig != proxyProtoV2Sig {
return nil, nil, fmt.Errorf("%w: invalid signature", errProxyProtoInvalid)
}
// Read rest of header: ver/cmd, fam/proto, addr-len (4 bytes)
header := make([]byte, 4)
if _, err := io.ReadFull(conn, header); err != nil {
return nil, nil, fmt.Errorf("failed to read v2 header: %w", err)
}
// Continue with parsing
addr, err := parseProxyProtoV2Header(conn, header)
return addr, nil, err
default:
return nil, nil, fmt.Errorf("unsupported PROXY protocol version: %d", version)
}
}
// readProxyProtoV2Header is kept for backward compatibility and direct testing.
// It reads and parses a PROXY protocol v2 header from the connection.
// If the command is LOCAL (health check), it returns nil for addr and no error.
// If the command is PROXY, it returns the parsed address information.
// The connection must be fresh (no data read yet).
func readProxyProtoV2Header(conn net.Conn) (*proxyProtoAddr, error) {
// Set read deadline to prevent hanging on slow/malicious clients
if err := conn.SetReadDeadline(time.Now().Add(proxyProtoReadTimeout)); err != nil {
return nil, err
}
defer conn.SetReadDeadline(time.Time{})
// Read fixed header (16 bytes)
header := make([]byte, proxyProtoV2HeaderSize)
if _, err := io.ReadFull(conn, header); err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
return nil, errProxyProtoTimeout
}
return nil, fmt.Errorf("failed to read PROXY protocol header: %w", err)
}
// Validate signature (first 12 bytes)
if string(header[:12]) != proxyProtoV2Sig {
return nil, fmt.Errorf("%w: invalid signature", errProxyProtoInvalid)
}
// Continue with parsing after signature
return parseProxyProtoV2Header(conn, header[12:16])
}
// parseProxyProtoV2Header parses v2 protocol after signature has been validated.
// header contains the 4 bytes: ver/cmd, fam/proto, addr-len (2 bytes).
func parseProxyProtoV2Header(conn net.Conn, header []byte) (*proxyProtoAddr, error) {
// Parse version and command
verCmd := header[0]
version := verCmd & proxyProtoV2VerMask
command := verCmd & proxyProtoCmdMask
if version != proxyProtoV2Ver {
return nil, fmt.Errorf("%w: invalid version 0x%02x", errProxyProtoInvalid, version)
}
// Parse address family and protocol
famProto := header[1]
family := famProto & proxyProtoFamilyMask
protocol := famProto & proxyProtoProtoMask
// Parse address length (big-endian uint16)
addrLen := binary.BigEndian.Uint16(header[2:4])
// Handle LOCAL command (health check)
if command == proxyProtoCmdLocal {
// For LOCAL, we should skip the address data if any
if addrLen > 0 {
// Discard the address data
if _, err := io.CopyN(io.Discard, conn, int64(addrLen)); err != nil {
return nil, fmt.Errorf("failed to discard LOCAL command address data: %w", err)
}
}
return nil, nil // nil addr indicates LOCAL command
}
// Handle PROXY command
if command != proxyProtoCmdProxy {
return nil, fmt.Errorf("unknown PROXY protocol command: 0x%02x", command)
}
// Validate protocol (we only support STREAM/TCP)
if protocol != proxyProtoProtoStream {
return nil, fmt.Errorf("%w: only STREAM protocol supported", errProxyProtoUnsupported)
}
// Parse address data based on family
var addr *proxyProtoAddr
var err error
switch family {
case proxyProtoFamilyInet:
addr, err = parseIPv4Addr(conn, addrLen)
case proxyProtoFamilyInet6:
addr, err = parseIPv6Addr(conn, addrLen)
case proxyProtoFamilyUnspec:
// UNSPEC family with PROXY command is valid but rare
// Just skip the address data
if addrLen > 0 {
if _, err := io.CopyN(io.Discard, conn, int64(addrLen)); err != nil {
return nil, fmt.Errorf("failed to discard UNSPEC address address data: %w", err)
}
}
return nil, nil
default:
return nil, fmt.Errorf("%w: unsupported address family 0x%02x", errProxyProtoUnsupported, family)
}
return addr, err
}
// parseIPv4Addr parses IPv4 address data from PROXY protocol header
func parseIPv4Addr(conn net.Conn, addrLen uint16) (*proxyProtoAddr, error) {
// IPv4: 4 (src IP) + 4 (dst IP) + 2 (src port) + 2 (dst port) = 12 bytes minimum
if addrLen < proxyProtoAddrSizeIPv4 {
return nil, fmt.Errorf("IPv4 address data too short: %d bytes", addrLen)
}
addrData := make([]byte, addrLen)
if _, err := io.ReadFull(conn, addrData); err != nil {
return nil, fmt.Errorf("failed to read IPv4 address data: %w", err)
}
return &proxyProtoAddr{
srcIP: net.IP(addrData[0:4]),
dstIP: net.IP(addrData[4:8]),
srcPort: binary.BigEndian.Uint16(addrData[8:10]),
dstPort: binary.BigEndian.Uint16(addrData[10:12]),
}, nil
}
// parseIPv6Addr parses IPv6 address data from PROXY protocol header
func parseIPv6Addr(conn net.Conn, addrLen uint16) (*proxyProtoAddr, error) {
// IPv6: 16 (src IP) + 16 (dst IP) + 2 (src port) + 2 (dst port) = 36 bytes minimum
if addrLen < proxyProtoAddrSizeIPv6 {
return nil, fmt.Errorf("IPv6 address data too short: %d bytes", addrLen)
}
addrData := make([]byte, addrLen)
if _, err := io.ReadFull(conn, addrData); err != nil {
return nil, fmt.Errorf("failed to read IPv6 address data: %w", err)
}
return &proxyProtoAddr{
srcIP: net.IP(addrData[0:16]),
dstIP: net.IP(addrData[16:32]),
srcPort: binary.BigEndian.Uint16(addrData[32:34]),
dstPort: binary.BigEndian.Uint16(addrData[34:36]),
}, nil
}
+251
View File
@@ -0,0 +1,251 @@
// Copyright 2012-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 server
import (
"regexp"
"runtime/debug"
"time"
)
// Command is a signal used to control a running nats-server process.
type Command string
// Valid Command values.
const (
CommandStop = Command("stop")
CommandQuit = Command("quit")
CommandReopen = Command("reopen")
CommandReload = Command("reload")
// private for now
commandLDMode = Command("ldm")
commandTerm = Command("term")
)
var (
// gitCommit and serverVersion injected at build.
gitCommit, serverVersion string
// trustedKeys is a whitespace separated array of trusted operator's public nkeys.
trustedKeys string
// SemVer regexp to validate the VERSION.
semVerRe = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
)
// formatRevision formats a VCS revision string for display.
func formatRevision(revision string) string {
if len(revision) >= 7 {
return revision[:7]
}
return revision
}
func init() {
// Use build info if present, it would be if building using 'go build .'
// or when using a release.
if info, ok := debug.ReadBuildInfo(); ok {
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
gitCommit = formatRevision(setting.Value)
}
}
}
}
const (
// VERSION is the current version for the server.
VERSION = "2.14.0"
// PROTO is the currently supported protocol.
// 0 was the original
// 1 maintains proto 0, adds echo abilities for CONNECT from the client. Clients
// should not send echo unless proto in INFO is >= 1.
PROTO = 1
// DEFAULT_PORT is the default port for client connections.
DEFAULT_PORT = 4222
// RANDOM_PORT is the value for port that, when supplied, will cause the
// server to listen on a randomly-chosen available port. The resolved port
// is available via the Addr() method.
RANDOM_PORT = -1
// DEFAULT_HOST defaults to all interfaces.
DEFAULT_HOST = "0.0.0.0"
// MAX_CONTROL_LINE_SIZE is the maximum allowed protocol control line size.
// 4k should be plenty since payloads sans connect/info string are separate.
MAX_CONTROL_LINE_SIZE = 4096
// MAX_PAYLOAD_SIZE is the maximum allowed payload size. Should be using
// something different if > 1MB payloads are needed.
MAX_PAYLOAD_SIZE = (1024 * 1024)
// MAX_PAYLOAD_MAX_SIZE is the size at which the server will warn about
// max_payload being too high. In the future, the server may enforce/reject
// max_payload above this value.
MAX_PAYLOAD_MAX_SIZE = (8 * 1024 * 1024)
// MAX_PENDING_SIZE is the maximum outbound pending bytes per client.
MAX_PENDING_SIZE = (64 * 1024 * 1024)
// DEFAULT_MAX_CONNECTIONS is the default maximum connections allowed.
DEFAULT_MAX_CONNECTIONS = (64 * 1024)
// TLS_TIMEOUT is the TLS wait time.
TLS_TIMEOUT = 2 * time.Second
// DEFAULT_TLS_HANDSHAKE_FIRST_FALLBACK_DELAY is the default amount of
// time for the server to wait for the TLS handshake with a client to
// be initiated before falling back to sending the INFO protocol first.
// See TLSHandshakeFirst and TLSHandshakeFirstFallback options.
DEFAULT_TLS_HANDSHAKE_FIRST_FALLBACK_DELAY = 50 * time.Millisecond
// AUTH_TIMEOUT is the authorization wait time.
AUTH_TIMEOUT = 2 * time.Second
// DEFAULT_PING_INTERVAL is how often pings are sent to clients, etc...
DEFAULT_PING_INTERVAL = 2 * time.Minute
// DEFAULT_PING_MAX_OUT is maximum allowed pings outstanding before disconnect.
DEFAULT_PING_MAX_OUT = 2
// CR_LF string
CR_LF = "\r\n"
// LEN_CR_LF hold onto the computed size.
LEN_CR_LF = len(CR_LF)
// DEFAULT_FLUSH_DEADLINE is the write/flush deadlines.
DEFAULT_FLUSH_DEADLINE = 10 * time.Second
// DEFAULT_HTTP_PORT is the default monitoring port.
DEFAULT_HTTP_PORT = 8222
// DEFAULT_HTTP_BASE_PATH is the default base path for monitoring.
DEFAULT_HTTP_BASE_PATH = "/"
// ACCEPT_MIN_SLEEP is the minimum acceptable sleep times on temporary errors.
ACCEPT_MIN_SLEEP = 10 * time.Millisecond
// ACCEPT_MAX_SLEEP is the maximum acceptable sleep times on temporary errors
ACCEPT_MAX_SLEEP = 1 * time.Second
// DEFAULT_ROUTE_CONNECT Route solicitation intervals.
DEFAULT_ROUTE_CONNECT = 1 * time.Second
// DEFAULT_ROUTE_CONNECT_MAX Route solicitation intervals (max).
DEFAULT_ROUTE_CONNECT_MAX = 30 * time.Second
// DEFAULT_ROUTE_RECONNECT Route reconnect delay.
DEFAULT_ROUTE_RECONNECT = 1 * time.Second
// DEFAULT_ROUTE_DIAL Route dial timeout.
DEFAULT_ROUTE_DIAL = 1 * time.Second
// DEFAULT_ROUTE_POOL_SIZE Route default pool size
DEFAULT_ROUTE_POOL_SIZE = 3
// DEFAULT_LEAF_NODE_RECONNECT LeafNode reconnect interval.
DEFAULT_LEAF_NODE_RECONNECT = time.Second
// DEFAULT_LEAF_TLS_TIMEOUT TLS timeout for LeafNodes
DEFAULT_LEAF_TLS_TIMEOUT = 2 * time.Second
// PROTO_SNIPPET_SIZE is the default size of proto to print on parse errors.
PROTO_SNIPPET_SIZE = 32
// MAX_CONTROL_LINE_SNIPPET_SIZE is the default size of proto to print on max control line errors.
MAX_CONTROL_LINE_SNIPPET_SIZE = 128
// MAX_MSG_ARGS Maximum possible number of arguments from MSG proto.
MAX_MSG_ARGS = 4
// MAX_RMSG_ARGS Maximum possible number of arguments from RMSG proto.
MAX_RMSG_ARGS = 6
// MAX_HMSG_ARGS Maximum possible number of arguments from HMSG proto.
MAX_HMSG_ARGS = 7
// MAX_PUB_ARGS Maximum possible number of arguments from PUB proto.
MAX_PUB_ARGS = 3
// MAX_HPUB_ARGS Maximum possible number of arguments from HPUB proto.
MAX_HPUB_ARGS = 4
// MAX_RSUB_ARGS Maximum possible number of arguments from a RS+/LS+ proto.
MAX_RSUB_ARGS = 6
// DEFAULT_MAX_CLOSED_CLIENTS is the maximum number of closed connections we hold onto.
DEFAULT_MAX_CLOSED_CLIENTS = 10000
// DEFAULT_LAME_DUCK_DURATION is the time in which the server spreads
// the closing of clients when signaled to go in lame duck mode.
DEFAULT_LAME_DUCK_DURATION = 2 * time.Minute
// DEFAULT_LAME_DUCK_GRACE_PERIOD is the duration the server waits, after entering
// lame duck mode, before starting closing client connections.
DEFAULT_LAME_DUCK_GRACE_PERIOD = 10 * time.Second
// DEFAULT_LEAFNODE_INFO_WAIT Route dial timeout.
DEFAULT_LEAFNODE_INFO_WAIT = 1 * time.Second
// DEFAULT_LEAFNODE_PORT is the default port for remote leafnode connections.
DEFAULT_LEAFNODE_PORT = 7422
// DEFAULT_CONNECT_ERROR_REPORTS is the number of attempts at which a
// repeated failed route, gateway or leaf node connection is reported.
// This is used for initial connection, that is, when the server has
// never had a connection to the given endpoint. Once connected, and
// if a disconnect occurs, DEFAULT_RECONNECT_ERROR_REPORTS is used
// instead.
// The default is to report every 3600 attempts (roughly every hour).
DEFAULT_CONNECT_ERROR_REPORTS = 3600
// DEFAULT_RECONNECT_ERROR_REPORTS is the default number of failed
// attempt to reconnect a route, gateway or leaf node connection.
// The default is to report every attempt.
DEFAULT_RECONNECT_ERROR_REPORTS = 1
// DEFAULT_RTT_MEASUREMENT_INTERVAL is how often we want to measure RTT from
// this server to clients, routes, gateways or leafnode connections.
DEFAULT_RTT_MEASUREMENT_INTERVAL = time.Hour
// DEFAULT_ALLOW_RESPONSE_MAX_MSGS is the default number of responses allowed
// for a reply subject.
DEFAULT_ALLOW_RESPONSE_MAX_MSGS = 1
// DEFAULT_ALLOW_RESPONSE_EXPIRATION is the default time allowed for a given
// dynamic response permission.
DEFAULT_ALLOW_RESPONSE_EXPIRATION = 2 * time.Minute
// DEFAULT_SERVICE_EXPORT_RESPONSE_THRESHOLD is the default time that the system will
// expect a service export response to be delivered. This is used in corner cases for
// time based cleanup of reverse mapping structures.
DEFAULT_SERVICE_EXPORT_RESPONSE_THRESHOLD = 2 * time.Minute
// DEFAULT_SERVICE_LATENCY_SAMPLING is the default sampling rate for service
// latency metrics
DEFAULT_SERVICE_LATENCY_SAMPLING = 100
// DEFAULT_SYSTEM_ACCOUNT
DEFAULT_SYSTEM_ACCOUNT = "$SYS"
// DEFAULT GLOBAL_ACCOUNT
DEFAULT_GLOBAL_ACCOUNT = "$G"
// DEFAULT_FETCH_TIMEOUT is the default time that the system will wait for an account fetch to return.
DEFAULT_ACCOUNT_FETCH_TIMEOUT = 1900 * time.Millisecond
)
File diff suppressed because it is too large Load Diff
+327
View File
@@ -0,0 +1,327 @@
// Copyright 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.
//
// Based on code from https://github.com/robfig/cron
// Copyright (C) 2012 Rob Figueiredo
// All Rights Reserved.
//
// MIT LICENSE
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package server
import (
"errors"
"fmt"
"math"
"strconv"
"strings"
"time"
)
// parseCron parses the given cron pattern and returns the next time it will fire based on the provided ts.
func parseCron(pattern string, loc *time.Location, ts int64) (time.Time, error) {
fields := strings.Fields(pattern)
if len(fields) != 6 {
return time.Time{}, fmt.Errorf("pattern requires 6 fields, got %d", len(fields))
}
// If no time zone is passed, default to UTC.
if loc == nil {
loc = time.UTC
}
// Parse each field.
var err error
var second, minute, hour, dayOfMonth, month, dayOfWeek uint64
if second, err = getField(fields[0], seconds); err != nil {
return time.Time{}, err
}
if minute, err = getField(fields[1], minutes); err != nil {
return time.Time{}, err
}
if hour, err = getField(fields[2], hours); err != nil {
return time.Time{}, err
}
if dayOfMonth, err = getField(fields[3], dom); err != nil {
return time.Time{}, err
}
if month, err = getField(fields[4], months); err != nil {
return time.Time{}, err
}
if dayOfWeek, err = getField(fields[5], dow); err != nil {
return time.Time{}, err
}
// General approach
//
// For Month, Day, Hour, Minute, Second:
// Check if the time value matches. If yes, continue to the next field.
// If the field doesn't match the schedule, then increment the field until it matches.
// While incrementing the field, a wrap-around brings it back to the beginning
// of the field list (since it is necessary to re-verify previous field values)
next := time.Unix(0, ts).In(loc)
// Start at the earliest possible time (the upcoming second).
next = next.Truncate(time.Second).Add(time.Second)
// This flag indicates whether a field has been truncated at one point.
truncated := false
// If no time is found within five years, return error.
yearLimit := next.Year() + 5
WRAP:
if next.Year() > yearLimit {
return time.Time{}, errors.New("pattern exceeds maximum range")
}
for 1<<uint(next.Month())&month == 0 {
if !truncated {
truncated = true
next = time.Date(next.Year(), next.Month(), 1, 0, 0, 0, 0, loc)
}
if next = next.AddDate(0, 1, 0); next.Month() == time.January {
goto WRAP
}
}
for !dayMatches(dayOfMonth, dayOfWeek, next) {
if !truncated {
truncated = true
next = time.Date(next.Year(), next.Month(), next.Day(), 0, 0, 0, 0, loc)
}
if next = next.AddDate(0, 0, 1); next.Day() == 1 {
goto WRAP
}
}
for 1<<uint(next.Hour())&hour == 0 {
if !truncated {
truncated = true
next = time.Date(next.Year(), next.Month(), next.Day(), next.Hour(), 0, 0, 0, loc)
}
if next = next.Add(time.Hour); next.Hour() == 0 {
goto WRAP
}
}
for 1<<uint(next.Minute())&minute == 0 {
if !truncated {
truncated = true
next = next.Truncate(time.Minute)
}
if next = next.Add(time.Minute); next.Minute() == 0 {
goto WRAP
}
}
for 1<<uint(next.Second())&second == 0 {
if !truncated {
truncated = true
next = next.Truncate(time.Second)
}
if next = next.Add(time.Second); next.Second() == 0 {
goto WRAP
}
}
return next, nil
}
// getField returns an Int with the bits set representing all of the times that
// the field represents or error parsing field value. A "field" is a comma-separated
// list of "ranges".
func getField(field string, r bounds) (uint64, error) {
var bits uint64
ranges := strings.FieldsFuncSeq(field, func(r rune) bool { return r == ',' })
for expr := range ranges {
bit, err := getRange(expr, r)
if err != nil {
return bits, err
}
bits |= bit
}
return bits, nil
}
// getRange returns the bits indicated by the given expression: number | number [ "-" number ] [ "/" number ]
// or error parsing range.
func getRange(expr string, r bounds) (uint64, error) {
var (
start, end, step uint
rangeAndStep = strings.Split(expr, "/")
lowAndHigh = strings.Split(rangeAndStep[0], "-")
singleDigit = len(lowAndHigh) == 1
err error
)
var extra uint64
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
start = r.min
end = r.max
extra = starBit
} else {
start, err = parseIntOrName(lowAndHigh[0], r.names)
if err != nil {
return 0, err
}
switch len(lowAndHigh) {
case 1:
end = start
case 2:
end, err = parseIntOrName(lowAndHigh[1], r.names)
if err != nil {
return 0, err
}
default:
return 0, fmt.Errorf("too many hyphens: %s", expr)
}
}
switch len(rangeAndStep) {
case 1:
step = 1
case 2:
step, err = mustParseInt(rangeAndStep[1])
if err != nil {
return 0, err
}
// Special handling: "N/step" means "N-max/step".
if singleDigit {
end = r.max
}
if step > 1 {
extra = 0
}
default:
return 0, fmt.Errorf("too many slashes: %s", expr)
}
if start < r.min {
return 0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
}
if end > r.max {
return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr)
}
if start > end {
return 0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
}
if step == 0 {
return 0, fmt.Errorf("step of range should be a positive number: %s", expr)
}
return getBits(start, end, step) | extra, nil
}
// parseIntOrName returns the (possibly-named) integer contained in expr.
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
if names != nil {
if namedInt, ok := names[strings.ToLower(expr)]; ok {
return namedInt, nil
}
}
return mustParseInt(expr)
}
// mustParseInt parses the given expression as an int or returns an error.
func mustParseInt(expr string) (uint, error) {
num, err := strconv.Atoi(expr)
if err != nil {
return 0, fmt.Errorf("failed to parse int from %s: %s", expr, err)
}
if num < 0 {
return 0, fmt.Errorf("negative number (%d) not allowed: %s", num, expr)
}
return uint(num), nil
}
// getBits sets all bits in the range [min, max], modulo the given step size.
func getBits(min, max, step uint) uint64 {
var bits uint64
// If step is 1, use shifts.
if step == 1 {
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
}
// Else, use a simple loop.
for i := min; i <= max; i += step {
bits |= 1 << i
}
return bits
}
// bounds provides a range of acceptable values (plus a map of name to value).
type bounds struct {
min, max uint
names map[string]uint
}
// The bounds for each field.
var (
seconds = bounds{0, 59, nil}
minutes = bounds{0, 59, nil}
hours = bounds{0, 23, nil}
dom = bounds{1, 31, nil}
months = bounds{1, 12, map[string]uint{
"jan": 1,
"feb": 2,
"mar": 3,
"apr": 4,
"may": 5,
"jun": 6,
"jul": 7,
"aug": 8,
"sep": 9,
"oct": 10,
"nov": 11,
"dec": 12,
}}
dow = bounds{0, 6, map[string]uint{
"sun": 0,
"mon": 1,
"tue": 2,
"wed": 3,
"thu": 4,
"fri": 5,
"sat": 6,
}}
)
const (
// Set the top bit if a star was included in the expression.
starBit = 1 << 63
)
// dayMatches returns true if the schedule's day-of-week and day-of-month
// restrictions are satisfied by the given time.
func dayMatches(dayOfMonth, dayOfWeek uint64, t time.Time) bool {
var (
domMatch = 1<<uint(t.Day())&dayOfMonth > 0
dowMatch = 1<<uint(t.Weekday())&dayOfWeek > 0
)
if dayOfMonth&starBit > 0 || dayOfWeek&starBit > 0 {
return domMatch && dowMatch
}
return domMatch || dowMatch
}
+724
View File
@@ -0,0 +1,724 @@
// Copyright 2012-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 server
import (
"bytes"
"container/heap"
"container/list"
"crypto/sha256"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/nats-io/nkeys"
"github.com/nats-io/jwt/v2" // only used to decode, not for storage
)
const (
fileExtension = ".jwt"
)
// validatePathExists checks that the provided path exists and is a dir if requested
func validatePathExists(path string, dir bool) (string, error) {
if path == _EMPTY_ {
return _EMPTY_, errors.New("path is not specified")
}
abs, err := filepath.Abs(path)
if err != nil {
return _EMPTY_, fmt.Errorf("error parsing path [%s]: %v", abs, err)
}
var finfo os.FileInfo
if finfo, err = os.Stat(abs); os.IsNotExist(err) {
return _EMPTY_, fmt.Errorf("the path [%s] doesn't exist", abs)
}
mode := finfo.Mode()
if dir && mode.IsRegular() {
return _EMPTY_, fmt.Errorf("the path [%s] is not a directory", abs)
}
if !dir && mode.IsDir() {
return _EMPTY_, fmt.Errorf("the path [%s] is not a file", abs)
}
return abs, nil
}
// ValidateDirPath checks that the provided path exists and is a dir
func validateDirPath(path string) (string, error) {
return validatePathExists(path, true)
}
// JWTChanged functions are called when the store file watcher notices a JWT changed
type JWTChanged func(publicKey string)
// DirJWTStore implements the JWT Store interface, keeping JWTs in an optionally sharded
// directory structure
type DirJWTStore struct {
sync.Mutex
directory string
shard bool
readonly bool
deleteType deleteType
operator map[string]struct{}
expiration *expirationTracker
changed JWTChanged
deleted JWTChanged
}
func newDir(dirPath string, create bool) (string, error) {
fullPath, err := validateDirPath(dirPath)
if err != nil {
if !create {
return _EMPTY_, err
}
if err = os.MkdirAll(dirPath, defaultDirPerms); err != nil {
return _EMPTY_, err
}
if fullPath, err = validateDirPath(dirPath); err != nil {
return _EMPTY_, err
}
}
return fullPath, nil
}
// future proofing in case new options will be added
type dirJWTStoreOption any
// Creates a directory based jwt store.
// Reads files only, does NOT watch directories and files.
func NewImmutableDirJWTStore(dirPath string, shard bool, _ ...dirJWTStoreOption) (*DirJWTStore, error) {
theStore, err := NewDirJWTStore(dirPath, shard, false, nil)
if err != nil {
return nil, err
}
theStore.readonly = true
return theStore, nil
}
// Creates a directory based jwt store.
// Operates on files only, does NOT watch directories and files.
func NewDirJWTStore(dirPath string, shard bool, create bool, _ ...dirJWTStoreOption) (*DirJWTStore, error) {
fullPath, err := newDir(dirPath, create)
if err != nil {
return nil, err
}
theStore := &DirJWTStore{
directory: fullPath,
shard: shard,
}
return theStore, nil
}
type deleteType int
const (
NoDelete deleteType = iota
RenameDeleted
HardDelete
)
// Creates a directory based jwt store.
//
// When ttl is set deletion of file is based on it and not on the jwt expiration
// To completely disable expiration (including expiration in jwt) set ttl to max duration time.Duration(math.MaxInt64)
//
// limit defines how many files are allowed at any given time. Set to math.MaxInt64 to disable.
// evictOnLimit determines the behavior once limit is reached.
// * true - Evict based on lru strategy
// * false - return an error
func NewExpiringDirJWTStore(dirPath string, shard bool, create bool, delete deleteType, expireCheck time.Duration, limit int64,
evictOnLimit bool, ttl time.Duration, changeNotification JWTChanged, _ ...dirJWTStoreOption) (*DirJWTStore, error) {
fullPath, err := newDir(dirPath, create)
if err != nil {
return nil, err
}
theStore := &DirJWTStore{
directory: fullPath,
shard: shard,
deleteType: delete,
changed: changeNotification,
}
if expireCheck <= 0 {
if ttl != 0 {
expireCheck = ttl / 2
}
if expireCheck == 0 || expireCheck > time.Minute {
expireCheck = time.Minute
}
}
if limit <= 0 {
limit = math.MaxInt64
}
theStore.startExpiring(expireCheck, limit, evictOnLimit, ttl)
theStore.Lock()
err = filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if strings.HasSuffix(path, fileExtension) {
if theJwt, err := os.ReadFile(path); err == nil {
hash := sha256.Sum256(theJwt)
_, file := filepath.Split(path)
theStore.expiration.track(strings.TrimSuffix(file, fileExtension), &hash, string(theJwt))
}
}
return nil
})
theStore.Unlock()
if err != nil {
theStore.Close()
return nil, err
}
return theStore, err
}
func (store *DirJWTStore) IsReadOnly() bool {
return store.readonly
}
func (store *DirJWTStore) LoadAcc(publicKey string) (string, error) {
return store.load(publicKey)
}
func (store *DirJWTStore) SaveAcc(publicKey string, theJWT string) error {
return store.save(publicKey, theJWT)
}
func (store *DirJWTStore) LoadAct(hash string) (string, error) {
return store.load(hash)
}
func (store *DirJWTStore) SaveAct(hash string, theJWT string) error {
return store.save(hash, theJWT)
}
func (store *DirJWTStore) Close() {
store.Lock()
defer store.Unlock()
if store.expiration != nil {
store.expiration.close()
store.expiration = nil
}
}
// Pack up to maxJWTs into a package
func (store *DirJWTStore) Pack(maxJWTs int) (string, error) {
count := 0
var pack []string
if maxJWTs > 0 {
pack = make([]string, 0, maxJWTs)
} else {
pack = []string{}
}
store.Lock()
err := filepath.Walk(store.directory, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && strings.HasSuffix(path, fileExtension) { // this is a JWT
if count == maxJWTs { // won't match negative
return nil
}
pubKey := strings.TrimSuffix(filepath.Base(path), fileExtension)
if store.expiration != nil {
if _, ok := store.expiration.idx[pubKey]; !ok {
return nil // only include indexed files
}
}
jwtBytes, err := os.ReadFile(path)
if err != nil {
return err
}
if store.expiration != nil {
claim, err := jwt.DecodeGeneric(string(jwtBytes))
if err == nil && claim.Expires > 0 && claim.Expires < time.Now().Unix() {
return nil
}
}
pack = append(pack, fmt.Sprintf("%s|%s", pubKey, string(jwtBytes)))
count++
}
return nil
})
store.Unlock()
if err != nil {
return _EMPTY_, err
} else {
return strings.Join(pack, "\n"), nil
}
}
// Pack up to maxJWTs into a message and invoke callback with it
func (store *DirJWTStore) PackWalk(maxJWTs int, cb func(partialPackMsg string)) error {
if maxJWTs <= 0 || cb == nil {
return errors.New("bad arguments to PackWalk")
}
var packMsg []string
store.Lock()
dir := store.directory
exp := store.expiration
store.Unlock()
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if info != nil && !info.IsDir() && strings.HasSuffix(path, fileExtension) { // this is a JWT
pubKey := strings.TrimSuffix(filepath.Base(path), fileExtension)
store.Lock()
if exp != nil {
if _, ok := exp.idx[pubKey]; !ok {
store.Unlock()
return nil // only include indexed files
}
}
store.Unlock()
jwtBytes, err := os.ReadFile(path)
if err != nil {
return err
}
if len(jwtBytes) == 0 {
// Skip if no contents in the JWT.
return nil
}
if exp != nil {
claim, err := jwt.DecodeGeneric(string(jwtBytes))
if err == nil && claim.Expires > 0 && claim.Expires < time.Now().Unix() {
return nil
}
}
packMsg = append(packMsg, fmt.Sprintf("%s|%s", pubKey, string(jwtBytes)))
if len(packMsg) == maxJWTs { // won't match negative
cb(strings.Join(packMsg, "\n"))
packMsg = nil
}
}
return nil
})
if packMsg != nil {
cb(strings.Join(packMsg, "\n"))
}
return err
}
// Merge takes the JWTs from package and adds them to the store
// Merge is destructive in the sense that it doesn't check if the JWT
// is newer or anything like that.
func (store *DirJWTStore) Merge(pack string) error {
newJWTs := strings.Split(pack, "\n")
for _, line := range newJWTs {
if line == _EMPTY_ { // ignore blank lines
continue
}
split := strings.Split(line, "|")
if len(split) != 2 {
return fmt.Errorf("line in package didn't contain 2 entries: %q", line)
}
pubKey := split[0]
if !nkeys.IsValidPublicAccountKey(pubKey) {
return fmt.Errorf("key to merge is not a valid public account key")
}
if err := store.saveIfNewer(pubKey, split[1]); err != nil {
return err
}
}
return nil
}
func (store *DirJWTStore) Reload() error {
store.Lock()
exp := store.expiration
if exp == nil || store.readonly {
store.Unlock()
return nil
}
idx := exp.idx
changed := store.changed
isCache := store.expiration.evictOnLimit
// clear out indexing data structures
exp.heap = make([]*jwtItem, 0, len(exp.heap))
exp.idx = make(map[string]*list.Element)
exp.lru = list.New()
exp.hash = [sha256.Size]byte{}
store.Unlock()
return filepath.Walk(store.directory, func(path string, info os.FileInfo, err error) error {
if strings.HasSuffix(path, fileExtension) {
if theJwt, err := os.ReadFile(path); err == nil {
hash := sha256.Sum256(theJwt)
_, file := filepath.Split(path)
pkey := strings.TrimSuffix(file, fileExtension)
notify := isCache // for cache, issue cb even when file not present (may have been evicted)
if i, ok := idx[pkey]; ok {
notify = !bytes.Equal(i.Value.(*jwtItem).hash[:], hash[:])
}
store.Lock()
exp.track(pkey, &hash, string(theJwt))
store.Unlock()
if notify && changed != nil {
changed(pkey)
}
}
}
return nil
})
}
func (store *DirJWTStore) pathForKey(publicKey string) string {
if len(publicKey) < 2 {
return _EMPTY_
}
if !nkeys.IsValidPublicKey(publicKey) {
return _EMPTY_
}
fileName := fmt.Sprintf("%s%s", publicKey, fileExtension)
if store.shard {
last := publicKey[len(publicKey)-2:]
return filepath.Join(store.directory, last, fileName)
} else {
return filepath.Join(store.directory, fileName)
}
}
// Load checks the memory store and returns the matching JWT or an error
// Assumes lock is NOT held
func (store *DirJWTStore) load(publicKey string) (string, error) {
store.Lock()
defer store.Unlock()
if path := store.pathForKey(publicKey); path == _EMPTY_ {
return _EMPTY_, fmt.Errorf("invalid public key")
} else if data, err := os.ReadFile(path); err != nil {
return _EMPTY_, err
} else {
if store.expiration != nil {
store.expiration.updateTrack(publicKey)
}
return string(data), nil
}
}
// write that keeps hash of all jwt in sync
// Assumes the lock is held. Does return true or an error never both.
func (store *DirJWTStore) write(path string, publicKey string, theJWT string) (bool, error) {
if len(theJWT) == 0 {
return false, fmt.Errorf("invalid JWT")
}
var newHash *[sha256.Size]byte
if store.expiration != nil {
h := sha256.Sum256([]byte(theJWT))
newHash = &h
if v, ok := store.expiration.idx[publicKey]; ok {
store.expiration.updateTrack(publicKey)
// this write is an update, move to back
it := v.Value.(*jwtItem)
oldHash := it.hash[:]
if bytes.Equal(oldHash, newHash[:]) {
return false, nil
}
} else if int64(store.expiration.Len()) >= store.expiration.limit {
if !store.expiration.evictOnLimit {
return false, errors.New("jwt store is full")
}
// this write is an add, pick the least recently used value for removal
i := store.expiration.lru.Front().Value.(*jwtItem)
if err := os.Remove(store.pathForKey(i.publicKey)); err != nil {
return false, err
} else {
store.expiration.unTrack(i.publicKey)
}
}
}
if err := os.WriteFile(path, []byte(theJWT), defaultFilePerms); err != nil {
return false, err
} else if store.expiration != nil {
store.expiration.track(publicKey, newHash, theJWT)
}
return true, nil
}
func (store *DirJWTStore) delete(publicKey string) error {
if store.readonly {
return fmt.Errorf("store is read-only")
} else if store.deleteType == NoDelete {
return fmt.Errorf("store is not set up to for delete")
}
store.Lock()
defer store.Unlock()
name := store.pathForKey(publicKey)
if store.deleteType == RenameDeleted {
if err := os.Rename(name, name+".deleted"); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
} else if err := os.Remove(name); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
store.expiration.unTrack(publicKey)
store.deleted(publicKey)
return nil
}
// Save puts the JWT in a map by public key and performs update callbacks
// Assumes lock is NOT held
func (store *DirJWTStore) save(publicKey string, theJWT string) error {
if store.readonly {
return fmt.Errorf("store is read-only")
}
store.Lock()
path := store.pathForKey(publicKey)
if path == _EMPTY_ {
store.Unlock()
return fmt.Errorf("invalid public key")
}
dirPath := filepath.Dir(path)
if _, err := validateDirPath(dirPath); err != nil {
if err := os.MkdirAll(dirPath, defaultDirPerms); err != nil {
store.Unlock()
return err
}
}
changed, err := store.write(path, publicKey, theJWT)
cb := store.changed
store.Unlock()
if changed && cb != nil {
cb(publicKey)
}
return err
}
// Assumes the lock is NOT held, and only updates if the jwt is new, or the one on disk is older
// When changed, invokes jwt changed callback
func (store *DirJWTStore) saveIfNewer(publicKey string, theJWT string) error {
if store.readonly {
return fmt.Errorf("store is read-only")
}
path := store.pathForKey(publicKey)
if path == _EMPTY_ {
return fmt.Errorf("invalid public key")
}
dirPath := filepath.Dir(path)
if _, err := validateDirPath(dirPath); err != nil {
if err := os.MkdirAll(dirPath, defaultDirPerms); err != nil {
return err
}
}
if _, err := os.Stat(path); err == nil {
if newJWT, err := jwt.DecodeGeneric(theJWT); err != nil {
return err
} else if existing, err := os.ReadFile(path); err != nil {
return err
} else if existingJWT, err := jwt.DecodeGeneric(string(existing)); err != nil {
// skip if it can't be decoded
} else if existingJWT.ID == newJWT.ID {
return nil
} else if existingJWT.IssuedAt > newJWT.IssuedAt {
return nil
} else if newJWT.Subject != publicKey {
return fmt.Errorf("jwt subject nkey and provided nkey do not match")
} else if existingJWT.Subject != newJWT.Subject {
return fmt.Errorf("subject of existing and new jwt do not match")
}
}
store.Lock()
cb := store.changed
changed, err := store.write(path, publicKey, theJWT)
store.Unlock()
if err != nil {
return err
} else if changed && cb != nil {
cb(publicKey)
}
return nil
}
func xorAssign(lVal *[sha256.Size]byte, rVal [sha256.Size]byte) {
for i := range rVal {
(*lVal)[i] ^= rVal[i]
}
}
// returns a hash representing all indexed jwt
func (store *DirJWTStore) Hash() [sha256.Size]byte {
store.Lock()
defer store.Unlock()
if store.expiration == nil {
return [sha256.Size]byte{}
} else {
return store.expiration.hash
}
}
// An jwtItem is something managed by the priority queue
type jwtItem struct {
index int
publicKey string
expiration int64 // consists of unix time of expiration (ttl when set or jwt expiration) in seconds
hash [sha256.Size]byte
}
// A expirationTracker implements heap.Interface and holds Items.
type expirationTracker struct {
heap []*jwtItem // sorted by jwtItem.expiration
idx map[string]*list.Element
lru *list.List // keep which jwt are least used
limit int64 // limit how many jwt are being tracked
evictOnLimit bool // when limit is hit, error or evict using lru
ttl time.Duration
hash [sha256.Size]byte // xor of all jwtItem.hash in idx
quit chan struct{}
wg sync.WaitGroup
}
func (q *expirationTracker) Len() int { return len(q.heap) }
func (q *expirationTracker) Less(i, j int) bool {
pq := q.heap
return pq[i].expiration < pq[j].expiration
}
func (q *expirationTracker) Swap(i, j int) {
pq := q.heap
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
func (q *expirationTracker) Push(x any) {
n := len(q.heap)
item := x.(*jwtItem)
item.index = n
q.heap = append(q.heap, item)
q.idx[item.publicKey] = q.lru.PushBack(item)
}
func (q *expirationTracker) Pop() any {
old := q.heap
n := len(old)
item := old[n-1]
old[n-1] = nil // avoid memory leak
item.index = -1
q.heap = old[0 : n-1]
q.lru.Remove(q.idx[item.publicKey])
delete(q.idx, item.publicKey)
return item
}
func (pq *expirationTracker) updateTrack(publicKey string) {
if e, ok := pq.idx[publicKey]; ok {
i := e.Value.(*jwtItem)
if pq.ttl != 0 {
// only update expiration when set
i.expiration = time.Now().Add(pq.ttl).UnixNano()
heap.Fix(pq, i.index)
}
if pq.evictOnLimit {
pq.lru.MoveToBack(e)
}
}
}
func (pq *expirationTracker) unTrack(publicKey string) {
if it, ok := pq.idx[publicKey]; ok {
xorAssign(&pq.hash, it.Value.(*jwtItem).hash)
heap.Remove(pq, it.Value.(*jwtItem).index)
delete(pq.idx, publicKey)
}
}
func (pq *expirationTracker) track(publicKey string, hash *[sha256.Size]byte, theJWT string) {
var exp int64
// prioritize ttl over expiration
if pq.ttl != 0 {
if pq.ttl == time.Duration(math.MaxInt64) {
exp = math.MaxInt64
} else {
exp = time.Now().Add(pq.ttl).UnixNano()
}
} else {
if g, err := jwt.DecodeGeneric(theJWT); err == nil {
exp = time.Unix(g.Expires, 0).UnixNano()
}
if exp == 0 {
exp = math.MaxInt64 // default to indefinite
}
}
if e, ok := pq.idx[publicKey]; ok {
i := e.Value.(*jwtItem)
xorAssign(&pq.hash, i.hash) // remove old hash
i.expiration = exp
i.hash = *hash
heap.Fix(pq, i.index)
} else {
heap.Push(pq, &jwtItem{-1, publicKey, exp, *hash})
}
xorAssign(&pq.hash, *hash) // add in new hash
}
func (pq *expirationTracker) close() {
if pq == nil || pq.quit == nil {
return
}
close(pq.quit)
pq.quit = nil
}
func (store *DirJWTStore) startExpiring(reCheck time.Duration, limit int64, evictOnLimit bool, ttl time.Duration) {
store.Lock()
defer store.Unlock()
quit := make(chan struct{})
pq := &expirationTracker{
make([]*jwtItem, 0, 10),
make(map[string]*list.Element),
list.New(),
limit,
evictOnLimit,
ttl,
[sha256.Size]byte{},
quit,
sync.WaitGroup{},
}
store.expiration = pq
pq.wg.Add(1)
go func() {
t := time.NewTicker(reCheck)
defer t.Stop()
defer pq.wg.Done()
for {
now := time.Now().UnixNano()
store.Lock()
if pq.Len() > 0 {
if it := pq.heap[0]; it.expiration <= now {
path := store.pathForKey(it.publicKey)
if err := os.Remove(path); err == nil {
heap.Pop(pq)
pq.unTrack(it.publicKey)
xorAssign(&pq.hash, it.hash)
store.Unlock()
continue // we removed an entry, check next one right away
}
}
}
store.Unlock()
select {
case <-t.C:
case <-quit:
return
}
}
}()
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2020-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.
//go:build !windows && !openbsd && !netbsd && !wasm && !illumos && !solaris
package server
import (
"os"
"syscall"
)
func diskAvailable(storeDir string) int64 {
var ba int64
if _, err := os.Stat(storeDir); os.IsNotExist(err) {
os.MkdirAll(storeDir, defaultDirPerms)
}
var fs syscall.Statfs_t
if err := syscall.Statfs(storeDir, &fs); err == nil {
// Estimate 75% of available storage.
ba = int64(uint64(fs.Bavail) * uint64(fs.Bsize) / 4 * 3)
} else {
// Used 1TB default as a guess if all else fails.
ba = JetStreamMaxStoreDefault
}
return ba
}
@@ -0,0 +1,21 @@
// Copyright 2022-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.
//go:build netbsd
package server
// TODO - See if there is a version of this for NetBSD.
func diskAvailable(storeDir string) int64 {
return JetStreamMaxStoreDefault
}
@@ -0,0 +1,37 @@
// Copyright 2021-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.
//go:build openbsd
package server
import (
"os"
"syscall"
)
func diskAvailable(storeDir string) int64 {
var ba int64
if _, err := os.Stat(storeDir); os.IsNotExist(err) {
os.MkdirAll(storeDir, defaultDirPerms)
}
var fs syscall.Statfs_t
if err := syscall.Statfs(storeDir, &fs); err == nil {
// Estimate 75% of available storage.
ba = int64(uint64(fs.F_bavail) * uint64(fs.F_bsize) / 4 * 3)
} else {
// Used 1TB default as a guess if all else fails.
ba = JetStreamMaxStoreDefault
}
return ba
}
@@ -0,0 +1,38 @@
// Copyright 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.
//go:build illumos || solaris
package server
import (
"os"
"golang.org/x/sys/unix"
)
func diskAvailable(storeDir string) int64 {
var ba int64
if _, err := os.Stat(storeDir); os.IsNotExist(err) {
os.MkdirAll(storeDir, defaultDirPerms)
}
var fs unix.Statvfs_t
if err := unix.Statvfs(storeDir, &fs); err == nil {
// Estimate 75% of available storage.
ba = int64(uint64(fs.Frsize) * uint64(fs.Bavail) / 4 * 3)
} else {
// Used 1TB default as a guess if all else fails.
ba = JetStreamMaxStoreDefault
}
return ba
}
@@ -0,0 +1,20 @@
// Copyright 2022-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.
//go:build wasm
package server
func diskAvailable(storeDir string) int64 {
return JetStreamMaxStoreDefault
}
@@ -0,0 +1,21 @@
// Copyright 2020-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.
//go:build windows
package server
// TODO(dlc) - See if there is a version of this for windows.
func diskAvailable(storeDir string) int64 {
return JetStreamMaxStoreDefault
}
@@ -0,0 +1,60 @@
// Copyright 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 elastic
import (
"weak"
)
func Make[T any](ptr *T) *Pointer[T] {
return &Pointer[T]{
weak: weak.Make(ptr),
}
}
type Pointer[T any] struct {
weak weak.Pointer[T]
strong *T
}
func (e *Pointer[T]) Set(ptr *T) {
e.weak = weak.Make(ptr)
if e.strong != nil {
e.strong = ptr
}
}
func (e *Pointer[T]) Strengthen() {
if e == nil || e.strong != nil {
return
}
e.strong = e.weak.Value()
}
func (e *Pointer[T]) Weaken() {
if e == nil || e.strong == nil {
return
}
e.strong = nil
}
func (e *Pointer[T]) Value() *T {
if e == nil {
return nil
}
if e.strong != nil {
return e.strong
}
return e.weak.Value()
}
+410
View File
@@ -0,0 +1,410 @@
// Copyright 2012-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 server
import (
"errors"
"fmt"
)
var (
// ErrConnectionClosed represents an error condition on a closed connection.
ErrConnectionClosed = errors.New("connection closed")
// ErrAuthentication represents an error condition on failed authentication.
ErrAuthentication = errors.New("authentication error")
// ErrAuthTimeout represents an error condition on failed authorization due to timeout.
ErrAuthTimeout = errors.New("authentication timeout")
// ErrAuthExpired represents an expired authorization due to timeout.
ErrAuthExpired = errors.New("authentication expired")
// ErrAuthProxyNotTrusted represents an error condition on failed authentication
// due to a connection from a proxy not in the list of trusted proxies.
ErrAuthProxyNotTrusted = errors.New("proxy is not trusted")
// ErrAuthProxyRequired represents an error condition on failed authentication
// due to a connection not coming from a proxy.
ErrAuthProxyRequired = errors.New("proxy connection required")
// ErrMaxPayload represents an error condition when the payload is too big.
ErrMaxPayload = errors.New("maximum payload exceeded")
// ErrMaxControlLine represents an error condition when the control line is too big.
ErrMaxControlLine = errors.New("maximum control line exceeded")
// ErrReservedPublishSubject represents an error condition when sending to a reserved subject, e.g. _SYS.>
ErrReservedPublishSubject = errors.New("reserved internal subject")
// ErrBadPublishSubject represents an error condition for an invalid publish subject.
ErrBadPublishSubject = errors.New("invalid publish subject")
// ErrBadSubject represents an error condition for an invalid subject.
ErrBadSubject = errors.New("invalid subject")
// ErrBadQualifier is used to error on a bad qualifier for a transform.
ErrBadQualifier = errors.New("bad qualifier")
// ErrBadClientProtocol signals a client requested an invalid client protocol.
ErrBadClientProtocol = errors.New("invalid client protocol")
// ErrTooManyConnections signals a client that the maximum number of connections supported by the
// server has been reached.
ErrTooManyConnections = errors.New("maximum connections exceeded")
// ErrTooManyAccountConnections signals that an account has reached its maximum number of active
// connections.
ErrTooManyAccountConnections = errors.New("maximum account active connections exceeded")
// ErrLeafNodeLoop signals a leafnode is trying to register for a cluster we already have registered.
ErrLeafNodeLoop = errors.New("leafnode loop detected")
// ErrTooManySubs signals a client that the maximum number of subscriptions per connection
// has been reached.
ErrTooManySubs = errors.New("maximum subscriptions exceeded")
// ErrTooManySubTokens signals a client that the subject has too many tokens.
ErrTooManySubTokens = errors.New("subject has exceeded number of tokens limit")
// ErrClientConnectedToRoutePort represents an error condition when a client
// attempted to connect to the route listen port.
ErrClientConnectedToRoutePort = errors.New("attempted to connect to route port")
// ErrClientConnectedToLeafNodePort represents an error condition when a client
// attempted to connect to the leaf node listen port.
ErrClientConnectedToLeafNodePort = errors.New("attempted to connect to leaf node port")
// ErrLeafNodeHasSameClusterName represents an error condition when a leafnode is a cluster
// and it has the same cluster name as the hub cluster.
ErrLeafNodeHasSameClusterName = errors.New("remote leafnode has same cluster name")
// ErrLeafNodeDisabled is when we disable leafnodes.
ErrLeafNodeDisabled = errors.New("leafnodes disabled")
// ErrConnectedToWrongPort represents an error condition when a connection is attempted
// to the wrong listen port (for instance a LeafNode to a client port, etc...)
ErrConnectedToWrongPort = errors.New("attempted to connect to wrong port")
// ErrAccountExists is returned when an account is attempted to be registered
// but already exists.
ErrAccountExists = errors.New("account exists")
// ErrBadAccount represents a malformed or incorrect account.
ErrBadAccount = errors.New("bad account")
// ErrReservedAccount represents a reserved account that can not be created.
ErrReservedAccount = errors.New("reserved account")
// ErrMissingAccount is returned when an account does not exist.
ErrMissingAccount = errors.New("account missing")
// ErrMissingService is returned when an account does not have an exported service.
ErrMissingService = errors.New("service missing")
// ErrBadServiceType is returned when latency tracking is being applied to non-singleton response types.
ErrBadServiceType = errors.New("bad service response type")
// ErrBadSampling is returned when the sampling for latency tracking is not 1 >= sample <= 100.
ErrBadSampling = errors.New("bad sampling percentage, should be 1-100")
// ErrAccountValidation is returned when an account has failed validation.
ErrAccountValidation = errors.New("account validation failed")
// ErrAccountExpired is returned when an account has expired.
ErrAccountExpired = errors.New("account expired")
// ErrNoAccountResolver is returned when we attempt an update but do not have an account resolver.
ErrNoAccountResolver = errors.New("account resolver missing")
// ErrAccountResolverUpdateTooSoon is returned when we attempt an update too soon to last request.
ErrAccountResolverUpdateTooSoon = errors.New("account resolver update too soon")
// ErrAccountResolverSameClaims is returned when same claims have been fetched.
ErrAccountResolverSameClaims = errors.New("account resolver no new claims")
// ErrStreamImportAuthorization is returned when a stream import is not authorized.
ErrStreamImportAuthorization = errors.New("stream import not authorized")
// ErrStreamImportBadPrefix is returned when a stream import prefix contains wildcards.
ErrStreamImportBadPrefix = errors.New("stream import prefix can not contain wildcard tokens")
// ErrStreamImportDuplicate is returned when a stream import is a duplicate of one that already exists.
ErrStreamImportDuplicate = errors.New("stream import already exists")
// ErrServiceImportAuthorization is returned when a service import is not authorized.
ErrServiceImportAuthorization = errors.New("service import not authorized")
// ErrImportFormsCycle is returned when an import would form a cycle.
ErrImportFormsCycle = errors.New("import forms a cycle")
// ErrCycleSearchDepth is returned when we have exceeded our maximum search depth..
ErrCycleSearchDepth = errors.New("search cycle depth exhausted")
// ErrClientOrRouteConnectedToGatewayPort represents an error condition when
// a client or route attempted to connect to the Gateway port.
ErrClientOrRouteConnectedToGatewayPort = errors.New("attempted to connect to gateway port")
// ErrWrongGateway represents an error condition when a server receives a connect
// request from a remote Gateway with a destination name that does not match the server's
// Gateway's name.
ErrWrongGateway = errors.New("wrong gateway")
// ErrGatewayNameHasSpaces signals that the gateway name contains spaces, which is not allowed.
ErrGatewayNameHasSpaces = errors.New("gateway name cannot contain spaces")
// ErrNoSysAccount is returned when an attempt to publish or subscribe is made
// when there is no internal system account defined.
ErrNoSysAccount = errors.New("system account not setup")
// ErrRevocation is returned when a credential has been revoked.
ErrRevocation = errors.New("credentials have been revoked")
// ErrServerNotRunning is used to signal an error that a server is not running.
ErrServerNotRunning = errors.New("server is not running")
// ErrServerNameHasSpaces signals that the server name contains spaces, which is not allowed.
ErrServerNameHasSpaces = errors.New("server name cannot contain spaces")
// ErrBadMsgHeader signals the parser detected a bad message header
ErrBadMsgHeader = errors.New("bad message header detected")
// ErrMsgHeadersNotSupported signals the parser detected a message header
// but they are not supported on this server.
ErrMsgHeadersNotSupported = errors.New("message headers not supported")
// ErrNoRespondersRequiresHeaders signals that a client needs to have headers
// on if they want no responders behavior.
ErrNoRespondersRequiresHeaders = errors.New("no responders requires headers support")
// ErrClusterNameConfigConflict signals that the options for cluster name in cluster and gateway are in conflict.
ErrClusterNameConfigConflict = errors.New("cluster name conflicts between cluster and gateway definitions")
// ErrClusterNameRemoteConflict signals that a remote server has a different cluster name.
ErrClusterNameRemoteConflict = errors.New("cluster name from remote server conflicts")
// ErrClusterNameHasSpaces signals that the cluster name contains spaces, which is not allowed.
ErrClusterNameHasSpaces = errors.New("cluster name cannot contain spaces")
// ErrMalformedSubject is returned when a subscription is made with a subject that does not conform to subject rules.
ErrMalformedSubject = errors.New("malformed subject")
// ErrSubscribePermissionViolation is returned when processing of a subscription fails due to permissions.
ErrSubscribePermissionViolation = errors.New("subscribe permission violation")
// ErrNoTransforms signals no subject transforms are available to map this subject.
ErrNoTransforms = errors.New("no matching transforms available")
// ErrCertNotPinned is returned when pinned certs are set and the certificate is not in it
ErrCertNotPinned = errors.New("certificate not pinned")
// ErrDuplicateServerName is returned when processing a server remote connection and
// the server reports that this server name is already used in the cluster.
ErrDuplicateServerName = errors.New("duplicate server name")
// ErrMinimumVersionRequired is returned when a connection is not at the minimum version required.
ErrMinimumVersionRequired = errors.New("minimum version required")
// ErrLeafNodeMinVersionRejected is the leafnode protocol error prefix used
// when rejecting a remote due to leafnodes.min_version.
ErrLeafNodeMinVersionRejected = errors.New("connection rejected since minimum version required is")
// ErrInvalidMappingDestination is used for all subject mapping destination errors
ErrInvalidMappingDestination = errors.New("invalid mapping destination")
// ErrInvalidMappingDestinationSubject is used to error on a bad transform destination mapping
ErrInvalidMappingDestinationSubject = fmt.Errorf("%w: invalid transform", ErrInvalidMappingDestination)
// ErrMappingDestinationNotUsingAllWildcards is used to error on a transform destination not using all of the token wildcards
ErrMappingDestinationNotUsingAllWildcards = fmt.Errorf("%w: not using all of the token wildcard(s)", ErrInvalidMappingDestination)
// ErrUnknownMappingDestinationFunction is returned when a subject mapping destination contains an unknown mustache-escaped mapping function.
ErrUnknownMappingDestinationFunction = fmt.Errorf("%w: unknown function", ErrInvalidMappingDestination)
// ErrMappingDestinationIndexOutOfRange is returned when the mapping destination function is passed an out of range wildcard index value for one of it's arguments
ErrMappingDestinationIndexOutOfRange = fmt.Errorf("%w: wildcard index out of range", ErrInvalidMappingDestination)
// ErrMappingDestinationNotEnoughArgs is returned when the mapping destination function is not passed enough arguments
ErrMappingDestinationNotEnoughArgs = fmt.Errorf("%w: not enough arguments passed to the function", ErrInvalidMappingDestination)
// ErrMappingDestinationInvalidArg is returned when the mapping destination function is passed and invalid argument
ErrMappingDestinationInvalidArg = fmt.Errorf("%w: function argument is invalid or in the wrong format", ErrInvalidMappingDestination)
// ErrMappingDestinationTooManyArgs is returned when the mapping destination function is passed too many arguments
ErrMappingDestinationTooManyArgs = fmt.Errorf("%w: too many arguments passed to the function", ErrInvalidMappingDestination)
// ErrMappingDestinationNotSupportedForImport is returned when you try to use a mapping function other than wildcard in a transform that needs to be reversible (i.e. an import)
ErrMappingDestinationNotSupportedForImport = fmt.Errorf("%w: the only mapping function allowed for import transforms is {{Wildcard()}}", ErrInvalidMappingDestination)
)
// mappingDestinationErr is a type of subject mapping destination error
type mappingDestinationErr struct {
token string
err error
}
func (e *mappingDestinationErr) Error() string {
if e.token == _EMPTY_ {
return e.err.Error()
}
return fmt.Sprintf("%s in %s", e.err, e.token)
}
func (e *mappingDestinationErr) Is(target error) bool {
return target == ErrInvalidMappingDestination
}
// configErr is a configuration error.
type configErr struct {
token token
reason string
}
// Source reports the location of a configuration error.
func (e *configErr) Source() string {
return fmt.Sprintf("%s:%d:%d", e.token.SourceFile(), e.token.Line(), e.token.Position())
}
// Error reports the location and reason from a configuration error.
func (e *configErr) Error() string {
if e.token != nil {
return fmt.Sprintf("%s: %s", e.Source(), e.reason)
}
return e.reason
}
// unknownConfigFieldErr is an error reported in pedantic mode.
type unknownConfigFieldErr struct {
configErr
field string
}
// Error reports that an unknown field was in the configuration.
func (e *unknownConfigFieldErr) Error() string {
return fmt.Sprintf("%s: unknown field %q", e.Source(), e.field)
}
// configWarningErr is an error reported in pedantic mode.
type configWarningErr struct {
configErr
field string
}
// Error reports a configuration warning.
func (e *configWarningErr) Error() string {
return fmt.Sprintf("%s: invalid use of field %q: %s", e.Source(), e.field, e.reason)
}
// processConfigErr is the result of processing the configuration from the server.
type processConfigErr struct {
errors []error
warnings []error
}
// Error returns the collection of errors separated by new lines,
// warnings appear first then hard errors.
func (e *processConfigErr) Error() string {
var msg string
for _, err := range e.Warnings() {
msg += err.Error() + "\n"
}
for _, err := range e.Errors() {
msg += err.Error() + "\n"
}
return msg
}
// Warnings returns the list of warnings.
func (e *processConfigErr) Warnings() []error {
return e.warnings
}
// Errors returns the list of errors.
func (e *processConfigErr) Errors() []error {
return e.errors
}
// errCtx wraps an error and stores additional ctx information for tracing.
// Does not print or return it unless explicitly requested.
type errCtx struct {
error
ctx string
}
func NewErrorCtx(err error, format string, args ...any) error {
return &errCtx{err, fmt.Sprintf(format, args...)}
}
// Unwrap implement to work with errors.Is and errors.As
func (e *errCtx) Unwrap() error {
if e == nil {
return nil
}
return e.error
}
// Context for error
func (e *errCtx) Context() string {
if e == nil {
return ""
}
return e.ctx
}
// UnpackIfErrorCtx return Error or, if type is right error and context
func UnpackIfErrorCtx(err error) string {
if e, ok := err.(*errCtx); ok {
if _, ok := e.error.(*errCtx); ok {
return fmt.Sprint(UnpackIfErrorCtx(e.error), ": ", e.Context())
}
return fmt.Sprint(e.Error(), ": ", e.Context())
}
return err.Error()
}
// implements: go 1.13 errors.Unwrap(err error) error
// TODO replace with native code once we no longer support go1.12
func errorsUnwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
// ErrorIs implements: go 1.13 errors.Is(err, target error) bool
// TODO replace with native code once we no longer support go1.12
func ErrorIs(err, target error) bool {
// this is an outright copy of go 1.13 errors.Is(err, target error) bool
// removed isComparable
if err == nil || target == nil {
return err == target
}
for {
if err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// TODO: consider supporing target.Is(err). This would allow
// user-definable predicates, but also may allow for coping with sloppy
// APIs, thereby making it easier to get away with them.
if err = errorsUnwrap(err); err == nil {
return false
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2026 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 server
import (
"maps"
"slices"
"strings"
)
const (
FeatureFlagJsAckFormatV2 = "js_ack_fc_v2"
FeatureFlagJsRaftDeleteRange = "js_raft_delete_range"
)
var featureFlags = map[string]bool{
// Use v2 format for `$JS.ACK.>` and `$JS.FC.>`.
// - Introduced: 2.14.0, both v1 and v2 supported, only using v1.
// - Enabled: TBD, both supported, v2 becomes the default.
//
// - v1: $JS.ACK.<stream name>.<consumer name>.<num delivered>.<stream sequence>.<consumer sequence>.<timestamp>.<num pending>
// - v2: $JS.ACK.<domain>.<account hash>.<stream name>.<consumer name>.<num delivered>.<stream sequence>.<consumer sequence>.<timestamp>.<num pending>
// See also: https://github.com/nats-io/nats-architecture-and-design/blob/main/adr/ADR-15.md#jsack
FeatureFlagJsAckFormatV2: false,
// Propose delete range gaps as a single `deleteRangeOp` Raft append entry
// instead of one entry per deleted sequence. Dramatically reduces Raft cost
// on mirrors whose origin has a large number of interior deletes.
// - Introduced: 2.14.0, apply-side always supports receiving `deleteRangeOp`.
// - Enabled: TBD, once all supported versions carry the apply-side.
//
// WARNING: Only enable once every peer in the cluster is on a version that
// supports receiving `deleteRangeOp`. Older peers panic on apply of an
// unknown stream entry operation.
FeatureFlagJsRaftDeleteRange: false,
}
// getFeatureFlag is used to retrieve either the default or overwritten value for a feature flag.
// The user's value takes precedence over the system's default. However, if the flag doesn't exist, it's disabled.
// The *Options returned by Server.getOpts() is treated as immutable, mutations go through setOpts,
// so no lock is required on the map read here.
func (o *Options) getFeatureFlag(k string) bool {
defaultValue, ok := featureFlags[k]
if !ok {
return false // Not supported.
}
if userValue, ok := o.FeatureFlags[k]; ok {
return userValue
}
return defaultValue
}
// getMergedFeatureFlags returns a merged map of feature flags, with the user's values taking precedence.
func (o *Options) getMergedFeatureFlags() map[string]bool {
merged := make(map[string]bool)
for k, v := range featureFlags {
merged[k] = v
}
for k, v := range o.FeatureFlags {
if _, ok := featureFlags[k]; !ok {
continue
}
merged[k] = v
}
return merged
}
// printFeatureFlags logs the currently used feature flags on server startup.
func (s *Server) printFeatureFlags(o *Options) {
if len(o.FeatureFlags) == 0 {
return
}
keys := slices.Sorted(maps.Keys(o.FeatureFlags))
var (
configured strings.Builder
unsupported strings.Builder
)
for _, k := range keys {
// Unsupported
defaultValue, ok := featureFlags[k]
if !ok {
if unsupported.Len() > 0 {
unsupported.WriteString(", ")
}
unsupported.WriteString(k)
continue
}
v := o.FeatureFlags[k]
if configured.Len() > 0 {
configured.WriteString(", ")
}
configured.WriteString(k)
configured.WriteString(" (")
if defaultValue {
if v {
configured.WriteString("enabled")
} else {
configured.WriteString("opt-out")
}
} else if v {
configured.WriteString("opt-in")
} else {
configured.WriteString("disabled")
}
configured.WriteString(")")
}
if configured.Len() == 0 {
configured.WriteString("none")
}
s.Noticef(" Feature flags:")
s.Noticef(" Configured: %s", configured.String())
if unsupported.Len() > 0 {
s.Noticef(" Unsupported: %s", unsupported.String())
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+566
View File
@@ -0,0 +1,566 @@
// Copyright 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 gsl
import (
"errors"
"strings"
"sync"
)
// Sublist is a routing mechanism to handle subject distribution and
// provides a facility to match subjects from published messages to
// interested subscribers. Subscribers can have wildcard subjects to
// match multiple published subjects.
// Common byte variables for wildcards and token separator.
const (
pwc = '*'
pwcs = "*"
fwc = '>'
fwcs = ">"
tsep = "."
btsep = '.'
_EMPTY_ = ""
)
// Sublist related errors
var (
ErrInvalidSubject = errors.New("gsl: invalid subject")
ErrNotFound = errors.New("gsl: no matches found")
ErrNilChan = errors.New("gsl: nil channel")
ErrAlreadyRegistered = errors.New("gsl: notification already registered")
)
// SimpleSublist is an alias type for GenericSublist that takes
// empty values, useful for tracking interest only without any
// unnecessary allocations.
type SimpleSublist = GenericSublist[struct{}]
// NewSimpleSublist will create a simple sublist.
func NewSimpleSublist() *SimpleSublist {
return &GenericSublist[struct{}]{root: newLevel[struct{}]()}
}
// A GenericSublist stores and efficiently retrieves subscriptions.
type GenericSublist[T comparable] struct {
sync.RWMutex
root *level[T]
count uint32
}
// A node contains subscriptions and a pointer to the next level.
type node[T comparable] struct {
next *level[T]
subs map[T]string // value -> subject
}
// A level represents a group of nodes and special pointers to
// wildcard nodes.
type level[T comparable] struct {
nodes map[string]*node[T]
pwc, fwc *node[T]
}
// Create a new default node.
func newNode[T comparable]() *node[T] {
return &node[T]{subs: make(map[T]string)}
}
// Create a new default level.
func newLevel[T comparable]() *level[T] {
return &level[T]{nodes: make(map[string]*node[T])}
}
// NewSublist will create a default sublist with caching enabled per the flag.
func NewSublist[T comparable]() *GenericSublist[T] {
return &GenericSublist[T]{root: newLevel[T]()}
}
// Insert adds a subscription into the sublist
func (s *GenericSublist[T]) Insert(subject string, value T) error {
s.Lock()
var sfwc bool
var n *node[T]
l := s.root
for t := range strings.SplitSeq(subject, tsep) {
lt := len(t)
if lt == 0 || sfwc {
s.Unlock()
return ErrInvalidSubject
}
if lt > 1 {
n = l.nodes[t]
} else {
switch t[0] {
case pwc:
n = l.pwc
case fwc:
n = l.fwc
sfwc = true
default:
n = l.nodes[t]
}
}
if n == nil {
n = newNode[T]()
if lt > 1 {
l.nodes[t] = n
} else {
switch t[0] {
case pwc:
l.pwc = n
case fwc:
l.fwc = n
default:
l.nodes[t] = n
}
}
}
if n.next == nil {
n.next = newLevel[T]()
}
l = n.next
}
n.subs[value] = subject
s.count++
s.Unlock()
return nil
}
// Match will match all entries to the literal subject.
// It will return a set of results for both normal and queue subscribers.
func (s *GenericSublist[T]) Match(subject string, cb func(T)) {
s.match(subject, cb, true)
}
// MatchBytes will match all entries to the literal subject.
// It will return a set of results for both normal and queue subscribers.
func (s *GenericSublist[T]) MatchBytes(subject []byte, cb func(T)) {
s.match(string(subject), cb, true)
}
// HasInterest will return whether or not there is any interest in the subject.
// In cases where more detail is not required, this may be faster than Match.
func (s *GenericSublist[T]) HasInterest(subject string) bool {
return s.hasInterest(subject, true, nil)
}
// NumInterest will return the number of subs interested in the subject.
// In cases where more detail is not required, this may be faster than Match.
func (s *GenericSublist[T]) NumInterest(subject string) (np int) {
s.hasInterest(subject, true, &np)
return
}
// MatchesFullWildcard returns true if there is top-level ">" interest.
func (s *GenericSublist[T]) MatchesFullWildcard() bool {
if s == nil {
return false
}
s.RLock()
defer s.RUnlock()
return s.root.fwc != nil
}
// MatchesSingleFilter returns the filter when the sublist contains exactly one unique subject.
func (s *GenericSublist[T]) MatchesSingleFilter() (string, bool) {
if s == nil {
return _EMPTY_, false
}
s.RLock()
defer s.RUnlock()
return singleFilter(s.root, _EMPTY_)
}
func singleFilter[T comparable](l *level[T], filter string) (string, bool) {
if l == nil {
return filter, filter != _EMPTY_
}
if len(l.nodes) > 1 {
return _EMPTY_, false
}
var next *node[T]
branches := 0
if l.pwc != nil {
next = l.pwc
branches++
}
if l.fwc != nil {
next = l.fwc
branches++
}
for _, n := range l.nodes {
next = n
branches++
}
if branches != 1 {
return _EMPTY_, false
}
for _, subj := range next.subs {
filter = subj
break
}
if next.next == nil {
return filter, filter != _EMPTY_
}
if filter != _EMPTY_ {
if next.next.numNodes() > 0 {
return _EMPTY_, false
}
return filter, true
}
return singleFilter(next.next, filter)
}
func (s *GenericSublist[T]) match(subject string, cb func(T), doLock bool) {
tsa := [32]string{}
tokens := tsa[:0]
start := 0
for i := 0; i < len(subject); i++ {
if subject[i] == btsep {
if i-start == 0 {
return
}
tokens = append(tokens, subject[start:i])
start = i + 1
}
}
if start >= len(subject) {
return
}
tokens = append(tokens, subject[start:])
if doLock {
s.RLock()
defer s.RUnlock()
}
matchLevel(s.root, tokens, cb)
}
func (s *GenericSublist[T]) hasInterest(subject string, doLock bool, np *int) bool {
tsa := [32]string{}
tokens := tsa[:0]
start := 0
for i := 0; i < len(subject); i++ {
if subject[i] == btsep {
if i-start == 0 {
return false
}
tokens = append(tokens, subject[start:i])
start = i + 1
}
}
if start >= len(subject) {
return false
}
tokens = append(tokens, subject[start:])
if doLock {
s.RLock()
defer s.RUnlock()
}
return matchLevelForAny(s.root, tokens, np)
}
func matchLevelForAny[T comparable](l *level[T], toks []string, np *int) bool {
var pwc, n *node[T]
for i, t := range toks {
if l == nil {
return false
}
if l.fwc != nil {
if np != nil {
*np += len(l.fwc.subs)
}
return true
}
if pwc = l.pwc; pwc != nil {
if match := matchLevelForAny(pwc.next, toks[i+1:], np); match {
return true
}
}
n = l.nodes[t]
if n != nil {
l = n.next
} else {
l = nil
}
}
if n != nil {
if np != nil {
*np += len(n.subs)
}
if len(n.subs) > 0 {
return true
}
}
if pwc != nil {
if np != nil {
*np += len(pwc.subs)
}
return len(pwc.subs) > 0
}
return false
}
// callbacksForResults will make the necessary callbacks for each
// result in this node.
func callbacksForResults[T comparable](n *node[T], cb func(T)) {
for sub := range n.subs {
cb(sub)
}
}
// matchLevel is used to recursively descend into the trie.
func matchLevel[T comparable](l *level[T], toks []string, cb func(T)) {
var pwc, n *node[T]
for i, t := range toks {
if l == nil {
return
}
if l.fwc != nil {
callbacksForResults(l.fwc, cb)
}
if pwc = l.pwc; pwc != nil {
matchLevel(pwc.next, toks[i+1:], cb)
}
n = l.nodes[t]
if n != nil {
l = n.next
} else {
l = nil
}
}
if n != nil {
callbacksForResults(n, cb)
}
if pwc != nil {
callbacksForResults(pwc, cb)
}
}
// lnt is used to track descent into levels for a removal for pruning.
type lnt[T comparable] struct {
l *level[T]
n *node[T]
t string
}
// Raw low level remove, can do batches with lock held outside.
func (s *GenericSublist[T]) remove(subject string, value T, shouldLock bool) error {
if shouldLock {
s.Lock()
defer s.Unlock()
}
var sfwc bool
var n *node[T]
l := s.root
// Track levels for pruning
var lnts [32]lnt[T]
levels := lnts[:0]
for t := range strings.SplitSeq(subject, tsep) {
lt := len(t)
if lt == 0 || sfwc {
return ErrInvalidSubject
}
if l == nil {
return ErrNotFound
}
if lt > 1 {
n = l.nodes[t]
} else {
switch t[0] {
case pwc:
n = l.pwc
case fwc:
n = l.fwc
sfwc = true
default:
n = l.nodes[t]
}
}
if n != nil {
levels = append(levels, lnt[T]{l, n, t})
l = n.next
} else {
l = nil
}
}
if !s.removeFromNode(n, value) {
return ErrNotFound
}
s.count--
for i := len(levels) - 1; i >= 0; i-- {
l, n, t := levels[i].l, levels[i].n, levels[i].t
if n.isEmpty() {
l.pruneNode(n, t)
}
}
return nil
}
// Remove will remove a subscription.
func (s *GenericSublist[T]) Remove(subject string, value T) error {
return s.remove(subject, value, true)
}
// HasInterestStartingIn is a helper for subject tree intersection.
func (s *GenericSublist[T]) HasInterestStartingIn(subj string) bool {
s.RLock()
defer s.RUnlock()
var _tokens [64]string
tokens := tokenizeSubjectIntoSlice(_tokens[:0], subj)
return hasInterestStartingIn(s.root, tokens)
}
func hasInterestStartingIn[T comparable](l *level[T], tokens []string) bool {
if l == nil {
return false
}
if len(tokens) == 0 {
return true
}
token := tokens[0]
if l.fwc != nil {
return true
}
found := false
if pwc := l.pwc; pwc != nil {
found = found || hasInterestStartingIn(pwc.next, tokens[1:])
}
if n := l.nodes[token]; n != nil {
found = found || hasInterestStartingIn(n.next, tokens[1:])
}
return found
}
// pruneNode is used to prune an empty node from the tree.
func (l *level[T]) pruneNode(n *node[T], t string) {
if n == nil {
return
}
if n == l.fwc {
l.fwc = nil
} else if n == l.pwc {
l.pwc = nil
} else {
delete(l.nodes, t)
}
}
// isEmpty will test if the node has any entries. Used
// in pruning.
func (n *node[T]) isEmpty() bool {
return len(n.subs) == 0 && (n.next == nil || n.next.numNodes() == 0)
}
// Return the number of nodes for the given level.
func (l *level[T]) numNodes() int {
if l == nil {
return 0
}
num := len(l.nodes)
if l.pwc != nil {
num++
}
if l.fwc != nil {
num++
}
return num
}
// Remove the sub for the given node.
func (s *GenericSublist[T]) removeFromNode(n *node[T], value T) (found bool) {
if n == nil {
return false
}
if _, found = n.subs[value]; found {
delete(n.subs, value)
}
return found
}
// Count returns the number of subscriptions.
func (s *GenericSublist[T]) Count() uint32 {
s.RLock()
defer s.RUnlock()
return s.count
}
// numLevels will return the maximum number of levels
// contained in the Sublist tree.
func (s *GenericSublist[T]) numLevels() int {
return visitLevel(s.root, 0)
}
// visitLevel is used to descend the Sublist tree structure
// recursively.
func visitLevel[T comparable](l *level[T], depth int) int {
if l == nil || l.numNodes() == 0 {
return depth
}
depth++
maxDepth := depth
for _, n := range l.nodes {
if n == nil {
continue
}
newDepth := visitLevel(n.next, depth)
if newDepth > maxDepth {
maxDepth = newDepth
}
}
if l.pwc != nil {
pwcDepth := visitLevel(l.pwc.next, depth)
if pwcDepth > maxDepth {
maxDepth = pwcDepth
}
}
if l.fwc != nil {
fwcDepth := visitLevel(l.fwc.next, depth)
if fwcDepth > maxDepth {
maxDepth = fwcDepth
}
}
return maxDepth
}
// use similar to append. meaning, the updated slice will be returned
func tokenizeSubjectIntoSlice(tts []string, subject string) []string {
start := 0
for i := 0; i < len(subject); i++ {
if subject[i] == btsep {
tts = append(tts, subject[start:i])
start = i + 1
}
}
tts = append(tts, subject[start:])
return tts
}
+286
View File
@@ -0,0 +1,286 @@
// Copyright 2021-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 server
import (
"errors"
"sync"
"sync/atomic"
)
const ipQueueDefaultMaxRecycleSize = 4 * 1024
// This is a generic intra-process queue.
type ipQueue[T any] struct {
inprogress int64
sync.Mutex
ch chan struct{}
elts []T
pos int
pool *sync.Pool
sz uint64 // Calculated size (only if calc != nil)
name string
m *sync.Map
ipQueueOpts[T]
}
type ipQueueOpts[T any] struct {
mrs int // Max recycle size
calc func(e T) uint64 // Calc function for tracking size
msz uint64 // Limit by total calculated size
mlen int // Limit by number of entries
}
type ipQueueOpt[T any] func(*ipQueueOpts[T])
// This option allows to set the maximum recycle size when attempting
// to put back a slice to the pool.
func ipqMaxRecycleSize[T any](max int) ipQueueOpt[T] {
return func(o *ipQueueOpts[T]) {
o.mrs = max
}
}
// This option enables total queue size counting by passing in a function
// that evaluates the size of each entry as it is pushed/popped. This option
// enables the size() function.
func ipqSizeCalculation[T any](calc func(e T) uint64) ipQueueOpt[T] {
return func(o *ipQueueOpts[T]) {
o.calc = calc
}
}
// This option allows setting the maximum queue size. Once the limit is
// reached, then push() will stop returning true and no more entries will
// be stored until some more are popped. The ipQueue_SizeCalculation must
// be provided for this to work.
func ipqLimitBySize[T any](max uint64) ipQueueOpt[T] {
return func(o *ipQueueOpts[T]) {
o.msz = max
}
}
// This option allows setting the maximum queue length. Once the limit is
// reached, then push() will stop returning true and no more entries will
// be stored until some more are popped.
func ipqLimitByLen[T any](max int) ipQueueOpt[T] {
return func(o *ipQueueOpts[T]) {
o.mlen = max
}
}
var errIPQLenLimitReached = errors.New("IPQ len limit reached")
var errIPQSizeLimitReached = errors.New("IPQ size limit reached")
func newIPQueue[T any](s *Server, name string, opts ...ipQueueOpt[T]) *ipQueue[T] {
q := &ipQueue[T]{
ch: make(chan struct{}, 1),
pool: &sync.Pool{
New: func() any {
// Reason we use pointer to slice instead of slice is explained
// here: https://staticcheck.io/docs/checks#SA6002
res := make([]T, 0, 32)
return &res
},
},
name: name,
m: &s.ipQueues,
ipQueueOpts: ipQueueOpts[T]{
mrs: ipQueueDefaultMaxRecycleSize,
},
}
for _, o := range opts {
o(&q.ipQueueOpts)
}
s.ipQueues.Store(name, q)
return q
}
// Add the element `e` to the queue, notifying the queue channel's `ch` if the
// entry is the first to be added, and returns the length of the queue after
// this element is added.
func (q *ipQueue[T]) push(e T) (int, error) {
q.Lock()
l := len(q.elts) - q.pos
if q.mlen > 0 && l == q.mlen {
q.Unlock()
return l, errIPQLenLimitReached
}
if q.calc != nil {
sz := q.calc(e)
if q.msz > 0 && q.sz+sz > q.msz {
q.Unlock()
return l, errIPQSizeLimitReached
}
q.sz += sz
}
if q.elts == nil {
// What comes out of the pool is already of size 0, so no need for [:0].
q.elts = *(q.pool.Get().(*[]T))
}
q.elts = append(q.elts, e)
q.Unlock()
if l == 0 {
select {
case q.ch <- struct{}{}:
default:
}
}
return l + 1, nil
}
// Returns the whole list of elements currently present in the queue,
// emptying the queue. This should be called after receiving a notification
// from the queue's `ch` notification channel that indicates that there
// is something in the queue.
// However, in cases where `drain()` may be called from another go
// routine, it is possible that a routine is notified that there is
// something, but by the time it calls `pop()`, the drain() would have
// emptied the queue. So the caller should never assume that pop() will
// return a slice of 1 or more, it could return `nil`.
func (q *ipQueue[T]) pop() []T {
if q == nil {
return nil
}
q.Lock()
if len(q.elts)-q.pos == 0 {
q.Unlock()
return nil
}
var elts []T
if q.pos == 0 {
elts = q.elts
} else {
elts = q.elts[q.pos:]
}
q.elts, q.pos, q.sz = nil, 0, 0
atomic.AddInt64(&q.inprogress, int64(len(elts)))
q.Unlock()
return elts
}
// Returns the first element from the queue, if any. See comment above
// regarding calling after being notified that there is something and
// the use of drain(). In short, the caller should always check the
// boolean return value to ensure that the value is genuine and not a
// default empty value.
func (q *ipQueue[T]) popOne() (T, bool) {
q.Lock()
l := len(q.elts) - q.pos
if l == 0 {
q.Unlock()
var empty T
return empty, false
}
e := q.elts[q.pos]
if l--; l > 0 {
q.pos++
if q.calc != nil {
q.sz -= q.calc(e)
}
// We need to re-signal
select {
case q.ch <- struct{}{}:
default:
}
} else {
// We have just emptied the queue, so we can reuse unless it is too big.
if cap(q.elts) <= q.mrs {
q.elts = q.elts[:0]
} else {
q.elts = nil
}
q.pos, q.sz = 0, 0
}
q.Unlock()
return e, true
}
// After a pop(), the slice can be recycled for the next push() when
// a first element is added to the queue.
// This will also decrement the "in progress" count with the length
// of the slice.
// WARNING: The caller MUST never reuse `elts`.
func (q *ipQueue[T]) recycle(elts *[]T) {
// If invoked with a nil list, nothing to do.
if elts == nil || *elts == nil {
return
}
// Update the in progress count.
if len(*elts) > 0 {
atomic.AddInt64(&q.inprogress, int64(-(len(*elts))))
}
// We also don't want to recycle huge slices, so check against the max.
// q.mrs is normally immutable but can be changed, in a safe way, in some tests.
if cap(*elts) > q.mrs {
return
}
(*elts) = (*elts)[:0]
q.pool.Put(elts)
}
// Returns the current length of the queue.
func (q *ipQueue[T]) len() int {
q.Lock()
defer q.Unlock()
return len(q.elts) - q.pos
}
// Returns the calculated size of the queue (if ipQueue_SizeCalculation has been
// passed in), otherwise returns zero.
func (q *ipQueue[T]) size() uint64 {
q.Lock()
defer q.Unlock()
return q.sz
}
// Empty the queue and consumes the notification signal if present.
// Returns the number of items that were drained from the queue.
// Note that this could cause a reader go routine that has been
// notified that there is something in the queue (reading from queue's `ch`)
// may then get nothing if `drain()` is invoked before the `pop()` or `popOne()`.
func (q *ipQueue[T]) drain() int {
if q == nil {
return 0
}
q.Lock()
olen := len(q.elts) - q.pos
q.elts, q.pos, q.sz = nil, 0, 0
// Consume the signal if it was present to reduce the chance of a reader
// routine to be think that there is something in the queue...
select {
case <-q.ch:
default:
}
q.Unlock()
return olen
}
// Since the length of the queue goes to 0 after a pop(), it is good to
// have an insight on how many elements are yet to be processed after a pop().
// For that reason, the queue maintains a count of elements returned through
// the pop() API. When the caller will call q.recycle(), this count will
// be reduced by the size of the slice returned by pop().
func (q *ipQueue[T]) inProgress() int64 {
return atomic.LoadInt64(&q.inprogress)
}
// Remove this queue from the server's map of ipQueues.
// All ipQueue operations (such as push/pop/etc..) are still possible.
func (q *ipQueue[T]) unregister() {
if q == nil {
return
}
q.m.Delete(q.name)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,102 @@
package server
import (
"fmt"
)
type errOpts struct {
err error
}
// ErrorOption configures a NATS Error helper
type ErrorOption func(*errOpts)
// Unless ensures that if err is a ApiErr that err will be returned rather than the one being created via the helper
func Unless(err error) ErrorOption {
return func(opts *errOpts) {
opts.err = err
}
}
func parseOpts(opts []ErrorOption) *errOpts {
eopts := &errOpts{}
for _, opt := range opts {
opt(eopts)
}
return eopts
}
type ErrorIdentifier uint16
// IsNatsErr determines if an error matches ID, if multiple IDs are given if the error matches any of these the function will be true
func IsNatsErr(err error, ids ...ErrorIdentifier) bool {
if err == nil {
return false
}
ce, ok := err.(*ApiError)
if !ok || ce == nil {
return false
}
for _, id := range ids {
ae, ok := ApiErrors[id]
if !ok || ae == nil {
continue
}
if ce.ErrCode == ae.ErrCode {
return true
}
}
return false
}
// ApiError is included in all responses if there was an error.
type ApiError struct {
Code int `json:"code"`
ErrCode uint16 `json:"err_code,omitempty"`
Description string `json:"description,omitempty"`
}
// ErrorsData is the source data for generated errors as found in errors.json
type ErrorsData struct {
Constant string `json:"constant"`
Code int `json:"code"`
ErrCode uint16 `json:"error_code"`
Description string `json:"description"`
Comment string `json:"comment"`
Help string `json:"help"`
URL string `json:"url"`
Deprecates string `json:"deprecates"`
}
func (e *ApiError) Error() string {
return fmt.Sprintf("%s (%d)", e.Description, e.ErrCode)
}
func (e *ApiError) toReplacerArgs(replacements []any) []string {
var (
ra []string
key string
)
for i, replacement := range replacements {
if i%2 == 0 {
key = replacement.(string)
continue
}
switch v := replacement.(type) {
case string:
ra = append(ra, key, v)
case error:
ra = append(ra, key, v.Error())
default:
ra = append(ra, key, fmt.Sprintf("%v", v))
}
}
return ra
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,366 @@
// Copyright 2020-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 server
import (
"encoding/json"
"time"
)
// publishAdvisory sends the given advisory into the account. Returns true if
// it was sent, false if not (i.e. due to lack of interest or a marshal error).
func (s *Server) publishAdvisory(acc *Account, subject string, adv any) bool {
if acc == nil {
acc = s.SystemAccount()
if acc == nil {
return false
}
}
// If there is no one listening for this advisory then save ourselves the effort
// and don't bother encoding the JSON or sending it.
if sl := acc.sl; (sl != nil && !sl.HasInterest(subject)) && !s.hasGatewayInterest(acc.Name, subject) {
return false
}
ej, err := json.Marshal(adv)
if err == nil {
err = s.sendInternalAccountMsg(acc, subject, ej)
if err != nil {
s.Warnf("Advisory could not be sent for account %q: %v", acc.Name, err)
}
} else {
s.Warnf("Advisory could not be serialized for account %q: %v", acc.Name, err)
}
return err == nil
}
// JSAPIAudit is an advisory about administrative actions taken on JetStream
type JSAPIAudit struct {
TypedEvent
Server string `json:"server"`
Client *ClientInfo `json:"client"`
Subject string `json:"subject"`
Request string `json:"request,omitempty"`
Response string `json:"response"`
Domain string `json:"domain,omitempty"`
}
const JSAPIAuditType = "io.nats.jetstream.advisory.v1.api_audit"
// ActionAdvisoryType indicates which action against a stream, consumer or template triggered an advisory
type ActionAdvisoryType string
const (
CreateEvent ActionAdvisoryType = "create"
DeleteEvent ActionAdvisoryType = "delete"
ModifyEvent ActionAdvisoryType = "modify"
)
// JSStreamActionAdvisory indicates that a stream was created, edited or deleted
type JSStreamActionAdvisory struct {
TypedEvent
Stream string `json:"stream"`
Action ActionAdvisoryType `json:"action"`
Domain string `json:"domain,omitempty"`
}
const JSStreamActionAdvisoryType = "io.nats.jetstream.advisory.v1.stream_action"
// JSConsumerActionAdvisory indicates that a consumer was created or deleted
type JSConsumerActionAdvisory struct {
TypedEvent
Stream string `json:"stream"`
Consumer string `json:"consumer"`
Action ActionAdvisoryType `json:"action"`
Domain string `json:"domain,omitempty"`
}
const JSConsumerActionAdvisoryType = "io.nats.jetstream.advisory.v1.consumer_action"
// JSConsumerPauseAdvisory indicates that a consumer was paused or unpaused
type JSConsumerPauseAdvisory struct {
TypedEvent
Stream string `json:"stream"`
Consumer string `json:"consumer"`
Paused bool `json:"paused"`
PauseUntil time.Time `json:"pause_until,omitempty"`
Domain string `json:"domain,omitempty"`
}
const JSConsumerPauseAdvisoryType = "io.nats.jetstream.advisory.v1.consumer_pause"
// JSConsumerAckMetric is a metric published when a user acknowledges a message, the
// number of these that will be published is dependent on SampleFrequency
type JSConsumerAckMetric struct {
TypedEvent
Stream string `json:"stream"`
Consumer string `json:"consumer"`
ConsumerSeq uint64 `json:"consumer_seq"`
StreamSeq uint64 `json:"stream_seq"`
Delay int64 `json:"ack_time"`
Deliveries uint64 `json:"deliveries"`
Domain string `json:"domain,omitempty"`
}
// JSConsumerAckMetricType is the schema type for JSConsumerAckMetricType
const JSConsumerAckMetricType = "io.nats.jetstream.metric.v1.consumer_ack"
// JSConsumerDeliveryExceededAdvisory is an advisory informing that a message hit
// its MaxDeliver threshold and so might be a candidate for DLQ handling
type JSConsumerDeliveryExceededAdvisory struct {
TypedEvent
Stream string `json:"stream"`
Consumer string `json:"consumer"`
StreamSeq uint64 `json:"stream_seq"`
Deliveries uint64 `json:"deliveries"`
Domain string `json:"domain,omitempty"`
}
// JSConsumerDeliveryExceededAdvisoryType is the schema type for JSConsumerDeliveryExceededAdvisory
const JSConsumerDeliveryExceededAdvisoryType = "io.nats.jetstream.advisory.v1.max_deliver"
// JSConsumerDeliveryNakAdvisory is an advisory informing that a message was
// naked by the consumer
type JSConsumerDeliveryNakAdvisory struct {
TypedEvent
Stream string `json:"stream"`
Consumer string `json:"consumer"`
ConsumerSeq uint64 `json:"consumer_seq"`
StreamSeq uint64 `json:"stream_seq"`
Deliveries uint64 `json:"deliveries"`
Domain string `json:"domain,omitempty"`
}
// JSConsumerDeliveryNakAdvisoryType is the schema type for JSConsumerDeliveryNakAdvisory
const JSConsumerDeliveryNakAdvisoryType = "io.nats.jetstream.advisory.v1.nak"
// JSConsumerDeliveryTerminatedAdvisory is an advisory informing that a message was
// terminated by the consumer, so might be a candidate for DLQ handling
type JSConsumerDeliveryTerminatedAdvisory struct {
TypedEvent
Stream string `json:"stream"`
Consumer string `json:"consumer"`
ConsumerSeq uint64 `json:"consumer_seq"`
StreamSeq uint64 `json:"stream_seq"`
Deliveries uint64 `json:"deliveries"`
Reason string `json:"reason,omitempty"`
Domain string `json:"domain,omitempty"`
}
// JSConsumerDeliveryTerminatedAdvisoryType is the schema type for JSConsumerDeliveryTerminatedAdvisory
const JSConsumerDeliveryTerminatedAdvisoryType = "io.nats.jetstream.advisory.v1.terminated"
// JSSnapshotCreateAdvisory is an advisory sent after a snapshot is successfully started
type JSSnapshotCreateAdvisory struct {
TypedEvent
Stream string `json:"stream"`
State StreamState `json:"state"`
Client *ClientInfo `json:"client"`
Domain string `json:"domain,omitempty"`
}
// JSSnapshotCreatedAdvisoryType is the schema type for JSSnapshotCreateAdvisory
const JSSnapshotCreatedAdvisoryType = "io.nats.jetstream.advisory.v1.snapshot_create"
// JSSnapshotCompleteAdvisory is an advisory sent after a snapshot is successfully started
type JSSnapshotCompleteAdvisory struct {
TypedEvent
Stream string `json:"stream"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Client *ClientInfo `json:"client"`
Domain string `json:"domain,omitempty"`
}
// JSSnapshotCompleteAdvisoryType is the schema type for JSSnapshotCreateAdvisory
const JSSnapshotCompleteAdvisoryType = "io.nats.jetstream.advisory.v1.snapshot_complete"
// JSRestoreCreateAdvisory is an advisory sent after a snapshot is successfully started
type JSRestoreCreateAdvisory struct {
TypedEvent
Stream string `json:"stream"`
Client *ClientInfo `json:"client"`
Domain string `json:"domain,omitempty"`
}
// JSRestoreCreateAdvisoryType is the schema type for JSSnapshotCreateAdvisory
const JSRestoreCreateAdvisoryType = "io.nats.jetstream.advisory.v1.restore_create"
// JSRestoreCompleteAdvisory is an advisory sent after a snapshot is successfully started
type JSRestoreCompleteAdvisory struct {
TypedEvent
Stream string `json:"stream"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Bytes int64 `json:"bytes"`
Client *ClientInfo `json:"client"`
Domain string `json:"domain,omitempty"`
}
// JSRestoreCompleteAdvisoryType is the schema type for JSSnapshotCreateAdvisory
const JSRestoreCompleteAdvisoryType = "io.nats.jetstream.advisory.v1.restore_complete"
// Clustering specific.
// JSClusterLeaderElectedAdvisoryType is sent when the system elects a new meta leader.
const JSDomainLeaderElectedAdvisoryType = "io.nats.jetstream.advisory.v1.domain_leader_elected"
// JSClusterLeaderElectedAdvisory indicates that a domain has elected a new leader.
type JSDomainLeaderElectedAdvisory struct {
TypedEvent
Leader string `json:"leader"`
Replicas []*PeerInfo `json:"replicas"`
Cluster string `json:"cluster"`
Domain string `json:"domain,omitempty"`
}
// JSStreamLeaderElectedAdvisoryType is sent when the system elects a new leader for a stream.
const JSStreamLeaderElectedAdvisoryType = "io.nats.jetstream.advisory.v1.stream_leader_elected"
// JSStreamLeaderElectedAdvisory indicates that a stream has elected a new leader.
type JSStreamLeaderElectedAdvisory struct {
TypedEvent
Account string `json:"account,omitempty"`
Stream string `json:"stream"`
Leader string `json:"leader"`
Replicas []*PeerInfo `json:"replicas"`
Domain string `json:"domain,omitempty"`
}
// JSStreamQuorumLostAdvisoryType is sent when the system detects a clustered stream and
// its consumers are stalled and unable to make progress.
const JSStreamQuorumLostAdvisoryType = "io.nats.jetstream.advisory.v1.stream_quorum_lost"
// JSStreamQuorumLostAdvisory indicates that a stream has lost quorum and is stalled.
type JSStreamQuorumLostAdvisory struct {
TypedEvent
Account string `json:"account,omitempty"`
Stream string `json:"stream"`
Replicas []*PeerInfo `json:"replicas"`
Domain string `json:"domain,omitempty"`
}
// JSStreamBatchAbandonedAdvisoryType is sent when a stream's atomic batch is abandoned.
const JSStreamBatchAbandonedAdvisoryType = "io.nats.jetstream.advisory.v1.stream_batch_abandoned"
// JSStreamBatchAbandonedAdvisory indicates that a stream's batch was abandoned.
type JSStreamBatchAbandonedAdvisory struct {
TypedEvent
Account string `json:"account,omitempty"`
Stream string `json:"stream"`
Domain string `json:"domain,omitempty"`
BatchId string `json:"batch"`
Reason BatchAbandonReason `json:"reason"`
}
type BatchAbandonReason string
var (
BatchTimeout BatchAbandonReason = "timeout"
BatchLarge BatchAbandonReason = "large"
BatchIncomplete BatchAbandonReason = "incomplete"
BatchRequirementsNotMet BatchAbandonReason = "unsupported"
)
// JSConsumerLeaderElectedAdvisoryType is sent when the system elects a leader for a consumer.
const JSConsumerLeaderElectedAdvisoryType = "io.nats.jetstream.advisory.v1.consumer_leader_elected"
// JSConsumerLeaderElectedAdvisory indicates that a consumer has elected a new leader.
type JSConsumerLeaderElectedAdvisory struct {
TypedEvent
Account string `json:"account,omitempty"`
Stream string `json:"stream"`
Consumer string `json:"consumer"`
Leader string `json:"leader"`
Replicas []*PeerInfo `json:"replicas"`
Domain string `json:"domain,omitempty"`
}
// JSConsumerQuorumLostAdvisoryType is sent when the system detects a clustered consumer and
// is stalled and unable to make progress.
const JSConsumerQuorumLostAdvisoryType = "io.nats.jetstream.advisory.v1.consumer_quorum_lost"
// JSConsumerQuorumLostAdvisory indicates that a consumer has lost quorum and is stalled.
type JSConsumerQuorumLostAdvisory struct {
TypedEvent
Account string `json:"account,omitempty"`
Stream string `json:"stream"`
Consumer string `json:"consumer"`
Replicas []*PeerInfo `json:"replicas"`
Domain string `json:"domain,omitempty"`
}
const JSConsumerGroupPinnedAdvisoryType = "io.nats.jetstream.advisory.v1.consumer_group_pinned"
// JSConsumerGroupPinnedAdvisory that a group switched to a new pinned client
type JSConsumerGroupPinnedAdvisory struct {
TypedEvent
Account string `json:"account,omitempty"`
Stream string `json:"stream"`
Consumer string `json:"consumer"`
Domain string `json:"domain,omitempty"`
Group string `json:"group"`
PinnedClientId string `json:"pinned_id"`
}
const JSConsumerGroupUnpinnedAdvisoryType = "io.nats.jetstream.advisory.v1.consumer_group_unpinned"
// JSConsumerGroupUnpinnedAdvisory indicates that a pin was lost
type JSConsumerGroupUnpinnedAdvisory struct {
TypedEvent
Account string `json:"account,omitempty"`
Stream string `json:"stream"`
Consumer string `json:"consumer"`
Domain string `json:"domain,omitempty"`
Group string `json:"group"`
// one of "admin" or "timeout", could be an enum up to the implementor to decide
Reason string `json:"reason"`
}
// JSServerOutOfStorageAdvisoryType is sent when the server is out of storage space.
const JSServerOutOfStorageAdvisoryType = "io.nats.jetstream.advisory.v1.server_out_of_space"
// JSServerOutOfSpaceAdvisory indicates that a stream has lost quorum and is stalled.
type JSServerOutOfSpaceAdvisory struct {
TypedEvent
Server string `json:"server"`
ServerID string `json:"server_id"`
Stream string `json:"stream,omitempty"`
Cluster string `json:"cluster"`
Domain string `json:"domain,omitempty"`
}
// JSServerRemovedAdvisoryType is sent when the server has been removed and JS disabled.
const JSServerRemovedAdvisoryType = "io.nats.jetstream.advisory.v1.server_removed"
// JSServerRemovedAdvisory indicates that a stream has lost quorum and is stalled.
type JSServerRemovedAdvisory struct {
TypedEvent
Server string `json:"server"`
ServerID string `json:"server_id"`
Cluster string `json:"cluster"`
Domain string `json:"domain,omitempty"`
}
// JSAPILimitReachedAdvisoryType is sent when the JS API request queue limit is reached.
const JSAPILimitReachedAdvisoryType = "io.nats.jetstream.advisory.v1.api_limit_reached"
// JSAPILimitReachedAdvisory is a advisory published when JetStream hits the queue length limit.
type JSAPILimitReachedAdvisory struct {
TypedEvent
Server string `json:"server"` // Server that created the event, name or ID
Domain string `json:"domain,omitempty"` // Domain the server belongs to
Dropped int64 `json:"dropped"` // How many messages did we drop from the queue
}
@@ -0,0 +1,246 @@
// Copyright 2024-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 server
import "strconv"
const (
// JSApiLevel is the maximum supported JetStream API level for this server.
JSApiLevel int = 4
JSRequiredLevelMetadataKey = "_nats.req.level"
JSServerVersionMetadataKey = "_nats.ver"
JSServerLevelMetadataKey = "_nats.level"
)
// getRequiredApiLevel returns the required API level for the JetStream asset.
func getRequiredApiLevel(metadata map[string]string) string {
if l, ok := metadata[JSRequiredLevelMetadataKey]; ok && l != _EMPTY_ {
return l
}
return _EMPTY_
}
// supportsRequiredApiLevel returns whether the required API level for the JetStream asset is supported.
func supportsRequiredApiLevel(metadata map[string]string) bool {
if l := getRequiredApiLevel(metadata); l != _EMPTY_ {
li, err := strconv.Atoi(l)
return err == nil && li <= JSApiLevel
}
return true
}
// setStaticStreamMetadata sets JetStream stream metadata, like the server version and API level.
// Any dynamic metadata is removed, it must not be stored and only be added for responses.
func setStaticStreamMetadata(cfg *StreamConfig) {
if cfg.Metadata == nil {
cfg.Metadata = make(map[string]string)
} else {
deleteDynamicMetadata(cfg.Metadata)
}
var requiredApiLevel int
requires := func(level int) {
if level > requiredApiLevel {
requiredApiLevel = level
}
}
// TTLs were added in v2.11 and require API level 1.
if cfg.AllowMsgTTL || cfg.SubjectDeleteMarkerTTL > 0 {
requires(1)
}
// Counter CRDTs were added in v2.12 and require API level 2.
if cfg.AllowMsgCounter {
requires(2)
}
// Atomic batch publishing was added in v2.12 and require API level 2.
if cfg.AllowAtomicPublish {
requires(2)
}
// Message scheduling was added in v2.12 and require API level 2.
if cfg.AllowMsgSchedules {
requires(2)
}
// Async persist mode was added in v2.12 and requires API level 2.
if cfg.PersistMode == AsyncPersistMode {
requires(2)
}
// Fast batch publishing was added in v2.14 and requires API level 4.
if cfg.AllowBatchPublish {
requires(4)
}
cfg.Metadata[JSRequiredLevelMetadataKey] = strconv.Itoa(requiredApiLevel)
}
// setDynamicStreamMetadata adds dynamic fields into the (copied) metadata.
func setDynamicStreamMetadata(cfg *StreamConfig) *StreamConfig {
var newCfg StreamConfig
if cfg != nil {
newCfg = *cfg
}
newCfg.Metadata = make(map[string]string)
if cfg != nil {
for key, value := range cfg.Metadata {
newCfg.Metadata[key] = value
}
}
newCfg.Metadata[JSServerVersionMetadataKey] = VERSION
newCfg.Metadata[JSServerLevelMetadataKey] = strconv.Itoa(JSApiLevel)
return &newCfg
}
// copyConsumerMetadata copies versioning fields from metadata of prevCfg into cfg.
// Removes versioning fields if no previous metadata, updates if set, and removes fields if it doesn't exist in prevCfg.
// Any dynamic metadata is removed, it must not be stored and only be added for responses.
//
// Note: useful when doing equality checks on cfg and prevCfg, but ignoring any versioning metadata differences.
func copyStreamMetadata(cfg *StreamConfig, prevCfg *StreamConfig) {
if cfg.Metadata != nil {
deleteDynamicMetadata(cfg.Metadata)
}
setOrDeleteInStreamMetadata(cfg, prevCfg, JSRequiredLevelMetadataKey)
}
// setOrDeleteInConsumerMetadata sets field with key/value in metadata of cfg if set, deletes otherwise.
func setOrDeleteInStreamMetadata(cfg *StreamConfig, prevCfg *StreamConfig, key string) {
if prevCfg != nil && prevCfg.Metadata != nil {
if value, ok := prevCfg.Metadata[key]; ok {
if cfg.Metadata == nil {
cfg.Metadata = make(map[string]string)
}
cfg.Metadata[key] = value
return
}
}
delete(cfg.Metadata, key)
if len(cfg.Metadata) == 0 {
cfg.Metadata = nil
}
}
// setStaticConsumerMetadata sets JetStream consumer metadata, like the server version and API level.
// Any dynamic metadata is removed, it must not be stored and only be added for responses.
func setStaticConsumerMetadata(cfg *ConsumerConfig) {
if cfg.Metadata == nil {
cfg.Metadata = make(map[string]string)
} else {
deleteDynamicMetadata(cfg.Metadata)
}
var requiredApiLevel int
requires := func(level int) {
if level > requiredApiLevel {
requiredApiLevel = level
}
}
// Added in 2.11, absent | zero is the feature is not used.
// one could be stricter and say even if its set but the time
// has already passed it is also not needed to restore the consumer
if cfg.PauseUntil != nil && !cfg.PauseUntil.IsZero() {
requires(1)
}
if cfg.PriorityPolicy != PriorityNone || cfg.PinnedTTL != 0 || len(cfg.PriorityGroups) > 0 {
requires(1)
}
// Added in 2.14
if cfg.AckPolicy == AckFlowControl {
requires(4)
}
cfg.Metadata[JSRequiredLevelMetadataKey] = strconv.Itoa(requiredApiLevel)
}
// setDynamicConsumerMetadata adds dynamic fields into the (copied) metadata.
func setDynamicConsumerMetadata(cfg *ConsumerConfig) *ConsumerConfig {
var newCfg ConsumerConfig
if cfg != nil {
newCfg = *cfg
}
newCfg.Metadata = make(map[string]string)
if cfg != nil {
for key, value := range cfg.Metadata {
newCfg.Metadata[key] = value
}
}
newCfg.Metadata[JSServerVersionMetadataKey] = VERSION
newCfg.Metadata[JSServerLevelMetadataKey] = strconv.Itoa(JSApiLevel)
return &newCfg
}
// setDynamicConsumerInfoMetadata adds dynamic fields into the (copied) metadata.
func setDynamicConsumerInfoMetadata(info *ConsumerInfo) *ConsumerInfo {
if info == nil {
return nil
}
newInfo := *info
cfg := setDynamicConsumerMetadata(info.Config)
newInfo.Config = cfg
return &newInfo
}
// copyConsumerMetadata copies versioning fields from metadata of prevCfg into cfg.
// Removes versioning fields if no previous metadata, updates if set, and removes fields if it doesn't exist in prevCfg.
// Any dynamic metadata is removed, it must not be stored and only be added for responses.
//
// Note: useful when doing equality checks on cfg and prevCfg, but ignoring any versioning metadata differences.
func copyConsumerMetadata(cfg *ConsumerConfig, prevCfg *ConsumerConfig) {
if cfg.Metadata != nil {
deleteDynamicMetadata(cfg.Metadata)
}
setOrDeleteInConsumerMetadata(cfg, prevCfg, JSRequiredLevelMetadataKey)
}
// setOrDeleteInConsumerMetadata sets field with key/value in metadata of cfg if set, deletes otherwise.
func setOrDeleteInConsumerMetadata(cfg *ConsumerConfig, prevCfg *ConsumerConfig, key string) {
if prevCfg != nil && prevCfg.Metadata != nil {
if value, ok := prevCfg.Metadata[key]; ok {
if cfg.Metadata == nil {
cfg.Metadata = make(map[string]string)
}
cfg.Metadata[key] = value
return
}
}
delete(cfg.Metadata, key)
if len(cfg.Metadata) == 0 {
cfg.Metadata = nil
}
}
// deleteDynamicMetadata deletes dynamic fields from the metadata.
func deleteDynamicMetadata(metadata map[string]string) {
delete(metadata, JSServerVersionMetadataKey)
delete(metadata, JSServerLevelMetadataKey)
}
// errorOnRequiredApiLevel returns whether a request should be rejected based on the JSRequiredApiLevel header.
func errorOnRequiredApiLevel(hdr []byte) bool {
reqApiLevel := sliceHeader(JSRequiredApiLevel, hdr)
if len(reqApiLevel) == 0 {
return false
}
minLevel, err := strconv.Atoi(string(reqApiLevel))
return err != nil || JSApiLevel < minLevel
}
+275
View File
@@ -0,0 +1,275 @@
// Copyright 2018-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 server
import (
"errors"
"fmt"
"net"
"os"
"strings"
"time"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nkeys"
)
// All JWTs once encoded start with this
const jwtPrefix = "eyJ"
// ReadOperatorJWT will read a jwt file for an operator claim. This can be a decorated file.
func ReadOperatorJWT(jwtfile string) (*jwt.OperatorClaims, error) {
_, claim, err := readOperatorJWT(jwtfile)
return claim, err
}
func readOperatorJWT(jwtfile string) (string, *jwt.OperatorClaims, error) {
contents, err := os.ReadFile(jwtfile)
if err != nil {
// Check to see if the JWT has been inlined.
if !strings.HasPrefix(jwtfile, jwtPrefix) {
return "", nil, err
}
// We may have an inline jwt here.
contents = []byte(jwtfile)
}
defer wipeSlice(contents)
theJWT, err := jwt.ParseDecoratedJWT(contents)
if err != nil {
return "", nil, err
}
opc, err := jwt.DecodeOperatorClaims(theJWT)
if err != nil {
return "", nil, err
}
return theJWT, opc, nil
}
// Just wipe slice with 'x', for clearing contents of nkey seed file.
func wipeSlice(buf []byte) {
for i := range buf {
buf[i] = 'x'
}
}
// validateTrustedOperators will check that we do not have conflicts with
// assigned trusted keys and trusted operators. If operators are defined we
// will expand the trusted keys in options.
func validateTrustedOperators(o *Options) error {
if len(o.TrustedOperators) == 0 {
// if we have no operator, default sentinel shouldn't be set
if o.DefaultSentinel != _EMPTY_ {
return fmt.Errorf("default sentinel requires operators and accounts")
}
return nil
}
if o.DefaultSentinel != _EMPTY_ {
juc, err := jwt.DecodeUserClaims(o.DefaultSentinel)
if err != nil {
return fmt.Errorf("default sentinel JWT not valid")
}
if !juc.BearerToken && juc.IssuerAccount != "" && juc.HasEmptyPermissions() {
// we cannot resolve the account yet - but this looks like a scoped user
// it will be rejected at runtime if not valid
} else if !juc.BearerToken {
return fmt.Errorf("default sentinel must be a bearer token")
}
}
if o.AccountResolver == nil {
return fmt.Errorf("operators require an account resolver to be configured")
}
if len(o.Accounts) > 0 {
return fmt.Errorf("operators do not allow Accounts to be configured directly")
}
if len(o.Users) > 0 || len(o.Nkeys) > 0 {
return fmt.Errorf("operators do not allow users to be configured directly")
}
if len(o.TrustedOperators) > 0 && len(o.TrustedKeys) > 0 {
return fmt.Errorf("conflicting options for 'TrustedKeys' and 'TrustedOperators'")
}
if o.SystemAccount != _EMPTY_ {
foundSys := false
foundNonEmpty := false
for _, op := range o.TrustedOperators {
if op.SystemAccount != _EMPTY_ {
foundNonEmpty = true
}
if op.SystemAccount == o.SystemAccount {
foundSys = true
break
}
}
if foundNonEmpty && !foundSys {
return fmt.Errorf("system_account in config and operator JWT must be identical")
}
} else if o.TrustedOperators[0].SystemAccount == _EMPTY_ {
// In case the system account is neither defined in config nor in the first operator.
// If it would be needed due to the nats account resolver, raise an error.
switch o.AccountResolver.(type) {
case *DirAccResolver, *CacheDirAccResolver:
return fmt.Errorf("using nats based account resolver - the system account needs to be specified in configuration or the operator jwt")
}
}
srvMajor, srvMinor, srvUpdate, _ := versionComponents(VERSION)
for _, opc := range o.TrustedOperators {
if major, minor, update, err := jwt.ParseServerVersion(opc.AssertServerVersion); err != nil {
return fmt.Errorf("operator %s expects version %s got error instead: %s",
opc.Subject, opc.AssertServerVersion, err)
} else if major > srvMajor {
return fmt.Errorf("operator %s expected major version %d > server major version %d",
opc.Subject, major, srvMajor)
} else if srvMajor > major {
} else if minor > srvMinor {
return fmt.Errorf("operator %s expected minor version %d > server minor version %d",
opc.Subject, minor, srvMinor)
} else if srvMinor > minor {
} else if update > srvUpdate {
return fmt.Errorf("operator %s expected update version %d > server update version %d",
opc.Subject, update, srvUpdate)
}
}
// If we have operators, fill in the trusted keys.
// FIXME(dlc) - We had TrustedKeys before TrustedOperators. The jwt.OperatorClaims
// has a DidSign(). Use that longer term. For now we can expand in place.
for _, opc := range o.TrustedOperators {
if o.TrustedKeys == nil {
o.TrustedKeys = make([]string, 0, 4)
}
if !opc.StrictSigningKeyUsage {
o.TrustedKeys = append(o.TrustedKeys, opc.Subject)
}
o.TrustedKeys = append(o.TrustedKeys, opc.SigningKeys...)
}
for _, key := range o.TrustedKeys {
if !nkeys.IsValidPublicOperatorKey(key) {
return fmt.Errorf("trusted Keys %q are required to be a valid public operator nkey", key)
}
}
if len(o.resolverPinnedAccounts) > 0 {
for key := range o.resolverPinnedAccounts {
if !nkeys.IsValidPublicAccountKey(key) {
return fmt.Errorf("pinned account key %q is not a valid public account nkey", key)
}
}
// ensure the system account (belonging to the operator can always connect)
if o.SystemAccount != _EMPTY_ {
o.resolverPinnedAccounts[o.SystemAccount] = struct{}{}
}
}
// If we have an auth callout defined make sure we are not in operator mode.
if o.AuthCallout != nil {
return errors.New("operators do not allow authorization callouts to be configured directly")
}
return nil
}
func validateSrc(claims *jwt.UserClaims, host string) bool {
if claims == nil {
return false
} else if len(claims.Src) == 0 {
return true
} else if host == "" {
return false
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
for _, cidr := range claims.Src {
if _, net, err := net.ParseCIDR(cidr); err != nil {
return false // should not happen as this jwt is invalid
} else if net.Contains(ip) {
return true
}
}
return false
}
func validateTimes(claims *jwt.UserClaims) (bool, time.Duration) {
return validateTimesAt(claims, time.Now())
}
func validateTimesAt(claims *jwt.UserClaims, now time.Time) (bool, time.Duration) {
if claims == nil {
return false, time.Duration(0)
} else if len(claims.Times) == 0 {
return true, time.Duration(0)
}
loc := time.Local
if claims.Locale != "" {
var err error
if loc, err = time.LoadLocation(claims.Locale); err != nil {
return false, time.Duration(0) // parsing not expected to fail at this point
}
now = now.In(loc)
}
var ok bool
var validFor time.Duration
for _, timeRange := range claims.Times {
start, err := time.ParseInLocation("15:04:05", timeRange.Start, loc)
if err != nil {
return false, time.Duration(0) // parsing not expected to fail at this point
}
end, err := time.ParseInLocation("15:04:05", timeRange.End, loc)
if err != nil {
return false, time.Duration(0) // parsing not expected to fail at this point
}
y, m, d := now.Date()
start = time.Date(y, m, d, start.Hour(), start.Minute(), start.Second(), 0, loc)
end = time.Date(y, m, d, end.Hour(), end.Minute(), end.Second(), 0, loc)
inRange, expires := validateTimeRangeAt(start, end, now)
if inRange && (!ok || expires > validFor) {
ok = true
validFor = expires
}
}
return ok, validFor
}
// Returns true if now is within `start` and `end`, and
// how much time is left until `end`.
// False if `now` is not within range.
func validateTimeRangeAt(start, end, now time.Time) (bool, time.Duration) {
// Now falls within range.
// For example 11:00-22:00 at 13:00
if start.Before(now) && end.After(now) {
return true, end.Sub(now)
}
// Range crosses midnight.
if start.After(end) {
// Now is after midnight.
// For example 22:00-06:00 at 05:00.
if end.After(now) {
return true, end.Sub(now)
}
// Now is before midnight.
// For example 22:00-06:00 at 23:30.
end = end.AddDate(0, 0, 1)
if start.Before(now) && end.After(now) {
return true, end.Sub(now)
}
}
return false, time.Duration(0)
}
File diff suppressed because it is too large Load Diff
+295
View File
@@ -0,0 +1,295 @@
// Copyright 2012-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 server
import (
"fmt"
"io"
"os"
"sync/atomic"
"time"
srvlog "github.com/nats-io/nats-server/v2/logger"
)
// Logger interface of the NATS Server
type Logger interface {
// Log a notice statement
Noticef(format string, v ...any)
// Log a warning statement
Warnf(format string, v ...any)
// Log a fatal error
Fatalf(format string, v ...any)
// Log an error
Errorf(format string, v ...any)
// Log a debug statement
Debugf(format string, v ...any)
// Log a trace statement
Tracef(format string, v ...any)
}
// ConfigureLogger configures and sets the logger for the server.
func (s *Server) ConfigureLogger() {
var (
log Logger
// Snapshot server options.
opts = s.getOpts()
)
if opts.NoLog {
return
}
syslog := opts.Syslog
if isWindowsService() && opts.LogFile == "" {
// Enable syslog if no log file is specified and we're running as a
// Windows service so that logs are written to the Windows event log.
syslog = true
}
if opts.LogFile != "" {
log = srvlog.NewFileLogger(opts.LogFile, opts.Logtime, opts.Debug, opts.Trace, true, srvlog.LogUTC(opts.LogtimeUTC))
if opts.LogSizeLimit > 0 {
if l, ok := log.(*srvlog.Logger); ok {
l.SetSizeLimit(opts.LogSizeLimit)
}
}
if opts.LogMaxFiles > 0 {
if l, ok := log.(*srvlog.Logger); ok {
al := int(opts.LogMaxFiles)
if int64(al) != opts.LogMaxFiles {
// set to default (no max) on overflow
al = 0
}
l.SetMaxNumFiles(al)
}
}
} else if opts.RemoteSyslog != "" {
log = srvlog.NewRemoteSysLogger(opts.RemoteSyslog, opts.Debug, opts.Trace)
} else if syslog {
log = srvlog.NewSysLogger(opts.Debug, opts.Trace)
} else {
colors := true
// Check to see if stderr is being redirected and if so turn off color
// Also turn off colors if we're running on Windows where os.Stderr.Stat() returns an invalid handle-error
stat, err := os.Stderr.Stat()
if err != nil || (stat.Mode()&os.ModeCharDevice) == 0 {
colors = false
}
log = srvlog.NewStdLogger(opts.Logtime, opts.Debug, opts.Trace, colors, true, srvlog.LogUTC(opts.LogtimeUTC))
}
s.SetLoggerV2(log, opts.Debug, opts.Trace, opts.TraceVerbose)
}
// Returns our current logger.
func (s *Server) Logger() Logger {
s.logging.Lock()
defer s.logging.Unlock()
return s.logging.logger
}
// SetLogger sets the logger of the server
func (s *Server) SetLogger(logger Logger, debugFlag, traceFlag bool) {
s.SetLoggerV2(logger, debugFlag, traceFlag, false)
}
// SetLogger sets the logger of the server
func (s *Server) SetLoggerV2(logger Logger, debugFlag, traceFlag, sysTrace bool) {
if debugFlag {
atomic.StoreInt32(&s.logging.debug, 1)
} else {
atomic.StoreInt32(&s.logging.debug, 0)
}
if traceFlag {
atomic.StoreInt32(&s.logging.trace, 1)
} else {
atomic.StoreInt32(&s.logging.trace, 0)
}
if sysTrace {
atomic.StoreInt32(&s.logging.traceSysAcc, 1)
} else {
atomic.StoreInt32(&s.logging.traceSysAcc, 0)
}
s.logging.Lock()
if s.logging.logger != nil {
// Check to see if the logger implements io.Closer. This could be a
// logger from another process embedding the NATS server or a dummy
// test logger that may not implement that interface.
if l, ok := s.logging.logger.(io.Closer); ok {
if err := l.Close(); err != nil {
s.Errorf("Error closing logger: %v", err)
}
}
}
s.logging.logger = logger
s.logging.Unlock()
}
// ReOpenLogFile if the logger is a file based logger, close and re-open the file.
// This allows for file rotation by 'mv'ing the file then signaling
// the process to trigger this function.
func (s *Server) ReOpenLogFile() {
// Check to make sure this is a file logger.
s.logging.RLock()
ll := s.logging.logger
s.logging.RUnlock()
if ll == nil {
s.Noticef("File log re-open ignored, no logger")
return
}
// Snapshot server options.
opts := s.getOpts()
if opts.LogFile == "" {
s.Noticef("File log re-open ignored, not a file logger")
} else {
fileLog := srvlog.NewFileLogger(
opts.LogFile, opts.Logtime,
opts.Debug, opts.Trace, true,
srvlog.LogUTC(opts.LogtimeUTC),
)
s.SetLogger(fileLog, opts.Debug, opts.Trace)
if opts.LogSizeLimit > 0 {
fileLog.SetSizeLimit(opts.LogSizeLimit)
}
s.Noticef("File log re-opened")
}
}
// Noticef logs a notice statement
func (s *Server) Noticef(format string, v ...any) {
s.executeLogCall(func(logger Logger, format string, v ...any) {
logger.Noticef(format, v...)
}, format, v...)
}
// Errorf logs an error
func (s *Server) Errorf(format string, v ...any) {
s.executeLogCall(func(logger Logger, format string, v ...any) {
logger.Errorf(format, v...)
}, format, v...)
}
// Error logs an error with a scope
func (s *Server) Errors(scope any, e error) {
s.executeLogCall(func(logger Logger, format string, v ...any) {
logger.Errorf(format, v...)
}, "%s - %s", scope, UnpackIfErrorCtx(e))
}
// Error logs an error with a context
func (s *Server) Errorc(ctx string, e error) {
s.executeLogCall(func(logger Logger, format string, v ...any) {
logger.Errorf(format, v...)
}, "%s: %s", ctx, UnpackIfErrorCtx(e))
}
// Error logs an error with a scope and context
func (s *Server) Errorsc(scope any, ctx string, e error) {
s.executeLogCall(func(logger Logger, format string, v ...any) {
logger.Errorf(format, v...)
}, "%s - %s: %s", scope, ctx, UnpackIfErrorCtx(e))
}
// Warnf logs a warning error
func (s *Server) Warnf(format string, v ...any) {
s.executeLogCall(func(logger Logger, format string, v ...any) {
logger.Warnf(format, v...)
}, format, v...)
}
func (s *Server) rateLimitFormatWarnf(format string, v ...any) {
if _, loaded := s.rateLimitLogging.LoadOrStore(format, time.Now()); loaded {
return
}
statement := fmt.Sprintf(format, v...)
s.Warnf("%s", statement)
}
func (s *Server) RateLimitErrorf(format string, v ...any) {
statement := fmt.Sprintf(format, v...)
if _, loaded := s.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded {
return
}
s.Errorf("%s", statement)
}
func (s *Server) RateLimitWarnf(format string, v ...any) {
statement := fmt.Sprintf(format, v...)
if _, loaded := s.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded {
return
}
s.Warnf("%s", statement)
}
func (s *Server) RateLimitDebugf(format string, v ...any) {
statement := fmt.Sprintf(format, v...)
if _, loaded := s.rateLimitLogging.LoadOrStore(statement, time.Now()); loaded {
return
}
s.Debugf("%s", statement)
}
// Fatalf logs a fatal error
func (s *Server) Fatalf(format string, v ...any) {
if s.isShuttingDown() {
s.Errorf(format, v)
return
}
s.executeLogCall(func(logger Logger, format string, v ...any) {
logger.Fatalf(format, v...)
}, format, v...)
}
// Debugf logs a debug statement
func (s *Server) Debugf(format string, v ...any) {
if atomic.LoadInt32(&s.logging.debug) == 0 {
return
}
s.executeLogCall(func(logger Logger, format string, v ...any) {
logger.Debugf(format, v...)
}, format, v...)
}
// Tracef logs a trace statement
func (s *Server) Tracef(format string, v ...any) {
if atomic.LoadInt32(&s.logging.trace) == 0 {
return
}
s.executeLogCall(func(logger Logger, format string, v ...any) {
logger.Tracef(format, v...)
}, format, v...)
}
func (s *Server) executeLogCall(f func(logger Logger, format string, v ...any), format string, args ...any) {
s.logging.RLock()
defer s.logging.RUnlock()
if s.logging.logger == nil {
return
}
f(s.logging.logger, format, args...)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,156 @@
// Copyright 2013-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 server
import (
"time"
)
// ConnInfos represents a connection info list. We use pointers since it will be sorted.
type ConnInfos []*ConnInfo
// For sorting
// Len returns length for sorting.
func (cl ConnInfos) Len() int { return len(cl) }
// Swap will sawap the elements.
func (cl ConnInfos) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
// SortOpt is a helper type to sort clients
type SortOpt string
// Possible sort options
const (
ByCid SortOpt = "cid" // By connection ID
ByStart SortOpt = "start" // By connection start time, same as CID
BySubs SortOpt = "subs" // By number of subscriptions
ByPending SortOpt = "pending" // By amount of data in bytes waiting to be sent to client
ByOutMsgs SortOpt = "msgs_to" // By number of messages sent
ByInMsgs SortOpt = "msgs_from" // By number of messages received
ByOutBytes SortOpt = "bytes_to" // By amount of bytes sent
ByInBytes SortOpt = "bytes_from" // By amount of bytes received
ByLast SortOpt = "last" // By the last activity
ByIdle SortOpt = "idle" // By the amount of inactivity
ByUptime SortOpt = "uptime" // By the amount of time connections exist
ByStop SortOpt = "stop" // By the stop time for a closed connection
ByReason SortOpt = "reason" // By the reason for a closed connection
ByRTT SortOpt = "rtt" // By the round trip time
)
// Individual sort options provide the Less for sort.Interface. Len and Swap are on cList.
// CID
type SortByCid struct{ ConnInfos }
func (l SortByCid) Less(i, j int) bool { return l.ConnInfos[i].Cid < l.ConnInfos[j].Cid }
// Number of Subscriptions
type SortBySubs struct{ ConnInfos }
func (l SortBySubs) Less(i, j int) bool { return l.ConnInfos[i].NumSubs < l.ConnInfos[j].NumSubs }
// Pending Bytes
type SortByPending struct{ ConnInfos }
func (l SortByPending) Less(i, j int) bool { return l.ConnInfos[i].Pending < l.ConnInfos[j].Pending }
// Outbound Msgs
type SortByOutMsgs struct{ ConnInfos }
func (l SortByOutMsgs) Less(i, j int) bool { return l.ConnInfos[i].OutMsgs < l.ConnInfos[j].OutMsgs }
// Inbound Msgs
type SortByInMsgs struct{ ConnInfos }
func (l SortByInMsgs) Less(i, j int) bool { return l.ConnInfos[i].InMsgs < l.ConnInfos[j].InMsgs }
// Outbound Bytes
type SortByOutBytes struct{ ConnInfos }
func (l SortByOutBytes) Less(i, j int) bool { return l.ConnInfos[i].OutBytes < l.ConnInfos[j].OutBytes }
// Inbound Bytes
type SortByInBytes struct{ ConnInfos }
func (l SortByInBytes) Less(i, j int) bool { return l.ConnInfos[i].InBytes < l.ConnInfos[j].InBytes }
// Last Activity
type SortByLast struct{ ConnInfos }
func (l SortByLast) Less(i, j int) bool {
return l.ConnInfos[i].LastActivity.UnixNano() < l.ConnInfos[j].LastActivity.UnixNano()
}
// Idle time
type SortByIdle struct {
ConnInfos
now time.Time
}
func (l SortByIdle) Less(i, j int) bool {
return l.now.Sub(l.ConnInfos[i].LastActivity) < l.now.Sub(l.ConnInfos[j].LastActivity)
}
// Uptime
type SortByUptime struct {
ConnInfos
now time.Time
}
func (l SortByUptime) Less(i, j int) bool {
ci := l.ConnInfos[i]
cj := l.ConnInfos[j]
var upi, upj time.Duration
if ci.Stop == nil || ci.Stop.IsZero() {
upi = l.now.Sub(ci.Start)
} else {
upi = ci.Stop.Sub(ci.Start)
}
if cj.Stop == nil || cj.Stop.IsZero() {
upj = l.now.Sub(cj.Start)
} else {
upj = cj.Stop.Sub(cj.Start)
}
return upi < upj
}
// Stop
type SortByStop struct{ ConnInfos }
func (l SortByStop) Less(i, j int) bool {
ciStop := l.ConnInfos[i].Stop
cjStop := l.ConnInfos[j].Stop
return ciStop.Before(*cjStop)
}
// Reason
type SortByReason struct{ ConnInfos }
func (l SortByReason) Less(i, j int) bool {
return l.ConnInfos[i].Reason < l.ConnInfos[j].Reason
}
// RTT - Default is descending
type SortByRTT struct{ ConnInfos }
func (l SortByRTT) Less(i, j int) bool { return l.ConnInfos[i].rtt < l.ConnInfos[j].rtt }
// IsValid determines if a sort option is valid
func (s SortOpt) IsValid() bool {
switch s {
case _EMPTY_, ByCid, ByStart, BySubs, ByPending, ByOutMsgs, ByInMsgs, ByOutBytes, ByInBytes, ByLast, ByIdle, ByUptime, ByStop, ByReason, ByRTT:
return true
default:
return false
}
}
File diff suppressed because it is too large Load Diff
+806
View File
@@ -0,0 +1,806 @@
// Copyright 2024-2026 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 server
import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
"strconv"
"strings"
"sync/atomic"
"time"
)
const (
MsgTraceDest = "Nats-Trace-Dest"
MsgTraceDestDisabled = "trace disabled" // This must be an invalid NATS subject
MsgTraceHop = "Nats-Trace-Hop"
MsgTraceOriginAccount = "Nats-Trace-Origin-Account"
MsgTraceOnly = "Nats-Trace-Only"
// External trace header. Note that this header is normally in lower
// case (https://www.w3.org/TR/trace-context/#header-name). Vendors
// MUST expect the header in any case (upper, lower, mixed), and
// SHOULD send the header name in lowercase. We used to change it
// to lower case, but no longer do that in 2.14.
traceParentHdr = "traceparent"
)
var (
traceDestHdrAsBytes = stringToBytes(MsgTraceDest)
traceDestDisabledAsBytes = stringToBytes(MsgTraceDestDisabled)
traceParentHdrAsBytes = stringToBytes(traceParentHdr)
crLFAsBytes = stringToBytes(CR_LF)
dashAsBytes = stringToBytes("-")
)
type MsgTraceType string
// Type of message trace events in the MsgTraceEvents list.
// This is needed to unmarshal the list.
const (
MsgTraceIngressType = "in"
MsgTraceSubjectMappingType = "sm"
MsgTraceStreamExportType = "se"
MsgTraceServiceImportType = "si"
MsgTraceJetStreamType = "js"
MsgTraceEgressType = "eg"
)
type MsgTraceEvent struct {
Server ServerInfo `json:"server"`
Request MsgTraceRequest `json:"request"`
Hops int `json:"hops,omitempty"`
Events MsgTraceEvents `json:"events"`
}
type MsgTraceRequest struct {
// We are not making this an http.Header so that header name case is preserved.
Header map[string][]string `json:"header,omitempty"`
MsgSize int `json:"msgsize,omitempty"`
}
type MsgTraceEvents []MsgTrace
type MsgTrace interface {
new() MsgTrace
typ() MsgTraceType
}
type MsgTraceBase struct {
Type MsgTraceType `json:"type"`
Timestamp time.Time `json:"ts"`
}
type MsgTraceIngress struct {
MsgTraceBase
Kind int `json:"kind"`
CID uint64 `json:"cid"`
Name string `json:"name,omitempty"`
Account string `json:"acc"`
Subject string `json:"subj"`
Error string `json:"error,omitempty"`
}
type MsgTraceSubjectMapping struct {
MsgTraceBase
MappedTo string `json:"to"`
}
type MsgTraceStreamExport struct {
MsgTraceBase
Account string `json:"acc"`
To string `json:"to"`
}
type MsgTraceServiceImport struct {
MsgTraceBase
Account string `json:"acc"`
From string `json:"from"`
To string `json:"to"`
}
type MsgTraceJetStream struct {
MsgTraceBase
Stream string `json:"stream"`
Subject string `json:"subject,omitempty"`
NoInterest bool `json:"nointerest,omitempty"`
Error string `json:"error,omitempty"`
}
type MsgTraceEgress struct {
MsgTraceBase
Kind int `json:"kind"`
CID uint64 `json:"cid"`
Name string `json:"name,omitempty"`
Hop string `json:"hop,omitempty"`
Account string `json:"acc,omitempty"`
Subscription string `json:"sub,omitempty"`
Queue string `json:"queue,omitempty"`
Error string `json:"error,omitempty"`
// This is for applications that unmarshal the trace events
// and want to link an egress to route/leaf/gateway with
// the MsgTraceEvent from that server.
Link *MsgTraceEvent `json:"-"`
}
// -------------------------------------------------------------
func (t MsgTraceBase) typ() MsgTraceType { return t.Type }
func (MsgTraceIngress) new() MsgTrace { return &MsgTraceIngress{} }
func (MsgTraceSubjectMapping) new() MsgTrace { return &MsgTraceSubjectMapping{} }
func (MsgTraceStreamExport) new() MsgTrace { return &MsgTraceStreamExport{} }
func (MsgTraceServiceImport) new() MsgTrace { return &MsgTraceServiceImport{} }
func (MsgTraceJetStream) new() MsgTrace { return &MsgTraceJetStream{} }
func (MsgTraceEgress) new() MsgTrace { return &MsgTraceEgress{} }
var msgTraceInterfaces = map[MsgTraceType]MsgTrace{
MsgTraceIngressType: MsgTraceIngress{},
MsgTraceSubjectMappingType: MsgTraceSubjectMapping{},
MsgTraceStreamExportType: MsgTraceStreamExport{},
MsgTraceServiceImportType: MsgTraceServiceImport{},
MsgTraceJetStreamType: MsgTraceJetStream{},
MsgTraceEgressType: MsgTraceEgress{},
}
func (t *MsgTraceEvents) UnmarshalJSON(data []byte) error {
var raw []json.RawMessage
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}
*t = make(MsgTraceEvents, len(raw))
var tt MsgTraceBase
for i, r := range raw {
if err = json.Unmarshal(r, &tt); err != nil {
return err
}
tr, ok := msgTraceInterfaces[tt.Type]
if !ok {
return fmt.Errorf("unknown trace type %v", tt.Type)
}
te := tr.new()
if err := json.Unmarshal(r, te); err != nil {
return err
}
(*t)[i] = te
}
return nil
}
func getTraceAs[T MsgTrace](e any) *T {
v, ok := e.(*T)
if ok {
return v
}
return nil
}
func (t *MsgTraceEvent) Ingress() *MsgTraceIngress {
if len(t.Events) < 1 {
return nil
}
return getTraceAs[MsgTraceIngress](t.Events[0])
}
func (t *MsgTraceEvent) SubjectMapping() *MsgTraceSubjectMapping {
for _, e := range t.Events {
if e.typ() == MsgTraceSubjectMappingType {
return getTraceAs[MsgTraceSubjectMapping](e)
}
}
return nil
}
func (t *MsgTraceEvent) StreamExports() []*MsgTraceStreamExport {
var se []*MsgTraceStreamExport
for _, e := range t.Events {
if e.typ() == MsgTraceStreamExportType {
se = append(se, getTraceAs[MsgTraceStreamExport](e))
}
}
return se
}
func (t *MsgTraceEvent) ServiceImports() []*MsgTraceServiceImport {
var si []*MsgTraceServiceImport
for _, e := range t.Events {
if e.typ() == MsgTraceServiceImportType {
si = append(si, getTraceAs[MsgTraceServiceImport](e))
}
}
return si
}
func (t *MsgTraceEvent) JetStream() *MsgTraceJetStream {
for _, e := range t.Events {
if e.typ() == MsgTraceJetStreamType {
return getTraceAs[MsgTraceJetStream](e)
}
}
return nil
}
func (t *MsgTraceEvent) Egresses() []*MsgTraceEgress {
var eg []*MsgTraceEgress
for _, e := range t.Events {
if e.typ() == MsgTraceEgressType {
eg = append(eg, getTraceAs[MsgTraceEgress](e))
}
}
return eg
}
const (
errMsgTraceOnlyNoSupport = "Not delivered because remote does not support message tracing"
errMsgTraceNoSupport = "Message delivered but remote does not support message tracing so no trace event generated from there"
errMsgTraceNoEcho = "Not delivered because of no echo"
errMsgTracePubViolation = "Not delivered because publish denied for this subject"
errMsgTraceSubDeny = "Not delivered because subscription denies this subject"
errMsgTraceSubClosed = "Not delivered because subscription is closed"
errMsgTraceClientClosed = "Not delivered because client is closed"
errMsgTraceAutoSubExceeded = "Not delivered because auto-unsubscribe exceeded"
errMsgTraceFastProdNoStall = "Not delivered because fast producer not stalled and consumer is slow"
)
type msgTrace struct {
ready int32
srv *Server
acc *Account
// Origin account name, set only if acc is nil when acc lookup failed.
oan string
dest string
event *MsgTraceEvent
js *MsgTraceJetStream
hop string
nhop string
tonly bool // Will only trace the message, not do delivery.
ct compressionType
}
// This will be false outside of the tests, so when building the server binary,
// any code where you see `if msgTraceRunInTests` statement will be compiled
// out, so this will have no performance penalty.
var (
msgTraceRunInTests bool
msgTraceCheckSupport bool
)
// Returns the message trace object, if message is being traced,
// and `true` if we want to only trace, not actually deliver the message.
func (c *client) isMsgTraceEnabled() (*msgTrace, bool) {
t := c.pa.trace
if t == nil {
return nil, false
}
return t, t.tonly
}
// For LEAF/ROUTER/GATEWAY, return false if the remote does not support
// message tracing (important if the tracing requests trace-only).
func (c *client) msgTraceSupport() bool {
// Exclude client connection from the protocol check.
return c.kind == CLIENT || c.opts.Protocol >= MsgTraceProto
}
func getConnName(c *client) string {
switch c.kind {
case ROUTER:
if n := c.route.remoteName; n != _EMPTY_ {
return n
}
case GATEWAY:
if n := c.gw.remoteName; n != _EMPTY_ {
return n
}
case LEAF:
if n := c.leaf.remoteServer; n != _EMPTY_ {
return n
}
}
return c.opts.Name
}
func getCompressionType(cts string) compressionType {
if cts == _EMPTY_ {
return noCompression
}
cts = strings.ToLower(cts)
if strings.Contains(cts, "snappy") || strings.Contains(cts, "s2") {
return snappyCompression
}
if strings.Contains(cts, "gzip") {
return gzipCompression
}
return unsupportedCompression
}
func (c *client) initMsgTrace() *msgTrace {
// The code in the "if" statement is only running in test mode.
if msgTraceRunInTests {
// Check the type of client that tries to initialize a trace struct.
if !(c.kind == CLIENT || c.kind == ROUTER || c.kind == GATEWAY || c.kind == LEAF) {
panic(fmt.Sprintf("Unexpected client type %q trying to initialize msgTrace", c.kindString()))
}
// In some tests, we want to make a server behave like an old server
// and so even if a trace header is received, we want the server to
// simply ignore it.
if msgTraceCheckSupport {
if c.srv == nil || c.srv.getServerProto() < MsgTraceProto {
return nil
}
}
}
if c.pa.hdr <= 0 {
return nil
}
hdr := c.msgBuf[:c.pa.hdr]
headers, external := genHeaderMapIfTraceHeadersPresent(hdr)
if len(headers) == 0 {
return nil
}
// Little helper to give us the first value of a given header, or _EMPTY_
// if key is not present.
getHdrVal := func(key string) string {
vv, ok := headers[key]
if !ok {
return _EMPTY_
}
return vv[0]
}
var (
dest string
traceOnly bool
)
// Check for traceOnly only if not external.
if !external {
if to := getHdrVal(MsgTraceOnly); to != _EMPTY_ {
tos := strings.ToLower(to)
switch tos {
case "1", "true", "on":
traceOnly = true
}
}
dest = getHdrVal(MsgTraceDest)
if c.kind == CLIENT {
if td, ok := c.allowedMsgTraceDest(hdr, false); !ok {
return nil
} else if td != _EMPTY_ {
dest = td
}
}
// Check the destination to see if this is a valid public subject.
if !IsValidPublishSubject(dest) {
// We still have to return a msgTrace object (if traceOnly is set)
// because if we don't, the message will end-up being delivered to
// applications, which may break them. We report the error in any case.
c.Errorf("Destination %q is not valid, won't be able to trace events", dest)
if !traceOnly {
// We can bail, tracing will be disabled for this message.
return nil
}
}
}
var (
// Account to use when sending the trace event
acc *Account
// Ingress' account name
ian string
// Origin account name
oan string
// The hop "id", taken from headers only when not from CLIENT
hop string
)
if c.kind == ROUTER || c.kind == GATEWAY || c.kind == LEAF {
// The ingress account name will always be c.pa.account, but `acc` may
// be different if we have an origin account header.
if c.kind == LEAF {
ian = c.acc.GetName()
} else {
ian = string(c.pa.account)
}
// The remote will have set the origin account header only if the
// message changed account (think of service imports).
oan = getHdrVal(MsgTraceOriginAccount)
if oan == _EMPTY_ {
// For LEAF or ROUTER with pinned-account, we can use the c.acc.
if c.kind == LEAF || (c.kind == ROUTER && len(c.route.accName) > 0) {
acc = c.acc
} else {
// We will lookup account with c.pa.account (or ian).
oan = ian
}
}
// Unless we already got the account, we need to look it up.
if acc == nil {
// We don't want to do account resolving here.
if acci, ok := c.srv.accounts.Load(oan); ok {
acc = acci.(*Account)
// Since we have looked-up the account, we don't need oan, so
// clear it in case it was set.
oan = _EMPTY_
} else {
// We still have to return a msgTrace object (if traceOnly is set)
// because if we don't, the message will end-up being delivered to
// applications, which may break them. We report the error in any case.
c.Errorf("Account %q was not found, won't be able to trace events", oan)
if !traceOnly {
// We can bail, tracing will be disabled for this message.
return nil
}
}
}
// Check the hop header
hop = getHdrVal(MsgTraceHop)
} else {
acc = c.acc
ian = acc.GetName()
}
// If external, we need to have the account's trace destination set,
// otherwise, we are not enabling tracing.
if external {
var sampling int
if acc != nil {
dest, sampling = acc.getTraceDestAndSampling()
}
if dest == _EMPTY_ {
// No account destination, no tracing for external trace headers.
return nil
}
// Check sampling, but only from origin server.
if c.kind == CLIENT && !sample(sampling) {
// Need to disable tracing so that if the message is routed, it won't
// trigger a trace there.
c.msgBuf = c.setHeader(MsgTraceDest, MsgTraceDestDisabled, c.msgBuf)
return nil
}
}
c.pa.trace = &msgTrace{
srv: c.srv,
acc: acc,
oan: oan,
dest: dest,
ct: getCompressionType(getHdrVal(acceptEncodingHeader)),
hop: hop,
event: &MsgTraceEvent{
Request: MsgTraceRequest{
Header: headers,
MsgSize: c.pa.size,
},
Events: append(MsgTraceEvents(nil), &MsgTraceIngress{
MsgTraceBase: MsgTraceBase{
Type: MsgTraceIngressType,
Timestamp: time.Now(),
},
Kind: c.kind,
CID: c.cid,
Name: getConnName(c),
Account: ian,
Subject: string(c.pa.subject),
}),
},
tonly: traceOnly,
}
return c.pa.trace
}
func sample(sampling int) bool {
// Option parsing should ensure that sampling is [1..100], but consider
// any value outside of this range to be 100%.
if sampling <= 0 || sampling >= 100 {
return true
}
return rand.Int31n(100) <= int32(sampling)
}
// This function will return the header as a map (instead of http.Header because
// we want to preserve the header names' case) and a boolean that indicates if
// the headers have been lifted due to the presence of the external trace header
// only.
// Note that because of the traceParentHdr, the search is done in a case
// insensitive way. We used to rewrite it in lower case but no longer do since v2.14.
func genHeaderMapIfTraceHeadersPresent(hdr []byte) (map[string][]string, bool) {
var (
_keys = [64][]byte{}
_vals = [64][]byte{}
m map[string][]string
traceDestHdrFound bool
traceParentHdrFound bool
)
// Skip the hdrLine
if !bytes.HasPrefix(hdr, stringToBytes(hdrLine)) {
return nil, false
}
keys := _keys[:0]
vals := _vals[:0]
for i := len(hdrLine); i < len(hdr); {
// Search for key/val delimiter
del := bytes.IndexByte(hdr[i:], ':')
if del < 0 {
break
}
keyStart := i
key := hdr[keyStart : keyStart+del]
i += del + 1
for i < len(hdr) && (hdr[i] == ' ' || hdr[i] == '\t') {
i++
}
valStart := i
nl := bytes.Index(hdr[valStart:], crLFAsBytes)
if nl < 0 {
break
}
valEnd := valStart + nl
for valEnd > valStart && (hdr[valEnd-1] == ' ' || hdr[valEnd-1] == '\t') {
valEnd--
}
val := hdr[valStart:valEnd]
if len(key) > 0 && len(val) > 0 {
vals = append(vals, val)
// We search for our special keys only if not already found.
// Check for the external trace header.
// Search needs to be case insensitive.
if !traceParentHdrFound && bytes.EqualFold(key, traceParentHdrAsBytes) {
// We will now check if the value has sampling or not.
// TODO(ik): Not sure if this header can have multiple values
// or not, and if so, what would be the rule to check for
// sampling. What is done here is to check them all until we
// found one with sampling.
tk := bytes.Split(val, dashAsBytes)
if len(tk) == 4 && len([]byte(tk[3])) == 2 {
if hexVal, err := strconv.ParseInt(bytesToString(tk[3]), 16, 8); err == nil {
if hexVal&0x1 == 0x1 {
traceParentHdrFound = true
}
}
}
} else if !traceDestHdrFound && bytes.Equal(key, traceDestHdrAsBytes) {
// This is the Nats-Trace-Dest header, check the value to see
// if it indicates that the trace was disabled.
if bytes.Equal(val, traceDestDisabledAsBytes) {
return nil, false
}
traceDestHdrFound = true
}
// Add to the keys and preserve the key's case
keys = append(keys, key)
}
i += nl + 2
}
if !traceDestHdrFound && !traceParentHdrFound {
return nil, false
}
m = make(map[string][]string, len(keys))
for i, k := range keys {
hname := string(k)
m[hname] = append(m[hname], string(vals[i]))
}
return m, !traceDestHdrFound && traceParentHdrFound
}
// Special case where we create a trace event before parsing the message.
// This is for cases where the connection will be closed when detecting
// an error during early message processing (for instance max payload).
func (c *client) initAndSendIngressErrEvent(hdr []byte, dest string, ingressError error) {
if ingressError == nil {
return
}
ct := getAcceptEncoding(hdr)
t := &msgTrace{
srv: c.srv,
acc: c.acc,
dest: dest,
ct: ct,
event: &MsgTraceEvent{
Request: MsgTraceRequest{MsgSize: c.pa.size},
Events: append(MsgTraceEvents(nil), &MsgTraceIngress{
MsgTraceBase: MsgTraceBase{
Type: MsgTraceIngressType,
Timestamp: time.Now(),
},
Kind: c.kind,
CID: c.cid,
Name: getConnName(c),
Error: ingressError.Error(),
}),
},
}
t.sendEvent()
}
// Returns `true` if message tracing is enabled and we are tracing only,
// that is, we are not going to deliver the inbound message, returns
// `false` otherwise (no tracing, or tracing and message delivery).
func (t *msgTrace) traceOnly() bool {
return t != nil && t.tonly
}
func (t *msgTrace) setOriginAccountHeaderIfNeeded(c *client, acc *Account, msg []byte) []byte {
var oan string
// If t.acc is set, only check that, not t.oan.
if t.acc != nil {
if t.acc != acc {
oan = t.acc.GetName()
}
} else if t.oan != acc.GetName() {
oan = t.oan
}
if oan != _EMPTY_ {
msg = c.setHeader(MsgTraceOriginAccount, oan, msg)
}
return msg
}
func (t *msgTrace) setHopHeader(c *client, msg []byte) []byte {
e := t.event
e.Hops++
if len(t.hop) > 0 {
t.nhop = fmt.Sprintf("%s.%d", t.hop, e.Hops)
} else {
t.nhop = fmt.Sprintf("%d", e.Hops)
}
return c.setHeader(MsgTraceHop, t.nhop, msg)
}
func (t *msgTrace) setIngressError(err string) {
if i := t.event.Ingress(); i != nil {
i.Error = err
}
}
func (t *msgTrace) addSubjectMappingEvent(subj []byte) {
if t == nil {
return
}
t.event.Events = append(t.event.Events, &MsgTraceSubjectMapping{
MsgTraceBase: MsgTraceBase{
Type: MsgTraceSubjectMappingType,
Timestamp: time.Now(),
},
MappedTo: string(subj),
})
}
func (t *msgTrace) addEgressEvent(dc *client, sub *subscription, err string) {
if t == nil {
return
}
e := &MsgTraceEgress{
MsgTraceBase: MsgTraceBase{
Type: MsgTraceEgressType,
Timestamp: time.Now(),
},
Kind: dc.kind,
CID: dc.cid,
Name: getConnName(dc),
Hop: t.nhop,
Error: err,
}
t.nhop = _EMPTY_
// Specific to CLIENT connections...
if dc.kind == CLIENT {
// Set the subscription's subject and possibly queue name.
e.Subscription = string(sub.subject)
if len(sub.queue) > 0 {
e.Queue = string(sub.queue)
}
}
if dc.kind == CLIENT || dc.kind == LEAF {
if i := t.event.Ingress(); i != nil {
// If the Ingress' account is different from the destination's
// account, add the account name into the Egress trace event.
// This would happen with service imports.
if dcAccName := dc.acc.GetName(); dcAccName != i.Account {
e.Account = dcAccName
}
}
}
t.event.Events = append(t.event.Events, e)
}
func (t *msgTrace) addStreamExportEvent(dc *client, to []byte) {
if t == nil {
return
}
dc.mu.Lock()
accName := dc.acc.GetName()
dc.mu.Unlock()
t.event.Events = append(t.event.Events, &MsgTraceStreamExport{
MsgTraceBase: MsgTraceBase{
Type: MsgTraceStreamExportType,
Timestamp: time.Now(),
},
Account: accName,
To: string(to),
})
}
func (t *msgTrace) addServiceImportEvent(accName, from, to string) {
if t == nil {
return
}
t.event.Events = append(t.event.Events, &MsgTraceServiceImport{
MsgTraceBase: MsgTraceBase{
Type: MsgTraceServiceImportType,
Timestamp: time.Now(),
},
Account: accName,
From: from,
To: to,
})
}
func (t *msgTrace) addJetStreamEvent(streamName string) {
if t == nil {
return
}
t.js = &MsgTraceJetStream{
MsgTraceBase: MsgTraceBase{
Type: MsgTraceJetStreamType,
Timestamp: time.Now(),
},
Stream: streamName,
}
t.event.Events = append(t.event.Events, t.js)
}
func (t *msgTrace) updateJetStreamEvent(subject string, noInterest bool) {
if t == nil {
return
}
// JetStream event should have been created in addJetStreamEvent
if t.js == nil {
return
}
t.js.Subject = subject
t.js.NoInterest = noInterest
// Update the timestamp since this is more accurate than when it
// was first added in addJetStreamEvent().
t.js.Timestamp = time.Now()
}
func (t *msgTrace) sendEventFromJetStream(err error) {
if t == nil {
return
}
// JetStream event should have been created in addJetStreamEvent
if t.js == nil {
return
}
if err != nil {
t.js.Error = err.Error()
}
t.sendEvent()
}
func (t *msgTrace) sendEvent() {
if t == nil {
return
}
if t.js != nil {
ready := atomic.AddInt32(&t.ready, 1) == 2
if !ready {
return
}
}
t.srv.sendInternalAccountSysMsg(t.acc, t.dest, &t.event.Server, t.event, t.ct)
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2018-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 server
import (
crand "crypto/rand"
"encoding/base64"
)
// Raw length of the nonce challenge
const (
nonceRawLen = 11
nonceLen = 15 // base64.RawURLEncoding.EncodedLen(nonceRawLen)
)
// NonceRequired tells us if we should send a nonce.
func (s *Server) NonceRequired() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.nonceRequired()
}
// nonceRequired tells us if we should send a nonce.
// Lock should be held on entry.
func (s *Server) nonceRequired() bool {
return s.getOpts().AlwaysEnableNonce || len(s.nkeys) > 0 || s.trustedKeys != nil || len(s.proxiesKeyPairs) > 0
}
// Generate a nonce for INFO challenge.
// Assumes server lock is held
func (s *Server) generateNonce(n []byte) {
var raw [nonceRawLen]byte
data := raw[:]
crand.Read(data)
base64.RawURLEncoding.Encode(n, data)
}
+992
View File
@@ -0,0 +1,992 @@
// Copyright 2021-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 server
import (
"bytes"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"golang.org/x/crypto/ocsp"
"github.com/nats-io/nats-server/v2/server/certidp"
"github.com/nats-io/nats-server/v2/server/certstore"
)
const (
defaultOCSPStoreDir = "ocsp"
defaultOCSPCheckInterval = 24 * time.Hour
minOCSPCheckInterval = 2 * time.Minute
)
type OCSPMode uint8
const (
// OCSPModeAuto staples a status, only if "status_request" is set in cert.
OCSPModeAuto OCSPMode = iota
// OCSPModeAlways enforces OCSP stapling for certs and shuts down the server in
// case a server is revoked or cannot get OCSP staples.
OCSPModeAlways
// OCSPModeNever disables OCSP stapling even if cert has Must-Staple flag.
OCSPModeNever
// OCSPModeMust honors the Must-Staple flag from a certificate but also causing shutdown
// in case the certificate has been revoked.
OCSPModeMust
)
// OCSPMonitor monitors the state of a staple per certificate.
type OCSPMonitor struct {
kind string
mu sync.Mutex
raw []byte
srv *Server
certFile string
resp *ocsp.Response
hc *http.Client
stopCh chan struct{}
Leaf *x509.Certificate
Issuer *x509.Certificate
shutdownOnRevoke bool
}
func (oc *OCSPMonitor) getNextRun() time.Duration {
oc.mu.Lock()
nextUpdate := oc.resp.NextUpdate
oc.mu.Unlock()
now := time.Now()
if nextUpdate.IsZero() {
// If response is missing NextUpdate, we check the day after.
// Technically, if NextUpdate is missing, we can try whenever.
// https://tools.ietf.org/html/rfc6960#section-4.2.2.1
return defaultOCSPCheckInterval
}
dur := nextUpdate.Sub(now) / 2
// If negative, then wait a couple of minutes before getting another staple.
if dur < 0 {
return minOCSPCheckInterval
}
return dur
}
func (oc *OCSPMonitor) getStatus() ([]byte, *ocsp.Response, error) {
raw, resp := oc.getCacheStatus()
if len(raw) > 0 && resp != nil {
// Check if the OCSP is still valid.
if err := validOCSPResponse(resp); err == nil {
return raw, resp, nil
}
}
var err error
raw, resp, err = oc.getLocalStatus()
if err == nil {
return raw, resp, nil
}
return oc.getRemoteStatus()
}
func (oc *OCSPMonitor) getCacheStatus() ([]byte, *ocsp.Response) {
oc.mu.Lock()
defer oc.mu.Unlock()
return oc.raw, oc.resp
}
func (oc *OCSPMonitor) getLocalStatus() ([]byte, *ocsp.Response, error) {
opts := oc.srv.getOpts()
storeDir := opts.StoreDir
if storeDir == _EMPTY_ {
return nil, nil, fmt.Errorf("store_dir not set")
}
// This key must be based upon the current full certificate, not the public key,
// so MUST be on the full raw certificate and not an SPKI or other reduced form.
key := fmt.Sprintf("%x", sha256.Sum256(oc.Leaf.Raw))
oc.mu.Lock()
raw, err := os.ReadFile(filepath.Join(storeDir, defaultOCSPStoreDir, key))
oc.mu.Unlock()
if err != nil {
return nil, nil, err
}
resp, err := ocsp.ParseResponse(raw, oc.Issuer)
if err != nil {
return nil, nil, fmt.Errorf("failed to get local status: %w", err)
}
if err := validOCSPResponse(resp); err != nil {
return nil, nil, err
}
// Cache the response.
oc.mu.Lock()
oc.raw = raw
oc.resp = resp
oc.mu.Unlock()
return raw, resp, nil
}
func (oc *OCSPMonitor) getRemoteStatus() ([]byte, *ocsp.Response, error) {
opts := oc.srv.getOpts()
var overrideURLs []string
if config := opts.OCSPConfig; config != nil {
overrideURLs = config.OverrideURLs
}
getRequestBytes := func(u string, reqDER []byte, hc *http.Client) ([]byte, error) {
reqEnc := base64.StdEncoding.EncodeToString(reqDER)
u = fmt.Sprintf("%s/%s", u, reqEnc)
start := time.Now()
resp, err := hc.Get(u)
if err != nil {
return nil, err
}
defer resp.Body.Close()
oc.srv.Debugf("Received OCSP response (method=GET, status=%v, url=%s, duration=%.3fs)",
resp.StatusCode, u, time.Since(start).Seconds())
if resp.StatusCode > 299 {
return nil, fmt.Errorf("non-ok http status on GET request (reqlen=%d): %d", len(reqEnc), resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
postRequestBytes := func(u string, body []byte, hc *http.Client) ([]byte, error) {
hreq, err := http.NewRequest("POST", u, bytes.NewReader(body))
if err != nil {
return nil, err
}
hreq.Header.Add("Content-Type", "application/ocsp-request")
hreq.Header.Add("Accept", "application/ocsp-response")
start := time.Now()
resp, err := hc.Do(hreq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
oc.srv.Debugf("Received OCSP response (method=POST, status=%v, url=%s, duration=%.3fs)",
resp.StatusCode, u, time.Since(start).Seconds())
if resp.StatusCode > 299 {
return nil, fmt.Errorf("non-ok http status on POST request (reqlen=%d): %d", len(body), resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
// Request documentation:
// https://tools.ietf.org/html/rfc6960#appendix-A.1
reqDER, err := ocsp.CreateRequest(oc.Leaf, oc.Issuer, nil)
if err != nil {
return nil, nil, err
}
responders := oc.Leaf.OCSPServer
if len(overrideURLs) > 0 {
responders = overrideURLs
}
if len(responders) == 0 {
return nil, nil, fmt.Errorf("no available ocsp servers")
}
oc.mu.Lock()
hc := oc.hc
oc.mu.Unlock()
var raw []byte
for _, u := range responders {
var postErr, getErr error
u = strings.TrimSuffix(u, "/")
// Prefer to make POST requests first.
raw, postErr = postRequestBytes(u, reqDER, hc)
if postErr == nil {
err = nil
break
} else {
// Fallback to use a GET request.
raw, getErr = getRequestBytes(u, reqDER, hc)
if getErr == nil {
err = nil
break
} else {
err = errors.Join(postErr, getErr)
}
}
}
if err != nil {
return nil, nil, fmt.Errorf("exhausted ocsp servers: %w", err)
}
resp, err := ocsp.ParseResponse(raw, oc.Issuer)
if err != nil {
return nil, nil, fmt.Errorf("failed to get remote status: %w", err)
}
if err := validOCSPResponse(resp); err != nil {
return nil, nil, err
}
if storeDir := opts.StoreDir; storeDir != _EMPTY_ {
key := fmt.Sprintf("%x", sha256.Sum256(oc.Leaf.Raw))
if err := oc.writeOCSPStatus(storeDir, key, raw); err != nil {
return nil, nil, fmt.Errorf("failed to write ocsp status: %w", err)
}
}
oc.mu.Lock()
oc.raw = raw
oc.resp = resp
oc.mu.Unlock()
return raw, resp, nil
}
func (oc *OCSPMonitor) run() {
s := oc.srv
s.mu.Lock()
quitCh := s.quitCh
s.mu.Unlock()
var doShutdown bool
defer func() {
// Need to decrement before shuting down, otherwise shutdown
// would be stuck waiting on grWG to go down to 0.
s.grWG.Done()
if doShutdown {
s.Shutdown()
}
}()
oc.mu.Lock()
shutdownOnRevoke := oc.shutdownOnRevoke
certFile := oc.certFile
stopCh := oc.stopCh
kind := oc.kind
oc.mu.Unlock()
var nextRun time.Duration
_, resp, err := oc.getStatus()
if err == nil && resp.Status == ocsp.Good {
nextRun = oc.getNextRun()
t := resp.NextUpdate.Format(time.RFC3339Nano)
s.Noticef(
"Found OCSP status for %s certificate at '%s': good, next update %s, checking again in %s",
kind, certFile, t, nextRun,
)
} else if err == nil && shutdownOnRevoke {
// If resp.Status is ocsp.Revoked, ocsp.Unknown, or any other value.
s.Errorf("Found OCSP status for %s certificate at '%s': %s", kind, certFile, ocspStatusString(resp.Status))
doShutdown = true
return
}
for {
// On reload, if the certificate changes then need to stop this monitor.
select {
case <-time.After(nextRun):
case <-stopCh:
// In case of reload and have to restart the OCSP stapling monitoring.
return
case <-quitCh:
// Server quit channel.
return
}
_, resp, err := oc.getRemoteStatus()
if err != nil {
nextRun = oc.getNextRun()
s.Errorf("Bad OCSP status update for certificate '%s': %s, trying again in %v", certFile, err, nextRun)
continue
}
switch n := resp.Status; n {
case ocsp.Good:
nextRun = oc.getNextRun()
t := resp.NextUpdate.Format(time.RFC3339Nano)
s.Noticef(
"Received OCSP status for %s certificate '%s': good, next update %s, checking again in %s",
kind, certFile, t, nextRun,
)
continue
default:
s.Errorf("Received OCSP status for %s certificate '%s': %s", kind, certFile, ocspStatusString(n))
if shutdownOnRevoke {
doShutdown = true
}
return
}
}
}
func (oc *OCSPMonitor) stop() {
oc.mu.Lock()
stopCh := oc.stopCh
oc.mu.Unlock()
stopCh <- struct{}{}
}
// NewOCSPMonitor takes a TLS configuration then wraps it with the callbacks set for OCSP verification
// along with a monitor that will periodically fetch OCSP staples.
func (srv *Server) NewOCSPMonitor(config *tlsConfigKind) (*tls.Config, *OCSPMonitor, error) {
kind := config.kind
tc := config.tlsConfig
tcOpts := config.tlsOpts
opts := srv.getOpts()
oc := opts.OCSPConfig
// We need to track the CA certificate in case the CA is not present
// in the chain to be able to verify the signature of the OCSP staple.
var (
certFile string
caFile string
)
if kind == kindStringMap[CLIENT] {
tcOpts = opts.tlsConfigOpts
if opts.TLSCert != _EMPTY_ {
certFile = opts.TLSCert
}
if opts.TLSCaCert != _EMPTY_ {
caFile = opts.TLSCaCert
}
}
if tcOpts != nil {
certFile = tcOpts.CertFile
caFile = tcOpts.CaFile
}
// NOTE: Currently OCSP Stapling is enabled only for the first certificate found.
var mon *OCSPMonitor
for _, currentCert := range tc.Certificates {
// Create local copy since this will be used in the GetCertificate callback.
cert := currentCert
// This is normally non-nil, but can still be nil here when in tests
// or in some embedded scenarios.
if cert.Leaf == nil {
if len(cert.Certificate) <= 0 {
return nil, nil, fmt.Errorf("no certificate found")
}
var err error
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return nil, nil, fmt.Errorf("error parsing certificate: %v", err)
}
}
var shutdownOnRevoke bool
mustStaple := hasOCSPStatusRequest(cert.Leaf)
if oc != nil {
switch {
case oc.Mode == OCSPModeNever:
if mustStaple {
srv.Warnf("Certificate at '%s' has MustStaple but OCSP is disabled", certFile)
}
return tc, nil, nil
case oc.Mode == OCSPModeAlways:
// Start the monitor for this cert even if it does not have
// the MustStaple flag and shutdown the server in case the
// staple ever gets revoked.
mustStaple = true
shutdownOnRevoke = true
case oc.Mode == OCSPModeMust && mustStaple:
shutdownOnRevoke = true
case oc.Mode == OCSPModeAuto && !mustStaple:
// "status_request" MustStaple flag not set in certificate. No need to do anything.
return tc, nil, nil
}
}
if !mustStaple {
// No explicit OCSP config and cert does not have MustStaple flag either.
return tc, nil, nil
}
if err := srv.setupOCSPStapleStoreDir(); err != nil {
return nil, nil, err
}
// TODO: Add OCSP 'responder_cert' option in case CA cert not available.
issuer, err := getOCSPIssuer(caFile, cert.Certificate)
if err != nil {
return nil, nil, err
}
mon = &OCSPMonitor{
kind: kind,
srv: srv,
hc: &http.Client{Timeout: 30 * time.Second},
shutdownOnRevoke: shutdownOnRevoke,
certFile: certFile,
stopCh: make(chan struct{}, 1),
Leaf: cert.Leaf,
Issuer: issuer,
}
// Get the certificate status from the memory, then remote OCSP responder.
if _, resp, err := mon.getStatus(); err != nil {
return nil, nil, fmt.Errorf("bad OCSP status update for certificate at '%s': %s", certFile, err)
} else if resp != nil && resp.Status != ocsp.Good && shutdownOnRevoke {
return nil, nil, fmt.Errorf("found existing OCSP status for certificate at '%s': %s", certFile, ocspStatusString(resp.Status))
}
// Callbacks below will be in charge of returning the certificate instead,
// so this has to be nil.
tc.Certificates = nil
// GetCertificate returns a certificate that's presented to a client.
tc.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
ccert := cert
raw, _, err := mon.getStatus()
if err != nil {
return nil, err
}
return &tls.Certificate{
OCSPStaple: raw,
Certificate: ccert.Certificate,
PrivateKey: ccert.PrivateKey,
SupportedSignatureAlgorithms: ccert.SupportedSignatureAlgorithms,
SignedCertificateTimestamps: ccert.SignedCertificateTimestamps,
Leaf: ccert.Leaf,
}, nil
}
// Check whether need to verify staples from a peer router or gateway connection.
switch kind {
case kindStringMap[ROUTER], kindStringMap[GATEWAY]:
tc.VerifyConnection = func(s tls.ConnectionState) error {
oresp := s.OCSPResponse
if oresp == nil {
return fmt.Errorf("%s peer missing OCSP Staple", kind)
}
// Peer connections will verify the response of the staple.
if len(s.VerifiedChains) == 0 {
return fmt.Errorf("%s peer missing TLS verified chains", kind)
}
chain := s.VerifiedChains[0]
peerLeaf := chain[0]
peerIssuer := certidp.GetLeafIssuerCert(chain, 0)
if peerIssuer == nil {
return fmt.Errorf("failed to get issuer certificate for %s peer", kind)
}
// Response signature of issuer or issuer delegate is checked in the library parse
resp, err := ocsp.ParseResponseForCert(oresp, peerLeaf, peerIssuer)
if err != nil {
return fmt.Errorf("failed to parse OCSP response from %s peer: %w", kind, err)
}
// If signer was issuer delegate double-check issuer delegate authorization
if resp.Certificate != nil {
ok := false
for _, eku := range resp.Certificate.ExtKeyUsage {
if eku == x509.ExtKeyUsageOCSPSigning {
ok = true
break
}
}
if !ok {
return fmt.Errorf("OCSP staple's signer missing authorization by CA to act as OCSP signer")
}
}
// Check that the OCSP response is effective, take defaults for clockskew and default validity
peerOpts := certidp.OCSPPeerConfig{ClockSkew: -1, TTLUnsetNextUpdate: -1}
sLog := certidp.Log{Debugf: srv.Debugf}
if !certidp.OCSPResponseCurrent(resp, &peerOpts, &sLog) {
return fmt.Errorf("OCSP staple from %s peer not current", kind)
}
if resp.Status != ocsp.Good {
return fmt.Errorf("bad status for OCSP Staple from %s peer: %s", kind, ocspStatusString(resp.Status))
}
return nil
}
// When server makes a peer connection, need to also present an OCSP Staple.
tc.GetClientCertificate = func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
ccert := cert
raw, _, err := mon.getStatus()
if err != nil {
return nil, err
}
// NOTE: crypto/tls.sendClientCertificate internally also calls getClientCertificate
// so if for some reason these callbacks are triggered concurrently during a reconnect
// there can be a race. To avoid that, the OCSP monitor lock is used to serialize access
// to the staple which could also change inflight during an update.
mon.mu.Lock()
ccert.OCSPStaple = raw
mon.mu.Unlock()
return &ccert, nil
}
default:
// GetClientCertificate returns a certificate that's presented to a server.
tc.GetClientCertificate = func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
return &cert, nil
}
}
}
return tc, mon, nil
}
func (s *Server) setupOCSPStapleStoreDir() error {
opts := s.getOpts()
storeDir := opts.StoreDir
if storeDir == _EMPTY_ {
return nil
}
storeDir = filepath.Join(storeDir, defaultOCSPStoreDir)
if stat, err := os.Stat(storeDir); os.IsNotExist(err) {
if err := os.MkdirAll(storeDir, defaultDirPerms); err != nil {
return fmt.Errorf("could not create OCSP storage directory - %v", err)
}
} else if stat == nil || !stat.IsDir() {
return fmt.Errorf("OCSP storage directory is not a directory")
}
return nil
}
type tlsConfigKind struct {
tlsConfig *tls.Config
tlsOpts *TLSConfigOpts
kind string
isLeafSpoke bool
apply func(*tls.Config)
}
func (s *Server) configureOCSP() []*tlsConfigKind {
sopts := s.getOpts()
configs := make([]*tlsConfigKind, 0)
if config := sopts.TLSConfig; config != nil {
opts := sopts.tlsConfigOpts
o := &tlsConfigKind{
kind: kindStringMap[CLIENT],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) { sopts.TLSConfig = tc },
}
configs = append(configs, o)
}
if config := sopts.Websocket.TLSConfig; config != nil {
opts := sopts.Websocket.tlsConfigOpts
o := &tlsConfigKind{
kind: kindStringMap[CLIENT],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) { sopts.Websocket.TLSConfig = tc },
}
configs = append(configs, o)
}
if config := sopts.MQTT.TLSConfig; config != nil {
opts := sopts.tlsConfigOpts
o := &tlsConfigKind{
kind: kindStringMap[CLIENT],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) { sopts.MQTT.TLSConfig = tc },
}
configs = append(configs, o)
}
if config := sopts.Cluster.TLSConfig; config != nil {
opts := sopts.Cluster.tlsConfigOpts
o := &tlsConfigKind{
kind: kindStringMap[ROUTER],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) { sopts.Cluster.TLSConfig = tc },
}
configs = append(configs, o)
}
if config := sopts.LeafNode.TLSConfig; config != nil {
opts := sopts.LeafNode.tlsConfigOpts
o := &tlsConfigKind{
kind: kindStringMap[LEAF],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) { sopts.LeafNode.TLSConfig = tc },
}
configs = append(configs, o)
}
for _, remote := range sopts.LeafNode.Remotes {
if config := remote.TLSConfig; config != nil {
// Use a copy of the remote here since will be used
// in the apply func callback below.
r, opts := remote, remote.tlsConfigOpts
o := &tlsConfigKind{
kind: kindStringMap[LEAF],
tlsConfig: config,
tlsOpts: opts,
isLeafSpoke: true,
apply: func(tc *tls.Config) { r.TLSConfig = tc },
}
configs = append(configs, o)
}
}
if config := sopts.Gateway.TLSConfig; config != nil {
opts := sopts.Gateway.tlsConfigOpts
o := &tlsConfigKind{
kind: kindStringMap[GATEWAY],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) { sopts.Gateway.TLSConfig = tc },
}
configs = append(configs, o)
}
for _, remote := range sopts.Gateway.Gateways {
if config := remote.TLSConfig; config != nil {
gw, opts := remote, remote.tlsConfigOpts
o := &tlsConfigKind{
kind: kindStringMap[GATEWAY],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) { gw.TLSConfig = tc },
}
configs = append(configs, o)
}
}
return configs
}
func (s *Server) enableOCSP() error {
configs := s.configureOCSP()
for _, config := range configs {
// We do not staple Leaf Hub and Leaf Spokes, use ocsp_peer
if config.kind != kindStringMap[LEAF] {
// OCSP Stapling feature, will also enable tls server peer check for gateway and route peers
tc, mon, err := s.NewOCSPMonitor(config)
if err != nil {
return err
}
// Check if an OCSP stapling monitor is required for this certificate.
if mon != nil {
s.ocsps = append(s.ocsps, mon)
// Override the TLS config with one that follows OCSP stapling
config.apply(tc)
}
}
// OCSP peer check (client mTLS, leaf mTLS, leaf remote TLS)
if config.kind == kindStringMap[CLIENT] || config.kind == kindStringMap[LEAF] {
tc, plugged, err := s.plugTLSOCSPPeer(config)
if err != nil {
return err
}
if plugged && tc != nil {
s.ocspPeerVerify = true
config.apply(tc)
}
}
}
return nil
}
func (s *Server) startOCSPMonitoring() {
s.mu.Lock()
ocsps := s.ocsps
s.mu.Unlock()
if ocsps == nil {
return
}
for _, mon := range ocsps {
m := mon
m.mu.Lock()
kind := m.kind
m.mu.Unlock()
s.Noticef("OCSP Stapling enabled for %s connections", kind)
s.startGoRoutine(func() { m.run() })
}
}
func (s *Server) reloadOCSP() error {
if err := s.setupOCSPStapleStoreDir(); err != nil {
return err
}
s.mu.Lock()
ocsps := s.ocsps
s.mu.Unlock()
// Stop all OCSP Stapling monitors in case there were any running.
for _, oc := range ocsps {
oc.stop()
}
configs := s.configureOCSP()
// Restart the monitors under the new configuration.
ocspm := make([]*OCSPMonitor, 0)
// Reset server's ocspPeerVerify flag to re-detect at least one plugged OCSP peer
s.mu.Lock()
s.ocspPeerVerify = false
s.mu.Unlock()
s.stopOCSPResponseCache()
for _, config := range configs {
// We do not staple Leaf Hub and Leaf Spokes, use ocsp_peer
if config.kind != kindStringMap[LEAF] {
tc, mon, err := s.NewOCSPMonitor(config)
if err != nil {
return err
}
// Check if an OCSP stapling monitor is required for this certificate.
if mon != nil {
ocspm = append(ocspm, mon)
// Apply latest TLS configuration after OCSP monitors have started.
defer config.apply(tc)
}
}
// OCSP peer check (client mTLS, leaf mTLS, leaf remote TLS)
if config.kind == kindStringMap[CLIENT] || config.kind == kindStringMap[LEAF] {
tc, plugged, err := s.plugTLSOCSPPeer(config)
if err != nil {
return err
}
if plugged && tc != nil {
s.ocspPeerVerify = true
defer config.apply(tc)
}
}
}
// Replace stopped monitors with the new ones.
s.mu.Lock()
s.ocsps = ocspm
s.mu.Unlock()
// Dispatch all goroutines once again.
s.startOCSPMonitoring()
// Init and restart OCSP responder cache
s.stopOCSPResponseCache()
s.initOCSPResponseCache()
s.startOCSPResponseCache()
return nil
}
func hasOCSPStatusRequest(cert *x509.Certificate) bool {
// OID for id-pe-tlsfeature defined in RFC here:
// https://datatracker.ietf.org/doc/html/rfc7633
tlsFeatures := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
const statusRequestExt = 5
// Example values:
// * [48 3 2 1 5] - seen when creating own certs locally
// * [30 3 2 1 5] - seen in the wild
// Documentation:
// https://tools.ietf.org/html/rfc6066
for _, ext := range cert.Extensions {
if !ext.Id.Equal(tlsFeatures) {
continue
}
var val []int
rest, err := asn1.Unmarshal(ext.Value, &val)
if err != nil || len(rest) > 0 {
return false
}
for _, n := range val {
if n == statusRequestExt {
return true
}
}
break
}
return false
}
// writeOCSPStatus writes an OCSP status to a temporary file then moves it to a
// new path, in an attempt to avoid corrupting existing data.
func (oc *OCSPMonitor) writeOCSPStatus(storeDir, file string, data []byte) error {
storeDir = filepath.Join(storeDir, defaultOCSPStoreDir)
tmp, err := os.CreateTemp(storeDir, "tmp-cert-status")
if err != nil {
return err
}
if _, err := tmp.Write(data); err != nil {
tmp.Close()
os.Remove(tmp.Name())
return err
}
if err := tmp.Close(); err != nil {
return err
}
oc.mu.Lock()
err = os.Rename(tmp.Name(), filepath.Join(storeDir, file))
oc.mu.Unlock()
if err != nil {
os.Remove(tmp.Name())
return err
}
return nil
}
func parseCertPEM(name string) ([]*x509.Certificate, error) {
data, err := os.ReadFile(name)
if err != nil {
return nil, err
}
var pemBytes []byte
var block *pem.Block
for len(data) != 0 {
block, data = pem.Decode(data)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("unexpected PEM certificate type: %s", block.Type)
}
pemBytes = append(pemBytes, block.Bytes...)
}
return x509.ParseCertificates(pemBytes)
}
// getOCSPIssuerLocally determines a leaf's issuer from locally configured certificates
func getOCSPIssuerLocally(trustedCAs []*x509.Certificate, certBundle []*x509.Certificate) (*x509.Certificate, error) {
var vOpts x509.VerifyOptions
var leaf *x509.Certificate
trustedCAPool := x509.NewCertPool()
// Require Leaf as first cert in bundle
if len(certBundle) > 0 {
leaf = certBundle[0]
} else {
return nil, fmt.Errorf("invalid ocsp ca configuration")
}
// Allow Issuer to be configured as second cert in bundle
if len(certBundle) > 1 {
// The operator may have misconfigured the cert bundle
issuerCandidate := certBundle[1]
err := issuerCandidate.CheckSignature(leaf.SignatureAlgorithm, leaf.RawTBSCertificate, leaf.Signature)
if err != nil {
return nil, fmt.Errorf("invalid issuer configuration: %w", err)
} else {
return issuerCandidate, nil
}
}
// Operator did not provide the Leaf Issuer in cert bundle second position
// so we will attempt to create at least one ordered verified chain from the
// trusted CA pool.
// Specify CA trust store to validator; if unset, system trust store used
if len(trustedCAs) > 0 {
for _, ca := range trustedCAs {
trustedCAPool.AddCert(ca)
}
vOpts.Roots = trustedCAPool
}
return certstore.GetLeafIssuer(leaf, vOpts), nil
}
// getOCSPIssuer determines an issuer certificate from the cert (bundle) or the file-based CA trust store
func getOCSPIssuer(caFile string, chain [][]byte) (*x509.Certificate, error) {
var issuer *x509.Certificate
var trustedCAs []*x509.Certificate
var certBundle []*x509.Certificate
var err error
// FIXME(tgb): extend if pluggable CA store provider added to NATS (i.e. other than PEM file)
// Non-system default CA trust store passed
if caFile != _EMPTY_ {
trustedCAs, err = parseCertPEM(caFile)
if err != nil {
return nil, fmt.Errorf("failed to parse ca_file: %v", err)
}
}
// Specify bundled intermediate CA store
for _, certBytes := range chain {
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse cert: %v", err)
}
certBundle = append(certBundle, cert)
}
issuer, err = getOCSPIssuerLocally(trustedCAs, certBundle)
if err != nil || issuer == nil {
return nil, fmt.Errorf("no issuers found")
}
if !issuer.IsCA {
return nil, fmt.Errorf("%s invalid ca basic constraints: is not ca", issuer.Subject)
}
return issuer, nil
}
func ocspStatusString(n int) string {
switch n {
case ocsp.Good:
return "good"
case ocsp.Revoked:
return "revoked"
default:
return "unknown"
}
}
func validOCSPResponse(r *ocsp.Response) error {
// Time validation not handled by ParseResponse.
// https://tools.ietf.org/html/rfc6960#section-4.2.2.1
if !r.NextUpdate.IsZero() && r.NextUpdate.Before(time.Now()) {
t := r.NextUpdate.Format(time.RFC3339Nano)
return fmt.Errorf("invalid ocsp NextUpdate, is past time: %s", t)
}
if r.ThisUpdate.After(time.Now()) {
t := r.ThisUpdate.Format(time.RFC3339Nano)
return fmt.Errorf("invalid ocsp ThisUpdate, is future time: %s", t)
}
return nil
}
+405
View File
@@ -0,0 +1,405 @@
// Copyright 2023-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 server
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strings"
"time"
"golang.org/x/crypto/ocsp"
"github.com/nats-io/nats-server/v2/server/certidp"
)
func parseOCSPPeer(v any) (pcfg *certidp.OCSPPeerConfig, retError error) {
var lt token
defer convertPanicToError(&lt, &retError)
tk, v := unwrapValue(v, &lt)
cm, ok := v.(map[string]any)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrIllegalPeerOptsConfig, v)}
}
pcfg = certidp.NewOCSPPeerConfig()
retError = nil
for mk, mv := range cm {
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "verify":
verify, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.Verify = verify
case "allowed_clockskew":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
if at >= 0 {
pcfg.ClockSkew = at
}
case "ca_timeout":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, err)}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
if at >= 0 {
pcfg.Timeout = at
}
case "cache_ttl_when_next_update_unset":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, err)}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
if at >= 0 {
pcfg.TTLUnsetNextUpdate = at
}
case "warn_only":
warnOnly, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.WarnOnly = warnOnly
case "unknown_is_good":
unknownIsGood, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.UnknownIsGood = unknownIsGood
case "allow_when_ca_unreachable":
allowWhenCAUnreachable, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.AllowWhenCAUnreachable = allowWhenCAUnreachable
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
}
return pcfg, nil
}
func peerFromVerifiedChains(chains [][]*x509.Certificate) *x509.Certificate {
if len(chains) == 0 || len(chains[0]) == 0 {
return nil
}
return chains[0][0]
}
// plugTLSOCSPPeer will plug the TLS handshake lifecycle for client mTLS connections and Leaf connections
func (s *Server) plugTLSOCSPPeer(config *tlsConfigKind) (*tls.Config, bool, error) {
if config == nil || config.tlsConfig == nil {
return nil, false, errors.New(certidp.ErrUnableToPlugTLSEmptyConfig)
}
kind := config.kind
isSpoke := config.isLeafSpoke
tcOpts := config.tlsOpts
if tcOpts == nil || tcOpts.OCSPPeerConfig == nil || !tcOpts.OCSPPeerConfig.Verify {
return nil, false, nil
}
s.Debugf(certidp.DbgPlugTLSForKind, config.kind)
// peer is a tls client
if kind == kindStringMap[CLIENT] || (kind == kindStringMap[LEAF] && !isSpoke) {
if !tcOpts.Verify {
return nil, false, errors.New(certidp.ErrMTLSRequired)
}
return s.plugClientTLSOCSPPeer(config)
}
// peer is a tls server
if kind == kindStringMap[LEAF] && isSpoke {
return s.plugServerTLSOCSPPeer(config)
}
return nil, false, nil
}
func (s *Server) plugClientTLSOCSPPeer(config *tlsConfigKind) (*tls.Config, bool, error) {
if config == nil || config.tlsConfig == nil || config.tlsOpts == nil {
return nil, false, errors.New(certidp.ErrUnableToPlugTLSClient)
}
tc := config.tlsConfig
tcOpts := config.tlsOpts
kind := config.kind
if tcOpts.OCSPPeerConfig == nil || !tcOpts.OCSPPeerConfig.Verify {
return tc, false, nil
}
tc.VerifyConnection = func(cs tls.ConnectionState) error {
if !s.tlsClientOCSPValid(cs.VerifiedChains, tcOpts.OCSPPeerConfig) {
s.sendOCSPPeerRejectEvent(kind, peerFromVerifiedChains(cs.VerifiedChains), certidp.MsgTLSClientRejectConnection)
return errors.New(certidp.MsgTLSClientRejectConnection)
}
return nil
}
return tc, true, nil
}
func (s *Server) plugServerTLSOCSPPeer(config *tlsConfigKind) (*tls.Config, bool, error) {
if config == nil || config.tlsConfig == nil || config.tlsOpts == nil {
return nil, false, errors.New(certidp.ErrUnableToPlugTLSServer)
}
tc := config.tlsConfig
tcOpts := config.tlsOpts
kind := config.kind
if tcOpts.OCSPPeerConfig == nil || !tcOpts.OCSPPeerConfig.Verify {
return tc, false, nil
}
tc.VerifyConnection = func(cs tls.ConnectionState) error {
if !s.tlsServerOCSPValid(cs.VerifiedChains, tcOpts.OCSPPeerConfig) {
s.sendOCSPPeerRejectEvent(kind, peerFromVerifiedChains(cs.VerifiedChains), certidp.MsgTLSServerRejectConnection)
return errors.New(certidp.MsgTLSServerRejectConnection)
}
return nil
}
return tc, true, nil
}
// tlsServerOCSPValid evaluates verified chains (post successful TLS handshake) against OCSP
// eligibility. A verified chain is considered OCSP Valid if either none of the links are
// OCSP eligible, or current "good" responses from the CA can be obtained for each eligible link.
// Upon first OCSP Valid chain found, the Server is deemed OCSP Valid. If none of the chains are
// OCSP Valid, the Server is deemed OCSP Invalid. A verified self-signed certificate (chain length 1)
// is also considered OCSP Valid.
func (s *Server) tlsServerOCSPValid(chains [][]*x509.Certificate, opts *certidp.OCSPPeerConfig) bool {
s.Debugf(certidp.DbgNumServerChains, len(chains))
return s.peerOCSPValid(chains, opts)
}
// tlsClientOCSPValid evaluates verified chains (post successful TLS handshake) against OCSP
// eligibility. A verified chain is considered OCSP Valid if either none of the links are
// OCSP eligible, or current "good" responses from the CA can be obtained for each eligible link.
// Upon first OCSP Valid chain found, the Client is deemed OCSP Valid. If none of the chains are
// OCSP Valid, the Client is deemed OCSP Invalid. A verified self-signed certificate (chain length 1)
// is also considered OCSP Valid.
func (s *Server) tlsClientOCSPValid(chains [][]*x509.Certificate, opts *certidp.OCSPPeerConfig) bool {
s.Debugf(certidp.DbgNumClientChains, len(chains))
return s.peerOCSPValid(chains, opts)
}
func (s *Server) peerOCSPValid(chains [][]*x509.Certificate, opts *certidp.OCSPPeerConfig) bool {
peer := peerFromVerifiedChains(chains)
if peer == nil {
s.Errorf(certidp.ErrPeerEmptyAutoReject)
return false
}
for ci, chain := range chains {
s.Debugf(certidp.DbgLinksInChain, ci, len(chain))
// Self-signed certificate is Client OCSP Valid (no CA)
if len(chain) == 1 {
s.Debugf(certidp.DbgSelfSignedValid, ci)
return true
}
// Check if any of the links in the chain are OCSP eligible
chainEligible := false
var eligibleLinks []*certidp.ChainLink
// Iterate over links skipping the root cert which is not OCSP eligible (self == issuer)
for linkPos := 0; linkPos < len(chain)-1; linkPos++ {
cert := chain[linkPos]
link := &certidp.ChainLink{
Leaf: cert,
}
if certidp.CertOCSPEligible(link) {
chainEligible = true
issuerCert := certidp.GetLeafIssuerCert(chain, linkPos)
if issuerCert == nil {
// unexpected chain condition, reject Client as OCSP Invalid
return false
}
link.Issuer = issuerCert
eligibleLinks = append(eligibleLinks, link)
}
}
// A trust-store verified chain that is not OCSP eligible is always OCSP Valid
if !chainEligible {
s.Debugf(certidp.DbgValidNonOCSPChain, ci)
return true
}
s.Debugf(certidp.DbgChainIsOCSPEligible, ci, len(eligibleLinks))
// Chain has at least one OCSP eligible link, so check each eligible link;
// any link with a !good OCSP response chain OCSP Invalid
chainValid := true
for _, link := range eligibleLinks {
// if option selected, good could reflect either ocsp.Good or ocsp.Unknown
if badReason, good := s.certOCSPGood(link, opts); !good {
s.Debugf(badReason)
s.sendOCSPPeerChainlinkInvalidEvent(peer, link.Leaf, badReason)
chainValid = false
break
}
}
if chainValid {
s.Debugf(certidp.DbgChainIsOCSPValid, ci)
return true
}
}
// If we are here, all chains had OCSP eligible links, but none of the chains achieved OCSP valid
s.Debugf(certidp.DbgNoOCSPValidChains)
return false
}
func (s *Server) certOCSPGood(link *certidp.ChainLink, opts *certidp.OCSPPeerConfig) (string, bool) {
if link == nil || link.Leaf == nil || link.Issuer == nil || link.OCSPWebEndpoints == nil || len(*link.OCSPWebEndpoints) < 1 {
return "Empty chainlink found", false
}
var err error
sLogs := &certidp.Log{
Debugf: s.Debugf,
Noticef: s.Noticef,
Warnf: s.Warnf,
Errorf: s.Errorf,
Tracef: s.Tracef,
}
fingerprint := certidp.GenerateFingerprint(link.Leaf)
// Used for debug/operator only, not match
subj := certidp.GetSubjectDNForm(link.Leaf)
var rawResp []byte
var ocspr *ocsp.Response
var useCachedResp bool
var rc = s.ocsprc
var cachedRevocation bool
// Check our cache before calling out to the CA OCSP responder
s.Debugf(certidp.DbgCheckingCacheForCert, subj, fingerprint)
if rawResp = rc.Get(fingerprint, sLogs); len(rawResp) > 0 {
// Signature validation of CA's OCSP response occurs in ParseResponse
ocspr, err = ocsp.ParseResponse(rawResp, link.Issuer)
if err == nil && ocspr != nil {
// Check if OCSP Response delegation present and if so is valid
if !certidp.ValidDelegationCheck(link.Issuer, ocspr) {
// Invalid delegation was already in cache, purge it and don't use it
s.Debugf(certidp.MsgCachedOCSPResponseInvalid, subj)
rc.Delete(fingerprint, true, sLogs)
goto AFTERCACHE
}
if certidp.OCSPResponseCurrent(ocspr, opts, sLogs) {
s.Debugf(certidp.DbgCurrentResponseCached, certidp.GetStatusAssertionStr(ocspr.Status))
useCachedResp = true
} else {
// Cached response is not current, delete it and tidy runtime stats to reflect a miss;
// if preserve_revoked is enabled, the cache will not delete the cached response
s.Debugf(certidp.DbgExpiredResponseCached, certidp.GetStatusAssertionStr(ocspr.Status))
rc.Delete(fingerprint, true, sLogs)
}
// Regardless of currency, record a cached revocation found in case AllowWhenCAUnreachable is set
if ocspr.Status == ocsp.Revoked {
cachedRevocation = true
}
} else {
// Bogus cached assertion, purge it and don't use it
s.Debugf(certidp.MsgCachedOCSPResponseInvalid, subj, fingerprint)
rc.Delete(fingerprint, true, sLogs)
goto AFTERCACHE
}
}
AFTERCACHE:
if !useCachedResp {
// CA OCSP responder callout needed
rawResp, err = certidp.FetchOCSPResponse(link, opts, sLogs)
if err != nil || rawResp == nil || len(rawResp) == 0 {
s.Warnf(certidp.ErrCAResponderCalloutFail, subj, err)
if opts.WarnOnly {
s.Warnf(certidp.MsgAllowWarnOnlyOccurred, subj)
return _EMPTY_, true
}
if opts.AllowWhenCAUnreachable && !cachedRevocation {
// Link has no cached history of revocation, so allow it to pass
s.Warnf(certidp.MsgAllowWhenCAUnreachableOccurred, subj)
return _EMPTY_, true
} else if opts.AllowWhenCAUnreachable {
// Link has cached but expired revocation so reject when CA is unreachable
s.Warnf(certidp.MsgAllowWhenCAUnreachableOccurredCachedRevoke, subj)
}
return certidp.MsgFailedOCSPResponseFetch, false
}
// Signature validation of CA's OCSP response occurs in ParseResponse
ocspr, err = ocsp.ParseResponse(rawResp, link.Issuer)
if err == nil && ocspr != nil {
// Check if OCSP Response delegation present and if so is valid
if !certidp.ValidDelegationCheck(link.Issuer, ocspr) {
s.Warnf(certidp.MsgOCSPResponseDelegationInvalid, subj)
if opts.WarnOnly {
// Can't use bogus assertion, but warn-only set so allow link to pass
s.Warnf(certidp.MsgAllowWarnOnlyOccurred, subj)
return _EMPTY_, true
}
return fmt.Sprintf(certidp.MsgOCSPResponseDelegationInvalid, subj), false
}
if !certidp.OCSPResponseCurrent(ocspr, opts, sLogs) {
s.Warnf(certidp.ErrNewCAResponseNotCurrent, subj)
if opts.WarnOnly {
// Can't use non-effective assertion, but warn-only set so allow link to pass
s.Warnf(certidp.MsgAllowWarnOnlyOccurred, subj)
return _EMPTY_, true
}
return certidp.MsgOCSPResponseNotEffective, false
}
} else {
s.Errorf(certidp.ErrCAResponseParseFailed, subj, err)
if opts.WarnOnly {
// Can't use bogus assertion, but warn-only set so allow link to pass
s.Warnf(certidp.MsgAllowWarnOnlyOccurred, subj)
return _EMPTY_, true
}
return certidp.MsgFailedOCSPResponseParse, false
}
// cache the valid fetched CA OCSP Response
rc.Put(fingerprint, ocspr, subj, sLogs)
}
// Whether through valid cache response available or newly fetched valid response, now check the status
if ocspr.Status == ocsp.Revoked || (ocspr.Status == ocsp.Unknown && !opts.UnknownIsGood) {
s.Warnf(certidp.ErrOCSPInvalidPeerLink, subj, certidp.GetStatusAssertionStr(ocspr.Status))
if opts.WarnOnly {
s.Warnf(certidp.MsgAllowWarnOnlyOccurred, subj)
return _EMPTY_, true
}
return fmt.Sprintf(certidp.MsgOCSPResponseInvalidStatus, certidp.GetStatusAssertionStr(ocspr.Status)), false
}
s.Debugf(certidp.DbgOCSPValidPeerLink, subj)
return _EMPTY_, true
}
@@ -0,0 +1,636 @@
// Copyright 2023-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 server
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/klauspost/compress/s2"
"golang.org/x/crypto/ocsp"
"github.com/nats-io/nats-server/v2/server/certidp"
)
const (
OCSPResponseCacheDefaultDir = "_rc_"
OCSPResponseCacheDefaultFilename = "cache.json"
OCSPResponseCacheDefaultTempFilePrefix = "ocsprc-*"
OCSPResponseCacheMinimumSaveInterval = 1 * time.Second
OCSPResponseCacheDefaultSaveInterval = 5 * time.Minute
)
type OCSPResponseCacheType int
const (
NONE OCSPResponseCacheType = iota + 1
LOCAL
)
var OCSPResponseCacheTypeMap = map[string]OCSPResponseCacheType{
"none": NONE,
"local": LOCAL,
}
type OCSPResponseCacheConfig struct {
Type OCSPResponseCacheType
LocalStore string
PreserveRevoked bool
SaveInterval float64
}
func NewOCSPResponseCacheConfig() *OCSPResponseCacheConfig {
return &OCSPResponseCacheConfig{
Type: LOCAL,
LocalStore: OCSPResponseCacheDefaultDir,
PreserveRevoked: false,
SaveInterval: OCSPResponseCacheDefaultSaveInterval.Seconds(),
}
}
type OCSPResponseCacheStats struct {
Responses int64 `json:"size"`
Hits int64 `json:"hits"`
Misses int64 `json:"misses"`
Revokes int64 `json:"revokes"`
Goods int64 `json:"goods"`
Unknowns int64 `json:"unknowns"`
}
type OCSPResponseCacheItem struct {
Subject string `json:"subject,omitempty"`
CachedAt time.Time `json:"cached_at"`
RespStatus certidp.StatusAssertion `json:"resp_status"`
RespExpires time.Time `json:"resp_expires,omitempty"`
Resp []byte `json:"resp"`
}
type OCSPResponseCache interface {
Put(key string, resp *ocsp.Response, subj string, log *certidp.Log)
Get(key string, log *certidp.Log) []byte
Delete(key string, miss bool, log *certidp.Log)
Type() string
Start(s *Server)
Stop(s *Server)
Online() bool
Config() *OCSPResponseCacheConfig
Stats() *OCSPResponseCacheStats
}
// NoOpCache is a no-op implementation of OCSPResponseCache
type NoOpCache struct {
config *OCSPResponseCacheConfig
stats *OCSPResponseCacheStats
online bool
mu *sync.RWMutex
}
func (c *NoOpCache) Put(_ string, _ *ocsp.Response, _ string, _ *certidp.Log) {}
func (c *NoOpCache) Get(_ string, _ *certidp.Log) []byte {
return nil
}
func (c *NoOpCache) Delete(_ string, _ bool, _ *certidp.Log) {}
func (c *NoOpCache) Start(_ *Server) {
c.mu.Lock()
defer c.mu.Unlock()
c.stats = &OCSPResponseCacheStats{}
c.online = true
}
func (c *NoOpCache) Stop(_ *Server) {
c.mu.Lock()
defer c.mu.Unlock()
c.online = false
}
func (c *NoOpCache) Online() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.online
}
func (c *NoOpCache) Type() string {
c.mu.RLock()
defer c.mu.RUnlock()
return "none"
}
func (c *NoOpCache) Config() *OCSPResponseCacheConfig {
c.mu.RLock()
defer c.mu.RUnlock()
return c.config
}
func (c *NoOpCache) Stats() *OCSPResponseCacheStats {
c.mu.RLock()
defer c.mu.RUnlock()
return c.stats
}
// LocalCache is a local file implementation of OCSPResponseCache
type LocalCache struct {
config *OCSPResponseCacheConfig
stats *OCSPResponseCacheStats
online bool
cache map[string]OCSPResponseCacheItem
mu *sync.RWMutex
saveInterval time.Duration
dirty bool
timer *time.Timer
}
// Put captures a CA OCSP response to the OCSP peer cache indexed by response fingerprint (a hash)
func (c *LocalCache) Put(key string, caResp *ocsp.Response, subj string, log *certidp.Log) {
c.mu.RLock()
if !c.online || caResp == nil || key == "" {
c.mu.RUnlock()
return
}
c.mu.RUnlock()
log.Debugf(certidp.DbgCachingResponse, subj, key)
rawC, err := c.Compress(caResp.Raw)
if err != nil {
log.Errorf(certidp.ErrResponseCompressFail, key, err)
return
}
log.Debugf(certidp.DbgAchievedCompression, float64(len(rawC))/float64(len(caResp.Raw)))
c.mu.Lock()
defer c.mu.Unlock()
// check if we are replacing and do stats
item, ok := c.cache[key]
if ok {
c.adjustStats(-1, item.RespStatus)
}
item = OCSPResponseCacheItem{
Subject: subj,
CachedAt: time.Now().UTC().Round(time.Second),
RespStatus: certidp.StatusAssertionIntToVal[caResp.Status],
RespExpires: caResp.NextUpdate,
Resp: rawC,
}
c.cache[key] = item
c.adjustStats(1, item.RespStatus)
c.dirty = true
}
// Get returns a CA OCSP response from the OCSP peer cache matching the response fingerprint (a hash)
func (c *LocalCache) Get(key string, log *certidp.Log) []byte {
c.mu.RLock()
defer c.mu.RUnlock()
if !c.online || key == "" {
return nil
}
val, ok := c.cache[key]
if ok {
atomic.AddInt64(&c.stats.Hits, 1)
log.Debugf(certidp.DbgCacheHit, key)
} else {
atomic.AddInt64(&c.stats.Misses, 1)
log.Debugf(certidp.DbgCacheMiss, key)
return nil
}
resp, err := c.Decompress(val.Resp)
if err != nil {
log.Errorf(certidp.ErrResponseDecompressFail, key, err)
return nil
}
return resp
}
func (c *LocalCache) adjustStatsHitToMiss() {
atomic.AddInt64(&c.stats.Misses, 1)
atomic.AddInt64(&c.stats.Hits, -1)
}
func (c *LocalCache) adjustStats(delta int64, rs certidp.StatusAssertion) {
if delta == 0 {
return
}
atomic.AddInt64(&c.stats.Responses, delta)
switch rs {
case ocsp.Good:
atomic.AddInt64(&c.stats.Goods, delta)
case ocsp.Revoked:
atomic.AddInt64(&c.stats.Revokes, delta)
case ocsp.Unknown:
atomic.AddInt64(&c.stats.Unknowns, delta)
}
}
// Delete removes a CA OCSP response from the OCSP peer cache matching the response fingerprint (a hash)
func (c *LocalCache) Delete(key string, wasMiss bool, log *certidp.Log) {
c.mu.Lock()
defer c.mu.Unlock()
if !c.online || key == "" || c.config == nil {
return
}
item, ok := c.cache[key]
if !ok {
return
}
if item.RespStatus == ocsp.Revoked && c.config.PreserveRevoked {
log.Debugf(certidp.DbgPreservedRevocation, key)
if wasMiss {
c.adjustStatsHitToMiss()
}
return
}
log.Debugf(certidp.DbgDeletingCacheResponse, key)
delete(c.cache, key)
c.adjustStats(-1, item.RespStatus)
if wasMiss {
c.adjustStatsHitToMiss()
}
c.dirty = true
}
// Start initializes the configured OCSP peer cache, loads a saved cache from disk (if present), and initializes runtime statistics
func (c *LocalCache) Start(s *Server) {
s.Debugf(certidp.DbgStartingCache)
c.loadCache(s)
c.initStats()
c.mu.Lock()
c.online = true
c.mu.Unlock()
}
func (c *LocalCache) Stop(s *Server) {
c.mu.Lock()
s.Debugf(certidp.DbgStoppingCache)
c.online = false
c.timer.Stop()
c.mu.Unlock()
c.saveCache(s)
}
func (c *LocalCache) Online() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.online
}
func (c *LocalCache) Type() string {
c.mu.RLock()
defer c.mu.RUnlock()
return "local"
}
func (c *LocalCache) Config() *OCSPResponseCacheConfig {
c.mu.RLock()
defer c.mu.RUnlock()
return c.config
}
func (c *LocalCache) Stats() *OCSPResponseCacheStats {
c.mu.RLock()
defer c.mu.RUnlock()
if c.stats == nil {
return nil
}
stats := OCSPResponseCacheStats{
Responses: c.stats.Responses,
Hits: c.stats.Hits,
Misses: c.stats.Misses,
Revokes: c.stats.Revokes,
Goods: c.stats.Goods,
Unknowns: c.stats.Unknowns,
}
return &stats
}
func (c *LocalCache) initStats() {
c.mu.Lock()
defer c.mu.Unlock()
c.stats = &OCSPResponseCacheStats{}
c.stats.Hits = 0
c.stats.Misses = 0
c.stats.Responses = int64(len(c.cache))
for _, resp := range c.cache {
switch resp.RespStatus {
case ocsp.Good:
c.stats.Goods++
case ocsp.Revoked:
c.stats.Revokes++
case ocsp.Unknown:
c.stats.Unknowns++
}
}
}
func (c *LocalCache) Compress(buf []byte) ([]byte, error) {
bodyLen := int64(len(buf))
var output bytes.Buffer
writer := s2.NewWriter(&output)
input := bytes.NewReader(buf[:bodyLen])
if n, err := io.CopyN(writer, input, bodyLen); err != nil {
return nil, fmt.Errorf(certidp.ErrCannotWriteCompressed, err)
} else if n != bodyLen {
return nil, fmt.Errorf(certidp.ErrTruncatedWrite, n, bodyLen)
}
if err := writer.Close(); err != nil {
return nil, fmt.Errorf(certidp.ErrCannotCloseWriter, err)
}
return output.Bytes(), nil
}
func (c *LocalCache) Decompress(buf []byte) ([]byte, error) {
bodyLen := int64(len(buf))
input := bytes.NewReader(buf[:bodyLen])
reader := io.NopCloser(s2.NewReader(input))
output, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf(certidp.ErrCannotReadCompressed, err)
}
return output, reader.Close()
}
func (c *LocalCache) loadCache(s *Server) {
d := s.opts.OCSPCacheConfig.LocalStore
if d == _EMPTY_ {
d = OCSPResponseCacheDefaultDir
}
f := OCSPResponseCacheDefaultFilename
store, err := filepath.Abs(path.Join(d, f))
if err != nil {
s.Errorf(certidp.ErrLoadCacheFail, err)
return
}
s.Debugf(certidp.DbgLoadingCache, store)
c.mu.Lock()
defer c.mu.Unlock()
c.cache = make(map[string]OCSPResponseCacheItem)
dat, err := os.ReadFile(store)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
s.Debugf(certidp.DbgNoCacheFound)
} else {
s.Warnf(certidp.ErrLoadCacheFail, err)
}
return
}
err = json.Unmarshal(dat, &c.cache)
if err != nil {
// make sure clean cache
c.cache = make(map[string]OCSPResponseCacheItem)
s.Warnf(certidp.ErrLoadCacheFail, err)
c.dirty = true
return
}
c.dirty = false
}
func (c *LocalCache) saveCache(s *Server) {
c.mu.RLock()
dirty := c.dirty
c.mu.RUnlock()
if !dirty {
return
}
s.Debugf(certidp.DbgCacheDirtySave)
var d string
if c.config.LocalStore != _EMPTY_ {
d = c.config.LocalStore
} else {
d = OCSPResponseCacheDefaultDir
}
f := OCSPResponseCacheDefaultFilename
store, err := filepath.Abs(path.Join(d, f))
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
s.Debugf(certidp.DbgSavingCache, store)
if _, err := os.Stat(d); os.IsNotExist(err) {
err = os.Mkdir(d, defaultDirPerms)
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
}
tmp, err := os.CreateTemp(d, OCSPResponseCacheDefaultTempFilePrefix)
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}() // clean up any temp files
// RW lock here because we're going to snapshot the cache to disk and mark as clean if successful
c.mu.Lock()
defer c.mu.Unlock()
dat, err := json.MarshalIndent(c.cache, "", " ")
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
cacheSize, err := tmp.Write(dat)
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
err = tmp.Sync()
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
err = tmp.Close()
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
// do the final swap and overwrite any old saved peer cache
err = os.Rename(tmp.Name(), store)
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
c.dirty = false
s.Debugf(certidp.DbgCacheSaved, cacheSize)
}
var OCSPResponseCacheUsage = `
You may enable OCSP peer response cacheing at server configuration root level:
(If no TLS blocks are configured with OCSP peer verification, ocsp_cache is ignored.)
...
# short form enables with defaults
ocsp_cache: true
# if false or undefined and one or more TLS blocks are configured with OCSP peer verification, "none" is implied
# long form includes settable options
ocsp_cache {
# Cache type <none, local> (default local)
type: local
# Cache file directory for local-type cache (default _rc_ in current working directory)
local_store: "_rc_"
# Ignore cache deletes if cached OCSP response is Revoked status (default false)
preserve_revoked: false
# For local store, interval to save in-memory cache to disk in seconds (default 300 seconds, minimum 1 second)
save_interval: 300
}
...
Note: Cache of server's own OCSP response (staple) is enabled using the 'ocsp' configuration option.
`
func (s *Server) initOCSPResponseCache() {
// No mTLS OCSP or Leaf OCSP enablements, so no need to init cache
s.mu.RLock()
if !s.ocspPeerVerify {
s.mu.RUnlock()
return
}
s.mu.RUnlock()
so := s.getOpts()
if so.OCSPCacheConfig == nil {
so.OCSPCacheConfig = NewOCSPResponseCacheConfig()
}
var cc = so.OCSPCacheConfig
s.mu.Lock()
defer s.mu.Unlock()
switch cc.Type {
case NONE:
s.ocsprc = &NoOpCache{config: cc, online: true, mu: &sync.RWMutex{}}
case LOCAL:
c := &LocalCache{
config: cc,
online: false,
cache: make(map[string]OCSPResponseCacheItem),
mu: &sync.RWMutex{},
dirty: false,
}
c.saveInterval = time.Duration(cc.SaveInterval) * time.Second
c.timer = time.AfterFunc(c.saveInterval, func() {
s.Debugf(certidp.DbgCacheSaveTimerExpired)
c.saveCache(s)
c.timer.Reset(c.saveInterval)
})
s.ocsprc = c
default:
s.Fatalf(certidp.ErrBadCacheTypeConfig, cc.Type)
}
}
func (s *Server) startOCSPResponseCache() {
// No mTLS OCSP or Leaf OCSP enablements, so no need to start cache
s.mu.RLock()
if !s.ocspPeerVerify || s.ocsprc == nil {
s.mu.RUnlock()
return
}
s.mu.RUnlock()
// Could be heavier operation depending on cache implementation
s.ocsprc.Start(s)
if s.ocsprc.Online() {
s.Noticef(certidp.MsgCacheOnline, s.ocsprc.Type())
} else {
s.Noticef(certidp.MsgCacheOffline, s.ocsprc.Type())
}
}
func (s *Server) stopOCSPResponseCache() {
s.mu.RLock()
if s.ocsprc == nil {
s.mu.RUnlock()
return
}
s.mu.RUnlock()
s.ocsprc.Stop(s)
}
func parseOCSPResponseCache(v any) (pcfg *OCSPResponseCacheConfig, retError error) {
var lt token
defer convertPanicToError(&lt, &retError)
tk, v := unwrapValue(v, &lt)
cm, ok := v.(map[string]any)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrIllegalCacheOptsConfig, v)}
}
pcfg = NewOCSPResponseCacheConfig()
retError = nil
for mk, mv := range cm {
// Again, unwrap token value if line check is required.
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "type":
cache, ok := mv.(string)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
cacheType, exists := OCSPResponseCacheTypeMap[strings.ToLower(cache)]
if !exists {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrUnknownCacheType, cache)}
}
pcfg.Type = cacheType
case "local_store":
store, ok := mv.(string)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
pcfg.LocalStore = store
case "preserve_revoked":
preserve, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
pcfg.PreserveRevoked = preserve
case "save_interval":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, err)}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldTypeConversion, "unexpected type")}
}
si := time.Duration(at) * time.Second
if si < OCSPResponseCacheMinimumSaveInterval {
si = OCSPResponseCacheMinimumSaveInterval
}
pcfg.SaveInterval = si.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
}
return pcfg, nil
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+275
View File
@@ -0,0 +1,275 @@
// Copyright 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.
// Inspired by https://github.com/protocolbuffers/protobuf-go/blob/master/encoding/protowire/wire.go
package server
import (
"errors"
"fmt"
"math"
)
var errProtoInsufficient = errors.New("insufficient data to read a value")
var errProtoOverflow = errors.New("too much data for a value")
var errProtoInvalidFieldNumber = errors.New("invalid field number")
func protoScanField(b []byte) (num, typ, size int, err error) {
num, typ, sizeTag, err := protoScanTag(b)
if err != nil {
return 0, 0, 0, err
}
b = b[sizeTag:]
sizeValue, err := protoScanFieldValue(typ, b)
if err != nil {
return 0, 0, 0, err
}
return num, typ, sizeTag + sizeValue, nil
}
func protoScanTag(b []byte) (num, typ, size int, err error) {
tagint, size, err := protoScanVarint(b)
if err != nil {
return 0, 0, 0, err
}
// NOTE: MessageSet allows for larger field numbers than normal.
if (tagint >> 3) > uint64(math.MaxInt32) {
return 0, 0, 0, errProtoInvalidFieldNumber
}
num = int(tagint >> 3)
if num < 1 {
return 0, 0, 0, errProtoInvalidFieldNumber
}
typ = int(tagint & 7)
return num, typ, size, nil
}
func protoScanFieldValue(typ int, b []byte) (size int, err error) {
switch typ {
case 0:
_, size, err = protoScanVarint(b)
case 5: // fixed32
if len(b) < 4 {
return 0, errProtoInsufficient
}
size = 4
case 1: // fixed64
if len(b) < 8 {
return 0, errProtoInsufficient
}
size = 8
case 2: // length-delimited
size, err = protoScanBytes(b)
default:
return 0, fmt.Errorf("unsupported type: %d", typ)
}
return size, err
}
func protoScanVarint(b []byte) (v uint64, size int, err error) {
var y uint64
if len(b) <= 0 {
return 0, 0, errProtoInsufficient
}
v = uint64(b[0])
if v < 0x80 {
return v, 1, nil
}
v -= 0x80
if len(b) <= 1 {
return 0, 0, errProtoInsufficient
}
y = uint64(b[1])
v += y << 7
if y < 0x80 {
return v, 2, nil
}
v -= 0x80 << 7
if len(b) <= 2 {
return 0, 0, errProtoInsufficient
}
y = uint64(b[2])
v += y << 14
if y < 0x80 {
return v, 3, nil
}
v -= 0x80 << 14
if len(b) <= 3 {
return 0, 0, errProtoInsufficient
}
y = uint64(b[3])
v += y << 21
if y < 0x80 {
return v, 4, nil
}
v -= 0x80 << 21
if len(b) <= 4 {
return 0, 0, errProtoInsufficient
}
y = uint64(b[4])
v += y << 28
if y < 0x80 {
return v, 5, nil
}
v -= 0x80 << 28
if len(b) <= 5 {
return 0, 0, errProtoInsufficient
}
y = uint64(b[5])
v += y << 35
if y < 0x80 {
return v, 6, nil
}
v -= 0x80 << 35
if len(b) <= 6 {
return 0, 0, errProtoInsufficient
}
y = uint64(b[6])
v += y << 42
if y < 0x80 {
return v, 7, nil
}
v -= 0x80 << 42
if len(b) <= 7 {
return 0, 0, errProtoInsufficient
}
y = uint64(b[7])
v += y << 49
if y < 0x80 {
return v, 8, nil
}
v -= 0x80 << 49
if len(b) <= 8 {
return 0, 0, errProtoInsufficient
}
y = uint64(b[8])
v += y << 56
if y < 0x80 {
return v, 9, nil
}
v -= 0x80 << 56
if len(b) <= 9 {
return 0, 0, errProtoInsufficient
}
y = uint64(b[9])
v += y << 63
if y < 2 {
return v, 10, nil
}
return 0, 0, errProtoOverflow
}
func protoScanBytes(b []byte) (size int, err error) {
l, lenSize, err := protoScanVarint(b)
if err != nil {
return 0, err
}
if l > uint64(len(b[lenSize:])) {
return 0, errProtoInsufficient
}
return lenSize + int(l), nil
}
func protoEncodeVarint(v uint64) []byte {
b := make([]byte, 0, 10)
switch {
case v < 1<<7:
b = append(b, byte(v))
case v < 1<<14:
b = append(b,
byte((v>>0)&0x7f|0x80),
byte(v>>7))
case v < 1<<21:
b = append(b,
byte((v>>0)&0x7f|0x80),
byte((v>>7)&0x7f|0x80),
byte(v>>14))
case v < 1<<28:
b = append(b,
byte((v>>0)&0x7f|0x80),
byte((v>>7)&0x7f|0x80),
byte((v>>14)&0x7f|0x80),
byte(v>>21))
case v < 1<<35:
b = append(b,
byte((v>>0)&0x7f|0x80),
byte((v>>7)&0x7f|0x80),
byte((v>>14)&0x7f|0x80),
byte((v>>21)&0x7f|0x80),
byte(v>>28))
case v < 1<<42:
b = append(b,
byte((v>>0)&0x7f|0x80),
byte((v>>7)&0x7f|0x80),
byte((v>>14)&0x7f|0x80),
byte((v>>21)&0x7f|0x80),
byte((v>>28)&0x7f|0x80),
byte(v>>35))
case v < 1<<49:
b = append(b,
byte((v>>0)&0x7f|0x80),
byte((v>>7)&0x7f|0x80),
byte((v>>14)&0x7f|0x80),
byte((v>>21)&0x7f|0x80),
byte((v>>28)&0x7f|0x80),
byte((v>>35)&0x7f|0x80),
byte(v>>42))
case v < 1<<56:
b = append(b,
byte((v>>0)&0x7f|0x80),
byte((v>>7)&0x7f|0x80),
byte((v>>14)&0x7f|0x80),
byte((v>>21)&0x7f|0x80),
byte((v>>28)&0x7f|0x80),
byte((v>>35)&0x7f|0x80),
byte((v>>42)&0x7f|0x80),
byte(v>>49))
case v < 1<<63:
b = append(b,
byte((v>>0)&0x7f|0x80),
byte((v>>7)&0x7f|0x80),
byte((v>>14)&0x7f|0x80),
byte((v>>21)&0x7f|0x80),
byte((v>>28)&0x7f|0x80),
byte((v>>35)&0x7f|0x80),
byte((v>>42)&0x7f|0x80),
byte((v>>49)&0x7f|0x80),
byte(v>>56))
default:
b = append(b,
byte((v>>0)&0x7f|0x80),
byte((v>>7)&0x7f|0x80),
byte((v>>14)&0x7f|0x80),
byte((v>>21)&0x7f|0x80),
byte((v>>28)&0x7f|0x80),
byte((v>>35)&0x7f|0x80),
byte((v>>42)&0x7f|0x80),
byte((v>>49)&0x7f|0x80),
byte((v>>56)&0x7f|0x80),
1)
}
return b
}
+30
View File
@@ -0,0 +1,30 @@
/*
* Compile and run this as a C program to get the kinfo_proc offsets
* for your architecture.
* While FreeBSD works hard at binary-compatibility within an ABI, various
* we can't say for sure that these are right for _all_ use on a hardware
* platform. The LP64 ifdef affects the offsets considerably.
*
* We use these offsets in hardware-specific files for FreeBSD, to avoid a cgo
* compilation-time dependency, allowing us to cross-compile for FreeBSD from
* other hardware platforms, letting us distribute binaries for FreeBSD.
*/
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/user.h>
#define SHOW_OFFSET(FIELD) printf(" KIP_OFF_%s = %zu\n", #FIELD, offsetof(struct kinfo_proc, ki_ ## FIELD))
int main(int argc, char *argv[]) {
/* Uncomment these if you want some extra debugging aids:
SHOW_OFFSET(pid);
SHOW_OFFSET(ppid);
SHOW_OFFSET(uid);
*/
SHOW_OFFSET(size);
SHOW_OFFSET(rssize);
SHOW_OFFSET(pctcpu);
}
@@ -0,0 +1,86 @@
// Copyright 2015-2021 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 pse
// On macs after some studying it seems that typical tools like ps and activity monitor report MaxRss and not
// current RSS. I wrote some C code to pull the real RSS and although it does not go down very often, when it does
// that is not reflected in the typical tooling one might compare us to, so we can skip cgo and just use rusage imo.
// We also do not use virtual memory in the upper layers at all, so ok to skip since rusage does not report vss.
import (
"math"
"sync"
"syscall"
"time"
)
type lastUsage struct {
sync.Mutex
last time.Time
cpu time.Duration
rss int64
pcpu float64
}
// To hold the last usage and call time.
var lu lastUsage
func init() {
updateUsage()
periodic()
}
// Get our usage.
func getUsage() (now time.Time, cpu time.Duration, rss int64) {
var ru syscall.Rusage
syscall.Getrusage(syscall.RUSAGE_SELF, &ru)
now = time.Now()
cpu = time.Duration(ru.Utime.Sec)*time.Second + time.Duration(ru.Utime.Usec)*time.Microsecond
cpu += time.Duration(ru.Stime.Sec)*time.Second + time.Duration(ru.Stime.Usec)*time.Microsecond
return now, cpu, ru.Maxrss
}
// Update last usage.
// We need to have a prior sample to compute pcpu.
func updateUsage() (pcpu float64, rss int64) {
lu.Lock()
defer lu.Unlock()
now, cpu, rss := getUsage()
// Don't skew pcpu by sampling too close to last sample.
if elapsed := now.Sub(lu.last); elapsed < 500*time.Millisecond {
// Always update rss.
lu.rss = rss
} else {
tcpu := float64(cpu - lu.cpu)
lu.last, lu.cpu, lu.rss = now, cpu, rss
// Want to make this one decimal place and not count on upper layers.
// Cores already taken into account via cpu time measurements.
lu.pcpu = math.Round(tcpu/float64(elapsed)*1000) / 10
}
return lu.pcpu, lu.rss
}
// Sampling function to keep pcpu relevant.
func periodic() {
updateUsage()
time.AfterFunc(time.Second, periodic)
}
// ProcUsage returns CPU and memory usage.
// Note upper layers do not use virtual memory size, so ok that it is not filled in here.
func ProcUsage(pcpu *float64, rss, vss *int64) error {
*pcpu, *rss = updateUsage()
return nil
}
@@ -0,0 +1,36 @@
// Copyright 2015-2023 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.
//
// Copied from pse_openbsd.go
package pse
import (
"fmt"
"os"
"os/exec"
)
// ProcUsage returns CPU usage
func ProcUsage(pcpu *float64, rss, vss *int64) error {
pidStr := fmt.Sprintf("%d", os.Getpid())
out, err := exec.Command("ps", "o", "pcpu=,rss=,vsz=", "-p", pidStr).Output()
if err != nil {
*rss, *vss = -1, -1
return fmt.Errorf("ps call failed:%v", err)
}
fmt.Sscanf(string(out), "%f %d %d", pcpu, rss, vss)
*rss *= 1024 // 1k blocks, want bytes.
*vss *= 1024 // 1k blocks, want bytes.
return nil
}
@@ -0,0 +1,85 @@
// Copyright 2015-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.
//go:build cgo && freebsd
package pse
/*
#include <sys/types.h>
#include <sys/sysctl.h>
#include <sys/user.h>
#include <stddef.h>
#include <unistd.h>
long pagetok(long size)
{
int pageshift, pagesize;
pagesize = getpagesize();
pageshift = 0;
while (pagesize > 1) {
pageshift++;
pagesize >>= 1;
}
return (size << pageshift);
}
int getusage(double *pcpu, unsigned int *rss, unsigned int *vss)
{
int mib[4], ret;
size_t len;
struct kinfo_proc kp;
len = 4;
sysctlnametomib("kern.proc.pid", mib, &len);
mib[3] = getpid();
len = sizeof(kp);
ret = sysctl(mib, 4, &kp, &len, NULL, 0);
if (ret != 0) {
return (errno);
}
*rss = pagetok(kp.ki_rssize);
*vss = kp.ki_size;
*pcpu = (double)kp.ki_pctcpu / FSCALE;
return 0;
}
*/
import "C"
import (
"syscall"
)
// This is a placeholder for now.
func ProcUsage(pcpu *float64, rss, vss *int64) error {
var r, v C.uint
var c C.double
if ret := C.getusage(&c, &r, &v); ret != 0 {
return syscall.Errno(ret)
}
*pcpu = float64(c)
*rss = int64(r)
*vss = int64(v)
return nil
}
@@ -0,0 +1,125 @@
// Copyright 2015-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.
// There are two FreeBSD implementations; one which uses cgo and should build
// locally on any FreeBSD, and this one which uses sysctl but needs us to know
// the offset constants for the fields we care about.
//
// The advantage of this one is that without cgo, it is much easier to
// cross-compile to a target. The official releases are all built with
// cross-compilation.
//
// We've switched the other implementation to include '_cgo' in the filename,
// to show that it's not the default. This isn't an os or arch build tag,
// so we have to use explicit build-tags within.
// If lacking CGO support and targeting an unsupported arch, then before the
// change you would have a compile failure for not being able to cross-compile.
// After the change, you have a compile failure for not having the symbols
// because no source file satisfies them.
// Thus we are no worse off, and it's now much easier to extend support for
// non-CGO to new architectures, just by editing this file.
//
// To generate for other architectures:
// 1. Copy `freebsd.txt` to have a .c filename on a box running the target
// architecture, compile and run it.
// 2. Update the init() function below to include a case for this architecture
// 3. Update the build-tags in this file.
//go:build !cgo && freebsd && (amd64 || arm64)
package pse
import (
"encoding/binary"
"runtime"
"syscall"
"golang.org/x/sys/unix"
)
// On FreeBSD, to get proc information we read it out of the kernel using a
// binary sysctl. The endianness of integers is thus explicitly "host", rather
// than little or big endian.
var nativeEndian = binary.LittleEndian
var pageshift int // derived from getpagesize(3) in init() below
var (
// These are populated in the init function, based on the current architecture.
// (It's less file-count explosion than having one small file for each
// FreeBSD architecture).
KIP_OFF_size int
KIP_OFF_rssize int
KIP_OFF_pctcpu int
)
func init() {
switch runtime.GOARCH {
// These are the values which come from compiling and running
// freebsd.txt as a C program.
// Most recently validated: 2025-04 with FreeBSD 14.2R in AWS.
case "amd64":
KIP_OFF_size = 256
KIP_OFF_rssize = 264
KIP_OFF_pctcpu = 308
case "arm64":
KIP_OFF_size = 256
KIP_OFF_rssize = 264
KIP_OFF_pctcpu = 308
default:
panic("code bug: server/pse FreeBSD support missing case for '" + runtime.GOARCH + "' but build-tags allowed us to build anyway?")
}
// To get the physical page size, the C library checks two places:
// process ELF auxiliary info, AT_PAGESZ
// as a fallback, the hw.pagesize sysctl
// In looking closely, I found that the Go runtime support is handling
// this for us, and exposing that as syscall.Getpagesize, having checked
// both in the same ways, at process start, so a call to that should return
// a memory value without even a syscall bounce.
pagesize := syscall.Getpagesize()
pageshift = 0
for pagesize > 1 {
pageshift += 1
pagesize >>= 1
}
}
func ProcUsage(pcpu *float64, rss, vss *int64) error {
rawdata, err := unix.SysctlRaw("kern.proc.pid", unix.Getpid())
if err != nil {
return err
}
r_vss_bytes := nativeEndian.Uint32(rawdata[KIP_OFF_size:])
r_rss_pages := nativeEndian.Uint32(rawdata[KIP_OFF_rssize:])
rss_bytes := r_rss_pages << pageshift
// In C: fixpt_t ki_pctcpu
// Doc: %cpu for process during ki_swtime
// fixpt_t is __uint32_t
// usr.bin/top uses pctdouble to convert to a double (float64)
// define pctdouble(p) ((double)(p) / FIXED_PCTCPU)
// FIXED_PCTCPU is _usually_ FSCALE (some architectures are special)
// <sys/param.h> has:
// #define FSHIFT 11 /* bits to right of fixed binary point */
// #define FSCALE (1<<FSHIFT)
r_pcpu := nativeEndian.Uint32(rawdata[KIP_OFF_pctcpu:])
f_pcpu := float64(r_pcpu) / float64(2048)
*rss = int64(rss_bytes)
*vss = int64(r_vss_bytes)
*pcpu = f_pcpu
return nil
}
+128
View File
@@ -0,0 +1,128 @@
// Copyright 2015-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 pse
import (
"bytes"
"fmt"
"os"
"sync/atomic"
"syscall"
"time"
)
var (
procStatFile string
ticks int64
lastTotal int64
lastSeconds int64
ipcpu int64
pageSize int64
)
const (
utimePos = 13
stimePos = 14
startPos = 21
vssPos = 22
rssPos = 23
)
func init() {
// Avoiding to generate docker image without CGO
ticks = 100 // int64(C.sysconf(C._SC_CLK_TCK))
procStatFile = fmt.Sprintf("/proc/%d/stat", os.Getpid())
pageSize = int64(os.Getpagesize())
periodic()
}
// Sampling function to keep pcpu relevant.
func periodic() {
contents, err := os.ReadFile(procStatFile)
if err != nil {
return
}
fields := bytes.Fields(contents)
// PCPU
pstart := parseInt64(fields[startPos])
utime := parseInt64(fields[utimePos])
stime := parseInt64(fields[stimePos])
total := utime + stime
var sysinfo syscall.Sysinfo_t
if err := syscall.Sysinfo(&sysinfo); err != nil {
return
}
seconds := int64(sysinfo.Uptime) - (pstart / ticks)
// Save off temps
lt := lastTotal
ls := lastSeconds
// Update last sample
lastTotal = total
lastSeconds = seconds
// Adjust to current time window
total -= lt
seconds -= ls
if seconds > 0 {
atomic.StoreInt64(&ipcpu, (total*1000/ticks)/seconds)
}
time.AfterFunc(1*time.Second, periodic)
}
// ProcUsage returns CPU usage
func ProcUsage(pcpu *float64, rss, vss *int64) error {
contents, err := os.ReadFile(procStatFile)
if err != nil {
return err
}
fields := bytes.Fields(contents)
// Memory
*rss = (parseInt64(fields[rssPos])) * pageSize
*vss = parseInt64(fields[vssPos])
// PCPU
// We track this with periodic sampling, so just load and go.
*pcpu = float64(atomic.LoadInt64(&ipcpu)) / 10.0
return nil
}
// Ascii numbers 0-9
const (
asciiZero = 48
asciiNine = 57
)
// parseInt64 expects decimal positive numbers. We
// return -1 to signal error
func parseInt64(d []byte) (n int64) {
if len(d) == 0 {
return -1
}
for _, dec := range d {
if dec < asciiZero || dec > asciiNine {
return -1
}
n = n*10 + (int64(dec) - asciiZero)
}
return n
}
@@ -0,0 +1,36 @@
// Copyright 2022 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.
//
// Copied from pse_openbsd.go
package pse
import (
"fmt"
"os"
"os/exec"
)
// ProcUsage returns CPU usage
func ProcUsage(pcpu *float64, rss, vss *int64) error {
pidStr := fmt.Sprintf("%d", os.Getpid())
out, err := exec.Command("ps", "o", "pcpu=,rss=,vsz=", "-p", pidStr).Output()
if err != nil {
*rss, *vss = -1, -1
return fmt.Errorf("ps call failed:%v", err)
}
fmt.Sscanf(string(out), "%f %d %d", pcpu, rss, vss)
*rss *= 1024 // 1k blocks, want bytes.
*vss *= 1024 // 1k blocks, want bytes.
return nil
}

Some files were not shown because too many files have changed in this diff Show More