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.
|
||||
+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
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright 2015-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.
|
||||
//
|
||||
// Copied from pse_darwin.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
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// 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 rumprun
|
||||
|
||||
package pse
|
||||
|
||||
// This is a placeholder for now.
|
||||
func ProcUsage(pcpu *float64, rss, vss *int64) error {
|
||||
*pcpu = 0.0
|
||||
*rss = 0
|
||||
*vss = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// 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.
|
||||
//
|
||||
// Copied from pse_openbsd.go
|
||||
|
||||
//go:build illumos || solaris
|
||||
|
||||
package pse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
lines := strings.Split(string(out), "\n")
|
||||
if len(lines) < 2 {
|
||||
*rss, *vss = -1, -1
|
||||
return fmt.Errorf("no ps output")
|
||||
}
|
||||
output := lines[1]
|
||||
fmt.Sscanf(output, "%f %d %d", pcpu, rss, vss)
|
||||
*rss *= 1024 // 1k blocks, want bytes.
|
||||
*vss *= 1024 // 1k blocks, want bytes.
|
||||
return nil
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// 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 pse
|
||||
|
||||
// This is a placeholder for now.
|
||||
func ProcUsage(pcpu *float64, rss, vss *int64) error {
|
||||
*pcpu = 0.0
|
||||
*rss = 0
|
||||
*vss = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
// 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 windows
|
||||
|
||||
package pse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
pdh = windows.NewLazySystemDLL("pdh.dll")
|
||||
winPdhOpenQuery = pdh.NewProc("PdhOpenQuery")
|
||||
winPdhAddCounter = pdh.NewProc("PdhAddCounterW")
|
||||
winPdhCollectQueryData = pdh.NewProc("PdhCollectQueryData")
|
||||
winPdhGetFormattedCounterValue = pdh.NewProc("PdhGetFormattedCounterValue")
|
||||
winPdhGetFormattedCounterArray = pdh.NewProc("PdhGetFormattedCounterArrayW")
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := pdh.Load(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, p := range []*windows.LazyProc{
|
||||
winPdhOpenQuery, winPdhAddCounter, winPdhCollectQueryData,
|
||||
winPdhGetFormattedCounterValue, winPdhGetFormattedCounterArray,
|
||||
} {
|
||||
if err := p.Find(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// global performance counter query handle and counters
|
||||
var (
|
||||
pcHandle PDH_HQUERY
|
||||
pidCounter, cpuCounter, rssCounter, vssCounter PDH_HCOUNTER
|
||||
prevCPU float64
|
||||
prevRss int64
|
||||
prevVss int64
|
||||
lastSampleTime time.Time
|
||||
processPid int
|
||||
pcQueryLock sync.Mutex
|
||||
initialSample = true
|
||||
)
|
||||
|
||||
// maxQuerySize is the number of values to return from a query.
|
||||
// It represents the maximum # of servers that can be queried
|
||||
// simultaneously running on a machine.
|
||||
const maxQuerySize = 512
|
||||
|
||||
// Keep static memory around to reuse; this works best for passing
|
||||
// into the pdh API.
|
||||
var counterResults [maxQuerySize]PDH_FMT_COUNTERVALUE_ITEM_DOUBLE
|
||||
|
||||
// PDH Types
|
||||
type (
|
||||
PDH_HQUERY syscall.Handle
|
||||
PDH_HCOUNTER syscall.Handle
|
||||
)
|
||||
|
||||
// PDH constants used here
|
||||
const (
|
||||
PDH_FMT_DOUBLE = 0x00000200
|
||||
PDH_INVALID_DATA = 0xC0000BC6
|
||||
PDH_MORE_DATA = 0x800007D2
|
||||
)
|
||||
|
||||
// PDH_FMT_COUNTERVALUE_DOUBLE - double value
|
||||
type PDH_FMT_COUNTERVALUE_DOUBLE struct {
|
||||
CStatus uint32
|
||||
DoubleValue float64
|
||||
}
|
||||
|
||||
// PDH_FMT_COUNTERVALUE_ITEM_DOUBLE is an array
|
||||
// element of a double value
|
||||
type PDH_FMT_COUNTERVALUE_ITEM_DOUBLE struct {
|
||||
SzName *uint16 // pointer to a string
|
||||
FmtValue PDH_FMT_COUNTERVALUE_DOUBLE
|
||||
}
|
||||
|
||||
func pdhAddCounter(hQuery PDH_HQUERY, szFullCounterPath string, dwUserData uintptr, phCounter *PDH_HCOUNTER) error {
|
||||
ptxt, _ := syscall.UTF16PtrFromString(szFullCounterPath)
|
||||
r0, _, _ := winPdhAddCounter.Call(
|
||||
uintptr(hQuery),
|
||||
uintptr(unsafe.Pointer(ptxt)),
|
||||
dwUserData,
|
||||
uintptr(unsafe.Pointer(phCounter)))
|
||||
|
||||
if r0 != 0 {
|
||||
return fmt.Errorf("pdhAddCounter failed. %d", r0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pdhOpenQuery(datasrc *uint16, userdata uint32, query *PDH_HQUERY) error {
|
||||
r0, _, _ := syscall.Syscall(winPdhOpenQuery.Addr(), 3, 0, uintptr(userdata), uintptr(unsafe.Pointer(query)))
|
||||
if r0 != 0 {
|
||||
return fmt.Errorf("pdhOpenQuery failed - %d", r0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pdhCollectQueryData(hQuery PDH_HQUERY) error {
|
||||
r0, _, _ := winPdhCollectQueryData.Call(uintptr(hQuery))
|
||||
if r0 != 0 {
|
||||
return fmt.Errorf("pdhCollectQueryData failed - %d", r0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pdhGetFormattedCounterArrayDouble returns the value of return code
|
||||
// rather than error, to easily check return codes
|
||||
func pdhGetFormattedCounterArrayDouble(hCounter PDH_HCOUNTER, lpdwBufferSize *uint32, lpdwBufferCount *uint32, itemBuffer *PDH_FMT_COUNTERVALUE_ITEM_DOUBLE) uint32 {
|
||||
ret, _, _ := winPdhGetFormattedCounterArray.Call(
|
||||
uintptr(hCounter),
|
||||
uintptr(PDH_FMT_DOUBLE),
|
||||
uintptr(unsafe.Pointer(lpdwBufferSize)),
|
||||
uintptr(unsafe.Pointer(lpdwBufferCount)),
|
||||
uintptr(unsafe.Pointer(itemBuffer)))
|
||||
|
||||
return uint32(ret)
|
||||
}
|
||||
|
||||
func getCounterArrayData(counter PDH_HCOUNTER) ([]float64, error) {
|
||||
var bufSize uint32
|
||||
var bufCount uint32
|
||||
|
||||
// Retrieving array data requires two calls, the first which
|
||||
// requires an addressable empty buffer, and sets size fields.
|
||||
// The second call returns the data.
|
||||
initialBuf := make([]PDH_FMT_COUNTERVALUE_ITEM_DOUBLE, 1)
|
||||
ret := pdhGetFormattedCounterArrayDouble(counter, &bufSize, &bufCount, &initialBuf[0])
|
||||
if ret == PDH_MORE_DATA {
|
||||
// we'll likely never get here, but be safe.
|
||||
if bufCount > maxQuerySize {
|
||||
bufCount = maxQuerySize
|
||||
}
|
||||
ret = pdhGetFormattedCounterArrayDouble(counter, &bufSize, &bufCount, &counterResults[0])
|
||||
if ret == 0 {
|
||||
rv := make([]float64, bufCount)
|
||||
for i := 0; i < int(bufCount); i++ {
|
||||
rv[i] = counterResults[i].FmtValue.DoubleValue
|
||||
}
|
||||
return rv, nil
|
||||
}
|
||||
}
|
||||
if ret != 0 {
|
||||
return nil, fmt.Errorf("getCounterArrayData failed - %d", ret)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getProcessImageName returns the name of the process image, as expected by
|
||||
// the performance counter API.
|
||||
func getProcessImageName() (name string) {
|
||||
name = filepath.Base(os.Args[0])
|
||||
name = strings.TrimSuffix(name, ".exe")
|
||||
return
|
||||
}
|
||||
|
||||
// initialize our counters
|
||||
func initCounters() (err error) {
|
||||
|
||||
processPid = os.Getpid()
|
||||
// require an addressible nil pointer
|
||||
var source uint16
|
||||
if err := pdhOpenQuery(&source, 0, &pcHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// setup the performance counters, search for all server instances
|
||||
name := fmt.Sprintf("%s*", getProcessImageName())
|
||||
pidQuery := fmt.Sprintf("\\Process(%s)\\ID Process", name)
|
||||
cpuQuery := fmt.Sprintf("\\Process(%s)\\%% Processor Time", name)
|
||||
rssQuery := fmt.Sprintf("\\Process(%s)\\Working Set - Private", name)
|
||||
vssQuery := fmt.Sprintf("\\Process(%s)\\Virtual Bytes", name)
|
||||
|
||||
if err = pdhAddCounter(pcHandle, pidQuery, 0, &pidCounter); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = pdhAddCounter(pcHandle, cpuQuery, 0, &cpuCounter); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = pdhAddCounter(pcHandle, rssQuery, 0, &rssCounter); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = pdhAddCounter(pcHandle, vssQuery, 0, &vssCounter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prime the counters by collecting once, and sleep to get somewhat
|
||||
// useful information the first request. Counters for the CPU require
|
||||
// at least two collect calls.
|
||||
if err = pdhCollectQueryData(pcHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(50)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcUsage returns process CPU and memory statistics
|
||||
func ProcUsage(pcpu *float64, rss, vss *int64) error {
|
||||
var err error
|
||||
|
||||
// For simplicity, protect the entire call.
|
||||
// Most simultaneous requests will immediately return
|
||||
// with cached values.
|
||||
pcQueryLock.Lock()
|
||||
defer pcQueryLock.Unlock()
|
||||
|
||||
// First time through, initialize counters.
|
||||
if initialSample {
|
||||
if err = initCounters(); err != nil {
|
||||
return err
|
||||
}
|
||||
initialSample = false
|
||||
} else if time.Since(lastSampleTime) < (2 * time.Second) {
|
||||
// only refresh every two seconds as to minimize impact
|
||||
// on the server.
|
||||
*pcpu = prevCPU
|
||||
*rss = prevRss
|
||||
*vss = prevVss
|
||||
return nil
|
||||
}
|
||||
|
||||
// always save the sample time, even on errors.
|
||||
defer func() {
|
||||
lastSampleTime = time.Now()
|
||||
}()
|
||||
|
||||
// refresh the performance counter data
|
||||
if err = pdhCollectQueryData(pcHandle); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// retrieve the data
|
||||
var pidAry, cpuAry, rssAry, vssAry []float64
|
||||
if pidAry, err = getCounterArrayData(pidCounter); err != nil {
|
||||
return err
|
||||
}
|
||||
if cpuAry, err = getCounterArrayData(cpuCounter); err != nil {
|
||||
return err
|
||||
}
|
||||
if rssAry, err = getCounterArrayData(rssCounter); err != nil {
|
||||
return err
|
||||
}
|
||||
if vssAry, err = getCounterArrayData(vssCounter); err != nil {
|
||||
return err
|
||||
}
|
||||
// find the index of the entry for this process
|
||||
idx := int(-1)
|
||||
for i := range pidAry {
|
||||
if int(pidAry[i]) == processPid {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
// no pid found...
|
||||
if idx < 0 {
|
||||
return fmt.Errorf("could not find pid in performance counter results")
|
||||
}
|
||||
// assign values from the performance counters
|
||||
*pcpu = cpuAry[idx]
|
||||
*rss = int64(rssAry[idx])
|
||||
*vss = int64(vssAry[idx])
|
||||
|
||||
// save off cache values
|
||||
prevCPU = *pcpu
|
||||
prevRss = *rss
|
||||
prevVss = *vss
|
||||
|
||||
return nil
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// 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.
|
||||
|
||||
//go:build zos
|
||||
|
||||
package pse
|
||||
|
||||
// This is a placeholder for now.
|
||||
func ProcUsage(pcpu *float64, rss, vss *int64) error {
|
||||
*pcpu = 0.0
|
||||
*rss = 0
|
||||
*vss = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
+5205
File diff suppressed because it is too large
Load Diff
+65
@@ -0,0 +1,65 @@
|
||||
// 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 (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type rateCounter struct {
|
||||
limit int64
|
||||
count int64
|
||||
blocked uint64
|
||||
end time.Time
|
||||
interval time.Duration
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newRateCounter(limit int64) *rateCounter {
|
||||
return &rateCounter{
|
||||
limit: limit,
|
||||
interval: time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rateCounter) allow() bool {
|
||||
now := time.Now()
|
||||
|
||||
r.mu.Lock()
|
||||
|
||||
if now.After(r.end) {
|
||||
r.count = 0
|
||||
r.end = now.Add(r.interval)
|
||||
} else {
|
||||
r.count++
|
||||
}
|
||||
allow := r.count < r.limit
|
||||
if !allow {
|
||||
r.blocked++
|
||||
}
|
||||
|
||||
r.mu.Unlock()
|
||||
|
||||
return allow
|
||||
}
|
||||
|
||||
func (r *rateCounter) countBlocked() uint64 {
|
||||
r.mu.Lock()
|
||||
blocked := r.blocked
|
||||
r.blocked = 0
|
||||
r.mu.Unlock()
|
||||
|
||||
return blocked
|
||||
}
|
||||
+2860
File diff suppressed because it is too large
Load Diff
+77
@@ -0,0 +1,77 @@
|
||||
// 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
|
||||
|
||||
// We wrap to hold onto optional items for /connz.
|
||||
type closedClient struct {
|
||||
ConnInfo
|
||||
subs []SubDetail
|
||||
user string
|
||||
acc string
|
||||
}
|
||||
|
||||
// Fixed sized ringbuffer for closed connections.
|
||||
type closedRingBuffer struct {
|
||||
total uint64
|
||||
conns []*closedClient
|
||||
}
|
||||
|
||||
// Create a new ring buffer with at most max items.
|
||||
func newClosedRingBuffer(max int) *closedRingBuffer {
|
||||
rb := &closedRingBuffer{}
|
||||
rb.conns = make([]*closedClient, max)
|
||||
return rb
|
||||
}
|
||||
|
||||
// Adds in a new closed connection. If there is no more room,
|
||||
// remove the oldest.
|
||||
func (rb *closedRingBuffer) append(cc *closedClient) {
|
||||
rb.conns[rb.next()] = cc
|
||||
rb.total++
|
||||
}
|
||||
|
||||
func (rb *closedRingBuffer) next() int {
|
||||
return int(rb.total % uint64(cap(rb.conns)))
|
||||
}
|
||||
|
||||
func (rb *closedRingBuffer) len() int {
|
||||
if rb.total > uint64(cap(rb.conns)) {
|
||||
return cap(rb.conns)
|
||||
}
|
||||
return int(rb.total)
|
||||
}
|
||||
|
||||
func (rb *closedRingBuffer) totalConns() uint64 {
|
||||
return rb.total
|
||||
}
|
||||
|
||||
// This will return a sorted copy of the list which recipient can
|
||||
// modify. If the contents of the client itself need to be modified,
|
||||
// meaning swapping in any optional items, a copy should be made. We
|
||||
// could introduce a new lock and hold that but since we return this
|
||||
// list inside monitor which allows programatic access, we do not
|
||||
// know when it would be done.
|
||||
func (rb *closedRingBuffer) closedClients() []*closedClient {
|
||||
dup := make([]*closedClient, rb.len())
|
||||
head := rb.next()
|
||||
if rb.total <= uint64(cap(rb.conns)) || head == 0 {
|
||||
copy(dup, rb.conns[:rb.len()])
|
||||
} else {
|
||||
fp := rb.conns[head:]
|
||||
sp := rb.conns[:head]
|
||||
copy(dup, fp)
|
||||
copy(dup[len(fp):], sp)
|
||||
}
|
||||
return dup
|
||||
}
|
||||
+3311
File diff suppressed because it is too large
Load Diff
+376
@@ -0,0 +1,376 @@
|
||||
// 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"
|
||||
"io"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/server/thw"
|
||||
)
|
||||
|
||||
// Error for when we try to decode a binary-encoded message schedule with an unknown version number.
|
||||
var ErrMsgScheduleInvalidVersion = errors.New("msg scheduling: encoded version not known")
|
||||
|
||||
const (
|
||||
headerLen = 17 // 1 byte magic + 2x uint64s
|
||||
)
|
||||
|
||||
type MsgScheduling struct {
|
||||
run func()
|
||||
ttls *thw.HashWheel
|
||||
timer *time.Timer
|
||||
running bool
|
||||
deadline int64
|
||||
schedules map[string]*MsgSchedule
|
||||
seqToSubj map[uint64]string
|
||||
inflight map[string]struct{}
|
||||
}
|
||||
|
||||
type MsgSchedule struct {
|
||||
seq uint64
|
||||
ts int64
|
||||
}
|
||||
|
||||
func newMsgScheduling(run func()) *MsgScheduling {
|
||||
return &MsgScheduling{
|
||||
run: run,
|
||||
ttls: thw.NewHashWheel(),
|
||||
schedules: make(map[string]*MsgSchedule),
|
||||
seqToSubj: make(map[uint64]string),
|
||||
inflight: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MsgScheduling) add(seq uint64, subj string, ts int64) {
|
||||
ms.init(seq, subj, ts)
|
||||
ms.resetTimer()
|
||||
}
|
||||
|
||||
func (ms *MsgScheduling) init(seq uint64, subj string, ts int64) {
|
||||
if sched, ok := ms.schedules[subj]; ok {
|
||||
delete(ms.seqToSubj, sched.seq)
|
||||
// Remove and add separately, since they'll have different sequences.
|
||||
ms.ttls.Remove(sched.seq, sched.ts)
|
||||
ms.ttls.Add(seq, ts)
|
||||
sched.ts, sched.seq = ts, seq
|
||||
} else {
|
||||
ms.ttls.Add(seq, ts)
|
||||
ms.schedules[subj] = &MsgSchedule{seq: seq, ts: ts}
|
||||
}
|
||||
ms.seqToSubj[seq] = subj
|
||||
delete(ms.inflight, subj)
|
||||
}
|
||||
|
||||
func (ms *MsgScheduling) update(subj string, ts int64) {
|
||||
if sched, ok := ms.schedules[subj]; ok {
|
||||
// Remove and add separately, it's for the same sequence, but if replicated
|
||||
// this server could not know the previous timestamp yet.
|
||||
ms.ttls.Remove(sched.seq, sched.ts)
|
||||
ms.ttls.Add(sched.seq, ts)
|
||||
sched.ts = ts
|
||||
delete(ms.inflight, subj)
|
||||
ms.resetTimer()
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MsgScheduling) markInflight(subj string) {
|
||||
if _, ok := ms.schedules[subj]; ok {
|
||||
ms.inflight[subj] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MsgScheduling) isInflight(subj string) bool {
|
||||
_, ok := ms.inflight[subj]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (ms *MsgScheduling) remove(seq uint64) {
|
||||
if subj, ok := ms.seqToSubj[seq]; ok {
|
||||
ms.removeSubject(subj)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MsgScheduling) removeSubject(subj string) {
|
||||
if sched, ok := ms.schedules[subj]; ok {
|
||||
ms.ttls.Remove(sched.seq, sched.ts)
|
||||
delete(ms.schedules, subj)
|
||||
delete(ms.seqToSubj, sched.seq)
|
||||
delete(ms.inflight, subj)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MsgScheduling) clearInflight() {
|
||||
ms.inflight = make(map[string]struct{})
|
||||
}
|
||||
|
||||
func (ms *MsgScheduling) resetTimer() {
|
||||
// If we're already scheduling messages, it will make sure to reset.
|
||||
// Don't trigger again, as that could result in many expire goroutines.
|
||||
if ms.running {
|
||||
return
|
||||
}
|
||||
|
||||
next := ms.ttls.GetNextExpiration(math.MaxInt64)
|
||||
if next == math.MaxInt64 {
|
||||
clearTimer(&ms.timer)
|
||||
return
|
||||
}
|
||||
fireIn := time.Until(time.Unix(0, next))
|
||||
|
||||
// Make sure we aren't firing too often either way, otherwise we can
|
||||
// negatively impact stream ingest performance.
|
||||
if fireIn < 250*time.Millisecond {
|
||||
fireIn = 250 * time.Millisecond
|
||||
}
|
||||
|
||||
// If we want to kick the timer to run later than what was assigned before, don't reset it.
|
||||
// Otherwise, we could get in a situation where the timer is continuously reset, and it never runs.
|
||||
deadline := time.Now().UnixNano() + fireIn.Nanoseconds()
|
||||
if ms.deadline > 0 && deadline > ms.deadline {
|
||||
return
|
||||
}
|
||||
|
||||
ms.deadline = deadline
|
||||
if ms.timer != nil {
|
||||
ms.timer.Reset(fireIn)
|
||||
} else {
|
||||
ms.timer = time.AfterFunc(fireIn, ms.run)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MsgScheduling) getScheduledMessages(loadMsg func(seq uint64, smv *StoreMsg) *StoreMsg, loadLast func(subj string, smv *StoreMsg) *StoreMsg) []*inMsg {
|
||||
var (
|
||||
smv StoreMsg
|
||||
srcSmv StoreMsg
|
||||
sm *StoreMsg
|
||||
msgs []*inMsg
|
||||
)
|
||||
ms.ttls.ExpireTasks(func(seq uint64, ts int64) bool {
|
||||
// Need to grab the message for the specified sequence, and check
|
||||
// if it hasn't been removed in the meantime.
|
||||
sm = loadMsg(seq, &smv)
|
||||
if sm != nil {
|
||||
// If already inflight, don't duplicate a scheduled message. The stream could
|
||||
// be replicated and the scheduled message could take some time to propagate.
|
||||
subj := sm.subj
|
||||
if ms.isInflight(subj) {
|
||||
return false
|
||||
}
|
||||
// Validate the contents are correct if not, we just remove it from THW.
|
||||
pattern := bytesToString(sliceHeader(JSSchedulePattern, sm.hdr))
|
||||
if pattern == _EMPTY_ {
|
||||
ms.remove(seq)
|
||||
return true
|
||||
}
|
||||
loc, apiErr := loadMessageScheduleLocation(sm.hdr)
|
||||
if apiErr != nil {
|
||||
ms.remove(seq)
|
||||
return true
|
||||
}
|
||||
next, repeat, ok := parseMsgSchedule(pattern, loc, ts)
|
||||
if !ok {
|
||||
ms.remove(seq)
|
||||
return true
|
||||
}
|
||||
ttl, ok := getMessageScheduleTTL(sm.hdr)
|
||||
if !ok {
|
||||
ms.remove(seq)
|
||||
return true
|
||||
}
|
||||
target := getMessageScheduleTarget(sm.hdr)
|
||||
if target == _EMPTY_ {
|
||||
ms.remove(seq)
|
||||
return true
|
||||
}
|
||||
rollup := getMessageScheduleRollup(sm.hdr)
|
||||
source := getMessageScheduleSource(sm.hdr)
|
||||
if source != _EMPTY_ {
|
||||
// Fall back to the scheduled message's own content if the source has no last message.
|
||||
if srcSm := loadLast(source, &srcSmv); srcSm != nil {
|
||||
sm = srcSm
|
||||
}
|
||||
}
|
||||
|
||||
// Copy, as this is retrieved directly from storage, and we'll need to keep hold of this for some time.
|
||||
// And in the case of headers, we'll copy all of them, but make changes.
|
||||
hdr, msg := copyBytes(sm.hdr), copyBytes(sm.msg)
|
||||
|
||||
// Strip headers specific to message scheduling.
|
||||
// Covers Nats-Schedule, Nats-Schedule-*, and Nats-Scheduler.
|
||||
hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Schedule")
|
||||
// Strip headers that could prevent persisting this scheduled message.
|
||||
hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Expected-")
|
||||
hdr = removeHeaderIfPresent(hdr, JSMsgId)
|
||||
hdr = removeHeaderIfPresent(hdr, JSMessageTTL)
|
||||
hdr = removeHeaderIfPresent(hdr, JSMsgRollup)
|
||||
|
||||
// Add headers for the scheduled message.
|
||||
hdr = genHeader(hdr, JSScheduler, subj)
|
||||
if !repeat {
|
||||
hdr = genHeader(hdr, JSScheduleNext, JSScheduleNextPurge) // Purge the schedule message itself.
|
||||
} else {
|
||||
hdr = genHeader(hdr, JSScheduleNext, next.Format(time.RFC3339)) // Next time the schedule fires.
|
||||
}
|
||||
if ttl != _EMPTY_ {
|
||||
hdr = genHeader(hdr, JSMessageTTL, ttl)
|
||||
}
|
||||
if rollup != _EMPTY_ {
|
||||
hdr = genHeader(hdr, JSMsgRollup, rollup)
|
||||
}
|
||||
msgs = append(msgs, &inMsg{seq: seq, subj: target, hdr: hdr, msg: msg})
|
||||
ms.markInflight(subj)
|
||||
return false
|
||||
}
|
||||
ms.remove(seq)
|
||||
return true
|
||||
})
|
||||
// THW is unordered, so must sort by sequence.
|
||||
slices.SortFunc(msgs, func(a, b *inMsg) int {
|
||||
if a.seq == b.seq {
|
||||
return 0
|
||||
} else if a.seq < b.seq {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
})
|
||||
return msgs
|
||||
}
|
||||
|
||||
// encode writes out the contents of the schedule into a binary snapshot
|
||||
// and returns it. The high seq number is included in the snapshot and will
|
||||
// be returned on decode.
|
||||
func (ms *MsgScheduling) encode(highSeq uint64) []byte {
|
||||
count := uint64(len(ms.schedules))
|
||||
b := make([]byte, 0, headerLen+(count*(2*binary.MaxVarintLen64)))
|
||||
b = append(b, 1) // Magic version
|
||||
b = binary.LittleEndian.AppendUint64(b, count) // Entry count
|
||||
b = binary.LittleEndian.AppendUint64(b, highSeq) // Stamp
|
||||
for subj, sched := range ms.schedules {
|
||||
slen := min(uint64(len(subj)), math.MaxUint16)
|
||||
b = binary.LittleEndian.AppendUint16(b, uint16(slen))
|
||||
b = append(b, subj[:slen]...)
|
||||
b = binary.AppendVarint(b, sched.ts)
|
||||
b = binary.AppendUvarint(b, sched.seq)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// decode snapshots a binary-encoded schedule and replaces the contents of this
|
||||
// schedule with them. Returns the high seq number from the snapshot.
|
||||
func (ms *MsgScheduling) decode(b []byte) (uint64, error) {
|
||||
if len(b) < headerLen {
|
||||
return 0, io.ErrShortBuffer
|
||||
}
|
||||
if b[0] != 1 {
|
||||
return 0, ErrMsgScheduleInvalidVersion
|
||||
}
|
||||
|
||||
count := binary.LittleEndian.Uint64(b[1:])
|
||||
stamp := binary.LittleEndian.Uint64(b[9:])
|
||||
b = b[headerLen:]
|
||||
for i := uint64(0); i < count; i++ {
|
||||
sl := int(binary.LittleEndian.Uint16(b))
|
||||
b = b[2:]
|
||||
if len(b) < sl {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
subj := string(b[:sl])
|
||||
b = b[sl:]
|
||||
ts, tn := binary.Varint(b)
|
||||
if tn < 0 {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
seq, vn := binary.Uvarint(b[tn:])
|
||||
if vn < 0 {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
ms.init(seq, subj, ts)
|
||||
b = b[tn+vn:]
|
||||
}
|
||||
return stamp, nil
|
||||
}
|
||||
|
||||
// parseMsgSchedule parses a message schedule pattern and returns the time
|
||||
// to fire, whether it is a repeating schedule, and whether the pattern was valid.
|
||||
func parseMsgSchedule(pattern string, loc *time.Location, ts int64) (time.Time, bool, bool) {
|
||||
if pattern == _EMPTY_ {
|
||||
return time.Time{}, false, true
|
||||
}
|
||||
// Exact time.
|
||||
if strings.HasPrefix(pattern, "@at ") {
|
||||
// Time zone is not supported for @at.
|
||||
if loc != nil {
|
||||
return time.Time{}, false, false
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, pattern[4:])
|
||||
return t, false, err == nil
|
||||
}
|
||||
// Repeating on a simple interval.
|
||||
if strings.HasPrefix(pattern, "@every ") {
|
||||
// Time zone is not supported for @every.
|
||||
if loc != nil {
|
||||
return time.Time{}, false, false
|
||||
}
|
||||
dur, err := time.ParseDuration(pattern[7:])
|
||||
if err != nil {
|
||||
return time.Time{}, false, false
|
||||
}
|
||||
// Only allow intervals of at least a second.
|
||||
if dur.Seconds() < 1 {
|
||||
return time.Time{}, false, false
|
||||
}
|
||||
// If this schedule would trigger multiple times, for example after a restart, skip ahead and only fire once.
|
||||
next := time.Unix(0, ts).UTC().Round(time.Second).Add(dur)
|
||||
if now := time.Now().UTC(); next.Before(now) {
|
||||
next = now.Round(time.Second).Add(dur)
|
||||
}
|
||||
return next, true, true
|
||||
}
|
||||
|
||||
// Predefined schedules for cron.
|
||||
switch pattern {
|
||||
case "@yearly", "@annually":
|
||||
pattern = "0 0 0 1 1 *"
|
||||
case "@monthly":
|
||||
pattern = "0 0 0 1 * *"
|
||||
case "@weekly":
|
||||
pattern = "0 0 0 * * 0"
|
||||
case "@daily", "@midnight":
|
||||
pattern = "0 0 0 * * *"
|
||||
case "@hourly":
|
||||
pattern = "0 0 * * * *"
|
||||
}
|
||||
|
||||
// Parse the cron pattern.
|
||||
next, err := parseCron(pattern, loc, ts)
|
||||
if err != nil {
|
||||
return time.Time{}, false, false
|
||||
}
|
||||
// If this schedule would trigger multiple times, for example after a restart, skip ahead and only fire once.
|
||||
if now := time.Now().UTC(); next.Before(now) {
|
||||
ts = now.Round(time.Second).UnixNano()
|
||||
next, err = parseCron(pattern, loc, ts)
|
||||
if err != nil {
|
||||
return time.Time{}, false, false
|
||||
}
|
||||
}
|
||||
return next, true, true
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SDMMeta holds pending/proposed data for subject delete markers or message removals.
|
||||
type SDMMeta struct {
|
||||
totals map[string]uint64
|
||||
pending map[uint64]SDMBySeq
|
||||
}
|
||||
|
||||
// SDMBySeq holds data for a message with a specific sequence.
|
||||
type SDMBySeq struct {
|
||||
last bool // Whether the message for this sequence was the last for this subject.
|
||||
ts int64 // Last timestamp we proposed a removal/sdm.
|
||||
}
|
||||
|
||||
func newSDMMeta() *SDMMeta {
|
||||
return &SDMMeta{
|
||||
totals: make(map[string]uint64, 1),
|
||||
pending: make(map[uint64]SDMBySeq, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// isSubjectDeleteMarker returns whether the headers indicate this message is a subject delete marker.
|
||||
// Either it's a usual marker with JSMarkerReason, or it's a KV Purge marker as the KVOperation.
|
||||
func isSubjectDeleteMarker(hdr []byte) bool {
|
||||
return len(sliceHeader(JSMarkerReason, hdr)) != 0 || bytes.Equal(sliceHeader(KVOperation, hdr), KVOperationValuePurge)
|
||||
}
|
||||
|
||||
// empty clears all data.
|
||||
func (sdm *SDMMeta) empty() {
|
||||
if sdm == nil {
|
||||
return
|
||||
}
|
||||
clear(sdm.totals)
|
||||
clear(sdm.pending)
|
||||
}
|
||||
|
||||
// trackPending caches the given seq and subj and whether it's the last message for that subject.
|
||||
func (sdm *SDMMeta) trackPending(seq uint64, subj string, last bool) bool {
|
||||
if p, ok := sdm.pending[seq]; ok {
|
||||
return p.last
|
||||
}
|
||||
sdm.pending[seq] = SDMBySeq{last, time.Now().UnixNano()}
|
||||
sdm.totals[subj]++
|
||||
return last
|
||||
}
|
||||
|
||||
// removeSeqAndSubject clears the seq and subj from the cache.
|
||||
func (sdm *SDMMeta) removeSeqAndSubject(seq uint64, subj string) {
|
||||
if sdm == nil {
|
||||
return
|
||||
}
|
||||
if _, ok := sdm.pending[seq]; ok {
|
||||
delete(sdm.pending, seq)
|
||||
if msgs, ok := sdm.totals[subj]; ok {
|
||||
if msgs <= 1 {
|
||||
delete(sdm.totals, subj)
|
||||
} else {
|
||||
sdm.totals[subj] = msgs - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// 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 (
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type outMsg struct {
|
||||
subj string
|
||||
rply string
|
||||
hdr []byte
|
||||
msg []byte
|
||||
}
|
||||
|
||||
type sendq struct {
|
||||
mu sync.Mutex
|
||||
q *ipQueue[*outMsg]
|
||||
s *Server
|
||||
a *Account
|
||||
}
|
||||
|
||||
func (s *Server) newSendQ(acc *Account) *sendq {
|
||||
sq := &sendq{s: s, q: newIPQueue[*outMsg](s, "SendQ"), a: acc}
|
||||
s.startGoRoutine(sq.internalLoop)
|
||||
return sq
|
||||
}
|
||||
|
||||
func (sq *sendq) internalLoop() {
|
||||
sq.mu.Lock()
|
||||
s, q := sq.s, sq.q
|
||||
sq.mu.Unlock()
|
||||
|
||||
defer s.grWG.Done()
|
||||
|
||||
c := s.createInternalSystemClient()
|
||||
c.registerWithAccount(sq.a)
|
||||
c.noIcb = true
|
||||
|
||||
defer c.closeConnection(ClientClosed)
|
||||
|
||||
// To optimize for not converting a string to a []byte slice.
|
||||
var (
|
||||
subj [256]byte
|
||||
rply [256]byte
|
||||
szb [10]byte
|
||||
hdb [10]byte
|
||||
_msg [4096]byte
|
||||
msg = _msg[:0]
|
||||
)
|
||||
|
||||
for s.isRunning() {
|
||||
select {
|
||||
case <-s.quitCh:
|
||||
return
|
||||
case <-q.ch:
|
||||
pms := q.pop()
|
||||
for _, pm := range pms {
|
||||
c.pa.subject = append(subj[:0], pm.subj...)
|
||||
c.pa.size = len(pm.msg) + len(pm.hdr)
|
||||
c.pa.szb = append(szb[:0], strconv.Itoa(c.pa.size)...)
|
||||
if len(pm.rply) > 0 {
|
||||
c.pa.reply = append(rply[:0], pm.rply...)
|
||||
} else {
|
||||
c.pa.reply = nil
|
||||
}
|
||||
msg = msg[:0]
|
||||
if len(pm.hdr) > 0 {
|
||||
c.pa.hdr = len(pm.hdr)
|
||||
c.pa.hdb = append(hdb[:0], strconv.Itoa(c.pa.hdr)...)
|
||||
msg = append(msg, pm.hdr...)
|
||||
msg = append(msg, pm.msg...)
|
||||
msg = append(msg, _CRLF_...)
|
||||
} else {
|
||||
c.pa.hdr = -1
|
||||
c.pa.hdb = nil
|
||||
msg = append(msg, pm.msg...)
|
||||
msg = append(msg, _CRLF_...)
|
||||
}
|
||||
c.processInboundClientMsg(msg)
|
||||
c.pa.szb = nil
|
||||
outMsgPool.Put(pm)
|
||||
}
|
||||
// TODO: should this be in the for-loop instead?
|
||||
c.flushClients(0)
|
||||
q.recycle(&pms)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var outMsgPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &outMsg{}
|
||||
},
|
||||
}
|
||||
|
||||
func (sq *sendq) send(subj, rply string, hdr, msg []byte) {
|
||||
if sq == nil {
|
||||
return
|
||||
}
|
||||
out := outMsgPool.Get().(*outMsg)
|
||||
out.subj, out.rply = subj, rply
|
||||
out.hdr = append(out.hdr[:0], hdr...)
|
||||
out.msg = append(out.msg[:0], msg...)
|
||||
sq.q.push(out)
|
||||
}
|
||||
+4752
File diff suppressed because it is too large
Load Diff
+28
@@ -0,0 +1,28 @@
|
||||
// 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 server
|
||||
|
||||
// Run starts the NATS server. This wrapper function allows Windows to add a
|
||||
// hook for running NATS as a service.
|
||||
func Run(server *Server) error {
|
||||
server.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
// isWindowsService indicates if NATS is running as a Windows service.
|
||||
func isWindowsService() bool {
|
||||
return false
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
// 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 (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows/svc"
|
||||
)
|
||||
|
||||
const (
|
||||
reopenLogCode = 128
|
||||
reopenLogCmd = svc.Cmd(reopenLogCode)
|
||||
ldmCode = 129
|
||||
ldmCmd = svc.Cmd(ldmCode)
|
||||
acceptReopenLog = svc.Accepted(reopenLogCode)
|
||||
)
|
||||
|
||||
var serviceName = "nats-server"
|
||||
|
||||
// SetServiceName allows setting a different service name
|
||||
func SetServiceName(name string) {
|
||||
serviceName = name
|
||||
}
|
||||
|
||||
// winServiceWrapper implements the svc.Handler interface for implementing
|
||||
// nats-server as a Windows service.
|
||||
type winServiceWrapper struct {
|
||||
server *Server
|
||||
}
|
||||
|
||||
var dockerized = false
|
||||
|
||||
func init() {
|
||||
if v, exists := os.LookupEnv("NATS_DOCKERIZED"); exists && v == "1" {
|
||||
dockerized = true
|
||||
}
|
||||
}
|
||||
|
||||
// Execute will be called by the package code at the start of
|
||||
// the service, and the service will exit once Execute completes.
|
||||
// Inside Execute you must read service change requests from r and
|
||||
// act accordingly. You must keep service control manager up to date
|
||||
// about state of your service by writing into s as required.
|
||||
// args contains service name followed by argument strings passed
|
||||
// to the service.
|
||||
// You can provide service exit code in exitCode return parameter,
|
||||
// with 0 being "no error". You can also indicate if exit code,
|
||||
// if any, is service specific or not by using svcSpecificEC
|
||||
// parameter.
|
||||
func (w *winServiceWrapper) Execute(args []string, changes <-chan svc.ChangeRequest,
|
||||
status chan<- svc.Status) (bool, uint32) {
|
||||
|
||||
status <- svc.Status{State: svc.StartPending}
|
||||
go w.server.Start()
|
||||
|
||||
var startupDelay = 10 * time.Second
|
||||
if v, exists := os.LookupEnv("NATS_STARTUP_DELAY"); exists {
|
||||
if delay, err := time.ParseDuration(v); err == nil {
|
||||
startupDelay = delay
|
||||
} else {
|
||||
w.server.Errorf("Failed to parse \"%v\" as a duration for startup: %s", v, err)
|
||||
}
|
||||
}
|
||||
// Wait for accept loop(s) to be started
|
||||
if !w.server.ReadyForConnections(startupDelay) {
|
||||
// Failed to start.
|
||||
return false, 1
|
||||
}
|
||||
|
||||
status <- svc.Status{
|
||||
State: svc.Running,
|
||||
Accepts: svc.AcceptStop | svc.AcceptShutdown | svc.AcceptParamChange | acceptReopenLog,
|
||||
}
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case change, ok := <-changes:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
switch change.Cmd {
|
||||
case svc.Interrogate:
|
||||
status <- change.CurrentStatus
|
||||
case svc.Stop, svc.Shutdown:
|
||||
w.server.Shutdown()
|
||||
break loop
|
||||
case reopenLogCmd:
|
||||
// File log re-open for rotating file logs.
|
||||
w.server.ReOpenLogFile()
|
||||
case ldmCmd:
|
||||
go w.server.lameDuckMode()
|
||||
case svc.ParamChange:
|
||||
if err := w.server.Reload(); err != nil {
|
||||
w.server.Errorf("Failed to reload server configuration: %s", err)
|
||||
}
|
||||
default:
|
||||
w.server.Debugf("Unexpected control request: %v", change.Cmd)
|
||||
}
|
||||
case <-w.server.quitCh:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
status <- svc.Status{State: svc.StopPending}
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// Run starts the NATS server as a Windows service.
|
||||
func Run(server *Server) error {
|
||||
if dockerized {
|
||||
server.Start()
|
||||
return nil
|
||||
}
|
||||
isWindowsService, err := svc.IsWindowsService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isWindowsService {
|
||||
server.Start()
|
||||
return nil
|
||||
}
|
||||
return svc.Run(serviceName, &winServiceWrapper{server})
|
||||
}
|
||||
|
||||
// isWindowsService indicates if NATS is running as a Windows service.
|
||||
func isWindowsService() bool {
|
||||
if dockerized {
|
||||
return false
|
||||
}
|
||||
isWindowsService, _ := svc.IsWindowsService()
|
||||
return isWindowsService
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
// 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 && !wasm
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var processName = "nats-server"
|
||||
|
||||
// SetProcessName allows to change the expected name of the process.
|
||||
func SetProcessName(name string) {
|
||||
processName = name
|
||||
}
|
||||
|
||||
// Signal Handling
|
||||
func (s *Server) handleSignals() {
|
||||
if s.getOpts().NoSigs {
|
||||
return
|
||||
}
|
||||
c := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGHUP)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case sig := <-c:
|
||||
s.Noticef("Trapped %q signal", sig)
|
||||
switch sig {
|
||||
case syscall.SIGINT:
|
||||
s.Shutdown()
|
||||
s.WaitForShutdown()
|
||||
os.Exit(0)
|
||||
case syscall.SIGTERM:
|
||||
// Shutdown unless graceful shutdown already in progress.
|
||||
s.mu.Lock()
|
||||
ldm := s.ldm
|
||||
s.mu.Unlock()
|
||||
|
||||
if !ldm {
|
||||
s.Shutdown()
|
||||
s.WaitForShutdown()
|
||||
os.Exit(0)
|
||||
}
|
||||
case syscall.SIGUSR1:
|
||||
// File log re-open for rotating file logs.
|
||||
s.ReOpenLogFile()
|
||||
case syscall.SIGUSR2:
|
||||
go s.lameDuckMode()
|
||||
case syscall.SIGHUP:
|
||||
// Config reload.
|
||||
if err := s.Reload(); err != nil {
|
||||
s.Errorf("Failed to reload server configuration: %s", err)
|
||||
}
|
||||
}
|
||||
case <-s.quitCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ProcessSignal sends the given signal command to the given process. If pidStr
|
||||
// is empty, this will send the signal to the single running instance of
|
||||
// nats-server. If multiple instances are running, pidStr can be a globular
|
||||
// expression ending with '*'. This returns an error if the given process is
|
||||
// not running or the command is invalid.
|
||||
func ProcessSignal(command Command, pidExpr string) error {
|
||||
var (
|
||||
err error
|
||||
errStr string
|
||||
pids = make([]int, 1)
|
||||
pidStr = strings.TrimSuffix(pidExpr, "*")
|
||||
isGlob = strings.HasSuffix(pidExpr, "*")
|
||||
)
|
||||
|
||||
// Validate input if given
|
||||
if pidStr != "" {
|
||||
if pids[0], err = strconv.Atoi(pidStr); err != nil {
|
||||
return fmt.Errorf("invalid pid: %s", pidStr)
|
||||
}
|
||||
}
|
||||
// Gather all PIDs unless the input is specific
|
||||
if pidStr == "" || isGlob {
|
||||
if pids, err = resolvePids(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Multiple instances are running and the input is not an expression
|
||||
if len(pids) > 1 && !isGlob {
|
||||
errStr = fmt.Sprintf("multiple %s processes running:", processName)
|
||||
for _, p := range pids {
|
||||
errStr += fmt.Sprintf("\n%d", p)
|
||||
}
|
||||
return errors.New(errStr)
|
||||
}
|
||||
// No instances are running
|
||||
if len(pids) == 0 {
|
||||
return fmt.Errorf("no %s processes running", processName)
|
||||
}
|
||||
|
||||
var signum syscall.Signal
|
||||
if signum, err = CommandToSignal(command); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pid := range pids {
|
||||
if _pidStr := strconv.Itoa(pid); _pidStr != pidStr && pidStr != "" {
|
||||
if !isGlob || !strings.HasPrefix(_pidStr, pidStr) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err = kill(pid, signum); err != nil {
|
||||
errStr += fmt.Sprintf("\nsignal %q %d: %s", command, pid, err)
|
||||
}
|
||||
}
|
||||
if errStr != "" {
|
||||
return errors.New(errStr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Translates a command to a signal number
|
||||
func CommandToSignal(command Command) (syscall.Signal, error) {
|
||||
switch command {
|
||||
case CommandStop:
|
||||
return syscall.SIGKILL, nil
|
||||
case CommandQuit:
|
||||
return syscall.SIGINT, nil
|
||||
case CommandReopen:
|
||||
return syscall.SIGUSR1, nil
|
||||
case CommandReload:
|
||||
return syscall.SIGHUP, nil
|
||||
case commandLDMode:
|
||||
return syscall.SIGUSR2, nil
|
||||
case commandTerm:
|
||||
return syscall.SIGTERM, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown signal %q", command)
|
||||
}
|
||||
}
|
||||
|
||||
// resolvePids returns the pids for all running nats-server processes.
|
||||
func resolvePids() ([]int, error) {
|
||||
// If pgrep isn't available, this will just bail out and the user will be
|
||||
// required to specify a pid.
|
||||
output, err := pgrep()
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *exec.ExitError:
|
||||
// ExitError indicates non-zero exit code, meaning no processes
|
||||
// found.
|
||||
break
|
||||
default:
|
||||
return nil, errors.New("unable to resolve pid, try providing one")
|
||||
}
|
||||
}
|
||||
var (
|
||||
myPid = os.Getpid()
|
||||
pidStrs = strings.Split(string(output), "\n")
|
||||
pids = make([]int, 0, len(pidStrs))
|
||||
)
|
||||
for _, pidStr := range pidStrs {
|
||||
if pidStr == "" {
|
||||
continue
|
||||
}
|
||||
pid, err := strconv.Atoi(pidStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("unable to resolve pid, try providing one")
|
||||
}
|
||||
// Ignore the current process.
|
||||
if pid == myPid {
|
||||
continue
|
||||
}
|
||||
pids = append(pids, pid)
|
||||
}
|
||||
return pids, nil
|
||||
}
|
||||
|
||||
var kill = func(pid int, signal syscall.Signal) error {
|
||||
return syscall.Kill(pid, signal)
|
||||
}
|
||||
|
||||
var pgrep = func() ([]byte, error) {
|
||||
return exec.Command("pgrep", processName).Output()
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// 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 (s *Server) handleSignals() {
|
||||
|
||||
}
|
||||
|
||||
func ProcessSignal(command Command, service string) error {
|
||||
return nil
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
// 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"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
)
|
||||
|
||||
// Signal Handling
|
||||
func (s *Server) handleSignals() {
|
||||
if s.getOpts().NoSigs {
|
||||
return
|
||||
}
|
||||
c := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case sig := <-c:
|
||||
s.Debugf("Trapped %q signal", sig)
|
||||
s.Shutdown()
|
||||
os.Exit(0)
|
||||
case <-s.quitCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ProcessSignal sends the given signal command to the running nats-server service.
|
||||
// If service is empty, this signals the "nats-server" service. This returns an
|
||||
// error is the given service is not running or the command is invalid.
|
||||
func ProcessSignal(command Command, service string) error {
|
||||
if service == "" {
|
||||
service = serviceName
|
||||
}
|
||||
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
s, err := m.OpenService(service)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not access service: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
var (
|
||||
cmd svc.Cmd
|
||||
to svc.State
|
||||
)
|
||||
|
||||
switch command {
|
||||
case CommandStop, CommandQuit:
|
||||
cmd = svc.Stop
|
||||
to = svc.Stopped
|
||||
case CommandReopen:
|
||||
cmd = reopenLogCmd
|
||||
to = svc.Running
|
||||
case CommandReload:
|
||||
cmd = svc.ParamChange
|
||||
to = svc.Running
|
||||
case commandLDMode:
|
||||
cmd = ldmCmd
|
||||
to = svc.Running
|
||||
default:
|
||||
return fmt.Errorf("unknown signal %q", command)
|
||||
}
|
||||
|
||||
status, err := s.Control(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not send control=%d: %v", cmd, err)
|
||||
}
|
||||
|
||||
timeout := time.Now().Add(10 * time.Second)
|
||||
for status.State != to {
|
||||
if timeout.Before(time.Now()) {
|
||||
return fmt.Errorf("timeout waiting for service to go to state=%d", to)
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
status, err = s.Query()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not retrieve service status: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+800
@@ -0,0 +1,800 @@
|
||||
// Copyright 2019-2025 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/server/avl"
|
||||
"github.com/nats-io/nats-server/v2/server/gsl"
|
||||
)
|
||||
|
||||
// StorageType determines how messages are stored for retention.
|
||||
type StorageType int
|
||||
|
||||
const (
|
||||
// File specifies on disk, designated by the JetStream config StoreDir.
|
||||
FileStorage = StorageType(22)
|
||||
// MemoryStorage specifies in memory only.
|
||||
MemoryStorage = StorageType(33)
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrStoreClosed is returned when the store has been closed
|
||||
ErrStoreClosed = errors.New("store is closed")
|
||||
// ErrStoreMsgNotFound when message was not found but was expected to be.
|
||||
ErrStoreMsgNotFound = errors.New("no message found")
|
||||
// ErrStoreEOF is returned when message seq is greater than the last sequence.
|
||||
ErrStoreEOF = errors.New("stream store EOF")
|
||||
// ErrMaxMsgs is returned when we have discard new as a policy and we reached the message limit.
|
||||
ErrMaxMsgs = errors.New("maximum messages exceeded")
|
||||
// ErrMaxBytes is returned when we have discard new as a policy and we reached the bytes limit.
|
||||
ErrMaxBytes = errors.New("maximum bytes exceeded")
|
||||
// ErrMaxMsgsPerSubject is returned when we have discard new as a policy and we reached the message limit per subject.
|
||||
ErrMaxMsgsPerSubject = errors.New("maximum messages per subject exceeded")
|
||||
// ErrStoreSnapshotInProgress is returned when RemoveMsg or EraseMsg is called
|
||||
// while a snapshot is in progress.
|
||||
ErrStoreSnapshotInProgress = errors.New("snapshot in progress")
|
||||
// ErrMsgTooLarge is returned when a message is considered too large.
|
||||
ErrMsgTooLarge = errors.New("message too large")
|
||||
// ErrStoreWrongType is for when you access the wrong storage type.
|
||||
ErrStoreWrongType = errors.New("wrong storage type")
|
||||
// ErrNoAckPolicy is returned when trying to update a consumer's acks with no ack policy.
|
||||
ErrNoAckPolicy = errors.New("ack policy is none")
|
||||
// ErrSequenceMismatch is returned when storing a raw message and the expected sequence is wrong.
|
||||
ErrSequenceMismatch = errors.New("expected sequence does not match store")
|
||||
// ErrCorruptStreamState
|
||||
ErrCorruptStreamState = errors.New("stream state snapshot is corrupt")
|
||||
// ErrTooManyResults
|
||||
ErrTooManyResults = errors.New("too many matching results for request")
|
||||
// ErrStoreOldUpdate is returned when a consumer update is older than the current state.
|
||||
ErrStoreOldUpdate = errors.New("old update ignored")
|
||||
)
|
||||
|
||||
// StoreMsg is the stored message format for messages that are retained by the Store layer.
|
||||
type StoreMsg struct {
|
||||
subj string
|
||||
hdr []byte
|
||||
msg []byte
|
||||
buf []byte
|
||||
seq uint64
|
||||
ts int64
|
||||
}
|
||||
|
||||
// Used to call back into the upper layers to report on changes in storage resources.
|
||||
// For the cases where its a single message we will also supply sequence number and subject.
|
||||
type StorageUpdateHandler func(msgs, bytes int64, seq uint64, subj string)
|
||||
|
||||
// Used to call back into the upper layers to remove a message.
|
||||
type StorageRemoveMsgHandler func(seq uint64)
|
||||
|
||||
// Used to call back into the upper layers to process a JetStream message.
|
||||
// Will propose the message if the stream is replicated.
|
||||
type ProcessJetStreamMsgHandler func(*inMsg)
|
||||
|
||||
type StreamStore interface {
|
||||
StoreMsg(subject string, hdr, msg []byte, ttl int64) (uint64, int64, error)
|
||||
StoreRawMsg(subject string, hdr, msg []byte, seq uint64, ts int64, ttl int64, discardNewCheck bool) error
|
||||
SkipMsg(seq uint64) (uint64, error)
|
||||
SkipMsgs(seq uint64, num uint64) error
|
||||
FlushAllPending() error
|
||||
LoadMsg(seq uint64, sm *StoreMsg) (*StoreMsg, error)
|
||||
LoadNextMsg(filter string, wc bool, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error)
|
||||
LoadNextMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error)
|
||||
LoadLastMsg(subject string, sm *StoreMsg) (*StoreMsg, error)
|
||||
LoadPrevMsg(filter string, wc bool, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error)
|
||||
LoadPrevMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error)
|
||||
RemoveMsg(seq uint64) (bool, error)
|
||||
EraseMsg(seq uint64) (bool, error)
|
||||
Purge() (uint64, error)
|
||||
PurgeEx(subject string, seq, keep uint64) (uint64, error)
|
||||
Compact(seq uint64) (uint64, error)
|
||||
Truncate(seq uint64) error
|
||||
GetSeqFromTime(t time.Time) uint64
|
||||
FilteredState(seq uint64, subject string) (SimpleState, error)
|
||||
SubjectsState(filterSubject string) map[string]SimpleState
|
||||
SubjectsTotals(filterSubject string) map[string]uint64
|
||||
AllLastSeqs() ([]uint64, error)
|
||||
MultiLastSeqs(filters []string, maxSeq uint64, maxAllowed int) ([]uint64, error)
|
||||
SubjectForSeq(seq uint64) (string, error)
|
||||
NumPending(sseq uint64, filter string, lastPerSubject bool) (total, validThrough uint64, err error)
|
||||
NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerSubject bool) (total, validThrough uint64, err error)
|
||||
State() StreamState
|
||||
FastState(*StreamState)
|
||||
EncodedStreamState(failed uint64) (enc []byte, err error)
|
||||
SyncDeleted(dbs DeleteBlocks) error
|
||||
Type() StorageType
|
||||
RegisterStorageUpdates(StorageUpdateHandler)
|
||||
RegisterStorageRemoveMsg(StorageRemoveMsgHandler)
|
||||
RegisterProcessJetStreamMsg(ProcessJetStreamMsgHandler)
|
||||
UpdateConfig(cfg *StreamConfig) error
|
||||
Delete(inline bool) error
|
||||
Stop() error
|
||||
ConsumerStore(name string, created time.Time, cfg *ConsumerConfig) (ConsumerStore, error)
|
||||
AddConsumer(o ConsumerStore) error
|
||||
RemoveConsumer(o ConsumerStore) error
|
||||
Snapshot(deadline time.Duration, includeConsumers, checkMsgs bool) (*SnapshotResult, error)
|
||||
Utilization() (total, reported uint64, err error)
|
||||
ResetState()
|
||||
}
|
||||
|
||||
// RetentionPolicy determines how messages in a set are retained.
|
||||
type RetentionPolicy int
|
||||
|
||||
const (
|
||||
// LimitsPolicy (default) means that messages are retained until any given limit is reached.
|
||||
// This could be one of MaxMsgs, MaxBytes, or MaxAge.
|
||||
LimitsPolicy RetentionPolicy = iota
|
||||
// InterestPolicy specifies that when all known consumers have acknowledged a message it can be removed.
|
||||
InterestPolicy
|
||||
// WorkQueuePolicy specifies that when the first worker or subscriber acknowledges the message it can be removed.
|
||||
WorkQueuePolicy
|
||||
)
|
||||
|
||||
// Discard Policy determines how we proceed when limits of messages or bytes are hit. The default, DicscardOld will
|
||||
// remove older messages. DiscardNew will fail to store the new message.
|
||||
type DiscardPolicy int
|
||||
|
||||
const (
|
||||
// DiscardOld will remove older messages to return to the limits.
|
||||
DiscardOld = iota
|
||||
// DiscardNew will error on a StoreMsg call
|
||||
DiscardNew
|
||||
)
|
||||
|
||||
// StreamState is information about the given stream.
|
||||
type StreamState struct {
|
||||
Msgs uint64 `json:"messages"`
|
||||
Bytes uint64 `json:"bytes"`
|
||||
FirstSeq uint64 `json:"first_seq"`
|
||||
FirstTime time.Time `json:"first_ts"`
|
||||
LastSeq uint64 `json:"last_seq"`
|
||||
LastTime time.Time `json:"last_ts"`
|
||||
NumSubjects int `json:"num_subjects,omitempty"`
|
||||
Subjects map[string]uint64 `json:"subjects,omitempty"`
|
||||
NumDeleted int `json:"num_deleted,omitempty"`
|
||||
Deleted []uint64 `json:"deleted,omitempty"`
|
||||
Lost *LostStreamData `json:"lost,omitempty"`
|
||||
Consumers int `json:"consumer_count"`
|
||||
}
|
||||
|
||||
// SimpleState for filtered subject specific state.
|
||||
type SimpleState struct {
|
||||
Msgs uint64 `json:"messages"`
|
||||
First uint64 `json:"first_seq"`
|
||||
Last uint64 `json:"last_seq"`
|
||||
|
||||
// Internal usage for when the first needs to be updated before use.
|
||||
firstNeedsUpdate bool
|
||||
// Internal usage for when the last needs to be updated before use.
|
||||
lastNeedsUpdate bool
|
||||
}
|
||||
|
||||
// LostStreamData indicates msgs that have been lost.
|
||||
type LostStreamData struct {
|
||||
Msgs []uint64 `json:"msgs"`
|
||||
Bytes uint64 `json:"bytes"`
|
||||
}
|
||||
|
||||
// SnapshotResult contains information about the snapshot.
|
||||
type SnapshotResult struct {
|
||||
Reader io.ReadCloser
|
||||
State StreamState
|
||||
errCh chan string
|
||||
}
|
||||
|
||||
const (
|
||||
// Magic is used to identify stream state encodings.
|
||||
streamStateMagic = uint8(42)
|
||||
// Version
|
||||
streamStateVersion = uint8(1)
|
||||
// Magic / Identifier for run length encodings.
|
||||
runLengthMagic = uint8(33)
|
||||
// Magic / Identifier for AVL seqsets.
|
||||
seqSetMagic = uint8(22)
|
||||
)
|
||||
|
||||
// Interface for DeleteBlock.
|
||||
// These will be of three types:
|
||||
// 1. AVL seqsets.
|
||||
// 2. Run length encoding of a deleted range.
|
||||
// 3. Legacy []uint64
|
||||
type DeleteBlock interface {
|
||||
State() (first, last, num uint64)
|
||||
Range(f func(uint64) bool)
|
||||
}
|
||||
|
||||
type DeleteBlocks []DeleteBlock
|
||||
|
||||
// StreamReplicatedState represents what is encoded in a binary stream snapshot used
|
||||
// for stream replication in an NRG.
|
||||
type StreamReplicatedState struct {
|
||||
Msgs uint64
|
||||
Bytes uint64
|
||||
FirstSeq uint64
|
||||
LastSeq uint64
|
||||
Failed uint64
|
||||
Deleted DeleteBlocks
|
||||
}
|
||||
|
||||
// Determine if this is an encoded stream state.
|
||||
func IsEncodedStreamState(buf []byte) bool {
|
||||
return len(buf) >= hdrLen && buf[0] == streamStateMagic && buf[1] == streamStateVersion
|
||||
}
|
||||
|
||||
var ErrBadStreamStateEncoding = errors.New("bad stream state encoding")
|
||||
|
||||
func DecodeStreamState(buf []byte) (*StreamReplicatedState, error) {
|
||||
ss := &StreamReplicatedState{}
|
||||
if len(buf) < hdrLen || buf[0] != streamStateMagic || buf[1] != streamStateVersion {
|
||||
return nil, ErrBadStreamStateEncoding
|
||||
}
|
||||
var bi = hdrLen
|
||||
|
||||
readU64 := func() uint64 {
|
||||
if bi < 0 || bi >= len(buf) {
|
||||
bi = -1
|
||||
return 0
|
||||
}
|
||||
num, n := binary.Uvarint(buf[bi:])
|
||||
if n <= 0 {
|
||||
bi = -1
|
||||
return 0
|
||||
}
|
||||
bi += n
|
||||
return num
|
||||
}
|
||||
|
||||
parserFailed := func() bool {
|
||||
return bi < 0
|
||||
}
|
||||
|
||||
ss.Msgs = readU64()
|
||||
ss.Bytes = readU64()
|
||||
ss.FirstSeq = readU64()
|
||||
ss.LastSeq = readU64()
|
||||
ss.Failed = readU64()
|
||||
|
||||
if parserFailed() {
|
||||
return nil, ErrCorruptStreamState
|
||||
}
|
||||
|
||||
if numDeleted := readU64(); numDeleted > 0 {
|
||||
// If we have some deleted blocks.
|
||||
for l := len(buf); l > bi; {
|
||||
switch buf[bi] {
|
||||
case seqSetMagic:
|
||||
dmap, n, err := avl.Decode(buf[bi:])
|
||||
if err != nil {
|
||||
return nil, ErrCorruptStreamState
|
||||
}
|
||||
bi += n
|
||||
ss.Deleted = append(ss.Deleted, dmap)
|
||||
case runLengthMagic:
|
||||
bi++
|
||||
var rl DeleteRange
|
||||
rl.First = readU64()
|
||||
rl.Num = readU64()
|
||||
if parserFailed() {
|
||||
return nil, ErrCorruptStreamState
|
||||
}
|
||||
ss.Deleted = append(ss.Deleted, &rl)
|
||||
default:
|
||||
return nil, ErrCorruptStreamState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
// DeleteRange is a run length encoded delete range.
|
||||
type DeleteRange struct {
|
||||
First uint64
|
||||
Num uint64
|
||||
}
|
||||
|
||||
func (dr *DeleteRange) State() (first, last, num uint64) {
|
||||
deletesAfterFirst := dr.Num
|
||||
if deletesAfterFirst > 0 {
|
||||
deletesAfterFirst--
|
||||
}
|
||||
return dr.First, dr.First + deletesAfterFirst, dr.Num
|
||||
}
|
||||
|
||||
// Range will range over all the deleted sequences represented by this block.
|
||||
func (dr *DeleteRange) Range(f func(uint64) bool) {
|
||||
for seq := dr.First; seq < dr.First+dr.Num; seq++ {
|
||||
if !f(seq) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy []uint64
|
||||
type DeleteSlice []uint64
|
||||
|
||||
func (ds DeleteSlice) State() (first, last, num uint64) {
|
||||
if len(ds) == 0 {
|
||||
return 0, 0, 0
|
||||
}
|
||||
return ds[0], ds[len(ds)-1], uint64(len(ds))
|
||||
}
|
||||
|
||||
// Range will range over all the deleted sequences represented by this []uint64.
|
||||
func (ds DeleteSlice) Range(f func(uint64) bool) {
|
||||
for _, seq := range ds {
|
||||
if !f(seq) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dbs DeleteBlocks) NumDeleted() (total uint64) {
|
||||
for _, db := range dbs {
|
||||
_, _, num := db.State()
|
||||
total += num
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// ConsumerStore stores state on consumers for streams.
|
||||
type ConsumerStore interface {
|
||||
SetStarting(sseq uint64) error
|
||||
UpdateStarting(sseq uint64)
|
||||
Reset(sseq uint64) error
|
||||
HasState() bool
|
||||
UpdateDelivered(dseq, sseq, dc uint64, ts int64) error
|
||||
UpdateAcks(dseq, sseq uint64) error
|
||||
UpdateConfig(cfg *ConsumerConfig) error
|
||||
Update(*ConsumerState) error
|
||||
ForceUpdate(*ConsumerState) error
|
||||
State() (*ConsumerState, error)
|
||||
BorrowState() (*ConsumerState, error)
|
||||
EncodedState() ([]byte, error)
|
||||
Type() StorageType
|
||||
Stop() error
|
||||
Delete() error
|
||||
StreamDelete() error
|
||||
}
|
||||
|
||||
// SequencePair has both the consumer and the stream sequence. They point to same message.
|
||||
type SequencePair struct {
|
||||
Consumer uint64 `json:"consumer_seq"`
|
||||
Stream uint64 `json:"stream_seq"`
|
||||
}
|
||||
|
||||
// ConsumerState represents a stored state for a consumer.
|
||||
type ConsumerState struct {
|
||||
// Delivered keeps track of last delivered sequence numbers for both the stream and the consumer.
|
||||
Delivered SequencePair `json:"delivered"`
|
||||
// AckFloor keeps track of the ack floors for both the stream and the consumer.
|
||||
AckFloor SequencePair `json:"ack_floor"`
|
||||
// These are both in stream sequence context.
|
||||
// Pending is for all messages pending and the timestamp for the delivered time.
|
||||
// This will only be present when the AckPolicy is ExplicitAck.
|
||||
Pending map[uint64]*Pending `json:"pending,omitempty"`
|
||||
// This is for messages that have been redelivered, so count > 1.
|
||||
Redelivered map[uint64]uint64 `json:"redelivered,omitempty"`
|
||||
}
|
||||
|
||||
// Encode consumer state.
|
||||
func encodeConsumerState(state *ConsumerState) []byte {
|
||||
var hdr [seqsHdrSize]byte
|
||||
var buf []byte
|
||||
|
||||
maxSize := seqsHdrSize
|
||||
if lp := len(state.Pending); lp > 0 {
|
||||
maxSize += lp*(3*binary.MaxVarintLen64) + binary.MaxVarintLen64
|
||||
}
|
||||
if lr := len(state.Redelivered); lr > 0 {
|
||||
maxSize += lr*(2*binary.MaxVarintLen64) + binary.MaxVarintLen64
|
||||
}
|
||||
if maxSize == seqsHdrSize {
|
||||
buf = hdr[:seqsHdrSize]
|
||||
} else {
|
||||
buf = make([]byte, maxSize)
|
||||
}
|
||||
|
||||
// Write header
|
||||
buf[0] = magic
|
||||
buf[1] = 2
|
||||
|
||||
n := hdrLen
|
||||
n += binary.PutUvarint(buf[n:], state.AckFloor.Consumer)
|
||||
n += binary.PutUvarint(buf[n:], state.AckFloor.Stream)
|
||||
n += binary.PutUvarint(buf[n:], state.Delivered.Consumer)
|
||||
n += binary.PutUvarint(buf[n:], state.Delivered.Stream)
|
||||
n += binary.PutUvarint(buf[n:], uint64(len(state.Pending)))
|
||||
|
||||
asflr := state.AckFloor.Stream
|
||||
adflr := state.AckFloor.Consumer
|
||||
|
||||
// These are optional, but always write len. This is to avoid a truncate inline.
|
||||
if len(state.Pending) > 0 {
|
||||
// To save space we will use now rounded to seconds to be our base timestamp.
|
||||
mints := time.Now().Round(time.Second).Unix()
|
||||
// Write minimum timestamp we found from above.
|
||||
n += binary.PutVarint(buf[n:], mints)
|
||||
|
||||
for k, v := range state.Pending {
|
||||
n += binary.PutUvarint(buf[n:], k-asflr)
|
||||
n += binary.PutUvarint(buf[n:], v.Sequence-adflr)
|
||||
// Downsample to seconds to save on space.
|
||||
// Subsecond resolution not needed for recovery etc.
|
||||
ts := v.Timestamp / int64(time.Second)
|
||||
n += binary.PutVarint(buf[n:], mints-ts)
|
||||
}
|
||||
}
|
||||
|
||||
// We always write the redelivered len.
|
||||
n += binary.PutUvarint(buf[n:], uint64(len(state.Redelivered)))
|
||||
|
||||
// We expect these to be small.
|
||||
if len(state.Redelivered) > 0 {
|
||||
for k, v := range state.Redelivered {
|
||||
n += binary.PutUvarint(buf[n:], k-asflr)
|
||||
n += binary.PutUvarint(buf[n:], v)
|
||||
}
|
||||
}
|
||||
|
||||
return buf[:n]
|
||||
}
|
||||
|
||||
// Represents a pending message for explicit ack or ack all.
|
||||
// Sequence is the original consumer sequence.
|
||||
type Pending struct {
|
||||
Sequence uint64
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
const (
|
||||
limitsPolicyJSONString = `"limits"`
|
||||
interestPolicyJSONString = `"interest"`
|
||||
workQueuePolicyJSONString = `"workqueue"`
|
||||
)
|
||||
|
||||
var (
|
||||
limitsPolicyJSONBytes = []byte(limitsPolicyJSONString)
|
||||
interestPolicyJSONBytes = []byte(interestPolicyJSONString)
|
||||
workQueuePolicyJSONBytes = []byte(workQueuePolicyJSONString)
|
||||
)
|
||||
|
||||
func (rp RetentionPolicy) String() string {
|
||||
switch rp {
|
||||
case LimitsPolicy:
|
||||
return "Limits"
|
||||
case InterestPolicy:
|
||||
return "Interest"
|
||||
case WorkQueuePolicy:
|
||||
return "WorkQueue"
|
||||
default:
|
||||
return "Unknown Retention Policy"
|
||||
}
|
||||
}
|
||||
|
||||
func (rp RetentionPolicy) MarshalJSON() ([]byte, error) {
|
||||
switch rp {
|
||||
case LimitsPolicy:
|
||||
return limitsPolicyJSONBytes, nil
|
||||
case InterestPolicy:
|
||||
return interestPolicyJSONBytes, nil
|
||||
case WorkQueuePolicy:
|
||||
return workQueuePolicyJSONBytes, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("can not marshal %v", rp)
|
||||
}
|
||||
}
|
||||
|
||||
func (rp *RetentionPolicy) UnmarshalJSON(data []byte) error {
|
||||
switch string(data) {
|
||||
case limitsPolicyJSONString:
|
||||
*rp = LimitsPolicy
|
||||
case interestPolicyJSONString:
|
||||
*rp = InterestPolicy
|
||||
case workQueuePolicyJSONString:
|
||||
*rp = WorkQueuePolicy
|
||||
default:
|
||||
return fmt.Errorf("can not unmarshal %q", data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dp DiscardPolicy) String() string {
|
||||
switch dp {
|
||||
case DiscardOld:
|
||||
return "DiscardOld"
|
||||
case DiscardNew:
|
||||
return "DiscardNew"
|
||||
default:
|
||||
return "Unknown Discard Policy"
|
||||
}
|
||||
}
|
||||
|
||||
func (dp DiscardPolicy) MarshalJSON() ([]byte, error) {
|
||||
switch dp {
|
||||
case DiscardOld:
|
||||
return []byte(`"old"`), nil
|
||||
case DiscardNew:
|
||||
return []byte(`"new"`), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("can not marshal %v", dp)
|
||||
}
|
||||
}
|
||||
|
||||
func (dp *DiscardPolicy) UnmarshalJSON(data []byte) error {
|
||||
switch strings.ToLower(string(data)) {
|
||||
case `"old"`:
|
||||
*dp = DiscardOld
|
||||
case `"new"`:
|
||||
*dp = DiscardNew
|
||||
default:
|
||||
return fmt.Errorf("can not unmarshal %q", data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
memoryStorageJSONString = `"memory"`
|
||||
fileStorageJSONString = `"file"`
|
||||
)
|
||||
|
||||
var (
|
||||
memoryStorageJSONBytes = []byte(memoryStorageJSONString)
|
||||
fileStorageJSONBytes = []byte(fileStorageJSONString)
|
||||
)
|
||||
|
||||
func (st StorageType) String() string {
|
||||
switch st {
|
||||
case MemoryStorage:
|
||||
return "Memory"
|
||||
case FileStorage:
|
||||
return "File"
|
||||
default:
|
||||
return "Unknown Storage Type"
|
||||
}
|
||||
}
|
||||
|
||||
func (st StorageType) MarshalJSON() ([]byte, error) {
|
||||
switch st {
|
||||
case MemoryStorage:
|
||||
return memoryStorageJSONBytes, nil
|
||||
case FileStorage:
|
||||
return fileStorageJSONBytes, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("can not marshal %v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func (st *StorageType) UnmarshalJSON(data []byte) error {
|
||||
switch string(data) {
|
||||
case memoryStorageJSONString:
|
||||
*st = MemoryStorage
|
||||
case fileStorageJSONString:
|
||||
*st = FileStorage
|
||||
default:
|
||||
return fmt.Errorf("can not unmarshal %q", data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
ackNonePolicyJSONString = `"none"`
|
||||
ackAllPolicyJSONString = `"all"`
|
||||
ackExplicitPolicyJSONString = `"explicit"`
|
||||
ackFlowControlPolicyJSONString = `"flow_control"`
|
||||
)
|
||||
|
||||
var (
|
||||
ackNonePolicyJSONBytes = []byte(ackNonePolicyJSONString)
|
||||
ackAllPolicyJSONBytes = []byte(ackAllPolicyJSONString)
|
||||
ackExplicitPolicyJSONBytes = []byte(ackExplicitPolicyJSONString)
|
||||
ackFlowControlPolicyJSONBytes = []byte(ackFlowControlPolicyJSONString)
|
||||
)
|
||||
|
||||
func (ap AckPolicy) MarshalJSON() ([]byte, error) {
|
||||
switch ap {
|
||||
case AckNone:
|
||||
return ackNonePolicyJSONBytes, nil
|
||||
case AckAll:
|
||||
return ackAllPolicyJSONBytes, nil
|
||||
case AckExplicit:
|
||||
return ackExplicitPolicyJSONBytes, nil
|
||||
case AckFlowControl:
|
||||
return ackFlowControlPolicyJSONBytes, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("can not marshal %v", ap)
|
||||
}
|
||||
}
|
||||
|
||||
func (ap *AckPolicy) UnmarshalJSON(data []byte) error {
|
||||
switch string(data) {
|
||||
case ackNonePolicyJSONString:
|
||||
*ap = AckNone
|
||||
case ackAllPolicyJSONString:
|
||||
*ap = AckAll
|
||||
case ackExplicitPolicyJSONString:
|
||||
*ap = AckExplicit
|
||||
case ackFlowControlPolicyJSONString:
|
||||
*ap = AckFlowControl
|
||||
default:
|
||||
return fmt.Errorf("can not unmarshal %q", data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
replayInstantPolicyJSONString = `"instant"`
|
||||
replayOriginalPolicyJSONString = `"original"`
|
||||
)
|
||||
|
||||
var (
|
||||
replayInstantPolicyJSONBytes = []byte(replayInstantPolicyJSONString)
|
||||
replayOriginalPolicyJSONBytes = []byte(replayOriginalPolicyJSONString)
|
||||
)
|
||||
|
||||
func (rp ReplayPolicy) MarshalJSON() ([]byte, error) {
|
||||
switch rp {
|
||||
case ReplayInstant:
|
||||
return replayInstantPolicyJSONBytes, nil
|
||||
case ReplayOriginal:
|
||||
return replayOriginalPolicyJSONBytes, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("can not marshal %v", rp)
|
||||
}
|
||||
}
|
||||
|
||||
func (rp *ReplayPolicy) UnmarshalJSON(data []byte) error {
|
||||
switch string(data) {
|
||||
case replayInstantPolicyJSONString:
|
||||
*rp = ReplayInstant
|
||||
case replayOriginalPolicyJSONString:
|
||||
*rp = ReplayOriginal
|
||||
default:
|
||||
return fmt.Errorf("can not unmarshal %q", data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
deliverAllPolicyJSONString = `"all"`
|
||||
deliverLastPolicyJSONString = `"last"`
|
||||
deliverNewPolicyJSONString = `"new"`
|
||||
deliverByStartSequenceJSONString = `"by_start_sequence"`
|
||||
deliverByStartTimeJSONString = `"by_start_time"`
|
||||
deliverLastPerPolicyJSONString = `"last_per_subject"`
|
||||
deliverUndefinedJSONString = `"undefined"`
|
||||
)
|
||||
|
||||
var (
|
||||
deliverAllPolicyJSONBytes = []byte(deliverAllPolicyJSONString)
|
||||
deliverLastPolicyJSONBytes = []byte(deliverLastPolicyJSONString)
|
||||
deliverNewPolicyJSONBytes = []byte(deliverNewPolicyJSONString)
|
||||
deliverByStartSequenceJSONBytes = []byte(deliverByStartSequenceJSONString)
|
||||
deliverByStartTimeJSONBytes = []byte(deliverByStartTimeJSONString)
|
||||
deliverLastPerPolicyJSONBytes = []byte(deliverLastPerPolicyJSONString)
|
||||
deliverUndefinedJSONBytes = []byte(deliverUndefinedJSONString)
|
||||
)
|
||||
|
||||
func (p *DeliverPolicy) UnmarshalJSON(data []byte) error {
|
||||
switch string(data) {
|
||||
case deliverAllPolicyJSONString, deliverUndefinedJSONString:
|
||||
*p = DeliverAll
|
||||
case deliverLastPolicyJSONString:
|
||||
*p = DeliverLast
|
||||
case deliverLastPerPolicyJSONString:
|
||||
*p = DeliverLastPerSubject
|
||||
case deliverNewPolicyJSONString:
|
||||
*p = DeliverNew
|
||||
case deliverByStartSequenceJSONString:
|
||||
*p = DeliverByStartSequence
|
||||
case deliverByStartTimeJSONString:
|
||||
*p = DeliverByStartTime
|
||||
default:
|
||||
return fmt.Errorf("can not unmarshal %q", data)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p DeliverPolicy) MarshalJSON() ([]byte, error) {
|
||||
switch p {
|
||||
case DeliverAll:
|
||||
return deliverAllPolicyJSONBytes, nil
|
||||
case DeliverLast:
|
||||
return deliverLastPolicyJSONBytes, nil
|
||||
case DeliverLastPerSubject:
|
||||
return deliverLastPerPolicyJSONBytes, nil
|
||||
case DeliverNew:
|
||||
return deliverNewPolicyJSONBytes, nil
|
||||
case DeliverByStartSequence:
|
||||
return deliverByStartSequenceJSONBytes, nil
|
||||
case DeliverByStartTime:
|
||||
return deliverByStartTimeJSONBytes, nil
|
||||
default:
|
||||
return deliverUndefinedJSONBytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
func isOutOfSpaceErr(err error) bool {
|
||||
return err != nil && (strings.Contains(err.Error(), "no space left"))
|
||||
}
|
||||
|
||||
// For when our upper layer catchup detects its missing messages from the beginning of the stream.
|
||||
var errFirstSequenceMismatch = errors.New("first sequence mismatch")
|
||||
|
||||
func isClusterResetErr(err error) bool {
|
||||
return err == errLastSeqMismatch || err == ErrStoreEOF || err == errFirstSequenceMismatch || errors.Is(err, errCatchupAbortedNoLeader) || err == errCatchupTooManyRetries || err == errAlreadyLeader
|
||||
}
|
||||
|
||||
// Copy all fields.
|
||||
func (smo *StoreMsg) copy(sm *StoreMsg) {
|
||||
if sm.buf != nil {
|
||||
sm.buf = sm.buf[:0]
|
||||
}
|
||||
sm.buf = append(sm.buf, smo.buf...)
|
||||
// We set cap on header in case someone wants to expand it.
|
||||
sm.hdr, sm.msg = sm.buf[:len(smo.hdr):len(smo.hdr)], sm.buf[len(smo.hdr):]
|
||||
sm.subj, sm.seq, sm.ts = smo.subj, smo.seq, smo.ts
|
||||
}
|
||||
|
||||
// Clear all fields except underlying buffer but reset that if present to [:0].
|
||||
func (sm *StoreMsg) clear() {
|
||||
if sm == nil {
|
||||
return
|
||||
}
|
||||
*sm = StoreMsg{_EMPTY_, nil, nil, sm.buf, 0, 0}
|
||||
if len(sm.buf) > 0 {
|
||||
sm.buf = sm.buf[:0]
|
||||
}
|
||||
}
|
||||
|
||||
// Note this will avoid a copy of the data used for the string, but it will also reference the existing slice's data pointer.
|
||||
// So this should be used sparingly when we know the encompassing byte slice's lifetime is the same.
|
||||
func bytesToString(b []byte) string {
|
||||
if len(b) == 0 {
|
||||
return _EMPTY_
|
||||
}
|
||||
p := unsafe.SliceData(b)
|
||||
return unsafe.String(p, len(b))
|
||||
}
|
||||
|
||||
// Same in reverse. Used less often.
|
||||
func stringToBytes(s string) []byte {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
p := unsafe.StringData(s)
|
||||
b := unsafe.Slice(p, len(s))
|
||||
return b
|
||||
}
|
||||
|
||||
// Forces a copy of a string, for use in the case that you might have been passed a value when bytesToString was used,
|
||||
// but now you need a separate copy of it to store for longer-term use.
|
||||
func copyString(s string) string {
|
||||
b := make([]byte, len(s))
|
||||
copy(b, s)
|
||||
return bytesToString(b)
|
||||
}
|
||||
|
||||
func isPermissionError(err error) bool {
|
||||
return err != nil && os.IsPermission(err)
|
||||
}
|
||||
+9260
File diff suppressed because it is too large
Load Diff
+70
@@ -0,0 +1,70 @@
|
||||
// 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.
|
||||
|
||||
package stree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// For dumping out a text representation of a tree.
|
||||
func (t *SubjectTree[T]) Dump(w io.Writer) {
|
||||
t.dump(w, t.root, 0)
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
|
||||
// Will dump out a node.
|
||||
func (t *SubjectTree[T]) dump(w io.Writer, n node, depth int) {
|
||||
if n == nil {
|
||||
fmt.Fprintf(w, "EMPTY\n")
|
||||
return
|
||||
}
|
||||
if n.isLeaf() {
|
||||
leaf := n.(*leaf[T])
|
||||
fmt.Fprintf(w, "%s LEAF: Suffix: %q Value: %+v\n", dumpPre(depth), leaf.suffix, leaf.value)
|
||||
n = nil
|
||||
} else {
|
||||
// We are a node type here, grab meta portion.
|
||||
bn := n.base()
|
||||
fmt.Fprintf(w, "%s %s Prefix: %q\n", dumpPre(depth), n.kind(), bn.prefix)
|
||||
depth++
|
||||
n.iter(func(n node) bool {
|
||||
t.dump(w, n, depth)
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// For individual node/leaf dumps.
|
||||
func (n *leaf[T]) kind() string { return "LEAF" }
|
||||
func (n *node4) kind() string { return "NODE4" }
|
||||
func (n *node10) kind() string { return "NODE10" }
|
||||
func (n *node16) kind() string { return "NODE16" }
|
||||
func (n *node48) kind() string { return "NODE48" }
|
||||
func (n *node256) kind() string { return "NODE256" }
|
||||
|
||||
// Calculates the indendation, etc.
|
||||
func dumpPre(depth int) string {
|
||||
if depth == 0 {
|
||||
return "-- "
|
||||
} else {
|
||||
var b strings.Builder
|
||||
for i := 0; i < depth; i++ {
|
||||
b.WriteString(" ")
|
||||
}
|
||||
b.WriteString("|__ ")
|
||||
return b.String()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user