Bump reva to pull in the latest fixes

This commit is contained in:
André Duffeck
2025-03-21 12:29:25 +01:00
parent 5953f950ef
commit 00d49804cf
103 changed files with 14057 additions and 1640 deletions
+5 -4
View File
@@ -55,15 +55,15 @@ require (
github.com/mitchellh/mapstructure v1.5.0
github.com/mna/pigeon v1.3.0
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/nats-io/nats-server/v2 v2.10.26
github.com/nats-io/nats-server/v2 v2.11.0
github.com/nats-io/nats.go v1.39.1
github.com/oklog/run v1.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.23.1
github.com/onsi/ginkgo/v2 v2.23.2
github.com/onsi/gomega v1.36.2
github.com/open-policy-agent/opa v1.2.0
github.com/opencloud-eu/reva/v2 v2.28.1-0.20250320135948-a946c0d6d289
github.com/opencloud-eu/reva/v2 v2.28.1-0.20250321112659-61a430bfb4c5
github.com/orcaman/concurrent-map v1.0.0
github.com/owncloud/libre-graph-api-go v1.0.5-0.20240829135935-80dc00d6f5ea
github.com/pkg/errors v0.9.1
@@ -215,6 +215,7 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/gomodule/redigo v1.9.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.3 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/renameio/v2 v2.0.0 // indirect
github.com/gookit/color v1.5.4 // indirect
@@ -321,7 +322,7 @@ require (
go.uber.org/zap v1.23.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/time v0.10.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
+12 -8
View File
@@ -111,6 +111,8 @@ github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op h1:+OSa/t11TFhqfrX0EOSqQBDJ0YlpmK0rDSiB19dg9M0=
github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
@@ -526,6 +528,8 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
github.com/google/go-tika v0.3.1/go.mod h1:DJh5N8qxXIl85QkqmXknd+PeeRkUOTbvwyYf7ieDz6c=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -823,8 +827,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
github.com/nats-io/jwt/v2 v2.7.3 h1:6bNPK+FXgBeAqdj4cYQ0F8ViHRbi7woQLq4W29nUAzE=
github.com/nats-io/jwt/v2 v2.7.3/go.mod h1:GvkcbHhKquj3pkioy5put1wvPxs78UlZ7D/pY+BgZk4=
github.com/nats-io/nats-server/v2 v2.10.26 h1:2i3rAsn4x5/2eOt2NEmuI/iSb8zfHpIUI7yiaOWbo2c=
github.com/nats-io/nats-server/v2 v2.10.26/go.mod h1:SGzoWGU8wUVnMr/HJhEMv4R8U4f7hF4zDygmRxpNsvg=
github.com/nats-io/nats-server/v2 v2.11.0 h1:fdwAT1d6DZW/4LUz5rkvQUe5leGEwjjOQYntzVRKvjE=
github.com/nats-io/nats-server/v2 v2.11.0/go.mod h1:leXySghbdtXSUmWem8K9McnJ6xbJOb0t9+NQ5HTRZjI=
github.com/nats-io/nats.go v1.39.1 h1:oTkfKBmz7W047vRxV762M67ZdXeOtUgvbBaNoQ+3PPk=
github.com/nats-io/nats.go v1.39.1/go.mod h1:MgRb8oOdigA6cYpEPhXJuRVH6UE/V4jblJ2jQ27IXYM=
github.com/nats-io/nkeys v0.4.10 h1:glmRrpCmYLHByYcePvnTBEAwawwapjCPMjy2huw20wc=
@@ -852,8 +856,8 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.23.1 h1:Ox0cOPv/t8RzKJUfDo9ZKtRvBOJY369sFJnl00CjqwY=
github.com/onsi/ginkgo/v2 v2.23.1/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
github.com/onsi/ginkgo/v2 v2.23.2 h1:LYLd7Wz401p0N7xR8y7WL6D2QZwKpbirDg0EVIvzvMM=
github.com/onsi/ginkgo/v2 v2.23.2/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
@@ -861,8 +865,8 @@ github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/open-policy-agent/opa v1.2.0 h1:88NDVCM0of1eO6Z4AFeL3utTEtMuwloFmWWU7dRV1z0=
github.com/open-policy-agent/opa v1.2.0/go.mod h1:30euUmOvuBoebRCcJ7DMF42bRBOPznvt0ACUMYDUGVY=
github.com/opencloud-eu/reva/v2 v2.28.1-0.20250320135948-a946c0d6d289 h1:gg37XG4j3Y7yWLrD+B+2uNQ72g4YasdvpzOKJnuQH1Y=
github.com/opencloud-eu/reva/v2 v2.28.1-0.20250320135948-a946c0d6d289/go.mod h1:iK0tNdLgqK0zBi0l7Q4uWSn9GPUbYtNxz3YAMfYvYNg=
github.com/opencloud-eu/reva/v2 v2.28.1-0.20250321112659-61a430bfb4c5 h1:R2HXrbl4RP78Pgjs9d/djzzc9h7RrePjFRZnBdXHiFM=
github.com/opencloud-eu/reva/v2 v2.28.1-0.20250321112659-61a430bfb4c5/go.mod h1:6KR5qe5pUogF48rnybIksQcxWIACB2ISEqBa3kbZzZA=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
@@ -1477,8 +1481,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+202
View File
@@ -0,0 +1,202 @@
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.
+35
View File
@@ -0,0 +1,35 @@
# TPM 2.0 client library
## Tests
This library contains unit tests in `github.com/google/go-tpm/tpm2`, which just
tests that various encoding and error checking functions work correctly. It also
contains more comprehensive integration tests in
`github.com/google/go-tpm/tpm2/test`, which run actual commands on a TPM.
By default, these integration tests are run against the
[`go-tpm-tools`](https://github.com/google/go-tpm-tools)
simulator, which is baesed on the
[Microsoft Reference TPM2 code](https://github.com/microsoft/ms-tpm-20-ref). To
run both the unit and integration tests, run (in this directory)
```bash
go test . ./test
```
These integration tests can also be run against a real TPM device. This is
slightly more complex as the tests often need to be built as a normal user and
then executed as root. For example,
```bash
# Build the test binary without running it
go test -c github.com/google/go-tpm/tpm2/test
# Execute the test binary as root
sudo ./test.test --tpm-path=/dev/tpmrm0
```
On Linux, The `--tpm-path` causes the integration tests to be run against a
real TPM located at that path (usually `/dev/tpmrm0` or `/dev/tpm0`). On Windows, the story is similar, execept that
the `--use-tbs` flag is used instead.
Tip: if your TPM host is remote and you don't want to install Go on it, this
same two-step process can be used. The test binary can be copied to a remote
host and run without extra installation (as the test binary has very few
*runtime* dependancies).
+575
View File
@@ -0,0 +1,575 @@
// Copyright (c) 2018, Google LLC All rights reserved.
//
// 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 tpm2
import (
"crypto"
"crypto/elliptic"
"fmt"
"strings"
// Register the relevant hash implementations to prevent a runtime failure.
_ "crypto/sha1"
_ "crypto/sha256"
_ "crypto/sha512"
"github.com/google/go-tpm/tpmutil"
)
var hashInfo = []struct {
alg Algorithm
hash crypto.Hash
}{
{AlgSHA1, crypto.SHA1},
{AlgSHA256, crypto.SHA256},
{AlgSHA384, crypto.SHA384},
{AlgSHA512, crypto.SHA512},
{AlgSHA3_256, crypto.SHA3_256},
{AlgSHA3_384, crypto.SHA3_384},
{AlgSHA3_512, crypto.SHA3_512},
}
// MAX_DIGEST_BUFFER is the maximum size of []byte request or response fields.
// Typically used for chunking of big blobs of data (such as for hashing or
// encryption).
const maxDigestBuffer = 1024
// Algorithm represents a TPM_ALG_ID value.
type Algorithm uint16
// HashToAlgorithm looks up the TPM2 algorithm corresponding to the provided crypto.Hash
func HashToAlgorithm(hash crypto.Hash) (Algorithm, error) {
for _, info := range hashInfo {
if info.hash == hash {
return info.alg, nil
}
}
return AlgUnknown, fmt.Errorf("go hash algorithm #%d has no TPM2 algorithm", hash)
}
// IsNull returns true if a is AlgNull or zero (unset).
func (a Algorithm) IsNull() bool {
return a == AlgNull || a == AlgUnknown
}
// UsesCount returns true if a signature algorithm uses count value.
func (a Algorithm) UsesCount() bool {
return a == AlgECDAA
}
// UsesHash returns true if the algorithm requires the use of a hash.
func (a Algorithm) UsesHash() bool {
return a == AlgOAEP
}
// Hash returns a crypto.Hash based on the given TPM_ALG_ID.
// An error is returned if the given algorithm is not a hash algorithm or is not available.
func (a Algorithm) Hash() (crypto.Hash, error) {
for _, info := range hashInfo {
if info.alg == a {
if !info.hash.Available() {
return crypto.Hash(0), fmt.Errorf("go hash algorithm #%d not available", info.hash)
}
return info.hash, nil
}
}
return crypto.Hash(0), fmt.Errorf("hash algorithm not supported: 0x%x", a)
}
func (a Algorithm) String() string {
var s strings.Builder
var err error
switch a {
case AlgUnknown:
_, err = s.WriteString("AlgUnknown")
case AlgRSA:
_, err = s.WriteString("RSA")
case AlgSHA1:
_, err = s.WriteString("SHA1")
case AlgHMAC:
_, err = s.WriteString("HMAC")
case AlgAES:
_, err = s.WriteString("AES")
case AlgKeyedHash:
_, err = s.WriteString("KeyedHash")
case AlgXOR:
_, err = s.WriteString("XOR")
case AlgSHA256:
_, err = s.WriteString("SHA256")
case AlgSHA384:
_, err = s.WriteString("SHA384")
case AlgSHA512:
_, err = s.WriteString("SHA512")
case AlgNull:
_, err = s.WriteString("AlgNull")
case AlgRSASSA:
_, err = s.WriteString("RSASSA")
case AlgRSAES:
_, err = s.WriteString("RSAES")
case AlgRSAPSS:
_, err = s.WriteString("RSAPSS")
case AlgOAEP:
_, err = s.WriteString("OAEP")
case AlgECDSA:
_, err = s.WriteString("ECDSA")
case AlgECDH:
_, err = s.WriteString("ECDH")
case AlgECDAA:
_, err = s.WriteString("ECDAA")
case AlgKDF2:
_, err = s.WriteString("KDF2")
case AlgECC:
_, err = s.WriteString("ECC")
case AlgSymCipher:
_, err = s.WriteString("SymCipher")
case AlgSHA3_256:
_, err = s.WriteString("SHA3_256")
case AlgSHA3_384:
_, err = s.WriteString("SHA3_384")
case AlgSHA3_512:
_, err = s.WriteString("SHA3_512")
case AlgCTR:
_, err = s.WriteString("CTR")
case AlgOFB:
_, err = s.WriteString("OFB")
case AlgCBC:
_, err = s.WriteString("CBC")
case AlgCFB:
_, err = s.WriteString("CFB")
case AlgECB:
_, err = s.WriteString("ECB")
default:
return fmt.Sprintf("Alg?<%d>", int(a))
}
if err != nil {
return fmt.Sprintf("Writing to string builder failed: %v", err)
}
return s.String()
}
// Supported Algorithms.
const (
AlgUnknown Algorithm = 0x0000
AlgRSA Algorithm = 0x0001
AlgSHA1 Algorithm = 0x0004
AlgHMAC Algorithm = 0x0005
AlgAES Algorithm = 0x0006
AlgKeyedHash Algorithm = 0x0008
AlgXOR Algorithm = 0x000A
AlgSHA256 Algorithm = 0x000B
AlgSHA384 Algorithm = 0x000C
AlgSHA512 Algorithm = 0x000D
AlgNull Algorithm = 0x0010
AlgRSASSA Algorithm = 0x0014
AlgRSAES Algorithm = 0x0015
AlgRSAPSS Algorithm = 0x0016
AlgOAEP Algorithm = 0x0017
AlgECDSA Algorithm = 0x0018
AlgECDH Algorithm = 0x0019
AlgECDAA Algorithm = 0x001A
AlgKDF2 Algorithm = 0x0021
AlgECC Algorithm = 0x0023
AlgSymCipher Algorithm = 0x0025
AlgSHA3_256 Algorithm = 0x0027
AlgSHA3_384 Algorithm = 0x0028
AlgSHA3_512 Algorithm = 0x0029
AlgCTR Algorithm = 0x0040
AlgOFB Algorithm = 0x0041
AlgCBC Algorithm = 0x0042
AlgCFB Algorithm = 0x0043
AlgECB Algorithm = 0x0044
)
// HandleType defines a type of handle.
type HandleType uint8
// Supported handle types
const (
HandleTypePCR HandleType = 0x00
HandleTypeNVIndex HandleType = 0x01
HandleTypeHMACSession HandleType = 0x02
HandleTypeLoadedSession HandleType = 0x02
HandleTypePolicySession HandleType = 0x03
HandleTypeSavedSession HandleType = 0x03
HandleTypePermanent HandleType = 0x40
HandleTypeTransient HandleType = 0x80
HandleTypePersistent HandleType = 0x81
)
// SessionType defines the type of session created in StartAuthSession.
type SessionType uint8
// Supported session types.
const (
SessionHMAC SessionType = 0x00
SessionPolicy SessionType = 0x01
SessionTrial SessionType = 0x03
)
// SessionAttributes represents an attribute of a session.
type SessionAttributes byte
// Session Attributes (Structures 8.4 TPMA_SESSION)
const (
AttrContinueSession SessionAttributes = 1 << iota
AttrAuditExclusive
AttrAuditReset
_ // bit 3 reserved
_ // bit 4 reserved
AttrDecrypt
AttrEcrypt
AttrAudit
)
// EmptyAuth represents the empty authorization value.
var EmptyAuth []byte
// KeyProp is a bitmask used in Attributes field of key templates. Individual
// flags should be OR-ed to form a full mask.
type KeyProp uint32
// Key properties.
const (
FlagFixedTPM KeyProp = 0x00000002
FlagStClear KeyProp = 0x00000004
FlagFixedParent KeyProp = 0x00000010
FlagSensitiveDataOrigin KeyProp = 0x00000020
FlagUserWithAuth KeyProp = 0x00000040
FlagAdminWithPolicy KeyProp = 0x00000080
FlagNoDA KeyProp = 0x00000400
FlagRestricted KeyProp = 0x00010000
FlagDecrypt KeyProp = 0x00020000
FlagSign KeyProp = 0x00040000
FlagSealDefault = FlagFixedTPM | FlagFixedParent
FlagSignerDefault = FlagSign | FlagRestricted | FlagFixedTPM |
FlagFixedParent | FlagSensitiveDataOrigin | FlagUserWithAuth
FlagStorageDefault = FlagDecrypt | FlagRestricted | FlagFixedTPM |
FlagFixedParent | FlagSensitiveDataOrigin | FlagUserWithAuth
)
// TPMProp represents a Property Tag (TPM_PT) used with calls to GetCapability(CapabilityTPMProperties).
type TPMProp uint32
// TPM Capability Properties, see TPM 2.0 Spec, Rev 1.38, Table 23.
// Fixed TPM Properties (PT_FIXED)
const (
FamilyIndicator TPMProp = 0x100 + iota
SpecLevel
SpecRevision
SpecDayOfYear
SpecYear
Manufacturer
VendorString1
VendorString2
VendorString3
VendorString4
VendorTPMType
FirmwareVersion1
FirmwareVersion2
InputMaxBufferSize
TransientObjectsMin
PersistentObjectsMin
LoadedObjectsMin
ActiveSessionsMax
PCRCount
PCRSelectMin
ContextGapMax
_ // (PT_FIXED + 21) is skipped
NVCountersMax
NVIndexMax
MemoryMethod
ClockUpdate
ContextHash
ContextSym
ContextSymSize
OrderlyCount
CommandMaxSize
ResponseMaxSize
DigestMaxSize
ObjectContextMaxSize
SessionContextMaxSize
PSFamilyIndicator
PSSpecLevel
PSSpecRevision
PSSpecDayOfYear
PSSpecYear
SplitSigningMax
TotalCommands
LibraryCommands
VendorCommands
NVMaxBufferSize
TPMModes
CapabilityMaxBufferSize
)
// Variable TPM Properties (PT_VAR)
const (
TPMAPermanent TPMProp = 0x200 + iota
TPMAStartupClear
HRNVIndex
HRLoaded
HRLoadedAvail
HRActive
HRActiveAvail
HRTransientAvail
CurrentPersistent
AvailPersistent
NVCounters
NVCountersAvail
AlgorithmSet
LoadedCurves
LockoutCounter
MaxAuthFail
LockoutInterval
LockoutRecovery
NVWriteRecovery
AuditCounter0
AuditCounter1
)
// Allowed ranges of different kinds of Handles (TPM_HANDLE)
// These constants have type TPMProp for backwards compatibility.
const (
PCRFirst TPMProp = 0x00000000
HMACSessionFirst TPMProp = 0x02000000
LoadedSessionFirst TPMProp = 0x02000000
PolicySessionFirst TPMProp = 0x03000000
ActiveSessionFirst TPMProp = 0x03000000
TransientFirst TPMProp = 0x80000000
PersistentFirst TPMProp = 0x81000000
PersistentLast TPMProp = 0x81FFFFFF
PlatformPersistent TPMProp = 0x81800000
NVIndexFirst TPMProp = 0x01000000
NVIndexLast TPMProp = 0x01FFFFFF
PermanentFirst TPMProp = 0x40000000
PermanentLast TPMProp = 0x4000010F
)
// Reserved Handles.
const (
HandleOwner tpmutil.Handle = 0x40000001 + iota
HandleRevoke
HandleTransport
HandleOperator
HandleAdmin
HandleEK
HandleNull
HandleUnassigned
HandlePasswordSession
HandleLockout
HandleEndorsement
HandlePlatform
)
// Capability identifies some TPM property or state type.
type Capability uint32
// TPM Capabilities.
const (
CapabilityAlgs Capability = iota
CapabilityHandles
CapabilityCommands
CapabilityPPCommands
CapabilityAuditCommands
CapabilityPCRs
CapabilityTPMProperties
CapabilityPCRProperties
CapabilityECCCurves
CapabilityAuthPolicies
)
// TPM Structure Tags. Tags are used to disambiguate structures, similar to Alg
// values: tag value defines what kind of data lives in a nested field.
const (
TagNull tpmutil.Tag = 0x8000
TagNoSessions tpmutil.Tag = 0x8001
TagSessions tpmutil.Tag = 0x8002
TagAttestCertify tpmutil.Tag = 0x8017
TagAttestQuote tpmutil.Tag = 0x8018
TagAttestCreation tpmutil.Tag = 0x801a
TagAuthSecret tpmutil.Tag = 0x8023
TagHashCheck tpmutil.Tag = 0x8024
TagAuthSigned tpmutil.Tag = 0x8025
)
// StartupType instructs the TPM on how to handle its state during Shutdown or
// Startup.
type StartupType uint16
// Startup types
const (
StartupClear StartupType = iota
StartupState
)
// EllipticCurve identifies specific EC curves.
type EllipticCurve uint16
// ECC curves supported by TPM 2.0 spec.
const (
CurveNISTP192 = EllipticCurve(iota + 1)
CurveNISTP224
CurveNISTP256
CurveNISTP384
CurveNISTP521
CurveBNP256 = EllipticCurve(iota + 10)
CurveBNP638
CurveSM2P256 = EllipticCurve(0x0020)
)
var toGoCurve = map[EllipticCurve]elliptic.Curve{
CurveNISTP224: elliptic.P224(),
CurveNISTP256: elliptic.P256(),
CurveNISTP384: elliptic.P384(),
CurveNISTP521: elliptic.P521(),
}
// Supported TPM operations.
const (
CmdNVUndefineSpaceSpecial tpmutil.Command = 0x0000011F
CmdEvictControl tpmutil.Command = 0x00000120
CmdUndefineSpace tpmutil.Command = 0x00000122
CmdClear tpmutil.Command = 0x00000126
CmdHierarchyChangeAuth tpmutil.Command = 0x00000129
CmdDefineSpace tpmutil.Command = 0x0000012A
CmdCreatePrimary tpmutil.Command = 0x00000131
CmdIncrementNVCounter tpmutil.Command = 0x00000134
CmdWriteNV tpmutil.Command = 0x00000137
CmdWriteLockNV tpmutil.Command = 0x00000138
CmdDictionaryAttackLockReset tpmutil.Command = 0x00000139
CmdDictionaryAttackParameters tpmutil.Command = 0x0000013A
CmdPCREvent tpmutil.Command = 0x0000013C
CmdPCRReset tpmutil.Command = 0x0000013D
CmdSequenceComplete tpmutil.Command = 0x0000013E
CmdStartup tpmutil.Command = 0x00000144
CmdShutdown tpmutil.Command = 0x00000145
CmdActivateCredential tpmutil.Command = 0x00000147
CmdCertify tpmutil.Command = 0x00000148
CmdCertifyCreation tpmutil.Command = 0x0000014A
CmdReadNV tpmutil.Command = 0x0000014E
CmdReadLockNV tpmutil.Command = 0x0000014F
CmdPolicySecret tpmutil.Command = 0x00000151
CmdCreate tpmutil.Command = 0x00000153
CmdECDHZGen tpmutil.Command = 0x00000154
CmdImport tpmutil.Command = 0x00000156
CmdLoad tpmutil.Command = 0x00000157
CmdQuote tpmutil.Command = 0x00000158
CmdRSADecrypt tpmutil.Command = 0x00000159
CmdSequenceUpdate tpmutil.Command = 0x0000015C
CmdSign tpmutil.Command = 0x0000015D
CmdUnseal tpmutil.Command = 0x0000015E
CmdPolicySigned tpmutil.Command = 0x00000160
CmdContextLoad tpmutil.Command = 0x00000161
CmdContextSave tpmutil.Command = 0x00000162
CmdECDHKeyGen tpmutil.Command = 0x00000163
CmdEncryptDecrypt tpmutil.Command = 0x00000164
CmdFlushContext tpmutil.Command = 0x00000165
CmdLoadExternal tpmutil.Command = 0x00000167
CmdMakeCredential tpmutil.Command = 0x00000168
CmdReadPublicNV tpmutil.Command = 0x00000169
CmdPolicyCommandCode tpmutil.Command = 0x0000016C
CmdPolicyOr tpmutil.Command = 0x00000171
CmdReadPublic tpmutil.Command = 0x00000173
CmdRSAEncrypt tpmutil.Command = 0x00000174
CmdStartAuthSession tpmutil.Command = 0x00000176
CmdGetCapability tpmutil.Command = 0x0000017A
CmdGetRandom tpmutil.Command = 0x0000017B
CmdHash tpmutil.Command = 0x0000017D
CmdPCRRead tpmutil.Command = 0x0000017E
CmdPolicyPCR tpmutil.Command = 0x0000017F
CmdReadClock tpmutil.Command = 0x00000181
CmdPCRExtend tpmutil.Command = 0x00000182
CmdEventSequenceComplete tpmutil.Command = 0x00000185
CmdHashSequenceStart tpmutil.Command = 0x00000186
CmdPolicyGetDigest tpmutil.Command = 0x00000189
CmdPolicyPassword tpmutil.Command = 0x0000018C
CmdEncryptDecrypt2 tpmutil.Command = 0x00000193
)
// Regular TPM 2.0 devices use 24-bit mask (3 bytes) for PCR selection.
const sizeOfPCRSelect = 3
const defaultRSAExponent = 1<<16 + 1
// NVAttr is a bitmask used in Attributes field of NV indexes. Individual
// flags should be OR-ed to form a full mask.
type NVAttr uint32
// NV Attributes
const (
AttrPPWrite NVAttr = 0x00000001
AttrOwnerWrite NVAttr = 0x00000002
AttrAuthWrite NVAttr = 0x00000004
AttrPolicyWrite NVAttr = 0x00000008
AttrPolicyDelete NVAttr = 0x00000400
AttrWriteLocked NVAttr = 0x00000800
AttrWriteAll NVAttr = 0x00001000
AttrWriteDefine NVAttr = 0x00002000
AttrWriteSTClear NVAttr = 0x00004000
AttrGlobalLock NVAttr = 0x00008000
AttrPPRead NVAttr = 0x00010000
AttrOwnerRead NVAttr = 0x00020000
AttrAuthRead NVAttr = 0x00040000
AttrPolicyRead NVAttr = 0x00080000
AttrNoDA NVAttr = 0x02000000
AttrOrderly NVAttr = 0x04000000
AttrClearSTClear NVAttr = 0x08000000
AttrReadLocked NVAttr = 0x10000000
AttrWritten NVAttr = 0x20000000
AttrPlatformCreate NVAttr = 0x40000000
AttrReadSTClear NVAttr = 0x80000000
)
var permMap = map[NVAttr]string{
AttrPPWrite: "PPWrite",
AttrOwnerWrite: "OwnerWrite",
AttrAuthWrite: "AuthWrite",
AttrPolicyWrite: "PolicyWrite",
AttrPolicyDelete: "PolicyDelete",
AttrWriteLocked: "WriteLocked",
AttrWriteAll: "WriteAll",
AttrWriteDefine: "WriteDefine",
AttrWriteSTClear: "WriteSTClear",
AttrGlobalLock: "GlobalLock",
AttrPPRead: "PPRead",
AttrOwnerRead: "OwnerRead",
AttrAuthRead: "AuthRead",
AttrPolicyRead: "PolicyRead",
AttrNoDA: "No Do",
AttrOrderly: "Oderly",
AttrClearSTClear: "ClearSTClear",
AttrReadLocked: "ReadLocked",
AttrWritten: "Writte",
AttrPlatformCreate: "PlatformCreate",
AttrReadSTClear: "ReadSTClear",
}
// String returns a textual representation of the set of NVAttr
func (p NVAttr) String() string {
var retString strings.Builder
for iterator, item := range permMap {
if (p & iterator) != 0 {
retString.WriteString(item + " + ")
}
}
if retString.String() == "" {
return "Permission/s not found"
}
return strings.TrimSuffix(retString.String(), " + ")
}
+362
View File
@@ -0,0 +1,362 @@
package tpm2
import (
"fmt"
"github.com/google/go-tpm/tpmutil"
)
type (
// RCFmt0 holds Format 0 error codes
RCFmt0 uint8
// RCFmt1 holds Format 1 error codes
RCFmt1 uint8
// RCWarn holds error codes used in warnings
RCWarn uint8
// RCIndex is used to reference arguments, handles and sessions in errors
RCIndex uint8
)
// Format 0 error codes.
const (
RCInitialize RCFmt0 = 0x00
RCFailure RCFmt0 = 0x01
RCSequence RCFmt0 = 0x03
RCPrivate RCFmt0 = 0x0B
RCHMAC RCFmt0 = 0x19
RCDisabled RCFmt0 = 0x20
RCExclusive RCFmt0 = 0x21
RCAuthType RCFmt0 = 0x24
RCAuthMissing RCFmt0 = 0x25
RCPolicy RCFmt0 = 0x26
RCPCR RCFmt0 = 0x27
RCPCRChanged RCFmt0 = 0x28
RCUpgrade RCFmt0 = 0x2D
RCTooManyContexts RCFmt0 = 0x2E
RCAuthUnavailable RCFmt0 = 0x2F
RCReboot RCFmt0 = 0x30
RCUnbalanced RCFmt0 = 0x31
RCCommandSize RCFmt0 = 0x42
RCCommandCode RCFmt0 = 0x43
RCAuthSize RCFmt0 = 0x44
RCAuthContext RCFmt0 = 0x45
RCNVRange RCFmt0 = 0x46
RCNVSize RCFmt0 = 0x47
RCNVLocked RCFmt0 = 0x48
RCNVAuthorization RCFmt0 = 0x49
RCNVUninitialized RCFmt0 = 0x4A
RCNVSpace RCFmt0 = 0x4B
RCNVDefined RCFmt0 = 0x4C
RCBadContext RCFmt0 = 0x50
RCCPHash RCFmt0 = 0x51
RCParent RCFmt0 = 0x52
RCNeedsTest RCFmt0 = 0x53
RCNoResult RCFmt0 = 0x54
RCSensitive RCFmt0 = 0x55
)
var fmt0Msg = map[RCFmt0]string{
RCInitialize: "TPM not initialized by TPM2_Startup or already initialized",
RCFailure: "commands not being accepted because of a TPM failure",
RCSequence: "improper use of a sequence handle",
RCPrivate: "not currently used",
RCHMAC: "not currently used",
RCDisabled: "the command is disabled",
RCExclusive: "command failed because audit sequence required exclusivity",
RCAuthType: "authorization handle is not correct for command",
RCAuthMissing: "5 command requires an authorization session for handle and it is not present",
RCPolicy: "policy failure in math operation or an invalid authPolicy value",
RCPCR: "PCR check fail",
RCPCRChanged: "PCR have changed since checked",
RCUpgrade: "TPM is in field upgrade mode unless called via TPM2_FieldUpgradeData(), then it is not in field upgrade mode",
RCTooManyContexts: "context ID counter is at maximum",
RCAuthUnavailable: "authValue or authPolicy is not available for selected entity",
RCReboot: "a _TPM_Init and Startup(CLEAR) is required before the TPM can resume operation",
RCUnbalanced: "the protection algorithms (hash and symmetric) are not reasonably balanced; the digest size of the hash must be larger than the key size of the symmetric algorithm",
RCCommandSize: "command commandSize value is inconsistent with contents of the command buffer; either the size is not the same as the octets loaded by the hardware interface layer or the value is not large enough to hold a command header",
RCCommandCode: "command code not supported",
RCAuthSize: "the value of authorizationSize is out of range or the number of octets in the Authorization Area is greater than required",
RCAuthContext: "use of an authorization session with a context command or another command that cannot have an authorization session",
RCNVRange: "NV offset+size is out of range",
RCNVSize: "Requested allocation size is larger than allowed",
RCNVLocked: "NV access locked",
RCNVAuthorization: "NV access authorization fails in command actions",
RCNVUninitialized: "an NV Index is used before being initialized or the state saved by TPM2_Shutdown(STATE) could not be restored",
RCNVSpace: "insufficient space for NV allocation",
RCNVDefined: "NV Index or persistent object already defined",
RCBadContext: "context in TPM2_ContextLoad() is not valid",
RCCPHash: "cpHash value already set or not correct for use",
RCParent: "handle for parent is not a valid parent",
RCNeedsTest: "some function needs testing",
RCNoResult: "returned when an internal function cannot process a request due to an unspecified problem; this code is usually related to invalid parameters that are not properly filtered by the input unmarshaling code",
RCSensitive: "the sensitive area did not unmarshal correctly after decryption",
}
// Format 1 error codes.
const (
RCAsymmetric = 0x01
RCAttributes = 0x02
RCHash = 0x03
RCValue = 0x04
RCHierarchy = 0x05
RCKeySize = 0x07
RCMGF = 0x08
RCMode = 0x09
RCType = 0x0A
RCHandle = 0x0B
RCKDF = 0x0C
RCRange = 0x0D
RCAuthFail = 0x0E
RCNonce = 0x0F
RCPP = 0x10
RCScheme = 0x12
RCSize = 0x15
RCSymmetric = 0x16
RCTag = 0x17
RCSelector = 0x18
RCInsufficient = 0x1A
RCSignature = 0x1B
RCKey = 0x1C
RCPolicyFail = 0x1D
RCIntegrity = 0x1F
RCTicket = 0x20
RCReservedBits = 0x21
RCBadAuth = 0x22
RCExpired = 0x23
RCPolicyCC = 0x24
RCBinding = 0x25
RCCurve = 0x26
RCECCPoint = 0x27
)
var fmt1Msg = map[RCFmt1]string{
RCAsymmetric: "asymmetric algorithm not supported or not correct",
RCAttributes: "inconsistent attributes",
RCHash: "hash algorithm not supported or not appropriate",
RCValue: "value is out of range or is not correct for the context",
RCHierarchy: "hierarchy is not enabled or is not correct for the use",
RCKeySize: "key size is not supported",
RCMGF: "mask generation function not supported",
RCMode: "mode of operation not supported",
RCType: "the type of the value is not appropriate for the use",
RCHandle: "the handle is not correct for the use",
RCKDF: "unsupported key derivation function or function not appropriate for use",
RCRange: "value was out of allowed range",
RCAuthFail: "the authorization HMAC check failed and DA counter incremented",
RCNonce: "invalid nonce size or nonce value mismatch",
RCPP: "authorization requires assertion of PP",
RCScheme: "unsupported or incompatible scheme",
RCSize: "structure is the wrong size",
RCSymmetric: "unsupported symmetric algorithm or key size, or not appropriate for instance",
RCTag: "incorrect structure tag",
RCSelector: "union selector is incorrect",
RCInsufficient: "the TPM was unable to unmarshal a value because there were not enough octets in the input buffer",
RCSignature: "the signature is not valid",
RCKey: "key fields are not compatible with the selected use",
RCPolicyFail: "a policy check failed",
RCIntegrity: "integrity check failed",
RCTicket: "invalid ticket",
RCReservedBits: "reserved bits not set to zero as required",
RCBadAuth: "authorization failure without DA implications",
RCExpired: "the policy has expired",
RCPolicyCC: "the commandCode in the policy is not the commandCode of the command or the command code in a policy command references a command that is not implemented",
RCBinding: "public and sensitive portions of an object are not cryptographically bound",
RCCurve: "curve not supported",
RCECCPoint: "point is not on the required curve",
}
// Warning codes.
const (
RCContextGap RCWarn = 0x01
RCObjectMemory RCWarn = 0x02
RCSessionMemory RCWarn = 0x03
RCMemory RCWarn = 0x04
RCSessionHandles RCWarn = 0x05
RCObjectHandles RCWarn = 0x06
RCLocality RCWarn = 0x07
RCYielded RCWarn = 0x08
RCCanceled RCWarn = 0x09
RCTesting RCWarn = 0x0A
RCReferenceH0 RCWarn = 0x10
RCReferenceH1 RCWarn = 0x11
RCReferenceH2 RCWarn = 0x12
RCReferenceH3 RCWarn = 0x13
RCReferenceH4 RCWarn = 0x14
RCReferenceH5 RCWarn = 0x15
RCReferenceH6 RCWarn = 0x16
RCReferenceS0 RCWarn = 0x18
RCReferenceS1 RCWarn = 0x19
RCReferenceS2 RCWarn = 0x1A
RCReferenceS3 RCWarn = 0x1B
RCReferenceS4 RCWarn = 0x1C
RCReferenceS5 RCWarn = 0x1D
RCReferenceS6 RCWarn = 0x1E
RCNVRate RCWarn = 0x20
RCLockout RCWarn = 0x21
RCRetry RCWarn = 0x22
RCNVUnavailable RCWarn = 0x23
)
var warnMsg = map[RCWarn]string{
RCContextGap: "gap for context ID is too large",
RCObjectMemory: "out of memory for object contexts",
RCSessionMemory: "out of memory for session contexts",
RCMemory: "out of shared object/session memory or need space for internal operations",
RCSessionHandles: "out of session handles",
RCObjectHandles: "out of object handles",
RCLocality: "bad locality",
RCYielded: "the TPM has suspended operation on the command; forward progress was made and the command may be retried",
RCCanceled: "the command was canceled",
RCTesting: "TPM is performing self-tests",
RCReferenceH0: "the 1st handle in the handle area references a transient object or session that is not loaded",
RCReferenceH1: "the 2nd handle in the handle area references a transient object or session that is not loaded",
RCReferenceH2: "the 3rd handle in the handle area references a transient object or session that is not loaded",
RCReferenceH3: "the 4th handle in the handle area references a transient object or session that is not loaded",
RCReferenceH4: "the 5th handle in the handle area references a transient object or session that is not loaded",
RCReferenceH5: "the 6th handle in the handle area references a transient object or session that is not loaded",
RCReferenceH6: "the 7th handle in the handle area references a transient object or session that is not loaded",
RCReferenceS0: "the 1st authorization session handle references a session that is not loaded",
RCReferenceS1: "the 2nd authorization session handle references a session that is not loaded",
RCReferenceS2: "the 3rd authorization session handle references a session that is not loaded",
RCReferenceS3: "the 4th authorization session handle references a session that is not loaded",
RCReferenceS4: "the 5th authorization session handle references a session that is not loaded",
RCReferenceS5: "the 6th authorization session handle references a session that is not loaded",
RCReferenceS6: "the 7th authorization session handle references a session that is not loaded",
RCNVRate: "the TPM is rate-limiting accesses to prevent wearout of NV",
RCLockout: "authorizations for objects subject to DA protection are not allowed at this time because the TPM is in DA lockout mode",
RCRetry: "the TPM was not able to start the command",
RCNVUnavailable: "the command may require writing of NV and NV is not current accessible",
}
// Indexes for arguments, handles and sessions.
const (
RC1 RCIndex = iota + 1
RC2
RC3
RC4
RC5
RC6
RC7
RC8
RC9
RCA
RCB
RCC
RCD
RCE
RCF
)
const unknownCode = "unknown error code"
// Error is returned for all Format 0 errors from the TPM. It is used for general
// errors not specific to a parameter, handle or session.
type Error struct {
Code RCFmt0
}
func (e Error) Error() string {
msg := fmt0Msg[e.Code]
if msg == "" {
msg = unknownCode
}
return fmt.Sprintf("error code 0x%x : %s", e.Code, msg)
}
// VendorError represents a vendor-specific error response. These types of responses
// are not decoded and Code contains the complete response code.
type VendorError struct {
Code uint32
}
func (e VendorError) Error() string {
return fmt.Sprintf("vendor error code 0x%x", e.Code)
}
// Warning is typically used to report transient errors.
type Warning struct {
Code RCWarn
}
func (w Warning) Error() string {
msg := warnMsg[w.Code]
if msg == "" {
msg = unknownCode
}
return fmt.Sprintf("warning code 0x%x : %s", w.Code, msg)
}
// ParameterError describes an error related to a parameter, and the parameter number.
type ParameterError struct {
Code RCFmt1
Parameter RCIndex
}
func (e ParameterError) Error() string {
msg := fmt1Msg[e.Code]
if msg == "" {
msg = unknownCode
}
return fmt.Sprintf("parameter %d, error code 0x%x : %s", e.Parameter, e.Code, msg)
}
// HandleError describes an error related to a handle, and the handle number.
type HandleError struct {
Code RCFmt1
Handle RCIndex
}
func (e HandleError) Error() string {
msg := fmt1Msg[e.Code]
if msg == "" {
msg = unknownCode
}
return fmt.Sprintf("handle %d, error code 0x%x : %s", e.Handle, e.Code, msg)
}
// SessionError describes an error related to a session, and the session number.
type SessionError struct {
Code RCFmt1
Session RCIndex
}
func (e SessionError) Error() string {
msg := fmt1Msg[e.Code]
if msg == "" {
msg = unknownCode
}
return fmt.Sprintf("session %d, error code 0x%x : %s", e.Session, e.Code, msg)
}
// Decode a TPM2 response code and return the appropriate error. Logic
// according to the "Response Code Evaluation" chart in Part 1 of the TPM 2.0
// spec.
func decodeResponse(code tpmutil.ResponseCode) error {
if code == tpmutil.RCSuccess {
return nil
}
if code&0x180 == 0 { // Bits 7:8 == 0 is a TPM1 error
return fmt.Errorf("response status 0x%x", code)
}
if code&0x80 == 0 { // Bit 7 unset
if code&0x400 > 0 { // Bit 10 set, vendor specific code
return VendorError{uint32(code)}
}
if code&0x800 > 0 { // Bit 11 set, warning with code in bit 0:6
return Warning{RCWarn(code & 0x7f)}
}
// error with code in bit 0:6
return Error{RCFmt0(code & 0x7f)}
}
if code&0x40 > 0 { // Bit 6 set, code in 0:5, parameter number in 8:11
return ParameterError{RCFmt1(code & 0x3f), RCIndex((code & 0xf00) >> 8)}
}
if code&0x800 == 0 { // Bit 11 unset, code in 0:5, handle in 8:10
return HandleError{RCFmt1(code & 0x3f), RCIndex((code & 0x700) >> 8)}
}
// Code in 0:5, Session in 8:10
return SessionError{RCFmt1(code & 0x3f), RCIndex((code & 0x700) >> 8)}
}
+116
View File
@@ -0,0 +1,116 @@
// Copyright (c) 2018, Google LLC All rights reserved.
//
// 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 tpm2
import (
"crypto"
"crypto/hmac"
"encoding/binary"
"hash"
)
// KDFa implements TPM 2.0's default key derivation function, as defined in
// section 11.4.9.2 of the TPM revision 2 specification part 1.
// See: https://trustedcomputinggroup.org/resource/tpm-library-specification/
// The key & label parameters must not be zero length.
// The label parameter is a non-null-terminated string.
// The contextU & contextV parameters are optional.
// Deprecated: Use KDFaHash.
func KDFa(hashAlg Algorithm, key []byte, label string, contextU, contextV []byte, bits int) ([]byte, error) {
h, err := hashAlg.Hash()
if err != nil {
return nil, err
}
return KDFaHash(h, key, label, contextU, contextV, bits), nil
}
// KDFe implements TPM 2.0's ECDH key derivation function, as defined in
// section 11.4.9.3 of the TPM revision 2 specification part 1.
// See: https://trustedcomputinggroup.org/resource/tpm-library-specification/
// The z parameter is the x coordinate of one party's private ECC key multiplied
// by the other party's public ECC point.
// The use parameter is a non-null-terminated string.
// The partyUInfo and partyVInfo are the x coordinates of the initiator's and
// Deprecated: Use KDFeHash.
func KDFe(hashAlg Algorithm, z []byte, use string, partyUInfo, partyVInfo []byte, bits int) ([]byte, error) {
h, err := hashAlg.Hash()
if err != nil {
return nil, err
}
return KDFeHash(h, z, use, partyUInfo, partyVInfo, bits), nil
}
// KDFaHash implements TPM 2.0's default key derivation function, as defined in
// section 11.4.9.2 of the TPM revision 2 specification part 1.
// See: https://trustedcomputinggroup.org/resource/tpm-library-specification/
// The key & label parameters must not be zero length.
// The label parameter is a non-null-terminated string.
// The contextU & contextV parameters are optional.
func KDFaHash(h crypto.Hash, key []byte, label string, contextU, contextV []byte, bits int) []byte {
mac := hmac.New(h.New, key)
out := kdf(mac, bits, func() {
mac.Write([]byte(label))
mac.Write([]byte{0}) // Terminating null character for C-string.
mac.Write(contextU)
mac.Write(contextV)
binary.Write(mac, binary.BigEndian, uint32(bits))
})
return out
}
// KDFeHash implements TPM 2.0's ECDH key derivation function, as defined in
// section 11.4.9.3 of the TPM revision 2 specification part 1.
// See: https://trustedcomputinggroup.org/resource/tpm-library-specification/
// The z parameter is the x coordinate of one party's private ECC key multiplied
// by the other party's public ECC point.
// The use parameter is a non-null-terminated string.
// The partyUInfo and partyVInfo are the x coordinates of the initiator's and
// the responder's ECC points, respectively.
func KDFeHash(h crypto.Hash, z []byte, use string, partyUInfo, partyVInfo []byte, bits int) []byte {
hash := h.New()
out := kdf(hash, bits, func() {
hash.Write(z)
hash.Write([]byte(use))
hash.Write([]byte{0}) // Terminating null character for C-string.
hash.Write(partyUInfo)
hash.Write(partyVInfo)
})
return out
}
func kdf(h hash.Hash, bits int, update func()) []byte {
bytes := (bits + 7) / 8
out := []byte{}
for counter := 1; len(out) < bytes; counter++ {
h.Reset()
binary.Write(h, binary.BigEndian, uint32(counter))
update()
out = h.Sum(out)
}
// out's length is a multiple of hash size, so there will be excess
// bytes if bytes isn't a multiple of hash size.
out = out[:bytes]
// As mentioned in the KDFa and KDFe specs mentioned above,
// the unused bits of the most significant octet are masked off.
if maskBits := uint8(bits % 8); maskBits > 0 {
out[0] &= (1 << maskBits) - 1
}
return out
}
+57
View File
@@ -0,0 +1,57 @@
//go:build !windows
// Copyright (c) 2019, Google LLC All rights reserved.
//
// 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 tpm2
import (
"errors"
"fmt"
"io"
"os"
"github.com/google/go-tpm/tpmutil"
)
// OpenTPM opens a channel to the TPM at the given path. If the file is a
// device, then it treats it like a normal TPM device, and if the file is a
// Unix domain socket, then it opens a connection to the socket.
//
// This function may also be invoked with no paths, as tpm2.OpenTPM(). In this
// case, the default paths on Linux (/dev/tpmrm0 then /dev/tpm0), will be used.
func OpenTPM(path ...string) (tpm io.ReadWriteCloser, err error) {
switch len(path) {
case 0:
tpm, err = tpmutil.OpenTPM("/dev/tpmrm0")
if errors.Is(err, os.ErrNotExist) {
tpm, err = tpmutil.OpenTPM("/dev/tpm0")
}
case 1:
tpm, err = tpmutil.OpenTPM(path[0])
default:
return nil, errors.New("cannot specify multiple paths to tpm2.OpenTPM")
}
if err != nil {
return nil, err
}
// Make sure this is a TPM 2.0
_, err = GetManufacturer(tpm)
if err != nil {
tpm.Close()
return nil, fmt.Errorf("open %s: device is not a TPM 2.0", path)
}
return tpm, nil
}
+39
View File
@@ -0,0 +1,39 @@
//go:build windows
// Copyright (c) 2018, Google LLC All rights reserved.
//
// 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 tpm2
import (
"fmt"
"io"
"github.com/google/go-tpm/tpmutil"
"github.com/google/go-tpm/tpmutil/tbs"
)
// OpenTPM opens a channel to the TPM.
func OpenTPM() (io.ReadWriteCloser, error) {
info, err := tbs.GetDeviceInfo()
if err != nil {
return nil, err
}
if info.TPMVersion != tbs.TPMVersion20 {
return nil, fmt.Errorf("openTPM: device is not a TPM 2.0")
}
return tpmutil.OpenTPM()
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+211
View File
@@ -0,0 +1,211 @@
// Copyright (c) 2018, Google LLC All rights reserved.
//
// 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 tpmutil
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"reflect"
)
var (
selfMarshalerType = reflect.TypeOf((*SelfMarshaler)(nil)).Elem()
handlesAreaType = reflect.TypeOf((*[]Handle)(nil))
)
// packWithHeader takes a header and a sequence of elements that are either of
// fixed length or slices of fixed-length types and packs them into a single
// byte array using binary.Write. It updates the CommandHeader to have the right
// length.
func packWithHeader(ch commandHeader, cmd ...interface{}) ([]byte, error) {
hdrSize := binary.Size(ch)
body, err := Pack(cmd...)
if err != nil {
return nil, fmt.Errorf("couldn't pack message body: %v", err)
}
bodySize := len(body)
ch.Size = uint32(hdrSize + bodySize)
header, err := Pack(ch)
if err != nil {
return nil, fmt.Errorf("couldn't pack message header: %v", err)
}
return append(header, body...), nil
}
// Pack encodes a set of elements into a single byte array, using
// encoding/binary. This means that all the elements must be encodeable
// according to the rules of encoding/binary.
//
// It has one difference from encoding/binary: it encodes byte slices with a
// prepended length, to match how the TPM encodes variable-length arrays. If
// you wish to add a byte slice without length prefix, use RawBytes.
func Pack(elts ...interface{}) ([]byte, error) {
buf := new(bytes.Buffer)
if err := packType(buf, elts...); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// tryMarshal attempts to use a TPMMarshal() method defined on the type
// to pack v into buf. True is returned if the method exists and the
// marshal was attempted.
func tryMarshal(buf io.Writer, v reflect.Value) (bool, error) {
t := v.Type()
if t.Implements(selfMarshalerType) {
if v.Kind() == reflect.Ptr && v.IsNil() {
return true, fmt.Errorf("cannot call TPMMarshal on a nil pointer of type %T", v)
}
return true, v.Interface().(SelfMarshaler).TPMMarshal(buf)
}
// We might have a non-pointer struct field, but we dont have a
// pointer with which to implement the interface.
// If the pointer of the type implements the interface, we should be
// able to construct a value to call TPMMarshal() with.
// TODO(awly): Try and avoid blowing away private data by using Addr() instead of Set()
if reflect.PtrTo(t).Implements(selfMarshalerType) {
tmp := reflect.New(t)
tmp.Elem().Set(v)
return true, tmp.Interface().(SelfMarshaler).TPMMarshal(buf)
}
return false, nil
}
func packValue(buf io.Writer, v reflect.Value) error {
if v.Type() == handlesAreaType {
v = v.Convert(reflect.TypeOf((*handleList)(nil)))
}
if canMarshal, err := tryMarshal(buf, v); canMarshal {
return err
}
switch v.Kind() {
case reflect.Ptr:
if v.IsNil() {
return fmt.Errorf("cannot pack nil %s", v.Type().String())
}
return packValue(buf, v.Elem())
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
if err := packValue(buf, f); err != nil {
return err
}
}
default:
return binary.Write(buf, binary.BigEndian, v.Interface())
}
return nil
}
func packType(buf io.Writer, elts ...interface{}) error {
for _, e := range elts {
if err := packValue(buf, reflect.ValueOf(e)); err != nil {
return err
}
}
return nil
}
// tryUnmarshal attempts to use TPMUnmarshal() to perform the
// unpack, if the given value implements SelfMarshaler.
// True is returned if v implements SelfMarshaler & TPMUnmarshal
// was called, along with an error returned from TPMUnmarshal.
func tryUnmarshal(buf io.Reader, v reflect.Value) (bool, error) {
t := v.Type()
if t.Implements(selfMarshalerType) {
if v.Kind() == reflect.Ptr && v.IsNil() {
return true, fmt.Errorf("cannot call TPMUnmarshal on a nil pointer")
}
return true, v.Interface().(SelfMarshaler).TPMUnmarshal(buf)
}
// We might have a non-pointer struct field, which is addressable,
// If the pointer of the type implements the interface, and the
// value is addressable, we should be able to call TPMUnmarshal().
if v.CanAddr() && reflect.PtrTo(t).Implements(selfMarshalerType) {
return true, v.Addr().Interface().(SelfMarshaler).TPMUnmarshal(buf)
}
return false, nil
}
// Unpack is a convenience wrapper around UnpackBuf. Unpack returns the number
// of bytes read from b to fill elts and error, if any.
func Unpack(b []byte, elts ...interface{}) (int, error) {
buf := bytes.NewBuffer(b)
err := UnpackBuf(buf, elts...)
read := len(b) - buf.Len()
return read, err
}
func unpackValue(buf io.Reader, v reflect.Value) error {
if v.Type() == handlesAreaType {
v = v.Convert(reflect.TypeOf((*handleList)(nil)))
}
if didUnmarshal, err := tryUnmarshal(buf, v); didUnmarshal {
return err
}
switch v.Kind() {
case reflect.Ptr:
if v.IsNil() {
return fmt.Errorf("cannot unpack nil %s", v.Type().String())
}
return unpackValue(buf, v.Elem())
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
if err := unpackValue(buf, f); err != nil {
return err
}
}
return nil
default:
// binary.Read can only set pointer values, so we need to take the address.
if !v.CanAddr() {
return fmt.Errorf("cannot unpack unaddressable leaf type %q", v.Type().String())
}
return binary.Read(buf, binary.BigEndian, v.Addr().Interface())
}
}
// UnpackBuf recursively unpacks types from a reader just as encoding/binary
// does under binary.BigEndian, but with one difference: it unpacks a byte
// slice by first reading an integer with lengthPrefixSize bytes, then reading
// that many bytes. It assumes that incoming values are pointers to values so
// that, e.g., underlying slices can be resized as needed.
func UnpackBuf(buf io.Reader, elts ...interface{}) error {
for _, e := range elts {
v := reflect.ValueOf(e)
if v.Kind() != reflect.Ptr {
return fmt.Errorf("non-pointer value %q passed to UnpackBuf", v.Type().String())
}
if v.IsNil() {
return errors.New("nil pointer passed to UnpackBuf")
}
if err := unpackValue(buf, v); err != nil {
return err
}
}
return nil
}
+10
View File
@@ -0,0 +1,10 @@
//go:build !linux && !darwin
package tpmutil
import (
"os"
)
// Not implemented on Windows.
func poll(_ *os.File) error { return nil }
+32
View File
@@ -0,0 +1,32 @@
//go:build linux || darwin
package tpmutil
import (
"fmt"
"os"
"golang.org/x/sys/unix"
)
// poll blocks until the file descriptor is ready for reading or an error occurs.
func poll(f *os.File) error {
var (
fds = []unix.PollFd{{
Fd: int32(f.Fd()),
Events: 0x1, // POLLIN
}}
timeout = -1 // Indefinite timeout
)
if _, err := unix.Poll(fds, timeout); err != nil {
return err
}
// Revents is filled in by the kernel.
// If the expected event happened, Revents should match Events.
if fds[0].Revents != fds[0].Events {
return fmt.Errorf("unexpected poll Revents 0x%x", fds[0].Revents)
}
return nil
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright (c) 2018, Google LLC All rights reserved.
//
// 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 tpmutil provides common utility functions for both TPM 1.2 and TPM
// 2.0 devices.
package tpmutil
import (
"errors"
"io"
"os"
"time"
)
// maxTPMResponse is the largest possible response from the TPM. We need to know
// this because we don't always know the length of the TPM response, and
// /dev/tpm insists on giving it all back in a single value rather than
// returning a header and a body in separate responses.
const maxTPMResponse = 4096
// RunCommandRaw executes the given raw command and returns the raw response.
// Does not check the response code except to execute retry logic.
func RunCommandRaw(rw io.ReadWriter, inb []byte) ([]byte, error) {
if rw == nil {
return nil, errors.New("nil TPM handle")
}
// f(t) = (2^t)ms, up to 2s
var backoffFac uint
var rh responseHeader
var outb []byte
for {
if _, err := rw.Write(inb); err != nil {
return nil, err
}
// If the TPM is a real device, it may not be ready for reading
// immediately after writing the command. Wait until the file
// descriptor is ready to be read from.
if f, ok := rw.(*os.File); ok {
if err := poll(f); err != nil {
return nil, err
}
}
outb = make([]byte, maxTPMResponse)
outlen, err := rw.Read(outb)
if err != nil {
return nil, err
}
// Resize the buffer to match the amount read from the TPM.
outb = outb[:outlen]
_, err = Unpack(outb, &rh)
if err != nil {
return nil, err
}
// If TPM is busy, retry the command after waiting a few ms.
if rh.Res == RCRetry {
if backoffFac < 11 {
dur := (1 << backoffFac) * time.Millisecond
time.Sleep(dur)
backoffFac++
} else {
return nil, err
}
} else {
break
}
}
return outb, nil
}
// RunCommand executes cmd with given tag and arguments. Returns TPM response
// body (without response header) and response code from the header. Returned
// error may be nil if response code is not RCSuccess; caller should check
// both.
func RunCommand(rw io.ReadWriter, tag Tag, cmd Command, in ...interface{}) ([]byte, ResponseCode, error) {
inb, err := packWithHeader(commandHeader{tag, 0, cmd}, in...)
if err != nil {
return nil, 0, err
}
outb, err := RunCommandRaw(rw, inb)
if err != nil {
return nil, 0, err
}
var rh responseHeader
read, err := Unpack(outb, &rh)
if err != nil {
return nil, 0, err
}
if rh.Res != RCSuccess {
return nil, rh.Res, nil
}
return outb[read:], rh.Res, nil
}
+111
View File
@@ -0,0 +1,111 @@
//go:build !windows
// Copyright (c) 2018, Google LLC All rights reserved.
//
// 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 tpmutil
import (
"fmt"
"io"
"net"
"os"
)
// OpenTPM opens a channel to the TPM at the given path. If the file is a
// device, then it treats it like a normal TPM device, and if the file is a
// Unix domain socket, then it opens a connection to the socket.
func OpenTPM(path string) (io.ReadWriteCloser, error) {
// If it's a regular file, then open it
var rwc io.ReadWriteCloser
fi, err := os.Stat(path)
if err != nil {
return nil, err
}
if fi.Mode()&os.ModeDevice != 0 {
var f *os.File
f, err = os.OpenFile(path, os.O_RDWR, 0600)
if err != nil {
return nil, err
}
rwc = io.ReadWriteCloser(f)
} else if fi.Mode()&os.ModeSocket != 0 {
rwc = NewEmulatorReadWriteCloser(path)
} else {
return nil, fmt.Errorf("unsupported TPM file mode %s", fi.Mode().String())
}
return rwc, nil
}
// dialer abstracts the net.Dial call so test code can provide its own net.Conn
// implementation.
type dialer func(network, path string) (net.Conn, error)
// EmulatorReadWriteCloser manages connections with a TPM emulator over a Unix
// domain socket. These emulators often operate in a write/read/disconnect
// sequence, so the Write method always connects, and the Read method always
// closes. EmulatorReadWriteCloser is not thread safe.
type EmulatorReadWriteCloser struct {
path string
conn net.Conn
dialer dialer
}
// NewEmulatorReadWriteCloser stores information about a Unix domain socket to
// write to and read from.
func NewEmulatorReadWriteCloser(path string) *EmulatorReadWriteCloser {
return &EmulatorReadWriteCloser{
path: path,
dialer: net.Dial,
}
}
// Read implements io.Reader by reading from the Unix domain socket and closing
// it.
func (erw *EmulatorReadWriteCloser) Read(p []byte) (int, error) {
// Read is always the second operation in a Write/Read sequence.
if erw.conn == nil {
return 0, fmt.Errorf("must call Write then Read in an alternating sequence")
}
n, err := erw.conn.Read(p)
erw.conn.Close()
erw.conn = nil
return n, err
}
// Write implements io.Writer by connecting to the Unix domain socket and
// writing.
func (erw *EmulatorReadWriteCloser) Write(p []byte) (int, error) {
if erw.conn != nil {
return 0, fmt.Errorf("must call Write then Read in an alternating sequence")
}
var err error
erw.conn, err = erw.dialer("unix", erw.path)
if err != nil {
return 0, err
}
return erw.conn.Write(p)
}
// Close implements io.Closer by closing the Unix domain socket if one is open.
func (erw *EmulatorReadWriteCloser) Close() error {
if erw.conn == nil {
return fmt.Errorf("cannot call Close when no connection is open")
}
err := erw.conn.Close()
erw.conn = nil
return err
}
+84
View File
@@ -0,0 +1,84 @@
// Copyright (c) 2018, Google LLC All rights reserved.
//
// 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 tpmutil
import (
"io"
"github.com/google/go-tpm/tpmutil/tbs"
)
// winTPMBuffer is a ReadWriteCloser to access the TPM in Windows.
type winTPMBuffer struct {
context tbs.Context
outBuffer []byte
}
// Executes the TPM command specified by commandBuffer (at Normal Priority), returning the number
// of bytes in the command and any error code returned by executing the TPM command. Command
// response can be read by calling Read().
func (rwc *winTPMBuffer) Write(commandBuffer []byte) (int, error) {
// TPM spec defines longest possible response to be maxTPMResponse.
rwc.outBuffer = rwc.outBuffer[:maxTPMResponse]
outBufferLen, err := rwc.context.SubmitCommand(
tbs.NormalPriority,
commandBuffer,
rwc.outBuffer,
)
if err != nil {
rwc.outBuffer = rwc.outBuffer[:0]
return 0, err
}
// Shrink outBuffer so it is length of response.
rwc.outBuffer = rwc.outBuffer[:outBufferLen]
return len(commandBuffer), nil
}
// Provides TPM response from the command called in the last Write call.
func (rwc *winTPMBuffer) Read(responseBuffer []byte) (int, error) {
if len(rwc.outBuffer) == 0 {
return 0, io.EOF
}
lenCopied := copy(responseBuffer, rwc.outBuffer)
// Cut out the piece of slice which was just read out, maintaining original slice capacity.
rwc.outBuffer = append(rwc.outBuffer[:0], rwc.outBuffer[lenCopied:]...)
return lenCopied, nil
}
func (rwc *winTPMBuffer) Close() error {
return rwc.context.Close()
}
// OpenTPM creates a new instance of a ReadWriteCloser which can interact with a
// Windows TPM.
func OpenTPM() (io.ReadWriteCloser, error) {
tpmContext, err := tbs.CreateContext(tbs.TPMVersion20, tbs.IncludeTPM12|tbs.IncludeTPM20)
rwc := &winTPMBuffer{
context: tpmContext,
outBuffer: make([]byte, 0, maxTPMResponse),
}
return rwc, err
}
// FromContext creates a new instance of a ReadWriteCloser which can
// interact with a Windows TPM, using the specified TBS handle.
func FromContext(ctx tbs.Context) io.ReadWriteCloser {
return &winTPMBuffer{
context: ctx,
outBuffer: make([]byte, 0, maxTPMResponse),
}
}
+195
View File
@@ -0,0 +1,195 @@
// Copyright (c) 2018, Google LLC All rights reserved.
//
// 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 tpmutil
import (
"bytes"
"encoding/binary"
"fmt"
"io"
)
// maxBytesBufferSize sets a sane upper bound on the size of a U32Bytes
// buffer. This limit exists to prevent a maliciously large size prefix
// from resulting in a massive memory allocation, potentially causing
// an OOM condition on the system.
// We expect no buffer from a TPM to approach 1Mb in size.
const maxBytesBufferSize uint32 = 1024 * 1024 // 1Mb.
// RawBytes is for Pack and RunCommand arguments that are already encoded.
// Compared to []byte, RawBytes will not be prepended with slice length during
// encoding.
type RawBytes []byte
// U16Bytes is a byte slice with a 16-bit header
type U16Bytes []byte
// TPMMarshal packs U16Bytes
func (b *U16Bytes) TPMMarshal(out io.Writer) error {
size := len([]byte(*b))
if err := binary.Write(out, binary.BigEndian, uint16(size)); err != nil {
return err
}
n, err := out.Write(*b)
if err != nil {
return err
}
if n != size {
return fmt.Errorf("unable to write all contents of U16Bytes")
}
return nil
}
// TPMUnmarshal unpacks a U16Bytes
func (b *U16Bytes) TPMUnmarshal(in io.Reader) error {
var tmpSize uint16
if err := binary.Read(in, binary.BigEndian, &tmpSize); err != nil {
return err
}
size := int(tmpSize)
if len(*b) >= size {
*b = (*b)[:size]
} else {
*b = append(*b, make([]byte, size-len(*b))...)
}
n, err := in.Read(*b)
if err != nil {
return err
}
if n != size {
return io.ErrUnexpectedEOF
}
return nil
}
// U32Bytes is a byte slice with a 32-bit header
type U32Bytes []byte
// TPMMarshal packs U32Bytes
func (b *U32Bytes) TPMMarshal(out io.Writer) error {
size := len([]byte(*b))
if err := binary.Write(out, binary.BigEndian, uint32(size)); err != nil {
return err
}
n, err := out.Write(*b)
if err != nil {
return err
}
if n != size {
return fmt.Errorf("unable to write all contents of U32Bytes")
}
return nil
}
// TPMUnmarshal unpacks a U32Bytes
func (b *U32Bytes) TPMUnmarshal(in io.Reader) error {
var tmpSize uint32
if err := binary.Read(in, binary.BigEndian, &tmpSize); err != nil {
return err
}
if tmpSize > maxBytesBufferSize {
return bytes.ErrTooLarge
}
// We can now safely cast to an int on 32-bit or 64-bit machines
size := int(tmpSize)
if len(*b) >= size {
*b = (*b)[:size]
} else {
*b = append(*b, make([]byte, size-len(*b))...)
}
n, err := in.Read(*b)
if err != nil {
return err
}
if n != size {
return fmt.Errorf("unable to read all contents in to U32Bytes")
}
return nil
}
// Tag is a command tag.
type Tag uint16
// Command is an identifier of a TPM command.
type Command uint32
// A commandHeader is the header for a TPM command.
type commandHeader struct {
Tag Tag
Size uint32
Cmd Command
}
// ResponseCode is a response code returned by TPM.
type ResponseCode uint32
// RCSuccess is response code for successful command. Identical for TPM 1.2 and
// 2.0.
const RCSuccess ResponseCode = 0x000
// RCRetry is response code for TPM is busy.
const RCRetry ResponseCode = 0x922
// A responseHeader is a header for TPM responses.
type responseHeader struct {
Tag Tag
Size uint32
Res ResponseCode
}
// A Handle is a reference to a TPM object.
type Handle uint32
// HandleValue returns the handle value. This behavior is intended to satisfy
// an interface that can be implemented by other, more complex types as well.
func (h Handle) HandleValue() uint32 {
return uint32(h)
}
type handleList []Handle
func (l *handleList) TPMMarshal(_ io.Writer) error {
return fmt.Errorf("TPMMarhsal on []Handle is not supported yet")
}
func (l *handleList) TPMUnmarshal(in io.Reader) error {
var numHandles uint16
if err := binary.Read(in, binary.BigEndian, &numHandles); err != nil {
return err
}
// Make len(e) match size exactly.
size := int(numHandles)
if len(*l) >= size {
*l = (*l)[:size]
} else {
*l = append(*l, make([]Handle, size-len(*l))...)
}
return binary.Read(in, binary.BigEndian, *l)
}
// SelfMarshaler allows custom types to override default encoding/decoding
// behavior in Pack, Unpack and UnpackBuf.
type SelfMarshaler interface {
TPMMarshal(out io.Writer) error
TPMUnmarshal(in io.Reader) error
}
+267
View File
@@ -0,0 +1,267 @@
// Copyright (c) 2018, Google LLC All rights reserved.
//
// 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 tbs provides an low-level interface directly mapping to Windows
// Tbs.dll system library commands:
// https://docs.microsoft.com/en-us/windows/desktop/TBS/tpm-base-services-portal
// Public field descriptions contain links to the high-level Windows documentation.
package tbs
import (
"fmt"
"syscall"
"unsafe"
)
// Context references the current TPM context
type Context uintptr
// Version of TPM being used by the application.
type Version uint32
// Flag indicates TPM versions that are supported by the application.
type Flag uint32
// CommandPriority is used to determine which pending command to submit whenever the TPM is free.
type CommandPriority uint32
// Command parameters:
// https://github.com/tpn/winsdk-10/blob/master/Include/10.0.10240.0/shared/tbs.h
const (
// https://docs.microsoft.com/en-us/windows/desktop/api/Tbs/ns-tbs-tdtbs_context_params2
// OR flags to use multiple.
RequestRaw Flag = 1 << iota // Add flag to request raw context
IncludeTPM12 // Add flag to support TPM 1.2
IncludeTPM20 // Add flag to support TPM 2
TPMVersion12 Version = 1 // For TPM 1.2 applications
TPMVersion20 Version = 2 // For TPM 2 applications or applications using multiple TPM versions
// https://docs.microsoft.com/en-us/windows/desktop/tbs/command-scheduling
// https://docs.microsoft.com/en-us/windows/desktop/api/Tbs/nf-tbs-tbsip_submit_command#parameters
LowPriority CommandPriority = 100 // For low priority application use
NormalPriority CommandPriority = 200 // For normal priority application use
HighPriority CommandPriority = 300 // For high priority application use
SystemPriority CommandPriority = 400 // For system tasks that access the TPM
commandLocalityZero uint32 = 0 // Windows currently only supports TBS_COMMAND_LOCALITY_ZERO.
)
// Error is the return type of all functions in this package.
type Error uint32
func (err Error) Error() string {
if description, ok := errorDescriptions[err]; ok {
return fmt.Sprintf("TBS Error 0x%X: %s", uint32(err), description)
}
return fmt.Sprintf("Unrecognized TBS Error 0x%X", uint32(err))
}
func getError(err uintptr) error {
// tbs.dll uses 0x0 as the return value for success.
if err == 0 {
return nil
}
return Error(err)
}
// TBS Return Codes:
// https://docs.microsoft.com/en-us/windows/desktop/TBS/tbs-return-codes
const (
ErrInternalError Error = 0x80284001
ErrBadParameter Error = 0x80284002
ErrInvalidOutputPointer Error = 0x80284003
ErrInvalidContext Error = 0x80284004
ErrInsufficientBuffer Error = 0x80284005
ErrIOError Error = 0x80284006
ErrInvalidContextParam Error = 0x80284007
ErrServiceNotRunning Error = 0x80284008
ErrTooManyTBSContexts Error = 0x80284009
ErrTooManyResources Error = 0x8028400A
ErrServiceStartPending Error = 0x8028400B
ErrPPINotSupported Error = 0x8028400C
ErrCommandCanceled Error = 0x8028400D
ErrBufferTooLarge Error = 0x8028400E
ErrTPMNotFound Error = 0x8028400F
ErrServiceDisabled Error = 0x80284010
ErrNoEventLog Error = 0x80284011
ErrAccessDenied Error = 0x80284012
ErrProvisioningNotAllowed Error = 0x80284013
ErrPPIFunctionUnsupported Error = 0x80284014
ErrOwnerauthNotFound Error = 0x80284015
)
var errorDescriptions = map[Error]string{
ErrInternalError: "An internal software error occurred.",
ErrBadParameter: "One or more parameter values are not valid.",
ErrInvalidOutputPointer: "A specified output pointer is bad.",
ErrInvalidContext: "The specified context handle does not refer to a valid context.",
ErrInsufficientBuffer: "The specified output buffer is too small.",
ErrIOError: "An error occurred while communicating with the TPM.",
ErrInvalidContextParam: "A context parameter that is not valid was passed when attempting to create a TBS context.",
ErrServiceNotRunning: "The TBS service is not running and could not be started.",
ErrTooManyTBSContexts: "A new context could not be created because there are too many open contexts.",
ErrTooManyResources: "A new virtual resource could not be created because there are too many open virtual resources.",
ErrServiceStartPending: "The TBS service has been started but is not yet running.",
ErrPPINotSupported: "The physical presence interface is not supported.",
ErrCommandCanceled: "The command was canceled.",
ErrBufferTooLarge: "The input or output buffer is too large.",
ErrTPMNotFound: "A compatible Trusted Platform Module (TPM) Security Device cannot be found on this computer.",
ErrServiceDisabled: "The TBS service has been disabled.",
ErrNoEventLog: "The TBS event log is not available.",
ErrAccessDenied: "The caller does not have the appropriate rights to perform the requested operation.",
ErrProvisioningNotAllowed: "The TPM provisioning action is not allowed by the specified flags.",
ErrPPIFunctionUnsupported: "The Physical Presence Interface of this firmware does not support the requested method.",
ErrOwnerauthNotFound: "The requested TPM OwnerAuth value was not found.",
}
// Tbs.dll provides an API for making calls to the TPM:
// https://docs.microsoft.com/en-us/windows/desktop/TBS/tpm-base-services-portal
var (
tbsDLL = syscall.NewLazyDLL("Tbs.dll")
tbsGetDeviceInfo = tbsDLL.NewProc("Tbsi_GetDeviceInfo")
tbsCreateContext = tbsDLL.NewProc("Tbsi_Context_Create")
tbsContextClose = tbsDLL.NewProc("Tbsip_Context_Close")
tbsSubmitCommand = tbsDLL.NewProc("Tbsip_Submit_Command")
tbsGetTCGLog = tbsDLL.NewProc("Tbsi_Get_TCG_Log")
)
// Returns the address of the beginning of a slice or 0 for a nil slice.
func sliceAddress(s []byte) uintptr {
if len(s) == 0 {
return 0
}
return uintptr(unsafe.Pointer(&(s[0])))
}
// DeviceInfo is TPM_DEVICE_INFO from tbs.h
type DeviceInfo struct {
StructVersion uint32
TPMVersion Version
TPMInterfaceType uint32
TPMImpRevision uint32
}
// GetDeviceInfo gets the DeviceInfo of the current TPM:
// https://docs.microsoft.com/en-us/windows/win32/api/tbs/nf-tbs-tbsi_getdeviceinfo
func GetDeviceInfo() (*DeviceInfo, error) {
info := DeviceInfo{}
// TBS_RESULT Tbsi_GetDeviceInfo(
// UINT32 Size,
// PVOID Info
// );
if err := tbsGetDeviceInfo.Find(); err != nil {
return nil, err
}
result, _, _ := tbsGetDeviceInfo.Call(
unsafe.Sizeof(info),
uintptr(unsafe.Pointer(&info)),
)
return &info, getError(result)
}
// CreateContext creates a new TPM context:
// https://docs.microsoft.com/en-us/windows/desktop/api/Tbs/nf-tbs-tbsi_context_create
func CreateContext(version Version, flag Flag) (Context, error) {
var context Context
params := struct {
Version
Flag
}{version, flag}
// TBS_RESULT Tbsi_Context_Create(
// _In_ PCTBS_CONTEXT_PARAMS pContextParams,
// _Out_ PTBS_HCONTEXT *phContext
// );
if err := tbsCreateContext.Find(); err != nil {
return context, err
}
result, _, _ := tbsCreateContext.Call(
uintptr(unsafe.Pointer(&params)),
uintptr(unsafe.Pointer(&context)),
)
return context, getError(result)
}
// Close closes an existing TPM context:
// https://docs.microsoft.com/en-us/windows/desktop/api/Tbs/nf-tbs-tbsip_context_close
func (context Context) Close() error {
// TBS_RESULT Tbsip_Context_Close(
// _In_ TBS_HCONTEXT hContext
// );
if err := tbsContextClose.Find(); err != nil {
return err
}
result, _, _ := tbsContextClose.Call(uintptr(context))
return getError(result)
}
// SubmitCommand sends commandBuffer to the TPM, returning the number of bytes
// written to responseBuffer. ErrInsufficientBuffer is returned if the
// responseBuffer is too short. ErrInvalidOutputPointer is returned if the
// responseBuffer is nil. On failure, the returned length is unspecified.
// https://docs.microsoft.com/en-us/windows/desktop/api/Tbs/nf-tbs-tbsip_submit_command
func (context Context) SubmitCommand(
priority CommandPriority,
commandBuffer []byte,
responseBuffer []byte,
) (uint32, error) {
responseBufferLen := uint32(len(responseBuffer))
// TBS_RESULT Tbsip_Submit_Command(
// _In_ TBS_HCONTEXT hContext,
// _In_ TBS_COMMAND_LOCALITY Locality,
// _In_ TBS_COMMAND_PRIORITY Priority,
// _In_ const PCBYTE *pabCommand,
// _In_ UINT32 cbCommand,
// _Out_ PBYTE *pabResult,
// _Inout_ UINT32 *pcbOutput
// );
if err := tbsSubmitCommand.Find(); err != nil {
return 0, err
}
result, _, _ := tbsSubmitCommand.Call(
uintptr(context),
uintptr(commandLocalityZero),
uintptr(priority),
sliceAddress(commandBuffer),
uintptr(len(commandBuffer)),
sliceAddress(responseBuffer),
uintptr(unsafe.Pointer(&responseBufferLen)),
)
return responseBufferLen, getError(result)
}
// GetTCGLog gets the system event log, returning the number of bytes written
// to logBuffer. If logBuffer is nil, the size of the TCG log is returned.
// ErrInsufficientBuffer is returned if the logBuffer is too short. On failure,
// the returned length is unspecified.
// https://docs.microsoft.com/en-us/windows/desktop/api/Tbs/nf-tbs-tbsi_get_tcg_log
func (context Context) GetTCGLog(logBuffer []byte) (uint32, error) {
logBufferLen := uint32(len(logBuffer))
// TBS_RESULT Tbsi_Get_TCG_Log(
// TBS_HCONTEXT hContext,
// PBYTE pOutputBuf,
// PUINT32 pOutputBufLen
// );
if err := tbsGetTCGLog.Find(); err != nil {
return 0, err
}
result, _, _ := tbsGetTCGLog.Call(
uintptr(context),
sliceAddress(logBuffer),
uintptr(unsafe.Pointer(&logBufferLen)),
)
return logBufferLen, getError(result)
}
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build gofuzz
// +build gofuzz
package conf
+55
View File
@@ -26,6 +26,8 @@ package conf
// see parse_test.go for more examples.
import (
"crypto/sha256"
"encoding/json"
"fmt"
"os"
"path/filepath"
@@ -35,6 +37,8 @@ import (
"unicode"
)
const _EMPTY_ = ""
type parser struct {
mapping map[string]any
lx *lexer
@@ -69,6 +73,15 @@ func Parse(data string) (map[string]any, error) {
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)
@@ -98,6 +111,44 @@ func ParseFileWithChecks(fp string) (map[string]any, error) {
return p.mapping, nil
}
// cleanupUsedEnvVars will recursively remove all already used
// environment variables which might be in the parsed tree.
func cleanupUsedEnvVars(m map[string]any) {
for k, v := range m {
t := v.(*token)
if t.usedVariable {
delete(m, k)
continue
}
// Cleanup any other env var that is still in the map.
if tm, ok := t.value.(map[string]any); ok {
cleanupUsedEnvVars(tm)
}
}
}
// ParseFileWithChecksDigest returns the processed config and a digest
// that represents the configuration.
func ParseFileWithChecksDigest(fp string) (map[string]any, string, error) {
data, err := os.ReadFile(fp)
if err != nil {
return nil, _EMPTY_, err
}
p, err := parse(string(data), fp, true)
if err != nil {
return nil, _EMPTY_, err
}
// Filter out any environment variables before taking the digest.
cleanupUsedEnvVars(p.mapping)
digest := sha256.New()
e := json.NewEncoder(digest)
err = e.Encode(p.mapping)
if err != nil {
return nil, _EMPTY_, err
}
return p.mapping, fmt.Sprintf("sha256:%x", digest.Sum(nil)), nil
}
type token struct {
item item
value any
@@ -105,6 +156,10 @@ type token struct {
sourceFile string
}
func (t *token) MarshalJSON() ([]byte, error) {
return json.Marshal(t.value)
}
func (t *token) Value() any {
return t.value
}
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !windows
// +build !windows
package logger
+1 -1
View File
@@ -10,7 +10,7 @@ The script `runTestsOnTravis.sh` will run a given job based on the definition fo
As for the naming convention:
- All JetStream tests name should start with `TestJetStream`
- 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`
+183 -24
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2024 The NATS Authors
// 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
@@ -61,6 +61,7 @@ type Account struct {
sqmu sync.Mutex
sl *Sublist
ic *client
sq *sendq
isid uint64
etmr *time.Timer
ctmr *time.Timer
@@ -97,6 +98,12 @@ type Account struct {
nameTag string
lastLimErr int64
routePoolIdx int
// If the trace destination is specified and a message with a traceParentHdr
// is received, and has the least significant bit of the last token set to 1,
// then if traceDestSampling is > 0 and < 100, a random value will be selected
// and if it falls between 0 and that value, message tracing will be triggered.
traceDest string
traceDestSampling int
// Guarantee that only one goroutine can be running either checkJetStreamMigrate
// or clearObserverState at a given time for this account to prevent interleaving.
jscmMu sync.Mutex
@@ -132,6 +139,10 @@ type streamImport struct {
claim *jwt.Import
usePub bool
invalid bool
// This is `allow_trace` and when true and message tracing is happening,
// we will trace egresses past the account boundary, if `false`, we stop
// at the account boundary.
atrc bool
}
const ClientInfoHdr = "Nats-Request-Info"
@@ -156,6 +167,7 @@ type serviceImport struct {
share bool
tracking bool
didDeliver bool
atrc bool // allow trace (got from service export)
trackingHdr http.Header // header from request
}
@@ -213,6 +225,11 @@ type serviceExport struct {
latency *serviceLatency
rtmr *time.Timer
respThresh time.Duration
// This is `allow_trace` and when true and message tracing is happening,
// when processing a service import we will go through account boundary
// and trace egresses on that other account. If `false`, we stop at the
// account boundary.
atrc bool
}
// Used to track service latency.
@@ -250,11 +267,29 @@ func (a *Account) String() string {
return a.Name
}
func (a *Account) setTraceDest(dest string) {
a.mu.Lock()
a.traceDest = dest
a.mu.Unlock()
}
func (a *Account) getTraceDestAndSampling() (string, int) {
a.mu.RLock()
dest := a.traceDest
sampling := a.traceDestSampling
a.mu.RUnlock()
return dest, sampling
}
// Used to create shallow copies of accounts for transfer
// from opts to real accounts in server struct.
// Account `na` write lock is expected to be held on entry
// while account `a` is the one from the Options struct
// being loaded/reloaded and do not need locking.
func (a *Account) shallowCopy(na *Account) {
na.Nkey = a.Nkey
na.Issuer = a.Issuer
na.traceDest, na.traceDestSampling = a.traceDest, a.traceDestSampling
if a.imports.streams != nil {
na.imports.streams = make([]*streamImport, 0, len(a.imports.streams))
@@ -425,6 +460,29 @@ func (a *Account) GetName() string {
return name
}
// getNameTag will return the name tag or the account name if not set.
func (a *Account) getNameTag() string {
if a == nil {
return _EMPTY_
}
a.mu.RLock()
defer a.mu.RUnlock()
return a.getNameTagLocked()
}
// getNameTagLocked will return the name tag or the account name if not set.
// Lock should be held.
func (a *Account) getNameTagLocked() string {
if a == nil {
return _EMPTY_
}
nameTag := a.nameTag
if nameTag == _EMPTY_ {
nameTag = a.Name
}
return nameTag
}
// NumConnections returns active number of clients for this account for
// all known servers.
func (a *Account) NumConnections() int {
@@ -623,7 +681,7 @@ func (a *Account) AddWeightedMappings(src string, dests ...*MapDest) error {
if tw[d.Cluster] > 100 {
return fmt.Errorf("total weight needs to be <= 100")
}
err := ValidateMappingDestination(d.Subject)
err := ValidateMapping(src, d.Subject)
if err != nil {
return err
}
@@ -1905,11 +1963,13 @@ func (a *Account) addServiceImport(dest *Account, from, to string, claim *jwt.Im
return nil, ErrMissingAccount
}
var atrc bool
dest.mu.RLock()
se := dest.getServiceExport(to)
if se != nil {
rt = se.respType
lat = se.latency
atrc = se.atrc
}
dest.mu.RUnlock()
@@ -1954,7 +2014,7 @@ func (a *Account) addServiceImport(dest *Account, from, to string, claim *jwt.Im
if claim != nil {
share = claim.Share
}
si := &serviceImport{dest, claim, se, nil, from, to, tr, 0, rt, lat, nil, nil, usePub, false, false, share, false, false, nil}
si := &serviceImport{dest, claim, se, nil, from, to, tr, 0, rt, lat, nil, nil, usePub, false, false, share, false, false, atrc, nil}
a.imports.services[from] = si
a.mu.Unlock()
@@ -2178,9 +2238,15 @@ func shouldSample(l *serviceLatency, c *client) (bool, http.Header) {
}
return true, http.Header{trcB3: b3} // sampling allowed or left to recipient of header
} else if tId := h[trcCtx]; len(tId) != 0 {
var sample bool
// sample 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tk := strings.Split(tId[0], "-")
if len(tk) == 4 && len([]byte(tk[3])) == 2 && tk[3] == "01" {
if len(tk) == 4 && len([]byte(tk[3])) == 2 {
if hexVal, err := strconv.ParseInt(tk[3], 16, 8); err == nil {
sample = hexVal&0x1 == 0x1
}
}
if sample {
return true, newTraceCtxHeader(h, tId)
} else {
return false, nil
@@ -2392,6 +2458,18 @@ func (a *Account) SetServiceExportResponseThreshold(export string, maxTime time.
return nil
}
func (a *Account) SetServiceExportAllowTrace(export string, allowTrace bool) error {
a.mu.Lock()
se := a.getServiceExport(export)
if se == nil {
a.mu.Unlock()
return fmt.Errorf("no export defined for %q", export)
}
se.atrc = allowTrace
a.mu.Unlock()
return nil
}
// This is for internal service import responses.
func (a *Account) addRespServiceImport(dest *Account, to string, osi *serviceImport, tracking bool, header http.Header) *serviceImport {
nrr := string(osi.acc.newServiceReply(tracking))
@@ -2401,7 +2479,7 @@ func (a *Account) addRespServiceImport(dest *Account, to string, osi *serviceImp
// dest is the requestor's account. a is the service responder with the export.
// Marked as internal here, that is how we distinguish.
si := &serviceImport{dest, nil, osi.se, nil, nrr, to, nil, 0, rt, nil, nil, nil, false, true, false, osi.share, false, false, nil}
si := &serviceImport{dest, nil, osi.se, nil, nrr, to, nil, 0, rt, nil, nil, nil, false, true, false, osi.share, false, false, false, nil}
if a.exports.responses == nil {
a.exports.responses = make(map[string]*serviceImport)
@@ -2430,6 +2508,10 @@ func (a *Account) addRespServiceImport(dest *Account, to string, osi *serviceImp
// AddStreamImportWithClaim will add in the stream import from a specific account with optional token.
func (a *Account) AddStreamImportWithClaim(account *Account, from, prefix string, imClaim *jwt.Import) error {
return a.addStreamImportWithClaim(account, from, prefix, false, imClaim)
}
func (a *Account) addStreamImportWithClaim(account *Account, from, prefix string, allowTrace bool, imClaim *jwt.Import) error {
if account == nil {
return ErrMissingAccount
}
@@ -2452,7 +2534,7 @@ func (a *Account) AddStreamImportWithClaim(account *Account, from, prefix string
}
}
return a.AddMappedStreamImportWithClaim(account, from, prefix+from, imClaim)
return a.addMappedStreamImportWithClaim(account, from, prefix+from, allowTrace, imClaim)
}
// AddMappedStreamImport helper for AddMappedStreamImportWithClaim
@@ -2462,6 +2544,10 @@ func (a *Account) AddMappedStreamImport(account *Account, from, to string) error
// AddMappedStreamImportWithClaim will add in the stream import from a specific account with optional token.
func (a *Account) AddMappedStreamImportWithClaim(account *Account, from, to string, imClaim *jwt.Import) error {
return a.addMappedStreamImportWithClaim(account, from, to, false, imClaim)
}
func (a *Account) addMappedStreamImportWithClaim(account *Account, from, to string, allowTrace bool, imClaim *jwt.Import) error {
if account == nil {
return ErrMissingAccount
}
@@ -2507,7 +2593,10 @@ func (a *Account) AddMappedStreamImportWithClaim(account *Account, from, to stri
a.mu.Unlock()
return ErrStreamImportDuplicate
}
a.imports.streams = append(a.imports.streams, &streamImport{account, from, to, tr, nil, imClaim, usePub, false})
if imClaim != nil {
allowTrace = imClaim.AllowTrace
}
a.imports.streams = append(a.imports.streams, &streamImport{account, from, to, tr, nil, imClaim, usePub, false, allowTrace})
a.mu.Unlock()
return nil
}
@@ -2525,7 +2614,7 @@ func (a *Account) isStreamImportDuplicate(acc *Account, from string) bool {
// AddStreamImport will add in the stream import from a specific account.
func (a *Account) AddStreamImport(account *Account, from, prefix string) error {
return a.AddStreamImportWithClaim(account, from, prefix, nil)
return a.addStreamImportWithClaim(account, from, prefix, false, nil)
}
// IsPublicExport is a placeholder to denote a public export.
@@ -2844,7 +2933,9 @@ func (a *Account) checkStreamImportsEqual(b *Account) bool {
bm[bim.acc.Name+bim.from+bim.to] = bim
}
for _, aim := range a.imports.streams {
if _, ok := bm[aim.acc.Name+aim.from+aim.to]; !ok {
if bim, ok := bm[aim.acc.Name+aim.from+aim.to]; !ok {
return false
} else if aim.atrc != bim.atrc {
return false
}
}
@@ -2930,6 +3021,9 @@ func isServiceExportEqual(a, b *serviceExport) bool {
return false
}
}
if a.atrc != b.atrc {
return false
}
return true
}
@@ -3205,6 +3299,19 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
// Grab trace label under lock.
tl := a.traceLabel()
var td string
var tds int
if ac.Trace != nil {
// Update trace destination and sampling
td, tds = string(ac.Trace.Destination), ac.Trace.Sampling
if !IsValidPublishSubject(td) {
td, tds = _EMPTY_, 0
} else if tds <= 0 || tds > 100 {
tds = 100
}
}
a.traceDest, a.traceDestSampling = td, tds
// Check for external authorization.
if ac.HasExternalAuthorization() {
a.extAuth = &jwt.ExternalAuthorization{}
@@ -3333,6 +3440,9 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
s.Debugf("Error adding service export response threshold for [%s]: %v", tl, err)
}
}
if err := a.SetServiceExportAllowTrace(sub, e.AllowTrace); err != nil {
s.Debugf("Error adding allow_trace for %q: %v", sub, err)
}
}
var revocationChanged *bool
@@ -3470,10 +3580,15 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
if si != nil && si.acc.Name == a.Name {
// Check for if we are still authorized for an import.
si.invalid = !a.checkServiceImportAuthorized(acc, si.to, si.claim)
if si.latency != nil && !si.response {
// Make sure we should still be tracking latency.
// Make sure we should still be tracking latency and if we
// are allowed to trace.
if !si.response {
if se := a.getServiceExport(si.to); se != nil {
si.latency = se.latency
if si.latency != nil {
si.latency = se.latency
}
// Update allow trace.
si.atrc = se.atrc
}
}
}
@@ -3567,6 +3682,7 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
a.updated = time.Now()
clients := a.getClientsLocked()
ajs := a.js
a.mu.Unlock()
// Sort if we are over the limit.
@@ -3591,6 +3707,26 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim
a.enableAllJetStreamServiceImportsAndMappings()
}
if ajs != nil {
// Check whether the account NRG status changed. If it has then we need to notify the
// Raft groups running on the system so that they can move their subs if needed.
a.mu.Lock()
previous := ajs.nrgAccount
switch ac.ClusterTraffic {
case "system", _EMPTY_:
ajs.nrgAccount = _EMPTY_
case "owner":
ajs.nrgAccount = a.Name
default:
s.Errorf("Account claim for %q has invalid value %q for cluster traffic account", a.Name, ac.ClusterTraffic)
}
changed := ajs.nrgAccount != previous
a.mu.Unlock()
if changed {
s.updateNRGAccountStatus()
}
}
for i, c := range clients {
a.mu.RLock()
exceeded := a.mconns != jwt.NoLimit && i >= int(a.mconns)
@@ -3906,6 +4042,25 @@ func (dr *DirAccResolver) Reload() error {
return dr.DirJWTStore.Reload()
}
// ServerAPIClaimUpdateResponse is the response to $SYS.REQ.ACCOUNT.<id>.CLAIMS.UPDATE and $SYS.REQ.CLAIMS.UPDATE
type ServerAPIClaimUpdateResponse struct {
Server *ServerInfo `json:"server"`
Data *ClaimUpdateStatus `json:"data,omitempty"`
Error *ClaimUpdateError `json:"error,omitempty"`
}
type ClaimUpdateError struct {
Account string `json:"account,omitempty"`
Code int `json:"code"`
Description string `json:"description,omitempty"`
}
type ClaimUpdateStatus struct {
Account string `json:"account,omitempty"`
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
func respondToUpdate(s *Server, respSubj string, acc string, message string, err error) {
if err == nil {
if acc == _EMPTY_ {
@@ -3923,22 +4078,26 @@ func respondToUpdate(s *Server, respSubj string, acc string, message string, err
if respSubj == _EMPTY_ {
return
}
server := &ServerInfo{}
response := map[string]interface{}{"server": server}
m := map[string]interface{}{}
if acc != _EMPTY_ {
m["account"] = acc
response := ServerAPIClaimUpdateResponse{
Server: &ServerInfo{},
}
if err == nil {
m["code"] = http.StatusOK
m["message"] = message
response["data"] = m
response.Data = &ClaimUpdateStatus{
Account: acc,
Code: http.StatusOK,
Message: message,
}
} else {
m["code"] = http.StatusInternalServerError
m["description"] = fmt.Sprintf("%s - %v", message, err)
response["error"] = m
response.Error = &ClaimUpdateError{
Account: acc,
Code: http.StatusInternalServerError,
Description: fmt.Sprintf("%s - %v", message, err),
}
}
s.sendInternalMsgLocked(respSubj, _EMPTY_, server, response)
s.sendInternalMsgLocked(respSubj, _EMPTY_, response.Server, response)
}
func handleListRequest(store *DirJWTStore, s *Server, reply string) {
+106 -81
View File
@@ -417,6 +417,10 @@ func (c *client) matchesPinnedCert(tlsPinnedCerts PinnedCertSet) bool {
return true
}
var (
mustacheRE = regexp.MustCompile(`{{2}([^}]+)}{2}`)
)
func processUserPermissionsTemplate(lim jwt.UserPermissionLimits, ujwt *jwt.UserClaims, acc *Account) (jwt.UserPermissionLimits, error) {
nArrayCartesianProduct := func(a ...[]string) [][]string {
c := 1
@@ -448,16 +452,26 @@ func processUserPermissionsTemplate(lim jwt.UserPermissionLimits, ujwt *jwt.User
}
return p
}
isTag := func(op string) []string {
if strings.EqualFold("tag(", op[:4]) && strings.HasSuffix(op, ")") {
v := strings.TrimPrefix(op, "tag(")
v = strings.TrimSuffix(v, ")")
return []string{"tag", v}
} else if strings.EqualFold("account-tag(", op[:12]) && strings.HasSuffix(op, ")") {
v := strings.TrimPrefix(op, "account-tag(")
v = strings.TrimSuffix(v, ")")
return []string{"account-tag", v}
}
return nil
}
applyTemplate := func(list jwt.StringList, failOnBadSubject bool) (jwt.StringList, error) {
found := false
FOR_FIND:
for i := 0; i < len(list); i++ {
// check if templates are present
for _, tk := range strings.Split(list[i], tsep) {
if strings.HasPrefix(tk, "{{") && strings.HasSuffix(tk, "}}") {
found = true
break FOR_FIND
}
if mustacheRE.MatchString(list[i]) {
found = true
break FOR_FIND
}
}
if !found {
@@ -466,94 +480,78 @@ func processUserPermissionsTemplate(lim jwt.UserPermissionLimits, ujwt *jwt.User
// process the templates
emittedList := make([]string, 0, len(list))
for i := 0; i < len(list); i++ {
tokens := strings.Split(list[i], tsep)
newTokens := make([]string, len(tokens))
tagValues := [][]string{}
// find all the templates {{}} in this acl
tokens := mustacheRE.FindAllString(list[i], -1)
srcs := make([]string, len(tokens))
values := make([][]string, len(tokens))
hasTags := false
for tokenNum, tk := range tokens {
if strings.HasPrefix(tk, "{{") && strings.HasSuffix(tk, "}}") {
op := strings.ToLower(strings.TrimSuffix(strings.TrimPrefix(tk, "{{"), "}}"))
switch {
case op == "name()":
tk = ujwt.Name
case op == "subject()":
tk = ujwt.Subject
case op == "account-name()":
srcs[tokenNum] = tk
op := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(tk, "{{"), "}}"))
if strings.EqualFold("name()", op) {
values[tokenNum] = []string{ujwt.Name}
} else if strings.EqualFold("subject()", op) {
values[tokenNum] = []string{ujwt.Subject}
} else if strings.EqualFold("account-name()", op) {
acc.mu.RLock()
values[tokenNum] = []string{acc.nameTag}
acc.mu.RUnlock()
} else if strings.EqualFold("account-subject()", op) {
// this always has an issuer account since this is a scoped signer
values[tokenNum] = []string{ujwt.IssuerAccount}
} else if isTag(op) != nil {
hasTags = true
match := isTag(op)
var tags jwt.TagList
if match[0] == "account-tag" {
acc.mu.RLock()
name := acc.nameTag
tags = acc.tags
acc.mu.RUnlock()
tk = name
case op == "account-subject()":
tk = ujwt.IssuerAccount
case (strings.HasPrefix(op, "tag(") || strings.HasPrefix(op, "account-tag(")) &&
strings.HasSuffix(op, ")"):
// insert dummy tav value that will throw of subject validation (in case nothing is found)
tk = _EMPTY_
// collect list of matching tag values
var tags jwt.TagList
var tagPrefix string
if strings.HasPrefix(op, "account-tag(") {
acc.mu.RLock()
tags = acc.tags
acc.mu.RUnlock()
tagPrefix = fmt.Sprintf("%s:", strings.ToLower(
strings.TrimSuffix(strings.TrimPrefix(op, "account-tag("), ")")))
} else {
tags = ujwt.Tags
tagPrefix = fmt.Sprintf("%s:", strings.ToLower(
strings.TrimSuffix(strings.TrimPrefix(op, "tag("), ")")))
}
valueList := []string{}
for _, tag := range tags {
if strings.HasPrefix(tag, tagPrefix) {
tagValue := strings.TrimPrefix(tag, tagPrefix)
valueList = append(valueList, tagValue)
}
}
if len(valueList) != 0 {
tagValues = append(tagValues, valueList)
}
default:
// if macro is not recognized, throw off subject check on purpose
tk = " "
} else {
tags = ujwt.Tags
}
tagPrefix := fmt.Sprintf("%s:", strings.ToLower(match[1]))
var valueList []string
for _, tag := range tags {
if strings.HasPrefix(tag, tagPrefix) {
tagValue := strings.TrimPrefix(tag, tagPrefix)
valueList = append(valueList, tagValue)
}
}
if len(valueList) != 0 {
values[tokenNum] = valueList
} else if failOnBadSubject {
return nil, fmt.Errorf("generated invalid subject %q: %q is not defined", list[i], match[1])
} else {
// generate an invalid subject?
values[tokenNum] = []string{" "}
}
} else if failOnBadSubject {
return nil, fmt.Errorf("template operation in %q: %q is not defined", list[i], op)
}
newTokens[tokenNum] = tk
}
// fill in tag value placeholders
if len(tagValues) == 0 {
emitSubj := strings.Join(newTokens, tsep)
if IsValidSubject(emitSubj) {
emittedList = append(emittedList, emitSubj)
if !hasTags {
subj := list[i]
for idx, m := range srcs {
subj = strings.Replace(subj, m, values[idx][0], -1)
}
if IsValidSubject(subj) {
emittedList = append(emittedList, subj)
} else if failOnBadSubject {
return nil, fmt.Errorf("generated invalid subject")
}
// else skip emitting
} else {
// compute the cartesian product and compute subject to emit for each combination
for _, valueList := range nArrayCartesianProduct(tagValues...) {
b := strings.Builder{}
for i, token := range newTokens {
if token == _EMPTY_ && len(valueList) > 0 {
b.WriteString(valueList[0])
valueList = valueList[1:]
} else {
b.WriteString(token)
}
if i != len(newTokens)-1 {
b.WriteString(tsep)
}
a := nArrayCartesianProduct(values...)
for _, aa := range a {
subj := list[i]
for j := 0; j < len(srcs); j++ {
subj = strings.Replace(subj, srcs[j], aa[j], -1)
}
emitSubj := b.String()
if IsValidSubject(emitSubj) {
emittedList = append(emittedList, emitSubj)
if IsValidSubject(subj) {
emittedList = append(emittedList, subj)
} else if failOnBadSubject {
return nil, fmt.Errorf("generated invalid subject")
}
// else skip emitting
}
}
}
@@ -606,13 +604,39 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
}
return
}
// We have a juc defined here, check account.
// We have a juc, check if externally managed, i.e. should be delegated
// to the auth callout service.
if juc != nil && !acc.hasExternalAuth() {
if !authorized {
s.sendAccountAuthErrorEvent(c, c.acc, reason)
}
return
}
// Check config-mode. The global account is a condition since users that
// are not found in the config are implicitly bound to the global account.
// This means those users should be implicitly delegated to auth callout
// if configured. Exclude LEAF connections from this check.
if c.kind != LEAF && juc == nil && opts.AuthCallout != nil && c.acc.Name != globalAccountName {
// If no allowed accounts are defined, then all accounts are in scope.
// Otherwise see if the account is in the list.
delegated := len(opts.AuthCallout.AllowedAccounts) == 0
if !delegated {
for _, n := range opts.AuthCallout.AllowedAccounts {
if n == c.acc.Name {
delegated = true
break
}
}
}
// Not delegated, so return with previous authorized result.
if !delegated {
if !authorized {
s.sendAccountAuthErrorEvent(c, c.acc, reason)
}
return
}
}
// We have auth callout set here.
var skip bool
@@ -1471,7 +1495,8 @@ func validateAllowedConnectionTypes(m map[string]struct{}) error {
switch ctuc {
case jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket,
jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeLeafnodeWS,
jwt.ConnectionTypeMqtt, jwt.ConnectionTypeMqttWS:
jwt.ConnectionTypeMqtt, jwt.ConnectionTypeMqttWS,
jwt.ConnectionTypeInProcess:
default:
return fmt.Errorf("unknown connection type %q", ct)
}
+294 -44
View File
@@ -144,6 +144,7 @@ const (
connectProcessFinished // Marks if this connection has finished the connect process.
compressionNegotiated // Marks if this connection has negotiated compression level with remote.
didTLSFirst // Marks if this connection requested and was accepted doing the TLS handshake first (prior to INFO).
isSlowConsumer // Marks connection as a slow consumer.
)
// set the flag (would be equivalent to set the boolean to true)
@@ -283,6 +284,7 @@ type client struct {
trace bool
echo bool
noIcb bool
iproc bool // In-Process connection, set at creation and immutable.
tags jwt.TagList
nameTag string
@@ -1703,9 +1705,11 @@ func (c *client) flushOutbound() bool {
}
// Ignore ErrShortWrite errors, they will be handled as partials.
var gotWriteTimeout bool
if err != nil && err != io.ErrShortWrite {
// Handle timeout error (slow consumer) differently
if ne, ok := err.(net.Error); ok && ne.Timeout() {
gotWriteTimeout = true
if closed := c.handleWriteTimeout(n, attempted, len(orig)); closed {
return true
}
@@ -1743,6 +1747,11 @@ func (c *client) flushOutbound() bool {
close(c.out.stc)
c.out.stc = nil
}
// Check if the connection is recovering from being a slow consumer.
if !gotWriteTimeout && c.flags.isSet(isSlowConsumer) {
c.Noticef("Slow Consumer Recovered: Flush took %.3fs with %d chunks of %d total bytes.", time.Since(start).Seconds(), len(orig), attempted)
c.flags.clear(isSlowConsumer)
}
return true
}
@@ -1768,6 +1777,11 @@ func (c *client) handleWriteTimeout(written, attempted int64, numChunks int) boo
c.markConnAsClosed(SlowConsumerWriteDeadline)
return true
}
alreadySC := c.flags.isSet(isSlowConsumer)
scState := "Detected"
if alreadySC {
scState = "State"
}
// Aggregate slow consumers.
atomic.AddInt64(&c.srv.slowConsumers, 1)
@@ -1775,7 +1789,10 @@ func (c *client) handleWriteTimeout(written, attempted int64, numChunks int) boo
case CLIENT:
c.srv.scStats.clients.Add(1)
case ROUTER:
c.srv.scStats.routes.Add(1)
// Only count each Slow Consumer event once.
if !alreadySC {
c.srv.scStats.routes.Add(1)
}
case GATEWAY:
c.srv.scStats.gateways.Add(1)
case LEAF:
@@ -1784,13 +1801,15 @@ func (c *client) handleWriteTimeout(written, attempted int64, numChunks int) boo
if c.acc != nil {
atomic.AddInt64(&c.acc.slowConsumers, 1)
}
c.Noticef("Slow Consumer Detected: WriteDeadline of %v exceeded with %d chunks of %d total bytes.",
c.out.wdl, numChunks, attempted)
c.Noticef("Slow Consumer %s: WriteDeadline of %v exceeded with %d chunks of %d total bytes.",
scState, c.out.wdl, numChunks, attempted)
// We always close CLIENT connections, or when nothing was written at all...
if c.kind == CLIENT || written == 0 {
c.markConnAsClosed(SlowConsumerWriteDeadline)
return true
} else {
c.flags.setIfNotSet(isSlowConsumer)
}
return false
}
@@ -2064,10 +2083,26 @@ func (c *client) processConnect(arg []byte) error {
}
}
// If websocket client and JWT not in the CONNECT, use the cookie JWT (possibly empty).
if ws := c.ws; ws != nil && c.opts.JWT == "" {
c.opts.JWT = ws.cookieJwt
// if websocket client, maybe some options through cookies
if ws := c.ws; ws != nil {
// if JWT not in the CONNECT, use the cookie JWT (possibly empty).
if c.opts.JWT == _EMPTY_ {
c.opts.JWT = ws.cookieJwt
}
// if user not in the CONNECT, use the cookie user (possibly empty)
if c.opts.Username == _EMPTY_ {
c.opts.Username = ws.cookieUsername
}
// if pass not in the CONNECT, use the cookie password (possibly empty).
if c.opts.Password == _EMPTY_ {
c.opts.Password = ws.cookiePassword
}
// if token not in the CONNECT, use the cookie token (possibly empty).
if c.opts.Token == _EMPTY_ {
c.opts.Token = ws.cookieToken
}
}
// when not in operator mode, discard the jwt
if srv != nil && srv.trustedKeys == nil {
c.opts.JWT = _EMPTY_
@@ -2526,7 +2561,7 @@ func (c *client) msgParts(data []byte) (hdr []byte, msg []byte) {
}
// Header pubs take form HPUB <subject> [reply] <hdr_len> <total_len>\r\n
func (c *client) processHeaderPub(arg []byte) error {
func (c *client) processHeaderPub(arg, remaining []byte) error {
if !c.headers {
return ErrMsgHeadersNotSupported
}
@@ -2584,6 +2619,16 @@ func (c *client) processHeaderPub(arg []byte) error {
maxPayload := atomic.LoadInt32(&c.mpay)
// Use int64() to avoid int32 overrun...
if maxPayload != jwt.NoLimit && int64(c.pa.size) > int64(maxPayload) {
// If we are given the remaining read buffer (since we do blind reads
// we may have the beginning of the message header/payload), we will
// look for the tracing header and if found, we will generate a
// trace event with the max payload ingress error.
// Do this only for CLIENT connections.
if c.kind == CLIENT && len(remaining) > 0 {
if td := getHeader(MsgTraceDest, remaining); len(td) > 0 {
c.initAndSendIngressErrEvent(remaining, string(td), ErrMaxPayload)
}
}
c.maxPayloadViolation(c.pa.size, maxPayload)
return ErrMaxPayload
}
@@ -3386,23 +3431,33 @@ var needFlush = struct{}{}
// deliverMsg will deliver a message to a matching subscription and its underlying client.
// We process all connection/client types. mh is the part that will be protocol/client specific.
func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, subject, reply, mh, msg []byte, gwrply bool) bool {
// Check if message tracing is enabled.
mt, traceOnly := c.isMsgTraceEnabled()
client := sub.client
// Check sub client and check echo. Only do this if not a service import.
if sub.client == nil || (c == sub.client && !sub.client.echo && !sub.si) {
if client == nil || (c == client && !client.echo && !sub.si) {
if client != nil && mt != nil {
client.mu.Lock()
mt.addEgressEvent(client, sub, errMsgTraceNoEcho)
client.mu.Unlock()
}
return false
}
client := sub.client
client.mu.Lock()
// Check if we have a subscribe deny clause. This will trigger us to check the subject
// for a match against the denied subjects.
if client.mperms != nil && client.checkDenySub(string(subject)) {
mt.addEgressEvent(client, sub, errMsgTraceSubDeny)
client.mu.Unlock()
return false
}
// New race detector forces this now.
if sub.isClosed() {
mt.addEgressEvent(client, sub, errMsgTraceSubClosed)
client.mu.Unlock()
return false
}
@@ -3410,15 +3465,56 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su
// Check if we are a leafnode and have perms to check.
if client.kind == LEAF && client.perms != nil {
if !client.pubAllowedFullCheck(string(subject), true, true) {
mt.addEgressEvent(client, sub, errMsgTracePubViolation)
client.mu.Unlock()
client.Debugf("Not permitted to deliver to %q", subject)
return false
}
}
var mtErr string
if mt != nil {
// For non internal subscription, and if the remote does not support
// the tracing feature...
if sub.icb == nil && !client.msgTraceSupport() {
if traceOnly {
// We are not sending the message at all because the user
// expects a trace-only and the remote does not support
// tracing, which means that it would process/deliver this
// message, which may break applications.
// Add the Egress with the no-support error message.
mt.addEgressEvent(client, sub, errMsgTraceOnlyNoSupport)
client.mu.Unlock()
return false
}
// If we are doing delivery, we will still forward the message,
// but we add an error to the Egress event to hint that one should
// not expect a tracing event from that remote.
mtErr = errMsgTraceNoSupport
}
// For ROUTER, GATEWAY and LEAF, even if we intend to do tracing only,
// we will still deliver the message. The remote side will
// generate an event based on what happened on that server.
if traceOnly && (client.kind == ROUTER || client.kind == GATEWAY || client.kind == LEAF) {
traceOnly = false
}
// If we skip delivery and this is not for a service import, we are done.
if traceOnly && (sub.icb == nil || c.noIcb) {
mt.addEgressEvent(client, sub, _EMPTY_)
client.mu.Unlock()
// Although the message is not actually delivered, for the
// purpose of "didDeliver", we need to return "true" here.
return true
}
}
srv := client.srv
sub.nm++
// We don't want to bump the number of delivered messages to the subscription
// if we are doing trace-only (since really we are not sending it to the sub).
if !traceOnly {
sub.nm++
}
// Check if we should auto-unsubscribe.
if sub.max > 0 {
@@ -3442,6 +3538,7 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su
defer client.unsubscribe(client.acc, sub, true, true)
} else if sub.nm > sub.max {
client.Debugf("Auto-unsubscribe limit [%d] exceeded", sub.max)
mt.addEgressEvent(client, sub, errMsgTraceAutoSubExceeded)
client.mu.Unlock()
client.unsubscribe(client.acc, sub, true, true)
if shouldForward {
@@ -3472,7 +3569,7 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su
// We do not update the outbound stats if we are doing trace only since
// this message will not be sent out.
// Also do not update on internal callbacks.
if sub.icb == nil {
if !traceOnly && sub.icb == nil {
// No atomic needed since accessed under client lock.
// Monitor is reading those also under client's lock.
client.outMsgs++
@@ -3514,6 +3611,7 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su
// with a limit.
if c.kind == CLIENT && client.out.stc != nil {
if srv.getOpts().NoFastProducerStall {
mt.addEgressEvent(client, sub, errMsgTraceFastProdNoStall)
client.mu.Unlock()
return false
}
@@ -3522,10 +3620,17 @@ func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, su
// Check for closed connection
if client.isClosed() {
mt.addEgressEvent(client, sub, errMsgTraceClientClosed)
client.mu.Unlock()
return false
}
// We have passed cases where we could possibly fail to deliver.
// Do not call for service-import.
if mt != nil && sub.icb == nil {
mt.addEgressEvent(client, sub, mtErr)
}
// Do a fast check here to see if we should be tracking this from a latency
// perspective. This will be for a request being received for an exported service.
// This needs to be from a non-client (otherwise tracking happens at requestor).
@@ -3715,24 +3820,34 @@ func (c *client) pruneDenyCache() {
// prunePubPermsCache will prune the cache via randomly
// deleting items. Doing so pruneSize items at a time.
func (c *client) prunePubPermsCache() {
// There is a case where we can invoke this from multiple go routines,
// (in deliverMsg() if sub.client is a LEAF), so we make sure to prune
// from only one go routine at a time.
if !atomic.CompareAndSwapInt32(&c.perms.prun, 0, 1) {
return
}
const maxPruneAtOnce = 1000
r := 0
c.perms.pcache.Range(func(k, _ any) bool {
c.perms.pcache.Delete(k)
if r++; (r > pruneSize && atomic.LoadInt32(&c.perms.pcsz) < int32(maxPermCacheSize)) ||
(r > maxPruneAtOnce) {
return false
// With parallel additions to the cache, it is possible that this function
// would not be able to reduce the cache to its max size in one go. We
// will try a few times but will release/reacquire the "lock" at each
// attempt to give a chance to another go routine to take over and not
// have this go routine do too many attempts.
for i := 0; i < 5; i++ {
// There is a case where we can invoke this from multiple go routines,
// (in deliverMsg() if sub.client is a LEAF), so we make sure to prune
// from only one go routine at a time.
if !atomic.CompareAndSwapInt32(&c.perms.prun, 0, 1) {
return
}
return true
})
atomic.AddInt32(&c.perms.pcsz, -int32(r))
atomic.StoreInt32(&c.perms.prun, 0)
const maxPruneAtOnce = 1000
r := 0
c.perms.pcache.Range(func(k, _ any) bool {
c.perms.pcache.Delete(k)
if r++; (r > pruneSize && atomic.LoadInt32(&c.perms.pcsz) < int32(maxPermCacheSize)) ||
(r > maxPruneAtOnce) {
return false
}
return true
})
n := atomic.AddInt32(&c.perms.pcsz, -int32(r))
atomic.StoreInt32(&c.perms.prun, 0)
if n <= int32(maxPermCacheSize) {
return
}
}
}
// pubAllowed checks on publish permissioning.
@@ -3841,6 +3956,10 @@ func (c *client) selectMappedSubject() bool {
return changed
}
// clientNRGPrefix is used in processInboundClientMsg to detect if publishes
// are being made from normal clients to NRG subjects.
var clientNRGPrefix = []byte("$NRG.")
// processInboundClientMsg is called to process an inbound msg from a client.
// Return if the message was delivered, and if the message was not delivered
// due to a permission issue.
@@ -3873,6 +3992,13 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
}
c.mu.Unlock()
// Check if the client is trying to publish to reserved NRG subjects.
// Doesn't apply to NRGs themselves as they use SYSTEM-kind clients instead.
if c.kind == CLIENT && bytes.HasPrefix(c.pa.subject, clientNRGPrefix) && acc != c.srv.SystemAccount() {
c.pubPermissionViolation(c.pa.subject)
return false, true
}
// Now check for reserved replies. These are used for service imports.
if c.kind == CLIENT && len(c.pa.reply) > 0 && isReservedReply(c.pa.reply) {
c.replySubjectViolation(c.pa.reply)
@@ -4244,6 +4370,7 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
}
}
siAcc := si.acc
allowTrace := si.atrc
acc.mu.RUnlock()
// We have a special case where JetStream pulls in all service imports through one export.
@@ -4254,6 +4381,8 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
return false
}
mt, traceOnly := c.isMsgTraceEnabled()
var nrr []byte
var rsi *serviceImport
@@ -4382,17 +4511,42 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
var lrts [routeTargetInit]routeTarget
c.in.rts = lrts[:0]
var skipProcessing bool
// If message tracing enabled, add the service import trace.
if mt != nil {
mt.addServiceImportEvent(siAcc.GetName(), string(pacopy.subject), to)
// If we are not allowing tracing and doing trace only, we stop at this level.
if !allowTrace {
if traceOnly {
skipProcessing = true
} else {
// We are going to do normal processing, and possibly chainning
// with other server imports, but the rest won't be traced.
// We do so by setting the c.pa.trace to nil (it will be restored
// with c.pa = pacopy).
c.pa.trace = nil
// We also need to disable the message trace headers so that
// if the message is routed, it does not initialize tracing in the
// remote.
positions := disableTraceHeaders(c, msg)
defer enableTraceHeaders(msg, positions)
}
}
}
var didDeliver bool
// If this is not a gateway connection but gateway is enabled,
// try to send this converted message to all gateways.
if c.srv.gateway.enabled {
flags |= pmrCollectQueueNames
var queues [][]byte
didDeliver, queues = c.processMsgResults(siAcc, rr, msg, c.pa.deliver, []byte(to), nrr, flags)
didDeliver = c.sendMsgToGateways(siAcc, msg, []byte(to), nrr, queues, false) || didDeliver
} else {
didDeliver, _ = c.processMsgResults(siAcc, rr, msg, c.pa.deliver, []byte(to), nrr, flags)
if !skipProcessing {
// If this is not a gateway connection but gateway is enabled,
// try to send this converted message to all gateways.
if c.srv.gateway.enabled {
flags |= pmrCollectQueueNames
var queues [][]byte
didDeliver, queues = c.processMsgResults(siAcc, rr, msg, c.pa.deliver, []byte(to), nrr, flags)
didDeliver = c.sendMsgToGateways(siAcc, msg, []byte(to), nrr, queues, false) || didDeliver
} else {
didDeliver, _ = c.processMsgResults(siAcc, rr, msg, c.pa.deliver, []byte(to), nrr, flags)
}
}
// Restore to original values.
@@ -4403,6 +4557,12 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
// If we override due to tracing and traceOnly we do not want to send back a no responders.
c.pa.delivered = didDeliver
// If this was a message trace but we skip last-mile delivery, we need to
// do the remove, so:
if mt != nil && traceOnly && didDeliver {
didDeliver = false
}
// Determine if we should remove this service import. This is for response service imports.
// We will remove if we did not deliver, or if we are a response service import and we are
// a singleton, or we have an EOF message.
@@ -4551,6 +4711,8 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
}
}
mt, traceOnly := c.isMsgTraceEnabled()
// Loop over all normal subscriptions that match.
for _, sub := range r.psubs {
// Check if this is a send to a ROUTER. We now process
@@ -4579,6 +4741,11 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
// Assume delivery subject is the normal subject to this point.
dsubj = subj
// We may need to disable tracing, by setting c.pa.trace to `nil`
// before the call to deliverMsg, if so, this will indicate that
// we need to put it back.
var restorePaTrace bool
// Check for stream import mapped subs (shadow subs). These apply to local subs only.
if sub.im != nil {
// If this message was a service import do not re-export to an exported stream.
@@ -4594,6 +4761,25 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
dsubj = append(_dsubj[:0], sub.im.to...)
}
if mt != nil {
mt.addStreamExportEvent(sub.client, dsubj)
// If allow_trace is false...
if !sub.im.atrc {
// If we are doing only message tracing, we can move to the
// next sub.
if traceOnly {
// Although the message was not delivered, for the purpose
// of didDeliver, we need to set to true (to avoid possible
// no responders).
didDeliver = true
continue
}
// If we are delivering the message, we need to disable tracing
// before calling deliverMsg().
c.pa.trace, restorePaTrace = nil, true
}
}
// Make sure deliver is set if inbound from a route.
if remapped && (c.kind == GATEWAY || c.kind == ROUTER || c.kind == LEAF) {
deliver = subj
@@ -4620,6 +4806,9 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
}
didDeliver = true
}
if restorePaTrace {
c.pa.trace = mt
}
}
// Set these up to optionally filter based on the queue lists.
@@ -4774,6 +4963,13 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
// Assume delivery subject is normal subject to this point.
dsubj = subj
// We may need to disable tracing, by setting c.pa.trace to `nil`
// before the call to deliverMsg, if so, this will indicate that
// we need to put it back.
var restorePaTrace bool
var skipDelivery bool
// Check for stream import mapped subs. These apply to local subs only.
if sub.im != nil {
// If this message was a service import do not re-export to an exported stream.
@@ -4788,6 +4984,23 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
} else {
dsubj = append(_dsubj[:0], sub.im.to...)
}
if mt != nil {
mt.addStreamExportEvent(sub.client, dsubj)
// If allow_trace is false...
if !sub.im.atrc {
// If we are doing only message tracing, we are done
// with this queue group.
if traceOnly {
skipDelivery = true
} else {
// If we are delivering, we need to disable tracing
// before the call to deliverMsg()
c.pa.trace, restorePaTrace = nil, true
}
}
}
// Make sure deliver is set if inbound from a route.
if remapped && (c.kind == GATEWAY || c.kind == ROUTER || c.kind == LEAF) {
deliver = subj
@@ -4800,11 +5013,20 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
}
}
mh := c.msgHeader(dsubj, creply, sub)
if c.deliverMsg(prodIsMQTT, sub, acc, subject, creply, mh, msg, rplyHasGWPrefix) {
if sub.icb == nil {
var delivered bool
if !skipDelivery {
mh := c.msgHeader(dsubj, creply, sub)
delivered = c.deliverMsg(prodIsMQTT, sub, acc, subject, creply, mh, msg, rplyHasGWPrefix)
if restorePaTrace {
c.pa.trace = mt
}
}
if skipDelivery || delivered {
// Update only if not skipped.
if !skipDelivery && sub.icb == nil {
dlvMsgs++
}
// Do the rest even when message delivery was skipped.
didDeliver = true
// Clear rsub
rsub = nil
@@ -4845,6 +5067,16 @@ sendToRoutesOrLeafs:
// Copy off original pa in case it changes.
pa := c.pa
if mt != nil {
// We are going to replace "pa" with our copy of c.pa, but to restore
// to the original copy of c.pa, we need to save it again.
cpa := pa
msg = mt.setOriginAccountHeaderIfNeeded(c, acc, msg)
defer func() { c.pa = cpa }()
// Update pa with our current c.pa state.
pa = c.pa
}
// We address by index to avoid struct copy.
// We have inline structs for memory layout and cache coherency.
for i := range c.in.rts {
@@ -4874,6 +5106,11 @@ sendToRoutesOrLeafs:
}
}
if mt != nil {
dmsg = mt.setHopHeader(c, dmsg)
hset = true
}
mh := c.msgHeaderForRouteOrLeaf(subject, reply, rt, acc)
if c.deliverMsg(prodIsMQTT, rt.sub, acc, subject, reply, mh, dmsg, false) {
if rt.sub.icb == nil {
@@ -4920,7 +5157,11 @@ func (c *client) checkLeafClientInfoHeader(msg []byte) (dmsg []byte, setHdr bool
}
func (c *client) pubPermissionViolation(subject []byte) {
c.sendErr(fmt.Sprintf("Permissions Violation for Publish to %q", subject))
errTxt := fmt.Sprintf("Permissions Violation for Publish to %q", subject)
if mt, _ := c.isMsgTraceEnabled(); mt != nil {
mt.setIngressError(errTxt)
}
c.sendErr(errTxt)
c.Errorf("Publish Violation - %s, Subject %q", c.getAuthUser(), subject)
}
@@ -4940,7 +5181,11 @@ func (c *client) subPermissionViolation(sub *subscription) {
}
func (c *client) replySubjectViolation(reply []byte) {
c.sendErr(fmt.Sprintf("Permissions Violation for Publish with Reply of %q", reply))
errTxt := fmt.Sprintf("Permissions Violation for Publish with Reply of %q", reply)
if mt, _ := c.isMsgTraceEnabled(); mt != nil {
mt.setIngressError(errTxt)
}
c.sendErr(errTxt)
c.Errorf("Publish Violation - %s, Reply %q", c.getAuthUser(), reply)
}
@@ -5874,7 +6119,8 @@ func convertAllowedConnectionTypes(cts []string) (map[string]struct{}, error) {
switch i {
case jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket,
jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeLeafnodeWS,
jwt.ConnectionTypeMqtt, jwt.ConnectionTypeMqttWS:
jwt.ConnectionTypeMqtt, jwt.ConnectionTypeMqttWS,
jwt.ConnectionTypeInProcess:
m[i] = struct{}{}
default:
unknown = append(unknown, i)
@@ -5901,7 +6147,11 @@ func (c *client) connectionTypeAllowed(acts map[string]struct{}) bool {
case CLIENT:
switch c.clientType() {
case NATS:
want = jwt.ConnectionTypeStandard
if c.iproc {
want = jwt.ConnectionTypeInProcess
} else {
want = jwt.ConnectionTypeStandard
}
case WS:
want = jwt.ConnectionTypeWebsocket
case MQTT:
+5 -2
View File
@@ -1,4 +1,4 @@
// Copyright 2012-2025 The NATS Authors
// Copyright 2012-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
@@ -14,6 +14,7 @@
package server
import (
"regexp"
"runtime/debug"
"time"
)
@@ -38,6 +39,8 @@ var (
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-]+)*))?$`)
)
func init() {
@@ -55,7 +58,7 @@ func init() {
const (
// VERSION is the current version for the server.
VERSION = "2.10.26"
VERSION = "2.11.0"
// PROTO is the currently supported protocol.
// 0 was the original
+528 -74
View File
@@ -21,6 +21,7 @@ import (
"fmt"
"math/rand"
"reflect"
"regexp"
"slices"
"strconv"
"strings"
@@ -37,6 +38,12 @@ import (
const (
JSPullRequestPendingMsgs = "Nats-Pending-Messages"
JSPullRequestPendingBytes = "Nats-Pending-Bytes"
JSPullRequestWrongPinID = "NATS/1.0 423 Nats-Wrong-Pin-Id\r\n\r\n"
JSPullRequestNatsPinId = "Nats-Pin-Id"
)
var (
validGroupName = regexp.MustCompile(`^[a-zA-Z0-9/_=-]{1,16}$`)
)
// Headers sent when batch size was completed, but there were remaining bytes.
@@ -55,8 +62,17 @@ type ConsumerInfo struct {
NumPending uint64 `json:"num_pending"`
Cluster *ClusterInfo `json:"cluster,omitempty"`
PushBound bool `json:"push_bound,omitempty"`
Paused bool `json:"paused,omitempty"`
PauseRemaining time.Duration `json:"pause_remaining,omitempty"`
// TimeStamp indicates when the info was gathered
TimeStamp time.Time `json:"ts"`
TimeStamp time.Time `json:"ts"`
PriorityGroups []PriorityGroupState `json:"priority_groups,omitempty"`
}
type PriorityGroupState struct {
Group string `json:"group"`
PinnedClientID string `json:"pinned_client_id,omitempty"`
PinnedTS time.Time `json:"pinned_ts,omitempty"`
}
type ConsumerConfig struct {
@@ -77,7 +93,6 @@ type ConsumerConfig struct {
SampleFrequency string `json:"sample_freq,omitempty"`
MaxWaiting int `json:"max_waiting,omitempty"`
MaxAckPending int `json:"max_ack_pending,omitempty"`
Heartbeat time.Duration `json:"idle_heartbeat,omitempty"`
FlowControl bool `json:"flow_control,omitempty"`
HeadersOnly bool `json:"headers_only,omitempty"`
@@ -87,8 +102,9 @@ type ConsumerConfig struct {
MaxRequestMaxBytes int `json:"max_bytes,omitempty"`
// Push based consumers.
DeliverSubject string `json:"deliver_subject,omitempty"`
DeliverGroup string `json:"deliver_group,omitempty"`
DeliverSubject string `json:"deliver_subject,omitempty"`
DeliverGroup string `json:"deliver_group,omitempty"`
Heartbeat time.Duration `json:"idle_heartbeat,omitempty"`
// Ephemeral inactivity threshold.
InactiveThreshold time.Duration `json:"inactive_threshold,omitempty"`
@@ -103,6 +119,14 @@ type ConsumerConfig struct {
// Metadata is additional metadata for the Consumer.
Metadata map[string]string `json:"metadata,omitempty"`
// PauseUntil is for suspending the consumer until the deadline.
PauseUntil *time.Time `json:"pause_until,omitempty"`
// Priority groups
PriorityGroups []string `json:"priority_groups,omitempty"`
PriorityPolicy PriorityPolicy `json:"priority_policy,omitempty"`
PinnedTTL time.Duration `json:"priority_timeout,omitempty"`
}
// SequenceInfo has both the consumer and the stream sequence and last activity.
@@ -113,9 +137,10 @@ type SequenceInfo struct {
}
type CreateConsumerRequest struct {
Stream string `json:"stream_name"`
Config ConsumerConfig `json:"config"`
Action ConsumerAction `json:"action"`
Stream string `json:"stream_name"`
Config ConsumerConfig `json:"config"`
Action ConsumerAction `json:"action"`
Pedantic bool `json:"pedantic,omitempty"`
}
type ConsumerAction int
@@ -182,6 +207,68 @@ type ConsumerNakOptions struct {
Delay time.Duration `json:"delay"`
}
// PriorityPolicy determines policy for selecting messages based on priority.
type PriorityPolicy int
const (
// No priority policy.
PriorityNone PriorityPolicy = iota
// Clients will get the messages only if certain criteria are specified.
PriorityOverflow
// Single client takes over handling of the messages, while others are on standby.
PriorityPinnedClient
)
const (
PriorityNoneJSONString = `"none"`
PriorityOverflowJSONString = `"overflow"`
PriorityPinnedClientJSONString = `"pinned_client"`
)
var (
PriorityNoneJSONBytes = []byte(PriorityNoneJSONString)
PriorityOverflowJSONBytes = []byte(PriorityOverflowJSONString)
PriorityPinnedClientJSONBytes = []byte(PriorityPinnedClientJSONString)
)
func (pp PriorityPolicy) String() string {
switch pp {
case PriorityOverflow:
return PriorityOverflowJSONString
case PriorityPinnedClient:
return PriorityPinnedClientJSONString
default:
return PriorityNoneJSONString
}
}
func (pp PriorityPolicy) MarshalJSON() ([]byte, error) {
switch pp {
case PriorityOverflow:
return PriorityOverflowJSONBytes, nil
case PriorityPinnedClient:
return PriorityPinnedClientJSONBytes, nil
case PriorityNone:
return PriorityNoneJSONBytes, nil
default:
return nil, fmt.Errorf("unknown priority policy: %v", pp)
}
}
func (pp *PriorityPolicy) UnmarshalJSON(data []byte) error {
switch string(data) {
case PriorityOverflowJSONString:
*pp = PriorityOverflow
case PriorityPinnedClientJSONString:
*pp = PriorityPinnedClient
case PriorityNoneJSONString:
*pp = PriorityNone
default:
return fmt.Errorf("unknown priority policy: %v", string(data))
}
return nil
}
// DeliverPolicy determines how the consumer should select the first message to deliver.
type DeliverPolicy int
@@ -357,11 +444,12 @@ type consumer struct {
active bool
replay bool
dtmr *time.Timer
uptmr *time.Timer // Unpause timer
gwdtmr *time.Timer
dthresh time.Duration
mch chan struct{}
qch chan struct{}
inch chan bool
mch chan struct{} // Message channel
qch chan struct{} // Quit channel
inch chan bool // Interest change channel
sfreq int32
ackEventT string
nakEventT string
@@ -395,6 +483,17 @@ type consumer struct {
// for stream signaling when multiple filters are set.
sigSubs []string
// Priority groups
// Details described in ADR-42.
// currentPinId is the current nuid for the pinned consumer.
// If the Consumer is running in `PriorityPinnedClient` mode, server will
// pick up a new nuid and assign it to first pending pull request.
currentPinId string
/// pinnedTtl is the remaining time before the current PinId expires.
pinnedTtl *time.Timer
pinnedTS time.Time
}
// A single subject filter.
@@ -431,10 +530,13 @@ const (
JsFlowControlMaxPending = 32 * 1024 * 1024
// JsDefaultMaxAckPending is set for consumers with explicit ack that do not set the max ack pending.
JsDefaultMaxAckPending = 1000
// JsDefaultPinnedTTL is the default grace period for the pinned consumer to send a new request before a new pin
// is picked by a server.
JsDefaultPinnedTTL = 2 * time.Minute
)
// Helper function to set consumer config defaults from above.
func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig, lim *JSLimitOpts, accLim *JetStreamAccountLimits) {
func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig, lim *JSLimitOpts, accLim *JetStreamAccountLimits, pedantic bool) *ApiError {
// Set to default if not specified.
if config.DeliverSubject == _EMPTY_ && config.MaxWaiting == 0 {
config.MaxWaiting = JSWaitQueueDefaultMax
@@ -449,12 +551,21 @@ func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig,
}
// If BackOff was specified that will override the AckWait and the MaxDeliver.
if len(config.BackOff) > 0 {
if pedantic && config.AckWait != config.BackOff[0] {
return NewJSPedanticError(errors.New("first backoff value has to equal batch AckWait"))
}
config.AckWait = config.BackOff[0]
}
if config.MaxAckPending == 0 {
if pedantic && streamCfg.ConsumerLimits.MaxAckPending > 0 {
return NewJSPedanticError(errors.New("max_ack_pending must be set if it's configured in stream limits"))
}
config.MaxAckPending = streamCfg.ConsumerLimits.MaxAckPending
}
if config.InactiveThreshold == 0 {
if pedantic && streamCfg.ConsumerLimits.InactiveThreshold > 0 {
return NewJSPedanticError(errors.New("inactive_threshold must be set if it's configured in stream limits"))
}
config.InactiveThreshold = streamCfg.ConsumerLimits.InactiveThreshold
}
// Set proper default for max ack pending if we are ack explicit and none has been set.
@@ -470,8 +581,17 @@ func setConsumerConfigDefaults(config *ConsumerConfig, streamCfg *StreamConfig,
}
// if applicable set max request batch size
if config.DeliverSubject == _EMPTY_ && config.MaxRequestBatch == 0 && lim.MaxRequestBatch > 0 {
if pedantic {
return NewJSPedanticError(errors.New("max_request_batch must be set if it's JetStream limits are set"))
}
config.MaxRequestBatch = lim.MaxRequestBatch
}
// set the default value only if pinned policy is used.
if config.PriorityPolicy == PriorityPinnedClient && config.PinnedTTL == 0 {
config.PinnedTTL = JsDefaultPinnedTTL
}
return nil
}
// Check the consumer config. If we are recovering don't check filter subjects.
@@ -696,18 +816,38 @@ func checkConsumerCfg(
return NewJSConsumerMetadataLengthError(fmt.Sprintf("%dKB", JSMaxMetadataLen/1024))
}
if config.PriorityPolicy != PriorityNone {
if len(config.PriorityGroups) == 0 {
return NewJSConsumerPriorityPolicyWithoutGroupError()
}
for _, group := range config.PriorityGroups {
if group == _EMPTY_ {
return NewJSConsumerEmptyGroupNameError()
}
if !validGroupName.MatchString(group) {
return NewJSConsumerInvalidGroupNameError()
}
}
}
// For now don't allow preferred server in placement.
if cfg.Placement != nil && cfg.Placement.Preferred != _EMPTY_ {
return NewJSStreamInvalidConfigError(fmt.Errorf("preferred server not permitted in placement"))
}
return nil
}
func (mset *stream) addConsumerWithAction(config *ConsumerConfig, action ConsumerAction) (*consumer, error) {
return mset.addConsumerWithAssignment(config, _EMPTY_, nil, false, action)
func (mset *stream) addConsumerWithAction(config *ConsumerConfig, action ConsumerAction, pedantic bool) (*consumer, error) {
return mset.addConsumerWithAssignment(config, _EMPTY_, nil, false, action, pedantic)
}
func (mset *stream) addConsumer(config *ConsumerConfig) (*consumer, error) {
return mset.addConsumerWithAction(config, ActionCreateOrUpdate)
return mset.addConsumerWithAction(config, ActionCreateOrUpdate, false)
}
func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname string, ca *consumerAssignment, isRecovering bool, action ConsumerAction) (*consumer, error) {
func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname string, ca *consumerAssignment, isRecovering bool, action ConsumerAction, pedantic bool) (*consumer, error) {
// Check if this stream has closed.
if mset.closed.Load() {
return nil, NewJSStreamInvalidError()
@@ -737,8 +877,11 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
// Make sure we have sane defaults. Do so with the JS lock, otherwise a
// badly timed meta snapshot can result in a race condition.
mset.js.mu.Lock()
setConsumerConfigDefaults(config, &cfg, srvLim, selectedLimits)
err := setConsumerConfigDefaults(config, &cfg, srvLim, selectedLimits, pedantic)
mset.js.mu.Unlock()
if err != nil {
return nil, err
}
if err := checkConsumerCfg(config, srvLim, &cfg, acc, selectedLimits, isRecovering); err != nil {
return nil, err
@@ -1080,6 +1223,34 @@ func (o *consumer) updateInactiveThreshold(cfg *ConsumerConfig) {
}
}
// Updates the paused state. If we are the leader and the pause deadline
// hasn't passed yet then we will start a timer to kick the consumer once
// that deadline is reached. Lock should be held.
func (o *consumer) updatePauseState(cfg *ConsumerConfig) {
if o.uptmr != nil {
stopAndClearTimer(&o.uptmr)
}
if !o.isLeader() {
// Only the leader will run the timer as only the leader will run
// loopAndGatherMsgs.
return
}
if cfg.PauseUntil == nil || cfg.PauseUntil.IsZero() || cfg.PauseUntil.Before(time.Now()) {
// Either the PauseUntil is unset (is effectively zero) or the
// deadline has already passed, in which case there is nothing
// to do.
return
}
o.uptmr = time.AfterFunc(time.Until(*cfg.PauseUntil), func() {
o.mu.Lock()
defer o.mu.Unlock()
stopAndClearTimer(&o.uptmr)
o.sendPauseAdvisoryLocked(&o.cfg)
o.signalNewMessages()
})
}
func (o *consumer) consumerAssignment() *consumerAssignment {
o.mu.RLock()
defer o.mu.RUnlock()
@@ -1202,8 +1373,14 @@ func (o *consumer) setLeader(isLeader bool) {
o.rdq = nil
o.rdqi.Empty()
// Restore our saved state. During non-leader status we just update our underlying store.
o.readStoredState(lseq)
// Restore our saved state.
// During non-leader status we just update our underlying store when not clustered.
// If clustered we need to propose our initial (possibly skipped ahead) o.sseq to the group.
if o.node == nil || o.dseq > 1 || (o.store != nil && o.store.HasState()) {
o.readStoredState(lseq)
} else if o.node != nil && o.sseq >= 1 {
o.updateSkipped(o.sseq)
}
// Setup initial num pending.
o.streamNumPending()
@@ -1213,11 +1390,6 @@ func (o *consumer) setLeader(isLeader bool) {
o.lss = nil
}
// Update the group on the our starting sequence if we are starting but we skipped some in the stream.
if o.dseq == 1 && o.sseq > 1 {
o.updateSkipped(o.sseq)
}
// Do info sub.
if o.infoSub == nil && jsa != nil {
isubj := fmt.Sprintf(clusterConsumerInfoT, jsa.acc(), stream, o.name)
@@ -1277,6 +1449,9 @@ func (o *consumer) setLeader(isLeader bool) {
o.dtmr = time.AfterFunc(o.dthresh, o.deleteNotActive)
}
// Update the consumer pause tracking.
o.updatePauseState(&o.cfg)
// If we are not in ReplayInstant mode mark us as in replay state until resolved.
if o.cfg.ReplayPolicy != ReplayInstant {
o.replay = true
@@ -1347,7 +1522,8 @@ func (o *consumer) setLeader(isLeader bool) {
}
// Stop any inactivity timers. Should only be running on leaders.
stopAndClearTimer(&o.dtmr)
// Stop any unpause timers. Should only be running on leaders.
stopAndClearTimer(&o.uptmr)
// Make sure to clear out any re-deliver queues
o.stopAndClearPtmr()
o.rdq = nil
@@ -1452,6 +1628,45 @@ func (o *consumer) sendDeleteAdvisoryLocked() {
o.sendAdvisory(subj, e)
}
func (o *consumer) sendPinnedAdvisoryLocked(group string) {
e := JSConsumerGroupPinnedAdvisory{
TypedEvent: TypedEvent{
Type: JSConsumerGroupPinnedAdvisoryType,
ID: nuid.Next(),
Time: time.Now().UTC(),
},
Account: o.acc.Name,
Stream: o.stream,
Consumer: o.name,
Domain: o.srv.getOpts().JetStreamDomain,
PinnedClientId: o.currentPinId,
Group: group,
}
subj := JSAdvisoryConsumerPinnedPre + "." + o.stream + "." + o.name
o.sendAdvisory(subj, e)
}
func (o *consumer) sendUnpinnedAdvisoryLocked(group string, reason string) {
e := JSConsumerGroupUnpinnedAdvisory{
TypedEvent: TypedEvent{
Type: JSConsumerGroupUnpinnedAdvisoryType,
ID: nuid.Next(),
Time: time.Now().UTC(),
},
Account: o.acc.Name,
Stream: o.stream,
Consumer: o.name,
Domain: o.srv.getOpts().JetStreamDomain,
Group: group,
Reason: reason,
}
subj := JSAdvisoryConsumerUnpinnedPre + "." + o.stream + "." + o.name
o.sendAdvisory(subj, e)
}
func (o *consumer) sendCreateAdvisory() {
o.mu.Lock()
defer o.mu.Unlock()
@@ -1472,6 +1687,27 @@ func (o *consumer) sendCreateAdvisory() {
o.sendAdvisory(subj, e)
}
func (o *consumer) sendPauseAdvisoryLocked(cfg *ConsumerConfig) {
e := JSConsumerPauseAdvisory{
TypedEvent: TypedEvent{
Type: JSConsumerPauseAdvisoryType,
ID: nuid.Next(),
Time: time.Now().UTC(),
},
Stream: o.stream,
Consumer: o.name,
Domain: o.srv.getOpts().JetStreamDomain,
}
if cfg.PauseUntil != nil {
e.PauseUntil = *cfg.PauseUntil
e.Paused = time.Now().Before(e.PauseUntil)
}
subj := JSAdvisoryConsumerPausePre + "." + o.stream + "." + o.name
o.sendAdvisory(subj, e)
}
// Created returns created time.
func (o *consumer) createdTime() time.Time {
o.mu.Lock()
@@ -1687,8 +1923,8 @@ func (o *consumer) deleteNotActive() {
}
nca := js.consumerAssignment(acc, stream, name)
js.mu.RUnlock()
// Make sure this is not a new consumer with the same name.
if nca != nil && nca == ca {
// Make sure this is the same consumer assignment, and not a new consumer with the same name.
if nca != nil && reflect.DeepEqual(nca, ca) {
s.Warnf("Consumer assignment for '%s > %s > %s' not cleaned up, retrying", acc, stream, name)
meta.ForwardProposal(removeEntry)
if interval < cnaMax {
@@ -1908,6 +2144,12 @@ func (o *consumer) updateConfig(cfg *ConsumerConfig) error {
return err
}
// Make sure we always store PauseUntil in UTC.
if cfg.PauseUntil != nil {
utc := (*cfg.PauseUntil).UTC()
cfg.PauseUntil = &utc
}
if o.store != nil {
// Update local state always.
if err := o.store.UpdateConfig(cfg); err != nil {
@@ -1956,6 +2198,22 @@ func (o *consumer) updateConfig(cfg *ConsumerConfig) error {
o.dtmr = time.AfterFunc(o.dthresh, o.deleteNotActive)
}
}
// Check whether the pause has changed
{
var old, new time.Time
if o.cfg.PauseUntil != nil {
old = *o.cfg.PauseUntil
}
if cfg.PauseUntil != nil {
new = *cfg.PauseUntil
}
if !old.Equal(new) {
o.updatePauseState(cfg)
if o.isLeader() {
o.sendPauseAdvisoryLocked(cfg)
}
}
}
// Check for Subject Filters update.
newSubjects := gatherSubjectFilters(cfg.FilterSubject, cfg.FilterSubjects)
@@ -2179,7 +2437,7 @@ func (o *consumer) loopAndForwardProposals(qch chan struct{}) {
forwardProposals := func() error {
o.mu.Lock()
if o.node == nil || o.node.State() != Leader {
if o.node == nil || !o.node.Leader() {
o.mu.Unlock()
return errors.New("no longer leader")
}
@@ -2559,10 +2817,7 @@ func (o *consumer) applyState(state *ConsumerState) {
return
}
// If o.sseq is greater don't update. Don't go backwards on o.sseq if leader.
if !o.isLeader() || o.sseq <= state.Delivered.Stream {
o.sseq = state.Delivered.Stream + 1
}
o.sseq = state.Delivered.Stream + 1
o.dseq = state.Delivered.Consumer + 1
o.adflr = state.AckFloor.Consumer
o.asflr = state.AckFloor.Stream
@@ -2673,6 +2928,16 @@ func (o *consumer) infoWithSnapAndReply(snap bool, reply string) *ConsumerInfo {
rg = o.ca.Group
}
priorityGroups := []PriorityGroupState{}
// TODO(jrm): when we introduce supporting many priority groups, we need to update assigning `o.currentNuid` for each group.
if len(o.cfg.PriorityGroups) > 0 {
priorityGroups = append(priorityGroups, PriorityGroupState{
Group: o.cfg.PriorityGroups[0],
PinnedClientID: o.currentPinId,
PinnedTS: o.pinnedTS,
})
}
cfg := o.cfg
info := &ConsumerInfo{
Stream: o.stream,
@@ -2692,6 +2957,13 @@ func (o *consumer) infoWithSnapAndReply(snap bool, reply string) *ConsumerInfo {
NumPending: o.checkNumPending(),
PushBound: o.isPushMode() && o.active,
TimeStamp: time.Now().UTC(),
PriorityGroups: priorityGroups,
}
if o.cfg.PauseUntil != nil {
p := *o.cfg.PauseUntil
if info.Paused = time.Now().Before(p); info.Paused {
info.PauseRemaining = time.Until(p)
}
}
// If we are replicated, we need to pull certain data from our store.
@@ -2703,9 +2975,13 @@ func (o *consumer) infoWithSnapAndReply(snap bool, reply string) *ConsumerInfo {
}
// If we are the leader we could have o.sseq that is skipped ahead.
// To maintain consistency in reporting (e.g. jsz) we always take the state for our delivered/ackfloor stream sequence.
info.Delivered.Consumer, info.Delivered.Stream = state.Delivered.Consumer, state.Delivered.Stream
// Only use skipped ahead o.sseq if we're a new consumer and have not yet replicated this state yet.
leader := o.isLeader()
if !leader || o.store.HasState() {
info.Delivered.Consumer, info.Delivered.Stream = state.Delivered.Consumer, state.Delivered.Stream
}
info.AckFloor.Consumer, info.AckFloor.Stream = state.AckFloor.Consumer, state.AckFloor.Stream
if !o.isLeader() {
if !leader {
info.NumAckPending = len(state.Pending)
info.NumRedelivered = len(state.Redelivered)
}
@@ -3054,6 +3330,13 @@ func (o *consumer) needAck(sseq uint64, subj string) bool {
return needAck
}
type PriorityGroup struct {
Group string `json:"group,omitempty"`
MinPending int64 `json:"min_pending,omitempty"`
MinAckPending int64 `json:"min_ack_pending,omitempty"`
Id string `json:"id,omitempty"`
}
// Used in nextReqFromMsg, since the json.Unmarshal causes the request
// struct to escape to the heap always. This should reduce GC pressure.
var jsGetNextPool = sync.Pool{
@@ -3063,12 +3346,12 @@ var jsGetNextPool = sync.Pool{
}
// Helper for the next message requests.
func nextReqFromMsg(msg []byte) (time.Time, int, int, bool, time.Duration, time.Time, error) {
func nextReqFromMsg(msg []byte) (time.Time, int, int, bool, time.Duration, time.Time, *PriorityGroup, error) {
req := bytes.TrimSpace(msg)
switch {
case len(req) == 0:
return time.Time{}, 1, 0, false, 0, time.Time{}, nil
return time.Time{}, 1, 0, false, 0, time.Time{}, nil, nil
case req[0] == '{':
cr := jsGetNextPool.Get().(*JSApiConsumerGetNextRequest)
@@ -3077,42 +3360,44 @@ func nextReqFromMsg(msg []byte) (time.Time, int, int, bool, time.Duration, time.
jsGetNextPool.Put(cr)
}()
if err := json.Unmarshal(req, &cr); err != nil {
return time.Time{}, -1, 0, false, 0, time.Time{}, err
return time.Time{}, -1, 0, false, 0, time.Time{}, nil, err
}
var hbt time.Time
if cr.Heartbeat > 0 {
if cr.Heartbeat*2 > cr.Expires {
return time.Time{}, 1, 0, false, 0, time.Time{}, errors.New("heartbeat value too large")
return time.Time{}, 1, 0, false, 0, time.Time{}, nil, errors.New("heartbeat value too large")
}
hbt = time.Now().Add(cr.Heartbeat)
}
priorityGroup := cr.PriorityGroup
if cr.Expires == time.Duration(0) {
return time.Time{}, cr.Batch, cr.MaxBytes, cr.NoWait, cr.Heartbeat, hbt, nil
return time.Time{}, cr.Batch, cr.MaxBytes, cr.NoWait, cr.Heartbeat, hbt, &priorityGroup, nil
}
return time.Now().Add(cr.Expires), cr.Batch, cr.MaxBytes, cr.NoWait, cr.Heartbeat, hbt, nil
return time.Now().Add(cr.Expires), cr.Batch, cr.MaxBytes, cr.NoWait, cr.Heartbeat, hbt, &priorityGroup, nil
default:
if n, err := strconv.Atoi(string(req)); err == nil {
return time.Time{}, n, 0, false, 0, time.Time{}, nil
return time.Time{}, n, 0, false, 0, time.Time{}, nil, nil
}
}
return time.Time{}, 1, 0, false, 0, time.Time{}, nil
return time.Time{}, 1, 0, false, 0, time.Time{}, nil, nil
}
// Represents a request that is on the internal waiting queue
type waitingRequest struct {
next *waitingRequest
acc *Account
interest string
reply string
n int // For batching
d int // num delivered
b int // For max bytes tracking
expires time.Time
received time.Time
hb time.Duration
hbt time.Time
noWait bool
next *waitingRequest
acc *Account
interest string
reply string
n int // For batching
d int // num delivered
b int // For max bytes tracking
expires time.Time
received time.Time
hb time.Duration
hbt time.Time
noWait bool
priorityGroup *PriorityGroup
}
// sync.Pool for waiting requests.
@@ -3210,6 +3495,16 @@ func (wq *waitQueue) peek() *waitingRequest {
return wq.head
}
func (wq *waitQueue) cycle() {
wr := wq.peek()
if wr != nil {
// Always remove current now on a pop, and move to end if still valid.
// If we were the only one don't need to remove since this can be a no-op.
wq.removeCurrent()
wq.add(wr)
}
}
// pop will return the next request and move the read cursor.
// This will now place a request that still has pending items at the ends of the list.
func (wq *waitQueue) pop() *waitingRequest {
@@ -3271,6 +3566,20 @@ func (o *consumer) pendingRequests() map[string]*waitingRequest {
return m
}
func (o *consumer) setPinnedTimer(priorityGroup string) {
if o.pinnedTtl != nil {
o.pinnedTtl.Reset(o.cfg.PinnedTTL)
} else {
o.pinnedTtl = time.AfterFunc(o.cfg.PinnedTTL, func() {
o.mu.Lock()
o.currentPinId = _EMPTY_
o.sendUnpinnedAdvisoryLocked(priorityGroup, "timeout")
o.mu.Unlock()
o.signalNewMessages()
})
}
}
// Return next waiting request. This will check for expirations but not noWait or interest.
// That will be handled by processWaiting.
// Lock should be held.
@@ -3278,6 +3587,16 @@ func (o *consumer) nextWaiting(sz int) *waitingRequest {
if o.waiting == nil || o.waiting.isEmpty() {
return nil
}
// Check if server needs to assign a new pin id.
needNewPin := o.currentPinId == _EMPTY_ && o.cfg.PriorityPolicy == PriorityPinnedClient
// As long as we support only one priority group, we can capture that group here and reuse it.
var priorityGroup string
if len(o.cfg.PriorityGroups) > 0 {
priorityGroup = o.cfg.PriorityGroups[0]
}
lastRequest := o.waiting.tail
for wr := o.waiting.peek(); !o.waiting.isEmpty(); wr = o.waiting.peek() {
if wr == nil {
break
@@ -3307,11 +3626,73 @@ func (o *consumer) nextWaiting(sz int) *waitingRequest {
}
if wr.expires.IsZero() || time.Now().Before(wr.expires) {
if needNewPin {
if wr.priorityGroup.Id == _EMPTY_ {
o.currentPinId = nuid.Next()
o.pinnedTS = time.Now().UTC()
wr.priorityGroup.Id = o.currentPinId
o.setPinnedTimer(priorityGroup)
} else {
// There is pin id set, but not a matching one. Send a notification to the client and remove the request.
// Probably this is the old pin id.
o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, []byte(JSPullRequestWrongPinID), nil, nil, 0))
o.waiting.removeCurrent()
if o.node != nil {
o.removeClusterPendingRequest(wr.reply)
}
wr.recycle()
continue
}
} else if o.currentPinId != _EMPTY_ {
// Check if we have a match on the currentNuid
if wr.priorityGroup != nil && wr.priorityGroup.Id == o.currentPinId {
// If we have a match, we do nothing here and will deliver the message later down the code path.
} else if wr.priorityGroup.Id == _EMPTY_ {
o.waiting.cycle()
if wr == lastRequest {
return nil
}
continue
} else {
// There is pin id set, but not a matching one. Send a notification to the client and remove the request.
o.outq.send(newJSPubMsg(wr.reply, _EMPTY_, _EMPTY_, []byte(JSPullRequestWrongPinID), nil, nil, 0))
o.waiting.removeCurrent()
if o.node != nil {
o.removeClusterPendingRequest(wr.reply)
}
wr.recycle()
continue
}
}
if o.cfg.PriorityPolicy == PriorityOverflow {
if wr.priorityGroup != nil &&
// We need to check o.npc+1, because before calling nextWaiting, we do o.npc--
(wr.priorityGroup.MinPending > 0 && wr.priorityGroup.MinPending > o.npc+1 ||
wr.priorityGroup.MinAckPending > 0 && wr.priorityGroup.MinAckPending > int64(len(o.pending))) {
o.waiting.cycle()
// We're done cycling through the requests.
if wr == lastRequest {
return nil
}
continue
}
}
if wr.acc.sl.HasInterest(wr.interest) {
if needNewPin {
o.sendPinnedAdvisoryLocked(priorityGroup)
}
return o.waiting.pop()
} else if time.Since(wr.received) < defaultGatewayRecentSubExpiration && (o.srv.leafNodeEnabled || o.srv.gateway.enabled) {
if needNewPin {
o.sendPinnedAdvisoryLocked(priorityGroup)
}
return o.waiting.pop()
} else if o.srv.gateway.enabled && o.srv.hasGatewayInterest(wr.acc.Name, wr.interest) {
if needNewPin {
o.sendPinnedAdvisoryLocked(priorityGroup)
}
return o.waiting.pop()
}
} else {
@@ -3338,6 +3719,7 @@ func (o *consumer) nextWaiting(sz int) *waitingRequest {
}
wr.recycle()
}
return nil
}
@@ -3411,7 +3793,7 @@ func (o *consumer) processNextMsgRequest(reply string, msg []byte) {
}
// Check payload here to see if they sent in batch size or a formal request.
expires, batchSize, maxBytes, noWait, hb, hbt, err := nextReqFromMsg(msg)
expires, batchSize, maxBytes, noWait, hb, hbt, priorityGroup, err := nextReqFromMsg(msg)
if err != nil {
sendErr(400, fmt.Sprintf("Bad Request - %v", err))
return
@@ -3433,6 +3815,44 @@ func (o *consumer) processNextMsgRequest(reply string, msg []byte) {
return
}
if priorityGroup != nil {
if (priorityGroup.MinPending != 0 || priorityGroup.MinAckPending != 0) && o.cfg.PriorityPolicy != PriorityOverflow {
sendErr(400, "Bad Request - Not a Overflow Priority consumer")
}
if priorityGroup.Id != _EMPTY_ && o.cfg.PriorityPolicy != PriorityPinnedClient {
sendErr(400, "Bad Request - Not a Pinned Client Priority consumer")
}
}
if priorityGroup != nil && o.cfg.PriorityPolicy != PriorityNone {
if priorityGroup.Group == _EMPTY_ {
sendErr(400, "Bad Request - Priority Group missing")
return
}
found := false
for _, group := range o.cfg.PriorityGroups {
if group == priorityGroup.Group {
found = true
break
}
}
if !found {
sendErr(400, "Bad Request - Invalid Priority Group")
return
}
if o.currentPinId != _EMPTY_ {
if priorityGroup.Id == o.currentPinId {
o.setPinnedTimer(priorityGroup.Group)
} else if priorityGroup.Id != _EMPTY_ {
sendErr(423, "Nats-Pin-Id mismatch")
return
}
}
}
// If we have the max number of requests already pending try to expire.
if o.waiting.isFull() {
// Try to expire some of the requests.
@@ -3469,7 +3889,7 @@ func (o *consumer) processNextMsgRequest(reply string, msg []byte) {
// Create a waiting request.
wr := wrPool.Get().(*waitingRequest)
wr.acc, wr.interest, wr.reply, wr.n, wr.d, wr.noWait, wr.expires, wr.hb, wr.hbt = acc, interest, reply, batchSize, 0, noWait, expires, hb, hbt
wr.acc, wr.interest, wr.reply, wr.n, wr.d, wr.noWait, wr.expires, wr.hb, wr.hbt, wr.priorityGroup = acc, interest, reply, batchSize, 0, noWait, expires, hb, hbt, priorityGroup
wr.b = maxBytes
wr.received = time.Now()
@@ -3980,6 +4400,8 @@ func (o *consumer) suppressDeletion() {
}
}
// loopAndGatherMsgs waits for messages for the consumer. qch is the quit channel,
// upch is the unpause channel which fires when the PauseUntil deadline is reached.
func (o *consumer) loopAndGatherMsgs(qch chan struct{}) {
// On startup check to see if we are in a reply situation where replay policy is not instant.
var (
@@ -4046,6 +4468,13 @@ func (o *consumer) loopAndGatherMsgs(qch chan struct{}) {
// Clear last error.
err = nil
// If the consumer is paused then stop sending.
if o.cfg.PauseUntil != nil && !o.cfg.PauseUntil.IsZero() && time.Now().Before(*o.cfg.PauseUntil) {
// If the consumer is paused and we haven't reached the deadline yet then
// go back to waiting.
goto waitForMsgs
}
// If we are in push mode and not active or under flowcontrol let's stop sending.
if o.isPushMode() {
if !o.active || (o.maxpb > 0 && o.pbytes > o.maxpb) {
@@ -4106,6 +4535,21 @@ func (o *consumer) loopAndGatherMsgs(qch chan struct{}) {
} else if wr := o.nextWaiting(sz); wr != nil {
wrn, wrb = wr.n, wr.b
dsubj = wr.reply
if o.cfg.PriorityPolicy == PriorityPinnedClient {
// FIXME(jrm): Can we make this prettier?
if len(pmsg.hdr) == 0 {
pmsg.hdr = genHeader(pmsg.hdr, JSPullRequestNatsPinId, o.currentPinId)
pmsg.buf = append(pmsg.hdr, pmsg.msg...)
} else {
pmsg.hdr = genHeader(pmsg.hdr, JSPullRequestNatsPinId, o.currentPinId)
bufLen := len(pmsg.hdr) + len(pmsg.msg)
pmsg.buf = make([]byte, bufLen)
pmsg.buf = append(pmsg.hdr, pmsg.msg...)
}
sz = len(pmsg.subj) + len(ackReply) + len(pmsg.hdr) + len(pmsg.msg)
}
if done := wr.recycleIfDone(); done && o.node != nil {
o.removeClusterPendingRequest(dsubj)
} else if !done && wr.hb > 0 {
@@ -4388,9 +4832,6 @@ func (o *consumer) deliverMsg(dsubj, ackReply string, pmsg *jsPubMsg, dc uint64,
// Update delivered first.
o.updateDelivered(dseq, seq, dc, ts)
// Send message.
o.outq.send(pmsg)
if ap == AckExplicit || ap == AckAll {
o.trackPending(seq, dseq)
} else if ap == AckNone {
@@ -4398,6 +4839,9 @@ func (o *consumer) deliverMsg(dsubj, ackReply string, pmsg *jsPubMsg, dc uint64,
o.asflr = seq
}
// Send message.
o.outq.send(pmsg)
// Flow control.
if o.maxpb > 0 && o.needFlowControl(psz) {
o.sendFlowControl()
@@ -4858,13 +5302,13 @@ func (o *consumer) selectStartingSeqNo() {
} else if o.cfg.DeliverPolicy == DeliverLast {
if o.subjf == nil {
o.sseq = state.LastSeq
return
}
// If we are partitioned here this will be properly set when we become leader.
for _, filter := range o.subjf {
ss := o.mset.store.FilteredState(1, filter.subject)
if ss.Last > o.sseq {
o.sseq = ss.Last
} else {
// If we are partitioned here this will be properly set when we become leader.
for _, filter := range o.subjf {
ss := o.mset.store.FilteredState(1, filter.subject)
if ss.Last > o.sseq {
o.sseq = ss.Last
}
}
}
} else if o.cfg.DeliverPolicy == DeliverLastPerSubject {
@@ -4942,12 +5386,18 @@ func (o *consumer) selectStartingSeqNo() {
o.sseq = o.cfg.OptStartSeq
}
if state.FirstSeq == 0 {
if state.FirstSeq == 0 && (o.cfg.Direct || o.cfg.OptStartSeq == 0) {
// If the stream is empty, deliver only new.
// But only if mirroring/sourcing, or start seq is unset, otherwise need to respect provided value.
o.sseq = 1
} else if o.sseq < state.FirstSeq {
o.sseq = state.FirstSeq
} else if o.sseq > state.LastSeq {
} else if o.sseq > state.LastSeq && (o.cfg.Direct || o.cfg.OptStartSeq == 0) {
// If selected sequence is in the future, clamp back down.
// But only if mirroring/sourcing, or start seq is unset, otherwise need to respect provided value.
o.sseq = state.LastSeq + 1
} else if o.sseq < state.FirstSeq {
// If the first sequence is further ahead than the starting sequence,
// there are no messages there anymore, so move the sequence up.
o.sseq = state.FirstSeq
}
}
@@ -4958,7 +5408,8 @@ func (o *consumer) selectStartingSeqNo() {
// Set ack store floor to store-1
o.asflr = o.sseq - 1
// Set our starting sequence state.
if o.store != nil && o.sseq > 0 {
// But only if we're not clustered, if clustered we propose upon becoming leader.
if o.store != nil && o.sseq > 0 && o.cfg.replicas(&o.mset.cfg) == 1 {
o.store.SetStarting(o.sseq - 1)
}
}
@@ -5182,7 +5633,7 @@ func (o *consumer) stopWithFlags(dflag, sdflag, doSignal, advisory bool) error {
// Check if we are the leader and are being deleted (as a node).
if dflag && o.isLeader() {
// If we are clustered and node leader (probable from above), stepdown.
if node := o.node; node != nil && node.Leader() {
if node := o.node; node != nil {
node.StepDown()
}
@@ -5335,8 +5786,10 @@ func (o *consumer) cleanupNoInterestMessages(mset *stream, ignoreInterest bool)
var rmseqs []uint64
mset.mu.RLock()
// If over this amount of messages to check, defer to checkInterestState() which
// will do the right thing since we are now removed.
// If over this amount of messages to check, optimistically call to checkInterestState().
// It will not always do the right thing in removing messages that lost interest, but ensures
// we don't degrade performance by doing a linear scan through the whole stream.
// Messages might need to expire based on limits to be cleaned up.
// TODO(dlc) - Better way?
const bailThresh = 100_000
@@ -5392,6 +5845,7 @@ func (o *consumer) switchToEphemeral() {
interest := o.acc.sl.HasInterest(o.cfg.DeliverSubject)
// Setup dthresh.
o.updateInactiveThreshold(&o.cfg)
o.updatePauseState(&o.cfg)
o.mu.Unlock()
// Update interest
@@ -5613,7 +6067,7 @@ func (o *consumer) checkStateForInterestStream(ss *StreamState) error {
// Only ack though if no error and seq <= ack floor.
if err == nil && seq <= asflr {
didRemove := mset.ackMsg(o, seq)
// Removing the message could fail. For example if we're behind on stream applies.
// Removing the message could fail. For example if clustered since we need to propose it.
// Overwrite retry floor (only the first time) to allow us to check next time if the removal was successful.
if didRemove && retryAsflr == 0 {
retryAsflr = seq
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !windows && !openbsd && !netbsd && !wasm
// +build !windows,!openbsd,!netbsd,!wasm
package server
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build netbsd
// +build netbsd
package server
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build openbsd
// +build openbsd
package server
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build wasm
// +build wasm
package server
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build windows
// +build windows
package server
+8 -2
View File
@@ -153,6 +153,9 @@ var (
// 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")
@@ -163,6 +166,9 @@ var (
// 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")
@@ -181,7 +187,7 @@ var (
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 or new lines")
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")
@@ -206,7 +212,7 @@ var (
ErrInvalidMappingDestination = errors.New("invalid mapping destination")
// ErrInvalidMappingDestinationSubject is used to error on a bad transform destination mapping
ErrInvalidMappingDestinationSubject = fmt.Errorf("%w: invalid subject", ErrInvalidMappingDestination)
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)
+145 -5
View File
@@ -203,7 +203,7 @@
"constant": "JSInvalidJSONErr",
"code": 400,
"error_code": 10025,
"description": "invalid JSON",
"description": "invalid JSON: {err}",
"comment": "",
"help": "",
"url": "",
@@ -833,7 +833,7 @@
"constant": "JSConsumerPullRequiresAckErr",
"code": 400,
"error_code": 10084,
"description": "consumer in pull mode requires ack policy",
"description": "consumer in pull mode requires explicit ack policy on workqueue stream",
"comment": "",
"help": "",
"url": "",
@@ -1433,7 +1433,7 @@
"constant": "JSSourceInvalidSubjectFilter",
"code": 400,
"error_code": 10145,
"description": "source subject filter is invalid",
"description": "source transform source: {err}",
"comment": "",
"help": "",
"url": "",
@@ -1443,7 +1443,7 @@
"constant": "JSSourceInvalidTransformDestination",
"code": 400,
"error_code": 10146,
"description": "source transform destination is invalid",
"description": "source transform: {err}",
"comment": "",
"help": "",
"url": "",
@@ -1493,7 +1493,7 @@
"constant": "JSMirrorInvalidSubjectFilter",
"code": 400,
"error_code": 10151,
"description": "mirror subject filter is invalid",
"description": "mirror transform source: {err}",
"comment": "",
"help": "",
"url": "",
@@ -1518,5 +1518,145 @@
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSMirrorInvalidTransformDestination",
"code": 400,
"error_code": 10154,
"description": "mirror transform: {err}",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSStreamTransformInvalidSource",
"code": 400,
"error_code": 10155,
"description": "stream transform source: {err}",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSStreamTransformInvalidDestination",
"code": 400,
"error_code": 10156,
"description": "stream transform: {err}",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSPedanticErrF",
"code": 400,
"error_code": 10157,
"description": "pedantic mode: {err}",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSStreamDuplicateMessageConflict",
"code": 409,
"error_code": 10158,
"description": "duplicate message id is in process",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerPriorityPolicyWithoutGroup",
"code": 400,
"error_code": 10159,
"description": "Setting PriorityPolicy requires at least one PriorityGroup to be set",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerInvalidPriorityGroupErr",
"code": 400,
"error_code": 10160,
"description": "Provided priority group does not exist for this consumer",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerEmptyGroupName",
"code": 400,
"error_code": 10161,
"description": "Group name cannot be an empty string",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSConsumerInvalidGroupNameErr",
"code": 400,
"error_code": 10162,
"description": "Valid priority group name must match A-Z, a-z, 0-9, -_/=)+ and may not exceed 16 characters",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSStreamExpectedLastSeqPerSubjectNotReady",
"code": 503,
"error_code": 10163,
"description": "expected last sequence per subject temporarily unavailable",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSStreamWrongLastSequenceConstantErr",
"code": 400,
"error_code": 10164,
"description": "wrong last sequence",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSMessageTTLInvalidErr",
"code": 400,
"error_code": 10165,
"description": "invalid per-message TTL",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSMessageTTLDisabledErr",
"code": 400,
"error_code": 10166,
"description": "per-message TTL is disabled",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
},
{
"constant": "JSStreamTooManyRequests",
"code": 429,
"error_code": 10167,
"description": "too many requests",
"comment": "",
"help": "",
"url": "",
"deprecates": ""
}
]
+82 -24
View File
@@ -31,7 +31,6 @@ import (
"time"
"github.com/klauspost/compress/s2"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nats-server/v2/server/certidp"
"github.com/nats-io/nats-server/v2/server/pse"
@@ -215,6 +214,7 @@ type AccountNumConns struct {
// AccountStat contains the data common between AccountNumConns and AccountStatz
type AccountStat struct {
Account string `json:"acc"`
Name string `json:"name"`
Conns int `json:"conns"`
LeafNodes int `json:"leafnodes"`
TotalConns int `json:"total_conns"`
@@ -264,6 +264,7 @@ type ServerInfo struct {
const (
JetStreamEnabled ServerCapability = 1 << iota // Server had JetStream enabled.
BinaryStreamSnapshot // New stream snapshot capability.
AccountNRG // Move NRG traffic out of system account.
)
// Set JetStream capability.
@@ -289,6 +290,17 @@ func (si *ServerInfo) BinaryStreamSnapshot() bool {
return si.Flags&BinaryStreamSnapshot != 0
}
// Set account NRG capability.
func (si *ServerInfo) SetAccountNRG() {
si.Flags |= AccountNRG
}
// AccountNRG indicates whether or not we support moving the NRG traffic out of the
// system account and into the asset account.
func (si *ServerInfo) AccountNRG() bool {
return si.Flags&AccountNRG != 0
}
// ClientInfo is detailed information about the client forming a connection.
type ClientInfo struct {
Start *time.Time `json:"start,omitempty"`
@@ -348,21 +360,22 @@ func (ci *ClientInfo) forAdvisory() *ClientInfo {
// ServerStats hold various statistics that we will periodically send out.
type ServerStats struct {
Start time.Time `json:"start"`
Mem int64 `json:"mem"`
Cores int `json:"cores"`
CPU float64 `json:"cpu"`
Connections int `json:"connections"`
TotalConnections uint64 `json:"total_connections"`
ActiveAccounts int `json:"active_accounts"`
NumSubs uint32 `json:"subscriptions"`
Sent DataStats `json:"sent"`
Received DataStats `json:"received"`
SlowConsumers int64 `json:"slow_consumers"`
Routes []*RouteStat `json:"routes,omitempty"`
Gateways []*GatewayStat `json:"gateways,omitempty"`
ActiveServers int `json:"active_servers,omitempty"`
JetStream *JetStreamVarz `json:"jetstream,omitempty"`
Start time.Time `json:"start"`
Mem int64 `json:"mem"`
Cores int `json:"cores"`
CPU float64 `json:"cpu"`
Connections int `json:"connections"`
TotalConnections uint64 `json:"total_connections"`
ActiveAccounts int `json:"active_accounts"`
NumSubs uint32 `json:"subscriptions"`
Sent DataStats `json:"sent"`
Received DataStats `json:"received"`
SlowConsumers int64 `json:"slow_consumers"`
SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats,omitempty"`
Routes []*RouteStat `json:"routes,omitempty"`
Gateways []*GatewayStat `json:"gateways,omitempty"`
ActiveServers int `json:"active_servers,omitempty"`
JetStream *JetStreamVarz `json:"jetstream,omitempty"`
}
// RouteStat holds route statistics.
@@ -506,10 +519,14 @@ RESET:
si.Version = VERSION
si.Time = time.Now().UTC()
si.Tags = tags
si.Flags = 0
if js {
// New capability based flags.
si.SetJetStreamEnabled()
si.SetBinaryStreamSnapshot()
if s.accountNRGAllowed.Load() {
si.SetAccountNRG()
}
}
}
var b []byte
@@ -684,7 +701,7 @@ func (s *Server) sendInternalAccountMsgWithReply(a *Account, subject, reply stri
}
// Send system style message to an account scope.
func (s *Server) sendInternalAccountSysMsg(a *Account, subj string, si *ServerInfo, msg interface{}) {
func (s *Server) sendInternalAccountSysMsg(a *Account, subj string, si *ServerInfo, msg any, ct compressionType) {
s.mu.RLock()
if s.sys == nil || s.sys.sendq == nil || a == nil {
s.mu.RUnlock()
@@ -697,7 +714,7 @@ func (s *Server) sendInternalAccountSysMsg(a *Account, subj string, si *ServerIn
c := a.internalClient()
a.mu.Unlock()
sendq.push(newPubMsg(c, subj, _EMPTY_, si, nil, msg, noCompression, false, false))
sendq.push(newPubMsg(c, subj, _EMPTY_, si, nil, msg, ct, false, false))
}
// This will queue up a message to be sent.
@@ -895,6 +912,16 @@ func (s *Server) sendStatsz(subj string) {
m.Stats.Sent.Msgs = atomic.LoadInt64(&s.outMsgs)
m.Stats.Sent.Bytes = atomic.LoadInt64(&s.outBytes)
m.Stats.SlowConsumers = atomic.LoadInt64(&s.slowConsumers)
// Evaluate the slow consumer stats, but set it only if one of the value is not 0.
scs := &SlowConsumersStats{
Clients: s.NumSlowConsumersClients(),
Routes: s.NumSlowConsumersRoutes(),
Gateways: s.NumSlowConsumersGateways(),
Leafs: s.NumSlowConsumersLeafs(),
}
if scs.Clients != 0 || scs.Routes != 0 || scs.Gateways != 0 || scs.Leafs != 0 {
m.Stats.SlowConsumersStats = scs
}
m.Stats.NumSubs = s.numSubscriptions()
// Routes
s.forEachRoute(func(r *client) {
@@ -980,6 +1007,7 @@ func (s *Server) sendStatsz(subj string) {
jStat.Meta.Pending = ipq.len()
}
}
jStat.Limits = &s.getOpts().JetStreamLimits
m.Stats.JetStream = jStat
s.mu.RLock()
}
@@ -1657,7 +1685,8 @@ func (s *Server) remoteServerUpdate(sub *subscription, c *client, _ *Account, su
}
node := getHash(si.Name)
s.nodeToInfo.Store(node, nodeInfo{
accountNRG := si.AccountNRG()
oldInfo, _ := s.nodeToInfo.Swap(node, nodeInfo{
si.Name,
si.Version,
si.Cluster,
@@ -1669,7 +1698,14 @@ func (s *Server) remoteServerUpdate(sub *subscription, c *client, _ *Account, su
false,
si.JetStreamEnabled(),
si.BinaryStreamSnapshot(),
accountNRG,
})
if oldInfo == nil || accountNRG != oldInfo.(nodeInfo).accountNRG {
// One of the servers we received statsz from changed its mind about
// whether or not it supports in-account NRG, so update the groups
// with this information.
s.updateNRGAccountStatus()
}
}
// updateRemoteServer is called when we have an update from a remote server.
@@ -1716,14 +1752,35 @@ func (s *Server) processNewServer(si *ServerInfo) {
false,
si.JetStreamEnabled(),
si.BinaryStreamSnapshot(),
si.AccountNRG(),
})
}
}
go s.updateNRGAccountStatus()
// Announce ourselves..
// Do this in a separate Go routine.
go s.sendStatszUpdate()
}
// Works out whether all nodes support moving the NRG traffic into
// the account and moves it appropriately.
// Server lock MUST NOT be held on entry.
func (s *Server) updateNRGAccountStatus() {
s.rnMu.RLock()
raftNodes := make([]RaftNode, 0, len(s.raftNodes))
for _, n := range s.raftNodes {
raftNodes = append(raftNodes, n)
}
s.rnMu.RUnlock()
for _, n := range raftNodes {
// In the event that the node is happy that all nodes that
// it cares about haven't changed, this will be a no-op.
if err := n.RecreateInternalSubs(); err != nil {
n.Stop()
}
}
}
// If GW is enabled on this server and there are any leaf node connections,
// this function will send a LeafNode connect system event to the super cluster
// to ensure that the GWs are in interest-only mode for this account.
@@ -2338,6 +2395,7 @@ func (a *Account) statz() *AccountStat {
leafConns := a.numLocalLeafNodes()
return &AccountStat{
Account: a.Name,
Name: a.getNameTagLocked(),
Conns: localConns,
LeafNodes: leafConns,
TotalConns: localConns + leafConns,
@@ -2408,7 +2466,7 @@ func (s *Server) accountConnectEvent(c *client) {
Jwt: c.opts.JWT,
IssuerKey: issuerForClient(c),
Tags: c.tags,
NameTag: c.nameTag,
NameTag: c.acc.getNameTag(),
Kind: c.kindString(),
ClientType: c.clientTypeString(),
MQTTClient: c.getMQTTClientID(),
@@ -2460,7 +2518,7 @@ func (s *Server) accountDisconnectEvent(c *client, now time.Time, reason string)
Jwt: c.opts.JWT,
IssuerKey: issuerForClient(c),
Tags: c.tags,
NameTag: c.nameTag,
NameTag: c.acc.getNameTag(),
Kind: c.kindString(),
ClientType: c.clientTypeString(),
MQTTClient: c.getMQTTClientID(),
@@ -2514,7 +2572,7 @@ func (s *Server) sendAuthErrorEvent(c *client) {
Jwt: c.opts.JWT,
IssuerKey: issuerForClient(c),
Tags: c.tags,
NameTag: c.nameTag,
NameTag: c.acc.getNameTag(),
Kind: c.kindString(),
ClientType: c.clientTypeString(),
MQTTClient: c.getMQTTClientID(),
@@ -2572,7 +2630,7 @@ func (s *Server) sendAccountAuthErrorEvent(c *client, acc *Account, reason strin
Jwt: c.opts.JWT,
IssuerKey: issuerForClient(c),
Tags: c.tags,
NameTag: c.nameTag,
NameTag: c.acc.getNameTag(),
Kind: c.kindString(),
ClientType: c.clientTypeString(),
MQTTClient: c.getMQTTClientID(),
@@ -2589,7 +2647,7 @@ func (s *Server) sendAccountAuthErrorEvent(c *client, acc *Account, reason strin
}
c.mu.Unlock()
s.sendInternalAccountSysMsg(acc, authErrorAccountEventSubj, &m.Server, &m)
s.sendInternalAccountSysMsg(acc, authErrorAccountEventSubj, &m.Server, &m, noCompression)
}
// Internal message callback.
File diff suppressed because it is too large Load Diff
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build gofuzz
// +build gofuzz
package server
+30 -5
View File
@@ -19,12 +19,14 @@ import (
"crypto/sha256"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"math/rand"
"net"
"net/url"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
@@ -217,6 +219,8 @@ type gateway struct {
// interest-only mode "immediately", so the outbound should disregard
// the optimistic mode when checking for interest.
interestOnlyMode bool
// Name of the remote server
remoteName string
}
// Outbound subject interest entry.
@@ -298,17 +302,20 @@ func (r *RemoteGatewayOpts) clone() *RemoteGatewayOpts {
// Ensure that gateway is properly configured.
func validateGatewayOptions(o *Options) error {
if o.Gateway.Name == "" && o.Gateway.Port == 0 {
if o.Gateway.Name == _EMPTY_ && o.Gateway.Port == 0 {
return nil
}
if o.Gateway.Name == "" {
return fmt.Errorf("gateway has no name")
if o.Gateway.Name == _EMPTY_ {
return errors.New("gateway has no name")
}
if strings.Contains(o.Gateway.Name, " ") {
return ErrGatewayNameHasSpaces
}
if o.Gateway.Port == 0 {
return fmt.Errorf("gateway %q has no port specified (select -1 for random port)", o.Gateway.Name)
}
for i, g := range o.Gateway.Gateways {
if g.Name == "" {
if g.Name == _EMPTY_ {
return fmt.Errorf("gateway in the list %d has no name", i)
}
if len(g.URLs) == 0 {
@@ -528,6 +535,7 @@ func (s *Server) startGatewayAcceptLoop() {
Gateway: opts.Gateway.Name,
GatewayNRP: true,
Headers: s.supportsHeaders(),
Proto: s.getServerProto(),
}
// Unless in some tests we want to keep the old behavior, we are now
// (since v2.9.0) indicate that this server will switch all accounts
@@ -1035,6 +1043,10 @@ func (c *client) processGatewayInfo(info *Info) {
}
if isFirstINFO {
c.opts.Name = info.ID
// Get the protocol version from the INFO protocol. This will be checked
// to see if this connection supports message tracing for instance.
c.opts.Protocol = info.Proto
c.gw.remoteName = info.Name
}
c.mu.Unlock()
@@ -2400,7 +2412,7 @@ func (s *Server) gatewayUpdateSubInterest(accName string, sub *subscription, cha
if change < 0 {
return
}
entry = &sitally{n: 1, q: sub.queue != nil}
entry = &sitally{n: change, q: sub.queue != nil}
st[string(key)] = entry
first = true
} else {
@@ -2528,6 +2540,14 @@ func (c *client) sendMsgToGateways(acc *Account, msg, subject, reply []byte, qgr
if len(gws) == 0 {
return false
}
mt, _ := c.isMsgTraceEnabled()
if mt != nil {
pa := c.pa
msg = mt.setOriginAccountHeaderIfNeeded(c, acc, msg)
defer func() { c.pa = pa }()
}
var (
queuesa = [512]byte{}
queues = queuesa[:0]
@@ -2635,6 +2655,11 @@ func (c *client) sendMsgToGateways(acc *Account, msg, subject, reply []byte, qgr
mreply = append(mreply, reply...)
}
}
if mt != nil {
msg = mt.setHopHeader(c, msg)
}
// Setup the message header.
// Make sure we are an 'R' proto by default
c.msgb[0] = 'R'
+110 -57
View File
@@ -14,6 +14,7 @@
package server
import (
"errors"
"sync"
"sync/atomic"
)
@@ -28,36 +29,79 @@ type ipQueue[T any] struct {
elts []T
pos int
pool *sync.Pool
mrs int
sz uint64 // Calculated size (only if calc != nil)
name string
m *sync.Map
ipQueueOpts[T]
}
type ipQueueOpts struct {
maxRecycleSize int
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 func(*ipQueueOpts)
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 ipQueue_MaxRecycleSize(max int) ipQueueOpt {
return func(o *ipQueueOpts) {
o.maxRecycleSize = max
func ipqMaxRecycleSize[T any](max int) ipQueueOpt[T] {
return func(o *ipQueueOpts[T]) {
o.mrs = max
}
}
func newIPQueue[T any](s *Server, name string, opts ...ipQueueOpt) *ipQueue[T] {
qo := ipQueueOpts{maxRecycleSize: ipQueueDefaultMaxRecycleSize}
for _, o := range opts {
o(&qo)
// 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),
mrs: qo.maxRecycleSize,
pool: &sync.Pool{},
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
@@ -66,32 +110,34 @@ func newIPQueue[T any](s *Server, name string, opts ...ipQueueOpt) *ipQueue[T] {
// 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 {
var signal bool
func (q *ipQueue[T]) push(e T) (int, error) {
q.Lock()
l := len(q.elts) - q.pos
if l == 0 {
signal = true
eltsi := q.pool.Get()
if eltsi != nil {
// Reason we use pointer to slice instead of slice is explained
// here: https://staticcheck.io/docs/checks#SA6002
q.elts = (*(eltsi.(*[]T)))[:0]
}
if cap(q.elts) == 0 {
q.elts = make([]T, 0, 32)
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)
l++
q.Unlock()
if signal {
if l == 0 {
select {
case q.ch <- struct{}{}:
default:
}
}
return l
return l + 1, nil
}
// Returns the whole list of elements currently present in the queue,
@@ -107,24 +153,23 @@ func (q *ipQueue[T]) pop() []T {
if q == nil {
return nil
}
var elts []T
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 = nil, 0
q.elts, q.pos, q.sz = nil, 0, 0
atomic.AddInt64(&q.inprogress, int64(len(elts)))
q.Unlock()
return elts
}
func (q *ipQueue[T]) resetAndReturnToPool(elts *[]T) {
(*elts) = (*elts)[:0]
q.pool.Put(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
@@ -133,24 +178,30 @@ func (q *ipQueue[T]) resetAndReturnToPool(elts *[]T) {
func (q *ipQueue[T]) popOne() (T, bool) {
q.Lock()
l := len(q.elts) - q.pos
if l < 1 {
if l == 0 {
q.Unlock()
var empty T
return empty, false
}
e := q.elts[q.pos]
q.pos++
l--
if l > 0 {
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 recycle now.
q.resetAndReturnToPool(&q.elts)
q.elts, q.pos = nil, 0
// 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
@@ -160,8 +211,7 @@ func (q *ipQueue[T]) popOne() (T, bool) {
// a first element is added to the queue.
// This will also decrement the "in progress" count with the length
// of the slice.
// Reason we use pointer to slice instead of slice is explained
// here: https://staticcheck.io/docs/checks#SA6002
// 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 {
@@ -169,24 +219,30 @@ func (q *ipQueue[T]) recycle(elts *[]T) {
}
// Update the in progress count.
if len(*elts) > 0 {
if atomic.AddInt64(&q.inprogress, int64(-(len(*elts)))) < 0 {
atomic.StoreInt64(&q.inprogress, 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
}
q.resetAndReturnToPool(elts)
(*elts) = (*elts)[:0]
q.pool.Put(elts)
}
// Returns the current length of the queue.
func (q *ipQueue[T]) len() int {
q.Lock()
l := len(q.elts) - q.pos
q.Unlock()
return l
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.
@@ -199,11 +255,8 @@ func (q *ipQueue[T]) drain() int {
return 0
}
q.Lock()
olen := len(q.elts)
if q.elts != nil {
q.resetAndReturnToPool(&q.elts)
q.elts, q.pos = nil, 0
}
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 {
+64 -4
View File
@@ -32,6 +32,7 @@ import (
"github.com/minio/highwayhash"
"github.com/nats-io/nats-server/v2/server/sysmem"
"github.com/nats-io/nats-server/v2/server/tpm"
"github.com/nats-io/nkeys"
"github.com/nats-io/nuid"
)
@@ -47,6 +48,7 @@ type JetStreamConfig struct {
Domain string `json:"domain,omitempty"`
CompressOK bool `json:"compress_ok,omitempty"`
UniqueTag string `json:"unique_tag,omitempty"`
Strict bool `json:"strict,omitempty"`
}
// Statistics about JetStream for this server.
@@ -90,6 +92,7 @@ type JetStreamAccountStats struct {
}
type JetStreamAPIStats struct {
Level int `json:"level"`
Total uint64 `json:"total"`
Errors uint64 `json:"errors"`
Inflight uint64 `json:"inflight,omitempty"`
@@ -173,6 +176,9 @@ type jsAccount struct {
updatesSub *subscription
lupdate time.Time
utimer *time.Timer
// Which account to send NRG traffic into. Empty string is system account.
nrgAccount string
}
// Track general usage for this account.
@@ -370,6 +376,40 @@ func (s *Server) checkStoreDir(cfg *JetStreamConfig) error {
return nil
}
// This function sets/updates the jetstream encryption key and cipher based
// on options. If the TPM options have been specified, a key is generated
// and sealed by the TPM.
func (s *Server) initJetStreamEncryption() (err error) {
opts := s.getOpts()
// The TPM settings and other encryption settings are mutually exclusive.
if opts.JetStreamKey != _EMPTY_ && opts.JetStreamTpm.KeysFile != _EMPTY_ {
return fmt.Errorf("JetStream encryption key may not be used with TPM options")
}
// if we are using the standard method to set the encryption key just return and carry on.
if opts.JetStreamKey != _EMPTY_ {
return nil
}
// if the tpm options are not used then no encryption has been configured and return.
if opts.JetStreamTpm.KeysFile == _EMPTY_ {
return nil
}
if opts.JetStreamTpm.Pcr == 0 {
// Default PCR to use in the TPM. Values can be 0-23, and most platforms
// reserve values 0-12 for the OS, boot locker, disc encryption, etc.
// 16 used for debugging. In sticking to NATS tradition, we'll use 22
// as the default with the option being configurable.
opts.JetStreamTpm.Pcr = 22
}
// Using the TPM to generate or get the encryption key and update the encryption options.
opts.JetStreamKey, err = tpm.LoadJetStreamEncryptionKeyFromTPM(opts.JetStreamTpm.SrkPassword,
opts.JetStreamTpm.KeysFile, opts.JetStreamTpm.KeyPassword, opts.JetStreamTpm.Pcr)
return err
}
// enableJetStream will start up the JetStream subsystem.
func (s *Server) enableJetStream(cfg JetStreamConfig) error {
js := &jetStream{srv: s, config: cfg, accounts: make(map[string]*jsAccount), apiSubs: NewSublistNoCache()}
@@ -402,6 +442,10 @@ func (s *Server) enableJetStream(cfg JetStreamConfig) error {
os.Remove(tmpfile.Name())
}
if err := s.initJetStreamEncryption(); err != nil {
return err
}
// JetStream is an internal service so we need to make sure we have a system account.
// This system account will export the JetStream service endpoints.
if s.SystemAccount() == nil {
@@ -419,6 +463,11 @@ func (s *Server) enableJetStream(cfg JetStreamConfig) error {
s.Noticef("")
}
s.Noticef("---------------- JETSTREAM ----------------")
if cfg.Strict {
s.Noticef(" Strict: %t", cfg.Strict)
}
s.Noticef(" Max Memory: %s", friendlyBytes(cfg.MaxMemory))
s.Noticef(" Max Storage: %s", friendlyBytes(cfg.MaxStore))
s.Noticef(" Store Directory: \"%s\"", cfg.StoreDir)
@@ -429,6 +478,11 @@ func (s *Server) enableJetStream(cfg JetStreamConfig) error {
if ek := opts.JetStreamKey; ek != _EMPTY_ {
s.Noticef(" Encryption: %s", opts.JetStreamCipher)
}
if opts.JetStreamTpm.KeysFile != _EMPTY_ {
s.Noticef(" TPM File: %q, Pcr: %d", opts.JetStreamTpm.KeysFile,
opts.JetStreamTpm.Pcr)
}
s.Noticef(" API Level: %d", JSApiLevel)
s.Noticef("-------------------------------------------")
// Setup our internal subscriptions.
@@ -508,6 +562,7 @@ func (s *Server) restartJetStream() error {
MaxMemory: opts.JetStreamMaxMemory,
MaxStore: opts.JetStreamMaxStore,
Domain: opts.JetStreamDomain,
Strict: opts.JetStreamStrict,
}
s.Noticef("Restarting JetStream")
err := s.EnableJetStream(&cfg)
@@ -1408,7 +1463,7 @@ func (a *Account) EnableJetStream(limits map[string]JetStreamAccountLimits) erro
// the consumer can reconnect. We will create it as a durable and switch it.
cfg.ConsumerConfig.Durable = ofi.Name()
}
obs, err := e.mset.addConsumerWithAssignment(&cfg.ConsumerConfig, _EMPTY_, nil, true, ActionCreateOrUpdate)
obs, err := e.mset.addConsumerWithAssignment(&cfg.ConsumerConfig, _EMPTY_, nil, true, ActionCreateOrUpdate, false)
if err != nil {
s.Warnf(" Error adding consumer %q: %v", cfg.Name, err)
continue
@@ -1652,6 +1707,7 @@ func (a *Account) JetStreamUsage() JetStreamAccountStats {
stats.Memory, stats.Store = jsa.storageTotals()
stats.Domain = js.config.Domain
stats.API = JetStreamAPIStats{
Level: JSApiLevel,
Total: jsa.apiTotal,
Errors: jsa.apiErrors,
}
@@ -2338,6 +2394,7 @@ func (js *jetStream) usageStats() *JetStreamStats {
stats.ReservedStore = uint64(js.storeReserved)
s := js.srv
js.mu.RUnlock()
stats.API.Level = JSApiLevel
stats.API.Total = uint64(atomic.LoadInt64(&js.apiTotal))
stats.API.Errors = uint64(atomic.LoadInt64(&js.apiErrors))
stats.API.Inflight = uint64(atomic.LoadInt64(&js.apiInflight))
@@ -2480,6 +2537,9 @@ func (s *Server) dynJetStreamConfig(storeDir string, maxStore, maxMem int64) *Je
opts := s.getOpts()
// Strict mode.
jsc.Strict = opts.JetStreamStrict
// Sync options.
jsc.SyncInterval = opts.SyncInterval
jsc.SyncAlways = opts.SyncAlways
@@ -2569,7 +2629,7 @@ func (a *Account) addStreamTemplate(tc *StreamTemplateConfig) (*streamTemplate,
// FIXME(dlc) - Hacky
tcopy := tc.deepCopy()
tcopy.Config.Name = "_"
cfg, apiErr := s.checkStreamCfg(tcopy.Config, a)
cfg, apiErr := s.checkStreamCfg(tcopy.Config, a, false)
if apiErr != nil {
return nil, apiErr
}
@@ -2871,11 +2931,11 @@ func (s *Server) resourcesExceededError() {
}
s.rerrMu.Unlock()
// If we are meta leader we should relinguish that here.
// If we are meta leader we should relinquish that here.
if didAlert {
if js := s.getJetStream(); js != nil {
js.mu.RLock()
if cc := js.cluster; cc != nil && cc.isLeader() {
if cc := js.cluster; cc != nil && cc.meta != nil {
cc.meta.StepDown()
}
js.mu.RUnlock()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+267 -23
View File
@@ -95,6 +95,9 @@ const (
// JSConsumerEmptyFilter consumer filter in FilterSubjects cannot be empty
JSConsumerEmptyFilter ErrorIdentifier = 10139
// JSConsumerEmptyGroupName Group name cannot be an empty string
JSConsumerEmptyGroupName ErrorIdentifier = 10161
// JSConsumerEphemeralWithDurableInSubjectErr consumer expected to be ephemeral but detected a durable name set in subject
JSConsumerEphemeralWithDurableInSubjectErr ErrorIdentifier = 10019
@@ -119,9 +122,15 @@ const (
// JSConsumerInvalidDeliverSubject invalid push consumer deliver subject
JSConsumerInvalidDeliverSubject ErrorIdentifier = 10112
// JSConsumerInvalidGroupNameErr Valid priority group name must match A-Z, a-z, 0-9, -_/=)+ and may not exceed 16 characters
JSConsumerInvalidGroupNameErr ErrorIdentifier = 10162
// JSConsumerInvalidPolicyErrF Generic delivery policy error ({err})
JSConsumerInvalidPolicyErrF ErrorIdentifier = 10094
// JSConsumerInvalidPriorityGroupErr Provided priority group does not exist for this consumer
JSConsumerInvalidPriorityGroupErr ErrorIdentifier = 10160
// JSConsumerInvalidSamplingErrF failed to parse consumer sampling configuration: {err}
JSConsumerInvalidSamplingErrF ErrorIdentifier = 10095
@@ -173,10 +182,13 @@ const (
// JSConsumerOverlappingSubjectFilters consumer subject filters cannot overlap
JSConsumerOverlappingSubjectFilters ErrorIdentifier = 10138
// JSConsumerPriorityPolicyWithoutGroup Setting PriorityPolicy requires at least one PriorityGroup to be set
JSConsumerPriorityPolicyWithoutGroup ErrorIdentifier = 10159
// JSConsumerPullNotDurableErr consumer in pull mode requires a durable name
JSConsumerPullNotDurableErr ErrorIdentifier = 10085
// JSConsumerPullRequiresAckErr consumer in pull mode requires ack policy
// JSConsumerPullRequiresAckErr consumer in pull mode requires explicit ack policy on workqueue stream
JSConsumerPullRequiresAckErr ErrorIdentifier = 10084
// JSConsumerPullWithRateLimitErr consumer in pull mode can not have rate limit set
@@ -218,7 +230,7 @@ const (
// JSInsufficientResourcesErr insufficient resources
JSInsufficientResourcesErr ErrorIdentifier = 10023
// JSInvalidJSONErr invalid JSON
// JSInvalidJSONErr invalid JSON: {err}
JSInvalidJSONErr ErrorIdentifier = 10025
// JSMaximumConsumersLimitErr maximum consumers limit reached
@@ -230,15 +242,24 @@ const (
// JSMemoryResourcesExceededErr insufficient memory resources available
JSMemoryResourcesExceededErr ErrorIdentifier = 10028
// JSMessageTTLDisabledErr per-message TTL is disabled
JSMessageTTLDisabledErr ErrorIdentifier = 10166
// JSMessageTTLInvalidErr invalid per-message TTL
JSMessageTTLInvalidErr ErrorIdentifier = 10165
// JSMirrorConsumerSetupFailedErrF generic mirror consumer setup failure string ({err})
JSMirrorConsumerSetupFailedErrF ErrorIdentifier = 10029
// JSMirrorInvalidStreamName mirrored stream name is invalid
JSMirrorInvalidStreamName ErrorIdentifier = 10142
// JSMirrorInvalidSubjectFilter mirror subject filter is invalid
// JSMirrorInvalidSubjectFilter mirror transform source: {err}
JSMirrorInvalidSubjectFilter ErrorIdentifier = 10151
// JSMirrorInvalidTransformDestination mirror transform: {err}
JSMirrorInvalidTransformDestination ErrorIdentifier = 10154
// JSMirrorMaxMessageSizeTooBigErr stream mirror must have max message size >= source
JSMirrorMaxMessageSizeTooBigErr ErrorIdentifier = 10030
@@ -281,6 +302,9 @@ const (
// JSNotEnabledForAccountErr JetStream not enabled for account
JSNotEnabledForAccountErr ErrorIdentifier = 10039
// JSPedanticErrF pedantic mode: {err}
JSPedanticErrF ErrorIdentifier = 10157
// JSPeerRemapErr peer remap failed
JSPeerRemapErr ErrorIdentifier = 10075
@@ -308,10 +332,10 @@ const (
// JSSourceInvalidStreamName sourced stream name is invalid
JSSourceInvalidStreamName ErrorIdentifier = 10141
// JSSourceInvalidSubjectFilter source subject filter is invalid
// JSSourceInvalidSubjectFilter source transform source: {err}
JSSourceInvalidSubjectFilter ErrorIdentifier = 10145
// JSSourceInvalidTransformDestination source transform destination is invalid
// JSSourceInvalidTransformDestination source transform: {err}
JSSourceInvalidTransformDestination ErrorIdentifier = 10146
// JSSourceMaxMessageSizeTooBigErr stream source must have max message size >= target
@@ -335,6 +359,12 @@ const (
// JSStreamDeleteErrF General stream deletion error string ({err})
JSStreamDeleteErrF ErrorIdentifier = 10050
// JSStreamDuplicateMessageConflict duplicate message id is in process
JSStreamDuplicateMessageConflict ErrorIdentifier = 10158
// JSStreamExpectedLastSeqPerSubjectNotReady expected last sequence per subject temporarily unavailable
JSStreamExpectedLastSeqPerSubjectNotReady ErrorIdentifier = 10163
// JSStreamExternalApiOverlapErrF stream external api prefix {prefix} must not overlap with {subject}
JSStreamExternalApiOverlapErrF ErrorIdentifier = 10021
@@ -446,12 +476,24 @@ const (
// JSStreamTemplateNotFoundErr template not found
JSStreamTemplateNotFoundErr ErrorIdentifier = 10068
// JSStreamTooManyRequests too many requests
JSStreamTooManyRequests ErrorIdentifier = 10167
// JSStreamTransformInvalidDestination stream transform: {err}
JSStreamTransformInvalidDestination ErrorIdentifier = 10156
// JSStreamTransformInvalidSource stream transform source: {err}
JSStreamTransformInvalidSource ErrorIdentifier = 10155
// JSStreamUpdateErrF Generic stream update error string ({err})
JSStreamUpdateErrF ErrorIdentifier = 10069
// JSStreamWrongLastMsgIDErrF wrong last msg ID: {id}
JSStreamWrongLastMsgIDErrF ErrorIdentifier = 10070
// JSStreamWrongLastSequenceConstantErr wrong last sequence
JSStreamWrongLastSequenceConstantErr ErrorIdentifier = 10164
// JSStreamWrongLastSequenceErrF wrong last sequence: {seq}
JSStreamWrongLastSequenceErrF ErrorIdentifier = 10071
@@ -494,6 +536,7 @@ var (
JSConsumerDurableNameNotMatchSubjectErr: {Code: 400, ErrCode: 10017, Description: "consumer name in subject does not match durable name in request"},
JSConsumerDurableNameNotSetErr: {Code: 400, ErrCode: 10018, Description: "consumer expected to be durable but a durable name was not set"},
JSConsumerEmptyFilter: {Code: 400, ErrCode: 10139, Description: "consumer filter in FilterSubjects cannot be empty"},
JSConsumerEmptyGroupName: {Code: 400, ErrCode: 10161, Description: "Group name cannot be an empty string"},
JSConsumerEphemeralWithDurableInSubjectErr: {Code: 400, ErrCode: 10019, Description: "consumer expected to be ephemeral but detected a durable name set in subject"},
JSConsumerEphemeralWithDurableNameErr: {Code: 400, ErrCode: 10020, Description: "consumer expected to be ephemeral but a durable name was set in request"},
JSConsumerExistingActiveErr: {Code: 400, ErrCode: 10105, Description: "consumer already exists and is still active"},
@@ -502,7 +545,9 @@ var (
JSConsumerHBRequiresPushErr: {Code: 400, ErrCode: 10088, Description: "consumer idle heartbeat requires a push based consumer"},
JSConsumerInactiveThresholdExcess: {Code: 400, ErrCode: 10153, Description: "consumer inactive threshold exceeds system limit of {limit}"},
JSConsumerInvalidDeliverSubject: {Code: 400, ErrCode: 10112, Description: "invalid push consumer deliver subject"},
JSConsumerInvalidGroupNameErr: {Code: 400, ErrCode: 10162, Description: "Valid priority group name must match A-Z, a-z, 0-9, -_/=)+ and may not exceed 16 characters"},
JSConsumerInvalidPolicyErrF: {Code: 400, ErrCode: 10094, Description: "{err}"},
JSConsumerInvalidPriorityGroupErr: {Code: 400, ErrCode: 10160, Description: "Provided priority group does not exist for this consumer"},
JSConsumerInvalidSamplingErrF: {Code: 400, ErrCode: 10095, Description: "failed to parse consumer sampling configuration: {err}"},
JSConsumerMaxDeliverBackoffErr: {Code: 400, ErrCode: 10116, Description: "max deliver is required to be > length of backoff values"},
JSConsumerMaxPendingAckExcessErrF: {Code: 400, ErrCode: 10121, Description: "consumer max ack pending exceeds system limit of {limit}"},
@@ -520,8 +565,9 @@ var (
JSConsumerOfflineErr: {Code: 500, ErrCode: 10119, Description: "consumer is offline"},
JSConsumerOnMappedErr: {Code: 400, ErrCode: 10092, Description: "consumer direct on a mapped consumer"},
JSConsumerOverlappingSubjectFilters: {Code: 400, ErrCode: 10138, Description: "consumer subject filters cannot overlap"},
JSConsumerPriorityPolicyWithoutGroup: {Code: 400, ErrCode: 10159, Description: "Setting PriorityPolicy requires at least one PriorityGroup to be set"},
JSConsumerPullNotDurableErr: {Code: 400, ErrCode: 10085, Description: "consumer in pull mode requires a durable name"},
JSConsumerPullRequiresAckErr: {Code: 400, ErrCode: 10084, Description: "consumer in pull mode requires ack policy"},
JSConsumerPullRequiresAckErr: {Code: 400, ErrCode: 10084, Description: "consumer in pull mode requires explicit ack policy on workqueue stream"},
JSConsumerPullWithRateLimitErr: {Code: 400, ErrCode: 10086, Description: "consumer in pull mode can not have rate limit set"},
JSConsumerPushMaxWaitingErr: {Code: 400, ErrCode: 10080, Description: "consumer in push mode can not set max waiting"},
JSConsumerReplacementWithDifferentNameErr: {Code: 400, ErrCode: 10106, Description: "consumer replacement durable config not the same"},
@@ -535,13 +581,16 @@ var (
JSConsumerWQRequiresExplicitAckErr: {Code: 400, ErrCode: 10098, Description: "workqueue stream requires explicit ack"},
JSConsumerWithFlowControlNeedsHeartbeats: {Code: 400, ErrCode: 10108, Description: "consumer with flow control also needs heartbeats"},
JSInsufficientResourcesErr: {Code: 503, ErrCode: 10023, Description: "insufficient resources"},
JSInvalidJSONErr: {Code: 400, ErrCode: 10025, Description: "invalid JSON"},
JSInvalidJSONErr: {Code: 400, ErrCode: 10025, Description: "invalid JSON: {err}"},
JSMaximumConsumersLimitErr: {Code: 400, ErrCode: 10026, Description: "maximum consumers limit reached"},
JSMaximumStreamsLimitErr: {Code: 400, ErrCode: 10027, Description: "maximum number of streams reached"},
JSMemoryResourcesExceededErr: {Code: 500, ErrCode: 10028, Description: "insufficient memory resources available"},
JSMessageTTLDisabledErr: {Code: 400, ErrCode: 10166, Description: "per-message TTL is disabled"},
JSMessageTTLInvalidErr: {Code: 400, ErrCode: 10165, Description: "invalid per-message TTL"},
JSMirrorConsumerSetupFailedErrF: {Code: 500, ErrCode: 10029, Description: "{err}"},
JSMirrorInvalidStreamName: {Code: 400, ErrCode: 10142, Description: "mirrored stream name is invalid"},
JSMirrorInvalidSubjectFilter: {Code: 400, ErrCode: 10151, Description: "mirror subject filter is invalid"},
JSMirrorInvalidSubjectFilter: {Code: 400, ErrCode: 10151, Description: "mirror transform source: {err}"},
JSMirrorInvalidTransformDestination: {Code: 400, ErrCode: 10154, Description: "mirror transform: {err}"},
JSMirrorMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10030, Description: "stream mirror must have max message size >= source"},
JSMirrorMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10150, Description: "mirror with multiple subject transforms cannot also have a single subject filter"},
JSMirrorOverlappingSubjectFilters: {Code: 400, ErrCode: 10152, Description: "mirror subject filters can not overlap"},
@@ -556,6 +605,7 @@ var (
JSNotEmptyRequestErr: {Code: 400, ErrCode: 10038, Description: "expected an empty request payload"},
JSNotEnabledErr: {Code: 503, ErrCode: 10076, Description: "JetStream not enabled"},
JSNotEnabledForAccountErr: {Code: 503, ErrCode: 10039, Description: "JetStream not enabled for account"},
JSPedanticErrF: {Code: 400, ErrCode: 10157, Description: "pedantic mode: {err}"},
JSPeerRemapErr: {Code: 503, ErrCode: 10075, Description: "peer remap failed"},
JSRaftGeneralErrF: {Code: 500, ErrCode: 10041, Description: "{err}"},
JSReplicasCountCannotBeNegative: {Code: 400, ErrCode: 10133, Description: "replicas count cannot be negative"},
@@ -565,8 +615,8 @@ var (
JSSourceConsumerSetupFailedErrF: {Code: 500, ErrCode: 10045, Description: "{err}"},
JSSourceDuplicateDetected: {Code: 400, ErrCode: 10140, Description: "duplicate source configuration detected"},
JSSourceInvalidStreamName: {Code: 400, ErrCode: 10141, Description: "sourced stream name is invalid"},
JSSourceInvalidSubjectFilter: {Code: 400, ErrCode: 10145, Description: "source subject filter is invalid"},
JSSourceInvalidTransformDestination: {Code: 400, ErrCode: 10146, Description: "source transform destination is invalid"},
JSSourceInvalidSubjectFilter: {Code: 400, ErrCode: 10145, Description: "source transform source: {err}"},
JSSourceInvalidTransformDestination: {Code: 400, ErrCode: 10146, Description: "source transform: {err}"},
JSSourceMaxMessageSizeTooBigErr: {Code: 400, ErrCode: 10046, Description: "stream source must have max message size >= target"},
JSSourceMultipleFiltersNotAllowed: {Code: 400, ErrCode: 10144, Description: "source with multiple subject transforms cannot also have a single subject filter"},
JSSourceOverlappingSubjectFilters: {Code: 400, ErrCode: 10147, Description: "source filters can not overlap"},
@@ -574,6 +624,8 @@ var (
JSStreamAssignmentErrF: {Code: 500, ErrCode: 10048, Description: "{err}"},
JSStreamCreateErrF: {Code: 500, ErrCode: 10049, Description: "{err}"},
JSStreamDeleteErrF: {Code: 500, ErrCode: 10050, Description: "{err}"},
JSStreamDuplicateMessageConflict: {Code: 409, ErrCode: 10158, Description: "duplicate message id is in process"},
JSStreamExpectedLastSeqPerSubjectNotReady: {Code: 503, ErrCode: 10163, Description: "expected last sequence per subject temporarily unavailable"},
JSStreamExternalApiOverlapErrF: {Code: 400, ErrCode: 10021, Description: "stream external api prefix {prefix} must not overlap with {subject}"},
JSStreamExternalDelPrefixOverlapsErrF: {Code: 400, ErrCode: 10022, Description: "stream external delivery prefix {prefix} overlaps with stream subject {subject}"},
JSStreamGeneralErrorF: {Code: 500, ErrCode: 10051, Description: "{err}"},
@@ -611,8 +663,12 @@ var (
JSStreamTemplateCreateErrF: {Code: 500, ErrCode: 10066, Description: "{err}"},
JSStreamTemplateDeleteErrF: {Code: 500, ErrCode: 10067, Description: "{err}"},
JSStreamTemplateNotFoundErr: {Code: 404, ErrCode: 10068, Description: "template not found"},
JSStreamTooManyRequests: {Code: 429, ErrCode: 10167, Description: "too many requests"},
JSStreamTransformInvalidDestination: {Code: 400, ErrCode: 10156, Description: "stream transform: {err}"},
JSStreamTransformInvalidSource: {Code: 400, ErrCode: 10155, Description: "stream transform source: {err}"},
JSStreamUpdateErrF: {Code: 500, ErrCode: 10069, Description: "{err}"},
JSStreamWrongLastMsgIDErrF: {Code: 400, ErrCode: 10070, Description: "wrong last msg ID: {id}"},
JSStreamWrongLastSequenceConstantErr: {Code: 400, ErrCode: 10164, Description: "wrong last sequence"},
JSStreamWrongLastSequenceErrF: {Code: 400, ErrCode: 10071, Description: "wrong last sequence: {seq}"},
JSTempStorageFailedErr: {Code: 500, ErrCode: 10072, Description: "JetStream unable to open temp storage for restore"},
JSTemplateNameNotMatchSubjectErr: {Code: 400, ErrCode: 10073, Description: "template name in subject does not match request"},
@@ -959,6 +1015,16 @@ func NewJSConsumerEmptyFilterError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSConsumerEmptyFilter]
}
// NewJSConsumerEmptyGroupNameError creates a new JSConsumerEmptyGroupName error: "Group name cannot be an empty string"
func NewJSConsumerEmptyGroupNameError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSConsumerEmptyGroupName]
}
// NewJSConsumerEphemeralWithDurableInSubjectError creates a new JSConsumerEphemeralWithDurableInSubjectErr error: "consumer expected to be ephemeral but detected a durable name set in subject"
func NewJSConsumerEphemeralWithDurableInSubjectError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1045,6 +1111,16 @@ func NewJSConsumerInvalidDeliverSubjectError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSConsumerInvalidDeliverSubject]
}
// NewJSConsumerInvalidGroupNameError creates a new JSConsumerInvalidGroupNameErr error: "Valid priority group name must match A-Z, a-z, 0-9, -_/=)+ and may not exceed 16 characters"
func NewJSConsumerInvalidGroupNameError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSConsumerInvalidGroupNameErr]
}
// NewJSConsumerInvalidPolicyError creates a new JSConsumerInvalidPolicyErrF error: "{err}"
func NewJSConsumerInvalidPolicyError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1061,6 +1137,16 @@ func NewJSConsumerInvalidPolicyError(err error, opts ...ErrorOption) *ApiError {
}
}
// NewJSConsumerInvalidPriorityGroupError creates a new JSConsumerInvalidPriorityGroupErr error: "Provided priority group does not exist for this consumer"
func NewJSConsumerInvalidPriorityGroupError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSConsumerInvalidPriorityGroupErr]
}
// NewJSConsumerInvalidSamplingError creates a new JSConsumerInvalidSamplingErrF error: "failed to parse consumer sampling configuration: {err}"
func NewJSConsumerInvalidSamplingError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1261,6 +1347,16 @@ func NewJSConsumerOverlappingSubjectFiltersError(opts ...ErrorOption) *ApiError
return ApiErrors[JSConsumerOverlappingSubjectFilters]
}
// NewJSConsumerPriorityPolicyWithoutGroupError creates a new JSConsumerPriorityPolicyWithoutGroup error: "Setting PriorityPolicy requires at least one PriorityGroup to be set"
func NewJSConsumerPriorityPolicyWithoutGroupError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSConsumerPriorityPolicyWithoutGroup]
}
// NewJSConsumerPullNotDurableError creates a new JSConsumerPullNotDurableErr error: "consumer in pull mode requires a durable name"
func NewJSConsumerPullNotDurableError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1271,7 +1367,7 @@ func NewJSConsumerPullNotDurableError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSConsumerPullNotDurableErr]
}
// NewJSConsumerPullRequiresAckError creates a new JSConsumerPullRequiresAckErr error: "consumer in pull mode requires ack policy"
// NewJSConsumerPullRequiresAckError creates a new JSConsumerPullRequiresAckErr error: "consumer in pull mode requires explicit ack policy on workqueue stream"
func NewJSConsumerPullRequiresAckError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
@@ -1417,14 +1513,20 @@ func NewJSInsufficientResourcesError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSInsufficientResourcesErr]
}
// NewJSInvalidJSONError creates a new JSInvalidJSONErr error: "invalid JSON"
func NewJSInvalidJSONError(opts ...ErrorOption) *ApiError {
// NewJSInvalidJSONError creates a new JSInvalidJSONErr error: "invalid JSON: {err}"
func NewJSInvalidJSONError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSInvalidJSONErr]
e := ApiErrors[JSInvalidJSONErr]
args := e.toReplacerArgs([]interface{}{"{err}", err})
return &ApiError{
Code: e.Code,
ErrCode: e.ErrCode,
Description: strings.NewReplacer(args...).Replace(e.Description),
}
}
// NewJSMaximumConsumersLimitError creates a new JSMaximumConsumersLimitErr error: "maximum consumers limit reached"
@@ -1457,6 +1559,26 @@ func NewJSMemoryResourcesExceededError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSMemoryResourcesExceededErr]
}
// NewJSMessageTTLDisabledError creates a new JSMessageTTLDisabledErr error: "per-message TTL is disabled"
func NewJSMessageTTLDisabledError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSMessageTTLDisabledErr]
}
// NewJSMessageTTLInvalidError creates a new JSMessageTTLInvalidErr error: "invalid per-message TTL"
func NewJSMessageTTLInvalidError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSMessageTTLInvalidErr]
}
// NewJSMirrorConsumerSetupFailedError creates a new JSMirrorConsumerSetupFailedErrF error: "{err}"
func NewJSMirrorConsumerSetupFailedError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1483,14 +1605,36 @@ func NewJSMirrorInvalidStreamNameError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSMirrorInvalidStreamName]
}
// NewJSMirrorInvalidSubjectFilterError creates a new JSMirrorInvalidSubjectFilter error: "mirror subject filter is invalid"
func NewJSMirrorInvalidSubjectFilterError(opts ...ErrorOption) *ApiError {
// NewJSMirrorInvalidSubjectFilterError creates a new JSMirrorInvalidSubjectFilter error: "mirror transform source: {err}"
func NewJSMirrorInvalidSubjectFilterError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSMirrorInvalidSubjectFilter]
e := ApiErrors[JSMirrorInvalidSubjectFilter]
args := e.toReplacerArgs([]interface{}{"{err}", err})
return &ApiError{
Code: e.Code,
ErrCode: e.ErrCode,
Description: strings.NewReplacer(args...).Replace(e.Description),
}
}
// NewJSMirrorInvalidTransformDestinationError creates a new JSMirrorInvalidTransformDestination error: "mirror transform: {err}"
func NewJSMirrorInvalidTransformDestinationError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
e := ApiErrors[JSMirrorInvalidTransformDestination]
args := e.toReplacerArgs([]interface{}{"{err}", err})
return &ApiError{
Code: e.Code,
ErrCode: e.ErrCode,
Description: strings.NewReplacer(args...).Replace(e.Description),
}
}
// NewJSMirrorMaxMessageSizeTooBigError creates a new JSMirrorMaxMessageSizeTooBigErr error: "stream mirror must have max message size >= source"
@@ -1633,6 +1777,22 @@ func NewJSNotEnabledForAccountError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSNotEnabledForAccountErr]
}
// NewJSPedanticError creates a new JSPedanticErrF error: "pedantic mode: {err}"
func NewJSPedanticError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
e := ApiErrors[JSPedanticErrF]
args := e.toReplacerArgs([]interface{}{"{err}", err})
return &ApiError{
Code: e.Code,
ErrCode: e.ErrCode,
Description: strings.NewReplacer(args...).Replace(e.Description),
}
}
// NewJSPeerRemapError creates a new JSPeerRemapErr error: "peer remap failed"
func NewJSPeerRemapError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -1747,24 +1907,36 @@ func NewJSSourceInvalidStreamNameError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSSourceInvalidStreamName]
}
// NewJSSourceInvalidSubjectFilterError creates a new JSSourceInvalidSubjectFilter error: "source subject filter is invalid"
func NewJSSourceInvalidSubjectFilterError(opts ...ErrorOption) *ApiError {
// NewJSSourceInvalidSubjectFilterError creates a new JSSourceInvalidSubjectFilter error: "source transform source: {err}"
func NewJSSourceInvalidSubjectFilterError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSSourceInvalidSubjectFilter]
e := ApiErrors[JSSourceInvalidSubjectFilter]
args := e.toReplacerArgs([]interface{}{"{err}", err})
return &ApiError{
Code: e.Code,
ErrCode: e.ErrCode,
Description: strings.NewReplacer(args...).Replace(e.Description),
}
}
// NewJSSourceInvalidTransformDestinationError creates a new JSSourceInvalidTransformDestination error: "source transform destination is invalid"
func NewJSSourceInvalidTransformDestinationError(opts ...ErrorOption) *ApiError {
// NewJSSourceInvalidTransformDestinationError creates a new JSSourceInvalidTransformDestination error: "source transform: {err}"
func NewJSSourceInvalidTransformDestinationError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSSourceInvalidTransformDestination]
e := ApiErrors[JSSourceInvalidTransformDestination]
args := e.toReplacerArgs([]interface{}{"{err}", err})
return &ApiError{
Code: e.Code,
ErrCode: e.ErrCode,
Description: strings.NewReplacer(args...).Replace(e.Description),
}
}
// NewJSSourceMaxMessageSizeTooBigError creates a new JSSourceMaxMessageSizeTooBigErr error: "stream source must have max message size >= target"
@@ -1855,6 +2027,26 @@ func NewJSStreamDeleteError(err error, opts ...ErrorOption) *ApiError {
}
}
// NewJSStreamDuplicateMessageConflictError creates a new JSStreamDuplicateMessageConflict error: "duplicate message id is in process"
func NewJSStreamDuplicateMessageConflictError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSStreamDuplicateMessageConflict]
}
// NewJSStreamExpectedLastSeqPerSubjectNotReadyError creates a new JSStreamExpectedLastSeqPerSubjectNotReady error: "expected last sequence per subject temporarily unavailable"
func NewJSStreamExpectedLastSeqPerSubjectNotReadyError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSStreamExpectedLastSeqPerSubjectNotReady]
}
// NewJSStreamExternalApiOverlapError creates a new JSStreamExternalApiOverlapErrF error: "stream external api prefix {prefix} must not overlap with {subject}"
func NewJSStreamExternalApiOverlapError(prefix interface{}, subject interface{}, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -2315,6 +2507,48 @@ func NewJSStreamTemplateNotFoundError(opts ...ErrorOption) *ApiError {
return ApiErrors[JSStreamTemplateNotFoundErr]
}
// NewJSStreamTooManyRequestsError creates a new JSStreamTooManyRequests error: "too many requests"
func NewJSStreamTooManyRequestsError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSStreamTooManyRequests]
}
// NewJSStreamTransformInvalidDestinationError creates a new JSStreamTransformInvalidDestination error: "stream transform: {err}"
func NewJSStreamTransformInvalidDestinationError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
e := ApiErrors[JSStreamTransformInvalidDestination]
args := e.toReplacerArgs([]interface{}{"{err}", err})
return &ApiError{
Code: e.Code,
ErrCode: e.ErrCode,
Description: strings.NewReplacer(args...).Replace(e.Description),
}
}
// NewJSStreamTransformInvalidSourceError creates a new JSStreamTransformInvalidSource error: "stream transform source: {err}"
func NewJSStreamTransformInvalidSourceError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
e := ApiErrors[JSStreamTransformInvalidSource]
args := e.toReplacerArgs([]interface{}{"{err}", err})
return &ApiError{
Code: e.Code,
ErrCode: e.ErrCode,
Description: strings.NewReplacer(args...).Replace(e.Description),
}
}
// NewJSStreamUpdateError creates a new JSStreamUpdateErrF error: "{err}"
func NewJSStreamUpdateError(err error, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
@@ -2347,6 +2581,16 @@ func NewJSStreamWrongLastMsgIDError(id interface{}, opts ...ErrorOption) *ApiErr
}
}
// NewJSStreamWrongLastSequenceConstantError creates a new JSStreamWrongLastSequenceConstantErr error: "wrong last sequence"
func NewJSStreamWrongLastSequenceConstantError(opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
if ae, ok := eopts.err.(*ApiError); ok {
return ae
}
return ApiErrors[JSStreamWrongLastSequenceConstantErr]
}
// NewJSStreamWrongLastSequenceError creates a new JSStreamWrongLastSequenceErrF error: "wrong last sequence: {seq}"
func NewJSStreamWrongLastSequenceError(seq uint64, opts ...ErrorOption) *ApiError {
eopts := parseOpts(opts)
+39
View File
@@ -90,6 +90,18 @@ type JSConsumerActionAdvisory struct {
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 {
@@ -269,6 +281,33 @@ type JSConsumerQuorumLostAdvisory struct {
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"
+179
View File
@@ -0,0 +1,179 @@
// 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 = 1
JSRequiredLevelMetadataKey = "_nats.req.level"
JSServerVersionMetadataKey = "_nats.ver"
JSServerLevelMetadataKey = "_nats.level"
)
// 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)
}
cfg.Metadata[JSRequiredLevelMetadataKey] = strconv.Itoa(requiredApiLevel)
}
// setDynamicStreamMetadata adds dynamic fields into the (copied) metadata.
func setDynamicStreamMetadata(cfg *StreamConfig) *StreamConfig {
newCfg := *cfg
newCfg.Metadata = make(map[string]string)
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)
}
cfg.Metadata[JSRequiredLevelMetadataKey] = strconv.Itoa(requiredApiLevel)
}
// setDynamicConsumerMetadata adds dynamic fields into the (copied) metadata.
func setDynamicConsumerMetadata(cfg *ConsumerConfig) *ConsumerConfig {
newCfg := *cfg
newCfg.Metadata = make(map[string]string)
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)
}
+3 -3
View File
@@ -1,4 +1,4 @@
// Copyright 2018-2022 The NATS Authors
// Copyright 2018-2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -106,8 +106,8 @@ func validateTrustedOperators(o *Options) error {
return fmt.Errorf("using nats based account resolver - the system account needs to be specified in configuration or the operator jwt")
}
}
ver := strings.Split(strings.Split(strings.Split(VERSION, "-")[0], ".RC")[0], ".beta")[0]
srvMajor, srvMinor, srvUpdate, _ := jwt.ParseServerVersion(ver)
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",
+101 -25
View File
@@ -20,6 +20,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/rand"
"net"
"net/http"
@@ -34,7 +35,6 @@ import (
"sync"
"sync/atomic"
"time"
"unicode"
"github.com/klauspost/compress/s2"
"github.com/nats-io/jwt/v2"
@@ -105,13 +105,14 @@ type leaf struct {
type leafNodeCfg struct {
sync.RWMutex
*RemoteLeafOpts
urls []*url.URL
curURL *url.URL
tlsName string
username string
password string
perms *Permissions
connDelay time.Duration // Delay before a connect, could be used while detecting loop condition, etc..
urls []*url.URL
curURL *url.URL
tlsName string
username string
password string
perms *Permissions
connDelay time.Duration // Delay before a connect, could be used while detecting loop condition, etc..
jsMigrateTimer *time.Timer
}
// Check to see if this is a solicited leafnode. We do special processing for solicited.
@@ -493,14 +494,15 @@ func (s *Server) connectToRemoteLeafNode(remote *leafNodeCfg, firstConnect bool)
opts := s.getOpts()
reconnectDelay := opts.LeafNode.ReconnectInterval
s.mu.Lock()
s.mu.RLock()
dialTimeout := s.leafNodeOpts.dialTimeout
resolver := s.leafNodeOpts.resolver
var isSysAcc bool
if s.eventsEnabled() {
isSysAcc = remote.LocalAccount == s.sys.account.Name
}
s.mu.Unlock()
jetstreamMigrateDelay := remote.JetStreamClusterMigrateDelay
s.mu.RUnlock()
// If we are sharing a system account and we are not standalone delay to gather some info prior.
if firstConnect && isSysAcc && !s.standAloneMode() {
@@ -522,6 +524,7 @@ func (s *Server) connectToRemoteLeafNode(remote *leafNodeCfg, firstConnect bool)
const connErrFmt = "Error trying to connect as leafnode to remote server %q (attempt %v): %v"
attempts := 0
for s.isRunning() && s.remoteLeafNodeStillValid(remote) {
rURL := remote.pickNextURL()
url, err := s.getRandomIP(resolver, rURL.Host, nil)
@@ -548,15 +551,28 @@ func (s *Server) connectToRemoteLeafNode(remote *leafNodeCfg, firstConnect bool)
} else {
s.Debugf(connErrFmt, rURL.Host, attempts, err)
}
remote.Lock()
// if we are using a delay to start migrating assets, kick off a migrate timer.
if remote.jsMigrateTimer == nil && jetstreamMigrateDelay > 0 {
remote.jsMigrateTimer = time.AfterFunc(jetstreamMigrateDelay, func() {
s.checkJetStreamMigrate(remote)
})
}
remote.Unlock()
select {
case <-s.quitCh:
remote.cancelMigrateTimer()
return
case <-time.After(delay):
// Check if we should migrate any JetStream assets while this remote is down.
s.checkJetStreamMigrate(remote)
// Check if we should migrate any JetStream assets immediately while this remote is down.
// This will be used if JetStreamClusterMigrateDelay was not set
if jetstreamMigrateDelay == 0 {
s.checkJetStreamMigrate(remote)
}
continue
}
}
remote.cancelMigrateTimer()
if !s.remoteLeafNodeStillValid(remote) {
conn.Close()
return
@@ -573,6 +589,12 @@ func (s *Server) connectToRemoteLeafNode(remote *leafNodeCfg, firstConnect bool)
}
}
func (cfg *leafNodeCfg) cancelMigrateTimer() {
cfg.Lock()
stopAndClearTimer(&cfg.jsMigrateTimer)
cfg.Unlock()
}
// This will clear any observer state such that stream or consumer assets on this server can become leaders again.
func (s *Server) clearObserverState(remote *leafNodeCfg) {
s.mu.RLock()
@@ -637,17 +659,13 @@ func (s *Server) checkJetStreamMigrate(remote *leafNodeCfg) {
// Collect any consumers
for _, o := range mset.getConsumers() {
if n := o.raftNode(); n != nil {
if n.Leader() {
n.StepDown()
}
n.StepDown()
// Ensure we can not become a leader while in this state.
n.SetObserver(true)
}
}
// Stepdown if this stream was leader.
if node.Leader() {
node.StepDown()
}
node.StepDown()
// Ensure we can not become a leader while in this state.
node.SetObserver(true)
}
@@ -728,7 +746,7 @@ func (s *Server) startLeafNodeAcceptLoop() {
Headers: s.supportsHeaders(),
JetStream: opts.JetStream,
Domain: opts.JetStreamDomain,
Proto: 1, // Fixed for now.
Proto: s.getServerProto(),
InfoOnConnect: true,
}
// If we have selected a random port...
@@ -792,6 +810,7 @@ func (c *client) sendLeafConnect(clusterName string, headers bool) error {
DenyPub: c.leaf.remote.DenyImports,
Compression: c.leaf.compression,
RemoteAccount: c.acc.GetName(),
Proto: c.srv.getServerProto(),
}
// If a signature callback is specified, this takes precedence over anything else.
@@ -996,8 +1015,11 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
c.initClient()
c.Noticef("Leafnode connection created%s %s", remoteSuffix, c.opts.Name)
var tlsFirst bool
var infoTimeout time.Duration
var (
tlsFirst bool
tlsFirstFallback time.Duration
infoTimeout time.Duration
)
if remote != nil {
solicited = true
remote.Lock()
@@ -1015,6 +1037,10 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
if ws != nil {
c.Debugf("Leafnode compression=%v", c.ws.compress)
}
tlsFirst = opts.LeafNode.TLSHandshakeFirst
if f := opts.LeafNode.TLSHandshakeFirstFallback; f > 0 {
tlsFirstFallback = f
}
}
c.mu.Unlock()
@@ -1078,7 +1104,33 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
info.Nonce = bytesToString(c.nonce)
info.CID = c.cid
proto := generateInfoJSON(info)
if !opts.LeafNode.TLSHandshakeFirst {
var pre []byte
// We need first to check for "TLS First" fallback delay.
if tlsFirstFallback > 0 {
// We wait and see if we are getting any data. Since we did not send
// the INFO protocol yet, only clients that use TLS first should be
// sending data (the TLS handshake). We don't really check the content:
// if it is a rogue agent and not an actual client performing the
// TLS handshake, the error will be detected when performing the
// handshake on our side.
pre = make([]byte, 4)
c.nc.SetReadDeadline(time.Now().Add(tlsFirstFallback))
n, _ := io.ReadFull(c.nc, pre[:])
c.nc.SetReadDeadline(time.Time{})
// If we get any data (regardless of possible timeout), we will proceed
// with the TLS handshake.
if n > 0 {
pre = pre[:n]
} else {
// We did not get anything so we will send the INFO protocol.
pre = nil
// Set the boolean to false for the rest of the function.
tlsFirst = false
}
}
if !tlsFirst {
// We have to send from this go routine because we may
// have to block for TLS handshake before we start our
// writeLoop go routine. The other side needs to receive
@@ -1095,6 +1147,10 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
// Check to see if we need to spin up TLS.
if !c.isWebsocket() && info.TLSRequired {
// If we have a prebuffer create a multi-reader.
if len(pre) > 0 {
c.nc = &tlsMixConn{c.nc, bytes.NewBuffer(pre)}
}
// Perform server-side TLS handshake.
if err := c.doTLSServerHandshake(tlsHandshakeLeaf, opts.LeafNode.TLSConfig, opts.LeafNode.TLSTimeout, opts.LeafNode.TLSPinnedCerts); err != nil {
c.mu.Unlock()
@@ -1104,7 +1160,8 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
// If the user wants the TLS handshake to occur first, now that it is
// done, send the INFO protocol.
if opts.LeafNode.TLSHandshakeFirst {
if tlsFirst {
c.flags.set(didTLSFirst)
c.sendProtoNow(proto)
if c.isClosed() {
c.mu.Unlock()
@@ -1297,6 +1354,13 @@ func (c *client) processLeafnodeInfo(info *Info) {
c.closeConnection(WrongPort)
return
}
// Reject a cluster that contains spaces.
if info.Cluster != _EMPTY_ && strings.Contains(info.Cluster, " ") {
c.mu.Unlock()
c.sendErrAndErr(ErrClusterNameHasSpaces.Error())
c.closeConnection(ProtocolViolation)
return
}
// Capture a nonce here.
c.nonce = []byte(info.Nonce)
if info.TLSRequired && didSolicit {
@@ -1316,6 +1380,10 @@ func (c *client) processLeafnodeInfo(info *Info) {
}
c.leaf.remoteDomain = info.Domain
c.leaf.remoteCluster = info.Cluster
// We send the protocol version in the INFO protocol.
// Keep track of it, so we know if this connection supports message
// tracing for instance.
c.opts.Protocol = info.Proto
}
// For both initial INFO and async INFO protocols, Possibly
@@ -1757,6 +1825,14 @@ type leafConnectInfo struct {
// Tells the accept side which account the remote is binding to.
RemoteAccount string `json:"remote_account,omitempty"`
// The accept side of a LEAF connection, unlike ROUTER and GATEWAY, receives
// only the CONNECT protocol, and no INFO. So we need to send the protocol
// version as part of the CONNECT. It will indicate if a connection supports
// some features, such as message tracing.
// We use `protocol` as the JSON tag, so this is automatically unmarshal'ed
// in the low level process CONNECT.
Proto int `json:"protocol,omitempty"`
}
// processLeafNodeConnect will process the inbound connect args.
@@ -1777,8 +1853,8 @@ func (c *client) processLeafNodeConnect(s *Server, arg []byte, lang string) erro
return err
}
// Reject a cluster that contains spaces or line breaks.
if proto.Cluster != _EMPTY_ && strings.ContainsFunc(proto.Cluster, unicode.IsSpace) {
// Reject a cluster that contains spaces.
if proto.Cluster != _EMPTY_ && strings.Contains(proto.Cluster, " ") {
c.sendErrAndErr(ErrClusterNameHasSpaces.Error())
c.closeConnection(ProtocolViolation)
return ErrClusterNameHasSpaces
+353 -73
View File
@@ -17,12 +17,15 @@ import (
crand "crypto/rand"
"encoding/binary"
"fmt"
"math"
"slices"
"sort"
"sync"
"time"
"github.com/nats-io/nats-server/v2/server/avl"
"github.com/nats-io/nats-server/v2/server/stree"
"github.com/nats-io/nats-server/v2/server/thw"
)
// TODO(dlc) - This is a fairly simplistic approach but should do for now.
@@ -35,9 +38,12 @@ type memStore struct {
dmap avl.SequenceSet
maxp int64
scb StorageUpdateHandler
sdmcb SubjectDeleteMarkerUpdateHandler
ageChk *time.Timer
consumers int
receivedAny bool
ttls *thw.HashWheel
markers []string
}
func newMemStore(cfg *StreamConfig) (*memStore, error) {
@@ -53,8 +59,12 @@ func newMemStore(cfg *StreamConfig) (*memStore, error) {
maxp: cfg.MaxMsgsPer,
cfg: *cfg,
}
// Only create a THW if we're going to allow TTLs.
if cfg.AllowMsgTTL {
ms.ttls = thw.NewHashWheel()
}
if cfg.FirstSeq > 0 {
if _, err := ms.purge(cfg.FirstSeq); err != nil {
if _, err := ms.purge(cfg.FirstSeq, true); err != nil {
return nil, err
}
}
@@ -109,7 +119,7 @@ func (ms *memStore) UpdateConfig(cfg *StreamConfig) error {
// Stores a raw message with expected sequence number and timestamp.
// Lock should be held.
func (ms *memStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts int64) error {
func (ms *memStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, ttl int64) error {
if ms.msgs == nil {
return ErrStoreClosed
}
@@ -210,17 +220,27 @@ func (ms *memStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts int
ms.enforceMsgLimit()
ms.enforceBytesLimit()
// Per-message TTL.
if ms.ttls != nil && ttl > 0 {
expires := time.Duration(ts) + (time.Second * time.Duration(ttl))
ms.ttls.Add(seq, int64(expires))
}
// Check if we have and need the age expiration timer running.
if ms.ageChk == nil && ms.cfg.MaxAge != 0 {
switch {
case ms.ttls != nil && ttl > 0:
ms.resetAgeChk(0)
case ms.ageChk == nil && (ms.cfg.MaxAge > 0 || ms.ttls != nil):
ms.startAgeChk()
}
return nil
}
// StoreRawMsg stores a raw message with expected sequence number and timestamp.
func (ms *memStore) StoreRawMsg(subj string, hdr, msg []byte, seq uint64, ts int64) error {
func (ms *memStore) StoreRawMsg(subj string, hdr, msg []byte, seq uint64, ts, ttl int64) error {
ms.mu.Lock()
err := ms.storeRawMsg(subj, hdr, msg, seq, ts)
err := ms.storeRawMsg(subj, hdr, msg, seq, ts, ttl)
cb := ms.scb
// Check if first message timestamp requires expiry
// sooner than initial replica expiry timer set to MaxAge when initializing.
@@ -239,10 +259,10 @@ func (ms *memStore) StoreRawMsg(subj string, hdr, msg []byte, seq uint64, ts int
}
// Store stores a message.
func (ms *memStore) StoreMsg(subj string, hdr, msg []byte) (uint64, int64, error) {
func (ms *memStore) StoreMsg(subj string, hdr, msg []byte, ttl int64) (uint64, int64, error) {
ms.mu.Lock()
seq, ts := ms.state.LastSeq+1, time.Now().UnixNano()
err := ms.storeRawMsg(subj, hdr, msg, seq, ts)
err := ms.storeRawMsg(subj, hdr, msg, seq, ts, ttl)
cb := ms.scb
ms.mu.Unlock()
@@ -312,6 +332,13 @@ func (ms *memStore) RegisterStorageUpdates(cb StorageUpdateHandler) {
ms.mu.Unlock()
}
// RegisterSubjectDeleteMarkerUpdates registers a callback for updates to new subject delete markers.
func (ms *memStore) RegisterSubjectDeleteMarkerUpdates(cb SubjectDeleteMarkerUpdateHandler) {
ms.mu.Lock()
ms.sdmcb = cb
ms.mu.Unlock()
}
// GetSeqFromTime looks for the first sequence number that has the message
// with >= timestamp.
// FIXME(dlc) - inefficient.
@@ -601,7 +628,55 @@ func (ms *memStore) SubjectsState(subject string) map[string]SimpleState {
return fss
}
// SubjectsTotal return message totals per subject.
func (ms *memStore) MultiLastSeqs(filters []string, maxSeq uint64, maxAllowed int) ([]uint64, error) {
ms.mu.RLock()
defer ms.mu.RUnlock()
if len(ms.msgs) == 0 {
return nil, nil
}
// Implied last sequence.
if maxSeq == 0 {
maxSeq = ms.state.LastSeq
}
//subs := make(map[string]*SimpleState)
seqs := make([]uint64, 0, 64)
seen := make(map[uint64]struct{})
addIfNotDupe := func(seq uint64) {
if _, ok := seen[seq]; !ok {
seqs = append(seqs, seq)
seen[seq] = struct{}{}
}
}
for _, filter := range filters {
ms.fss.Match(stringToBytes(filter), func(subj []byte, ss *SimpleState) {
if ss.Last <= maxSeq {
addIfNotDupe(ss.Last)
} else if ss.Msgs > 1 {
// The last is greater than maxSeq.
s := bytesToString(subj)
for seq := maxSeq; seq > 0; seq-- {
if sm, ok := ms.msgs[seq]; ok && sm.subj == s {
addIfNotDupe(seq)
break
}
}
}
})
// If maxAllowed was sepcified check that we will not exceed that.
if maxAllowed > 0 && len(seqs) > maxAllowed {
return nil, ErrTooManyResults
}
}
slices.Sort(seqs)
return seqs, nil
}
// SubjectsTotals return message totals per subject.
func (ms *memStore) SubjectsTotals(filterSubject string) map[string]uint64 {
ms.mu.RLock()
defer ms.mu.RUnlock()
@@ -797,7 +872,7 @@ func (ms *memStore) enforcePerSubjectLimit(subj string, ss *SimpleState) {
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
ms.recalculateForSubj(subj, ss)
}
if !ms.removeMsg(ss.First, false) {
if !ms.removeMsg(ss.First, false, _EMPTY_) {
break
}
}
@@ -834,25 +909,52 @@ func (ms *memStore) enforceBytesLimit() {
// Will start the age check timer.
// Lock should be held.
func (ms *memStore) startAgeChk() {
if ms.ageChk == nil && ms.cfg.MaxAge != 0 {
if ms.ageChk != nil {
return
}
if ms.cfg.MaxAge != 0 || ms.ttls != nil {
ms.ageChk = time.AfterFunc(ms.cfg.MaxAge, ms.expireMsgs)
}
}
// Lock should be held.
func (ms *memStore) resetAgeChk(delta int64) {
if ms.cfg.MaxAge == 0 {
var next int64 = math.MaxInt64
if ms.ttls != nil {
next = ms.ttls.GetNextExpiration(next)
}
// If there's no MaxAge and there's nothing waiting to be expired then
// don't bother continuing. The next storeRawMsg() will wake us up if
// needs be.
if ms.cfg.MaxAge <= 0 && next == math.MaxInt64 {
clearTimer(&ms.ageChk)
return
}
// Check to see if we should be firing sooner than MaxAge for an expiring TTL.
fireIn := ms.cfg.MaxAge
if delta > 0 && time.Duration(delta) < fireIn {
if fireIn = time.Duration(delta); fireIn < 250*time.Millisecond {
// Only fire at most once every 250ms.
// Excessive firing can effect ingest performance.
fireIn = time.Second
if next < math.MaxInt64 {
// Looks like there's a next expiration, use it either if there's no
// MaxAge set or if it looks to be sooner than MaxAge is.
if until := time.Until(time.Unix(0, next)); fireIn == 0 || until < fireIn {
fireIn = until
}
}
// If not then look at the delta provided (usually gap to next age expiry).
if delta > 0 {
if fireIn == 0 || time.Duration(delta) < fireIn {
fireIn = time.Duration(delta)
}
}
// 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 ms.ageChk != nil {
ms.ageChk.Reset(fireIn)
} else {
@@ -860,56 +962,147 @@ func (ms *memStore) resetAgeChk(delta int64) {
}
}
// Lock should be held.
func (ms *memStore) cancelAgeChk() {
if ms.ageChk != nil {
ms.ageChk.Stop()
ms.ageChk = nil
}
}
// Lock must be held so that nothing else can interleave and write a
// new message on this subject before we get the chance to write the
// delete marker. If the delete marker is written successfully then
// this function returns a callback func to call scb and sdmcb after
// the lock has been released.
func (ms *memStore) subjectDeleteMarkerIfNeeded(subj string, reason string) func() {
if ms.cfg.SubjectDeleteMarkerTTL <= 0 {
return nil
}
if _, ok := ms.fss.Find(stringToBytes(subj)); ok {
// There are still messages left with this subject,
// therefore it wasn't the last message deleted.
return nil
}
// Build the subject delete marker. If no TTL is specified then
// we'll default to 15 minutes — by that time every possible condition
// should have cleared (i.e. ordered consumer timeout, client timeouts,
// route/gateway interruptions, even device/client restarts etc).
ttl := int64(ms.cfg.SubjectDeleteMarkerTTL.Seconds())
if ttl <= 0 {
return nil
}
var _hdr [128]byte
hdr := fmt.Appendf(
_hdr[:0],
"NATS/1.0\r\n%s: %s\r\n%s: %s\r\n%s: %d\r\n%s: %s\r\n\r\n\r\n",
JSMarkerReason, reason,
JSMessageTTL, time.Duration(ttl)*time.Second,
JSExpectedLastSubjSeq, 0,
JSExpectedLastSubjSeqSubj, subj,
)
msg := &inMsg{
subj: subj,
hdr: hdr,
}
sdmcb := ms.sdmcb
return func() {
if sdmcb != nil {
sdmcb(msg)
}
}
}
// Memstore lock must be held. The caller should call the callback, if non-nil,
// after releasing the memstore lock.
func (ms *memStore) subjectDeleteMarkersAfterOperation(reason string) func() {
if ms.cfg.SubjectDeleteMarkerTTL <= 0 || len(ms.markers) == 0 {
return nil
}
cbs := make([]func(), 0, len(ms.markers))
for _, subject := range ms.markers {
if cb := ms.subjectDeleteMarkerIfNeeded(subject, reason); cb != nil {
cbs = append(cbs, cb)
}
}
ms.markers = nil
return func() {
for _, cb := range cbs {
cb()
}
}
}
// Will expire msgs that are too old.
func (ms *memStore) expireMsgs() {
var smv StoreMsg
var sm *StoreMsg
ms.mu.RLock()
now := time.Now().UnixNano()
minAge := now - int64(ms.cfg.MaxAge)
maxAge := int64(ms.cfg.MaxAge)
minAge := time.Now().UnixNano() - maxAge
ms.mu.RUnlock()
for {
ms.mu.Lock()
if sm, ok := ms.msgs[ms.state.FirstSeq]; ok && sm.ts <= minAge {
ms.deleteFirstMsgOrPanic()
// Recalculate in case we are expiring a bunch.
now = time.Now().UnixNano()
minAge = now - int64(ms.cfg.MaxAge)
ms.mu.Unlock()
} else {
// We will exit here
if len(ms.msgs) == 0 {
if ms.ageChk != nil {
ms.ageChk.Stop()
ms.ageChk = nil
}
} else {
var fireIn time.Duration
if sm == nil {
fireIn = ms.cfg.MaxAge
} else {
fireIn = time.Duration(sm.ts - minAge)
}
if ms.ageChk != nil {
ms.ageChk.Reset(fireIn)
} else {
ms.ageChk = time.AfterFunc(fireIn, ms.expireMsgs)
if maxAge > 0 {
var seq uint64
for sm, seq, _ = ms.LoadNextMsg(fwcs, true, 0, &smv); sm != nil && sm.ts <= minAge; sm, seq, _ = ms.LoadNextMsg(fwcs, true, seq+1, &smv) {
if len(sm.hdr) > 0 {
if ttl, err := getMessageTTL(sm.hdr); err == nil && ttl < 0 {
// The message has a negative TTL, therefore it must "never expire".
minAge = time.Now().UnixNano() - maxAge
continue
}
}
ms.mu.Lock()
ms.removeMsg(seq, false, JSMarkerReasonMaxAge)
ms.mu.Unlock()
break
// Recalculate in case we are expiring a bunch.
minAge = time.Now().UnixNano() - maxAge
}
}
ms.mu.Lock()
defer ms.mu.Unlock()
// TODO: Not great that we're holding the lock here, but the timed hash wheel isn't thread-safe.
nextTTL := int64(math.MaxInt64)
if ms.ttls != nil {
ms.ttls.ExpireTasks(func(seq uint64, ts int64) {
ms.removeMsg(seq, false, _EMPTY_)
})
if maxAge > 0 {
// Only check if we're expiring something in the next MaxAge interval, saves us a bit
// of work if MaxAge will beat us to the next expiry anyway.
nextTTL = ms.ttls.GetNextExpiration(time.Now().Add(time.Duration(maxAge)).UnixNano())
} else {
nextTTL = ms.ttls.GetNextExpiration(math.MaxInt64)
}
}
// Only cancel if no message left, not on potential lookup error that would result in sm == nil.
if ms.state.Msgs == 0 && nextTTL == math.MaxInt64 {
ms.cancelAgeChk()
} else {
if sm == nil {
ms.resetAgeChk(0)
} else {
ms.resetAgeChk(sm.ts - minAge)
}
}
}
// PurgeEx will remove messages based on subject filters, sequence and number of messages to keep.
// Will return the number of purged messages.
func (ms *memStore) PurgeEx(subject string, sequence, keep uint64) (purged uint64, err error) {
func (ms *memStore) PurgeEx(subject string, sequence, keep uint64, _ /* noMarkers */ bool) (purged uint64, err error) {
// TODO: Don't write markers on purge until we have solved performance
// issues with them.
noMarkers := true
if subject == _EMPTY_ || subject == fwcs {
if keep == 0 && sequence == 0 {
return ms.Purge()
return ms.purge(0, noMarkers)
}
if sequence > 1 {
return ms.Compact(sequence)
return ms.compact(sequence, noMarkers)
} else if keep > 0 {
ms.mu.RLock()
msgs, lseq := ms.state.Msgs, ms.state.LastSeq
@@ -917,7 +1110,7 @@ func (ms *memStore) PurgeEx(subject string, sequence, keep uint64) (purged uint6
if keep >= msgs {
return 0, nil
}
return ms.Compact(lseq - keep + 1)
return ms.compact(lseq-keep+1, noMarkers)
}
return 0, nil
@@ -935,9 +1128,13 @@ func (ms *memStore) PurgeEx(subject string, sequence, keep uint64) (purged uint6
last = sequence - 1
}
ms.mu.Lock()
var removeReason string
if !noMarkers {
removeReason = JSMarkerReasonPurge
}
for seq := ss.First; seq <= last; seq++ {
if sm, ok := ms.msgs[seq]; ok && eq(sm.subj, subject) {
if ok := ms.removeMsg(sm.seq, false); ok {
if ok := ms.removeMsg(sm.seq, false, removeReason); ok {
purged++
if purged >= ss.Msgs {
break
@@ -956,10 +1153,14 @@ func (ms *memStore) Purge() (uint64, error) {
ms.mu.RLock()
first := ms.state.LastSeq + 1
ms.mu.RUnlock()
return ms.purge(first)
return ms.purge(first, false)
}
func (ms *memStore) purge(fseq uint64) (uint64, error) {
func (ms *memStore) purge(fseq uint64, _ /* noMarkers */ bool) (uint64, error) {
// TODO: Don't write markers on purge until we have solved performance
// issues with them.
noMarkers := true
ms.mu.Lock()
purged := uint64(len(ms.msgs))
cb := ms.scb
@@ -974,12 +1175,23 @@ func (ms *memStore) purge(fseq uint64) (uint64, error) {
ms.state.Bytes = 0
ms.state.Msgs = 0
ms.msgs = make(map[uint64]*StoreMsg)
// Subject delete markers if needed.
if !noMarkers && ms.cfg.SubjectDeleteMarkerTTL > 0 {
ms.fss.IterOrdered(func(bsubj []byte, ss *SimpleState) bool {
ms.markers = append(ms.markers, string(bsubj))
return true
})
}
ms.fss = stree.NewSubjectTree[SimpleState]()
sdmcb := ms.subjectDeleteMarkersAfterOperation(JSMarkerReasonPurge)
ms.mu.Unlock()
if cb != nil {
cb(-int64(purged), -bytes, 0, _EMPTY_)
}
if sdmcb != nil {
sdmcb()
}
return purged, nil
}
@@ -988,6 +1200,14 @@ func (ms *memStore) purge(fseq uint64) (uint64, error) {
// but not including the seq parameter.
// Will return the number of purged messages.
func (ms *memStore) Compact(seq uint64) (uint64, error) {
return ms.compact(seq, false)
}
func (ms *memStore) compact(seq uint64, _ /* noMarkers */ bool) (uint64, error) {
// TODO: Don't write markers on compact until we have solved performance
// issues with them.
noMarkers := true
if seq == 0 {
return ms.Purge()
}
@@ -1010,7 +1230,7 @@ func (ms *memStore) Compact(seq uint64) (uint64, error) {
if sm := ms.msgs[seq]; sm != nil {
bytes += memStoreMsgSize(sm.subj, sm.hdr, sm.msg)
purged++
ms.removeSeqPerSubject(sm.subj, seq)
ms.removeSeqPerSubject(sm.subj, seq, !noMarkers && ms.cfg.SubjectDeleteMarkerTTL > 0)
// Must delete message after updating per-subject info, to be consistent with file store.
delete(ms.msgs, seq)
} else if !ms.dmap.IsEmpty() {
@@ -1035,16 +1255,28 @@ func (ms *memStore) Compact(seq uint64) (uint64, error) {
ms.state.FirstSeq = seq
ms.state.FirstTime = time.Time{}
ms.state.LastSeq = seq - 1
// Subject delete markers if needed.
if !noMarkers && ms.cfg.SubjectDeleteMarkerTTL > 0 {
ms.fss.IterOrdered(func(bsubj []byte, ss *SimpleState) bool {
ms.markers = append(ms.markers, string(bsubj))
return true
})
}
// Reset msgs, fss and dmap.
ms.msgs = make(map[uint64]*StoreMsg)
ms.fss = stree.NewSubjectTree[SimpleState]()
ms.dmap.Empty()
}
// Subject delete markers if needed.
sdmcb := ms.subjectDeleteMarkersAfterOperation(JSMarkerReasonPurge)
ms.mu.Unlock()
if cb != nil {
cb(-int64(purged), -int64(bytes), 0, _EMPTY_)
}
if sdmcb != nil {
sdmcb()
}
return purged, nil
}
@@ -1104,7 +1336,7 @@ func (ms *memStore) Truncate(seq uint64) error {
if sm := ms.msgs[i]; sm != nil {
purged++
bytes += memStoreMsgSize(sm.subj, sm.hdr, sm.msg)
ms.removeSeqPerSubject(sm.subj, i)
ms.removeSeqPerSubject(sm.subj, i, false)
// Must delete message after updating per-subject info, to be consistent with file store.
delete(ms.msgs, i)
} else if !ms.dmap.IsEmpty() {
@@ -1141,7 +1373,8 @@ func (ms *memStore) deleteFirstMsgOrPanic() {
}
func (ms *memStore) deleteFirstMsg() bool {
return ms.removeMsg(ms.state.FirstSeq, false)
// TODO: Currently no markers for these types of limits (max msgs or max bytes)
return ms.removeMsg(ms.state.FirstSeq, false, _EMPTY_)
}
// LoadMsg will lookup the message by sequence number and return it if found.
@@ -1337,7 +1570,8 @@ func (ms *memStore) LoadPrevMsg(start uint64, smp *StoreMsg) (sm *StoreMsg, err
// Will return the number of bytes removed.
func (ms *memStore) RemoveMsg(seq uint64) (bool, error) {
ms.mu.Lock()
removed := ms.removeMsg(seq, false)
// TODO: Don't write markers on removes via the API yet, only via limits.
removed := ms.removeMsg(seq, false, _EMPTY_)
ms.mu.Unlock()
return removed, nil
}
@@ -1345,7 +1579,8 @@ func (ms *memStore) RemoveMsg(seq uint64) (bool, error) {
// EraseMsg will remove the message and rewrite its contents.
func (ms *memStore) EraseMsg(seq uint64) (bool, error) {
ms.mu.Lock()
removed := ms.removeMsg(seq, true)
// TODO: Don't write markers on removes via the API yet, only via limits.
removed := ms.removeMsg(seq, true, _EMPTY_)
ms.mu.Unlock()
return removed, nil
}
@@ -1385,20 +1620,39 @@ func (ms *memStore) updateFirstSeq(seq uint64) {
// Remove a seq from the fss and select new first.
// Lock should be held.
func (ms *memStore) removeSeqPerSubject(subj string, seq uint64) {
func (ms *memStore) removeSeqPerSubject(subj string, seq uint64, marker bool) bool {
ss, ok := ms.fss.Find(stringToBytes(subj))
if !ok {
return
return false
}
if ss.Msgs == 1 {
ms.fss.Delete(stringToBytes(subj))
return
if marker {
ms.markers = append(ms.markers, subj)
}
return true
}
ss.Msgs--
// Only one left
if ss.Msgs == 1 {
if !ss.lastNeedsUpdate && seq != ss.Last {
ss.First = ss.Last
ss.firstNeedsUpdate = false
return false
}
if !ss.firstNeedsUpdate && seq != ss.First {
ss.Last = ss.First
ss.lastNeedsUpdate = false
return false
}
}
// We can lazily calculate the first/last sequence when needed.
ss.firstNeedsUpdate = seq == ss.First || ss.firstNeedsUpdate
ss.lastNeedsUpdate = seq == ss.Last || ss.lastNeedsUpdate
return false
}
// Will recalculate the first and/or last sequence for this subject.
@@ -1443,7 +1697,7 @@ func (ms *memStore) recalculateForSubj(subj string, ss *SimpleState) {
// Removes the message referenced by seq.
// Lock should be held.
func (ms *memStore) removeMsg(seq uint64, secure bool) bool {
func (ms *memStore) removeMsg(seq uint64, secure bool, marker string) bool {
var ss uint64
sm, ok := ms.msgs[seq]
if !ok {
@@ -1475,15 +1729,29 @@ func (ms *memStore) removeMsg(seq uint64, secure bool) bool {
}
// Remove any per subject tracking.
ms.removeSeqPerSubject(sm.subj, seq)
needMarker := marker != _EMPTY_ && ms.cfg.SubjectDeleteMarkerTTL > 0 && len(getHeader(JSMarkerReason, sm.hdr)) == 0
wasLast := ms.removeSeqPerSubject(sm.subj, seq, needMarker)
// Must delete message after updating per-subject info, to be consistent with file store.
delete(ms.msgs, seq)
if ms.scb != nil {
// If the deleted message was itself a delete marker then
// don't write out more of them or we'll churn endlessly.
var sdmcb func()
if needMarker && wasLast {
sdmcb = ms.subjectDeleteMarkersAfterOperation(marker)
}
if ms.scb != nil || sdmcb != nil {
// We do not want to hold any locks here.
ms.mu.Unlock()
delta := int64(ss)
ms.scb(-1, -delta, seq, sm.subj)
if ms.scb != nil {
delta := int64(ss)
ms.scb(-1, -delta, seq, sm.subj)
}
if sdmcb != nil {
sdmcb()
}
ms.mu.Lock()
}
@@ -1677,7 +1945,7 @@ func (ms *memStore) SyncDeleted(dbs DeleteBlocks) {
continue
}
db.Range(func(seq uint64) bool {
ms.removeMsg(seq, false)
ms.removeMsg(seq, false, _EMPTY_)
return true
})
}
@@ -1736,9 +2004,26 @@ func (o *consumerMemStore) SetStarting(sseq uint64) error {
return nil
}
// UpdateStarting updates our starting stream sequence.
func (o *consumerMemStore) UpdateStarting(sseq uint64) {
o.mu.Lock()
defer o.mu.Unlock()
if sseq > o.state.Delivered.Stream {
o.state.Delivered.Stream = sseq
// For AckNone just update delivered and ackfloor at the same time.
if o.cfg.AckPolicy == AckNone {
o.state.AckFloor.Stream = sseq
}
}
}
// HasState returns if this store has a recorded state.
func (o *consumerMemStore) HasState() bool {
return false
o.mu.Lock()
defer o.mu.Unlock()
// We have a running state.
return o.state.Delivered.Consumer != 0 || o.state.Delivered.Stream != 0
}
func (o *consumerMemStore) UpdateDelivered(dseq, sseq, dc uint64, ts int64) error {
@@ -1749,6 +2034,7 @@ func (o *consumerMemStore) UpdateDelivered(dseq, sseq, dc uint64, ts int64) erro
return ErrNoAckPolicy
}
// On restarts the old leader may get a replay from the raft logs that are old.
if dseq <= o.state.AckFloor.Consumer {
return nil
}
@@ -1819,12 +2105,6 @@ func (o *consumerMemStore) UpdateAcks(dseq, sseq uint64) error {
return nil
}
// Match leader logic on checking if ack is ahead of delivered.
// This could happen on a cooperative takeover with high speed deliveries.
if sseq > o.state.Delivered.Stream {
o.state.Delivered.Stream = sseq + 1
}
if len(o.state.Pending) == 0 || o.state.Pending[sseq] == nil {
delete(o.state.Redelivered, sseq)
return ErrStoreMsgNotFound
+67 -41
View File
@@ -415,14 +415,13 @@ func (s *Server) Connz(opts *ConnzOptions) (*Connz, error) {
// Fill in user if auth requested.
if auth {
ci.AuthorizedUser = client.getRawAuthUser()
// Add in account iff not the global account.
if client.acc != nil && (client.acc.Name != globalAccountName) {
ci.Account = client.acc.Name
if name := client.acc.GetName(); name != globalAccountName {
ci.Account = name
}
ci.JWT = client.opts.JWT
ci.IssuerKey = issuerForClient(client)
ci.Tags = client.tags
ci.NameTag = client.nameTag
ci.NameTag = client.acc.getNameTag()
}
client.mu.Unlock()
pconns[i] = ci
@@ -465,9 +464,11 @@ func (s *Server) Connz(opts *ConnzOptions) (*Connz, error) {
// Fill in user if auth requested.
if auth {
cc.AuthorizedUser = cc.user
// Add in account iff not the global account.
if cc.acc != _EMPTY_ && (cc.acc != globalAccountName) {
cc.Account = cc.acc
if acc, err := s.LookupAccount(cc.acc); err == nil {
cc.NameTag = acc.getNameTag()
}
}
}
pconns[i] = &cc.ConnInfo
@@ -926,21 +927,21 @@ type SubszOptions struct {
// SubDetail is for verbose information for subscriptions.
type SubDetail struct {
Account string `json:"account,omitempty"`
Subject string `json:"subject"`
Queue string `json:"qgroup,omitempty"`
Sid string `json:"sid"`
Msgs int64 `json:"msgs"`
Max int64 `json:"max,omitempty"`
Cid uint64 `json:"cid"`
Account string `json:"account,omitempty"`
AccountTag string `json:"account_tag,omitempty"`
Subject string `json:"subject"`
Queue string `json:"qgroup,omitempty"`
Sid string `json:"sid"`
Msgs int64 `json:"msgs"`
Max int64 `json:"max,omitempty"`
Cid uint64 `json:"cid"`
}
// Subscription client should be locked and guaranteed to be present.
func newSubDetail(sub *subscription) SubDetail {
sd := newClientSubDetail(sub)
if sub.client.acc != nil {
sd.Account = sub.client.acc.Name
}
sd.Account = sub.client.acc.GetName()
sd.AccountTag = sub.client.acc.getNameTag()
return sd
}
@@ -1228,6 +1229,7 @@ type Varz struct {
Subscriptions uint32 `json:"subscriptions"`
HTTPReqStats map[string]uint64 `json:"http_req_stats"`
ConfigLoadTime time.Time `json:"config_load_time"`
ConfigDigest string `json:"config_digest"`
Tags jwt.TagList `json:"tags,omitempty"`
TrustedOperatorsJwt []string `json:"trusted_operators_jwt,omitempty"`
TrustedOperatorsClaim []*jwt.OperatorClaims `json:"trusted_operators_claim,omitempty"`
@@ -1242,6 +1244,7 @@ type JetStreamVarz struct {
Config *JetStreamConfig `json:"config,omitempty"`
Stats *JetStreamStats `json:"stats,omitempty"`
Meta *MetaClusterInfo `json:"meta,omitempty"`
Limits *JSLimitOpts `json:"limits,omitempty"`
}
// ClusterOptsVarz contains monitoring cluster information
@@ -1467,6 +1470,7 @@ func (s *Server) updateJszVarz(js *jetStream, v *JetStreamVarz, doConfig bool) {
js.mu.RUnlock()
}
v.Stats = js.usageStats()
v.Limits = &s.getOpts().JetStreamLimits
if mg := js.getMetaGroup(); mg != nil {
if ci := s.raftNodeToClusterInfo(mg); ci != nil {
v.Meta = &MetaClusterInfo{Name: ci.Name, Leader: ci.Leader, Peer: getHash(ci.Leader), Size: mg.ClusterSize()}
@@ -1601,6 +1605,11 @@ func (s *Server) createVarz(pcpu float64, rss int64) *Varz {
TrustedOperatorsJwt: opts.operatorJWT,
TrustedOperatorsClaim: opts.TrustedOperators,
}
// If this is a leaf without cluster, reset the cluster name (that is otherwise
// set to the server name).
if s.leafNoCluster {
varz.Cluster.Name = _EMPTY_
}
if len(opts.Routes) > 0 {
varz.Cluster.URLs = urlsToStrings(opts.Routes)
}
@@ -1675,6 +1684,7 @@ func (s *Server) updateVarzConfigReloadableFields(v *Varz) {
v.TLSTimeout = opts.TLSTimeout
v.WriteDeadline = opts.WriteDeadline
v.ConfigLoadTime = s.configTime.UTC()
v.ConfigDigest = opts.configDigest
// Update route URLs if applicable
if s.varzUpdateRouteURLs {
v.Cluster.URLs = urlsToStrings(opts.Routes)
@@ -1844,6 +1854,7 @@ func (s *Server) HandleVarz(w http.ResponseWriter, r *http.Request) {
}
sv.Stats = v.Stats
sv.Meta = v.Meta
sv.Limits = v.Limits
s.mu.RUnlock()
}
@@ -2747,27 +2758,27 @@ func (s *Server) accountInfo(accName string) (*AccountInfo, error) {
mappings[src] = dests
}
return &AccountInfo{
accName,
a.updated.UTC(),
isSys,
a.expired.Load(),
!a.incomplete,
a.js != nil,
a.numLocalLeafNodes(),
a.numLocalConnections(),
a.sl.Count(),
mappings,
exports,
imports,
a.claimJWT,
a.Issuer,
a.nameTag,
a.tags,
claim,
vrIssues,
collectRevocations(a.usersRevoked),
a.sl.Stats(),
responses,
AccountName: accName,
LastUpdate: a.updated.UTC(),
IsSystem: isSys,
Expired: a.expired.Load(),
Complete: !a.incomplete,
JetStream: a.js != nil,
LeafCnt: a.numLocalLeafNodes(),
ClientCnt: a.numLocalConnections(),
SubCnt: a.sl.Count(),
Mappings: mappings,
Exports: exports,
Imports: imports,
Jwt: a.claimJWT,
IssuerKey: a.Issuer,
NameTag: a.getNameTagLocked(),
Tags: a.tags,
Claim: claim,
Vr: vrIssues,
RevokedUser: collectRevocations(a.usersRevoked),
Sublist: a.sl.Stats(),
Responses: responses,
}, nil
}
@@ -2791,6 +2802,7 @@ type HealthzOptions struct {
JSEnabled bool `json:"js-enabled,omitempty"`
JSEnabledOnly bool `json:"js-enabled-only,omitempty"`
JSServerOnly bool `json:"js-server-only,omitempty"`
JSMetaOnly bool `json:"js-meta-only,omitempty"`
Account string `json:"account,omitempty"`
Stream string `json:"stream,omitempty"`
Consumer string `json:"consumer,omitempty"`
@@ -2859,6 +2871,7 @@ type JSInfo struct {
Now time.Time `json:"now"`
Disabled bool `json:"disabled,omitempty"`
Config JetStreamConfig `json:"config,omitempty"`
Limits *JSLimitOpts `json:"limits,omitempty"`
JetStreamStats
Streams int `json:"streams"`
Consumers int `json:"consumers"`
@@ -3027,6 +3040,8 @@ func (s *Server) Jsz(opts *JSzOptions) (*JSInfo, error) {
return jsi, nil
}
jsi.Limits = &s.getOpts().JetStreamLimits
js.mu.RLock()
isLeader := js.cluster == nil || js.cluster.isLeader()
js.mu.RUnlock()
@@ -3268,6 +3283,10 @@ func (s *Server) HandleHealthz(w http.ResponseWriter, r *http.Request) {
if err != nil {
return
}
jsMetaOnly, err := decodeBool(w, r, "js-meta-only")
if err != nil {
return
}
includeDetails, err := decodeBool(w, r, "details")
if err != nil {
@@ -3278,6 +3297,7 @@ func (s *Server) HandleHealthz(w http.ResponseWriter, r *http.Request) {
JSEnabled: jsEnabled,
JSEnabledOnly: jsEnabledOnly,
JSServerOnly: jsServerOnly,
JSMetaOnly: jsMetaOnly,
Account: r.URL.Query().Get("account"),
Stream: r.URL.Query().Get("stream"),
Consumer: r.URL.Query().Get("consumer"),
@@ -3371,6 +3391,11 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
return health
}
// If JSServerOnly is true, then do not check further accounts, streams and consumers.
if opts.JSServerOnly {
return health
}
sopts := s.getOpts()
// If JS is not enabled in the config, we stop.
@@ -3549,6 +3574,7 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
}
return health
}
// If we are not current with the meta leader.
if !meta.Healthy() {
if !details {
@@ -3565,11 +3591,6 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
return health
}
// If JSServerOnly is true, then do not check further accounts, streams and consumers.
if opts.JSServerOnly {
return health
}
// Are we still recovering meta layer?
if js.isMetaRecovering() {
if !details {
@@ -3587,6 +3608,11 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
return health
}
// Skips doing full healthz and only checks the meta leader.
if opts.JSMetaOnly {
return health
}
// Range across all accounts, the streams assigned to them, and the consumers.
// If they are assigned to this server check their status.
ourID := meta.ID()
+337 -86
View File
@@ -191,6 +191,18 @@ const (
mqttRetainedTransferTimeout = 10 * time.Second
)
const (
sparkbNBIRTH = "NBIRTH"
sparkbDBIRTH = "DBIRTH"
sparkbNDEATH = "NDEATH"
sparkbDDEATH = "DDEATH"
)
var (
sparkbNamespaceTopicPrefix = []byte("spBv1.0/")
sparkbCertificatesTopicPrefix = []byte("$sparkplug/certificates/")
)
var (
mqttPingResponse = []byte{mqttPacketPingResp, 0x0}
mqttProtoName = []byte("MQTT")
@@ -456,8 +468,22 @@ type mqttPublish struct {
// When we submit a PUBREL for delivery, we add a "Nmqtt-PubRel" header that
// contains the PI.
const (
mqttNatsHeader = "Nmqtt-Pub"
mqttNatsPubRelHeader = "Nmqtt-PubRel"
// NATS header that indicates that the message originated from MQTT and
// stores the published message QOS.
mqttNatsHeader = "Nmqtt-Pub"
// NATS headers to store retained message metadata (along with the original
// message as binary).
mqttNatsRetainedMessageTopic = "Nmqtt-RTopic"
mqttNatsRetainedMessageOrigin = "Nmqtt-ROrigin"
mqttNatsRetainedMessageFlags = "Nmqtt-RFlags"
mqttNatsRetainedMessageSource = "Nmqtt-RSource"
// NATS header that indicates that the message is an MQTT PubRel and stores
// the PI.
mqttNatsPubRelHeader = "Nmqtt-PubRel"
// NATS headers to store the original MQTT subject and the subject mapping.
mqttNatsHeaderSubject = "Nmqtt-Subject"
mqttNatsHeaderMapped = "Nmqtt-Mapped"
)
@@ -1636,13 +1662,16 @@ func (jsa *mqttJSA) newRequestExMulti(kind, subject, cidHash string, hdrs []int,
}
func (jsa *mqttJSA) sendAck(ackSubject string) {
if ackSubject == _EMPTY_ {
return
}
// We pass -1 for the hdr so that the send loop does not need to
// add the "client info" header. This is not a JS API request per se.
jsa.sendq.push(&mqttJSPubMsg{subj: ackSubject, hdr: -1})
jsa.sendMsg(ackSubject, nil)
}
func (jsa *mqttJSA) sendMsg(subj string, msg []byte) {
if subj == _EMPTY_ {
return
}
jsa.sendq.push(&mqttJSPubMsg{subj: subj, msg: msg, hdr: -1})
}
func (jsa *mqttJSA) createEphemeralConsumer(cfg *CreateConsumerRequest) (*JSApiConsumerCreateResponse, error) {
@@ -1673,13 +1702,6 @@ func (jsa *mqttJSA) createDurableConsumer(cfg *CreateConsumerRequest) (*JSApiCon
return ccr, ccr.ToError()
}
func (jsa *mqttJSA) sendMsg(subj string, msg []byte) {
if subj == _EMPTY_ {
return
}
jsa.sendq.push(&mqttJSPubMsg{subj: subj, msg: msg, hdr: -1})
}
// if noWait is specified, does not wait for the JS response, returns nil
func (jsa *mqttJSA) deleteConsumer(streamName, consName string, noWait bool) (*JSApiConsumerDeleteResponse, error) {
subj := fmt.Sprintf(JSApiConsumerDeleteT, streamName, consName)
@@ -1890,61 +1912,61 @@ func (as *mqttAccountSessionManager) processJSAPIReplies(_ *subscription, pc *cl
case mqttJSAStreamCreate:
var resp = &JSApiStreamCreateResponse{}
if err := json.Unmarshal(msg, resp); err != nil {
resp.Error = NewJSInvalidJSONError()
resp.Error = NewJSInvalidJSONError(err)
}
out(resp)
case mqttJSAStreamUpdate:
var resp = &JSApiStreamUpdateResponse{}
if err := json.Unmarshal(msg, resp); err != nil {
resp.Error = NewJSInvalidJSONError()
resp.Error = NewJSInvalidJSONError(err)
}
out(resp)
case mqttJSAStreamLookup:
var resp = &JSApiStreamInfoResponse{}
if err := json.Unmarshal(msg, &resp); err != nil {
resp.Error = NewJSInvalidJSONError()
resp.Error = NewJSInvalidJSONError(err)
}
out(resp)
case mqttJSAStreamDel:
var resp = &JSApiStreamDeleteResponse{}
if err := json.Unmarshal(msg, &resp); err != nil {
resp.Error = NewJSInvalidJSONError()
resp.Error = NewJSInvalidJSONError(err)
}
out(resp)
case mqttJSAConsumerCreate:
var resp = &JSApiConsumerCreateResponse{}
if err := json.Unmarshal(msg, resp); err != nil {
resp.Error = NewJSInvalidJSONError()
resp.Error = NewJSInvalidJSONError(err)
}
out(resp)
case mqttJSAConsumerDel:
var resp = &JSApiConsumerDeleteResponse{}
if err := json.Unmarshal(msg, resp); err != nil {
resp.Error = NewJSInvalidJSONError()
resp.Error = NewJSInvalidJSONError(err)
}
out(resp)
case mqttJSAMsgStore, mqttJSASessPersist:
var resp = &JSPubAckResponse{}
if err := json.Unmarshal(msg, resp); err != nil {
resp.Error = NewJSInvalidJSONError()
resp.Error = NewJSInvalidJSONError(err)
}
out(resp)
case mqttJSAMsgLoad:
var resp = &JSApiMsgGetResponse{}
if err := json.Unmarshal(msg, &resp); err != nil {
resp.Error = NewJSInvalidJSONError()
resp.Error = NewJSInvalidJSONError(err)
}
out(resp)
case mqttJSAStreamNames:
var resp = &JSApiStreamNamesResponse{}
if err := json.Unmarshal(msg, resp); err != nil {
resp.Error = NewJSInvalidJSONError()
resp.Error = NewJSInvalidJSONError(err)
}
out(resp)
case mqttJSAMsgDelete:
var resp = &JSApiMsgDeleteResponse{}
if err := json.Unmarshal(msg, resp); err != nil {
resp.Error = NewJSInvalidJSONError()
resp.Error = NewJSInvalidJSONError(err)
}
out(resp)
default:
@@ -1957,9 +1979,9 @@ func (as *mqttAccountSessionManager) processJSAPIReplies(_ *subscription, pc *cl
// Run from various go routines (JS consumer, etc..).
// No lock held on entry.
func (as *mqttAccountSessionManager) processRetainedMsg(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
_, msg := c.msgParts(rmsg)
rm := &mqttRetainedMsg{}
if err := json.Unmarshal(msg, rm); err != nil {
h, m := c.msgParts(rmsg)
rm, err := mqttDecodeRetainedMessage(h, m)
if err != nil {
return
}
// If lastSeq is 0 (nothing to recover, or done doing it) and this is
@@ -2791,20 +2813,95 @@ func (as *mqttAccountSessionManager) loadRetainedMessages(subjects map[string]st
w.Warnf("failed to load retained message for subject %q: %v", ss[i], err)
continue
}
var rm mqttRetainedMsg
if err := json.Unmarshal(result.Message.Data, &rm); err != nil {
rm, err := mqttDecodeRetainedMessage(result.Message.Header, result.Message.Data)
if err != nil {
w.Warnf("failed to decode retained message for subject %q: %v", ss[i], err)
continue
}
// Add the loaded retained message to the cache, and to the results map.
key := ss[i][len(mqttRetainedMsgsStreamSubject):]
as.setCachedRetainedMsg(key, &rm, false, false)
rms[key] = &rm
as.setCachedRetainedMsg(key, rm, false, false)
rms[key] = rm
}
return rms
}
// Composes a NATS message for a storeable mqttRetainedMsg.
func mqttEncodeRetainedMessage(rm *mqttRetainedMsg) (natsMsg []byte, headerLen int) {
// No need to encode the subject, we can restore it from topic.
l := len(hdrLine)
l += len(mqttNatsRetainedMessageTopic) + 1 + len(rm.Topic) + 2 // 1 byte for ':', 2 bytes for CRLF
if rm.Origin != _EMPTY_ {
l += len(mqttNatsRetainedMessageOrigin) + 1 + len(rm.Origin) + 2 // 1 byte for ':', 2 bytes for CRLF
}
if rm.Source != _EMPTY_ {
l += len(mqttNatsRetainedMessageSource) + 1 + len(rm.Source) + 2 // 1 byte for ':', 2 bytes for CRLF
}
l += len(mqttNatsRetainedMessageFlags) + 1 + 2 + 2 // 1 byte for ':', 2 bytes for the flags, 2 bytes for CRLF
l += 2 // 2 bytes for the extra CRLF after the header
l += len(rm.Msg)
buf := bytes.NewBuffer(make([]byte, 0, l))
buf.WriteString(hdrLine)
buf.WriteString(mqttNatsRetainedMessageTopic)
buf.WriteByte(':')
buf.WriteString(rm.Topic)
buf.WriteString(_CRLF_)
buf.WriteString(mqttNatsRetainedMessageFlags)
buf.WriteByte(':')
buf.WriteString(strconv.FormatUint(uint64(rm.Flags), 16))
buf.WriteString(_CRLF_)
if rm.Origin != _EMPTY_ {
buf.WriteString(mqttNatsRetainedMessageOrigin)
buf.WriteByte(':')
buf.WriteString(rm.Origin)
buf.WriteString(_CRLF_)
}
if rm.Source != _EMPTY_ {
buf.WriteString(mqttNatsRetainedMessageSource)
buf.WriteByte(':')
buf.WriteString(rm.Source)
buf.WriteString(_CRLF_)
}
// End of header, finalize
buf.WriteString(_CRLF_)
headerLen = buf.Len()
buf.Write(rm.Msg)
return buf.Bytes(), headerLen
}
func mqttDecodeRetainedMessage(h, m []byte) (*mqttRetainedMsg, error) {
fHeader := getHeader(mqttNatsRetainedMessageFlags, h)
if len(fHeader) > 0 {
flags, err := strconv.ParseUint(string(fHeader), 16, 8)
if err != nil {
return nil, fmt.Errorf("invalid retained message flags: %v", err)
}
topic := getHeader(mqttNatsRetainedMessageTopic, h)
subj, _ := mqttToNATSSubjectConversion(topic, false)
return &mqttRetainedMsg{
Flags: byte(flags),
Subject: string(subj),
Topic: string(topic),
Origin: string(getHeader(mqttNatsRetainedMessageOrigin, h)),
Source: string(getHeader(mqttNatsRetainedMessageSource, h)),
Msg: m,
}, nil
} else {
var rm mqttRetainedMsg
if err := json.Unmarshal(m, &rm); err != nil {
return nil, err
}
return &rm, nil
}
}
// Creates the session stream (limit msgs of 1) for this client ID if it does
// not already exist. If it exists, recover the single record to rebuild the
// state of the session. If there is a session record but this session is not
@@ -2951,7 +3048,9 @@ func (as *mqttAccountSessionManager) transferRetainedToPerKeySubjectStream(log *
return err
}
// Unmarshal the message so that we can obtain the subject name.
// Unmarshal the message so that we can obtain the subject name. Do not
// use mqttDecodeRetainedMessage() here because these messages are from
// older versions, and contain the full JSON encoding in payload.
var rmsg mqttRetainedMsg
if err = json.Unmarshal(smsg.Data, &rmsg); err == nil {
// Store the message again, this time with the new per-key subject.
@@ -3331,7 +3430,7 @@ func (sess *mqttSession) untrackPublish(pi uint16) (jsAckSubject string) {
return ack.jsAckSubject
}
// trackPubRel is invoked in 2 cases: (a) when we receive a PUBREC and we need
// trackAsPubRel is invoked in 2 cases: (a) when we receive a PUBREC and we need
// to change from tracking the PI as a PUBLISH to a PUBREL; and (b) when we
// attempt to deliver the PUBREL to record the JS ack subject for it.
//
@@ -3547,7 +3646,7 @@ func (c *client) mqttParseConnect(r *mqttReader, hasMappings bool) (byte, *mqttC
cp.will.mapped = c.pa.mapped
// We also now need to map the original MQTT topic to the new topic
// based on the new subject.
topic = natsSubjectToMQTTTopic(string(cp.will.subject))
topic = natsSubjectToMQTTTopic(cp.will.subject)
}
// Reset those now.
c.pa.subject, c.pa.mapped = nil, nil
@@ -3644,7 +3743,7 @@ func (s *Server) mqttProcessConnect(c *client, cp *mqttConnectProto, trace bool)
c.authViolation()
return ErrAuthentication
}
// Now that we are are authenticated, we have the client bound to the account.
// Now that we are authenticated, we have the client bound to the account.
// Get the account's level MQTT sessions manager. If it does not exists yet,
// this will create it along with the streams where sessions and messages
// are stored.
@@ -3896,7 +3995,7 @@ func (c *client) mqttParsePub(r *mqttReader, pl int, pp *mqttPublish, hasMapping
pp.mapped = c.pa.mapped
// We also now need to map the original MQTT topic to the new topic
// based on the new subject.
pp.topic = natsSubjectToMQTTTopic(string(pp.subject))
pp.topic = natsSubjectToMQTTTopic(pp.subject)
}
// Reset those now.
c.pa.subject, c.pa.mapped = nil, nil
@@ -3952,7 +4051,7 @@ func mqttNewDeliverableMessage(pp *mqttPublish, encodePP bool) (natsMsg []byte,
size := len(hdrLine) +
len(mqttNatsHeader) + 2 + 2 + // 2 for ':<qos>', and 2 for CRLF
2 + // end-of-header CRLF
len(pp.msg)
pp.sz
if encodePP {
size += len(mqttNatsHeaderSubject) + 1 + // +1 for ':'
len(pp.subject) + 2 // 2 for CRLF
@@ -4160,7 +4259,7 @@ func (s *Server) mqttProcessPubRel(c *client, pi uint16, trace bool) error {
}
pp := &mqttPublish{
topic: natsSubjectToMQTTTopic(string(h.subject)),
topic: natsSubjectToMQTTTopic(h.subject),
subject: h.subject,
mapped: h.mapped,
msg: stored.Data,
@@ -4179,50 +4278,106 @@ func (s *Server) mqttProcessPubRel(c *client, pi uint16, trace bool) error {
// Invoked from the MQTT publisher's readLoop. No client lock is held on entry.
func (c *client) mqttHandlePubRetain() {
pp := c.mqtt.pp
if !mqttIsRetained(pp.flags) {
retainMQTT := mqttIsRetained(pp.flags)
isBirth, _, isCertificate := sparkbParseBirthDeathTopic(pp.topic)
retainSparkbBirth := isBirth && !isCertificate
// [tck-id-topics-nbirth-mqtt] NBIRTH messages MUST be published with MQTT
// QoS equal to 0 and retain equal to false.
//
// [tck-id-conformance-mqtt-aware-nbirth-mqtt-retain] A Sparkplug Aware MQTT
// Server MUST make NBIRTH messages available on the topic:
// $sparkplug/certificates/namespace/group_id/NBIRTH/edge_node_id with the
// MQTT retain flag set to true.
if retainMQTT == retainSparkbBirth {
// (retainSparkbBirth && retainMQTT) : not valid, so ignore altogether.
// (!retainSparkbBirth && !retainMQTT) : nothing to do.
return
}
key := string(pp.subject)
asm := c.mqtt.asm
// Spec [MQTT-3.3.1-11]. Payload of size 0 removes the retained message,
// but should still be delivered as a normal message.
key := string(pp.subject)
// Always clear the retain flag to deliver a normal published message.
defer func() {
pp.flags &= ^mqttPubFlagRetain
}()
// Spec [MQTT-3.3.1-11]. Payload of size 0 removes the retained message, but
// should still be delivered as a normal message.
if pp.sz == 0 {
if seqToRemove := asm.handleRetainedMsgDel(key, 0); seqToRemove > 0 {
asm.deleteRetainedMsg(seqToRemove)
asm.notifyRetainedMsgDeleted(key, seqToRemove)
}
} else {
// Spec [MQTT-3.3.1-5]. Store the retained message with its QoS.
// When coming from a publish protocol, `pp` is referencing a stack
// variable that itself possibly references the read buffer.
rm := &mqttRetainedMsg{
Origin: asm.jsa.id,
Subject: key,
Topic: string(pp.topic),
Msg: pp.msg,
Flags: pp.flags,
Source: c.opts.Username,
}
rmBytes, _ := json.Marshal(rm)
smr, err := asm.jsa.storeMsg(mqttRetainedMsgsStreamSubject+key, -1, rmBytes)
if err == nil {
// Update the new sequence
rf := &mqttRetainedMsgRef{
sseq: smr.Sequence,
}
// Add/update the map
asm.handleRetainedMsg(key, rf, rm, true) // will copy the payload bytes if needs to update rmsCache
} else {
c.mu.Lock()
acc := c.acc
c.mu.Unlock()
c.Errorf("unable to store retained message for account %q, subject %q: %v",
acc.GetName(), key, err)
}
return
}
// Clear the retain flag for a normal published message.
pp.flags &= ^mqttPubFlagRetain
rm := &mqttRetainedMsg{
Origin: asm.jsa.id,
Msg: pp.msg, // will copy these bytes later as we process rm.
Flags: pp.flags,
Source: c.opts.Username,
}
if retainSparkbBirth {
// [tck-id-conformance-mqtt-aware-store] A Sparkplug Aware MQTT Server
// MUST store NBIRTH and DBIRTH messages as they pass through the MQTT
// Server.
//
// [tck-id-conformance-mqtt-aware-nbirth-mqtt-topic]. A Sparkplug Aware
// MQTT Server MUST make NBIRTH messages available on a topic of the
// form: $sparkplug/certificates/namespace/group_id/NBIRTH/edge_node_id
//
// [tck-id-conformance-mqtt-aware-dbirth-mqtt-topic] A Sparkplug Aware
// MQTT Server MUST make DBIRTH messages available on a topic of the
// form:
// $sparkplug/certificates/namespace/group_id/DBIRTH/edge_node_id/device_id
topic := append(sparkbCertificatesTopicPrefix, pp.topic...)
subject, _ := mqttTopicToNATSPubSubject(topic)
rm.Topic = string(topic)
rm.Subject = string(subject)
// will use to save the retained message.
key = string(subject)
// Store the retained message with the RETAIN flag set.
rm.Flags |= mqttPubFlagRetain
// Copy the payload out of pp since we will be sending the message
// asynchronously.
msg := make([]byte, pp.sz)
copy(msg, pp.msg[:pp.sz])
asm.jsa.sendMsg(key, msg)
} else { // isRetained
// Spec [MQTT-3.3.1-5]. Store the retained message with its QoS.
//
// When coming from a publish protocol, `pp` is referencing a stack
// variable that itself possibly references the read buffer.
rm.Topic = string(pp.topic)
}
// Set the key to the subject of the message for retained, or the composed
// $sparkplug subject for sparkB.
rm.Subject = key
rmBytes, hdr := mqttEncodeRetainedMessage(rm) // will copy the payload bytes
smr, err := asm.jsa.storeMsg(mqttRetainedMsgsStreamSubject+key, hdr, rmBytes)
if err == nil {
// Update the new sequence.
rf := &mqttRetainedMsgRef{
sseq: smr.Sequence,
}
// Add/update the map. `true` to copy the payload bytes if needs to
// update rmsCache.
asm.handleRetainedMsg(key, rf, rm, true)
} else {
c.mu.Lock()
acc := c.acc
c.mu.Unlock()
c.Errorf("unable to store retained message for account %q, subject %q: %v",
acc.GetName(), key, err)
}
}
// After a config reload, it is possible that the source of a publish retained
@@ -4286,8 +4441,8 @@ func (s *Server) mqttCheckPubRetainedPerms() {
if err != nil || jsm == nil {
continue
}
var rm mqttRetainedMsg
if err := json.Unmarshal(jsm.Data, &rm); err != nil {
rm, err := mqttDecodeRetainedMessage(jsm.Header, jsm.Data)
if err != nil {
continue
}
if rm.Source == _EMPTY_ {
@@ -4484,6 +4639,32 @@ func mqttIsRetained(flags byte) bool {
return flags&mqttPubFlagRetain != 0
}
func sparkbParseBirthDeathTopic(topic []byte) (isBirth, isDeath, isCertificate bool) {
if bytes.HasPrefix(topic, sparkbCertificatesTopicPrefix) {
isCertificate = true
topic = topic[len(sparkbCertificatesTopicPrefix):]
}
if !bytes.HasPrefix(topic, sparkbNamespaceTopicPrefix) {
return false, false, false
}
topic = topic[len(sparkbNamespaceTopicPrefix):]
parts := bytes.Split(topic, []byte{'/'})
if len(parts) < 3 || len(parts) > 4 {
return false, false, false
}
typ := bytesToString(parts[1])
switch typ {
case sparkbNBIRTH, sparkbDBIRTH:
isBirth = true
case sparkbNDEATH, sparkbDDEATH:
isDeath = true
default:
return false, false, false
}
return isBirth, isDeath, isCertificate
}
//////////////////////////////////////////////////////////////////////////////
//
// SUBSCRIBE related functions
@@ -4631,18 +4812,16 @@ func mqttDeliverMsgCbQoS0(sub *subscription, pc *client, _ *Account, subject, re
topic = pc.mqtt.pp.topic
// Check for service imports where subject mapping is in play.
if len(pc.pa.mapped) > 0 && len(pc.pa.psi) > 0 {
topic = natsSubjectToMQTTTopic(subject)
topic = natsSubjectStrToMQTTTopic(subject)
}
} else {
// Non MQTT client, could be NATS publisher, or ROUTER, etc..
h := mqttParsePublishNATSHeader(hdr)
// If the message does not have the MQTT header, it is not a MQTT and
// should be delivered here, at QOS0. If it does have the header, we
// need to lock the session to check the sub QoS, and then ignore the
// message if the Sub wants higher QOS delivery. It will be delivered by
// mqttDeliverMsgCbQoS12.
// Check the subscription's QoS. If the message was published with a
// QoS>0 (in the header) and the sub has the QoS>0 then the message will
// be delivered by mqttDeliverMsgCbQoS12.
if subQoS > 0 && h != nil && h.qos > 0 {
return
}
@@ -4652,7 +4831,7 @@ func mqttDeliverMsgCbQoS0(sub *subscription, pc *client, _ *Account, subject, re
if len(msg) > mqttMaxPayloadSize {
msg = msg[:mqttMaxPayloadSize]
}
topic = natsSubjectToMQTTTopic(subject)
topic = natsSubjectStrToMQTTTopic(subject)
}
// Message never has a packet identifier nor is marked as duplicate.
@@ -4714,7 +4893,7 @@ func mqttDeliverMsgCbQoS12(sub *subscription, pc *client, _ *Account, subject, r
// Check for reserved subject violation. If so, we will send the ack to
// remove the message, and do nothing else.
strippedSubj := string(subject[len(mqttStreamSubjectPrefix):])
strippedSubj := subject[len(mqttStreamSubjectPrefix):]
if mqttMustIgnoreForReservedSub(sub, strippedSubj) {
sess.mu.Unlock()
sess.jsa.sendAck(reply)
@@ -4731,7 +4910,7 @@ func mqttDeliverMsgCbQoS12(sub *subscription, pc *client, _ *Account, subject, r
return
}
originalTopic := natsSubjectToMQTTTopic(strippedSubj)
originalTopic := natsSubjectStrToMQTTTopic(strippedSubj)
pc.mqttEnqueuePublishMsgTo(cc, sub, pi, qos, dup, originalTopic, msg)
}
@@ -4789,10 +4968,78 @@ func isMQTTReservedSubscription(subject string) bool {
return false
}
func sparkbReplaceDeathTimestamp(msg []byte) []byte {
const VARINT = 0
const TIMESTAMP = 1
orig := msg
buf := bytes.NewBuffer(make([]byte, 0, len(msg)+16)) // 16 bytes should be enough if we need to add a timestamp
writeDeathTimestamp := func() {
// [tck-id-conformance-mqtt-aware-ndeath-timestamp] A Sparkplug Aware
// MQTT Server MAY replace the timestamp of NDEATH messages. If it does,
// it MUST set the timestamp to the UTC time at which it attempts to
// deliver the NDEATH to subscribed clients
//
// sparkB spec: 6.4.1. Google Protocol Buffer Schema
// optional uint64 timestamp = 1; // Timestamp at message sending time
//
// SparkplugB timestamps are milliseconds since epoch, represented as
// uint64 in go, transmitted as protobuf varint.
ts := uint64(time.Now().UnixMilli())
buf.Write(protoEncodeVarint(TIMESTAMP<<3 | VARINT))
buf.Write(protoEncodeVarint(ts))
}
for len(msg) > 0 {
fieldNumericID, fieldType, size, err := protoScanField(msg)
if err != nil {
return orig
}
if fieldType != VARINT || fieldNumericID != TIMESTAMP {
// Add the field as is
buf.Write(msg[:size])
msg = msg[size:]
continue
}
writeDeathTimestamp()
// Add the rest of the message as is, we are done
buf.Write(msg[size:])
return buf.Bytes()
}
// Add timestamp if we did not find one.
writeDeathTimestamp()
return buf.Bytes()
}
// Common function to mqtt delivery callbacks to serialize and send the message
// to the `cc` client.
func (c *client) mqttEnqueuePublishMsgTo(cc *client, sub *subscription, pi uint16, qos byte, dup bool, topic, msg []byte) {
flags, headerBytes := mqttMakePublishHeader(pi, qos, dup, false, topic, len(msg))
// [tck-id-conformance-mqtt-aware-nbirth-mqtt-retain] A Sparkplug Aware
// MQTT Server MUST make NBIRTH messages available on the topic:
// $sparkplug/certificates/namespace/group_id/NBIRTH/edge_node_id with
// the MQTT retain flag set to true
//
// [tck-id-conformance-mqtt-aware-dbirth-mqtt-retain] A Sparkplug Aware
// MQTT Server MUST make DBIRTH messages available on the topic:
// $sparkplug/certificates/namespace/group_id/DBIRTH/edge_node_id/device_id
// with the MQTT retain flag set to true
//
// $sparkplug/certificates messages are sent as NATS messages, so we
// need to add the retain flag when sending them to MQTT clients.
retain := false
isBirth, isDeath, isCertificate := sparkbParseBirthDeathTopic(topic)
if isBirth && qos == 0 {
retain = isCertificate
} else if isDeath && !isCertificate {
msg = sparkbReplaceDeathTimestamp(msg)
}
flags, headerBytes := mqttMakePublishHeader(pi, qos, dup, retain, topic, len(msg))
cc.mu.Lock()
if sub.mqtt.prm != nil {
@@ -5351,8 +5598,12 @@ func mqttToNATSSubjectConversion(mt []byte, wcOk bool) ([]byte, error) {
// Converts a NATS subject to MQTT topic. This is for publish
// messages only, so there is no checking for wildcards.
// Rules are reversed of mqttToNATSSubjectConversion.
func natsSubjectToMQTTTopic(subject string) []byte {
topic := []byte(subject)
func natsSubjectStrToMQTTTopic(subject string) []byte {
return natsSubjectToMQTTTopic(stringToBytes(subject))
}
func natsSubjectToMQTTTopic(subject []byte) []byte {
topic := make([]byte, len(subject))
end := len(subject) - 1
var j int
for i := 0; i < len(subject); i++ {
+846
View File
@@ -0,0 +1,846 @@
// 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 server
import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
"strconv"
"strings"
"sync/atomic"
"time"
)
const (
MsgTraceDest = "Nats-Trace-Dest"
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.
traceParentHdr = "traceparent"
)
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]
}
ct := getCompressionType(getHdrVal(acceptEncodingHeader))
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)
// 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 desactivate the traceParentHdr so that if the message
// is routed, it does possibly trigger a trace there.
disableTraceHeaders(c, hdr)
return nil
}
}
c.pa.trace = &msgTrace{
srv: c.srv,
acc: acc,
oan: oan,
dest: dest,
ct: ct,
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, but if the header is found, it is rewritten in lower case
// as suggested by the spec, but also to make it easier to disable the header
// when needed.
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
}
traceDestHdrAsBytes := stringToBytes(MsgTraceDest)
traceParentHdrAsBytes := stringToBytes(traceParentHdr)
crLFAsBytes := stringToBytes(CR_LF)
dashAsBytes := stringToBytes("-")
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
valStart := i
nl := bytes.Index(hdr[valStart:], crLFAsBytes)
if nl < 0 {
break
}
if len(key) > 0 {
val := bytes.Trim(hdr[valStart:valStart+nl], " \t")
vals = append(vals, val)
// Check for the external trace header.
if bytes.EqualFold(key, traceParentHdrAsBytes) {
// Rewrite the header using lower case if needed.
if !bytes.Equal(key, traceParentHdrAsBytes) {
copy(hdr[keyStart:], 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.
if !traceParentHdrFound {
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
}
}
}
}
// Add to the keys with the external trace header in lower case.
keys = append(keys, traceParentHdrAsBytes)
} else {
// Is the key the Nats-Trace-Dest header?
if bytes.EqualFold(key, traceDestHdrAsBytes) {
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)
}
// Will look for the MsgTraceSendTo and traceParentHdr headers and change the first
// character to an 'X' so that if this message is sent to a remote, the remote
// will not initialize tracing since it won't find the actual trace headers.
// The function returns the position of the headers so it can efficiently be
// re-enabled by calling enableTraceHeaders.
// Note that if `msg` can be either the header alone or the full message
// (header and payload). This function will use c.pa.hdr to limit the
// search to the header section alone.
func disableTraceHeaders(c *client, msg []byte) []int {
// Code largely copied from getHeader(), except that we don't need the value
if c.pa.hdr <= 0 {
return []int{-1, -1}
}
hdr := msg[:c.pa.hdr]
headers := [2]string{MsgTraceDest, traceParentHdr}
positions := [2]int{-1, -1}
for i := 0; i < 2; i++ {
key := stringToBytes(headers[i])
pos := bytes.Index(hdr, key)
if pos < 0 {
continue
}
// Make sure this key does not have additional prefix.
if pos < 2 || hdr[pos-1] != '\n' || hdr[pos-2] != '\r' {
continue
}
index := pos + len(key)
if index >= len(hdr) {
continue
}
if hdr[index] != ':' {
continue
}
// Disable the trace by altering the first character of the header
hdr[pos] = 'X'
positions[i] = pos
}
// Return the positions of those characters so we can re-enable the headers.
return positions[:2]
}
// Changes back the character at the given position `pos` in the `msg`
// byte slice to the first character of the MsgTraceSendTo header.
func enableTraceHeaders(msg []byte, positions []int) {
firstChar := [2]byte{MsgTraceDest[0], traceParentHdr[0]}
for i, pos := range positions {
if pos == -1 {
continue
}
msg[pos] = firstChar[i]
}
}
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)
}
+433 -124
View File
@@ -1,4 +1,4 @@
// Copyright 2012-2023 The NATS Authors
// 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
@@ -145,22 +145,33 @@ type RemoteGatewayOpts struct {
// LeafNodeOpts are options for a given server to accept leaf node connections and/or connect to a remote cluster.
type LeafNodeOpts struct {
Host string `json:"addr,omitempty"`
Port int `json:"port,omitempty"`
Username string `json:"-"`
Password string `json:"-"`
Nkey string `json:"-"`
Account string `json:"-"`
Users []*User `json:"-"`
AuthTimeout float64 `json:"auth_timeout,omitempty"`
TLSConfig *tls.Config `json:"-"`
TLSTimeout float64 `json:"tls_timeout,omitempty"`
TLSMap bool `json:"-"`
TLSPinnedCerts PinnedCertSet `json:"-"`
TLSHandshakeFirst bool `json:"-"`
Advertise string `json:"-"`
NoAdvertise bool `json:"-"`
ReconnectInterval time.Duration `json:"-"`
Host string `json:"addr,omitempty"`
Port int `json:"port,omitempty"`
Username string `json:"-"`
Password string `json:"-"`
Nkey string `json:"-"`
Account string `json:"-"`
Users []*User `json:"-"`
AuthTimeout float64 `json:"auth_timeout,omitempty"`
TLSConfig *tls.Config `json:"-"`
TLSTimeout float64 `json:"tls_timeout,omitempty"`
TLSMap bool `json:"-"`
TLSPinnedCerts PinnedCertSet `json:"-"`
// When set to true, the server will perform the TLS handshake before
// sending the INFO protocol. For remote leafnodes that are not configured
// with a similar option, their connection will fail with some sort
// of timeout or EOF error since they are expecting to receive an
// INFO protocol first.
TLSHandshakeFirst bool `json:"-"`
// If TLSHandshakeFirst is true and this value is strictly positive,
// the server will wait for that amount of time for the TLS handshake
// to start before falling back to previous behavior of sending the
// INFO protocol first. It allows for a mix of newer remote leafnodes
// that can require a TLS handshake first, and older that can't.
TLSHandshakeFirstFallback time.Duration `json:"-"`
Advertise string `json:"-"`
NoAdvertise bool `json:"-"`
ReconnectInterval time.Duration `json:"-"`
// Compression options
Compression CompressionOpts `json:"-"`
@@ -230,13 +241,24 @@ type RemoteLeafOpts struct {
// not be able to work. This tells the system to migrate the leaders away from this server.
// This only changes leader for R>1 assets.
JetStreamClusterMigrate bool `json:"jetstream_cluster_migrate,omitempty"`
// If JetStreamClusterMigrate is set to true, this is the time after which the leader
// will be migrated away from this server if still disconnected.
JetStreamClusterMigrateDelay time.Duration `json:"jetstream_cluster_migrate_delay,omitempty"`
}
type JSLimitOpts struct {
MaxRequestBatch int
MaxAckPending int
MaxHAAssets int
Duplicates time.Duration
MaxRequestBatch int `json:"max_request_batch,omitempty"`
MaxAckPending int `json:"max_ack_pending,omitempty"`
MaxHAAssets int `json:"max_ha_assets,omitempty"`
Duplicates time.Duration `json:"max_duplicate_window,omitempty"`
}
type JSTpmOpts struct {
KeysFile string
KeyPassword string
SrkPassword string
Pcr int
}
// AuthCallout option used to map external AuthN to NATS based AuthZ.
@@ -250,6 +272,9 @@ type AuthCallout struct {
// XKey is a public xkey for the authorization service.
// This will enable encryption for server requests and the authorization service responses.
XKey string
// AllowedAccounts that will be delegated to the auth service.
// If empty then all accounts will be delegated.
AllowedAccounts []string
}
// Options block for nats-server.
@@ -300,6 +325,7 @@ type Options struct {
Gateway GatewayOpts `json:"gateway,omitempty"`
LeafNode LeafNodeOpts `json:"leaf,omitempty"`
JetStream bool `json:"jetstream"`
JetStreamStrict bool `json:"-"`
JetStreamMaxMemory int64 `json:"-"`
JetStreamMaxStore int64 `json:"-"`
JetStreamDomain string `json:"-"`
@@ -309,8 +335,11 @@ type Options struct {
JetStreamCipher StoreCipher `json:"-"`
JetStreamUniqueTag string
JetStreamLimits JSLimitOpts
JetStreamTpm JSTpmOpts
JetStreamMaxCatchup int64
JetStreamRequestQueueLimit int64
StreamMaxBufferedMsgs int `json:"-"`
StreamMaxBufferedSize int64 `json:"-"`
StoreDir string `json:"-"`
SyncInterval time.Duration `json:"-"`
SyncAlways bool `json:"-"`
@@ -409,7 +438,7 @@ type Options struct {
// private fields, used for testing
gatewaysSolicitDelay time.Duration
routeProto int
overrideProto int
// JetStream
maxMemSet bool
@@ -421,6 +450,9 @@ type Options struct {
// Used to mark that we had a top level authorization block.
authBlockDefined bool
// configDigest represents the state of configuration.
configDigest string
}
// WebsocketOpts are options for websocket
@@ -441,6 +473,26 @@ type WebsocketOpts struct {
// "jwt" specified in the CONNECT options is missing or empty.
JWTCookie string
// Name of the cookie, which if present in WebSocket upgrade headers,
// will be treated as Username during CONNECT phase as long as
// "user" specified in the CONNECT options is missing or empty.
UsernameCookie string
// Name of the cookie, which if present in WebSocket upgrade headers,
// will be treated as Password during CONNECT phase as long as
// "pass" specified in the CONNECT options is missing or empty.
PasswordCookie string
// Name of the cookie, which if present in WebSocket upgrade headers,
// will be treated as Token during CONNECT phase as long as
// "auth_token" specified in the CONNECT options is missing or empty.
// Note that when this is useful for passing a JWT to an cuth callout
// when the server uses delegated authentication ("operator mode") or
// when using delegated authentication, but the auth callout validates some
// other JWT or string. Note that this does map to an actual server-wide
// "auth_token", note that using it for that purpose is greatly discouraged.
TokenCookie string
// Authentication section. If anything is configured in this section,
// it will override the authorization configuration of regular clients.
Username string
@@ -485,6 +537,10 @@ type WebsocketOpts struct {
// time needed for the TLS Handshake.
HandshakeTimeout time.Duration
// Headers to be added to the upgrade response.
// Useful for adding custom headers like Strict-Transport-Security.
Headers map[string]string
// Snapshot of configured TLS options.
tlsConfigOpts *TLSConfigOpts
}
@@ -839,10 +895,32 @@ func (o *Options) ProcessConfigFile(configFile string) error {
if configFile == _EMPTY_ {
return nil
}
m, err := conf.ParseFileWithChecks(configFile)
m, digest, err := conf.ParseFileWithChecksDigest(configFile)
if err != nil {
return err
}
o.configDigest = digest
return o.processConfigFile(configFile, m)
}
// ProcessConfigString is the same as ProcessConfigFile, but expects the
// contents of the config file to be passed in rather than the file name.
func (o *Options) ProcessConfigString(data string) error {
m, err := conf.ParseWithChecks(data)
if err != nil {
return err
}
return o.processConfigFile(_EMPTY_, m)
}
// ConfigDigest returns the digest representing the configuration.
func (o *Options) ConfigDigest() string {
return o.configDigest
}
func (o *Options) processConfigFile(configFile string, m map[string]any) error {
// Collect all errors and warnings and report them all together.
errors := make([]error, 0)
warnings := make([]error, 0)
@@ -860,6 +938,21 @@ func (o *Options) ProcessConfigFile(configFile string) error {
o.processConfigFileLine(k, v, &errors, &warnings)
}
// Post-process: check auth callout allowed accounts against configured accounts.
if o.AuthCallout != nil {
accounts := make(map[string]struct{})
for _, acc := range o.Accounts {
accounts[acc.Name] = struct{}{}
}
for _, acc := range o.AuthCallout.AllowedAccounts {
if _, ok := accounts[acc]; !ok {
err := &configErr{nil, fmt.Sprintf("auth_callout allowed account %q not found in configured accounts", acc)}
errors = append(errors, err)
}
}
}
if len(errors) > 0 || len(warnings) > 0 {
return &processConfigErr{
errors: errors,
@@ -889,7 +982,13 @@ func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnin
case "port":
o.Port = int(v.(int64))
case "server_name":
o.ServerName = v.(string)
sn := v.(string)
if strings.Contains(sn, " ") {
err := &configErr{tk, ErrServerNameHasSpaces.Error()}
*errors = append(*errors, err)
return
}
o.ServerName = sn
case "host", "net":
o.Host = v.(string)
case "debug":
@@ -1682,7 +1781,13 @@ func parseCluster(v any, opts *Options, errors *[]error, warnings *[]error) erro
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "name":
opts.Cluster.Name = mv.(string)
cn := mv.(string)
if strings.Contains(cn, " ") {
err := &configErr{tk, ErrClusterNameHasSpaces.Error()}
*errors = append(*errors, err)
continue
}
opts.Cluster.Name = cn
case "listen":
hp, err := parseListen(mv)
if err != nil {
@@ -1912,7 +2017,13 @@ func parseGateway(v any, o *Options, errors *[]error, warnings *[]error) error {
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "name":
o.Gateway.Name = mv.(string)
gn := mv.(string)
if strings.Contains(gn, " ") {
err := &configErr{tk, ErrGatewayNameHasSpaces.Error()}
*errors = append(*errors, err)
continue
}
o.Gateway.Name = gn
case "listen":
hp, err := parseListen(mv)
if err != nil {
@@ -2068,6 +2179,19 @@ func parseJetStreamForAccount(v any, acc *Account, errors *[]error) error {
return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)}
}
jsLimits.MaxAckPending = int(vv)
case "cluster_traffic":
vv, ok := mv.(string)
if !ok {
return &configErr{tk, fmt.Sprintf("Expected either 'system' or 'account' string value for %q, got %v", mk, mv)}
}
switch vv {
case "system", _EMPTY_:
acc.js.nrgAccount = _EMPTY_
case "owner":
acc.js.nrgAccount = acc.Name
default:
return &configErr{tk, fmt.Sprintf("Expected 'system' or 'owner' string value for %q, got %v", mk, mv)}
}
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
@@ -2165,6 +2289,61 @@ func parseJetStreamLimits(v any, opts *Options, errors *[]error) error {
return nil
}
// Parse the JetStream TPM options.
func parseJetStreamTPM(v interface{}, opts *Options, errors *[]error) error {
var lt token
tk, v := unwrapValue(v, &lt)
tpm := JSTpmOpts{}
vv, ok := v.(map[string]interface{})
if !ok {
return &configErr{tk, fmt.Sprintf("Expected a map to define JetStreamLimits, got %T", v)}
}
for mk, mv := range vv {
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "keys_file":
tpm.KeysFile = mv.(string)
case "encryption_password":
tpm.KeyPassword = mv.(string)
case "srk_password":
tpm.SrkPassword = mv.(string)
case "pcr":
tpm.Pcr = int(mv.(int64))
case "cipher":
if err := setJetStreamEkCipher(opts, mv, tk); err != nil {
return err
}
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
field: mk,
configErr: configErr{
token: tk,
},
}
*errors = append(*errors, err)
continue
}
}
}
opts.JetStreamTpm = tpm
return nil
}
func setJetStreamEkCipher(opts *Options, mv interface{}, tk token) error {
switch strings.ToLower(mv.(string)) {
case "chacha", "chachapoly":
opts.JetStreamCipher = ChaCha
case "aes":
opts.JetStreamCipher = AES
default:
return &configErr{tk, fmt.Sprintf("Unknown cipher type: %q", mv)}
}
return nil
}
// Parse enablement of jetstream for a server.
func parseJetStream(v any, opts *Options, errors *[]error, warnings *[]error) error {
var lt token
@@ -2189,6 +2368,12 @@ func parseJetStream(v any, opts *Options, errors *[]error, warnings *[]error) er
for mk, mv := range vv {
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "strict":
if v, ok := mv.(bool); ok {
opts.JetStreamStrict = v
} else {
return &configErr{tk, fmt.Sprintf("Expected 'true' or 'false' for bool value, got '%s'", mv)}
}
case "store", "store_dir", "storedir":
// StoreDir can be set at the top level as well so have to prevent ambiguous declarations.
if opts.StoreDir != _EMPTY_ {
@@ -2226,13 +2411,8 @@ func parseJetStream(v any, opts *Options, errors *[]error, warnings *[]error) er
case "prev_key", "prev_ek", "prev_encryption_key":
opts.JetStreamOldKey = mv.(string)
case "cipher":
switch strings.ToLower(mv.(string)) {
case "chacha", "chachapoly":
opts.JetStreamCipher = ChaCha
case "aes":
opts.JetStreamCipher = AES
default:
return &configErr{tk, fmt.Sprintf("Unknown cipher type: %q", mv)}
if err := setJetStreamEkCipher(opts, mv, tk); err != nil {
return err
}
case "extension_hint":
opts.JetStreamExtHint = mv.(string)
@@ -2240,6 +2420,10 @@ func parseJetStream(v any, opts *Options, errors *[]error, warnings *[]error) er
if err := parseJetStreamLimits(tk, opts, errors); err != nil {
return err
}
case "tpm":
if err := parseJetStreamTPM(tk, opts, errors); err != nil {
return err
}
case "unique_tag":
opts.JetStreamUniqueTag = strings.ToLower(strings.TrimSpace(mv.(string)))
case "max_outstanding_catchup":
@@ -2248,6 +2432,18 @@ func parseJetStream(v any, opts *Options, errors *[]error, warnings *[]error) er
return &configErr{tk, fmt.Sprintf("%s %s", strings.ToLower(mk), err)}
}
opts.JetStreamMaxCatchup = s
case "max_buffered_size":
s, err := getStorageSize(mv)
if err != nil {
return &configErr{tk, fmt.Sprintf("%s %s", strings.ToLower(mk), err)}
}
opts.StreamMaxBufferedSize = s
case "max_buffered_msgs":
mlen, ok := mv.(int64)
if !ok {
return &configErr{tk, fmt.Sprintf("Expected a parseable size for %q, got %v", mk, mv)}
}
opts.StreamMaxBufferedMsgs = int(mlen)
case "request_queue_limit":
lim, ok := mv.(int64)
if !ok {
@@ -2345,6 +2541,7 @@ func parseLeafNodes(v any, opts *Options, errors *[]error, warnings *[]error) er
opts.LeafNode.TLSMap = tc.Map
opts.LeafNode.TLSPinnedCerts = tc.PinnedCerts
opts.LeafNode.TLSHandshakeFirst = tc.HandshakeFirst
opts.LeafNode.TLSHandshakeFirstFallback = tc.FallbackDelay
opts.LeafNode.tlsConfigOpts = tc
case "leafnode_advertise", "advertise":
opts.LeafNode.Advertise = mv.(string)
@@ -2611,7 +2808,26 @@ func parseRemoteLeafNodes(v any, errors *[]error, warnings *[]error) ([]*RemoteL
case "ws_no_masking", "websocket_no_masking":
remote.Websocket.NoMasking = v.(bool)
case "jetstream_cluster_migrate", "js_cluster_migrate":
remote.JetStreamClusterMigrate = true
var lt token
tk, v := unwrapValue(v, &lt)
switch vv := v.(type) {
case bool:
remote.JetStreamClusterMigrate = vv
case map[string]any:
remote.JetStreamClusterMigrate = true
migrateConfig, ok := v.(map[string]any)
if !ok {
continue
}
val, ok := migrateConfig["leader_migrate_delay"]
tk, delay := unwrapValue(val, &tk)
if ok {
remote.JetStreamClusterMigrateDelay = parseDuration("leader_migrate_delay", tk, delay, errors, warnings)
}
default:
*errors = append(*errors, &configErr{tk, fmt.Sprintf("Expected boolean or map for jetstream_cluster_migrate, got %T", v)})
}
case "compression":
if err := parseCompression(&remote.Compression, CompressionS2Auto, tk, k, v); err != nil {
*errors = append(*errors, err)
@@ -2747,14 +2963,16 @@ type export struct {
lat *serviceLatency
rthr time.Duration
tPos uint
atrc bool // allow_trace
}
type importStream struct {
acc *Account
an string
sub string
to string
pre string
acc *Account
an string
sub string
to string
pre string
atrc bool // allow_trace
}
type importService struct {
@@ -2932,6 +3150,69 @@ func parseAccountLimits(mv any, acc *Account, errors *[]error) error {
return nil
}
func parseAccountMsgTrace(mv any, topKey string, acc *Account) error {
processDest := func(tk token, k string, v any) error {
td, ok := v.(string)
if !ok {
return &configErr{tk, fmt.Sprintf("Field %q should be a string, got %T", k, v)}
}
if !IsValidPublishSubject(td) {
return &configErr{tk, fmt.Sprintf("Trace destination %q is not valid", td)}
}
acc.traceDest = td
return nil
}
processSampling := func(tk token, n int) error {
if n <= 0 || n > 100 {
return &configErr{tk, fmt.Sprintf("Ttrace destination sampling value %d is invalid, needs to be [1..100]", n)}
}
acc.traceDestSampling = n
return nil
}
var lt token
tk, v := unwrapValue(mv, &lt)
switch vv := v.(type) {
case string:
return processDest(tk, topKey, v)
case map[string]any:
for k, v := range vv {
tk, v := unwrapValue(v, &lt)
switch strings.ToLower(k) {
case "dest":
if err := processDest(tk, k, v); err != nil {
return err
}
case "sampling":
switch vv := v.(type) {
case int64:
if err := processSampling(tk, int(vv)); err != nil {
return err
}
case string:
s := strings.TrimSuffix(vv, "%")
n, err := strconv.Atoi(s)
if err != nil {
return &configErr{tk, fmt.Sprintf("Invalid trace destination sampling value %q", vv)}
}
if err := processSampling(tk, n); err != nil {
return err
}
default:
return &configErr{tk, fmt.Sprintf("Trace destination sampling field %q should be an integer or a percentage, got %T", k, v)}
}
default:
if !tk.IsUsedVariable() {
return &configErr{tk, fmt.Sprintf("Unknown field %q parsing account message trace map/struct %q", k, topKey)}
}
}
}
default:
return &configErr{tk, fmt.Sprintf("Expected account message trace %q to be a string or a map/struct, got %T", topKey, v)}
}
return nil
}
// parseAccounts will parse the different accounts syntax.
func parseAccounts(v any, opts *Options, errors *[]error, warnings *[]error) error {
var (
@@ -3061,6 +3342,23 @@ func parseAccounts(v any, opts *Options, errors *[]error, warnings *[]error) err
*errors = append(*errors, err)
continue
}
case "msg_trace", "trace_dest":
if err := parseAccountMsgTrace(tk, k, acc); err != nil {
*errors = append(*errors, err)
continue
}
// If trace destination is set but no sampling, set it to 100%.
if acc.traceDest != _EMPTY_ && acc.traceDestSampling == 0 {
acc.traceDestSampling = 100
} else if acc.traceDestSampling > 0 && acc.traceDest == _EMPTY_ {
// If no trace destination is provided, no trace would be
// triggered, so if the user set a sampling value expecting
// something to happen, want and set the value to 0 for good
// measure.
*warnings = append(*warnings,
&configErr{tk, "Trace destination sampling ignored since no destination was set"})
acc.traceDestSampling = 0
}
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
@@ -3185,6 +3483,14 @@ func parseAccounts(v any, opts *Options, errors *[]error, warnings *[]error) err
continue
}
}
if service.atrc {
if err := service.acc.SetServiceExportAllowTrace(service.sub, true); err != nil {
msg := fmt.Sprintf("Error adding allow_trace for %q: %v", service.sub, err)
*errors = append(*errors, &configErr{tk, msg})
continue
}
}
}
for _, stream := range importStreams {
ta := am[stream.an]
@@ -3194,13 +3500,13 @@ func parseAccounts(v any, opts *Options, errors *[]error, warnings *[]error) err
continue
}
if stream.pre != _EMPTY_ {
if err := stream.acc.AddStreamImport(ta, stream.sub, stream.pre); err != nil {
if err := stream.acc.addStreamImportWithClaim(ta, stream.sub, stream.pre, stream.atrc, nil); err != nil {
msg := fmt.Sprintf("Error adding stream import %q: %v", stream.sub, err)
*errors = append(*errors, &configErr{tk, msg})
continue
}
} else {
if err := stream.acc.AddMappedStreamImport(ta, stream.sub, stream.to); err != nil {
if err := stream.acc.addMappedStreamImportWithClaim(ta, stream.sub, stream.to, stream.atrc, nil); err != nil {
msg := fmt.Sprintf("Error adding stream import %q: %v", stream.sub, err)
*errors = append(*errors, &configErr{tk, msg})
continue
@@ -3358,6 +3664,9 @@ func parseExportStreamOrService(v any, errors *[]error) (*export, *export, error
latToken token
lt token
accTokPos uint
atrc bool
atrcSeen bool
atrcToken token
)
defer convertPanicToErrorList(&lt, errors)
@@ -3385,6 +3694,11 @@ func parseExportStreamOrService(v any, errors *[]error) (*export, *export, error
*errors = append(*errors, err)
continue
}
if atrcToken != nil {
err := &configErr{atrcToken, "Detected allow_trace directive on non-service"}
*errors = append(*errors, err)
continue
}
mvs, ok := mv.(string)
if !ok {
err := &configErr{tk, fmt.Sprintf("Expected stream name to be string, got %T", mv)}
@@ -3420,6 +3734,9 @@ func parseExportStreamOrService(v any, errors *[]error) (*export, *export, error
if threshSeen {
curService.rthr = thresh
}
if atrcSeen {
curService.atrc = atrc
}
case "response", "response_type":
if rtSeen {
err := &configErr{tk, "Duplicate response type definition"}
@@ -3508,6 +3825,18 @@ func parseExportStreamOrService(v any, errors *[]error) (*export, *export, error
}
case "account_token_position":
accTokPos = uint(mv.(int64))
case "allow_trace":
atrcSeen = true
atrcToken = tk
atrc = mv.(bool)
if curStream != nil {
*errors = append(*errors,
&configErr{tk, "Detected allow_trace directive on non-service"})
continue
}
if curService != nil {
curService.atrc = atrc
}
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
@@ -3618,6 +3947,9 @@ func parseImportStreamOrService(v any, errors *[]error) (*importStream, *importS
pre, to string
share bool
lt token
atrc bool
atrcSeen bool
atrcToken token
)
defer convertPanicToErrorList(&lt, errors)
@@ -3659,13 +3991,21 @@ func parseImportStreamOrService(v any, errors *[]error) (*importStream, *importS
if pre != _EMPTY_ {
curStream.pre = pre
}
if atrcSeen {
curStream.atrc = atrc
}
case "service":
if curStream != nil {
err := &configErr{tk, "Detected service but already saw a stream"}
*errors = append(*errors, err)
continue
}
ac, ok := mv.(map[string]interface{})
if atrcToken != nil {
err := &configErr{atrcToken, "Detected allow_trace directive on a non-stream"}
*errors = append(*errors, err)
continue
}
ac, ok := mv.(map[string]any)
if !ok {
err := &configErr{tk, fmt.Sprintf("Service entry should be an account map, got %T", mv)}
*errors = append(*errors, err)
@@ -3712,6 +4052,18 @@ func parseImportStreamOrService(v any, errors *[]error) (*importStream, *importS
if curService != nil {
curService.share = share
}
case "allow_trace":
if curService != nil {
err := &configErr{tk, "Detected allow_trace directive on a non-stream"}
*errors = append(*errors, err)
continue
}
atrcSeen = true
atrc = mv.(bool)
atrcToken = tk
if curStream != nil {
curStream.atrc = atrc
}
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
@@ -3964,6 +4316,15 @@ func parseAuthCallout(mv any, errors *[]error) (*AuthCallout, error) {
if !nkeys.IsValidPublicCurveKey(ac.XKey) {
return nil, &configErr{tk, fmt.Sprintf("Expected callout xkey to be a valid public xkey, got %q", ac.XKey)}
}
case "allowed_accounts":
aua, ok := mv.([]any)
if !ok {
return nil, &configErr{tk, fmt.Sprintf("Expected allowed accounts field to be an array, got %T", v)}
}
for _, uv := range aua {
_, uv = unwrapValue(uv, &lt)
ac.AllowedAccounts = append(ac.AllowedAccounts, uv.(string))
}
default:
if !tk.IsUsedVariable() {
err := &configErr{tk, fmt.Sprintf("Unknown field %q parsing authorization callout", k)}
@@ -4440,7 +4801,7 @@ func parseTLS(v any, isClientCtx bool) (t *TLSConfigOpts, retErr error) {
rv = append(rv, mv)
case []string:
rv = append(rv, mv...)
case []interface{}:
case []any:
for _, t := range mv {
if token, ok := t.(token); ok {
if ts, ok := token.Value().(string); ok {
@@ -4713,8 +5074,31 @@ func parseWebsocket(v any, o *Options, errors *[]error) error {
o.Websocket.AuthTimeout = auth.timeout
case "jwt_cookie":
o.Websocket.JWTCookie = mv.(string)
case "user_cookie":
o.Websocket.UsernameCookie = mv.(string)
case "pass_cookie":
o.Websocket.PasswordCookie = mv.(string)
case "token_cookie":
o.Websocket.TokenCookie = mv.(string)
case "no_auth_user":
o.Websocket.NoAuthUser = mv.(string)
case "headers":
m, ok := mv.(map[string]any)
if !ok {
err := &configErr{tk, fmt.Sprintf("error parsing headers: unsupported type %T", mv)}
*errors = append(*errors, err)
continue
}
o.Websocket.Headers = make(map[string]string)
for key, val := range m {
tk, val = unwrapValue(val, &lt)
if headerValue, ok := val.(string); !ok {
*errors = append(*errors, &configErr{tk, fmt.Sprintf("error parsing header key %s: unsupported type %T", key, val)})
continue
} else {
o.Websocket.Headers[key] = headerValue
}
}
default:
if !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
@@ -4985,7 +5369,10 @@ func MergeOptions(fileOpts, flagOpts *Options) *Options {
mergeRoutes(&opts, flagOpts)
}
if flagOpts.JetStream {
fileOpts.JetStream = flagOpts.JetStream
opts.JetStream = flagOpts.JetStream
}
if flagOpts.StoreDir != _EMPTY_ {
opts.StoreDir = flagOpts.StoreDir
}
return &opts
}
@@ -5015,86 +5402,6 @@ func mergeRoutes(opts, flagOpts *Options) {
opts.RoutesStr = flagOpts.RoutesStr
}
// RemoveSelfReference removes this server from an array of routes
func RemoveSelfReference(clusterPort int, routes []*url.URL) ([]*url.URL, error) {
var cleanRoutes []*url.URL
cport := strconv.Itoa(clusterPort)
selfIPs, err := getInterfaceIPs()
if err != nil {
return nil, err
}
for _, r := range routes {
host, port, err := net.SplitHostPort(r.Host)
if err != nil {
return nil, err
}
ipList, err := getURLIP(host)
if err != nil {
return nil, err
}
if cport == port && isIPInList(selfIPs, ipList) {
continue
}
cleanRoutes = append(cleanRoutes, r)
}
return cleanRoutes, nil
}
func isIPInList(list1 []net.IP, list2 []net.IP) bool {
for _, ip1 := range list1 {
for _, ip2 := range list2 {
if ip1.Equal(ip2) {
return true
}
}
}
return false
}
func getURLIP(ipStr string) ([]net.IP, error) {
ipList := []net.IP{}
ip := net.ParseIP(ipStr)
if ip != nil {
ipList = append(ipList, ip)
return ipList, nil
}
hostAddr, err := net.LookupHost(ipStr)
if err != nil {
return nil, fmt.Errorf("Error looking up host with route hostname: %v", err)
}
for _, addr := range hostAddr {
ip = net.ParseIP(addr)
if ip != nil {
ipList = append(ipList, ip)
}
}
return ipList, nil
}
func getInterfaceIPs() ([]net.IP, error) {
var localIPs []net.IP
interfaceAddr, err := net.InterfaceAddrs()
if err != nil {
return nil, fmt.Errorf("Error getting self referencing address: %v", err)
}
for i := 0; i < len(interfaceAddr); i++ {
interfaceIP, _, _ := net.ParseCIDR(interfaceAddr[i].String())
if net.ParseIP(interfaceIP.String()) != nil {
localIPs = append(localIPs, interfaceIP)
} else {
return nil, fmt.Errorf("Error parsing self referencing address: %v", err)
}
}
return localIPs, nil
}
func setBaselineOptions(opts *Options) {
// Setup non-standard Go defaults
if opts.Host == _EMPTY_ {
@@ -5450,6 +5757,8 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
trackExplicitVal(&FlagSnapshot.inCmdLine, "Syslog", FlagSnapshot.Syslog)
case "no_advertise":
trackExplicitVal(&FlagSnapshot.inCmdLine, "Cluster.NoAdvertise", FlagSnapshot.Cluster.NoAdvertise)
case "js":
trackExplicitVal(&FlagSnapshot.inCmdLine, "JetStream", FlagSnapshot.JetStream)
}
})
+20 -4
View File
@@ -49,6 +49,7 @@ type pubArg struct {
size int
hdr int
psi []*serviceImport
trace *msgTrace
delivered bool // Only used for service imports
}
@@ -286,7 +287,11 @@ func (c *client) parse(buf []byte) error {
if trace {
c.traceInOp("HPUB", arg)
}
if err := c.processHeaderPub(arg); err != nil {
var remaining []byte
if i < len(buf) {
remaining = buf[i+1:]
}
if err := c.processHeaderPub(arg, remaining); err != nil {
return err
}
@@ -484,11 +489,19 @@ func (c *client) parse(buf []byte) error {
c.msgBuf = buf[c.as : i+1]
}
var mt *msgTrace
if c.pa.hdr > 0 {
mt = c.initMsgTrace()
}
// Check for mappings.
if (c.kind == CLIENT || c.kind == LEAF) && c.in.flags.isSet(hasMappings) {
changed := c.selectMappedSubject()
if trace && changed {
c.traceInOp("MAPPING", []byte(fmt.Sprintf("%s -> %s", c.pa.mapped, c.pa.subject)))
if changed {
if trace {
c.traceInOp("MAPPING", []byte(fmt.Sprintf("%s -> %s", c.pa.mapped, c.pa.subject)))
}
// c.pa.subject is the subject the original is now mapped to.
mt.addSubjectMappingEvent(c.pa.subject)
}
}
if trace {
@@ -496,11 +509,14 @@ func (c *client) parse(buf []byte) error {
}
c.processInboundMsg(c.msgBuf)
mt.sendEvent()
c.argBuf, c.msgBuf, c.header = nil, nil, nil
c.drop, c.as, c.state = 0, i+1, OP_START
// Drop all pub args
c.pa.arg, c.pa.pacache, c.pa.origin, c.pa.account, c.pa.subject, c.pa.mapped = nil, nil, nil, nil, nil, nil
c.pa.reply, c.pa.hdr, c.pa.size, c.pa.szb, c.pa.hdb, c.pa.queues = nil, -1, 0, nil, nil, nil
c.pa.trace = nil
c.pa.delivered = false
lmsg = false
case OP_A:
@@ -1273,7 +1289,7 @@ func (c *client) clonePubArg(lmsg bool) error {
if c.pa.hdr < 0 {
return c.processPub(c.argBuf)
} else {
return c.processHeaderPub(c.argBuf)
return c.processHeaderPub(c.argBuf, nil)
}
}
}
+269
View File
@@ -0,0 +1,269 @@
// 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
size = 4
case 1: // fixed64
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
}
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !amd64
// +build !amd64
package pse
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build rumprun
// +build rumprun
package pse
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build wasm
// +build wasm
package pse
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build windows
// +build windows
package pse
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build zos
// +build zos
package pse
+206 -60
View File
@@ -78,17 +78,18 @@ type RaftNode interface {
Stop()
WaitForStop()
Delete()
RecreateInternalSubs() error
IsSystemAccount() bool
}
type WAL interface {
Type() StorageType
StoreMsg(subj string, hdr, msg []byte) (uint64, int64, error)
StoreMsg(subj string, hdr, msg []byte, ttl int64) (uint64, int64, error)
LoadMsg(index uint64, sm *StoreMsg) (*StoreMsg, error)
RemoveMsg(index uint64) (bool, error)
Compact(index uint64) (uint64, error)
Purge() (uint64, error)
PurgeEx(subject string, seq, keep uint64) (uint64, error)
PurgeEx(subject string, seq, keep uint64, noMarkers bool) (uint64, error)
Truncate(seq uint64) error
State() StreamState
FastState(*StreamState)
@@ -132,6 +133,7 @@ type raft struct {
created time.Time // Time that the group was created
accName string // Account name of the asset this raft group is for
acc *Account // Account that NRG traffic will be sent/received in
group string // Raft group
sd string // Store directory
id string // Node ID
@@ -142,9 +144,10 @@ type raft struct {
track bool //
werr error // Last write error
state atomic.Int32 // RaftState
hh hash.Hash64 // Highwayhash, used for snapshots
snapfile string // Snapshot filename
state atomic.Int32 // RaftState
leaderState atomic.Bool // Is in (complete) leader state.
hh hash.Hash64 // Highwayhash, used for snapshots
snapfile string // Snapshot filename
csz int // Cluster size
qn int // Number of nodes needed to establish quorum
@@ -166,6 +169,8 @@ type raft struct {
commit uint64 // Index of the most recent commit
applied uint64 // Index of the most recently applied commit
aflr uint64 // Index when to signal initial messages have been applied after becoming leader. 0 means signaling is disabled.
leader string // The ID of the leader
vote string // Our current vote state
lxfer bool // Are we doing a leadership transfer?
@@ -179,7 +184,9 @@ type raft struct {
dflag bool // Debug flag
hasleader atomic.Bool // Is there a group leader right now?
pleader atomic.Bool // Has the group ever had a leader?
observer bool // The node is observing, i.e. not participating in voting
isSysAcc atomic.Bool // Are we utilizing the system account?
observer bool // The node is observing, i.e. not participating in voting
extSt extensionState // Extension state
@@ -361,8 +368,6 @@ func (s *Server) initRaftNode(accName string, cfg *RaftConfig, labels pprofLabel
s.mu.RUnlock()
return nil, ErrNoSysAccount
}
sq := s.sys.sq
sacc := s.sys.account
hash := s.sys.shash
s.mu.RUnlock()
@@ -390,9 +395,7 @@ func (s *Server) initRaftNode(accName string, cfg *RaftConfig, labels pprofLabel
acks: make(map[uint64]map[string]struct{}),
pae: make(map[uint64]*appendEntry),
s: s,
c: s.createInternalSystemClient(),
js: s.getJetStream(),
sq: sq,
quit: make(chan struct{}),
reqs: newIPQueue[*voteRequest](s, qpfx+"vreq"),
votes: newIPQueue[*voteResponse](s, qpfx+"vresp"),
@@ -405,7 +408,14 @@ func (s *Server) initRaftNode(accName string, cfg *RaftConfig, labels pprofLabel
observer: cfg.Observer,
extSt: ps.domainExt,
}
n.c.registerWithAccount(sacc)
// Setup our internal subscriptions for proposals, votes and append entries.
// If we fail to do this for some reason then this is fatal — we cannot
// continue setting up or the Raft node may be partially/totally isolated.
if err := n.RecreateInternalSubs(); err != nil {
n.shutdown()
return nil, err
}
if atomic.LoadInt32(&s.logging.debug) > 0 {
n.dflag = true
@@ -498,14 +508,6 @@ func (s *Server) initRaftNode(accName string, cfg *RaftConfig, labels pprofLabel
}
}
// Setup our internal subscriptions for proposals, votes and append entries.
// If we fail to do this for some reason then this is fatal — we cannot
// continue setting up or the Raft node may be partially/totally isolated.
if err := n.createInternalSubs(); err != nil {
n.shutdown()
return nil, err
}
n.debug("Started")
// Check if we need to start in observer mode due to lame duck status.
@@ -545,10 +547,116 @@ func (s *Server) startRaftNode(accName string, cfg *RaftConfig, labels pprofLabe
return n, nil
}
// Returns whether peers within this group claim to support
// moving NRG traffic into the asset account.
// Lock must be held.
func (n *raft) checkAccountNRGStatus() bool {
if !n.s.accountNRGAllowed.Load() {
return false
}
enabled := true
for pn := range n.peers {
if si, ok := n.s.nodeToInfo.Load(pn); ok && si != nil {
enabled = enabled && si.(nodeInfo).accountNRG
}
}
return enabled
}
// Whether we are using the system account or not.
// In 2.10.x this is always true as there is no account NRG like in 2.11.x.
func (n *raft) IsSystemAccount() bool {
return true
return n.isSysAcc.Load()
}
func (n *raft) RecreateInternalSubs() error {
n.Lock()
defer n.Unlock()
return n.recreateInternalSubsLocked()
}
func (n *raft) recreateInternalSubsLocked() error {
// Sanity check for system account, as it can disappear when
// the system is shutting down.
if n.s == nil {
return fmt.Errorf("server not found")
}
n.s.mu.RLock()
sys := n.s.sys
n.s.mu.RUnlock()
if sys == nil {
return fmt.Errorf("system account not found")
}
// Default is the system account.
nrgAcc := sys.account
n.isSysAcc.Store(true)
// Is account NRG enabled in this account and do all group
// peers claim to also support account NRG?
if n.checkAccountNRGStatus() {
// Check whether the account that the asset belongs to
// has volunteered a different NRG account.
target := nrgAcc.Name
if a, _ := n.s.lookupAccount(n.accName); a != nil {
a.mu.RLock()
if a.js != nil {
target = a.js.nrgAccount
}
a.mu.RUnlock()
}
// If the target account exists, then we'll use that.
if target != _EMPTY_ {
if a, _ := n.s.lookupAccount(target); a != nil {
nrgAcc = a
if a != sys.account {
n.isSysAcc.Store(false)
}
}
}
}
if n.aesub != nil && n.acc == nrgAcc {
// Subscriptions already exist and the account NRG state
// hasn't changed.
return nil
}
// Need to cancel any in-progress catch-ups, otherwise the
// inboxes are about to be pulled out from underneath it in
// the next step...
n.cancelCatchup()
// If we have an existing client then tear down any existing
// subscriptions and close the internal client.
if c := n.c; c != nil {
c.mu.Lock()
subs := make([]*subscription, 0, len(c.subs))
for _, sub := range c.subs {
subs = append(subs, sub)
}
c.mu.Unlock()
for _, sub := range subs {
n.unsubscribe(sub)
}
c.closeConnection(InternalClient)
}
if n.acc != nrgAcc {
n.debug("Subscribing in '%s'", nrgAcc.GetName())
}
c := n.s.createInternalSystemClient()
c.registerWithAccount(nrgAcc)
if nrgAcc.sq == nil {
nrgAcc.sq = n.s.newSendQ(nrgAcc)
}
n.c = c
n.sq = nrgAcc.sq
n.acc = nrgAcc
// Recreate any internal subscriptions for voting, append
// entries etc in the new account.
return n.createInternalSubs()
}
// outOfResources checks to see if we are out of resources.
@@ -650,9 +758,7 @@ func (s *Server) stepdownRaftNodes() {
s.rnMu.RUnlock()
for _, node := range nodes {
if node.Leader() {
node.StepDown()
}
node.StepDown()
node.SetObserver(true)
}
}
@@ -701,8 +807,7 @@ func (s *Server) transferRaftLeaders() bool {
var didTransfer bool
for _, node := range nodes {
if node.Leader() {
node.StepDown()
if err := node.StepDown(); err == nil {
didTransfer = true
}
node.SetObserver(true)
@@ -753,7 +858,7 @@ func (n *raft) ProposeMulti(entries []*Entry) error {
// ForwardProposal will forward the proposal to the leader if known.
// If we are the leader this is the same as calling propose.
func (n *raft) ForwardProposal(entry []byte) error {
if n.Leader() {
if n.State() == Leader {
return n.Propose(entry)
}
@@ -952,7 +1057,7 @@ func (n *raft) ResumeApply() {
}
}
// Applied is a callback that must be be called by the upper layer when it
// Applied is a callback that must be called by the upper layer when it
// has successfully applied the committed entries that it received from the
// apply queue. It will return the number of entries and an estimation of the
// byte size that could be removed with a snapshot/compact.
@@ -970,6 +1075,17 @@ func (n *raft) Applied(index uint64) (entries uint64, bytes uint64) {
n.applied = index
}
// If it was set, and we reached the minimum applied index, reset and send signal to upper layer.
if n.aflr > 0 && index >= n.aflr {
n.aflr = 0
// Quick sanity-check to confirm we're still leader.
// In which case we must signal, since switchToLeader would not have done so already.
if n.State() == Leader {
n.leaderState.Store(true)
n.updateLeadChange(true)
}
}
// Calculate the number of entries and estimate the byte size that
// we can now remove with a compaction/snapshot.
var state StreamState
@@ -1262,7 +1378,7 @@ func (n *raft) Leader() bool {
if n == nil {
return false
}
return n.State() == Leader
return n.leaderState.Load()
}
// stepdown immediately steps down the Raft node to the
@@ -1435,18 +1551,16 @@ func (n *raft) selectNextLeader() string {
// StepDown will have a leader stepdown and optionally do a leader transfer.
func (n *raft) StepDown(preferred ...string) error {
n.Lock()
if n.State() != Leader {
return errNotLeader
}
n.Lock()
if len(preferred) > 1 {
n.Unlock()
return errTooManyPrefs
}
if n.State() != Leader {
n.Unlock()
return errNotLeader
}
n.debug("Being asked to stepdown")
// See if we have up to date followers.
@@ -1557,6 +1671,7 @@ func (n *raft) xferCampaign() error {
}
// State returns the current state for this node.
// Upper layers should not check State to check if we're Leader, use n.Leader() instead.
func (n *raft) State() RaftState {
return RaftState(n.state.Load())
}
@@ -1565,7 +1680,7 @@ func (n *raft) State() RaftState {
func (n *raft) Progress() (index, commit, applied uint64) {
n.RLock()
defer n.RUnlock()
return n.pindex + 1, n.commit, n.applied
return n.pindex, n.commit, n.applied
}
// Size returns number of entries and total bytes for our WAL.
@@ -1674,6 +1789,7 @@ func (n *raft) shutdown() {
// First call to Stop or Delete should close the quit chan
// to notify the runAs goroutines to stop what they're doing.
if n.state.Swap(int32(Closed)) != int32(Closed) {
n.leaderState.Store(false)
close(n.quit)
}
}
@@ -1725,9 +1841,8 @@ func (n *raft) unsubscribe(sub *subscription) {
}
}
// Lock should be held.
func (n *raft) createInternalSubs() error {
n.Lock()
defer n.Unlock()
n.vsubj, n.vreply = fmt.Sprintf(raftVoteSubj, n.group), n.newInbox()
n.asubj, n.areply = fmt.Sprintf(raftAppendSubj, n.group), n.newInbox()
n.psubj = fmt.Sprintf(raftPropSubj, n.group)
@@ -2316,7 +2431,7 @@ func (n *raft) decodeAppendEntryResponse(msg []byte) *appendEntryResponse {
func (n *raft) handleForwardedRemovePeerProposal(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) {
n.debug("Received forwarded remove peer proposal: %q", msg)
if !n.Leader() {
if n.State() != Leader {
n.debug("Ignoring forwarded peer removal proposal, not leader")
return
}
@@ -2341,7 +2456,7 @@ func (n *raft) handleForwardedRemovePeerProposal(sub *subscription, c *client, _
// Called when a peer has forwarded a proposal.
func (n *raft) handleForwardedProposal(sub *subscription, c *client, _ *Account, _, reply string, msg []byte) {
if !n.Leader() {
if n.State() != Leader {
n.debug("Ignoring forwarded proposal, not leader")
return
}
@@ -2605,14 +2720,14 @@ func (n *raft) runCatchup(ar *appendEntryResponse, indexUpdatesQ *ipQueue[uint64
defer stepCheck.Stop()
// Run as long as we are leader and still not caught up.
for n.Leader() {
for n.State() == Leader {
select {
case <-n.s.quitCh:
return
case <-n.quit:
return
case <-stepCheck.C:
if !n.Leader() {
if n.State() != Leader {
n.debug("Catching up canceled, no longer leader")
return
}
@@ -2904,18 +3019,23 @@ func (n *raft) trackResponse(ar *appendEntryResponse) {
// See if we have items to apply.
var sendHB bool
if results := n.acks[ar.index]; results != nil {
results[ar.peer] = struct{}{}
if nr := len(results); nr >= n.qn {
// We have a quorum.
for index := n.commit + 1; index <= ar.index; index++ {
if err := n.applyCommit(index); err != nil && err != errNodeClosed {
n.error("Got an error applying commit for %d: %v", index, err)
break
}
results := n.acks[ar.index]
if results == nil {
results = make(map[string]struct{})
n.acks[ar.index] = results
}
results[ar.peer] = struct{}{}
// We don't count ourselves to account for leader changes, so add 1.
if nr := len(results); nr+1 >= n.qn {
// We have a quorum.
for index := n.commit + 1; index <= ar.index; index++ {
if err := n.applyCommit(index); err != nil && err != errNodeClosed {
n.error("Got an error applying commit for %d: %v", index, err)
break
}
sendHB = n.prop.len() == 0
}
sendHB = n.prop.len() == 0
}
n.Unlock()
@@ -2945,6 +3065,9 @@ func (n *raft) adjustClusterSizeAndQuorum() {
go n.sendHeartbeat()
}
}
if ncsz != pcsz {
n.recreateInternalSubsLocked()
}
}
// Track interactions with this peer.
@@ -3143,7 +3266,7 @@ func (n *raft) truncateWAL(term, index uint64) {
n.wal.Truncate(0)
// If our index is non-zero use PurgeEx to set us to the correct next index.
if index > 0 {
n.wal.PurgeEx(fwcs, index+1, 0)
n.wal.PurgeEx(fwcs, index+1, 0, true)
}
} else {
n.warn("Error truncating WAL: %v", err)
@@ -3604,7 +3727,7 @@ func (n *raft) storeToWAL(ae *appendEntry) error {
return n.werr
}
seq, _, err := n.wal.StoreMsg(_EMPTY_, nil, ae.buf)
seq, _, err := n.wal.StoreMsg(_EMPTY_, nil, ae.buf, 0)
if err != nil {
n.setWriteErrLocked(err)
return err
@@ -3651,8 +3774,6 @@ func (n *raft) sendAppendEntry(entries []*Entry) {
if err := n.storeToWAL(ae); err != nil {
return
}
// We count ourselves.
n.acks[n.pindex] = map[string]struct{}{n.id: {}}
n.active = time.Now()
// Save in memory for faster processing during applyCommit.
@@ -4111,11 +4232,11 @@ func (n *raft) updateLeadChange(isLeader bool) {
}
// Lock should be held.
func (n *raft) switchState(state RaftState) {
func (n *raft) switchState(state RaftState) bool {
retry:
pstate := n.State()
if pstate == Closed {
return
return false
}
// Set our state. If something else has changed our state
@@ -4127,19 +4248,23 @@ retry:
// Reset the election timer.
n.resetElectionTimeout()
var leadChange bool
if pstate == Leader && state != Leader {
leadChange = true
n.updateLeadChange(false)
// Drain the append entry response and proposal queues.
n.resp.drain()
n.prop.drain()
} else if state == Leader && pstate != Leader {
// Don't updateLeadChange here, it will be done in switchToLeader or after initial messages are applied.
leadChange = true
if len(n.pae) > 0 {
n.pae = make(map[uint64]*appendEntry)
}
n.updateLeadChange(true)
}
n.writeTermVote()
return leadChange
}
const (
@@ -4161,7 +4286,13 @@ func (n *raft) switchToFollowerLocked(leader string) {
n.debug("Switching to follower")
n.aflr = 0
n.leaderState.Store(false)
n.lxfer = false
// Reset acks, we can't assume acks from a previous term are still valid in another term.
if len(n.acks) > 0 {
n.acks = make(map[uint64]map[string]struct{})
}
n.updateLeader(leader)
n.switchState(Follower)
}
@@ -4214,7 +4345,22 @@ func (n *raft) switchToLeader() {
n.lxfer = false
n.updateLeader(n.id)
n.switchState(Leader)
leadChange := n.switchState(Leader)
if leadChange {
// Wait for messages to be applied if we've stored more, otherwise signal immediately.
// It's important to wait signaling we're leader if we're not up-to-date yet, as that
// would mean we're in a consistent state compared with the previous leader.
if n.pindex > n.applied {
n.aflr = n.pindex
} else {
// We know we have applied all entries in our log and can signal immediately.
// For sanity reset applied floor back down to 0, so we aren't able to signal twice.
n.aflr = 0
n.leaderState.Store(true)
n.updateLeadChange(true)
}
}
n.Unlock()
if sendHB {
+15 -4
View File
@@ -848,6 +848,7 @@ func (l *leafNodeOption) Apply(s *Server) {
opts := s.getOpts()
if l.tlsFirstChanged {
s.Noticef("Reloaded: LeafNode TLS HandshakeFirst value is: %v", opts.LeafNode.TLSHandshakeFirst)
s.Noticef("Reloaded: LeafNode TLS HandshakeFirstFallback value is: %v", opts.LeafNode.TLSHandshakeFirstFallback)
for _, r := range opts.LeafNode.Remotes {
s.Noticef("Reloaded: LeafNode Remote to %v TLS HandshakeFirst value is: %v", r.URLs, r.TLSHandshakeFirst)
}
@@ -1168,6 +1169,7 @@ func imposeOrder(value any) error {
*OCSPConfig, map[string]string, JSLimitOpts, StoreCipher, *OCSPResponseCacheConfig:
// explicitly skipped types
case *AuthCallout:
case JSTpmOpts:
default:
// this will fail during unit tests
return fmt.Errorf("OnReload, sort or explicitly skip type: %s",
@@ -1367,14 +1369,16 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
tmpNew.TLSConfig = nil
tmpOld.tlsConfigOpts = nil
tmpNew.tlsConfigOpts = nil
// We will allow TLSHandshakeFirst to me config reloaded. First,
// We will allow TLSHandshakeFirst to be config reloaded. First,
// we just want to detect if there was a change in the leafnodes{}
// block, and if not, we will check the remotes.
handshakeFirstChanged := tmpOld.TLSHandshakeFirst != tmpNew.TLSHandshakeFirst
handshakeFirstChanged := tmpOld.TLSHandshakeFirst != tmpNew.TLSHandshakeFirst ||
tmpOld.TLSHandshakeFirstFallback != tmpNew.TLSHandshakeFirstFallback
// If changed, set them (in the temporary variables) to false so that the
// rest of the comparison does not fail.
if handshakeFirstChanged {
tmpOld.TLSHandshakeFirst, tmpNew.TLSHandshakeFirst = false, false
tmpOld.TLSHandshakeFirstFallback, tmpNew.TLSHandshakeFirstFallback = 0, 0
} else if len(tmpOld.Remotes) == len(tmpNew.Remotes) {
// Since we don't support changes in the remotes, we will do a
// simple pass to see if there was a change of this field.
@@ -1636,6 +1640,10 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
if new != old {
diffOpts = append(diffOpts, &profBlockRateReload{newValue: new})
}
case "configdigest":
// skip changes in config digest, this is handled already while
// processing the config.
continue
case "nofastproducerstall":
diffOpts = append(diffOpts, &noFastProdStallReload{noStall: newValue.(bool)})
default:
@@ -1785,8 +1793,11 @@ func (s *Server) applyOptions(ctx *reloadContext, opts []option) {
if err := s.reloadOCSP(); err != nil {
s.Warnf("Can't restart OCSP features: %v", err)
}
s.Noticef("Reloaded server configuration")
var cd string
if newOpts.configDigest != "" {
cd = fmt.Sprintf("(%s)", newOpts.configDigest)
}
s.Noticef("Reloaded server configuration %s", cd)
}
// This will send a reset to the internal send loop.
+164 -118
View File
@@ -42,17 +42,6 @@ const (
Explicit
)
const (
// RouteProtoZero is the original Route protocol from 2009.
// http://nats.io/documentation/internals/nats-protocol/
RouteProtoZero = iota
// RouteProtoInfo signals a route can receive more then the original INFO block.
// This can be used to update remote cluster permissions, etc...
RouteProtoInfo
// RouteProtoV2 is the new route/cluster protocol that provides account support.
RouteProtoV2
)
// Include the space for the proto
var (
aSubBytes = []byte{'A', '+', ' '}
@@ -63,11 +52,6 @@ var (
lUnsubBytes = []byte{'L', 'S', '-', ' '}
)
// Used by tests
func setRouteProtoForTest(wantedProto int) int {
return (wantedProto + 1) * -1
}
type route struct {
remoteID string
remoteName string
@@ -99,8 +83,18 @@ type route struct {
// Selected compression mode, which may be different from the
// server configured mode.
compression string
// Transient value used to set the Info.GossipMode when initiating
// an implicit route and sending to the remote.
gossipMode byte
}
// Do not change the values/order since they are exchanged between servers.
const (
gossipDefault = byte(iota)
gossipDisabled
gossipOverride
)
type connectInfo struct {
Echo bool `json:"echo"`
Verbose bool `json:"verbose"`
@@ -578,6 +572,12 @@ func (c *client) processRouteInfo(info *Info) {
return
}
s.mu.Lock()
// If running without system account and adding a dedicated
// route for an account for the first time, it could be that
// the map is nil. If so, create it.
if s.accRoutes == nil {
s.accRoutes = make(map[string]map[string]*client)
}
if _, ok := s.accRoutes[an]; !ok {
s.accRoutes[an] = make(map[string]*client)
}
@@ -702,12 +702,15 @@ func (c *client) processRouteInfo(info *Info) {
return
}
var sendDelayedInfo bool
// First INFO, check if this server is configured for compression because
// if that is the case, we need to negotiate it with the remote server.
if needsCompression(opts.Cluster.Compression.Mode) {
accName := bytesToString(c.route.accName)
// If we did not yet negotiate...
if !c.flags.isSet(compressionNegotiated) {
compNeg := c.flags.isSet(compressionNegotiated)
if !compNeg {
// Prevent from getting back here.
c.flags.set(compressionNegotiated)
// Release client lock since following function will need server lock.
@@ -724,24 +727,21 @@ func (c *client) processRouteInfo(info *Info) {
}
// No compression because one side does not want/can't, so proceed.
c.mu.Lock()
} else if didSolicit {
// The other side has switched to compression, so we can now set
// the first ping timer and send the delayed INFO for situations
// where it was not already sent.
c.setFirstPingTimer()
if !routeShouldDelayInfo(accName, opts) {
cm := compressionModeForInfoProtocol(&opts.Cluster.Compression, c.route.compression)
// Need to release and then reacquire...
// Check that the connection did not close if the lock was released.
if c.isClosed() {
c.mu.Unlock()
s.sendDelayedRouteInfo(c, accName, cm)
c.mu.Lock()
return
}
}
// Check that the connection did not close if the lock was released.
if c.isClosed() {
c.mu.Unlock()
return
// We can set the ping timer after we just negotiated compression above,
// or for solicited routes if we already negotiated.
if !compNeg || didSolicit {
c.setFirstPingTimer()
}
// When compression is configured, we delay the initial INFO for any
// solicited route. So we need to send the delayed INFO simply based
// on the didSolicit boolean.
sendDelayedInfo = didSolicit
} else {
// Coming from an old server, the Compression field would be the empty
// string. For servers that are configured with CompressionNotSupported,
@@ -751,12 +751,17 @@ func (c *client) processRouteInfo(info *Info) {
} else {
c.route.compression = CompressionOff
}
// When compression is not configured, we delay the initial INFO only
// for solicited pooled routes, so use the same check that we did when
// we decided to delay in createRoute().
sendDelayedInfo = didSolicit && routeShouldDelayInfo(bytesToString(c.route.accName), opts)
}
// Mark that the INFO protocol has been received, so we can detect updates.
c.flags.set(infoReceived)
// Get the route's proto version
// Get the route's proto version. It will be used to check if the connection
// supports certain features, such as message tracing.
c.opts.Protocol = info.Proto
// Headers
@@ -828,11 +833,15 @@ func (c *client) processRouteInfo(info *Info) {
}
accName := string(c.route.accName)
// Capture the noGossip value and reset it here.
gossipMode := c.route.gossipMode
c.route.gossipMode = 0
// Check to see if we have this remote already registered.
// This can happen when both servers have routes to each other.
c.mu.Unlock()
if added := s.addRoute(c, didSolicit, info, accName); added {
if added := s.addRoute(c, didSolicit, sendDelayedInfo, gossipMode, info, accName); added {
if accName != _EMPTY_ {
c.Debugf("Registering remote route %q for account %q", info.ID, accName)
} else {
@@ -866,7 +875,7 @@ func (s *Server) negotiateRouteCompression(c *client, didSolicit bool, accName,
if needsCompression(cm) {
// Generate an INFO with the chosen compression mode.
s.mu.Lock()
infoProto := s.generateRouteInitialInfoJSON(accName, cm, 0)
infoProto := s.generateRouteInitialInfoJSON(accName, cm, 0, gossipDefault)
s.mu.Unlock()
// If we solicited, then send this INFO protocol BEFORE switching
@@ -895,29 +904,9 @@ func (s *Server) negotiateRouteCompression(c *client, didSolicit bool, accName,
c.mu.Unlock()
return true, nil
}
// We are not using compression, set the ping timer.
c.mu.Lock()
c.setFirstPingTimer()
c.mu.Unlock()
// If this is a solicited route, we need to send the INFO if it was not
// done during createRoute() and will not be done in addRoute().
if didSolicit && !routeShouldDelayInfo(accName, opts) {
cm = compressionModeForInfoProtocol(&opts.Cluster.Compression, cm)
s.sendDelayedRouteInfo(c, accName, cm)
}
return false, nil
}
func (s *Server) sendDelayedRouteInfo(c *client, accName, cm string) {
s.mu.Lock()
infoProto := s.generateRouteInitialInfoJSON(accName, cm, 0)
s.mu.Unlock()
c.mu.Lock()
c.enqueueProto(infoProto)
c.mu.Unlock()
}
// Possibly sends local subscriptions interest to this route
// based on changes in the remote's Export permissions.
func (s *Server) updateRemoteRoutePerms(c *client, info *Info) {
@@ -1053,7 +1042,7 @@ func (s *Server) processImplicitRoute(info *Info, routeNoPool bool) {
if info.AuthRequired {
r.User = url.UserPassword(opts.Cluster.Username, opts.Cluster.Password)
}
s.startGoRoutine(func() { s.connectToRoute(r, false, true, info.RouteAccount) })
s.startGoRoutine(func() { s.connectToRoute(r, Implicit, true, info.GossipMode, info.RouteAccount) })
// If we are processing an implicit route from a route that does not
// support pooling/pinned-accounts, we won't receive an INFO for each of
// the pinned-accounts that we would normally receive. In that case, just
@@ -1063,7 +1052,7 @@ func (s *Server) processImplicitRoute(info *Info, routeNoPool bool) {
rURL := r
for _, an := range opts.Cluster.PinnedAccounts {
accName := an
s.startGoRoutine(func() { s.connectToRoute(rURL, false, true, accName) })
s.startGoRoutine(func() { s.connectToRoute(rURL, Implicit, true, info.GossipMode, accName) })
}
}
}
@@ -1102,26 +1091,89 @@ func (s *Server) hasThisRouteConfigured(info *Info) bool {
return false
}
// forwardNewRouteInfoToKnownServers sends the INFO protocol of the new route
// to all routes known by this server. In turn, each server will contact this
// new route.
// forwardNewRouteInfoToKnownServers possibly sends the INFO protocol of the
// new route to all routes known by this server. In turn, each server will
// contact this new route.
// Server lock held on entry.
func (s *Server) forwardNewRouteInfoToKnownServers(info *Info) {
func (s *Server) forwardNewRouteInfoToKnownServers(info *Info, rtype RouteType, didSolicit bool, localGossipMode byte) {
// Determine if this connection is resulting from a gossip notification.
fromGossip := didSolicit && rtype == Implicit
// If from gossip (but we are not overriding it) or if the remote disabled gossip, bail out.
if (fromGossip && localGossipMode != gossipOverride) || info.GossipMode == gossipDisabled {
return
}
// Note: nonce is not used in routes.
// That being said, the info we get is the initial INFO which
// contains a nonce, but we now forward this to existing routes,
// so clear it now.
info.Nonce = _EMPTY_
b, _ := json.Marshal(info)
infoJSON := []byte(fmt.Sprintf(InfoProto, b))
var (
infoGMDefault []byte
infoGMDisabled []byte
infoGMOverride []byte
)
generateJSON := func(gm byte) []byte {
info.GossipMode = gm
b, _ := json.Marshal(info)
return []byte(fmt.Sprintf(InfoProto, b))
}
getJSON := func(r *client) []byte {
if (!didSolicit && r.route.routeType == Explicit) || (didSolicit && rtype == Explicit) {
if infoGMOverride == nil {
infoGMOverride = generateJSON(gossipOverride)
}
return infoGMOverride
} else if !didSolicit {
if infoGMDisabled == nil {
infoGMDisabled = generateJSON(gossipDisabled)
}
return infoGMDisabled
}
if infoGMDefault == nil {
infoGMDefault = generateJSON(0)
}
return infoGMDefault
}
var accRemotes map[string]*client
pinnedAccount := info.RouteAccount != _EMPTY_
// If this is for a pinned account, we will try to send the gossip
// through our pinned account routes, but fall back to the other
// routes in case we don't have one for a given remote.
if pinnedAccount {
var ok bool
if accRemotes, ok = s.accRoutes[info.RouteAccount]; ok {
for remoteID, r := range accRemotes {
if r == nil {
continue
}
r.mu.Lock()
// Do not send to a remote that does not support pooling/pinned-accounts.
if remoteID != info.ID && !r.route.noPool {
r.enqueueProto(getJSON(r))
}
r.mu.Unlock()
}
}
}
s.forEachRemote(func(r *client) {
r.mu.Lock()
remoteID := r.route.remoteID
if pinnedAccount {
if _, processed := accRemotes[remoteID]; processed {
r.mu.Unlock()
return
}
}
// If this is a new route for a given account, do not send to a server
// that does not support pooling/pinned-accounts.
if r.route.remoteID != info.ID &&
(info.RouteAccount == _EMPTY_ || (info.RouteAccount != _EMPTY_ && !r.route.noPool)) {
r.enqueueProto(infoJSON)
if remoteID != info.ID && (!pinnedAccount || !r.route.noPool) {
r.enqueueProto(getJSON(r))
}
r.mu.Unlock()
})
@@ -1839,17 +1891,12 @@ func (c *client) sendRouteSubOrUnSubProtos(subs []*subscription, isSubProto, tra
c.enqueueProto(buf)
}
func (s *Server) createRoute(conn net.Conn, rURL *url.URL, accName string) *client {
func (s *Server) createRoute(conn net.Conn, rURL *url.URL, rtype RouteType, gossipMode byte, accName string) *client {
// Snapshot server options.
opts := s.getOpts()
didSolicit := rURL != nil
r := &route{didSolicit: didSolicit, poolIdx: -1}
for _, route := range opts.Routes {
if rURL != nil && (strings.EqualFold(rURL.Host, route.Host)) {
r.routeType = Explicit
}
}
r := &route{routeType: rtype, didSolicit: didSolicit, poolIdx: -1, gossipMode: gossipMode}
c := &client{srv: s, nc: conn, opts: ClientOpts{}, kind: ROUTER, msubs: -1, mpay: -1, route: r, start: time.Now()}
@@ -1866,7 +1913,7 @@ func (s *Server) createRoute(conn net.Conn, rURL *url.URL, accName string) *clie
// the incoming INFO from the remote. Also delay if configured for compression.
delayInfo := didSolicit && (compressionConfigured || routeShouldDelayInfo(accName, opts))
if !delayInfo {
infoJSON = s.generateRouteInitialInfoJSON(accName, opts.Cluster.Compression.Mode, 0)
infoJSON = s.generateRouteInitialInfoJSON(accName, opts.Cluster.Compression.Mode, 0, gossipMode)
}
authRequired := s.routeInfo.AuthRequired
tlsRequired := s.routeInfo.TLSRequired
@@ -1999,7 +2046,7 @@ func routeShouldDelayInfo(accName string, opts *Options) bool {
// To be used only when a route is created (to send the initial INFO protocol).
//
// Server lock held on entry.
func (s *Server) generateRouteInitialInfoJSON(accName, compression string, poolIdx int) []byte {
func (s *Server) generateRouteInitialInfoJSON(accName, compression string, poolIdx int, gossipMode byte) []byte {
// New proto wants a nonce (although not used in routes, that is, not signed in CONNECT)
var raw [nonceLen]byte
nonce := raw[:]
@@ -2009,11 +2056,11 @@ func (s *Server) generateRouteInitialInfoJSON(accName, compression string, poolI
if s.getOpts().Cluster.Compression.Mode == CompressionS2Auto {
compression = CompressionS2Auto
}
ri.Nonce, ri.RouteAccount, ri.RoutePoolIdx, ri.Compression = string(nonce), accName, poolIdx, compression
ri.Nonce, ri.RouteAccount, ri.RoutePoolIdx, ri.Compression, ri.GossipMode = string(nonce), accName, poolIdx, compression, gossipMode
infoJSON := generateInfoJSON(&s.routeInfo)
// Clear now that it has been serialized. Will prevent nonce to be included in async INFO that we may send.
// Same for some other fields.
ri.Nonce, ri.RouteAccount, ri.RoutePoolIdx, ri.Compression = _EMPTY_, _EMPTY_, 0, _EMPTY_
ri.Nonce, ri.RouteAccount, ri.RoutePoolIdx, ri.Compression, ri.GossipMode = _EMPTY_, _EMPTY_, 0, _EMPTY_, 0
return infoJSON
}
@@ -2022,7 +2069,7 @@ const (
_EMPTY_ = ""
)
func (s *Server) addRoute(c *client, didSolicit bool, info *Info, accName string) bool {
func (s *Server) addRoute(c *client, didSolicit, sendDelayedInfo bool, gossipMode byte, info *Info, accName string) bool {
id := info.ID
var acc *Account
@@ -2108,6 +2155,11 @@ func (s *Server) addRoute(c *client, didSolicit bool, info *Info, accName string
c.mu.Lock()
idHash := c.route.idHash
cid := c.cid
rtype := c.route.routeType
if sendDelayedInfo {
cm := compressionModeForInfoProtocol(&opts.Cluster.Compression, c.route.compression)
c.enqueueProto(s.generateRouteInitialInfoJSON(accName, cm, 0, gossipMode))
}
if c.last.IsZero() {
c.last = time.Now()
}
@@ -2122,8 +2174,10 @@ func (s *Server) addRoute(c *client, didSolicit bool, info *Info, accName string
// Now that we have registered the route, we can remove from the temp map.
s.removeFromTempClients(cid)
// Notify other routes about this new route
s.forwardNewRouteInfoToKnownServers(info)
// We don't need to send if the only route is the one we just accepted.
if len(conns) > 1 {
s.forwardNewRouteInfoToKnownServers(info, rtype, didSolicit, gossipMode)
}
// Send subscription interest
s.sendSubsToRoute(c, -1, accName)
@@ -2204,9 +2258,9 @@ func (s *Server) addRoute(c *client, didSolicit bool, info *Info, accName string
rHash := c.route.hash
rn := c.route.remoteName
url := c.route.url
// For solicited routes, we need now to send the INFO protocol.
if didSolicit {
c.enqueueProto(s.generateRouteInitialInfoJSON(_EMPTY_, c.route.compression, idx))
if sendDelayedInfo {
cm := compressionModeForInfoProtocol(&opts.Cluster.Compression, c.route.compression)
c.enqueueProto(s.generateRouteInitialInfoJSON(_EMPTY_, cm, idx, gossipMode))
}
if c.last.IsZero() {
c.last = time.Now()
@@ -2224,7 +2278,7 @@ func (s *Server) addRoute(c *client, didSolicit bool, info *Info, accName string
// check to be consistent and future proof. but will be same domain
if s.sameDomain(info.Domain) {
s.nodeToInfo.Store(rHash,
nodeInfo{rn, s.info.Version, s.info.Cluster, info.Domain, id, nil, nil, nil, false, info.JetStream, false})
nodeInfo{rn, s.info.Version, s.info.Cluster, info.Domain, id, nil, nil, nil, false, info.JetStream, false, false})
}
}
@@ -2243,10 +2297,9 @@ func (s *Server) addRoute(c *client, didSolicit bool, info *Info, accName string
s.sendAsyncGatewayInfo()
}
// we don't need to send if the only route is the one we just accepted.
// We don't need to send if the only route is the one we just accepted.
if len(s.routes) > 1 {
// Now let the known servers know about this new route
s.forwardNewRouteInfoToKnownServers(info)
s.forwardNewRouteInfoToKnownServers(info, rtype, didSolicit, gossipMode)
}
// Send info about the known gateways to this route.
@@ -2281,7 +2334,7 @@ func (s *Server) addRoute(c *client, didSolicit bool, info *Info, accName string
s.grWG.Done()
return
}
s.connectToRoute(url, rtype == Explicit, true, _EMPTY_)
s.connectToRoute(url, rtype, true, gossipMode, _EMPTY_)
})
}
}
@@ -2596,17 +2649,6 @@ func (s *Server) startRouteAcceptLoop() {
s.Noticef("Listening for route connections on %s",
net.JoinHostPort(opts.Cluster.Host, strconv.Itoa(l.Addr().(*net.TCPAddr).Port)))
proto := RouteProtoV2
// For tests, we want to be able to make this server behave
// as an older server so check this option to see if we should override
if opts.routeProto < 0 {
// We have a private option that allows test to override the route
// protocol. We want this option initial value to be 0, however,
// since original proto is RouteProtoZero, tests call setRouteProtoForTest(),
// which sets as negative value the (desired proto + 1) * -1.
// Here we compute back the real value.
proto = (opts.routeProto * -1) - 1
}
// Check for TLSConfig
tlsReq := opts.Cluster.TLSConfig != nil
info := Info{
@@ -2619,7 +2661,7 @@ func (s *Server) startRouteAcceptLoop() {
TLSVerify: tlsReq,
MaxPayload: s.info.MaxPayload,
JetStream: s.info.JetStream,
Proto: proto,
Proto: s.getServerProto(),
GatewayURL: s.getGatewayURL(),
Headers: s.supportsHeaders(),
Cluster: s.info.Cluster,
@@ -2696,7 +2738,7 @@ func (s *Server) startRouteAcceptLoop() {
}
// Start the accept loop in a different go routine.
go s.acceptConnections(l, "Route", func(conn net.Conn) { s.createRoute(conn, nil, _EMPTY_) }, nil)
go s.acceptConnections(l, "Route", func(conn net.Conn) { s.createRoute(conn, nil, Implicit, gossipDefault, _EMPTY_) }, nil)
// Solicit Routes if applicable. This will not block.
s.solicitRoutes(opts.Routes, opts.Cluster.PinnedAccounts)
@@ -2728,7 +2770,7 @@ func (s *Server) setRouteInfoHostPortAndIP() error {
func (s *Server) StartRouting(clientListenReady chan struct{}) {
defer s.grWG.Done()
// Wait for the client and and leafnode listen ports to be opened,
// Wait for the client and leafnode listen ports to be opened,
// and the possible ephemeral ports to be selected.
<-clientListenReady
@@ -2738,14 +2780,13 @@ func (s *Server) StartRouting(clientListenReady chan struct{}) {
}
func (s *Server) reConnectToRoute(rURL *url.URL, rtype RouteType, accName string) {
tryForEver := rtype == Explicit
// If A connects to B, and B to A (regardless if explicit or
// implicit - due to auto-discovery), and if each server first
// registers the route on the opposite TCP connection, the
// two connections will end-up being closed.
// Add some random delay to reduce risk of repeated failures.
delay := time.Duration(rand.Intn(100)) * time.Millisecond
if tryForEver {
if rtype == Explicit {
delay += DEFAULT_ROUTE_RECONNECT
}
select {
@@ -2754,7 +2795,7 @@ func (s *Server) reConnectToRoute(rURL *url.URL, rtype RouteType, accName string
s.grWG.Done()
return
}
s.connectToRoute(rURL, tryForEver, false, accName)
s.connectToRoute(rURL, rtype, false, gossipDefault, accName)
}
// Checks to make sure the route is still valid.
@@ -2767,21 +2808,26 @@ func (s *Server) routeStillValid(rURL *url.URL) bool {
return false
}
func (s *Server) connectToRoute(rURL *url.URL, tryForEver, firstConnect bool, accName string) {
func (s *Server) connectToRoute(rURL *url.URL, rtype RouteType, firstConnect bool, gossipMode byte, accName string) {
defer s.grWG.Done()
if rURL == nil {
return
}
// For explicit routes, we will try to connect until we succeed. For implicit
// we will try only based on the number of ConnectRetries optin.
tryForEver := rtype == Explicit
// Snapshot server options.
opts := s.getOpts()
defer s.grWG.Done()
const connErrFmt = "Error trying to connect to route (attempt %v): %v"
s.mu.Lock()
s.mu.RLock()
resolver := s.routeResolver
excludedAddresses := s.routesToSelf
s.mu.Unlock()
s.mu.RUnlock()
attempts := 0
for s.isRunning() && rURL != nil {
for attempts := 0; s.isRunning(); {
if tryForEver {
if !s.routeStillValid(rURL) {
return
@@ -2835,7 +2881,7 @@ func (s *Server) connectToRoute(rURL *url.URL, tryForEver, firstConnect bool, ac
// We have a route connection here.
// Go ahead and create it and exit this func.
s.createRoute(conn, rURL, accName)
s.createRoute(conn, rURL, rtype, gossipMode, accName)
return
}
}
@@ -2864,13 +2910,13 @@ func (s *Server) solicitRoutes(routes []*url.URL, accounts []string) {
s.saveRouteTLSName(routes)
for _, r := range routes {
route := r
s.startGoRoutine(func() { s.connectToRoute(route, true, true, _EMPTY_) })
s.startGoRoutine(func() { s.connectToRoute(route, Explicit, true, gossipDefault, _EMPTY_) })
}
// Now go over possible per-account routes and create them.
for _, an := range accounts {
for _, r := range routes {
route, accName := r, an
s.startGoRoutine(func() { s.connectToRoute(route, true, true, accName) })
s.startGoRoutine(func() { s.connectToRoute(route, Explicit, true, gossipDefault, accName) })
}
}
}
@@ -2993,7 +3039,7 @@ func (s *Server) removeRoute(c *client) {
opts = s.getOpts()
rURL *url.URL
noPool bool
didSolicit bool
rtype RouteType
)
c.mu.Lock()
cid := c.cid
@@ -3012,7 +3058,7 @@ func (s *Server) removeRoute(c *client) {
connectURLs = r.connectURLs
wsConnectURLs = r.wsConnURLs
rURL = r.url
didSolicit = r.didSolicit
rtype = r.routeType
}
c.mu.Unlock()
if accName != _EMPTY_ {
@@ -3075,12 +3121,12 @@ func (s *Server) removeRoute(c *client) {
// this remote was a "no pool" route, attempt to reconnect.
if noPool {
if s.routesPoolSize > 1 {
s.startGoRoutine(func() { s.connectToRoute(rURL, didSolicit, true, _EMPTY_) })
s.startGoRoutine(func() { s.connectToRoute(rURL, rtype, true, gossipDefault, _EMPTY_) })
}
if len(opts.Cluster.PinnedAccounts) > 0 {
for _, an := range opts.Cluster.PinnedAccounts {
accName := an
s.startGoRoutine(func() { s.connectToRoute(rURL, didSolicit, true, accName) })
s.startGoRoutine(func() { s.connectToRoute(rURL, rtype, true, gossipDefault, accName) })
}
}
}
+4 -3
View File
@@ -29,10 +29,11 @@ type sendq struct {
mu sync.Mutex
q *ipQueue[*outMsg]
s *Server
a *Account
}
func (s *Server) newSendQ() *sendq {
sq := &sendq{s: s, q: newIPQueue[*outMsg](s, "SendQ")}
func (s *Server) newSendQ(acc *Account) *sendq {
sq := &sendq{s: s, q: newIPQueue[*outMsg](s, "SendQ"), a: acc}
s.startGoRoutine(sq.internalLoop)
return sq
}
@@ -45,7 +46,7 @@ func (sq *sendq) internalLoop() {
defer s.grWG.Done()
c := s.createInternalSystemClient()
c.registerWithAccount(s.SystemAccount())
c.registerWithAccount(sq.a)
c.noIcb = true
defer c.closeConnection(ClientClosed)
+115 -20
View File
@@ -1,4 +1,4 @@
// Copyright 2012-2024 The NATS Authors
// 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
@@ -28,28 +28,26 @@ import (
"net"
"net/http"
"net/url"
"regexp"
"runtime/pprof"
"unicode"
// Allow dynamic profiling.
_ "net/http/pprof"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"runtime/pprof"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
// Allow dynamic profiling.
_ "net/http/pprof"
"github.com/klauspost/compress/s2"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nats-server/v2/logger"
"github.com/nats-io/nkeys"
"github.com/nats-io/nuid"
"github.com/nats-io/nats-server/v2/logger"
)
const (
@@ -60,6 +58,49 @@ const (
firstClientPingInterval = 2 * time.Second
)
// These are protocol versions sent between server connections: ROUTER, LEAF and
// GATEWAY. We may have protocol versions that have a meaning only for a certain
// type of connections, but we don't have to have separate enums for that.
// However, it is CRITICAL to not change the order of those constants since they
// are exchanged between servers. When adding a new protocol version, add to the
// end of the list, don't try to group them by connection types.
const (
// RouteProtoZero is the original Route protocol from 2009.
// http://nats.io/documentation/internals/nats-protocol/
RouteProtoZero = iota
// RouteProtoInfo signals a route can receive more then the original INFO block.
// This can be used to update remote cluster permissions, etc...
RouteProtoInfo
// RouteProtoV2 is the new route/cluster protocol that provides account support.
RouteProtoV2
// MsgTraceProto indicates that this server understands distributed message tracing.
MsgTraceProto
)
// Will return the latest server-to-server protocol versions, unless the
// option to override it is set.
func (s *Server) getServerProto() int {
opts := s.getOpts()
// Initialize with the latest protocol version.
proto := MsgTraceProto
// For tests, we want to be able to make this server behave
// as an older server so check this option to see if we should override.
if opts.overrideProto < 0 {
// The option overrideProto is set to 0 by default (when creating an
// Options structure). Since this is the same value than the original
// proto RouteProtoZero, tests call setServerProtoForTest() with the
// desired protocol level, which sets it as negative value equal to:
// (wantedProto + 1) * -1. Here we compute back the real value.
proto = (opts.overrideProto * -1) - 1
}
return proto
}
// Used by tests.
func setServerProtoForTest(wantedProto int) int {
return (wantedProto + 1) * -1
}
// Info is the information sent to clients, routes, gateways, and leaf nodes,
// to help them understand information about this server.
type Info struct {
@@ -101,6 +142,7 @@ type Info struct {
RoutePoolIdx int `json:"route_pool_idx,omitempty"`
RouteAccount string `json:"route_account,omitempty"`
RouteAccReqID string `json:"route_acc_add_reqid,omitempty"`
GossipMode byte `json:"gossip_mode,omitempty"`
// Gateways Specific
Gateway string `json:"gateway,omitempty"` // Name of the origin Gateway (sent by gateway's INFO)
@@ -189,6 +231,7 @@ type Server struct {
leafRemoteAccounts sync.Map
leafNodeEnabled bool
leafDisableConnect bool // Used in test only
leafNoCluster bool // Indicate that this server has only remotes and no cluster defined
quitCh chan struct{}
startupComplete chan struct{}
@@ -320,6 +363,14 @@ type Server struct {
// Queue to process JS API requests that come from routes (or gateways)
jsAPIRoutedReqs *ipQueue[*jsAPIRoutedReq]
// Delayed API responses.
delayedAPIResponses *ipQueue[*delayedAPIResponse]
// Whether moving NRG traffic into accounts is permitted on this server.
// Controls whether or not the account NRG capability is set in statsz.
// Currently used by unit tests to simulate nodes not supporting account NRG.
accountNRGAllowed atomic.Bool
}
// For tracking JS nodes.
@@ -335,6 +386,7 @@ type nodeInfo struct {
offline bool
js bool
binarySnapshots bool
accountNRG bool
}
// Make sure all are 64bits for atomic use
@@ -683,6 +735,14 @@ func NewServer(opts *Options) (*Server, error) {
syncOutSem: make(chan struct{}, maxConcurrentSyncRequests),
}
// Delayed API response queue. Create regardless if JetStream is configured
// or not (since it can be enabled/disabled with config reload, we want this
// queue to exist at all times).
s.delayedAPIResponses = newIPQueue[*delayedAPIResponse](s, "delayed API responses")
// By default we'll allow account NRG.
s.accountNRGAllowed.Store(true)
// Fill up the maximum in flight syncRequests for this server.
// Used in JetStream catchup semantics.
for i := 0; i < maxConcurrentSyncRequests; i++ {
@@ -701,6 +761,7 @@ func NewServer(opts *Options) (*Server, error) {
// If we have solicited leafnodes but no clustering and no clustername.
// However we may need a stable clustername so use the server name.
if len(opts.LeafNode.Remotes) > 0 && opts.Cluster.Port == 0 && opts.Cluster.Name == _EMPTY_ {
s.leafNoCluster = true
opts.Cluster.Name = opts.ServerName
}
@@ -726,7 +787,7 @@ func NewServer(opts *Options) (*Server, error) {
opts.Tags,
&JetStreamConfig{MaxMemory: opts.JetStreamMaxMemory, MaxStore: opts.JetStreamMaxStore, CompressOK: true},
nil,
false, true, true,
false, true, true, true,
})
}
@@ -968,6 +1029,9 @@ func (s *Server) ClientURL() string {
}
func validateCluster(o *Options) error {
if o.Cluster.Name != _EMPTY_ && strings.Contains(o.Cluster.Name, " ") {
return ErrClusterNameHasSpaces
}
if o.Cluster.Compression.Mode != _EMPTY_ {
if err := validateAndNormalizeCompressionOption(&o.Cluster.Compression, CompressionS2Fast); err != nil {
return err
@@ -977,8 +1041,9 @@ func validateCluster(o *Options) error {
return fmt.Errorf("cluster: %v", err)
}
// Check that cluster name if defined matches any gateway name.
if o.Gateway.Name != "" && o.Gateway.Name != o.Cluster.Name {
if o.Cluster.Name != "" {
// Note that we have already verified that the gateway name does not have spaces.
if o.Gateway.Name != _EMPTY_ && o.Gateway.Name != o.Cluster.Name {
if o.Cluster.Name != _EMPTY_ {
return ErrClusterNameConfigConflict
}
// Set this here so we do not consider it dynamic.
@@ -1019,6 +1084,9 @@ func validateOptions(o *Options) error {
return fmt.Errorf("max_payload (%v) cannot be higher than max_pending (%v)",
o.MaxPayload, o.MaxPending)
}
if o.ServerName != _EMPTY_ && strings.Contains(o.ServerName, " ") {
return errors.New("server name cannot contain spaces")
}
// Check that the trust configuration is correct.
if err := validateTrustedOperators(o); err != nil {
return err
@@ -1315,8 +1383,9 @@ func (s *Server) configureAccounts(reloading bool) (map[string]struct{}, error)
// Add any required exports from system account.
if s.sys != nil {
sysAcc := s.sys.account
s.mu.Unlock()
s.addSystemAccountExports(s.sys.account)
s.addSystemAccountExports(sysAcc)
s.mu.Lock()
}
@@ -1702,7 +1771,7 @@ func (s *Server) setSystemAccount(acc *Account) error {
recvq: newIPQueue[*inSysMsg](s, "System recvQ"),
recvqp: newIPQueue[*inSysMsg](s, "System recvQ Pings"),
resetCh: make(chan struct{}),
sq: s.newSendQ(),
sq: s.newSendQ(acc),
statsz: statsHBInterval,
orphMax: 5 * eventsHBInterval,
chkOrph: 3 * eventsHBInterval,
@@ -2120,7 +2189,17 @@ func (s *Server) Start() {
// Snapshot server options.
opts := s.getOpts()
clusterName := s.ClusterName()
// Capture if this server is a leaf that has no cluster, so we don't
// display the cluster name if that is the case.
s.mu.RLock()
leafNoCluster := s.leafNoCluster
s.mu.RUnlock()
var clusterName string
if !leafNoCluster {
clusterName = s.ClusterName()
}
s.Noticef(" Version: %s", VERSION)
s.Noticef(" Git: [%s]", gc)
@@ -2165,7 +2244,11 @@ func (s *Server) Start() {
}
if opts.ConfigFile != _EMPTY_ {
s.Noticef("Using configuration file: %s", opts.ConfigFile)
var cd string
if opts.configDigest != "" {
cd = fmt.Sprintf("(%s)", opts.configDigest)
}
s.Noticef("Using configuration file: %s %s", opts.ConfigFile, cd)
}
hasOperators := len(opts.TrustedOperators) > 0
@@ -2280,6 +2363,7 @@ func (s *Server) Start() {
StoreDir: opts.StoreDir,
SyncInterval: opts.SyncInterval,
SyncAlways: opts.SyncAlways,
Strict: opts.JetStreamStrict,
MaxMemory: opts.JetStreamMaxMemory,
MaxStore: opts.JetStreamMaxStore,
Domain: opts.JetStreamDomain,
@@ -2326,6 +2410,11 @@ func (s *Server) Start() {
}
}
// Delayed API response handling. Start regardless of JetStream being
// currently configured or not (since it can be enabled/disabled with
// configuration reload).
s.startGoRoutine(s.delayedAPIResponder)
// Start OCSP Stapling monitoring for TLS certificates if enabled. Hook TLS handshake for
// OCSP check on peers (LEAF and CLIENT kind) if enabled.
s.startOCSPMonitoring()
@@ -2357,9 +2446,6 @@ func (s *Server) Start() {
// Solicit remote servers for leaf node connections.
if len(opts.LeafNode.Remotes) > 0 {
s.solicitLeafNodeRemotes(opts.LeafNode.Remotes)
if opts.Cluster.Name == opts.ServerName && strings.ContainsFunc(opts.Cluster.Name, unicode.IsSpace) {
s.Warnf("Server name has spaces and used as the cluster name, leaf remotes may not connect properly")
}
}
// TODO (ik): I wanted to refactor this by starting the client
@@ -3075,7 +3161,16 @@ func (s *Server) createClientEx(conn net.Conn, inProcess bool) *client {
}
now := time.Now()
c := &client{srv: s, nc: conn, opts: defaultOpts, mpay: maxPay, msubs: maxSubs, start: now, last: now}
c := &client{
srv: s,
nc: conn,
opts: defaultOpts,
mpay: maxPay,
msubs: maxSubs,
start: now,
last: now,
iproc: inProcess,
}
c.registerWithAccount(s.globalAccount())
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !windows
// +build !windows
package server
+2 -3
View File
@@ -1,4 +1,4 @@
// Copyright 2012-2024 The NATS Authors
// 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
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !windows && !wasm
// +build !windows,!wasm
package server
@@ -62,7 +61,7 @@ func (s *Server) handleSignals() {
if !ldm {
s.Shutdown()
s.WaitForShutdown()
os.Exit(1)
os.Exit(0)
}
case syscall.SIGUSR1:
// File log re-open for rotating file logs.
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build wasm
// +build wasm
package server
+12 -3
View File
@@ -66,6 +66,8 @@ var (
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")
)
// StoreMsg is the stored message format for messages that are retained by the Store layer.
@@ -82,9 +84,12 @@ type StoreMsg struct {
// 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 report on newly created subject delete markers.
type SubjectDeleteMarkerUpdateHandler func(*inMsg)
type StreamStore interface {
StoreMsg(subject string, hdr, msg []byte) (uint64, int64, error)
StoreRawMsg(subject string, hdr, msg []byte, seq uint64, ts int64) error
StoreMsg(subject string, hdr, msg []byte, ttl int64) (uint64, int64, error)
StoreRawMsg(subject string, hdr, msg []byte, seq uint64, ts int64, ttl int64) error
SkipMsg() uint64
SkipMsgs(seq uint64, num uint64) error
LoadMsg(seq uint64, sm *StoreMsg) (*StoreMsg, error)
@@ -95,13 +100,14 @@ type StreamStore interface {
RemoveMsg(seq uint64) (bool, error)
EraseMsg(seq uint64) (bool, error)
Purge() (uint64, error)
PurgeEx(subject string, seq, keep uint64) (uint64, error)
PurgeEx(subject string, seq, keep uint64, noMarkers bool) (uint64, error)
Compact(seq uint64) (uint64, error)
Truncate(seq uint64) error
GetSeqFromTime(t time.Time) uint64
FilteredState(seq uint64, subject string) SimpleState
SubjectsState(filterSubject string) map[string]SimpleState
SubjectsTotals(filterSubject string) map[string]uint64
MultiLastSeqs(filters []string, maxSeq uint64, maxAllowed int) ([]uint64, error)
NumPending(sseq uint64, filter string, lastPerSubject bool) (total, validThrough uint64)
NumPendingMulti(sseq uint64, sl *Sublist, lastPerSubject bool) (total, validThrough uint64)
State() StreamState
@@ -110,6 +116,7 @@ type StreamStore interface {
SyncDeleted(dbs DeleteBlocks)
Type() StorageType
RegisterStorageUpdates(StorageUpdateHandler)
RegisterSubjectDeleteMarkerUpdates(SubjectDeleteMarkerUpdateHandler)
UpdateConfig(cfg *StreamConfig) error
Delete() error
Stop() error
@@ -182,6 +189,7 @@ type LostStreamData struct {
type SnapshotResult struct {
Reader io.ReadCloser
State StreamState
errCh chan string
}
const (
@@ -342,6 +350,7 @@ func (dbs DeleteBlocks) NumDeleted() (total uint64) {
// ConsumerStore stores state on consumers for streams.
type ConsumerStore interface {
SetStarting(sseq uint64) error
UpdateStarting(sseq uint64)
HasState() bool
UpdateDelivered(dseq, sseq, dc uint64, ts int64) error
UpdateAcks(dseq, sseq uint64) error
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -459,7 +459,16 @@ func (tr *subjectTransform) TransformTokenizedSubject(tokens []string) string {
}
b.WriteString(tr.getHashPartition(keyForHashing, int(tr.dtokmfintargs[i])))
case Wildcard: // simple substitution
b.WriteString(tokens[tr.dtokmftokindexesargs[i][0]])
switch {
case len(tr.dtokmftokindexesargs) < i:
break
case len(tr.dtokmftokindexesargs[i]) < 1:
break
case len(tokens) <= tr.dtokmftokindexesargs[i][0]:
break
default:
b.WriteString(tokens[tr.dtokmftokindexesargs[i][0]])
}
case SplitFromLeft:
sourceToken := tokens[tr.dtokmftokindexesargs[i][0]]
sourceTokenLen := len(sourceToken)
+9 -5
View File
@@ -1271,12 +1271,12 @@ func isValidLiteralSubject(tokens []string) bool {
return true
}
// ValidateMappingDestination returns nil error if the subject is a valid subject mapping destination subject
func ValidateMappingDestination(subject string) error {
if subject == _EMPTY_ {
// ValidateMapping returns nil error if the subject is a valid subject mapping destination subject
func ValidateMapping(src string, dest string) error {
if dest == _EMPTY_ {
return nil
}
subjectTokens := strings.Split(subject, tsep)
subjectTokens := strings.Split(dest, tsep)
sfwc := false
for _, t := range subjectTokens {
length := len(t)
@@ -1284,6 +1284,7 @@ func ValidateMappingDestination(subject string) error {
return &mappingDestinationErr{t, ErrInvalidMappingDestinationSubject}
}
// if it looks like it contains a mapping function, it should be a valid mapping function
if length > 4 && t[0] == '{' && t[1] == '{' && t[length-2] == '}' && t[length-1] == '}' {
if !partitionMappingFunctionRegEx.MatchString(t) &&
!wildcardMappingFunctionRegEx.MatchString(t) &&
@@ -1304,7 +1305,10 @@ func ValidateMappingDestination(subject string) error {
return ErrInvalidMappingDestinationSubject
}
}
return nil
// Finally, verify that the transform can actually be created from the source and destination
_, err := NewSubjectTransform(src, dest)
return err
}
// Will check tokens and report back if the have any partial or full wildcards.
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build freebsd || openbsd || dragonfly || netbsd
// +build freebsd openbsd dragonfly netbsd
package sysmem
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build darwin
// +build darwin
package sysmem
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build linux
// +build linux
package sysmem
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build wasm
// +build wasm
package sysmem
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build windows
// +build windows
package sysmem
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build zos
// +build zos
package sysmem
-1
View File
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build darwin || freebsd || openbsd || dragonfly || netbsd
// +build darwin freebsd openbsd dragonfly netbsd
package sysmem
+257
View File
@@ -0,0 +1,257 @@
// 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 thw
import (
"encoding/binary"
"errors"
"io"
"math"
"time"
)
// Error for when we can not locate a task for removal or updates.
var ErrTaskNotFound = errors.New("thw: task not found")
// Error for when we try to decode a binary-encoded THW with an unknown version number.
var ErrInvalidVersion = errors.New("thw: encoded version not known")
const (
tickDuration = int64(time.Second) // Tick duration in nanoseconds.
wheelBits = 12 // 2^12 = 4096 slots.
wheelSize = 1 << wheelBits // Number of slots in the wheel.
wheelMask = wheelSize - 1 // Mask for calculating position.
headerLen = 17 // 1 byte magic + 2x uint64s
)
// slot represents a single slot in the wheel.
type slot struct {
entries map[uint64]int64 // Map of sequence to expires.
lowest int64 // Lowest expiration time in this slot.
}
// HashWheel represents the timing wheel.
type HashWheel struct {
wheel []*slot // Array of slots.
lowest int64 // Track the lowest expiration time across all slots.
count uint64 // How many entries are present?
}
// NewHashWheel initializes a new HashWheel.
func NewHashWheel() *HashWheel {
return &HashWheel{
wheel: make([]*slot, wheelSize),
lowest: math.MaxInt64,
}
}
// getPosition calculates the slot position for a given expiration time.
func (hw *HashWheel) getPosition(expires int64) int64 {
return (expires / tickDuration) & wheelMask
}
// updateLowestExpires finds the new lowest expiration time across all slots.
func (hw *HashWheel) updateLowestExpires() {
lowest := int64(math.MaxInt64)
for _, s := range hw.wheel {
if s != nil && s.lowest < lowest {
lowest = s.lowest
}
}
hw.lowest = lowest
}
// newSlot creates a new slot.
func newSlot() *slot {
return &slot{
entries: make(map[uint64]int64),
lowest: math.MaxInt64,
}
}
// Add schedules a new timer task.
func (hw *HashWheel) Add(seq uint64, expires int64) error {
pos := hw.getPosition(expires)
// Initialize the slot lazily.
if hw.wheel[pos] == nil {
hw.wheel[pos] = newSlot()
}
if _, ok := hw.wheel[pos].entries[seq]; !ok {
hw.count++
}
hw.wheel[pos].entries[seq] = expires
// Update slot's lowest expiration if this is earlier.
if expires < hw.wheel[pos].lowest {
hw.wheel[pos].lowest = expires
// Update global lowest if this is now the earliest.
if expires < hw.lowest {
hw.lowest = expires
}
}
return nil
}
// Remove removes a timer task.
func (hw *HashWheel) Remove(seq uint64, expires int64) error {
pos := hw.getPosition(expires)
s := hw.wheel[pos]
if s == nil {
return ErrTaskNotFound
}
if _, exists := s.entries[seq]; !exists {
return ErrTaskNotFound
}
delete(s.entries, seq)
hw.count--
// If the slot is empty, we can set it to nil to free memory.
if len(s.entries) == 0 {
hw.wheel[pos] = nil
} else if expires == s.lowest {
// Find new lowest in this slot.
lowest := int64(math.MaxInt64)
for _, exp := range s.entries {
if exp < lowest {
lowest = exp
}
}
s.lowest = lowest
}
// If we removed the global lowest, find the new one.
if expires == hw.lowest {
hw.updateLowestExpires()
}
return nil
}
// Update updates the expiration time of an existing timer task.
func (hw *HashWheel) Update(seq uint64, oldExpires int64, newExpires int64) error {
// Remove from old position.
if err := hw.Remove(seq, oldExpires); err != nil {
return err
}
// Add to new position.
return hw.Add(seq, newExpires)
}
// ExpireTasks processes all expired tasks using a callback.
func (hw *HashWheel) ExpireTasks(callback func(seq uint64, expires int64)) {
now := time.Now().UnixNano()
// Quick return if nothing is expired.
if hw.lowest > now {
return
}
// Start from the slot containing the lowest expiration.
startPos, exitPos := hw.getPosition(hw.lowest), hw.getPosition(now+tickDuration)
var updateLowest bool
for offset := int64(0); ; offset++ {
pos := (startPos + offset) & wheelMask
if pos == exitPos {
if updateLowest {
hw.updateLowestExpires()
}
return
}
// Grab our slot.
slot := hw.wheel[pos]
if slot == nil || slot.lowest > now {
continue
}
// Track new lowest while processing expirations
newLowest := int64(math.MaxInt64)
for seq, expires := range slot.entries {
if expires <= now {
callback(seq, expires)
delete(slot.entries, seq)
hw.count--
updateLowest = true
} else if expires < newLowest {
newLowest = expires
}
}
// Nil out if we are empty.
if len(slot.entries) == 0 {
hw.wheel[pos] = nil
} else {
slot.lowest = newLowest
}
}
}
// GetNextExpiration returns the earliest expiration time before the given time.
// Returns math.MaxInt64 if no expirations exist before the specified time.
func (hw *HashWheel) GetNextExpiration(before int64) int64 {
if hw.lowest < before {
return hw.lowest
}
return math.MaxInt64
}
// AppendEncode writes out the contents of the THW into a binary snapshot
// and returns it. The high seq number is included in the snapshot and will
// be returned on decode.
func (hw *HashWheel) Encode(highSeq uint64) []byte {
b := make([]byte, 0, headerLen+(hw.count*(2*binary.MaxVarintLen64)))
b = append(b, 1) // Magic version
b = binary.LittleEndian.AppendUint64(b, hw.count) // Entry count
b = binary.LittleEndian.AppendUint64(b, highSeq) // Stamp
for _, slot := range hw.wheel {
if slot == nil || slot.entries == nil {
continue
}
for v, ts := range slot.entries {
b = binary.AppendVarint(b, ts)
b = binary.AppendUvarint(b, v)
}
}
return b
}
// Decode snapshots a binary-encoded THW and replaces the contents of this
// THW with them. Returns the high seq number from the snapshot.
func (hw *HashWheel) Decode(b []byte) (uint64, error) {
if len(b) < headerLen {
return 0, io.ErrShortBuffer
}
if b[0] != 1 {
return 0, ErrInvalidVersion
}
hw.wheel = make([]*slot, wheelSize)
hw.lowest = math.MaxInt64
count := binary.LittleEndian.Uint64(b[1:])
stamp := binary.LittleEndian.Uint64(b[9:])
b = b[headerLen:]
for i := uint64(0); i < count; i++ {
ts, tn := binary.Varint(b)
if tn < 0 {
return 0, io.ErrUnexpectedEOF
}
v, vn := binary.Uvarint(b[tn:])
if vn < 0 {
return 0, io.ErrUnexpectedEOF
}
hw.Add(v, ts)
b = b[tn+vn:]
}
return stamp, nil
}
+23
View File
@@ -0,0 +1,23 @@
// 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.
//go:build !windows
package tpm
import "fmt"
// LoadJetStreamEncryptionKeyFromTPM here is a stub for unsupported platforms.
func LoadJetStreamEncryptionKeyFromTPM(srkPassword, jsKeyFile, jsKeyPassword string, pcr int) (string, error) {
return "", fmt.Errorf("TPM functionality is not supported on this platform")
}
@@ -0,0 +1,281 @@
// 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.
//go:build windows
package tpm
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"github.com/google/go-tpm/legacy/tpm2"
"github.com/google/go-tpm/tpmutil"
"github.com/nats-io/nkeys"
)
var (
// Version of the NATS TPM JS implmentation
JsKeyTPMVersion = 1
)
// How this works:
// Create a Storage Root Key (SRK) in the TPM.
// If existing JS Encryption keys do not exist on disk.
// - Create a JetStream encryption key (js key) and seal it to the SRK
// using a provided js encryption key password.
// - Save the public and private blobs to a file on disk.
// - Return the new js encryption key (the private portion of the nkey)
// Otherwise (keys exist on disk)
// - Read the public and private blobs from disk
// - Load them into the TPM
// - Unseal the js key using the TPM, and the provided js encryption keys password.
//
// Note: a SRK password for the SRK is supported but not tested here.
// Gets/Regenerates the Storage Root Key (SRK) from the TPM. Caller MUST flush this handle when done.
func regenerateSRK(rwc io.ReadWriteCloser, srkPassword string) (tpmutil.Handle, error) {
// Default EK template defined in:
// https://trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf
// Shared SRK template based off of EK template and specified in:
// https://trustedcomputinggroup.org/wp-content/uploads/TCG-TPM-v2.0-Provisioning-Guidance-Published-v1r1.pdf
srkTemplate := tpm2.Public{
Type: tpm2.AlgRSA,
NameAlg: tpm2.AlgSHA256,
Attributes: tpm2.FlagFixedTPM | tpm2.FlagFixedParent | tpm2.FlagSensitiveDataOrigin | tpm2.FlagUserWithAuth | tpm2.FlagRestricted | tpm2.FlagDecrypt | tpm2.FlagNoDA,
AuthPolicy: nil,
// We must use RSA 2048 for the intel TSS2 stack
RSAParameters: &tpm2.RSAParams{
Symmetric: &tpm2.SymScheme{
Alg: tpm2.AlgAES,
KeyBits: 128,
Mode: tpm2.AlgCFB,
},
KeyBits: 2048,
ModulusRaw: make([]byte, 256),
},
}
// Create the parent key against which to seal the data
srkHandle, _, err := tpm2.CreatePrimary(rwc, tpm2.HandleOwner, tpm2.PCRSelection{}, "", srkPassword, srkTemplate)
return srkHandle, err
}
type natsTPMPersistedKeys struct {
Version int `json:"version"`
PrivateKey []byte `json:"private_key"`
PublicKey []byte `json:"public_key"`
}
// Writes the private and public blobs to disk in a single file. If the directory does
// not exist, it will be created. If the file already exists it will be overwritten.
func writeTPMKeysToFile(filename string, privateBlob []byte, publicBlob []byte) error {
keyDir := filepath.Dir(filename)
if err := os.MkdirAll(keyDir, 0750); err != nil {
return fmt.Errorf("unable to create/access directory %q: %v", keyDir, err)
}
// Create a new set of persisted keys. Note that the private key doesn't necessarily
// need to be protected as the TPM password is required to use unseal, although it's
// a good idea to put this in a secure location accessible to the server.
tpmKeys := natsTPMPersistedKeys{
Version: JsKeyTPMVersion,
PrivateKey: make([]byte, base64.StdEncoding.EncodedLen(len(privateBlob))),
PublicKey: make([]byte, base64.StdEncoding.EncodedLen(len(publicBlob))),
}
base64.StdEncoding.Encode(tpmKeys.PrivateKey, privateBlob)
base64.StdEncoding.Encode(tpmKeys.PublicKey, publicBlob)
// Convert to JSON
keysJSON, err := json.Marshal(tpmKeys)
if err != nil {
return fmt.Errorf("unable to marshal keys to JSON: %v", err)
}
// Write the JSON to a file
if err := os.WriteFile(filename, keysJSON, 0640); err != nil {
return fmt.Errorf("unable to write keys file to %q: %v", filename, err)
}
return nil
}
// Reads the private and public blobs from a single file. If the file does not exist,
// or the file cannot be read and the keys decoded, an error is returned.
func readTPMKeysFromFile(filename string) ([]byte, []byte, error) {
keysJSON, err := os.ReadFile(filename)
if err != nil {
return nil, nil, err
}
var tpmKeys natsTPMPersistedKeys
if err := json.Unmarshal(keysJSON, &tpmKeys); err != nil {
return nil, nil, fmt.Errorf("unable to unmarshal TPM file keys JSON from %s: %v", filename, err)
}
// Placeholder for future-proofing. Here is where we would
// check the current version against tpmKeys.Version and
// handle any changes.
// Base64 decode the private and public blobs.
privateBlob := make([]byte, base64.StdEncoding.DecodedLen(len(tpmKeys.PrivateKey)))
publicBlob := make([]byte, base64.StdEncoding.DecodedLen(len(tpmKeys.PublicKey)))
prn, err := base64.StdEncoding.Decode(privateBlob, tpmKeys.PrivateKey)
if err != nil {
return nil, nil, fmt.Errorf("unable to decode privateBlob from base64: %v", err)
}
pun, err := base64.StdEncoding.Decode(publicBlob, tpmKeys.PublicKey)
if err != nil {
return nil, nil, fmt.Errorf("unable to decode publicBlob from base64: %v", err)
}
return publicBlob[:pun], privateBlob[:prn], nil
}
// Creates a new JetStream encryption key, seals it to the TPM, and saves the public and
// private blobs to disk in a JSON encoded file. The key is returned as a string.
func createAndSealJsEncryptionKey(rwc io.ReadWriteCloser, srkHandle tpmutil.Handle, srkPassword, jsKeyFile, jsKeyPassword string, pcr int) (string, error) {
// Get the authorization policy that will protect the data to be sealed
sessHandle, policy, err := policyPCRPasswordSession(rwc, pcr)
if err != nil {
return "", fmt.Errorf("unable to get policy: %v", err)
}
if err := tpm2.FlushContext(rwc, sessHandle); err != nil {
return "", fmt.Errorf("unable to flush session: %v", err)
}
// Seal the data to the parent key and the policy
user, err := nkeys.CreateUser()
if err != nil {
return "", fmt.Errorf("unable to create seed: %v", err)
}
// We'll use the seed to represent the encryption key.
jsStoreKey, err := user.Seed()
if err != nil {
return "", fmt.Errorf("unable to get seed: %v", err)
}
privateArea, publicArea, err := tpm2.Seal(rwc, srkHandle, srkPassword, jsKeyPassword, policy, jsStoreKey)
if err != nil {
return "", fmt.Errorf("unable to seal data: %v", err)
}
err = writeTPMKeysToFile(jsKeyFile, privateArea, publicArea)
if err != nil {
return "", fmt.Errorf("unable to write key file: %v", err)
}
return string(jsStoreKey), nil
}
// Unseals the JetStream encryption key from the TPM with the provided keys.
// The key is returned as a string.
func unsealJsEncrpytionKey(rwc io.ReadWriteCloser, pcr int, srkHandle tpmutil.Handle, srkPassword, objectPassword string, publicBlob, privateBlob []byte) (string, error) {
// Load the public/private blobs into the TPM for decryption.
objectHandle, _, err := tpm2.Load(rwc, srkHandle, srkPassword, publicBlob, privateBlob)
if err != nil {
return "", fmt.Errorf("unable to load data: %v", err)
}
defer tpm2.FlushContext(rwc, objectHandle)
// Create the authorization session with TPM.
sessHandle, _, err := policyPCRPasswordSession(rwc, pcr)
if err != nil {
return "", fmt.Errorf("unable to get auth session: %v", err)
}
defer func() {
tpm2.FlushContext(rwc, sessHandle)
}()
// Unseal the data we've loaded into the TPM with the object (js key) password.
unsealedData, err := tpm2.UnsealWithSession(rwc, sessHandle, objectHandle, objectPassword)
if err != nil {
return "", fmt.Errorf("unable to unseal data: %v", err)
}
return string(unsealedData), nil
}
// Returns session handle and policy digest.
func policyPCRPasswordSession(rwc io.ReadWriteCloser, pcr int) (sessHandle tpmutil.Handle, policy []byte, retErr error) {
sessHandle, _, err := tpm2.StartAuthSession(
rwc,
tpm2.HandleNull, /*tpmKey*/
tpm2.HandleNull, /*bindKey*/
make([]byte, 16), /*nonceCaller*/
nil, /*secret*/
tpm2.SessionPolicy,
tpm2.AlgNull,
tpm2.AlgSHA256)
if err != nil {
return tpm2.HandleNull, nil, fmt.Errorf("unable to start session: %v", err)
}
defer func() {
if sessHandle != tpm2.HandleNull && err != nil {
if err := tpm2.FlushContext(rwc, sessHandle); err != nil {
retErr = fmt.Errorf("%v\nunable to flush session: %v", retErr, err)
}
}
}()
pcrSelection := tpm2.PCRSelection{
Hash: tpm2.AlgSHA256,
PCRs: []int{pcr},
}
if err := tpm2.PolicyPCR(rwc, sessHandle, nil, pcrSelection); err != nil {
return sessHandle, nil, fmt.Errorf("unable to bind PCRs to auth policy: %v", err)
}
if err := tpm2.PolicyPassword(rwc, sessHandle); err != nil {
return sessHandle, nil, fmt.Errorf("unable to require password for auth policy: %v", err)
}
policy, err = tpm2.PolicyGetDigest(rwc, sessHandle)
if err != nil {
return sessHandle, nil, fmt.Errorf("unable to get policy digest: %v", err)
}
return sessHandle, policy, nil
}
// LoadJetStreamEncryptionKeyFromTPM loads the JetStream encryption key from the TPM.
// If the keyfile does not exist, a key will be created and sealed. Public and private blobs
// used to decrypt the key in future sessions will be saved to disk in the file provided.
// The key will be unsealed and returned only with the correct password and PCR value.
func LoadJetStreamEncryptionKeyFromTPM(srkPassword, jsKeyFile, jsKeyPassword string, pcr int) (string, error) {
rwc, err := tpm2.OpenTPM()
if err != nil {
return "", fmt.Errorf("could not open the TPM: %v", err)
}
defer rwc.Close()
// Load the key from the TPM
srkHandle, err := regenerateSRK(rwc, srkPassword)
defer func() {
tpm2.FlushContext(rwc, srkHandle)
}()
if err != nil {
return "", fmt.Errorf("unable to regenerate SRK from the TPM: %v", err)
}
// Read the keys from the key file. If the filed doesn't exist it means we need to create
// a new js encrytpion key.
publicBlob, privateBlob, err := readTPMKeysFromFile(jsKeyFile)
if err != nil {
if os.IsNotExist(err) {
jsek, err := createAndSealJsEncryptionKey(rwc, srkHandle, srkPassword, jsKeyFile, jsKeyPassword, pcr)
if err != nil {
return "", fmt.Errorf("unable to generate new key from the TPM: %v", err)
}
// we've created and sealed the JS Encryption key, now we just return it.
return jsek, nil
}
return "", fmt.Errorf("unable to load key from TPM: %v", err)
}
// Unseal the JetStream encryption key using the TPM.
jsek, err := unsealJsEncrpytionKey(rwc, pcr, srkHandle, srkPassword, jsKeyPassword, publicBlob, privateBlob)
if err != nil {
return "", fmt.Errorf("unable to unseal key from the TPM: %v", err)
}
return jsek, nil
}
+1 -4
View File
@@ -23,7 +23,6 @@ import (
"net"
"net/url"
"reflect"
"regexp"
"strconv"
"strings"
"time"
@@ -39,11 +38,9 @@ const (
asciiNine = 57
)
var semVerRe = regexp.MustCompile(`\Av?([0-9]+)\.?([0-9]+)?\.?([0-9]+)?`)
func versionComponents(version string) (major, minor, patch int, err error) {
m := semVerRe.FindStringSubmatch(version)
if m == nil {
if len(m) == 0 {
return 0, 0, 0, errors.New("invalid semver")
}
major, err = strconv.Atoi(m[1])
+68 -17
View File
@@ -105,18 +105,21 @@ var wsGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
var wsTestRejectNoMasking = false
type websocket struct {
frames net.Buffers
fs int64
closeMsg []byte
compress bool
closeSent bool
browser bool
nocompfrag bool // No fragment for compressed frames
maskread bool
maskwrite bool
compressor *flate.Writer
cookieJwt string
clientIP string
frames net.Buffers
fs int64
closeMsg []byte
compress bool
closeSent bool
browser bool
nocompfrag bool // No fragment for compressed frames
maskread bool
maskwrite bool
compressor *flate.Writer
cookieJwt string
cookieUsername string
cookiePassword string
cookieToken string
clientIP string
}
type srvWebsocket struct {
@@ -128,7 +131,8 @@ type srvWebsocket struct {
sameOrigin bool
connectURLs []string
connectURLsMap refCountedUrlSet
authOverride bool // indicate if there is auth override in websocket config
authOverride bool // indicate if there is auth override in websocket config
rawHeaders string // raw headers to be used in the upgrade response.
// These are immutable and can be accessed without lock.
// This is the case when generating the client INFO.
@@ -791,6 +795,9 @@ func (s *Server) wsUpgrade(w http.ResponseWriter, r *http.Request) (*wsUpgradeRe
if kind == MQTT {
p = append(p, wsMQTTSecProto...)
}
if s.websocket.rawHeaders != _EMPTY_ {
p = append(p, s.websocket.rawHeaders...)
}
p = append(p, _CRLF_...)
if _, err = conn.Write(p); err != nil {
@@ -824,9 +831,19 @@ func (s *Server) wsUpgrade(w http.ResponseWriter, r *http.Request) (*wsUpgradeRe
// So make the combination of the two.
ws.nocompfrag = ws.compress && strings.Contains(ua, "Version/") && strings.Contains(ua, "Safari/")
}
if opts.Websocket.JWTCookie != _EMPTY_ {
if c, err := r.Cookie(opts.Websocket.JWTCookie); err == nil && c != nil {
ws.cookieJwt = c.Value
if cookies := r.Cookies(); len(cookies) > 0 {
ows := &opts.Websocket
for _, c := range cookies {
if ows.JWTCookie == c.Name {
ws.cookieJwt = c.Value
} else if ows.UsernameCookie == c.Name {
ws.cookieUsername = c.Value
} else if ows.PasswordCookie == c.Name {
ws.cookiePassword = c.Value
} else if ows.TokenCookie == c.Name {
ws.cookieToken = c.Value
}
}
}
}
@@ -1023,6 +1040,24 @@ func validateWebsocketOptions(o *Options) error {
if err := validatePinnedCerts(wo.TLSPinnedCerts); err != nil {
return fmt.Errorf("websocket: %v", err)
}
// Check for invalid headers here.
for key := range wo.Headers {
k := strings.ToLower(key)
switch k {
case "host",
"content-length",
"connection",
"upgrade",
"nats-no-masking":
return fmt.Errorf("websocket: invalid header %q not allowed", key)
}
if strings.HasPrefix(k, "sec-websocket-") {
return fmt.Errorf("websocket: invalid header %q, \"Sec-WebSocket-\" prefix not allowed", key)
}
}
return nil
}
@@ -1054,6 +1089,21 @@ func (s *Server) wsSetOriginOptions(o *WebsocketOpts) {
}
}
// Calculate the raw headers for websocket upgrade response.
func (s *Server) wsSetHeadersOptions(o *WebsocketOpts) {
var sb strings.Builder
for k, v := range o.Headers {
sb.WriteString(k)
sb.WriteString(": ")
sb.WriteString(v)
sb.WriteString(_CRLF_)
}
ws := &s.websocket
ws.mu.Lock()
defer ws.mu.Unlock()
ws.rawHeaders = sb.String()
}
// Given the websocket options, we check if any auth configuration
// has been provided. If so, possibly create users/nkey users and
// store them in s.websocket.users/nkeys.
@@ -1075,6 +1125,7 @@ func (s *Server) startWebsocketServer() {
o := &sopts.Websocket
s.wsSetOriginOptions(o)
s.wsSetHeadersOptions(o)
var hl net.Listener
var proto string
@@ -1149,7 +1200,7 @@ func (s *Server) startWebsocketServer() {
if !hasLeaf {
s.Errorf("Not configured to accept leaf node connections")
// Silently close for now. If we want to send an error back, we would
// need to create the leafnode client anyway, so that is is handling websocket
// need to create the leafnode client anyway, so that is handling websocket
// frames, then send the error to the remote.
res.conn.Close()
return
+12
View File
@@ -1,3 +1,15 @@
## 2.23.2
🎉🎉🎉
At long last, some long-standing performance gaps between `ginkgo` and `go test` have been resolved!
Ginkgo operates by running `go test -c` to generate test binaries, and then running those binaries. It turns out that the compilation step of `go test -c` is slower than `go test`'s compilation step because `go test` strips out debug symbols (`ldflags=-w`) whereas `go test -c` does not.
Ginkgo now passes the appropriate `ldflags` to `go test -c` when running specs to strip out symbols. This is only done when it is safe to do so and symbols are preferred when profiling is enabled and when `ginkgo build` is called explicitly.
This, coupled, with the [instructions for disabling XProtect on MacOS](https://onsi.github.io/ginkgo/#if-you-are-running-on-macos) yields a much better performance experience with Ginkgo.
## 2.23.1
## 🚨 For users on MacOS 🚨
+1 -1
View File
@@ -44,7 +44,7 @@ func buildSpecs(args []string, cliConfig types.CLIConfig, goFlagsConfig types.Go
internal.VerifyCLIAndFrameworkVersion(suites)
opc := internal.NewOrderedParallelCompiler(cliConfig.ComputedNumCompilers())
opc.StartCompiling(suites, goFlagsConfig)
opc.StartCompiling(suites, goFlagsConfig, true)
for {
suiteIdx, suite := opc.Next()
+1 -1
View File
@@ -26,7 +26,7 @@ func (c Command) Run(args []string, additionalArgs []string) {
}
for _, arg := range args {
if strings.HasPrefix(arg, "-") {
AbortWith("Malformed arguments - make sure all flags appear {{bold}}after{{/}} the Ginkgo subcommand and {{bold}}before{{/}} your list of packages.\n{{gray}}e.g. 'ginkgo run -p my_package' is valid `ginkgo -p run my_package` is not.{{/}}")
AbortWith(types.GinkgoErrors.FlagAfterPositionalParameter().Error())
}
}
c.Command(args, additionalArgs)
+4 -4
View File
@@ -11,7 +11,7 @@ import (
"github.com/onsi/ginkgo/v2/types"
)
func CompileSuite(suite TestSuite, goFlagsConfig types.GoFlagsConfig) TestSuite {
func CompileSuite(suite TestSuite, goFlagsConfig types.GoFlagsConfig, preserveSymbols bool) TestSuite {
if suite.PathToCompiledTest != "" {
return suite
}
@@ -46,7 +46,7 @@ func CompileSuite(suite TestSuite, goFlagsConfig types.GoFlagsConfig) TestSuite
suite.CompilationError = fmt.Errorf("Failed to get relative path from package to the current working directory:\n%s", err.Error())
return suite
}
args, err := types.GenerateGoTestCompileArgs(goFlagsConfig, "./", pathToInvocationPath)
args, err := types.GenerateGoTestCompileArgs(goFlagsConfig, "./", pathToInvocationPath, preserveSymbols)
if err != nil {
suite.State = TestSuiteStateFailedToCompile
suite.CompilationError = fmt.Errorf("Failed to generate go test compile flags:\n%s", err.Error())
@@ -120,7 +120,7 @@ func NewOrderedParallelCompiler(numCompilers int) *OrderedParallelCompiler {
}
}
func (opc *OrderedParallelCompiler) StartCompiling(suites TestSuites, goFlagsConfig types.GoFlagsConfig) {
func (opc *OrderedParallelCompiler) StartCompiling(suites TestSuites, goFlagsConfig types.GoFlagsConfig, preserveSymbols bool) {
opc.stopped = false
opc.idx = 0
opc.numSuites = len(suites)
@@ -135,7 +135,7 @@ func (opc *OrderedParallelCompiler) StartCompiling(suites TestSuites, goFlagsCon
stopped := opc.stopped
opc.mutex.Unlock()
if !stopped {
suite = CompileSuite(suite, goFlagsConfig)
suite = CompileSuite(suite, goFlagsConfig, preserveSymbols)
}
c <- suite
}
+1 -1
View File
@@ -107,7 +107,7 @@ OUTER_LOOP:
}
opc := internal.NewOrderedParallelCompiler(r.cliConfig.ComputedNumCompilers())
opc.StartCompiling(suites, r.goFlagsConfig)
opc.StartCompiling(suites, r.goFlagsConfig, false)
SUITE_LOOP:
for {
+1 -1
View File
@@ -153,7 +153,7 @@ func (w *SpecWatcher) WatchSpecs(args []string, additionalArgs []string) {
}
func (w *SpecWatcher) compileAndRun(suite internal.TestSuite, additionalArgs []string) internal.TestSuite {
suite = internal.CompileSuite(suite, w.goFlagsConfig)
suite = internal.CompileSuite(suite, w.goFlagsConfig, false)
if suite.State.Is(internal.TestSuiteStateFailedToCompile) {
fmt.Println(suite.CompilationError.Error())
return suite
+9 -1
View File
@@ -231,6 +231,10 @@ func (g GoFlagsConfig) BinaryMustBePreserved() bool {
return g.BlockProfile != "" || g.CPUProfile != "" || g.MemProfile != "" || g.MutexProfile != ""
}
func (g GoFlagsConfig) NeedsSymbols() bool {
return g.BinaryMustBePreserved()
}
// Configuration that were deprecated in 2.0
type deprecatedConfig struct {
DebugParallel bool
@@ -640,7 +644,7 @@ func VetAndInitializeCLIAndGoConfig(cliConfig CLIConfig, goFlagsConfig GoFlagsCo
}
// GenerateGoTestCompileArgs is used by the Ginkgo CLI to generate command line arguments to pass to the go test -c command when compiling the test
func GenerateGoTestCompileArgs(goFlagsConfig GoFlagsConfig, packageToBuild string, pathToInvocationPath string) ([]string, error) {
func GenerateGoTestCompileArgs(goFlagsConfig GoFlagsConfig, packageToBuild string, pathToInvocationPath string, preserveSymbols bool) ([]string, error) {
// if the user has set the CoverProfile run-time flag make sure to set the build-time cover flag to make sure
// the built test binary can generate a coverprofile
if goFlagsConfig.CoverProfile != "" {
@@ -663,6 +667,10 @@ func GenerateGoTestCompileArgs(goFlagsConfig GoFlagsConfig, packageToBuild strin
goFlagsConfig.CoverPkg = strings.Join(adjustedCoverPkgs, ",")
}
if !goFlagsConfig.NeedsSymbols() && goFlagsConfig.LDFlags == "" && !preserveSymbols {
goFlagsConfig.LDFlags = "-w -s"
}
args := []string{"test", "-c", packageToBuild}
goArgs, err := GenerateFlagArgs(
GoBuildFlags,
+7
View File
@@ -636,6 +636,13 @@ func (g ginkgoErrors) ExpectFilenameNotPath(flag string, path string) error {
}
}
func (g ginkgoErrors) FlagAfterPositionalParameter() error {
return GinkgoError{
Heading: "Malformed arguments - detected a flag after the package liste",
Message: "Make sure all flags appear {{bold}}after{{/}} the Ginkgo subcommand and {{bold}}before{{/}} your list of packages (or './...').\n{{gray}}e.g. 'ginkgo run -p my_package' is valid but `ginkgo -p run my_package` is not.\n{{gray}}e.g. 'ginkgo -p -vet ./...' is valid but 'ginkgo -p ./... -vet' is not{{/}}",
}
}
/* Stack-Trace parsing errors */
func (g ginkgoErrors) FailedToParseStackTrace(message string) error {
+1 -1
View File
@@ -1,3 +1,3 @@
package types
const VERSION = "2.23.1"
const VERSION = "2.23.2"
@@ -45,7 +45,7 @@ import (
var tracer trace.Tracer
const RevisionsDir = ".oc-nodes"
const MetadataDir = ".oc-nodes"
var _spaceTypePersonal = "personal"
var _spaceTypeProject = "project"
@@ -288,7 +288,7 @@ func (lu *Lookup) InternalPath(spaceID, nodeID string) string {
if len(spaceRoot) == 0 {
return ""
}
return filepath.Join(spaceRoot, RevisionsDir, Pathify(nodeID, 4, 2))
return filepath.Join(spaceRoot, MetadataDir, Pathify(nodeID, 4, 2))
}
path, _ := lu.IDCache.Get(context.Background(), spaceID, nodeID)
@@ -303,7 +303,7 @@ func (lu *Lookup) VersionPath(spaceID, nodeID, version string) string {
return ""
}
return filepath.Join(spaceRoot, RevisionsDir, Pathify(nodeID, 4, 2)+node.RevisionIDDelimiter+version)
return filepath.Join(spaceRoot, MetadataDir, Pathify(nodeID, 4, 2)+node.RevisionIDDelimiter+version)
}
// VersionPath returns the "current" path of the node
@@ -313,7 +313,7 @@ func (lu *Lookup) CurrentPath(spaceID, nodeID string) string {
return ""
}
return filepath.Join(spaceRoot, RevisionsDir, Pathify(nodeID, 4, 2)+node.CurrentIDDelimiter)
return filepath.Join(spaceRoot, MetadataDir, Pathify(nodeID, 4, 2)+node.CurrentIDDelimiter)
}
// refFromCS3 creates a CS3 reference from a set of bytes. This method should remain private
+1 -1
View File
@@ -87,7 +87,7 @@ func New(m map[string]interface{}, stream events.Stream, log *zerolog.Logger) (s
return ""
}
return filepath.Join(spaceRoot, lookup.RevisionsDir, lookup.Pathify(n.GetID(), 4, 2)+".mpk")
return filepath.Join(spaceRoot, lookup.MetadataDir)
},
o.FileMetadataCache), um, o, &timemanager.Manager{})
default:
@@ -24,6 +24,7 @@ import (
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -63,7 +64,44 @@ func (tp *Tree) CreateRevision(ctx context.Context, n *node.Node, version string
vf, err := os.OpenFile(versionPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0600)
if err != nil {
if os.IsExist(err) {
err := os.Remove(versionPath)
dir := filepath.Dir(versionPath)
base := filepath.Base(versionPath)
files, err := os.ReadDir(dir)
if err != nil {
return "", err
}
// find revision with highest number
highest := 0
for _, file := range files {
if file.IsDir() {
continue
}
name := file.Name()
if !strings.HasPrefix(name, base) {
continue
}
ext := strings.TrimPrefix(name, base+".")
if ext == "" || ext == base {
continue
}
num, err := strconv.Atoi(ext)
if err != nil {
continue
}
if num > highest {
highest = num
}
}
// rename existing revision
oldNode := node.NewBaseNode(n.SpaceID, n.ID+node.RevisionIDDelimiter+version+"."+strconv.Itoa(highest+1), tp.lookup)
err = tp.lookup.MetadataBackend().Rename(revNode, oldNode)
if err != nil {
return "", err
}
newPath := versionPath + "." + strconv.Itoa(highest+1)
err = os.Rename(versionPath, newPath)
if err != nil {
return "", err
}
+1 -1
View File
@@ -662,7 +662,7 @@ func (t *Tree) isIndex(path string) bool {
func (t *Tree) isInternal(path string) bool {
return path == t.options.Root ||
path == filepath.Join(t.options.Root, "users") ||
t.isIndex(path) || strings.Contains(path, lookup.RevisionsDir)
t.isIndex(path) || strings.Contains(path, lookup.MetadataDir)
}
func isLockFile(path string) bool {
@@ -199,15 +199,11 @@ func (b HybridBackend) Set(ctx context.Context, n MetadataNode, key string, val
func (b HybridBackend) SetMultiple(ctx context.Context, n MetadataNode, attribs map[string][]byte, acquireLock bool) (err error) {
path := n.InternalPath()
if acquireLock {
err := os.MkdirAll(filepath.Dir(path), 0600)
unlock, err := b.Lock(n)
if err != nil {
return err
}
lockedFile, err := lockedfile.OpenFile(b.LockfilePath(n), os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer cleanupLockfile(ctx, lockedFile)
defer func() { _ = unlock() }()
}
offloadAttr, err := xattr.Get(path, _metadataOffloadedAttr)
@@ -476,17 +472,37 @@ func (b HybridBackend) Rename(oldNode, newNode MetadataNode) error {
}
// MetadataPath returns the path of the file holding the metadata for the given path
func (b HybridBackend) MetadataPath(n MetadataNode) string { return b.metadataPathFunc(n) }
func (b HybridBackend) MetadataPath(n MetadataNode) string {
base := b.metadataPathFunc(n)
return filepath.Join(base, pathify(n.GetID(), 4, 2)+".mpk")
}
// LockfilePath returns the path of the lock file
func (HybridBackend) LockfilePath(n MetadataNode) string { return n.InternalPath() + ".mlock" }
func (b HybridBackend) LockfilePath(n MetadataNode) string {
base := b.metadataPathFunc(n)
return filepath.Join(base, "locks", n.GetID()+".mlock")
}
// Lock locks the metadata for the given path
func (b HybridBackend) Lock(n MetadataNode) (UnlockFunc, error) {
metaLockPath := b.LockfilePath(n)
mlock, err := lockedfile.OpenFile(metaLockPath, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return nil, err
if errors.Is(err, os.ErrNotExist) {
// create the parent directory
err = os.MkdirAll(filepath.Dir(metaLockPath), 0700)
if err != nil {
return nil, err
}
mlock, err = lockedfile.OpenFile(metaLockPath, os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return nil, err
}
} else {
return nil, err
}
}
return func() error {
err := mlock.Close()
@@ -513,3 +529,17 @@ func (b HybridBackend) cacheKey(n MetadataNode) string {
func isOffloadingAttribute(key string) bool {
return strings.HasPrefix(key, prefixes.GrantPrefix) || strings.HasPrefix(key, prefixes.MetadataPrefix)
}
func pathify(id string, depth, width int) string {
b := strings.Builder{}
i := 0
for ; i < depth; i++ {
if len(id) <= i*width+width {
break
}
b.WriteString(id[i*width : i*width+width])
b.WriteRune(filepath.Separator)
}
b.WriteString(id[i*width:])
return b.String()
}
@@ -43,12 +43,6 @@ type MessagePackBackend struct {
metaCache cache.FileMetadataCache
}
type readWriteCloseSeekTruncater interface {
io.ReadWriteCloser
io.Seeker
Truncate(int64) error
}
// NewMessagePackBackend returns a new MessagePackBackend instance
func NewMessagePackBackend(o cache.Config) MessagePackBackend {
return MessagePackBackend{
@@ -148,7 +142,6 @@ func (b MessagePackBackend) AllWithLockedSource(ctx context.Context, n MetadataN
func (b MessagePackBackend) saveAttributes(ctx context.Context, n MetadataNode, setAttribs map[string][]byte, deleteAttribs []string, acquireLock bool) error {
var (
err error
f readWriteCloseSeekTruncater
)
ctx, span := tracer.Start(ctx, "saveAttributes")
defer func() {
@@ -160,16 +153,13 @@ func (b MessagePackBackend) saveAttributes(ctx context.Context, n MetadataNode,
span.End()
}()
lockPath := b.LockfilePath(n)
metaPath := b.MetadataPath(n)
if acquireLock {
_, subspan := tracer.Start(ctx, "lockedfile.OpenFile")
f, err = lockedfile.OpenFile(lockPath, os.O_RDWR|os.O_CREATE, 0600)
subspan.End()
unlock, err := b.Lock(n)
if err != nil {
return err
}
defer f.Close()
defer func() { _ = unlock() }()
}
// Read current state
_, subspan := tracer.Start(ctx, "os.ReadFile")
@@ -168,7 +168,7 @@ func (b XattrsBackend) Set(ctx context.Context, n MetadataNode, key string, val
func (b XattrsBackend) SetMultiple(ctx context.Context, n MetadataNode, attribs map[string][]byte, acquireLock bool) (err error) {
path := n.InternalPath()
if acquireLock {
err := os.MkdirAll(filepath.Dir(path), 0600)
err := os.MkdirAll(filepath.Dir(path), 0700)
if err != nil {
return err
}

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