Initial QSfera import
This commit is contained in:
+201
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+1319
File diff suppressed because it is too large
Load Diff
+514
@@ -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
@@ -0,0 +1,6 @@
|
||||
listen: 127.0.0.1:4222
|
||||
|
||||
authorization {
|
||||
include 'includes/users.conf' # Pull in from file
|
||||
timeout: 0.5
|
||||
}
|
||||
+27
@@ -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.
|
||||
+23
@@ -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
@@ -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
@@ -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
@@ -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...))
|
||||
}
|
||||
}
|
||||
+112
@@ -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
@@ -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
|
||||
consumer’s 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 client’s
|
||||
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 manager’s sublist’s
|
||||
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 session’s 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 don’t 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 session’s 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 NATS’s `“*”` wildcard, however, MQTT’s 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
@@ -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.
|
||||
+4784
File diff suppressed because it is too large
Load Diff
+86
@@ -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
|
||||
}
|
||||
+1744
File diff suppressed because it is too large
Load Diff
+503
@@ -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
@@ -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
|
||||
}
|
||||
+312
@@ -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
|
||||
}
|
||||
+106
@@ -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]"
|
||||
)
|
||||
+92
@@ -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)
|
||||
}
|
||||
+104
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+45
@@ -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{}
|
||||
Generated
Vendored
+965
@@ -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{}
|
||||
+79
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
+6863
File diff suppressed because it is too large
Load Diff
+405
@@ -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
@@ -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
|
||||
)
|
||||
+7059
File diff suppressed because it is too large
Load Diff
+327
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+21
@@ -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
|
||||
}
|
||||
+37
@@ -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
|
||||
}
|
||||
+38
@@ -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
|
||||
}
|
||||
|
||||
+20
@@ -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
|
||||
}
|
||||
+21
@@ -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
|
||||
}
|
||||
+60
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+2222
File diff suppressed because it is too large
Load Diff
+3359
File diff suppressed because it is too large
Load Diff
+130
@@ -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())
|
||||
}
|
||||
}
|
||||
+13703
File diff suppressed because it is too large
Load Diff
+3434
File diff suppressed because it is too large
Load Diff
+566
@@ -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
@@ -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)
|
||||
}
|
||||
+2880
File diff suppressed because it is too large
Load Diff
+5341
File diff suppressed because it is too large
Load Diff
+1065
File diff suppressed because it is too large
Load Diff
+11244
File diff suppressed because it is too large
Load Diff
+102
@@ -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
|
||||
}
|
||||
Generated
Vendored
+3442
File diff suppressed because it is too large
Load Diff
+366
@@ -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
|
||||
}
|
||||
+246
@@ -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
@@ -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)
|
||||
}
|
||||
+3711
File diff suppressed because it is too large
Load Diff
+295
@@ -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...)
|
||||
}
|
||||
+2758
File diff suppressed because it is too large
Load Diff
+4280
File diff suppressed because it is too large
Load Diff
+156
@@ -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
|
||||
}
|
||||
}
|
||||
+5950
File diff suppressed because it is too large
Load Diff
+806
@@ -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
@@ -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
@@ -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
@@ -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(<, &retError)
|
||||
tk, v := unwrapValue(v, <)
|
||||
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, <)
|
||||
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
|
||||
}
|
||||
+636
@@ -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(<, &retError)
|
||||
tk, v := unwrapValue(v, <)
|
||||
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, <)
|
||||
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
|
||||
}
|
||||
+6610
File diff suppressed because it is too large
Load Diff
+1325
File diff suppressed because it is too large
Load Diff
+275
@@ -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
@@ -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);
|
||||
}
|
||||
+86
@@ -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
|
||||
}
|
||||
+36
@@ -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
|
||||
}
|
||||
+85
@@ -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
|
||||
}
|
||||
+125
@@ -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
@@ -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
|
||||
}
|
||||
+36
@@ -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
Reference in New Issue
Block a user